rubytree 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8718114c71cea254b3c2a5a05c9d543e303fdc9ebebdc5a2a82f442389f71c4a
4
- data.tar.gz: 3056e228bd10659486e4d3d41d5704bb08f4172c764adfaf7fb5e87db573973a
3
+ metadata.gz: 4453bf8edf542985ddd52c289e7194263a5251cedd9ffd0655248a8c13585dd0
4
+ data.tar.gz: 67a85efeb8483aecdd9a576708a92eee8e333103b34b3ed04fcf748b73f1a07f
5
5
  SHA512:
6
- metadata.gz: 76b2a2e9aea0042ad632081d32279c5da9eee6734b6bb0f2963689d0ef9ccde74ae6b4ac0ade8c3347a310988e86ff36456a02cf28d2babd2282123f3ea21cbe
7
- data.tar.gz: 93eafb68eb8d943a7b0a7c7b34ef105aea8999696aeb77975017739f10576689c4905a871c46ed9f95110958e8a757ff62cdfc3d353ef56498f3749ccac44bba
6
+ metadata.gz: 66220dddff61b9bb9198c72530ae6a6e66d0d66c713236e1aca5b44f77818798acaae9e4f2d6356d95cf0872f219f3c74d7b3e21e2b570a4e6d285499a6dc582
7
+ data.tar.gz: e15ecf207beb379c10a298dc1202c8bf9ec5ad3b4e6538793b7b139a64af8fa8b2883b8155dbd222e6b2d304fc15bf6b32d0a12088ee27fcf9c23ac75d67186e
data/API-CHANGES.md CHANGED
@@ -7,6 +7,28 @@ _Note_: API changes are expected to reduce significantly after the `1.x`
7
7
  release. In most cases, an alternative will be provided to ensure relatively
8
8
  smooth transition to the new APIs.
9
9
 
