caruby-core 1.4.2 → 1.4.3

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.
Files changed (52) hide show
  1. data/History.txt +10 -0
  2. data/lib/caruby/cli/command.rb +10 -8
  3. data/lib/caruby/database/fetched_matcher.rb +28 -39
  4. data/lib/caruby/database/lazy_loader.rb +101 -0
  5. data/lib/caruby/database/persistable.rb +190 -167
  6. data/lib/caruby/database/persistence_service.rb +21 -7
  7. data/lib/caruby/database/persistifier.rb +185 -0
  8. data/lib/caruby/database/reader.rb +106 -176
  9. data/lib/caruby/database/saved_matcher.rb +56 -0
  10. data/lib/caruby/database/search_template_builder.rb +1 -1
  11. data/lib/caruby/database/sql_executor.rb +8 -7
  12. data/lib/caruby/database/store_template_builder.rb +134 -61
  13. data/lib/caruby/database/writer.rb +252 -52
  14. data/lib/caruby/database.rb +88 -67
  15. data/lib/caruby/domain/attribute_initializer.rb +16 -0
  16. data/lib/caruby/domain/attribute_metadata.rb +161 -72
  17. data/lib/caruby/domain/id_alias.rb +22 -0
  18. data/lib/caruby/domain/inversible.rb +91 -0
  19. data/lib/caruby/domain/merge.rb +116 -35
  20. data/lib/caruby/domain/properties.rb +1 -1
  21. data/lib/caruby/domain/reference_visitor.rb +207 -71
  22. data/lib/caruby/domain/resource_attributes.rb +93 -80
  23. data/lib/caruby/domain/resource_dependency.rb +22 -97
  24. data/lib/caruby/domain/resource_introspection.rb +21 -28
  25. data/lib/caruby/domain/resource_inverse.rb +134 -0
  26. data/lib/caruby/domain/resource_metadata.rb +41 -19
  27. data/lib/caruby/domain/resource_module.rb +42 -33
  28. data/lib/caruby/import/java.rb +8 -9
  29. data/lib/caruby/migration/migrator.rb +20 -7
  30. data/lib/caruby/migration/resource_module.rb +0 -2
  31. data/lib/caruby/resource.rb +132 -351
  32. data/lib/caruby/util/cache.rb +4 -1
  33. data/lib/caruby/util/class.rb +48 -1
  34. data/lib/caruby/util/collection.rb +54 -18
  35. data/lib/caruby/util/inflector.rb +7 -0
  36. data/lib/caruby/util/options.rb +35 -31
  37. data/lib/caruby/util/partial_order.rb +1 -1
  38. data/lib/caruby/util/properties.rb +2 -2
  39. data/lib/caruby/util/stopwatch.rb +16 -8
  40. data/lib/caruby/util/transitive_closure.rb +1 -1
  41. data/lib/caruby/util/visitor.rb +342 -328
  42. data/lib/caruby/version.rb +1 -1
  43. data/lib/caruby/yard/resource_metadata_handler.rb +8 -0
  44. data/lib/caruby.rb +2 -0
  45. metadata +10 -9
  46. data/lib/caruby/database/saved_merger.rb +0 -131
  47. data/lib/caruby/domain/annotatable.rb +0 -25
  48. data/lib/caruby/domain/annotation.rb +0 -23
  49. data/lib/caruby/import/annotatable_class.rb +0 -28
  50. data/lib/caruby/import/annotation_class.rb +0 -27
  51. data/lib/caruby/import/annotation_module.rb +0 -67
  52. data/lib/caruby/migration/resource.rb +0 -8
@@ -8,13 +8,16 @@ module CaRuby
8
8
  attr_reader :sticky
9
9
 
10
10
  # Returns a new Cache whose value key is determined by calling the given
11
- # extractor block on the cached value.
11
+ # extractor block on the cached value. The key must uniquely identify the
12
+ # cached value within the scope of the value class.
12
13
  #
13
14
  # If the value is not cached and there is a factory Proc, then the result of
14
15
  # calling the factory on the missing value is cached with the value key.
15
16
  #
16
17
  # @param [Proc] optional factory Proc called with a missing value as argument
17
18
  # to create a cached object
19
+ # @yield [value] returns the value key
20
+ # @yieldparam value the value to cach
18
21
  def initialize(factory=nil, &extractor)
19
22
  @factory = factory
20
23
  # Make the class => { key => value } hash.
@@ -3,7 +3,7 @@ require 'enumerator'
3
3
  class Class
4
4
  # Returns an Enumerable on this class and its ancestors.
5
5
  def class_hierarchy
6
- @hierarchy ||= Enumerable::Enumerator.new(self, :each_class_in_hierarchy)
6
+ @class__hierarchy ||= Enumerable::Enumerator.new(self, :each_class_in_hierarchy)
7
7
  end
8
8
 
9
9
  # Returns this class's superclass, thereby enabling class ranges, e.g.
@@ -58,6 +58,53 @@ class Class
58
58
  end
59
59
  end
60
60
 
61
+ # Defines an instance variable accessor attribute whose reader calls the block given
62
+ # to this method to create a new instance variable on demand, if necessary.
63
+ #
64
+ # For example, the declaration
65
+ # class AlertHandler
66
+ # attr_create_on_demand_accessor(:pings) { Array.new }
67
+ # end
68
+ # is equivalent to:
69
+ # class AlertHandler
70
+ # attr_writer :pings
71
+ # def pings
72
+ # instance_variable_defined?(@pings) ? @pings : Array.new
73
+ # end
74
+ # end
75
+ #
76
+ # This method is useful either as a short-hand for the create-on-demand idiom
77
+ # as shown in the example above, or when it is desirable to dynamically add a
78
+ # mix-in attribute to a class at runtime whose name is not known when the class
79
+ # is defined.
80
+ #
81
+ # @example
82
+ # class AlertHandler
83
+ # def self.handle(alert)
84
+ # attr_create_on_demand_accessor(alert) { AlertQueue.new }
85
+ # end
86
+ # end
87
+ # ...
88
+ # AlertHandler.handle(:pings)
89
+ # AlertHandler.new.pings #=> empty AlertQueue
90
+ #
91
+ # @param [Symbol] symbol the attribute to define
92
+ # @yield [obj] factory to create the new attribute value for the given instance
93
+ # @yieldparam obj the class instance for which the attribute will be set
94
+ def attr_create_on_demand_accessor(symbol)
95
+ attr_writer(symbol)
96
+ wtr = "#{symbol}=".to_sym
97
+ iv = "@#{symbol}".to_sym
98
+ # the attribute reader creates a new proxy on demand
99
+ define_method(symbol) do
100
+ instance_variable_defined?(iv) ? instance_variable_get(iv) : send(wtr, yield(self))
101
+ end
102
+ end
103
+
104
+ # Enumerates each class in the hierarchy.
105
+ #
106
+ # @yield [klass] the enumeration block
107
+ # @yieldparam [Class] klass the class in the hierarchy
61
108
  def each_class_in_hierarchy
