hashdiff 0.3.4 → 0.4.0
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 +24 -0
- data/.travis.yml +6 -5
- data/Gemfile +3 -1
- data/README.md +94 -29
- data/Rakefile +9 -4
- data/changelog.md +28 -3
- data/hashdiff.gemspec +27 -12
- data/lib/hashdiff/compare_hashes.rb +56 -0
- data/lib/hashdiff/diff.rb +65 -111
- data/lib/hashdiff/lcs.rb +27 -30
- data/lib/hashdiff/lcs_compare_arrays.rb +32 -0
- data/lib/hashdiff/linear_compare_array.rb +159 -0
- data/lib/hashdiff/patch.rb +16 -12
- data/lib/hashdiff/util.rb +59 -37
- data/lib/hashdiff/version.rb +4 -2
- data/lib/hashdiff.rb +9 -0
- data/spec/hashdiff/best_diff_spec.rb +47 -37
- data/spec/hashdiff/diff_array_spec.rb +19 -19
- data/spec/hashdiff/diff_spec.rb +217 -139
- data/spec/hashdiff/lcs_spec.rb +27 -26
- data/spec/hashdiff/linear_compare_array_spec.rb +50 -0
- data/spec/hashdiff/patch_spec.rb +129 -105
- data/spec/hashdiff/util_spec.rb +60 -43
- data/spec/spec_helper.rb +2 -0
- metadata +50 -11
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.to_f / (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,20 +55,18 @@ 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
|
-
|
58
|
+
def self.decode_property_path(path, delimiter = '.')
|
59
|
+
path.split(delimiter).inject([]) do |memo, part|
|
59
60
|
if part =~ /^(.*)\[(\d+)\]$/
|
60
|
-
if
|
61
|
-
[
|
61
|
+
if !Regexp.last_match(1).empty?
|
62
|
+
memo + [Regexp.last_match(1), Regexp.last_match(2).to_i]
|
62
63
|
else
|
63
|
-
|
64
|
+
memo + [Regexp.last_match(2).to_i]
|
64
65
|
end
|
65
66
|
else
|
66
|
-
part
|
67
|
+
memo + [part]
|
67
68
|
end
|
68
69
|
end
|
69
|
-
|
70
|
-
parts.flatten
|
71
70
|
end
|
72
71
|
|
73
72
|
# @private
|
@@ -85,8 +84,8 @@ module HashDiff
|
|
85
84
|
#
|
86
85
|
# check for equality or "closeness" within given tolerance
|
87
86
|
def self.compare_values(obj1, obj2, options = {})
|
88
|
-
if
|
89
|
-
|
87
|
+
if options[:numeric_tolerance].is_a?(Numeric) &&
|
88
|
+
obj1.is_a?(Numeric) && obj2.is_a?(Numeric)
|
90
89
|
return (obj1 - obj2).abs <= options[:numeric_tolerance]
|
91
90
|
end
|
92
91
|
|
@@ -107,10 +106,9 @@ module HashDiff
|
|
107
106
|
#
|
108
107
|
# check if objects are comparable
|
109
108
|
def self.comparable?(obj1, obj2, strict = true)
|
110
|
-
|
111
|
-
return true if obj1.is_a?(type) && obj2.is_a?(type)
|
112
|
-
end
|
109
|
+
return true if (obj1.is_a?(Array) || obj1.is_a?(Hash)) && obj2.is_a?(obj1.class)
|
113
110
|
return true if !strict && obj1.is_a?(Numeric) && obj2.is_a?(Numeric)
|
111
|
+
|
114
112
|
obj1.is_a?(obj2.class) && obj2.is_a?(obj1.class)
|
115
113
|
end
|
116
114
|
|
@@ -118,15 +116,39 @@ module HashDiff
|
|
118
116
|
#
|
119
117
|
# try custom comparison
|
120
118
|
def self.custom_compare(method, key, obj1, obj2)
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
119
|
+
return unless method
|
120
|
+
|
121
|
+
res = method.call(key, obj1, obj2)
|
122
|
+
|
123
|
+
# nil != false here
|
124
|
+
return [['~', key, obj1, obj2]] if res == false
|
125
|
+
return [] if res == true
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.prefix_append_key(prefix, key, opts)
|
129
|
+
if opts[:array_path]
|
130
|
+
prefix + [key]
|
131
|
+
else
|
132
|
+
prefix.empty? ? key.to_s : "#{prefix}#{opts[:delimiter]}#{key}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.prefix_append_array_index(prefix, array_index, opts)
|
137
|
+
if opts[:array_path]
|
138
|
+
prefix + [array_index]
|
139
|
+
else
|
140
|
+
"#{prefix}[#{array_index}]"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class << self
|
145
|
+
private
|
146
|
+
|
147
|
+
# @private
|
148
|
+
#
|
149
|
+
# checks if both objects are Arrays or Hashes
|
150
|
+
def any_hash_or_array?(obja, objb)
|
151
|
+
obja.is_a?(Array) || obja.is_a?(Hash) || objb.is_a?(Array) || objb.is_a?(Hash)
|
130
152
|
end
|
131
153
|
end
|
132
154
|
end
|
data/lib/hashdiff/version.rb
CHANGED
data/lib/hashdiff.rb
CHANGED
@@ -1,5 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'hashdiff/util'
|
4
|
+
require 'hashdiff/compare_hashes'
|
2
5
|
require 'hashdiff/lcs'
|
6
|
+
require 'hashdiff/lcs_compare_arrays'
|
7
|
+
require 'hashdiff/linear_compare_array'
|
3
8
|
require 'hashdiff/diff'
|
4
9
|
require 'hashdiff/patch'
|
5
10
|
require 'hashdiff/version'
|
11
|
+
|
12
|
+
HashDiff = Hashdiff
|
13
|
+
|
14
|
+
warn 'The HashDiff constant used by this gem conflicts with another gem of a similar name. As of version 1.0 the HashDiff constant will be completely removed and replaced by Hashdiff. For more information see https://github.com/liufengyun/hashdiff/issues/45.'
|
@@ -1,65 +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
|
67
|
+
|
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 }] }
|
71
|
+
|
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 }]]
|
74
|
+
end
|
65
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
|
-
|