hashdiff_sym 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,7 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
6
+ - 2.1.1
7
+ script: "bundle exec rake spec"
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-private
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+ gemspec
3
+
4
+ group :test do
5
+ gem 'rake'
6
+ end
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