hashdiff 0.3.4 → 0.3.5

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
  SHA1:
3
- metadata.gz: 284b7371148ba31499e35fdfb3783faced5a8498
4
- data.tar.gz: 3d902c91f2d00eb00df4cb96d27f5d6c242e9ee8
3
+ metadata.gz: 4ba86448df8842fe6323572feb2c89110954b775
4
+ data.tar.gz: e3d610f32c07e65e06f825e107371caaaf2e5896
5
5
  SHA512:
6
- metadata.gz: e80104dc13c931f44352337d0027fd774f8a54a67c2ae15b2870e05723c41aafbe00be06d5073078ccfcf44b320e3ae03693623f0d61cab95fa040671e708599
7
- data.tar.gz: 8efef12e5b3ff1c9a78dfc7655f121211d91e6b7d62521d33a5376b202e31913da440e2754f5ae0a3f1cba7189e2006db033027786c7402410ecf5dac58d44f2
6
+ metadata.gz: 7e7b28d277160a603df99c83022f6b8bd7bb27ac5d467e351e5834ba955195ed493249b7a7e213dafe0cb1aa630c5de46fe11a95b5942de4ba9c1f4e66262ec3
7
+ data.tar.gz: d5290cbc0b5c97038cce213e627fc20fcaf4e7405888c217c78b949c8fc98990ab6d3ae18b0f91d2909c069ffccc20a9d0d45aa572419252aa6eae960df6491d
data/Gemfile CHANGED
@@ -3,4 +3,5 @@ gemspec
3
3
 
4
4
  group :test do
5
5
  gem 'rake', '< 11'
6
+ gem 'codecov'
6
7
  end
data/README.md CHANGED
@@ -72,8 +72,8 @@ diff.should == [['-', 'a[0].x', 2], ['-', 'a[0].z', 4], ['-', 'a[1].y', 22], ['-
72
72
  patch example:
73
73
 
74
74
  ```ruby
75
- a = {a: 3}
76
- b = {a: {a1: 1, a2: 2}}
75
+ a = {'a' => 3}
76
+ b = {'a' => {'a1' => 1, 'a2' => 2}}
77
77
 
78
78
  diff = HashDiff.diff(a, b)
79
79
  HashDiff.patch!(a, diff).should == b
@@ -82,8 +82,8 @@ HashDiff.patch!(a, diff).should == b
82
82
  unpatch example:
83
83
 
84
84
  ```ruby
85
- a = [{a: 1, b: 2, c: 3, d: 4, e: 5}, {x: 5, y: 6, z: 3}, 1]
86
- b = [1, {a: 1, b: 2, c: 3, e: 5}]
85
+ a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, {'x' => 5, 'y' => 6, 'z' => 3}, 1]
86
+ b = [1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}]
87
87
 
88
88
  diff = HashDiff.diff(a, b) # diff two array is OK
89
89
  HashDiff.unpatch!(b, diff).should == a
@@ -91,8 +91,9 @@ HashDiff.unpatch!(b, diff).should == a
91
91
 
92
92
  ### Options
93
93
 
94
- There are six options available: `:delimiter`, `:similarity`,
95
- `:strict`, `:numeric_tolerance`, `:strip` and `:case_insensitive`.
94
+ There are seven options available: `:delimiter`, `:similarity`,
95
+ `:strict`, `:numeric_tolerance`, `:strip`, `:case_insensitive`
96
+ and `:array_path`.
96
97
 
97
98
  #### `:delimiter`
98
99
 
@@ -140,7 +141,7 @@ diff.should == [["~", "x", 5, 6]]
140
141
 
141
142
  #### `:case_insensitive`
142
143
 
143
- The :case_insensitive option makes string comparisions ignore case.
144
+ The :case_insensitive option makes string comparisons ignore case.
144
145
 
145
146
  ```ruby
146
147
  a = {x:5, s:'FooBar'}
@@ -150,6 +151,39 @@ diff = HashDiff.diff(a, b, :comparison => { :numeric_tolerance => 0.1, :case_ins
150
151
  diff.should == [["~", "x", 5, 6]]
151
152
  ```
152
153
 
154
+ #### `:array_path`
155
+
156
+ The :array_path option represents the path of the diff in an array rather than
157
+ a string. This can be used to show differences in between hash key types and
158
+ is useful for `patch!` when used on hashes without string keys.
159
+
160
+ ```ruby
161
+ a = {x:5}
162
+ b = {'x'=>6}
163
+
164
+ diff = HashDiff.diff(a, b, :array_path => true)
165
+ diff.should == [['-', [:x], 5], ['+', ['x'], 6]]
166
+ ```
167
+
168
+ For cases where there are arrays in paths their index will be added to the path.
169
+ ```ruby
170
+ a = {x:[0,1]}
171
+ b = {x:[0,2]}
172
+
173
+ diff = HashDiff.diff(a, b, :array_path => true)
174
+ diff.should == [["-", [:x, 1], 1], ["+", [:x, 1], 2]]
175
+ ```
176
+
177
+ This shouldn't cause problems if you are comparing an array with a hash:
178
+
179
+ ```ruby
180
+ a = {x:{0=>1}}
181
+ b = {x:[1]}
182
+
183
+ diff = HashDiff.diff(a, b, :array_path => true)
184
+ diff.should == [["~", [:a], [1], {0=>1}]]
185
+ ```
186
+
153
187
  #### Specifying a custom comparison method
154
188
 
155
189
  It's possible to specify how the values of a key should be compared.
