tdiff 0.1.0

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.
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