hashdiff_sym 0.3.1
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 +7 -0
- data/.gitignore +15 -0
- data/.rspec +1 -0
- data/.travis.yml +7 -0
- data/.yardopts +1 -0
- data/Gemfile +6 -0
- data/LICENSE +19 -0
- data/README.md +22 -0
- data/Rakefile +13 -0
- data/changelog.md +48 -0
- data/hashdiff_sym.gemspec +25 -0
- data/lib/hashdiff_sym/diff.rb +220 -0
- data/lib/hashdiff_sym/lcs.rb +69 -0
- data/lib/hashdiff_sym/patch.rb +84 -0
- data/lib/hashdiff_sym/util.rb +147 -0
- data/lib/hashdiff_sym/version.rb +3 -0
- data/lib/hashdiff_sym.rb +5 -0
- data/spec/hashdiff_sym/best_diff_spec.rb +65 -0
- data/spec/hashdiff_sym/diff_array_spec.rb +60 -0
- data/spec/hashdiff_sym/diff_spec.rb +263 -0
- data/spec/hashdiff_sym/lcs_spec.rb +75 -0
- data/spec/hashdiff_sym/patch_spec.rb +154 -0
- data/spec/hashdiff_sym/util_spec.rb +78 -0
- data/spec/spec_helper.rb +13 -0
- metadata +112 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b1c3c20780bd5ac5b4651aa308d5efe2978c1d87
|
4
|
+
data.tar.gz: 5ed5b1c281805ea581428c5574dc5ff7d93f2237
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 73de6c8ca7869ea1990376f6e3a4c8daedfe9e16f50e15b920df5efb69f59dc84b2bdb81f920c807fb57e46d53c70209f72ffe00494d81cf1fe83b3c81d68a3b
|
7
|
+
data.tar.gz: 26ff1285b00bc83824f946e06499a2c2b371969b0149eeb09245d2e41d66930008d626656fa8fc3e2947bf206a307b1a8f28e083cdec29a8c8d03b6e20899e7c
|
data/.gitignore
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
2
|
+
#
|
3
|
+
# If you find yourself ignoring temporary files generated by your text editor
|
4
|
+
# or operating system, you probably want to add a global ignore instead:
|
5
|
+
# git config --global core.excludesfile ~/.gitignore_global
|
6
|
+
|
7
|
+
# Ignore bundler config
|
8
|
+
/.bundle
|
9
|
+
/doc
|
10
|
+
/.yardoc
|
11
|
+
/Gemfile.lock
|
12
|
+
|
13
|
+
*.swp
|
14
|
+
*.bak
|
15
|
+
*.gem
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.travis.yml
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2012 Liu Fengyun
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
## Summary
|
2
|
+
|
3
|
+
HashDiffSym is a fork of [HashDiff](https://github.com/liufengyun/hashdiff) (by [liufengyun](https://github.com/liufengyun)) with symbols support.
|
4
|
+
|
5
|
+
```
|
6
|
+
diff = HashDiffSym.diff({ 'a' => { x: 2, y: 3, z: 4 }, 'b' => { x: 3, z: [1, 2, 3] } },
|
7
|
+
{ 'a' => { y: 3 }, 'b' => { y: 3, z: [2, 3, 4] } })
|
8
|
+
diff.should == [['-', 'a.:x', 2], ['-', 'a.:z', 4], ['-', 'b.:x', 3], ["-", "b.:z[0]", 1], ["+", "b.:z[2]", 4], ['+', 'b.:y', 3]]
|
9
|
+
```
|
10
|
+
|
11
|
+
Also works for patch:
|
12
|
+
|
13
|
+
```
|
14
|
+
a = {a: 3}
|
15
|
+
b = {a: {a1: 1, a2: 2}}
|
16
|
+
diff = HashDiffSym.diff(a, b)
|
17
|
+
HashDiffSym.patch!(a, diff).should == b
|
18
|
+
```
|
19
|
+
|
20
|
+
## License
|
21
|
+
|
22
|
+
HashDiffSym is distributed under the MIT-LICENSE.
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
|
3
|
+
require 'bundler'
|
4
|
+
Bundler::GemHelper.install_tasks
|
5
|
+
|
6
|
+
require 'rspec/core/rake_task'
|
7
|
+
|
8
|
+
task :default => :spec
|
9
|
+
|
10
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
11
|
+
spec.pattern = "./spec/**/*_spec.rb"
|
12
|
+
end
|
13
|
+
|
data/changelog.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# Change Log
|
2
|
+
|
3
|
+
## v0.3.0 2016-2-11
|
4
|
+
|
5
|
+
* support `:case_insensitive` option
|
6
|
+
|
7
|
+
## v0.2.3 2015-11-5
|
8
|
+
|
9
|
+
* improve performance of LCS algorithm #12
|
10
|
+
|
11
|
+
## v0.2.2 2014-10-6
|
12
|
+
|
13
|
+
* make library 1.8.7 compatible
|
14
|
+
|
15
|
+
## v0.2.1 2014-7-13
|
16
|
+
|
17
|
+
* yield added/deleted keys for custom comparison
|
18
|
+
|
19
|
+
## v0.2.0 2014-3-29
|
20
|
+
|
21
|
+
* support custom comparison blocks
|
22
|
+
* support `:strip`, `:numeric_tolerance` and `:strict` options
|
23
|
+
|
24
|
+
## v0.1.0 2013-8-25
|
25
|
+
|
26
|
+
* use options for parameters `:delimiter` and `:similarity` in interfaces
|
27
|
+
|
28
|
+
## v0.0.6 2013-3-2
|
29
|
+
|
30
|
+
* Add parameter for custom property-path delimiter.
|
31
|
+
|
32
|
+
## v0.0.5 2012-7-1
|
33
|
+
|
34
|
+
* fix a bug in judging whehter two objects are similiar.
|
35
|
+
* add more spec test for HashDiffSym.best_diff
|
36
|
+
|
37
|
+
## v0.0.4 2012-6-24
|
38
|
+
|
39
|
+
Main changes in this version is to output the whole object in addition & deletion, instead of recursely add/deletes the object.
|
40
|
+
|
41
|
+
For example, `diff({a:2, c:[4, 5]}, {a:2}) will generate following output:
|
42
|
+
|
43
|
+
[['-', 'c', [4, 5]]]
|
44
|
+
|
45
|
+
instead of following:
|
46
|
+
|
47
|
+
[['-', 'c[0]', 4], ['-', 'c[1]', 5], ['-', 'c', []]]
|
48
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
$LOAD_PATH << File.expand_path("../lib", __FILE__)
|
2
|
+
require 'hashdiff_sym/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = %q{hashdiff_sym}
|
6
|
+
s.version = HashDiffSym::VERSION
|
7
|
+
s.license = 'MIT'
|
8
|
+
s.summary = %q{ HashDiffSym is a diff lib to compute the smallest difference between two hashes. }
|
9
|
+
s.description = %q{ HashDiffSym is a diff lib to compute the smallest difference between two hashes. }
|
10
|
+
|
11
|
+
s.files = `git ls-files`.split("\n")
|
12
|
+
s.test_files = `git ls-files -- Appraisals {spec}/*`.split("\n")
|
13
|
+
|
14
|
+
s.require_paths = ['lib']
|
15
|
+
s.required_ruby_version = Gem::Requirement.new(">= 1.8.7")
|
16
|
+
|
17
|
+
s.authors = ["Liu Fengyun", "Alexander Morozov"]
|
18
|
+
s.email = ["ntcomp12@gmail.com"]
|
19
|
+
|
20
|
+
s.homepage = "https://github.com/kengho/hashdiff_sym"
|
21
|
+
|
22
|
+
s.add_development_dependency("rspec", "~> 2.0")
|
23
|
+
s.add_development_dependency("yard")
|
24
|
+
s.add_development_dependency("bluecloth")
|
25
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
module HashDiffSym
|
2
|
+
|
3
|
+
# Best diff two objects, which tries to generate the smallest change set using different similarity values.
|
4
|
+
#
|
5
|
+
# HashDiffSym.best_diff is useful in case of comparing two objects which include similar hashes in arrays.
|
6
|
+
#
|
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.
|
16
|
+
#
|
17
|
+
# @return [Array] an array of changes.
|
18
|
+
# e.g. [[ '+', 'a.b', '45' ], [ '-', 'a.c', '5' ], [ '~', 'a.x', '45', '63']]
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# a = {'x' => [{'a' => 1, 'c' => 3, 'e' => 5}, {'y' => 3}]}
|
22
|
+
# b = {'x' => [{'a' => 1, 'b' => 2, 'e' => 5}] }
|
23
|
+
# diff = HashDiffSym.best_diff(a, b)
|
24
|
+
# diff.should == [['-', 'x[0].c', 3], ['+', 'x[0].b', 2], ['-', 'x[1].y', 3], ['-', 'x[1]', {}]]
|
25
|
+
#
|
26
|
+
# @since 0.0.1
|
27
|
+
def self.best_diff(obj1, obj2, options = {}, &block)
|
28
|
+
options[:comparison] = block if block_given?
|
29
|
+
|
30
|
+
opts = { :similarity => 0.3 }.merge!(options)
|
31
|
+
diffs_1 = diff(obj1, obj2, opts)
|
32
|
+
count_1 = count_diff diffs_1
|
33
|
+
|
34
|
+
opts = { :similarity => 0.5 }.merge!(options)
|
35
|
+
diffs_2 = diff(obj1, obj2, opts)
|
36
|
+
count_2 = count_diff diffs_2
|
37
|
+
|
38
|
+
opts = { :similarity => 0.8 }.merge!(options)
|
39
|
+
diffs_3 = diff(obj1, obj2, opts)
|
40
|
+
count_3 = count_diff diffs_3
|
41
|
+
|
42
|
+
count, diffs = count_1 < count_2 ? [count_1, diffs_1] : [count_2, diffs_2]
|
43
|
+
diffs = count < count_3 ? diffs : diffs_3
|
44
|
+
end
|
45
|
+
|
46
|
+
# Compute the diff of two hashes or arrays
|
47
|
+
#
|
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
|
56
|
+
#
|
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.
|
58
|
+
#
|
59
|
+
# @return [Array] an array of changes.
|
60
|
+
# e.g. [[ '+', 'a.b', '45' ], [ '-', 'a.c', '5' ], [ '~', 'a.x', '45', '63']]
|
61
|
+
#
|
62
|
+
# @example
|
63
|
+
# a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
|
64
|
+
# b = {"a" => 1, "b" => {}}
|
65
|
+
#
|
66
|
+
# diff = HashDiffSym.diff(a, b)
|
67
|
+
# diff.should == [['-', 'b.b1', 1], ['-', 'b.b2', 2]]
|
68
|
+
#
|
69
|
+
# @since 0.0.1
|
70
|
+
def self.diff(obj1, obj2, options = {}, &block)
|
71
|
+
opts = {
|
72
|
+
:prefix => '',
|
73
|
+
:similarity => 0.8,
|
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
|
+
result = custom_compare(opts[:comparison], opts[:prefix], obj1, obj2)
|
84
|
+
return result if result
|
85
|
+
|
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
|
124
|
+
|
125
|
+
deleted_keys = obj1.keys - obj2.keys
|
126
|
+
common_keys = obj1.keys & obj2.keys
|
127
|
+
added_keys = obj2.keys - obj1.keys
|
128
|
+
|
129
|
+
# add deleted properties
|
130
|
+
deleted_keys.sort.each do |k|
|
131
|
+
custom_result = custom_compare(opts[:comparison], "#{prefix}#{export_key(k)}", obj1[k], nil)
|
132
|
+
|
133
|
+
if custom_result
|
134
|
+
result.concat(custom_result)
|
135
|
+
else
|
136
|
+
result << ['-', "#{prefix}#{export_key(k)}", obj1[k]]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# recursive comparison for common keys
|
141
|
+
common_keys.sort.each {|k| result.concat(diff(obj1[k], obj2[k], opts.merge(:prefix => "#{prefix}#{export_key(k)}"))) }
|
142
|
+
|
143
|
+
# added properties
|
144
|
+
added_keys.sort.each do |k|
|
145
|
+
unless obj1.key?(k)
|
146
|
+
custom_result = custom_compare(opts[:comparison], "#{prefix}#{export_key(k)}", nil, obj2[k])
|
147
|
+
|
148
|
+
if custom_result
|
149
|
+
result.concat(custom_result)
|
150
|
+
else
|
151
|
+
result << ['+', "#{prefix}#{export_key(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
|
159
|
+
|
160
|
+
result
|
161
|
+
end
|
162
|
+
|
163
|
+
# @private
|
164
|
+
#
|
165
|
+
# 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)
|
172
|
+
|
173
|
+
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]]
|
179
|
+
end
|
180
|
+
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]]
|
185
|
+
end
|
186
|
+
return change_set
|
187
|
+
end
|
188
|
+
|
189
|
+
links = lcs(a, b, opts)
|
190
|
+
|
191
|
+
# yield common
|
192
|
+
yield links if block_given?
|
193
|
+
|
194
|
+
# padding the end
|
195
|
+
links << [a.size, b.size]
|
196
|
+
|
197
|
+
last_x = -1
|
198
|
+
last_y = -1
|
199
|
+
links.each do |pair|
|
200
|
+
x, y = pair
|
201
|
+
|
202
|
+
# 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]]
|
205
|
+
end
|
206
|
+
|
207
|
+
# 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]]
|
210
|
+
end
|
211
|
+
|
212
|
+
# update flags
|
213
|
+
last_x = x
|
214
|
+
last_y = y
|
215
|
+
end
|
216
|
+
|
217
|
+
change_set
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module HashDiffSym
|
2
|
+
# @private
|
3
|
+
#
|
4
|
+
# caculate array difference using LCS algorithm
|
5
|
+
# http://en.wikipedia.org/wiki/Longest_common_subsequence_problem
|
6
|
+
def self.lcs(a, b, options = {})
|
7
|
+
opts = { :similarity => 0.8 }.merge!(options)
|
8
|
+
|
9
|
+
opts[:prefix] = "#{opts[:prefix]}[*]"
|
10
|
+
|
11
|
+
return [] if a.size == 0 or b.size == 0
|
12
|
+
|
13
|
+
a_start = b_start = 0
|
14
|
+
a_finish = a.size - 1
|
15
|
+
b_finish = b.size - 1
|
16
|
+
vector = []
|
17
|
+
|
18
|
+
lcs = []
|
19
|
+
(b_start..b_finish).each do |bi|
|
20
|
+
lcs[bi] = []
|
21
|
+
(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
|
+
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
|
29
|
+
|
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
|
44
|
+
|
45
|
+
lcs[bi][ai] = [direction, count]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
x = a_finish
|
51
|
+
y = b_finish
|
52
|
+
while x >= 0 and y >= 0 and lcs[y][x][1] > 0
|
53
|
+
if lcs[y][x][0] == :both
|
54
|
+
x -= 1
|
55
|
+
elsif lcs[y][x][0] == :topleft
|
56
|
+
vector.insert(0, [x, y])
|
57
|
+
x -= 1
|
58
|
+
y -= 1
|
59
|
+
elsif lcs[y][x][0] == :top
|
60
|
+
y -= 1
|
61
|
+
elsif lcs[y][x][0] == :left
|
62
|
+
x -= 1
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
vector
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
#
|
2
|
+
# This module provides methods to diff two hash, patch and unpatch hash
|
3
|
+
#
|
4
|
+
module HashDiffSym
|
5
|
+
|
6
|
+
# Apply patch to object
|
7
|
+
#
|
8
|
+
# @param [Hash, Array] obj the object to be patched, can be an Array or a Hash
|
9
|
+
# @param [Array] changes e.g. [[ '+', 'a.b', '45' ], [ '-', 'a.c', '5' ], [ '~', 'a.x', '45', '63']]
|
10
|
+
# @param [Hash] options supports following keys:
|
11
|
+
# * :delimiter (String) ['.'] delimiter string for representing nested keys in changes array
|
12
|
+
#
|
13
|
+
# @return the object after patch
|
14
|
+
#
|
15
|
+
# @since 0.0.1
|
16
|
+
def self.patch!(obj, changes, options = {})
|
17
|
+
delimiter = options[:delimiter] || '.'
|
18
|
+
|
19
|
+
changes.each do |change|
|
20
|
+
parts = decode_property_path(change[1], delimiter)
|
21
|
+
last_part = parts.last
|
22
|
+
|
23
|
+
parent_node = node(obj, parts[0, parts.size-1])
|
24
|
+
|
25
|
+
if change[0] == '+'
|
26
|
+
if last_part.is_a?(Fixnum)
|
27
|
+
parent_node.insert(last_part, change[2])
|
28
|
+
else
|
29
|
+
parent_node[last_part] = change[2]
|
30
|
+
end
|
31
|
+
elsif change[0] == '-'
|
32
|
+
if last_part.is_a?(Fixnum)
|
33
|
+
parent_node.delete_at(last_part)
|
34
|
+
else
|
35
|
+
parent_node.delete(last_part)
|
36
|
+
end
|
37
|
+
elsif change[0] == '~'
|
38
|
+
parent_node[last_part] = change[3]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
obj
|
43
|
+
end
|
44
|
+
|
45
|
+
# Unpatch an object
|
46
|
+
#
|
47
|
+
# @param [Hash, Array] obj the object to be unpatched, can be an Array or a Hash
|
48
|
+
# @param [Array] changes e.g. [[ '+', 'a.b', '45' ], [ '-', 'a.c', '5' ], [ '~', 'a.x', '45', '63']]
|
49
|
+
# @param [Hash] options supports following keys:
|
50
|
+
# * :delimiter (String) ['.'] delimiter string for representing nested keys in changes array
|
51
|
+
#
|
52
|
+
# @return the object after unpatch
|
53
|
+
#
|
54
|
+
# @since 0.0.1
|
55
|
+
def self.unpatch!(obj, changes, options = {})
|
56
|
+
delimiter = options[:delimiter] || '.'
|
57
|
+
|
58
|
+
changes.reverse_each do |change|
|
59
|
+
parts = decode_property_path(change[1], delimiter)
|
60
|
+
last_part = parts.last
|
61
|
+
|
62
|
+
parent_node = node(obj, parts[0, parts.size-1])
|
63
|
+
|
64
|
+
if change[0] == '+'
|
65
|
+
if last_part.is_a?(Fixnum)
|
66
|
+
parent_node.delete_at(last_part)
|
67
|
+
else
|
68
|
+
parent_node.delete(last_part)
|
69
|
+
end
|
70
|
+
elsif change[0] == '-'
|
71
|
+
if last_part.is_a?(Fixnum)
|
72
|
+
parent_node.insert(last_part, change[2])
|
73
|
+
else
|
74
|
+
parent_node[last_part] = change[2]
|
75
|
+
end
|
76
|
+
elsif change[0] == '~'
|
77
|
+
parent_node[last_part] = change[2]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
obj
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|