@@ -186,6 +220,8 @@ diff.should == [["~", "a", "car", "bus"], ["~", "b[1]", "plane", " plan"], ["-",
186
220
 
187
221
  When a comparison block is given, it'll be given priority over other specified options. If the block returns value other than `true` or `false`, then the two values will be compared with other specified options.
188
222
 
223
+ When used in conjunction with the `array_path` option, the path passed in as an argument will be an array. When determining the ordering of an array a key of `"*"` will be used in place of the `key[*]` field. It is possible, if you have hashes with integer or `"*"` keys, to have problems distinguishing between arrays and hashes - although this shouldn't be an issue unless your data is very difficult to predict and/or your custom rules are very specific.
224
+
189
225
  #### Sorting arrays before comparison
190
226
 
191
227
  An order difference alone between two arrays can create too many diffs to be useful. Consider sorting them prior to diffing.
@@ -1,5 +1,9 @@
1
1
  # Change Log
2
2
 
3
+ ## v0.3.5 2017-08-06
4
+
5
+ * add option `array_path` #34
6
+
3
7
  ## v0.3.4 2017-05-01
4
8
 
5
9
  * performance improvement of HashDiff#similar? #31
@@ -11,6 +11,7 @@ module HashDiff
11
11
  # * :delimiter (String) ['.'] the delimiter used when returning nested key references
12
12
  # * :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.
13
13
  # * :strip (Boolean) [false] whether or not to call #strip on strings before comparing
14
+ # * :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.
14
15
  #
15
16
  # @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.
16
17
  #
@@ -53,6 +54,7 @@ module HashDiff
53
54
  # * :delimiter (String) ['.'] the delimiter used when returning nested key references
54
55
  # * :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.
55
56
  # * :strip (Boolean) [false] whether or not to call #strip on strings before comparing
57
+ # * :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.
56
58
  #
57
59
  # @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.
58
60
  #
@@ -74,9 +76,12 @@ module HashDiff
74
76
  :delimiter => '.',
75
77
  :strict => true,
76
78
  :strip => false,
77
- :numeric_tolerance => 0
79
+ :numeric_tolerance => 0,
80
+ :array_path => false
78
81
  }.merge!(options)
79
82
 
83
+ opts[:prefix] = [] if opts[:array_path] && opts[:prefix] == ''
84
+
80
85
  opts[:comparison] = block if block_given?
81
86
 
82
87
  # prefer to compare with provided block
@@ -104,23 +109,20 @@ module HashDiff
104
109
  changeset = diff_array(obj1, obj2, opts) do |lcs|
105
110
  # use a's index for similarity
106
111
  lcs.each do |pair|
107
- result.concat(diff(obj1[pair[0]], obj2[pair[1]], opts.merge(:prefix => "#{opts[:prefix]}[#{pair[0]}]")))
112
+ prefix = prefix_append_array_index(opts[:prefix], pair[0], opts)
113
+ result.concat(diff(obj1[pair[0]], obj2[pair[1]], opts.merge(:prefix => prefix)))
108
114
  end
109
115
  end
110
116
 
111
117
  changeset.each do |change|
118
+ change_key = prefix_append_array_index(opts[:prefix], change[1], opts)
112
119
  if change[0] == '-'
113
- result << ['-', "#{opts[:prefix]}[#{change[1]}]", change[2]]
120
+ result << ['-', change_key, change[2]]
114
121
  elsif change[0] == '+'
115
- result << ['+', "#{opts[:prefix]}[#{change[1]}]", change[2]]
122
+ result << ['+', change_key, change[2]]
116
123
  end
117
124
  end
118
125
  elsif obj1.is_a?(Hash)
119
- if opts[:prefix].empty?
120
- prefix = ""
121
- else
122
- prefix = "#{opts[:prefix]}#{opts[:delimiter]}"
123
- end
124
126
 
125
127
  deleted_keys = obj1.keys - obj2.keys
126
128
  common_keys = obj1.keys & obj2.keys
@@ -128,27 +130,32 @@ module HashDiff
128
130
 
129
131
  # add deleted properties
130
132
  deleted_keys.sort_by{|k,v| k.to_s }.each do |k|
131
- custom_result = custom_compare(opts[:comparison], "#{prefix}#{k}", obj1[k], nil)
133
+ change_key = prefix_append_key(opts[:prefix], k, opts)
134
+ custom_result = custom_compare(opts[:comparison], change_key, obj1[k], nil)
132
135
 
133
136
  if custom_result
134
137
  result.concat(custom_result)
135
138
  else
136
- result << ['-', "#{prefix}#{k}", obj1[k]]
139
+ result << ['-', change_key, obj1[k]]
137
140
  end
138
141
  end
139
142
 
140
143
  # recursive comparison for common keys
141
- common_keys.sort_by{|k,v| k.to_s }.each {|k| result.concat(diff(obj1[k], obj2[k], opts.merge(:prefix => "#{prefix}#{k}"))) }
144
+ common_keys.sort_by{|k,v| k.to_s }.each do |k|
145
+ prefix = prefix_append_key(opts[:prefix], k, opts)
146
+ result.concat(diff(obj1[k], obj2[k], opts.merge(:prefix => prefix)))
147
+ end
142
148
 
143
149
  # added properties
144
150
  added_keys.sort_by{|k,v| k.to_s }.each do |k|
151
+ change_key = prefix_append_key(opts[:prefix], k, opts)
145
152
  unless obj1.key?(k)
146
- custom_result = custom_compare(opts[:comparison], "#{prefix}#{k}", nil, obj2[k])
153
+ custom_result = custom_compare(opts[:comparison], change_key, nil, obj2[k])
147
154
 
148
155
  if custom_result
149
156
  result.concat(custom_result)
150
157
  else
151
- result << ['+', "#{prefix}#{k}", obj2[k]]
158
+ result << ['+', change_key, obj2[k]]
152
159
  end
153
160
  end
154
161
  end
@@ -6,7 +6,7 @@ module HashDiff
6
6
  def self.lcs(a, b, options = {})
7
7
  opts = { :similarity => 0.8 }.merge!(options)
8
8
 
9
- opts[:prefix] = "#{opts[:prefix]}[*]"
9
+ opts[:prefix] = prefix_append_array_index(opts[:prefix], '*', opts)
10
10
 
11
11
  return [] if a.size == 0 or b.size == 0
12
12
 
@@ -1,4 +1,4 @@
1
- #
1
+ #
2
2
  # This module provides methods to diff two hash, patch and unpatch hash
3
3
  #
4
4
  module HashDiff
@@ -17,19 +17,21 @@ module HashDiff
17
17
  delimiter = options[:delimiter] || '.'
18
18
 
19
19
  changes.each do |change|
20
- parts = decode_property_path(change[1], delimiter)
20
+ parts = change[1]
21
+ parts = decode_property_path(parts, delimiter) unless parts.is_a?(Array)
22
+
21
23
  last_part = parts.last
22
24
 
23
25
  parent_node = node(obj, parts[0, parts.size-1])
24
26
 
25
27
  if change[0] == '+'
26
- if last_part.is_a?(Integer)
28
+ if parent_node.is_a?(Array)
27
29
  parent_node.insert(last_part, change[2])
28
30
  else
29
31
  parent_node[last_part] = change[2]
30
32
  end
31
33
  elsif change[0] == '-'
32
- if last_part.is_a?(Integer)
34
+ if parent_node.is_a?(Array)
33
35
  parent_node.delete_at(last_part)
34
36
  else
35
37
  parent_node.delete(last_part)
@@ -56,19 +58,21 @@ module HashDiff
56
58
  delimiter = options[:delimiter] || '.'
57
59
 
58
60
  changes.reverse_each do |change|
59
- parts = decode_property_path(change[1], delimiter)
61
+ parts = change[1]
62
+ parts = decode_property_path(parts, delimiter) unless parts.is_a?(Array)
63
+
60
64
  last_part = parts.last
61
65
 
62
66
  parent_node = node(obj, parts[0, parts.size-1])
63
67
 
64
68
  if change[0] == '+'
65
- if last_part.is_a?(Integer)
69
+ if parent_node.is_a?(Array)
66
70
  parent_node.delete_at(last_part)
67
71
  else
68
72
  parent_node.delete(last_part)
69
73
  end
70
74
  elsif change[0] == '-'
71
- if last_part.is_a?(Integer)
75
+ if parent_node.is_a?(Array)
72
76
  parent_node.insert(last_part, change[2])
73
77
  else
74
78
  parent_node[last_part] = change[2]
@@ -55,19 +55,17 @@ module HashDiff
55
55
  #
56
56
  # e.g. "a.b[3].c" => ['a', 'b', 3, 'c']
57
57
  def self.decode_property_path(path, delimiter='.')
58
- parts = path.split(delimiter).collect do |part|
58
+ path.split(delimiter).inject([]) do |memo, part|
59
59
  if part =~ /^(.*)\[(\d+)\]$/
60
60
  if $1.size > 0
61
- [$1, $2.to_i]
61
+ memo + [$1, $2.to_i]
62
62
  else
63
- $2.to_i
63
+ memo + [$2.to_i]
64
64
  end
65
65
  else
66
- part
66
+ memo + [part]
67
67
  end
68
68
  end
69
-
70
- parts.flatten
71
69
  end
72
70
 
73
71
  # @private
@@ -129,4 +127,20 @@ module HashDiff
129
127
  end
130
128
  end
131
129
  end
130
+
131
+ def self.prefix_append_key(prefix, key, opts)
132
+ if opts[:array_path]
133
+ prefix + [key]
134
+ else
135
+ prefix.empty? ? "#{key}" : "#{prefix}#{opts[:delimiter]}#{key}"
136
+ end
137
+ end
138
+
139
+ def self.prefix_append_array_index(prefix, array_index, opts)
140
+ if opts[:array_path]
141
+ prefix + [array_index]
142
+ else
143
+ "#{prefix}[#{array_index}]"
144
+ end
145
+ end
132
146
  end
@@ -1,3 +1,3 @@
1
1
  module HashDiff
2
- VERSION = '0.3.4'
2
+ VERSION = '0.3.5'
3
3
  end
@@ -62,4 +62,13 @@ describe HashDiff do
62
62
  ['+', 'menu.popup.menuitem[1]', {"value" => "Open", "onclick" => "OpenDoc()"}]
63
63
  ]
