ntable 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.rdoc +10 -0
- data/Version +1 -1
- data/lib/ntable.rb +57 -0
- data/lib/ntable/axis.rb +65 -3
- data/lib/ntable/construction.rb +277 -0
- data/lib/ntable/errors.rb +6 -0
- data/lib/ntable/structure.rb +39 -5
- data/lib/ntable/table.rb +38 -138
- data/test/tc_axes.rb +40 -1
- data/test/tc_basic_values.rb +27 -1
- data/test/tc_nested_object.rb +14 -0
- metadata +4 -3
data/History.rdoc
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
=== 0.1.1 / 2012-09-07
|
2
|
+
|
3
|
+
* The "preferred" construction methods are now module methods on ::NTable.
|
4
|
+
* Added an ObjectAxis type that lets labels be any arbitrary object.
|
5
|
+
* NTable.from_nested_object now understands :objectify and :stringify options. These control whether the axis ends up as an ObjectAxis or LabeledAxis, and optionally let you provide a proc to convert the labels yourself.
|
6
|
+
* INCOMPATIBLE CHANGE: Table#get and Table#set! now raise NoSuchCellError if the cell specification doesn't exist.
|
7
|
+
* Added Table#include? to test for the correctness of a cell specification.
|
8
|
+
# Table#set!, Table#load!, and Table#fill! now return self so they can be chained.
|
9
|
+
* Filled out some more documentation.
|
10
|
+
|
1
11
|
=== 0.1.0 / 2012-09-01
|
2
12
|
|
3
13
|
* Initial test release
|
data/Version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.1
|
data/lib/ntable.rb
CHANGED
@@ -35,6 +35,62 @@
|
|
35
35
|
|
36
36
|
|
37
37
|
# NTable is an N-dimensional table data structure for Ruby.
|
38
|
+
#
|
39
|
+
# == Basics
|
40
|
+
#
|
41
|
+
# This is a convenient data structure for storing tabular data of
|
42
|
+
# arbitrary dimensionality. An NTable can represent zero-dimensional data
|
43
|
+
# (i.e. a simple scalar value), one-dimensional data (i.e. an array or
|
44
|
+
# dictionary), a two-dimensional table such as a database result set or
|
45
|
+
# spreadsheet, or any number of higher dimensions.
|
46
|
+
#
|
47
|
+
# The structure of the table is defined explicitly. Each dimension is
|
48
|
+
# represented by an axis, which describes how many "rows" the table has
|
49
|
+
# in that dimension, and how each row is labeled. For example, you could
|
50
|
+
# have a "numeric" indexed axis whose rows are identified by indexes.
|
51
|
+
# Or you could have a "string" labeled axis identified by names (e.g.
|
52
|
+
# columns in a database.)
|
53
|
+
#
|
54
|
+
# For example, a typical two-dimensional spreadsheet would have
|
55
|
+
# numerically-identified "rows", and columns identified by name. You might
|
56
|
+
# describe the structure of the table with two axes, the major one a
|
57
|
+
# numeric indexed axis, and the minor one a string labeled axis. In code,
|
58
|
+
# such a table with 100 rows and two columns could be created like this:
|
59
|
+
#
|
60
|
+
# table = NTable.structure(NTable::IndexedAxis.new(100)).
|
61
|
+
# add(NTable::LabeledAxis(:name, :address)).
|
62
|
+
# create
|
63
|
+
#
|
64
|
+
# You can then look up individual cells like this:
|
65
|
+
#
|
66
|
+
# value = table[10, :address]
|
67
|
+
#
|
68
|
+
# Axes can be given names as well:
|
69
|
+
#
|
70
|
+
# table = NTable.structure(NTable::IndexedAxis.new(100), :row).
|
71
|
+
# add(NTable::LabeledAxis(:name, :address), :col).
|
72
|
+
# create
|
73
|
+
#
|
74
|
+
# Then you can specify the axes by name when you look up:
|
75
|
+
#
|
76
|
+
# value = table[:row => 10, :col => :address]
|
77
|
+
#
|
78
|
+
# You can use the same syntax to set data:
|
79
|
+
#
|
80
|
+
# table[10, :address] = "123 Main Street"
|
81
|
+
# table[:row => 10, :col => :address] = "123 Main Street"
|
82
|
+
#
|
83
|
+
# == Iterating
|
84
|
+
#
|
85
|
+
# (to be written)
|
86
|
+
#
|
87
|
+
# == Slicing and decomposition
|
88
|
+
#
|
89
|
+
# (to be written)
|
90
|
+
#
|
91
|
+
# == Serialization
|
92
|
+
#
|
93
|
+
# (to be written)
|
38
94
|
|
39
95
|
module NTable
|
40
96
|
end
|
@@ -44,3 +100,4 @@ require 'ntable/errors'
|
|
44
100
|
require 'ntable/axis'
|
45
101
|
require 'ntable/structure'
|
46
102
|
require 'ntable/table'
|
103
|
+
require 'ntable/construction'
|
data/lib/ntable/axis.rb
CHANGED
@@ -114,7 +114,8 @@ module NTable
|
|
114
114
|
# Create a LabeledAxis given an array of the label strings.
|
115
115
|
# Symbols may also be provided, but will be converted to strings.
|
116
116
|
|
117
|
-
def initialize(labels_)
|
117
|
+
def initialize(*labels_)
|
118
|
+
labels_ = labels_.flatten
|
118
119
|
@a = labels_.map{ |label_| label_.to_s }
|
119
120
|
@h = {}
|
120
121
|
@a.each_with_index{ |n_, i_| @h[n_] = i_ }
|
@@ -123,9 +124,12 @@ module NTable
|
|
123
124
|
|
124
125
|
|
125
126
|
def eql?(obj_)
|
126
|
-
obj_.is_a?(LabeledAxis) && obj_.instance_variable_get(:@a)
|
127
|
+
obj_.is_a?(LabeledAxis) && @a.eql?(obj_.instance_variable_get(:@a))
|
128
|
+
end
|
129
|
+
|
130
|
+
def ==(obj_)
|
131
|
+
obj_.is_a?(LabeledAxis) && @a == obj_.instance_variable_get(:@a)
|
127
132
|
end
|
128
|
-
alias_method :==, :eql?
|
129
133
|
|
130
134
|
def hash
|
131
135
|
@a.hash
|
@@ -217,4 +221,62 @@ module NTable
|
|
217
221
|
end
|
218
222
|
|
219
223
|
|
224
|
+
# An axis in which the labels are arbitrary objects.
|
225
|
+
# This axis cannot be serialized.
|
226
|
+
|
227
|
+
class ObjectAxis
|
228
|
+
|
229
|
+
|
230
|
+
# Create a ObjectAxis given an array of the label objects.
|
231
|
+
|
232
|
+
def initialize(labels_)
|
233
|
+
@a = labels_.dup
|
234
|
+
@h = {}
|
235
|
+
@a.each_with_index{ |n_, i_| @h[n_] = i_ }
|
236
|
+
@size = labels_.size
|
237
|
+
end
|
238
|
+
|
239
|
+
|
240
|
+
def eql?(obj_)
|
241
|
+
obj_.is_a?(ObjectAxis) && @a.eql?(obj_.instance_variable_get(:@a))
|
242
|
+
end
|
243
|
+
|
244
|
+
def ==(obj_)
|
245
|
+
obj_.is_a?(ObjectAxis) && @a == obj_.instance_variable_get(:@a)
|
246
|
+
end
|
247
|
+
|
248
|
+
def hash
|
249
|
+
@a.hash
|
250
|
+
end
|
251
|
+
|
252
|
+
def inspect
|
253
|
+
"#<#{self.class}:0x#{object_id.to_s(16)} #{@a.inspect}>"
|
254
|
+
end
|
255
|
+
alias_method :to_s, :inspect
|
256
|
+
|
257
|
+
|
258
|
+
attr_reader :size
|
259
|
+
|
260
|
+
|
261
|
+
def label_to_index(label_)
|
262
|
+
@h[label_]
|
263
|
+
end
|
264
|
+
|
265
|
+
def index_to_label(index_)
|
266
|
+
@a[index_]
|
267
|
+
end
|
268
|
+
|
269
|
+
|
270
|
+
def to_json_object(json_obj_)
|
271
|
+
raise "Unable to JSON serialize an ObjectAxis"
|
272
|
+
end
|
273
|
+
|
274
|
+
def from_json_object(json_obj_)
|
275
|
+
raise "Unable to JSON serialize an ObjectAxis"
|
276
|
+
end
|
277
|
+
|
278
|
+
|
279
|
+
end
|
280
|
+
|
281
|
+
|
220
282
|
end
|
@@ -0,0 +1,277 @@
|
|
1
|
+
# -----------------------------------------------------------------------------
|
2
|
+
#
|
3
|
+
# NTable constructors
|
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
|
+
@numeric_sort = ::Proc.new{ |a_, b_| a_.to_f <=> b_.to_f }
|
44
|
+
|
45
|
+
|
46
|
+
class << self
|
47
|
+
|
48
|
+
|
49
|
+
# Create and return a new Structure.
|
50
|
+
#
|
51
|
+
# If you pass the optional axis argument, that axis will be added
|
52
|
+
# to the structure.
|
53
|
+
#
|
54
|
+
# The most convenient way to create a table is probably to chain
|
55
|
+
# methods off this method. For example:
|
56
|
+
#
|
57
|
+
# NTable.structure(NTable::IndexedAxis.new(10)).
|
58
|
+
# add(NTable::LabeledAxis.new(:column1, :column2)).
|
59
|
+
# create(:fill => 0)
|
60
|
+
|
61
|
+
def structure(axis_=nil, name_=nil)
|
62
|
+
axis_ ? Structure.add(axis_, name_) : Structure.new
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
# Create a table with the given Structure.
|
67
|
+
#
|
68
|
+
# You can initialize the data using the following options:
|
69
|
+
#
|
70
|
+
# [<tt>:fill</tt>]
|
71
|
+
# Fill all cells with the given value.
|
72
|
+
# [<tt>:load</tt>]
|
73
|
+
# Load the cell data with the values from the given array, in order.
|
74
|
+
|
75
|
+
def create(structure_, data_={})
|
76
|
+
Table.new(structure_, data_)
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
# Construct a table given a JSON object representation.
|
81
|
+
|
82
|
+
def from_json_object(json_)
|
83
|
+
Table.new(Structure.from_json_array(json_['axes'] || []), :load => json_['values'] || [])
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
# Construct a table given a JSON unparsed string representation.
|
88
|
+
|
89
|
+
def parse_json(json_)
|
90
|
+
from_json_object(::JSON.parse(json_))
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
# Construct a table given nested hashes and arrays.
|
95
|
+
#
|
96
|
+
# The second argument is an array of hashes, providing options for
|
97
|
+
# the axes in order. Recognized keys in these hashes include:
|
98
|
+
#
|
99
|
+
# [<tt>:name</tt>]
|
100
|
+
# The name of the axis, as a string or symbol
|
101
|
+
# [<tt>:sort</tt>]
|
102
|
+
# The sort strategy. You can provide a callable object such as a
|
103
|
+
# Proc, or one of the constants <tt>:numeric</tt> or
|
104
|
+
# <tt>:string</tt>. If you omit this key or set it to false, no
|
105
|
+
# sort is done on the labels for this axis.
|
106
|
+
# [<tt>:objectify</tt>]
|
107
|
+
# An optional Proc that modifies the labels. The Proc should take
|
108
|
+
# a single argument and return the new label. If an objectify
|
109
|
+
# proc is provided, the resulting axis will be an ObjectAxis.
|
110
|
+
# You can also pass true instead of a Proc; this will create an
|
111
|
+
# ObjectAxis and make the conversion a nop.
|
112
|
+
# [<tt>:stringify</tt>]
|
113
|
+
# An optional Proc that modifies the labels. The Proc should take
|
114
|
+
# a single argument and return the new label, which will then be
|
115
|
+
# converted to a string if it isn't one already. If a stringify
|
116
|
+
# proc is provided, the resulting axis will be a LabeledAxis.
|
117
|
+
# You can also pass true instead of a Proc; this will create an
|
118
|
+
# LabeledAxis and make the conversion a simple to_s.
|
119
|
+
#
|
120
|
+
# The third argument is an optional hash of miscellaneous options.
|
121
|
+
# The following keys are recognized:
|
122
|
+
#
|
123
|
+
# [<tt>:fill</tt>]
|
124
|
+
# Fill all cells not explicitly set, with the given value.
|
125
|
+
# Default is nil.
|
126
|
+
# [<tt>:objectify_by_default</tt>]
|
127
|
+
# By default, all hash-created axes are LabeledAxis unless an
|
128
|
+
# <tt>:objectify</tt> field option is explicitly provided. This
|
129
|
+
# option, if true, reverses this behavior. You can pass true, or
|
130
|
+
# a Proc that transforms the label.
|
131
|
+
# [<tt>:stringify_by_default</tt>]
|
132
|
+
# If set to a Proc, this Proc is used as the default stringification
|
133
|
+
# routine for converting labels for a LabeledAxis.
|
134
|
+
|
135
|
+
def from_nested_object(obj_, field_opts_=[], opts_={})
|
136
|
+
axis_data_ = []
|
137
|
+
_populate_nested_axes(axis_data_, 0, obj_)
|
138
|
+
objectify_by_default_ = opts_[:objectify_by_default]
|
139
|
+
stringify_by_default_ = opts_[:stringify_by_default]
|
140
|
+
struct_ = Structure.new
|
141
|
+
axis_data_.each_with_index do |ai_, i_|
|
142
|
+
field_ = field_opts_[i_] || {}
|
143
|
+
axis_ = nil
|
144
|
+
name_ = field_[:name]
|
145
|
+
case ai_
|
146
|
+
when ::Hash
|
147
|
+
objectify_ = field_[:objectify]
|
148
|
+
stringify_ = field_[:stringify] || stringify_by_default_
|
149
|
+
objectify_ ||= objectify_by_default_ unless stringify_
|
150
|
+
if objectify_
|
151
|
+
labels_ = ai_.keys
|
152
|
+
labels_.map!(&objectify_) if objectify_.respond_to?(:call)
|
153
|
+
klass_ = ObjectAxis
|
154
|
+
else
|
155
|
+
h_ = {}
|
156
|
+
stringify_ = nil unless stringify_.respond_to?(:call)
|
157
|
+
ai_.each do |k_, v_|
|
158
|
+
k_ = stringify_.call(k_) if stringify_
|
159
|
+
h_[k_.to_s] = true
|
160
|
+
end
|
161
|
+
labels_ = h_.keys
|
162
|
+
klass_ = LabeledAxis
|
163
|
+
end
|
164
|
+
if (sort_ = field_[:sort])
|
165
|
+
if sort_.respond_to?(:call)
|
166
|
+
func_ = sort_
|
167
|
+
elsif sort_ == :numeric
|
168
|
+
func_ = @numeric_sort
|
169
|
+
else
|
170
|
+
func_ = nil
|
171
|
+
end
|
172
|
+
labels_.sort!(&func_)
|
173
|
+
end
|
174
|
+
axis_ = klass_.new(labels_)
|
175
|
+
when ::Array
|
176
|
+
axis_ = IndexedAxis.new(ai_[1].to_i - ai_[0].to_i, ai_[0].to_i)
|
177
|
+
end
|
178
|
+
struct_.add(axis_, name_) if axis_
|
179
|
+
end
|
180
|
+
table_ = Table.new(struct_, :fill => opts_[:fill])
|
181
|
+
_populate_nested_values(table_, [], obj_)
|
182
|
+
table_
|
183
|
+
end
|
184
|
+
|
185
|
+
|
186
|
+
def _populate_nested_axes(axis_data_, index_, obj_) # :nodoc:
|
187
|
+
ai_ = axis_data_[index_]
|
188
|
+
case obj_
|
189
|
+
when ::Hash
|
190
|
+
if ::Hash === ai_
|
191
|
+
set_ = ai_
|
192
|
+
else
|
193
|
+
set_ = axis_data_[index_] = {}
|
194
|
+
(ai_[0]...ai_[1]).each{ |i_| set_[i_] = true } if ::Array === ai_
|
195
|
+
end
|
196
|
+
obj_.each do |k_, v_|
|
197
|
+
set_[k_] = true
|
198
|
+
_populate_nested_axes(axis_data_, index_+1, v_)
|
199
|
+
end
|
200
|
+
when ::Array
|
201
|
+
if ::Hash === ai_
|
202
|
+
obj_.each_with_index do |v_, i_|
|
203
|
+
ai_[i_] = true
|
204
|
+
_populate_nested_axes(axis_data_, index_+1, v_)
|
205
|
+
end
|
206
|
+
else
|
207
|
+
s_ = obj_.size
|
208
|
+
if ::Array === ai_
|
209
|
+
if s_ > 0
|
210
|
+
ai_[1] = s_ if !ai_[1] || s_ > ai_[1]
|
211
|
+
ai_[0] = s_ if !ai_[0]
|
212
|
+
end
|
213
|
+
else
|
214
|
+
ai_ = axis_data_[index_] = (s_ == 0 ? [nil, nil] : [s_, s_])
|
215
|
+
end
|
216
|
+
obj_.each_with_index do |v_, i_|
|
217
|
+
ai_[0] = i_ if ai_[0] > i_ && !v_.nil?
|
218
|
+
_populate_nested_axes(axis_data_, index_+1, v_)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
def _populate_nested_values(table_, path_, obj_) # :nodoc:
|
226
|
+
if path_.size == table_.dim
|
227
|
+
table_.set!(*path_, obj_)
|
228
|
+
else
|
229
|
+
case obj_
|
230
|
+
when ::Hash
|
231
|
+
obj_.each do |k_, v_|
|
232
|
+
_populate_nested_values(table_, path_ + [k_], v_)
|
233
|
+
end
|
234
|
+
when ::Array
|
235
|
+
obj_.each_with_index do |v_, i_|
|
236
|
+
_populate_nested_values(table_, path_ + [i_], v_) unless v_.nil?
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
|
243
|
+
end
|
244
|
+
|
245
|
+
|
246
|
+
class Table
|
247
|
+
|
248
|
+
class << self
|
249
|
+
|
250
|
+
|
251
|
+
# Deprecated synonym for ::NTable.from_json_object
|
252
|
+
|
253
|
+
def from_json_object(json_)
|
254
|
+
::NTable.from_json_object(json_)
|
255
|
+
end
|
256
|
+
|
257
|
+
|
258
|
+
# Deprecated synonym for ::NTable.parse_json
|
259
|
+
|
260
|
+
def parse_json(json_)
|
261
|
+
::NTable.parse_json(json_)
|
262
|
+
end
|
263
|
+
|
264
|
+
|
265
|
+
# Deprecated synonym for ::NTable.from_nested_object
|
266
|
+
|
267
|
+
def from_nested_object(obj_, field_opts_=[], opts_={})
|
268
|
+
::NTable.from_nested_object(obj_, field_opts_, opts_)
|
269
|
+
end
|
270
|
+
|
271
|
+
|
272
|
+
end
|
273
|
+
|
274
|
+
end
|
275
|
+
|
276
|
+
|
277
|
+
end
|
data/lib/ntable/errors.rb
CHANGED
data/lib/ntable/structure.rb
CHANGED
@@ -412,11 +412,19 @@ module NTable
|
|
412
412
|
end
|
413
413
|
|
414
414
|
|
415
|
+
# Create a new substructure of this structure. The new structure
|
416
|
+
# has this structure as its parent, but includes only the given
|
417
|
+
# axes, which can be provided as an array of axis names or indexes.
|
418
|
+
|
415
419
|
def substructure_including(*axes_)
|
416
420
|
_substructure(axes_.flatten, true)
|
417
421
|
end
|
418
422
|
|
419
423
|
|
424
|
+
# Create a new substructure of this structure. The new structure
|
425
|
+
# has this structure as its parent, but includes all axes EXCEPT the
|
426
|
+
# given axes, provided as an array of axis names or indexes.
|
427
|
+
|
420
428
|
def substructure_omitting(*axes_)
|
421
429
|
_substructure(axes_.flatten, false)
|
422
430
|
end
|
@@ -469,6 +477,21 @@ module NTable
|
|
469
477
|
end
|
470
478
|
|
471
479
|
|
480
|
+
# Create a new table using this structure as the structure.
|
481
|
+
# Note that this also has the side effect of locking this structure.
|
482
|
+
#
|
483
|
+
# You can initialize the data using the following options:
|
484
|
+
#
|
485
|
+
# [<tt>:fill</tt>]
|
486
|
+
# Fill all cells with the given value.
|
487
|
+
# [<tt>:load</tt>]
|
488
|
+
# Load the cell data with the values from the given array, in order.
|
489
|
+
|
490
|
+
def create(data_={})
|
491
|
+
Table.new(self, data_)
|
492
|
+
end
|
493
|
+
|
494
|
+
|
472
495
|
def _substructure(axes_, bool_) # :nodoc:
|
473
496
|
raise StructureStateError, "Structure not locked" unless @locked
|
474
497
|
sub_ = Structure.new
|
@@ -609,13 +632,24 @@ module NTable
|
|
609
632
|
end
|
610
633
|
|
611
634
|
|
612
|
-
|
613
|
-
|
614
|
-
|
635
|
+
class << self
|
636
|
+
|
637
|
+
|
638
|
+
# Create a new structure and automatically add the given axis.
|
639
|
+
# See Structure#add.
|
640
|
+
|
641
|
+
def add(axis_, name_=nil)
|
642
|
+
self.new.add(axis_, name_)
|
643
|
+
end
|
644
|
+
|
645
|
+
|
646
|
+
# Deserialize a structure from the given JSON array
|
647
|
+
|
648
|
+
def from_json_array(array_)
|
649
|
+
self.new.from_json_array(array_)
|
650
|
+
end
|
615
651
|
|
616
652
|
|
617
|
-
def self.from_json_array(array_)
|
618
|
-
self.new.from_json_array(array_)
|
619
653
|
end
|
620
654
|
|
621
655
|
|
data/lib/ntable/table.rb
CHANGED
@@ -45,14 +45,8 @@ module NTable
|
|
45
45
|
class Table
|
46
46
|
|
47
47
|
|
48
|
-
#
|
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.
|
48
|
+
# This is a low-level table creation mechanism.
|
49
|
+
# Generally, you should use ::NTable.create instead.
|
56
50
|
|
57
51
|
def initialize(structure_, data_={})
|
58
52
|
@structure = structure_
|
@@ -189,18 +183,27 @@ module NTable
|
|
189
183
|
# get(3, 'name')
|
190
184
|
# get([3, 'name'])
|
191
185
|
# get(:row => 3, :col => 'name')
|
186
|
+
#
|
187
|
+
# Raises NoSuchCellError if the coordinates do not exist.
|
192
188
|
|
193
189
|
def get(*args_)
|
194
|
-
|
195
|
-
|
196
|
-
|
190
|
+
offset_ = _offset_for_args(args_)
|
191
|
+
unless offset_
|
192
|
+
raise NoSuchCellError
|
197
193
|
end
|
198
|
-
|
199
|
-
offset_ ? @vals[@offset + offset_] : nil
|
194
|
+
@vals[@offset + offset_]
|
200
195
|
end
|
201
196
|
alias_method :[], :get
|
202
197
|
|
203
198
|
|
199
|
+
# Returns a boolean indicating whether the given cell coordinates
|
200
|
+
# actually exist. The arguments use the same syntax as for Table#get.
|
201
|
+
|
202
|
+
def include?(*args_)
|
203
|
+
_offset_for_args(args_) ? true : false
|
204
|
+
end
|
205
|
+
|
206
|
+
|
204
207
|
# Set the value in the cell at the given coordinates. If a block is
|
205
208
|
# given, it is passed the current value and expects the new value
|
206
209
|
# to be its result. If no block is given, the last argument is taken
|
@@ -209,23 +212,23 @@ module NTable
|
|
209
212
|
#
|
210
213
|
# You cannot set a value in a table with a parent. Instead, you must
|
211
214
|
# modify the parent, and those changes will be reflected in the child.
|
215
|
+
#
|
216
|
+
# Raises NoSuchCellError if the coordinates do not exist.
|
217
|
+
#
|
218
|
+
# Returns self so calls can be chained.
|
212
219
|
|
213
220
|
def set!(*args_, &block_)
|
214
221
|
raise TableLockedError if @parent
|
215
222
|
value_ = block_ ? nil : args_.pop
|
216
|
-
|
217
|
-
|
218
|
-
|
223
|
+
offset_ = _offset_for_args(args_)
|
224
|
+
unless offset_
|
225
|
+
raise NoSuchCellError
|
219
226
|
end
|
220
|
-
|
221
|
-
|
222
|
-
if block_
|
223
|
-
value_ = block_.call(@vals[@offset + offset_])
|
224
|
-
end
|
225
|
-
@vals[@offset + offset_] = value_
|
226
|
-
else
|
227
|
-
@missing_value
|
227
|
+
if block_
|
228
|
+
value_ = block_.call(@vals[@offset + offset_])
|
228
229
|
end
|
230
|
+
@vals[@offset + offset_] = value_
|
231
|
+
self
|
229
232
|
end
|
230
233
|
alias_method :[]=, :set!
|
231
234
|
|
@@ -234,6 +237,8 @@ module NTable
|
|
234
237
|
#
|
235
238
|
# You cannot load values into a table with a parent. Instead, you must
|
236
239
|
# modify the parent, and those changes will be reflected in the child.
|
240
|
+
#
|
241
|
+
# Returns self so calls can be chained.
|
237
242
|
|
238
243
|
def load!(vals_)
|
239
244
|
raise TableLockedError if @parent
|
@@ -246,6 +251,7 @@ module NTable
|
|
246
251
|
else
|
247
252
|
@vals = vals_.dup
|
248
253
|
end
|
254
|
+
self
|
249
255
|
end
|
250
256
|
|
251
257
|
|
@@ -253,10 +259,13 @@ module NTable
|
|
253
259
|
#
|
254
260
|
# You cannot load values into a table with a parent. Instead, you must
|
255
261
|
# modify the parent, and those changes will be reflected in the child.
|
262
|
+
#
|
263
|
+
# Returns self so calls can be chained.
|
256
264
|
|
257
265
|
def fill!(value_)
|
258
266
|
raise TableLockedError if @parent
|
259
267
|
@vals.fill(value_)
|
268
|
+
self
|
260
269
|
end
|
261
270
|
|
262
271
|
|
@@ -600,121 +609,12 @@ module NTable
|
|
600
609
|
end
|
601
610
|
|
602
611
|
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
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
|
612
|
+
def _offset_for_args(args_)
|
613
|
+
if args_.size == 1
|
614
|
+
first_ = args_.first
|
615
|
+
args_ = first_ if first_.is_a?(::Hash) || first_.is_a?(::Array)
|
715
616
|
end
|
716
|
-
|
717
|
-
|
617
|
+
@structure._offset(args_)
|
718
618
|
end
|
719
619
|
|
720
620
|
|
data/test/tc_axes.rb
CHANGED
@@ -54,6 +54,7 @@ module NTable
|
|
54
54
|
axis_ = LabeledAxis.new([:one, :two])
|
55
55
|
assert_equal(0, axis_.label_to_index(:one))
|
56
56
|
assert_equal(1, axis_.label_to_index(:two))
|
57
|
+
assert_equal(0, axis_.label_to_index('one'))
|
57
58
|
assert_nil(axis_.label_to_index(:three))
|
58
59
|
end
|
59
60
|
|
@@ -68,7 +69,7 @@ module NTable
|
|
68
69
|
|
69
70
|
def test_labeled_axis_equality
|
70
71
|
axis1_ = LabeledAxis.new([:one, :two])
|
71
|
-
axis2_ = LabeledAxis.new([:one,
|
72
|
+
axis2_ = LabeledAxis.new([:one, 'two'])
|
72
73
|
axis3_ = LabeledAxis.new([:one, :three])
|
73
74
|
assert_equal(axis1_, axis2_)
|
74
75
|
refute_equal(axis1_, axis3_)
|
@@ -81,6 +82,44 @@ module NTable
|
|
81
82
|
end
|
82
83
|
|
83
84
|
|
85
|
+
def test_object_axis_size
|
86
|
+
axis_ = ObjectAxis.new([:one, :two])
|
87
|
+
assert_equal(2, axis_.size)
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
def test_object_axis_label_to_index
|
92
|
+
axis_ = ObjectAxis.new([:one, :two])
|
93
|
+
assert_equal(0, axis_.label_to_index(:one))
|
94
|
+
assert_equal(1, axis_.label_to_index(:two))
|
95
|
+
assert_nil(axis_.label_to_index('one'))
|
96
|
+
assert_nil(axis_.label_to_index(:three))
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
def test_object_axis_index_to_label
|
101
|
+
axis_ = ObjectAxis.new([:one, :two])
|
102
|
+
assert_equal(:one, axis_.index_to_label(0))
|
103
|
+
assert_equal(:two, axis_.index_to_label(1))
|
104
|
+
assert_nil(axis_.index_to_label(2))
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
def test_object_axis_equality
|
109
|
+
axis1_ = ObjectAxis.new([:one, :two])
|
110
|
+
axis2_ = ObjectAxis.new([:one, :two])
|
111
|
+
axis3_ = ObjectAxis.new([:one, 'two'])
|
112
|
+
assert_equal(axis1_, axis2_)
|
113
|
+
refute_equal(axis1_, axis3_)
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
def test_object_axis_empty
|
118
|
+
axis_ = ObjectAxis.new([])
|
119
|
+
assert_equal(0, axis_.size)
|
120
|
+
end
|
121
|
+
|
122
|
+
|
84
123
|
def test_indexed_axis_size
|
85
124
|
axis_ = IndexedAxis.new(2)
|
86
125
|
assert_equal(2, axis_.size)
|
data/test/tc_basic_values.rb
CHANGED
@@ -47,7 +47,7 @@ module NTable
|
|
47
47
|
def setup
|
48
48
|
@labeled_axis = LabeledAxis.new([:red, :white, :blue])
|
49
49
|
@indexed_axis = IndexedAxis.new(10)
|
50
|
-
@structure = Structure.
|
50
|
+
@structure = Structure.add(@indexed_axis, :row).add(@labeled_axis, :column)
|
51
51
|
end
|
52
52
|
|
53
53
|
|
@@ -123,6 +123,32 @@ module NTable
|
|
123
123
|
end
|
124
124
|
|
125
125
|
|
126
|
+
def test_convenience_construction
|
127
|
+
t_ = NTable.structure(@indexed_axis, :row).add(@labeled_axis, :column).create(:fill => 1)
|
128
|
+
assert_equal(1, t_.get(0, :red))
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
def test_include_p
|
133
|
+
t1_ = Table.new(@structure, :fill => 0)
|
134
|
+
assert_equal(true, t1_.include?(0, :red))
|
135
|
+
assert_equal(true, t1_.include?(9, :blue))
|
136
|
+
assert_equal(false, t1_.include?(10, :red))
|
137
|
+
assert_equal(false, t1_.include?(0, :black))
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
def test_no_such_cell
|
142
|
+
t1_ = Table.new(@structure, :fill => 0)
|
143
|
+
assert_raises(NoSuchCellError) do
|
144
|
+
t1_[10, :red]
|
145
|
+
end
|
146
|
+
assert_raises(NoSuchCellError) do
|
147
|
+
t1_[0, :black]
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
|
126
152
|
end
|
127
153
|
|
128
154
|
end
|
data/test/tc_nested_object.rb
CHANGED
@@ -46,6 +46,7 @@ module NTable
|
|
46
46
|
|
47
47
|
def setup
|
48
48
|
@labeled_axis_2 = LabeledAxis.new([:one, :two])
|
49
|
+
@object_axis_2 = ObjectAxis.new([:one, :two])
|
49
50
|
@labeled_axis_3 = LabeledAxis.new([:blue, :red, :white])
|
50
51
|
@indexed_axis_2 = IndexedAxis.new(2)
|
51
52
|
@indexed_axis_10 = IndexedAxis.new(10, 1)
|
@@ -185,6 +186,19 @@ module NTable
|
|
185
186
|
end
|
186
187
|
|
187
188
|
|
189
|
+
def test_from_level_1_labeled_with_objectify
|
190
|
+
obj_ = {:one => 1, :two => 2}
|
191
|
+
t1_ = Table.from_nested_object(obj_, [{:sort => true, :objectify => true}])
|
192
|
+
assert_equal(Table.new(Structure.add(@object_axis_2), :load => [1,2]), t1_)
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
def test_to_level_1_labeled_with_objectify
|
197
|
+
t1_ = Table.new(Structure.add(@object_axis_2), :load => [1,2])
|
198
|
+
assert_equal({:one => 1, :two => 2}, t1_.to_nested_object)
|
199
|
+
end
|
200
|
+
|
201
|
+
|
188
202
|
end
|
189
203
|
|
190
204
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ntable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-09-
|
12
|
+
date: 2012-09-07 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: NTable provides a convenient data structure for storing n-dimensional
|
15
15
|
tabular data. It works with zero-dimensional scalar values, arrays, tables, and
|
@@ -26,6 +26,7 @@ extra_rdoc_files:
|
|
26
26
|
- README.rdoc
|
27
27
|
files:
|
28
28
|
- lib/ntable/axis.rb
|
29
|
+
- lib/ntable/construction.rb
|
29
30
|
- lib/ntable/errors.rb
|
30
31
|
- lib/ntable/structure.rb
|
31
32
|
- lib/ntable/table.rb
|
@@ -62,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
63
|
version: 1.3.1
|
63
64
|
requirements: []
|
64
65
|
rubyforge_project: virtuoso
|
65
|
-
rubygems_version: 1.8.
|
66
|
+
rubygems_version: 1.8.21
|
66
67
|
signing_key:
|
67
68
|
specification_version: 3
|
68
69
|
summary: NTable is an n-dimensional table data structure for Ruby.
|