modular_tree 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1b145f3b7daeb0bddc2a2415e3a98d6d6232d95afeca8147908bffa2089ff412
4
- data.tar.gz: 68271887a655b0196e5f9819f72ee80ca9e853c15568ab48cc1d7e15c4a29d22
3
+ metadata.gz: a3644f92428a2febb9c5eae96e14a4fb560f4661bf7b16234779dfa00f56395c
4
+ data.tar.gz: 527392507133a17f32e157e26f6ab670998d157858efbc8e7b31dbc892ab0128
5
5
  SHA512:
6
- metadata.gz: b9a39254ef102c23dbba9445b5b95b3d925d0954ef28de91107318c24ce9c450dd1fc2612cae18fc2c4033314bcd85b14a7126eb09e01daee076e380b05fe54c
7
- data.tar.gz: 3b84c591c7816199835041074b6fb8b9506c5b85b438000045ce4d21627559835edeadf0336ca6635d76cef563082254318a1cf13d15342c431f407f51868615
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 (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, :do_each_preorder, this, &block)
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, :value, :each_postorder, this)
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
- # def pairs(filter, filter, &block)
101
- # end
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
- # Stops further recursion if the block returns truthy
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
- def do_propagate(filter, this, key, parent, &block)
196
- select, traverse = filter.match(self)
197
- if select && this
198
- return if yield(self, key, parent)
199
- end
200
- if !this || traverse
201
- each_branch { |branch, key| branch.do_propagate(filter, key, self.value) }
202
- end
203
- end
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.value, values)
252
+ yield(self.node_value, values)
218
253
  end
219
-
220
254
  end
221
255
 
222
256
  module DownTreeAlgorithms
@@ -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 Proc, Symbol, Class, or an array of classes. In
10
- # addition, +select+ can be true, and +traverse+ can be true, false, or
11
- # nil. These values have special meanings:
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 return a two-tuple of booleans. Can't be
20
- # used when +select+ is true. TODO: Explain
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 the expression is a class or an array of classes, a given node matches
30
- # if it is an instance of one of the classes or any subclass
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
- # TODO: block argument
37
- #
38
- def initialize(select_expr = true, traverse_expr = true, &block)
39
- constrain select_expr, Proc, Symbol, Class, [Class], true
40
- constrain traverse_expr, Proc, Symbol, Class, [Class], true, false, nil
41
- select = mk_lambda(select_expr)
42
- traverse = mk_lambda(traverse_expr)
43
- @matcher =
44
- case select
45
- when Proc
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 value = self
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 value = array.first # FIXME What is this? It is a problem several places
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
+
@@ -13,7 +13,7 @@ module Tree
13
13
  def node = abstract_method
14
14
 
15
15
  # FIXME: What is this. Also describe all properties in this file
16
- def value = abstract_method
16
+ def node_value = abstract_method
17
17
  end
18
18
 
19
19
  module StemProperty # Aka. 'parent'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tree
4
- VERSION = "0.0.3"
4
+ VERSION = "0.1.0"
5
5
  end
data/lib/modular_tree.rb CHANGED
@@ -10,6 +10,7 @@ include ForwardTo
10
10
 
11
11
  require_relative "modular_tree/version"
12
12
  require_relative "modular_tree/separator"
13
+ require_relative "modular_tree/matcher"
13
14
  require_relative "modular_tree/filter"
14
15
  require_relative "modular_tree/pairs"
15
16
 
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.3
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-04 00:00:00.000000000 Z
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