62
109
  current = self
63
110
  until current.nil?
@@ -151,6 +151,8 @@ module Enumerable
151
151
  # Note, however, that unlike select, filter does not return an Array.
152
152
  # The default filter block returns the passed item.
153
153
  #
154
+ # @yield [item] filter the selection filter
155
+ # @yieldparam item the collection member to filter
154
156
  # @return [Enumerable] the filtered result
155
157
  # @example
156
158
  # [1, nil, 3].filter.to_a #=> [1, 3]
@@ -174,7 +176,7 @@ module Enumerable
174
176
  # Returns an Enumerable which iterates over items in this Enumerable and the other Enumerable in sequence, e.g.:
175
177
  # [1, 2, 3] + [3, 4] #=> [1, 2, 3, 3, 4]
176
178
  #
177
- # Unlike Array#+, {#union} reflects changes to the underlying enumerators.
179
+ # Unlike the Array plus (+) operator, {#union} reflects changes to the underlying enumerators.
178
180
  #
179
181
  # @example
180
182
  # a = [1, 2]
@@ -183,7 +185,8 @@ module Enumerable
183
185
  # ab #=> [1, 2, 4, 5]
184
186
  # a << 3
185
187
  # ab #=> [1, 2, 3, 4, 5]
186
- # @return [Enumerable] self followed by other
188
+ # @param [Enumerable] other the Enumerable to compose with this Enumerable
189
+ # @return [Enumerable] an enumerator over self followed by other
187
190
  def union(other)
188
191
  MultiEnumerator.new(self, other)
189
192
  end
@@ -207,16 +210,31 @@ module Enumerable
207
210
  # Returns a new Enumerable that iterates over the base Enumerable applying the transformer block to each item, e.g.:
208
211
  # [1, 2, 3].transform { |n| n * 2 }.to_a #=> [2, 4, 6]
209
212
  #
210
- # Unlike #collect, {#wrap} reflects changes to the base Enumerable, e.g.:
213
+ # Unlike Array.map, {#wrap} reflects changes to the base Enumerable, e.g.:
211
214
  # a = [2, 4, 6]
212
215
  ``# transformed = a.wrap { |n| n * 2 }
213
216
  # a << 4
214
217
  # transformed.to_a #=> [2, 4, 6, 8]
215
218
  #
216
219
  # In addition, transform has a small, fixed storage requirement, making it preferable to select for large collections.
217
- # Note, however, that unlike collect, transform does not return an Array.
218
- def wrap(&transformer) # :yields: item
219
- Transformer.new(self, &transformer)
220
+ # Note, however, that unlike map, transform does not return an Array.
221
+ #
222
+ # @yield [item] the transformer on the enumerated items
223
+ # @yieldparam item an enumerated item
224
+ # @return [Enumerable] an enumeration on the transformed values
225
+ def wrap(&mapper)
226
+ Transformer.new(self, &mapper)
227
+ end
228
+
229
+ def join(other)
230
+ Joiner.new(self, other)
231
+ end
232
+
233
+ # @yield [item] the transformer on the enumerated items
234
+ # @yieldparam item an enumerated item
235
+ # @return [Enumerable] the mapped values excluding null values
236
+ def compact_map(&mapper)
237
+ wrap(&mapper).compact
220
238
  end
221
239
 
222
240
  private
@@ -229,30 +247,45 @@ module Enumerable
229
247
  @filter = filter
230
248
  end
231
249
 
232
- # Calls block on each item which passes this Filter's filter test.
233
- def each(&block)
250
+ # Calls the given block on each item which passes this Filter's filter test.
251
+ #
252
+ # @yield [item] the block called on each item
253
+ # @yieldparam item the enumerated item
254
+ def each
234
255
  @base.each { |item| yield(item) if @filter ? @filter.call(item) : item }
235
256
  end
236
257
 
237
258
  # Optimized for a Set base.
259
+ #
260
+ # @param [item] the item to check
261
+ # @return [Boolean] whether the item is a member of this Enumerable
238
262
  def include?(item)
239
263
  return false if Set === @base and not @base.include?(item)
240
264
  super
241
265
  end
242
266
 
243
- # Adds value to the base Enumerable, if the base supports it.
244
- def <<(value)
267
+ # Adds an item to the base Enumerable, if thif Filter's base supports it.
268
+ #
269
+ # @param item the item to add
270
+ # @return [Filter] self
271
+ def <<(item)
245
272
  @base << value
273
+ self
246
274
  end
247
275
 
248
- # @return a new Array consisting of this Filter's filtered content merged with the other Enumerable
276
+ # @param [Enumerable] other the Enumerable to merge
277
+ # @return [Array] this Filter's filtered content merged with the other Enumerable
249
278
  def merge(other)
250
- to_a.merge(other)
279
+ to_a.merge!(other)
251
280
  end
252
281
 
253
282
  # Merges the other Enumerable into the base Enumerable, if the base supports it.
283
+ #
284
+ # @param other (see #merge)
285
+ # @return [Filter, nil] this Filter's filtered content merged with the other Enumerable
254
286
  def merge!(other)
255
287
  @base.merge!(other)
288
+ self
256
289
  end
257
290
  end
258
291
 
@@ -272,7 +305,7 @@ module Enumerable
272
305
  self
273
306
  end
274
307
 
275
- # Calls block on each item after this Transformer's transformer block is applied.
308
+ # Calls the block on each item after this Transformer's transformer block is applied.
276
309
  def each
277
310
  @base.each { |item| yield(item.nil? ? nil : @xfm.call(item)) }
278
311
  end
@@ -400,7 +433,7 @@ end
400
433
  module Hashable
401
434
  include Enumerable
402
435
 
403
- # @see Hash#each
436
+ # @see Hash#each_pair
404
437
  def each_pair(&block)
405
438
  each(&block)
406
439
  end
