modular_tree 0.0.3 → 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 +4 -4
- data/lib/modular_tree/algorithms.rb +57 -23
- data/lib/modular_tree/filter.rb +27 -51
- data/lib/modular_tree/implementations.rb +2 -2
- data/lib/modular_tree/matcher.rb +64 -0
- data/lib/modular_tree/properties.rb +1 -1
- data/lib/modular_tree/version.rb +1 -1
- data/lib/modular_tree.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a3644f92428a2febb9c5eae96e14a4fb560f4661bf7b16234779dfa00f56395c
|
4
|
+
data.tar.gz: 527392507133a17f32e157e26f6ab670998d157858efbc8e7b31dbc892ab0128
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 339ac6d0bcdb76f76234db24f166755bbf3ab5760acf9288a790f1db047f36ad9202aad55af4304b1e72abacd1a18d106d0e046de1671821fd619df9857fdbec
|
7
|
+
data.tar.gz: b2953433442cfd6eb6beba7ab2fb6940d1031b8b13bf6ee0ce7eece775895c69ed059573b931c05738627089717c5aed4423f872c9c4529ff9cfb3efd46c4f37
|
@@ -61,9 +61,10 @@ module Tree
|
|
61
61
|
def descendants(*filter) = each(filter, this: false)
|
62
62
|
|
63
63
|
# Implementation of Enumerable#each extended with filters. The block is
|
64
|
-
# called with value, key, and parent as arguments
|
65
|
-
# ignore key and/or parent
|
66
|
-
|
64
|
+
# called with value, key, and parent as arguments but it may choose to
|
65
|
+
# ignore the key and/or parent argument. Returns an enumerator of values
|
66
|
+
# without a block
|
67
|
+
def each(*filter, this: true, &block) = common_each(*filter, :node_value, :do_each_preorder, this, &block)
|
67
68
|
|
68
69
|
# Implementation of Enumerable#select extended with a single filter. As
|
69
70
|
# #each, the block is called with value, key, and parent arguments
|
@@ -83,22 +84,49 @@ module Tree
|
|
83
84
|
def preorder(*filter, this: true) = each(*filter, this: this)
|
84
85
|
|
85
86
|
# Post-order enumerator of selected nodes
|
86
|
-
def postorder(*filter, this: true) = common_each(*filter, :
|
87
|
+
def postorder(*filter, this: true) = common_each(*filter, :node_value, :each_postorder, this)
|
87
88
|
|
88
89
|
# Enumerator of edges in the tree. Edges are [previous-matching-node,
|
89
90
|
# matching-node] tuples. Top-level nodes have previous-matching-node set to
|
90
91
|
# nil
|
91
92
|
#
|
93
|
+
# FIXME: Not working right now. Not sure how #edges relates to #pairs
|
92
94
|
def edges(*filter, this: true, &block)
|
93
|
-
if block_given
|
95
|
+
if block_given?
|
94
96
|
each(*filter, this: this) { |node, _, parent| yield parent, node }
|
95
97
|
else
|
96
98
|
Pairs.new { |enum| each(*filter, this: this) { |node, _, parent| enum << [parent, node] } }
|
97
99
|
end
|
98
100
|
end
|
99
101
|
|
100
|
-
#
|
101
|
-
#
|
102
|
+
# Find nodes matching +filter+ and call +traverse_block+ with a node and a
|
103
|
+
# block argument. +traverse_block+ can recurse into children by calling the
|
104
|
+
# supplied inner block
|
105
|
+
#
|
106
|
+
# An example of how to create a nested array implementation:
|
107
|
+
#
|
108
|
+
# root.traverse(true) { |node, inner| [node, inner.call(node)] }
|
109
|
+
#
|
110
|
+
def traverse(*filter, this: true, &traverse_block)
|
111
|
+
filter = self.class.filter(*filter)
|
112
|
+
inner_block_proxy = [nil] # Hack because you can't refer to a lambda within its declaration
|
113
|
+
inner_block = inner_block_proxy[0] = lambda { |node|
|
114
|
+
node.nodes(filter, this: false).map { |n| traverse_block.call(n, inner_block_proxy.first) }
|
115
|
+
}
|
116
|
+
nodes(filter, this: this).map { |node| traverse_block.call(node, inner_block) }
|
117
|
+
end
|
118
|
+
|
119
|
+
# Return array of [previous-matching-node, matching-node] tuples
|
120
|
+
def pairs(first_matcher_expr, second_matcher_expr, this: true)
|
121
|
+
first_matcher = Matcher.new(first_matcher_expr)
|
122
|
+
second_matcher = Matcher.new(second_matcher_expr)
|
123
|
+
or_matcher = first_matcher | second_matcher # avoids re-computing this value over and over
|
124
|
+
result = []
|
125
|
+
nodes(first_matcher, false, this: this) { |node|
|
126
|
+
node.do_pairs(result, first_matcher, or_matcher)
|
127
|
+
}
|
128
|
+
result
|
129
|
+
end
|
102
130
|
|
103
131
|
# Traverse the tree top-down while accumulating information in an
|
104
132
|
# accumulator object. The block takes a [accumulator, node] tuple and is
|
@@ -126,10 +154,7 @@ module Tree
|
|
126
154
|
do_aggregate(filter, this, &block)
|
127
155
|
end
|
128
156
|
|
129
|
-
|
130
|
-
def propagate(*filter, this: true, &block)
|
131
|
-
|
132
|
-
end
|
157
|
+
# def propagate(*filter, this: true, &block) raise NotImplemented end
|
133
158
|
|
134
159
|
# tree.each(DocumentNode).select(BriefNode).each { |doc, brief| ... }
|
135
160
|
# tree.edges(DocumentNode, BriefNode).group.all? { |doc, briefs| briefs.size <= 1 }
|
@@ -142,7 +167,7 @@ module Tree
|
|
142
167
|
args.size == 1 or raise ArgumentError
|
143
168
|
args.first
|
144
169
|
else
|
145
|
-
Filter.new(*args)
|
170
|
+
Filter.new(*args)
|
146
171
|
end
|
147
172
|
end
|
148
173
|
|
@@ -156,6 +181,16 @@ module Tree
|
|
156
181
|
end
|
157
182
|
end
|
158
183
|
|
184
|
+
def do_pairs(acc, first_matcher, or_matcher) # self is matched by first_matcher
|
185
|
+
nodes(or_matcher, false, this: false) { |node|
|
186
|
+
if first_matcher.match?(node)
|
187
|
+
node.do_pairs(acc, first_matcher, or_matcher)
|
188
|
+
else
|
189
|
+
acc << [self, node]
|
190
|
+
end
|
191
|
+
}
|
192
|
+
end
|
193
|
+
|
159
194
|
# TODO: Split into automatically generated variants
|
160
195
|
def do_each_preorder(enum, filter, value_method, key, parent, this, &block)
|
161
196
|
select, traverse = filter.match(self)
|
@@ -192,15 +227,15 @@ module Tree
|
|
192
227
|
end
|
193
228
|
end
|
194
229
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
230
|
+
# def do_propagate(filter, this, key, parent, &block)
|
231
|
+
# select, traverse = filter.match(self)
|
232
|
+
# if select && this
|
233
|
+
# return if yield(self, key, parent)
|
234
|
+
# end
|
235
|
+
# if !this || traverse
|
236
|
+
# each_branch { |branch, key| branch.do_propagate(filter, key, self.value) }
|
237
|
+
# end
|
238
|
+
# end
|
204
239
|
|
205
240
|
def do_accumulate(filter, this, acc, &block)
|
206
241
|
select, traverse = filter.match(self)
|
@@ -214,9 +249,8 @@ module Tree
|
|
214
249
|
values = traverse ? each_branch { |branch|
|
215
250
|
r = branch.do_aggregate(filter, true, &block)
|
216
251
|
}.to_a : []
|
217
|
-
yield(self.
|
252
|
+
yield(self.node_value, values)
|
218
253
|
end
|
219
|
-
|
220
254
|
end
|
221
255
|
|
222
256
|
module DownTreeAlgorithms
|
data/lib/modular_tree/filter.rb
CHANGED
@@ -1,86 +1,62 @@
|
|
1
1
|
module Tree
|
2
2
|
class Filter
|
3
|
+
# :call-seq:
|
4
|
+
# Filter.new(select_expr, traverse_expr)
|
5
|
+
# Filter.new(select_expr, &block)
|
6
|
+
# Filter.new(&block)
|
7
|
+
#
|
3
8
|
# Create a node filter. The filter is initialized by a select expression
|
4
9
|
# and a traverse expression. The select expression decides if the node
|
5
10
|
# should be given to the block (or submitted to an enumerator) and the
|
6
11
|
# traverse expression decides if the child nodes should be traversed
|
7
12
|
# recursively
|
8
13
|
#
|
9
|
-
# The expressions can be a
|
10
|
-
#
|
11
|
-
# nil. These values have
|
14
|
+
# The expressions can be a Matcher object or one of Matcher's initializers:
|
15
|
+
# Proc, Symbol, Class, or an array of classes. In addition, +select+ can be
|
16
|
+
# true, and +traverse+ can be true, false, or nil. These values have
|
17
|
+
# special meanings:
|
12
18
|
#
|
13
19
|
# when +select+ is
|
14
20
|
# true Select always. This is the default
|
21
|
+
# false This is an allowed value but it doesn't select any node
|
15
22
|
#
|
16
23
|
# when +traverse+ is
|
17
24
|
# true Traverse always. This is the default
|
18
25
|
# false Traverse only if select didn't match
|
19
|
-
# nil Expects +select+ to
|
20
|
-
#
|
26
|
+
# nil Expects +select+ to be a Proc object that returns a [select,
|
27
|
+
# traverse] tuple of booleans
|
21
28
|
#
|
22
29
|
# If the expression is a Proc object, it will be called with the current
|
23
30
|
# node as argument. If the return value is true, the node is
|
24
31
|
# selected/traversed and skipped otherwise. If the expression is a method
|
25
32
|
# name (Symbol), the method will be called on each node with no arguments.
|
26
33
|
# It is not an error if the method doesn't exists on the a given node but
|
27
|
-
# the node is not selected/traversed
|
34
|
+
# the node is not selected/traversed. If the expression is a class or an
|
35
|
+
# array of classes, a given node matches if it is an instance of one of the
|
36
|
+
# classes or any subclass
|
28
37
|
#
|
29
|
-
# If
|
30
|
-
#
|
38
|
+
# If a block is given, it is supposed to return a [select, traverse] tuple
|
39
|
+
# of booleans
|
31
40
|
#
|
32
41
|
# Filters should not have side-effects because they can be used in
|
33
42
|
# enumerators that doesn't execute the filter unless the enumerator is
|
34
43
|
# evaluated
|
35
44
|
#
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
case traverse
|
47
|
-
when Proc; lambda { |node| [select.call(node), traverse.call(node)] }
|
48
|
-
when true; lambda { |node| [select.call(node), true] }
|
49
|
-
when false; lambda { |node| r = select.call(node); [r, !r] }
|
50
|
-
when nil; lambda { |node| select.call(node) }
|
51
|
-
end
|
52
|
-
when true
|
53
|
-
case traverse
|
54
|
-
when Proc; lambda { |node| [true, traverse.call(node)] }
|
55
|
-
when true; lambda { |_| [true, true] }
|
56
|
-
when false; lambda { |_| [true, false] } # effectively same as #children.each
|
57
|
-
when nil; raise ArgumentError
|
58
|
-
end
|
59
|
-
end
|
45
|
+
def initialize(select_expr = nil, traverse_expr = nil, &block)
|
46
|
+
if select_expr.nil? && block_given?
|
47
|
+
@matcher = block
|
48
|
+
else
|
49
|
+
select_expr = true if select_expr.nil?
|
50
|
+
select = Matcher.new(select_expr)
|
51
|
+
traverse_expr = (block_given? ? block : true) if traverse_expr.nil?
|
52
|
+
traverse = traverse_expr ? Matcher.new(traverse_expr) : !select
|
53
|
+
@matcher = lambda { |node| [select.match?(node), traverse.match?(node)] }
|
54
|
+
end
|
60
55
|
end
|
61
56
|
|
62
57
|
# Match +node+ against the filter and return a [select, traverse] tuple of booleans
|
63
58
|
def match(node) = @matcher.call(node)
|
64
59
|
|
65
|
-
# Create a proc if arg is a Symbol or an Array of classes. Pass through
|
66
|
-
# Proc objects, true, false, and nil
|
67
|
-
def mk_lambda(arg) = self.class.mk_lambda(arg)
|
68
|
-
def self.mk_lambda(arg)
|
69
|
-
case arg
|
70
|
-
when Proc, true, false, nil
|
71
|
-
arg
|
72
|
-
when Symbol
|
73
|
-
lambda { |node| node.respond_to?(arg) && node.send(arg) }
|
74
|
-
when Class
|
75
|
-
lambda { |node| node.is_a? arg }
|
76
|
-
when Array
|
77
|
-
arg.all? { |a| a.is_a? Class } or raise ArgumentError, "Array elements should be classes"
|
78
|
-
lambda { |node| arg.any? { |a| node.is_a? a } }
|
79
|
-
else
|
80
|
-
raise ArgumentError
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
60
|
ALL = Filter.new
|
85
61
|
end
|
86
62
|
end
|
@@ -29,7 +29,7 @@ module Tree
|
|
29
29
|
include Implementation
|
30
30
|
def node = self
|
31
31
|
def this = self
|
32
|
-
def
|
32
|
+
def node_value = self
|
33
33
|
end
|
34
34
|
|
35
35
|
module ParentImplementation
|
@@ -69,7 +69,7 @@ module Tree
|
|
69
69
|
|
70
70
|
def node = array
|
71
71
|
def this = array.first
|
72
|
-
def
|
72
|
+
def node_value = array.first # FIXME What is this? It is a problem several places
|
73
73
|
|
74
74
|
attr_accessor :array
|
75
75
|
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Tree
|
2
|
+
class AbstractMatcher
|
3
|
+
def match(node) = abstract_method
|
4
|
+
|
5
|
+
def |(other) = MatchExpression::OrExpr.new(self, other)
|
6
|
+
def &(other) = MatchExpression::AndExpr.new(self, other)
|
7
|
+
def !() = MatchExpression::NegationExpr.new(self)
|
8
|
+
end
|
9
|
+
|
10
|
+
class Matcher < AbstractMatcher
|
11
|
+
def match(node) = match?(node)
|
12
|
+
|
13
|
+
def match?(node)
|
14
|
+
case @expr
|
15
|
+
when Proc
|
16
|
+
@expr.call(node)
|
17
|
+
when Symbol
|
18
|
+
node.respond_to?(@expr) && node.send(@expr)
|
19
|
+
when Class
|
20
|
+
node.is_a? @expr
|
21
|
+
when Array
|
22
|
+
@expr.any? { |klass| node.is_a? klass }
|
23
|
+
when true, false
|
24
|
+
@expr
|
25
|
+
else
|
26
|
+
raise ArgumentError, @expr.inspect
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(expr = nil, &block)
|
31
|
+
constrain expr, Proc, Symbol, Class, [Class], true, false, nil
|
32
|
+
expr.nil? == block_given? or raise ArgumentError
|
33
|
+
@expr = (expr.nil? ? block : expr)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.new(expr = nil, *args, &block)
|
37
|
+
expr.is_a?(AbstractMatcher) ? expr : super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module MatchExpression
|
42
|
+
class BinaryExpr < AbstractMatcher
|
43
|
+
def initialize(left, right)
|
44
|
+
constrain left, Matcher
|
45
|
+
constrain right, Matcher
|
46
|
+
@left, @right = left, right
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class AndExpr < BinaryExpr
|
51
|
+
def match?(node) = @left.match?(node) && @right.match?(node)
|
52
|
+
end
|
53
|
+
|
54
|
+
class OrExpr < BinaryExpr
|
55
|
+
def match?(node) = @left.match?(node) || @right.match?(node) ? true : false
|
56
|
+
end
|
57
|
+
|
58
|
+
class NegationExpr < AbstractMatcher
|
59
|
+
def initialize(matcher) = @matcher = matcher
|
60
|
+
def match?(node) = !@matcher.match?(node)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
data/lib/modular_tree/version.rb
CHANGED
data/lib/modular_tree.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: modular_tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Claus Rasmussen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-12-
|
11
|
+
date: 2022-12-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: constrain
|
@@ -82,6 +82,7 @@ files:
|
|
82
82
|
- lib/modular_tree/algorithms.rb
|
83
83
|
- lib/modular_tree/filter.rb
|
84
84
|
- lib/modular_tree/implementations.rb
|
85
|
+
- lib/modular_tree/matcher.rb
|
85
86
|
- lib/modular_tree/pairs.rb
|
86
87
|
- lib/modular_tree/pool.rb
|
87
88
|
- lib/modular_tree/properties.rb
|