hashdiff 0.3.4 → 0.4.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.
data/lib/hashdiff/diff.rb CHANGED
@@ -1,8 +1,9 @@
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
@@ -11,6 +12,8 @@ module HashDiff
11
12
  # * :delimiter (String) ['.'] the delimiter used when returning nested key references
12
13
  # * :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
14
  # * :strip (Boolean) [false] whether or not to call #strip on strings before comparing
15
+ # * :array_path (Boolean) [false] whether to return the path references for nested values in an array, can be used for patch compatibility with non string keys.
16
+ # * :use_lcs (Boolean) [true] whether or not to use an implementation of the Longest common subsequence algorithm for comparing arrays, produces better diffs but is slower.
14
17
  #
15
18
  # @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.
16
19
  #
@@ -20,27 +23,27 @@ module HashDiff
20
23
  # @example
21
24
  # a = {'x' => [{'a' => 1, 'c' => 3, 'e' => 5}, {'y' => 3}]}
22
25
  # b = {'x' => [{'a' => 1, 'b' => 2, 'e' => 5}] }
23
- # diff = HashDiff.best_diff(a, b)
26
+ # diff = Hashdiff.best_diff(a, b)
24
27
  # diff.should == [['-', 'x[0].c', 3], ['+', 'x[0].b', 2], ['-', 'x[1].y', 3], ['-', 'x[1]', {}]]
25
28
  #
26
29
  # @since 0.0.1
27
30
  def self.best_diff(obj1, obj2, options = {}, &block)
28
31
  options[:comparison] = block if block_given?
29
32
 
30
- opts = { :similarity => 0.3 }.merge!(options)
31
- diffs_1 = diff(obj1, obj2, opts)
32
- count_1 = count_diff diffs_1
33
+ opts = { similarity: 0.3 }.merge!(options)
34
+ diffs1 = diff(obj1, obj2, opts)
35
+ count1 = count_diff diffs1
33
36
 
34
- opts = { :similarity => 0.5 }.merge!(options)
35
- diffs_2 = diff(obj1, obj2, opts)
36
- count_2 = count_diff diffs_2
37
+ opts = { similarity: 0.5 }.merge!(options)
38
+ diffs2 = diff(obj1, obj2, opts)
39
+ count2 = count_diff diffs2
37
40
 
38
- opts = { :similarity => 0.8 }.merge!(options)
39
- diffs_3 = diff(obj1, obj2, opts)
40
- count_3 = count_diff diffs_3
41
+ opts = { similarity: 0.8 }.merge!(options)
42
+ diffs3 = diff(obj1, obj2, opts)
43
+ count3 = count_diff diffs3
41
44
 
42
- count, diffs = count_1 < count_2 ? [count_1, diffs_1] : [count_2, diffs_2]
43
- diffs = count < count_3 ? diffs : diffs_3
45
+ count, diffs = count1 < count2 ? [count1, diffs1] : [count2, diffs2]
46
+ count < count3 ? diffs : diffs3
44
47
  end
45
48
 
46
49
  # Compute the diff of two hashes or arrays
@@ -53,6 +56,9 @@ module HashDiff
53
56
  # * :delimiter (String) ['.'] the delimiter used when returning nested key references
54
57
  # * :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
58
  # * :strip (Boolean) [false] whether or not to call #strip on strings before comparing
59
+ # * :array_path (Boolean) [false] whether to return the path references for nested values in an array, can be used for patch compatibility with non string keys.
60
+ # * :use_lcs (Boolean) [true] whether or not to use an implementation of the Longest common subsequence algorithm for comparing arrays, produces better diffs but is slower.
61
+ #
56
62
  #
57
63
  # @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.
58
64
  #
@@ -63,136 +69,85 @@ module HashDiff
63
69
  # a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
64
70
  # b = {"a" => 1, "b" => {}}
65
71
  #
66
- # diff = HashDiff.diff(a, b)
72
+ # diff = Hashdiff.diff(a, b)
67
73
  # diff.should == [['-', 'b.b1', 1], ['-', 'b.b2', 2]]
68
74
  #
69
75
  # @since 0.0.1
70
76
  def self.diff(obj1, obj2, options = {}, &block)
71
77
  opts = {
72
- :prefix => '',
73
- :similarity => 0.8,
74
- :delimiter => '.',
75
- :strict => true,
76
- :strip => false,
77
- :numeric_tolerance => 0
78
+ prefix: '',
79
+ similarity: 0.8,
80
+ delimiter: '.',
81
+ strict: true,
82
+ strip: false,
83
+ numeric_tolerance: 0,
84
+ array_path: false,
85
+ use_lcs: true
78
86
  }.merge!(options)
79
87
 
88
+ opts[:prefix] = [] if opts[:array_path] && opts[:prefix] == ''
89
+
80
90
  opts[:comparison] = block if block_given?
