├── .document ├── .gemtest ├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── .rspec ├── .yardopts ├── ChangeLog.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── gemspec.yml ├── lib ├── tdiff.rb └── tdiff │ ├── tdiff.rb │ ├── unordered.rb │ └── version.rb ├── spec ├── classes │ └── node.rb ├── helpers │ └── trees.rb ├── spec_helper.rb ├── tdiff_examples.rb ├── tdiff_spec.rb └── unordered_spec.rb └── tdiff.gemspec /.document: -------------------------------------------------------------------------------- 1 | - 2 | ChangeLog.md 3 | LICENSE.txt 4 | -------------------------------------------------------------------------------- /.gemtest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postmodern/tdiff/a3fce27e234db4fb12c93859c83ec85b19ec99dc/.gemtest -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | ruby: 12 | - '3.0' 13 | - '3.1' 14 | - '3.2' 15 | - '3.3' 16 | - jruby 17 | - truffleruby 18 | name: Ruby ${{ matrix.ruby }} 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up Ruby 22 | uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby }} 25 | - name: Install dependencies 26 | run: bundle install --jobs 4 --retry 3 27 | - name: Run tests 28 | run: bundle exec rake test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /Gemfile.lock 2 | /coverage 3 | /doc 4 | /pkg 5 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour --format documentation 2 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --markup markdown --title "TDiff Documentation" --protected 2 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | ### 0.4.0 / 2024-01-24 2 | 3 | * Require [ruby](http://www.ruby-lang.org/) >= 2.0.0. 4 | * Switched to using `require_relative` to improve load-times. 5 | * Added `# frozen_string_literal: true` to all files. 6 | 7 | ### 0.3.4 / 2018-06-11 8 | 9 | * Fixed shadowed variable warning (@bhollis). 10 | 11 | ### 0.3.3 / 2012-05-28 12 | 13 | * Require ruby >= 1.8.7. 14 | * Added {TDiff::VERSION}. 15 | * Replaced ore-tasks with 16 | [rubygems-tasks](https://github.com/postmodern/rubygems-tasks#readme). 17 | 18 | ### 0.3.2 / 2010-11-28 19 | 20 | * Added {TDiff#tdiff_recursive} to only handle recursively traversing 21 | and diffing the children nodes. 22 | * Added {TDiff::Unordered#tdiff_recursive_unordered} to only handle 23 | recursively traversing and diffing the children nodes, without respecting 24 | the order of the nodes. 25 | 26 | ### 0.3.1 / 2010-11-28 27 | 28 | * Fixed a typo in {TDiff::Unordered#tdiff_unordered}, which was causing 29 | all nodes to be marked as added. 30 | 31 | ### 0.3.0 / 2010-11-15 32 | 33 | * Changed {TDiff#tdiff_equal} to compare `self` with another node. 34 | 35 | ### 0.2.0 / 2010-11-14 36 | 37 | * Added {TDiff::Unordered}. 38 | 39 | ### 0.1.0 / 2010-11-13 40 | 41 | * Initial release: 42 | * Provides the {TDiff} mixin. 43 | * Allows custom node equality and traversal logic by overriding the 44 | {TDiff#tdiff_equal} and {TDiff#tdiff_each_child} methods. 45 | * Implements the [Longest Common Subsequence (LCS)](http://en.wikipedia.org/wiki/Longest_common_subsequence_problem). 46 | 47 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem 'rake' 7 | gem 'rubygems-tasks', '~> 0.1' 8 | 9 | gem 'rspec', '~> 3.0' 10 | gem 'simplecov', '~> 0.20', require: false 11 | 12 | gem 'kramdown' 13 | gem 'redcarpet', platform: :mri 14 | gem 'yard', '~> 0.9' 15 | end 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2024 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. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TDiff 2 | 3 | [](https://github.com/postmodern/tdiff/actions/workflows/ruby.yml) 4 | 5 | * [Source](https://github.com/postmodern/tdiff) 6 | * [Issues](https://github.com/postmodern/tdiff/issues) 7 | * [Documentation](https://rubydoc.info/gems/tdiff) 8 | 9 | ## Description 10 | 11 | Calculates the differences between two tree-like structures. Similar to 12 | Rubys built-in [TSort](http://rubydoc.info/docs/ruby-stdlib/1.9.2/TSort) 13 | module. 14 | 15 | ## Features 16 | 17 | * Provides the {TDiff} mixin. 18 | * Provides the {TDiff::Unordered} mixin for unordered diffing. 19 | * Allows custom node equality and traversal logic by overriding the 20 | {TDiff#tdiff_equal} and {TDiff#tdiff_each_child} methods. 21 | * Implements the [Longest Common Subsequence (LCS)](http://en.wikipedia.org/wiki/Longest_common_subsequence_problem) algorithm. 22 | 23 | ## Examples 24 | 25 | Diff two HTML documents: 26 | 27 | ```ruby 28 | require 'nokogiri' 29 | require 'tdiff' 30 | 31 | class Nokogiri::XML::Node 32 | 33 | include TDiff 34 | 35 | def tdiff_equal(node) 36 | if (self.text? && node.text?) 37 | self.text == node.text 38 | elsif (self.respond_to?(:root) && node.respond_to?(:root)) 39 | self.root.tdiff_equal(node.root) 40 | elsif (self.respond_to?(:name) && node.respond_to?(:name)) 41 | self.name == node.name 42 | else 43 | false 44 | end 45 | end 46 | 47 | def tdiff_each_child(node,&block) 48 | node.children.each(&block) 49 | end 50 | 51 | end 52 | 53 | doc1 = Nokogiri::HTML('
one
three
one
two
three
one
/html/body/div 65 | + /html/body/div 66 |one
/html/body/div 67 | /html/body/div 68 |three
/html/body/div 69 | - one /html/body/div/p[1] 70 | + two /html/body/div/p[2] 71 | three /html/body/div/p[2] 72 | ``` 73 | 74 | ## Requirements 75 | 76 | * [ruby](http://www.ruby-lang.org/) >= 2.0.0 77 | 78 | ## Install 79 | 80 | ```shell 81 | $ gem install tdiff 82 | ``` 83 | 84 | ## Copyright 85 | 86 | See {file:LICENSE.txt} for details. 87 | 88 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems/tasks' 2 | Gem::Tasks.new 3 | 4 | require 'rspec/core/rake_task' 5 | RSpec::Core::RakeTask.new 6 | task :test => :spec 7 | task :default => :spec 8 | 9 | require 'yard' 10 | YARD::Rake::YardocTask.new 11 | -------------------------------------------------------------------------------- /gemspec.yml: -------------------------------------------------------------------------------- 1 | name: tdiff 2 | summary: Calculates the differences between two tree-like structures. 3 | description: 4 | Calculates the differences between two tree-like structures. Similar to 5 | Rubys built-in TSort module. 6 | 7 | license: MIT 8 | authors: Postmodern 9 | email: postmodern.mod3@gmail.com 10 | homepage: https://github.com/postmodern/tdiff#readme 11 | has_yard: true 12 | 13 | metadata: 14 | documentation_uri: https://rubydoc.info/gems/tdiff 15 | source_code_uri: https://github.com/postmodern/tdiff 16 | bug_tracker_uri: https://github.com/postmodern/tdiff/issues 17 | changelog_uri: https://github.com/postmodern/tdiff/blob/main/ChangeLog.md 18 | rubygems_mfa_required: 'true' 19 | 20 | required_ruby_version: ">= 2.0.0" 21 | 22 | development_dependencies: 23 | bundler: ~> 2.0 24 | -------------------------------------------------------------------------------- /lib/tdiff.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'tdiff/tdiff' 4 | require_relative 'tdiff/unordered' 5 | require_relative 'tdiff/version' 6 | -------------------------------------------------------------------------------- /lib/tdiff/tdiff.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # 4 | # {TDiff} adds the ability to calculate the differences between two tree-like 5 | # objects. Simply include {TDiff} into the class which represents the tree 6 | # nodes and define the {#tdiff_each_child} and {#tdiff_equal} methods. 7 | # 8 | module TDiff 9 | # 10 | # Default method which will enumerate over every child of a parent node. 11 | # 12 | # @param [Object] node 13 | # The parent node. 14 | # 15 | # @yield [child] 16 | # The given block will be passed each child of the parent node. 17 | # 18 | def tdiff_each_child(node,&block) 19 | node.each(&block) if node.kind_of?(Enumerable) 20 | end 21 | 22 | # 23 | # Default method which compares nodes. 24 | # 25 | # @param [Object] node 26 | # A node from the new tree. 27 | # 28 | # @return [Boolean] 29 | # Specifies whether the nodes are equal. 30 | # 31 | def tdiff_equal(node) 32 | self == node 33 | end 34 | 35 | # 36 | # Finds the differences between `self` and another tree. 37 | # 38 | # @param [#tdiff_each_child] tree 39 | # The other tree. 40 | # 41 | # @yield [change, node] 42 | # The given block will be passed the added or removed nodes. 43 | # 44 | # @yieldparam [' ', '+', '-'] change 45 | # The state-change of the node. 46 | # 47 | # @yieldparam [Object] node 48 | # A node from one of the two trees. 49 | # 50 | # @return [Enumerator] 51 | # If no block is given, an Enumerator object will be returned. 52 | # 53 | def tdiff(tree,&block) 54 | return enum_for(:tdiff,tree) unless block 55 | 56 | # check if the nodes differ 57 | unless tdiff_equal(tree) 58 | yield '-', self 59 | yield '+', tree 60 | return self 61 | end 62 | 63 | yield ' ', self 64 | 65 | tdiff_recursive(tree,&block) 66 | return self 67 | end 68 | 69 | protected 70 | 71 | # 72 | # Recursively compares the differences between the children nodes. 73 | # 74 | # @param [#tdiff_each_child] tree 75 | # The other tree. 76 | # 77 | # @yield [change, node] 78 | # The given block will be passed the added or removed nodes. 79 | # 80 | # @yieldparam [' ', '+', '-'] change 81 | # The state-change of the node. 82 | # 83 | # @yieldparam [Object] node 84 | # A node from one of the two trees. 85 | # 86 | # @since 0.3.2 87 | # 88 | def tdiff_recursive(tree,&block) 89 | c = Hash.new { |hash,key| hash[key] = Hash.new(0) } 90 | x = enum_for(:tdiff_each_child,self) 91 | y = enum_for(:tdiff_each_child,tree) 92 | 93 | x.each_with_index do |xi,i| 94 | y.each_with_index do |yj,j| 95 | c[i][j] = if xi.tdiff_equal(yj) 96 | c[i-1][j-1] + 1 97 | else 98 | if c[i][j-1] > c[i-1][j] 99 | c[i][j-1] 100 | else 101 | c[i-1][j] 102 | end 103 | end 104 | end 105 | end 106 | 107 | unchanged = [] 108 | changes = [] 109 | 110 | x_backtrack = x.each_with_index.reverse_each 111 | y_backtrack = y.each_with_index.reverse_each 112 | 113 | next_child = lambda { |children| 114 | begin 115 | children.next 116 | rescue StopIteration 117 | # end of iteration, return a -1 index 118 | [nil, -1] 119 | end 120 | } 121 | 122 | xi, i = next_child[x_backtrack] 123 | yj, j = next_child[y_backtrack] 124 | 125 | until (i == -1 && j == -1) 126 | if (i != -1 && j != -1 && xi.tdiff_equal(yj)) 127 | changes.unshift [' ', xi] 128 | unchanged.unshift [xi, yj] 129 | 130 | xi, i = next_child[x_backtrack] 131 | yj, j = next_child[y_backtrack] 132 | else 133 | if (j >= 0 && (i == -1 || c[i][j-1] >= c[i-1][j])) 134 | changes.unshift ['+', yj] 135 | 136 | yj, j = next_child[y_backtrack] 137 | elsif (i >= 0 && (j == -1 || c[i][j-1] < c[i-1][j])) 138 | changes.unshift ['-', xi] 139 | 140 | xi, i = next_child[x_backtrack] 141 | end 142 | end 143 | end 144 | 145 | # explicitly discard the c matrix 146 | c = nil 147 | 148 | # sequentially iterate over the changed nodes 149 | changes.each(&block) 150 | changes = nil 151 | 152 | # recurse down through unchanged nodes 153 | unchanged.each { |a,b| a.tdiff_recursive(b,&block) } 154 | unchanged = nil 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /lib/tdiff/unordered.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'tdiff' 4 | 5 | module TDiff 6 | # 7 | # Calculates the differences between two trees, without respecting the 8 | # order of children nodes. 9 | # 10 | module Unordered 11 | # 12 | # Includes {TDiff}. 13 | # 14 | def self.included(base) 15 | base.send :include, TDiff 16 | end 17 | 18 | # 19 | # Finds the differences between `self` and another tree, not respecting 20 | # the order of the nodes. 21 | # 22 | # @param [#tdiff_each_child] tree 23 | # The other tree. 24 | # 25 | # @yield [change, node] 26 | # The given block will be passed the added or removed nodes. 27 | # 28 | # @yieldparam [' ', '+', '-'] change 29 | # The state-change of the node. 30 | # 31 | # @yieldparam [Object] node 32 | # A node from one of the two trees. 33 | # 34 | # @return [Enumerator] 35 | # If no block is given, an Enumerator object will be returned. 36 | # 37 | # @since 0.2.0 38 | # 39 | def tdiff_unordered(tree,&block) 40 | return enum_for(:tdiff_unordered,tree) unless block 41 | 42 | # check if the nodes differ 43 | unless tdiff_equal(tree) 44 | yield '-', self 45 | yield '+', tree 46 | return self 47 | end 48 | 49 | yield ' ', self 50 | 51 | tdiff_recursive_unordered(tree,&block) 52 | return self 53 | end 54 | 55 | protected 56 | 57 | # 58 | # Recursively compares the differences between the children nodes, 59 | # without respecting the order of the nodes. 60 | # 61 | # @param [#tdiff_each_child] tree 62 | # The other tree. 63 | # 64 | # @yield [change, node] 65 | # The given block will be passed the added or removed nodes. 66 | # 67 | # @yieldparam [' ', '+', '-'] change 68 | # The state-change of the node. 69 | # 70 | # @yieldparam [Object] node 71 | # A node from one of the two trees. 72 | # 73 | # @since 0.3.2 74 | # 75 | def tdiff_recursive_unordered(tree,&block) 76 | x = enum_for(:tdiff_each_child,self) 77 | y = enum_for(:tdiff_each_child,tree) 78 | 79 | unchanged = {} 80 | changes = [] 81 | 82 | x.each_with_index do |xi,i| 83 | y.each_with_index do |yj,j| 84 | if (!unchanged.has_value?(yj) && xi.tdiff_equal(yj)) 85 | unchanged[xi] = yj 86 | changes << [i, ' ', xi] 87 | break 88 | end 89 | end 90 | 91 | unless unchanged.has_key?(xi) 92 | changes << [i, '-', xi] 93 | end 94 | end 95 | 96 | y.each_with_index do |yj,j| 97 | unless unchanged.has_value?(yj) 98 | changes << [j, '+', yj] 99 | end 100 | end 101 | 102 | # order the changes by index to match the behavior of `tdiff` 103 | changes.sort_by { |change| change[0] }.each do |index,change,node| 104 | yield change, node 105 | end 106 | 107 | # explicitly release the changes variable 108 | changes = nil 109 | 110 | # recurse down the unchanged nodes 111 | unchanged.each do |xi,yj| 112 | xi.tdiff_recursive_unordered(yj,&block) 113 | end 114 | 115 | unchanged = nil 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/tdiff/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TDiff 4 | VERSION = '0.4.0' 5 | end 6 | -------------------------------------------------------------------------------- /spec/classes/node.rb: -------------------------------------------------------------------------------- 1 | require 'tdiff' 2 | 3 | class Node < Struct.new(:name, :children) 4 | 5 | include TDiff 6 | include TDiff::Unordered 7 | 8 | def tdiff_each_child(node,&block) 9 | node.children.each(&block) 10 | end 11 | 12 | def tdiff_equal(node) 13 | self.name == node.name 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /spec/helpers/trees.rb: -------------------------------------------------------------------------------- 1 | require 'classes/node' 2 | 3 | module Helpers 4 | module Trees 5 | def self.included(base) 6 | base.module_eval do 7 | before(:all) do 8 | @tree = Node.new('root', [ 9 | Node.new('leaf1', [ 10 | Node.new('subleaf1', []), 11 | Node.new('subleaf2', []) 12 | ]), 13 | 14 | Node.new('leaf2', [ 15 | Node.new('subleaf1', []), 16 | Node.new('subleaf2', []) 17 | ]) 18 | ]) 19 | 20 | @different_root = Node.new('wrong', []) 21 | 22 | @added = Node.new('root', [ 23 | Node.new('leaf1', [ 24 | Node.new('subleaf1', []), 25 | Node.new('subleaf3', []), 26 | Node.new('subleaf2', []) 27 | ]), 28 | 29 | Node.new('leaf2', [ 30 | Node.new('subleaf1', []), 31 | Node.new('subleaf2', []) 32 | ]) 33 | ]) 34 | 35 | @removed = Node.new('root', [ 36 | Node.new('leaf1', [ 37 | Node.new('subleaf1', []) 38 | ]), 39 | 40 | Node.new('leaf2', [ 41 | Node.new('subleaf1', []), 42 | Node.new('subleaf2', []) 43 | ]) 44 | ]) 45 | 46 | @changed_order = Node.new('root', [ 47 | Node.new('leaf2', [ 48 | Node.new('subleaf1', []), 49 | Node.new('subleaf2', []) 50 | ]), 51 | 52 | Node.new('leaf1', [ 53 | Node.new('subleaf1', []), 54 | Node.new('subleaf2', []) 55 | ]) 56 | ]) 57 | end 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'simplecov' 3 | SimpleCov.start 4 | 5 | require 'helpers/trees' 6 | -------------------------------------------------------------------------------- /spec/tdiff_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'helpers/trees' 3 | 4 | shared_examples_for 'TDiff' do |method| 5 | include Helpers::Trees 6 | 7 | it "should tell if two trees are identical" do 8 | expect( 9 | @tree.send(method,@tree).all? { |change,node| change == ' ' } 10 | ).to be true 11 | end 12 | 13 | it "should stop if the root nodes have changed" do 14 | changes = @tree.send(method,@different_root).to_a 15 | 16 | expect(changes.length).to be 2 17 | 18 | expect(changes[0][0]).to be == '-' 19 | expect(changes[0][1]).to be == @tree 20 | 21 | expect(changes[1][0]).to be == '+' 22 | expect(changes[1][1]).to be == @different_root 23 | end 24 | 25 | it "should tell when sub-nodes are added" do 26 | changes = @tree.send(method,@added).select { |change,node| change == '+' } 27 | 28 | expect(changes.length).to be 1 29 | expect(changes[0][0]).to be == '+' 30 | expect(changes[0][1]).to be == @added.children[0].children[1] 31 | end 32 | 33 | it "should tell when sub-nodes are removed" do 34 | changes = @tree.send(method,@removed).select { |change,node| change == '-' } 35 | 36 | expect(changes.length).to be 1 37 | expect(changes[0][0]).to be == '-' 38 | expect(changes[0][1]).to be == @tree.children[0].children[1] 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/tdiff_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tdiff_examples' 3 | 4 | require 'tdiff/tdiff' 5 | 6 | describe TDiff do 7 | include Helpers::Trees 8 | 9 | it_should_behave_like 'TDiff', :tdiff 10 | 11 | it "should detect when the order of children has changed" do 12 | changes = @tree.tdiff(@changed_order).to_a 13 | 14 | expect(changes.length).to be == 6 15 | 16 | expect(changes[0][0]).to be == ' ' 17 | expect(changes[0][1]).to be == @tree 18 | 19 | expect(changes[1][0]).to be == '-' 20 | expect(changes[1][1]).to be == @tree.children[0] 21 | 22 | expect(changes[2][0]).to be == ' ' 23 | expect(changes[2][1]).to be == @tree.children[1] 24 | 25 | expect(changes[3][0]).to be == '+' 26 | expect(changes[3][1]).to be == @changed_order.children[1] 27 | 28 | expect(changes[4][0]).to be == ' ' 29 | expect(changes[4][1]).to be == @tree.children[1].children[0] 30 | 31 | expect(changes[5][0]).to be == ' ' 32 | expect(changes[5][1]).to be == @tree.children[1].children[1] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/unordered_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tdiff_examples' 3 | 4 | require 'tdiff/unordered' 5 | 6 | describe TDiff::Unordered do 7 | include Helpers::Trees 8 | 9 | it "should include TDiff when included" do 10 | base = Class.new do 11 | include TDiff::Unordered 12 | end 13 | 14 | expect(base).to include(TDiff) 15 | end 16 | 17 | it_should_behave_like 'TDiff', :tdiff_unordered 18 | 19 | it "should not detect when the order of children has changed" do 20 | changes = @tree.tdiff_unordered(@changed_order).select do |change,node| 21 | change != ' ' 22 | end 23 | 24 | expect(changes).to be_empty 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /tdiff.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'yaml' 4 | 5 | Gem::Specification.new do |gem| 6 | gemspec = YAML.load_file('gemspec.yml') 7 | 8 | gem.name = gemspec.fetch('name') 9 | gem.version = gemspec.fetch('version') do 10 | require_relative 'lib/tdiff/version' 11 | TDiff::VERSION 12 | end 13 | 14 | gem.summary = gemspec['summary'] 15 | gem.description = gemspec['description'] 16 | gem.licenses = Array(gemspec['license']) 17 | gem.authors = Array(gemspec['authors']) 18 | gem.email = gemspec['email'] 19 | gem.homepage = gemspec['homepage'] 20 | 21 | glob = lambda { |patterns| gem.files & Dir[*patterns] } 22 | 23 | gem.files = `git ls-files`.split($/) 24 | gem.files = glob[gemspec['files']] if gemspec['files'] 25 | 26 | gem.executables = gemspec.fetch('executables') do 27 | glob['bin/*'].map { |path| File.basename(path) } 28 | end 29 | gem.default_executable = gem.executables.first if Gem::VERSION < '1.7.' 30 | 31 | gem.extensions = glob[gemspec['extensions'] || 'ext/**/extconf.rb'] 32 | gem.test_files = glob[gemspec['test_files'] || '{test/{**/}*_test.rb'] 33 | gem.extra_rdoc_files = glob[gemspec['extra_doc_files'] || '*.{txt,md}'] 34 | 35 | gem.require_paths = Array(gemspec.fetch('require_paths') { 36 | %w[ext lib].select { |dir| File.directory?(dir) } 37 | }) 38 | 39 | gem.requirements = gemspec['requirements'] 40 | gem.required_ruby_version = gemspec['required_ruby_version'] 41 | gem.required_rubygems_version = gemspec['required_rubygems_version'] 42 | gem.post_install_message = gemspec['post_install_message'] 43 | 44 | split = lambda { |string| string.split(/,\s*/) } 45 | 46 | if gemspec['dependencies'] 47 | gemspec['dependencies'].each do |name,versions| 48 | gem.add_dependency(name,split[versions]) 49 | end 50 | end 51 | 52 | if gemspec['development_dependencies'] 53 | gemspec['development_dependencies'].each do |name,versions| 54 | gem.add_development_dependency(name,split[versions]) 55 | end 56 | end 57 | end 58 | --------------------------------------------------------------------------------