NTable is an N-dimensional table data structure for Ruby.
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"
(to be written)
(to be written)
(to be written)
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
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
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
Convenience method for creating an IndexWrapper
# File lib/ntable/index_wrapper.rb, line 79 def self.index(val_) IndexWrapper.new(val_) end
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
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