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
-
._populate_nested_axes(axis_data_, index_, obj_) ⇒ Object
:nodoc:.
-
._populate_nested_values(table_, path_, axis_data_, obj_) ⇒ Object
:nodoc:.
-
.create(structure_, data_ = {}) ⇒ Object
Create a table with the given Structure.
-
.from_json_object(json_) ⇒ Object
Construct a table given a JSON object representation.
-
.from_nested_object(obj_, field_opts_ = [], opts_ = {}) ⇒ Object
Construct a table given nested hashes and arrays.
-
.index(val_) ⇒ Object
Convenience method for creating an IndexWrapper.
-
.parse_json(json_) ⇒ Object
Construct a table given a JSON unparsed string representation.
-
.structure(axis_ = nil, name_ = nil) ⇒ Object
Create and return a new Structure.
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 |