ntable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ === 0.1.0 / 2012-09-01
2
+
3
+ * Initial test release
@@ -0,0 +1,73 @@
1
+ == NTable
2
+
3
+ NTable is an N-dimensional table data structure for Ruby.
4
+
5
+ === Summary
6
+
7
+ NTable provides a convenient data structure for storing n-dimensional tabular data. It works with zero-dimensional scalar values, arrays, tables, and any arbitrary-dimensional hypertables. Each dimension is described by an axis object. The "rows" in that dimension might be identified by numbers or names. You can perform slice operations across any dimension, as well as reductions and dimensional decomposition. Finally, serialization is provided via a custom JSON schema, as well as a simple "hash of hashes" or "array of arrays" approach.
8
+
9
+ === Dependencies
10
+
11
+ NTable is known to work with the following Ruby implementations:
12
+
13
+ * Standard "MRI" Ruby 1.9.2 or later.
14
+ * Rubinius 2.0 or later, in 1.9 mode.
15
+ * JRuby 1.6 or later, in 1.9 mode.
16
+
17
+ === Installation
18
+
19
+ Install NTable as a gem:
20
+
21
+ gem install ntable
22
+
23
+ === Development and support
24
+
25
+ Documentation is available at http://dazuma.github.com/ntable/rdoc
26
+
27
+ Source code is hosted on Github at http://github.com/dazuma/ntable
28
+
29
+ Contributions are welcome. Fork the project on Github.
30
+
31
+ Build status: {<img src="https://secure.travis-ci.org/dazuma/ntable.png" />}[http://travis-ci.org/dazuma/ntable]
32
+
33
+ Report bugs on Github issues at http://github.org/dazuma/ntable/issues
34
+
35
+ Contact the author at dazuma at gmail dot com.
36
+
37
+ === Acknowledgments
38
+
39
+ NTable is written by Daniel Azuma (http://www.daniel-azuma.com).
40
+
41
+ Development is supported by Pirq (http://www.pirq.com).
42
+
43
+ Continuous integration service provided by Travis-CI (http://travis-ci.org).
44
+
45
+ === License
46
+
47
+ Copyright 2012 Daniel Azuma
48
+
49
+ All rights reserved.
50
+
51
+ Redistribution and use in source and binary forms, with or without
52
+ modification, are permitted provided that the following conditions are met:
53
+
54
+ * Redistributions of source code must retain the above copyright notice,
55
+ this list of conditions and the following disclaimer.
56
+ * Redistributions in binary form must reproduce the above copyright notice,
57
+ this list of conditions and the following disclaimer in the documentation
58
+ and/or other materials provided with the distribution.
59
+ * Neither the name of the copyright holder, nor the names of any other
60
+ contributors to this software, may be used to endorse or promote products
61
+ derived from this software without specific prior written permission.
62
+
63
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
64
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
65
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
66
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
67
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
68
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
69
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
70
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
71
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
72
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
73
+ POSSIBILITY OF SUCH DAMAGE.
data/Version ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,46 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # NTable main file
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
+ # NTable is an N-dimensional table data structure for Ruby.
38
+
39
+ module NTable
40
+ end
41
+
42
+
43
+ require 'ntable/errors'
44
+ require 'ntable/axis'
45
+ require 'ntable/structure'
46
+ require 'ntable/table'
@@ -0,0 +1,220 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # NTable axis objects
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
+ module NTable
38
+
39
+
40
+ # This is a "null" axis that has no elements.
41
+ # Not terribly useful by itself, but may be a reasonable base class.
42
+ # Accordingly, we will use this class to document the methods
43
+ # required for an axis object.
44
+ #
45
+ # In general, an axis describes a particular dimension in the table:
46
+ # how large the table is in that dimension, and how the ordered
47
+ # "rows" along that dimension are named.
48
+
49
+ class EmptyAxis
50
+
51
+
52
+ def eql?(obj_)
53
+ obj_.is_a?(EmptyAxis)
54
+ end
55
+ alias_method :==, :eql?
56
+
57
+ def hash
58
+ self.class.hash
59
+ end
60
+
61
+ def inspect
62
+ "#<#{self.class}:0x#{object_id.to_s(16)}>"
63
+ end
64
+ alias_method :to_s, :inspect
65
+
66
+
67
+ # Return the number of rows along this axis.
68
+ # An empty axis will return 0.
69
+
70
+ def size
71
+ 0
72
+ end
73
+
74
+
75
+ # Given a label object, return the corresponding 0-based integer index.
76
+ # Returns nil if the label is not recognized.
77
+
78
+ def label_to_index(label_)
79
+ nil
80
+ end
81
+
82
+
83
+ # Given a 0-based integer index, return the corresponding label object.
84
+ # Returns nil if the index is out of bounds (i.e. is less than 0 or
85
+ # greater than or equal to size.)
86
+
87
+ def index_to_label(index_)
88
+ nil
89
+ end
90
+
91
+
92
+ # Populate the given hash with the configuration of this axis.
93
+ # The hash will eventually be serialized via JSON.
94
+
95
+ def to_json_object(json_obj_)
96
+ end
97
+
98
+
99
+ # Configure this axis given a hash configuration that came from
100
+ # a JSON serialization.
101
+
102
+ def from_json_object(json_obj_)
103
+ end
104
+
105
+
106
+ end
107
+
108
+
109
+ # An axis in which the labels are explicitly provided as strings.
110
+
111
+ class LabeledAxis
112
+
113
+
114
+ # Create a LabeledAxis given an array of the label strings.
115
+ # Symbols may also be provided, but will be converted to strings.
116
+
117
+ def initialize(labels_)
118
+ @a = labels_.map{ |label_| label_.to_s }
119
+ @h = {}
120
+ @a.each_with_index{ |n_, i_| @h[n_] = i_ }
121
+ @size = labels_.size
122
+ end
123
+
124
+
125
+ def eql?(obj_)
126
+ obj_.is_a?(LabeledAxis) && obj_.instance_variable_get(:@a).eql?(@a)
127
+ end
128
+ alias_method :==, :eql?
129
+
130
+ def hash
131
+ @a.hash
132
+ end
133
+
134
+ def inspect
135
+ "#<#{self.class}:0x#{object_id.to_s(16)} #{@a.inspect}>"
136
+ end
137
+ alias_method :to_s, :inspect
138
+
139
+
140
+ attr_reader :size
141
+
142
+
143
+ def label_to_index(label_)
144
+ @h[label_.to_s]
145
+ end
146
+
147
+ def index_to_label(index_)
148
+ @a[index_]
149
+ end
150
+
151
+
152
+ def to_json_object(json_obj_)
153
+ json_obj_['labels'] = @a
154
+ end
155
+
156
+ def from_json_object(json_obj_)
157
+ initialize(json_obj_['labels'] || [])
158
+ end
159
+
160
+
161
+ end
162
+
163
+
164
+ # An axis in which the rows are numerically identified by a range
165
+ # of consecutive integers.
166
+
167
+ class IndexedAxis
168
+
169
+
170
+ # Create an IndexedAxis with the given number of rows. The optional
171
+ # start parameter indicates the number of the first row (default 0).
172
+
173
+ def initialize(size_, start_=0)
174
+ @size = size_
175
+ @start = start_
176
+ end
177
+
178
+
179
+ def eql?(obj_)
180
+ obj_.is_a?(IndexedAxis) && obj_.size.eql?(@size) && obj_.start.eql?(@start)
181
+ end
182
+ alias_method :==, :eql?
183
+
184
+ def hash
185
+ @size.hash + @start.hash
186
+ end
187
+
188
+ def inspect
189
+ "#<#{self.class}:0x#{object_id.to_s(16)} size=#{@size} start=#{@start}>"
190
+ end
191
+ alias_method :to_s, :inspect
192
+
193
+
194
+ attr_reader :size
195
+ attr_reader :start
196
+
197
+
198
+ def label_to_index(label_)
199
+ label_ >= @start && label_ < @size + @start ? label_ - @start : nil
200
+ end
201
+
202
+ def index_to_label(index_)
203
+ index_ >= 0 && index_ < @size ? index_ + @start : nil
204
+ end
205
+
206
+
207
+ def to_json_object(json_obj_)
208
+ json_obj_['size'] = @size
209
+ json_obj_['start'] = @start unless @start == 0
210
+ end
211
+
212
+ def from_json_object(json_obj_)
213
+ initialize(json_obj_['size'], json_obj_['start'].to_i)
214
+ end
215
+
216
+
217
+ end
218
+
219
+
220
+ end
@@ -0,0 +1,75 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # NTable error objects
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
+ module NTable
38
+
39
+
40
+ # Base class for all NTable errors
41
+
42
+ class NTableError < ::StandardError
43
+ end
44
+
45
+
46
+ # Raised if the structure lock/unlock state is incorrect for the
47
+ # current operation. For example, it is raised if you attempt to
48
+ # add an axis to a locked structure.
49
+
50
+ class StructureStateError < NTableError
51
+ end
52
+
53
+
54
+ # An attempt was made to perform an operation on two tables that were
55
+ # not compatible with one another.
56
+
57
+ class StructureMismatchError < NTableError
58
+ end
59
+
60
+
61
+ # A given axis name or index was not recognized.
62
+
63
+ class UnknownAxisError < NTableError
64
+ end
65
+
66
+
67
+ # Raised if you attempt to modify a table that is locked. Locked
68
+ # tables are usually "sub-views" into other tables, and cannot be
69
+ # edited directly because they share data with the parent table.
70
+
71
+ class TableLockedError < NTableError
72
+ end
73
+
74
+
75
+ end
@@ -0,0 +1,625 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # NTable structure object
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
+ module NTable
38
+
39
+
40
+ # A Structure describes how a table is laid out: how many dimensions
41
+ # it has, how large the table is in each of those dimensions, what the
42
+ # axes are called, and how the coordinates are labeled/named. It is
43
+ # essentially an ordered list of named axes, along with some
44
+ # meta-information. A Structure is capable of performing computations
45
+ # such as determining how to look up data at a particular coordinate.
46
+ #
47
+ # Generally, you create a new empty structure, and then use the #add
48
+ # method to define the axes. Provide the axis by creating an axis
49
+ # object (for example, an instance of IndexedAxis or LabeledAxis.)
50
+ # You can also optionally provide a name for the axis.
51
+ #
52
+ # Once a Structure is used by a table, it is locked and cannot be
53
+ # modified further. However, a Structure can be shared by multiple
54
+ # tables.
55
+ #
56
+ # Many table operations (such as slice) automatically compute the
57
+ # structure of the result.
58
+
59
+ class Structure
60
+
61
+
62
+ # A data structure that provides information about a particular
63
+ # axis/dimension in a Structure. It provides access to the axis
64
+ # itself, as well as the axis's name (if any) and 0-based index
65
+ # into the list of axes. You should never need to create an
66
+ # AxisInfo yourself, but you can obtain one from Structure#axis_info.
67
+
68
+ class AxisInfo # :nodoc:
69
+
70
+ def initialize(axis_, index_, name_, step_=nil) # :nodoc:
71
+ @axis = axis_
72
+ @index = index_
73
+ @name = name_
74
+ @step = step_
75
+ end
76
+
77
+ # The axis object
78
+ attr_reader :axis
79
+ # The 0-based index of this axis in the structure. i.e. the first,
80
+ # most major axis has index 0.
81
+ attr_reader :index
82
+ # The name of this axis in the structure as a string, or nil for
83
+ # no name.
84
+ attr_reader :name
85
+
86
+ attr_reader :step # :nodoc:
87
+
88
+
89
+ def eql?(obj_) # :nodoc:
90
+ obj_.is_a?(AxisInfo) && obj_.axis.eql?(@axis) && obj_.name.eql?(@name)
91
+ end
92
+ alias_method :==, :eql? # :nodoc:
93
+
94
+
95
+ def _set_axis(axis_) # :nodoc:
96
+ @axis = axis_
97
+ end
98
+
99
+ def _set_step(step_) # :nodoc:
100
+ @step = step_
101
+ end
102
+
103
+ def _dec_index # :nodoc:
104
+ @index -= 1
105
+ end
106
+
107
+ end
108
+
109
+
110
+ # A coordinate into a table. This object is often provided during
111
+ # iteration to indicate where you are in the iteration. You should
112
+ # not need to create a Position object yourself.
113
+
114
+ class Position
115
+
116
+ def initialize(structure_, vector_) # :nodoc:
117
+ @structure = structure_
118
+ @vector = vector_
119
+ @offset = @coords = nil
120
+ end
121
+
122
+
123
+ def eql?(obj_)
124
+ obj_.is_a?(Position) && obj_.structure.eql?(@structure) && obj_._offset.eql?(self._offset)
125
+ end
126
+ alias_method :==, :eql?
127
+
128
+ attr_reader :structure # :nodoc:
129
+
130
+
131
+ # Returns the label of the coordinate along the given axis. The
132
+ # axis may be provided by name or index.
133
+
134
+ def coord(axis_)
135
+ ainfo_ = @structure.axis_info(axis_)
136
+ ainfo_ ? _coords[ainfo_.index] : nil
137
+ end
138
+ alias_method :[], :coord
139
+
140
+
141
+ # Returns an array of all coordinate labels along the axes in
142
+ # order.
143
+
144
+ def coord_array
145
+ _coords.dup
146
+ end
147
+
148
+
149
+ # Returns the Position of the "next" cell in the table, or nil
150
+ # if this is the last cell.
151
+
152
+ def next
153
+ v_ = @vector.dup
154
+ @structure._inc_vector(v_) ? nil : Position.new(@structure, v_)
155
+ end
156
+
157
+
158
+ # Returns the Position of the "previous" cell in the table, or nil
159
+ # if this is the first cell.
160
+
161
+ def prev
162
+ v_ = @vector.dup
163
+ @structure._dec_vector(v_) ? nil : Position.new(@structure, v_)
164
+ end
165
+
166
+
167
+ def _offset # :nodoc:
168
+ @offset ||= @structure._compute_offset_for_vector(@vector)
169
+ end
170
+
171
+ def _coords # :nodoc:
172
+ @coords ||= @structure._compute_coords_for_vector(@vector)
173
+ end
174
+
175
+ end
176
+
177
+
178
+ # Create an empty Structure. An empty structure corresponds to a
179
+ # table with no axes and a single value (i.e. a scalar). Generally,
180
+ # you should add axes using the Structure#add method before using
181
+ # the structure.
182
+
183
+ def initialize
184
+ @indexes = []
185
+ @names = {}
186
+ @size = 1
187
+ @locked = false
188
+ @parent = nil
189
+ end
190
+
191
+
192
+ def initialize_copy(other_) # :nodoc:
193
+ initialize
194
+ other_.instance_variable_get(:@indexes).each do |ai_|
195
+ ai_ = ai_.dup
196
+ @indexes << ai_
197
+ if (name_ = ai_.name)
198
+ @names[name_] = ai_
199
+ end
200
+ end
201
+ @size = other_.size
202
+ end
203
+
204
+
205
+ # Create an unlocked copy of this structure that can be further
206
+ # modified.
207
+
208
+ def unlocked_copy
209
+ copy_ = Structure.new
210
+ @indexes.each{ |ai_| copy_.add(ai_.axis, ai_.name) }
211
+ copy_
212
+ end
213
+
214
+
215
+ # Returns true if the two structures are equivalent, both in the
216
+ # axes and in the parentage. The structure of a shared slice is not
217
+ # equivalent, in this sense, to the "same" structure created from
218
+ # scratch, because the former is a subview of a larger structure
219
+ # whereas the latter is not.
220
+
221
+ def eql?(rhs_)
222
+ rhs_.equal?(self) ||
223
+ rhs_.is_a?(Structure) &&
224
+ @parent.eql?(rhs_.instance_variable_get(:@parent)) &&
225
+ @indexes.eql?(rhs_.instance_variable_get(:@indexes))
226
+ end
227
+
228
+
229
+ # Returns true if the two structures are equivalent in the axes but
230
+ # not necessarily in the offsets. The structure of a shared slice
231
+ # is equivalent, in this sense, to the "same" structure created from
232
+ # scratch, even though one is a subview and the other is not.
233
+
234
+ def ==(rhs_)
235
+ if rhs_.equal?(self)
236
+ true
237
+ elsif rhs_.is_a?(Structure)
238
+ rhs_indexes_ = rhs_.instance_variable_get(:@indexes)
239
+ if rhs_indexes_.size == @indexes.size
240
+ rhs_indexes_.each_with_index do |rhs_ai_, i_|
241
+ lhs_ai_ = @indexes[i_]
242
+ return false unless lhs_ai_.axis == rhs_ai_.axis && lhs_ai_.name == rhs_ai_.name
243
+ end
244
+ return true
245
+ end
246
+ false
247
+ else
248
+ false
249
+ end
250
+ end
251
+
252
+
253
+ # Append an axis to the configuration of this structure. You must
254
+ # provide the axis, as an object that duck-types EmptyAxis. You may
255
+ # also provide an optional name string.
256
+
257
+ def add(axis_, name_=nil)
258
+ raise StructureStateError, "Structure locked" if @locked
259
+ name_ = name_ ? name_.to_s : nil
260
+ ainfo_ = AxisInfo.new(axis_, @indexes.size, name_)
261
+ @indexes << ainfo_
262
+ @names[name_] = ainfo_ if name_
263
+ @size *= axis_.size
264
+ self
265
+ end
266
+
267
+
268
+ # Remove the given axis from the configuration.
269
+ # You may specify the axis by 0-based index, or by name string.
270
+ # Raises UnknownAxisError if there is no such axis.
271
+
272
+ def remove(axis_)
273
+ raise StructureStateError, "Structure locked" if @locked
274
+ ainfo_ = axis_info(axis_)
275
+ unless ainfo_
276
+ raise UnknownAxisError, "Unknown axis: #{axis_.inspect}"
277
+ end
278
+ index_ = ainfo_.index
279
+ @names.delete(ainfo_.name)
280
+ @indexes.delete_at(index_)
281
+ @indexes[index_..-1].each{ |ai_| ai_._dec_index }
282
+ size_ = ainfo_.axis.size
283
+ if size_ == 0
284
+ @size = @indexes.inject(1){ |s_, ai_| s_ * ai_.axis.size }
285
+ else
286
+ @size /= size_
287
+ end
288
+ self
289
+ end
290
+
291
+
292
+ # Replace the given axis already in the configuration, with the
293
+ # given new axis. The old axis must be specified by 0-based index
294
+ # or by name string. The new axis must be provided as an axis
295
+ # object that duck-types EmptyAxis.
296
+ #
297
+ # Raises UnknownAxisError if the given old axis specification
298
+ # does not match an actual axis.
299
+
300
+ def replace(axis_, naxis_=nil)
301
+ raise StructureStateError, "Structure locked" if @locked
302
+ ainfo_ = axis_info(axis_)
303
+ unless ainfo_
304
+ raise UnknownAxisError, "Unknown axis: #{axis_.inspect}"
305
+ end
306
+ osize_ = ainfo_.axis.size
307
+ naxis_ ||= yield(ainfo_)
308
+ ainfo_._set_axis(naxis_)
309
+ if osize_ == 0
310
+ @size = @indexes.inject(1){ |size_, ai_| size_ * ai_.axis.size }
311
+ else
312
+ @size = @size / osize_ * naxis_.size
313
+ end
314
+ self
315
+ end
316
+
317
+
318
+ # Returns the parent structure if this is a sub-view into a larger
319
+ # structure, or nil if not.
320
+
321
+ def parent
322
+ @parent
323
+ end
324
+
325
+
326
+ # Returns the number of axes/dimensions currently in this structure.
327
+
328
+ def dim
329
+ @indexes.size
330
+ end
331
+
332
+
333
+ # Returns true if this is a degenerate/scalar structure. That is,
334
+ # if the dimension is 0.
335
+
336
+ def degenerate?
337
+ @indexes.size == 0
338
+ end
339
+
340
+
341
+ # Returns an array of AxisInfo objects representing all the axes
342
+ # of this structure.
343
+
344
+ def all_axis_info
345
+ @indexes.dup
346
+ end
347
+
348
+
349
+ # Returns the AxisInfo object representing the given axis. The axis
350
+ # must be specified by 0-based index or by name string. Returns nil
351
+ # if there is no such axis.
352
+
353
+ def axis_info(axis_)
354
+ case axis_
355
+ when ::Integer
356
+ @indexes[axis_]
357
+ else
358
+ @names[axis_.to_s]
359
+ end
360
+ end
361
+
362
+
363
+ # Lock this structure, preventing further modification. Generally,
364
+ # this is done automatically when a structure is used by a table,
365
+ # and you do not need to call it yourself.
366
+
367
+ def lock!
368
+ unless @locked
369
+ @locked = true
370
+ if @size > 0
371
+ s_ = @size
372
+ @indexes.each do |ainfo_|
373
+ s_ /= ainfo_.axis.size
374
+ ainfo_._set_step(s_)
375
+ end
376
+ end
377
+ end
378
+ self
379
+ end
380
+
381
+
382
+ # Returns true if this structure has been locked.
383
+
384
+ def locked?
385
+ @locked
386
+ end
387
+
388
+
389
+ # Returns the number of cells in a table with this structure.
390
+
391
+ def size
392
+ @size
393
+ end
394
+
395
+
396
+ # Returns true if this structure implies an "empty" table, one with
397
+ # no cells. This happens only if at least one of the axes has a
398
+ # zero size.
399
+
400
+ def empty?
401
+ @size == 0
402
+ end
403
+
404
+
405
+ # Creates a Position object for the given argument. The argument
406
+ # may be a hash of row labels by axis name, or it may be an array
407
+ # of row labels for the axes in order.
408
+
409
+ def position(arg_)
410
+ vector_ = _vector(arg_)
411
+ vector_ ? Position.new(self, vector_) : nil
412
+ end
413
+
414
+
415
+ def substructure_including(*axes_)
416
+ _substructure(axes_.flatten, true)
417
+ end
418
+
419
+
420
+ def substructure_omitting(*axes_)
421
+ _substructure(axes_.flatten, false)
422
+ end
423
+
424
+
425
+ # Returns an array of objects representing the configuration of
426
+ # this structure. Such an array can be serialized as JSON, and
427
+ # used to replicate this structure using from_json_array.
428
+
429
+ def to_json_array
430
+ @indexes.map do |ai_|
431
+ name_ = ai_.name
432
+ axis_ = ai_.axis
433
+ type_ = axis_.class.name
434
+ if type_ =~ /^NTable::(\w+)Axis$/
435
+ type_ = $1
436
+ type_ = type_[0..0].downcase + type_[1..-1]
437
+ end
438
+ obj_ = {'type' => type_}
439
+ obj_['name'] = name_ if name_
440
+ axis_.to_json_object(obj_)
441
+ obj_
442
+ end
443
+ end
444
+
445
+
446
+ # Use the given array to reconstitute a structure previously
447
+ # serialized using Structure#to_json_array.
448
+
449
+ def from_json_array(array_)
450
+ if @indexes.size > 0
451
+ raise StructureStateError, "There are already axes in this structure"
452
+ end
453
+ array_.each do |obj_|
454
+ name_ = obj_['name']
455
+ type_ = obj_['type'] || 'Empty'
456
+ if type_ =~ /^([a-z])(.*)$/
457
+ mod_ = ::NTable.const_get("#{$1.upcase}#{$2}Axis")
458
+ else
459
+ mod_ = ::Kernel
460
+ type_.split('::').each do |t_|
461
+ mod_ = mod_.const_get(t_)
462
+ end
463
+ end
464
+ axis_ = mod_.allocate
465
+ axis_.from_json_object(obj_)
466
+ add(axis_, name_)
467
+ end
468
+ self
469
+ end
470
+
471
+
472
+ def _substructure(axes_, bool_) # :nodoc:
473
+ raise StructureStateError, "Structure not locked" unless @locked
474
+ sub_ = Structure.new
475
+ indexes_ = []
476
+ names_ = {}
477
+ size_ = 1
478
+ @indexes.each do |ainfo_|
479
+ if axes_.include?(ainfo_.index) == bool_
480
+ nainfo_ = AxisInfo.new(ainfo_.axis, indexes_.size, ainfo_.name, ainfo_.step)
481
+ indexes_ << nainfo_
482
+ names_[ainfo_.name] = nainfo_
483
+ size_ *= ainfo_.axis.size
484
+ end
485
+ end
486
+ sub_.instance_variable_set(:@indexes, indexes_)
487
+ sub_.instance_variable_set(:@names, names_)
488
+ sub_.instance_variable_set(:@size, size_)
489
+ sub_.instance_variable_set(:@locked, true)
490
+ sub_.instance_variable_set(:@parent, self)
491
+ sub_
492
+ end
493
+
494
+
495
+ def _offset(arg_) # :nodoc:
496
+ raise StructureStateError, "Structure not locked" unless @locked
497
+ return nil unless @size > 0
498
+ case arg_
499
+ when ::Hash
500
+ offset_ = 0
501
+ arg_.each do |k_, v_|
502
+ if (ainfo_ = axis_info(k_))
503
+ index_ = ainfo_.axis.label_to_index(v_)
504
+ return nil unless index_
505
+ offset_ += ainfo_.step * index_
506
+ else
507
+ return nil
508
+ end
509
+ end
510
+ offset_
511
+ when ::Array
512
+ offset_ = 0
513
+ arg_.each_with_index do |v_, i_|
514
+ if (ainfo_ = @indexes[i_])
515
+ index_ = ainfo_.axis.label_to_index(v_)
516
+ return nil unless index_
517
+ offset_ += ainfo_.step * index_
518
+ else
519
+ return nil
520
+ end
521
+ end
522
+ offset_
523
+ else
524
+ nil
525
+ end
526
+ end
527
+
528
+
529
+ def _vector(arg_) # :nodoc:
530
+ raise StructureStateError, "Structure not locked" unless @locked
531
+ return nil unless @size > 0
532
+ vec_ = ::Array.new(@indexes.size, 0)
533
+ case arg_
534
+ when ::Hash
535
+ arg_.each do |k_, v_|
536
+ if (ainfo_ = axis_info(k_))
537
+ val_ = ainfo_.axis.label_to_index(v_)
538
+ vec_[ainfo_.index] = val_ if val_
539
+ end
540
+ end
541
+ vec_
542
+ when ::Array
543
+ arg_.each_with_index do |v_, i_|
544
+ if (ainfo_ = @indexes[i_])
545
+ val_ = ainfo_.axis.label_to_index(v_)
546
+ vec_[i_] = val_ if val_
547
+ end
548
+ end
549
+ vec_
550
+ else
551
+ nil
552
+ end
553
+ end
554
+
555
+
556
+ def _compute_offset_for_vector(vector_) # :nodoc:
557
+ offset_ = 0
558
+ vector_.each_with_index do |v_, i_|
559
+ offset_ += v_ * @indexes[i_].step
560
+ end
561
+ offset_
562
+ end
563
+
564
+
565
+ def _compute_coords_for_vector(vector_) # :nodoc:
566
+ vector_.map.with_index do |v_, i_|
567
+ @indexes[i_].axis.index_to_label(v_)
568
+ end
569
+ end
570
+
571
+
572
+ def _inc_vector(vector_) # :nodoc:
573
+ (vector_.size - 1).downto(-1) do |i_|
574
+ return true if i_ < 0
575
+ v_ = vector_[i_] + 1
576
+ if v_ >= @indexes[i_].axis.size
577
+ vector_[i_] = 0
578
+ else
579
+ vector_[i_] = v_
580
+ break
581
+ end
582
+ end
583
+ false
584
+ end
585
+
586
+
587
+ def _dec_vector(vector_) # :nodoc:
588
+ (vector_.size - 1).downto(-1) do |i_|
589
+ return true if i_ < 0
590
+ v_ = vector_[i_] - 1
591
+ if v_ < 0
592
+ vector_[i_] = @indexes[i_].axis.size - 1
593
+ else
594
+ vector_[i_] = v_
595
+ break
596
+ end
597
+ end
598
+ false
599
+ end
600
+
601
+
602
+ def _compute_position_coords(offset_) # :nodoc:
603
+ raise StructureStateError, "Structure not locked" unless @locked
604
+ @indexes.map do |ainfo_|
605
+ i_ = offset_ / ainfo_.step
606
+ offset_ -= ainfo_.step * i_
607
+ ainfo_.axis.index_to_label(i_)
608
+ end
609
+ end
610
+
611
+
612
+ def self.add(axis_, name_=nil)
613
+ self.new.add(axis_, name_)
614
+ end
615
+
616
+
617
+ def self.from_json_array(array_)
618
+ self.new.from_json_array(array_)
619
+ end
620
+
621
+
622
+ end
623
+
624
+
625
+ end