hashdiff 0.3.7 → 0.3.8
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 +21 -0
- data/.travis.yml +2 -5
- data/Gemfile +1 -1
- data/README.md +1 -1
- data/Rakefile +7 -4
- data/changelog.md +4 -0
- data/hashdiff.gemspec +13 -11
- data/lib/hashdiff/diff.rb +56 -64
- data/lib/hashdiff/lcs.rb +23 -28
- data/lib/hashdiff/linear_compare_array.rb +4 -2
- data/lib/hashdiff/patch.rb +2 -4
- data/lib/hashdiff/util.rb +27 -31
- data/lib/hashdiff/version.rb +1 -1
- data/spec/hash_diff/best_diff_spec.rb +73 -0
- data/spec/hash_diff/diff_array_spec.rb +58 -0
- data/spec/hash_diff/diff_spec.rb +339 -0
- data/spec/hash_diff/lcs_spec.rb +74 -0
- data/spec/hash_diff/linear_compare_array_spec.rb +48 -0
- data/spec/hash_diff/patch_spec.rb +183 -0
- data/spec/hash_diff/util_spec.rb +85 -0
- metadata +55 -26
- data/spec/hashdiff/best_diff_spec.rb +0 -74
- data/spec/hashdiff/diff_array_spec.rb +0 -60
- data/spec/hashdiff/diff_spec.rb +0 -339
- data/spec/hashdiff/lcs_spec.rb +0 -75
- data/spec/hashdiff/linear_compare_array_spec.rb +0 -48
- data/spec/hashdiff/patch_spec.rb +0 -183
- data/spec/hashdiff/util_spec.rb +0 -78
data/lib/hashdiff/patch.rb
CHANGED
@@ -2,7 +2,6 @@
|
|
2
2
|
# This module provides methods to diff two hash, patch and unpatch hash
|
3
3
|
#
|
4
4
|
module HashDiff
|
5
|
-
|
6
5
|
# Apply patch to object
|
7
6
|
#
|
8
7
|
# @param [Hash, Array] obj the object to be patched, can be an Array or a Hash
|
@@ -22,7 +21,7 @@ module HashDiff
|
|
22
21
|
|
23
22
|
last_part = parts.last
|
24
23
|
|
25
|
-
parent_node = node(obj, parts[0, parts.size-1])
|
24
|
+
parent_node = node(obj, parts[0, parts.size - 1])
|
26
25
|
|
27
26
|
if change[0] == '+'
|
28
27
|
if parent_node.is_a?(Array)
|
@@ -63,7 +62,7 @@ module HashDiff
|
|
63
62
|
|
64
63
|
last_part = parts.last
|
65
64
|
|
66
|
-
parent_node = node(obj, parts[0, parts.size-1])
|
65
|
+
parent_node = node(obj, parts[0, parts.size - 1])
|
67
66
|
|
68
67
|
if change[0] == '+'
|
69
68
|
if parent_node.is_a?(Array)
|
@@ -84,5 +83,4 @@ module HashDiff
|
|
84
83
|
|
85
84
|
obj
|
86
85
|
end
|
87
|
-
|
88
86
|
end
|
data/lib/hashdiff/util.rb
CHANGED
@@ -1,21 +1,19 @@
|
|
1
1
|
module HashDiff
|
2
|
-
|
3
2
|
# @private
|
4
3
|
#
|
5
4
|
# judge whether two objects are similar
|
6
|
-
def self.similar?(
|
7
|
-
return compare_values(
|
8
|
-
opts = { :similarity => 0.8 }.merge(options)
|
5
|
+
def self.similar?(obja, objb, options = {})
|
6
|
+
return compare_values(obja, objb, options) unless obja.is_a?(Array) || obja.is_a?(Hash) || objb.is_a?(Array) || objb.is_a?(Hash)
|
9
7
|
|
10
|
-
|
11
|
-
count_b = count_nodes(b)
|
12
|
-
diffs = count_diff diff(a, b, opts)
|
8
|
+
opts = { similarity: 0.8 }.merge(options)
|
13
9
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
10
|
+
count_a = count_nodes(obja)
|
11
|
+
count_b = count_nodes(objb)
|
12
|
+
diffs = count_diff diff(obja, objb, opts)
|
13
|
+
|
14
|
+
return true if (count_a + count_b).zero?
|
15
|
+
|
16
|
+
(1 - diffs.to_f / (count_a + count_b).to_f) >= opts[:similarity]
|
19
17
|
end
|
20
18
|
|
21
19
|
# @private
|
@@ -25,7 +23,7 @@ module HashDiff
|
|
25
23
|
diffs.inject(0) do |sum, item|
|
26
24
|
old_change_count = count_nodes(item[2])
|
27
25
|
new_change_count = count_nodes(item[3])
|
28
|
-
sum
|
26
|
+
sum + (old_change_count + new_change_count)
|
29
27
|
end
|
30
28
|
end
|
31
29
|
|
@@ -37,9 +35,9 @@ module HashDiff
|
|
37
35
|
|
38
36
|
count = 0
|
39
37
|
if obj.is_a?(Array)
|
40
|
-
obj.each {|e| count += count_nodes(e) }
|
38
|
+
obj.each { |e| count += count_nodes(e) }
|
41
39
|
elsif obj.is_a?(Hash)
|
42
|
-
obj.
|
40
|
+
obj.each_value { |v| count += count_nodes(v) }
|
43
41
|
else
|
44
42
|
return 1
|
45
43
|
end
|
@@ -54,13 +52,13 @@ module HashDiff
|
|
54
52
|
# @param [String] delimiter Property-string delimiter
|
55
53
|
#
|
56
54
|
# e.g. "a.b[3].c" => ['a', 'b', 3, 'c']
|
57
|
-
def self.decode_property_path(path, delimiter='.')
|
55
|
+
def self.decode_property_path(path, delimiter = '.')
|
58
56
|
path.split(delimiter).inject([]) do |memo, part|
|
59
57
|
if part =~ /^(.*)\[(\d+)\]$/
|
60
|
-
if
|
61
|
-
memo + [
|
58
|
+
if !Regexp.last_match(1).empty?
|
59
|
+
memo + [Regexp.last_match(1), Regexp.last_match(2).to_i]
|
62
60
|
else
|
63
|
-
memo + [
|
61
|
+
memo + [Regexp.last_match(2).to_i]
|
64
62
|
end
|
65
63
|
else
|
66
64
|
memo + [part]
|
@@ -84,7 +82,7 @@ module HashDiff
|
|
84
82
|
# check for equality or "closeness" within given tolerance
|
85
83
|
def self.compare_values(obj1, obj2, options = {})
|
86
84
|
if (options[:numeric_tolerance].is_a? Numeric) &&
|
87
|
-
|
85
|
+
[obj1, obj2].all? { |v| v.is_a? Numeric }
|
88
86
|
return (obj1 - obj2).abs <= options[:numeric_tolerance]
|
89
87
|
end
|
90
88
|
|
@@ -109,6 +107,7 @@ module HashDiff
|
|
109
107
|
return true if obj1.is_a?(type) && obj2.is_a?(type)
|
110
108
|
end
|
111
109
|
return true if !strict && obj1.is_a?(Numeric) && obj2.is_a?(Numeric)
|
110
|
+
|
112
111
|
obj1.is_a?(obj2.class) && obj2.is_a?(obj1.class)
|
113
112
|
end
|
114
113
|
|
@@ -116,23 +115,20 @@ module HashDiff
|
|
116
115
|
#
|
117
116
|
# try custom comparison
|
118
117
|
def self.custom_compare(method, key, obj1, obj2)
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
return []
|
127
|
-
end
|
128
|
-
end
|
118
|
+
return unless method
|
119
|
+
|
120
|
+
res = method.call(key, obj1, obj2)
|
121
|
+
|
122
|
+
# nil != false here
|
123
|
+
return [['~', key, obj1, obj2]] if res == false
|
124
|
+
return [] if res == true
|
129
125
|
end
|
130
126
|
|
131
127
|
def self.prefix_append_key(prefix, key, opts)
|
132
128
|
if opts[:array_path]
|
133
129
|
prefix + [key]
|
134
130
|
else
|
135
|
-
prefix.empty? ?
|
131
|
+
prefix.empty? ? key.to_s : "#{prefix}#{opts[:delimiter]}#{key}"
|
136
132
|
end
|
137
133
|
end
|
138
134
|
|
data/lib/hashdiff/version.rb
CHANGED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe HashDiff do
|
4
|
+
it 'is able to best diff' do
|
5
|
+
a = { 'x' => [{ 'a' => 1, 'c' => 3, 'e' => 5 }, { 'y' => 3 }] }
|
6
|
+
b = { 'x' => [{ 'a' => 1, 'b' => 2, 'e' => 5 }] }
|
7
|
+
|
8
|
+
diff = described_class.best_diff(a, b)
|
9
|
+
diff.should == [['-', 'x[0].c', 3], ['+', 'x[0].b', 2], ['-', 'x[1]', { 'y' => 3 }]]
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'uses custom delimiter when provided' do
|
13
|
+
a = { 'x' => [{ 'a' => 1, 'c' => 3, 'e' => 5 }, { 'y' => 3 }] }
|
14
|
+
b = { 'x' => [{ 'a' => 1, 'b' => 2, 'e' => 5 }] }
|
15
|
+
|
16
|
+
diff = described_class.best_diff(a, b, delimiter: "\t")
|
17
|
+
diff.should == [['-', "x[0]\tc", 3], ['+', "x[0]\tb", 2], ['-', 'x[1]', { 'y' => 3 }]]
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'uses custom comparison when provided' do
|
21
|
+
a = { 'x' => [{ 'a' => 'foo', 'c' => 'goat', 'e' => 'snake' }, { 'y' => 'baz' }] }
|
22
|
+
b = { 'x' => [{ 'a' => 'bar', 'b' => 'cow', 'e' => 'puppy' }] }
|
23
|
+
|
24
|
+
diff = described_class.best_diff(a, b) do |path, obj1, obj2|
|
25
|
+
case path
|
26
|
+
when /^x\[.\]\..$/
|
27
|
+
obj1.length == obj2.length if obj1 && obj2
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
diff.should == [['-', 'x[0].c', 'goat'], ['+', 'x[0].b', 'cow'], ['-', 'x[1]', { 'y' => 'baz' }]]
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'is able to best diff array in hash' do
|
35
|
+
a = { 'menu' => {
|
36
|
+
'id' => 'file',
|
37
|
+
'value' => 'File',
|
38
|
+
'popup' => {
|
39
|
+
'menuitem' => [
|
40
|
+
{ 'value' => 'New', 'onclick' => 'CreateNewDoc()' },
|
41
|
+
{ 'value' => 'Close', 'onclick' => 'CloseDoc()' }
|
42
|
+
]
|
43
|
+
}
|
44
|
+
} }
|
45
|
+
|
46
|
+
b = { 'menu' => {
|
47
|
+
'id' => 'file 2',
|
48
|
+
'value' => 'File',
|
49
|
+
'popup' => {
|
50
|
+
'menuitem' => [
|
51
|
+
{ 'value' => 'New1', 'onclick' => 'CreateNewDoc()' },
|
52
|
+
{ 'value' => 'Open', 'onclick' => 'OpenDoc()' },
|
53
|
+
{ 'value' => 'Close', 'onclick' => 'CloseDoc()' }
|
54
|
+
]
|
55
|
+
}
|
56
|
+
} }
|
57
|
+
|
58
|
+
diff = described_class.best_diff(a, b)
|
59
|
+
diff.should == [
|
60
|
+
['~', 'menu.id', 'file', 'file 2'],
|
61
|
+
['~', 'menu.popup.menuitem[0].value', 'New', 'New1'],
|
62
|
+
['+', 'menu.popup.menuitem[1]', { 'value' => 'Open', 'onclick' => 'OpenDoc()' }]
|
63
|
+
]
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'is able to have an array_path specified' do
|
67
|
+
a = { 'x' => [{ 'a' => 1, 'c' => 3, 'e' => 5 }, { 'y' => 3 }] }
|
68
|
+
b = { 'x' => [{ 'a' => 1, 'b' => 2, 'e' => 5 }] }
|
69
|
+
|
70
|
+
diff = described_class.best_diff(a, b, array_path: true)
|
71
|
+
diff.should == [['-', ['x', 0, 'c'], 3], ['+', ['x', 0, 'b'], 2], ['-', ['x', 1], { 'y' => 3 }]]
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe HashDiff do
|
4
|
+
it 'is able to diff two equal array' do
|
5
|
+
a = [1, 2, 3]
|
6
|
+
b = [1, 2, 3]
|
7
|
+
|
8
|
+
diff = described_class.diff_array_lcs(a, b)
|
9
|
+
diff.should == []
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'is able to diff two arrays with one element in common' do
|
13
|
+
a = [1, 2, 3]
|
14
|
+
b = [1, 8, 7]
|
15
|
+
|
16
|
+
diff = described_class.diff_array_lcs(a, b)
|
17
|
+
diff.should == [['-', 2, 3], ['-', 1, 2], ['+', 1, 8], ['+', 2, 7]]
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'is able to diff two arrays with nothing in common' do
|
21
|
+
a = [1, 2]
|
22
|
+
b = []
|
23
|
+
|
24
|
+
diff = described_class.diff_array_lcs(a, b)
|
25
|
+
diff.should == [['-', 1, 2], ['-', 0, 1]]
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'is able to diff an empty array with an non-empty array' do
|
29
|
+
a = []
|
30
|
+
b = [1, 2]
|
31
|
+
|
32
|
+
diff = described_class.diff_array_lcs(a, b)
|
33
|
+
diff.should == [['+', 0, 1], ['+', 1, 2]]
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'is able to diff two arrays with two elements in common' do
|
37
|
+
a = [1, 3, 5, 7]
|
38
|
+
b = [2, 3, 7, 5]
|
39
|
+
|
40
|
+
diff = described_class.diff_array_lcs(a, b)
|
41
|
+
diff.should == [['-', 0, 1], ['+', 0, 2], ['+', 2, 7], ['-', 4, 7]]
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'is able to test two arrays with two common elements in different order' do
|
45
|
+
a = [1, 3, 4, 7]
|
46
|
+
b = [2, 3, 7, 5]
|
47
|
+
|
48
|
+
diff = described_class.diff_array_lcs(a, b)
|
49
|
+
diff.should == [['-', 0, 1], ['+', 0, 2], ['-', 2, 4], ['+', 3, 5]]
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'is able to diff two arrays with similar elements' do
|
53
|
+
a = [{ 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5 }, 3]
|
54
|
+
b = [1, { 'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5 }]
|
55
|
+
diff = described_class.diff_array_lcs(a, b)
|
56
|
+
diff.should == [['+', 0, 1], ['-', 2, 3]]
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,339 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe HashDiff do
|
4
|
+
it 'is able to diff two empty hashes' do
|
5
|
+
diff = described_class.diff({}, {})
|
6
|
+
diff.should == []
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'is able to diff an hash with an empty hash' do
|
10
|
+
a = { 'a' => 3, 'b' => 2 }
|
11
|
+
b = {}
|
12
|
+
|
13
|
+
diff = described_class.diff(a, b)
|
14
|
+
expect(diff).to eq([['-', 'a', 3], ['-', 'b', 2]])
|
15
|
+
|
16
|
+
diff = described_class.diff(b, a)
|
17
|
+
diff.should == [['+', 'a', 3], ['+', 'b', 2]]
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'is able to diff two equal hashes' do
|
21
|
+
diff = described_class.diff({ 'a' => 2, 'b' => 2 }, 'a' => 2, 'b' => 2)
|
22
|
+
diff.should == []
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'is able to diff two equal hashes with mixed key types' do
|
26
|
+
a = { 'a' => 1, :b => 1 }
|
27
|
+
diff = described_class.diff(a, a)
|
28
|
+
diff.should == []
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'is able to diff if mixed key types are removed' do
|
32
|
+
a = { 'a' => 1, :b => 1 }
|
33
|
+
b = {}
|
34
|
+
diff = described_class.diff(a, b)
|
35
|
+
diff.should == [['-', 'a', 1], ['-', 'b', 1]]
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'is able to diff if mixed key types are added' do
|
39
|
+
a = { 'a' => 1, :b => 1 }
|
40
|
+
b = {}
|
41
|
+
diff = described_class.diff(b, a)
|
42
|
+
diff.should == [['+', 'a', 1], ['+', 'b', 1]]
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'is able to diff two hashes with equivalent numerics, when strict is false' do
|
46
|
+
diff = described_class.diff({ 'a' => 2.0, 'b' => 2 }, { 'a' => 2, 'b' => 2.0 }, strict: false)
|
47
|
+
diff.should == []
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'is able to diff changes in hash value' do
|
51
|
+
diff = described_class.diff({ 'a' => 2, 'b' => 3, 'c' => ' hello' }, 'a' => 2, 'b' => 4, 'c' => 'hello')
|
52
|
+
diff.should == [['~', 'b', 3, 4], ['~', 'c', ' hello', 'hello']]
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'is able to diff changes in hash value which is array' do
|
56
|
+
diff = described_class.diff({ 'a' => 2, 'b' => [1, 2, 3] }, 'a' => 2, 'b' => [1, 3, 4])
|
57
|
+
diff.should == [['-', 'b[1]', 2], ['+', 'b[2]', 4]]
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'is able to diff changes in hash value which is hash' do
|
61
|
+
diff = described_class.diff({ 'a' => { 'x' => 2, 'y' => 3, 'z' => 4 }, 'b' => { 'x' => 3, 'z' => 45 } },
|
62
|
+
'a' => { 'y' => 3 }, 'b' => { 'y' => 3, 'z' => 30 })
|
63
|
+
diff.should == [['-', 'a.x', 2], ['-', 'a.z', 4], ['-', 'b.x', 3], ['~', 'b.z', 45, 30], ['+', 'b.y', 3]]
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'is able to best diff similar objects in array' do
|
67
|
+
diff = described_class.best_diff({ 'a' => [{ 'x' => 2, 'y' => 3, 'z' => 4 }, { 'x' => 11, 'y' => 22, 'z' => 33 }], 'b' => { 'x' => 3, 'z' => 45 } },
|
68
|
+
'a' => [{ 'y' => 3 }, { 'x' => 11, 'z' => 33 }], 'b' => { 'y' => 22 })
|
69
|
+
diff.should == [['-', 'a[0].x', 2], ['-', 'a[0].z', 4], ['-', 'a[1].y', 22], ['-', 'b.x', 3], ['-', 'b.z', 45], ['+', 'b.y', 22]]
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'is able to diff addition of key value pair' do
|
73
|
+
a = { 'a' => 3, 'c' => 11, 'd' => 45, 'e' => 100, 'f' => 200 }
|
74
|
+
b = { 'a' => 3, 'c' => 11, 'd' => 45, 'e' => 100, 'f' => 200, 'g' => 300 }
|
75
|
+
|
76
|
+
diff = described_class.diff(a, b)
|
77
|
+
expect(diff).to eq([['+', 'g', 300]])
|
78
|
+
|
79
|
+
diff = described_class.diff(b, a)
|
80
|
+
diff.should == [['-', 'g', 300]]
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'is able to diff value type changes' do
|
84
|
+
a = { 'a' => 3 }
|
85
|
+
b = { 'a' => { 'a1' => 1, 'a2' => 2 } }
|
86
|
+
|
87
|
+
diff = described_class.diff(a, b)
|
88
|
+
expect(diff).to eq([['~', 'a', 3, { 'a1' => 1, 'a2' => 2 }]])
|
89
|
+
|
90
|
+
diff = described_class.diff(b, a)
|
91
|
+
diff.should == [['~', 'a', { 'a1' => 1, 'a2' => 2 }, 3]]
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'is able to diff value changes: array <=> []' do
|
95
|
+
a = { 'a' => 1, 'b' => [1, 2] }
|
96
|
+
b = { 'a' => 1, 'b' => [] }
|
97
|
+
|
98
|
+
diff = described_class.diff(a, b)
|
99
|
+
diff.should == [['-', 'b[1]', 2], ['-', 'b[0]', 1]]
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'is able to diff value changes: array <=> nil' do
|
103
|
+
a = { 'a' => 1, 'b' => [1, 2] }
|
104
|
+
b = { 'a' => 1, 'b' => nil }
|
105
|
+
|
106
|
+
diff = described_class.diff(a, b)
|
107
|
+
diff.should == [['~', 'b', [1, 2], nil]]
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'is able to diff value chagnes: remove array completely' do
|
111
|
+
a = { 'a' => 1, 'b' => [1, 2] }
|
112
|
+
b = { 'a' => 1 }
|
113
|
+
|
114
|
+
diff = described_class.diff(a, b)
|
115
|
+
diff.should == [['-', 'b', [1, 2]]]
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'is able to diff value changes: remove whole hash' do
|
119
|
+
a = { 'a' => 1, 'b' => { 'b1' => 1, 'b2' => 2 } }
|
120
|
+
b = { 'a' => 1 }
|
121
|
+
|
122
|
+
diff = described_class.diff(a, b)
|
123
|
+
diff.should == [['-', 'b', { 'b1' => 1, 'b2' => 2 }]]
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'is able to diff value changes: hash <=> {}' do
|
127
|
+
a = { 'a' => 1, 'b' => { 'b1' => 1, 'b2' => 2 } }
|
128
|
+
b = { 'a' => 1, 'b' => {} }
|
129
|
+
|
130
|
+
diff = described_class.diff(a, b)
|
131
|
+
diff.should == [['-', 'b.b1', 1], ['-', 'b.b2', 2]]
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'is able to diff value changes: hash <=> nil' do
|
135
|
+
a = { 'a' => 1, 'b' => { 'b1' => 1, 'b2' => 2 } }
|
136
|
+
b = { 'a' => 1, 'b' => nil }
|
137
|
+
|
138
|
+
diff = described_class.diff(a, b)
|
139
|
+
diff.should == [['~', 'b', { 'b1' => 1, 'b2' => 2 }, nil]]
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'is able to diff similar objects in array' do
|
143
|
+
a = [{ 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5 }, 3]
|
144
|
+
b = [1, { 'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5 }]
|
145
|
+
|
146
|
+
diff = described_class.diff(a, b)
|
147
|
+
diff.should == [['-', '[0].d', 4], ['+', '[0]', 1], ['-', '[2]', 3]]
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'is able to diff similar & equal objects in array' do
|
151
|
+
a = [{ 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5 }, { 'x' => 5, 'y' => 6, 'z' => 3 }, 3]
|
152
|
+
b = [{ 'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5 }, 3]
|
153
|
+
|
154
|
+
diff = described_class.diff(a, b)
|
155
|
+
diff.should == [['-', '[0].d', 4], ['-', '[1]', { 'x' => 5, 'y' => 6, 'z' => 3 }]]
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'uses custom delimiter when provided' do
|
159
|
+
a = [{ 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5 }, { 'x' => 5, 'y' => 6, 'z' => 3 }, 3]
|
160
|
+
b = [{ 'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5 }, 3]
|
161
|
+
|
162
|
+
diff = described_class.diff(a, b, similarity: 0.8, delimiter: "\t")
|
163
|
+
diff.should == [['-', "[0]\td", 4], ['-', '[1]', { 'x' => 5, 'y' => 6, 'z' => 3 }]]
|
164
|
+
end
|
165
|
+
|
166
|
+
context 'when :numeric_tolerance requested' do
|
167
|
+
it 'is able to diff changes in hash value' do
|
168
|
+
a = { 'a' => 0.558, 'b' => 0.0, 'c' => 0.65, 'd' => 'fin' }
|
169
|
+
b = { 'a' => 0.557, 'b' => 'hats', 'c' => 0.67, 'd' => 'fin' }
|
170
|
+
|
171
|
+
diff = described_class.diff(a, b, numeric_tolerance: 0.01)
|
172
|
+
expect(diff).to eq([['~', 'b', 0.0, 'hats'], ['~', 'c', 0.65, 0.67]])
|
173
|
+
|
174
|
+
diff = described_class.diff(b, a, numeric_tolerance: 0.01)
|
175
|
+
diff.should == [['~', 'b', 'hats', 0.0], ['~', 'c', 0.67, 0.65]]
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'is able to diff changes in nested values' do
|
179
|
+
a = { 'a' => { 'x' => 0.4, 'y' => 0.338 }, 'b' => [13, 68.03] }
|
180
|
+
b = { 'a' => { 'x' => 0.6, 'y' => 0.341 }, 'b' => [14, 68.025] }
|
181
|
+
|
182
|
+
diff = described_class.diff(a, b, numeric_tolerance: 0.01)
|
183
|
+
expect(diff).to eq([['~', 'a.x', 0.4, 0.6], ['-', 'b[0]', 13], ['+', 'b[0]', 14]])
|
184
|
+
|
185
|
+
diff = described_class.diff(b, a, numeric_tolerance: 0.01)
|
186
|
+
diff.should == [['~', 'a.x', 0.6, 0.4], ['-', 'b[0]', 14], ['+', 'b[0]', 13]]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
context 'when :strip requested' do
|
191
|
+
it 'strips strings before comparing' do
|
192
|
+
a = { 'a' => ' foo', 'b' => 'fizz buzz' }
|
193
|
+
b = { 'a' => 'foo', 'b' => 'fizzbuzz' }
|
194
|
+
diff = described_class.diff(a, b, strip: true)
|
195
|
+
diff.should == [['~', 'b', 'fizz buzz', 'fizzbuzz']]
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'strips nested strings before comparing' do
|
199
|
+
a = { 'a' => { 'x' => ' foo' }, 'b' => ['fizz buzz', 'nerf'] }
|
200
|
+
b = { 'a' => { 'x' => 'foo' }, 'b' => %w[fizzbuzz nerf] }
|
201
|
+
diff = described_class.diff(a, b, strip: true)
|
202
|
+
diff.should == [['-', 'b[0]', 'fizz buzz'], ['+', 'b[0]', 'fizzbuzz']]
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
context 'when :case_insensitive requested' do
|
207
|
+
it 'strips strings before comparing' do
|
208
|
+
a = { 'a' => 'Foo', 'b' => 'fizz buzz' }
|
209
|
+
b = { 'a' => 'foo', 'b' => 'fizzBuzz' }
|
210
|
+
diff = described_class.diff(a, b, case_insensitive: true)
|
211
|
+
diff.should == [['~', 'b', 'fizz buzz', 'fizzBuzz']]
|
212
|
+
end
|
213
|
+
|
214
|
+
it 'ignores case on nested strings before comparing' do
|
215
|
+
a = { 'a' => { 'x' => 'Foo' }, 'b' => ['fizz buzz', 'nerf'] }
|
216
|
+
b = { 'a' => { 'x' => 'foo' }, 'b' => %w[fizzbuzz nerf] }
|
217
|
+
diff = described_class.diff(a, b, case_insensitive: true)
|
218
|
+
diff.should == [['-', 'b[0]', 'fizz buzz'], ['+', 'b[0]', 'fizzbuzz']]
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
context 'when both :strip and :numeric_tolerance requested' do
|
223
|
+
it 'applies filters to proper object types' do
|
224
|
+
a = { 'a' => ' foo', 'b' => 35, 'c' => 'bar', 'd' => 'baz' }
|
225
|
+
b = { 'a' => 'foo', 'b' => 35.005, 'c' => 'bar', 'd' => 18.5 }
|
226
|
+
diff = described_class.diff(a, b, strict: false, numeric_tolerance: 0.01, strip: true)
|
227
|
+
diff.should == [['~', 'd', 'baz', 18.5]]
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
context 'when both :strip and :case_insensitive requested' do
|
232
|
+
it 'applies both filters to strings' do
|
233
|
+
a = { 'a' => ' Foo', 'b' => 'fizz buzz' }
|
234
|
+
b = { 'a' => 'foo', 'b' => 'fizzBuzz' }
|
235
|
+
diff = described_class.diff(a, b, case_insensitive: true, strip: true)
|
236
|
+
diff.should == [['~', 'b', 'fizz buzz', 'fizzBuzz']]
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
context 'with custom comparison' do
|
241
|
+
let(:a) { { 'a' => 'car', 'b' => 'boat', 'c' => 'plane' } }
|
242
|
+
let(:b) { { 'a' => 'bus', 'b' => 'truck', 'c' => ' plan' } }
|
243
|
+
|
244
|
+
it 'compares using proc specified in block' do
|
245
|
+
diff = described_class.diff(a, b) do |prefix, obj1, obj2|
|
246
|
+
case prefix
|
247
|
+
when /a|b|c/
|
248
|
+
obj1.length == obj2.length
|
249
|
+
end
|
250
|
+
end
|
251
|
+
diff.should == [['~', 'b', 'boat', 'truck']]
|
252
|
+
end
|
253
|
+
|
254
|
+
it 'yields added keys' do
|
255
|
+
x = { 'a' => 'car', 'b' => 'boat' }
|
256
|
+
y = { 'a' => 'car' }
|
257
|
+
|
258
|
+
diff = described_class.diff(x, y) do |prefix, _obj1, _obj2|
|
259
|
+
case prefix
|
260
|
+
when /b/
|
261
|
+
true
|
262
|
+
end
|
263
|
+
end
|
264
|
+
diff.should == []
|
265
|
+
end
|
266
|
+
|
267
|
+
it 'compares with both proc and :strip when both provided' do
|
268
|
+
diff = described_class.diff(a, b, strip: true) do |prefix, obj1, obj2|
|
269
|
+
case prefix
|
270
|
+
when 'a'
|
271
|
+
obj1.length == obj2.length
|
272
|
+
end
|
273
|
+
end
|
274
|
+
diff.should == [['~', 'b', 'boat', 'truck'], ['~', 'c', 'plane', ' plan']]
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
context 'when :array_path is true' do
|
279
|
+
it 'returns the diff path in an array rather than a string' do
|
280
|
+
x = { 'a' => 'foo' }
|
281
|
+
y = { 'a' => 'bar' }
|
282
|
+
diff = described_class.diff(x, y, array_path: true)
|
283
|
+
|
284
|
+
diff.should == [['~', ['a'], 'foo', 'bar']]
|
285
|
+
end
|
286
|
+
|
287
|
+
it 'shows array indexes in paths' do
|
288
|
+
x = { 'a' => [0, 1, 2] }
|
289
|
+
y = { 'a' => [0, 1, 2, 3] }
|
290
|
+
|
291
|
+
diff = described_class.diff(x, y, array_path: true)
|
292
|
+
|
293
|
+
diff.should == [['+', ['a', 3], 3]]
|
294
|
+
end
|
295
|
+
|
296
|
+
it 'shows differences with string and symbol keys' do
|
297
|
+
x = { 'a' => 'foo' }
|
298
|
+
y = { a: 'bar' }
|
299
|
+
|
300
|
+
diff = described_class.diff(x, y, array_path: true)
|
301
|
+
diff.should == [['-', ['a'], 'foo'], ['+', [:a], 'bar']]
|
302
|
+
end
|
303
|
+
|
304
|
+
it 'supports other key types' do
|
305
|
+
time = Time.now
|
306
|
+
x = { time => 'foo' }
|
307
|
+
y = { 0 => 'bar' }
|
308
|
+
|
309
|
+
diff = described_class.diff(x, y, array_path: true)
|
310
|
+
diff.should == [['-', [time], 'foo'], ['+', [0], 'bar']]
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
context 'when :use_lcs is false' do
|
315
|
+
it 'shows items in an array as changed' do
|
316
|
+
x = %i[a b]
|
317
|
+
y = %i[c d]
|
318
|
+
diff = described_class.diff(x, y, use_lcs: false)
|
319
|
+
|
320
|
+
diff.should == [['~', '[0]', :a, :c], ['~', '[1]', :b, :d]]
|
321
|
+
end
|
322
|
+
|
323
|
+
it 'shows additions to arrays' do
|
324
|
+
x = { a: [0] }
|
325
|
+
y = { a: [0, 1] }
|
326
|
+
diff = described_class.diff(x, y, use_lcs: false)
|
327
|
+
|
328
|
+
diff.should == [['+', 'a[1]', 1]]
|
329
|
+
end
|
330
|
+
|
331
|
+
it 'shows changes to nested arrays' do
|
332
|
+
x = { a: [[0, 1]] }
|
333
|
+
y = { a: [[1, 2]] }
|
334
|
+
diff = described_class.diff(x, y, use_lcs: false)
|
335
|
+
|
336
|
+
diff.should == [['~', 'a[0][0]', 0, 1], ['~', 'a[0][1]', 1, 2]]
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|