64
64
  end
65
+
66
+ it "should be able to have an array_path specified" do
67
+ a = {'x' => [{'a' => 1, 'c' => 3, 'e' => 5}, {'y' => 3}]}
68
+ b = {'x' => [{'a' => 1, 'b' => 2, 'e' => 5}] }
69
+
70
+ diff = HashDiff.best_diff(a, b, :array_path => true)
71
+ diff.should == [["-", ["x", 0, "c"], 3], ["+", ["x", 0, "b"], 2], ["-", ["x", 1], {"y"=>3}]]
72
+ end
73
+
65
74
  end
@@ -274,4 +274,40 @@ describe HashDiff do
274
274
  diff.should == [['~', 'b', 'boat', 'truck'], ['~', 'c', 'plane', ' plan']]
275
275
  end
276
276
  end
277
+
278
+ context 'when :array_path is true' do
279
+ it 'should return the diff path in an array rather than a string' do
280
+ x = { 'a' => 'foo' }
281
+ y = { 'a' => 'bar' }
282
+ diff = HashDiff.diff(x, y, :array_path => true)
283
+
284
+ diff.should == [['~', ['a'], 'foo', 'bar']]
285
+ end
286
+
287
+ it 'should show array indexes in paths' do
288
+ x = { 'a' => [0, 1, 2] }
289
+ y = { 'a' => [0, 1, 2, 3] }
290
+
291
+ diff = HashDiff.diff(x, y, :array_path => true)
292
+
293
+ diff.should == [['+', ['a', 3], 3]]
294
+ end
295
+
296
+ it 'should show differences with string and symbol keys' do
297
+ x = { 'a' => 'foo' }
298
+ y = { :a => 'bar' }
299
+
300
+ diff = HashDiff.diff(x, y, :array_path => true)
301
+ diff.should == [['-', ['a'], 'foo'], ['+', [:a], 'bar']]
302
+ end
303
+
304
+ it 'should support other key types' do
305
+ time = Time.now
306
+ x = { time => 'foo' }
307
+ y = { 0 => 'bar' }
308
+
309
+ diff = HashDiff.diff(x, y, :array_path => true)
310
+ diff.should == [['-', [time], 'foo'], ['+', [0], 'bar']]
311
+ end
312
+ end
277
313
  end