@@ -992,15 +1025,18 @@ class Array
992
1025
  base__flatten
993
1026
  end
994
1027
 
995
- # Adds the other Enumerable to this array.
1028
+ # Concatenates the other Enumerable to this array.
996
1029
  #
997
- # Raises ArgumentError if other does not respond to the +to_a+ method.
1030
+ # @param [#to_a] other the other Enumerable
1031
+ # @raise [ArgumentError] if other does not respond to the +to_a+ method
998
1032
  def add_all(other)
999
1033
  return concat(other) if Array === other
1000
1034
  begin
1001
- return add_all(other.to_a)
1035
+ add_all(other.to_a)
1036
+ rescue NoMethodError
1037
+ raise
1002
1038
  rescue
1003
- raise ArgumentError.new("Can't convert #{other.class.name} to array") unless other.respond_to?(:to_a)
1039
+ raise ArgumentError.new("Can't convert #{other.class.name} to array")
1004
1040
  end
1005
1041
  end
1006
1042
 
@@ -17,4 +17,11 @@ class String
17
17
  def capitalize_first
18
18
  sub(/(?:^)(.)/) { $1.upcase }
19
19
  end
20
+
21
+ # @return this String with the first letter decapitalized and other letters preserved.
22
+ # @example
23
+ # "RosesAreRed".decapitalize #=> "rosesAreRed"
24
+ def decapitalize
25
+ sub(/(?:^)(.)/) { $1.downcase }
26
+ end
20
27
  end
@@ -42,42 +42,46 @@ class Options
42
42
  end
43
43
  end
44
44
 
45
- # Merges the others options with options and returns the new merged option hash.
45
+ # Returns the given argument list as a hash, determined as follows:
46
+ # * If the sole argument member is a hash, then that hash is the options.
47
+ # * An argument list of option symbols followed by zero, one or more non-option parameters is composed as the option hash.
48
+ # * An empty argument list is a new empty option hash.
46
49
  #
47
50
  # @example
48
- # Options.merge(nil, :create) #=> {:create => :true}
49
- # Options.merge(:create, :optional => :a, :required => :b) #=> {:create => :true, :optional => :a, :required => :b}
50
- # Options.merge({:required => [:b]}, :required => [:c]) #=> {:required => [:b, :c]}
51
- def self.merge(options, others)
52
- options = options.dup if Hash === options
53
- self.merge!(options, others)
54
- end
55
-
56
- # Merges the others options into the given options and returns the created or modified option hash.
57
- # This method differs from {Options.merge} by modifying an existing options hash.
58
- def self.merge!(options, others)
59
- to_hash(options).merge!(to_hash(others)) { |key, oldval, newval| oldval.respond_to?(:merge) ? oldval.merge(newval) : newval }
60
- end
61
-
62
- # Returns the options as a hash. If options is already a hash, then this method returns hash.
63
- # * If options is a Symbol _s_, then this method returns +{+_s_+=>true}+.
64
- # * An Array of Symbols is enumerated as individual Symbol options.
65
- # * If options is nil, then this method returns a new empty hash.
66
- def self.to_hash(options)
67
- return Hash.new if options.nil?
68
- case options
69
- when Hash then
70
- options
71
- when Array then
72
- options.to_hash { |item| Symbol === item or raise ArgumentError.new("Option is not supported; expected Symbol, found: #{options.class}") }
73
- when Symbol then
74
- {options => true}
75
- else
76
- raise ArgumentError.new("Options argument type is not supported; expected Hash or Symbol, found: #{options.class}")
51
+ # Options.to_hash() #=> {}
52
+ # Options.to_hash(nil) #=> {}
53
+ # Options.to_hash(:a => 1) #=> {:a => 1}
54
+ # Options.to_hash(:a) #=> {:a => true}
55
+ # Options.to_hash(:a, 1, :b, 2) #=> {:a => 1, :b => 2}
56
+ # Options.to_hash(:a, 1, :b, :c, 2, 3) #=> {:a => 1, :b => true, :c => [2, 3]}
57
+ # @param [Array] args the option list
58
+ # @return [Hash] the option hash
59
+ def self.to_hash(*args)
60
+ unless Enumerable === args then
61
+ raise ArgumentError.new("Expected Enumerable, found #{args.class.qp}")
62
+ end
63
+ oargs = {}
64
+ opt = args.first
65
+ return oargs if opt.nil?
66
+ return opt if oargs.empty? and Hash === opt
67
+ unless Symbol === opt then
68
+ raise ArgumentError.new("Expected Symbol as first argument, found #{args.first.class.qp}")
69
+ end
70
+ args.inject(nil) do |list, item|
71
+ Symbol === item ? oargs[item] = Array.new : list << item
77
72
  end
73
+ # convert the value list to true, a single value or leave as an array
74
+ oargs.transform do |list|
75
+ case list.size
76
+ when 0 then true
77
+ when 1 then list.first
78
+ else list
79
+ end
80
+ end.to_hash
78
81
  end
79
82
 
80
- # Raises a ValidationError if the given options are not in the given allowable choices.
83
+ # @param [Hash, Symbol, nil] opts the options to validate
84
+ # @raise [ValidationError] if the given options are not in the given allowable choices
81
85
  def self.validate(options, choices)
82
86
  to_hash(options).each_key do |opt|
83
87
  raise ValidationError.new("Option is not supported: #{opt}") unless choices.include?(opt)
@@ -9,7 +9,7 @@
9
9
  # module Queued
10
10
  # attr_reader :queue
11
11
  # def <=>(other)
12
- # raise TypeError.new("Comparison argument is not another Queued item") unless Queued == other
12
+ # raise TypeError.new("Comparison argument is not another Queued item") unless Queued === other
13
13
  # queue.index(self) <=> queue.index(other) if queue.equal?(other.queue)
14
14
  # end
15
15
  # end
@@ -97,8 +97,8 @@ module CaRuby
97
97
  # or nil if key is neither a String nor a Symbol.
98
98
  def alternate_key(key)
99
99
  case key
100
- when String then key.to_sym
101
- when Symbol then key.to_s
100
+ when String then key.to_sym
101
+ when Symbol then key.to_s
102
102
  end
103
103
  end
104
104
 
@@ -4,36 +4,42 @@ require 'benchmark'
4
4
  class Stopwatch
5
5
  # Time accumulates elapsed real time and total CPU time.
6
6
  class Time
