├── .gemtest ├── .github └── workflows │ ├── coveralls.yml │ └── ruby.yml ├── .gitignore ├── .readthedocs.yaml ├── .rubocop.yml ├── .rubocop_todo.yml ├── .ruby-gemset ├── .ruby-version ├── API-CHANGES.md ├── Gemfile ├── Gemfile.lock ├── History.md ├── LICENSE.md ├── README.md ├── Rakefile ├── TODO.org ├── examples └── example_basic.rb ├── lib ├── rubytree.rb ├── tree.rb └── tree │ ├── binarytree.rb │ ├── tree_deps.rb │ ├── utils │ ├── hash_converter.rb │ ├── json_converter.rb │ ├── metrics_methods.rb │ ├── path_methods.rb │ ├── tree_merge_handler.rb │ └── utils.rb │ └── version.rb ├── rubytree.gemspec ├── spec ├── spec_helper.rb └── tree_spec.rb └── test ├── run_test.rb ├── test_binarytree.rb ├── test_rubytree_require.rb ├── test_subclassed_node.rb ├── test_thread_and_fiber.rb └── test_tree.rb /.gemtest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evolve75/RubyTree/f99daed0289274485a5f7e0786b773b4acbb5f08/.gemtest -------------------------------------------------------------------------------- /.github/workflows/coveralls.yml: -------------------------------------------------------------------------------- 1 | # Check the test coverage of the code. 2 | # 3 | # This runs as a Github Action. Note that the Coverall repository token is 4 | # injected as an environment variable, instead of being read from 5 | #`.coveralls.yaml` config file. 6 | # 7 | # See https://github.com/marketplace/actions/coveralls-github-action 8 | # 9 | # Also see 10 | 11 | name: Test Coveralls 12 | on: ["push", "pull_request"] 13 | 14 | env: 15 | COVERAGE: true 16 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 17 | 18 | jobs: 19 | test: 20 | name: Ruby ${{ matrix.ruby }} 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | ruby: ["2.7", "3.0", "3.1", "3.2", "3.3"] 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - name: Clone Repository 29 | uses: actions/checkout@v2 30 | 31 | - name: Setup Ruby and bundler dependencies 32 | uses: ruby/setup-ruby@v1 33 | with: 34 | ruby-version: ${{ matrix.ruby }} 35 | bundler-cache: true 36 | 37 | - name: Run Ruby Tests 38 | run: bundle exec rake clobber test:coverage 39 | 40 | - name: Coveralls 41 | uses: coverallsapp/github-action@master 42 | with: 43 | github-token: ${{ secrets.GITHUB_TOKEN }} 44 | flag-name: ruby-${{ matrix.ruby }} 45 | parallel: true 46 | 47 | finish: 48 | needs: test 49 | runs-on: ubuntu-latest 50 | 51 | steps: 52 | - name: Coveralls Finished 53 | uses: coverallsapp/github-action@master 54 | with: 55 | github-token: ${{ secrets.GITHUB_TOKEN }} 56 | parallel-finished: true 57 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | # Build and test the RubyTree library. 2 | # 3 | # This runs as a Github Action. 4 | 5 | name: Build and Test 6 | on: [push, pull_request] 7 | jobs: 8 | test: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | ruby: ["2.7", "3.0", "3.1", "3.2", "3.3"] 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: ${{ matrix.ruby }} 19 | bundler-cache: true 20 | - run: bundle exec rake clobber test:all gem:package 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | TAGS 3 | .yardoc/ 4 | coverage.info 5 | coverage/ 6 | doc/ 7 | pkg/ 8 | rdoc/ 9 | tmp/ 10 | .bundle/ 11 | vendor/ 12 | .idea/ 13 | Gemfile.lock 14 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Ruby and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | ruby: "3.3" 13 | commands: 14 | - gem install bundler -v 2.3 15 | - bundle install 16 | - bundle exec rake doc:yard 17 | - mkdir -p $READTHEDOCS_OUTPUT/html 18 | - cp -r doc/. $READTHEDOCS_OUTPUT/html/ 19 | 20 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Local configuration for the rubocop linter. 2 | 3 | # Inherit from the working file. 4 | inherit_from: .rubocop_todo.yml 5 | 6 | AllCops: 7 | NewCops: enable 8 | TargetRubyVersion: 2.7 9 | 10 | Lint/UselessAssignment: 11 | Exclude: 12 | - 'examples/example_basic.rb' 13 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2022-06-20 06:06:47 UTC using RuboCop version 1.24.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 47 10 | # Configuration parameters: IgnoredMethods, CountRepeatedAttributes. 11 | Metrics/AbcSize: 12 | Max: 97 13 | 14 | # Offense count: 2 15 | # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. 16 | # IgnoredMethods: refine 17 | Metrics/BlockLength: 18 | Max: 119 19 | 20 | # Offense count: 3 21 | # Configuration parameters: CountComments, CountAsOne. 22 | Metrics/ClassLength: 23 | Max: 1066 24 | 25 | # Offense count: 2 26 | # Configuration parameters: IgnoredMethods. 27 | Metrics/CyclomaticComplexity: 28 | Max: 8 29 | 30 | # Offense count: 47 31 | # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. 32 | Metrics/MethodLength: 33 | Max: 45 34 | 35 | # Offense count: 1 36 | # Configuration parameters: IgnoredMethods. 37 | Metrics/PerceivedComplexity: 38 | Max: 9 39 | 40 | # Offense count: 5 41 | Security/MarshalLoad: 42 | Exclude: 43 | - 'lib/tree.rb' 44 | - 'spec/tree_spec.rb' 45 | - 'test/test_tree.rb' 46 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | rubytree 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.7.2 2 | -------------------------------------------------------------------------------- /API-CHANGES.md: -------------------------------------------------------------------------------- 1 | # API Changes in RubyTree 2 | 3 | This lists the various API changes that have been made to the `RubyTree` 4 | package. 5 | 6 | _Note_: API changes are expected to reduce significantly after the `1.x` 7 | release. In most cases, an alternative will be provided to ensure relatively 8 | smooth transition to the new APIs. 9 | 10 | ## Release 2.1.0 Changes 11 | 12 | * Minimum Ruby version has been bumped to 2.7 and above 13 | 14 | * Updated all upstream dependencies to their latest stable versions 15 | 16 | ## Release 2.0.0 Changes 17 | 18 | * The _minimum_ required Ruby version is now `2.6` (or higher). 19 | 20 | * The long-broken `Tree::TreeNode#depth` method has _finally_ been removed. Use 21 | [Tree::TreeNode#node_depth][node_depth] instead. 22 | 23 | * Support for `CamelCase` methods has been removed. This was a legacy shim 24 | that has hopefully outlived its usefulness. 25 | 26 | * Use of integers as node-names now no longer requires the optional 27 | `num_as_name` method argument. 28 | 29 | * The predicate methods beginning with `is_` or `has_` are now aliases to the 30 | real methods **without** these prefixes. For example, 31 | `Tree::TreeNode#is_root?` is now aliased to `Tree::TreeNode#root?`. This is to 32 | comply with the Ruby standard. These original prefixed method names should be 33 | considered as deprecated and the corresponding non-prefixed method names 34 | should be used instead. it is possible that the old prefixed method names 35 | might be removed in the future. 36 | 37 | * [structured_warnings][] has been **removed** from the code-base and is no 38 | longer a dependency. This was a long-standing point of friction for many 39 | users. 40 | 41 | ## Release 0.9.5 Changes 42 | 43 | * The [Tree::TreeNode#add][add] method now provides **move** semantics, if a 44 | child node on an existing tree is added to another tree, or another location 45 | on the same tree. In this situation, the child node is removed from its old 46 | position and added to the new parent node. After the add operation is 47 | complete, the child no longer exists on the old tree/location. 48 | 49 | ## Release 0.9.3 Changes 50 | 51 | * Validation for unique node names has changed in the [Tree::TreeNode#add][add] 52 | method. `RubyTree` no longer enforces globally unique names. The node-names 53 | need to be unique _only between_ the sibling nodes. 54 | 55 | ## Release 0.9.0 Changes 56 | 57 | * New post-ordered traversal via the 58 | [Tree::TreeNode#postordered_each][postordered_each] method. 59 | 60 | * The Binary Tree implementation now supports in-order traversal via the 61 | [Tree::BinaryTreeNode#inordered_each][inordered_each] method. 62 | 63 | * `RubyTree` now mixes in the 64 | [Comparable](http://ruby-doc.org/core-1.8.7/Comparable.html) module. 65 | 66 | * The traversal methods ([Tree::TreeNode#each][each], 67 | [Tree::TreeNode#preordered_each][preordered_each], 68 | [Tree::TreeNode#postordered_each][postordered_each] and 69 | [Tree::TreeNode#breadth_each][breadth_each] now correctly return an 70 | [Enumerator](rdoc-ref:http://ruby-doc.org/core-1.8.7/Enumerable.html) as the 71 | return value when no block is given, and return the receiver node if a block 72 | was provided. This is consistent with how the standard Ruby collections work. 73 | 74 | ## Release 0.8.3 Changes 75 | 76 | * [Tree::TreeNode#siblings][siblings] will now return an empty array for the 77 | root node. 78 | 79 | ## Release 0.8.0 Changes 80 | 81 | * Added the ability to specify an optional insertion position in the 82 | [Tree::TreeNode#add][add] method. Idea and original code contributed by Dirk. 83 | 84 | * Added a new method 85 | [Tree::TreeNode#detached_subtree_copy][detached_subtree_copy] to allow cloning 86 | the entire tree. This method is also aliased to 87 | [Tree::TreeNode#dup][dup]. Idea and original code contributed by Vincenzo 88 | Farruggia. 89 | 90 | * Converted all _CamelCase_ method names to the canonical ruby_method_names 91 | (underscore separated). The CamelCase methods can still be invoked, but 92 | will throw a deprecated warning. 93 | 94 | ## Release 0.7.0 Changes 95 | 96 | * Converted all exceptions thrown on invalid method arguments to from 97 | `RuntimeError` to `ArgumentError`. This impacts the following methods: 98 | 99 | * [Tree::TreeNode#initialize][initialize] 100 | * [Tree::TreeNode#add][add] 101 | * [Tree::TreeNode#[]][access] 102 | * [Tree::BinaryTreeNode#add][btree_add] 103 | 104 | 105 | * Added [Tree::TreeNode#level][level] as an alias for 106 | [Tree::TreeNode#node_depth][node_depth] 107 | 108 | * Added new methods [Tree::TreeNode#in_degree][in_degree] and 109 | [Tree::TreeNode#out_degree][out_degree] to report the node's degree stats. 110 | 111 | * [Tree::TreeNode#is_only_child?][is_only_child] now returns `true` for a root 112 | node. 113 | 114 | * [Tree::TreeNode#next_sibling][next_sibling] and 115 | [Tree::TreeNode#previous_sibling][previous_sibling] now return `nil` for a 116 | root node. 117 | 118 | * [Tree::TreeNode#add][add] and [Tree::TreeNode#<<][append] now throw an 119 | `ArgumentError` exception if a `nil` node is passed as an argument. 120 | 121 | * Added new methods 122 | [Tree::TreeNode#to_json][to_json] and 123 | [Tree::TreeNode#json_create][json_create] 124 | to convert to/from the JSON format. Thanks to 125 | [Dirk](http://github.com/railsbros-dirk) for this change. 126 | 127 | ## Release 0.6.1 Changes 128 | 129 | * Deprecated the [Tree::Utils::TreeMetricsHandler#depth][depth] method as it was 130 | returning an incorrect depth value. Have introduced a new replacement method 131 | [Tree::Utils::TreeMetricsHandler#node_depth][node_depth] which returns the 132 | correct result. 133 | 134 | 135 | [structured_warnings]: https://github.com/schmidt/structured_warnings 136 | 137 | [access]: rdoc-ref:Tree::TreeNode#[] 138 | [add]: rdoc-ref:Tree::TreeNode#add 139 | [append]: rdoc-ref:Tree::TreeNode#<< 140 | [breadth_each]: rdoc-ref:Tree::TreeNode#breadth_each 141 | [btree_add]: rdoc-ref:Tree::BinaryTreeNode#add 142 | [depth]: rdoc-ref:Tree::Utils::TreeMetricsHandler#depth 143 | [detached_subtree_copy]: rdoc-ref:Tree::TreeNode#detached_subtree_copy 144 | [dup]: rdoc-ref:Tree::TreeNode#dup 145 | [each]: rdoc-ref:Tree::TreeNode#each 146 | [in_degree]: rdoc-ref:Tree::Utils::TreeMetricsHandler#in_degree 147 | [initialize]: rdoc-ref:Tree::TreeNode#initialize 148 | [inordered_each]: rdoc-ref:Tree::BinaryTreeNode#inordered_each 149 | [is_only_child]: rdoc-ref:Tree::TreeNode#is_only_child? 150 | [json_create]: rdoc-ref:Tree::Utils::JSONConverter::ClassMethods#json_create 151 | [level]: rdoc-ref:Tree::Utils::TreeMetricsHandler#level 152 | [next_sibling]: rdoc-ref:Tree::TreeNode#next_sibling 153 | [node_depth]: rdoc-ref:Tree::Utils::TreeMetricsHandler#node_depth 154 | [out_degree]: rdoc-ref:Tree::Utils::TreeMetricsHandler#out_degree 155 | [postordered_each]: rdoc-ref:Tree::TreeNode#postordered_each 156 | [preordered_each]: rdoc-ref:Tree::TreeNode#preordered_each 157 | [previous_sibling]: rdoc-ref:Tree::TreeNode#previous_sibling 158 | [siblings]: rdoc-ref:Tree::TreeNode#siblings 159 | [to_json]: rdoc-ref:Tree::Utils::JSONConverter#to_json 160 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | rubytree (2.1.1) 5 | json (~> 2.0, > 2.9) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | ast (2.4.2) 11 | date (3.4.1) 12 | diff-lcs (1.5.1) 13 | docile (1.4.1) 14 | json (2.9.1) 15 | language_server-protocol (3.17.0.3) 16 | parallel (1.26.3) 17 | parser (3.3.6.0) 18 | ast (~> 2.4.1) 19 | racc 20 | power_assert (2.0.4) 21 | psych (5.2.2) 22 | date 23 | stringio 24 | racc (1.8.1) 25 | rainbow (3.1.1) 26 | rake (13.2.1) 27 | rdoc (6.10.0) 28 | psych (>= 4.0.0) 29 | regexp_parser (2.9.3) 30 | rspec (3.13.0) 31 | rspec-core (~> 3.13.0) 32 | rspec-expectations (~> 3.13.0) 33 | rspec-mocks (~> 3.13.0) 34 | rspec-core (3.13.2) 35 | rspec-support (~> 3.13.0) 36 | rspec-expectations (3.13.3) 37 | diff-lcs (>= 1.2.0, < 2.0) 38 | rspec-support (~> 3.13.0) 39 | rspec-mocks (3.13.2) 40 | diff-lcs (>= 1.2.0, < 2.0) 41 | rspec-support (~> 3.13.0) 42 | rspec-support (3.13.2) 43 | rtags (0.97) 44 | rtagstask (0.0.4) 45 | rtags (> 0.0.0) 46 | rubocop (1.69.2) 47 | json (~> 2.3) 48 | language_server-protocol (>= 3.17.0) 49 | parallel (~> 1.10) 50 | parser (>= 3.3.0.2) 51 | rainbow (>= 2.2.2, < 4.0) 52 | regexp_parser (>= 2.9.3, < 3.0) 53 | rubocop-ast (>= 1.36.2, < 2.0) 54 | ruby-progressbar (~> 1.7) 55 | unicode-display_width (>= 2.4.0, < 4.0) 56 | rubocop-ast (1.37.0) 57 | parser (>= 3.3.1.0) 58 | rubocop-rake (0.6.0) 59 | rubocop (~> 1.0) 60 | rubocop-rspec (3.3.0) 61 | rubocop (~> 1.61) 62 | ruby-progressbar (1.13.0) 63 | simplecov (0.22.0) 64 | docile (~> 1.1) 65 | simplecov-html (~> 0.11) 66 | simplecov_json_formatter (~> 0.1) 67 | simplecov-html (0.13.1) 68 | simplecov-lcov (0.8.0) 69 | simplecov_json_formatter (0.1.4) 70 | stringio (3.1.2) 71 | test-unit (3.6.7) 72 | power_assert 73 | unicode-display_width (3.1.2) 74 | unicode-emoji (~> 4.0, >= 4.0.4) 75 | unicode-emoji (4.0.4) 76 | yard (0.9.37) 77 | 78 | PLATFORMS 79 | ruby 80 | 81 | DEPENDENCIES 82 | bundler (~> 2.3) 83 | rake (~> 13.2) 84 | rdoc (~> 6.10) 85 | rspec (~> 3.0, >= 3.13) 86 | rtagstask (~> 0.0.4) 87 | rubocop (~> 1.69) 88 | rubocop-rake (~> 0.6) 89 | rubocop-rspec (~> 3.3) 90 | rubytree! 91 | simplecov (~> 0.22) 92 | simplecov-lcov (~> 0.8) 93 | test-unit (~> 3.6) 94 | yard (~> 0.0, >= 0.9.37) 95 | 96 | BUNDLED WITH 97 | 2.3.4 98 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | # History of Changes 2 | 3 | ### 2.1.1 / 2024-12-19 4 | 5 | * 2.1.1 is a minor update that updates all dependencies and updates the guard 6 | clause for creating a tree from a hash. 7 | 8 | ### 2.1.0 / 2024-08-12 9 | 10 | * Minimum Ruby version has been bumped to 2.7. This is needed to use the 11 | upstream gems, many of which no longer support 2.6. 12 | 13 | * Updated all dependencies to their latest stable versions. 14 | 15 | ### 2.0.0 / 2022-06-21 16 | 17 | * A major release with significant modernization to the code base and removal of 18 | legacy cruft, thanks to [jmortlock][]. 19 | 20 | * The long deprecated `Tree::TreeNode#depth` method has finally been 21 | **removed**. Use [Tree::TreeNode#node_depth][node_depth] instead. 22 | 23 | * Support for `CamelCase` methods has been dropped. 24 | 25 | * The predicate methods beginning with `is_` or `has_` are now aliases to real 26 | methods **without** these prefixes. For example, `Tree::TreeNode#is_root?` is 27 | now aliased to `Tree::TreeNode#root?`. This is to comply with the Ruby 28 | standard. The original prefixed method names should be considered as 29 | deprecated and the corresponding non-prefixed method names should be used 30 | instead. it is possible that the old prefixed method names might be removed in 31 | the future. 32 | 33 | * RubyTree now supports MRI Ruby versions `2.6.x`, `2.7.x`, and `3.0.x`. 34 | 35 | * Explicit support for `rbx` Ruby has been removed (_might_ still work, but not 36 | tested.) 37 | 38 | * RubyTree now uses [Github Workflows][workflow] for its CI pipeline. 39 | 40 | * RubyTree now allows proper sub-classing of [Tree::TreeNode][TreeNode]. Thanks to 41 | [jack12816][] for this. 42 | 43 | * RubyTree now correctly handles creating detached copies of un-clonable objects 44 | such as `:symbol`, `true|false`, etc. Thanks to [igneus][] for this. 45 | 46 | ### 1.0.2 / 2021-12-29 47 | 48 | * A minor maintenance version to address a minor but annoying warning for 49 | circular dependencies. 50 | 51 | ### 1.0.1 / 2021-12-29 52 | 53 | * Updated all dependencies (dev and runtime) to their _latest_ stable 54 | releases. This is to primarily address potential CVE exposures from upstream 55 | libraries. 56 | 57 | * Updated the supported version of MRI Ruby to `2.7.x`. 58 | 59 | * Minor code cleanup using the safe automated corrections using `rubocop`. 60 | 61 | * Note that this was never released to . 62 | 63 | ### 1.0.0 / 2017-12-21 64 | 65 | * Finally! Released version `1.0.0`. 66 | 67 | * This is a maintenance release that updates the dependent gem versions and 68 | addresses a few security vulnerabilities for older upstream gem packages. 69 | 70 | * With this release, Rubytree now requires Ruby version `2.2.x` or higher. 71 | 72 | ### 0.9.7 / 2015-12-31 73 | 74 | * Released `0.9.6`. This is a minor bug-fix release. 75 | 76 | * This release allows the [Tree::TreeNode#print_tree][print_tree] method to be 77 | used on non-root nodes. Thanks to [Ojab][Ojab]. 78 | 79 | * The spaceship operator (`<=>`) now returns `nil` if the object being compared 80 | to is _itself_ `nil` or not another [Tree::TreeNode][TreeNode]. 81 | 82 | ### 0.9.6 / 2015-05-30 83 | 84 | * Released `0.9.6`, which is identical to `0.9.5`, _except_ for an update to the 85 | gem's release date. 86 | 87 | ### 0.9.5 / 2015-05-30 88 | 89 | * Released `0.9.5`. 90 | 91 | ### 0.9.5pre7 / 2015-05-30 92 | 93 | * Added new methods for getting the path of a node as a `string`. These have 94 | been added as a new `mixin` 95 | [Tree::Utils::TreePathHandler][TreePathHandler]. Thanks to [Marco][]. 96 | 97 | ### 0.9.5pre5 / 2015-01-01 98 | 99 | * Fixed [bug-32][] and enabled _move_ semantics on the [Tree::TreeNode#add][add] 100 | method, so that if a child is added, which has an existing parent, then it 101 | will be _removed_ from its old parent, prior to being added to the new location. 102 | 103 | ### 0.9.5pre4 / 2014-12-17 104 | 105 | * Added performance improvements to [Tree::TreeNode#is_root?][is_root] and 106 | [Tree::Utils::TreeMetricsHandler#node_depth][mnode_depth]. Thanks to [Aidan Steel][Aidan]. 107 | 108 | ### 0.9.5pre3 / 2014-12-16 109 | 110 | * Minor fix to correct the release date. This release is otherwise identical 111 | to `0.9.5pre2`. 112 | 113 | ### 0.9.5pre2 / 2014-12-16 114 | 115 | * Added [Tree::TreeNode#rename][rename] and 116 | [Tree::TreeNode#rename_child][rename_child] methods by merging in code from 117 | [pr-35][]. Thanks to [Evan Sharp][Evan]. 118 | 119 | ### 0.9.5pre / 2014-11-01 120 | 121 | * Fixed [bug-13][] with the patch provided by [Jen Hamon][jhamon]. 122 | 123 | * Fixed a bug in [Tree::TreeNode#print_tree][print_tree] with the patch provided 124 | by [Evan Sharp][Evan]. 125 | 126 | * Fixed [bug-31][], which was causing incorrect behavior in 127 | [Tree::TreeNode#postordered_each][postordered_each] and 128 | [Tree::TreeNode#breadth_each][breadth_each] methods when a block was not 129 | provided. 130 | 131 | ### 0.9.4 / 2014-07-04 132 | 133 | * Changed all references to . 134 | 135 | ### 0.9.3 / 2014-02-01 136 | 137 | * Fixed the issue with globally unique node names. See [bug-24][]. 138 | 139 | ### 0.9.2 / 2014-01-03 140 | 141 | * Yanked `R0.9.1` as the `History.rdoc` file was not updated. 142 | 143 | * Updated the gem description. 144 | 145 | * Changed the [travis-ci][] build to include `coverall` support. 146 | 147 | ### 0.9.1 / 2014-01-03 148 | 149 | * Updated the gem description. 150 | 151 | * Incorporated code coverage using the `coverall` gem. 152 | 153 | ### 0.9.0 / 2014-01-02 154 | 155 | This is a feature and bug-fix release. 156 | 157 | #### The Features 158 | 159 | * Rubytree now supports `postordered` traversal via the 160 | [Tree::TreeNode#postordered_each][postordered_each] method. Thanks to [Paul de 161 | Courcel][Paul] for this. 162 | 163 | * The Binary tree now supports `inorder` traversal via the 164 | [Tree::BinaryTreeNode#inordered_each][inordered_each] method. 165 | 166 | * Ability to merge in another tree at a chosen node, or merge two trees to 167 | create a third tree. Thanks to [Darren Oakley][Darren] for this [pr-2][]. 168 | 169 | * RubyTree now mixes in the [Comparable][] module. 170 | 171 | #### The Fixes 172 | 173 | * (_Partial_) fix for preventing cyclic graphs in the tree. 174 | 175 | * Refactored the [Tree::TreeNode#each][each] method to prevent stack errors while 176 | navigating deep trees ([bug-12][]). 177 | 178 | * Check to ensure that the added node's name is unique to the destination tree 179 | ([pr-9][]). Thanks to [Youssef Rebahi-Gilbert][Youssef] for the idea and the 180 | initial code. 181 | 182 | * Fix for [bug-23][], where the tree traversal on a binary tree would fail if 183 | the _left_ child was `nil`. 184 | 185 | * The following traversal methods now correctly return an 186 | [Enumerator][] as the return value when no block is given, and 187 | return the _receiver node_ if a block was provided. This is consistent with 188 | how the standard Ruby collections work. 189 | 190 | * [Tree::TreeNode#each][each], 191 | * [Tree::TreeNode#preordered_each][preordered_each], 192 | * [Tree::TreeNode#postordered_each][postordered_each] and 193 | * [Tree::TreeNode#breadth_each][breadth_each]. 194 | 195 | #### Other Changes 196 | 197 | * Structural changes in the code to refactor out the non-core functions into 198 | modules (mostly by extracting out non-core code as `mixins`). 199 | 200 | * Significant refactoring of the documentation. The [Yard][] tags are now 201 | extensively used. 202 | 203 | * Basic support built-in for including example code in the gem. This will be 204 | fully expanded in the next release. 205 | 206 | * Various changes to the [Bundler][], [travis-ci][] and other `Rakefile` 207 | changes. 208 | 209 | ### 0.8.3 / 2012-08-21 210 | 211 | This is a primarily a bug-fix release, with some packaging changes. 212 | 213 | * Have removed the dependency on [Hoe][]. The build is now based on vanilla 214 | [gemspec][]. 215 | 216 | * Included support for [gem-testers][]. 217 | 218 | * Included support for [Bundler][]. 219 | 220 | * Implemented the [Tree::Utils::JSONConverter#as_json][as_json] method to 221 | support Rails' `JSON` encoding, by pulling in the changes from [Eric Cline][Eric]. 222 | 223 | * Partial fix for [bug-5][]. This is to prevent infinite looping if an existing 224 | node is added again elsewhere in the tree. 225 | 226 | * Fixed the issue with using `integers` as node names, and its interaction 227 | with the `Tree::TreeNode#[]` access method as documented in [bug-6][]. 228 | 229 | * Clarified the need to have _unique_ node names in the documentation ([bug-7][]). 230 | 231 | * Fixed [Tree::TreeNode#siblings][siblings] method to return an _empty_ array 232 | for the root node as well (it returned `nil` earlier). 233 | 234 | ### 0.8.2 / 2011-12-15 235 | 236 | * Minor bug-fix release to address [bug-1215][] ([Tree::TreeNode#to_s][to_s] 237 | breaks if `@content` or `@parent.name` is not a string). 238 | 239 | ### 0.8.1 / 2010-10-02 240 | 241 | * This is the public release of `R0.8.0`, with additional bug-fixes. Note that 242 | `R0.8.0` will **not be** released separately as a publicly available 243 | Rubygem. All changes as listed for `R0.8.0` are available in this release. 244 | 245 | * The main change in `R0.8.0`/`R0.8.1` is conversion of all `CamelCase` method 246 | names to `snake_case`. The old `CamelCase` method names will _still_ work (to 247 | ensure backwards compatibility), but will also display a warning. 248 | 249 | * The [Tree::TreeNode#add][add] method now accepts an _optional_ child insertion 250 | point. 251 | 252 | * The sub-tree from the current node can now be cloned in its _entirety_ using 253 | the [Tree::TreeNode#detached_subtree_copy][detached_subtree_copy] method. 254 | 255 | * A major bug-fix for [bug-28613][] which impacted the `Binarytree` 256 | implementation. 257 | 258 | * Minor code re-factoring driven by the code-smell checks using 259 | [reek][]. 260 | 261 | * Inclusion of the `reek` code-smell detection tool in the `Rakefile`. 262 | 263 | ### 0.8.0 / 2010-05-04 264 | 265 | * Updated the [Tree::TreeNode#add][add] method to allow the optional 266 | specification of an insertion position in the child array. 267 | 268 | * Added a new method 269 | [Tree::TreeNode#detached_subtree_copy][detached_subtree_copy] to allow cloning 270 | the entire tree (this method is also aliased as `dup`). 271 | 272 | * Converted all `CamelCase` method names to the canonical `ruby_method_names` 273 | (underscore separated). The `CamelCase` methods _can still_ be invoked, but 274 | will throw a [Deprecated Warning][dep-warning]. The support for old 275 | `CamelCase` methods **will** go away some time in the future, so the user is 276 | advised to convert all current method invocations to the new names. 277 | 278 | ### 0.7.0 / 2010-05-03 279 | 280 | * Added new methods to report the degree-statistics of a node. 281 | 282 | * Added a convenience method alias [Tree::TreeNode#level][level] to `nodeDepth`. 283 | 284 | * Converted the exceptions thrown on invalid arguments to [ArgumentError][] 285 | instead of [RuntimeError][]. 286 | 287 | * Converted the documentation to [Yard][] format. 288 | 289 | * Added new methods for converting to/from [JSON][] format. Thanks to Dirk 290 | [Breuer][] for this [fork](http://github.com/galaxycats/). 291 | 292 | * Added a separate [API-CHANGES.md](file:API-CHANGES.md) documentation file. 293 | 294 | * Added fixes for root related edge conditions to the: 295 | 296 | * [Tree::TreeNode#is_only_child?][is_only_child], 297 | * [Tree::TreeNode#next_sibling][next_sibling], 298 | * [Tree::TreeNode#previous_sibling][previous_sibling] and 299 | * [Tree::TreeNode#remove!][remove] methods. 300 | 301 | * Removed the `ChangeLog` file as this can now be generated from the git logs. 302 | 303 | * Other minor code cleanup. 304 | 305 | ### 0.6.2 / 2010-01-30 306 | 307 | * Updated the documentation. 308 | 309 | ### 0.6.1 / 2010-01-04 310 | 311 | * Changed the hard-dependency on the [structured_warnings][] gem to a 312 | _soft-dependency_ - which lets `RubyTree` still work if this gem is not 313 | available. The rationale for this is that we _should not_ require the user to 314 | install a separate library just for _one_ edge-case function (in this case, to 315 | indicate a deprecated method). However, if the library *is* available on the 316 | user's system, then it **will** get used. 317 | 318 | ### 0.6.0 / 2010-01-03 319 | 320 | * Fixed [bug-22535][] where the `Tree::TreeNode#depth` method was actually 321 | returning `height+1` (**not** the `depth`). 322 | 323 | * Marked the `Tree::TreeNode#depth` method as **deprecated** (and introduced the 324 | run-time dependency on the [structured-warnings][] gem). 325 | 326 | ### 0.5.3 / 2009-12-31 327 | 328 | * Cleanup of the build system to exclusively use [Hoe][]. 329 | * Modifications and reformatting to the documentation. 330 | * No user visible changes. 331 | 332 | ### 0.5.2 / 2007-12-21 333 | 334 | * Added more test cases and enabled [ZenTest][] compatibility for the test case 335 | names. 336 | 337 | ### 0.5.1 / 2007-12-20 338 | 339 | * Minor code refactoring. 340 | 341 | ### 0.5.0 / 2007-12-18 342 | 343 | * Fixed the marshalling code to correctly handle non-string content. 344 | 345 | ### 0.4.3 / 2007-10-09 346 | 347 | * Changes to the build mechanism (now uses [Hoe]). 348 | 349 | ### 0.4.2 / 2007-10-01 350 | 351 | * Minor code refactoring. Changes in the `Rakefile`. 352 | 353 | [bug-5]: https://github.com/evolve75/RubyTree/issues/5 354 | [bug-6]: https://github.com/evolve75/RubyTree/issues/6 355 | [bug-7]: https://github.com/evolve75/RubyTree/issues/7 356 | [bug-12]: https://github.com/evolve75/RubyTree/issues/12 357 | [bug-13]: https://github.com/evolve75/RubyTree/issues/13 358 | [bug-23]: https://github.com/evolve75/RubyTree/issues/23 359 | [bug-24]: https://github.com/evolve75/RubyTree/issues/24 360 | [bug-31]: https://github.com/evolve75/RubyTree/issues/31 361 | [bug-32]: https://github.com/evolve75/RubyTree/issues/32 362 | [bug-1215]: http://rubyforge.org/tracker/index.php?func=detail&aid=1215&group_id=1215&atid=4793 363 | [bug-28613]: http://rubyforge.org/tracker/index.php?func=detail&aid=28613&group_id=1215&atid=4793 364 | [bug-22535]: http://rubyforge.org/tracker/index.php?func=detail&aid=22535&group_id=1215&atid=4793 365 | 366 | [pr-2]: https://github.com/evolve75/RubyTree/pull/2 367 | [pr-9]: https://github.com/evolve75/RubyTree/pull/9 368 | [pr-35]: https://github.com/evolve75/RubyTree/pull/35 369 | 370 | [ArgumentError]: http://www.ruby-doc.org/core-2.0.0/ArgumentError.html 371 | [Bundler]: https://bundler.io 372 | [Comparable]: http://ruby-doc.org/core-1.8.7/Comparable.html 373 | [Enumerator]: http://ruby-doc.org/core-1.8.7/Enumerable.html 374 | [Hoe]: http://www.zenspider.com/projects/hoe.html 375 | [JSON]: http://www.json.org 376 | [RuntimeError]: http://www.ruby-doc.org/core-2.0.0/RuntimeError.html 377 | [Yard]: http://yardoc.org 378 | [ZenTest]: https://github.com/seattlerb/zentest 379 | [dep-warning]: http://rug-b.rubyforge.org/structured_warnings/rdoc/ 380 | [gem-testers]: https://test.rubygems.org/ 381 | [gemspec]: https://guides.rubygems.org/specification-reference/ 382 | [reek]: https://github.com/troessner/reek 383 | [structured-warnings]: http://github.com/schmidt/structured_warnings 384 | [travis-ci]: https://travis-ci.org 385 | [workflow]: https://docs.github.com/en/actions/using-workflows 386 | 387 | [Aidan]: https://github.com/aidansteele 388 | [Breuer]: http://github.com/railsbros-dirk 389 | [Darren]: https://github.com/dazoakley 390 | [Eric]: https://github.com/escline 391 | [Evan]: https://github.com/packetmonkey 392 | [Marco]: https://github.com/MZic 393 | [Ojab]: https://github.com/ojab 394 | [Paul]: https://github.com/pdecourcel 395 | [Youssef]: https://github.com/ysf 396 | [igneus]: https://github.com/igneus 397 | [jack12816]: https://github.com/jack12816 398 | [jhamon]: https://www.github.com/jhamon 399 | [jmortlock]: https://github.com/jmortlock 400 | 401 | [TreeNode]: rdoc-ref:Tree::TreeNode 402 | [TreePathHandler]: Tree::Utils::TreePathHandler 403 | [add]: rdoc-ref:Tree::TreeNode#add 404 | [as_json]: rdoc-ref:Tree::Utils::JSONConverter#as_json 405 | [breadth_each]: rdoc-ref:Tree::TreeNode#breadth_each 406 | [detached_subtree_copy]: rdoc-ref:Tree::TreeNode#detached_subtree_copy 407 | [each]: rdoc-ref:Tree::TreeNode#each 408 | [inordered_each]: rdoc-ref:Tree::BinaryTreeNode#inordered_each 409 | [is_only_child]: rdoc-ref:Tree::TreeNode#is_only_child? 410 | [is_root]: rdoc-ref:Tree::TreeNode#is_root? 411 | [level]: rdoc-ref:Tree::TreeNode#level 412 | [mnode_depth]: rdoc-ref:Tree::Utils::TreeMetricsHandler#node_depth 413 | [next_sibling]: rdoc-ref:Tree::TreeNode#next_sibling 414 | [node_depth]: rdoc-ref:Tree::TreeNode#node_depth 415 | [postordered_each]: rdoc-ref:Tree::TreeNode#postordered_each 416 | [previous_sibling]: rdoc-ref:Tree::TreeNode#previous_sibling 417 | [print_tree]: rdoc-ref:Tree::TreeNode#print_tree 418 | [remove]: rdoc-ref:Tree::TreeNode#remove! 419 | [rename]: rdoc-ref:Tree::TreeNode#rename 420 | [rename_child]: rdoc-ref:Tree::TreeNode#rename_child 421 | [siblings]: rdoc-ref:Tree::TreeNode#siblings 422 | [to_s]: rdoc-ref:Tree::TreeNode#to_s 423 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | [RubyTree][] is licensed under the [BSD-2-clause][] license. 2 | 3 | Copyright (c) 2006-2023 Anupam Sengupta (). 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 19 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 22 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | [BSD-2-clause]: https://opensource.org/license/bsd-2-clause/ "BSD License" 27 | [RubyTree]: http://rubytree.anupamsg.me/ "RubyTree Home Page" 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **RubyTree** # 2 | 3 | [![Gem Version](https://badge.fury.io/rb/rubytree.png)](http://badge.fury.io/rb/rubytree) 4 | ![Build State](https://github.com/evolve75/rubytree/actions/workflows/ruby.yml/badge.svg) 5 | [![Code Climate](https://codeclimate.com/github/evolve75/RubyTree.png)](https://codeclimate.com/github/evolve75/RubyTree) 6 | [![Coverage Status](https://coveralls.io/repos/evolve75/RubyTree/badge.png)](https://coveralls.io/r/evolve75/RubyTree) 7 | 8 | __ _ _ 9 | /__\_ _| |__ _ _| |_ _ __ ___ ___ 10 | / \// | | | '_ \| | | | __| '__/ _ \/ _ \ 11 | / _ \ |_| | |_) | |_| | |_| | | __/ __/ 12 | \/ \_/\__,_|_.__/ \__, |\__|_| \___|\___| 13 | |___/ 14 | 15 | ## DESCRIPTION: ## 16 | 17 | **RubyTree** is a pure Ruby implementation of the generic 18 | [tree data structure][tree_data_structure]. It provides a node-based model to 19 | store named nodes in the tree, and provides simple APIs to access, modify and 20 | traverse the structure. 21 | 22 | The implementation is *node-centric*, where individual nodes in the tree are the 23 | primary structural elements. All common tree-traversal methods ([pre-order][], 24 | [post-order][], and [breadth-first][]) are supported. 25 | 26 | The library mixes in the [Enumerable][] and [Comparable][] modules to allow 27 | access to the tree as a standard collection (iteration, comparison, etc.). 28 | 29 | A [Binary tree][] is also provided, which provides the [in-order][] traversal in 30 | addition to the other methods. 31 | 32 | **RubyTree** supports importing from, and exporting to [JSON][], and also 33 | supports the Ruby's standard object [marshaling][]. 34 | 35 | This is a [BSD licensed][BSD] open source project, and is hosted at 36 | [github.com/evolve75/RubyTree][rt@github], and is available as a standard gem 37 | from [rubygems.org/gems/rubytree][rt_gem]. 38 | 39 | The home page for **RubyTree** is at [rubytree.anupamsg.me][rt_site]. 40 | 41 | ## WHAT'S NEW: ## 42 | 43 | See [History](./History.md) for the detailed Changelog. 44 | 45 | See [API-CHANGES](./API-CHANGES.md) for the detailed description of 46 | API level changes. 47 | 48 | ## GETTING STARTED: ## 49 | 50 | This is a basic usage example of the library to create and manipulate a tree. 51 | See the [API][rt_doc] documentation for more details. 52 | 53 | ```ruby 54 | #!/usr/bin/env ruby 55 | # 56 | # example_basic.rb:: Basic usage of the tree library. 57 | # 58 | # Copyright (C) 2013-2022 Anupam Sengupta 59 | # 60 | # The following example implements this tree structure: 61 | # 62 | # +------------+ 63 | # | ROOT | 64 | # +-----+------+ 65 | # +-------------+------------+ 66 | # | | 67 | # +-------+-------+ +-------+-------+ 68 | # | CHILD 1 | | CHILD 2 | 69 | # +-------+-------+ +---------------+ 70 | # | 71 | # | 72 | # +-------+-------+ 73 | # | GRANDCHILD 1 | 74 | # +---------------+ 75 | 76 | # ..... Example starts. 77 | require 'tree' # Load the library 78 | 79 | # ..... Create the root node first. 80 | # ..... Note that every node has a name and an optional content payload. 81 | root_node = Tree::TreeNode.new("ROOT", "Root Content") 82 | root_node.print_tree 83 | 84 | # ..... Now insert the child nodes. 85 | # Note that you can "chain" the child insertions to any depth. 86 | root_node << Tree::TreeNode.new("CHILD1", "Child1 Content") << Tree::TreeNode.new("GRANDCHILD1", "GrandChild1 Content") 87 | root_node << Tree::TreeNode.new("CHILD2", "Child2 Content") 88 | 89 | # ..... Lets print the representation to stdout. 90 | # ..... This is primarily used for debugging purposes. 91 | root_node.print_tree 92 | 93 | # ..... Lets directly access children and grandchildren of the root. 94 | # ..... The can be "chained" for a given path to any depth. 95 | child1 = root_node["CHILD1"] 96 | grand_child1 = root_node["CHILD1"]["GRANDCHILD1"] 97 | 98 | # ..... Now retrieve siblings of the current node as an array. 99 | siblings_of_child1 = child1.siblings 100 | 101 | # ..... Retrieve immediate children of the root node as an array. 102 | children_of_root = root_node.children 103 | 104 | # ..... Retrieve the parent of a node. 105 | parent = child1.parent 106 | 107 | # ..... This is a depth-first and L-to-R pre-ordered traversal. 108 | root_node.each { |node| node.content.reverse } 109 | 110 | # ..... Remove a child node from the root node. 111 | root_node.remove!(child1) 112 | 113 | # .... Many more methods are available. Check out the documentation! 114 | ``` 115 | 116 | This example can also be found at 117 | [examples/example_basic.rb](examples/example_basic.rb). 118 | 119 | ## REQUIREMENTS: ## 120 | 121 | * [Ruby][] 2.7.x and above. 122 | 123 | 124 | * Run-time Dependencies: 125 | 126 | * [JSON][] for converting to/from the JSON format 127 | 128 | 129 | * Development dependencies (not required for installing the gem): 130 | 131 | * [Bundler][] for creating the stable build environment 132 | * [Rake][] for building the package 133 | * [Yard][] for the documentation 134 | * [RSpec][] for additional Ruby Spec test files 135 | * [Rubocop][] for linting the code 136 | 137 | ## INSTALL: ## 138 | 139 | To install the [gem][rt_gem], run this command from a terminal/shell: 140 | 141 | $ gem install rubytree 142 | 143 | This should install the gem file for **RubyTree**. Note that you might need to 144 | have super-user privileges (root/sudo) to successfully install the gem. 145 | 146 | ## DOCUMENTATION: ## 147 | 148 | The primary class **RubyTree** is [Tree::TreeNode][TreeNode]. See the class 149 | documentation for an example of using the library. 150 | 151 | If the *ri* documentation was generated during install, you can use this 152 | command at the terminal to view the text mode ri documentation: 153 | 154 | $ ri Tree::TreeNode 155 | 156 | Documentation for the latest released version is available at: 157 | 158 | [rubytree.anupamsg.me/rdoc][rt_doc] 159 | 160 | Documentation for the latest git HEAD is available at: 161 | 162 | [rdoc.info/projects/evolve75/RubyTree][rt_doc@head] 163 | 164 | Note that the documentation is formatted using [Yard][]. 165 | 166 | ## DEVELOPERS: ## 167 | 168 | This section is only for modifying **RubyTree** itself. It is not required for 169 | using the library! 170 | 171 | You can download the latest released source code as a tar or zip file, as 172 | mentioned above in the installation section. 173 | 174 | Alternatively, you can checkout the latest commit/revision from the Version 175 | Control System. Note that **RubyTree**'s primary [SCM][] is [git][] and is 176 | hosted on [github.com][rt@github]. 177 | 178 | ### Using the git Repository ### 179 | 180 | The git repository is available at [github.com/evolve75/RubyTree][rt@github]. 181 | 182 | For cloning the git repository, use one of the following commands: 183 | 184 | $ git clone git://github.com/evolve75/RubyTree.git # using ssh 185 | 186 | or 187 | 188 | $ git clone https://github.com/evolve75/RubyTree.git # using https 189 | 190 | ### Setting up the Development Environment ### 191 | 192 | **RubyTree** uses [Bundler][] to manage its dependencies. This allows for a 193 | simplified dependency management, for both run-time as well as during build. 194 | 195 | After checking out the source, run: 196 | 197 | $ gem install bundler 198 | $ bundle install 199 | $ bundle exec rake test:all 200 | $ bundle exec rake doc:yard 201 | $ bundle exec rake gem:package 202 | 203 | These steps will install any missing dependencies, run the tests/specs, 204 | generate the documentation, and finally generate the gem file. 205 | 206 | Note that the documentation uses [Yard][], which will be 207 | downloaded and installed automatically by [Bundler][]. 208 | 209 | ## ACKNOWLEDGMENTS: ## 210 | 211 | A big thanks to the following contributors for helping improve **RubyTree**: 212 | 213 | 1. Dirk Breuer for contributing the JSON conversion code. 214 | 2. Vincenzo Farruggia for contributing the (sub)tree cloning code. 215 | 3. [Eric Cline](https://github.com/escline) for the Rails JSON encoding fix. 216 | 4. [Darren Oakley](https://github.com/dazoakley) for the tree merge methods. 217 | 5. [Youssef Rebahi-Gilbert](https://github.com/ysf) for the code to check 218 | duplicate node names in the tree (globally unique names). 219 | 6. [Paul de Courcel](https://github.com/pdecourcel) for adding the 220 | `postordered_each` method. 221 | 7. [Jen Hamon](http://www.github.com/jhamon) for adding the `from_hash` method. 222 | 8. [Evan Sharp](https://github.com/packetmonkey) for adding the `rename` and 223 | `rename_child` methods. 224 | 9. [Aidan Steele](https://github.com/aidansteele) for performance improvements 225 | to `is_root?` and `node_depth`. 226 | 10. [Marco Ziccadi](https://github.com/MZic) for adding the `path_as_string` and 227 | `path_as_array` methods. 228 | 11. [John Mortlock](https://github.com/jmortlock) for significant modernization 229 | of the library code and addition of Github `workflows`. 230 | 12. [Hermann Mayer](https://github.com/jack12816) for adding support for 231 | specialized tree nodes (sub-classes of `Tree::TreeNode`). 232 | 13. [Jakub Pavlik](https://github.com/igneus) for fixing the creation of 233 | detached copies of unclonable objects such as `:symbol`, `true|false`, etc. 234 | 14. [bghalami-rc](https://github.com/bghalami-rc) for updating the guard clause 235 | in the `from_hash` method. 236 | 237 | 238 | ## LICENSE: ## 239 | 240 | **RubyTree** is licensed under the terms of the [BSD][] license. See 241 | [LICENSE.md](./LICENSE.md) for details. 242 | 243 | 244 | [BSD]: http://opensource.org/licenses/bsd-license.php "BSD License" 245 | [Binary tree]: http://en.wikipedia.org/wiki/Binary_tree "Binary Tree Data Structure" 246 | [Bundler]: http://bundler.io "Bundler" 247 | [Comparable]: http://ruby-doc.org/core/Comparable.html "Comparable mix-in" 248 | [Enumerable]: http://ruby-doc.org/core/Enumerable.html "Enumerable mix-in" 249 | [JSON]: https://rubygems.org/gems/json "JSON" 250 | [Rake]: https://rubygems.org/gems/rake "Rake" 251 | [Ruby]: http://www.ruby-lang.org "Ruby Programming Language" 252 | [SCM]: http://en.wikipedia.org/wiki/Source_Code_Management "Source Code Management" 253 | [Yard]: http://yardoc.org "Yard Document Generator" 254 | [breadth-first]: http://en.wikipedia.org/wiki/Breadth-first_search "Breadth-first (level-first) Traversal" 255 | [git]: http://git-scm.com "Git SCM" 256 | [in-order]: http://en.wikipedia.org/wiki/Tree_traversal#In-order "In-order (symmetric) Traversal" 257 | [marshaling]: http://ruby-doc.org/core/Marshal.html "Marshaling in Ruby" 258 | [post-order]: http://en.wikipedia.org/wiki/Tree_traversal#Post-order "Post-ordered Traversal" 259 | [pre-order]: http://en.wikipedia.org/wiki/Tree_traversal#Pre-order "Pre-ordered Traversal" 260 | [rt@github]: http://github.com/evolve75/RubyTree "RubyTree Project Page on Github" 261 | [rt_doc@head]: http://rdoc.info/projects/evolve75/RubyTree "RubyTree Documentation for VCS Head" 262 | [rt_doc]: http://rubytree.anupamsg.me/rdoc "RubyTree Documentation" 263 | [rt_gem]: http://rubygems.org/gems/rubytree "RubyTree Gem" 264 | [rt_site]: http://rubytree.anupamsg.me "RubyTree Site" 265 | [tree_data_structure]: http://en.wikipedia.org/wiki/Tree_data_structure "Tree Data Structure" 266 | [RSpec]: https://relishapp.com/rspec/ 267 | [Rubocop]: https://rubocop.org/ 268 | 269 | [TreeNode]: rdoc-ref:Tree::TreeNode 270 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby; -*- 2 | # 3 | # Rakefile - This file is part of the RubyTree package. 4 | # 5 | # Copyright (c) 2006-2022 Anupam Sengupta 6 | # 7 | # All rights reserved. 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # - Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # - Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # - Neither the name of the organization nor the names of its contributors may 20 | # be used to endorse or promote products derived from this software without 21 | # specific prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 27 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | # 34 | # 35 | # frozen_string_literal: true 36 | 37 | require 'rubygems' 38 | 39 | # @todo: Check if Bundler needs to be `require`d. 40 | GEM_SPEC = Bundler.load_gemspec(File.join(__dir__, 'rubytree.gemspec')) 41 | 42 | PKG_NAME = GEM_SPEC.name 43 | PKG_VER = GEM_SPEC.version 44 | GEM_NAME = "#{PKG_NAME}-#{PKG_VER}.gem" 45 | 46 | desc 'Default Task (Run the tests)' 47 | task :default do 48 | if ENV['COVERAGE'] 49 | Rake::Task['test:coverage'].invoke 50 | else 51 | Rake::Task['test:unit'].invoke 52 | Rake::Task['spec'].invoke 53 | end 54 | end 55 | 56 | desc 'Display the current gem version' 57 | task :version do 58 | puts "Current Version: #{GEM_NAME}" 59 | end 60 | 61 | require 'rake/clean' 62 | desc 'Remove all generated files.' 63 | task clean: 'gem:clobber_package' 64 | CLEAN.include('coverage') 65 | task clobber: [:clean, 'doc:clobber_rdoc', 'doc:clobber_yard'] 66 | 67 | desc 'Open an irb session preloaded with this library' 68 | task :console do 69 | sh 'irb -rubygems -r ./lib/tree.rb' 70 | end 71 | 72 | namespace :doc do # ................................ Documentation 73 | begin 74 | gem 'rdoc', '>= 6.4.0' # To get around a stupid bug in Ruby 1.9.2 Rake. 75 | require 'rdoc/task' 76 | Rake::RDocTask.new do |rdoc| 77 | rdoc.rdoc_dir = 'rdoc' 78 | rdoc.title = "RubyTree Documenation: #{PKG_NAME}-#{PKG_VER}" 79 | rdoc.main = 'README.md' 80 | rdoc.rdoc_files.include(GEM_SPEC.extra_rdoc_files) 81 | rdoc.rdoc_files.include('./lib/**/*.rb') 82 | end 83 | rescue LoadError 84 | # Oh well. 85 | end 86 | 87 | begin 88 | require 'yard' 89 | YARD::Rake::YardocTask.new do |t| 90 | t.files = ['lib/**/*.rb', '-', GEM_SPEC.extra_rdoc_files] 91 | t.options = %w[--no-private --embed-mixins] 92 | end 93 | rescue LoadError 94 | # Oh well. 95 | end 96 | 97 | desc 'Remove YARD Documentation' 98 | task :clobber_yard do 99 | rm_rf 'doc' 100 | end 101 | end 102 | 103 | desc 'Run the unit tests' 104 | task test: %w[test:unit] 105 | 106 | # ................................ Test related 107 | namespace :test do 108 | desc 'Run all the tests' 109 | task all: %w[test:unit test:spec test:examples] 110 | 111 | require 'rake/testtask' 112 | Rake::TestTask.new(:unit) do |test| 113 | test.libs << 'lib' << 'test' 114 | test.pattern = 'test/**/test_*.rb' 115 | test.verbose = false 116 | end 117 | 118 | # ................................ rspec tests 119 | begin 120 | require 'rspec/core/rake_task' 121 | 122 | RSpec::Core::RakeTask.new(:spec) do |t| 123 | t.fail_on_error = false 124 | t.rspec_opts = ['--color', '--format doc'] 125 | end 126 | rescue LoadError 127 | # Cannot load rspec. 128 | end 129 | 130 | desc 'Run the examples' 131 | Rake::TestTask.new(:examples) do |example| 132 | example.libs << 'lib' << 'examples' 133 | example.pattern = 'examples/**/example_*.rb' 134 | example.verbose = true 135 | example.warning = false 136 | end 137 | 138 | desc 'Run the code coverage' 139 | task :coverage do 140 | ruby 'test/run_test.rb' 141 | end 142 | end 143 | 144 | # ................................ Emacs Tags 145 | namespace :tag do 146 | require 'rtagstask' 147 | RTagsTask.new(:tags) do |rd| 148 | rd.vi = false 149 | CLEAN.include('TAGS') 150 | end 151 | rescue LoadError 152 | # Oh well. Can't have everything. 153 | end 154 | 155 | # ................................ Gem related 156 | namespace :gem do 157 | require 'rubygems/package_task' 158 | Gem::PackageTask.new(GEM_SPEC) do |pkg| 159 | pkg.need_zip = true 160 | pkg.need_tar = true 161 | end 162 | 163 | desc 'Push the gem into the Rubygems repository' 164 | task push: :gem do 165 | sh "gem push pkg/#{GEM_NAME}" 166 | end 167 | end 168 | 169 | # ................................ Ruby linting 170 | require 'rubocop/rake_task' 171 | 172 | RuboCop::RakeTask.new(:rubocop) do |t| 173 | t.options = ['--display-cop-names'] 174 | t.requires << 'rubocop-rake' 175 | t.requires << 'rubocop-rspec' 176 | end 177 | -------------------------------------------------------------------------------- /TODO.org: -------------------------------------------------------------------------------- 1 | # -*- mode: org; coding: utf-8-unix; fill-column: 120; -*- 2 | #+OPTIONS: ^:{} 3 | #+TODO: TODO(t) STARTED(s) | DONE(d) CANCELED(c) 4 | #+LINK: Issue https://github.com/evolve75/RubyTree/issues/%s 5 | #+LINK: Pull https://github.com/evolve75/RubyTree/pull/%s 6 | 7 | * R0.7.0 :ARCHIVE: 8 | *** DONE Start using signed tags from R0.7.0 :ARCHIVE: 9 | *** DONE Add a check in the Tree::TreeNode.add method to prevent addition of nil child nodes :ARCHIVE: 10 | CLOSED: [2010-02-23 Tue 23:07] 11 | *** DONE Fix the edge condition for Tree::TreeNode.isOnlyChild? when the root node is the receiver. :ARCHIVE: 12 | CLOSED: [2010-02-23 Tue 22:03] 13 | There really is no good default to this situation. We will return 'true' simply because there is no other sibling 14 | to a root. However, a good case can be made that a root node does not have any parent either. 15 | *** DONE Add a convenience 'level' method to the TreeNode class (will be an alias to nodeDepth) :ARCHIVE: 16 | CLOSED: [2010-02-21 Sun 01:02] 17 | *** DONE Add a API-CHANGES file to document the various API changes made till date :ARCHIVE: 18 | CLOSED: [2010-01-31 Sun 00:52] 19 | *** DONE Add new methods to return the degree counts of the receiver node (in-degree and out-degree) :ARCHIVE: 20 | CLOSED: [2010-01-30 Sat 23:56] 21 | 22 | 23 | * R0.8.0 :ARCHIVE: 24 | *** DONE Convert all method names to the canonical /^[_a-z<>=\[|+-\/\*`]+[_a-z0-9_<>=~@\[\]]*[=!\?]?$/ pattern :ARCHIVE: 25 | See Roodi report at http://getcaliper.com/caliper/tool?tool=roodi&repo=git://github.com/evolve75/RubyTree.git 26 | *** DONE Integrate the subtree cloning patch submitted by Vincenzo Farrugia. :ARCHIVE: 27 | 28 | 29 | 30 | * R0.8.1 :ARCHIVE: 31 | *** DONE Fix [[http://rubyforge.org/tracker/index.php?func%3Ddetail&aid%3D28613&group_id%3D1215&atid%3D4793][bug #28613]] which was affecting the `leftChild=' and `rightChild=' methods for binary trees. :ARCHIVE: 32 | 33 | 34 | * R0.8.3 :ARCHIVE: 35 | 36 | This is a bugfix release. 37 | 38 | *** DONE Make Rubytree compatible with Bundler :ARCHIVE: 39 | CLOSED: [2012-08-21 Tue 21:04] 40 | 41 | *** DONE Make Rubytree compatible wth gem-testers :ARCHIVE: 42 | CLOSED: [2012-08-21 Tue 21:04] 43 | 44 | *** DONE Remove the dependency on Hoe :ARCHIVE: 45 | CLOSED: [2012-08-21 Tue 21:05] 46 | *** DONE Resolve the _tree.rb_ file conflict with the [[http://netaddr.rubyforge.org/][netaddr gem]] :ARCHIVE: 47 | CLOSED: [2012-08-20 Mon 01:03] 48 | Issue https://github.com/evolve75/RubyTree/issues/8 49 | 50 | *** DONE Update documentation to be more explicit about duplicate node names :ARCHIVE: 51 | CLOSED: [2012-08-19 Sun 21:46] 52 | Issue https://github.com/evolve75/RubyTree/issues/7 53 | Update documentation for :name attribute in tree.rb. There is no 54 | specific code fix needed. 55 | 56 | *** DONE Allow integers to be used as node names (clarify the scenario). Fixed issue #6. :ARCHIVE: 57 | CLOSED: [2012-08-19 Sun 15:17] 58 | Issue https://github.com/evolve75/RubyTree/issues/6 59 | We will need to warn the user when an Integer is used as a name 60 | for the node (but still allow the usage), 61 | and 62 | also add an optional flag to the TreeNode#[] method to allow the 63 | user to explicitly indicate use of the Integer parameter as a 64 | literal name, and not as an /index/ to the children array. 65 | 66 | *** DONE Clarify (or fix) the scenario whether a root node without children is a leaf :ARCHIVE: 67 | CLOSED: [2012-08-19 Sun 15:09] 68 | Issue http://rubyforge.org/tracker/index.php?func=detail&aid=29549&group_id=1215&atid=4793 69 | 70 | #+begin_src ruby -n :eval no 71 | tree.each_leaf do |tree_leaf| 72 | tree_leaf_parent = tree_leaf.parent 73 | tree_leaf.remove_from_parent! 74 | puts tree_leaf_parent.is_leaf? 75 | end 76 | #+end_src 77 | 78 | will return ~false~, while technically ~tree_leaf_parent~ becomes leaf itself when ~tree_leaf~ is removed. 79 | 80 | The problem here is that the code above is trying to concurrently modify the collection over which the ~each_leaf~ 81 | iterator is looping, which has unpredicable results. As an example, try this with an array: 82 | 83 | #+begin_src ruby -n 84 | a = Array(1..5) 85 | a.each do |e| 86 | a.delete(e) 87 | end 88 | a 89 | #+end_src 90 | 91 | #+RESULTS: 92 | | 2 | 4 | 93 | 94 | The result is surprising, as not all elements are being deleted. A good explanation is available in [[https://groups.google.com/forum/?fromgroups#!topic/ruby-talk-google/iEDF8qhojss%255B1-25%255D][this thread]] on 95 | Ruby-Talk @ Google. 96 | 97 | The correct way to handle the original need is: 98 | 99 | #+begin_src ruby -n :eval no 100 | leafs = @root.each_leaf 101 | parents = leafs.collect {|leaf| leaf.parent } 102 | leafs.each {|leaf| leaf.remove_from_parent!} 103 | parents.each {|parent| assert(parent.is_leaf?) if not parent.has_children?} 104 | #+end_src 105 | 106 | Basically, the parent removal is done in a separate block, and *then* the check for the parents becoming leafs is done. 107 | 108 | *** DONE Fix the ~first_sibling~ and ~last_sibling~ for the root :ARCHIVE: 109 | CLOSED: [2012-08-19 Sun 21:01] 110 | The current behavior is correct, and has been left as is. 111 | *** DONE Fix the ~siblings~ method to return an empty array for root :ARCHIVE: 112 | CLOSED: [2012-08-19 Sun 21:03] 113 | *** DONE Fix the TreeNode#root method to return nil for root's root. :ARCHIVE: 114 | CLOSED: [2012-08-19 Sun 21:13] 115 | 116 | Left the code as-is, since we need some way to un-ambiguously find the root, regardless of the node given. 117 | 118 | 119 | 120 | * R0.9.0 :ARCHIVE: 121 | DEADLINE: <2013-02-24 Sun> 122 | 123 | This release contains the following features and fixes: 124 | 125 | 1. Ability to merge in another tree at a chosen node 126 | 2. Support for the [[http://ruby-doc.org/core-1.8.7/Comparable.html][Comparable]] mixin module 127 | 3. Ability to export the tree to a hash, and create a new tree from 128 | another existing hash 129 | 4. Fix (partial) for preventing cyclic graphs in the tree 130 | 5. Refactored =each= method to prevent stack errors while navigating 131 | deep trees 132 | 6. Check to ensure that the added node's name is unique to the destination tree 133 | 7. Fix for the issue where tree traversal would fail if the binary-tree's left child was nil 134 | 8. Fixed the return type for the iterator methods (each, postordered_each, breadth_each, etc.). They now return an 135 | Enumerator if *no* block is provided, or else return the receiver node itself, if a block *was* provided. This is 136 | consistent with how Ruby's standard collections work 137 | 9. Structural changes in the code to refactor out the non-core functions into modules 138 | 10. Massive documentation updates 139 | 11. Addition of the examples directory (only a bare-bones placeholder for now, with the basic example code) 140 | 12. Ability to run the examples from the Rakefile 141 | 13. Various bundler and travis-ci related changes 142 | 143 | 144 | *** DONE Fix the stack exhaustion issue due to deep recursion on very large unbalanced trees :ARCHIVE: 145 | CLOSED: [2013-12-28 Sat 10:59] 146 | See [[Issue:12][Issue #12.]] The following methods need fixes: 147 | - [X] [[file:lib/tree.rb::def%20each(][each]] 148 | - [X] [[file:lib/tree.rb::def%20postordered_each][postordered_each]] 149 | 150 | *** DONE Extract non-essential methods from Tree::TreeNode into separate files. :ARCHIVE: 151 | CLOSED: [2013-12-31 Tue 21:55] 152 | - [X] Handling of CamelCase methods 153 | - [X] Convertion to and from [[http://flori.github.com/json/][JSON]] 154 | - [X] The merge feature 155 | - [X] The metrics measurements 156 | 157 | *** DONE Fix the documentation strings for the methods (the Yard attributes) :ARCHIVE: 158 | CLOSED: [2013-12-31 Tue 21:55] DEADLINE: <2013-12-28 Sat> 159 | 160 | *** DONE Implement an `inordered_each` method for the [[file:lib/tree/b][BinaryTree]] :ARCHIVE: 161 | CLOSED: [2013-12-28 Sat 16:32] DEADLINE: <2013-12-28 Sat> 162 | *** DONE Add some example code to the Gem :ARCHIVE: 163 | CLOSED: [2013-12-28 Sat 12:12] 164 | *** DONE Pull in the unique node name validation from [[Pull:9][ysf]] :ARCHIVE: 165 | CLOSED: [2013-02-21 Thu 20:29] 166 | Will need to make this configurable. 167 | 168 | *** DONE Pull in the tree merge feature from [[Pull:9][Dazoakley]] :ARCHIVE: 169 | CLOSED: [2013-02-21 Thu 20:26] 170 | 171 | *** DONE Rename the [[file:COPYING.rdoc][COPYING.rdoc]] file to LICENSING.rdoc :ARCHIVE: 172 | CLOSED: [2012-08-25 Sat 21:19] 173 | 174 | *** CANCELED Fix the inconsistency of returning root as its first sibling, and returning a nil instead. Ditto for last sibling. :ARCHIVE: 175 | CLOSED: [2012-08-25 Sat 20:49] 176 | This is actually consistent. 177 | *** CANCELED fix the inconsistency of returning nil for the root, and an empty array for nodes which have no siblings. :ARCHIVE: 178 | CLOSED: [2012-08-25 Sat 20:51] 179 | Already fixed in [[R0.8.3]]. 180 | 181 | *** CANCELED We should perhaps return nil as root's root. (Scrapped). :ARCHIVE: 182 | CLOSED: [2012-08-25 Sat 20:35] 183 | This proposed change does make sense at one level (since the root node does not have any parent), but returning root 184 | as root's root (no pun intended) makes accessing the root from anywhere in the tree much easier. 185 | 186 | * R0.9.5 :ARCHIVE: 187 | ** DONE Add the `#get_path_as_string` method from feature request #48 :ARCHIVE: 188 | CLOSED: [2015-05-30 Sat 15:55] 189 | ** DONE Fix [[Issue:32][Issue #32]] and enable move semantics on the TreeNode#add method. :ARCHIVE: 190 | CLOSED: [2015-01-01 Thu 16:05] 191 | ** DONE Check the lazy initialization of =@node_depth= and changes in parent nodes :ARCHIVE: 192 | CLOSED: [2014-12-18 Thu 11:05] 193 | ** DONE Pull the performance improvements from Aidan [[Pull:37][#37]] :ARCHIVE: 194 | CLOSED: [2014-12-18 Thu 10:27] 195 | ** DONE Pull the =hash= converter code from [[https://github.com/markthomas/RubyTree/commits/master][Mark Thomas]] ([[Issue:13][Issue #13]]). :ARCHIVE: 196 | CLOSED: [2014-11-01 Sat 20:10] 197 | This was contributed by @jhamon. 198 | ** DONE Misc. bug fixes :ARCHIVE: 199 | CLOSED: [2014-11-01 Sat 20:11] 200 | 201 | 202 | * R2.0.0 203 | 204 | This is primarily a *modernization* of the library, with removal of deprecated methods, the much-hated dependency on 205 | ~structured_warnings~, and cleanup of other cruft. 206 | 207 | In addition, the CI pipeline has been moved from to ~Github Actions~. 208 | 209 | - [X] Merge the modernization PR from @jmortlock (multiple changes). 210 | - [X] Update the documentation to reflect the modernization changes. 211 | 212 | 213 | * Unplanned / Not assigned to any release 214 | *** STARTED Convert all documentation to markdown mode. 215 | *** STARTED [#A] Resolve the infinite loop bug if a node is added to itself as a child :Partial: 216 | [[Issue:5][Issue #5.]] 217 | 218 | This is a subtle problem to resolve. The specific case of a node 219 | being added to itself is trivial to resolve, and the fix has been 220 | put in for 0.8.3. 221 | 222 | However, the general problem is that in the current code, a node 223 | can be added as a child to any portion of the tree down the 224 | hierarchy (e.g., as a grandchild), which will need a more thorough 225 | detection code in the ~TreeNode#add~ method, if it is to be done at 226 | runtime. 227 | 228 | The issue is really to prevent the tree becoming a graph. Note 229 | that the issue is with duplicate nodes, /not/ duplicated content. 230 | 231 | A few options exist: 232 | 1. Perform a runtime check in the ~TreeNode#add~ method. This will 233 | cause a performance hit as the tree becomes larger. 234 | 2. Allow the additions to go through, but create a new ~validate~ 235 | method that checks for such cycles. 236 | 3. Create separate configuration object which can be attached to 237 | the root of the tree, which allows per-tree configuration of 238 | the behavior - this does allow for the user to take control, 239 | but also introduces complications during tree mergers and 240 | spitting subtrees. 241 | 4. Create a registry (to be maintained at the root?) of all nodes, 242 | and use this for validating the node additions (and preventing 243 | duplicates). This needs to be a hash (to allow O(1) access), 244 | and will sacrifice memory. There might be a need to 245 | restructure the internals to make better use of memory. 246 | *** TODO Expand the examples section, and add supporting documentation 247 | 248 | *** TODO Create a cycle-detection/validation mechanism to prevent cyclic graphs of nodes. 249 | *** TODO Create a generic validation method to check for various issues in the created tree. 250 | *** TODO Add a FAQ document to the project. 251 | *** TODO The semantic of length is probably unclear. Should return the node_depth instead (or remove the method) 252 | The current equivalence of length to size should also be removed. 253 | 254 | *** TODO Create the basic UML diagrams and upload to the Site 255 | DEADLINE: <2010-01-04 Mon> 256 | 257 | *** TODO Add a YAML export method to the TreeNode class. 258 | 259 | *** TODO marshal_load method probably should be a class method. It currently clobbers self. 260 | *** DONE Revert the forced install of rubygem 2.1.11 from [[file:.travis.yml][.travis.yml]] :ARCHIVE: 261 | CLOSED: [2014-01-12 Sun 19:06] 262 | The issue seems to have been resolved with the 2.2.1 release of Rubygems. 263 | *** DONE [#A] Migrate the website and references from http://rubyforge.org/ :ARCHIVE: 264 | CLOSED: [2014-07-04 Fri 22:18] 265 | *** DONE Fix bug # [[http://rubyforge.org/tracker/index.php%3Ffunc%3Ddetail&aid%3D22535&group_id%3D1215&atid%3D4793][22535]]: The method Tree::TreeNode#depth is a misnomer. The current definition actually provides the height function. :ARCHIVE: 266 | DEADLINE: <2010-01-09 Sat> CLOSED: [2010-01-03 Sun 22:15] 267 | 268 | *** DONE Get the version control moved from CVS to Subversion (request submitted to RubyForge) :ARCHIVE: 269 | CLOSED: [2010-01-02 Sat 17:58] 270 | 271 | *** DONE Add logic in Rakefile to read the file list from Manifest.txt file. :ARCHIVE: 272 | CLOSED: [2009-12-31 Thu 23:37] 273 | -------------------------------------------------------------------------------- /examples/example_basic.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # example_basic.rb:: Basic usage of the tree library. 4 | # 5 | # Author: Anupam Sengupta 6 | # Time-stamp: <2022-06-19 22:52:29 anupam> 7 | # Copyright (C) 2013, 2015, 2022 Anupam Sengupta 8 | # 9 | # The following example implements this tree structure: 10 | # 11 | # +------------+ 12 | # | ROOT | 13 | # +-----+------+ 14 | # +-------------+------------+ 15 | # | | 16 | # +-------+-------+ +-------+-------+ 17 | # | CHILD 1 | | CHILD 2 | 18 | # +-------+-------+ +---------------+ 19 | # | 20 | # | 21 | # +-------+-------+ 22 | # | GRANDCHILD 1 | 23 | # +---------------+ 24 | # 25 | # frozen_string_literal: true 26 | 27 | # ..... Example starts. 28 | require 'tree' # Load the library 29 | 30 | # ..... Create the root node first. Note that every node has a name and an 31 | # ..... optional content payload. 32 | root_node = Tree::TreeNode.new('ROOT', 'Root Content') 33 | root_node.print_tree 34 | 35 | # ..... Now insert the child nodes. Note that you can "chain" the child 36 | # ..... insertions for a given path to any depth. 37 | root_node << Tree::TreeNode.new('CHILD1', 'Child1 Content') \ 38 | << Tree::TreeNode.new('GRANDCHILD1', 'GrandChild1 Content') 39 | root_node << Tree::TreeNode.new('CHILD2', 'Child2 Content') 40 | 41 | # ..... Lets print the representation to stdout. This is primarily used for 42 | # ..... debugging purposes. 43 | root_node.print_tree 44 | 45 | # ..... Lets directly access children and grandchildren of the root. The can be 46 | # ..... "chained" for a given path to any depth. 47 | child1 = root_node['CHILD1'] 48 | grand_child1 = root_node['CHILD1']['GRANDCHILD1'] 49 | 50 | # ..... Now lets retrieve siblings of the current node as an array. 51 | siblings_of_child1 = child1.siblings 52 | 53 | # ..... Lets retrieve immediate children of the root node as an array. 54 | children_of_root = root_node.children 55 | 56 | # ..... Retrieve the parent of a node. 57 | parent = child1.parent 58 | 59 | # ..... This is a depth-first and L-to-R pre-ordered traversal. 60 | root_node.each { |node| node.content.reverse } 61 | 62 | # ..... Lets remove a child node from the root node. 63 | root_node.remove!(child1) 64 | -------------------------------------------------------------------------------- /lib/rubytree.rb: -------------------------------------------------------------------------------- 1 | # rubytree.rb - This file is part of the RubyTree package. 2 | # 3 | 4 | # = rubytree.rb - Generic implementation of an N-ary tree data structure. 5 | # 6 | # This file provides an alternative mechanism to require 'tree.rb', in case 7 | # there is a conflict with another gem or Ruby package. 8 | # 9 | # Author:: Anupam Sengupta (anupamsg@gmail.com) 10 | # 11 | # Copyright (c) 2012-2022 Anupam Sengupta. All rights reserved. 12 | # 13 | # Redistribution and use in source and binary forms, with or without 14 | # modification, are permitted provided that the following conditions are met: 15 | # 16 | # - Redistributions of source code must retain the above copyright notice, this 17 | # list of conditions and the following disclaimer. 18 | # 19 | # - Redistributions in binary form must reproduce the above copyright notice, 20 | # this list of conditions and the following disclaimer in the documentation 21 | # and/or other materials provided with the distribution. 22 | # 23 | # - Neither the name of the organization nor the names of its contributors may 24 | # be used to endorse or promote products derived from this software without 25 | # specific prior written permission. 26 | # 27 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 28 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 29 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 30 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 31 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 32 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 33 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 34 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 35 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 36 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 37 | # 38 | # frozen_string_literal: true 39 | 40 | require 'tree' 41 | -------------------------------------------------------------------------------- /lib/tree.rb: -------------------------------------------------------------------------------- 1 | # tree.rb - This file is part of the RubyTree package. 2 | # 3 | # = tree.rb - Generic implementation of an N-ary tree data structure. 4 | # 5 | # Provides a generic tree data structure with ability to 6 | # store keyed node elements in the tree. This implementation 7 | # mixes in the Enumerable module. 8 | # 9 | # Author:: Anupam Sengupta (anupamsg@gmail.com) 10 | # 11 | 12 | # Copyright (c) 2006-2022 Anupam Sengupta. All rights reserved. 13 | # 14 | # Redistribution and use in source and binary forms, with or without 15 | # modification, are permitted provided that the following conditions are met: 16 | # 17 | # - Redistributions of source code must retain the above copyright notice, this 18 | # list of conditions and the following disclaimer. 19 | # 20 | # - Redistributions in binary form must reproduce the above copyright notice, 21 | # this list of conditions and the following disclaimer in the documentation 22 | # and/or other materials provided with the distribution. 23 | # 24 | # - Neither the name of the organization nor the names of its contributors may 25 | # be used to endorse or promote products derived from this software without 26 | # specific prior written permission. 27 | # 28 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 29 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 30 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 31 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 32 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 33 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 34 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 35 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 36 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 37 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 38 | # 39 | # frozen_string_literal: true 40 | 41 | require 'tree/tree_deps' 42 | 43 | # This module provides a *TreeNode* class whose instances are the primary 44 | # objects for representing nodes in the tree. 45 | # 46 | # This module also acts as the namespace for all classes in the *RubyTree* 47 | # package. 48 | module Tree 49 | # == TreeNode Class Description 50 | # 51 | # This class models the nodes for an *N-ary* tree data structure. The 52 | # nodes are *named*, and have a place-holder for the node data (i.e., 53 | # _content_ of the node). The node names are required to be *unique* 54 | # amongst the sibling/peer nodes. Note that the name is implicitly 55 | # used as an _ID_ within the data structure). 56 | # 57 | # The node's _content_ is *not* required to be unique across 58 | # different nodes in the tree, and can be +nil+ as well. 59 | # 60 | # The class provides various methods to navigate the tree, traverse 61 | # the structure, modify contents of the node, change position of the 62 | # node in the tree, and to make structural changes to the tree. 63 | # 64 | # A node can have any number of *child* nodes attached to it and 65 | # hence can be used to create N-ary trees. Access to the child 66 | # nodes can be made in order (with the conventional left to right 67 | # access), or randomly. 68 | # 69 | # The node also provides direct access to its *parent* node as well 70 | # as other superior parents in the path to root of the tree. In 71 | # addition, a node can also access its *sibling* nodes, if present. 72 | # 73 | # Note that while this implementation does not _explicitly_ support 74 | # directed graphs, the class itself makes no restrictions on 75 | # associating a node's *content* with multiple nodes in the tree. 76 | # However, having duplicate nodes within the structure is likely to 77 | # cause unpredictable behavior. 78 | # 79 | # == Example 80 | # 81 | # {include:file:examples/example_basic.rb} 82 | # 83 | # @author Anupam Sengupta 84 | # noinspection RubyTooManyMethodsInspection 85 | class TreeNode 86 | include Enumerable 87 | include Comparable 88 | include Tree::Utils::TreeMetricsHandler 89 | include Tree::Utils::TreePathHandler 90 | include Tree::Utils::JSONConverter 91 | include Tree::Utils::TreeMergeHandler 92 | include Tree::Utils::HashConverter 93 | 94 | # @!group Core Attributes 95 | 96 | # @!attribute [r] name 97 | # 98 | # Name of this node. Expected to be unique within the tree. 99 | # 100 | # Note that the name attribute really functions as an *ID* within 101 | # the tree structure, and hence the uniqueness constraint is 102 | # required. 103 | # 104 | # This may be changed in the future, but for now it is best to 105 | # retain unique names within the tree structure, and use the 106 | # +content+ attribute for any non-unique node requirements. 107 | # 108 | # If you want to change the name, you probably want to call +rename+ 109 | # instead. Note that +name=+ is a protected method. 110 | # 111 | # @see content 112 | # @see rename 113 | attr_accessor :name 114 | 115 | # @!attribute [rw] content 116 | # Content of this node. Can be +nil+. Note that there is no 117 | # uniqueness constraint related to this attribute. 118 | # 119 | # @see name 120 | attr_accessor :content 121 | 122 | # @!attribute [r] parent 123 | # Parent of this node. Will be +nil+ for a root node. 124 | attr_reader :parent 125 | 126 | # @!attribute [r] root 127 | # Root node for the (sub)tree to which this node belongs. 128 | # A root node's root is itself. 129 | # 130 | # @return [Tree::TreeNode] Root of the (sub)tree. 131 | def root 132 | root = self 133 | root = root.parent until root.root? 134 | root 135 | end 136 | 137 | # @!attribute [r] root? 138 | # Returns +true+ if this is a root node. Note that 139 | # orphaned children will also be reported as root nodes. 140 | # 141 | # @return [Boolean] +true+ if this is a root node. 142 | def root? 143 | @parent.nil? 144 | end 145 | 146 | alias is_root? root? # @todo: Aliased for eventual replacement 147 | 148 | # @!attribute [r] content? 149 | # +true+ if this node has content. 150 | # 151 | # @return [Boolean] +true+ if the node has content. 152 | def content? 153 | @content != nil 154 | end 155 | 156 | alias has_content? content? # @todo: Aliased for eventual replacement 157 | 158 | # @!attribute [r] leaf? 159 | # +true+ if this node is a _leaf_ - i.e., one without 160 | # any children. 161 | # 162 | # @return [Boolean] +true+ if this is a leaf node. 163 | # 164 | # @see #children? 165 | def leaf? 166 | !children? 167 | end 168 | 169 | alias is_leaf? leaf? # @todo: Aliased for eventual replacement 170 | 171 | # @!attribute [r] parentage 172 | # An array of ancestors of this node in reversed order 173 | # (the first element is the immediate parent of this node). 174 | # 175 | # Returns +nil+ if this is a root node. 176 | # 177 | # @return [Array] An array of ancestors of this node 178 | # @return [nil] if this is a root node. 179 | def parentage 180 | return nil if root? 181 | 182 | parentage_array = [] 183 | prev_parent = parent 184 | while prev_parent 185 | parentage_array << prev_parent 186 | prev_parent = prev_parent.parent 187 | end 188 | parentage_array 189 | end 190 | 191 | # @!attribute [r] children? 192 | # +true+ if the this node has any child node. 193 | # 194 | # @return [Boolean] +true+ if child nodes exist. 195 | # 196 | # @see #leaf? 197 | def children? 198 | !@children.empty? 199 | end 200 | 201 | alias has_children? children? # @todo: Aliased for eventual replacement 202 | 203 | # @!group Node Creation 204 | 205 | # Creates a new node with a name and optional content. 206 | # The node name is expected to be unique within the tree. 207 | # 208 | # The content can be of any type, and defaults to +nil+. 209 | # 210 | # @param [Object] name Name of the node. Conventional usage is to pass a 211 | # String (Integer names may cause *surprises*) 212 | # 213 | # @param [Object] content Content of the node. 214 | # 215 | # @raise [ArgumentError] Raised if the node name is empty. 216 | # 217 | # @note If the name is an +Integer+, then the semantics of {#[]} access 218 | # method can be surprising, as an +Integer+ parameter to that method 219 | # normally acts as an index to the children array, and follows the 220 | # _zero-based_ indexing convention. 221 | # 222 | # @see #[] 223 | def initialize(name, content = nil) 224 | raise ArgumentError, 'Node name HAS to be provided!' if name.nil? 225 | 226 | name = name.to_s if name.is_a?(Integer) 227 | @name = name 228 | @content = content 229 | 230 | set_as_root! 231 | @children_hash = {} 232 | @children = [] 233 | end 234 | 235 | # Returns a copy of this node, with its parent and children links removed. 236 | # The original node remains attached to its tree. 237 | # 238 | # @return [Tree::TreeNode] A copy of this node. 239 | def detached_copy 240 | cloned_content = 241 | begin 242 | @content&.clone 243 | rescue TypeError 244 | @content 245 | end 246 | self.class.new(@name, cloned_content) 247 | end 248 | 249 | # Returns a copy of entire (sub-)tree from this node. 250 | # 251 | # @author Vincenzo Farruggia 252 | # @since 0.8.0 253 | # 254 | # @return [Tree::TreeNode] A copy of (sub-)tree from this node. 255 | def detached_subtree_copy 256 | new_node = detached_copy 257 | children { |child| new_node << child.detached_subtree_copy } 258 | new_node 259 | end 260 | 261 | # Alias for {Tree::TreeNode#detached_subtree_copy} 262 | # 263 | # @see Tree::TreeNode#detached_subtree_copy 264 | alias dup detached_subtree_copy 265 | 266 | # Returns a {marshal-dump}[http://ruby-doc.org/core-1.8.7/Marshal.html] 267 | # representation of the (sub)tree rooted at this node. 268 | # 269 | def marshal_dump 270 | collect(&:create_dump_rep) 271 | end 272 | 273 | # Creates a dump representation of this node and returns the same as 274 | # a hash. 275 | def create_dump_rep # :nodoc: 276 | { name: @name, 277 | parent: (root? ? nil : @parent.name), 278 | content: Marshal.dump(@content) } 279 | end 280 | 281 | protected :create_dump_rep 282 | 283 | # Loads a marshaled dump of a tree and returns the root node of the 284 | # reconstructed tree. See the 285 | # {Marshal}[http://ruby-doc.org/core-1.8.7/Marshal.html] class for 286 | # additional details. 287 | # 288 | # NOTE: This is a potentially *unsafe* method with similar concerns as with 289 | # the Marshal#load method, and should *not* be used with untrusted user 290 | # provided data. 291 | # 292 | # @todo This method probably should be a class method. It currently clobbers 293 | # self and makes itself the root. 294 | # 295 | def marshal_load(dumped_tree_array) 296 | nodes = {} 297 | dumped_tree_array.each do |node_hash| 298 | name = node_hash[:name] 299 | parent_name = node_hash[:parent] 300 | content = Marshal.load(node_hash[:content]) 301 | 302 | if parent_name 303 | nodes[name] = current_node = self.class.new(name, content) 304 | nodes[parent_name].add current_node 305 | else 306 | # This is the root node, hence initialize self. 307 | initialize(name, content) 308 | 309 | nodes[name] = self # Add self to the list of nodes 310 | end 311 | end 312 | end 313 | 314 | # @!endgroup 315 | 316 | # Returns string representation of this node. 317 | # This method is primarily meant for debugging purposes. 318 | # 319 | # @return [String] A string representation of the node. 320 | def to_s 321 | "Node Name: #{@name} Content: #{@content.to_s || ''} " \ 322 | "Parent: #{root? ? '' : @parent.name.to_s} " \ 323 | "Children: #{@children.length} Total Nodes: #{size}" 324 | end 325 | 326 | # @!group Structure Modification 327 | 328 | # Convenience synonym for {Tree::TreeNode#add} method. 329 | # 330 | # This method allows an easy mechanism to add node hierarchies to the tree 331 | # on a given path via chaining the method calls to successive child nodes. 332 | # 333 | # @example Add a child and grand-child to the root 334 | # root << child << grand_child 335 | # 336 | # @param [Tree::TreeNode] child the child node to add. 337 | # 338 | # @return [Tree::TreeNode] The added child node. 339 | # 340 | # @see Tree::TreeNode#add 341 | def <<(child) 342 | add(child) 343 | end 344 | 345 | # Adds the specified child node to this node. 346 | # 347 | # This method can also be used for *grafting* a subtree into this 348 | # node's tree, if the specified child node is the root of a subtree (i.e., 349 | # has child nodes under it). 350 | # 351 | # this node becomes parent of the node passed in as the argument, and 352 | # the child is added as the last child ("right most") in the current set of 353 | # children of this node. 354 | # 355 | # Additionally you can specify a insert position. The new node will be 356 | # inserted BEFORE that position. If you don't specify any position the node 357 | # will be just appended. This feature is provided to make implementation of 358 | # node movement within the tree very simple. 359 | # 360 | # If an insertion position is provided, it needs to be within the valid 361 | # range of: 362 | # 363 | # -children.size..children.size 364 | # 365 | # This is to prevent +nil+ nodes being created as children if a non-existent 366 | # position is used. 367 | # 368 | # If the new node being added has an existing parent node, then it will be 369 | # removed from this pre-existing parent prior to being added as a child to 370 | # this node. I.e., the child node will effectively be moved from its old 371 | # parent to this node. In this situation, after the operation is complete, 372 | # the node will no longer exist as a child for its old parent. 373 | # 374 | # @param [Tree::TreeNode] child The child node to add. 375 | # 376 | # @param [optional, Number] at_index The optional position where the node is 377 | # to be inserted. 378 | # 379 | # @return [Tree::TreeNode] The added child node. 380 | # 381 | # @raise [RuntimeError] This exception is raised if another child node with 382 | # the same name exists, or if an invalid insertion 383 | # position is specified. 384 | # 385 | # @raise [ArgumentError] This exception is raised if a +nil+ node is passed 386 | # as the argument. 387 | # 388 | # @see #<< 389 | def add(child, at_index = -1) 390 | # Only handles the immediate child scenario 391 | raise ArgumentError, 'Attempting to add a nil node' unless child 392 | 393 | raise ArgumentError, 'Attempting add node to itself' if equal?(child) 394 | 395 | raise ArgumentError, 'Attempting add root as a child' if child.equal?(root) 396 | 397 | # Lazy man's unique test, won't test if children of child are unique in 398 | # this tree too. 399 | raise "Child #{child.name} already added!"\ 400 | if @children_hash.include?(child.name) 401 | 402 | child.parent&.remove! child # Detach from the old parent 403 | 404 | if insertion_range.include?(at_index) 405 | @children.insert(at_index, child) 406 | else 407 | raise 'Attempting to insert a child at a non-existent location'\ 408 | " (#{at_index}) "\ 409 | 'when only positions from '\ 410 | "#{insertion_range.min} to #{insertion_range.max} exist." 411 | end 412 | 413 | @children_hash[child.name] = child 414 | child.parent = self 415 | child 416 | end 417 | 418 | # Return a range of valid insertion positions. Used in the #add method. 419 | def insertion_range 420 | max = @children.size 421 | min = -(max + 1) 422 | min..max 423 | end 424 | 425 | private :insertion_range 426 | 427 | # Renames the node and updates the parent's reference to it 428 | # 429 | # @param [Object] new_name Name of the node. Conventional usage is to pass a 430 | # String (Integer names may cause *surprises*) 431 | # 432 | # @return [Object] The old name 433 | def rename(new_name) 434 | old_name = @name 435 | 436 | if root? 437 | self.name = new_name 438 | else 439 | @parent.rename_child old_name, new_name 440 | end 441 | 442 | old_name 443 | end 444 | 445 | # Renames the specified child node 446 | # 447 | # @param [Object] old_name old Name of the node. Conventional usage is to 448 | # pass a String (Integer names may cause *surprises*) 449 | # 450 | # @param [Object] new_name new Name of the node. Conventional usage is to 451 | # pass a String (Integer names may cause *surprises*) 452 | def rename_child(old_name, new_name) 453 | raise ArgumentError, "Invalid child name specified: #{old_name}"\ 454 | unless @children_hash.key?(old_name) 455 | 456 | @children_hash[new_name] = @children_hash.delete(old_name) 457 | @children_hash[new_name].name = new_name 458 | end 459 | 460 | # Replaces the specified child node with another child node on this node. 461 | # 462 | # @param [Tree::TreeNode] old_child The child node to be replaced. 463 | # @param [Tree::TreeNode] new_child The child node to be replaced with. 464 | # 465 | # @return [Tree::TreeNode] The removed child node 466 | def replace!(old_child, new_child) 467 | child_index = @children.find_index(old_child) 468 | 469 | old_child = remove! old_child 470 | add new_child, child_index 471 | 472 | old_child 473 | end 474 | 475 | # Replaces the node with another node 476 | # 477 | # @param [Tree::TreeNode] node The node to replace this node with 478 | # 479 | # @return [Tree::TreeNode] The replaced child node 480 | def replace_with(node) 481 | @parent.replace!(self, node) 482 | end 483 | 484 | # Removes the specified child node from this node. 485 | # 486 | # This method can also be used for *pruning* a sub-tree, in cases where the removed child node is 487 | # the root of the sub-tree to be pruned. 488 | # 489 | # The removed child node is orphaned but accessible if an alternate reference exists. If accessible via 490 | # an alternate reference, the removed child will report itself as a root node for its sub-tree. 491 | # 492 | # @param [Tree::TreeNode] child The child node to remove. 493 | # 494 | # @return [Tree::TreeNode] The removed child node, or +nil+ if a +nil+ was passed in as argument. 495 | # 496 | # @see #remove_from_parent! 497 | # @see #remove_all! 498 | def remove!(child) 499 | return nil unless child 500 | 501 | @children_hash.delete(child.name) 502 | @children.delete(child) 503 | child.set_as_root! 504 | child 505 | end 506 | 507 | # Protected method to set the parent node for this node. 508 | # This method should *NOT* be invoked by client code. 509 | # 510 | # @param [Tree::TreeNode] parent The parent node. 511 | # 512 | # @return [Tree::TreeNode] The parent node. 513 | def parent=(parent) # :nodoc: 514 | @parent = parent 515 | @node_depth = nil 516 | end 517 | 518 | protected :parent=, :name= 519 | 520 | # Removes this node from its parent. This node becomes the new root for its 521 | # subtree. 522 | # 523 | # If this is the root node, then does nothing. 524 | # 525 | # @return [Tree:TreeNode] +self+ (the removed node) if the operation is 526 | # successful, +nil+ otherwise. 527 | # 528 | # @see #remove_all! 529 | def remove_from_parent! 530 | @parent.remove!(self) unless root? 531 | end 532 | 533 | # Removes all children from this node. If an independent reference exists to 534 | # the child nodes, then these child nodes report themselves as roots after 535 | # this operation. 536 | # 537 | # @return [Tree::TreeNode] this node (+self+) 538 | # 539 | # @see #remove! 540 | # @see #remove_from_parent! 541 | def remove_all! 542 | @children.each(&:remove_all!) 543 | 544 | @children_hash.clear 545 | @children.clear 546 | self 547 | end 548 | 549 | # Protected method which sets this node as a root node. 550 | # 551 | # @return +nil+. 552 | def set_as_root! # :nodoc: 553 | self.parent = nil 554 | end 555 | 556 | protected :set_as_root! 557 | 558 | # Freezes all nodes in the (sub)tree rooted at this node. 559 | # 560 | # The nodes become immutable after this operation. In effect, the entire tree's 561 | # structure and contents become _read-only_ and cannot be changed. 562 | def freeze_tree! 563 | each(&:freeze) 564 | end 565 | 566 | # @!endgroup 567 | 568 | # @!group Tree Traversal 569 | 570 | # Returns the requested node from the set of immediate children. 571 | # 572 | # - If the +name+ argument is an _Integer_, then the in-sequence 573 | # array of children is accessed using the argument as the 574 | # *index* (zero-based). 575 | # 576 | # - If the +name+ argument is *NOT* an _Integer_, then it is taken to 577 | # be the *name* of the child node to be returned. 578 | # 579 | # - To use an _Integer_ as the name, convert it to a _String_ first using 580 | # +.to_s+. 581 | # 582 | # @param [String|Number] name_or_index Name of the child, or its 583 | # positional index in the array of child nodes. 584 | # 585 | # @return [Tree::TreeNode] the requested child node. If the index 586 | # in not in range, or the name is not present, then a +nil+ 587 | # is returned. 588 | # 589 | # @raise [ArgumentError] Raised if the +name_or_index+ argument is +nil+. 590 | # 591 | # @see #add 592 | # @see #initialize 593 | def [](name_or_index) 594 | raise ArgumentError, 'Name_or_index needs to be provided!' if name_or_index.nil? 595 | 596 | if name_or_index.is_a?(Integer) 597 | @children[name_or_index] 598 | else 599 | @children_hash[name_or_index] 600 | end 601 | end 602 | 603 | # Traverses each node (including this node) of the (sub)tree rooted at this 604 | # node by yielding the nodes to the specified block. 605 | # 606 | # The traversal is *depth-first* and from *left-to-right* in pre-ordered 607 | # sequence. 608 | # 609 | # @yieldparam node [Tree::TreeNode] Each node. 610 | # 611 | # @see #preordered_each 612 | # @see #breadth_each 613 | # 614 | # @return [Tree::TreeNode] this node, if a block if given 615 | # @return [Enumerator] an enumerator on this tree, if a block is *not* given 616 | # noinspection RubyUnusedLocalVariable 617 | def each # :yields: node 618 | return to_enum unless block_given? 619 | 620 | node_stack = [self] # Start with this node 621 | 622 | until node_stack.empty? 623 | current = node_stack.shift # Pop the top-most node 624 | next unless current # Might be 'nil' (esp. for binary trees) 625 | 626 | yield current # and process it 627 | # Stack children of the current node at top of the stack 628 | node_stack = current.children.concat(node_stack) 629 | end 630 | 631 | self if block_given? 632 | end 633 | 634 | # Traverses the (sub)tree rooted at this node in pre-ordered sequence. 635 | # This is a synonym of {Tree::TreeNode#each}. 636 | # 637 | # @yieldparam node [Tree::TreeNode] Each node. 638 | # 639 | # @see #each 640 | # @see #breadth_each 641 | # 642 | # @return [Tree::TreeNode] this node, if a block if given 643 | # @return [Enumerator] an enumerator on this tree, if a block is *not* given 644 | def preordered_each(&block) # :yields: node 645 | each(&block) 646 | end 647 | 648 | # Traverses the (sub)tree rooted at this node in post-ordered sequence. 649 | # 650 | # @yieldparam node [Tree::TreeNode] Each node. 651 | # 652 | # @see #preordered_each 653 | # @see #breadth_each 654 | # @return [Tree::TreeNode] this node, if a block if given 655 | # @return [Enumerator] an enumerator on this tree, if a block is *not* given 656 | # noinspection RubyUnusedLocalVariable 657 | def postordered_each 658 | return to_enum(:postordered_each) unless block_given? 659 | 660 | # Using a marked node in order to skip adding the children of nodes that 661 | # have already been visited. This allows the stack depth to be controlled, 662 | # and also allows stateful backtracking. 663 | marked_node = Struct.new(:node, :visited) 664 | node_stack = [marked_node.new(self, false)] # Start with self 665 | 666 | until node_stack.empty? 667 | peek_node = node_stack[0] 668 | if peek_node.node.children? && !peek_node.visited 669 | peek_node.visited = true 670 | # Add the children to the stack. Use the marking structure. 671 | marked_children = 672 | peek_node.node.children.map { |node| marked_node.new(node, false) } 673 | node_stack = marked_children.concat(node_stack) 674 | next 675 | else 676 | yield node_stack.shift.node # Pop and yield the current node 677 | end 678 | end 679 | 680 | self if block_given? 681 | end 682 | 683 | # Performs breadth-first traversal of the (sub)tree rooted at this node. The 684 | # traversal at a given level is from *left-to-right*. this node itself is 685 | # the first node to be traversed. 686 | # 687 | # @yieldparam node [Tree::TreeNode] Each node. 688 | # 689 | # @see #preordered_each 690 | # @see #breadth_each 691 | # 692 | # @return [Tree::TreeNode] this node, if a block if given 693 | # @return [Enumerator] an enumerator on this tree, if a block is *not* given 694 | # noinspection RubyUnusedLocalVariable 695 | def breadth_each 696 | return to_enum(:breadth_each) unless block_given? 697 | 698 | node_queue = [self] # Create a queue with self as the initial entry 699 | 700 | # Use a queue to do breadth traversal 701 | until node_queue.empty? 702 | node_to_traverse = node_queue.shift 703 | yield node_to_traverse 704 | # Enqueue the children from left to right. 705 | node_to_traverse.children { |child| node_queue.push child } 706 | end 707 | 708 | self if block_given? 709 | end 710 | 711 | # An array of all the immediate children of this node. The child 712 | # nodes are ordered "left-to-right" in the returned array. 713 | # 714 | # If a block is given, yields each child node to the block 715 | # traversing from left to right. 716 | # 717 | # @yieldparam child [Tree::TreeNode] Each child node. 718 | # 719 | # @return [Tree::TreeNode] This node, if a block is given 720 | # 721 | # @return [Array] An array of the child nodes, if no block 722 | # is given. 723 | def children(&block) 724 | if block_given? 725 | @children.each(&block) 726 | self 727 | else 728 | @children.clone 729 | end 730 | end 731 | 732 | # Yields every leaf node of the (sub)tree rooted at this node to the 733 | # specified block. 734 | # 735 | # May yield this node as well if this is a leaf node. 736 | # Leaf traversal is *depth-first* and *left-to-right*. 737 | # 738 | # @yieldparam node [Tree::TreeNode] Each leaf node. 739 | # 740 | # @see #each 741 | # @see #breadth_each 742 | # 743 | # @return [Tree::TreeNode] this node, if a block if given 744 | # @return [Array] An array of the leaf nodes 745 | # noinspection RubyUnusedLocalVariable 746 | def each_leaf 747 | if block_given? 748 | each { |node| yield(node) if node.leaf? } 749 | self 750 | else 751 | self.select(&:leaf?) 752 | end 753 | end 754 | 755 | # Yields every level of the (sub)tree rooted at this node to the 756 | # specified block. 757 | # 758 | # Will yield this node as well since it is considered the first level. 759 | # 760 | # @yieldparam level [Array] All nodes in the level 761 | # 762 | # @return [Tree::TreeNode] this node, if a block if given 763 | # @return [Enumerator] an enumerator on this tree, if a block is *not* given 764 | def each_level 765 | if block_given? 766 | level = [self] 767 | until level.empty? 768 | yield level 769 | level = level.map(&:children).flatten 770 | end 771 | self 772 | else 773 | each 774 | end 775 | end 776 | 777 | # @!endgroup 778 | 779 | # @!group Navigating the Child Nodes 780 | 781 | # First child of this node. 782 | # Will be +nil+ if no children are present. 783 | # 784 | # @return [Tree::TreeNode] The first child, or +nil+ if none is present. 785 | def first_child 786 | @children.first 787 | end 788 | 789 | # Last child of this node. 790 | # Will be +nil+ if no children are present. 791 | # 792 | # @return [Tree::TreeNode] The last child, or +nil+ if none is present. 793 | def last_child 794 | @children.last 795 | end 796 | 797 | # @!group Navigating the Sibling Nodes 798 | 799 | # First sibling of this node. If this is the root node, then returns 800 | # itself. 801 | # 802 | # 'First' sibling is defined as follows: 803 | # 804 | # First sibling:: The left-most child of this node's parent, which may be 805 | # this node itself 806 | # 807 | # @return [Tree::TreeNode] The first sibling node. 808 | # 809 | # @see #first_sibling? 810 | # @see #last_sibling 811 | def first_sibling 812 | root? ? self : parent.children.first 813 | end 814 | 815 | # Returns +true+ if this node is the first sibling at its level. 816 | # 817 | # @return [Boolean] +true+ if this is the first sibling. 818 | # 819 | # @see #last_sibling? 820 | # @see #first_sibling 821 | def first_sibling? 822 | first_sibling == self 823 | end 824 | 825 | alias is_first_sibling? first_sibling? # @todo: Aliased for eventual replacement 826 | 827 | # Last sibling of this node. If this is the root node, then returns 828 | # itself. 829 | # 830 | # 'Last' sibling is defined as follows: 831 | # 832 | # Last sibling:: The right-most child of this node's parent, which may be 833 | # this node itself 834 | # 835 | # @return [Tree::TreeNode] The last sibling node. 836 | # 837 | # @see #last_sibling? 838 | # @see #first_sibling 839 | def last_sibling 840 | root? ? self : parent.children.last 841 | end 842 | 843 | # Returns +true+ if this node is the last sibling at its level. 844 | # 845 | # @return [Boolean] +true+ if this is the last sibling. 846 | # 847 | # @see #first_sibling? 848 | # @see #last_sibling 849 | def last_sibling? 850 | last_sibling == self 851 | end 852 | 853 | alias is_last_sibling? last_sibling? # @todo: Aliased for eventual replacement 854 | 855 | # An array of siblings for this node. This node is excluded. 856 | # 857 | # If a block is provided, yields each of the sibling nodes to the block. 858 | # The root always has +nil+ siblings. 859 | # 860 | # @yieldparam sibling [Tree::TreeNode] Each sibling node. 861 | # 862 | # @return [Array] Array of siblings of this node. Will 863 | # return an empty array for *root* 864 | # 865 | # @return [Tree::TreeNode] This node, if no block is given 866 | # 867 | # @see #first_sibling 868 | # @see #last_sibling 869 | def siblings 870 | if block_given? 871 | parent.children.each { |sibling| yield sibling if sibling != self } 872 | self 873 | else 874 | return [] if root? 875 | 876 | siblings = [] 877 | parent.children do |my_sibling| 878 | siblings << my_sibling if my_sibling != self 879 | end 880 | siblings 881 | end 882 | end 883 | 884 | # +true+ if this node is the only child of its parent. 885 | # 886 | # As a special case, a root node will always return +true+. 887 | # 888 | # @return [Boolean] +true+ if this is the only child of its parent. 889 | # 890 | # @see #siblings 891 | def only_child? 892 | root? ? true : parent.children.size == 1 893 | end 894 | 895 | alias is_only_child? only_child? # @todo: Aliased for eventual replacement 896 | 897 | # Next sibling for this node. 898 | # The _next_ node is defined as the node to right of this node. 899 | # 900 | # Will return +nil+ if no subsequent node is present, or if this is a root 901 | # node. 902 | # 903 | # @return [Tree::treeNode] the next sibling node, if present. 904 | # 905 | # @see #previous_sibling 906 | # @see #siblings 907 | def next_sibling 908 | return nil if root? 909 | 910 | idx = parent.children.index(self) 911 | parent.children.at(idx + 1) if idx 912 | end 913 | 914 | # Previous sibling of this node. 915 | # _Previous_ node is defined to be the node to left of this node. 916 | # 917 | # Will return +nil+ if no predecessor node is present, or if this is a root 918 | # node. 919 | # 920 | # @return [Tree::treeNode] the previous sibling node, if present. 921 | # 922 | # @see #next_sibling 923 | # @see #siblings 924 | def previous_sibling 925 | return nil if root? 926 | 927 | idx = parent.children.index(self) 928 | parent.children.at(idx - 1) if idx&.positive? 929 | end 930 | 931 | # @!endgroup 932 | 933 | # Provides a comparison operation for the nodes. 934 | # 935 | # Comparison is based on the natural ordering of the node name objects. 936 | # 937 | # @param [Tree::TreeNode] other The other node to compare against. 938 | # 939 | # @return [Integer] +1 if this node is a 'successor', 0 if equal and -1 if 940 | # this node is a 'predecessor'. Returns 'nil' if the other 941 | # object is not a 'Tree::TreeNode'. 942 | def <=>(other) 943 | return nil if other.nil? || !other.is_a?(Tree::TreeNode) 944 | 945 | name <=> other.name 946 | end 947 | 948 | # Pretty prints the (sub)tree rooted at this node. 949 | # 950 | # @param [Integer] level The indentation level (4 spaces) to start with. 951 | # @param [Integer] max_depth optional maximum depth at which the printing 952 | # with stop. 953 | # @param [Proc] block optional block to use for rendering 954 | def print_tree(level = node_depth, max_depth = nil, 955 | block = lambda { |node, prefix| 956 | puts "#{prefix} #{node.name}" 957 | }) 958 | prefix = ''.dup # dup NEEDs to be invoked to make this mutable. 959 | 960 | if root? 961 | prefix << '*' 962 | else 963 | prefix << '|' unless parent.last_sibling? 964 | prefix << (' ' * (level - 1) * 4) 965 | prefix << (last_sibling? ? '+' : '|') 966 | prefix << '---' 967 | prefix << (children? ? '+' : '>') 968 | end 969 | 970 | block.call(self, prefix) 971 | 972 | # Exit if the max level is defined, and reached. 973 | return unless max_depth.nil? || level < max_depth 974 | 975 | # Child might be 'nil' 976 | children do |child| 977 | child&.print_tree(level + 1, max_depth, block) 978 | end 979 | end 980 | end 981 | end 982 | -------------------------------------------------------------------------------- /lib/tree/binarytree.rb: -------------------------------------------------------------------------------- 1 | # binarytree.rb - This file is part of the RubyTree package. 2 | # 3 | # = binarytree.rb - An implementation of the binary tree data structure. 4 | # 5 | # Provides a binary tree data structure with ability to 6 | # store two node elements as children at each node in the tree. 7 | # 8 | # Author:: Anupam Sengupta (anupamsg@gmail.com) 9 | # 10 | 11 | # Copyright (c) 2007-2022 Anupam Sengupta. All rights reserved. 12 | # 13 | # Redistribution and use in source and binary forms, with or without 14 | # modification, are permitted provided that the following conditions are met: 15 | # 16 | # - Redistributions of source code must retain the above copyright notice, this 17 | # list of conditions and the following disclaimer. 18 | # 19 | # - Redistributions in binary form must reproduce the above copyright notice, 20 | # this list of conditions and the following disclaimer in the documentation 21 | # and/or other materials provided with the distribution. 22 | # 23 | # - Neither the name of the organization nor the names of its contributors may 24 | # be used to endorse or promote products derived from this software without 25 | # specific prior written permission. 26 | # 27 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 28 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 29 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 30 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 31 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 32 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 33 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 34 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 35 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 36 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 37 | # 38 | # frozen_string_literal: true 39 | 40 | require_relative '../tree' 41 | 42 | module Tree 43 | # Provides a Binary tree implementation. This node allows only two child nodes 44 | # (left and right child). It also provides direct access to the left or right 45 | # child, including assignment to the same. 46 | # 47 | # This inherits from the {Tree::TreeNode} class. 48 | # 49 | # @author Anupam Sengupta 50 | # 51 | class BinaryTreeNode < TreeNode 52 | # @!group Core Attributes 53 | 54 | # @!attribute [rw] left_child 55 | # Left child of the receiver node. Note that left Child == first Child. 56 | # 57 | # @return [Tree::BinaryTreeNode] The left most (or first) child. 58 | # 59 | # @see #right_child 60 | def left_child 61 | children.first 62 | end 63 | 64 | # @!attribute [rw] right_child 65 | # Right child of the receiver node. Note that right child == last child 66 | # unless there is only one child. 67 | # 68 | # Returns +nil+ if the right child does not exist. 69 | # 70 | # @return [Tree::BinaryTreeNode] The right child, or +nil+ if the right side 71 | # child does not exist. 72 | # 73 | # @see #left_child 74 | def right_child 75 | children[1] 76 | end 77 | 78 | # @!attribute left_child? 79 | # +true+ if the receiver node is the left child of its parent. 80 | # Always returns +false+ if it is a root node. 81 | # 82 | # @return [Boolean] +true+ if this is the left child of its parent. 83 | def left_child? 84 | return false if root? 85 | 86 | self == parent.left_child 87 | end 88 | 89 | alias is_left_child? left_child? # @todo: Aliased for eventual replacement 90 | 91 | # @!attribute [r] right_child? 92 | # +true+ if the receiver node is the right child of its parent. 93 | # Always returns +false+ if it is a root node. 94 | # 95 | # @return [Boolean] +true+ if this is the right child of its parent. 96 | def right_child? 97 | return false if root? 98 | 99 | self == parent.right_child 100 | end 101 | 102 | alias is_right_child? right_child? # @todo: Aliased for eventual replacement 103 | 104 | # @!group Structure Modification 105 | 106 | # Adds the specified child node to the receiver node. The child node's 107 | # parent is set to be the receiver. 108 | # 109 | # The child nodes are added in the order of addition, i.e., the first child 110 | # added becomes the left node, and the second child added will be the second 111 | # node. 112 | # 113 | # If only one child is present, then this will be the left child. 114 | # 115 | # @param [Tree::BinaryTreeNode] child The child to add. 116 | # 117 | # @raise [ArgumentError] This exception is raised if two children are 118 | # already present. 119 | def add(child) 120 | raise ArgumentError, 'Already has two child nodes' if @children.size == 2 121 | 122 | super(child) 123 | end 124 | 125 | # Instantiate and insert child nodes from data in a Ruby +Hash+ 126 | # 127 | # This method is used in conjunction with {Tree::TreeNode.from_hash} to 128 | # provide a convenient way of building and inserting child nodes present 129 | # in a Ruby hashes. 130 | # 131 | # This method will instantiate a {Tree::TreeNode} instance for each top- 132 | # level key of the input hash, to be inserted as children of the receiver 133 | # instance. 134 | # 135 | # Nested hashes are expected and further child nodes will be created and 136 | # added accordingly. If a hash key is a single value that value will be 137 | # used as the name for the node. If a hash key is an Array, both node 138 | # name and content will be populated. 139 | # 140 | # A leaf element of the tree should be represented as a hash key with 141 | # corresponding value nil or {}. 142 | # 143 | # @example 144 | # root = Tree::TreeNode.new(:A, "Root content!") 145 | # root.add_from_hash({:B => {:D => {}}, [:C, "C content!"] => {}}) 146 | # 147 | # @param [Hash] hashed_subtree The hash of child subtrees. 148 | # 149 | # @raise [ArgumentError] This exception is raised if hash contains too 150 | # many children. 151 | # => 152 | # @raise [ArgumentError] This exception is raised if a non-hash is passed. 153 | # @return [Array] Array of child nodes added 154 | def add_from_hash(hashed_subtree) 155 | raise ArgumentError, 'Too many children'\ 156 | if hashed_subtree.size + @children.size > 2 157 | 158 | super(hashed_subtree) 159 | end 160 | 161 | # Performs in-order traversal (including this node). 162 | # 163 | # @yieldparam node [Tree::BinaryTreeNode] Each node (in-order). 164 | # 165 | # @return [Tree::BinaryTreeNode] This node, if a block is given 166 | # @return [Enumerator] An enumerator on this tree, if a block is *not* given 167 | # 168 | # @since 0.9.0 169 | # 170 | # @see #each 171 | # @see #preordered_each 172 | # @see #postordered_each 173 | # noinspection RubyUnusedLocalVariable 174 | def inordered_each 175 | return to_enum unless block_given? 176 | 177 | node_stack = [] 178 | current_node = self 179 | 180 | until node_stack.empty? && current_node.nil? 181 | if current_node 182 | node_stack.push(current_node) 183 | current_node = current_node.left_child 184 | else 185 | current_node = node_stack.pop 186 | yield current_node 187 | current_node = current_node.right_child 188 | end 189 | end 190 | 191 | self if block_given? 192 | end 193 | 194 | # A protected method to allow insertion of child nodes at the specified 195 | # location. Note that this method allows insertion of +nil+ nodes. 196 | # 197 | # @param [Tree::BinaryTreeNode] child The child to add at the specified 198 | # location. 199 | # 200 | # @param [Integer] at_index The location to add the entry at (0 or 1). 201 | # 202 | # @return [Tree::BinaryTreeNode] The added child. 203 | # 204 | # @raise [ArgumentError] If the index is out of limits. 205 | def set_child_at(child, at_index) 206 | raise ArgumentError 'A binary tree cannot have more than two children.'\ 207 | unless (0..1).include? at_index 208 | 209 | @children[at_index] = child 210 | @children_hash[child.name] = child if child # Assign the name mapping 211 | child.parent = self if child 212 | child 213 | end 214 | 215 | protected :set_child_at 216 | 217 | # Sets the left child of the receiver node. If a previous child existed, it 218 | # is replaced. 219 | # 220 | # @param [Tree::BinaryTreeNode] child The child to add as the left-side 221 | # node. 222 | # 223 | # @return [Tree::BinaryTreeNode] The assigned child node. 224 | # 225 | # @see #left_child 226 | # @see #right_child= 227 | def left_child=(child) 228 | set_child_at child, 0 229 | end 230 | 231 | # Sets the right child of the receiver node. If a previous child existed, it 232 | # is replaced. 233 | # 234 | # @param [Tree::BinaryTreeNode] child The child to add as the right-side 235 | # node. 236 | # 237 | # @return [Tree::BinaryTreeNode] The assigned child node. 238 | # 239 | # @see #right_child 240 | # @see #left_child= 241 | def right_child=(child) 242 | set_child_at child, 1 243 | end 244 | 245 | # Swaps the left and right child nodes of the receiver node with each other. 246 | def swap_children 247 | self.left_child, self.right_child = right_child, left_child 248 | end 249 | end 250 | end 251 | -------------------------------------------------------------------------------- /lib/tree/tree_deps.rb: -------------------------------------------------------------------------------- 1 | # rubytree_deps.rb - This file is part of the RubyTree package. 2 | # 3 | # = rubytree_deps.rb - Dependencies for RubyTree 4 | # 5 | # Centralizes and lists the dependencies for the RubyTree gem. 6 | # 7 | # Author:: Anupam Sengupta (anupamsg@gmail.com) 8 | # 9 | 10 | # Copyright (c) 2006-2022 Anupam Sengupta. All rights reserved. 11 | # 12 | # Redistribution and use in source and binary forms, with or without 13 | # modification, are permitted provided that the following conditions are met: 14 | # 15 | # - Redistributions of source code must retain the above copyright notice, this 16 | # list of conditions and the following disclaimer. 17 | # 18 | # - Redistributions in binary form must reproduce the above copyright notice, 19 | # this list of conditions and the following disclaimer in the documentation 20 | # and/or other materials provided with the distribution. 21 | # 22 | # - Neither the name of the organization nor the names of its contributors may 23 | # be used to endorse or promote products derived from this software without 24 | # specific prior written permission. 25 | # 26 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 27 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 28 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 29 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 30 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 31 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 32 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 33 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 34 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 35 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | # 37 | # frozen_string_literal: true 38 | 39 | require 'json' 40 | 41 | require_relative '../tree/version' 42 | require_relative '../tree/utils/metrics_methods' 43 | require_relative '../tree/utils/path_methods' 44 | require_relative '../tree/utils/json_converter' 45 | require_relative '../tree/utils/tree_merge_handler' 46 | require_relative '../tree/utils/hash_converter' 47 | -------------------------------------------------------------------------------- /lib/tree/utils/hash_converter.rb: -------------------------------------------------------------------------------- 1 | # hash_converter.rb - This file is part of the RubyTree package. 2 | # 3 | # = hash_converter.rb - Provides utility methods for converting between 4 | # {Tree::TreeNode} and Ruby's native +Hash+. 5 | # 6 | # Author:: Jen Hamon (http://www.github.com/jhamon) 7 | # 8 | # Time-stamp: <2022-06-20 22:16:39 anupam> 9 | # 10 | # Copyright (C) 2014, 2015, 2022, 2022 Jen Hamon (http://www.github.com/jhamon) and 11 | # Anupam Sengupta 12 | # 13 | # All rights reserved. 14 | # 15 | # Redistribution and use in source and binary forms, with or without 16 | # modification, are permitted provided that the following conditions are met: 17 | # 18 | # - Redistributions of source code must retain the above copyright notice, this 19 | # list of conditions and the following disclaimer. 20 | # 21 | # - Redistributions in binary form must reproduce the above copyright notice, 22 | # this list of conditions and the following disclaimer in the documentation 23 | # and/or other materials provided with the distribution. 24 | # 25 | # - Neither the name of the organization nor the names of its contributors may 26 | # be used to endorse or promote products derived from this software without 27 | # specific prior written permission. 28 | # 29 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 30 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 31 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 32 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 33 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 34 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 35 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 36 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 37 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 38 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 39 | # 40 | # frozen_string_literal: true 41 | 42 | module Tree 43 | module Utils 44 | # Provides a utility for marshalling/unmarshalling TreeNode objects to Ruby 45 | # +hash+ objects. 46 | # 47 | # @author Jen Hamon (https://www.github.com/jhamon) 48 | module HashConverter 49 | def self.included(base) 50 | base.extend(ClassMethods) 51 | end 52 | 53 | # Methods in {Tree::Utils::HashConverter::ClassMethods} will be added as 54 | # class methods on any class mixing in the {Tree::Utils::HashConverter} 55 | # module. 56 | module ClassMethods 57 | # Factory method builds a {Tree::TreeNode} from a +Hash+. 58 | # 59 | # This method will interpret each key of your +Hash+ as a {Tree::TreeNode}. 60 | # Nested hashes are expected and child nodes will be added accordingly. If 61 | # a hash key is a single value that value will be used as the name for the 62 | # node. If a hash key is an Array, both node name and content will be 63 | # populated. 64 | # 65 | # A leaf element of the tree should be represented as a hash key with 66 | # corresponding value +nil+ or +{}+. 67 | # 68 | # @example 69 | # TreeNode.from_hash({:A => {:B => {}, :C => {:D => {}, :E => {}}}}) 70 | # # would be parsed into the following tree structure: 71 | # # A 72 | # # / \ 73 | # # B C 74 | # # / \ 75 | # # D E 76 | # 77 | # # The same tree would result from this nil-terminated Hash 78 | # {:A => {:B => nil, :C => {:D => nil, :E => nil}}} 79 | # 80 | # # A tree with equivalent structure but with content present for 81 | # # nodes A and D could be built from a hash like this: 82 | # {[:A, "A content"] => {:B => {}, 83 | # :C => { [:D, "D content"] => {}, 84 | # :E => {} }}} 85 | # 86 | # @author Jen Hamon (http://www.github.com/jhamon) 87 | # @param [Hash] hash Hash to build tree from. 88 | # 89 | # @return [Tree::TreeNode] The {Tree::TreeNode} instance representing the 90 | # root of your tree. 91 | # 92 | # @raise [ArgumentError] This exception is raised if a non-Hash is passed. 93 | # 94 | # @raise [ArgumentError] This exception is raised if the hash has multiple 95 | # top-level elements. 96 | # 97 | # @raise [ArgumentError] This exception is raised if the hash contains 98 | # values that are not hashes or nils. 99 | 100 | def from_hash(hash) 101 | raise ArgumentError, 'Argument must be a type of hash'\ 102 | unless hash.is_a?(Hash) 103 | 104 | raise ArgumentError, 'Hash must have one top-level element'\ 105 | if hash.size != 1 106 | 107 | root, children = hash.first 108 | 109 | raise ArgumentError, 'Invalid child. Must be nil or hash.'\ 110 | unless [Hash, NilClass].any? { |c| children.is_a? c } 111 | 112 | node = new(*root) 113 | node.add_from_hash(children) unless children.nil? 114 | node 115 | end 116 | end 117 | 118 | # Instantiate and insert child nodes from data in a Ruby +Hash+ 119 | # 120 | # This method is used in conjunction with from_hash to provide a 121 | # convenient way of building and inserting child nodes present in a Ruby 122 | # hashes. 123 | # 124 | # This method will instantiate a node instance for each top- 125 | # level key of the input hash, to be inserted as children of the receiver 126 | # instance. 127 | # 128 | # Nested hashes are expected and further child nodes will be created and 129 | # added accordingly. If a hash key is a single value that value will be 130 | # used as the name for the node. If a hash key is an Array, both node 131 | # name and content will be populated. 132 | # 133 | # A leaf element of the tree should be represented as a hash key with 134 | # corresponding value +nil+ or {}. 135 | # 136 | # @example 137 | # root = Tree::TreeNode.new(:A, "Root content!") 138 | # root.add_from_hash({:B => {:D => {}}, [:C, "C content!"] => {}}) 139 | # 140 | # @author Jen Hamon (http://www.github.com/jhamon) 141 | # @param [Hash] children The hash of child subtrees. 142 | # @raise [ArgumentError] This exception is raised if a non-hash is passed. 143 | # @return [Array] Array of child nodes added 144 | # @see ClassMethods#from_hash 145 | def add_from_hash(children) 146 | raise ArgumentError, 'Argument must be a type of hash'\ 147 | unless children.is_a?(Hash) 148 | 149 | child_nodes = [] 150 | children.each do |child, grandchildren| 151 | child_node = self.class.from_hash({ child => grandchildren }) 152 | child_nodes << child_node 153 | self << child_node 154 | end 155 | 156 | child_nodes 157 | end 158 | 159 | # Convert a node and its subtree into a Ruby hash. 160 | # 161 | # @example 162 | # root = Tree::TreeNode.new(:root, "root content") 163 | # root << Tree::TreeNode.new(:child1, "child1 content") 164 | # root << Tree::TreeNode.new(:child2, "child2 content") 165 | # root.to_h # => {[:root, "root content"] => 166 | # { [:child1, "child1 content"] => 167 | # {}, [:child2, "child2 content"] => {}}} 168 | # @author Jen Hamon (http://www.github.com/jhamon) 169 | # @return [Hash] Hash representation of tree. 170 | def to_h 171 | key = content? ? [name, content] : name 172 | 173 | children_hash = {} 174 | children do |child| 175 | children_hash.merge! child.to_h 176 | end 177 | 178 | { key => children_hash } 179 | end 180 | end 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /lib/tree/utils/json_converter.rb: -------------------------------------------------------------------------------- 1 | # json_converter.rb - This file is part of the RubyTree package. 2 | # 3 | # = json_converter.rb - Provides conversion to and from JSON. 4 | # 5 | # Author:: Anupam Sengupta (anupamsg@gmail.com) 6 | # 7 | # Time-stamp: <2023-12-27 12:46:07 anupam> 8 | # 9 | # Copyright (C) 2012, 2013, 2014, 2015, 2022, 2023 Anupam Sengupta 10 | # 11 | # All rights reserved. 12 | # 13 | # Redistribution and use in source and binary forms, with or without 14 | # modification, are permitted provided that the following conditions are met: 15 | # 16 | # - Redistributions of source code must retain the above copyright notice, this 17 | # list of conditions and the following disclaimer. 18 | # 19 | # - Redistributions in binary form must reproduce the above copyright notice, 20 | # this list of conditions and the following disclaimer in the documentation 21 | # and/or other materials provided with the distribution. 22 | # 23 | # - Neither the name of the organization nor the names of its contributors may 24 | # be used to endorse or promote products derived from this software without 25 | # specific prior written permission. 26 | # 27 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 28 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 29 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 30 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 31 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 32 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 33 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 34 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 35 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 36 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 37 | # 38 | # frozen_string_literal: true 39 | 40 | require 'json' 41 | 42 | module Tree 43 | module Utils 44 | # Provides utility methods to convert a {Tree::TreeNode} to and from 45 | # JSON[http://flori.github.com/json/]. 46 | module JSONConverter 47 | def self.included(base) 48 | base.extend(ClassMethods) 49 | end 50 | 51 | # @!group Converting to/from JSON 52 | 53 | # Creates a JSON ready Hash for the #to_json method. 54 | # 55 | # @author Eric Cline (https://github.com/escline) 56 | # @since 0.8.3 57 | # 58 | # @return A hash based representation of the JSON 59 | # 60 | # Rails uses JSON in ActiveSupport, and all Rails JSON encoding goes through 61 | # +as_json+. 62 | # 63 | # @param [Object] _options 64 | # 65 | # @see #to_json 66 | # @see http://stackoverflow.com/a/6880638/273808 67 | # noinspection RubyUnusedLocalVariable 68 | def as_json(_options = {}) 69 | json_hash = { 70 | name: name, 71 | content: content, 72 | JSON.create_id => self.class.name 73 | } 74 | 75 | json_hash['children'] = children if children? 76 | 77 | json_hash 78 | end 79 | 80 | # Creates a JSON representation of this node including all it's children. 81 | # This requires the JSON gem to be available, or else the operation fails with 82 | # a warning message. Uses the Hash output of #as_json method. 83 | # 84 | # @author Dirk Breuer (http://github.com/railsbros-dirk) 85 | # @since 0.7.0 86 | # 87 | # @return The JSON representation of this subtree. 88 | # 89 | # @see ClassMethods#json_create 90 | # @see #as_json 91 | # @see http://flori.github.com/json 92 | def to_json(*args) 93 | as_json.to_json(*args) 94 | end 95 | 96 | # ClassMethods for the {JSONConverter} module. Will become class methods in 97 | # the +include+ target. 98 | module ClassMethods 99 | # Helper method to create a Tree::TreeNode instance from the JSON hash 100 | # representation. Note that this method should *NOT* be called directly. 101 | # Instead, to convert the JSON hash back to a tree, do: 102 | # 103 | # tree = JSON.parse(the_json_hash, create_additions: true) 104 | # 105 | # This operation requires the {JSON gem}[http://flori.github.com/json/] to 106 | # be available, or else the operation fails with a warning message. 107 | # 108 | # Note the +create_additions: true+ option, which is *required* for 109 | # successfully parsing the string or hash. 110 | # 111 | # @author Dirk Breuer (http://github.com/railsbros-dirk) 112 | # @since 0.7.0 113 | # 114 | # @param [Hash] json_hash The JSON hash to convert from. 115 | # 116 | # @return [Tree::TreeNode] The created tree. 117 | # 118 | # @see #to_json 119 | # @see http://flori.github.com/json 120 | def json_create(json_hash) 121 | node = new(json_hash['name'], json_hash['content']) 122 | 123 | json_hash['children']&.each do |child| 124 | node << child 125 | end 126 | 127 | node 128 | end 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/tree/utils/metrics_methods.rb: -------------------------------------------------------------------------------- 1 | # metrics_methods.rb - This file is part of the RubyTree package. 2 | # 3 | # = metrics_methods.rb - Provides methods for various tree measurements 4 | # 5 | # Author:: Anupam Sengupta (anupamsg@gmail.com) 6 | # 7 | # Time-stamp: <2022-06-20 22:17:00 anupam> 8 | # 9 | # Copyright (C) 2013, 2015, 2017, 2021, 2022 Anupam Sengupta 10 | # 11 | # All rights reserved. 12 | # 13 | # Redistribution and use in source and binary forms, with or without 14 | # modification, are permitted provided that the following conditions are met: 15 | # 16 | # - Redistributions of source code must retain the above copyright notice, this 17 | # list of conditions and the following disclaimer. 18 | # 19 | # - Redistributions in binary form must reproduce the above copyright notice, 20 | # this list of conditions and the following disclaimer in the documentation 21 | # and/or other materials provided with the distribution. 22 | # 23 | # - Neither the name of the organization nor the names of its contributors may 24 | # be used to endorse or promote products derived from this software without 25 | # specific prior written permission. 26 | # 27 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 28 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 29 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 30 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 31 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 32 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 33 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 34 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 35 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 36 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 37 | # 38 | # frozen_string_literal: true 39 | 40 | module Tree 41 | module Utils 42 | # Provides utility functions to measure various tree metrics. 43 | module TreeMetricsHandler 44 | # @!group Metrics and Measures 45 | 46 | # @!attribute [r] size 47 | # Total number of nodes in this (sub)tree, including this node. 48 | # 49 | # Size of the tree is defined as: 50 | # 51 | # Size:: Total number nodes in the subtree including this node. 52 | # 53 | # @return [Integer] Total number of nodes in this (sub)tree. 54 | def size 55 | inject(0) { |sum, node| sum + 1 if node } 56 | end 57 | 58 | # @!attribute [r] length 59 | # Convenience synonym for {#size}. 60 | # 61 | # @deprecated This method name is ambiguous and may be removed. Use 62 | # {#size} instead. 63 | # 64 | # @return [Integer] The total number of nodes in this (sub)tree. 65 | # @see #size 66 | def length 67 | size 68 | end 69 | 70 | # @!attribute [r] node_height 71 | # Height of the (sub)tree from this node. Height of a node is defined as: 72 | # 73 | # Height:: Length of the longest downward path to a leaf from the node. 74 | # 75 | # - Height from a root node is height of the entire tree. 76 | # - The height of a leaf node is zero. 77 | # 78 | # @return [Integer] Height of the node. 79 | def node_height 80 | return 0 if leaf? 81 | 82 | 1 + @children.collect(&:node_height).max 83 | end 84 | 85 | # @!attribute [r] node_depth 86 | # Depth of this node in its tree. Depth of a node is defined as: 87 | # 88 | # Depth:: Length of the node's path to its root. Depth of a root node is 89 | # zero. 90 | # 91 | # {#level} is an alias for this method. 92 | # 93 | # @return [Integer] Depth of this node. 94 | def node_depth 95 | return 0 if root? 96 | 97 | 1 + parent.node_depth 98 | end 99 | 100 | # @!attribute [r] level 101 | # Alias for {#node_depth} 102 | # 103 | # @see #node_depth 104 | def level 105 | node_depth 106 | end 107 | 108 | # @!attribute [r] breadth 109 | # Breadth of the tree at this node's level. 110 | # A single node without siblings has a breadth of 1. 111 | # 112 | # Breadth is defined to be: 113 | # Breadth:: Number of sibling nodes to this node + 1 (this node itself), 114 | # i.e., the number of children the parent of this node has. 115 | # 116 | # @return [Integer] breadth of the node's level. 117 | def breadth 118 | root? ? 1 : parent.children.size 119 | end 120 | 121 | # @!attribute [r] in_degree 122 | # The incoming edge-count of this node. 123 | # 124 | # In-degree is defined as: 125 | # In-degree:: Number of edges arriving at the node (0 for root, 1 for 126 | # all other nodes) 127 | # 128 | # - In-degree = 0 for a root or orphaned node 129 | # - In-degree = 1 for a node which has a parent 130 | # 131 | # @return [Integer] The in-degree of this node. 132 | def in_degree 133 | root? ? 0 : 1 134 | end 135 | 136 | # @!attribute [r] out_degree 137 | # The outgoing edge-count of this node. 138 | # 139 | # Out-degree is defined as: 140 | # Out-degree:: Number of edges leaving the node (zero for leafs) 141 | # 142 | # @return [Integer] The out-degree of this node. 143 | def out_degree 144 | leaf? ? 0 : children.size 145 | end 146 | 147 | # @!endgroup 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /lib/tree/utils/path_methods.rb: -------------------------------------------------------------------------------- 1 | # path_methods.rb - This file is part of the RubyTree package. 2 | # 3 | # = path_methods.rb - Provides methods for extracting the node path. 4 | # 5 | # Author:: Marco Ziccardi and Anupam Sengupta (anupamsg@gmail.com) 6 | # 7 | # Time-stamp: <2022-06-20 02:00:11 anupam> 8 | # 9 | # Copyright (C) 2015, 2021, 2022 Anupam Sengupta 10 | # 11 | # All rights reserved. 12 | # 13 | # Redistribution and use in source and binary forms, with or without 14 | # modification, are permitted provided that the following conditions are met: 15 | # 16 | # - Redistributions of source code must retain the above copyright notice, this 17 | # list of conditions and the following disclaimer. 18 | # 19 | # - Redistributions in binary form must reproduce the above copyright notice, 20 | # this list of conditions and the following disclaimer in the documentation 21 | # and/or other materials provided with the distribution. 22 | # 23 | # - Neither the name of the organization nor the names of its contributors may 24 | # be used to endorse or promote products derived from this software without 25 | # specific prior written permission. 26 | # 27 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 28 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 29 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 30 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 31 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 32 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 33 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 34 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 35 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 36 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 37 | # 38 | # frozen_string_literal: true 39 | 40 | module Tree 41 | module Utils 42 | # Provides utility methods for path extraction 43 | module TreePathHandler 44 | # @!group Node Path 45 | 46 | # Returns the path of this node from the root as a string, with the node 47 | # names separated using the specified separator. The path is listed left 48 | # to right from the root node. 49 | # 50 | # @param separator The optional separator to use. The default separator is 51 | # '+=>+'. 52 | # 53 | # @return [String] The node path with names separated using the specified 54 | # separator. 55 | def path_as_string(separator = '=>') 56 | path_as_array.join(separator) 57 | end 58 | 59 | # Returns the node-names from this node to the root as an array. The first 60 | # element is the root node name, and the last element is this node's name. 61 | # 62 | # @return [Array] The array containing the node names for the path to this 63 | # node 64 | def path_as_array 65 | get_path_name_array.reverse 66 | end 67 | 68 | # @!visibility private 69 | # 70 | # Returns the path names in an array. The first element is the name of 71 | # this node, and the last element is the root node name. 72 | # 73 | # @return [Array] An array of the node names for the path from this node 74 | # to its root. 75 | def get_path_name_array(current_array_path = []) 76 | path_array = current_array_path + [name] 77 | 78 | if parent # Recurse to parent node. 79 | parent.get_path_name_array(path_array) 80 | else # else If detached node or root node. 81 | path_array 82 | end 83 | end 84 | 85 | protected :get_path_name_array 86 | 87 | # @!endgroup 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/tree/utils/tree_merge_handler.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # tree_merge_handler.rb 4 | # 5 | # Author: Anupam Sengupta 6 | # Time-stamp: <2022-06-20 22:17:12 anupam> 7 | # 8 | # Copyright (C) 2013, 2015, 2022 Anupam Sengupta (anupamsg@gmail.com) 9 | # 10 | # All rights reserved. 11 | # 12 | # Redistribution and use in source and binary forms, with or without 13 | # modification, are permitted provided that the following conditions are met: 14 | # 15 | # - Redistributions of source code must retain the above copyright notice, this 16 | # list of conditions and the following disclaimer. 17 | # 18 | # - Redistributions in binary form must reproduce the above copyright notice, 19 | # this list of conditions and the following disclaimer in the documentation 20 | # and/or other materials provided with the distribution. 21 | # 22 | # - Neither the name of the organization nor the names of its contributors may 23 | # be used to endorse or promote products derived from this software without 24 | # specific prior written permission. 25 | # 26 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 27 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 28 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 29 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 30 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 31 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 32 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 33 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 34 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 35 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | # 37 | # frozen_string_literal: true 38 | 39 | # Provides utility methods to merge two {Tree::TreeNode} based trees. 40 | # @since 0.9.0 41 | module Tree 42 | module Utils 43 | # Handles merging of two trees. 44 | module TreeMergeHandler 45 | # @!group Merging Trees 46 | 47 | # Merge two trees that share the same root node and returns a new 48 | # tree. 49 | # 50 | # The new tree contains the contents of the merge between _other_tree_ and 51 | # self. Duplicate nodes (coming from _other_tree_) will *NOT* be overwritten 52 | # in self. 53 | # 54 | # @author Darren Oakley (https://github.com/dazoakley) 55 | # 56 | # @param [Tree::TreeNode] other_tree The other tree to merge with. 57 | # @return [Tree::TreeNode] the resulting tree following the merge. 58 | # 59 | # @raise [TypeError] This exception is raised if _other_tree_ is not a 60 | # {Tree::TreeNode}. 61 | # 62 | # @raise [ArgumentError] This exception is raised if _other_tree_ does not 63 | # have the same root node as self. 64 | def merge(other_tree) 65 | check_merge_prerequisites(other_tree) 66 | merge_trees(root.dup, other_tree.root) 67 | end 68 | 69 | # Merge in another tree (that shares the same root node) into +this+ tree. 70 | # Duplicate nodes (coming from _other_tree_) will NOT be overwritten in 71 | # self. 72 | # 73 | # @author Darren Oakley (https://github.com/dazoakley) 74 | # 75 | # @param [Tree::TreeNode] other_tree The other tree to merge with. 76 | # 77 | # @raise [TypeError] This exception is raised if _other_tree_ is not a 78 | # {Tree::TreeNode}. 79 | # 80 | # @raise [ArgumentError] This exception is raised if _other_tree_ does not 81 | # have the same root node as self. 82 | def merge!(other_tree) 83 | check_merge_prerequisites(other_tree) 84 | merge_trees(root, other_tree.root) 85 | end 86 | 87 | private 88 | 89 | # Utility function to check that the conditions for a tree merge are met. 90 | # 91 | # @author Darren Oakley (https://github.com/dazoakley) 92 | # 93 | # @see #merge 94 | # @see #merge! 95 | def check_merge_prerequisites(other_tree) 96 | raise TypeError, 'You can only merge in another instance of Tree::TreeNode' \ 97 | unless other_tree.is_a?(Tree::TreeNode) 98 | 99 | raise ArgumentError, 'Unable to merge trees as they do not share the same root' \ 100 | unless root.name == other_tree.root.name 101 | end 102 | 103 | # Utility function to recursively merge two subtrees. 104 | # 105 | # @author Darren Oakley (https://github.com/dazoakley) 106 | # 107 | # @param [Tree::TreeNode] tree1 The target tree to merge into. 108 | # @param [Tree::TreeNode] tree2 The donor tree (that will be merged 109 | # into target). 110 | # @raise [Tree::TreeNode] The merged tree. 111 | def merge_trees(tree1, tree2) 112 | names1 = tree1.children? ? tree1.children.map(&:name) : [] 113 | names2 = tree2.children? ? tree2.children.map(&:name) : [] 114 | 115 | names_to_merge = names2 - names1 116 | names_to_merge.each do |name| 117 | tree1 << tree2[name].detached_subtree_copy 118 | end 119 | 120 | tree1.children.each do |child| 121 | merge_trees(child, tree2[child.name]) unless tree2[child.name].nil? 122 | end 123 | 124 | tree1 125 | end 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/tree/utils/utils.rb: -------------------------------------------------------------------------------- 1 | # utils.rb - This file is part of the RubyTree package. 2 | # 3 | # = utils.rb - Provides utility functions and mixins for RubyTree. 4 | # 5 | # Author:: Anupam Sengupta (anupamsg@gmail.com) 6 | # 7 | # Time-stamp: <2022-06-20 01:21:38 anupam> 8 | # 9 | # Copyright (C) 2012-2022 Anupam Sengupta. All rights reserved. 10 | # 11 | # Redistribution and use in source and binary forms, with or without 12 | # modification, are permitted provided that the following conditions are met: 13 | # 14 | # - Redistributions of source code must retain the above copyright notice, this 15 | # list of conditions and the following disclaimer. 16 | # 17 | # - Redistributions in binary form must reproduce the above copyright notice, 18 | # this list of conditions and the following disclaimer in the documentation 19 | # and/or other materials provided with the distribution. 20 | # 21 | # - Neither the name of the organization nor the names of its contributors may 22 | # be used to endorse or promote products derived from this software without 23 | # specific prior written permission. 24 | # 25 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 26 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 27 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 28 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 29 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 30 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 31 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 32 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 33 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 34 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | # 36 | # frozen_string_literal: true 37 | 38 | # Provides utilities and mixin modules for RubyTree. 39 | module Tree 40 | # Provides various utilities for the TreeNode class. 41 | module Utils 42 | # Empty module. Being used as a namespace. 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/tree/version.rb: -------------------------------------------------------------------------------- 1 | # version.rb - This file is part of the RubyTree package. 2 | # 3 | # This file provides the version number for Rubytree. 4 | # 5 | # Author:: Anupam Sengupta (anupamsg@gmail.com) 6 | # 7 | # Copyright (c) 2012-2024 Anupam Sengupta. All rights reserved. 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # - Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # - Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # - Neither the name of the organization nor the names of its contributors may 20 | # be used to endorse or promote products derived from this software without 21 | # specific prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 27 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | # 34 | # frozen_string_literal: true 35 | 36 | module Tree 37 | # Rubytree Package Version 38 | VERSION = '2.1.1' 39 | end 40 | -------------------------------------------------------------------------------- /rubytree.gemspec: -------------------------------------------------------------------------------- 1 | # 2 | # gemspec for the rubytree gem. 3 | # 4 | # Author:: Anupam Sengupta (anupamsg@gmail.com) 5 | # 6 | # Copyright (c) 2012-2024 Anupam Sengupta. All rights reserved. 7 | # 8 | # frozen_string_literal: true 9 | 10 | require File.join(__dir__, '/lib/tree/version') 11 | 12 | Gem::Specification.new do |s| 13 | s.name = 'rubytree' 14 | s.version = Tree::VERSION 15 | s.license = 'BSD-2-Clause' 16 | # NOTE: s.date should NOT be assigned. It is automatically set to pkg date. 17 | s.platform = Gem::Platform::RUBY 18 | s.author = 'Anupam Sengupta' 19 | s.email = 'anupamsg@gmail.com' 20 | s.homepage = 'http://rubytree.anupamsg.me' 21 | 22 | s.required_ruby_version = '>=2.7' 23 | 24 | s.summary = 'A generic tree data structure for Ruby.' 25 | 26 | s.description = <<-END_DESC 27 | 28 | RubyTree is a Ruby implementation of the generic tree data structure. It 29 | provides simple APIs to store named nodes, and to access, modify, and 30 | traverse the tree. 31 | 32 | The data model is node-centric, where nodes in the tree are the primary 33 | structural elements. It supports all common tree-traversal methods (pre-order, 34 | post-order, and breadth first). 35 | 36 | RubyTree mixes in the Enumerable and Comparable modules and behaves like a 37 | standard Ruby collection (iteration, comparison, etc.). 38 | 39 | RubyTree also includes a binary tree implementation, which provides in-order 40 | node traversal besides the other methods. 41 | 42 | RubyTree can import from and export to JSON, and supports Ruby’s object 43 | marshaling. 44 | END_DESC 45 | 46 | s.metadata = { 47 | 'rubygems_mfa_required' => 'true' 48 | } 49 | 50 | s.files = Dir['lib/**/*.rb'] # The actual code 51 | s.files += Dir['[A-Z]*'] # Various documentation files 52 | s.files += Dir['test/**/*.rb'] # Test cases 53 | s.files += Dir['spec/**/*.rb'] # Rspec Test cases 54 | s.files += Dir['examples/**/*.rb'] # Examples 55 | 56 | # @todo: Check if this is really needed. 57 | s.files += ['.gemtest'] # Support for gem-test 58 | 59 | s.require_paths = ['lib'] 60 | 61 | s.extra_rdoc_files = %w[README.md LICENSE.md API-CHANGES.md History.md] 62 | s.rdoc_options = ['--title', "Rubytree Documentation: #{s.name}-#{s.version}", 63 | '--main', 'README.md', 64 | '--quiet'] 65 | 66 | s.add_runtime_dependency 'json', '~> 2.0', '> 2.9' 67 | 68 | # NOTE: Rake is added as a development and test dependency in the Gemfile. 69 | s.add_development_dependency 'bundler', '~> 2.3' 70 | s.add_development_dependency 'rake', '~> 13.2' 71 | s.add_development_dependency 'rdoc', '~> 6.10' 72 | s.add_development_dependency 'rspec', '~> 3.0', '>= 3.13' 73 | s.add_development_dependency 'rtagstask', '~> 0.0.4' 74 | s.add_development_dependency 'rubocop', '~> 1.69' 75 | s.add_development_dependency 'rubocop-rake', '~> 0.6' 76 | s.add_development_dependency 'rubocop-rspec', '~> 3.3' 77 | s.add_development_dependency 'simplecov', '~> 0.22' 78 | s.add_development_dependency 'simplecov-lcov', '~> 0.8' 79 | s.add_development_dependency 'test-unit', '~> 3.6' 80 | s.add_development_dependency 'yard', '~> 0.0', '>= 0.9.37' 81 | 82 | s.post_install_message = <<-END_MESSAGE 83 | ======================================================================== 84 | Thank you for installing RubyTree. 85 | 86 | Note:: 87 | - 2.1.1 is a minor update that updates all dependencies and 88 | Updates the guard clause for creating a tree from a hash. 89 | 90 | - 2.1.0 is a minor update that brings all libraries to their 91 | latest stable versions. This version no longer supports 92 | Ruby 2.6 (minimum requirement is now >= 2.7). 93 | 94 | - 2.0.0 is a major release with BREAKING API changes. 95 | See `API-CHANGES.md` for details. 96 | 97 | - `Tree::TreeNode#depth` method has been removed (it was broken). 98 | 99 | - Support for `CamelCase` methods names has bee removed. 100 | 101 | - The predicate methods no longer have `is_` or `has_` prefixes. However, 102 | aliases with these prefixes exist to support existing client code. 103 | 104 | - Use of integers as node names does not require the optional 105 | `num_as_name` flag. 106 | 107 | - `structured_warnings` is no longer a dependency. 108 | 109 | - Explicit support for rbx Ruby has been removed. 110 | 111 | ======================================================================== 112 | END_MESSAGE 113 | end 114 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # spec_helper.rb 4 | # 5 | # Author: Anupam Sengupta 6 | # Time-stamp: <2022-06-22 13:54:00 anupam> 7 | # 8 | # Copyright (C) 2015, 2022 Anupam Sengupta 9 | # 10 | # frozen_string_literal: true 11 | 12 | require 'tree' 13 | 14 | if ENV['COVERAGE'] 15 | begin 16 | require 'simplecov' 17 | require 'simplecov-lcov' 18 | 19 | base_dir = File.expand_path(File.join(File.dirname(__FILE__), '..')) 20 | 21 | SimpleCov::Formatter::LcovFormatter.config do |cfg| 22 | cfg.report_with_single_file = true 23 | cfg.lcov_file_name = 'lcov.info' 24 | cfg.single_report_path = "#{base_dir}/coverage/lcov.info" 25 | end 26 | 27 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new( 28 | [ 29 | SimpleCov::Formatter::HTMLFormatter, 30 | SimpleCov::Formatter::LcovFormatter 31 | ] 32 | ) 33 | 34 | SimpleCov.start do 35 | add_filter '/test' 36 | add_filter '/spec' 37 | enable_coverage :branch 38 | end 39 | rescue LoadError => e 40 | puts "Could not load simplecov; continuing without code coverage #{e.cause}" 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/tree_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # tree_spec.rb 4 | # 5 | # Author: Anupam Sengupta 6 | # Time-stamp: <2022-06-22 13:52:50 anupam> 7 | # Copyright (C) 2015-2022 Anupam Sengupta 8 | # 9 | # frozen_string_literal: true 10 | 11 | require 'rspec' 12 | require 'spec_helper' 13 | 14 | class SpecializedTreeNode < Tree::TreeNode; end 15 | 16 | describe Tree do 17 | shared_examples_for 'any detached node' do 18 | it 'does not equal "Object.new"' do 19 | expect(tree).not_to eq(Object.new) 20 | end 21 | 22 | it 'does not equal 1 or any other fixnum' do 23 | expect(tree).not_to eq(1) 24 | end 25 | 26 | it 'identifies itself as a root node' do 27 | expect(tree.root?).to be(true) 28 | end 29 | 30 | it 'does not have a parent node' do 31 | expect(tree.parent).to be_nil 32 | end 33 | end 34 | 35 | describe '#initialize with empty name and nil content' do 36 | let(:tree) { Tree::TreeNode.new('') } 37 | 38 | it 'creates the tree node with name as ""' do 39 | expect(tree.name).to eq('') 40 | end 41 | 42 | it "has 'nil' content" do 43 | expect(tree.content).to be_nil 44 | end 45 | 46 | it_behaves_like 'any detached node' 47 | end 48 | 49 | describe "#initialize with name 'A' and nil content" do 50 | let(:tree) { Tree::TreeNode.new('A') } 51 | 52 | it 'creates the tree node with name as "A"' do 53 | expect(tree.name).to eq('A') 54 | end 55 | 56 | it "has 'nil' content" do 57 | expect(tree.content).to be_nil 58 | end 59 | 60 | it_behaves_like 'any detached node' 61 | end 62 | 63 | describe "#initialize with node name 'A' and some content" do 64 | sample = 'some content' 65 | let(:tree) { Tree::TreeNode.new('A', sample) } 66 | 67 | it 'creates the tree node with name as "A"' do 68 | expect(tree.name).to eq('A') 69 | end 70 | 71 | it "has some content #{sample}" do 72 | expect(tree.content).to eq(sample) 73 | end 74 | 75 | it_behaves_like 'any detached node' 76 | end 77 | 78 | describe 'comparison' do 79 | let(:node1) { Tree::TreeNode.new('a') } 80 | let(:node2) { SpecializedTreeNode.new('b') } 81 | 82 | it 'allows comparison of specialized tree nodes' do 83 | expect(node1 <=> node2).to be_eql(-1) 84 | end 85 | 86 | it 'does not allow comparison with nil' do 87 | expect(node1 <=> nil).to be_nil 88 | end 89 | 90 | it 'does not allow comparison with other objects' do 91 | expect(node1 <=> 'c').to be_nil 92 | end 93 | end 94 | 95 | describe 'serialization' do 96 | let(:serialized_node1) { Marshal.dump(SpecializedTreeNode.new('a')) } 97 | let(:serialized_node2) { Marshal.dump(Tree::TreeNode.new('b')) } 98 | let(:tree) do 99 | SpecializedTreeNode.new('root').tap do |root| 100 | root << SpecializedTreeNode.new('a') 101 | root << Tree::TreeNode.new('b') 102 | end 103 | end 104 | let(:serialized_tree) { Marshal.dump(tree) } 105 | 106 | it 'parses the serialized specialized tree node correctly (root)' do 107 | expect(Marshal.load(serialized_tree)).to be_a(SpecializedTreeNode) 108 | end 109 | 110 | it 'parses the serialized specialized tree node correctly (child)' do 111 | expect(Marshal.load(serialized_tree).children.first).to \ 112 | be_a(SpecializedTreeNode) 113 | end 114 | 115 | it 'parses the serialized tree node correctly' do 116 | expect(Marshal.load(serialized_node2)).to be_a(Tree::TreeNode) 117 | end 118 | end 119 | 120 | shared_examples_for 'any cloned node' do 121 | it 'is equal to the original' do 122 | expect(clone).to eq tree 123 | end 124 | 125 | it 'is not identical to the original' do 126 | expect(clone).not_to be tree 127 | end 128 | end 129 | 130 | describe '#detached_copy', 'Without content' do 131 | let(:tree) { Tree::TreeNode.new('A', nil) } 132 | let(:clone) { tree.detached_copy } 133 | 134 | it_behaves_like 'any cloned node' 135 | end 136 | 137 | describe '#detached_copy with clonable content' do 138 | let(:tree) { Tree::TreeNode.new('A', 'clonable content') } 139 | let(:clone) { tree.detached_copy } 140 | 141 | it 'makes a clone of the content' do 142 | expect(clone.content).to eq tree.content 143 | end 144 | 145 | it 'is not the same as the original content' do 146 | expect(clone.content).not_to be tree.content 147 | end 148 | 149 | it_behaves_like 'any cloned node' 150 | end 151 | 152 | describe '#detached_copy with unclonable content' do 153 | let(:tree) { Tree::TreeNode.new('A', :unclonable_content) } 154 | let(:clone) { tree.detached_copy } 155 | 156 | it 'retains the original content' do 157 | expect(clone.content).to be tree.content 158 | end 159 | 160 | it_behaves_like 'any cloned node' 161 | end 162 | 163 | describe '#detached_copy with false as content' do 164 | let(:tree) { Tree::TreeNode.new('A', false) } 165 | let(:clone) { tree.detached_copy } 166 | 167 | it 'retains the original content' do 168 | expect(clone.content).to be tree.content 169 | end 170 | 171 | it_behaves_like 'any cloned node' 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /test/run_test.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # run_test.rb:: Run all the tests from the Ruby command line. 4 | # 5 | # Author: Anupam Sengupta 6 | # Time-stamp: <2022-06-22 13:54:25 anupam> 7 | # Copyright (C) 2014, 2022 Anupam Sengupta 8 | # 9 | # Redistribution and use in source and binary forms, with or without modification, 10 | # are permitted provided that the following conditions are met: 11 | # 12 | # - Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # - Redistributions in binary form must reproduce the above copyright notice, this 16 | # list of conditions and the following disclaimer in the documentation and/or 17 | # other materials provided with the distribution. 18 | # 19 | # - Neither the name of the organization nor the names of its contributors may 20 | # be used to endorse or promote products derived from this software without 21 | # specific prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 27 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 28 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 30 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 32 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | # 34 | # frozen_string_literal: true 35 | 36 | base_dir = File.expand_path(File.join(File.dirname(__FILE__), '..')) 37 | lib_dir = File.join(base_dir, 'lib') 38 | test_dir = File.join(base_dir, 'test') 39 | 40 | $LOAD_PATH.unshift(lib_dir) 41 | 42 | if ENV['COVERAGE'] 43 | begin 44 | require 'simplecov' 45 | require 'simplecov-lcov' 46 | 47 | SimpleCov::Formatter::LcovFormatter.config do |cfg| 48 | cfg.report_with_single_file = true 49 | cfg.lcov_file_name = 'lcov.info' 50 | cfg.single_report_path = "#{base_dir}/coverage/lcov.info" 51 | end 52 | 53 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new( 54 | [ 55 | SimpleCov::Formatter::HTMLFormatter, 56 | SimpleCov::Formatter::LcovFormatter 57 | ] 58 | ) 59 | 60 | SimpleCov.start do 61 | add_filter '/test' 62 | add_filter '/spec' 63 | enable_coverage :branch 64 | end 65 | rescue LoadError => e 66 | puts "Could not load simplecov; continuing without code coverage #{e.cause}" 67 | end 68 | end 69 | 70 | require 'test/unit' 71 | 72 | Test::Unit::AutoRunner.run(true, test_dir) 73 | -------------------------------------------------------------------------------- /test/test_binarytree.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # test_binarytree.rb - This file is part of the RubyTree package. 4 | # 5 | # 6 | # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2012, 2013, 2015, 2017, 2022 Anupam Sengupta 7 | # 8 | # All rights reserved. 9 | # 10 | # Redistribution and use in source and binary forms, with or without modification, 11 | # are permitted provided that the following conditions are met: 12 | # 13 | # - Redistributions of source code must retain the above copyright notice, this 14 | # list of conditions and the following disclaimer. 15 | # 16 | # - Redistributions in binary form must reproduce the above copyright notice, this 17 | # list of conditions and the following disclaimer in the documentation and/or 18 | # other materials provided with the distribution. 19 | # 20 | # - Neither the name of the organization nor the names of its contributors may 21 | # be used to endorse or promote products derived from this software without 22 | # specific prior written permission. 23 | # 24 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 25 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 28 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 30 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 31 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 33 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | # 35 | # frozen_string_literal: true 36 | 37 | require 'test/unit' 38 | require_relative '../lib/tree/binarytree' 39 | 40 | module TestTree 41 | # Test class for the binary tree node. 42 | class TestBinaryTreeNode < Test::Unit::TestCase 43 | # Setup the test data scaffolding. 44 | def setup 45 | @root = Tree::BinaryTreeNode.new('ROOT', 'Root Node') 46 | 47 | @left_child1 = Tree::BinaryTreeNode.new('A Child at Left', 'Child Node @ left') 48 | @right_child1 = Tree::BinaryTreeNode.new('B Child at Right', 'Child Node @ right') 49 | end 50 | 51 | # Tear down the test data scaffolding. 52 | def teardown 53 | @root.remove!(@left_child1) 54 | @root.remove!(@right_child1) 55 | @root = nil 56 | end 57 | 58 | # Test initialization of the binary tree. 59 | def test_initialize 60 | assert_not_nil(@root, "Binary tree's Root should have been created") 61 | assert_nil(@root.left_child, 'The initial left child of root should be nil') 62 | assert_nil(@root.right_child, 'The initial right child of root should be nil') 63 | assert_equal(@root.children.size, 0, 'Initially no children should be present') 64 | end 65 | 66 | def test_from_hash 67 | # Can't make a root node without a name 68 | assert_raise(ArgumentError) { Tree::BinaryTreeNode.from_hash({}) } 69 | # Can't have multiple roots 70 | assert_raise(ArgumentError) { Tree::BinaryTreeNode.from_hash({ A: {}, B: {} }) } 71 | 72 | # Can't have more than 2 children 73 | too_many_kids = { A: { B: {}, C: {}, D: {} } } 74 | assert_raise(ArgumentError) { Tree::BinaryTreeNode.from_hash(too_many_kids) } 75 | 76 | valid_hash = { A: { B: {}, C: { D: {} } } } 77 | tree = Tree::BinaryTreeNode.from_hash(valid_hash) 78 | # A 79 | # / \ 80 | # B C 81 | # | 82 | # D 83 | 84 | assert_same(tree.class, Tree::BinaryTreeNode) 85 | assert_same(:A, tree.name) 86 | assert_equal(true, tree.root?) 87 | assert_equal(false, tree.leaf?) 88 | assert_equal(2, tree.children.count) # B, C, D 89 | assert_equal(4, tree.size) 90 | 91 | valid_hash_with_content = { [:A, 'Content!'] => { B: {}, C: { [:D, 'More content'] => {} } } } 92 | tree2 = Tree::BinaryTreeNode.from_hash(valid_hash_with_content) 93 | 94 | assert_equal(Tree::BinaryTreeNode, tree2.class) 95 | assert_equal('Content!', tree2.content) 96 | assert_equal('More content', tree2[:C][:D].content) 97 | end 98 | 99 | def test_add_from_hash 100 | root = Tree::BinaryTreeNode.new('Root') 101 | 102 | # Can't have too many children 103 | too_many_kids = { child1: {}, child2: {}, child3: {} } 104 | assert_raise(ArgumentError) { root.add_from_hash(too_many_kids) } 105 | assert_equal(0, root.children.count) # Nothing added 106 | 107 | # Well behaved when adding nothing 108 | assert_equal([], root.add_from_hash({})) 109 | assert_equal(1, root.size) 110 | 111 | valid_hash = { A: {}, B: { C: {}, [:D, 'leaf'] => {} } } 112 | added = root.add_from_hash(valid_hash) 113 | # root 114 | # / \ 115 | # A B 116 | # / \ 117 | # C D 118 | 119 | assert_equal(Array, added.class) 120 | assert_equal(2, added.count) 121 | assert_equal(5, root.size) 122 | assert_equal(root.children.count, 2) 123 | assert_equal('leaf', root[:B][:D].content) 124 | 125 | # Can't add more than two children 126 | assert_raise(ArgumentError) { root.add_from_hash({ X: {} }) } 127 | node = Tree::BinaryTreeNode.new('Root 2') 128 | assert_raise(ArgumentError) { node.add_from_hash({ A: {}, B: {}, C: {} }) } 129 | end 130 | 131 | # Test the add method. 132 | def test_add 133 | @root.add @left_child1 134 | assert(!@left_child1.root?, 'Left child1 cannot be a root after addition to the ROOT node') 135 | 136 | assert_same(@left_child1, @root.left_child, 'The left node should be left_child1') 137 | assert_same(@left_child1, @root.first_child, 'The first node should be left_child1') 138 | 139 | @root.add @right_child1 140 | assert(!@right_child1.root?, 'Right child1 cannot be a root after addition to the ROOT node') 141 | 142 | assert_same(@right_child1, @root.right_child, 'The right node should be right_child1') 143 | assert_same(@right_child1, @root.last_child, 'The first node should be right_child1') 144 | 145 | assert_raise ArgumentError do 146 | @root.add Tree::BinaryTreeNode.new('The third child!') 147 | end 148 | 149 | assert_raise ArgumentError do 150 | @root << Tree::BinaryTreeNode.new('The third child!') 151 | end 152 | end 153 | 154 | # Test the inordered_each method. 155 | def test_inordered_each 156 | a = Tree::BinaryTreeNode.new('a') 157 | b = Tree::BinaryTreeNode.new('b') 158 | c = Tree::BinaryTreeNode.new('c') 159 | d = Tree::BinaryTreeNode.new('d') 160 | e = Tree::BinaryTreeNode.new('e') 161 | f = Tree::BinaryTreeNode.new('f') 162 | g = Tree::BinaryTreeNode.new('g') 163 | h = Tree::BinaryTreeNode.new('h') 164 | i = Tree::BinaryTreeNode.new('i') 165 | 166 | # Create the following Tree 167 | # f <-- level 0 (Root) 168 | # / \ 169 | # b g <-- level 1 170 | # / \ \ 171 | # a d i <-- level 2 172 | # / \ / 173 | # c e h <-- level 3 174 | f << b << a 175 | f << g 176 | b << d << c 177 | d << e 178 | g.right_child = i # This needs to be explicit 179 | i << h 180 | 181 | # The expected order of response 182 | expected_array = [a, b, c, d, e, f, g, h, i] 183 | 184 | result_array = [] 185 | result = f.inordered_each { |node| result_array << node.detached_copy } 186 | 187 | assert_equal(f, result) # each should return the original object 188 | 189 | expected_array.each_index do |idx| 190 | # Match only the names. 191 | assert_equal(expected_array[idx].name, result_array[idx].name) 192 | end 193 | 194 | assert_equal(Enumerator, f.inordered_each.class) if defined?(Enumerator.class) # Without a block 195 | 196 | assert_equal(Enumerable::Enumerator, f.inordered_each.class) if defined?(Enumerable::Enumerator.class) 197 | end 198 | 199 | # Test the left_child method. 200 | def test_left_child 201 | @root << @left_child1 202 | @root << @right_child1 203 | assert_same(@left_child1, @root.left_child, "The left child should be 'left_child1") 204 | assert_not_same(@right_child1, @root.left_child, 'The right_child1 is not the left child') 205 | end 206 | 207 | # Test the right_child method. 208 | def test_right_child 209 | @root << @left_child1 210 | @root << @right_child1 211 | assert_same(@right_child1, @root.right_child, "The right child should be 'right_child1") 212 | assert_not_same(@left_child1, @root.right_child, 'The left_child1 is not the left child') 213 | end 214 | 215 | # Test left_child= method. 216 | def test_left_child_equals 217 | @root << @left_child1 218 | @root << @right_child1 219 | assert_same(@left_child1, @root.left_child, "The left child should be 'left_child1") 220 | assert(!@left_child1.root?, 'The left child now cannot be a root.') 221 | 222 | @root.left_child = Tree::BinaryTreeNode.new('New Left Child') 223 | assert(!@root.left_child.root?, 'The left child now cannot be a root.') 224 | assert_equal('New Left Child', @root.left_child.name, 'The left child should now be the new child') 225 | assert_equal('B Child at Right', @root.last_child.name, 'The last child should now be the right child') 226 | 227 | # Now set the left child as nil, and retest 228 | @root.left_child = nil 229 | assert_nil(@root.left_child, 'The left child should now be nil') 230 | assert_nil(@root.first_child, 'The first child is now nil') 231 | assert_equal('B Child at Right', @root.last_child.name, 'The last child should now be the right child') 232 | end 233 | 234 | # Test right_child= method. 235 | def test_right_child_equals 236 | @root << @left_child1 237 | @root << @right_child1 238 | assert_same(@right_child1, @root.right_child, "The right child should be 'right_child1") 239 | assert(!@right_child1.root?, 'The right child now cannot be a root.') 240 | 241 | @root.right_child = Tree::BinaryTreeNode.new('New Right Child') 242 | assert(!@root.right_child.root?, 'The right child now cannot be a root.') 243 | assert_equal('New Right Child', @root.right_child.name, 'The right child should now be the new child') 244 | assert_equal('A Child at Left', @root.first_child.name, 'The first child should now be the left child') 245 | assert_equal('New Right Child', @root.last_child.name, 'The last child should now be the right child') 246 | 247 | # Now set the right child as nil, and retest 248 | @root.right_child = nil 249 | assert_nil(@root.right_child, 'The right child should now be nil') 250 | assert_equal('A Child at Left', @root.first_child.name, 'The first child should now be the left child') 251 | assert_nil(@root.last_child, 'The first child is now nil') 252 | end 253 | 254 | # Test isLeft_child? method. 255 | def test_is_left_child_eh 256 | @root << @left_child1 257 | @root << @right_child1 258 | 259 | assert(@left_child1.left_child?, 'left_child1 should be the left child') 260 | assert(!@right_child1.left_child?, 'left_child1 should be the left child') 261 | 262 | # Now set the right child as nil, and retest 263 | @root.right_child = nil 264 | assert(@left_child1.left_child?, 'left_child1 should be the left child') 265 | 266 | assert(!@root.left_child?, 'Root is neither left child nor right') 267 | end 268 | 269 | # Test right_child? method. 270 | def test_is_right_child_eh 271 | @root << @left_child1 272 | @root << @right_child1 273 | 274 | assert(@right_child1.right_child?, 'right_child1 should be the right child') 275 | assert(!@left_child1.right_child?, 'right_child1 should be the right child') 276 | 277 | # Now set the left child as nil, and retest 278 | @root.left_child = nil 279 | assert(@right_child1.right_child?, 'right_child1 should be the right child') 280 | assert(!@root.right_child?, 'Root is neither left child nor right') 281 | end 282 | 283 | # Test swap_children method. 284 | def test_swap_children 285 | @root << @left_child1 286 | @root << @right_child1 287 | 288 | assert(@right_child1.right_child?, 'right_child1 should be the right child') 289 | assert(!@left_child1.right_child?, 'right_child1 should be the right child') 290 | 291 | @root.swap_children 292 | 293 | assert(@right_child1.left_child?, 'right_child1 should now be the left child') 294 | assert(@left_child1.right_child?, 'left_child1 should now be the right child') 295 | assert_equal(@right_child1, @root.first_child, 'right_child1 should now be the first child') 296 | assert_equal(@left_child1, @root.last_child, 'left_child1 should now be the last child') 297 | assert_equal(@right_child1, @root[0], 'right_child1 should now be the first child') 298 | assert_equal(@left_child1, @root[1], 'left_child1 should now be the last child') 299 | end 300 | end 301 | end 302 | -------------------------------------------------------------------------------- /test/test_rubytree_require.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # test_rubytree_require.rb - This file is part of the RubyTree package. 4 | # 5 | # Copyright (c) 2012, 2022 Anupam Sengupta 6 | # 7 | # All rights reserved. 8 | # 9 | # Redistribution and use in source and binary forms, with or without modification, 10 | # are permitted provided that the following conditions are met: 11 | # 12 | # - Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # - Redistributions in binary form must reproduce the above copyright notice, this 16 | # list of conditions and the following disclaimer in the documentation and/or 17 | # other materials provided with the distribution. 18 | # 19 | # - Neither the name of the organization nor the names of its contributors may 20 | # be used to endorse or promote products derived from this software without 21 | # specific prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 27 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 28 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 30 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 32 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | # 34 | # frozen_string_literal: true 35 | 36 | require 'test/unit' 37 | require_relative '../lib/rubytree' 38 | 39 | module TestTree 40 | # Test class for checking whether require 'rubytree' works. 41 | class TestRequireRubytree < Test::Unit::TestCase 42 | # A simple test. We are just checking whether the require worked. 43 | def test_create_a_simple_node 44 | assert_not_nil(Tree::TreeNode.new('Root', 'A Root node')) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/test_subclassed_node.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # test_subclassed_node.rb - This file is part of the RubyTree package. 4 | # 5 | # Copyright (c) 2012, 2017, 2022 Anupam Sengupta 6 | # 7 | # All rights reserved. 8 | # 9 | # Redistribution and use in source and binary forms, with or without modification, 10 | # are permitted provided that the following conditions are met: 11 | # 12 | # - Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # - Redistributions in binary form must reproduce the above copyright notice, this 16 | # list of conditions and the following disclaimer in the documentation and/or 17 | # other materials provided with the distribution. 18 | # 19 | # - Neither the name of the organization nor the names of its contributors may 20 | # be used to endorse or promote products derived from this software without 21 | # specific prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 27 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 28 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 30 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 32 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | # 34 | # frozen_string_literal: true 35 | 36 | require 'test/unit' 37 | require 'json' 38 | require_relative '../lib/tree' 39 | 40 | module TestTree 41 | # Test class for the Tree node. 42 | class TestSubclassedTreeNode < Test::Unit::TestCase 43 | # A subclassed node to test various inheritance related features. 44 | class MyNode < Tree::TreeNode 45 | end 46 | 47 | def test_detached_copy_same_clz 48 | root = MyNode.new('Root') 49 | assert_equal(MyNode, root.detached_copy.class) 50 | end 51 | end 52 | end 53 | 54 | __END__ 55 | -------------------------------------------------------------------------------- /test/test_thread_and_fiber.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # test_thread_and_fiber.rb - This file is part of the RubyTree package. 4 | # 5 | # Copyright (c) 2012, 2013, 2022 Anupam Sengupta 6 | # 7 | # All rights reserved. 8 | # 9 | # Redistribution and use in source and binary forms, with or without modification, 10 | # are permitted provided that the following conditions are met: 11 | # 12 | # - Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # - Redistributions in binary form must reproduce the above copyright notice, this 16 | # list of conditions and the following disclaimer in the documentation and/or 17 | # other materials provided with the distribution. 18 | # 19 | # - Neither the name of the organization nor the names of its contributors may 20 | # be used to endorse or promote products derived from this software without 21 | # specific prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 27 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 28 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 30 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 32 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | # 34 | # frozen_string_literal: true 35 | 36 | require 'test/unit' 37 | require 'json' 38 | require_relative '../lib/tree' 39 | 40 | module TestTree 41 | # Test class for the Tree node. 42 | class TestFiberAndThreadOnNode < Test::Unit::TestCase 43 | # Test long and unbalanced trees 44 | def create_long_depth_trees(depth = 100) 45 | tree = Tree::TreeNode.new('/') 46 | current = tree 47 | depth.times do |i| 48 | new_node = Tree::TreeNode.new(i.to_s) 49 | current << new_node 50 | current = new_node 51 | end 52 | 53 | tree 54 | end 55 | 56 | # Test the recursive methods with a fiber. The stack usage is causing 57 | # failure for very large depths on unbalanced nodes. 58 | def test_fiber_for_recursion 59 | return unless defined?(Fiber.class) # Fibers exist only from Ruby 1.9 onwards. 60 | 61 | assert_nothing_thrown do 62 | Fiber.new do 63 | depth = 1000 # Use a reasonably large depth, which would trip a recursive stack 64 | root = create_long_depth_trees(depth) 65 | assert_equal(depth + 1, root.size) 66 | end.resume 67 | end 68 | end 69 | 70 | # Test the recursive methods with a thread. The stack usage is causing 71 | # failure for very large depths on unbalanced nodes. 72 | def test_thread_for_recursion 73 | assert_nothing_thrown do 74 | depth = 1000 # Use a reasonably large depth, which would trip a recursive stack 75 | Thread.abort_on_exception = true 76 | Thread.new do 77 | root = create_long_depth_trees(depth) 78 | assert_equal(depth + 1, root.size) 79 | end 80 | end 81 | end 82 | end 83 | end 84 | 85 | __END__ 86 | --------------------------------------------------------------------------------