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.
- data/History.rdoc +3 -0
- data/README.rdoc +73 -0
- data/Version +1 -0
- data/lib/ntable.rb +46 -0
- data/lib/ntable/axis.rb +220 -0
- data/lib/ntable/errors.rb +75 -0
- data/lib/ntable/structure.rb +625 -0
- data/lib/ntable/table.rb +724 -0
- data/test/tc_axes.rb +124 -0
- data/test/tc_basic_values.rb +129 -0
- data/test/tc_decompose.rb +104 -0
- data/test/tc_enumeration.rb +238 -0
- data/test/tc_json.rb +137 -0
- data/test/tc_nested_object.rb +191 -0
- data/test/tc_reduce.rb +161 -0
- data/test/tc_slice.rb +130 -0
- data/test/tc_structure.rb +202 -0
- metadata +78 -0
data/lib/ntable/table.rb
ADDED
@@ -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
|