7
- # The Benchmark::Tms wrapped by this Time.
7
+ # @return [Benchmark::Tms] the Tms wrapped by this Time
8
8
  attr_reader :tms
9
9
 
10
+ # @param [Benchmark::Tms, nil] the starting time (default is now)
10
11
  def initialize(tms=nil)
11
12
  @tms = tms || Benchmark::Tms.new
12
13
  end
13
14
 
14
- # Returns the cumulative elapsed real clock time.
15
+ # @return [Numeric] the cumulative elapsed real clock time
15
16
  def elapsed
16
17
  @tms.real
17
18
  end
18
19
 
19
- # Returns the cumulative CPU total time.
20
+ # @return [Numeric] the cumulative CPU total time
20
21
  def cpu
21
22
  @tms.total
22
23
  end
23
24
 
24
- # Adds the time to execute the given block to this time. Returns the split execution Time.
25
+ # Adds the time to execute the given block to this time.
26
+ #
27
+ # @return [Numeric] the split execution Time
25
28
  def split(&block)
26
29
  stms = Benchmark.measure(&block)
27
30
  @tms += stms
28
31
  Time.new(stms)
29
32
  end
30
33
 
34
+ # Sets this benchmark timer to zero.
31
35
  def reset
32
36
  @tms = Benchmark::Tms.new
33
37
  end
34
38
  end
35
39
 
36
- # Executes the given block. Returns the execution Time.
40
+ # Executes the given block
41
+ #
42
+ # @return [Numeric] the execution Time
37
43
  def self.measure(&block)
38
44
  new.run(&block)
39
45
  end
@@ -44,17 +50,19 @@ class Stopwatch
44
50
  end
45
51
 
46
52
  # Executes the given block. Accumulates the execution time in this Stopwatch.
47
- # Returns the execution Time.
53
+ #
54
+ # @return [Numeric] the execution run Time
48
55
  def run(&block)
49
56
  @time.split(&block)
50
57
  end
51
58
 
52
- # Returns the cumulative elapsed real clock time spent in {#run} executions.
59
+ # @return [Numeric] the cumulative elapsed real clock time spent in {#run} executions
53
60
  def elapsed
54
61
  @time.elapsed
55
62
  end
56
63
 
57
- # Returns the cumulative CPU total time spent in {#run} executions for the current process and its children.
64
+ # @return [Numeric] the cumulative CPU total time spent in {#run} executions for the
65
+ # current process and its children
58
66
  def cpu
59
67
  @time.cpu
60
68
  end
@@ -26,7 +26,7 @@ class Object
26
26
  def transitive_closure(method=nil)
27
27
  raise ArgumentError.new("Missing both a method argument and a block") if method.nil? and not block_given?
28
28
  return transitive_closure() { |node| node.send(method) } if method
29
- Visitor.new(:depth_first) { |node| yield node }.to_enum(self).to_a.reverse
29
+ CaRuby::Visitor.new(:depth_first) { |node| yield node }.to_enum(self).to_a.reverse
30
30
  end
31
31
  end
32
32
 
@@ -5,346 +5,360 @@ require 'caruby/util/options'
5
5
  require 'enumerator'
6
6
  require 'generator'
7
7
 
8
- # Error raised on a visit failure.
9
- class VisitError < RuntimeError; end
10
-
11
- # Visitor traverses items and applies an operation, e.g.:
12
- # class Node
13
- # attr_accessor :children, :value
14
- # def initialize(value, parent=nil)
15
- # @value = value
16
- # @children = []
17
- # @parent = parent
18
- # @parent.children << self if @parent
19
- # end
20
- # end
21
- # parent = Node.new(1)
22
- # child = Node.new(2, parent)
23
- # multiplier = 2
24
- # Visitor.new { |node| node.children }.visit(parent) { |node| node.value *= multiplier } #=> 2
25
- # parent.value #=> 2
26
- # child.value #=> 4
27
- #
28
- # The visit result is the result of evaluating the operation block on the initial visited node.
29
- # Visiting a collection returns an array of the result of visiting each member of the collection,
30
- # e.g. augmenting the preceding example:
31
- # parent2 = Node.new(3)
32
- # child2 = Node.new(4, parent2)
33
- # Visitor.new { |node| node.children }.visit([parent, parent2]) { |node| node.value *= multiplier } #=> [2, 6]
34
- # Each visit captures the visit result in the +visited+ hash, e.g.:
35
- # parent = Node.new(1)
36
- # child = Node.new(2, parent)
37
- # visitor = Visitor.new { |node| node.children }
38
- # visitor.visit([parent]) { |node| node.value += 1 }
39
- # parent.value #=> 2
40
- # visitor.visited[parent] #=> 2
41
- # child.value #=> 3
42
- # visitor.visited[child] #=> 3
43
- #
44
- # A +return+ from the operation block terminates the visit and exits from the defining scope with the block return value,
45
- # e.g. given the preceding example:
46
- # def increment(parent, limit)
47
- # Visitor.new { |node| node.children }.visit(parent) { |node| node.value < limit ? node.value += 1 : return }
48
- # end
49
- # increment(parent, 2) #=> nil
50
- # parent.value #=> 2
51
- # child.value #=> 2
52
- #
53
- # The to_enum method allows navigator iteration, e.g.:
54
- # Visitor.new { |node| node.children }.to_enum(parent).detect { |node| node.value == 2 }
55
- class Visitor
56
-
57
- attr_reader :options, :visited, :lineage, :cycles
58
-
59
- # Creates a new Visitor which traverses the child objects returned by the navigator block.
60
- # The navigator block takes a parent argument and returns the children to visit. If the block
61
- # return value is not nil and not a collection, then the returned object is visited. A nil or
62
- # empty child is not visited.
63
- #
64
- # options is a symbol => value hash. A Symbol argument _symbol_ is the same as +{+_symbol_+=>true}+.
65
- # Supported options include the follwing:
66
- #
67
- # The value of :depth_first can be +true+, +false+ or a Proc. If the value is a Proc, then
68
- # value determines whether a child is visited depth-first. See the {#visit} method for more information.
69
- #
70
- # If the the :visited option is set, then the visited nodes are recorded in the :visited option hash.
71
- # In that case, the {#visit} call does not clear the visited hash.
72
- #
73
- # If the :operator option is set, then the visit operator block is called when a node is visited.
74
- # The operator block argument is the visited node.
75
- #
76
- # @param [Symbol, {Symbol => Object}] options the visit options. A symbol argument is the same
77
- # as symbol => true
78
- # @option options [String] :depth_first depth-first traversal
79
- # @option options [Hash] :visited the hash to use when recording visited node => value associations
80
- # @option options [Proc] :operator the visit operator block
81
- # @option options [String] :prune_cycle flag indicating whether to exclude cycles to the root in a visit
82
- # @yield [parent] the parent being visited
83
- def initialize(options=nil, &navigator)
84
- @navigator = navigator
85
- @options = Options.to_hash(options)
86
- @depth_first_flag = @options[:depth_first]
87
- @visited = @options[:visited] || {}
88
- @prune_cycle_flag = @options[:prune_cycle]
89
- @lineage = []
90
- @cycles = []
91
- @exclude = Set.new
92
- end
93
-
94
- # Navigates to node and the children returned by this Visitor's navigator block.
95
- # Applies the optional operator block to each child node if the block is given to this method.
96
- # Returns the result of the operator block if given, or the node itself otherwise.
97
- #
98
- # The nodes to visit from a parent node are determined in the following sequence:
99
- # * Return if the parent node has already been visited.
100
- # * If depth_first, then call the navigator block defined in the initializer on
101
- # the parent node and visit each child node.
102
- # * Visit the parent node.
103
- # * If not depth-first, then call the navigator block defined in the initializer
104
- # on the parent node and visit each child node.
105
- # The :depth option value constrains child traversal to that number of levels.
106
- #
107
- # This method first clears the _visited_ hash, unless the :visited option was set in the initializer.
8
+ module CaRuby
9
+ # Error raised on a visit failure.
10
+ class VisitError < RuntimeError; end
11
+
12
+ # Visitor traverses items and applies an operation, e.g.:
13
+ # class Node
14
+ # attr_accessor :children, :value
15
+ # def initialize(value, parent=nil)
16
+ # @value = value
17
+ # @children = []
18
+ # @parent = parent
19
+ # @parent.children << self if @parent
20
+ # end
21
+ # end
22
+ # parent = Node.new(1)
23
+ # child = Node.new(2, parent)
24
+ # multiplier = 2
25
+ # CaRuby::Visitor.new { |node| node.children }.visit(parent) { |node| node.value *= multiplier } #=> 2
26
+ # parent.value #=> 2
27
+ # child.value #=> 4
108
28
  #
