hashdiff 0.3.7 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/hashdiff/diff.rb CHANGED
@@ -1,13 +1,15 @@
1
- module HashDiff
1
+ # frozen_string_literal: true
2
2
 
3
+ module Hashdiff
3
4
  # Best diff two objects, which tries to generate the smallest change set using different similarity values.
4
5
  #
5
- # HashDiff.best_diff is useful in case of comparing two objects which include similar hashes in arrays.
6
+ # Hashdiff.best_diff is useful in case of comparing two objects which include similar hashes in arrays.
6
7
  #
7
8
  # @param [Array, Hash] obj1
8
9
  # @param [Array, Hash] obj2
9
10
  # @param [Hash] options the options to use when comparing
10
11
  # * :strict (Boolean) [true] whether numeric values will be compared on type as well as value. Set to false to allow comparing Integer, Float, BigDecimal to each other
12
+ # * :indifferent (Boolean) [false] whether to treat hash keys indifferently. Set to true to ignore differences between symbol keys (ie. {a: 1} ~= {'a' => 1})
11
13
  # * :delimiter (String) ['.'] the delimiter used when returning nested key references
12
14
  # * :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
15
  # * :strip (Boolean) [false] whether or not to call #strip on strings before comparing
@@ -22,27 +24,27 @@ module HashDiff
22
24
  # @example
23
25
  # a = {'x' => [{'a' => 1, 'c' => 3, 'e' => 5}, {'y' => 3}]}
24
26
  # b = {'x' => [{'a' => 1, 'b' => 2, 'e' => 5}] }
25
- # diff = HashDiff.best_diff(a, b)
27
+ # diff = Hashdiff.best_diff(a, b)
26
28
  # diff.should == [['-', 'x[0].c', 3], ['+', 'x[0].b', 2], ['-', 'x[1].y', 3], ['-', 'x[1]', {}]]
27
29
  #
28
30
  # @since 0.0.1
29
31
  def self.best_diff(obj1, obj2, options = {}, &block)
30
32
  options[:comparison] = block if block_given?
31
33
 
32
- opts = { :similarity => 0.3 }.merge!(options)
33
- diffs_1 = diff(obj1, obj2, opts)
34
- count_1 = count_diff diffs_1
34
+ opts = { similarity: 0.3 }.merge!(options)
35
+ diffs1 = diff(obj1, obj2, opts)
36
+ count1 = count_diff diffs1
35
37
 
36
- opts = { :similarity => 0.5 }.merge!(options)
37
- diffs_2 = diff(obj1, obj2, opts)
38
- count_2 = count_diff diffs_2
38
+ opts = { similarity: 0.5 }.merge!(options)
39
+ diffs2 = diff(obj1, obj2, opts)
40
+ count2 = count_diff diffs2
39
41
 
40
- opts = { :similarity => 0.8 }.merge!(options)
41
- diffs_3 = diff(obj1, obj2, opts)
42
- count_3 = count_diff diffs_3
42
+ opts = { similarity: 0.8 }.merge!(options)
43
+ diffs3 = diff(obj1, obj2, opts)
44
+ count3 = count_diff diffs3
43
45
 
44
- count, diffs = count_1 < count_2 ? [count_1, diffs_1] : [count_2, diffs_2]
45
- diffs = count < count_3 ? diffs : diffs_3
46
+ count, diffs = count1 < count2 ? [count1, diffs1] : [count2, diffs2]
47
+ count < count3 ? diffs : diffs3
46
48
  end
47
49
 
48
50
  # Compute the diff of two hashes or arrays
@@ -51,6 +53,7 @@ module HashDiff
51
53
  # @param [Array, Hash] obj2
52
54
  # @param [Hash] options the options to use when comparing
53
55
  # * :strict (Boolean) [true] whether numeric values will be compared on type as well as value. Set to false to allow comparing Integer, Float, BigDecimal to each other
56
+ # * :indifferent (Boolean) [false] whether to treat hash keys indifferently. Set to true to ignore differences between symbol keys (ie. {a: 1} ~= {'a' => 1})
54
57
  # * :similarity (Numeric) [0.8] should be between (0, 1]. Meaningful if there are similar hashes in arrays. See {best_diff}.
