hashdiff 0.3.4 → 0.3.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/README.md +43 -7
- data/changelog.md +4 -0
- data/lib/hashdiff/diff.rb +21 -14
- data/lib/hashdiff/lcs.rb +1 -1
- data/lib/hashdiff/patch.rb +11 -7
- data/lib/hashdiff/util.rb +20 -6
- data/lib/hashdiff/version.rb +1 -1
- data/spec/hashdiff/best_diff_spec.rb +9 -0
- data/spec/hashdiff/diff_spec.rb +36 -0
- data/spec/hashdiff/patch_spec.rb +22 -0
- data/spec/spec_helper.rb +7 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4ba86448df8842fe6323572feb2c89110954b775
|
4
|
+
data.tar.gz: e3d610f32c07e65e06f825e107371caaaf2e5896
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7e7b28d277160a603df99c83022f6b8bd7bb27ac5d467e351e5834ba955195ed493249b7a7e213dafe0cb1aa630c5de46fe11a95b5942de4ba9c1f4e66262ec3
|
7
|
+
data.tar.gz: d5290cbc0b5c97038cce213e627fc20fcaf4e7405888c217c78b949c8fc98990ab6d3ae18b0f91d2909c069ffccc20a9d0d45aa572419252aa6eae960df6491d
|
data/Gemfile
CHANGED
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
|
76
|
-
b = {a
|
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
|
86
|
-
b = [1, {a
|
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
|
95
|
-
`:strict`, `:numeric_tolerance`, `:strip
|
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
|
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.
|
data/changelog.md
CHANGED
data/lib/hashdiff/diff.rb
CHANGED
@@ -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
|
-
|
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 << ['-',
|
120
|
+
result << ['-', change_key, change[2]]
|
114
121
|
elsif change[0] == '+'
|
115
|
-
result << ['+',
|
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
|
-
|
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 << ['-',
|
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
|
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],
|
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 << ['+',
|
158
|
+
result << ['+', change_key, obj2[k]]
|
152
159
|
end
|
153
160
|
end
|
154
161
|
end
|
data/lib/hashdiff/lcs.rb
CHANGED
data/lib/hashdiff/patch.rb
CHANGED
@@ -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 =
|
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
|
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
|
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 =
|
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
|
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
|
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]
|
data/lib/hashdiff/util.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/hashdiff/version.rb
CHANGED
@@ -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
|
data/spec/hashdiff/diff_spec.rb
CHANGED
@@ -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
|
data/spec/hashdiff/patch_spec.rb
CHANGED
@@ -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
|
data/spec/spec_helper.rb
CHANGED
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
|
+
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-
|
11
|
+
date: 2017-08-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|