module NTable

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)

Public Class Methods

create(structure_, data_={}) click to toggle source

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.

# File lib/ntable/construction.rb, line 78
def create(structure_, data_={})
  Table.new(structure_, data_)
end
from_json_object(json_) click to toggle source

Construct a table given a JSON object representation.

# 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_={}) click to toggle source

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_labels

An optional Proc that postprocesses the final labels array, if a LabeledAxis or an ObjectAxis is being generated. It should take an array of labels and return the desired array. You may modify the array in place and return the original. This is 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. WARNING: if you remove labels from the array, any data in those locations will silently be lost.

:postprocess_range

An optional Proc that postprocesses the final integer range, if an IndexedAxis is being generated. It should take a Range of integer as an argument, and return either the original or a different Range. You can use this, for example, to extend the range of this axis beyond that for which data exists. WARNING: if you remove values from the range, any data in those locations will silently be lost.

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.

:structure

Force the use of the given Structure. Any data that does not fit into this structure is ignored. When this option is provided, the :name, :sort, :postprocess_labels, and :postprocess_range field options are ignored. However, :stringify and :objectify may still be provided to specify how hash keys should map to labels.

# File lib/ntable/construction.rb, line 163
def from_nested_object(obj_, field_opts_=[], opts_={})
  if field_opts_.is_a?(::Hash)
    opts_ = field_opts_
    field_opts_ = []
  end
  axis_data_ = []
  _populate_nested_axes(axis_data_, 0, obj_)
  objectify_by_default_ = opts_[:objectify_by_default]
  stringify_by_default_ = opts_[:stringify_by_default]
  fixed_struct_ = opts_[:structure]
  struct_ = Structure.new unless fixed_struct_
  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 struct_
        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]
        labels_ = postprocess_.call(labels_) || labels_ if postprocess_.respond_to?(:call)
        axis_ = klass_.new(labels_)
      end
    when ::Array
      if struct_
        range_ = ((ai_[0].to_i)...(ai_[1].to_i))
        postprocess_ = field_[:postprocess_range]
        range_ = postprocess_.call(range_) || range_ if postprocess_.respond_to?(:call)
        ai_[0] = range_.first.to_i
        ai_[1] = range_.last.to_i
        ai_[1] += 1 unless range_.exclude_end?
        axis_ = IndexedAxis.new(ai_[1] - ai_[0], ai_[0])
      end
    end
    struct_.add(axis_, name_) if axis_
  end
  table_ = Table.new(fixed_struct_ || struct_, :fill => opts_[:fill])
  _populate_nested_values(table_, [], axis_data_, obj_)
  table_
end
index(val_) click to toggle source

Convenience method for creating an IndexWrapper

# File lib/ntable/index_wrapper.rb, line 79
def self.index(val_)
  IndexWrapper.new(val_)
end
parse_json(json_) click to toggle source

Construct a table given a JSON unparsed string representation.

# File lib/ntable/construction.rb, line 92
def parse_json(json_)
  from_json_object(::JSON.parse(json_))
end
structure(axis_=nil, name_=nil) click to toggle source

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)
# File lib/ntable/construction.rb, line 64
def structure(axis_=nil, name_=nil)
  axis_ ? Structure.add(axis_, name_) : Structure.new
end