55
58
  # * :delimiter (String) ['.'] the delimiter used when returning nested key references
56
59
  # * :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.
@@ -68,20 +71,21 @@ module HashDiff
68
71
  # a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
69
72
  # b = {"a" => 1, "b" => {}}
70
73
  #
71
- # diff = HashDiff.diff(a, b)
74
+ # diff = Hashdiff.diff(a, b)
72
75
  # diff.should == [['-', 'b.b1', 1], ['-', 'b.b2', 2]]
73
76
  #
74
77
  # @since 0.0.1
75
78
  def self.diff(obj1, obj2, options = {}, &block)
76
79
  opts = {
77
- :prefix => '',
78
- :similarity => 0.8,
79
- :delimiter => '.',
80
- :strict => true,
81
- :strip => false,
82
- :numeric_tolerance => 0,
83
- :array_path => false,
84
- :use_lcs => true
80
+ prefix: '',
81
+ similarity: 0.8,
82
+ delimiter: '.',
83
+ strict: true,
84
+ indifferent: false,
85
+ strip: false,
86
+ numeric_tolerance: 0,
87
+ array_path: false,
88
+ use_lcs: true
85
89
  }.merge!(options)
86
90
 
87
91
  opts[:prefix] = [] if opts[:array_path] && opts[:prefix] == ''
@@ -92,120 +96,61 @@ module HashDiff
92
96
  result = custom_compare(opts[:comparison], opts[:prefix], obj1, obj2)
93
97
  return result if result
94
98
 
95
- if obj1.nil? and obj2.nil?
96
- return []
97
- end
99
+ return [] if obj1.nil? && obj2.nil?
98
100
 
99
- if obj1.nil?
100
- return [['~', opts[:prefix], nil, obj2]]
101
- end
101
+ return [['~', opts[:prefix], obj1, obj2]] if obj1.nil? || obj2.nil?
102
102
 
103
- if obj2.nil?
104
- return [['~', opts[:prefix], obj1, nil]]
105
- end
103
+ return [['~', opts[:prefix], obj1, obj2]] unless comparable?(obj1, obj2, opts[:strict])
106
104
 
107
- unless comparable?(obj1, obj2, opts[:strict])
108
- return [['~', opts[:prefix], obj1, obj2]]
109
- end
105
+ return LcsCompareArrays.call(obj1, obj2, opts) if obj1.is_a?(Array) && opts[:use_lcs]
110
106
 
111
- result = []
112
- if obj1.is_a?(Array) && opts[:use_lcs]
113
- changeset = diff_array_lcs(obj1, obj2, opts) do |lcs|
114
- # use a's index for similarity
115
- lcs.each do |pair|
116
- prefix = prefix_append_array_index(opts[:prefix], pair[0], opts)
117
- result.concat(diff(obj1[pair[0]], obj2[pair[1]], opts.merge(:prefix => prefix)))
118
- end
119
- end
107
+ return LinearCompareArray.call(obj1, obj2, opts) if obj1.is_a?(Array) && !opts[:use_lcs]
120
108
 
121
- changeset.each do |change|
122
- change_key = prefix_append_array_index(opts[:prefix], change[1], opts)
123
- if change[0] == '-'
124
- result << ['-', change_key, change[2]]
125
- elsif change[0] == '+'
126
- result << ['+', change_key, change[2]]
127
- end
128
- end
129
- elsif obj1.is_a?(Array) && !opts[:use_lcs]
130
- result.concat(LinearCompareArray.call(obj1, obj2, opts))
131
- elsif obj1.is_a?(Hash)
132
-
133
- deleted_keys = obj1.keys - obj2.keys
134
- common_keys = obj1.keys & obj2.keys
135
- added_keys = obj2.keys - obj1.keys
136
-
137
- # add deleted properties
138
- deleted_keys.sort_by{|k,v| k.to_s }.each do |k|
139
- change_key = prefix_append_key(opts[:prefix], k, opts)
140
- custom_result = custom_compare(opts[:comparison], change_key, obj1[k], nil)
141
-
142
- if custom_result
143
- result.concat(custom_result)
144
- else
145
- result << ['-', change_key, obj1[k]]
146
- end
147
- end
148
-
149
- # recursive comparison for common keys
150
- common_keys.sort_by{|k,v| k.to_s }.each do |k|
151
- prefix = prefix_append_key(opts[:prefix], k, opts)
152
- result.concat(diff(obj1[k], obj2[k], opts.merge(:prefix => prefix)))
153
- end
109
+ return CompareHashes.call(obj1, obj2, opts) if obj1.is_a?(Hash)
154
110
 