109
- # @param node the root object to visit
110
- # @yield [visited] an operator applied to each visited object
111
- # @yieldparam visited the object currently being visited
112
- # @return the result of the yield block on node, or node itself if no block is given
113
- def visit(node, &operator)
114
- visit_root(node, &operator)
115
- end
116
-
117
- # @param node the node to check
118
- # @return whether the node was visited
119
- def visited?(node)
120
- @visited.has_key?(node)
121
- end
122
-
123
- # @return the top node visited
124
- def root
125
- @lineage.first
126
- end
127
-
128
- # @return the current node being visited
129
- def current
130
- @lineage.last
131
- end
132
-
133
- # @return the node most recently passed as an argument to this visitor's navigator block, or nil if
134
- # visiting the first node
135
- def parent
136
- @lineage[-2]
137
- end
138
-
139
- # @return [Enumerable] iterator over each visited node
140
- def to_enum(node)
141
- # could use Generator, but that results in dire behavior on any error by crashing with an elided Java lineage trace
142
- VisitorEnumerator.new(self, node)
143
- end
144
-
145
- # Returns a new visitor that traverses a collection of parent nodes in lock-step fashion using this visitor.
146
- # The synced {#visit} method applies the visit operator block to an array of child nodes taken
147
- # from each parent node, e.g. given the class documentation example:
148
- # parent1 = Node.new(1)
149
- # child11 = Node.new(2, parent1)
150
- # child12 = Node.new(3, parent1)
151
- # parent2 = Node.new(1)
152
- # child21 = Node.new(3, parent2)
153
- # Visitor.new { |node| node.children }.sync.enum.to_a #=> [
154
- # [parent1, parent2],
155
- # [child11, child21],
156
- # [child12, nil]
157
- # ]
29
+ # The visit result is the result of evaluating the operation block on the initial visited node.
30
+ # Visiting a collection returns an array of the result of visiting each member of the collection,
31
+ # e.g. augmenting the preceding example:
32
+ # parent2 = Node.new(3)
33
+ # child2 = Node.new(4, parent2)
34
+ # CaRuby::Visitor.new { |node| node.children }.visit([parent, parent2]) { |node| node.value *= multiplier } #=> [2, 6]
35
+ # Each visit captures the visit result in the +visited+ hash, e.g.:
36
+ # parent = Node.new(1)
37
+ # child = Node.new(2, parent)
38
+ # visitor = CaRuby::Visitor.new { |node| node.children }
39
+ # visitor.visit([parent]) { |node| node.value += 1 }
40
+ # parent.value #=> 2
41
+ # visitor.visited[parent] #=> 2
42
+ # child.value #=> 3
43
+ # visitor.visited[child] #=> 3
158
44
  #
159
- # By default, the children are grouped in enumeration order. If a block is given to this
160
- # method, then the block is called to match child nodes, e.g. using the above example:
161
- # visitor = Visitor.new { |node| node.children }
162
- # synced = visitor.sync { |node, others| others.detect { |other| node.value == other.value }
163
- # synced.enum.to_a #=> [
164
- # [parent1, parent2],
165
- # [child11, nil],
166
- # [child12, child21]
167
- # ]
45
+ # A +return+ from the operation block terminates the visit and exits from the defining scope with the block return value,
46
+ # e.g. given the preceding example:
47
+ # def increment(parent, limit)
48
+ # CaRuby::Visitor.new { |node| node.children }.visit(parent) { |node| node.value < limit ? node.value += 1 : return }
49
+ # end
50
+ # increment(parent, 2) #=> nil
51
+ # parent.value #=> 2
52
+ # child.value #=> 2
168
53
  #
169
- # @yield [node, others] matches node in others (optional)
170
- # @yieldparam [Resource] node the visited node to match
171
- # @yieldparam [<Resource>] the candidates for matching the node
172
- def sync(&matcher) # :yields: node, others
173
- SyncVisitor.new(self, &matcher)
174
- end
175
-
176
- # Returns a new Visitor which determines which nodes to visit by applying the given block
177
- # to this visitor, e.g.:
178
- # Visitor.new { |node| node.children }.filter { |parent, children| children.first if parent.age >= 18 }
179
- # navigates to the first child of parents 18 or older.
180
- #
181
- # The filter block arguments consist of a parent node and an array of children nodes for the parent.
182
- # The block can return nil, a single node to visit or a collection of nodes to visit.
183
- #
184
- # @return [Visitor] the filter visitor
185
- # @yield [parent, children] the filter to select which of the children to visit next
186
- # @yieldparam parent the currently visited node
187
- # @yieldparam children the nodes slated by this Visitor to visit next
188
- # @raise [ArgumentError] if a block is not given to this method
189
- def filter
190
- raise ArgumentError.new("Filter block not given to visitor filter method") unless block_given?
191
- Visitor.new(@options) { |node| yield(node, node_children(node)) }
192
- end
193
-
194
- protected
195
-
196
- # Resets this visitor's state in preparation for a new visit.
197
- def clear
198
- # clear the lineage
199
- @lineage.clear
200
- # if the visited hash is not shared, then clear it
201
- @visited.clear unless @options.has_key?(:visited)
202
- # clear the cycles
203
- @cycles.clear
204
- end
205
-
206
- # Sets the visited hash.
207
- def visited=(hash)
208
- @visited = hash ||= {}
209
- end
210
-
211
- # Visits the given node using the block given to this method.
212
- # The default block returns node.
213
- def visit_node(node)
214
- @visited[node] = block_given? ? yield(node) : node
215
- end
216
-
217
- # Returns the children to visit for the given node.
218
- def node_children(node)
219
- children = @navigator.call(node)
220
- return Array::EMPTY_ARRAY if children.nil?
221
- Enumerable === children ? children.to_a.compact : [children]
222
- end
223
-
224
- private
225
-
226
- # Visits the root node and all descendants.
227
- def visit_root(node, &operator)
228
- clear
229
- prune_cycle_nodes(node) if @prune_cycle_flag
230
- # visit the root node
231
- visit_recursive(node, &operator)
232
- end
233
-
234
- # Excludes the internal nodes in cycles starting and ending at the given root.
235
- def prune_cycle_nodes(root)
236
- @exclude.clear
237
- # visit the root, which will detect cycles, and remove the visited nodes afterwords
238
- @prune_cycle_flag = false
239
- to_enum(root).collect.each { |node| @visited.delete(node) }
240
- @prune_cycle_flag = true
241
- # add each cyclic internal node to the exclude list
242
- @cycles.each { |cycle| cycle[1...-1].each { |node| @exclude << node } if cycle.first == root }
243
- end
244
-
245
- def visit_recursive(node, &operator)
246
- return if node.nil? or @exclude.include?(node)
247
- # return the visited value if the node has already been visited
248
- if @visited.has_key?(node) then
249
- #capture a cycle
250
- index = @lineage.index(node)
251
- if index then
252
- cycle = @lineage[index..-1] << node
253
- @cycles << cycle
254
- end
255
- return @visited[node]
54
+ # The to_enum method allows navigator iteration, e.g.:
55
+ # CaRuby::Visitor.new { |node| node.children }.to_enum(parent).detect { |node| node.value == 2 }
56
+ class Visitor
57
+
58
+ attr_reader :options, :visited, :lineage, :cycles
59
+
60
+ # Creates a new Visitor which traverses the child objects returned by the navigator block.
61
+ # The navigator block takes a parent argument and returns the children to visit. If the block
62
+ # return value is not nil and not a collection, then the returned object is visited. A nil or
63
+ # empty child is not visited.
64
+ #
65
+ # options is a symbol => value hash. A Symbol argument _symbol_ is the same as +{+_symbol_+=>true}+.
66
+ # Supported options include the follwing:
67
+ #
68
+ # The value of :depth_first can be +true+, +false+ or a Proc. If the value is a Proc, then
69
+ # value determines whether a child is visited depth-first. See the {#visit} method for more information.
70
+ #
71
+ # If the the :visited option is set, then the visited nodes are recorded in the :visited option hash.
72
+ # In that case, the {#visit} call does not clear the visited hash.
73
+ #
74
+ # If the :operator option is set, then the visit operator block is called when a node is visited.
75
+ # The operator block argument is the visited node.
76
+ #
77
+ # @param [Symbol, {Symbol => Object}] opts the visit options. A symbol argument is the same
78
+ # as symbol => true
79
+ # @option opts [String] :depth_first depth-first traversal
80
+ # @option opts [Hash] :visited the hash to use when recording visited node => value associations
81
+ # @option opts [Proc] :operator the visit operator block
82
+ # @option opts [String] :prune_cycle flag indicating whether to exclude cycles to the root in a visit
83
+ # @yield [parent] the parent being visited
84
+ def initialize(opts=nil, &navigator)
85
+ @navigator = navigator
86
+ @options = Options.to_hash(opts)
87
+ @depth_first_flag = @options[:depth_first]
88
+ @visited = @options[:visited] || {}
89
+ @prune_cycle_flag = @options[:prune_cycle]
90
+ @lineage = []
91
+ @cycles = []
92
+ @exclude = Set.new
256
93
  end
257
- # return nil if the node has not been visited but has been navigated in a depth-first visit
258
- return if @lineage.include?(node)
259
- visit_node_and_children(node, &operator)
260
- end
261
-
262
- def visit_node_and_children(node, &operator)
263
- # set the current node
264
- @lineage.push(node)
265
- # if depth-first, then visit the children before the current node
266
- visit_children(node, &operator) if @depth_first_flag
267
- # visit the current node
268
- result = visit_node(node, &operator)
269
- # if not depth-first, then visit the children after the current node
270
- visit_children(node, &operator) unless @depth_first_flag
271
- @lineage.pop
272
- # return the visit result
273
- result
274
- end
275
-
276
- def visit_children(node, &operator)
277
- children = node_children(node)
278
- children.each { |child| visit_recursive(child, &operator) }
279
- end
280
-
281
- class VisitorEnumerator
282
- include Enumerable
283
-
284
- def initialize(visitor, node)
285
- @visitor = visitor
286
- @root = node
94
+
95
+ # Navigates to node and the children returned by this Visitor's navigator block.
96
+ # Applies the optional operator block to each child node if the block is given to this method.
97
+ # Returns the result of the operator block if given, or the node itself otherwise.
98
+ #
99
+ # The nodes to visit from a parent node are determined in the following sequence:
100
+ # * Return if the parent node has already been visited.
101
+ # * If depth_first, then call the navigator block defined in the initializer on
102
+ # the parent node and visit each child node.
103
+ # * Visit the parent node.
104
+ # * If not depth-first, then call the navigator block defined in the initializer
105
+ # on the parent node and visit each child node.
106
+ # The :depth option value constrains child traversal to that number of levels.
107
+ #
108
+ # This method first clears the _visited_ hash, unless the :visited option was set in the initializer.
109
+ #
110
+ # @param node the root object to visit
111
+ # @yield [visited] an operator applied to each visited object
112
+ # @yieldparam visited the object currently being visited
113
+ # @return the result of the yield block on node, or node itself if no block is given
114
+ def visit(node, &operator)
115
+ visit_root(node, &operator)
287
116
  end
