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.
- checksums.yaml +4 -4
- data/.rubocop.yml +24 -0
- data/.travis.yml +6 -5
- data/Gemfile +3 -1
- data/README.md +94 -29
- data/Rakefile +9 -4
- data/changelog.md +28 -3
- data/hashdiff.gemspec +27 -12
- data/lib/hashdiff/compare_hashes.rb +56 -0
- data/lib/hashdiff/diff.rb +65 -111
- data/lib/hashdiff/lcs.rb +27 -30
- data/lib/hashdiff/lcs_compare_arrays.rb +32 -0
- data/lib/hashdiff/linear_compare_array.rb +159 -0
- data/lib/hashdiff/patch.rb +16 -12
- data/lib/hashdiff/util.rb +59 -37
- data/lib/hashdiff/version.rb +4 -2
- data/lib/hashdiff.rb +9 -0
- data/spec/hashdiff/best_diff_spec.rb +47 -37
- data/spec/hashdiff/diff_array_spec.rb +19 -19
- data/spec/hashdiff/diff_spec.rb +217 -139
- data/spec/hashdiff/lcs_spec.rb +27 -26
- data/spec/hashdiff/linear_compare_array_spec.rb +50 -0
- data/spec/hashdiff/patch_spec.rb +129 -105
- data/spec/hashdiff/util_spec.rb +60 -43
- data/spec/spec_helper.rb +2 -0
- metadata +50 -11
data/lib/hashdiff/diff.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
-
|
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
|
-
#
|
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 =
|
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 = { :
|
31
|
-
|
32
|
-
|
33
|
+
opts = { similarity: 0.3 }.merge!(options)
|
34
|
+
diffs1 = diff(obj1, obj2, opts)
|
35
|
+
count1 = count_diff diffs1
|
33
36
|
|
34
|
-
opts = { :
|
35
|
-
|
36
|
-
|
37
|
+
opts = { similarity: 0.5 }.merge!(options)
|
38
|
+
diffs2 = diff(obj1, obj2, opts)
|
39
|
+
count2 = count_diff diffs2
|
37
40
|
|
38
|
-
opts = { :
|
39
|
-
|
40
|
-
|
41
|
+
opts = { similarity: 0.8 }.merge!(options)
|
42
|
+
diffs3 = diff(obj1, obj2, opts)
|
43
|
+
count3 = count_diff diffs3
|
41
44
|
|
42
|
-
count, diffs =
|
43
|
-
|
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 =
|
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
|
-
:
|
73
|
-
:
|
74
|
-
:
|
75
|
-
:
|
76
|
-
:
|
77
|
-
:
|
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?
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
167
|
-
|
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
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
-
|
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 << [
|
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)
|
204
|
-
change_set << ['-', last_y + i + 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)
|
209
|
-
change_set << ['+', last_y + i + 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
|
-
|
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(
|
7
|
-
|
8
|
+
def self.lcs(arraya, arrayb, options = {})
|
9
|
+
return [] if arraya.empty? || arrayb.empty?
|
8
10
|
|
9
|
-
opts
|
11
|
+
opts = { similarity: 0.8 }.merge!(options)
|
10
12
|
|
11
|
-
|
13
|
+
opts[:prefix] = prefix_append_array_index(opts[:prefix], '*', opts)
|
12
14
|
|
13
15
|
a_start = b_start = 0
|
14
|
-
a_finish =
|
15
|
-
b_finish =
|
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?(
|
23
|
-
topleft = (ai > 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
|
-
|
27
|
-
|
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 =
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
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
|
data/lib/hashdiff/patch.rb
CHANGED
@@ -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
|
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 =
|
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
|
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
|
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 =
|
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
|
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
|
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
|