hashdiff 0.1.1 → 0.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 +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
|