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 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