ntable 0.1.0 → 0.1.1
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 +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.
|