diff options
| -rw-r--r-- | .github/workflows/ci.yml | 27 | ||||
| -rw-r--r-- | .rubocop.yml | 6 | ||||
| -rw-r--r-- | Gemfile | 2 | ||||
| -rw-r--r-- | README.md | 56 | ||||
| -rw-r--r-- | changelog.md | 57 | ||||
| -rw-r--r-- | debian/changelog | 9 | ||||
| -rw-r--r-- | debian/control | 2 | ||||
| -rw-r--r-- | debian/gbp.conf | 4 | ||||
| -rw-r--r-- | debian/salsa-ci.yml | 3 | ||||
| -rw-r--r-- | debian/watch | 2 | ||||
| -rw-r--r-- | hashdiff.gemspec | 2 | ||||
| -rw-r--r-- | lib/hashdiff/compare_hashes.rb | 92 | ||||
| -rw-r--r-- | lib/hashdiff/diff.rb | 11 | ||||
| -rw-r--r-- | lib/hashdiff/version.rb | 2 | ||||
| -rw-r--r-- | spec/hashdiff/diff_spec.rb | 74 |
15 files changed, 267 insertions, 82 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fc3d489 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: ci + +on: + - pull_request + - push + +jobs: + build: + strategy: + fail-fast: false + matrix: + ruby: + - 2.7 + - '3.0' + - 3.1 + - 3.2 + - 3.3 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true # 'bundle install' and cache gems + - name: Run rake + run: bundle exec rake diff --git a/.rubocop.yml b/.rubocop.yml index 5d2ed17..7668a83 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -9,7 +9,7 @@ Metrics/MethodLength: Enabled: false Metrics/AbcSize: Enabled: false -Metrics/LineLength: +Layout/LineLength: Enabled: false Metrics/ClassLength: Enabled: false @@ -36,5 +36,7 @@ RSpec/ExampleLength: Enabled: false RSpec/DescribeClass: Enabled: false -RSpec/FilePath: +RSpec/SpecFilePathFormat: + Enabled: false +RSpec/NoExpectationExample: Enabled: false @@ -1,6 +1,6 @@ # frozen_string_literal: true -source 'http://rubygems.org' +source 'https://rubygems.org' gemspec group :test do @@ -1,4 +1,4 @@ -# Hashdiff [](http://travis-ci.org/liufengyun/hashdiff) [](http://badge.fury.io/rb/hashdiff) +# Hashdiff [](https://github.com/liufengyun/hashdiff/actions?query=workflow%3Aci) [](http://badge.fury.io/rb/hashdiff) Hashdiff is a ruby library to compute the smallest difference between two hashes. @@ -95,7 +95,8 @@ Hashdiff.unpatch!(b, diff).should == a ### Options The following options are available: `:delimiter`, `:similarity`, `:strict`, `:ignore_keys`, -`:indifferent`, `:numeric_tolerance`, `:strip`, `:case_insensitive`, `:array_path` and `:use_lcs` +`:indifferent`, `:numeric_tolerance`, `:strip`, `:case_insensitive`, `:array_path`, +`:use_lcs`, and `:preserve_key_order` #### `:delimiter` @@ -119,23 +120,26 @@ The `:strict` option, which defaults to `true`, specifies whether numeric types #### `:ignore_keys` -The `:ignore_keys` option allows you to specify one or more keys to ignore, which defaults to `[]` (none). Ignored keys are ignored at all levels. For example: +The `:ignore_keys` option allows you to specify one or more keys to ignore, which defaults to `[]` (none). Ignored keys are ignored at all levels in both hashes. For example: ```ruby -a = { a: 1, b: { d: 2, a: 3 }, c: 4 } -b = { a: 2, b: { d: 2, a: 7 }, c: 5 } -diff = Hashdiff.diff(a, b, ignore_keys: :a) -diff.should == [['~', 'c', 4, 5]] +a = { a: 4, g: 0, b: { a: 5, c: 6, e: 1 } } +b = { b: { a: 7, c: 3, f: 1 }, d: 8 } +diff = Hashdiff.diff(a, b, ignore_keys: %i[a f]) +diff.should == [['-', 'g', 0], ['-', 'b.e', 1], ['~', 'b.c', 6, 3], ['+', 'd', 8]] ``` -If you wish instead to ignore keys at a particlar level you should -use a [custom comparison method](https://github.com/liufengyun/hashdiff#specifying-a-custom-comparison-method) instead. For example: +If you wish instead to ignore keys at a particlar level you should +use a [custom comparison method](https://github.com/liufengyun/hashdiff#specifying-a-custom-comparison-method) instead. For example to diff only at the 2nd level of both hashes: ```ruby -a = { a: 1, b: { d: 2, a: 3 }, c: 4 } -b = { a: 2, b: { d: 2, a: 7 }, c: 5 } -diff = Hashdiff.diff(a, b) { |path, _e, _a| true if path == 'b.a' } # note '.' is the default delimiter -diff.should == [['~', 'a', 1, 2], ['~', 'c', 4, 5]] -``` +a = { a: 4, g: 0, b: { a: 5, c: 6, e: 1 } } +b = { b: { a: 7, c: 3, f: 1 }, d: 8 } +diff = Hashdiff.diff(a, b) do |path, _e, _a| + arr = path.split('.') + true if %w[a f].include?(arr.last) && arr.size == 2 # note '.' is the default delimiter +end +diff.should == [['-', 'a', 4], ['-', 'g', 0], ['-', 'b.e', 1], ['~', 'b.c', 6, 3], ['+', 'd', 8]] +``` #### `:indifferent` @@ -232,6 +236,30 @@ diff = Hashdiff.diff(a, b, use_lcs: false) diff.should == [["~", "x[1]", 1, 2], ["+", "x[3]", 3]] ``` +#### `:preserve_key_order` + +By default, the change set is ordered by operation type: deletions (-) first, then updates (~), and finally additions (+). +Within each operation group, keys are sorted alphabetically: + +```ruby +a = {d: 1, c: 1, a: 1} +b = {d: 2, b: 2, a: 2} + +diff = Hashdiff.diff(a, b) +diff.should == [["-", "c", 1], ["~", "a", 1, 2], ["~", "d", 1, 2], ["+", "b", 2]] +``` + +Setting :preserve_key_order to true processes keys in the order they appear in the first hash. +Keys that only exist in the second hash are appended in their original order: + +```ruby +a = {d: 1, c: 1, a: 1} +b = {d: 2, b: 2, a: 2} + +diff = Hashdiff.diff(a, b, preserve_key_order: true) +diff.should == [["~", "d", 1, 2], ["-", "c", 1], ["~", "a", 1, 2], ["+", "b", 2]] +``` + #### Specifying a custom comparison method It's possible to specify how the values of a key should be compared. diff --git a/changelog.md b/changelog.md index f944d5e..bd2d4ea 100644 --- a/changelog.md +++ b/changelog.md @@ -1,8 +1,27 @@ # Change Log -## v1.1.0 2020-02-25 +## v1.2.1 2025-09-06 -* Add ignore_keys option (#86 @Matzfan) +* Use HTTPS for the source in the Gemfile [#101](https://github.com/liufengyun/hashdiff/issues/101) ([@krzysiek1507](https://github.com/krzysiek1507)) + +## v1.2.0 2025-5-20 + +* Added :preserve_key_order option to maintain original hash key order [#99](https://github.com/liufengyun/hashdiff/issues/99) ([@robkiessling](https://github.com/robkiessling)) + +## v1.1.2 2024-11-12 + +* Fix bundler cache [#96](https://github.com/liufengyun/hashdiff/issues/96) ([@olleolleolle](https://github.com/olleolleolle)) +* Quote the '3.0' in YAML [#95](https://github.com/liufengyun/hashdiff/issues/95) ([@olleolleolle](https://github.com/olleolleolle)) +* Fix version in source code [#97](https://github.com/liufengyun/hashdiff/issues/97) ([@liufengyun](https://github.com/liufengyun)) + +## v1.1.1 2024-08-02 + +* Fix bug in ignore_keys option [#88](https://github.com/liufengyun/hashdiff/issues/88) ([@Matzfan](https://github.com/Matzfan)) +* Exclude spec files from gem package [#94](https://github.com/liufengyun/hashdiff/issues/94) ([@amatsuda](https://github.com/amatsuda)) + +## v1.1.0 2023-12-14 + +* Add ignore_keys option ([#86](https://github.com/liufengyun/hashdiff/issues/86) [@Matzfan](https://github.com/Matzfan)) * Remove pinned version of rake < 11 * Bump rspec dep ~> 3.5 * Bump rubocop dep >= 1.52.1 @@ -14,52 +33,52 @@ ## v1.0.0 2019-06-06 -* Fix typo in readme (#72 @koic) -* Fix Rubocop offence (#73 @koic) -* Bumps version to v1.0.0 (#74 @jfelchner) +* Fix typo in readme ([#72](https://github.com/liufengyun/hashdiff/issues/72) [@koic](https://github.com/koic)) +* Fix Rubocop offence ([#73](https://github.com/liufengyun/hashdiff/issues/73) [@koic](https://github.com/koic)) +* Bumps version to v1.0.0 ([#74](https://github.com/liufengyun/hashdiff/issues/74) [@jfelchner](https://github.com/jfelchner)) ## v1.0.0.beta1 2019-06-06 -* fix warnings in ci (#69 @y-yagi) -* drop warnings of the constant change (#70 @jfelchner) +* fix warnings in ci ([#69](https://github.com/liufengyun/hashdiff/issues/69) [@y-yagi](https://github.com/y-yagi)) +* drop warnings of the constant change ([#70](https://github.com/liufengyun/hashdiff/issues/70) [@jfelchner](https://github.com/jfelchner)) ## v0.4.0 2019-05-28 -* refactoring (#56 #57 #59 #61 krzysiek1507) -* fix typo in README (#64 @pboling) -* change HashDiff to Hashdiff (#65 @jfelchner) +* refactoring ([#56](https://github.com/liufengyun/hashdiff/issues/56) [#57](https://github.com/liufengyun/hashdiff/issues/57) [#59](https://github.com/liufengyun/hashdiff/issues/59) [#61](https://github.com/liufengyun/hashdiff/issues/61) [@krzysiek1507](https://github.com/krzysiek1507)) +* fix typo in README ([#64](https://github.com/liufengyun/hashdiff/issues/64) [@pboling](https://github.com/pboling)) +* change HashDiff to Hashdiff ([#65](https://github.com/liufengyun/hashdiff/issues/65) [@jfelchner](https://github.com/jfelchner)) ## v0.3.9 2019-04-22 -* Performance tweak (thanks @krzysiek1507: #51 #52 #53) +* Performance tweak (thanks [@krzysiek1507](https://github.com/krzysiek1507): [#51](https://github.com/liufengyun/hashdiff/issues/51) [#52](https://github.com/liufengyun/hashdiff/issues/52) [#53](https://github.com/liufengyun/hashdiff/issues/53)) ## v0.3.8 2018-12-30 -* Add Rubocop and drops Ruby 1.9 support #47 +* Add Rubocop and drops Ruby 1.9 support [#47](https://github.com/liufengyun/hashdiff/issues/47) ## v0.3.7 2017-10-08 -* remove 1.8.7 support from gemspec #39 +* remove 1.8.7 support from gemspec [#39](https://github.com/liufengyun/hashdiff/issues/39) ## v0.3.6 2017-08-22 -* add option `use_lcs` #35 +* add option `use_lcs` [#35](https://github.com/liufengyun/hashdiff/issues/35) ## v0.3.5 2017-08-06 -* add option `array_path` #34 +* add option `array_path` [#34](https://github.com/liufengyun/hashdiff/issues/34) ## v0.3.4 2017-05-01 -* performance improvement of `#similar?` #31 +* performance improvement of `#similar?` [#31](https://github.com/liufengyun/hashdiff/issues/31) ## v0.3.2 2016-12-27 -* replace `Fixnum` by `Integer` #28 +* replace `Fixnum` by `Integer` [#28](https://github.com/liufengyun/hashdiff/issues/28) ## v0.3.1 2016-11-24 -* fix an error when a hash has mixed types #26 +* fix an error when a hash has mixed types [#26](https://github.com/liufengyun/hashdiff/issues/26) ## v0.3.0 2016-2-11 @@ -67,7 +86,7 @@ ## v0.2.3 2015-11-5 -* improve performance of LCS algorithm #12 +* improve performance of LCS algorithm [#12](https://github.com/liufengyun/hashdiff/issues/12) ## v0.2.2 2014-10-6 diff --git a/debian/changelog b/debian/changelog index c87313e..87821ab 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,12 @@ +ruby-hashdiff (1.2.1-1) unstable; urgency=medium + + * Team upload. + * Use GitHub in watch to include tests. + * New upstream release. + * Update Standards-Version to 4.7.2, no changes needed. + + -- Simon Quigley <tsimonq2@debian.org> Tue, 04 Nov 2025 09:05:46 -0600 + ruby-hashdiff (1.1.0-1) unstable; urgency=medium [ Debian Janitor ] diff --git a/debian/control b/debian/control index a1cadb3..a91f7ef 100644 --- a/debian/control +++ b/debian/control @@ -7,7 +7,7 @@ Build-Depends: debhelper-compat (= 13), gem2deb (>= 1), rake, ruby-rspec -Standards-Version: 4.6.2 +Standards-Version: 4.7.2 Vcs-Git: https://salsa.debian.org/ruby-team/ruby-hashdiff.git Vcs-Browser: https://salsa.debian.org/ruby-team/ruby-hashdiff Homepage: https://github.com/liufengyun/hashdiff diff --git a/debian/gbp.conf b/debian/gbp.conf new file mode 100644 index 0000000..6b65fe0 --- /dev/null +++ b/debian/gbp.conf @@ -0,0 +1,4 @@ +[DEFAULT] +debian-branch = debian/latest +upstream-branch = upstream/latest +pristine-tar = True diff --git a/debian/salsa-ci.yml b/debian/salsa-ci.yml new file mode 100644 index 0000000..7b5d2f7 --- /dev/null +++ b/debian/salsa-ci.yml @@ -0,0 +1,3 @@ +--- +include: + - https://salsa.debian.org/ruby-team/meta/raw/master/salsa-ci.yml diff --git a/debian/watch b/debian/watch index c667309..9c4b7fd 100644 --- a/debian/watch +++ b/debian/watch @@ -1,2 +1,2 @@ version=4 -https://gemwatch.debian.net/hashdiff .*/hashdiff-(.*).tar.gz +https://github.com/liufengyun/hashdiff/tags .*/v?(\d.*)@ARCHIVE_EXT@ diff --git a/hashdiff.gemspec b/hashdiff.gemspec index f72da71..001af53 100644 --- a/hashdiff.gemspec +++ b/hashdiff.gemspec @@ -10,7 +10,7 @@ Gem::Specification.new do |s| s.summary = ' Hashdiff is a diff lib to compute the smallest difference between two hashes. ' s.description = ' Hashdiff is a diff lib to compute the smallest difference between two hashes. ' - s.files = `git ls-files`.split("\n") + s.files = `git ls-files`.split("\n").grep_v(%r{^spec/}) s.test_files = `git ls-files -- Appraisals {spec}/*`.split("\n") s.require_paths = ['lib'] diff --git a/lib/hashdiff/compare_hashes.rb b/lib/hashdiff/compare_hashes.rb index 54e7207..6422895 100644 --- a/lib/hashdiff/compare_hashes.rb +++ b/lib/hashdiff/compare_hashes.rb @@ -20,48 +20,78 @@ module Hashdiff obj2_keys = obj2_keys.map { |k| k.is_a?(Symbol) ? k.to_s : k } end - added_keys = (obj2_keys - obj1_keys).sort_by(&:to_s) - common_keys = (obj1_keys & obj2_keys).sort_by(&:to_s) - deleted_keys = (obj1_keys - obj2_keys).sort_by(&:to_s) + added_keys = obj2_keys - obj1_keys + common_keys = obj1_keys & obj2_keys + deleted_keys = obj1_keys - obj2_keys result = [] - opts[:ignore_keys].each { |k| common_keys.delete k } - - # add deleted properties - deleted_keys.each do |k| - k = opts[:indifferent] ? obj1_lookup[k] : k - change_key = Hashdiff.prefix_append_key(opts[:prefix], k, opts) - custom_result = Hashdiff.custom_compare(opts[:comparison], change_key, obj1[k], nil) - - if custom_result - result.concat(custom_result) - else - result << ['-', change_key, obj1[k]] - end + opts[:ignore_keys].each do |k| + added_keys.delete k + common_keys.delete k + deleted_keys.delete k end - # recursive comparison for common keys - common_keys.each do |k| - prefix = Hashdiff.prefix_append_key(opts[:prefix], k, opts) + handle_key = lambda do |k, type| + case type + when :deleted + # add deleted properties + k = opts[:indifferent] ? obj1_lookup[k] : k + change_key = Hashdiff.prefix_append_key(opts[:prefix], k, opts) + custom_result = Hashdiff.custom_compare(opts[:comparison], change_key, obj1[k], nil) - k1 = opts[:indifferent] ? obj1_lookup[k] : k - k2 = opts[:indifferent] ? obj2_lookup[k] : k - result.concat(Hashdiff.diff(obj1[k1], obj2[k2], opts.merge(prefix: prefix))) - end + if custom_result + result.concat(custom_result) + else + result << ['-', change_key, obj1[k]] + end + when :common + # recursive comparison for common keys + prefix = Hashdiff.prefix_append_key(opts[:prefix], k, opts) - # added properties - added_keys.each do |k| - change_key = Hashdiff.prefix_append_key(opts[:prefix], k, opts) + k1 = opts[:indifferent] ? obj1_lookup[k] : k + k2 = opts[:indifferent] ? obj2_lookup[k] : k + result.concat(Hashdiff.diff(obj1[k1], obj2[k2], opts.merge(prefix: prefix))) + when :added + # added properties + change_key = Hashdiff.prefix_append_key(opts[:prefix], k, opts) - k = opts[:indifferent] ? obj2_lookup[k] : k - custom_result = Hashdiff.custom_compare(opts[:comparison], change_key, nil, obj2[k]) + k = opts[:indifferent] ? obj2_lookup[k] : k + custom_result = Hashdiff.custom_compare(opts[:comparison], change_key, nil, obj2[k]) - if custom_result - result.concat(custom_result) + if custom_result + result.concat(custom_result) + else + result << ['+', change_key, obj2[k]] + end else - result << ['+', change_key, obj2[k]] + raise "Invalid type: #{type}" + end + end + + if opts[:preserve_key_order] + # Building lookups to speed up key classification + added_keys_lookup = added_keys.each_with_object({}) { |k, h| h[k] = true } + common_keys_lookup = common_keys.each_with_object({}) { |k, h| h[k] = true } + deleted_keys_lookup = deleted_keys.each_with_object({}) { |k, h| h[k] = true } + + # Iterate through all keys, preserving obj1's key order and appending any new keys from obj2. Shared keys + # (found in both obj1 and obj2) follow obj1's order since uniq only keeps the first occurrence. + (obj1_keys + obj2_keys).uniq.each do |k| + if added_keys_lookup[k] + handle_key.call(k, :added) + elsif common_keys_lookup[k] + handle_key.call(k, :common) + elsif deleted_keys_lookup[k] + handle_key.call(k, :deleted) + end end + else + # Keys are first grouped by operation type (deletions first, then changes, then additions), and then sorted + # alphabetically within each group. + deleted_keys.sort_by(&:to_s).each { |k| handle_key.call(k, :deleted) } + common_keys.sort_by(&:to_s).each { |k| handle_key.call(k, :common) } + added_keys.sort_by(&:to_s).each { |k| handle_key.call(k, :added) } end result diff --git a/lib/hashdiff/diff.rb b/lib/hashdiff/diff.rb index 6073c9c..8e24d16 100644 --- a/lib/hashdiff/diff.rb +++ b/lib/hashdiff/diff.rb @@ -9,13 +9,14 @@ module Hashdiff # @param [Array, Hash] obj2 # @param [Hash] options the options to use when comparing # * :strict (Boolean) [true] whether numeric values will be compared on type as well as value. Set to false to allow comparing Integer, Float, BigDecimal to each other - # * :ignore_keys (Symbol, String or Array) [[]] a list of keys to ignore. No comparison is made for the specified key(s) + # * :ignore_keys (Symbol, String or Array) [[]] a list of keys to ignore. No comparison is made for the specified key(s) in either hash # * :indifferent (Boolean) [false] whether to treat hash keys indifferently. Set to true to ignore differences between symbol keys (ie. {a: 1} ~= {'a' => 1}) # * :delimiter (String) ['.'] the delimiter used when returning nested key references # * :numeric_tolerance (Numeric) [0] should be a positive numeric value. Value by which numeric differences must be greater than. By default, numeric values are compared exactly; with the :tolerance option, the difference between numeric values must be greater than the given value. # * :strip (Boolean) [false] whether or not to call #strip on strings before comparing # * :array_path (Boolean) [false] whether to return the path references for nested values in an array, can be used for patch compatibility with non string keys. # * :use_lcs (Boolean) [true] whether or not to use an implementation of the Longest common subsequence algorithm for comparing arrays, produces better diffs but is slower. + # * :preserve_key_order (Boolean) [false] If false, operations are grouped by type (-, ~, then +) then by hash key alphabetically. If true, preserves the original key order from the first hash and appends new keys from the second hash in order. # # @yield [path, value1, value2] Optional block is used to compare each value, instead of default #==. If the block returns value other than true of false, then other specified comparison options will be used to do the comparison. # @@ -54,7 +55,7 @@ module Hashdiff # @param [Array, Hash] obj2 # @param [Hash] options the options to use when comparing # * :strict (Boolean) [true] whether numeric values will be compared on type as well as value. Set to false to allow comparing Integer, Float, BigDecimal to each other - # * :ignore_keys (Symbol, String or Array) [[]] a list of keys to ignore. No comparison is made for the specified key(s) + # * :ignore_keys (Symbol, String or Array) [[]] a list of keys to ignore. No comparison is made for the specified key(s) in either hash # * :indifferent (Boolean) [false] whether to treat hash keys indifferently. Set to true to ignore differences between symbol keys (ie. {a: 1} ~= {'a' => 1}) # * :similarity (Numeric) [0.8] should be between (0, 1]. Meaningful if there are similar hashes in arrays. See {best_diff}. # * :delimiter (String) ['.'] the delimiter used when returning nested key references @@ -62,6 +63,7 @@ module Hashdiff # * :strip (Boolean) [false] whether or not to call #strip on strings before comparing # * :array_path (Boolean) [false] whether to return the path references for nested values in an array, can be used for patch compatibility with non string keys. # * :use_lcs (Boolean) [true] whether or not to use an implementation of the Longest common subsequence algorithm for comparing arrays, produces better diffs but is slower. + # * :preserve_key_order (Boolean) [false] If false, operations are grouped by type (-, ~, then +) then by hash key alphabetically. If true, preserves the original key order from the first hash and appends new keys from the second hash in order. # # # @yield [path, value1, value2] Optional block is used to compare each value, instead of default #==. If the block returns value other than true of false, then other specified comparison options will be used to do the comparison. @@ -88,12 +90,13 @@ module Hashdiff strip: false, numeric_tolerance: 0, array_path: false, - use_lcs: true + use_lcs: true, + preserve_key_order: false }.merge!(options) opts[:prefix] = [] if opts[:array_path] && opts[:prefix] == '' - opts[:ignore_keys] = [*opts[:ignore_keys]] # splat covers single sym/string case + opts[:ignore_keys] = [*opts[:ignore_keys]] opts[:comparison] = block if block_given? diff --git a/lib/hashdiff/version.rb b/lib/hashdiff/version.rb index 310b3a0..b8151a4 100644 --- a/lib/hashdiff/version.rb +++ b/lib/hashdiff/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Hashdiff - VERSION = '1.1.0'.freeze + VERSION = '1.2.1'.freeze end diff --git a/spec/hashdiff/diff_spec.rb b/spec/hashdiff/diff_spec.rb index 1bfeb4a..cbebe4e 100644 --- a/spec/hashdiff/diff_spec.rb +++ b/spec/hashdiff/diff_spec.rb @@ -50,17 +50,37 @@ describe Hashdiff do end context 'with the ignore_keys option' do - a = { a: 1, b: { d: 2, a: 3 }, c: 4 } - b = { a: 2, b: { d: 2, a: 7 }, c: 5 } + a = { a: 4, g: 0, b: { a: 5, c: 6, e: 1 } } + b = { b: { a: 7, c: 3, f: 1 }, d: 8 } - it 'ignores a single key' do + it 'ignores a single key at first level' do + diff = described_class.diff(a, b, ignore_keys: :d) + diff.should == [['-', 'a', 4], ['-', 'g', 0], ['-', 'b.e', 1], ['~', 'b.a', 5, 7], ['~', 'b.c', 6, 3], ['+', 'b.f', 1]] + end + + it 'ignores a single key in nested hash' do + diff = described_class.diff(a, b, ignore_keys: :e) + diff.should == [['-', 'a', 4], ['-', 'g', 0], ['~', 'b.a', 5, 7], ['~', 'b.c', 6, 3], ['+', 'b.f', 1], ['+', 'd', 8]] + end + + it 'ignores a single key at all levels' do diff = described_class.diff(a, b, ignore_keys: :a) - diff.should == [['~', 'c', 4, 5]] + diff.should == [['-', 'g', 0], ['-', 'b.e', 1], ['~', 'b.c', 6, 3], ['+', 'b.f', 1], ['+', 'd', 8]] end - it 'ignores an array of keys' do - diff = described_class.diff(a, b, ignore_keys: %i[a c]) - diff.should == [] + it 'ignores an array of keys at first level' do + diff = described_class.diff(a, b, ignore_keys: %i[g b]) + diff.should == [['-', 'a', 4], ['+', 'd', 8]] + end + + it 'ignores an array of keys in nested hash' do + diff = described_class.diff(a, b, ignore_keys: %i[c e]) + diff.should == [['-', 'a', 4], ['-', 'g', 0], ['~', 'b.a', 5, 7], ['+', 'b.f', 1], ['+', 'd', 8]] + end + + it 'ignores an array of keys at all levels' do + diff = described_class.diff(a, b, ignore_keys: %i[a f]) + diff.should == [['-', 'g', 0], ['-', 'b.e', 1], ['~', 'b.c', 6, 3], ['+', 'd', 8]] end end @@ -372,4 +392,44 @@ describe Hashdiff do diff.should == [['~', 'a[0][0]', 0, 1], ['~', 'a[0][1]', 1, 2]] end end + + context 'when :preserve_key_order is nil or false' do + # rubocop:disable Layout/ExtraSpacing + it 'sorts hash changes by operation type (-, ~, +), then alphabetically by key' do + a = { 'f' => 1, 'd' => 1, 'c' => 1, 'a' => 1 } + b = { 'e' => 2, 'd' => 2, 'b' => 2, 'a' => 2 } + + diff = described_class.diff(a, b) + expect(diff).to eq([['-', 'c', 1], ['-', 'f', 1], ['~', 'a', 1, 2], ['~', 'd', 1, 2], ['+', 'b', 2], ['+', 'e', 2]]) + end + + it 'sorts changes at each level of a nested hash by operation type, then alphabetically by key' do + a = { 'y' => { 'c' => 1, 'b' => 1, 'a' => 1 }, 'x' => { 'b' => 1 } } + b = { 'y' => { 'b' => 2 }, 'x' => { 'c' => 2, 'b' => 2, 'a' => 2 } } + + diff = described_class.diff(a, b) + expect(diff).to eq([['~', 'x.b', 1, 2], ['+', 'x.a', 2], ['+', 'x.c', 2], ['-', 'y.a', 1], ['-', 'y.c', 1], ['~', 'y.b', 1, 2]]) + end + # rubocop:enable Layout/ExtraSpacing + end + + context 'when :preserve_key_order is true' do + # rubocop:disable Layout/ExtraSpacing + it 'preserves the key order from the first hash and appends new keys from the second hash in their original order' do + a = { 'f' => 1, 'd' => 1, 'c' => 1, 'a' => 1 } + b = { 'e' => 2, 'd' => 2, 'b' => 2, 'a' => 2 } + + diff = described_class.diff(a, b, preserve_key_order: true) + expect(diff).to eq([['-', 'f', 1], ['~', 'd', 1, 2], ['-', 'c', 1], ['~', 'a', 1, 2], ['+', 'e', 2], ['+', 'b', 2]]) + end + + it 'preserves the key order at each level of a nested hash and appends new keys from the second hash in their original order' do + a = { 'y' => { 'c' => 1, 'b' => 1, 'a' => 1 }, 'x' => { 'b' => 1 } } + b = { 'y' => { 'b' => 2 }, 'x' => { 'c' => 2, 'b' => 2, 'a' => 2 } } + + diff = described_class.diff(a, b, preserve_key_order: true) + expect(diff).to eq([['-', 'y.c', 1], ['~', 'y.b', 1, 2], ['-', 'y.a', 1], ['~', 'x.b', 1, 2], ['+', 'x.c', 2], ['+', 'x.a', 2]]) + end + # rubocop:enable Layout/ExtraSpacing + end end |
