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.
@@ -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.