ntable 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.rdoc +3 -0
- data/README.rdoc +73 -0
- data/Version +1 -0
- data/lib/ntable.rb +46 -0
- data/lib/ntable/axis.rb +220 -0
- data/lib/ntable/errors.rb +75 -0
- data/lib/ntable/structure.rb +625 -0
- data/lib/ntable/table.rb +724 -0
- data/test/tc_axes.rb +124 -0
- data/test/tc_basic_values.rb +129 -0
- data/test/tc_decompose.rb +104 -0
- data/test/tc_enumeration.rb +238 -0
- data/test/tc_json.rb +137 -0
- data/test/tc_nested_object.rb +191 -0
- data/test/tc_reduce.rb +161 -0
- data/test/tc_slice.rb +130 -0
- data/test/tc_structure.rb +202 -0
- metadata +78 -0
data/History.rdoc
ADDED
data/README.rdoc
ADDED
@@ -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
|
data/lib/ntable.rb
ADDED
@@ -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'
|
data/lib/ntable/axis.rb
ADDED
@@ -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
|