ntable 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,724 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # NTable table value object
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2012 Daniel Azuma
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are met:
12
+ #
13
+ # * Redistributions of source code must retain the above copyright notice,
14
+ # this list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of the copyright holder, nor the names of any other
19
+ # contributors to this software, may be used to endorse or promote products
20
+ # derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+ # -----------------------------------------------------------------------------
34
+ ;
35
+
36
+
37
+ require 'json'
38
+
39
+
40
+ module NTable
41
+
42
+
43
+ # An N-dimensional table object, comprising structure and values.
44
+
45
+ class Table
46
+
47
+
48
+ # Create a table with the given structure.
49
+ #
50
+ # You can initialize the data using the following hash keys:
51
+ #
52
+ # [<tt>:fill</tt>]
53
+ # Fill all cells with the given value.
54
+ # [<tt>:load</tt>]
55
+ # Load the cell data with the values from the given array, in order.
56
+
57
+ def initialize(structure_, data_={})
58
+ @structure = structure_
59
+ @structure.lock!
60
+ size_ = @structure.size
61
+ if (load_ = data_[:load])
62
+ load_size_ = load_.size
63
+ if load_size_ > size_
64
+ @vals = load_[0, size_]
65
+ elsif load_size_ < size_
66
+ @vals = load_ + ::Array.new(size_ - load_size_, data_[:fill])
67
+ else
68
+ @vals = load_.dup
69
+ end
70
+ elsif (acquire_ = data_[:acquire])
71
+ @vals = acquire_
72
+ else
73
+ @vals = ::Array.new(size_, data_[:fill])
74
+ end
75
+ @offset = data_[:offset].to_i
76
+ @parent = data_[:parent]
77
+ end
78
+
79
+
80
+ def initialize_copy(other_) # :nodoc:
81
+ if other_.parent
82
+ @structure = other_.structure.unlocked_copy
83
+ @structure.lock!
84
+ @vals = other_._compacted_vals
85
+ @offset = 0
86
+ @parent = nil
87
+ else
88
+ initialize(other_.structure, :load => other_.instance_variable_get(:@vals))
89
+ end
90
+ end
91
+
92
+
93
+ # Returns true if the two tables are equivalent, both in the data
94
+ # and in the parentage. The structure of a shared slice is not
95
+ # equivalent, in this sense, to the "same" table created from
96
+ # scratch, because the former is a "sparse" subset of a parent
97
+ # whereas the latter is not.
98
+
99
+ def eql?(rhs_)
100
+ self.equal?(rhs_) ||
101
+ rhs_.is_a?(Table) &&
102
+ @structure.eql?(rhs_.structure) && @parent.eql?(rhs_.parent) &&
103
+ rhs_.instance_variable_get(:@offset) == @offset &&
104
+ rhs_.instance_variable_get(:@vals).eql?(@vals)
105
+ end
106
+
107
+
108
+ # Returns true if the two tables are equivalent in data but not
109
+ # necessarily parentage. The structure of a shared slice may be
110
+ # equivalent, in this sense, to the "same" table created from
111
+ # scratch with no parent.
112
+
113
+ def ==(rhs_)
114
+ if self.equal?(rhs_)
115
+ true
116
+ elsif rhs_.is_a?(Table)
117
+ if rhs_.parent || self.parent
118
+ if @structure == rhs_.structure
119
+ riter_ = rhs_.each
120
+ liter_ = self.each
121
+ @structure.size.times do
122
+ return false unless liter_.next == riter_.next
123
+ end
124
+ true
125
+ else
126
+ false
127
+ end
128
+ else
129
+ rhs_.structure.eql?(@structure) && rhs_.instance_variable_get(:@vals).eql?(@vals)
130
+ end
131
+ else
132
+ false
133
+ end
134
+ end
135
+
136
+
137
+ # The Structure of this table
138
+ attr_reader :structure
139
+
140
+
141
+ # The number of cells in this table.
142
+
143
+ def size
144
+ @structure.size
145
+ end
146
+
147
+
148
+ # The number of dimensions/axes in this table.
149
+
150
+ def dim
151
+ @structure.dim
152
+ end
153
+
154
+
155
+ # True if this table has no cells.
156
+
157
+ def empty?
158
+ @structure.empty?
159
+ end
160
+
161
+
162
+ # True if this is a degenerate (scalar) table with a single cell
163
+ # and no dimensions.
164
+
165
+ def degenerate?
166
+ @structure.degenerate?
167
+ end
168
+
169
+
170
+ # Return the parent of this table. A table with a parent shares the
171
+ # parent's data, and cannot have its data modified directly. Instead,
172
+ # if the parent table is modified, the changes are reflected in the
173
+ # child. Returns nil if this table has no parent.
174
+
175
+ def parent
176
+ @parent
177
+ end
178
+
179
+
180
+ # Returns the value in the cell at the given coordinates, which
181
+ # must be given as labels.
182
+ # You may specify the cell as an array of coordinates, or as a
183
+ # hash mapping axis name to coordinate.
184
+ #
185
+ # For example, for a typical database result set with an axis called
186
+ # "row" of numerically identified rows, and an axis called "col" with
187
+ # string-named columns, these call sequences are equivalent:
188
+ #
189
+ # get(3, 'name')
190
+ # get([3, 'name'])
191
+ # get(:row => 3, :col => 'name')
192
+
193
+ def get(*args_)
194
+ if args_.size == 1
195
+ first_ = args_.first
196
+ args_ = first_ if first_.is_a?(::Hash) || first_.is_a?(::Array)
197
+ end
198
+ offset_ = @structure._offset(args_)
199
+ offset_ ? @vals[@offset + offset_] : nil
200
+ end
201
+ alias_method :[], :get
202
+
203
+
204
+ # Set the value in the cell at the given coordinates. If a block is
205
+ # given, it is passed the current value and expects the new value
206
+ # to be its result. If no block is given, the last argument is taken
207
+ # to be the new value. The remaining arguments identify the cell,
208
+ # using the same syntax as for Table#get.
209
+ #
210
+ # You cannot set a value in a table with a parent. Instead, you must
211
+ # modify the parent, and those changes will be reflected in the child.
212
+
213
+ def set!(*args_, &block_)
214
+ raise TableLockedError if @parent
215
+ value_ = block_ ? nil : args_.pop
216
+ if args_.size == 1
217
+ first_ = args_.first
218
+ args_ = first_ if first_.is_a?(::Hash) || first_.is_a?(::Array)
219
+ end
220
+ offset_ = @structure._offset(args_)
221
+ if offset_
222
+ if block_
223
+ value_ = block_.call(@vals[@offset + offset_])
224
+ end
225
+ @vals[@offset + offset_] = value_
226
+ else
227
+ @missing_value
228
+ end
229
+ end
230
+ alias_method :[]=, :set!
231
+
232
+
233
+ # Load an array of values into the table cells, in order.
234
+ #
235
+ # You cannot load values into a table with a parent. Instead, you must
236
+ # modify the parent, and those changes will be reflected in the child.
237
+
238
+ def load!(vals_)
239
+ raise TableLockedError if @parent
240
+ is_ = vals_.size
241
+ vs_ = @vals.size
242
+ if is_ < vs_
243
+ @vals = vals_.dup + @vals[is_..-1]
244
+ elsif is_ > vs_
245
+ @vals = vals_[0,vs_]
246
+ else
247
+ @vals = vals_.dup
248
+ end
249
+ end
250
+
251
+
252
+ # Fill all table cells with the given value.
253
+ #
254
+ # You cannot load values into a table with a parent. Instead, you must
255
+ # modify the parent, and those changes will be reflected in the child.
256
+
257
+ def fill!(value_)
258
+ raise TableLockedError if @parent
259
+ @vals.fill(value_)
260
+ end
261
+
262
+
263
+ # Iterate over all table cells, in order, and call the given block.
264
+ # If no block is given, an ::Enumerator is returned.
265
+
266
+ def each(&block_)
267
+ if @parent
268
+ if block_given?
269
+ vec_ = ::Array.new(@structure.dim, 0)
270
+ @structure.size.times do
271
+ yield(@vals[@offset + @structure._compute_offset_for_vector(vec_)])
272
+ @structure._inc_vector(vec_)
273
+ end
274
+ else
275
+ enum_for
276
+ end
277
+ else
278
+ @vals.each(&block_)
279
+ end
280
+ end
281
+
282
+
283
+ # Iterate over all table cells, and call the given block with the
284
+ # value and the Structure::Position for the cell.
285
+
286
+ def each_with_position
287
+ vec_ = ::Array.new(@structure.dim, 0)
288
+ @structure.size.times do
289
+ yield(@vals[@offset + @structure._compute_offset_for_vector(vec_)],
290
+ Structure::Position.new(@structure, vec_))
291
+ @structure._inc_vector(vec_)
292
+ end
293
+ self
294
+ end
295
+
296
+
297
+ # Return a new table whose structure is the same as this table, and
298
+ # whose values are given by mapping the current table's values through
299
+ # the given block.
300
+
301
+ def map(&block_)
302
+ if @parent
303
+ vec_ = ::Array.new(@structure.dim, 0)
304
+ nvals_ = (0...@structure.size).map do |i_|
305
+ val_ = yield(@vals[@offset + @structure._compute_offset_for_vector(vec_)])
306
+ @structure._inc_vector(vec_)
307
+ val_
308
+ end
309
+ Table.new(@structure.unlocked_copy, :acquire => nvals_)
310
+ else
311
+ Table.new(@structure, :acquire => @vals.map(&block_))
312
+ end
313
+ end
314
+
315
+
316
+ # Same as Table#map except the block is passed the current table's
317
+ # value for each cell, and the cell's Structure::Position.
318
+
319
+ def map_with_position
320
+ nstructure_ = @structure.parent ? @structure.unlocked_copy : @structure
321
+ vec_ = ::Array.new(@structure.dim, 0)
322
+ nvals_ = (0...@structure.size).map do |i_|
323
+ nval_ = yield(@vals[@offset + @structure._compute_offset_for_vector(vec_)],
324
+ Structure::Position.new(@structure, vec_))
325
+ @structure._inc_vector(vec_)
326
+ nval_
327
+ end
328
+ Table.new(nstructure_, :acquire => nvals_)
329
+ end
330
+
331
+
332
+ # Modify the current table in place, mapping values through the given
333
+ # block.
334
+ #
335
+ # You cannot set values in a table with a parent. Instead, you must
336
+ # modify the parent, and those changes will be reflected in the child.
337
+
338
+ def map!(&block_)
339
+ raise TableLockedError if @parent
340
+ @vals.map!(&block_)
341
+ self
342
+ end
343
+
344
+
345
+ # Modify the current table in place, mapping values through the given
346
+ # block, which takes both the old value and the Structure::Position.
347
+ #
348
+ # You cannot set values in a table with a parent. Instead, you must
349
+ # modify the parent, and those changes will be reflected in the child.
350
+
351
+ def map_with_position!
352
+ raise TableLockedError if @parent
353
+ vec_ = ::Array.new(@structure.dim, 0)
354
+ @vals.map! do |val_|
355
+ nval_ = yield(val_, Structure::Position.new(@structure, vec_))
356
+ @structure._inc_vector(vec_)
357
+ nval_
358
+ end
359
+ self
360
+ end
361
+
362
+
363
+ # Performs a reduce on the entire table and returns the result.
364
+ # You may use one of the following call sequences:
365
+ #
366
+ # [reduce{ |accumulator, value| <i>block</i> }]
367
+ # Reduces using the given block as the reduction function. The
368
+ # first element in the table is used as the initial accumulator.
369
+ # [reduce(initial){ |accumulator, value| <i>block</i> }]
370
+ # Reduces using the given block as the reduction function, with
371
+ # the given initial value for the accumulator.
372
+ # [reduce(:<i>method-name</i>)
373
+ # Reduces using the given binary operator or method name as the
374
+ # reduction function. If it is a method, the method must take a
375
+ # single argument for the right-hand-side of the operation. The
376
+ # first element in the table is used as the initial accumulator.
377
+ # [reduce(<i>initial</i>, :<i>method-name</i>)
378
+ # Reduces using the given binary operator or method name as the
379
+ # reduction function. If it is a method, the method must take a
380
+ # single argument for the right-hand-side of the operation. The
381
+ # given initial accumulator value is used.
382
+
383
+ def reduce(*args_)
384
+ nothing_ = ::Object.new
385
+ if block_given?
386
+ case args_.size
387
+ when 1
388
+ obj_ = args_.first
389
+ when 0
390
+ obj_ = nothing_
391
+ else
392
+ raise ::ArgumentError, "Wrong number of arguments"
393
+ end
394
+ each do |e_|
395
+ if nothing_ == obj_
396
+ obj_ = e_
397
+ else
398
+ obj_ = yield(obj_, e_)
399
+ end
400
+ end
401
+ else
402
+ sym_ = args_.pop
403
+ case args_.size
404
+ when 1
405
+ obj_ = args_.first
406
+ when 0
407
+ obj_ = nothing_
408
+ else
409
+ raise ::ArgumentError, "Wrong number of arguments"
410
+ end
411
+ each do |e_|
412
+ if nothing_ == obj_
413
+ obj_ = e_
414
+ else
415
+ obj_ = obj_.send(sym_, e_)
416
+ end
417
+ end
418
+ end
419
+ nothing_ == obj_ ? nil : obj_
420
+ end
421
+ alias_method :inject, :reduce
422
+
423
+
424
+ # Performs a reduce on the entire table and returns the result.
425
+ # You may use one of the following call sequences:
426
+ #
427
+ # [reduce{ |accumulator, value, position| <i>block</i> }]
428
+ # Reduces using the given block as the reduction function. The
429
+ # first element in the table is used as the initial accumulator.
430
+ # [reduce(initial){ |accumulator, value, position| <i>block</i> }]
431
+ # Reduces using the given block as the reduction function, with
432
+ # the given initial value for the accumulator.
433
+
434
+ def reduce_with_position(*args_)
435
+ nothing_ = ::Object.new
436
+ case args_.size
437
+ when 1
438
+ obj_ = args_.first
439
+ when 0
440
+ obj_ = nothing_
441
+ else
442
+ raise ::ArgumentError, "Wrong number of arguments"
443
+ end
444
+ each_with_position do |val_, pos_|
445
+ if nothing_ == obj_
446
+ obj_ = val_
447
+ else
448
+ obj_ = yield(obj_, val_, pos_)
449
+ end
450
+ end
451
+ nothing_ == obj_ ? nil : obj_
452
+ end
453
+ alias_method :inject_with_position, :reduce_with_position
454
+
455
+
456
+ # Decomposes this table, breaking it into a set of lower-dimensional
457
+ # tables, all arranged in a table. For example, you could decompose
458
+ # a two-dimensional table into a one-dimensional table of one-dimensional
459
+ # tables. You must provide an array of axis specifications (indexes or
460
+ # names) identifying which axes should be part of the lower-dimensional
461
+ # tables.
462
+
463
+ def decompose(*axes_)
464
+ axes_ = axes_.flatten
465
+ axis_indexes_ = []
466
+ axes_.each do |a_|
467
+ if (ainfo_ = @structure.axis_info(a_))
468
+ axis_indexes_ << ainfo_.index
469
+ else
470
+ raise UnknownAxisError, "Unknown axis: #{a_.inspect}"
471
+ end
472
+ end
473
+ inner_struct_ = @structure.substructure_including(axis_indexes_)
474
+ outer_struct_ = @structure.substructure_omitting(axis_indexes_)
475
+ vec_ = ::Array.new(outer_struct_.dim, 0)
476
+ tables_ = (0...outer_struct_.size).map do |i_|
477
+ t_ = Table.new(inner_struct_, :acquire => @vals,
478
+ :offset => outer_struct_._compute_offset_for_vector(vec_),
479
+ :parent => self)
480
+ outer_struct_._inc_vector(vec_)
481
+ t_
482
+ end
483
+ Table.new(outer_struct_.unlocked_copy, :acquire => tables_)
484
+ end
485
+
486
+
487
+ # Decompose this table using the given axes, and then reduce each
488
+ # inner table, returning a table of the reduction values.
489
+
490
+ def decompose_reduce(decompose_axes_, *reduce_args_, &block_)
491
+ decompose(decompose_axes_).map{ |sub_| sub_.reduce(*reduce_args_, &block_) }
492
+ end
493
+
494
+
495
+ # Decompose this table using the given axes, and then reduce each
496
+ # inner table with position, returning a table of the reduction values.
497
+
498
+ def decompose_reduce_with_position(decompose_axes_, *reduce_args_, &block_)
499
+ decompose(decompose_axes_).map{ |sub_| sub_.reduce_with_position(*reduce_args_, &block_) }
500
+ end
501
+
502
+
503
+ # Returns a table containing a "slice" of this table. The given hash
504
+ # should be keyed by axis indexes or axis names, and should provide
505
+ # specific values for zero or more dimensions, which provides the
506
+ # constraints for the slice.
507
+ #
508
+ # Returns a slice table whose parent is this table. Because the slice
509
+ # table has a parent, it is not mutable because it shares data with
510
+ # this table. If this table has values modified, the slice data will
511
+ # reflect those changes.
512
+
513
+ def shared_slice(hash_)
514
+ offset_ = @offset
515
+ select_set_ = {}
516
+ hash_.each do |k_, v_|
517
+ if (ainfo_ = @structure.axis_info(k_))
518
+ aindex_ = ainfo_.index
519
+ unless select_set_.include?(aindex_)
520
+ lindex_ = ainfo_.axis.label_to_index(v_)
521
+ if lindex_
522
+ offset_ += ainfo_.step * lindex_
523
+ select_set_[aindex_] = true
524
+ end
525
+ end
526
+ end
527
+ end
528
+ Table.new(@structure.substructure_omitting(select_set_.keys),
529
+ :acquire => @vals, :offset => offset_, :parent => self)
530
+ end
531
+
532
+
533
+ # Returns a table containing a "slice" of this table. The given hash
534
+ # should be keyed by axis indexes or axis names, and should provide
535
+ # specific values for zero or more dimensions, which provides the
536
+ # constraints for the slice.
537
+ #
538
+ # Returns a new table independent of this table. The new table can
539
+ # have cell values modified independently of this table.
540
+
541
+ def slice(hash_)
542
+ shared_slice(hash_).dup
543
+ end
544
+
545
+
546
+ # Returns a JSON serialization of this table, as an object. If you
547
+ # need to output a JSON string, you must unparse separately.
548
+
549
+ def to_json_object
550
+ {'type' => 'ntable', 'axes' => @structure.to_json_array, 'values' => @parent ? _compacted_vals : @vals}
551
+ end
552
+
553
+
554
+ # Returns a JSON serialization of this table, as an unparsed string.
555
+
556
+ def to_json
557
+ to_json_object.to_json
558
+ end
559
+
560
+
561
+ # Returns a nested-object (nested arrays and hashes) serialization
562
+ # of this table.
563
+
564
+ def to_nested_object(opts_={})
565
+ if @structure.degenerate?
566
+ @vals[@offset]
567
+ else
568
+ _to_nested_obj(0, ::Array.new(@structure.dim, 0), opts_)
569
+ end
570
+ end
571
+
572
+
573
+ def _to_nested_obj(aidx_, vec_, opts_) # :nodoc:
574
+ exclude_ = opts_.include?(:exclude_value)
575
+ exclude_value_ = opts_[:exclude_value] if exclude_
576
+ axis_ = @structure.axis_info(aidx_).axis
577
+ result_ = IndexedAxis === axis_ ? [] : {}
578
+ (0...axis_.size).map do |i_|
579
+ vec_[aidx_] = i_
580
+ val_ = if aidx_ + 1 == vec_.size
581
+ @vals[@offset + @structure._compute_offset_for_vector(vec_)]
582
+ else
583
+ _to_nested_obj(aidx_ + 1, vec_, opts_)
584
+ end
585
+ if !exclude_ || !val_.eql?(exclude_value_)
586
+ result_[axis_.index_to_label(i_)] = val_
587
+ end
588
+ end
589
+ result_
590
+ end
591
+
592
+
593
+ def _compacted_vals # :nodoc:
594
+ vec_ = ::Array.new(@structure.dim, 0)
595
+ ::Array.new(@structure.size) do
596
+ val_ = @vals[@offset + @structure._compute_offset_for_vector(vec_)]
597
+ @structure._inc_vector(vec_)
598
+ val_
599
+ end
600
+ end
601
+
602
+
603
+ @numeric_sort = ::Proc.new{ |a_, b_| a_.to_f <=> b_.to_f }
604
+
605
+
606
+ class << self
607
+
608
+
609
+ # Construct a table given a JSON object representation.
610
+
611
+ def from_json_object(json_)
612
+ new(Structure.from_json_array(json_['axes'] || []), :load => json_['values'] || [])
613
+ end
614
+
615
+
616
+ # Construct a table given a JSON unparsed string representation.
617
+
618
+ def parse_json(json_)
619
+ from_json_object(::JSON.parse(json_))
620
+ end
621
+
622
+
623
+ # Construct a table given nested hashes and arrays.
624
+
625
+ def from_nested_object(obj_, field_opts_=[], opts_={})
626
+ axis_data_ = []
627
+ _populate_nested_axes(axis_data_, 0, obj_)
628
+ struct_ = Structure.new
629
+ axis_data_.each_with_index do |ai_, i_|
630
+ field_ = field_opts_[i_] || {}
631
+ axis_ = nil
632
+ name_ = field_[:name]
633
+ case ai_
634
+ when ::Hash
635
+ labels_ = ai_.keys
636
+ if (sort_ = field_[:sort])
637
+ if sort_.respond_to?(:call)
638
+ func_ = sort_
639
+ elsif sort_ == :numeric
640
+ func_ = @numeric_sort
641
+ else
642
+ func_ = nil
643
+ end
644
+ labels_.sort!(&func_)
645
+ end
646
+ if (xform_ = field_[:transform])
647
+ labels_.map!(&xform_)
648
+ end
649
+ axis_ = LabeledAxis.new(labels_)
650
+ when ::Array
651
+ axis_ = IndexedAxis.new(ai_[1].to_i - ai_[0].to_i, ai_[0].to_i)
652
+ end
653
+ struct_.add(axis_, name_) if axis_
654
+ end
655
+ table_ = new(struct_, :fill => opts_[:fill])
656
+ _populate_nested_values(table_, [], obj_)
657
+ table_
658
+ end
659
+
660
+
661
+ def _populate_nested_axes(axis_data_, index_, obj_) # :nodoc:
662
+ ai_ = axis_data_[index_]
663
+ case obj_
664
+ when ::Hash
665
+ if ::Hash === ai_
666
+ set_ = ai_
667
+ else
668
+ set_ = axis_data_[index_] = {}
669
+ (ai_[0]...ai_[1]).each{ |i_| set_[i_.to_s] = true } if ::Array === ai_
670
+ end
671
+ obj_.each do |k_, v_|
672
+ set_[k_.to_s] = true
673
+ _populate_nested_axes(axis_data_, index_+1, v_)
674
+ end
675
+ when ::Array
676
+ if ::Hash === ai_
677
+ obj_.each_with_index do |v_, i_|
678
+ ai_[i_.to_s] = true
679
+ _populate_nested_axes(axis_data_, index_+1, v_)
680
+ end
681
+ else
682
+ s_ = obj_.size
683
+ if ::Array === ai_
684
+ if s_ > 0
685
+ ai_[1] = s_ if !ai_[1] || s_ > ai_[1]
686
+ ai_[0] = s_ if !ai_[0]
687
+ end
688
+ else
689
+ ai_ = axis_data_[index_] = (s_ == 0 ? [nil, nil] : [s_, s_])
690
+ end
691
+ obj_.each_with_index do |v_, i_|
692
+ ai_[0] = i_ if ai_[0] > i_ && !v_.nil?
693
+ _populate_nested_axes(axis_data_, index_+1, v_)
694
+ end
695
+ end
696
+ end
697
+ end
698
+
699
+
700
+ def _populate_nested_values(table_, path_, obj_) # :nodoc:
701
+ if path_.size == table_.dim
702
+ table_.set!(*path_, obj_)
703
+ else
704
+ case obj_
705
+ when ::Hash
706
+ obj_.each do |k_, v_|
707
+ _populate_nested_values(table_, path_ + [k_.to_s], v_)
708
+ end
709
+ when ::Array
710
+ obj_.each_with_index do |v_, i_|
711
+ _populate_nested_values(table_, path_ + [i_], v_) unless v_.nil?
712
+ end
713
+ end
714
+ end
715
+ end
716
+
717
+
718
+ end
719
+
720
+
721
+ end
722
+
723
+
724
+ end