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.
@@ -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
@@ -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?(a, b, options = {})
7
- return compare_values(a, b, options) unless a.is_a?(Array) || a.is_a?(Hash) || b.is_a?(Array) || b.is_a?(Hash)
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
- count_a = count_nodes(a)
11
- count_b = count_nodes(b)
12
- diffs = count_diff diff(a, b, opts)
8
+ opts = { similarity: 0.8 }.merge(options)
13
9
 
14
- if count_a + count_b == 0
15
- return true
16
- else
17
- (1 - diffs.to_f/(count_a + count_b).to_f) >= opts[:similarity]
18
- end
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 += (old_change_count + new_change_count)
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.each {|k, v| count += count_nodes(v) }
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 $1.size > 0
61
- memo + [$1, $2.to_i]
58
+ if !Regexp.last_match(1).empty?
59
+ memo + [Regexp.last_match(1), Regexp.last_match(2).to_i]
62
60
  else
63
- memo + [$2.to_i]
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
- [obj1, obj2].all? { |v| v.is_a? Numeric }
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
- if method
120
- res = method.call(key, obj1, obj2)
121
-
122
- # nil != false here
123
- if res == false
124
- return [['~', key, obj1, obj2]]
125
- elsif res == true
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? ? "#{key}" : "#{prefix}#{opts[:delimiter]}#{key}"
131
+ prefix.empty? ? key.to_s : "#{prefix}#{opts[:delimiter]}#{key}"
136
132
  end
137
133
  end
138
134
 
@@ -1,3 +1,3 @@
1
1
  module HashDiff
2
- VERSION = '0.3.7'
2
+ VERSION = '0.3.8'.freeze
3
3
  end
@@ -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