hashdiff 0.0.2

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.
@@ -0,0 +1,13 @@
1
+ # See http://help.github.com/ignore-files/ for more about ignoring files.
2
+ #
3
+ # If you find yourself ignoring temporary files generated by your text editor
4
+ # or operating system, you probably want to add a global ignore instead:
5
+ # git config --global core.excludesfile ~/.gitignore_global
6
+
7
+ # Ignore bundler config
8
+ /.bundle
9
+ /doc
10
+ /.yardoc
11
+
12
+ *.swp
13
+ *.bak
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,8 @@
1
+ rvm:
2
+ - 1.8.7
3
+ - 1.9.2
4
+ - rbx
5
+ - rbx-2.0
6
+ - ree
7
+ - jruby
8
+ - ruby-head
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
@@ -0,0 +1,28 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ hashdiff (0.0.1)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ bluecloth (2.2.0)
10
+ diff-lcs (1.1.3)
11
+ rspec (2.10.0)
12
+ rspec-core (~> 2.10.0)
13
+ rspec-expectations (~> 2.10.0)
14
+ rspec-mocks (~> 2.10.0)
15
+ rspec-core (2.10.1)
16
+ rspec-expectations (2.10.0)
17
+ diff-lcs (~> 1.1.3)
18
+ rspec-mocks (2.10.1)
19
+ yard (0.8.1)
20
+
21
+ PLATFORMS
22
+ ruby
23
+
24
+ DEPENDENCIES
25
+ bluecloth
26
+ hashdiff!
27
+ rspec (~> 2.0)
28
+ yard
@@ -0,0 +1,70 @@
1
+ HashDiff
2
+ =========
3
+
4
+ HashDiff is a ruby library to compute the smallest difference between two hashes.
5
+
6
+ Requirements
7
+ ------------
8
+ HashDiff is tested on following platforms:
9
+
10
+ - 1.8.7
11
+ - 1.9.2
12
+ - rbx
13
+ - rbx-2.0
14
+ - ree
15
+ - jruby
16
+ - ruby-head
17
+
18
+ Quick Start
19
+ -----------
20
+
21
+ ### Diff
22
+
23
+ Two simple hash:
24
+
25
+ a = {a:3, b:2}
26
+ b = {}
27
+
28
+ diff = HashDiff.diff(a, b)
29
+ diff.should == [['-', 'a', 3], ['-', 'b', 2]]
30
+
31
+ More complex hash:
32
+
33
+ a = {a:{x:2, y:3, z:4}, b:{x:3, z:45}}
34
+ b = {a:{y:3}, b:{y:3, z:30}}
35
+
36
+ diff = HashDiff.diff(a, b)
37
+ diff.should == [['-', 'a.x', 2], ['-', 'a.z', 4], ['-', 'b.x', 3], ['~', 'b.z', 45, 30], ['+', 'b.y', 3]]
38
+
39
+ Array in hash:
40
+
41
+ a = {a:[{x:2, y:3, z:4}, {x:11, y:22, z:33}], b:{x:3, z:45}}
42
+ b = {a:[{y:3}, {x:11, z:33}], b:{y:22}}
43
+
44
+ diff = HashDiff.best_diff(a, b) # best_diff will try to match similar objects in array in order to generate the smallest change set
45
+ diff.should == [['-', 'a[0].x', 2], ['-', 'a[0].z', 4], ['-', 'a[1].y', 22], ['-', 'b.x', 3], ['-', 'b.z', 45], ['+', 'b.y', 22]]
46
+
47
+ ### Patch
48
+
49
+ patch example:
50
+
51
+ a = {a: 3}
52
+ b = {a: {a1: 1, a2: 2}}
53
+
54
+ diff = HashDiff.diff(a, b)
55
+ HashDiff.patch(a, diff).should == b
56
+
57
+ unpatch example:
58
+
59
+ a = [{a: 1, b: 2, c: 3, d: 4, e: 5}, {x: 5, y: 6, z: 3}, 1]
60
+ b = [1, {a: 1, b: 2, c: 3, e: 5}]
61
+
62
+ diff = HashDiff.diff(a, b) # diff two array is OK
63
+ HashDiff.unpatch(b, diff).should == a
64
+
65
+
66
+ License
67
+ -------
68
+
69
+ HashDiff is distributed under the MIT-LICENSE.
70
+
@@ -0,0 +1,13 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+
3
+ require 'bundler'
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ require 'rspec/core/rake_task'
7
+
8
+ task :default => :spec
9
+
10
+ RSpec::Core::RakeTask.new(:spec) do |spec|
11
+ spec.pattern = "./spec/**/*_spec.rb"
12
+ end
13
+
@@ -0,0 +1,24 @@
1
+ $LOAD_PATH << File.expand_path("../lib", __FILE__)
2
+ require 'hashdiff/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = %q{hashdiff}
6
+ s.version = HashDiff::VERSION
7
+ s.summary = %q{ HashDiff is a diff lib to compute the smallest difference between two hashes. }
8
+ s.description = %q{ HashDiff is a diff lib to compute the smallest difference between two hashes. }
9
+
10
+ s.files = `git ls-files`.split("\n")
11
+ s.test_files = `git ls-files -- Appraisals {spec}/*`.split("\n")
12
+
13
+ s.require_paths = ['lib']
14
+ s.required_ruby_version = Gem::Requirement.new(">= 1.8.7")
15
+
16
+ s.authors = ["Liu Fengyun"]
17
+ s.email = ["liufengyunchina@gmail.com"]
18
+
19
+ s.homepage = "https://github.com/liufengyun/hashdiff"
20
+
21
+ s.add_development_dependency("rspec", "~> 2.0")
22
+ s.add_development_dependency("yard")
23
+ s.add_development_dependency("bluecloth")
24
+ end
@@ -0,0 +1,5 @@
1
+ require 'hashdiff/util'
2
+ require 'hashdiff/lcs'
3
+ require 'hashdiff/diff'
4
+ require 'hashdiff/patch'
5
+ require 'hashdiff/version'
@@ -0,0 +1,142 @@
1
+ module HashDiff
2
+
3
+ # try to make the best diff that generates the smallest change set
4
+ def self.best_diff(obj1, obj2)
5
+ diffs_1 = diff(obj1, obj2, "", 0.3)
6
+ diffs_2 = diff(obj1, obj2, "", 0.5)
7
+ diffs_3 = diff(obj1, obj2, "", 0.8)
8
+
9
+ diffs = diffs_1.size < diffs_2.size ? diffs_1 : diffs_2
10
+ diffs = diffs.size < diffs_3.size ? diffs : diffs_3
11
+ end
12
+
13
+ # compute the diff of two hashes, return an array of changes
14
+ # e.g. [[ '+', 'a.b', '45' ], [ '-', 'a.c', '5' ], [ '~', 'a.x', '45', '63']]
15
+ #
16
+ # NOTE: diff will treat nil as [], {} or "" in comparison according to different context.
17
+ #
18
+ def self.diff(obj1, obj2, prefix = "", similarity = 0.8)
19
+ if obj1.nil?
20
+ if obj2.is_a?(Array)
21
+ return diff([], obj2, prefix, similarity)
22
+ elsif obj2.is_a?(Hash)
23
+ return diff({}, obj2, prefix, similarity)
24
+ else
25
+ return diff('', obj2, prefix, similarity)
26
+ end
27
+ end
28
+
29
+ if obj2.nil?
30
+ if obj1.is_a?(Array)
31
+ return diff(obj1, [], prefix, similarity)
32
+ elsif obj1.is_a?(Hash)
33
+ return diff(obj1, {}, prefix, similarity)
34
+ else
35
+ return diff(obj1, '', prefix, similarity)
36
+ end
37
+ end
38
+
39
+ if !(obj1.is_a?(Array) and obj2.is_a?(Array)) and !(obj1.is_a?(Hash) and obj2.is_a?(Hash)) and !(obj1.is_a?(obj2.class) or obj2.is_a?(obj1.class))
40
+ return changed(obj1, '-', prefix) + changed(obj2, '+', prefix)
41
+ end
42
+
43
+ result = []
44
+ if obj1.is_a?(Array)
45
+ changeset = diff_array(obj1, obj2, similarity) do |lcs|
46
+ # use a's index for similarity
47
+ lcs.each do |pair|
48
+ result.concat(diff(obj1[pair[0]], obj2[pair[1]], "#{prefix}[#{pair[0]}]", similarity))
49
+ end
50
+ end
51
+
52
+ changeset.each do |change|
53
+ if change[0] == '-'
54
+ result.concat(changed(change[2], '-', "#{prefix}[#{change[1]}]"))
55
+ elsif change[0] == '+'
56
+ result.concat(changed(change[2], '+', "#{prefix}[#{change[1]}]"))
57
+ end
58
+ end
59
+ elsif obj1.is_a?(Hash)
60
+ prefix = prefix.empty? ? "" : "#{prefix}."
61
+
62
+ deleted_keys = []
63
+ common_keys = []
64
+
65
+ obj1.each do |k, v|
66
+ if obj2.key?(k)
67
+ common_keys << k
68
+ else
69
+ deleted_keys << k
70
+ end
71
+ end
72
+
73
+ # add deleted properties
74
+ deleted_keys.each {|k| result.concat(changed(obj1[k], '-', "#{prefix}#{k}")) }
75
+
76
+ # recursive comparison for common keys
77
+ common_keys.each {|k| result.concat(diff(obj1[k], obj2[k], "#{prefix}#{k}", similarity)) }
78
+
79
+ # added properties
80
+ obj2.each do |k, v|
81
+ unless obj1.key?(k)
82
+ result.concat(changed(obj2[k], '+', "#{prefix}#{k}"))
83
+ end
84
+ end
85
+ else
86
+ return [] if obj1 == obj2
87
+ return [['~', prefix, obj1, obj2]]
88
+ end
89
+
90
+ result
91
+ end
92
+
93
+ # diff array using LCS algorithm
94
+ def self.diff_array(a, b, similarity = 0.8)
95
+ change_set = []
96
+ if a.size == 0 and b.size == 0
97
+ return []
98
+ elsif a.size == 0
99
+ b.each_index do |index|
100
+ change_set << ['+', index, b[index]]
101
+ end
102
+ return change_set
103
+ elsif b.size == 0
104
+ a.each_index do |index|
105
+ i = a.size - index - 1
106
+ change_set << ['-', i, a[i]]
107
+ end
108
+ return change_set
109
+ end
110
+
111
+ links = lcs(a, b, similarity)
112
+
113
+ # yield common
114
+ yield links if block_given?
115
+
116
+ # padding the end
117
+ links << [a.size, b.size]
118
+
119
+ last_x = -1
120
+ last_y = -1
121
+ links.each do |pair|
122
+ x, y = pair
123
+
124
+ # remove from a, beginning from the end
125
+ (x > last_x + 1) and (x - last_x - 2).downto(0).each do |i|
126
+ change_set << ['-', last_y + i + 1, a[i + last_x + 1]]
127
+ end
128
+
129
+ # add from b, beginning from the head
130
+ (y > last_y + 1) and 0.upto(y - last_y - 2).each do |i|
131
+ change_set << ['+', last_y + i + 1, b[i + last_y + 1]]
132
+ end
133
+
134
+ # update flags
135
+ last_x = x
136
+ last_y = y
137
+ end
138
+
139
+ change_set
140
+ end
141
+
142
+ end
@@ -0,0 +1,64 @@
1
+ module HashDiff
2
+
3
+ # caculate array difference using LCS algorithm
4
+ # http://en.wikipedia.org/wiki/Longest_common_subsequence_problem
5
+ def self.lcs(a, b, similarity = 0.8)
6
+ return [] if a.size == 0 or b.size == 0
7
+
8
+ a_start = b_start = 0
9
+ a_finish = a.size - 1
10
+ b_finish = b.size - 1
11
+ vector = []
12
+
13
+ lcs = []
14
+ (0..b_finish).each do |bi|
15
+ lcs[bi] = []
16
+ (0..a_finish).each do |ai|
17
+ if similiar?(a[ai], b[bi], similarity)
18
+ topleft = (ai > 0 and bi > 0)? lcs[bi-1][ai-1][1] : 0
19
+ lcs[bi][ai] = [:topleft, topleft + 1]
20
+ elsif
21
+ top = (bi > 0)? lcs[bi-1][ai][1] : 0
22
+ left = (ai > 0)? lcs[bi][ai-1][1] : 0
23
+ count = (top > left) ? top : left
24
+
25
+ direction = :both
26
+ if top > left
27
+ direction = :top
28
+ elsif top < left
29
+ direction = :left
30
+ else
31
+ if bi == 0
32
+ direction = :top
33
+ elsif ai == 0
34
+ direction = :left
35
+ else
36
+ direction = :both
37
+ end
38
+ end
39
+
40
+ lcs[bi][ai] = [direction, count]
41
+ end
42
+ end
43
+ end
44
+
45
+ x = a_finish
46
+ y = b_finish
47
+ while x >= 0 and y >= 0 and lcs[y][x][1] > 0
48
+ if lcs[y][x][0] == :both
49
+ x -= 1
50
+ elsif lcs[y][x][0] == :topleft
51
+ vector.insert(0, [x, y])
52
+ x -= 1
53
+ y -= 1
54
+ elsif lcs[y][x][0] == :top
55
+ y -= 1
56
+ elsif lcs[y][x][0] == :left
57
+ x -= 1
58
+ end
59
+ end
60
+
61
+ vector
62
+ end
63
+
64
+ end
@@ -0,0 +1,86 @@
1
+ #
2
+ # This class provides methods to diff two hash, patch and unpatch hash
3
+ #
4
+ module HashDiff
5
+
6
+ # apply changes to object
7
+ #
8
+ # changes: [[ '+', 'a.b', '45' ], [ '-', 'a.c', '5' ], [ '~', 'a.x', '45', '63']]
9
+ def self.patch(hash, changes)
10
+ changes.each do |change|
11
+ parts = decode_property_path(change[1])
12
+ last_part = parts.last
13
+
14
+ dest_node = node(hash, parts[0, parts.size-1])
15
+
16
+ if change[0] == '+'
17
+ if dest_node == nil
18
+ parent_key = parts[parts.size-2]
19
+ parent_node = node(hash, parts[0, parts.size-2])
20
+ if last_part.is_a?(Fixnum)
21
+ dest_node = parent_node[parent_key] = []
22
+ else
23
+ dest_node = parent_node[parent_key] = {}
24
+ end
25
+ end
26
+
27
+ if last_part.is_a?(Fixnum)
28
+ dest_node.insert(last_part, change[2])
29
+ else
30
+ dest_node[last_part] = change[2]
31
+ end
32
+ elsif change[0] == '-'
33
+ if last_part.is_a?(Fixnum)
34
+ dest_node.delete_at(last_part)
35
+ else
36
+ dest_node.delete(last_part)
37
+ end
38
+ elsif change[0] == '~'
39
+ dest_node[last_part] = change[3]
40
+ end
41
+ end
42
+
43
+ hash
44
+ end
45
+
46
+ # undo changes from object.
47
+ #
48
+ # changes: [[ '+', 'a.b', '45' ], [ '-', 'a.c', '5' ], [ '~', 'a.x', '45', '63']]
49
+ def self.unpatch(hash, changes)
50
+ changes.reverse_each do |change|
51
+ parts = decode_property_path(change[1])
52
+ last_part = parts.last
53
+
54
+ dest_node = node(hash, parts[0, parts.size-1])
55
+
56
+ if change[0] == '+'
57
+ if last_part.is_a?(Fixnum)
58
+ dest_node.delete_at(last_part)
59
+ else
60
+ dest_node.delete(last_part)
61
+ end
62
+ elsif change[0] == '-'
63
+ if dest_node == nil
64
+ parent_key = parts[parts.size-2]
65
+ parent_node = node(hash, parts[0, parts.size-2])
66
+ if last_part.is_a?(Fixnum)
67
+ dest_node = parent_node[parent_key] = []
68
+ else
69
+ dest_node = parent_node[parent_key] = {}
70
+ end
71
+ end
72
+
73
+ if last_part.is_a?(Fixnum)
74
+ dest_node.insert(last_part, change[2])
75
+ else
76
+ dest_node[last_part] = change[2]
77
+ end
78
+ elsif change[0] == '~'
79
+ dest_node[last_part] = change[2]
80
+ end
81
+ end
82
+
83
+ hash
84
+ end
85
+
86
+ end
@@ -0,0 +1,97 @@
1
+ module HashDiff
2
+
3
+ # return an array of added properties
4
+ # e.g. [[ '+', 'a.b', 45 ], [ '-', 'a.c', 5 ]]
5
+ def self.changed(obj, sign, prefix = "")
6
+ return [[sign, prefix, obj]] unless obj
7
+
8
+ results = []
9
+ if obj.is_a?(Array)
10
+ if sign == '+'
11
+ # add from the begining
12
+ results << [sign, prefix, []]
13
+ obj.each_index do |index|
14
+ results.concat(changed(obj[index], sign, "#{prefix}[#{index}]"))
15
+ end
16
+ elsif sign == '-'
17
+ # delete from the end
18
+ obj.each_index do |index|
19
+ i = obj.size - index - 1
20
+ results.concat(changed(obj[i], sign, "#{prefix}[#{i}]"))
21
+ end
22
+ results << [sign, prefix, []]
23
+ end
24
+ elsif obj.is_a?(Hash)
25
+ results << [sign, prefix, {}] if sign == '+'
26
+ prefix_t = prefix.empty? ? "" : "#{prefix}."
27
+ obj.each do |k, v|
28
+ results.concat(changed(v, sign, "#{prefix_t}#{k}"))
29
+ end
30
+ results << [sign, prefix, {}] if sign == '-'
31
+ else
32
+ return [[sign, prefix, obj]]
33
+ end
34
+
35
+ results
36
+ end
37
+
38
+ # judge whether two objects are similar
39
+ def self.similiar?(a, b, similarity = 0.8)
40
+ count_a = count_nodes(a)
41
+ count_b = count_nodes(b)
42
+ count_diff = diff(a, b, "", similarity).count
43
+
44
+ if count_a + count_b == 0
45
+ return true
46
+ else
47
+ (1 - count_diff.to_f/(count_a + count_b).to_f) >= similarity
48
+ end
49
+ end
50
+
51
+ # count total nodes for an object
52
+ def self.count_nodes(obj)
53
+ return 0 unless obj
54
+
55
+ count = 0
56
+ if obj.is_a?(Array)
57
+ count = obj.size
58
+ obj.each {|e| count += count_nodes(e) }
59
+ elsif obj.is_a?(Hash)
60
+ count = obj.size
61
+ obj.each {|k, v| count += count_nodes(v) }
62
+ else
63
+ return 1
64
+ end
65
+
66
+ count
67
+ end
68
+
69
+ # decode property path into an array
70
+ #
71
+ # e.g. "a.b[3].c" => ['a', 'b', 3, 'c']
72
+ def self.decode_property_path(path)
73
+ parts = path.split('.').collect do |part|
74
+ if part =~ /^(\w*)\[(\d+)\]$/
75
+ if $1.size > 0
76
+ [$1, $2.to_i]
77
+ else
78
+ $2.to_i
79
+ end
80
+ else
81
+ part
82
+ end
83
+ end
84
+
85
+ parts.flatten
86
+ end
87
+
88
+ # get the node of hash by given path parts
89
+ def self.node(hash, parts)
90
+ temp = hash
91
+ parts.each do |part|
92
+ temp = temp[part]
93
+ end
94
+ temp
95
+ end
96
+
97
+ end
@@ -0,0 +1,3 @@
1
+ module HashDiff
2
+ VERSION = '0.0.2'
3
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ describe HashDiff do
4
+ it "should be able to diff two equal array" do
5
+ a = [1, 2, 3]
6
+ b = [1, 2, 3]
7
+
8
+ diff = HashDiff.diff_array(a, b)
9
+ diff.should == []
10
+ end
11
+
12
+ it "should be able to diff two arrays with one element in common" do
13
+ a = [1, 2, 3]
14
+ b = [1, 8, 7]
15
+
16
+ diff = HashDiff.diff_array(a, b)
17
+ diff.should == [['-', 2, 3], ['-', 1, 2], ['+', 1, 8], ['+', 2, 7]]
18
+ end
19
+
20
+ it "should be able to diff two arrays with nothing in common" do
21
+ a = [1, 2]
22
+ b = []
23
+
24
+ diff = HashDiff.diff_array(a, b)
25
+ diff.should == [['-', 1, 2], ['-', 0, 1]]
26
+ end
27
+
28
+ it "should be able to diff an empty array with an non-empty array" do
29
+ a = []
30
+ b = [1, 2]
31
+
32
+ diff = HashDiff.diff_array(a, b)
33
+ diff.should == [['+', 0, 1], ['+', 1, 2]]
34
+ end
35
+
36
+ it "should be 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 = HashDiff.diff_array(a, b)
41
+ diff.should == [['-', 0, 1], ['+', 0, 2], ['+', 2, 7], ['-', 4, 7]]
42
+ end
43
+
44
+ it "should be 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 = HashDiff.diff_array(a, b)
49
+ diff.should == [['-', 0, 1], ['+', 0, 2], ['-', 2, 4], ['+', 3, 5]]
50
+ end
51
+
52
+ it "should be 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 = HashDiff.diff_array(a, b)
56
+ diff.should == [['+', 0, 1], ['-', 2, 3]]
57
+ end
58
+
59
+ end
60
+
@@ -0,0 +1,137 @@
1
+ require 'spec_helper'
2
+
3
+ describe HashDiff do
4
+ it "should be able to diff two empty hashes" do
5
+ diff = HashDiff.diff({}, {})
6
+ diff.should == []
7
+ end
8
+
9
+ it "should be able to diff an hash with an empty hash" do
10
+ a = {a:3, b:2}
11
+ b = {}
12
+
13
+ diff = HashDiff.diff(a, b)
14
+ diff.should == [['-', 'a', 3], ['-', 'b', 2]]
15
+
16
+ diff = HashDiff.diff(b, a)
17
+ diff.should == [['+', 'a', 3], ['+', 'b', 2]]
18
+ end
19
+
20
+ it "should be able to diff two equal hashes" do
21
+ diff = HashDiff.diff({a:2, b:2}, {a:2, b:2})
22
+ diff.should == []
23
+ end
24
+
25
+ it "should be able to diff changes in hash value which is array" do
26
+ diff = HashDiff.diff({a:2, b:[1, 2, 3]}, {a:2, b:[1, 3, 4]})
27
+ diff.should == [['-', 'b[1]', 2], ['+', 'b[2]', 4]]
28
+ end
29
+
30
+ it "should be able to diff changes in hash value which is hash" do
31
+ diff = HashDiff.diff({a:{x:2, y:3, z:4}, b:{x:3, z:45}}, {a:{y:3}, b:{y:3, z:30}})
32
+ diff.should == [['-', 'a.x', 2], ['-', 'a.z', 4], ['-', 'b.x', 3], ['~', 'b.z', 45, 30], ['+', 'b.y', 3]]
33
+ end
34
+
35
+ it "should be able to diff similar objects in array" do
36
+ diff = HashDiff.best_diff({a:[{x:2, y:3, z:4}, {x:11, y:22, z:33}], b:{x:3, z:45}}, {a:[{y:3}, {x:11, z:33}], b:{y:22}})
37
+ diff.should == [['-', 'a[0].x', 2], ['-', 'a[0].z', 4], ['-', 'a[1].y', 22], ['-', 'b.x', 3], ['-', 'b.z', 45], ['+', 'b.y', 22]]
38
+ end
39
+
40
+ it 'should be able to diff addition of key value pair' do
41
+ a = {"a"=>3, "c"=>11, "d"=>45, "e"=>100, "f"=>200}
42
+ b = {"a"=>3, "c"=>11, "d"=>45, "e"=>100, "f"=>200, "g"=>300}
43
+
44
+ diff = HashDiff.diff(a, b)
45
+ diff.should == [['+', 'g', 300]]
46
+
47
+ diff = HashDiff.diff(b, a)
48
+ diff.should == [['-', 'g', 300]]
49
+ end
50
+
51
+ it 'should be able to diff value type changes' do
52
+ a = {"a" => 3}
53
+ b = {"a" => {"a1" => 1, "a2" => 2}}
54
+
55
+ diff = HashDiff.diff(a, b)
56
+ diff.should == [['-', 'a', 3], ['+', 'a', {}], ['+', 'a.a1', 1], ['+', 'a.a2', 2]]
57
+
58
+ diff = HashDiff.diff(b, a)
59
+ diff.should == [['-', 'a.a1', 1], ['-', 'a.a2', 2], ['-', 'a', {}], ['+', 'a', 3]]
60
+ end
61
+
62
+ it "should be able to diff value changes: array <=> []" do
63
+ a = {"a" => 1, "b" => [1, 2]}
64
+ b = {"a" => 1, "b" => []}
65
+
66
+ diff = HashDiff.diff(a, b)
67
+ diff.should == [['-', 'b[1]', 2], ['-', 'b[0]', 1]]
68
+ end
69
+
70
+ # treat nil as empty array
71
+ it "should be able to diff value changes: array <=> nil" do
72
+ a = {"a" => 1, "b" => [1, 2]}
73
+ b = {"a" => 1, "b" => nil}
74
+
75
+ diff = HashDiff.diff(a, b)
76
+ diff.should == [['-', 'b[1]', 2], ['-', 'b[0]', 1]]
77
+ end
78
+
79
+ it "should be able to diff value chagnes: remove array completely" do
80
+ a = {"a" => 1, "b" => [1, 2]}
81
+ b = {"a" => 1}
82
+
83
+ diff = HashDiff.diff(a, b)
84
+ diff.should == [['-', 'b[1]', 2], ['-', 'b[0]', 1], ['-', 'b', []]]
85
+ end
86
+
87
+ it "should be able to diff value changes: remove whole hash" do
88
+ a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
89
+ b = {"a" => 1}
90
+
91
+ diff = HashDiff.diff(a, b)
92
+ diff.should == [['-', 'b.b1', 1], ['-', 'b.b2', 2], ['-', 'b', {}]]
93
+ end
94
+
95
+ it "should be able to diff value changes: hash <=> {}" do
96
+ a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
97
+ b = {"a" => 1, "b" => {}}
98
+
99
+ diff = HashDiff.diff(a, b)
100
+ diff.should == [['-', 'b.b1', 1], ['-', 'b.b2', 2]]
101
+ end
102
+
103
+ # treat nil as empty hash
104
+ it "should be able to diff value changes: hash <=> nil" do
105
+ a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
106
+ b = {"a" => 1, "b" => nil}
107
+
108
+ diff = HashDiff.diff(a, b)
109
+ diff.should == [['-', 'b.b1', 1], ['-', 'b.b2', 2]]
110
+ end
111
+
112
+ it "should be able to diff similar objects in array" do
113
+ a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, 3]
114
+ b = [1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}]
115
+
116
+ diff = HashDiff.diff(a, b)
117
+ diff.should == [['-', '[0].d', 4], ['+', '[0]', 1], ['-', '[2]', 3]]
118
+ end
119
+
120
+ it "should be able to diff similar & equal objects in array" do
121
+ a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, {'x' => 5, 'y' => 6, 'z' => 3}, 3]
122
+ b = [{'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}, 3]
123
+
124
+ diff = HashDiff.diff(a, b)
125
+ diff.should == [['-', '[0].d', 4], ['-', '[1].x', 5], ['-', '[1].y', 6], ['-', '[1].z', 3], ['-', '[1]', {}]]
126
+ end
127
+
128
+ it "should be able to best diff" do
129
+ a = {'x' => [{'a' => 1, 'c' => 3, 'e' => 5}, {'y' => 3}]}
130
+ b = {'x' => [{'a' => 1, 'b' => 2, 'e' => 5}] }
131
+
132
+ diff = HashDiff.best_diff(a, b)
133
+ diff.should == [['-', 'x[0].c', 3], ['+', 'x[0].b', 2], ['-', 'x[1].y', 3], ['-', 'x[1]', {}]]
134
+ end
135
+
136
+ end
137
+
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ describe HashDiff do
4
+ it "should be able to find LCS between two equal array" do
5
+ a = [1, 2, 3]
6
+ b = [1, 2, 3]
7
+
8
+ lcs = HashDiff.lcs(a, b)
9
+ lcs.should == [[0, 0], [1, 1], [2, 2]]
10
+ end
11
+
12
+ it "should be able to find LCS with one common elements" do
13
+ a = [1, 2, 3]
14
+ b = [1, 8, 7]
15
+
16
+ lcs = HashDiff.lcs(a, b)
17
+ lcs.should == [[0, 0]]
18
+ end
19
+
20
+ it "should be able to find LCS with two common elements" do
21
+ a = [1, 3, 5, 7]
22
+ b = [2, 3, 7, 5]
23
+
24
+ lcs = HashDiff.lcs(a, b)
25
+ lcs.should == [[1, 1], [2, 3]]
26
+ end
27
+
28
+ it "should be able to find LCS with two common elements in different ordering" do
29
+ a = [1, 3, 4, 7]
30
+ b = [2, 3, 7, 5]
31
+
32
+ lcs = HashDiff.lcs(a, b)
33
+ lcs.should == [[1, 1], [3, 2]]
34
+ end
35
+ end
36
+
@@ -0,0 +1,139 @@
1
+ require 'spec_helper'
2
+
3
+ describe HashDiff do
4
+ it "it should be able to patch key addition" do
5
+ a = {"a"=>3, "c"=>11, "d"=>45, "e"=>100, "f"=>200}
6
+ b = {"a"=>3, "c"=>11, "d"=>45, "e"=>100, "f"=>200, "g"=>300}
7
+ diff = HashDiff.diff(a, b)
8
+
9
+ HashDiff.patch(a, diff).should == b
10
+
11
+ a = {"a"=>3, "c"=>11, "d"=>45, "e"=>100, "f"=>200}
12
+ b = {"a"=>3, "c"=>11, "d"=>45, "e"=>100, "f"=>200, "g"=>300}
13
+ HashDiff.unpatch(b, diff).should == a
14
+ end
15
+
16
+ it "should be able to patch value type changes" do
17
+ a = {"a" => 3}
18
+ b = {"a" => {"a1" => 1, "a2" => 2}}
19
+ diff = HashDiff.diff(a, b)
20
+
21
+ HashDiff.patch(a, diff).should == b
22
+
23
+ a = {"a" => 3}
24
+ b = {"a" => {"a1" => 1, "a2" => 2}}
25
+ HashDiff.unpatch(b, diff).should == a
26
+ end
27
+
28
+ it "should be able to patch value array <=> []" do
29
+ a = {"a" => 1, "b" => [1, 2]}
30
+ b = {"a" => 1, "b" => []}
31
+ diff = HashDiff.diff(a, b)
32
+
33
+ HashDiff.patch(a, diff).should == b
34
+
35
+ a = {"a" => 1, "b" => [1, 2]}
36
+ b = {"a" => 1, "b" => []}
37
+ HashDiff.unpatch(b, diff).should == a
38
+ end
39
+
40
+ it "should be able to patch value array <=> nil" do
41
+ a = {"a" => 1, "b" => [1, 2]}
42
+ b = {"a" => 1, "b" => nil}
43
+ diff = HashDiff.diff(a, b)
44
+
45
+ # NOTE: nil is treated as [] in this context
46
+ HashDiff.patch(a, diff).should == {"a" => 1, "b" => []}
47
+
48
+ a = {"a" => 1, "b" => [1, 2]}
49
+ b = {"a" => 1, "b" => nil}
50
+ HashDiff.unpatch(b, diff).should == a
51
+ end
52
+
53
+ it "should be able to patch array value removal" do
54
+ a = {"a" => 1, "b" => [1, 2]}
55
+ b = {"a" => 1}
56
+ diff = HashDiff.diff(a, b)
57
+
58
+ HashDiff.patch(a, diff).should == b
59
+
60
+ a = {"a" => 1, "b" => [1, 2]}
61
+ b = {"a" => 1}
62
+ HashDiff.unpatch(b, diff).should == a
63
+ end
64
+
65
+ it "should be able to patch hash value removal" do
66
+ a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
67
+ b = {"a" => 1}
68
+ diff = HashDiff.diff(a, b)
69
+
70
+ HashDiff.patch(a, diff).should == b
71
+
72
+ a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
73
+ b = {"a" => 1}
74
+ HashDiff.unpatch(b, diff).should == a
75
+ end
76
+
77
+ it "should be able to patch value hash <=> {}" do
78
+ a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
79
+ b = {"a" => 1, "b" => {}}
80
+ diff = HashDiff.diff(a, b)
81
+
82
+ HashDiff.patch(a, diff).should == b
83
+
84
+ a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
85
+ b = {"a" => 1, "b" => {}}
86
+ HashDiff.unpatch(b, diff).should == a
87
+ end
88
+
89
+ it "should be able to patch value hash <=> nil" do
90
+ a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
91
+ b = {"a" => 1, "b" => nil}
92
+ diff = HashDiff.diff(a, b)
93
+
94
+ # NOTE: nil will be taken as {} in the context
95
+ HashDiff.patch(a, diff).should == {"a" => 1, "b" => {}}
96
+
97
+ a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
98
+ b = {"a" => 1, "b" => nil}
99
+ HashDiff.unpatch(b, diff).should == a
100
+ end
101
+
102
+ it "should be able to patch value nil removal" do
103
+ a = {"a" => 1, "b" => nil}
104
+ b = {"a" => 1}
105
+ diff = HashDiff.diff(a, b)
106
+
107
+ HashDiff.patch(a, diff).should == b
108
+
109
+ a = {"a" => 1, "b" => nil}
110
+ b = {"a" => 1}
111
+ HashDiff.unpatch(b, diff).should == a
112
+ end
113
+
114
+ it "should be able to patch similar objects between arrays" do
115
+ a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, 3]
116
+ b = [1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}]
117
+
118
+ diff = HashDiff.diff(a, b)
119
+ HashDiff.patch(a, diff).should == b
120
+
121
+ a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, 3]
122
+ b = [1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}]
123
+ HashDiff.unpatch(b, diff).should == a
124
+ end
125
+
126
+ it "should be able to patch similar & equal objects between arrays" do
127
+ a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, {'x' => 5, 'y' => 6, 'z' => 3}, 1]
128
+ b = [1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}]
129
+
130
+ diff = HashDiff.diff(a, b)
131
+ HashDiff.patch(a, diff).should == b
132
+
133
+ a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, {'x' => 5, 'y' => 6, 'z' => 3}, 1]
134
+ b = [1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}]
135
+ HashDiff.unpatch(b, diff).should == a
136
+ end
137
+
138
+ end
139
+
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe HashDiff do
4
+ it "should be able to decode property path" do
5
+ decoded = HashDiff.send(:decode_property_path, "a.b[0].c.city[5]")
6
+ decoded.should == ['a', 'b', 0, 'c', 'city', 5]
7
+ end
8
+ end
9
+
@@ -0,0 +1,13 @@
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
2
+
3
+ require 'rubygems'
4
+ require 'rspec'
5
+ require 'rspec/autorun'
6
+
7
+ require 'hashdiff'
8
+
9
+ RSpec.configure do |config|
10
+ config.mock_framework = :rspec
11
+
12
+ config.include RSpec::Matchers
13
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hashdiff
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Liu Fengyun
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-22 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &8876060 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '2.0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *8876060
25
+ - !ruby/object:Gem::Dependency
26
+ name: yard
27
+ requirement: &8874480 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *8874480
36
+ - !ruby/object:Gem::Dependency
37
+ name: bluecloth
38
+ requirement: &8859740 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *8859740
47
+ description: ! ' HashDiff is a diff lib to compute the smallest difference between
48
+ two hashes. '
49
+ email:
50
+ - liufengyunchina@gmail.com
51
+ executables: []
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - .gitignore
56
+ - .rspec
57
+ - .travis.yml
58
+ - Gemfile
59
+ - Gemfile.lock
60
+ - README.md
61
+ - Rakefile
62
+ - hashdiff.gemspec
63
+ - lib/hashdiff.rb
64
+ - lib/hashdiff/diff.rb
65
+ - lib/hashdiff/lcs.rb
66
+ - lib/hashdiff/patch.rb
67
+ - lib/hashdiff/util.rb
68
+ - lib/hashdiff/version.rb
69
+ - spec/hashdiff/diff_array_spec.rb
70
+ - spec/hashdiff/diff_spec.rb
71
+ - spec/hashdiff/lcs_spec.rb
72
+ - spec/hashdiff/patch_spec.rb
73
+ - spec/hashdiff/util_spec.rb
74
+ - spec/spec_helper.rb
75
+ homepage: https://github.com/liufengyun/hashdiff
76
+ licenses: []
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: 1.8.7
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ! '>='
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubyforge_project:
95
+ rubygems_version: 1.8.11
96
+ signing_key:
97
+ specification_version: 3
98
+ summary: HashDiff is a diff lib to compute the smallest difference between two hashes.
99
+ test_files: []
100
+ has_rdoc: