hashdiff 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/README.md +116 -43
- data/changelog.md +5 -0
- data/lib/hashdiff/diff.rb +56 -22
- data/lib/hashdiff/lcs.rb +6 -2
- data/lib/hashdiff/patch.rb +4 -4
- data/lib/hashdiff/util.rb +33 -3
- data/lib/hashdiff/version.rb +1 -1
- data/spec/hashdiff/best_diff_spec.rb +14 -0
- data/spec/hashdiff/diff_spec.rb +83 -1
- data/spec/hashdiff/lcs_spec.rb +25 -1
- data/spec/hashdiff/util_spec.rb +34 -3
- 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: ca3dcdddb4d1143b186229d4e7d27e9ded3b752b
|
4
|
+
data.tar.gz: edf3e5858ede25b65ad79a9fcbbb5903ff60e5b7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a7f0e3f36f65b01281792c5f81fc7a3d4f1bc6b9e0bcfa47dd65202aabcb5ea93acb956e6deeadc437b6a0a812a517336eb2430b6d9f1c1056ebbb080980a1f0
|
7
|
+
data.tar.gz: be5bd3ca90b482c7287f65da86b64559cc83f0073901f61f549a627025c277187c160488519ea91a92ab688478bd008af7273fc725e515bd8c6f2f090a074a0a
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -10,93 +10,166 @@ HashDiff is a ruby library to compute the smallest difference between two hashes
|
|
10
10
|
|
11
11
|
Given two Hashes A and B, sometimes you face the question: what's the smallest changes that can be made to change A to B?
|
12
12
|
|
13
|
-
An algorithm responds to this question has to do following:
|
13
|
+
An algorithm that responds to this question has to do following:
|
14
14
|
|
15
15
|
* Generate a list of additions, deletions and changes, so that `A + ChangeSet = B` and `B - ChangeSet = A`.
|
16
16
|
* Compute recursively -- Arrays and Hashes may be nested arbitrarily in A or B.
|
17
|
-
* Compute the smallest change -- it should
|
17
|
+
* Compute the smallest change -- it should recognize similar child Hashes or child Arrays between A and B.
|
18
18
|
|
19
19
|
HashDiff answers the question above in an opinionated approach:
|
20
20
|
|
21
21
|
* Hash can be represented as a list of (dot-syntax-path, value) pairs. For example, `{a:[{c:2}]}` can be represented as `["a[0].c", 2]`.
|
22
|
-
* The change set can be represented using the
|
22
|
+
* The change set can be represented using the dot-syntax representation. For example, `[['-', 'b.x', 3], ['~', 'b.z', 45, 30], ['+', 'b.y', 3]]`.
|
23
23
|
* It compares Arrays using LCS(longest common subsequence) algorithm.
|
24
|
-
* It
|
24
|
+
* It recognizes similar Hashes in an Array using a similarity value (0 < similarity <= 1).
|
25
25
|
|
26
26
|
## Usage
|
27
27
|
|
28
|
-
|
28
|
+
To use the gem, add the following to your Gemfile:
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
require 'hashdiff'
|
30
|
+
```ruby
|
31
|
+
gem 'hashdiff'
|
32
|
+
```
|
35
33
|
|
36
34
|
## Quick Start
|
37
35
|
|
38
36
|
### Diff
|
39
37
|
|
40
|
-
Two simple
|
38
|
+
Two simple hashes:
|
41
39
|
|
42
|
-
|
43
|
-
|
40
|
+
```ruby
|
41
|
+
a = {a:3, b:2}
|
42
|
+
b = {}
|
44
43
|
|
45
|
-
|
46
|
-
|
44
|
+
diff = HashDiff.diff(a, b)
|
45
|
+
diff.should == [['-', 'a', 3], ['-', 'b', 2]]
|
46
|
+
```
|
47
47
|
|
48
|
-
More complex
|
48
|
+
More complex hashes:
|
49
49
|
|
50
|
-
|
51
|
-
|
50
|
+
```ruby
|
51
|
+
a = {a:{x:2, y:3, z:4}, b:{x:3, z:45}}
|
52
|
+
b = {a:{y:3}, b:{y:3, z:30}}
|
52
53
|
|
53
|
-
|
54
|
-
|
54
|
+
diff = HashDiff.diff(a, b)
|
55
|
+
diff.should == [['-', 'a.x', 2], ['-', 'a.z', 4], ['-', 'b.x', 3], ['~', 'b.z', 45, 30], ['+', 'b.y', 3]]
|
56
|
+
```
|
55
57
|
|
56
|
-
|
58
|
+
Arrays in hashes:
|
57
59
|
|
58
|
-
|
59
|
-
|
60
|
+
```ruby
|
61
|
+
a = {a:[{x:2, y:3, z:4}, {x:11, y:22, z:33}], b:{x:3, z:45}}
|
62
|
+
b = {a:[{y:3}, {x:11, z:33}], b:{y:22}}
|
60
63
|
|
61
|
-
|
62
|
-
|
64
|
+
diff = HashDiff.best_diff(a, b)
|
65
|
+
diff.should == [['-', 'a[0].x', 2], ['-', 'a[0].z', 4], ['-', 'a[1].y', 22], ['-', 'b.x', 3], ['-', 'b.z', 45], ['+', 'b.y', 22]]
|
66
|
+
```
|
63
67
|
|
64
68
|
### Patch
|
65
69
|
|
66
70
|
patch example:
|
67
71
|
|
68
|
-
|
69
|
-
|
72
|
+
```ruby
|
73
|
+
a = {a: 3}
|
74
|
+
b = {a: {a1: 1, a2: 2}}
|
70
75
|
|
71
|
-
|
72
|
-
|
76
|
+
diff = HashDiff.diff(a, b)
|
77
|
+
HashDiff.patch!(a, diff).should == b
|
78
|
+
```
|
73
79
|
|
74
80
|
unpatch example:
|
75
81
|
|
76
|
-
|
77
|
-
|
82
|
+
```ruby
|
83
|
+
a = [{a: 1, b: 2, c: 3, d: 4, e: 5}, {x: 5, y: 6, z: 3}, 1]
|
84
|
+
b = [1, {a: 1, b: 2, c: 3, e: 5}]
|
78
85
|
|
79
|
-
|
80
|
-
|
86
|
+
diff = HashDiff.diff(a, b) # diff two array is OK
|
87
|
+
HashDiff.unpatch!(b, diff).should == a
|
88
|
+
```
|
81
89
|
|
82
90
|
### Options
|
83
91
|
|
84
|
-
There
|
92
|
+
There are five options available: `:delimiter`, `:similarity`, `:strict`, `:numeric_tolerance` and `:strip`.
|
93
|
+
|
94
|
+
#### `:delimiter`
|
95
|
+
|
96
|
+
You can specify `:delimiter` to be something other than the default dot. For example:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
a = {a:{x:2, y:3, z:4}, b:{x:3, z:45}}
|
100
|
+
b = {a:{y:3}, b:{y:3, z:30}}
|
101
|
+
|
102
|
+
diff = HashDiff.diff(a, b, :delimiter => '\t')
|
103
|
+
diff.should == [['-', 'a\tx', 2], ['-', 'a\tz', 4], ['-', 'b\tx', 3], ['~', 'b\tz', 45, 30], ['+', 'b\ty', 3]]
|
104
|
+
```
|
105
|
+
|
106
|
+
#### `:similarity`
|
107
|
+
|
108
|
+
In cases where you have similar hash objects in arrays, you can pass a custom value for `:similarity` instead of the default `0.8`. This is interpreted as a ratio of similarity (default is 80% similar, whereas `:similarity => 0.5` would look for at least a 50% similarity).
|
109
|
+
|
110
|
+
#### `:strict`
|
111
|
+
|
112
|
+
The `:strict` option, which defaults to `true`, specifies whether numeric types are compared on type as well as value. By default, a Fixnum will never be equal to a Float (e.g. 4 != 4.0). Setting `:strict` to false makes the comparison looser (e.g. 4 == 4.0).
|
113
|
+
|
114
|
+
#### `:numeric_tolerance`
|
115
|
+
|
116
|
+
The :numeric_tolerance option allows for a small numeric tolerance.
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
a = {x:5, y:3.75, z:7}
|
120
|
+
b = {x:6, y:3.76, z:7}
|
121
|
+
|
122
|
+
diff = HashDiff.diff(a, b, :numeric_tolerance => 0.1)
|
123
|
+
diff.should == [["~", "x", 5, 6]]
|
124
|
+
```
|
125
|
+
|
126
|
+
#### `:strip`
|
127
|
+
|
128
|
+
The :strip option strips all strings before comparing.
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
a = {x:5, s:'foo '}
|
132
|
+
b = {x:6, s:'foo'}
|
133
|
+
|
134
|
+
diff = HashDiff.diff(a, b, :comparison => { :numeric_tolerance => 0.1, :strip => true })
|
135
|
+
diff.should == [["~", "x", 5, 6]]
|
136
|
+
```
|
137
|
+
|
138
|
+
#### Specifying a custom comparison method
|
139
|
+
|
140
|
+
It's possible to specify how the values of a key should be compared.
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
a = {a:'car', b:'boat', c:'plane'}
|
144
|
+
b = {a:'bus', b:'truck', c:' plan'}
|
145
|
+
|
146
|
+
diff = HashDiff.diff(a, b) do |path, obj1, obj2|
|
147
|
+
case path
|
148
|
+
when /a|b|c/
|
149
|
+
obj1.length == obj2.length
|
150
|
+
end
|
151
|
+
end
|
85
152
|
|
86
|
-
|
153
|
+
diff.should == [['~', 'b', 'boat', 'truck']]
|
154
|
+
```
|
87
155
|
|
88
|
-
|
89
|
-
b = {a:{y:3}, b:{y:3, z:30}}
|
156
|
+
The yielded params of the comparison block is `|path, obj1, obj2|`, in which path is the key (or delimited compound key) to the value being compared. When comparing elements in array, the path is with the format `array[*]`. For example:
|
90
157
|
|
91
|
-
|
92
|
-
|
158
|
+
```ruby
|
159
|
+
a = {a:'car', b:['boat', 'plane'] }
|
160
|
+
b = {a:'bus', b:['truck', ' plan'] }
|
93
161
|
|
94
|
-
|
162
|
+
diff = HashDiff.diff(a, b) do |path, obj1, obj2|
|
163
|
+
case path
|
164
|
+
when 'b[*]'
|
165
|
+
obj1.length == obj2.length
|
166
|
+
end
|
167
|
+
end
|
95
168
|
|
96
|
-
|
169
|
+
diff.should == [["~", "a", "car", "bus"], ["~", "b[1]", "plane", " plan"], ["-", "b[0]", "boat"], ["+", "b[0]", "truck"]]
|
170
|
+
```
|
97
171
|
|
98
|
-
|
99
|
-
- [@m-o-e](https://github.com/m-o-e)
|
172
|
+
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.
|
100
173
|
|
101
174
|
## License
|
102
175
|
|
data/changelog.md
CHANGED
data/lib/hashdiff/diff.rb
CHANGED
@@ -2,12 +2,17 @@ module HashDiff
|
|
2
2
|
|
3
3
|
# Best diff two objects, which tries to generate the smallest change set using different similarity values.
|
4
4
|
#
|
5
|
-
# HashDiff.best_diff is useful in case of comparing two objects which
|
5
|
+
# HashDiff.best_diff is useful in case of comparing two objects which include similar hashes in arrays.
|
6
6
|
#
|
7
|
-
# @param [
|
8
|
-
# @param [
|
9
|
-
# @param [Hash] options
|
10
|
-
# :
|
7
|
+
# @param [Array, Hash] obj1
|
8
|
+
# @param [Array, Hash] obj2
|
9
|
+
# @param [Hash] options the options to use when comparing
|
10
|
+
# * :strict (Boolean) [true] whether numeric values will be compared on type as well as value. Set to false to allow comparing Fixnum, Float, BigDecimal to each other
|
11
|
+
# * :delimiter (String) ['.'] the delimiter used when returning nested key references
|
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
|
+
# * :strip (Boolean) [false] whether or not to call #strip on strings before comparing
|
14
|
+
#
|
15
|
+
# @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.
|
11
16
|
#
|
12
17
|
# @return [Array] an array of changes.
|
13
18
|
# e.g. [[ '+', 'a.b', '45' ], [ '-', 'a.c', '5' ], [ '~', 'a.x', '45', '63']]
|
@@ -19,7 +24,9 @@ module HashDiff
|
|
19
24
|
# diff.should == [['-', 'x[0].c', 3], ['+', 'x[0].b', 2], ['-', 'x[1].y', 3], ['-', 'x[1]', {}]]
|
20
25
|
#
|
21
26
|
# @since 0.0.1
|
22
|
-
def self.best_diff(obj1, obj2, options = {})
|
27
|
+
def self.best_diff(obj1, obj2, options = {}, &block)
|
28
|
+
options[:comparison] = block if block_given?
|
29
|
+
|
23
30
|
opts = {similarity: 0.3}.merge!(options)
|
24
31
|
diffs_1 = diff(obj1, obj2, opts)
|
25
32
|
count_1 = count_diff diffs_1
|
@@ -36,14 +43,18 @@ module HashDiff
|
|
36
43
|
diffs = count < count_3 ? diffs : diffs_3
|
37
44
|
end
|
38
45
|
|
39
|
-
# Compute the diff of two hashes
|
46
|
+
# Compute the diff of two hashes or arrays
|
40
47
|
#
|
41
|
-
# @param [
|
42
|
-
# @param [
|
43
|
-
# @param [Hash] options
|
44
|
-
# :
|
48
|
+
# @param [Array, Hash] obj1
|
49
|
+
# @param [Array, Hash] obj2
|
50
|
+
# @param [Hash] options the options to use when comparing
|
51
|
+
# * :strict (Boolean) [true] whether numeric values will be compared on type as well as value. Set to false to allow comparing Fixnum, Float, BigDecimal to each other
|
52
|
+
# * :similarity (Numeric) [0.8] should be between (0, 1]. Meaningful if there are similar hashes in arrays. See {best_diff}.
|
53
|
+
# * :delimiter (String) ['.'] the delimiter used when returning nested key references
|
54
|
+
# * :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
|
+
# * :strip (Boolean) [false] whether or not to call #strip on strings before comparing
|
45
56
|
#
|
46
|
-
#
|
57
|
+
# @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.
|
47
58
|
#
|
48
59
|
# @return [Array] an array of changes.
|
49
60
|
# e.g. [[ '+', 'a.b', '45' ], [ '-', 'a.c', '5' ], [ '~', 'a.x', '45', '63']]
|
@@ -56,14 +67,31 @@ module HashDiff
|
|
56
67
|
# diff.should == [['-', 'b.b1', 1], ['-', 'b.b2', 2]]
|
57
68
|
#
|
58
69
|
# @since 0.0.1
|
59
|
-
def self.diff(obj1, obj2, options = {})
|
70
|
+
def self.diff(obj1, obj2, options = {}, &block)
|
60
71
|
opts = {
|
61
72
|
:prefix => '',
|
62
73
|
:similarity => 0.8,
|
63
|
-
:delimiter => '.'
|
64
|
-
|
65
|
-
|
66
|
-
|
74
|
+
:delimiter => '.',
|
75
|
+
:strict => true,
|
76
|
+
:strip => false,
|
77
|
+
:numeric_tolerance => 0
|
78
|
+
}.merge!(options)
|
79
|
+
|
80
|
+
opts[:comparison] = block if block_given?
|
81
|
+
|
82
|
+
# prefer to compare with provided block
|
83
|
+
if opts[:comparison]
|
84
|
+
res = opts[:comparison].call(opts[:prefix], obj1, obj2)
|
85
|
+
|
86
|
+
# nil != false here
|
87
|
+
if res == false
|
88
|
+
return [['~', opts[:prefix], obj1, obj2]]
|
89
|
+
elsif res == true
|
90
|
+
return []
|
91
|
+
else
|
92
|
+
# compare with specified built-in helpers
|
93
|
+
end
|
94
|
+
end
|
67
95
|
|
68
96
|
if obj1.nil? and obj2.nil?
|
69
97
|
return []
|
@@ -77,13 +105,13 @@ module HashDiff
|
|
77
105
|
return [['~', opts[:prefix], obj1, nil]]
|
78
106
|
end
|
79
107
|
|
80
|
-
|
108
|
+
unless comparable?(obj1, obj2, opts[:strict])
|
81
109
|
return [['~', opts[:prefix], obj1, obj2]]
|
82
110
|
end
|
83
111
|
|
84
112
|
result = []
|
85
113
|
if obj1.is_a?(Array)
|
86
|
-
changeset = diff_array(obj1, obj2, opts
|
114
|
+
changeset = diff_array(obj1, obj2, opts) do |lcs|
|
87
115
|
# use a's index for similarity
|
88
116
|
lcs.each do |pair|
|
89
117
|
result.concat(diff(obj1[pair[0]], obj2[pair[1]], opts.merge(prefix: "#{opts[:prefix]}[#{pair[0]}]")))
|
@@ -128,7 +156,7 @@ module HashDiff
|
|
128
156
|
end
|
129
157
|
end
|
130
158
|
else
|
131
|
-
return [] if obj1
|
159
|
+
return [] if compare_values(obj1, obj2, opts)
|
132
160
|
return [['~', opts[:prefix], obj1, obj2]]
|
133
161
|
end
|
134
162
|
|
@@ -138,7 +166,13 @@ module HashDiff
|
|
138
166
|
# @private
|
139
167
|
#
|
140
168
|
# diff array using LCS algorithm
|
141
|
-
def self.diff_array(a, b,
|
169
|
+
def self.diff_array(a, b, options = {})
|
170
|
+
opts = {
|
171
|
+
:prefix => '',
|
172
|
+
:similarity => 0.8,
|
173
|
+
:delimiter => '.'
|
174
|
+
}.merge!(options)
|
175
|
+
|
142
176
|
change_set = []
|
143
177
|
if a.size == 0 and b.size == 0
|
144
178
|
return []
|
@@ -155,7 +189,7 @@ module HashDiff
|
|
155
189
|
return change_set
|
156
190
|
end
|
157
191
|
|
158
|
-
links = lcs(a, b,
|
192
|
+
links = lcs(a, b, opts)
|
159
193
|
|
160
194
|
# yield common
|
161
195
|
yield links if block_given?
|
data/lib/hashdiff/lcs.rb
CHANGED
@@ -3,7 +3,11 @@ module HashDiff
|
|
3
3
|
#
|
4
4
|
# caculate array difference using LCS algorithm
|
5
5
|
# http://en.wikipedia.org/wiki/Longest_common_subsequence_problem
|
6
|
-
def self.lcs(a, b,
|
6
|
+
def self.lcs(a, b, options = {})
|
7
|
+
opts = { :similarity => 0.8 }.merge!(options)
|
8
|
+
|
9
|
+
opts[:prefix] = "#{opts[:prefix]}[*]"
|
10
|
+
|
7
11
|
return [] if a.size == 0 or b.size == 0
|
8
12
|
|
9
13
|
a_start = b_start = 0
|
@@ -15,7 +19,7 @@ module HashDiff
|
|
15
19
|
(0..b_finish).each do |bi|
|
16
20
|
lcs[bi] = []
|
17
21
|
(0..a_finish).each do |ai|
|
18
|
-
if similar?(a[ai], b[bi],
|
22
|
+
if similar?(a[ai], b[bi], opts)
|
19
23
|
topleft = (ai > 0 and bi > 0)? lcs[bi-1][ai-1][1] : 0
|
20
24
|
lcs[bi][ai] = [:topleft, topleft + 1]
|
21
25
|
elsif
|
data/lib/hashdiff/patch.rb
CHANGED
@@ -5,10 +5,10 @@ module HashDiff
|
|
5
5
|
|
6
6
|
# Apply patch to object
|
7
7
|
#
|
8
|
-
# @param [Hash, Array] obj the object to be
|
8
|
+
# @param [Hash, Array] obj the object to be patched, can be an Array or a Hash
|
9
9
|
# @param [Array] changes e.g. [[ '+', 'a.b', '45' ], [ '-', 'a.c', '5' ], [ '~', 'a.x', '45', '63']]
|
10
10
|
# @param [Hash] options supports following keys:
|
11
|
-
# :delimiter
|
11
|
+
# * :delimiter (String) ['.'] delimiter string for representing nested keys in changes array
|
12
12
|
#
|
13
13
|
# @return the object after patch
|
14
14
|
#
|
@@ -44,10 +44,10 @@ module HashDiff
|
|
44
44
|
|
45
45
|
# Unpatch an object
|
46
46
|
#
|
47
|
-
# @param [Hash, Array] obj the object to be
|
47
|
+
# @param [Hash, Array] obj the object to be unpatched, can be an Array or a Hash
|
48
48
|
# @param [Array] changes e.g. [[ '+', 'a.b', '45' ], [ '-', 'a.c', '5' ], [ '~', 'a.x', '45', '63']]
|
49
49
|
# @param [Hash] options supports following keys:
|
50
|
-
# :delimiter
|
50
|
+
# * :delimiter (String) ['.'] delimiter string for representing nested keys in changes array
|
51
51
|
#
|
52
52
|
# @return the object after unpatch
|
53
53
|
#
|
data/lib/hashdiff/util.rb
CHANGED
@@ -3,15 +3,17 @@ module HashDiff
|
|
3
3
|
# @private
|
4
4
|
#
|
5
5
|
# judge whether two objects are similar
|
6
|
-
def self.similar?(a, b,
|
6
|
+
def self.similar?(a, b, options = {})
|
7
|
+
opts = { :similarity => 0.8 }.merge(options)
|
8
|
+
|
7
9
|
count_a = count_nodes(a)
|
8
10
|
count_b = count_nodes(b)
|
9
|
-
diffs = count_diff diff(a, b,
|
11
|
+
diffs = count_diff diff(a, b, opts)
|
10
12
|
|
11
13
|
if count_a + count_b == 0
|
12
14
|
return true
|
13
15
|
else
|
14
|
-
(1 - diffs.to_f/(count_a + count_b).to_f) >= similarity
|
16
|
+
(1 - diffs.to_f/(count_a + count_b).to_f) >= opts[:similarity]
|
15
17
|
end
|
16
18
|
end
|
17
19
|
|
@@ -78,4 +80,32 @@ module HashDiff
|
|
78
80
|
temp
|
79
81
|
end
|
80
82
|
|
83
|
+
# @private
|
84
|
+
#
|
85
|
+
# check for equality or "closeness" within given tolerance
|
86
|
+
def self.compare_values(obj1, obj2, options = {})
|
87
|
+
if (options[:numeric_tolerance].is_a? Numeric) &&
|
88
|
+
[obj1, obj2].all? { |v| v.is_a? Numeric }
|
89
|
+
return (obj1 - obj2).abs <= options[:numeric_tolerance]
|
90
|
+
end
|
91
|
+
|
92
|
+
if options[:strip] == true
|
93
|
+
first = obj1.strip if obj1.respond_to?(:strip)
|
94
|
+
second = obj2.strip if obj2.respond_to?(:strip)
|
95
|
+
return first == second
|
96
|
+
end
|
97
|
+
|
98
|
+
obj1 == obj2
|
99
|
+
end
|
100
|
+
|
101
|
+
# @private
|
102
|
+
#
|
103
|
+
# check if objects are comparable
|
104
|
+
def self.comparable?(obj1, obj2, strict = true)
|
105
|
+
[Array, Hash].each do |type|
|
106
|
+
return true if obj1.is_a?(type) && obj2.is_a?(type)
|
107
|
+
end
|
108
|
+
return true if !strict && obj1.is_a?(Numeric) && obj2.is_a?(Numeric)
|
109
|
+
obj1.is_a?(obj2.class) && obj2.is_a?(obj1.class)
|
110
|
+
end
|
81
111
|
end
|
data/lib/hashdiff/version.rb
CHANGED
@@ -17,6 +17,20 @@ describe HashDiff do
|
|
17
17
|
diff.should == [["-", "x[0]\tc", 3], ["+", "x[0]\tb", 2], ["-", "x[1]", {"y"=>3}]]
|
18
18
|
end
|
19
19
|
|
20
|
+
it "should use custom comparison when provided" do
|
21
|
+
a = {'x' => [{'a' => 'foo', 'c' => 'goat', 'e' => 'snake'}, {'y' => 'baz'}]}
|
22
|
+
b = {'x' => [{'a' => 'bar', 'b' => 'cow', 'e' => 'puppy'}] }
|
23
|
+
|
24
|
+
diff = HashDiff.best_diff(a, b) do |path, obj1, obj2|
|
25
|
+
case path
|
26
|
+
when /^x\[.\]\..$/
|
27
|
+
obj1.length == obj2.length
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
diff.should == [["-", "x[0].c", 'goat'], ["+", "x[0].b", 'cow'], ["-", "x[1]", {"y"=>'baz'}]]
|
32
|
+
end
|
33
|
+
|
20
34
|
it "should be able to best diff array in hash" do
|
21
35
|
a = {"menu" => {
|
22
36
|
"id" => "file",
|
data/spec/hashdiff/diff_spec.rb
CHANGED
@@ -22,6 +22,16 @@ describe HashDiff do
|
|
22
22
|
diff.should == []
|
23
23
|
end
|
24
24
|
|
25
|
+
it "should be able to diff two hashes with equivalent numerics, when strict is false" do
|
26
|
+
diff = HashDiff.diff({a:2.0, b:2}, {a:2, b:2.0}, :strict => false)
|
27
|
+
diff.should == []
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should be able to diff changes in hash value" do
|
31
|
+
diff = HashDiff.diff({a:2, b:3, c:" hello"}, {a:2, b:4, c:"hello"})
|
32
|
+
diff.should == [['~', 'b', 3, 4], ['~', 'c', " hello", "hello"]]
|
33
|
+
end
|
34
|
+
|
25
35
|
it "should be able to diff changes in hash value which is array" do
|
26
36
|
diff = HashDiff.diff({a:2, b:[1, 2, 3]}, {a:2, b:[1, 3, 4]})
|
27
37
|
diff.should == [['-', 'b[1]', 2], ['+', 'b[2]', 4]]
|
@@ -131,5 +141,77 @@ describe HashDiff do
|
|
131
141
|
diff.should == [["-", "[0]\td", 4], ["-", "[1]", {"x"=>5, "y"=>6, "z"=>3}]]
|
132
142
|
end
|
133
143
|
|
144
|
+
context 'when :numeric_tolerance requested' do
|
145
|
+
it "should be able to diff changes in hash value" do
|
146
|
+
a = {'a' => 0.558, 'b' => 0.0, 'c' => 0.65, 'd' => 'fin'}
|
147
|
+
b = {'a' => 0.557, 'b' => 'hats', 'c' => 0.67, 'd' => 'fin'}
|
148
|
+
|
149
|
+
diff = HashDiff.diff(a, b, :numeric_tolerance => 0.01)
|
150
|
+
diff.should == [["~", "b", 0.0, 'hats'], ["~", "c", 0.65, 0.67]]
|
151
|
+
|
152
|
+
diff = HashDiff.diff(b, a, :numeric_tolerance => 0.01)
|
153
|
+
diff.should == [["~", "b", 'hats', 0.0], ["~", "c", 0.67, 0.65]]
|
154
|
+
end
|
155
|
+
|
156
|
+
it "should be able to diff changes in nested values" do
|
157
|
+
a = {'a' => {'x' => 0.4, 'y' => 0.338}, 'b' => [13, 68.03]}
|
158
|
+
b = {'a' => {'x' => 0.6, 'y' => 0.341}, 'b' => [14, 68.025]}
|
159
|
+
|
160
|
+
diff = HashDiff.diff(a, b, :numeric_tolerance => 0.01)
|
161
|
+
diff.should == [["~", "a.x", 0.4, 0.6], ["-", "b[0]", 13], ["+", "b[0]", 14]]
|
162
|
+
|
163
|
+
diff = HashDiff.diff(b, a, :numeric_tolerance => 0.01)
|
164
|
+
diff.should == [["~", "a.x", 0.6, 0.4], ["-", "b[0]", 14], ["+", "b[0]", 13]]
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
context 'when :strip requested' do
|
169
|
+
it "should strip strings before comparing" do
|
170
|
+
a = {a:" foo", b:"fizz buzz"}
|
171
|
+
b = {a:"foo", b:"fizzbuzz"}
|
172
|
+
diff = HashDiff.diff(a, b, :strip => true)
|
173
|
+
diff.should == [['~', 'b', "fizz buzz", "fizzbuzz"]]
|
174
|
+
end
|
175
|
+
|
176
|
+
it "should strip nested strings before comparing" do
|
177
|
+
a = {a:{x:" foo"}, b:["fizz buzz", "nerf"]}
|
178
|
+
b = {a:{x:"foo"}, b:["fizzbuzz", "nerf"]}
|
179
|
+
diff = HashDiff.diff(a, b, :strip => true)
|
180
|
+
diff.should == [['-', 'b[0]', "fizz buzz"], ['+', 'b[0]', "fizzbuzz"]]
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
context 'when both :strip and :numeric_tolerance requested' do
|
185
|
+
it 'should apply filters to proper object types' do
|
186
|
+
a = {a:" foo", b:35, c:'bar', d:'baz'}
|
187
|
+
b = {a:"foo", b:35.005, c:'bar', d:18.5}
|
188
|
+
diff = HashDiff.diff(a, b, :strict => false, :numeric_tolerance => 0.01, :strip => true)
|
189
|
+
diff.should == [['~', 'd', "baz", 18.5]]
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
context 'with custom comparison' do
|
194
|
+
let(:a) { {a:'car', b:'boat', c:'plane'} }
|
195
|
+
let(:b) { {a:'bus', b:'truck', c:' plan'} }
|
196
|
+
|
197
|
+
it 'should compare using proc specified in block' do
|
198
|
+
diff = HashDiff.diff(a, b) do |prefix, obj1, obj2|
|
199
|
+
case prefix
|
200
|
+
when /a|b|c/
|
201
|
+
obj1.length == obj2.length
|
202
|
+
end
|
203
|
+
end
|
204
|
+
diff.should == [['~', 'b', 'boat', 'truck']]
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'should compare with both proc and :strip when both provided' do
|
208
|
+
diff = HashDiff.diff(a, b, :strip => true) do |prefix, obj1, obj2|
|
209
|
+
case prefix
|
210
|
+
when 'a'
|
211
|
+
obj1.length == obj2.length
|
212
|
+
end
|
213
|
+
end
|
214
|
+
diff.should == [['~', 'b', 'boat', 'truck'], ['~', 'c', 'plane', ' plan']]
|
215
|
+
end
|
216
|
+
end
|
134
217
|
end
|
135
|
-
|
data/spec/hashdiff/lcs_spec.rb
CHANGED
@@ -9,6 +9,22 @@ describe HashDiff do
|
|
9
9
|
lcs.should == [[0, 0], [1, 1], [2, 2]]
|
10
10
|
end
|
11
11
|
|
12
|
+
it "should be able to find LCS between two close arrays" do
|
13
|
+
a = [1.05, 2, 3.25]
|
14
|
+
b = [1.06, 2, 3.24]
|
15
|
+
|
16
|
+
lcs = HashDiff.lcs(a, b, :numeric_tolerance => 0.1)
|
17
|
+
lcs.should == [[0, 0], [1, 1], [2, 2]]
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should strip strings when finding LCS if requested" do
|
21
|
+
a = ['foo', 'bar', 'baz']
|
22
|
+
b = [' foo', 'bar', 'zab']
|
23
|
+
|
24
|
+
lcs = HashDiff.lcs(a, b, :strip => true)
|
25
|
+
lcs.should == [[0, 0], [1, 1]]
|
26
|
+
end
|
27
|
+
|
12
28
|
it "should be able to find LCS with one common elements" do
|
13
29
|
a = [1, 2, 3]
|
14
30
|
b = [1, 8, 7]
|
@@ -25,6 +41,14 @@ describe HashDiff do
|
|
25
41
|
lcs.should == [[1, 1], [2, 3]]
|
26
42
|
end
|
27
43
|
|
44
|
+
it "should be able to find LCS with two close elements" do
|
45
|
+
a = [1, 3.05, 5, 7]
|
46
|
+
b = [2, 3.06, 7, 5]
|
47
|
+
|
48
|
+
lcs = HashDiff.lcs(a, b, :numeric_tolerance => 0.1)
|
49
|
+
lcs.should == [[1, 1], [2, 3]]
|
50
|
+
end
|
51
|
+
|
28
52
|
it "should be able to find LCS with two common elements in different ordering" do
|
29
53
|
a = [1, 3, 4, 7]
|
30
54
|
b = [2, 3, 7, 5]
|
@@ -44,7 +68,7 @@ describe HashDiff do
|
|
44
68
|
{"value" => "Close", "onclick" => "CloseDoc()"}
|
45
69
|
]
|
46
70
|
|
47
|
-
lcs = HashDiff.lcs(a, b, 0.5)
|
71
|
+
lcs = HashDiff.lcs(a, b, :similarity => 0.5)
|
48
72
|
lcs.should == [[0, 0], [1, 2]]
|
49
73
|
end
|
50
74
|
end
|
data/spec/hashdiff/util_spec.rb
CHANGED
@@ -15,7 +15,14 @@ describe HashDiff do
|
|
15
15
|
a = {'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}
|
16
16
|
b = {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}
|
17
17
|
HashDiff.similar?(a, b).should be_true
|
18
|
-
HashDiff.similar?(a, b, 1).should be_false
|
18
|
+
HashDiff.similar?(a, b, :similarity => 1).should be_false
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should be able to tell similiar hash with values within tolerance" do
|
22
|
+
a = {'a' => 1.5, 'b' => 2.25, 'c' => 3, 'd' => 4, 'e' => 5}
|
23
|
+
b = {'a' => 1.503, 'b' => 2.22, 'c' => 3, 'e' => 5}
|
24
|
+
HashDiff.similar?(a, b, :numeric_tolerance => 0.05).should be_true
|
25
|
+
HashDiff.similar?(a, b).should be_false
|
19
26
|
end
|
20
27
|
|
21
28
|
it "should be able to tell numbers and strings" do
|
@@ -29,14 +36,38 @@ describe HashDiff do
|
|
29
36
|
a = {"value" => "New1", "onclick" => "CreateNewDoc()"}
|
30
37
|
b = {"value" => "New", "onclick" => "CreateNewDoc()"}
|
31
38
|
|
32
|
-
HashDiff.similar?(a, b, 0.5).should be_true
|
39
|
+
HashDiff.similar?(a, b, :similarity => 0.5).should be_true
|
33
40
|
end
|
34
41
|
|
35
42
|
it "should be able to tell false when similarity == 0.5" do
|
36
43
|
a = {"value" => "New1", "onclick" => "open()"}
|
37
44
|
b = {"value" => "New", "onclick" => "CreateNewDoc()"}
|
38
45
|
|
39
|
-
HashDiff.similar?(a, b, 0.5).should be_false
|
46
|
+
HashDiff.similar?(a, b, :similarity => 0.5).should be_false
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '.compare_values' do
|
50
|
+
it "should compare numeric values exactly when no tolerance" do
|
51
|
+
expect(HashDiff.compare_values(10.004, 10.003)).to be_false
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should allow tolerance with numeric values" do
|
55
|
+
expect(HashDiff.compare_values(10.004, 10.003, :numeric_tolerance => 0.01)).to be_true
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should compare other objects with or without tolerance" do
|
59
|
+
expect(HashDiff.compare_values('hats', 'ninjas')).to be_false
|
60
|
+
expect(HashDiff.compare_values('hats', 'ninjas', :numeric_tolerance => 0.01)).to be_false
|
61
|
+
expect(HashDiff.compare_values('horse', 'horse')).to be_true
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should compare strings exactly by default' do
|
65
|
+
expect(HashDiff.compare_values(' horse', 'horse')).to be_false
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should strip strings before comparing when requested' do
|
69
|
+
expect(HashDiff.compare_values(' horse', 'horse', :strip => true)).to be_true
|
70
|
+
end
|
40
71
|
end
|
41
72
|
end
|
42
73
|
|
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.
|
4
|
+
version: 0.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:
|
11
|
+
date: 2014-03-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|