hashdiff 0.2.1 → 0.2.2
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/.travis.yml +1 -0
- data/README.md +35 -3
- data/changelog.md +4 -0
- data/lib/hashdiff/diff.rb +11 -18
- data/lib/hashdiff/version.rb +1 -1
- data/spec/hashdiff/diff_spec.rb +20 -18
- metadata +81 -59
- checksums.yaml +0 -7
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -8,7 +8,7 @@ HashDiff is a ruby library to compute the smallest difference between two hashes
|
|
8
8
|
|
9
9
|
## Why HashDiff?
|
10
10
|
|
11
|
-
Given two Hashes A and B, sometimes you face the question: what's the smallest
|
11
|
+
Given two Hashes A and B, sometimes you face the question: what's the smallest modification that can be made to change A into B?
|
12
12
|
|
13
13
|
An algorithm that responds to this question has to do following:
|
14
14
|
|
@@ -16,11 +16,11 @@ An algorithm that responds to this question has to do following:
|
|
16
16
|
* Compute recursively -- Arrays and Hashes may be nested arbitrarily in A or B.
|
17
17
|
* Compute the smallest change -- it should recognize similar child Hashes or child Arrays between A and B.
|
18
18
|
|
19
|
-
HashDiff answers the question above
|
19
|
+
HashDiff answers the question above using an opinionated approach:
|
20
20
|
|
21
21
|
* Hash can be represented as a list of (dot-syntax-path, value) pairs. For example, `{a:[{c:2}]}` can be represented as `["a[0].c", 2]`.
|
22
22
|
* The change set can be represented using the dot-syntax representation. For example, `[['-', 'b.x', 3], ['~', 'b.z', 45, 30], ['+', 'b.y', 3]]`.
|
23
|
-
* It compares Arrays using LCS(longest common subsequence) algorithm.
|
23
|
+
* It compares Arrays using the [LCS(longest common subsequence)](http://en.wikipedia.org/wiki/Longest_common_subsequence_problem) algorithm.
|
24
24
|
* It recognizes similar Hashes in an Array using a similarity value (0 < similarity <= 1).
|
25
25
|
|
26
26
|
## Usage
|
@@ -171,6 +171,38 @@ diff.should == [["~", "a", "car", "bus"], ["~", "b[1]", "plane", " plan"], ["-",
|
|
171
171
|
|
172
172
|
When a comparison block is given, it'll be given priority over other specified options. If the block returns value other than `true` or `false`, then the two values will be compared with other specified options.
|
173
173
|
|
174
|
+
#### Sorting arrays before comparison
|
175
|
+
|
176
|
+
An order difference alone between two arrays can create too many diffs to be useful. Consider sorting them prior to diffing.
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
a = {a:'car', b:['boat', 'plane'] }
|
180
|
+
b = {a:'car', b:['plane', 'boat'] }
|
181
|
+
|
182
|
+
HashDiff.diff(a, b) => [["+", "b[0]", "plane"], ["-", "b[2]", "plane"]]
|
183
|
+
|
184
|
+
b[:b].sort!
|
185
|
+
|
186
|
+
HashDiff.diff(a, b) => []
|
187
|
+
```
|
188
|
+
|
189
|
+
### Special use cases
|
190
|
+
|
191
|
+
#### Using HashDiff on JSON API results
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
require 'uri'
|
195
|
+
require 'net/http'
|
196
|
+
require 'json'
|
197
|
+
|
198
|
+
uri = URI('http://time.jsontest.com/')
|
199
|
+
json_resp = ->(uri) { JSON.parse(Net::HTTP.get_response(uri).body) }
|
200
|
+
a = json_resp.call(uri)
|
201
|
+
b = json_resp.call(uri)
|
202
|
+
|
203
|
+
HashDiff.diff(a,b) => [["~", "milliseconds_since_epoch", 1410542545874, 1410542545985]]
|
204
|
+
```
|
205
|
+
|
174
206
|
## License
|
175
207
|
|
176
208
|
HashDiff is distributed under the MIT-LICENSE.
|
data/changelog.md
CHANGED
data/lib/hashdiff/diff.rb
CHANGED
@@ -27,15 +27,15 @@ module HashDiff
|
|
27
27
|
def self.best_diff(obj1, obj2, options = {}, &block)
|
28
28
|
options[:comparison] = block if block_given?
|
29
29
|
|
30
|
-
opts = {similarity
|
30
|
+
opts = { :similarity => 0.3 }.merge!(options)
|
31
31
|
diffs_1 = diff(obj1, obj2, opts)
|
32
32
|
count_1 = count_diff diffs_1
|
33
33
|
|
34
|
-
opts = {similarity
|
34
|
+
opts = { :similarity => 0.5 }.merge!(options)
|
35
35
|
diffs_2 = diff(obj1, obj2, opts)
|
36
36
|
count_2 = count_diff diffs_2
|
37
37
|
|
38
|
-
opts = {similarity
|
38
|
+
opts = { :similarity => 0.8 }.merge!(options)
|
39
39
|
diffs_3 = diff(obj1, obj2, opts)
|
40
40
|
count_3 = count_diff diffs_3
|
41
41
|
|
@@ -104,7 +104,7 @@ module HashDiff
|
|
104
104
|
changeset = diff_array(obj1, obj2, opts) do |lcs|
|
105
105
|
# use a's index for similarity
|
106
106
|
lcs.each do |pair|
|
107
|
-
result.concat(diff(obj1[pair[0]], obj2[pair[1]], opts.merge(prefix
|
107
|
+
result.concat(diff(obj1[pair[0]], obj2[pair[1]], opts.merge(:prefix => "#{opts[:prefix]}[#{pair[0]}]")))
|
108
108
|
end
|
109
109
|
end
|
110
110
|
|
@@ -122,19 +122,12 @@ module HashDiff
|
|
122
122
|
prefix = "#{opts[:prefix]}#{opts[:delimiter]}"
|
123
123
|
end
|
124
124
|
|
125
|
-
deleted_keys =
|
126
|
-
common_keys =
|
127
|
-
|
128
|
-
obj1.each do |k, v|
|
129
|
-
if obj2.key?(k)
|
130
|
-
common_keys << k
|
131
|
-
else
|
132
|
-
deleted_keys << k
|
133
|
-
end
|
134
|
-
end
|
125
|
+
deleted_keys = obj1.keys - obj2.keys
|
126
|
+
common_keys = obj1.keys & obj2.keys
|
127
|
+
added_keys = obj2.keys - obj1.keys
|
135
128
|
|
136
129
|
# add deleted properties
|
137
|
-
deleted_keys.each do |k|
|
130
|
+
deleted_keys.sort.each do |k|
|
138
131
|
custom_result = custom_compare(opts[:comparison], "#{prefix}#{k}", obj1[k], nil)
|
139
132
|
|
140
133
|
if custom_result
|
@@ -145,12 +138,12 @@ module HashDiff
|
|
145
138
|
end
|
146
139
|
|
147
140
|
# recursive comparison for common keys
|
148
|
-
common_keys.each {|k| result.concat(diff(obj1[k], obj2[k], opts.merge(prefix
|
141
|
+
common_keys.sort.each {|k| result.concat(diff(obj1[k], obj2[k], opts.merge(:prefix => "#{prefix}#{k}"))) }
|
149
142
|
|
150
143
|
# added properties
|
151
|
-
|
144
|
+
added_keys.sort.each do |k|
|
152
145
|
unless obj1.key?(k)
|
153
|
-
custom_result = custom_compare(opts[:comparison], "#{prefix}#{k}", nil,
|
146
|
+
custom_result = custom_compare(opts[:comparison], "#{prefix}#{k}", nil, obj2[k])
|
154
147
|
|
155
148
|
if custom_result
|
156
149
|
result.concat(custom_result)
|
data/lib/hashdiff/version.rb
CHANGED
data/spec/hashdiff/diff_spec.rb
CHANGED
@@ -7,7 +7,7 @@ describe HashDiff do
|
|
7
7
|
end
|
8
8
|
|
9
9
|
it "should be able to diff an hash with an empty hash" do
|
10
|
-
a = {a
|
10
|
+
a = { 'a' => 3, 'b' => 2 }
|
11
11
|
b = {}
|
12
12
|
|
13
13
|
diff = HashDiff.diff(a, b)
|
@@ -18,32 +18,34 @@ describe HashDiff do
|
|
18
18
|
end
|
19
19
|
|
20
20
|
it "should be able to diff two equal hashes" do
|
21
|
-
diff = HashDiff.diff({a
|
21
|
+
diff = HashDiff.diff({ 'a' => 2, 'b' => 2}, { 'a' => 2, 'b' => 2 })
|
22
22
|
diff.should == []
|
23
23
|
end
|
24
24
|
|
25
25
|
it "should be able to diff two hashes with equivalent numerics, when strict is false" do
|
26
|
-
diff = HashDiff.diff({a
|
26
|
+
diff = HashDiff.diff({ 'a' => 2.0, 'b' => 2 }, { 'a' => 2, 'b' => 2.0 }, :strict => false)
|
27
27
|
diff.should == []
|
28
28
|
end
|
29
29
|
|
30
30
|
it "should be able to diff changes in hash value" do
|
31
|
-
diff = HashDiff.diff({a
|
31
|
+
diff = HashDiff.diff({ 'a' => 2, 'b' => 3, 'c' => " hello" }, { 'a' => 2, 'b' => 4, 'c' => "hello" })
|
32
32
|
diff.should == [['~', 'b', 3, 4], ['~', 'c', " hello", "hello"]]
|
33
33
|
end
|
34
34
|
|
35
35
|
it "should be able to diff changes in hash value which is array" do
|
36
|
-
diff = HashDiff.diff({a
|
36
|
+
diff = HashDiff.diff({ 'a' => 2, 'b' => [1, 2, 3] }, { 'a' => 2, 'b' => [1, 3, 4]})
|
37
37
|
diff.should == [['-', 'b[1]', 2], ['+', 'b[2]', 4]]
|
38
38
|
end
|
39
39
|
|
40
40
|
it "should be able to diff changes in hash value which is hash" do
|
41
|
-
diff = HashDiff.diff({a
|
41
|
+
diff = HashDiff.diff({ 'a' => { 'x' => 2, 'y' => 3, 'z' => 4 }, 'b' => { 'x' => 3, 'z' => 45 } },
|
42
|
+
{ 'a' => { 'y' => 3 }, 'b' => { 'y' => 3, 'z' => 30 } })
|
42
43
|
diff.should == [['-', 'a.x', 2], ['-', 'a.z', 4], ['-', 'b.x', 3], ['~', 'b.z', 45, 30], ['+', 'b.y', 3]]
|
43
44
|
end
|
44
45
|
|
45
46
|
it "should be able to diff similar objects in array" do
|
46
|
-
diff = HashDiff.best_diff({a
|
47
|
+
diff = HashDiff.best_diff({ 'a' => [{ 'x' => 2, 'y' => 3, 'z' => 4 }, { 'x' => 11, 'y' => 22, 'z' => 33 }], 'b' => { 'x' => 3, 'z' => 45 } },
|
48
|
+
{ 'a' => [{ 'y' => 3 }, { 'x' => 11, 'z' => 33 }], 'b' => { 'y' => 22 } })
|
47
49
|
diff.should == [['-', 'a[0].x', 2], ['-', 'a[0].z', 4], ['-', 'a[1].y', 22], ['-', 'b.x', 3], ['-', 'b.z', 45], ['+', 'b.y', 22]]
|
48
50
|
end
|
49
51
|
|
@@ -137,7 +139,7 @@ describe HashDiff do
|
|
137
139
|
a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, {'x' => 5, 'y' => 6, 'z' => 3}, 3]
|
138
140
|
b = [{'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}, 3]
|
139
141
|
|
140
|
-
diff = HashDiff.diff(a, b, similarity
|
142
|
+
diff = HashDiff.diff(a, b, :similarity => 0.8, :delimiter => "\t")
|
141
143
|
diff.should == [["-", "[0]\td", 4], ["-", "[1]", {"x"=>5, "y"=>6, "z"=>3}]]
|
142
144
|
end
|
143
145
|
|
@@ -167,15 +169,15 @@ describe HashDiff do
|
|
167
169
|
|
168
170
|
context 'when :strip requested' do
|
169
171
|
it "should strip strings before comparing" do
|
170
|
-
a = {a
|
171
|
-
b = {a
|
172
|
+
a = { 'a' => " foo", 'b' => "fizz buzz"}
|
173
|
+
b = { 'a' => "foo", 'b' => "fizzbuzz"}
|
172
174
|
diff = HashDiff.diff(a, b, :strip => true)
|
173
175
|
diff.should == [['~', 'b', "fizz buzz", "fizzbuzz"]]
|
174
176
|
end
|
175
177
|
|
176
178
|
it "should strip nested strings before comparing" do
|
177
|
-
a = {a
|
178
|
-
b = {a
|
179
|
+
a = { 'a' => { 'x' => " foo" }, 'b' => ["fizz buzz", "nerf"] }
|
180
|
+
b = { 'a' => { 'x' => "foo" }, 'b' => ["fizzbuzz", "nerf"] }
|
179
181
|
diff = HashDiff.diff(a, b, :strip => true)
|
180
182
|
diff.should == [['-', 'b[0]', "fizz buzz"], ['+', 'b[0]', "fizzbuzz"]]
|
181
183
|
end
|
@@ -183,16 +185,16 @@ describe HashDiff do
|
|
183
185
|
|
184
186
|
context 'when both :strip and :numeric_tolerance requested' do
|
185
187
|
it 'should apply filters to proper object types' do
|
186
|
-
a = {a
|
187
|
-
b = {a
|
188
|
+
a = { 'a' => " foo", 'b' => 35, 'c' => 'bar', 'd' => 'baz' }
|
189
|
+
b = { 'a' => "foo", 'b' => 35.005, 'c' => 'bar', 'd' => 18.5}
|
188
190
|
diff = HashDiff.diff(a, b, :strict => false, :numeric_tolerance => 0.01, :strip => true)
|
189
191
|
diff.should == [['~', 'd', "baz", 18.5]]
|
190
192
|
end
|
191
193
|
end
|
192
194
|
|
193
195
|
context 'with custom comparison' do
|
194
|
-
let(:a) { {a
|
195
|
-
let(:b) { {a
|
196
|
+
let(:a) { { 'a' => 'car', 'b' => 'boat', 'c' => 'plane'} }
|
197
|
+
let(:b) { { 'a' => 'bus', 'b' => 'truck', 'c' => ' plan'} }
|
196
198
|
|
197
199
|
it 'should compare using proc specified in block' do
|
198
200
|
diff = HashDiff.diff(a, b) do |prefix, obj1, obj2|
|
@@ -205,8 +207,8 @@ describe HashDiff do
|
|
205
207
|
end
|
206
208
|
|
207
209
|
it 'should yield added keys' do
|
208
|
-
x = {a
|
209
|
-
y = {a
|
210
|
+
x = { 'a' => 'car', 'b' => 'boat'}
|
211
|
+
y = { 'a' => 'car' }
|
210
212
|
|
211
213
|
diff = HashDiff.diff(x, y) do |prefix, obj1, obj2|
|
212
214
|
case prefix
|
metadata
CHANGED
@@ -1,65 +1,75 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: hashdiff
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 19
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 2
|
9
|
+
- 2
|
10
|
+
version: 0.2.2
|
5
11
|
platform: ruby
|
6
|
-
authors:
|
12
|
+
authors:
|
7
13
|
- Liu Fengyun
|
8
14
|
autorequire:
|
9
15
|
bindir: bin
|
10
16
|
cert_chain: []
|
11
|
-
|
12
|
-
|
13
|
-
|
17
|
+
|
18
|
+
date: 2014-10-06 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
14
21
|
name: rspec
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ~>
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '2.0'
|
20
|
-
type: :development
|
21
22
|
prerelease: false
|
22
|
-
|
23
|
-
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
24
26
|
- - ~>
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
- - '>='
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0'
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 2
|
31
|
+
- 0
|
32
|
+
version: "2.0"
|
34
33
|
type: :development
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: yard
|
35
37
|
prerelease: false
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
- - '>='
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 3
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
48
47
|
type: :development
|
48
|
+
version_requirements: *id002
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: bluecloth
|
49
51
|
prerelease: false
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
type: :development
|
62
|
+
version_requirements: *id003
|
63
|
+
description: " HashDiff is a diff lib to compute the smallest difference between two hashes. "
|
64
|
+
email:
|
58
65
|
- liufengyunchina@gmail.com
|
59
66
|
executables: []
|
67
|
+
|
60
68
|
extensions: []
|
69
|
+
|
61
70
|
extra_rdoc_files: []
|
62
|
-
|
71
|
+
|
72
|
+
files:
|
63
73
|
- .gitignore
|
64
74
|
- .rspec
|
65
75
|
- .travis.yml
|
@@ -84,28 +94,40 @@ files:
|
|
84
94
|
- spec/hashdiff/util_spec.rb
|
85
95
|
- spec/spec_helper.rb
|
86
96
|
homepage: https://github.com/liufengyun/hashdiff
|
87
|
-
licenses:
|
97
|
+
licenses:
|
88
98
|
- MIT
|
89
|
-
metadata: {}
|
90
99
|
post_install_message:
|
91
100
|
rdoc_options: []
|
92
|
-
|
101
|
+
|
102
|
+
require_paths:
|
93
103
|
- lib
|
94
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
-
|
96
|
-
|
97
|
-
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
hash: 57
|
110
|
+
segments:
|
111
|
+
- 1
|
112
|
+
- 8
|
113
|
+
- 7
|
98
114
|
version: 1.8.7
|
99
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
|
+
none: false
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
hash: 3
|
121
|
+
segments:
|
122
|
+
- 0
|
123
|
+
version: "0"
|
104
124
|
requirements: []
|
125
|
+
|
105
126
|
rubyforge_project:
|
106
|
-
rubygems_version:
|
127
|
+
rubygems_version: 1.8.25
|
107
128
|
signing_key:
|
108
|
-
specification_version:
|
129
|
+
specification_version: 3
|
109
130
|
summary: HashDiff is a diff lib to compute the smallest difference between two hashes.
|
110
131
|
test_files: []
|
132
|
+
|
111
133
|
has_rdoc:
|
checksums.yaml
DELETED
@@ -1,7 +0,0 @@
|
|
1
|
-
---
|
2
|
-
SHA1:
|
3
|
-
metadata.gz: d8b0a6ff22612f26b5bca9a05e0aa2a59903d8ea
|
4
|
-
data.tar.gz: 239c5cc0d80aad921ab88ae4712c215598b964e5
|
5
|
-
SHA512:
|
6
|
-
metadata.gz: f32454eb369913c1ef2da688d843d3fa2f06b7d98d14cbea8a586534735cf0ac548beef2c49608e5d637d0569c08567b11b8bc89a575b01c13ec855be7f51f29
|
7
|
-
data.tar.gz: dcd6291074a3038274b07ea772f210050dcd8cd448ed0d9b53a47572206bae3d9a64d19b1807194fd10a5ba69e9466999889fc7b190c4e505198c72b9a8887c2
|