caruby-core 1.4.2 → 1.4.3

Sign up to get free protection for your applications and to get access to all the features.
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