ntable 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.0
1
+ 0.1.1
@@ -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'
@@ -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).eql?(@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
@@ -72,4 +72,10 @@ module NTable
72
72
  end
73
73
 
74
74
 
75
+ # Raised if you attempt to access a nonexistent cell.
76
+
77
+ class NoSuchCellError < NTableError
78
+ end
79
+
80
+
75
81
  end
@@ -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
- def self.add(axis_, name_=nil)
613
- self.new.add(axis_, name_)
614
- end
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
 
@@ -45,14 +45,8 @@ module NTable
45
45
  class Table
46
46
 
47
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.
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
- if args_.size == 1
195
- first_ = args_.first
196
- args_ = first_ if first_.is_a?(::Hash) || first_.is_a?(::Array)
190
+ offset_ = _offset_for_args(args_)
191
+ unless offset_
192
+ raise NoSuchCellError
197
193
  end
198
- offset_ = @structure._offset(args_)
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
- if args_.size == 1
217
- first_ = args_.first
218
- args_ = first_ if first_.is_a?(::Hash) || first_.is_a?(::Array)
223
+ offset_ = _offset_for_args(args_)
224
+ unless offset_
225
+ raise NoSuchCellError
219
226
  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
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
- @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
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
 
@@ -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, :two])
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)
@@ -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.new.add(@indexed_axis, :row).add(@labeled_axis, :column)
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
@@ -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.0
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-02 00:00:00.000000000 Z
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.24
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.