hashdiff 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 113e55eeac757365f792e7e2554a8fbb5f16c06cfcf83d4dcf7db268637ef47e
4
- data.tar.gz: ae222f4980bff17fa8f9fecef25cb1fa54db61503c075cc49b3ef755927ec5f1
3
+ metadata.gz: 4752d45ff706f5b763bb7bc2b6b6c4302657caad5e45fce2d02730d962ef3d31
4
+ data.tar.gz: 5d9ff9c909184d3bbd798a3dbc8ea82d1c2752450c0c04671755aaa0d8d34c25
5
5
  SHA512:
6
- metadata.gz: 7476c18bd05f2e5c57da5b15d9bd938bbfd5fa330825b21be8d867fded3b404d6ae3405acca5f235397f816b866aede13425e0ab314ee71497bfa54010ab6e82
7
- data.tar.gz: ce5d8d3ea4e8bca768688aa782e21bab01ae4a5ae8cd0b8f5b11927abc5c5163da3dc2a1135a069ad139998654471d11359b48e2a2eeb1d87f17bc461813745f
6
+ metadata.gz: 6ee25347a9b92ec1ff0849c319221101d2a53c0194586227395d79ced3d7d4172a3a3448ca197886ae175dcc2ab5f681d13790f4a75d69b3c31cec8150c82779
7
+ data.tar.gz: 0da5f54d158ad5dbe806bd0c25279554c6e47d22faff4334ae427351ff00cca338ea490fb96246611cde5b505d9970b9216b8d6de5960a0532a349f738e0ed52
@@ -11,7 +11,7 @@ jobs:
11
11
  matrix:
12
12
  ruby:
13
13
  - 2.7
14
- - 3.0
14
+ - '3.0'
15
15
  - 3.1
16
16
  - 3.2
17
17
  - 3.3
@@ -22,7 +22,6 @@ jobs:
22
22
  uses: ruby/setup-ruby@v1
23
23
  with:
24
24
  ruby-version: ${{ matrix.ruby }}
25
- - name: Install dependencies
26
- run: bundle install --jobs $(nproc) --retry 3
25
+ bundler-cache: true # 'bundle install' and cache gems
27
26
  - name: Run rake
28
27
  run: bundle exec rake
data/README.md CHANGED
@@ -95,7 +95,8 @@ Hashdiff.unpatch!(b, diff).should == a
95
95
  ### Options
96
96
 
97
97
  The following options are available: `:delimiter`, `:similarity`, `:strict`, `:ignore_keys`,
98
- `:indifferent`, `:numeric_tolerance`, `:strip`, `:case_insensitive`, `:array_path` and `:use_lcs`
98
+ `:indifferent`, `:numeric_tolerance`, `:strip`, `:case_insensitive`, `:array_path`,
99
+ `:use_lcs`, and `:preserve_key_order`
99
100
 
100
101
  #### `:delimiter`
101
102
 
@@ -235,6 +236,30 @@ diff = Hashdiff.diff(a, b, use_lcs: false)
235
236
  diff.should == [["~", "x[1]", 1, 2], ["+", "x[3]", 3]]
