modular_tree 0.0.3 → 0.2.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: 420d72c774f0602ebf2b50b7346ee64759aaf48f89b8e041c80fe76464dbd379
4
+ data.tar.gz: 4ceb72733d99138a6244c4b163b46d0fb10fcdf20b8f7d61e582903bab750bc8
5
5
  SHA512:
6
- metadata.gz: b9a39254ef102c23dbba9445b5b95b3d925d0954ef28de91107318c24ce9c450dd1fc2612cae18fc2c4033314bcd85b14a7126eb09e01daee076e380b05fe54c
7
- data.tar.gz: 3b84c591c7816199835041074b6fb8b9506c5b85b438000045ce4d21627559835edeadf0336ca6635d76cef563082254318a1cf13d15342c431f407f51868615
6
+ metadata.gz: a5b2b90bcaaa71fd2754093cbd3ccca1fea3c9193c3261a82911bd10c7c8c6e74476804006238215aaf448062424dbfeef606550b69f378fd742d159d58b05b2
7
+ data.tar.gz: 8330f74bf4368cc7d56f35b42e48ff4f1dd51641ed94f05f4340f222eef22d62d0a3b32c883e2e6d916b78762a3d23870e62131b3e2f461c10ccabf16070d6ce
@@ -49,8 +49,9 @@ module Tree
49
49
  include NodeProperty
50
50
  include BranchesProperty
51
51
 
52
- # True if the node doesn't contain any branches (#empty? for trees)
53
- def bare? = raise NotImplemented
52
+ # True if the node doesn't contain any branches (#empty? for trees).
53
+ # Default implementation in BranchesProperty
54
+ # def bare? = raise NotImplemented
54
55
 
55
56
  # The number of nodes in the tree. Note that this can be an expensive
56
57
  # operation because every node has to be visited
@@ -61,9 +62,10 @@ module Tree
61
62
  def descendants(*filter) = each(filter, this: false)
62
63
 
63
64
  # 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)
65
+ # called with value, key, and parent as arguments but it may choose to
66
+ # ignore the key and/or parent argument. Returns an enumerator of values
67
+ # without a block
68
+ def each(*filter, this: true, &block) = common_each(*filter, :node_value, :do_each_preorder, this, &block)
67
69
 
68
70
  # Implementation of Enumerable#select extended with a single filter. As
69
71
  # #each, the block is called with value, key, and parent arguments
@@ -83,22 +85,49 @@ module Tree
83
85
  def preorder(*filter, this: true) = each(*filter, this: this)
84
86
 
85
87
  # Post-order enumerator of selected nodes
86
- def postorder(*filter, this: true) = common_each(*filter, :value, :each_postorder, this)
88
+ def postorder(*filter, this: true) = common_each(*filter, :node_value, :each_postorder, this)
87
89
 
88
90
  # Enumerator of edges in the tree. Edges are [previous-matching-node,
89
91
  # matching-node] tuples. Top-level nodes have previous-matching-node set to
90
92
  # nil
91
93
  #
94
+ # FIXME: Not working right now. Not sure how #edges relates to #pairs
92
95
  def edges(*filter, this: true, &block)
93
- if block_given
96
+ if block_given?
94
97
  each(*filter, this: this) { |node, _, parent| yield parent, node }
95
98
  else
96
99
  Pairs.new { |enum| each(*filter, this: this) { |node, _, parent| enum << [parent, node] } }
97
100
  end
98
101
  end
99
102
 
100
- # def pairs(filter, filter, &block)
101
- # end
103
+ # Find nodes matching +filter+ and call +traverse_block+ with a node and a
104
+ # block argument. +traverse_block+ can recurse into children by calling the
105
+ # supplied inner block
106
+ #
107
+ # An example of how to create a nested array implementation:
108
+ #
109
+ # root.traverse(true) { |node, inner| [node, inner.call(node)] }
110
+ #
111
+ def traverse(*filter, this: true, &traverse_block)
112
+ filter = self.class.filter(*filter)
113
+ inner_block_proxy = [nil] # Hack because you can't refer to a lambda within its declaration
114
+ inner_block = inner_block_proxy[0] = lambda { |node|
115
+ node.nodes(filter, this: false).map { |n| traverse_block.call(n, inner_block_proxy.first) }
116
+ }
117
+ nodes(filter, this: this).map { |node| traverse_block.call(node, inner_block) }
118
+ end
119
+
120
+ # Return array of [previous-matching-node, matching-node] tuples
121
+ def pairs(first_matcher_expr, second_matcher_expr, this: true)
122
+ first_matcher = Matcher.new(first_matcher_expr)
123
+ second_matcher = Matcher.new(second_matcher_expr)
124
+ or_matcher = first_matcher | second_matcher # avoids re-computing this value over and over
125
+ result = []
126
+ nodes(first_matcher, false, this: this) { |node|
127
+ node.do_pairs(result, first_matcher, or_matcher)
128
+ }
129
+ result
130
+ end
102
131
 
