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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f76f1e64a73818c32f00f87cac3ba70d68253b10
4
- data.tar.gz: a21cb6f9b8657866cca3628f1489301546baf2f8
3
+ metadata.gz: ca3dcdddb4d1143b186229d4e7d27e9ded3b752b
4
+ data.tar.gz: edf3e5858ede25b65ad79a9fcbbb5903ff60e5b7
5
5
  SHA512:
6
- metadata.gz: 9d5e16abeea6634037f6e3696530ea575a8373a19a431ecbe4ab792e9e47d907a380fee1eb65f9aa33e5b0fc48e1237a004fc1859d74583161909ee35d72ac4d
7
- data.tar.gz: 92a95d686c55d47db575f768cbf9fc0c4e274b86b1c454f6780135a2db5bf6ac007fa54e5e3e3fad37628313791624634a1e309999e1810d21a37585a051cdfb
6
+ metadata.gz: a7f0e3f36f65b01281792c5f81fc7a3d4f1bc6b9e0bcfa47dd65202aabcb5ea93acb956e6deeadc437b6a0a812a517336eb2430b6d9f1c1056ebbb080980a1f0
7
+ data.tar.gz: be5bd3ca90b482c7287f65da86b64559cc83f0073901f61f549a627025c277187c160488519ea91a92ab688478bd008af7273fc725e515bd8c6f2f090a074a0a
@@ -1,6 +1,6 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.9.2
4
3
  - 1.9.3
5
4
  - 2.0.0
5
+ - 2.1.1
6
6
  script: "bundle exec rake spec"
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 recoganize similar child Hashes or child Arrays between A and B.
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 do-syntax representation. For example, `[['-', 'b.x', 3], ['~', 'b.z', 45, 30], ['+', 'b.y', 3]]`.
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 recoganize similar Hashes in Array using a similarity value(0 < similarity <= 1).
24
+ * It recognizes similar Hashes in an Array using a similarity value (0 < similarity <= 1).
25
25
 
26
26
  ## Usage
27
27
 
28
- If you're using bundler, add following to the Gemfile:
28
+ To use the gem, add the following to your Gemfile:
29
29
 
30
- gem 'hashdiff'
31
-
32
- Or, you can run `gem install hashdiff`, then add following line to your ruby file which uses HashDiff:
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 hash:
38
+ Two simple hashes:
41
39
 
42
- a = {a:3, b:2}
43
- b = {}
40
+ ```ruby
41
+ a = {a:3, b:2}
42
+ b = {}
44
43
 
45
- diff = HashDiff.diff(a, b)
46
- diff.should == [['-', 'a', 3], ['-', 'b', 2]]
44
+ diff = HashDiff.diff(a, b)
45
+ diff.should == [['-', 'a', 3], ['-', 'b', 2]]
46
+ ```
47
47
 
48
- More complex hash:
48
+ More complex hashes:
49
49
 
50
- a = {a:{x:2, y:3, z:4}, b:{x:3, z:45}}
51
- b = {a:{y:3}, b:{y:3, z:30}}
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
- diff = HashDiff.diff(a, b)
54
- diff.should == [['-', 'a.x', 2], ['-', 'a.z', 4], ['-', 'b.x', 3], ['~', 'b.z', 45, 30], ['+', 'b.y', 3]]
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
- Array in hash:
58
+ Arrays in hashes:
57
59
 
58
- a = {a:[{x:2, y:3, z:4}, {x:11, y:22, z:33}], b:{x:3, z:45}}
59
- b = {a:[{y:3}, {x:11, z:33}], b:{y:22}}
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
- diff = HashDiff.best_diff(a, b)
62
- diff.should == [['-', 'a[0].x', 2], ['-', 'a[0].z', 4], ['-', 'a[1].y', 22], ['-', 'b.x', 3], ['-', 'b.z', 45], ['+', 'b.y', 22]]
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
- a = {a: 3}
69
- b = {a: {a1: 1, a2: 2}}
72
+ ```ruby
73
+ a = {a: 3}
74
+ b = {a: {a1: 1, a2: 2}}
70
75
 
71
- diff = HashDiff.diff(a, b)
72
- HashDiff.patch!(a, diff).should == b
76
+ diff = HashDiff.diff(a, b)
77
+ HashDiff.patch!(a, diff).should == b
78
+ ```
73
79
 
74
80
  unpatch example:
75
81
 
76
- a = [{a: 1, b: 2, c: 3, d: 4, e: 5}, {x: 5, y: 6, z: 3}, 1]
77
- b = [1, {a: 1, b: 2, c: 3, e: 5}]
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
- diff = HashDiff.diff(a, b) # diff two array is OK
80
- HashDiff.unpatch!(b, diff).should == a
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're two options available: `:delimiter` and `:similarity`.
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
- You can specify `:delimiter` to be something else than the dot. For example:
153
+ diff.should == [['~', 'b', 'boat', 'truck']]
154
+ ```
87
155
 
88
- a = {a:{x:2, y:3, z:4}, b:{x:3, z:45}}
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
- diff = HashDiff.diff(a, b, :delimiter => '\t')
92
- diff.should == [['-', 'a\tx', 2], ['-', 'a\tz', 4], ['-', 'b\tx', 3], ['~', 'b\tz', 45, 30], ['+', 'b\ty', 3]]
158
+ ```ruby
159
+ a = {a:'car', b:['boat', 'plane'] }
160
+ b = {a:'bus', b:['truck', ' plan'] }
93
161
 
94
- In cases you have similar hash objects in array, you can pass a custom value for `:similarity` instead of the default `0.8`.
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
- ## Contributors
169
+ diff.should == [["~", "a", "car", "bus"], ["~", "b[1]", "plane", " plan"], ["-", "b[0]", "boat"], ["+", "b[0]", "truck"]]
170
+ ```
97
171
 
98
- - [@liufengyun](https://github.com/liufengyun)
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
 
@@ -1,5 +1,10 @@
1
1
  # Change Log
2
2
 
3
+ ## v0.2.0 2014-3-29
4
+
5
+ * support custom comparison blocks
6
+ * support `:strip`, `:numeric_tolerance` and `:strict` options
7
+
3
8
  ## v0.1.0 2013-8-25
4
9
 
5
10
  * use options for parameters `:delimiter` and `:similarity` in interfaces
@@ -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 includes similar hashes in array.
5
+ # HashDiff.best_diff is useful in case of comparing two objects which include similar hashes in arrays.
6
6
  #
7
- # @param [Arrary, Hash] obj1
8
- # @param [Arrary, Hash] obj2
9
- # @param [Hash] options supports following keys:
10
- # :delimiter - default value is '.'(dot).
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 [Arrary, Hash] obj1
42
- # @param [Arrary, Hash] obj2
43
- # @param [Hash] options supports following keys:
44
- # :similarity - should be between (0, 1]. The default value is 0.8. :similarity is meaningful if there're similar hashes in arrays. See {best_diff}.
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
- # :delimiter - defaults to '.'(dot).
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
- opts = opts.merge!(options)
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
- if !(obj1.is_a?(Array) and obj2.is_a?(Array)) and !(obj1.is_a?(Hash) and obj2.is_a?(Hash)) and !(obj1.is_a?(obj2.class) or obj2.is_a?(obj1.class))
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[:similarity]) do |lcs|
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 == obj2
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, similarity = 0.8)
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, similarity)
192
+ links = lcs(a, b, opts)
159
193
 
160
194
  # yield common
161
195
  yield links if block_given?
@@ -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, similarity = 0.8)
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], similarity)
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
@@ -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 patchted, can be an Array of a Hash
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 - default value is '.'(dot).
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 unpatchted, can be an Array of a Hash
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 - default value is '.'(dot).
50
+ # * :delimiter (String) ['.'] delimiter string for representing nested keys in changes array
51
51
  #
52
52
  # @return the object after unpatch
53
53
  #
@@ -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, similarity = 0.8)
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, :similarity => similarity)
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
@@ -1,3 +1,3 @@
1
1
  module HashDiff
2
- VERSION = '0.1.1'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -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",
@@ -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
-
@@ -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
@@ -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.1.1
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: 2013-08-30 00:00:00.000000000 Z
11
+ date: 2014-03-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec