modular_tree 0.0.2 → 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: ab7be4bf786460addacf0cabad30d5b8c0f5a0aa00900e894f933fd6f53230be
4
- data.tar.gz: 6b9c53ba9e2edccaa298bf1696e466dccfec7262c0cb5190bd82e1cf3055d60f
3
+ metadata.gz: a3644f92428a2febb9c5eae96e14a4fb560f4661bf7b16234779dfa00f56395c
4
+ data.tar.gz: 527392507133a17f32e157e26f6ab670998d157858efbc8e7b31dbc892ab0128
5
5
  SHA512:
6
- metadata.gz: 471dc9d79129be7692a3442a72ad03da18203461a7741a3815d2cb71a69c93ae68c5af787769cee2c6af14a8d787b5a1867551d681b8a88826be05227c47b2d1
7
- data.tar.gz: 1b676cc1284e7bd48611da0cf99912262ca7f9eb4cfd8edd801e4f1ecbd5ec73941bd8cfebd069242024f71993302aa1766ef44cc38278fd0659d5791c2bec8b
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
 
@@ -167,6 +167,33 @@ module Tree
167
167
 
168
168
  def each_child(&block) = @children.map(&block)
169
169
  def attach(child) = @children << child
170
+
171
+ # Can be used with any array implementation
172
+ def insert(where, child) = insert_append(:insert, where, child)
173
+ def append(where, child) = insert_append(:append, where, child)
174
+
175
+ def replace(where, *children)
176
+ children = Array(children).flatten
177
+ case where
178
+ when Integer
179
+ subject = @children[where] or raise ArgumentError
180
+ index = where
181
+ else
182
+ subject = where
183
+ index = @children.index(where) or raise ArgumentError
184
+ end
185
+ @children = @children[0...index] + children + @children[index + 1..-1]
186
+ subject
187
+ end
188
+
189
+ protected
190
+ def insert_append(which, where, child)
191
+ if !where.is_a?(Integer)
192
+ where = @children.index(where) or raise ArgumentError, "Can't find object"
193
+ end
194
+ where += 1 if which == :append
195
+ @children.insert(where, child)
196
+ end
170
197
  end
171
198
 
172
199
  module InternalChildrenHashImplementation
@@ -198,5 +225,28 @@ module Tree
198
225
  child.send(:parent=, self)
199
226
  end
200
227
  end
228
+
229
+ module InternalParentChildArrayImplementation
230
+ include InternalParentChildImplementation
231
+
232
+ def insert(where, child)
233
+ super
234
+ child.send(:parent=, self)
235
+ end
236
+
237
+ def append(where, child)
238
+ super
239
+ child.send(:parent=, self)
240
+ end
241
+
242
+ # Requires that Child classes already has defined this
243
+ def replace(where, *children)
244
+ children = Array(children).flatten
245
+ subject = super(where, children)
246
+ subject.send(:parent=, nil)
247
+ children.each { |child| child.send(:parent=, self) }
248
+ subject
249
+ end
250
+ end
201
251
  end
202
252
 
@@ -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'
@@ -36,6 +36,10 @@ module Tree
36
36
  def attach(child) = abstract_method
37
37
  end
38
38
 
39
+ # TODO
40
+ module ParentChildProperty
41
+ end
42
+
39
43
  module KeyProperty # Set
40
44
  def key = abstract_method
41
45
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tree
4
- VERSION = "0.0.2"
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
 
@@ -39,7 +40,7 @@ module Tree
39
40
  class ArrayTree < AbstractTree # Aka. SetTree aka. Tree
40
41
  include InternalParentImplementation
41
42
  include InternalChildrenArrayImplementation
42
- include InternalParentChildImplementation
43
+ include InternalParentChildArrayImplementation
43
44
  include UpTreeAlgorithms
44
45
  include DownTreeAlgorithms
45
46
  include DownTreeFilteredAlgorithms
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.2
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