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 +4 -4
- data/API-CHANGES.md +26 -0
- data/Gemfile.lock +1 -1
- data/History.md +18 -0
- data/Rakefile +9 -2
- data/lib/tree/binarytree.rb +22 -5
- data/lib/tree/version.rb +1 -1
- data/lib/tree.rb +18 -5
- data/rubytree.gemspec +2 -1
- data/test/test_binarytree.rb +34 -0
- data/test/test_tree.rb +35 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4453bf8edf542985ddd52c289e7194263a5251cedd9ffd0655248a8c13585dd0
|
|
4
|
+
data.tar.gz: 67a85efeb8483aecdd9a576708a92eee8e333103b34b3ed04fcf748b73f1a07f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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-
|
|
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
|
|
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
|
|
data/lib/tree/binarytree.rb
CHANGED
|
@@ -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
|
-
|
|
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]
|
|
210
|
-
|
|
211
|
-
|
|
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
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
|
-
|
|
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
|
|
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
|
-
|
|
786
|
+
to_enum(:each_level)
|
|
774
787
|
end
|
|
775
788
|
end
|
|
776
789
|
|
data/rubytree.gemspec
CHANGED
data/test/test_binarytree.rb
CHANGED
|
@@ -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.
|
|
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:
|
|
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.
|
|
308
|
+
- 'Rubytree Documentation: rubytree-2.2.0'
|
|
308
309
|
- "--main"
|
|
309
310
|
- README.md
|
|
310
311
|
- "--quiet"
|