155
- # added properties
156
- added_keys.sort_by{|k,v| k.to_s }.each do |k|
157
- change_key = prefix_append_key(opts[:prefix], k, opts)
158
- unless obj1.key?(k)
159
- custom_result = custom_compare(opts[:comparison], change_key, nil, obj2[k])
160
-
161
- if custom_result
162
- result.concat(custom_result)
163
- else
164
- result << ['+', change_key, obj2[k]]
165
- end
166
- end
167
- end
168
- else
169
- return [] if compare_values(obj1, obj2, opts)
170
- return [['~', opts[:prefix], obj1, obj2]]
171
- end
111
+ return [] if compare_values(obj1, obj2, opts)
172
112
 
173
- result
113
+ [['~', opts[:prefix], obj1, obj2]]
174
114
  end
175
115
 
176
116
  # @private
177
117
  #
178
118
  # diff array using LCS algorithm
179
- def self.diff_array_lcs(a, b, options = {})
180
- opts = {
181
- :prefix => '',
182
- :similarity => 0.8,
183
- :delimiter => '.'
184
- }.merge!(options)
119
+ def self.diff_array_lcs(arraya, arrayb, options = {})
120
+ return [] if arraya.empty? && arrayb.empty?
185
121
 
186
122
  change_set = []
187
- if a.size == 0 and b.size == 0
188
- return []
189
- elsif a.size == 0
190
- b.each_index do |index|
191
- change_set << ['+', index, b[index]]
123
+
124
+ if arraya.empty?
125
+ arrayb.each_index do |index|
126
+ change_set << ['+', index, arrayb[index]]
192
127
  end
128
+
193
129
  return change_set
194
- elsif b.size == 0
195
- a.each_index do |index|
196
- i = a.size - index - 1
197
- change_set << ['-', i, a[i]]
130
+ end
131
+
132
+ if arrayb.empty?
133
+ arraya.each_index do |index|
134
+ i = arraya.size - index - 1
135
+ change_set << ['-', i, arraya[i]]
198
136
  end
137
+
199
138
  return change_set
200
139
  end
201
140
 
202
- links = lcs(a, b, opts)
141
+ opts = {
142
+ prefix: '',
143
+ similarity: 0.8,
144
+ delimiter: '.'
145
+ }.merge!(options)
146
+
147
+ links = lcs(arraya, arrayb, opts)
203
148
 
204
149
  # yield common
205
150
  yield links if block_given?
206
151
 
207
152
  # padding the end
208
- links << [a.size, b.size]
153
+ links << [arraya.size, arrayb.size]
209
154
 
210
155
  last_x = -1
211
156
  last_y = -1
@@ -213,13 +158,13 @@ module HashDiff
213
158
  x, y = pair
214
159
 
215
160
  # remove from a, beginning from the end
216
- (x > last_x + 1) and (x - last_x - 2).downto(0).each do |i|
217
- change_set << ['-', last_y + i + 1, a[i + last_x + 1]]
161
+ (x > last_x + 1) && (x - last_x - 2).downto(0).each do |i|
162
+ change_set << ['-', last_y + i + 1, arraya[i + last_x + 1]]
218
163
  end
219
164
 
220
165
  # add from b, beginning from the head
221
- (y > last_y + 1) and 0.upto(y - last_y - 2).each do |i|
222
- change_set << ['+', last_y + i + 1, b[i + last_y + 1]]
166
+ (y > last_y + 1) && 0.upto(y - last_y - 2).each do |i|
167
+ change_set << ['+', last_y + i + 1, arrayb[i + last_y + 1]]
223
168
  end
