multimatrix 0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 270b3c84f142d440457ef16d2f3632351234d405
4
+ data.tar.gz: 58401fba050650b4aa3e076fd050c7de8a8159a2
5
+ SHA512:
6
+ metadata.gz: aa9c8a1215fd93ce7d6bf575f674f42bde33c06a7554ef7c73de7fef07c8a940604eec01a7485790d8044d60be790922844aa627b467cd4cd14002b9de43e454
7
+ data.tar.gz: ca4e08406a23535a8856fb48852cba777b6371da47dbcdb123d570f1a887f2c241416e6a674524a4389b0d98fb99db09b3a8c5f4e1e49e12ebe16ff2b1b05b01
@@ -0,0 +1,296 @@
1
+ class MultiMatrixException < Exception
2
+ end
3
+
4
+ # An n-dimensonal matrix, where dimensions are labels
5
+ #
6
+ # Imagine: your urgent need is to store a dataset more than 1 or 2 dimensions large.
7
+ # The default way to do this is tu use multi dimensional arrays, like "array[][][]" for
8
+ # example. Inserting or retrieveing a value from this dataset would be kind of an easy task,
9
+ # but what happens when You have to summarize all the elements of a dimension. For example
10
+ # sum all the elements where the second level array's index is 2. You have to loop through
11
+ # n*n*n elements, just to find some of them. Tedious.
12
+ #
13
+ # MultiMatrix comes to the rescue.
14
+ #
15
+ # MultiMatrix makes it easy to create multi-level data structures, and makes finding and looping through
16
+ # them nice and easy. And fast.
17
+ #
18
+ # @author Gergely Dömsödi <mailto:gergely.domsodi@uhusystems.com>
19
+ #
20
+ # To easily show an example, and concept of this class let's see a 2 dimensional array
21
+ #
22
+ # m = MultiMatrix.new(:x, :y)
23
+ # m.insert(:x => 1, :y = 3) = 10
24
+ # m.insert(:x => 2, :y = 1) = 20
25
+ # m.insert(:x => 2, :y =>2) = 30
26
+ #
27
+ # could be imagined as
28
+ # x| 1 | 2 | 3 |
29
+ # y |___|___|___|
30
+ # 1 | ? | 20| ? |
31
+ # |___|___|___|
32
+ # 2 | ? | 30| ? |
33
+ # |___|___|___|
34
+ # 3 | 10| ? | ? |
35
+ # |___|___|___|
36
+ #
37
+ # >> m.sum(x: 2)
38
+ # => 50
39
+ # >> m.sum(x: 3)
40
+ # => 0
41
+ # >> m.to_a(y: 2)
42
+ # => [30]
43
+ # >> m[:x => 1, :y => 1]
44
+ # => nil
45
+ # >> m[:x => 3] == MultiMatrix.new(:y)
46
+ # => true
47
+ #
48
+ # Usage:
49
+ #
50
+ # >> x = MultiMatrix.new(:x, :y, :z)
51
+ # >> x[:x => 1, :y => 1, :z => 1] = 1
52
+ # >> x[:x => 1, :y => 1, :z => 2] = 1
53
+ # >> x[:x => 2, :y => 1, :z => 2] = 1
54
+ #
55
+ # >> x[:x => 1, :y => 1, :z => 1]
56
+ # => 1
57
+ #
58
+ # >> x[:x => 1, :y => 1]
59
+ # => MultiMatrix({:z=>2}=>2)
60
+ #
61
+ # >> x.sum(:x => 1)
62
+ # => 2
63
+ # >> x.sum(:z => 1)
64
+ # => 1
65
+ # >> x.to_a(:z => 1)
66
+ # => [1]
67
+ #
68
+ # Wait! There is more:
69
+ #
70
+ # >> x[:z => [1,2]]
71
+ # => [1,1,1]
72
+ # >> x.sum(:z => [1,2])
73
+ # => 3
74
+
75
+ class MultiMatrix
76
+
77
+ # Creates a new MultiMatrix object, where labels will be the dimensions of the dataset
78
+ # use labels appropriate for Hash keys, because they will be stored as such internally.
79
+ # labels could be Symbols, Integers, even ActiveModel objects, the only requirement for it that it have to
80
+ # have a meaningful == method.
81
+ # @param labels an [Array] of values to be used as dimension labels
82
+ def initialize(*labelskeys)
83
+ raise MultiMatrixException, "no dimensions" if labelskeys.length == 0
84
+ @labels = labelskeys
85
+ @dim = @labels.length
86
+ @values = Hash.new
87
+ @values_by_id = Hash.new
88
+ @labels_by_id = Hash.new
89
+ @sumcache = {}
90
+ @seq = 0
91
+ @finderhash = {}
92
+ end
93
+
94
+ # Inserts a value to the data set.
95
+ #
96
+ # @param labels [Hash] the position where to insert the value.
97
+ # beware, at insert all dimension of the positin must be given
98
+ # @param value The value to be inserted
99
+ # @return [true] on success, raises MultiMatrixException otherwise
100
+ def insert(labels, value)
101
+ if check_labels(labels) and labels.keys.length == @dim
102
+ if @values.key?(labels)
103
+ id = @values[labels]
104
+ if block_given?
105
+ save_value(labels, yield(get_value(labels), value), :override => id)
106
+ else
107
+ save_value(labels, get_value(labels) + value, :override => id)
108
+ end
109
+ else
110
+ save_value(labels, value)
111
+ end
112
+ else
113
+ raise MultiMatrixException, 'not enough key supplied'
114
+ end
115
+ @sumcache = {}
116
+ true
117
+ end
118
+
119
+ # Executes block on all value at position given by labels
120
+ # @param labels finder hash
121
+ def each(labels = {})
122
+ if check_labels(labels) and labels.keys.length <= @dim
123
+ ids = find_ids(labels)
124
+ ids.each do |id|
125
+ if block_given?
126
+ yield @values_by_id[id]
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ # Same as Enumerable#inject, but filter hash is accepted
133
+ # @param labels finder hash
134
+ # @param default beginning value of the memo
135
+ def inject(labels = {}, default = 0)
136
+ memo = default
137
+ if check_labels(labels) and labels.keys.length <= @dim
138
+ ids = find_ids(labels)
139
+ ids.each do |id|
140
+ if block_given?
141
+ memo = yield memo, @values_by_id[id]
142
+ end
143
+ end
144
+ end
145
+ return memo
146
+ end
147
+
148
+ # Returns an array of values matching @param labels
149
+ # @param labels finder hash
150
+ # @return [Array] values found
151
+ def to_a(labels = {})
152
+ if check_labels(labels)
153
+ ids=find_ids(labels)
154
+ return ids.map{|id| @values_by_id[id]}
155
+ end
156
+ end
157
+
158
+ # Summarizes all elements matching labels
159
+ # a little faster than using MultiMatrix#inject(labels) {|a,b| a+b} because of two factors:
160
+ # - if every dimension is given, sum return only one element without looping through the data structure
161
+ # - sum is cached so if sum is called twice with the same selector hash, the result is given back instantly
162
+ # @param labels finder hash
163
+ # @param default default value of sum
164
+ def sum(labels = {}, default = 0)
165
+ if check_labels(labels)
166
+ if labels.keys.length == @dim and labels.values.find_all {|val| val.class.to_s == 'Array'}.empty?
167
+ return get_value(labels)
168
+ end
169
+ if @sumcache.key?(labels)
170
+ return @sumcache[labels]
171
+ end
172
+ ret = self.inject(labels, default) {|a,b| a+b}
173
+ @sumcache[labels] = ret
174
+ ret
175
+ end
176
+ end
177
+
178
+ # Retrieves a value
179
+ # If every dimension is given, retrieve just a value at the specified position,
180
+ # if not, retrieves an MultiMatrix of the matching elements, leaving the fixed dimensions behind
181
+ # @param labels finder hash
182
+ # @return value/MultiMatrix
183
+ def retrieve(labels = {})
184
+ if check_labels(labels)
185
+ if labels.keys.length < @dim or labels.values.find_all {|val| val.class == Array}.length > 0
186
+ tmp = MultiMatrix.new(*(@labels - labels.keys.find_all {|val| labels[val].class != Array}))
187
+ ids = find_ids(labels)
188
+ ids.each do |id|
189
+ hash = @labels_by_id[id]
190
+ labels.keys.find_all {|val| labels[val].class != Array}.each do |key|
191
+ hash.delete(key)
192
+ end
193
+ tmp[hash] = @values_by_id[id]
194
+ end
195
+ return tmp
196
+ elsif labels.keys.length == @dim
197
+ return get_value(labels)
198
+ end
199
+ end
200
+ end
201
+
202
+ # return the possible values of a dimension
203
+ # where values match the labels given in [rest]
204
+ # @param needle a key of the labels hash
205
+ # @param rest a selector hash
206
+ def values_for_label(needle, rest = {})
207
+ raise MultiMatrixException, 'labels don\'t match: '+rest.keys.join(",") unless check_labels(rest)
208
+ raise MultiMatrixException, 'label does not exist' unless check_labels({needle => nil})
209
+ ids = find_ids(rest)
210
+ ids.map{|id| @labels_by_id[id][needle]}
211
+ end
212
+
213
+ alias_method :[], :retrieve
214
+ alias_method :[]=, :insert
215
+
216
+ # Test the equality of two NMatrices
217
+
218
+ # NMatrices are equal if they have the same labels,
219
+ # label values, and all their respective values are equal
220
+ # @param other [MultiMatrix]
221
+ def ==(other)
222
+ [[self, other],[other,self]].each do |x|
223
+ x.first.keys do |k|
224
+ if !x.last.key?(k)
225
+ return false
226
+ end
227
+ if x.last[k] != x.first[k]
228
+ return false
229
+ end
230
+ end
231
+ end
232
+ return true
233
+ end
234
+
235
+ # @return labels in MultiMatrix
236
+ def keys
237
+ @values.keys
238
+ end
239
+
240
+ # @return String representation of an MultiMatrix - shown as a Hash of label => value pairs
241
+ def to_s
242
+ hash = {}
243
+ @values.each_pair do |k,v|
244
+ hash[k] = @values_by_id[v]
245
+ end
246
+ hash.to_s
247
+ end
248
+
249
+ private
250
+
251
+ def check_labels(to_check)
252
+ raise MultiMatrixException, 'labels not a hash' if to_check.class != Hash
253
+ raise MultiMatrixException, 'labels hash contains an unknown label' unless (to_check.keys-@labels).empty?
254
+ return true;
255
+ end
256
+
257
+ def get_value(labels)
258
+ @values_by_id[@values[labels]]
259
+ end
260
+
261
+ def save_value(labels, val, options = {})
262
+ if options.key?(:override)
263
+ @values_by_id[options[:override]] = val
264
+ else
265
+ id = generate_id
266
+ @values[labels] = id
267
+ @values_by_id[id] = val
268
+ @labels_by_id[id] = labels
269
+ labels.each_pair do |k, v|
270
+ @finderhash[k] ||= {}
271
+ @finderhash[k][v] ||= []
272
+ @finderhash[k][v].push(id)
273
+ end
274
+ end
275
+ end
276
+
277
+ def find_ids(labels)
278
+ ids = @values.values
279
+ labels.each_pair do |k,v|
280
+ if v.class == Array
281
+ ids &= v.map {|v| @finderhash[k][v]}.flatten.uniq
282
+ elsif @finderhash[k][v].nil?
283
+ return []
284
+ else
285
+ ids &= @finderhash[k][v]
286
+ end
287
+ end
288
+ return ids
289
+ end
290
+
291
+ def generate_id
292
+ @seq = @seq.succ
293
+ return @seq
294
+ end
295
+
296
+ end
@@ -0,0 +1,142 @@
1
+ require 'test/unit'
2
+ require 'multimatrix'
3
+
4
+ class MultiMatrixTest < Test::Unit::TestCase
5
+ def test_new
6
+ assert_raise(MultiMatrixException, "no dimensions") do
7
+ MultiMatrix.new()
8
+ end
9
+ assert_nothing_raised do
10
+ MultiMatrix.new(:x, :y, :z)
11
+ end
12
+ assert_equal MultiMatrix.new(:x,:y,:z).class, MultiMatrix
13
+ end
14
+
15
+ def test_insert
16
+ x = MultiMatrix.new(:x, :y, :z)
17
+
18
+ assert_raise MultiMatrixException do
19
+ x[:x => 1 , :y => 1, :a => 1] = 1
20
+ end
21
+
22
+ assert_raise MultiMatrixException do
23
+ x[:x => 1, :y => 1] = 1
24
+ end
25
+
26
+ assert_raise ArgumentError do
27
+ x[] = 1
28
+ end
29
+
30
+ assert_nothing_raised do
31
+ x[:x => 1, :y => 1, :z => 1] = 1
32
+ x[:x => 1, :y => 1, :z => 2] = 1
33
+ x[:x => 1, :y => 1, :z => 3] = 1
34
+
35
+ x[:x => 1, :y => 1, :z => 1] = 1
36
+ x.insert({:x => 1, :y => 1, :z => 1}, 1) {|a,b| a+b}
37
+ x.insert({:x => 1, :y => 1, :z => 1}, 1) {|a,b| a-b}
38
+ end
39
+ end
40
+
41
+ def test_values
42
+ x = MultiMatrix.new(:i, :j, :x, :y, :z)
43
+ x[:i => 1, :j => 1, :x => 1, :y => 1, :z => 1] = 1
44
+
45
+ assert_equal x[:i => 1, :j => 1, :x => 1, :y => 1, :z => 1], 1
46
+
47
+ x[:i => 1, :j => 1, :x => 1, :y => 1, :z => 1] = 1
48
+
49
+ assert_equal x[:i => 1, :j => 1, :x => 1, :y => 1, :z => 1], 2
50
+
51
+ x.insert({:i => 1, :j => 1, :x => 1, :y => 1, :z => 1}, 1) {|a,b| a-b}
52
+
53
+ assert_equal x[:i => 1, :j => 1, :x => 1, :y => 1, :z => 1], 1
54
+
55
+ x[:i => 1, :j => 1, :x => 1, :y => 1, :z => 2] = 1
56
+
57
+ assert_equal x[:i => 1, :j => 1, :x => 1, :y => 1, :z => 1], 1
58
+ end
59
+
60
+ def test_equality
61
+ x = MultiMatrix.new(:i, :j, :x, :y, :z)
62
+ x[:i => 1, :j => 1, :x => 1, :y => 1, :z => 1] = 1
63
+ x[:i => 1, :j => 1, :x => 1, :y => 1, :z => 2] = 1
64
+
65
+ y = MultiMatrix.new(:i, :j, :x, :y, :z)
66
+ y[:i => 1, :j => 1, :x => 1, :y => 1, :z => 1] = 1
67
+ y[:i => 1, :j => 1, :x => 1, :y => 1, :z => 2] = 1
68
+
69
+ assert_equal x,y
70
+ end
71
+
72
+ def test_MultiMatrix_returns
73
+ x = MultiMatrix.new(:i, :j, :x, :y, :z)
74
+ x[:i => 1, :j => 1, :x => 1, :y => 1, :z => 1] = 1
75
+ x[:i => 1, :j => 1, :x => 1, :y => 1, :z => 2] = 1
76
+
77
+ y = MultiMatrix.new(:z)
78
+ y[:z => 1] = 1
79
+ y[:z => 2] = 1
80
+
81
+ assert_equal x[:i => 1, :j => 1, :x => 1, :y => 1], y
82
+ assert_equal x[:i => 1, :j => 1, :x => 1, :y => 1, :z => [1,2]], y
83
+ end
84
+
85
+ def test_sum
86
+ x = MultiMatrix.new(:i, :j, :x, :y, :z)
87
+ x[:i => 1, :j => 1, :x => 1, :y => 1, :z => 1] = 1
88
+ x[:i => 1, :j => 1, :x => 1, :y => 1, :z => 2] = 1
89
+ x[:i => 1, :j => 1, :x => 1, :y => 1, :z => 3] = 1
90
+
91
+ x[:i => 1, :j => 1, :x => 1, :y => 2, :z => 1] = 1
92
+ x[:i => 1, :j => 1, :x => 1, :y => 2, :z => 2] = 1
93
+ x[:i => 1, :j => 1, :x => 1, :y => 2, :z => 3] = 1
94
+
95
+ assert_equal x.sum({:y => 1}), 3
96
+ assert_equal x.sum({:z => 1}), 2
97
+ assert_equal x.sum({:z => 1, :y=> 1}), 1
98
+ assert_equal x.sum({:i => 1}), 6
99
+ assert_equal x.sum({:y => [1,2]}), 6
100
+ assert_equal x.sum({:y => [1,2], :z => [1,2]}), 4
101
+
102
+ end
103
+
104
+ def test_to_a
105
+ x = MultiMatrix.new(:i, :j, :x, :y, :z)
106
+ x[:i => 1, :j => 1, :x => 1, :y => 1, :z => 1] = 1
107
+ x[:i => 1, :j => 1, :x => 1, :y => 1, :z => 2] = 2
108
+ x[:i => 1, :j => 1, :x => 1, :y => 1, :z => 3] = 3
109
+
110
+ x[:i => 1, :j => 1, :x => 1, :y => 2, :z => 1] = 4
111
+ x[:i => 1, :j => 1, :x => 1, :y => 2, :z => 2] = 5
112
+ x[:i => 1, :j => 1, :x => 1, :y => 2, :z => 3] = 6
113
+
114
+ assert_equal x.to_a.sort, [1,2,3,4,5,6]
115
+ assert_equal x.to_a(:i => 1).sort, [1,2,3,4,5,6]
116
+ assert_equal x.to_a(:z => 2).sort, [2,5]
117
+ assert_equal x.to_a(:y => 1).sort, [1,2,3]
118
+ assert_equal x.to_a(:y => 1, :z => 1), [1]
119
+ end
120
+
121
+ def test_values_for_label
122
+ x = MultiMatrix.new(:i, :j, :x, :y, :z)
123
+ x[:i => 1, :j => 1, :x => 1, :y => 1, :z => 1] = 4
124
+ x[:i => 1, :j => 1, :x => 1, :y => 1, :z => 2] = 5
125
+ x[:i => 1, :j => 1, :x => 1, :y => 2, :z => 3] = 6
126
+
127
+ assert_equal x.values_for_label(:i), [1,1,1]
128
+ assert_equal x.values_for_label(:z).sort, [1,2,3]
129
+ assert_equal x.values_for_label(:y).sort, [1,1,2]
130
+ assert_equal x.values_for_label(:y, :z => 3).sort, [2]
131
+ end
132
+
133
+ def test_nonexistent_values
134
+ x = MultiMatrix.new(:x, :y)
135
+ x[:x => 2, :y => 2] = 1
136
+
137
+ assert_nil x[:x => 1, :y => 1]
138
+ y = MultiMatrix.new(:y)
139
+ assert_equal x[:x => 1],y
140
+ end
141
+
142
+ end
metadata ADDED
@@ -0,0 +1,46 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: multimatrix
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.2'
5
+ platform: ruby
6
+ authors:
7
+ - Gergely Dömsödi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-15 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: An n-dim matrix featuring labels
14
+ email: doome@uhusystems.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/multimatrix.rb
20
+ - test/test_multimatrix.rb
21
+ homepage: http://rubygems.org/gems/multimatrix
22
+ licenses:
23
+ - WTFPL
24
+ metadata: {}
25
+ post_install_message:
26
+ rdoc_options: []
27
+ require_paths:
28
+ - lib
29
+ required_ruby_version: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ required_rubygems_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ requirements: []
40
+ rubyforge_project:
41
+ rubygems_version: 2.1.11
42
+ signing_key:
43
+ specification_version: 4
44
+ summary: n-dim Matrix!
45
+ test_files: []
46
+ has_rdoc: