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 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
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-3.1.2
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in modular_tree.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -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,14 @@
1
+ module Tree
2
+ class Pairs < Enumerator
3
+ def group
4
+ h = {}
5
+ each { |first, last| (h[first] ||= []) << last }
6
+ h.each
7
+ end
8
+
9
+ # Turn into a nested array tree
10
+ def fold = abstract_method
11
+
12
+ def to_h = abstract_method
13
+ end
14
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tree
4
+ VERSION = "0.0.1"
5
+ end
@@ -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
@@ -0,0 +1,4 @@
1
+ module ModularTree
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ 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: []