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 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