103
132
  # Traverse the tree top-down while accumulating information in an
104
133
  # accumulator object. The block takes a [accumulator, node] tuple and is
@@ -126,10 +155,7 @@ module Tree
126
155
  do_aggregate(filter, this, &block)
127
156
  end
128
157
 
129
- # Stops further recursion if the block returns truthy
130
- def propagate(*filter, this: true, &block)
131
-
132
- end
158
+ # def propagate(*filter, this: true, &block) raise NotImplemented end
133
159
 
134
160
  # tree.each(DocumentNode).select(BriefNode).each { |doc, brief| ... }
135
161
  # tree.edges(DocumentNode, BriefNode).group.all? { |doc, briefs| briefs.size <= 1 }
@@ -142,7 +168,7 @@ module Tree
142
168
  args.size == 1 or raise ArgumentError
143
169
  args.first
144
170
  else
145
- Filter.new(*args)
171
+ Filter.new(*args)
146
172
  end
147
173
  end
148
174
 
@@ -156,6 +182,16 @@ module Tree
156
182
  end
157
183
  end
158
184
 
185
+ def do_pairs(acc, first_matcher, or_matcher) # self is matched by first_matcher
186
+ nodes(or_matcher, false, this: false) { |node|
187
+ if first_matcher.match?(node)
188
+ node.do_pairs(acc, first_matcher, or_matcher)
189
+ else
190
+ acc << [self, node]
191
+ end
192
+ }
193
+ end
194
+
159
195
  # TODO: Split into automatically generated variants
160
196
  def do_each_preorder(enum, filter, value_method, key, parent, this, &block)
161
197
  select, traverse = filter.match(self)
@@ -192,15 +228,15 @@ module Tree
192
228
  end
193
229
  end
194
230
 
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
231
+ # def do_propagate(filter, this, key, parent, &block)
232
+ # select, traverse = filter.match(self)
233
+ # if select && this
234
+ # return if yield(self, key, parent)
235
+ # end
236
+ # if !this || traverse
237
+ # each_branch { |branch, key| branch.do_propagate(filter, key, self.value) }
238
+ # end
239
+ # end
204
240
 
205
241
  def do_accumulate(filter, this, acc, &block)
206
242
  select, traverse = filter.match(self)
@@ -214,9 +250,8 @@ module Tree
214
250
  values = traverse ? each_branch { |branch|
215
251
  r = branch.do_aggregate(filter, true, &block)
216
252
  }.to_a : []
217
- yield(self.value, values)
253
+ yield(self.node_value, values)
218
254
  end
219
-
220
255
  end
221
256
 
222
257
  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
 
@@ -168,7 +168,10 @@ module Tree
168
168
  def each_child(&block) = @children.map(&block)
169
169
  def attach(child) = @children << child
170
170
 
171
- # Can be used with any array implementation
171
+ # Can be used with any array implementation. +where+ is either an Integer
172
+ # index or an object
173
+ #
174
+ # TODO: Rename #attach. #insert & #append are Enumerable operations
172
175
  def insert(where, child) = insert_append(:insert, where, child)
173
176
  def append(where, child) = insert_append(:append, where, child)
174
177
 
@@ -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'
@@ -22,6 +22,7 @@ module Tree
22
22
 
23
23
  module BranchesProperty
24
24
  def branches = abstract_method
25
+ def bare? = branches.empty?
25
26
  def each_branch(&block) = branches.each(&block)
26
27
  end
27
28
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tree
4
- VERSION = "0.0.3"
4
+ VERSION = "0.2.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.2.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-15 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