224
169
 
225
170
  # update flags
data/lib/hashdiff/lcs.rb CHANGED
@@ -1,46 +1,44 @@
1
- module HashDiff
1
+ # frozen_string_literal: true
2
+
3
+ module Hashdiff
2
4
  # @private
3
5
  #
4
6
  # caculate array difference using LCS algorithm
5
7
  # http://en.wikipedia.org/wiki/Longest_common_subsequence_problem
6
- def self.lcs(a, b, options = {})
7
- opts = { :similarity => 0.8 }.merge!(options)
8
+ def self.lcs(arraya, arrayb, options = {})
9
+ return [] if arraya.empty? || arrayb.empty?
8
10
 
9
- opts[:prefix] = prefix_append_array_index(opts[:prefix], '*', opts)
11
+ opts = { similarity: 0.8 }.merge!(options)
10
12
 
11
- return [] if a.size == 0 or b.size == 0
13
+ opts[:prefix] = prefix_append_array_index(opts[:prefix], '*', opts)
12
14
 
13
15
  a_start = b_start = 0
14
- a_finish = a.size - 1
15
- b_finish = b.size - 1
16
+ a_finish = arraya.size - 1
17
+ b_finish = arrayb.size - 1
16
18
  vector = []
17
19
 
18
20
  lcs = []
19
21
  (b_start..b_finish).each do |bi|
20
- lcs[bi] = []
22
+ lcs[bi] = []
21
23
  (a_start..a_finish).each do |ai|
22
- if similar?(a[ai], b[bi], opts)
23
- topleft = (ai > 0 and bi > 0)? lcs[bi-1][ai-1][1] : 0
24
+ if similar?(arraya[ai], arrayb[bi], opts)
25
+ topleft = (ai > 0) && (bi > 0) ? lcs[bi - 1][ai - 1][1] : 0
24
26
  lcs[bi][ai] = [:topleft, topleft + 1]
25
- elsif
26
- top = (bi > 0)? lcs[bi-1][ai][1] : 0
27
- left = (ai > 0)? lcs[bi][ai-1][1] : 0
28
- count = (top > left) ? top : left
27
+ elsif (top = bi > 0 ? lcs[bi - 1][ai][1] : 0)
28
+ left = ai > 0 ? lcs[bi][ai - 1][1] : 0
29
+ count = top > left ? top : left
29
30
 
30
- direction = :both
31
- if top > left
32
- direction = :top
33
- elsif top < left
34
- direction = :left
35
- else
36
- if bi == 0
37
- direction = :top
38
- elsif ai == 0
39
- direction = :left
40
- else
41
- direction = :both
42
- end
43
- end
31
+ direction = if top > left
32
+ :top
33
+ elsif top < left
34
+ :left
35
+ elsif bi.zero?
36
+ :top
37
+ elsif ai.zero?
38
+ :left
39
+ else
40
+ :both
41
+ end
44
42
 
45
43
  lcs[bi][ai] = [direction, count]
46
44
  end
@@ -49,7 +47,7 @@ module HashDiff
49
47
 
50
48
  x = a_finish
51
49
  y = b_finish
52
- while x >= 0 and y >= 0 and lcs[y][x][1] > 0
50
+ while (x >= 0) && (y >= 0) && (lcs[y][x][1] > 0)
53
51
  if lcs[y][x][0] == :both
54
52
  x -= 1
55
53
  elsif lcs[y][x][0] == :topleft
@@ -65,5 +63,4 @@ module HashDiff
65
63
 
66
64
  vector
67
65
  end
