summaryrefslogtreecommitdiff
diff options
-rw-r--r--.github/workflows/ci.yml27
-rw-r--r--.rubocop.yml6
-rw-r--r--Gemfile2
-rw-r--r--README.md56
-rw-r--r--changelog.md57
-rw-r--r--debian/changelog9
-rw-r--r--debian/control2
-rw-r--r--debian/gbp.conf4
-rw-r--r--debian/salsa-ci.yml3
-rw-r--r--debian/watch2
-rw-r--r--hashdiff.gemspec2
-rw-r--r--lib/hashdiff/compare_hashes.rb92
-rw-r--r--lib/hashdiff/diff.rb11
-rw-r--r--lib/hashdiff/version.rb2
-rw-r--r--spec/hashdiff/diff_spec.rb74
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
diff --git a/Gemfile b/Gemfile
index 2dce3e5..0c4b9b6 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-source 'http://rubygems.org'
+source 'https://rubygems.org'
gemspec
group :test do
diff --git a/README.md b/README.md
index 824dac7..b0ac072 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Hashdiff [![Build Status](https://secure.travis-ci.org/liufengyun/hashdiff.svg)](http://travis-ci.org/liufengyun/hashdiff) [![Gem Version](https://badge.fury.io/rb/hashdiff.svg)](http://badge.fury.io/rb/hashdiff)
+# Hashdiff [![Build Status](https://github.com/liufengyun/hashdiff/workflows/ci/badge.svg)](https://github.com/liufengyun/hashdiff/actions?query=workflow%3Aci) [![Gem Version](https://badge.fury.io/rb/hashdiff.svg)](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