hashdiff 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: