tdiff 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,3 @@
1
+ -
2
+ ChangeLog.*
3
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour --format documentation
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup markdown --title "TDiff Documentation" --protected
data/ChangeLog.md ADDED
@@ -0,0 +1,8 @@
1
+ ### 0.1.0 / 2010-11-13
2
+
3
+ * Initial release:
4
+ * Provides the {TDiff} mixin.
5
+ * Allows custom node equality and traversal logic by overriding the
6
+ {TDiff#tdiff_equal} and {TDiff#tdiff_each_child} methods.
7
+ * Implements the [Longest Common Subsequence (LCS)](http://en.wikipedia.org/wiki/Longest_common_subsequence_problem).
8
+
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Hal Brodigan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # TDiff
2
+
3
+ * [Source](http://github.com/postmodern/tdiff)
4
+ * [Issues](http://github.com/postmodern/tdiff/issues)
5
+ * Postmodern (postmodern.mod3 at gmail.com)
6
+
7
+ ## Description
8
+
9
+ Calculates the differences between two tree-like structures. Similar to
10
+ Rubys built-in [TSort](http://rubydoc.info/docs/ruby-stdlib/1.9.2/TSort)
11
+ module.
12
+
13
+ ## Features
14
+
15
+ * Provides the {TDiff} mixin.
16
+ * Allows custom node equality and traversal logic by overriding the
17
+ {TDiff#tdiff_equal} and {TDiff#tdiff_each_child} methods.
18
+ * Implements the [Longest Common Subsequence (LCS)](http://en.wikipedia.org/wiki/Longest_common_subsequence_problem).
19
+
20
+ ## Examples
21
+
22
+ Diff two HTML documents:
23
+
24
+ require 'nokogiri'
25
+ require 'tdiff'
26
+
27
+ class Nokogiri::XML::Node
28
+
29
+ include TDiff
30
+
31
+ def tdiff_each_child(node,&block)
32
+ node.children.each(&block)
33
+ end
34
+
35
+ def tdiff_equal(node1,node2)
36
+ if (node1.text? && node2.text?)
37
+ node1.text == node2.text
38
+ elsif (node1.respond_to?(:root) && node2.respond_to?(:root))
39
+ tdiff_equal(node1.root,node2.root)
40
+ elsif (node1.respond_to?(:name) && node2.respond_to?(:name))
41
+ node1.name == node2.name
42
+ else
43
+ false
44
+ end
45
+ end
46
+
47
+ end
48
+
49
+ doc1 = Nokogiri::HTML('<div><p>one</p> <p>three</p></div>')
50
+ doc2 = Nokogiri::HTML('<div><p>one</p> <p>two</p> <p>three</p></div>')
51
+
52
+ doc1.at('div').tdiff(doc2.at('div')) do |change,node|
53
+ puts "#{change} #{node.to_html}".ljust(30) + node.parent.path
54
+ end
55
+
56
+ ### Output
57
+
58
+ + <p>one</p> /html/body/div
59
+ + /html/body/div
60
+ <p>one</p> /html/body/div
61
+ /html/body/div
62
+ <p>three</p> /html/body/div
63
+ - one /html/body/div/p[1]
64
+ + two /html/body/div/p[2]
65
+ three /html/body/div/p[2]
66
+
67
+ ## Install
68
+
69
+ $ gem install tdiff
70
+
71
+ ## Copyright
72
+
73
+ See {file:LICENSE.txt} for details.
74
+
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ gem 'ore-tasks', '~> 0.2.0'
6
+ require 'ore/tasks'
7
+
8
+ Ore::Tasks.new
9
+ rescue LoadError => e
10
+ STDERR.puts e.message
11
+ STDERR.puts "Run `gem install ore-tasks` to install 'ore/tasks'."
12
+ end
13
+
14
+ begin
15
+ gem 'rspec', '~> 2.1.0'
16
+ require 'rspec/core/rake_task'
17
+
18
+ RSpec::Core::RakeTask.new
19
+ rescue LoadError => e
20
+ task :spec do
21
+ abort "Please run `gem install rspec` to install RSpec."
22
+ end
23
+ end
24
+ task :default => :spec
25
+
26
+ begin
27
+ gem 'yard', '~> 0.6.0'
28
+ require 'yard'
29
+
30
+ YARD::Rake::YardocTask.new
31
+ rescue LoadError => e
32
+ task :yard do
33
+ abort "Please run `gem install yard` to install YARD."
34
+ end
35
+ end
data/gemspec.yml ADDED
@@ -0,0 +1,17 @@
1
+ name: tdiff
2
+ version: 0.1.0
3
+ summary: Calculates the differences between two tree-like structures.
4
+ description:
5
+ Calculates the differences between two tree-like structures. Similar to
6
+ Rubys built-in TSort module.
7
+
8
+ license: MIT
9
+ authors: Postmodern
10
+ email: postmodern.mod3@gmail.com
11
+ homepage: http://github.com/postmodern/tdiff
12
+ has_yard: true
13
+
14
+ development_dependencies:
15
+ ore-tasks: ~> 0.2.0
16
+ rspec: ~> 2.1.0
17
+ yard: ~> 0.6.0
data/lib/tdiff.rb ADDED
@@ -0,0 +1,133 @@
1
+ #
2
+ # {TDiff} adds the ability to calculate the differences between two tree-like
3
+ # objects. Simply include {TDiff} into the class which represents the tree
4
+ # nodes and define the {#tdiff_each_child} and {#tdiff_equal} methods.
5
+ #
6
+ module TDiff
7
+ #
8
+ # Default method which will enumerate over every child of a parent node.
9
+ #
10
+ # @param [Object] node
11
+ # The parent node.
12
+ #
13
+ # @yield [child]
14
+ # The given block will be passed each child of the parent node.
15
+ #
16
+ def tdiff_each_child(node,&block)
17
+ node.each(&block) if node.kind_of?(Enumerable)
18
+ end
19
+
20
+ #
21
+ # Default method which compares two nodes.
22
+ #
23
+ # @param [Object] original_node
24
+ # A node from the original tree.
25
+ #
26
+ # @param [Object] new_node
27
+ # A node from the new tree.
28
+ #
29
+ # @return [Boolean]
30
+ # Specifies whether the two nodes are equal.
31
+ #
32
+ def tdiff_equal(original_node,new_node)
33
+ original_node == new_node
34
+ end
35
+
36
+ #
37
+ # Finds the differences between `self` and another tree.
38
+ #
39
+ # @param [#tdiff_each_child] tree
40
+ # The other tree.
41
+ #
42
+ # @yield [change, node]
43
+ # The given block will be passed the added or removed nodes.
44
+ #
45
+ # @yieldparam [' ', '+', '-'] change
46
+ # The state-change of the node.
47
+ #
48
+ # @yieldparam [Object] node
49
+ # A node from one of the two trees.
50
+ #
51
+ # @return [Enumerator]
52
+ # If no block is given, an Enumerator object will be returned.
53
+ #
54
+ def tdiff(tree,&block)
55
+ return enum_for(:tdiff,tree) unless block
56
+
57
+ # check if the nodes differ
58
+ unless tdiff_equal(self,tree)
59
+ yield '-', self
60
+ yield '+', tree
61
+ return self
62
+ end
63
+
64
+ c = Hash.new { |hash,key| hash[key] = Hash.new(0) }
65
+ x = enum_for(:tdiff_each_child,self)
66
+ y = enum_for(:tdiff_each_child,tree)
67
+
68
+ x.each_with_index do |xi,i|
69
+ y.each_with_index do |yi,j|
70
+ c[i][j] = if tdiff_equal(xi,yi)
71
+ c[i-1][j-1] + 1
72
+ else
73
+ if c[i][j-1] > c[i-1][j]
74
+ c[i][j-1]
75
+ else
76
+ c[i-1][j]
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ unchanged = []
83
+ changes = []
84
+
85
+ x_backtrack = x.each_with_index.reverse_each
86
+ y_backtrack = y.each_with_index.reverse_each
87
+
88
+ next_child = lambda { |children|
89
+ begin
90
+ children.next
91
+ rescue StopIteration
92
+ # end of iteration, return a -1 index
93
+ [nil, -1]
94
+ end
95
+ }
96
+
97
+ xi, i = next_child[x_backtrack]
98
+ yi, j = next_child[y_backtrack]
99
+
100
+ until (i == -1 && j == -1)
101
+ if (i != -1 && j != -1 && tdiff_equal(xi,yi))
102
+ changes.unshift [' ', xi]
103
+ unchanged.unshift [xi, yi]
104
+
105
+ xi, i = next_child[x_backtrack]
106
+ yi, j = next_child[y_backtrack]
107
+ else
108
+ if (j >= 0 && (i == -1 || c[i][j-1] >= c[i-1][j]))
109
+ changes.unshift ['+', yi]
110
+
111
+ yi, j = next_child[y_backtrack]
112
+ elsif (i >= 0 && (j == -1 || c[i][j-1] < c[i-1][j]))
113
+ changes.unshift ['-', xi]
114
+
115
+ xi, i = next_child[x_backtrack]
116
+ end
117
+ end
118
+ end
119
+
120
+ # explicitly discard the c matrix
121
+ c = nil
122
+
123
+ # sequentially iterate over the changed nodes
124
+ changes.each(&block)
125
+ changes = nil
126
+
127
+ # recurse down through unchanged nodes
128
+ unchanged.each { |x,y| x.tdiff(y,&block) }
129
+ unchanged = nil
130
+
131
+ return self
132
+ end
133
+ end
@@ -0,0 +1,15 @@
1
+ require 'tdiff'
2
+
3
+ class Node < Struct.new(:name, :children)
4
+
5
+ include TDiff
6
+
7
+ def tdiff_each_child(node,&block)
8
+ node.children.each(&block)
9
+ end
10
+
11
+ def tdiff_equal(node1,node2)
12
+ node1.name == node2.name
13
+ end
14
+
15
+ end
@@ -0,0 +1,2 @@
1
+ gem 'rspec', '~> 2.1.0'
2
+ require 'rspec'
@@ -0,0 +1,111 @@
1
+ require 'spec_helper'
2
+ require 'classes/node'
3
+ require 'tdiff'
4
+
5
+ describe TDiff do
6
+ before(:all) do
7
+ @tree = Node.new('root', [
8
+ Node.new('leaf1', [
9
+ Node.new('subleaf1', []),
10
+ Node.new('subleaf2', [])
11
+ ]),
12
+
13
+ Node.new('leaf2', [
14
+ Node.new('subleaf1', []),
15
+ Node.new('subleaf2', [])
16
+ ])
17
+ ])
18
+
19
+ @different_root = Node.new('wrong', [])
20
+
21
+ @added = Node.new('root', [
22
+ Node.new('leaf1', [
23
+ Node.new('subleaf1', []),
24
+ Node.new('subleaf3', []),
25
+ Node.new('subleaf2', [])
26
+ ]),
27
+
28
+ Node.new('leaf2', [
29
+ Node.new('subleaf1', []),
30
+ Node.new('subleaf2', [])
31
+ ])
32
+ ])
33
+
34
+ @removed = Node.new('root', [
35
+ Node.new('leaf1', [
36
+ Node.new('subleaf1', [])
37
+ ]),
38
+
39
+ Node.new('leaf2', [
40
+ Node.new('subleaf1', []),
41
+ Node.new('subleaf2', [])
42
+ ])
43
+ ])
44
+
45
+ @changed_order = Node.new('root', [
46
+ Node.new('leaf2', [
47
+ Node.new('subleaf1', []),
48
+ Node.new('subleaf2', [])
49
+ ]),
50
+
51
+ Node.new('leaf1', [
52
+ Node.new('subleaf1', []),
53
+ Node.new('subleaf2', [])
54
+ ])
55
+ ])
56
+ end
57
+
58
+ it "should tell if two trees are identical" do
59
+ @tree.tdiff(@tree).all? { |change,node|
60
+ change == ' '
61
+ }.should == true
62
+ end
63
+
64
+ it "should stop if the root nodes have changed" do
65
+ changes = @tree.tdiff(@different_root).to_a
66
+
67
+ changes.length.should == 2
68
+
69
+ changes[0][0].should == '-'
70
+ changes[0][1].should == @tree
71
+
72
+ changes[1][0].should == '+'
73
+ changes[1][1].should == @different_root
74
+ end
75
+
76
+ it "should tell when sub-nodes are added" do
77
+ changes = @tree.tdiff(@added).select { |change,node| change == '+' }
78
+
79
+ changes.length.should == 1
80
+ changes[0][0].should == '+'
81
+ changes[0][1].should == @added.children[0].children[1]
82
+ end
83
+
84
+ it "should tell when sub-nodes are removed" do
85
+ changes = @tree.tdiff(@removed).select { |change,node| change == '-' }
86
+
87
+ changes.length.should == 1
88
+ changes[0][0].should == '-'
89
+ changes[0][1].should == @tree.children[0].children[1]
90
+ end
91
+
92
+ it "should detect when the order of children has changed" do
93
+ changes = @tree.tdiff(@changed_order).to_a
94
+
95
+ changes.length.should == 5
96
+ changes[0][0].should == '-'
97
+ changes[0][1].should == @tree.children[0]
98
+
99
+ changes[1][0].should == ' '
100
+ changes[1][1].should == @tree.children[1]
101
+
102
+ changes[2][0].should == '+'
103
+ changes[2][1].should == @changed_order.children[1]
104
+
105
+ changes[3][0].should == ' '
106
+ changes[3][1].should == @tree.children[1].children[0]
107
+
108
+ changes[4][0].should == ' '
109
+ changes[4][1].should == @tree.children[1].children[1]
110
+ end
111
+ end
data/tdiff.gemspec ADDED
@@ -0,0 +1,10 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ begin
4
+ Ore::Specification.new do |gemspec|
5
+ # custom logic here
6
+ end
7
+ rescue NameError
8
+ STDERR.puts "The 'tdiff.gemspec' file requires Ore."
9
+ STDERR.puts "Run `gem install ore-core` to install Ore."
10
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tdiff
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Postmodern
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-11-13 00:00:00 -08:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: ore-tasks
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ - 2
31
+ - 0
32
+ version: 0.2.0
33
+ type: :development
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: rspec
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ segments:
44
+ - 2
45
+ - 1
46
+ - 0
47
+ version: 2.1.0
48
+ type: :development
49
+ version_requirements: *id002
50
+ - !ruby/object:Gem::Dependency
51
+ name: yard
52
+ prerelease: false
53
+ requirement: &id003 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ~>
57
+ - !ruby/object:Gem::Version
58
+ segments:
59
+ - 0
60
+ - 6
61
+ - 0
62
+ version: 0.6.0
63
+ type: :development
64
+ version_requirements: *id003
65
+ description: Calculates the differences between two tree-like structures. Similar to Rubys built-in TSort module.
66
+ email: postmodern.mod3@gmail.com
67
+ executables: []
68
+
69
+ extensions: []
70
+
71
+ extra_rdoc_files:
72
+ - README.md
73
+ - ChangeLog.md
74
+ - LICENSE.txt
75
+ files:
76
+ - .document
77
+ - .rspec
78
+ - .yardopts
79
+ - ChangeLog.md
80
+ - LICENSE.txt
81
+ - README.md
82
+ - Rakefile
83
+ - gemspec.yml
84
+ - lib/tdiff.rb
85
+ - spec/classes/node.rb
86
+ - spec/spec_helper.rb
87
+ - spec/tdiff_spec.rb
88
+ - tdiff.gemspec
89
+ has_rdoc: yard
90
+ homepage: http://github.com/postmodern/tdiff
91
+ licenses:
92
+ - MIT
93
+ post_install_message:
94
+ rdoc_options: []
95
+
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ segments:
104
+ - 0
105
+ version: "0"
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ segments:
112
+ - 0
113
+ version: "0"
114
+ requirements: []
115
+
116
+ rubyforge_project: tdiff
117
+ rubygems_version: 1.3.7
118
+ signing_key:
119
+ specification_version: 3
120
+ summary: Calculates the differences between two tree-like structures.
121
+ test_files:
122
+ - spec/tdiff_spec.rb