philiprehberger-tree 0.1.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 +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE +21 -0
- data/README.md +109 -0
- data/lib/philiprehberger/tree/node.rb +182 -0
- data/lib/philiprehberger/tree/version.rb +7 -0
- data/lib/philiprehberger/tree.rb +10 -0
- metadata +56 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bd37f9905975c79b4fa407d42276f950c4a267bf3133127f7c631139fe1d4b3e
|
|
4
|
+
data.tar.gz: b88f671ca4f70ff37dd929decb5fe063e0080bccbe4bd789c01140d48e4a9942
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6b20812cb1d392ea095dbbe2a2752e76b037f739703ea696de6d79b0dcac69dc47a2210534470f0902191ac611d8c478a57eb85aee95d0a42e86b674f2429b06
|
|
7
|
+
data.tar.gz: b03f869248187d0e481b122500768bb69ed89587d5cd75653f46e070e737bb50e719fad2debd9d9191a6ede94afc0a35283d04a30bf1330ed6fb979fe91836eb
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this gem will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-03-22
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release
|
|
14
|
+
- Generic tree node with value, children, and parent tracking
|
|
15
|
+
- Depth-first (pre-order) and breadth-first traversal
|
|
16
|
+
- Node search with predicate block
|
|
17
|
+
- Path finding from root to target value
|
|
18
|
+
- Leaf node collection
|
|
19
|
+
- Tree metrics: depth, height, and size
|
|
20
|
+
- Hash serialization via `to_h`
|
|
21
|
+
- Visual tree printing via `print_tree`
|
|
22
|
+
- Add and remove child operations
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 philiprehberger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# philiprehberger-tree
|
|
2
|
+
|
|
3
|
+
[](https://github.com/philiprehberger/rb-tree/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/philiprehberger-tree)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Generic tree data structure with traversal, search, and serialization
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Ruby >= 3.1
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Add to your Gemfile:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "philiprehberger-tree"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install directly:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
gem install philiprehberger-tree
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require "philiprehberger/tree"
|
|
31
|
+
|
|
32
|
+
root = Philiprehberger::Tree::Node.new('root')
|
|
33
|
+
child = root.add_child('child')
|
|
34
|
+
child.add_child('grandchild')
|
|
35
|
+
|
|
36
|
+
root.size # => 3
|
|
37
|
+
root.height # => 2
|
|
38
|
+
root.leaf? # => false
|
|
39
|
+
child.depth # => 1
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Traversal
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
root.each_dfs { |node| puts node.value } # depth-first pre-order
|
|
46
|
+
root.each_bfs { |node| puts node.value } # breadth-first
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Search and Path Finding
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
node = root.find { |n| n.value == 'grandchild' }
|
|
53
|
+
path = root.path_to('grandchild')
|
|
54
|
+
path.map(&:value) # => ['root', 'child', 'grandchild']
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Serialization
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
root.to_h
|
|
61
|
+
# => { value: 'root', children: [{ value: 'child', children: [...] }] }
|
|
62
|
+
|
|
63
|
+
puts root.print_tree
|
|
64
|
+
# root
|
|
65
|
+
# └── child
|
|
66
|
+
# └── grandchild
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Leaf Collection
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
root.leaves.map(&:value) # => ['grandchild']
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## API
|
|
76
|
+
|
|
77
|
+
### `Node`
|
|
78
|
+
|
|
79
|
+
| Method | Description |
|
|
80
|
+
|--------|-------------|
|
|
81
|
+
| `.new(value)` | Create a new tree node |
|
|
82
|
+
| `#add_child(value)` | Add a child node and return it |
|
|
83
|
+
| `#remove_child(value)` | Remove a child by value |
|
|
84
|
+
| `#children` | Array of child nodes |
|
|
85
|
+
| `#parent` | Parent node or nil |
|
|
86
|
+
| `#root?` | True if node has no parent |
|
|
87
|
+
| `#leaf?` | True if node has no children |
|
|
88
|
+
| `#depth` | Distance from root |
|
|
89
|
+
| `#height` | Height of subtree |
|
|
90
|
+
| `#size` | Total nodes in subtree |
|
|
91
|
+
| `#each_dfs` | Depth-first pre-order iteration |
|
|
92
|
+
| `#each_bfs` | Breadth-first iteration |
|
|
93
|
+
| `#find { \|n\| }` | Find first node matching predicate |
|
|
94
|
+
| `#path_to(value)` | Array of nodes from root to target |
|
|
95
|
+
| `#leaves` | All leaf nodes in subtree |
|
|
96
|
+
| `#to_h` | Serialize subtree to hash |
|
|
97
|
+
| `#print_tree` | Visual string representation |
|
|
98
|
+
|
|
99
|
+
## Development
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
bundle install
|
|
103
|
+
bundle exec rspec # Run tests
|
|
104
|
+
bundle exec rubocop # Check code style
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module Tree
|
|
5
|
+
# A generic tree node with traversal, search, and serialization.
|
|
6
|
+
class Node
|
|
7
|
+
# @return [Object] the value stored in this node
|
|
8
|
+
attr_reader :value
|
|
9
|
+
|
|
10
|
+
# @return [Array<Node>] child nodes
|
|
11
|
+
attr_reader :children
|
|
12
|
+
|
|
13
|
+
# @return [Node, nil] parent node (nil for root)
|
|
14
|
+
attr_reader :parent
|
|
15
|
+
|
|
16
|
+
# Create a new tree node.
|
|
17
|
+
#
|
|
18
|
+
# @param value [Object] the value to store
|
|
19
|
+
def initialize(value)
|
|
20
|
+
@value = value
|
|
21
|
+
@children = []
|
|
22
|
+
@parent = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Add a child node with the given value.
|
|
26
|
+
#
|
|
27
|
+
# @param value [Object] value for the new child node
|
|
28
|
+
# @return [Node] the newly created child node
|
|
29
|
+
def add_child(value)
|
|
30
|
+
child = value.is_a?(Node) ? value : Node.new(value)
|
|
31
|
+
child.instance_variable_set(:@parent, self)
|
|
32
|
+
@children << child
|
|
33
|
+
child
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Remove a child node by value.
|
|
37
|
+
#
|
|
38
|
+
# @param value [Object] value of the child to remove
|
|
39
|
+
# @return [Node, nil] the removed child node, or nil if not found
|
|
40
|
+
def remove_child(value)
|
|
41
|
+
index = @children.index { |c| c.value == value }
|
|
42
|
+
return nil unless index
|
|
43
|
+
|
|
44
|
+
child = @children.delete_at(index)
|
|
45
|
+
child.instance_variable_set(:@parent, nil)
|
|
46
|
+
child
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if this node is the root (has no parent).
|
|
50
|
+
#
|
|
51
|
+
# @return [Boolean]
|
|
52
|
+
def root?
|
|
53
|
+
@parent.nil?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Check if this node is a leaf (has no children).
|
|
57
|
+
#
|
|
58
|
+
# @return [Boolean]
|
|
59
|
+
def leaf?
|
|
60
|
+
@children.empty?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Return the depth of this node (distance from root).
|
|
64
|
+
#
|
|
65
|
+
# @return [Integer]
|
|
66
|
+
def depth
|
|
67
|
+
node = self
|
|
68
|
+
d = 0
|
|
69
|
+
while node.parent
|
|
70
|
+
d += 1
|
|
71
|
+
node = node.parent
|
|
72
|
+
end
|
|
73
|
+
d
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Return the height of the subtree rooted at this node.
|
|
77
|
+
#
|
|
78
|
+
# @return [Integer]
|
|
79
|
+
def height
|
|
80
|
+
return 0 if leaf?
|
|
81
|
+
|
|
82
|
+
1 + @children.map(&:height).max
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Return the total number of nodes in the subtree rooted at this node.
|
|
86
|
+
#
|
|
87
|
+
# @return [Integer]
|
|
88
|
+
def size
|
|
89
|
+
1 + @children.sum(&:size)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Iterate depth-first (pre-order) over the subtree.
|
|
93
|
+
#
|
|
94
|
+
# @yield [Node] each node in depth-first order
|
|
95
|
+
# @return [Enumerator] if no block given
|
|
96
|
+
def each_dfs(&block)
|
|
97
|
+
return enum_for(:each_dfs) unless block
|
|
98
|
+
|
|
99
|
+
block.call(self)
|
|
100
|
+
@children.each { |child| child.each_dfs(&block) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Iterate breadth-first over the subtree.
|
|
104
|
+
#
|
|
105
|
+
# @yield [Node] each node in breadth-first order
|
|
106
|
+
# @return [Enumerator] if no block given
|
|
107
|
+
def each_bfs(&block)
|
|
108
|
+
return enum_for(:each_bfs) unless block
|
|
109
|
+
|
|
110
|
+
queue = [self]
|
|
111
|
+
until queue.empty?
|
|
112
|
+
node = queue.shift
|
|
113
|
+
block.call(node)
|
|
114
|
+
queue.concat(node.children)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Find the first node matching the block.
|
|
119
|
+
#
|
|
120
|
+
# @yield [Node] predicate block
|
|
121
|
+
# @return [Node, nil] the first matching node, or nil
|
|
122
|
+
def find(&block)
|
|
123
|
+
return nil unless block
|
|
124
|
+
|
|
125
|
+
each_dfs { |node| return node if block.call(node) }
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Return the path from root to the first node with the given value.
|
|
130
|
+
#
|
|
131
|
+
# @param value [Object] value to search for
|
|
132
|
+
# @return [Array<Node>, nil] array of nodes from root to target, or nil if not found
|
|
133
|
+
def path_to(value)
|
|
134
|
+
target = find { |n| n.value == value }
|
|
135
|
+
return nil unless target
|
|
136
|
+
|
|
137
|
+
path = []
|
|
138
|
+
node = target
|
|
139
|
+
while node
|
|
140
|
+
path.unshift(node)
|
|
141
|
+
node = node.parent
|
|
142
|
+
end
|
|
143
|
+
path
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Return all leaf nodes in the subtree.
|
|
147
|
+
#
|
|
148
|
+
# @return [Array<Node>]
|
|
149
|
+
def leaves
|
|
150
|
+
each_dfs.select(&:leaf?)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Serialize the subtree to a hash.
|
|
154
|
+
#
|
|
155
|
+
# @return [Hash] hash with :value and :children keys
|
|
156
|
+
def to_h
|
|
157
|
+
{
|
|
158
|
+
value: @value,
|
|
159
|
+
children: @children.map(&:to_h)
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Print a visual representation of the tree.
|
|
164
|
+
#
|
|
165
|
+
# @param indent [String] prefix for indentation (used internally)
|
|
166
|
+
# @param last [Boolean] whether this is the last sibling (used internally)
|
|
167
|
+
# @return [String] the tree as a formatted string
|
|
168
|
+
def print_tree(indent: '', last: true)
|
|
169
|
+
lines = []
|
|
170
|
+
connector = root? ? '' : (last ? '└── ' : '├── ')
|
|
171
|
+
lines << "#{indent}#{connector}#{@value}"
|
|
172
|
+
|
|
173
|
+
child_indent = indent + (root? ? '' : (last ? ' ' : '│ '))
|
|
174
|
+
@children.each_with_index do |child, i|
|
|
175
|
+
lines << child.print_tree(indent: child_indent, last: i == @children.size - 1)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
lines.join("\n")
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: philiprehberger-tree
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Philip Rehberger
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-22 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: A generic tree data structure supporting depth-first and breadth-first
|
|
14
|
+
traversal, node search, path finding, and hash serialization. Each node tracks its
|
|
15
|
+
parent, children, depth, height, and size.
|
|
16
|
+
email:
|
|
17
|
+
- me@philiprehberger.com
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- CHANGELOG.md
|
|
23
|
+
- LICENSE
|
|
24
|
+
- README.md
|
|
25
|
+
- lib/philiprehberger/tree.rb
|
|
26
|
+
- lib/philiprehberger/tree/node.rb
|
|
27
|
+
- lib/philiprehberger/tree/version.rb
|
|
28
|
+
homepage: https://github.com/philiprehberger/rb-tree
|
|
29
|
+
licenses:
|
|
30
|
+
- MIT
|
|
31
|
+
metadata:
|
|
32
|
+
homepage_uri: https://github.com/philiprehberger/rb-tree
|
|
33
|
+
source_code_uri: https://github.com/philiprehberger/rb-tree
|
|
34
|
+
changelog_uri: https://github.com/philiprehberger/rb-tree/blob/main/CHANGELOG.md
|
|
35
|
+
bug_tracker_uri: https://github.com/philiprehberger/rb-tree/issues
|
|
36
|
+
rubygems_mfa_required: 'true'
|
|
37
|
+
post_install_message:
|
|
38
|
+
rdoc_options: []
|
|
39
|
+
require_paths:
|
|
40
|
+
- lib
|
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: 3.1.0
|
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
47
|
+
requirements:
|
|
48
|
+
- - ">="
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: '0'
|
|
51
|
+
requirements: []
|
|
52
|
+
rubygems_version: 3.5.22
|
|
53
|
+
signing_key:
|
|
54
|
+
specification_version: 4
|
|
55
|
+
summary: Generic tree data structure with traversal, search, and serialization
|
|
56
|
+
test_files: []
|