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 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