81
91
 
82
92
  # prefer to compare with provided block
83
93
  result = custom_compare(opts[:comparison], opts[:prefix], obj1, obj2)
84
94
  return result if result
85
95
 
86
- if obj1.nil? and obj2.nil?
87
- return []
88
- end
89
-
90
- if obj1.nil?
91
- return [['~', opts[:prefix], nil, obj2]]
92
- end
93
-
94
- if obj2.nil?
95
- return [['~', opts[:prefix], obj1, nil]]
96
- end
97
-
98
- unless comparable?(obj1, obj2, opts[:strict])
99
- return [['~', opts[:prefix], obj1, obj2]]
100
- end
101
-
102
- result = []
103
- if obj1.is_a?(Array)
104
- changeset = diff_array(obj1, obj2, opts) do |lcs|
105
- # use a's index for similarity
106
- lcs.each do |pair|
107
- result.concat(diff(obj1[pair[0]], obj2[pair[1]], opts.merge(:prefix => "#{opts[:prefix]}[#{pair[0]}]")))
108
- end
109
- end
110
-
111
- changeset.each do |change|
112
- if change[0] == '-'
113
- result << ['-', "#{opts[:prefix]}[#{change[1]}]", change[2]]
114
- elsif change[0] == '+'
115
- result << ['+', "#{opts[:prefix]}[#{change[1]}]", change[2]]
116
- end
117
- end
118
- elsif obj1.is_a?(Hash)
119
- if opts[:prefix].empty?
120
- prefix = ""
121
- else
122
- prefix = "#{opts[:prefix]}#{opts[:delimiter]}"
123
- end
96
+ return [] if obj1.nil? && obj2.nil?
124
97
 
125
- deleted_keys = obj1.keys - obj2.keys
126
- common_keys = obj1.keys & obj2.keys
127
- added_keys = obj2.keys - obj1.keys
98
+ return [['~', opts[:prefix], obj1, obj2]] if obj1.nil? || obj2.nil?
128
99
 
129
- # add deleted properties
130
- deleted_keys.sort_by{|k,v| k.to_s }.each do |k|
131
- custom_result = custom_compare(opts[:comparison], "#{prefix}#{k}", obj1[k], nil)
100
+ return [['~', opts[:prefix], obj1, obj2]] unless comparable?(obj1, obj2, opts[:strict])
132
101
 
133
- if custom_result
134
- result.concat(custom_result)
135
- else
136
- result << ['-', "#{prefix}#{k}", obj1[k]]
137
- end
138
- end
102
+ return LcsCompareArrays.call(obj1, obj2, opts) if obj1.is_a?(Array) && opts[:use_lcs]
139
103
 
140
- # recursive comparison for common keys
141
- common_keys.sort_by{|k,v| k.to_s }.each {|k| result.concat(diff(obj1[k], obj2[k], opts.merge(:prefix => "#{prefix}#{k}"))) }
104
+ return LinearCompareArray.call(obj1, obj2, opts) if obj1.is_a?(Array) && !opts[:use_lcs]
142
105
 
143
- # added properties
144
- added_keys.sort_by{|k,v| k.to_s }.each do |k|
145
- unless obj1.key?(k)
146
- custom_result = custom_compare(opts[:comparison], "#{prefix}#{k}", nil, obj2[k])
106
+ return CompareHashes.call(obj1, obj2, opts) if obj1.is_a?(Hash)
147
107
 
148
- if custom_result
149
- result.concat(custom_result)
150
- else
151
- result << ['+', "#{prefix}#{k}", obj2[k]]
152
- end
153
- end
154
- end
155
- else
156
- return [] if compare_values(obj1, obj2, opts)
157
- return [['~', opts[:prefix], obj1, obj2]]
158
- end
108
+ return [] if compare_values(obj1, obj2, opts)
159
109
 
160
- result
110
+ [['~', opts[:prefix], obj1, obj2]]
161
111
  end
162
112
 
163
113
  # @private
164
114
  #
165
115
  # diff array using LCS algorithm
166
- def self.diff_array(a, b, options = {})
167
- opts = {
168
- :prefix => '',
169
- :similarity => 0.8,
170
- :delimiter => '.'
171
- }.merge!(options)
116
+ def self.diff_array_lcs(arraya, arrayb, options = {})
117
+ return [] if arraya.empty? && arrayb.empty?
172
118
 
173
119
  change_set = []
174
- if a.size == 0 and b.size == 0
175
- return []
176
- elsif a.size == 0
177
- b.each_index do |index|
178
- change_set << ['+', index, b[index]]
120
+
121
+ if arraya.empty?
122
+ arrayb.each_index do |index|
123
+ change_set << ['+', index, arrayb[index]]
179
124
  end
125
+
180
126
  return change_set
181
- elsif b.size == 0
182
- a.each_index do |index|
183
- i = a.size - index - 1
184
- change_set << ['-', i, a[i]]
127
+ end
128
+
129
+ if arrayb.empty?
130
+ arraya.each_index do |index|
131
+ i = arraya.size - index - 1
132
+ change_set << ['-', i, arraya[i]]
185
133
  end
134
+
186
135
  return change_set
187
136
  end
188
137
 
189
- links = lcs(a, b, opts)
138
+ opts = {
139
+ prefix: '',
140
+ similarity: 0.8,
141
+ delimiter: '.'
142
+ }.merge!(options)
143
+
144
+ links = lcs(arraya, arrayb, opts)
190
145
 
191
146
  # yield common
192
147
  yield links if block_given?
193
148
 
194
149
  # padding the end
195
- links << [a.size, b.size]
150
+ links << [arraya.size, arrayb.size]
196
151
 
197
152
  last_x = -1
198
153
  last_y = -1
@@ -200,13 +155,13 @@ module HashDiff
200
155
  x, y = pair
201
156
 
202
157
  # remove from a, beginning from the end
203
- (x > last_x + 1) and (x - last_x - 2).downto(0).each do |i|
204
- change_set << ['-', last_y + i + 1, a[i + last_x + 1]]
158
+ (x > last_x + 1) && (x - last_x - 2).downto(0).each do |i|
159
+ change_set << ['-', last_y + i + 1, arraya[i + last_x + 1]]
205
160
  end
206
161
 
207
162
  # add from b, beginning from the head
208
- (y > last_y + 1) and 0.upto(y - last_y - 2).each do |i|
209
- change_set << ['+', last_y + i + 1, b[i + last_y + 1]]
163
+ (y > last_y + 1) && 0.upto(y - last_y - 2).each do |i|
164
+ change_set << ['+', last_y + i + 1, arrayb[i + last_y + 1]]
210
165
  end
211
166
 
212
167
  # update flags
@@ -216,5 +171,4 @@ module HashDiff
216
171
 
217
172
  change_set
218
173
  end
219
-
220
174
  end
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] = "#{opts[:prefix]}[*]"
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
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hashdiff
4
+ # @private
5
+ #
6
+ # Used to compare arrays in a linear complexity, which produces longer diffs
7
+ # than using the lcs algorithm but is considerably faster
8
+ class LinearCompareArray
9
+ def self.call(old_array, new_array, options = {})
10
+ instance = new(old_array, new_array, options)
11
+ instance.call
12
+ end
13
+
14
+ def call
15
+ return [] if old_array.empty? && new_array.empty?
16
+
17
+ self.old_index = 0
18
+ self.new_index = 0
19
+ # by comparing the array lengths we can expect that a number of items
20
+ # are either added or removed
21
+ self.expected_additions = new_array.length - old_array.length
22
+
23
+ loop do
24
+ if extra_items_in_old_array?
25
+ append_deletion(old_array[old_index], old_index)
26
+ elsif extra_items_in_new_array?
27
+ append_addition(new_array[new_index], new_index)
28
+ else
29
+ compare_at_index
30
+ end
31
+
32
+ self.old_index = old_index + 1
33
+ self.new_index = new_index + 1
34
+ break if iterated_through_both_arrays?
35
+ end
36
+
37
+ changes
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :old_array, :new_array, :options, :additions, :deletions, :differences
43
+ attr_accessor :old_index, :new_index, :expected_additions
44
+
45
+ def initialize(old_array, new_array, options)
46
+ @old_array = old_array
47
+ @new_array = new_array
48
+ @options = { prefix: '' }.merge!(options)
49
+
50
+ @additions = []
51
+ @deletions = []
52
+ @differences = []
53
+ end
54
+
55
+ def extra_items_in_old_array?
56
+ old_index < old_array.length && new_index >= new_array.length
57
+ end
58
+
59
+ def extra_items_in_new_array?
60
+ new_index < new_array.length && old_index >= old_array.length
61
+ end
62
+
63
+ def iterated_through_both_arrays?
64
+ old_index >= old_array.length && new_index >= new_array.length
65
+ end
66
+
67
+ def compare_at_index
68
+ difference = item_difference(old_array[old_index], new_array[new_index], old_index)
69
+ return if difference.empty?
70
+
71
+ index_after_additions = index_of_match_after_additions
72
+ append_addititions_before_match(index_after_additions)
73
+
74
+ index_after_deletions = index_of_match_after_deletions
75
+ append_deletions_before_match(index_after_deletions)
76
+
77
+ match = index_after_additions || index_after_deletions
78
+
79
+ append_differences(difference) unless match
80
+ end
81
+
82
+ def item_difference(old_item, new_item, item_index)
83
+ prefix = Hashdiff.prefix_append_array_index(options[:prefix], item_index, options)
84
+ Hashdiff.diff(old_item, new_item, options.merge(prefix: prefix))
85
+ end
86
+
87
+ # look ahead in the new array to see if the current item appears later
88
+ # thereby having new items added
89
+ def index_of_match_after_additions
90
+ return unless expected_additions > 0
91
+
92
+ (1..expected_additions).each do |i|
93
+ next_difference = item_difference(
94
+ old_array[old_index],
95
+ new_array[new_index + i],
96
+ old_index
97
+ )
98
+
99
+ return new_index + i if next_difference.empty?
100
+ end
101
+
102
+ nil
103
+ end
104
+
105
+ # look ahead in the old array to see if the current item appears later
106
+ # thereby having items removed
107
+ def index_of_match_after_deletions
108
+ return unless expected_additions < 0
109
+
110
+ (1..(expected_additions.abs)).each do |i|
111
+ next_difference = item_difference(
112
+ old_array[old_index + i],
113
+ new_array[new_index],
114
+ old_index
115
+ )
116
+
117
+ return old_index + i if next_difference.empty?
118
+ end
119
+
120
+ nil
121
+ end
122
+
123
+ def append_addititions_before_match(match_index)
124
+ return unless match_index
125
+
126
+ (new_index...match_index).each { |i| append_addition(new_array[i], i) }
127
+ self.expected_additions = expected_additions - (match_index - new_index)
128
+ self.new_index = match_index
129
+ end
130
+
131
+ def append_deletions_before_match(match_index)
132
+ return unless match_index
133
+
134
+ (old_index...match_index).each { |i| append_deletion(old_array[i], i) }
135
+ self.expected_additions = expected_additions + (match_index - new_index)
136
+ self.old_index = match_index
137
+ end
138
+
139
+ def append_addition(item, index)
140
+ key = Hashdiff.prefix_append_array_index(options[:prefix], index, options)
141
+ additions << ['+', key, item]
142
+ end
143
+
144
+ def append_deletion(item, index)
145
+ key = Hashdiff.prefix_append_array_index(options[:prefix], index, options)
146
+ deletions << ['-', key, item]
147
+ end
148
+
149
+ def append_differences(difference)
150
+ differences.concat(difference)
151
+ end
152
+
153
+ def changes
154
+ # this algorithm only allows there to be additions or deletions
155
+ # deletions are reverse so they don't change the index of earlier items
156
+ differences + additions + deletions.reverse
157
+ end
158
+ end
159
+ end
@@ -1,8 +1,9 @@
1
- #
1
+ # frozen_string_literal: true
2
+
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
@@ -17,19 +18,21 @@ module HashDiff
17
18
  delimiter = options[:delimiter] || '.'
18
19
 
19
20
  changes.each do |change|
20
- parts = decode_property_path(change[1], delimiter)
21
+ parts = change[1]
22
+ parts = decode_property_path(parts, delimiter) unless parts.is_a?(Array)
23
+
21
24
  last_part = parts.last
22
25
 
23
- parent_node = node(obj, parts[0, parts.size-1])
26
+ parent_node = node(obj, parts[0, parts.size - 1])
24
27
 
25
28
  if change[0] == '+'
26
- if last_part.is_a?(Integer)
29
+ if parent_node.is_a?(Array)
27
30
  parent_node.insert(last_part, change[2])
28
31
  else
29
32
  parent_node[last_part] = change[2]
30
33
  end
31
34
  elsif change[0] == '-'
32
- if last_part.is_a?(Integer)
35
+ if parent_node.is_a?(Array)
33
36
  parent_node.delete_at(last_part)
34
37
  else
35
38
  parent_node.delete(last_part)
@@ -56,19 +59,21 @@ module HashDiff
56
59
  delimiter = options[:delimiter] || '.'
57
60
 
58
61
  changes.reverse_each do |change|
59
- parts = decode_property_path(change[1], delimiter)
62
+ parts = change[1]
63
+ parts = decode_property_path(parts, delimiter) unless parts.is_a?(Array)
64
+
60
65
  last_part = parts.last
61
66
 
62
- parent_node = node(obj, parts[0, parts.size-1])
67
+ parent_node = node(obj, parts[0, parts.size - 1])
63
68
 
64
69
  if change[0] == '+'
65
- if last_part.is_a?(Integer)
70
+ if parent_node.is_a?(Array)
66
71
  parent_node.delete_at(last_part)
67
72
  else
68
73
  parent_node.delete(last_part)
69
74
  end
70
75
  elsif change[0] == '-'
71
- if last_part.is_a?(Integer)
76
+ if parent_node.is_a?(Array)
72
77
  parent_node.insert(last_part, change[2])
73
78
  else
74
79
  parent_node[last_part] = change[2]
@@ -80,5 +85,4 @@ module HashDiff
80
85
 
81
86
  obj
82
87
  end
83
-
84
88
  end