@@ -157,5 +157,27 @@ describe HashDiff do
157
157
  HashDiff.unpatch!(b, diff, :delimiter => "\n").should == a
158
158
  end
159
159
 
160
+ it "should be able to patch when the diff is generated with an array_path" do
161
+ a = {"a" => 1, "b" => 1}
162
+ b = {"a" => 1, "b" => 2}
163
+ diff = HashDiff.diff(a, b, :array_path => true)
160
164
 
165
+ HashDiff.patch!(a, diff).should == b
166
+
167
+ a = {"a" => 1, "b" => 1}
168
+ b = {"a" => 1, "b" => 2}
169
+ HashDiff.unpatch!(b, diff).should == a
170
+ end
171
+
172
+ it "should be able to use non string keys when diff is generated with an array_path" do
173
+ a = {"a" => 1, :a => 2, 0 => 3}
174
+ b = {"a" => 5, :a => 6, 0 => 7}
175
+ diff = HashDiff.diff(a, b, :array_path => true)
176
+
177
+ HashDiff.patch!(a, diff).should == b
178
+
179
+ a = {"a" => 1, :a => 2, 0 => 3}
180
+ b = {"a" => 5, :a => 6, 0 => 7}
181
+ HashDiff.unpatch!(b, diff).should == a
182
+ end
161
183
  end
@@ -1,3 +1,10 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+ if ENV['CI'] == 'true'
4
+ require 'codecov'
5
+ SimpleCov.formatter = SimpleCov::Formatter::Codecov
6
+ end
7
+
1
8
  $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
2
9
 
3
10
  require 'rubygems'
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: 0.3.4
4
+ version: 0.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Liu Fengyun
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-05-01 00:00:00.000000000 Z
11
+ date: 2017-08-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec