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