modular_tree 0.0.1
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/.rspec +3 -0
- data/.ruby-version +1 -0
- data/Gemfile +10 -0
- data/README.md +155 -0
- data/Rakefile +8 -0
- data/lib/modular_tree/algorithms.rb +255 -0
- data/lib/modular_tree/filter.rb +84 -0
- data/lib/modular_tree/implementations.rb +202 -0
- data/lib/modular_tree/pairs.rb +14 -0
- data/lib/modular_tree/pool.rb +26 -0
- data/lib/modular_tree/properties.rb +53 -0
- data/lib/modular_tree/separator.rb +21 -0
- data/lib/modular_tree/tree_array.rb +19 -0
- data/lib/modular_tree/version.rb +5 -0
- data/lib/modular_tree.rb +137 -0
- data/modular_tree.gemspec +49 -0
- data/sig/modular_tree.rbs +4 -0
- metadata +116 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 87b4426ea3d2f00ff5959831b0297d21190fee942d720f9e239cccc74e02ddcb
|
4
|
+
data.tar.gz: feb82cbe67f18d2f0855f041a4612fbe023ae5564706123dee7bbef1e0d39b4a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ef3db04b865ec12612ccae42104bad67344892857f3bed8b523cc70b4aaab9ace7675cbb30bfab751143c741d0e45c13157cc6f8594b90f296fb4e2fb0efe15a
|
7
|
+
data.tar.gz: dc1b4179bf2f46d3506af4c8660d71d552ff07567dda93403a5eca618744ca1a2aa53f8adf7f89d251d9b9f3ea4acb3080df4bd0933b4b33840656e8f874fc00
|
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-3.1.2
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
# ModularTree
|
2
|
+
|
3
|
+
A modular tree implementation for Ruby
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
require "modular_tree"
|
9
|
+
|
10
|
+
class Node < Tree::Tree
|
11
|
+
attr_reader :name
|
12
|
+
def initialize(parent, name)
|
13
|
+
@name = name
|
14
|
+
super(parent)
|
15
|
+
end
|
16
|
+
def to_s(name)
|
17
|
+
end
|
18
|
+
|
19
|
+
root = Node.new(nil, "root")
|
20
|
+
a = Node.new(root, "a")
|
21
|
+
b = Node.new(root, "b")
|
22
|
+
|
23
|
+
puts root.children.join(', ') # a, b
|
24
|
+
puts a.parent # root
|
25
|
+
|
26
|
+
|
27
|
+
## Description
|
28
|
+
|
29
|
+
ModularTree classes are defined by using a set of modules that can be divided
|
30
|
+
into the following categories:
|
31
|
+
|
32
|
+
* Property modules
|
33
|
+
* Implementation modules
|
34
|
+
* Algorithms modules
|
35
|
+
|
36
|
+
Property modules defines abstract properties that are defined by the
|
37
|
+
implementation modules while algorithms are defined in terms of properties.
|
38
|
+
This decouples algorithms and implementations and makes it possible to use the
|
39
|
+
same algorithms on different tree implementations
|
40
|
+
|
41
|
+
## Block arguments
|
42
|
+
|
43
|
+
Blocks are called with three arguments - value, key, and parent - but
|
44
|
+
usually only 'value' is used (Ruby allows you to ignore the remaining
|
45
|
+
arguments)
|
46
|
+
|
47
|
+
Keys may be nil if the underlying data structure doesn't support
|
48
|
+
them efficiently. Keys for array implementations are the node indexes. Note
|
49
|
+
that keys are only unique if you don't apply filters because filters may
|
50
|
+
combine nodes from different parents
|
51
|
+
|
52
|
+
Algorithms are supposed to take care of traversing the tree so blocks are
|
53
|
+
called with the values and not the tree node. This makes a difference for
|
54
|
+
external implementations where the values don't know their position in
|
55
|
+
the tree
|
56
|
+
|
57
|
+
If you don't need the keys, then the node/parent combination is already covered
|
58
|
+
by #edges
|
59
|
+
|
60
|
+
## External data structures
|
61
|
+
|
62
|
+
### Hash
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
{
|
66
|
+
"root" => {
|
67
|
+
"a" => {
|
68
|
+
"b" => {},
|
69
|
+
"c" => {}
|
70
|
+
},
|
71
|
+
"d" => {
|
72
|
+
"e" => {}
|
73
|
+
}
|
74
|
+
}
|
75
|
+
}
|
76
|
+
```
|
77
|
+
|
78
|
+
`#value` returns the hash value of a node. Eg. {"e"=>{}} if called on the "d" node
|
79
|
+
|
80
|
+
\#each_branch & #each_child will be called with
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
{...}, "root"
|
84
|
+
{...}, "a"
|
85
|
+
{}, "b"
|
86
|
+
{}, "c"
|
87
|
+
{...}, "d"
|
88
|
+
{}, "e"
|
89
|
+
```
|
90
|
+
|
91
|
+
### Nested Array
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
[
|
95
|
+
["root", [
|
96
|
+
["a", [
|
97
|
+
["b", []],
|
98
|
+
["c", []]
|
99
|
+
]],
|
100
|
+
["d", [
|
101
|
+
["e", []]
|
102
|
+
]]
|
103
|
+
]]
|
104
|
+
]
|
105
|
+
```
|
106
|
+
|
107
|
+
`#value` returns first element in each node tuple. Eg. "d" if called on the "d" node
|
108
|
+
|
109
|
+
|
110
|
+
\#each_child will be called with
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
"root", 0
|
114
|
+
"a", 0
|
115
|
+
"b", 0
|
116
|
+
"c", 1
|
117
|
+
"d", 1
|
118
|
+
"e", 0
|
119
|
+
```
|
120
|
+
|
121
|
+
\#each_branch will be called with
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
["root", [...]], 0
|
125
|
+
["a", [...]], 0
|
126
|
+
["b", []], 0
|
127
|
+
["c", []], 1
|
128
|
+
["d", [...]], 1
|
129
|
+
["e", []], 0
|
130
|
+
|
131
|
+
|
132
|
+
Note that nested arrays is a structural representation where the "key" is the
|
133
|
+
real objects the tree is made of while hashes are maps from a value to the
|
134
|
+
object modelled as a hash. Hash trees have keys (typically strings) while
|
135
|
+
nested arrays has integer indexes as keys
|
136
|
+
|
137
|
+
## Installation
|
138
|
+
|
139
|
+
Install the gem and add to the application's Gemfile by executing:
|
140
|
+
|
141
|
+
$ bundle add modular_tree
|
142
|
+
|
143
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
144
|
+
|
145
|
+
$ gem install modular_tree
|
146
|
+
|
147
|
+
## Development
|
148
|
+
|
149
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
150
|
+
|
151
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
152
|
+
|
153
|
+
## Contributing
|
154
|
+
|
155
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/modular_tree.
|
data/Rakefile
ADDED
@@ -0,0 +1,255 @@
|
|
1
|
+
|
2
|
+
module Tree
|
3
|
+
|
4
|
+
# A down tree can only be traversed bottom-up
|
5
|
+
#
|
6
|
+
# TODO: Add FilteredUpTreeAlgorithms
|
7
|
+
#
|
8
|
+
module UpTreeAlgorithms
|
9
|
+
include NodeProperty
|
10
|
+
include BranchesProperty
|
11
|
+
|
12
|
+
# Bottom-up
|
13
|
+
def ancestors
|
14
|
+
curr = self
|
15
|
+
a = []
|
16
|
+
a.push curr.node while curr = curr.branch
|
17
|
+
a
|
18
|
+
end
|
19
|
+
|
20
|
+
# Top-down
|
21
|
+
def ancestry
|
22
|
+
curr = self
|
23
|
+
a = []
|
24
|
+
a.unshift curr.node while curr = curr.branch
|
25
|
+
a
|
26
|
+
end
|
27
|
+
|
28
|
+
def depth = @depth ||= ancestors.size
|
29
|
+
end
|
30
|
+
|
31
|
+
# IDEA
|
32
|
+
#
|
33
|
+
# tree.forrest -> Forrest
|
34
|
+
# tree.nodes -> Use node instead of value
|
35
|
+
# tree.forrest.nodes -> both
|
36
|
+
|
37
|
+
|
38
|
+
# A down tree can only be traversed top-down as it has no reference to its
|
39
|
+
# parent. It is also a kind of a graph node
|
40
|
+
#
|
41
|
+
# TODO: Turn into class methods to implement array/hash/other adaptors.
|
42
|
+
# #children should then be called as 'children(node)' everywhere
|
43
|
+
#
|
44
|
+
# A #node method should also be defined at the top-most level
|
45
|
+
#
|
46
|
+
# TODO: Better split between DownTreeAlgorithms and DownTreeFilteredAlgorithms
|
47
|
+
#
|
48
|
+
module DownTreeFilteredAlgorithms
|
49
|
+
include NodeProperty
|
50
|
+
include BranchesProperty
|
51
|
+
|
52
|
+
# True if the node doesn't contain any branches (#empty? for trees)
|
53
|
+
def bare? = raise NotImplemented
|
54
|
+
|
55
|
+
# The number of nodes in the tree. Note that this can be an expensive
|
56
|
+
# operation because every node has to be visited
|
57
|
+
def size = 1 + descendants.to_a.size
|
58
|
+
|
59
|
+
# Enumerator of descendant nodes matching filter. Same as #preorder with
|
60
|
+
# :this set to false. TODO: Maybe introduce forrests: tree.forrest.each
|
61
|
+
def descendants(*filter) = each(filter, this: false)
|
62
|
+
|
63
|
+
# Implementation of Enumerable#each extended with filters. The block is
|
64
|
+
# called with value, key, and parent as arguments (it may choose to
|
65
|
+
# ignore key and/or parent). Returns an enumerator of values without a block
|
66
|
+
def each(*filter, this: true, &block) = common_each(*filter, :value, :each_preorder, this, &block)
|
67
|
+
|
68
|
+
# Like #each but the block is called with node, key, and parent instead of
|
69
|
+
# value, key, and parent
|
70
|
+
def nodes(*filter, this: true, &block) = common_each(*filter, :node, :each_preorder, this, &block)
|
71
|
+
|
72
|
+
# Pre-order enumerator of selected nodes. Same as #each without a block
|
73
|
+
def preorder(*filter, this: true) = each(*filter, this: this)
|
74
|
+
|
75
|
+
# Post-order enumerator of selected nodes
|
76
|
+
def postorder(*filter, this: true) = common_each(*filter, :value, :each_postorder, this)
|
77
|
+
|
78
|
+
# Enumerator of edges in the tree. Edges are [previous-matching-node,
|
79
|
+
# matching-node] tuples. Top-level nodes have previous-matching-node set to
|
80
|
+
# nil
|
81
|
+
#
|
82
|
+
def edges(*filter, this: true, &block)
|
83
|
+
if block_given
|
84
|
+
each(*filter, this: this) { |node, _, parent| yield parent, node }
|
85
|
+
else
|
86
|
+
Pairs.new { |enum| each(*filter, this: this) { |node, _, parent| enum << [parent, node] } }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# def pairs(filter, filter, &block)
|
91
|
+
# end
|
92
|
+
|
93
|
+
# Traverse the tree top-down while accumulating information in an
|
94
|
+
# accumulator object. The block takes a [accumulator, node] tuple and is
|
95
|
+
# responsible for adding itself to the accumulator. The return value from
|
96
|
+
# the block is then used as the accumulator for the branch nodes. Note that
|
97
|
+
# it returns the original accumulator and not the final result - this makes
|
98
|
+
# it different from #inject
|
99
|
+
#
|
100
|
+
# #accumulate is a kind of "preorder" algorithm
|
101
|
+
def accumulate(*filter, accumulator, this: true, &block)
|
102
|
+
filter = self.class.filter(*filter)
|
103
|
+
block_given? or raise ArgumentError, "Block is required"
|
104
|
+
do_accumulate(filter, this, accumulator, &block)
|
105
|
+
accumulator
|
106
|
+
end
|
107
|
+
|
108
|
+
# Traverse the tree bottom-up while aggregating information. The block is
|
109
|
+
# called with a [current-node, branch-node-results] tuple
|
110
|
+
#
|
111
|
+
# #aggregate is a kind of "postorder" algorithm
|
112
|
+
#
|
113
|
+
# TODO: Remove +this+ flag - it not used and doesn't make sense
|
114
|
+
def aggregate(*filter, this: true, &block)
|
115
|
+
filter = self.class.filter(*filter)
|
116
|
+
do_aggregate(filter, this, &block)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Stops further recursion if the block returns truthy
|
120
|
+
def propagate(*filter, this: true, &block)
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
# tree.each(DocumentNode).select(BriefNode).each { |doc, brief| ... }
|
125
|
+
# tree.edges(DocumentNode, BriefNode).group.all? { |doc, briefs| briefs.size <= 1 }
|
126
|
+
# tree.map(DocumentNode) { |doc| doc.select(BriefNode).size <= 1 or raise }
|
127
|
+
|
128
|
+
# Create a Tree::Filter object. Can also take an existing filter as
|
129
|
+
# argument in which case the given filter will just be passed through
|
130
|
+
def self.filter(*args)
|
131
|
+
if args.first.is_a?(Filter)
|
132
|
+
args.size == 1 or raise ArgumentError
|
133
|
+
args.first
|
134
|
+
else
|
135
|
+
Filter.new(*args)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
protected
|
140
|
+
def common_each(*filter, value_method, each_method, this, &block)
|
141
|
+
filter = self.class.filter(*filter)
|
142
|
+
if block_given?
|
143
|
+
self.send(each_method, nil, filter, value_method, nil, nil, this, &block)
|
144
|
+
else
|
145
|
+
Enumerator.new { |enum| self.send(each_method, enum, filter, value_method, nil, nil, this) }
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# TODO: Split into automatically generated variants
|
150
|
+
def do_each_preorder(enum, filter, value_method, key, parent, this, &block)
|
151
|
+
select, traverse = filter.match(self)
|
152
|
+
if select && this
|
153
|
+
value = self.send(value_method)
|
154
|
+
if block_given?
|
155
|
+
yield(value, key, parent)
|
156
|
+
else
|
157
|
+
enum << value
|
158
|
+
end
|
159
|
+
parent = value
|
160
|
+
end
|
161
|
+
if !this || traverse
|
162
|
+
each_branch { |branch, key|
|
163
|
+
branch.do_each_preorder(enum, filter, value_method, key, parent, true, &block)
|
164
|
+
}
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def do_each_postorder(enum, filter, value_method, key, parent, this, &block)
|
169
|
+
select, traverse = filter.match(self)
|
170
|
+
if !this || traverse
|
171
|
+
each_branch { |branch, key|
|
172
|
+
branch.do_each_postorder(enum, filter, value_method, key, p, true, &block)
|
173
|
+
}
|
174
|
+
end
|
175
|
+
if select && this
|
176
|
+
value = self.send(value_method)
|
177
|
+
if block_given?
|
178
|
+
yield(value, key, parent)
|
179
|
+
else
|
180
|
+
enum << value
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def do_propagate(filter, this, key, parent, &block)
|
186
|
+
select, traverse = filter.match(self)
|
187
|
+
if select && this
|
188
|
+
return if yield(self, key, parent)
|
189
|
+
end
|
190
|
+
if !this || traverse
|
191
|
+
each_branch { |branch, key| branch.do_propagate(filter, key, self.value) }
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def do_accumulate(filter, this, acc, &block)
|
196
|
+
select, traverse = filter.match(self)
|
197
|
+
acc = yield(acc, self.value) if this && select
|
198
|
+
each_branch.each { |branch| branch.do_accumulate(filter, true, acc, &block) } if traverse || !this
|
199
|
+
end
|
200
|
+
|
201
|
+
def do_aggregate(filter, this, &block) # TODO: use select-status
|
202
|
+
block_given? or raise ArgumentError
|
203
|
+
select, traverse = filter.match(self)
|
204
|
+
values = traverse ? each_branch { |branch|
|
205
|
+
r = branch.do_aggregate(filter, true, &block)
|
206
|
+
}.to_a : []
|
207
|
+
yield(self.value, values)
|
208
|
+
end
|
209
|
+
|
210
|
+
end
|
211
|
+
|
212
|
+
module DownTreeAlgorithms
|
213
|
+
include DownTreeFilteredAlgorithms
|
214
|
+
|
215
|
+
# def self.included(other)
|
216
|
+
# other.include DownTreeFilteredAlgorithms
|
217
|
+
# super
|
218
|
+
# end
|
219
|
+
|
220
|
+
def self.filter(*args) = DownTreeFilteredAlgorithms.filter(*args)
|
221
|
+
|
222
|
+
# include DownTreeFilteredAlgorithms
|
223
|
+
|
224
|
+
# Very lazy implementation
|
225
|
+
# def descendants = preorder(Filter::ALL, this: false)
|
226
|
+
# def filter(*args) = abstract_method
|
227
|
+
# alias_method :nodes, :each
|
228
|
+
# def edges(*args, **opts, &block) = super(Filter::ALL, *args, **opts, &block)
|
229
|
+
# def pairs(*args, **opts, &block) = super(Filter::ALL, *args, **opts, &block)
|
230
|
+
# def preorder(*args, **opts, &block) = super(Filter::ALL, *args, **opts, &block)
|
231
|
+
# def postorder(*args, **opts, &block) = super(Filter::ALL, *args, **opts, &block)
|
232
|
+
# def visit(*args, **opts, &block) = super(Filter::ALL, *args, **opts, &block)
|
233
|
+
# def accumulate(*args, **opts, &block) = super(Filter::ALL, *args, **opts, &block)
|
234
|
+
# def aggregate(*args, **opts, &block) = super(Filter::ALL, *args, **opts, &block)
|
235
|
+
# def find(*args, **opts, &block) = super(Filter::ALL, *args, **opts, &block)
|
236
|
+
end
|
237
|
+
|
238
|
+
module PathAlgorithms
|
239
|
+
include KeyProperty
|
240
|
+
include UpTreeAlgorithms
|
241
|
+
|
242
|
+
def separator = @separator ||= parent&.separator || ::Tree.separator
|
243
|
+
def separator=(s) @separator = s end
|
244
|
+
|
245
|
+
def path = @path ||= ancestry[1..-1]&.map(&:key)&.join(separator) || ""
|
246
|
+
def uid() @uid ||= [parent&.uid, key].compact.join(separator) end
|
247
|
+
end
|
248
|
+
|
249
|
+
module DotAlgorithms
|
250
|
+
include KeysProperty
|
251
|
+
|
252
|
+
def dot(path) = Separator.split(path).keys.each.inject(self) { |a,e| a[e] }
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Tree
|
2
|
+
class Filter
|
3
|
+
# Create a node filter. The filter is initialized by a select expression
|
4
|
+
# and a traverse expression. The select expression decides if the node
|
5
|
+
# should be given to the block (or submitted to an enumerator) and the
|
6
|
+
# traverse expression decides if the child nodes should be traversed
|
7
|
+
# recursively
|
8
|
+
#
|
9
|
+
# The expressions can be a Proc, Symbol, or an array of classes. In
|
10
|
+
# addition, +select+ can also be true, and +traverse+ can be true,
|
11
|
+
# false, or nil. True, false, and nil have special meanings:
|
12
|
+
#
|
13
|
+
# when +select+ is
|
14
|
+
# true Select always. This is the default
|
15
|
+
#
|
16
|
+
# when +traverse+ is
|
17
|
+
# true Traverse always. This is the default
|
18
|
+
# false Traverse only if select didn't match
|
19
|
+
# nil Expects +select+ to return a two-tuple of booleans. Can't be
|
20
|
+
# used when +select+ is true
|
21
|
+
#
|
22
|
+
# If the expression is a Proc object, it will be called with the current
|
23
|
+
# node as argument. If the return value is true, the node is
|
24
|
+
# selected/traversed and skipped otherwise. If the expression is a method
|
25
|
+
# name (Symbol), the method will be called on each node with no arguments.
|
26
|
+
# It is not an error if the method doesn't exists but the node is not
|
27
|
+
# selected/traversed
|
28
|
+
#
|
29
|
+
# Filters should not have side-effects because they can be used in
|
30
|
+
# enumerators that doesn't execute the filter unless the enumerator is
|
31
|
+
# evaluated
|
32
|
+
#
|
33
|
+
# TODO: block argument
|
34
|
+
#
|
35
|
+
def initialize(select_expr = true, traverse_expr = true, &block)
|
36
|
+
constrain select_expr, Proc, Symbol, Class, [Class], true
|
37
|
+
constrain traverse_expr, Proc, Symbol, Class, [Class], true, false, nil
|
38
|
+
select = mk_lambda(select_expr)
|
39
|
+
traverse = mk_lambda(traverse_expr)
|
40
|
+
@matcher =
|
41
|
+
case select
|
42
|
+
when Proc
|
43
|
+
case traverse
|
44
|
+
when Proc; lambda { |node| [select.call(node), traverse.call(node)] }
|
45
|
+
when true; lambda { |node| [select.call(node), true] }
|
46
|
+
when false; lambda { |node| r = select.call(node); [r, !r] }
|
47
|
+
when nil; lambda { |node| select.call(node) }
|
48
|
+
end
|
49
|
+
when true
|
50
|
+
case traverse
|
51
|
+
when Proc; lambda { |node| [true, traverse.call(node)] }
|
52
|
+
when true; lambda { |_| [true, true] }
|
53
|
+
when false; lambda { |_| [true, false] } # effectively same as #children.each
|
54
|
+
when nil; raise ArgumentError
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Match +node+ against the filter and return a [select, traverse] tuple of booleans
|
60
|
+
def match(node) = @matcher.call(node)
|
61
|
+
|
62
|
+
# Create a proc if arg is a Symbol or an Array of classes. Pass through
|
63
|
+
# Proc objects, true, false, and nil
|
64
|
+
def mk_lambda(arg) = self.class.mk_lambda(arg)
|
65
|
+
def self.mk_lambda(arg)
|
66
|
+
case arg
|
67
|
+
when Proc, true, false, nil
|
68
|
+
arg
|
69
|
+
when Symbol
|
70
|
+
lambda { |node| node.respond_to?(arg) && node.send(arg) }
|
71
|
+
when Class
|
72
|
+
lambda { |node| node.is_a? arg }
|
73
|
+
when Array
|
74
|
+
arg.all? { |a| a.is_a? Class } or raise ArgumentError, "Array elements should be classes"
|
75
|
+
lambda { |node| arg.any? { |a| node.is_a? a } }
|
76
|
+
else
|
77
|
+
raise ArgumentError
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
ALL = Filter.new
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
@@ -0,0 +1,202 @@
|
|
1
|
+
|
2
|
+
# TODO TODO TODO IDEA
|
3
|
+
#
|
4
|
+
# External trees
|
5
|
+
# node/branch/branches
|
6
|
+
# returns tree
|
7
|
+
#
|
8
|
+
# this/parent/children
|
9
|
+
# returns data field
|
10
|
+
#
|
11
|
+
# Internal trees
|
12
|
+
# Unifies node and this
|
13
|
+
#
|
14
|
+
# Both of internal and external trees have branch/branches relations.
|
15
|
+
#
|
16
|
+
# Internal trees adds a parent/children relation
|
17
|
+
#
|
18
|
+
#
|
19
|
+
#
|
20
|
+
# Only internal trees have parent/child relations. External trees have only branch/branches relations
|
21
|
+
|
22
|
+
module Tree
|
23
|
+
module Implementation
|
24
|
+
include NodeProperty
|
25
|
+
def initialize(_arg) end
|
26
|
+
end
|
27
|
+
|
28
|
+
module InternalImplementation
|
29
|
+
include Implementation
|
30
|
+
def node = self
|
31
|
+
def this = self
|
32
|
+
def value = self
|
33
|
+
end
|
34
|
+
|
35
|
+
module ParentImplementation
|
36
|
+
include ParentProperty
|
37
|
+
include StemProperty
|
38
|
+
include Implementation
|
39
|
+
end
|
40
|
+
|
41
|
+
module InternalParentImplementation
|
42
|
+
include InternalImplementation
|
43
|
+
include ParentImplementation
|
44
|
+
|
45
|
+
attr_reader :parent
|
46
|
+
alias_method :branch, :parent
|
47
|
+
|
48
|
+
def initialize(parent) = @parent = parent
|
49
|
+
|
50
|
+
# protected
|
51
|
+
attr_writer :parent
|
52
|
+
end
|
53
|
+
|
54
|
+
module ExternalParentImplementation
|
55
|
+
include ParentImplementation
|
56
|
+
end
|
57
|
+
|
58
|
+
module ChildrenImplementation
|
59
|
+
include ChildrenProperty
|
60
|
+
include BranchesProperty
|
61
|
+
include Implementation
|
62
|
+
# protected
|
63
|
+
def attach(child) = abstract_method
|
64
|
+
end
|
65
|
+
|
66
|
+
module ExternalChildrenArrayImplementation
|
67
|
+
include NodeProperty
|
68
|
+
include ChildrenImplementation
|
69
|
+
|
70
|
+
def node = array
|
71
|
+
def this = array.first
|
72
|
+
def value = array.first
|
73
|
+
|
74
|
+
attr_accessor :array
|
75
|
+
|
76
|
+
def children = @array.last.map(&:first)
|
77
|
+
def branches = Enumerator.new { |enum| each_branch { |branch| enum << branch } }
|
78
|
+
|
79
|
+
# FIXME: each_child/branch/etc. are actually map methods
|
80
|
+
def each_child(&block) = @array.last.map { |*node| yield(*node) }
|
81
|
+
# def each_child(&block) = array.second.each { |node| yield(*node, self) } # Actually possible
|
82
|
+
# def each_child(&block) = array.last.each(&:first)
|
83
|
+
|
84
|
+
def each_branch(&block)
|
85
|
+
block_given? or raise ArgumentError
|
86
|
+
# impl = self.class.new(nil)
|
87
|
+
@array.last.map { |node| yield self.class.new(node) }
|
88
|
+
end
|
89
|
+
|
90
|
+
def each_edge(&block)
|
91
|
+
@array.last.map { |node| yield self, self.class.new(node) }
|
92
|
+
end
|
93
|
+
|
94
|
+
def each(&block)
|
95
|
+
@array.last.map.with_index { |node, i| yield i, self.class.new(node) }
|
96
|
+
end
|
97
|
+
|
98
|
+
def each_node(&block)
|
99
|
+
@array.last.map.with_index { |node, i| yield self, i, self.class.new(node) }
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.new(array)
|
103
|
+
object = super(nil)
|
104
|
+
object.array = array
|
105
|
+
object
|
106
|
+
end
|
107
|
+
|
108
|
+
# protected
|
109
|
+
def attach(child) = array.last << child
|
110
|
+
end
|
111
|
+
|
112
|
+
module InternalChildrenImplementation
|
113
|
+
include InternalImplementation
|
114
|
+
include ChildrenImplementation
|
115
|
+
|
116
|
+
# def children = abstract_method # Repeated here because provide_module is not executed yet
|
117
|
+
# def each_child = abstract_method
|
118
|
+
|
119
|
+
# alias_method :branches, :children
|
120
|
+
# alias_method :each_branch, :each_child
|
121
|
+
|
122
|
+
def branches = children
|
123
|
+
def each_branch(&block) = each_child(&block)
|
124
|
+
|
125
|
+
def attach(child) = abstract_method
|
126
|
+
end
|
127
|
+
|
128
|
+
# Demonstrates a linked list implementation
|
129
|
+
module InternalChildrenListImplementation
|
130
|
+
include InternalChildrenImplementation
|
131
|
+
|
132
|
+
attr_reader :first_child
|
133
|
+
attr_reader :next_sibling
|
134
|
+
|
135
|
+
def children
|
136
|
+
n = self.first_child or return []
|
137
|
+
a = [n]
|
138
|
+
a << n while n = n.next_sibling
|
139
|
+
a
|
140
|
+
end
|
141
|
+
|
142
|
+
def each_child(&block)
|
143
|
+
curr = first_child or return
|
144
|
+
yield(curr)
|
145
|
+
yield curr while curr = next_sibling
|
146
|
+
end
|
147
|
+
|
148
|
+
def attach(child)
|
149
|
+
child.instance_variable_set(:@next_sibling, first_child)
|
150
|
+
@first_child = child
|
151
|
+
end
|
152
|
+
|
153
|
+
# protected
|
154
|
+
attr_writer :first_child
|
155
|
+
attr_writer :next_sibling
|
156
|
+
end
|
157
|
+
|
158
|
+
module InternalChildrenArrayImplementation
|
159
|
+
include InternalChildrenImplementation
|
160
|
+
|
161
|
+
attr_reader :children
|
162
|
+
|
163
|
+
def initialize(_parent)
|
164
|
+
@children = []
|
165
|
+
super
|
166
|
+
end
|
167
|
+
|
168
|
+
def each_child(&block) = @children.map(&block)
|
169
|
+
def attach(child) = @children << child
|
170
|
+
end
|
171
|
+
|
172
|
+
module InternalChildrenHashImplementation
|
173
|
+
include InternalChildrenImplementation
|
174
|
+
|
175
|
+
attr_reader :hash
|
176
|
+
|
177
|
+
def children = hash.values
|
178
|
+
|
179
|
+
def initialize
|
180
|
+
@hash = {}
|
181
|
+
super
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
module InternalParentChildImplementation
|
186
|
+
# include InternalParentImplementation
|
187
|
+
# include InternalChildrenImplementation
|
188
|
+
include ParentProperty
|
189
|
+
include ChildrenProperty
|
190
|
+
|
191
|
+
def initialize(parent)
|
192
|
+
super
|
193
|
+
parent&.attach(self)
|
194
|
+
end
|
195
|
+
|
196
|
+
def attach(child)
|
197
|
+
super(child)
|
198
|
+
child.send(:parent=, self)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Tree
|
2
|
+
# For internal trees with a global namespace for nodes
|
3
|
+
module Pool
|
4
|
+
include PathAlgorithms
|
5
|
+
|
6
|
+
def self.included(other)
|
7
|
+
other.extend(ClassMethods)
|
8
|
+
other.instance_variable_set(:@pool, {})
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(_parent)
|
13
|
+
super
|
14
|
+
self.class[self.uid] = self
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def key?(uid) = @pool.key?(uid)
|
19
|
+
def keys = @pool.keys
|
20
|
+
def nodes = @pool.values
|
21
|
+
def [](uid) = @pool[uid]
|
22
|
+
def []=(uid, node) @pool[uid] = node end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,53 @@
|
|
1
|
+
|
2
|
+
module Tree
|
3
|
+
# A child node is not necessarily a tree, branches are, but they have to use
|
4
|
+
# #node to get the data
|
5
|
+
#
|
6
|
+
# Internal trees have child == branch
|
7
|
+
|
8
|
+
module Property
|
9
|
+
def initialize(_arg) end
|
10
|
+
end
|
11
|
+
|
12
|
+
module NodeProperty
|
13
|
+
def node = abstract_method
|
14
|
+
|
15
|
+
# FIXME: What is this. Also describe all properties in this file
|
16
|
+
def value = abstract_method
|
17
|
+
end
|
18
|
+
|
19
|
+
module StemProperty # Aka. 'parent'
|
20
|
+
def stem = abstract_method
|
21
|
+
end
|
22
|
+
|
23
|
+
module BranchesProperty
|
24
|
+
def branches = abstract_method
|
25
|
+
def each_branch(&block) = branches.each(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
module ParentProperty
|
29
|
+
def parent = abstract_method
|
30
|
+
def parent=(arg) abstract_method end
|
31
|
+
end
|
32
|
+
|
33
|
+
module ChildrenProperty
|
34
|
+
def children = abstract_method
|
35
|
+
def each_child(&block) = children.each(&block)
|
36
|
+
def attach(child) = abstract_method
|
37
|
+
end
|
38
|
+
|
39
|
+
module KeyProperty # Set
|
40
|
+
def key = abstract_method
|
41
|
+
end
|
42
|
+
|
43
|
+
module KeysProperty # Map
|
44
|
+
def keys = abstract_method
|
45
|
+
def key?(k) = keys.include? k
|
46
|
+
def [](key) = abstract_method
|
47
|
+
end
|
48
|
+
|
49
|
+
module RootProperty
|
50
|
+
def root = abstract_method # @root ||= parent&.root
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Tree
|
2
|
+
module Separator
|
3
|
+
DEFAULT_SEPARATOR = "."
|
4
|
+
|
5
|
+
def self.included(other)
|
6
|
+
puts "including Separator"
|
7
|
+
super(other)
|
8
|
+
other.instance_variable_set(:@separator, DEFAULT_SEPARATOR)
|
9
|
+
other.extend(ClassMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
def separator
|
13
|
+
self.class.instance_variable_get(:@separator)
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
attr_accessor :separator
|
18
|
+
def split(s) = s.split /#{separator}/
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
module ModularTree
|
3
|
+
# Doesn't work atm. because DownTreeAlgorithms methods are implemented as
|
4
|
+
# members and not as class methods and perhaps because of other stuff
|
5
|
+
class TreeArray < AbstractTree
|
6
|
+
include DownTreeAlgorithms
|
7
|
+
|
8
|
+
attr_reader :array
|
9
|
+
|
10
|
+
def node = array.first
|
11
|
+
def children = array[1..-1]
|
12
|
+
|
13
|
+
def initialize(array)
|
14
|
+
@array = array
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.filter(*args) = DownTreeAlgorithms.filter(*args)
|
18
|
+
end
|
19
|
+
end
|
data/lib/modular_tree.rb
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'abstract_method_error'
|
2
|
+
|
3
|
+
require "indented_io"
|
4
|
+
|
5
|
+
require 'constrain'
|
6
|
+
include Constrain
|
7
|
+
|
8
|
+
require 'forward_to'
|
9
|
+
include ForwardTo
|
10
|
+
|
11
|
+
require_relative "modular_tree/version"
|
12
|
+
require_relative "modular_tree/separator"
|
13
|
+
require_relative "modular_tree/filter"
|
14
|
+
require_relative "modular_tree/pairs"
|
15
|
+
|
16
|
+
# Order is important here
|
17
|
+
require_relative "modular_tree/properties"
|
18
|
+
require_relative "modular_tree/implementations"
|
19
|
+
require_relative "modular_tree/algorithms"
|
20
|
+
|
21
|
+
# Should be after the above group
|
22
|
+
require_relative "modular_tree/pool"
|
23
|
+
|
24
|
+
module Tree
|
25
|
+
DEFAULT_SEPARATOR = "."
|
26
|
+
|
27
|
+
# TODO: Move to algorithms
|
28
|
+
@separator = nil
|
29
|
+
def Tree.separator = @separator ||= DEFAULT_SEPARATOR
|
30
|
+
def Tree.separator=(s) @separator = s end
|
31
|
+
|
32
|
+
class AbstractTree
|
33
|
+
def empty? = abstract_method
|
34
|
+
def size = abstract_method
|
35
|
+
end
|
36
|
+
|
37
|
+
# A regular tree
|
38
|
+
#
|
39
|
+
class ArrayTree < AbstractTree # Aka. SetTree aka. Tree
|
40
|
+
include InternalParentImplementation
|
41
|
+
include InternalChildrenArrayImplementation
|
42
|
+
include InternalParentChildImplementation
|
43
|
+
include UpTreeAlgorithms
|
44
|
+
include DownTreeAlgorithms
|
45
|
+
|
46
|
+
def self.filter(*args) = DownTreeAlgorithms.filter(*args)
|
47
|
+
end
|
48
|
+
|
49
|
+
class FilteredArrayTree < ArrayTree
|
50
|
+
include DownTreeFilteredAlgorithms
|
51
|
+
|
52
|
+
def self.filter(*args) = FilteredDownTreeAlgorithms.filter(*args)
|
53
|
+
end
|
54
|
+
|
55
|
+
# TODO: Hide
|
56
|
+
class NestedArrayTree < AbstractTree
|
57
|
+
include ExternalChildrenArrayImplementation
|
58
|
+
include DownTreeAlgorithms
|
59
|
+
|
60
|
+
def initialize(array)
|
61
|
+
super(nil)
|
62
|
+
self.array = array
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.filter(*args) = DownTreeAlgorithms.filter(*args)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Module level algorithms on nested array trees
|
69
|
+
#
|
70
|
+
def self.aggregate(arg, *args, &block)
|
71
|
+
case arg
|
72
|
+
when Array; NestedArrayTree.new(arg).aggregate(*args, &block)
|
73
|
+
else
|
74
|
+
raise ArgumentError
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.aggregate(arg, *args, &block) = class_of(arg).new(arg).aggregate(*args, &block)
|
79
|
+
|
80
|
+
# TODO: Hide
|
81
|
+
def self.class_of(arg)
|
82
|
+
constrain arg, Array
|
83
|
+
if arg.size == 2 && arg.last.is_a?(Array)
|
84
|
+
NestedArrayTree
|
85
|
+
else
|
86
|
+
raise "Oops"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# data =
|
91
|
+
# ["root", [
|
92
|
+
# ["a", [
|
93
|
+
# ["b", []],
|
94
|
+
# ["c", []]
|
95
|
+
# ]],
|
96
|
+
# ["d", [
|
97
|
+
# ["e", []],
|
98
|
+
# ]]
|
99
|
+
# ]]
|
100
|
+
#
|
101
|
+
# tree = NestedArrayTree.new(data)
|
102
|
+
# tree.visit { ... }
|
103
|
+
#
|
104
|
+
# tree.traverse { before(); yield; after() }
|
105
|
+
#
|
106
|
+
# NestedArrayTree.visit(data) { ... }
|
107
|
+
#
|
108
|
+
# NestedArrayTree.adapt(data)
|
109
|
+
# data.visit { ... }
|
110
|
+
#
|
111
|
+
|
112
|
+
|
113
|
+
# p Tree.ancestors
|
114
|
+
# exit
|
115
|
+
|
116
|
+
# module KeyedUpTreeAlgorithmsRoot
|
117
|
+
# include KeyedUpTreeAlgorithms
|
118
|
+
#
|
119
|
+
# def initialize
|
120
|
+
# super
|
121
|
+
# @pool = {}
|
122
|
+
#
|
123
|
+
# end
|
124
|
+
|
125
|
+
# class TreeAdapter < AbstractTree
|
126
|
+
# attr_reader :parent_method
|
127
|
+
# attr_reader :children_method
|
128
|
+
# def parent = self.send(parent_method)
|
129
|
+
# def children = self.send(children_method)
|
130
|
+
#
|
131
|
+
# def initialize(root, parent_method, children_method)
|
132
|
+
# @parent_method = parent_method
|
133
|
+
# @children_method = children_method
|
134
|
+
# end
|
135
|
+
# end
|
136
|
+
end
|
137
|
+
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/modular_tree/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "modular_tree"
|
7
|
+
spec.version = Tree::VERSION
|
8
|
+
spec.authors = ["Claus Rasmussen"]
|
9
|
+
spec.email = ["claus.l.rasmussen@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "Gem modular_tree"
|
12
|
+
spec.description = "Gem modular_tree"
|
13
|
+
spec.homepage = "http://www.nowhere.com/"
|
14
|
+
spec.required_ruby_version = ">= 2.6.0"
|
15
|
+
|
16
|
+
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
18
|
+
|
19
|
+
# Specify which files should be added to the gem when it is released.
|
20
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
21
|
+
spec.files = Dir.chdir(__dir__) do
|
22
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
23
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
24
|
+
end
|
25
|
+
end
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
# Uncomment to register a new dependency of your gem
|
31
|
+
# spec.add_dependency "example-gem", "~> 1.0"
|
32
|
+
|
33
|
+
# For more information and examples about making a new gem, check out our
|
34
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
35
|
+
|
36
|
+
# Add your production dependencies here
|
37
|
+
# spec.add_dependency GEM [, VERSION]
|
38
|
+
|
39
|
+
# Add your development dependencies here
|
40
|
+
# spec.add_development_dependency GEM [, VERSION]
|
41
|
+
|
42
|
+
# Also un-comment in spec/spec_helper to use simplecov
|
43
|
+
# spec.add_development_dependency "simplecov"
|
44
|
+
|
45
|
+
spec.add_dependency 'constrain'
|
46
|
+
spec.add_dependency 'forward_to'
|
47
|
+
spec.add_dependency 'abstract_method_error'
|
48
|
+
spec.add_development_dependency 'indented_io'
|
49
|
+
end
|
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: modular_tree
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Claus Rasmussen
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-12-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: constrain
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: forward_to
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: abstract_method_error
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: indented_io
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Gem modular_tree
|
70
|
+
email:
|
71
|
+
- claus.l.rasmussen@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".rspec"
|
77
|
+
- ".ruby-version"
|
78
|
+
- Gemfile
|
79
|
+
- README.md
|
80
|
+
- Rakefile
|
81
|
+
- lib/modular_tree.rb
|
82
|
+
- lib/modular_tree/algorithms.rb
|
83
|
+
- lib/modular_tree/filter.rb
|
84
|
+
- lib/modular_tree/implementations.rb
|
85
|
+
- lib/modular_tree/pairs.rb
|
86
|
+
- lib/modular_tree/pool.rb
|
87
|
+
- lib/modular_tree/properties.rb
|
88
|
+
- lib/modular_tree/separator.rb
|
89
|
+
- lib/modular_tree/tree_array.rb
|
90
|
+
- lib/modular_tree/version.rb
|
91
|
+
- modular_tree.gemspec
|
92
|
+
- sig/modular_tree.rbs
|
93
|
+
homepage: http://www.nowhere.com/
|
94
|
+
licenses: []
|
95
|
+
metadata:
|
96
|
+
homepage_uri: http://www.nowhere.com/
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: 2.6.0
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
requirements: []
|
112
|
+
rubygems_version: 3.3.18
|
113
|
+
signing_key:
|
114
|
+
specification_version: 4
|
115
|
+
summary: Gem modular_tree
|
116
|
+
test_files: []
|