236
237
  ```
237
238
 
239
+ #### `:preserve_key_order`
240
+
241
+ By default, the change set is ordered by operation type: deletions (-) first, then updates (~), and finally additions (+).
242
+ Within each operation group, keys are sorted alphabetically:
243
+
244
+ ```ruby
245
+ a = {d: 1, c: 1, a: 1}
246
+ b = {d: 2, b: 2, a: 2}
247
+
248
+ diff = Hashdiff.diff(a, b)
249
+ diff.should == [["-", "c", 1], ["~", "a", 1, 2], ["~", "d", 1, 2], ["+", "b", 2]]
250
+ ```
251
+
252
+ Setting :preserve_key_order to true processes keys in the order they appear in the first hash.
253
+ Keys that only exist in the second hash are appended in their original order:
254
+
255
+ ```ruby
256
+ a = {d: 1, c: 1, a: 1}
257
+ b = {d: 2, b: 2, a: 2}
258
+
259
+ diff = Hashdiff.diff(a, b, preserve_key_order: true)
260
+ diff.should == [["~", "d", 1, 2], ["-", "c", 1], ["~", "a", 1, 2], ["+", "b", 2]]
261
+ ```
262
+
238
263
  #### Specifying a custom comparison method
239
264
 
240
265
  It's possible to specify how the values of a key should be compared.
data/changelog.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Change Log
2
2
 
3
+ ## v1.2.0 2025-5-20
4
+
5
+ * Added :preserve_key_order option to maintain original hash key order #99 (@robkiessling)
6
+
3
7
  ## v1.1.2 2024-11-12
4
8
 
5
9
  * Fix bundler cache #96 (@olleolleolle)
@@ -20,9 +20,9 @@ module Hashdiff
20
20
  obj2_keys = obj2_keys.map { |k| k.is_a?(Symbol) ? k.to_s : k }
21
21
  end
22
22
 
23
- added_keys = (obj2_keys - obj1_keys).sort_by(&:to_s)
24
- common_keys = (obj1_keys & obj2_keys).sort_by(&:to_s)
25
- deleted_keys = (obj1_keys - obj2_keys).sort_by(&:to_s)
23
+ added_keys = obj2_keys - obj1_keys
24
+ common_keys = obj1_keys & obj2_keys
25
+ deleted_keys = obj1_keys - obj2_keys
26
26
 
27
27
  result = []
28
28
 
@@ -32,40 +32,66 @@ module Hashdiff
32
32
  deleted_keys.delete k
33
33
  end
34
34
 
35
- # add deleted properties
36
- deleted_keys.each do |k|
37
- k = opts[:indifferent] ? obj1_lookup[k] : k
38
- change_key = Hashdiff.prefix_append_key(opts[:prefix], k, opts)
39
- custom_result = Hashdiff.custom_compare(opts[:comparison], change_key, obj1[k], nil)
35
+ handle_key = lambda do |k, type|
36
+ case type
37
+ when :deleted
38
+ # add deleted properties
39
+ k = opts[:indifferent] ? obj1_lookup[k] : k
40
+ change_key = Hashdiff.prefix_append_key(opts[:prefix], k, opts)
41
+ custom_result = Hashdiff.custom_compare(opts[:comparison], change_key, obj1[k], nil)
40
42
 
41
- if custom_result
42
- result.concat(custom_result)
43
- else
44
- result << ['-', change_key, obj1[k]]
45
- end
46
- end
43
+ if custom_result
44
+ result.concat(custom_result)
45
+ else
46
+ result << ['-', change_key, obj1[k]]
47
+ end
48
+ when :common
49
+ # recursive comparison for common keys
50
+ prefix = Hashdiff.prefix_append_key(opts[:prefix], k, opts)
47
51
 
48
- # recursive comparison for common keys
49
- common_keys.each do |k|
50
- prefix = Hashdiff.prefix_append_key(opts[:prefix], k, opts)
52
+ k1 = opts[:indifferent] ? obj1_lookup[k] : k
53
+ k2 = opts[:indifferent] ? obj2_lookup[k] : k
54
+ result.concat(Hashdiff.diff(obj1[k1], obj2[k2], opts.merge(prefix: prefix)))
55
+ when :added
56
+ # added properties
57
+ change_key = Hashdiff.prefix_append_key(opts[:prefix], k, opts)
51
58
 
52
- k1 = opts[:indifferent] ? obj1_lookup[k] : k
53
- k2 = opts[:indifferent] ? obj2_lookup[k] : k
54
- result.concat(Hashdiff.diff(obj1[k1], obj2[k2], opts.merge(prefix: prefix)))
55
- end
59
+ k = opts[:indifferent] ? obj2_lookup[k] : k
60
+ custom_result = Hashdiff.custom_compare(opts[:comparison], change_key, nil, obj2[k])
56
61
 
57
- # added properties
58
- added_keys.each do |k|
59
- change_key = Hashdiff.prefix_append_key(opts[:prefix], k, opts)
62
+ if custom_result
63
+ result.concat(custom_result)
64
+ else
65
+ result << ['+', change_key, obj2[k]]
66
+ end
67
+ else
68
+ raise "Invalid type: #{type}"
69
+ end
70
+ end
60
71
 
61
- k = opts[:indifferent] ? obj2_lookup[k] : k
62
- custom_result = Hashdiff.custom_compare(opts[:comparison], change_key, nil, obj2[k])
72
+ if opts[:preserve_key_order]
73
+ # Building lookups to speed up key classification
74
+ added_keys_lookup = added_keys.each_with_object({}) { |k, h| h[k] = true }
75
+ common_keys_lookup = common_keys.each_with_object({}) { |k, h| h[k] = true }
76
+ deleted_keys_lookup = deleted_keys.each_with_object({}) { |k, h| h[k] = true }
63
77
 
64
- if custom_result
65
- result.concat(custom_result)
66
- else
67
- result << ['+', change_key, obj2[k]]
78
+ # Iterate through all keys, preserving obj1's key order and appending any new keys from obj2. Shared keys
79
+ # (found in both obj1 and obj2) follow obj1's order since uniq only keeps the first occurrence.
80
+ (obj1_keys + obj2_keys).uniq.each do |k|
81
+ if added_keys_lookup[k]
82
+ handle_key.call(k, :added)
83
+ elsif common_keys_lookup[k]
84
+ handle_key.call(k, :common)
85
+ elsif deleted_keys_lookup[k]
86
+ handle_key.call(k, :deleted)
87
+ end
68
88
  end
89
+ else
90
+ # Keys are first grouped by operation type (deletions first, then changes, then additions), and then sorted
91
+ # alphabetically within each group.
92
+ deleted_keys.sort_by(&:to_s).each { |k| handle_key.call(k, :deleted) }
93
+ common_keys.sort_by(&:to_s).each { |k| handle_key.call(k, :common) }
94
+ added_keys.sort_by(&:to_s).each { |k| handle_key.call(k, :added) }
69
95
  end
70
96
 
71
97
  result
data/lib/hashdiff/diff.rb CHANGED
@@ -16,6 +16,7 @@ module Hashdiff
16
16
  # * :strip (Boolean) [false] whether or not to call #strip on strings before comparing
17
17
  # * :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.
18
18
  # * :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.
19
+ # * :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.
19
20
  #
20
21
  # @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.
21
22
  #
@@ -62,6 +63,7 @@ module Hashdiff
62
63
  # * :strip (Boolean) [false] whether or not to call #strip on strings before comparing
63
64
  # * :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.
64
65
  # * :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.
66
+ # * :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.
65
67
  #
66
68
  #
67
69
  # @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,7 +90,8 @@ module Hashdiff
88
90
  strip: false,
89
91
  numeric_tolerance: 0,
90
92
  array_path: false,
91
- use_lcs: true
93
+ use_lcs: true,
94
+ preserve_key_order: false
92
95
  }.merge!(options)
93
96
 
94
97
  opts[:prefix] = [] if opts[:array_path] && opts[:prefix] == ''
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hashdiff
4
- VERSION = '1.1.2'.freeze
4
+ VERSION = '1.2.0'.freeze
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hashdiff
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Liu Fengyun
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-12 00:00:00.000000000 Z
11
+ date: 2025-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bluecloth