hashdiff 0.3.5 → 0.3.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4ba86448df8842fe6323572feb2c89110954b775
4
- data.tar.gz: e3d610f32c07e65e06f825e107371caaaf2e5896
3
+ metadata.gz: aa9fedd172b3d7020ea1078bc10fc290bf77381c
4
+ data.tar.gz: cf7ad118f84f90035283165ce4bc8c473f642ccc
5
5
  SHA512:
6
- metadata.gz: 7e7b28d277160a603df99c83022f6b8bd7bb27ac5d467e351e5834ba955195ed493249b7a7e213dafe0cb1aa630c5de46fe11a95b5942de4ba9c1f4e66262ec3
7
- data.tar.gz: d5290cbc0b5c97038cce213e627fc20fcaf4e7405888c217c78b949c8fc98990ab6d3ae18b0f91d2909c069ffccc20a9d0d45aa572419252aa6eae960df6491d
6
+ metadata.gz: 970a50d8a7cd540d7bb8bd91736f2030fb48b2bacfe505621fc0e9fda93e5835bab16fcc0e40310c5b62774121bd80b279c52b97a2af024e46b63f8b1bb02114
7
+ data.tar.gz: 96de7b52391861d461a663ce2a7e12220558a65e6f6a001a68a54763a336bc5795fa5307ed3b41e84026060a5c80681b6914538a60112a305d100636589d2998
data/Gemfile CHANGED
@@ -3,5 +3,4 @@ gemspec
3
3
 
4
4
  group :test do
5
5
  gem 'rake', '< 11'
6
- gem 'codecov'
7
6
  end
@@ -1,5 +1,9 @@
1
1
  # Change Log
2
2
 
3
+ ## v0.3.6 2017-08-22
4
+
5
+ * add option `use_lcs` #35
6
+
3
7
  ## v0.3.5 2017-08-06
4
8
 
5
9
  * add option `array_path` #34
@@ -1,5 +1,6 @@
1
1
  require 'hashdiff/util'
2
2
  require 'hashdiff/lcs'
3
+ require 'hashdiff/linear_compare_array'
3
4
  require 'hashdiff/diff'
4
5
  require 'hashdiff/patch'
5
6
  require 'hashdiff/version'
@@ -77,7 +77,8 @@ module HashDiff
77
77
  :strict => true,
78
78
  :strip => false,
79
79
  :numeric_tolerance => 0,
80
- :array_path => false
80
+ :array_path => false,
81
+ :use_lcs => true
81
82
  }.merge!(options)
82
83
 
83
84
  opts[:prefix] = [] if opts[:array_path] && opts[:prefix] == ''
@@ -105,8 +106,8 @@ module HashDiff
105
106
  end
106
107
 
107
108
  result = []
108
- if obj1.is_a?(Array)
109
- changeset = diff_array(obj1, obj2, opts) do |lcs|
109
+ if obj1.is_a?(Array) && opts[:use_lcs]
110
+ changeset = diff_array_lcs(obj1, obj2, opts) do |lcs|
110
111
  # use a's index for similarity
111
112
  lcs.each do |pair|
112
113
  prefix = prefix_append_array_index(opts[:prefix], pair[0], opts)
@@ -122,6 +123,8 @@ module HashDiff
122
123
  result << ['+', change_key, change[2]]
123
124
  end
124
125
  end
126
+ elsif obj1.is_a?(Array) && !opts[:use_lcs]
127
+ result.concat(LinearCompareArray.call(obj1, obj2, opts))
125
128
  elsif obj1.is_a?(Hash)
126
129
 
127
130
  deleted_keys = obj1.keys - obj2.keys
@@ -170,7 +173,7 @@ module HashDiff
170
173
  # @private
171
174
  #
172
175
  # diff array using LCS algorithm
173
- def self.diff_array(a, b, options = {})
176
+ def self.diff_array_lcs(a, b, options = {})
174
177
  opts = {
175
178
  :prefix => '',
176
179
  :similarity => 0.8,
@@ -223,5 +226,4 @@ module HashDiff
223
226
 
224
227
  change_set
225
228
  end
226
-
227
229
  end
@@ -0,0 +1,155 @@
1
+ module HashDiff
2
+ # @private
3
+ #
4
+ # Used to compare arrays in a linear complexity, which produces longer diffs
5
+ # than using the lcs algorithm but is considerably faster
6
+ class LinearCompareArray
7
+ def self.call(old_array, new_array, options = {})
8
+ instance = self.new(old_array, new_array, options)
9
+ instance.call
10
+ end
11
+
12
+ def call
13
+ return [] if old_array.empty? && new_array.empty?
14
+
15
+ self.old_index = 0
16
+ self.new_index = 0
17
+ # by comparing the array lengths we can expect that a number of items
18
+ # are either added or removed
19
+ self.expected_additions = new_array.length - old_array.length
20
+
21
+ loop do
22
+ if extra_items_in_old_array?
23
+ append_deletion(old_array[old_index], old_index)
24
+ elsif extra_items_in_new_array?
25
+ append_addition(new_array[new_index], new_index)
26
+ else
27
+ compare_at_index
28
+ end
29
+
30
+ self.old_index = old_index + 1
31
+ self.new_index = new_index + 1
32
+ break if iterated_through_both_arrays?
33
+ end
34
+
35
+ changes
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :old_array, :new_array, :options, :additions, :deletions, :differences
41
+ attr_accessor :old_index, :new_index, :expected_additions
42
+
43
+ def initialize(old_array, new_array, options)
44
+ @old_array = old_array
45
+ @new_array = new_array
46
+ @options = { prefix: '' }.merge!(options)
47
+
48
+ @additions = []
49
+ @deletions = []
50
+ @differences = []
51
+ end
52
+
53
+ def extra_items_in_old_array?
54
+ old_index < old_array.length && new_index >= new_array.length
55
+ end
56
+
57
+ def extra_items_in_new_array?
58
+ new_index < new_array.length && old_index >= old_array.length
59
+ end
60
+
61
+ def iterated_through_both_arrays?
62
+ old_index >= old_array.length && new_index >= new_array.length
63
+ end
64
+
65
+ def compare_at_index
66
+ difference = item_difference(old_array[old_index], new_array[new_index], old_index)
67
+ return if difference.empty?
68
+
69
+ index_after_additions = index_of_match_after_additions
70
+ append_addititions_before_match(index_after_additions)
71
+
72
+ index_after_deletions = index_of_match_after_deletions
73
+ append_deletions_before_match(index_after_deletions)
74
+
75
+ match = index_after_additions || index_after_deletions
76
+
77
+ append_differences(difference) unless match
78
+ end
79
+
80
+ def item_difference(old_item, new_item, item_index)
81
+ prefix = HashDiff.prefix_append_array_index(options[:prefix], item_index, options)
82
+ HashDiff.diff(old_item, new_item, options.merge(:prefix => prefix))
83
+ end
84
+
85
+ # look ahead in the new array to see if the current item appears later
86
+ # thereby having new items added
87
+ def index_of_match_after_additions
88
+ return unless expected_additions > 0
89
+
90
+ (1..expected_additions).each do |i|
91
+ next_difference = item_difference(
92
+ old_array[old_index],
93
+ new_array[new_index + i],
94
+ old_index
95
+ )
96
+
97
+ return new_index + i if next_difference.empty?
98
+ end
99
+
100
+ nil
101
+ end
102
+
103
+ # look ahead in the old array to see if the current item appears later
104
+ # thereby having items removed
105
+ def index_of_match_after_deletions
106
+ return unless expected_additions < 0
107
+
108
+ (1..(expected_additions.abs)).each do |i|
109
+ next_difference = item_difference(
110
+ old_array[old_index + i],
111
+ new_array[new_index],
112
+ old_index
113
+ )
114
+
115
+ return old_index + i if next_difference.empty?
116
+ end
117
+
118
+ nil
119
+ end
120
+
121
+ def append_addititions_before_match(match_index)
122
+ return unless match_index
123
+ (new_index...match_index).each { |i| append_addition(new_array[i], i) }
124
+ self.expected_additions = expected_additions - (match_index - new_index)
125
+ self.new_index = match_index
126
+ end
127
+
128
+ def append_deletions_before_match(match_index)
129
+ return unless match_index
130
+ (old_index...match_index).each { |i| append_deletion(old_array[i], i) }
131
+ self.expected_additions = expected_additions + (match_index - new_index)
132
+ self.old_index = match_index
133
+ end
134
+
135
+ def append_addition(item, index)
136
+ key = HashDiff.prefix_append_array_index(options[:prefix], index, options)
137
+ additions << ['+', key, item]
138
+ end
139
+
140
+ def append_deletion(item, index)
141
+ key = HashDiff.prefix_append_array_index(options[:prefix], index, options)
142
+ deletions << ['-', key, item]
143
+ end
144
+
145
+ def append_differences(difference)
146
+ differences.concat(difference)
147
+ end
148
+
149
+ def changes
150
+ # this algorithm only allows there to be additions or deletions
151
+ # deletions are reverse so they don't change the index of earlier items
152
+ differences + additions + deletions.reverse
153
+ end
154
+ end
155
+ end
@@ -1,3 +1,3 @@
1
1
  module HashDiff
2
- VERSION = '0.3.5'
2
+ VERSION = '0.3.6'
3
3
  end
@@ -5,7 +5,7 @@ describe HashDiff do
5
5
  a = [1, 2, 3]
6
6
  b = [1, 2, 3]
7
7
 
8
- diff = HashDiff.diff_array(a, b)
8
+ diff = HashDiff.diff_array_lcs(a, b)
9
9
  diff.should == []
10
10
  end
11
11
 
@@ -13,7 +13,7 @@ describe HashDiff do
13
13
  a = [1, 2, 3]
14
14
  b = [1, 8, 7]
15
15
 
16
- diff = HashDiff.diff_array(a, b)
16
+ diff = HashDiff.diff_array_lcs(a, b)
17
17
  diff.should == [['-', 2, 3], ['-', 1, 2], ['+', 1, 8], ['+', 2, 7]]
18
18
  end
19
19
 
@@ -21,7 +21,7 @@ describe HashDiff do
21
21
  a = [1, 2]
22
22
  b = []
23
23
 
24
- diff = HashDiff.diff_array(a, b)
24
+ diff = HashDiff.diff_array_lcs(a, b)
25
25
  diff.should == [['-', 1, 2], ['-', 0, 1]]
26
26
  end
27
27
 
@@ -29,7 +29,7 @@ describe HashDiff do
29
29
  a = []
30
30
  b = [1, 2]
31
31
 
32
- diff = HashDiff.diff_array(a, b)
32
+ diff = HashDiff.diff_array_lcs(a, b)
33
33
  diff.should == [['+', 0, 1], ['+', 1, 2]]
34
34
  end
35
35
 
@@ -37,7 +37,7 @@ describe HashDiff do
37
37
  a = [1, 3, 5, 7]
38
38
  b = [2, 3, 7, 5]
39
39
 
40
- diff = HashDiff.diff_array(a, b)
40
+ diff = HashDiff.diff_array_lcs(a, b)
41
41
  diff.should == [['-', 0, 1], ['+', 0, 2], ['+', 2, 7], ['-', 4, 7]]
42
42
  end
43
43
 
@@ -45,14 +45,14 @@ describe HashDiff do
45
45
  a = [1, 3, 4, 7]
46
46
  b = [2, 3, 7, 5]
47
47
 
48
- diff = HashDiff.diff_array(a, b)
48
+ diff = HashDiff.diff_array_lcs(a, b)
49
49
  diff.should == [['-', 0, 1], ['+', 0, 2], ['-', 2, 4], ['+', 3, 5]]
50
50
  end
51
51
 
52
52
  it "should be able to diff two arrays with similar elements" do
53
53
  a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, 3]
54
54
  b = [1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}]
55
- diff = HashDiff.diff_array(a, b)
55
+ diff = HashDiff.diff_array_lcs(a, b)
56
56
  diff.should == [['+', 0, 1], ['-', 2, 3]]
57
57
  end
58
58
 
@@ -310,4 +310,30 @@ describe HashDiff do
310
310
  diff.should == [['-', [time], 'foo'], ['+', [0], 'bar']]
311
311
  end
312
312
  end
313
+
314
+ context 'when :use_lcs is false' do
315
+ it 'should show items in an array as changed' do
316
+ x = [:a, :b]
317
+ y = [:c, :d]
318
+ diff = HashDiff.diff(x, y, :use_lcs => false)
319
+
320
+ diff.should == [['~', '[0]', :a, :c], ['~', '[1]', :b, :d]]
321
+ end
322
+
323
+ it 'should show additions to arrays' do
324
+ x = { :a => [0] }
325
+ y = { :a => [0, 1] }
326
+ diff = HashDiff.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 = HashDiff.diff(x, y, :use_lcs => false)
335
+
336
+ diff.should == [['~', 'a[0][0]', 0, 1], ['~', 'a[0][1]', 1, 2]]
337
+ end
338
+ end
313
339
  end
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ describe HashDiff::LinearCompareArray do
4
+ it "should find no differences between two empty arrays" do
5
+ difference = described_class.call([], [])
6
+ difference.should == []
7
+ end
8
+
9
+ it "should find added items when the old array is empty" do
10
+ difference = described_class.call([], [:a, :b])
11
+ difference.should == [['+', '[0]', :a], ['+', '[1]', :b]]
12
+ end
13
+
14
+ it "should find removed items when the new array is empty" do
15
+ difference = described_class.call([:a, :b], [])
16
+ difference.should == [['-', '[1]', :b], ['-', '[0]', :a]]
17
+ end
18
+
19
+ it "should find no differences between identical arrays" do
20
+ difference = described_class.call([:a, :b], [:a, :b])
21
+ difference.should == []
22
+ end
23
+
24
+ it "should find added items in an array" do
25
+ difference = described_class.call([:a, :d], [:a, :b, :c, :d])
26
+ difference.should == [['+', '[1]', :b], ['+', '[2]', :c]]
27
+ end
28
+
29
+ it "should find removed items in an array" do
30
+ difference = described_class.call([:a, :b, :c, :d, :e, :f], [:a, :d, :f])
31
+ difference.should == [['-', '[4]', :e], ['-', '[2]', :c], ['-', '[1]', :b]]
32
+ end
33
+
34
+ it "should show additions and deletions as changed items" do
35
+ difference = described_class.call([:a, :b, :c], [:c, :b, :a])
36
+ difference.should == [['~', '[0]', :a, :c], ['~', '[2]', :c, :a]]
37
+ end
38
+
39
+ it "should show changed items in a hash" do
40
+ difference = described_class.call([{ :a => :b }], [{ :a => :c }])
41
+ difference.should == [['~', '[0].a', :b, :c]]
42
+ end
43
+
44
+ it "should show changed items and added items" do
45
+ difference = described_class.call([{ :a => 1, :b => 2 }], [{ :a => 2, :b => 2 }, :item])
46
+ difference.should == [['~', '[0].a', 1, 2], ['+', '[1]', :item]]
47
+ end
48
+ end
@@ -1,10 +1,3 @@
1
- require 'simplecov'
2
- SimpleCov.start
3
- if ENV['CI'] == 'true'
4
- require 'codecov'
5
- SimpleCov.formatter = SimpleCov::Formatter::Codecov
6
- end
7
-
8
1
  $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
9
2
 
10
3
  require 'rubygems'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hashdiff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Liu Fengyun
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-08-06 00:00:00.000000000 Z
11
+ date: 2017-08-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -73,6 +73,7 @@ files:
73
73
  - lib/hashdiff.rb
74
74
  - lib/hashdiff/diff.rb
75
75
  - lib/hashdiff/lcs.rb
76
+ - lib/hashdiff/linear_compare_array.rb
76
77
  - lib/hashdiff/patch.rb
77
78
  - lib/hashdiff/util.rb
78
79
  - lib/hashdiff/version.rb
@@ -80,6 +81,7 @@ files:
80
81
  - spec/hashdiff/diff_array_spec.rb
81
82
  - spec/hashdiff/diff_spec.rb
82
83
  - spec/hashdiff/lcs_spec.rb
84
+ - spec/hashdiff/linear_compare_array_spec.rb
83
85
  - spec/hashdiff/patch_spec.rb
84
86
  - spec/hashdiff/util_spec.rb
85
87
  - spec/spec_helper.rb