288
-
289
- def each
290
- @visitor.visit(@root) { |node| yield(node) }
117
+
118
+ # @param node the node to check
119
+ # @return whether the node was visited
120
+ def visited?(node)
121
+ @visited.has_key?(node)
291
122
  end
292
- end
293
-
294
- class SyncVisitor < Visitor
295
- # @param [Visitor] visitor the Visitor which will visit synchronized input
296
- # @yield (see Visitor#sync)
297
- def initialize(visitor, &matcher)
298
- # the next node to visit is an array of child node pairs matched by the given matcher block
299
- super() { |nodes| match_children(visitor, nodes, &matcher) }
123
+
124
+ # @return the top node visited
125
+ def root
126
+ @lineage.first
300
127
  end
301
-
302
- # Visits the given pair of nodes.
128
+
129
+ # @return the current node being visited
130
+ def current
131
+ @lineage.last
132
+ end
133
+
134
+ # @return the node most recently passed as an argument to this visitor's navigator block,
135
+ # or nil if visiting the first node
136
+ def parent
137
+ @lineage[-2]
138
+ end
139
+
140
+ # @return [Enumerable] iterator over each visited node
141
+ def to_enum(node)
142
+ # JRuby alert - could use Generator instead, but that results in dire behavior on any error
143
+ # by crashing with an elided Java lineage trace.
144
+ VisitorEnumerator.new(self, node)
145
+ end
146
+
147
+ # Returns a new visitor that traverses a collection of parent nodes in lock-step fashion using
148
+ # this visitor. The synced {#visit} method applies the visit operator block to an array of child
149
+ # nodes taken from each parent node, e.g.:
150
+ # parent1 = Node.new(1)
151
+ # child11 = Node.new(2, parent1)
152
+ # child12 = Node.new(3, parent1)
153
+ # parent2 = Node.new(1)
154
+ # child21 = Node.new(3, parent2)
155
+ # CaRuby::Visitor.new { |node| node.children }.sync.to_enum.to_a #=> [
156
+ # [parent1, parent2],
157
+ # [child11, child21],
158
+ # [child12, nil]
159
+ # ]
303
160
  #
304
- # Raises ArgumentError if nodes does not consist of either two node arguments or one two-item Array
305
- # argument.
306
- def visit(*nodes)
307
- if nodes.size == 1 then
308
- nodes = nodes.first
309
- raise ArgumentError.new("Sync visitor requires a pair of entry nodes") unless nodes.size == 2
310
- end
311
- super(nodes)
161
+ # By default, the children are grouped in enumeration order. If a block is given to this method,
162
+ # then the block is called to match child nodes, e.g. using the above example:
163
+ # visitor = CaRuby::Visitor.new { |node| node.children }
164
+ # synced = visitor.sync { |nodes, others| nodes.to_compact_hash { others.detect { |other| node.value == other.value } } }
165
+ # synced.to_enum.to_a #=> [
166
+ # [parent1, parent2],
167
+ # [child11, nil],
168
+ # [child12, child21]
169
+ # ]
170
+ #
171
+ # @yield [nodes, others] matches node in others (optional)
172
+ # @yieldparam [<Resource>] nodes the visited nodes to match
173
+ # @yieldparam [<Resource>] others the candidates for matching the node
174
+ def sync(&matcher)
175
+ SyncVisitor.new(self, &matcher)
312
176
  end
313
-
314
- # Returns an Enumerable which applies the given block to each matched node starting at the given nodes.
177
+
178
+ # Returns a new Visitor which determines which nodes to visit by applying the given block
179
+ # to this visitor, e.g.:
180
+ # CaRuby::Visitor.new { |node| node.children }.filter { |parent, children| children.first if parent.age >= 18 }
181
+ # navigates to the first child of parents 18 or older.
315
182
  #
316
- # Raises ArgumentError if nodes does not consist of either two node arguments or one two-item Array
317
- # argument.
318
- def to_enum(*nodes)
319
- if nodes.size == 1 then
320
- nodes = nodes.first
321
- raise ArgumentError.new("Sync visitor requires a pair of entry nodes") unless nodes.size == 2
322
- end
323
- super(nodes)
183
+ # The filter block arguments consist of a parent node and an array of children nodes for the parent.
184
+ # The block can return nil, a single node to visit or a collection of nodes to visit.
185
+ #
186
+ # @return [Visitor] the filter visitor
187
+ # @yield [parent, children] the filter to select which of the children to visit next
188
+ # @yieldparam parent the currently visited node
189
+ # @yieldparam children the nodes slated by this Visitor to visit next
190
+ # @raise [ArgumentError] if a block is not given to this method
191
+ def filter
192
+ raise ArgumentError.new("Filter block not given to visitor filter method") unless block_given?
193
+ Visitor.new(@options) { |node| yield(node, node_children(node)) }
324
194
  end
325
-
195
+
196
+ protected
197
+
198
+ # Resets this visitor's state in preparation for a new visit.
199
+ def clear
200
+ # clear the lineage
201
+ @lineage.clear
202
+ # if the visited hash is not shared, then clear it
203
+ @visited.clear unless @options.has_key?(:visited)
204
+ # clear the cycles
205
+ @cycles.clear
206
+ end
207
+
208
+ # Sets the visited hash.
209
+ def visited=(hash)
210
+ @visited = hash ||= {}
211
+ end
212
+
213
+ # Visits the given node using the block given to this method.
214
+ # The default block returns node.
215
+ def visit_node(node)
216
+ @visited[node] = block_given? ? yield(node) : node
217
+ end
218
+
219
+ # Returns the children to visit for the given node.
220
+ def node_children(node)
221
+ children = @navigator.call(node)
222
+ return Array::EMPTY_ARRAY if children.nil?
223
+ Enumerable === children ? children.to_a.compact : [children]
224
+ end
225
+
326
226
  private