10
+ ## Release 2.2.0 Changes
11
+
12
+ * [Tree::TreeNode#add][add] now raises `ArgumentError` when attempting to add
13
+ an ancestor node as a child, preventing cycles.
14
+
15
+ * [Tree::TreeNode#remove_all!][remove_all] now detaches children by clearing
16
+ their parent links.
17
+
18
+ * [Tree::TreeNode#rename_child][rename_child] now raises `ArgumentError` if the
19
+ new name collides with an existing sibling.
20
+
21
+ * [Tree::BinaryTreeNode#set_child_at][set_child_at] now raises `ArgumentError`
22
+ for invalid indices and cleans up parent/hash references when replacing or
23
+ clearing a child.
24
+
25
+ * [Tree::TreeNode#postordered_each][postordered_each] and
26
+ [Tree::TreeNode#breadth_each][breadth_each] now skip `nil` children to
27
+ support binary trees with missing children.
28
+
29
+ * [Tree::TreeNode#each_level][each_level] now returns a level-wise enumerator
30
+ when called without a block.
31
+
10
32
  ## Release 2.1.0 Changes
11
33
 
12
34
  * Minimum Ruby version has been bumped to 2.7 and above
@@ -143,6 +165,7 @@ smooth transition to the new APIs.
143
165
  [detached_subtree_copy]: rdoc-ref:Tree::TreeNode#detached_subtree_copy
144
166
  [dup]: rdoc-ref:Tree::TreeNode#dup
145
167
  [each]: rdoc-ref:Tree::TreeNode#each
168
+ [each_level]: rdoc-ref:Tree::TreeNode#each_level
146
169
  [in_degree]: rdoc-ref:Tree::Utils::TreeMetricsHandler#in_degree
147
170
  [initialize]: rdoc-ref:Tree::TreeNode#initialize
148
171
  [inordered_each]: rdoc-ref:Tree::BinaryTreeNode#inordered_each
@@ -155,5 +178,8 @@ smooth transition to the new APIs.
155
178
  [postordered_each]: rdoc-ref:Tree::TreeNode#postordered_each
156
179
  [preordered_each]: rdoc-ref:Tree::TreeNode#preordered_each
157
180
  [previous_sibling]: rdoc-ref:Tree::TreeNode#previous_sibling
181
+ [remove_all]: rdoc-ref:Tree::TreeNode#remove_all!
182
+ [rename_child]: rdoc-ref:Tree::TreeNode#rename_child
183
+ [set_child_at]: rdoc-ref:Tree::BinaryTreeNode#set_child_at
158
184
  [siblings]: rdoc-ref:Tree::TreeNode#siblings
159
185
  [to_json]: rdoc-ref:Tree::Utils::JSONConverter#to_json
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rubytree (2.1.1)
4
+ rubytree (2.2.0)
5
5
  json (~> 2.0, > 2.9)
6
6
 
7
7
  GEM
data/History.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # History of Changes
2
2
 
3
+ ### 2.2.0 / 2026-02-06
4
+
5
+ * Prevent cycles by rejecting attempts to add an ancestor as a child.
6
+
7
+ * Ensure `remove_all!` detaches children by clearing their parent links.
8
+
9
+ * Raise on sibling name collisions in `rename_child`.
10
+
11
+ * Harden binary tree child assignment (`set_child_at`) with proper index errors
12
+ and cleanup of parent/hash references.
13
+
14
+ * Make traversals resilient to missing children by skipping `nil` nodes in
15
+ `postordered_each` and `breadth_each`.
16
+
17
+ * Return a level-wise enumerator from `each_level` when no block is given.
18
+
19
+ * Improve `to_s` formatting to show `<Empty>` for nil content.
20
+
3
21
  ### 2.1.1 / 2024-12-19
4
22
 
5
23
  * 2.1.1 is a minor update that updates all dependencies and updates the guard
data/Rakefile CHANGED
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # Rakefile - This file is part of the RubyTree package.
4
4
  #
5
- # Copyright (c) 2006-2022 Anupam Sengupta
5
+ # Copyright (c) 2006-2024 Anupam Sengupta
6
6
  #
7
7
  # All rights reserved.
8
8
  #
@@ -160,9 +160,16 @@ namespace :gem do
160
160
  pkg.need_tar = true
161
161
  end
162
162
 
163
- desc 'Push the gem into the Rubygems repository'
163
+ desc 'Push the gem into the Rubygems and Github repositories'
164
164
  task push: :gem do
165
+ github_repo = 'https://rubygems.pkg.github.com/evolve75'
166
+
167
+ # This pushes to the standard RubyGems registry
165
168
  sh "gem push pkg/#{GEM_NAME}"
169
+
170
+ # For github, the credentials key is assumed to be github
171
+ # See: https://docs.github.com/en/packages/working-with-a-github-packages-registry/
172
+ sh "gem push --key github --host #{github_repo} pkg/#{GEM_NAME}"
166
173
  end
167
174
  end
168
175
 
@@ -203,12 +203,29 @@ module Tree
203
203
  #
204
204
  # @raise [ArgumentError] If the index is out of limits.
205
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
206
+ raise ArgumentError, 'A binary tree cannot have more than two children.'\
207
+ unless (0..1).include? at_index
208
208
 
209
- @children[at_index] = child
210
- @children_hash[child.name] = child if child # Assign the name mapping
211
- child.parent = self if child
209
+ old_child = @children[at_index]
210
+ if old_child && old_child != child
211
+ still_present = @children.each_with_index.any? do |existing, idx|
212
+ idx != at_index && existing.equal?(old_child)
213
+ end
214
+
215
+ unless still_present
216
+ @children_hash.delete(old_child.name)
217
+ old_child.set_as_root!
218
+ end
219
+ end
220
+
221
+ if child
222
+ child.parent&.remove!(child) unless child.parent == self
223
+ @children[at_index] = child
224
+ @children_hash[child.name] = child # Assign the name mapping
225
+ child.parent = self
226
+ else
227
+ @children[at_index] = nil
228
+ end
212
229
  child
213
230
  end
214
231
 
data/lib/tree/version.rb CHANGED
@@ -35,5 +35,5 @@
35
35
 
36
36
  module Tree
37
37
  # Rubytree Package Version
38
- VERSION = '2.1.1'
38
+ VERSION = '2.2.0'
39
39
  end
data/lib/tree.rb CHANGED
@@ -318,7 +318,8 @@ module Tree
318
318
  #
319
319
  # @return [String] A string representation of the node.
320
320
  def to_s
321
- "Node Name: #{@name} Content: #{@content.to_s || '<Empty>'} " \
321
+ content_str = @content.nil? ? '<Empty>' : @content.to_s
322
+ "Node Name: #{@name} Content: #{content_str} " \
322
323
  "Parent: #{root? ? '<None>' : @parent.name.to_s} " \
323
324
  "Children: #{@children.length} Total Nodes: #{size}"
324
325
  end
@@ -394,6 +395,10 @@ module Tree
394
395
 
395
396
  raise ArgumentError, 'Attempting add root as a child' if child.equal?(root)
396
397
 
398
+ if (ancestors = parentage) && ancestors.include?(child)
399
+ raise ArgumentError, 'Attempting add ancestor as a child'
400
+ end
401
+
397
402
  # Lazy man's unique test, won't test if children of child are unique in
398
403
  # this tree too.
399
404
  raise "Child #{child.name} already added!"\
@@ -453,6 +458,9 @@ module Tree
453
458
  raise ArgumentError, "Invalid child name specified: #{old_name}"\
454
459
  unless @children_hash.key?(old_name)
455
460
 
461
+ raise ArgumentError, "Child name already exists: #{new_name}"\
462
+ if @children_hash.key?(new_name)
463
+
456
464
  @children_hash[new_name] = @children_hash.delete(old_name)
457
465
  @children_hash[new_name].name = new_name
458
466
  end
@@ -539,7 +547,10 @@ module Tree
539
547
  # @see #remove!
540
548
  # @see #remove_from_parent!
541
549
  def remove_all!
542
- @children.each(&:remove_all!)
550
+ @children.each do |child|
551
+ child.remove_all!
552
+ child.set_as_root!
553
+ end
543
554
 
544
555
  @children_hash.clear
545
556
  @children.clear
@@ -669,7 +680,7 @@ module Tree
669
680
  peek_node.visited = true
670
681
  # Add the children to the stack. Use the marking structure.
671
682
  marked_children =
672
- peek_node.node.children.map { |node| marked_node.new(node, false) }
683
+ peek_node.node.children.compact.map { |node| marked_node.new(node, false) }
673
684
  node_stack = marked_children.concat(node_stack)
674
685
  next
675
686
  else
@@ -700,9 +711,11 @@ module Tree
700
711
  # Use a queue to do breadth traversal
701
712
  until node_queue.empty?
702
713
  node_to_traverse = node_queue.shift
714
+ next unless node_to_traverse
715
+
703
716
  yield node_to_traverse
704
717
  # Enqueue the children from left to right.
705
- node_to_traverse.children { |child| node_queue.push child }
718
+ node_to_traverse.children { |child| node_queue.push child if child }
706
719
  end
707
720
 
708
721
  self if block_given?
@@ -770,7 +783,7 @@ module Tree
770
783
  end
771
784
  self
772
785
  else
773
- each
786
+ to_enum(:each_level)
774
787
  end
775
788
  end
776
789
 
data/rubytree.gemspec CHANGED
@@ -44,7 +44,8 @@ Gem::Specification.new do |s|
44
44
  END_DESC
45
45
 
46
46
  s.metadata = {
47
- 'rubygems_mfa_required' => 'true'
47
+ 'rubygems_mfa_required' => 'true',
48
+ 'github_repo' => 'ssh://github.com/evolve75/rubytree'
48
49
  }
49
50
 
50
51
  s.files = Dir['lib/**/*.rb'] # The actual code
@@ -229,6 +229,9 @@ module TestTree
229
229
  assert_nil(@root.left_child, 'The left child should now be nil')
230
230
  assert_nil(@root.first_child, 'The first child is now nil')
231
231
  assert_equal('B Child at Right', @root.last_child.name, 'The last child should now be the right child')
232
+ assert(@left_child1.root?, 'The old left child should now be a root')
233
+ assert_nil(@left_child1.parent, 'The old left child should not have a parent')
234
+ assert_nil(@root['A Child at Left'], 'Lookup by old left name should be nil')
232
235
  end
233
236
 
234
237
  # Test right_child= method.
@@ -249,6 +252,15 @@ module TestTree
249
252
  assert_nil(@root.right_child, 'The right child should now be nil')
250
253
  assert_equal('A Child at Left', @root.first_child.name, 'The first child should now be the left child')
251
254
  assert_nil(@root.last_child, 'The first child is now nil')
255
+ assert(@right_child1.root?, 'The old right child should now be a root')
256
+ assert_nil(@right_child1.parent, 'The old right child should not have a parent')
257
+ assert_nil(@root['B Child at Right'], 'Lookup by old right name should be nil')
258
+ end
259
+
260
+ # Test invalid index error for set_child_at.
261
+ def test_set_child_at_invalid_index
262
+ error = assert_raise(ArgumentError) { @root.send(:set_child_at, @left_child1, 2) }
263
+ assert_match(/cannot have more than two children/i, error.message)
252
264
  end
253
265
 
254
266
  # Test isLeft_child? method.
@@ -297,5 +309,27 @@ module TestTree
297
309
  assert_equal(@right_child1, @root[0], 'right_child1 should now be the first child')
298
310
  assert_equal(@left_child1, @root[1], 'left_child1 should now be the last child')
299
311
  end
312
+
313
+ # Test traversals when nil children exist.
314
+ def test_traversal_with_nil_children
315
+ @root << @left_child1
316
+ @root << @right_child1
317
+
318
+ @root.right_child = nil
319
+
320
+ breadth_nodes = []
321
+ post_nodes = []
322
+
323
+ assert_nothing_raised do
324
+ @root.breadth_each { |node| breadth_nodes << node }
325
+ end
326
+
327
+ assert_nothing_raised do
328
+ @root.postordered_each { |node| post_nodes << node }
329
+ end
330
+
331
+ assert_equal([@root, @left_child1], breadth_nodes)
332
+ assert_equal([@left_child1, @root], post_nodes)
333
+ end
300
334
  end
301
335
  end
data/test/test_tree.rb CHANGED
@@ -94,6 +94,7 @@ module TestTree
94
94
  assert_not_nil(@root.name, 'Name should not be nil')
95
95
  assert_equal('ROOT', @root.name, "Name should be 'ROOT'")
96
96
  assert_equal('Root Node', @root.content, "Content should be 'Root Node'")
97
+ assert(@root.to_s.include?('Content: Root Node'), 'to_s should include content value')
97
98
  assert(@root.root?, 'Should identify as root')
98
99
  assert(!@root.children?, 'Cannot have any children')
99
100
  assert(@root.content?, 'This root should have content')
@@ -117,6 +118,11 @@ module TestTree
117
118
  assert_equal(2, @root.node_height, "Root's height after adding the children should be 2")
118
119
  end
119
120
 
121
+ def test_to_s_empty_content
122
+ empty = Tree::TreeNode.new('EMPTY')
123
+ assert_match(/Content: <Empty>/, empty.to_s)
124
+ end
125
+
120
126
  def test_from_hash
121
127
  # A
122
128
  # / | \
@@ -491,6 +497,10 @@ module TestTree
491
497
 
492
498
  # Test the addition of a nil node.
493
499
  assert_raise(ArgumentError) { @root.add(nil) }
500
+
501
+ # Test adding an ancestor as a child (cycle prevention).
502
+ error = assert_raise(ArgumentError) { @child4.add(@child3) }
503
+ assert_match(/Attempting add ancestor as a child/, error.message)
494
504
  end
495
505
 
496
506
  # Test the addition of a duplicate node (duplicate being defined as a node with the same name).
@@ -689,6 +699,16 @@ module TestTree
689
699
 
690
700
  assert(!@root.children?, 'Should have no children')
691
701
  assert_equal(1, @root.size, 'Should have one node')
702
+
703
+ # Removed children should be detached (root? == true).
704
+ assert(@child1.root?, 'Child1 should be a root after remove_all!')
705
+ assert(@child2.root?, 'Child2 should be a root after remove_all!')
706
+ assert(@child3.root?, 'Child3 should be a root after remove_all!')
707
+ assert(@child4.root?, 'Child4 should be a root after remove_all!')
708
+ assert_nil(@child1.parent, 'Child1 parent should be nil after remove_all!')
709
+ assert_nil(@child2.parent, 'Child2 parent should be nil after remove_all!')
710
+ assert_nil(@child3.parent, 'Child3 parent should be nil after remove_all!')
711
+ assert_nil(@child4.parent, 'Child4 parent should be nil after remove_all!')
692
712
  end
693
713
 
694
714
  # Test the remove_from_parent! method.
@@ -832,6 +852,18 @@ module TestTree
832
852
  assert(result_array.include?(@child4), 'Should have child 4')
833
853
  end
834
854
 
855
+ # Test the each_level method without a block (Enumerator).
856
+ def test_each_level
857
+ setup_test_tree
858
+
859
+ levels = @root.each_level.to_a
860
+
861
+ assert_equal(3, levels.length, 'Should have three levels')
862
+ assert_equal([@root], levels[0])
863
+ assert_equal([@child1, @child2, @child3], levels[1])
864
+ assert_equal([@child4], levels[2])
865
+ end
866
+
835
867
  # Test the parent method.
836
868
  def test_parent
837
869
  setup_test_tree
@@ -1531,6 +1563,9 @@ module TestTree
1531
1563
 
1532
1564
  assert_raise(ArgumentError) { @root.rename_child('Not_Present_Child1', 'ALT_Child1') }
1533
1565
 
1566
+ error = assert_raise(ArgumentError) { @root.rename_child('Child1', 'Child2') }
1567
+ assert_match(/Child name already exists: Child2/, error.message)
1568
+
1534
1569
  @root.rename_child('Child1', 'ALT_Child1')
1535
1570
  assert_equal('ALT_Child1', @child1.name, "Name should be 'ALT_Child1'")
1536
1571
  assert_equal(@child1, @root['ALT_Child1'], 'Should be able to access from parent using new name')
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubytree
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anupam Sengupta
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-20 00:00:00.000000000 Z
11
+ date: 2026-02-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -272,6 +272,7 @@ licenses:
272
272
  - BSD-2-Clause
273
273
  metadata:
274
274
  rubygems_mfa_required: 'true'
275
+ github_repo: ssh://github.com/evolve75/rubytree
275
276
  post_install_message: |2
276
277
  ========================================================================
277
278
  Thank you for installing RubyTree.
@@ -304,7 +305,7 @@ post_install_message: |2
304
305
  ========================================================================
305
306
  rdoc_options:
306
307
  - "--title"
307
- - 'Rubytree Documentation: rubytree-2.1.1'
308
+ - 'Rubytree Documentation: rubytree-2.2.0'
308
309
  - "--main"
309
310
  - README.md
310
311
  - "--quiet"