hashdiff 0.3.7 → 1.0.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 +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
|