tdiff-0.4.0/ 0000755 0000041 0000041 00000000000 14574713761 012656 5 ustar www-data www-data tdiff-0.4.0/.gitignore 0000644 0000041 0000041 00000000027 14574713761 014645 0 ustar www-data www-data Gemfile.lock doc/ pkg/ tdiff-0.4.0/.document 0000644 0000041 0000041 00000000034 14574713761 014472 0 ustar www-data www-data - ChangeLog.md LICENSE.txt tdiff-0.4.0/.github/ 0000755 0000041 0000041 00000000000 14574713761 014216 5 ustar www-data www-data tdiff-0.4.0/.github/workflows/ 0000755 0000041 0000041 00000000000 14574713761 016253 5 ustar www-data www-data tdiff-0.4.0/.github/workflows/ruby.yml 0000644 0000041 0000041 00000001123 14574713761 017754 0 ustar www-data www-data name: CI on: [ push, pull_request ] jobs: tests: runs-on: ubuntu-latest strategy: fail-fast: false matrix: ruby: - '3.0' - '3.1' - '3.2' - '3.3' - jruby - truffleruby name: Ruby ${{ matrix.ruby }} steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - name: Install dependencies run: bundle install --jobs 4 --retry 3 - name: Run tests run: bundle exec rake test tdiff-0.4.0/lib/ 0000755 0000041 0000041 00000000000 14574713761 013424 5 ustar www-data www-data tdiff-0.4.0/lib/tdiff/ 0000755 0000041 0000041 00000000000 14574713761 014520 5 ustar www-data www-data tdiff-0.4.0/lib/tdiff/unordered.rb 0000644 0000041 0000041 00000005346 14574713761 017044 0 ustar www-data www-data # frozen_string_literal: true require_relative 'tdiff' module TDiff # # Calculates the differences between two trees, without respecting the # order of children nodes. # module Unordered # # Includes {TDiff}. # def self.included(base) base.send :include, TDiff end # # Finds the differences between `self` and another tree, not respecting # the order of the nodes. # # @param [#tdiff_each_child] tree # The other tree. # # @yield [change, node] # The given block will be passed the added or removed nodes. # # @yieldparam [' ', '+', '-'] change # The state-change of the node. # # @yieldparam [Object] node # A node from one of the two trees. # # @return [Enumerator] # If no block is given, an Enumerator object will be returned. # # @since 0.2.0 # def tdiff_unordered(tree,&block) return enum_for(:tdiff_unordered,tree) unless block # check if the nodes differ unless tdiff_equal(tree) yield '-', self yield '+', tree return self end yield ' ', self tdiff_recursive_unordered(tree,&block) return self end protected # # Recursively compares the differences between the children nodes, # without respecting the order of the nodes. # # @param [#tdiff_each_child] tree # The other tree. # # @yield [change, node] # The given block will be passed the added or removed nodes. # # @yieldparam [' ', '+', '-'] change # The state-change of the node. # # @yieldparam [Object] node # A node from one of the two trees. # # @since 0.3.2 # def tdiff_recursive_unordered(tree,&block) x = enum_for(:tdiff_each_child,self) y = enum_for(:tdiff_each_child,tree) unchanged = {} changes = [] x.each_with_index do |xi,i| y.each_with_index do |yj,j| if (!unchanged.has_value?(yj) && xi.tdiff_equal(yj)) unchanged[xi] = yj changes << [i, ' ', xi] break end end unless unchanged.has_key?(xi) changes << [i, '-', xi] end end y.each_with_index do |yj,j| unless unchanged.has_value?(yj) changes << [j, '+', yj] end end # order the changes by index to match the behavior of `tdiff` changes.sort_by { |change| change[0] }.each do |index,change,node| yield change, node end # explicitly release the changes variable changes = nil # recurse down the unchanged nodes unchanged.each do |xi,yj| xi.tdiff_recursive_unordered(yj,&block) end unchanged = nil end end end tdiff-0.4.0/lib/tdiff/version.rb 0000644 0000041 0000041 00000000104 14574713761 016525 0 ustar www-data www-data # frozen_string_literal: true module TDiff VERSION = '0.4.0' end tdiff-0.4.0/lib/tdiff/tdiff.rb 0000644 0000041 0000041 00000007246 14574713761 016152 0 ustar www-data www-data # frozen_string_literal: true # # {TDiff} adds the ability to calculate the differences between two tree-like # objects. Simply include {TDiff} into the class which represents the tree # nodes and define the {#tdiff_each_child} and {#tdiff_equal} methods. # module TDiff # # Default method which will enumerate over every child of a parent node. # # @param [Object] node # The parent node. # # @yield [child] # The given block will be passed each child of the parent node. # def tdiff_each_child(node,&block) node.each(&block) if node.kind_of?(Enumerable) end # # Default method which compares nodes. # # @param [Object] node # A node from the new tree. # # @return [Boolean] # Specifies whether the nodes are equal. # def tdiff_equal(node) self == node end # # Finds the differences between `self` and another tree. # # @param [#tdiff_each_child] tree # The other tree. # # @yield [change, node] # The given block will be passed the added or removed nodes. # # @yieldparam [' ', '+', '-'] change # The state-change of the node. # # @yieldparam [Object] node # A node from one of the two trees. # # @return [Enumerator] # If no block is given, an Enumerator object will be returned. # def tdiff(tree,&block) return enum_for(:tdiff,tree) unless block # check if the nodes differ unless tdiff_equal(tree) yield '-', self yield '+', tree return self end yield ' ', self tdiff_recursive(tree,&block) return self end protected # # Recursively compares the differences between the children nodes. # # @param [#tdiff_each_child] tree # The other tree. # # @yield [change, node] # The given block will be passed the added or removed nodes. # # @yieldparam [' ', '+', '-'] change # The state-change of the node. # # @yieldparam [Object] node # A node from one of the two trees. # # @since 0.3.2 # def tdiff_recursive(tree,&block) c = Hash.new { |hash,key| hash[key] = Hash.new(0) } x = enum_for(:tdiff_each_child,self) y = enum_for(:tdiff_each_child,tree) x.each_with_index do |xi,i| y.each_with_index do |yj,j| c[i][j] = if xi.tdiff_equal(yj) c[i-1][j-1] + 1 else if c[i][j-1] > c[i-1][j] c[i][j-1] else c[i-1][j] end end end end unchanged = [] changes = [] x_backtrack = x.each_with_index.reverse_each y_backtrack = y.each_with_index.reverse_each next_child = lambda { |children| begin children.next rescue StopIteration # end of iteration, return a -1 index [nil, -1] end } xi, i = next_child[x_backtrack] yj, j = next_child[y_backtrack] until (i == -1 && j == -1) if (i != -1 && j != -1 && xi.tdiff_equal(yj)) changes.unshift [' ', xi] unchanged.unshift [xi, yj] xi, i = next_child[x_backtrack] yj, j = next_child[y_backtrack] else if (j >= 0 && (i == -1 || c[i][j-1] >= c[i-1][j])) changes.unshift ['+', yj] yj, j = next_child[y_backtrack] elsif (i >= 0 && (j == -1 || c[i][j-1] < c[i-1][j])) changes.unshift ['-', xi] xi, i = next_child[x_backtrack] end end end # explicitly discard the c matrix c = nil # sequentially iterate over the changed nodes changes.each(&block) changes = nil # recurse down through unchanged nodes unchanged.each { |a,b| a.tdiff_recursive(b,&block) } unchanged = nil end end tdiff-0.4.0/lib/tdiff.rb 0000644 0000041 0000041 00000000202 14574713761 015037 0 ustar www-data www-data # frozen_string_literal: true require_relative 'tdiff/tdiff' require_relative 'tdiff/unordered' require_relative 'tdiff/version' tdiff-0.4.0/LICENSE.txt 0000644 0000041 0000041 00000002045 14574713761 014502 0 ustar www-data www-data Copyright (c) 2010-2024 Hal Brodigan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. tdiff-0.4.0/spec/ 0000755 0000041 0000041 00000000000 14574713761 013610 5 ustar www-data www-data tdiff-0.4.0/spec/unordered_spec.rb 0000644 0000041 0000041 00000001042 14574713761 017133 0 ustar www-data www-data require 'spec_helper' require 'tdiff_examples' require 'tdiff/unordered' describe TDiff::Unordered do include Helpers::Trees it "should include TDiff when included" do base = Class.new do include TDiff::Unordered end expect(base).to include(TDiff) end it_should_behave_like 'TDiff', :tdiff_unordered it "should not detect when the order of children has changed" do changes = @tree.tdiff_unordered(@changed_order).select do |change,node| change != ' ' end expect(changes).to be_empty end end tdiff-0.4.0/spec/tdiff_examples.rb 0000644 0000041 0000041 00000002166 14574713761 017134 0 ustar www-data www-data require 'spec_helper' require 'helpers/trees' shared_examples_for 'TDiff' do |method| include Helpers::Trees it "should tell if two trees are identical" do expect( @tree.send(method,@tree).all? { |change,node| change == ' ' } ).to be true end it "should stop if the root nodes have changed" do changes = @tree.send(method,@different_root).to_a expect(changes.length).to be 2 expect(changes[0][0]).to be == '-' expect(changes[0][1]).to be == @tree expect(changes[1][0]).to be == '+' expect(changes[1][1]).to be == @different_root end it "should tell when sub-nodes are added" do changes = @tree.send(method,@added).select { |change,node| change == '+' } expect(changes.length).to be 1 expect(changes[0][0]).to be == '+' expect(changes[0][1]).to be == @added.children[0].children[1] end it "should tell when sub-nodes are removed" do changes = @tree.send(method,@removed).select { |change,node| change == '-' } expect(changes.length).to be 1 expect(changes[0][0]).to be == '-' expect(changes[0][1]).to be == @tree.children[0].children[1] end end tdiff-0.4.0/spec/spec_helper.rb 0000644 0000041 0000041 00000000051 14574713761 016422 0 ustar www-data www-data require 'rspec' require 'helpers/trees' tdiff-0.4.0/spec/helpers/ 0000755 0000041 0000041 00000000000 14574713761 015252 5 ustar www-data www-data tdiff-0.4.0/spec/helpers/trees.rb 0000644 0000041 0000041 00000003502 14574713761 016721 0 ustar www-data www-data require 'classes/node' module Helpers module Trees def self.included(base) base.module_eval do before(:all) do @tree = Node.new('root', [ Node.new('leaf1', [ Node.new('subleaf1', []), Node.new('subleaf2', []) ]), Node.new('leaf2', [ Node.new('subleaf1', []), Node.new('subleaf2', []) ]) ]) @different_root = Node.new('wrong', []) @added = Node.new('root', [ Node.new('leaf1', [ Node.new('subleaf1', []), Node.new('subleaf3', []), Node.new('subleaf2', []) ]), Node.new('leaf2', [ Node.new('subleaf1', []), Node.new('subleaf2', []) ]) ]) @removed = Node.new('root', [ Node.new('leaf1', [ Node.new('subleaf1', []) ]), Node.new('leaf2', [ Node.new('subleaf1', []), Node.new('subleaf2', []) ]) ]) @changed_order = Node.new('root', [ Node.new('leaf2', [ Node.new('subleaf1', []), Node.new('subleaf2', []) ]), Node.new('leaf1', [ Node.new('subleaf1', []), Node.new('subleaf2', []) ]) ]) end end end end end tdiff-0.4.0/spec/tdiff_spec.rb 0000644 0000041 0000041 00000001576 14574713761 016254 0 ustar www-data www-data require 'spec_helper' require 'tdiff_examples' require 'tdiff/tdiff' describe TDiff do include Helpers::Trees it_should_behave_like 'TDiff', :tdiff it "should detect when the order of children has changed" do changes = @tree.tdiff(@changed_order).to_a expect(changes.length).to be == 6 expect(changes[0][0]).to be == ' ' expect(changes[0][1]).to be == @tree expect(changes[1][0]).to be == '-' expect(changes[1][1]).to be == @tree.children[0] expect(changes[2][0]).to be == ' ' expect(changes[2][1]).to be == @tree.children[1] expect(changes[3][0]).to be == '+' expect(changes[3][1]).to be == @changed_order.children[1] expect(changes[4][0]).to be == ' ' expect(changes[4][1]).to be == @tree.children[1].children[0] expect(changes[5][0]).to be == ' ' expect(changes[5][1]).to be == @tree.children[1].children[1] end end tdiff-0.4.0/spec/classes/ 0000755 0000041 0000041 00000000000 14574713761 015245 5 ustar www-data www-data tdiff-0.4.0/spec/classes/node.rb 0000644 0000041 0000041 00000000360 14574713761 016516 0 ustar www-data www-data require 'tdiff' class Node < Struct.new(:name, :children) include TDiff include TDiff::Unordered def tdiff_each_child(node,&block) node.children.each(&block) end def tdiff_equal(node) self.name == node.name end end tdiff-0.4.0/.yardopts 0000644 0000041 0000041 00000000074 14574713761 014525 0 ustar www-data www-data --markup markdown --title "TDiff Documentation" --protected tdiff-0.4.0/gemspec.yml 0000644 0000041 0000041 00000001311 14574713761 015020 0 ustar www-data www-data name: tdiff summary: Calculates the differences between two tree-like structures. description: Calculates the differences between two tree-like structures. Similar to Rubys built-in TSort module. license: MIT authors: Postmodern email: postmodern.mod3@gmail.com homepage: https://github.com/postmodern/tdiff#readme has_yard: true metadata: documentation_uri: https://rubydoc.info/gems/tdiff source_code_uri: https://github.com/postmodern/tdiff bug_tracker_uri: https://github.com/postmodern/tdiff/issues changelog_uri: https://github.com/postmodern/tdiff/blob/main/ChangeLog.md rubygems_mfa_required: 'true' required_ruby_version: ">= 2.0.0" development_dependencies: bundler: ~> 2.0 tdiff-0.4.0/.rspec 0000644 0000041 0000041 00000000040 14574713761 013765 0 ustar www-data www-data --colour --format documentation tdiff-0.4.0/Rakefile 0000644 0000041 0000041 00000000315 14574713761 014322 0 ustar www-data www-data require 'rubygems' require 'rubygems/tasks' Gem::Tasks.new require 'rspec/core/rake_task' RSpec::Core::RakeTask.new task :test => :spec task :default => :spec require 'yard' YARD::Rake::YardocTask.new tdiff-0.4.0/ChangeLog.md 0000644 0000041 0000041 00000002413 14574713761 015027 0 ustar www-data www-data ### 0.4.0 / 2024-01-24 * Switched to using `require_relative` to improve load-times. * Added `# frozen_string_literal: true` to all files. ### 0.3.4 / 2018-06-11 * Fixed shadowed variable warning (@bhollis). ### 0.3.3 / 2012-05-28 * Require ruby >= 1.8.7. * Added {TDiff::VERSION}. * Replaced ore-tasks with [rubygems-tasks](https://github.com/postmodern/rubygems-tasks#readme). ### 0.3.2 / 2010-11-28 * Added {TDiff#tdiff_recursive} to only handle recursively traversing and diffing the children nodes. * Added {TDiff::Unordered#tdiff_recursive_unordered} to only handle recursively traversing and diffing the children nodes, without respecting the order of the nodes. ### 0.3.1 / 2010-11-28 * Fixed a typo in {TDiff::Unordered#tdiff_unordered}, which was causing all nodes to be marked as added. ### 0.3.0 / 2010-11-15 * Changed {TDiff#tdiff_equal} to compare `self` with another node. ### 0.2.0 / 2010-11-14 * Added {TDiff::Unordered}. ### 0.1.0 / 2010-11-13 * Initial release: * Provides the {TDiff} mixin. * Allows custom node equality and traversal logic by overriding the {TDiff#tdiff_equal} and {TDiff#tdiff_each_child} methods. * Implements the [Longest Common Subsequence (LCS)](http://en.wikipedia.org/wiki/Longest_common_subsequence_problem). tdiff-0.4.0/Gemfile 0000644 0000041 0000041 00000000335 14574713761 014152 0 ustar www-data www-data source 'https://rubygems.org' gemspec group :development do gem 'rake' gem 'rubygems-tasks', '~> 0.1' gem 'rspec', '~> 3.0' gem 'kramdown' gem 'redcarpet', platform: :mri gem 'yard', '~> 0.9' end tdiff-0.4.0/README.md 0000644 0000041 0000041 00000003763 14574713761 014146 0 ustar www-data www-data # TDiff * [Source](https://github.com/postmodern/tdiff) * [Issues](https://github.com/postmodern/tdiff/issues) * [Documentation](https://rubydoc.info/gems/tdiff) ## Description Calculates the differences between two tree-like structures. Similar to Rubys built-in [TSort](http://rubydoc.info/docs/ruby-stdlib/1.9.2/TSort) module. ## Features * Provides the {TDiff} mixin. * Provides the {TDiff::Unordered} mixin for unordered diffing. * Allows custom node equality and traversal logic by overriding the {TDiff#tdiff_equal} and {TDiff#tdiff_each_child} methods. * Implements the [Longest Common Subsequence (LCS)](http://en.wikipedia.org/wiki/Longest_common_subsequence_problem) algorithm. ## Examples Diff two HTML documents: ```ruby require 'nokogiri' require 'tdiff' class Nokogiri::XML::Node include TDiff def tdiff_equal(node) if (self.text? && node.text?) self.text == node.text elsif (self.respond_to?(:root) && node.respond_to?(:root)) self.root.tdiff_equal(node.root) elsif (self.respond_to?(:name) && node.respond_to?(:name)) self.name == node.name else false end end def tdiff_each_child(node,&block) node.children.each(&block) end end doc1 = Nokogiri::HTML('
one
three
one
two
three
one
/html/body/div + /html/body/divone
/html/body/div /html/body/divthree
/html/body/div - one /html/body/div/p[1] + two /html/body/div/p[2] three /html/body/div/p[2] ``` ## Requirements * [ruby](http://www.ruby-lang.org/) >= 2.0.0 ## Install ```shell $ gem install tdiff ``` ## Copyright See {file:LICENSE.txt} for details. tdiff-0.4.0/.gemtest 0000644 0000041 0000041 00000000000 14574713761 014315 0 ustar www-data www-data tdiff-0.4.0/tdiff.gemspec 0000644 0000041 0000041 00000003536 14574713761 015326 0 ustar www-data www-data # encoding: utf-8 require 'yaml' Gem::Specification.new do |gem| gemspec = YAML.load_file('gemspec.yml') gem.name = gemspec.fetch('name') gem.version = gemspec.fetch('version') do require_relative 'lib/tdiff/version' TDiff::VERSION end gem.summary = gemspec['summary'] gem.description = gemspec['description'] gem.licenses = Array(gemspec['license']) gem.authors = Array(gemspec['authors']) gem.email = gemspec['email'] gem.homepage = gemspec['homepage'] glob = lambda { |patterns| gem.files & Dir[*patterns] } gem.files = `git ls-files`.split($/) gem.files = glob[gemspec['files']] if gemspec['files'] gem.executables = gemspec.fetch('executables') do glob['bin/*'].map { |path| File.basename(path) } end gem.default_executable = gem.executables.first if Gem::VERSION < '1.7.' gem.extensions = glob[gemspec['extensions'] || 'ext/**/extconf.rb'] gem.test_files = glob[gemspec['test_files'] || '{test/{**/}*_test.rb'] gem.extra_rdoc_files = glob[gemspec['extra_doc_files'] || '*.{txt,md}'] gem.require_paths = Array(gemspec.fetch('require_paths') { %w[ext lib].select { |dir| File.directory?(dir) } }) gem.requirements = gemspec['requirements'] gem.required_ruby_version = gemspec['required_ruby_version'] gem.required_rubygems_version = gemspec['required_rubygems_version'] gem.post_install_message = gemspec['post_install_message'] split = lambda { |string| string.split(/,\s*/) } if gemspec['dependencies'] gemspec['dependencies'].each do |name,versions| gem.add_dependency(name,split[versions]) end end if gemspec['development_dependencies'] gemspec['development_dependencies'].each do |name,versions| gem.add_development_dependency(name,split[versions]) end end end