Module: NTable

Defined in:
lib/ntable.rb,
lib/ntable/axis.rb,
lib/ntable/table.rb,
lib/ntable/errors.rb,
lib/ntable/structure.rb,
lib/ntable/construction.rb,
lib/ntable/index_wrapper.rb

Overview

NTable is an N-dimensional table data structure for Ruby.

Basics

This is a convenient data structure for storing tabular data of arbitrary dimensionality. An NTable can represent zero-dimensional data (i.e. a simple scalar value), one-dimensional data (i.e. an array or dictionary), a two-dimensional table such as a database result set or spreadsheet, or any number of higher dimensions.

The structure of the table is defined explicitly. Each dimension is represented by an axis, which describes how many “rows” the table has in that dimension, and how each row is labeled. For example, you could have a “numeric” indexed axis whose rows are identified by indexes. Or you could have a “string” labeled axis identified by names (e.g. columns in a database.)

For example, a typical two-dimensional spreadsheet would have numerically-identified “rows”, and columns identified by name. You might describe the structure of the table with two axes, the major one a numeric indexed axis, and the minor one a string labeled axis. In code, such a table with 100 rows and two columns could be created like this:

table = NTable.structure(NTable::IndexedAxis.new(100)).
               add(NTable::LabeledAxis.new(:name, :address)).
               create

You can then look up individual cells like this:

value = table[10, :address]

Axes can be given names as well:

table = NTable.structure(NTable::IndexedAxis.new(100), :row).
               add(NTable::LabeledAxis.new(:name, :address), :col).
               create

Then you can specify the axes by name when you look up:

value = table[:row => 10, :col => :address]

You can use the same syntax to set data:

table[10, :address] = "123 Main Street"
table[:row => 10, :col => :address] = "123 Main Street"

Iterating

(to be written)

Slicing and decomposition

(to be written)

Serialization

(to be written)

Defined Under Namespace

Classes: EmptyAxis, IndexWrapper, IndexedAxis, LabeledAxis, NTableError, NoSuchCellError, ObjectAxis, Structure, StructureMismatchError, StructureStateError, Table, TableLockedError, UnknownAxisError

Class Method Summary collapse

Class Method Details

._populate_nested_axes(axis_data_, index_, obj_) ⇒ Object

:nodoc:



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/ntable/construction.rb', line 212

def _populate_nested_axes(axis_data_, index_, obj_)  # :nodoc:
  ai_ = axis_data_[index_]
  case obj_
  when ::Hash
    if ::Hash === ai_
      set_ = ai_
    else
      set_ = axis_data_[index_] = {}
      (ai_[0]...ai_[1]).each{ |i_| set_[i_] = i_ } if ::Array === ai_
    end
    obj_.each do |k_, v_|
      set_[k_] = k_
      _populate_nested_axes(axis_data_, index_+1, v_)
    end
  when ::Array
    if ::Hash === ai_
      obj_.each_with_index do |v_, i_|
        ai_[i_] = i_
        _populate_nested_axes(axis_data_, index_+1, v_)
      end
    else
      s_ = obj_.size
      if ::Array === ai_
        if s_ > 0
          ai_[1] = s_ if !ai_[1] || s_ > ai_[1]
          ai_[0] = s_ if !ai_[0]
        end
      else
        ai_ = axis_data_[index_] = (s_ == 0 ? [nil, nil] : [s_, s_])
      end
      obj_.each_with_index do |v_, i_|
        ai_[0] = i_ if ai_[0] > i_ && !v_.nil?
        _populate_nested_axes(axis_data_, index_+1, v_)
      end
    end
  end
end

._populate_nested_values(table_, path_, axis_data_, obj_) ⇒ Object

:nodoc:



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/ntable/construction.rb', line 251

def _populate_nested_values(table_, path_, axis_data_, obj_)  # :nodoc:
  if path_.size == table_.dim
    table_.set!(*path_, obj_)
  else
    case obj_
    when ::Hash
      h_ = axis_data_[path_.size]
      obj_.each do |k_, v_|
        _populate_nested_values(table_, path_ + [h_[k_]], axis_data_, v_)
      end
    when ::Array
      obj_.each_with_index do |v_, i_|
        _populate_nested_values(table_, path_ + [i_], axis_data_, v_) unless v_.nil?
      end
    end
  end
end

.create(structure_, data_ = {}) ⇒ Object

Create a table with the given Structure.

You can initialize the data using the following options:

:fill

Fill all cells with the given value.

:load

Load the cell data with the values from the given array, in order.



78
79
80
# File 'lib/ntable/construction.rb', line 78

def create(structure_, data_={})
  Table.new(structure_, data_)
end

.from_json_object(json_) ⇒ Object

Construct a table given a JSON object representation.



85
86
87
# File 'lib/ntable/construction.rb', line 85

def from_json_object(json_)
  Table.new(Structure.from_json_array(json_['axes'] || []), :load => json_['values'] || [])
end

.from_nested_object(obj_, field_opts_ = [], opts_ = {}) ⇒ Object

Construct a table given nested hashes and arrays.

The second argument is an array of hashes, providing options for the axes in order. Recognized keys in these hashes include:

:name

The name of the axis, as a string or symbol

:sort

The sort strategy. You can provide a callable object such as a Proc, or one of the constants :numeric or :string. If you omit this key or set it to false, no sort is done on the labels for this axis.

:objectify

An optional Proc that modifies the labels. The Proc should take a single argument and return the new label. If an objectify proc is provided, the resulting axis will be an ObjectAxis. You can also pass true instead of a Proc; this will create an ObjectAxis and make the conversion a nop.

:stringify

An optional Proc that modifies the labels. The Proc should take a single argument and return the new label, which will then be converted to a string if it isn’t one already. If a stringify proc is provided, the resulting axis will be a LabeledAxis. You can also pass true instead of a Proc; this will create an LabeledAxis and make the conversion a simple to_s.

:postprocess

An optional Proc that postprocesses the final labels array. It should take an array of labels and return a modified array (which can be the original array modified in place). Called after any sort has been completed. You can use this, for example, to “fill in” labels that were not present in the original data.

The third argument is an optional hash of miscellaneous options. The following keys are recognized:

:fill

Fill all cells not explicitly set, with the given value. Default is nil.

:objectify_by_default

By default, all hash-created axes are LabeledAxis unless an :objectify field option is explicitly provided. This option, if true, reverses this behavior. You can pass true, or a Proc that transforms the label.

:stringify_by_default

If set to a Proc, this Proc is used as the default stringification routine for converting labels for a LabeledAxis.



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/ntable/construction.rb', line 145

def from_nested_object(obj_, field_opts_=[], opts_={})
  axis_data_ = []
  _populate_nested_axes(axis_data_, 0, obj_)
  objectify_by_default_ = opts_[:objectify_by_default]
  stringify_by_default_ = opts_[:stringify_by_default]
  struct_ = Structure.new
  axis_data_.each_with_index do |ai_, i_|
    field_ = field_opts_[i_] || {}
    axis_ = nil
    name_ = field_[:name]
    case ai_
    when ::Hash
      objectify_ = field_[:objectify]
      stringify_ = field_[:stringify] || stringify_by_default_
      objectify_ ||= objectify_by_default_ unless stringify_
      if objectify_
        if objectify_.respond_to?(:call)
          h_ = ::Set.new
          ai_.keys.each do |k_|
            nv_ = objectify_.call(k_)
            ai_[k_] = nv_
            h_ << nv_
          end
          labels_ = h_.to_a
        else
          labels_ = ai_.keys
        end
        klass_ = ObjectAxis
      else
        stringify_ = nil unless stringify_.respond_to?(:call)
          h_ = ::Set.new
        ai_.keys.each do |k_|
          nv_ = (stringify_ ? stringify_.call(k_) : k_).to_s
          ai_[k_] = nv_
          h_ << nv_
        end
        labels_ = h_.to_a
        klass_ = LabeledAxis
      end
      if (sort_ = field_[:sort])
        if sort_.respond_to?(:call)
          func_ = sort_
        elsif sort_ == :string
          func_ = @string_sort
        elsif sort_ == :integer
          func_ = @integer_sort
        elsif sort_ == :numeric
          func_ = @numeric_sort
        else
          func_ = nil
        end
        labels_.sort!(&func_)
      end
      postprocess_ = field_[:postprocess]
      labels_ = postprocess_.call(labels_) if postprocess_.respond_to?(:call)
      axis_ = klass_.new(labels_)
    when ::Array
      axis_ = IndexedAxis.new(ai_[1].to_i - ai_[0].to_i, ai_[0].to_i)
    end
    struct_.add(axis_, name_) if axis_
  end
  table_ = Table.new(struct_, :fill => opts_[:fill])
  _populate_nested_values(table_, [], axis_data_, obj_)
  table_
end

.index(val_) ⇒ Object

Convenience method for creating an IndexWrapper



79
80
81
# File 'lib/ntable/index_wrapper.rb', line 79

def self.index(val_)
  IndexWrapper.new(val_)
end

.parse_json(json_) ⇒ Object

Construct a table given a JSON unparsed string representation.



92
93
94
# File 'lib/ntable/construction.rb', line 92

def parse_json(json_)
  from_json_object(::JSON.parse(json_))
end

.structure(axis_ = nil, name_ = nil) ⇒ Object

Create and return a new Structure.

If you pass the optional axis argument, that axis will be added to the structure.

The most convenient way to create a table is probably to chain methods off this method. For example:

NTable.structure(NTable::IndexedAxis.new(10)).
  add(NTable::LabeledAxis.new(:column1, :column2)).
  create(:fill => 0)


64
65
66
# File 'lib/ntable/construction.rb', line 64

def structure(axis_=nil, name_=nil)
  axis_ ? Structure.add(axis_, name_) : Structure.new
end