68
-
69
66
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hashdiff
4
+ # @private
5
+ # Used to compare arrays using the lcs algorithm
6
+ class LcsCompareArrays
7
+ class << self
8
+ def call(obj1, obj2, opts = {})
9
+ result = []
10
+
11
+ changeset = Hashdiff.diff_array_lcs(obj1, obj2, opts) do |lcs|
12
+ # use a's index for similarity
13
+ lcs.each do |pair|
14
+ prefix = Hashdiff.prefix_append_array_index(opts[:prefix], pair[0], opts)
15
+
16
+ result.concat(Hashdiff.diff(obj1[pair[0]], obj2[pair[1]], opts.merge(prefix: prefix)))
17
+ end
18
+ end
19
+
20
+ changeset.each do |change|
21
+ next if change[0] != '-' && change[0] != '+'
22
+
23
+ change_key = Hashdiff.prefix_append_array_index(opts[:prefix], change[1], opts)
24
+
25
+ result << [change[0], change_key, change[2]]
26
+ end
27
+
28
+ result
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,11 +1,13 @@
1
- module HashDiff
1
+ # frozen_string_literal: true
2
+
3
+ module Hashdiff
2
4
  # @private
3
5
  #
4
6
  # Used to compare arrays in a linear complexity, which produces longer diffs
5
7
  # than using the lcs algorithm but is considerably faster
6
8
  class LinearCompareArray
7
9
  def self.call(old_array, new_array, options = {})
8
- instance = self.new(old_array, new_array, options)
10
+ instance = new(old_array, new_array, options)
9
11
  instance.call
10
12
  end
11
13
 
@@ -78,8 +80,8 @@ module HashDiff
78
80
  end
79
81
 
80
82
  def item_difference(old_item, new_item, item_index)
81
- prefix = HashDiff.prefix_append_array_index(options[:prefix], item_index, options)
82
- HashDiff.diff(old_item, new_item, options.merge(:prefix => prefix))
83
+ prefix = Hashdiff.prefix_append_array_index(options[:prefix], item_index, options)
84
+ Hashdiff.diff(old_item, new_item, options.merge(prefix: prefix))
83
85
  end
84
86
 
85
87
  # look ahead in the new array to see if the current item appears later
@@ -120,6 +122,7 @@ module HashDiff
120
122
 
121
123
  def append_addititions_before_match(match_index)
122
124
  return unless match_index
125
+
123
126
  (new_index...match_index).each { |i| append_addition(new_array[i], i) }
124
127
  self.expected_additions = expected_additions - (match_index - new_index)
125
128
  self.new_index = match_index
@@ -127,18 +130,19 @@ module HashDiff
127
130
 
128
131
  def append_deletions_before_match(match_index)
129
132
  return unless match_index
133
+
130
134
  (old_index...match_index).each { |i| append_deletion(old_array[i], i) }
131
135
  self.expected_additions = expected_additions + (match_index - new_index)
132
136
  self.old_index = match_index
133
137
  end
134
138
 
135
139
  def append_addition(item, index)
136
- key = HashDiff.prefix_append_array_index(options[:prefix], index, options)
140
+ key = Hashdiff.prefix_append_array_index(options[:prefix], index, options)
137
141
  additions << ['+', key, item]
138
142
  end
139
143
 
140
144
  def append_deletion(item, index)
141
- key = HashDiff.prefix_append_array_index(options[:prefix], index, options)
145
+ key = Hashdiff.prefix_append_array_index(options[:prefix], index, options)
142
146
  deletions << ['-', key, item]
143
147
  end
144
148
 
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # This module provides methods to diff two hash, patch and unpatch hash
3
5
  #
4
- module HashDiff
5
-
6
+ module Hashdiff
6
7
  # Apply patch to object
7
8
  #
8
9
  # @param [Hash, Array] obj the object to be patched, can be an Array or a Hash
@@ -22,7 +23,7 @@ module HashDiff
22
23
 
23
24
  last_part = parts.last
24
25
 
25
- parent_node = node(obj, parts[0, parts.size-1])
26
+ parent_node = node(obj, parts[0, parts.size - 1])
26
27
 
27
28
  if change[0] == '+'
28
29
  if parent_node.is_a?(Array)
@@ -63,7 +64,7 @@ module HashDiff
63
64
 
64
65
  last_part = parts.last
65
66
 
66
- parent_node = node(obj, parts[0, parts.size-1])
67
+ parent_node = node(obj, parts[0, parts.size - 1])
67
68
 
68
69
  if change[0] == '+'
69
70
  if parent_node.is_a?(Array)
@@ -84,5 +85,4 @@ module HashDiff
84
85
 
85
86
  obj
86
87
  end
87
-
88
88
  end