327
-
328
- # Returns an array of arrays of matched children from the given parent nodes. The children are matched
329
- # using the block given to this method, if supplied, or by index otherwise.
330
- #
331
- # @see #sync a usage example
332
- def match_children(visitor, nodes) # :yields: child, others
333
- # the parent nodes
334
- p1, p2 = nodes
335
- # this visitor's children
336
- c1 = visitor.node_children(p1)
337
- c2 = p2 ? visitor.node_children(p2) : []
338
-
339
- # apply the matcher block on each of this visitor's children and the other children.
340
- # if no block, then group the children by index, which is the transpose of the array of children arrays.
341
- if block_given? then
342
- c1.map { |c| [c, yield(c, c2)] }
343
- else
344
- # ensure that both children arrays are the same size
345
- others = c2.size <= c1.size ? c2.fill(nil, c2.size...c1.size) : c2[0, c1.size]
346
- # the children grouped by index is the transpose of the array of children arrays
347
- [c1, others].transpose
227
+
228
+ def depth_first?
229
+ @depth_first_flag
230
+ end
231
+
232
+ # Visits the root node and all descendants.
233
+ def visit_root(node, &operator)
234
+ clear
235
+ prune_cycle_nodes(node) if @prune_cycle_flag
236
+ # visit the root node
237
+ visit_recursive(node, &operator)
238
+ end
239
+
240
+ # Excludes the internal nodes in cycles starting and ending at the given root.
241
+ def prune_cycle_nodes(root)
242
+ @exclude.clear
243
+ # visit the root, which will detect cycles, and remove the visited nodes afterwords
244
+ @prune_cycle_flag = false
245
+ to_enum(root).collect.each { |node| @visited.delete(node) }
246
+ @prune_cycle_flag = true
247
+ # add each cyclic internal node to the exclude list
248
+ @cycles.each { |cycle| cycle[1...-1].each { |node| @exclude << node } if cycle.first == root }
249
+ end
250
+
251
+ def visit_recursive(node, &operator)
252
+ # bail if no node or the node is specifically excluded
253
+ return if node.nil? or @exclude.include?(node)
254
+ # return the visited value if the node has already been visited
255
+ if @visited.has_key?(node) then
256
+ #capture a cycle
257
+ index = @lineage.index(node)
258
+ if index then
259
+ cycle = @lineage[index..-1] << node
260
+ @cycles << cycle
261
+ end
262
+ return @visited[node]
263
+ end
264
+ # return nil if the node has not been visited but has been navigated in a depth-first visit
265
+ return if @lineage.include?(node)
266
+ # all systems go: visit the node graph
267
+ visit_node_and_children(node, &operator)
268
+ end
269
+
270
+ def visit_node_and_children(node, &operator)
271
+ # set the current node
272
+ @lineage.push(node)
273
+ # if depth-first, then visit the children before the current node
274
+ visit_children(node, &operator) if depth_first?
275
+ # visit the current node
276
+ result = visit_node(node, &operator)
277
+ # if not depth-first, then visit the children after the current node
278
+ visit_children(node, &operator) unless depth_first?
279
+ @lineage.pop
280
+ # return the visit result
281
+ result
282
+ end
283
+
284
+ def visit_children(node, &operator)
285
+ children = node_children(node)
286
+ children.each { |child| visit_recursive(child, &operator) }
287
+ end
288
+
289
+ class VisitorEnumerator
290
+ include Enumerable
291
+
292
+ def initialize(visitor, node)
293
+ @visitor = visitor
294
+ @root = node
295
+ end
296
+
297
+ def each
298
+ @visitor.visit(@root) { |node| yield(node) }
299
+ end
300
+ end
301
+
302
+ class SyncVisitor < Visitor
303
+ # @param [Visitor] visitor the Visitor which will visit synchronized input
304
+ # @yield (see Visitor#sync)
305
+ def initialize(visitor, &matcher)
306
+ # the next node to visit is an array of child node pairs matched by the given matcher block
307
+ super() { |nodes| match_children(visitor, nodes, &matcher) }
308
+ end
309
+
310
+ # Visits the given pair of nodes.
311
+ #
312
+ # Raises ArgumentError if nodes does not consist of either two node arguments or one two-item Array
313
+ # argument.
314
+ def visit(*nodes)
315
+ if nodes.size == 1 then
316
+ nodes = nodes.first
317
+ raise ArgumentError.new("Sync visitor requires a pair of entry nodes") unless nodes.size == 2
318
+ end
319
+ super(nodes)
320
+ end
321
+
322
+ # Returns an Enumerable which applies the given block to each matched node starting at the given nodes.
323
+ #
324
+ # Raises ArgumentError if nodes does not consist of either two node arguments or one two-item Array
325
+ # argument.
326
+ def to_enum(*nodes)
327
+ if nodes.size == 1 then
328
+ nodes = nodes.first
329
+ raise ArgumentError.new("Sync visitor requires a pair of entry nodes") unless nodes.size == 2
330
+ end
331
+ super(nodes)
332
+ end
333
+
334
+ private
335
+
336
+ # Returns an array of arrays of matched children from the given parent nodes. The children are matched
337
+ # using the block given to this method, if supplied, or by index otherwise.
338
+ #
339
+ # @see #sync a usage example
340
+ # @yield (see Visitor#sync)
341
+ def match_children(visitor, nodes)
342
+ # the parent nodes
343
+ p1, p2 = nodes
344
+ # this visitor's children
345
+ c1 = visitor.node_children(p1)
346
+ c2 = p2 ? visitor.node_children(p2) : []
347
+
348
+ # Apply the matcher block on each of this visitor's children and the other children.
349
+ # If no block is given, then group the children by index, which is the transpose of the array of
350
+ # children arrays.
351
+ if block_given? then
352
+ # Match each item in the first children array to an item from the second children array using
353
+ # then given block.
354
+ matches = yield(c1, c2)
355
+ c1.map { |c| [c, matches[c]] }
356
+ else
357
+ # Ensure that both children arrays are the same size.
358
+ others = c2.size <= c1.size ? c2.fill(nil, c2.size...c1.size) : c2[0, c1.size]
359
+ # The children grouped by index is the transpose of the array of children arrays.
360
+ [c1, others].transpose
361
+ end
348
362
  end
349
363
  end
350
364
  end