namo 0.4.0 → 0.6.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG +25 -0
- data/README.md +164 -0
- data/Rakefile +33 -0
- data/lib/Namo/VERSION.rb +1 -1
- data/lib/namo.rb +98 -9
- data/namo.gemspec +0 -1
- data/test/namo_test.rb +750 -0
- metadata +3 -3
- data/test/Namo_test.rb +0 -345
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: namo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- thoran
|
|
@@ -72,8 +72,8 @@ files:
|
|
|
72
72
|
- namo.gemspec
|
|
73
73
|
- test/Namo/NegatedDimension_test.rb
|
|
74
74
|
- test/Namo/Row_test.rb
|
|
75
|
-
- test/Namo_test.rb
|
|
76
75
|
- test/Symbol_test.rb
|
|
76
|
+
- test/namo_test.rb
|
|
77
77
|
homepage: https://github.com/thoran/namo
|
|
78
78
|
licenses:
|
|
79
79
|
- MIT
|
|
@@ -92,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
92
92
|
- !ruby/object:Gem::Version
|
|
93
93
|
version: '0'
|
|
94
94
|
requirements: []
|
|
95
|
-
rubygems_version: 4.0.
|
|
95
|
+
rubygems_version: 4.0.11
|
|
96
96
|
specification_version: 4
|
|
97
97
|
summary: Named dimensional data for Ruby.
|
|
98
98
|
test_files: []
|
data/test/Namo_test.rb
DELETED
|
@@ -1,345 +0,0 @@
|
|
|
1
|
-
require 'minitest/autorun'
|
|
2
|
-
require 'minitest-spec-context'
|
|
3
|
-
|
|
4
|
-
require_relative '../lib/namo'
|
|
5
|
-
|
|
6
|
-
describe Namo do
|
|
7
|
-
let(:sample_data) do
|
|
8
|
-
[
|
|
9
|
-
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
10
|
-
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
|
|
11
|
-
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
|
|
12
|
-
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
13
|
-
]
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
let(:sales) do
|
|
17
|
-
Namo.new(sample_data)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
describe "#dimensions" do
|
|
21
|
-
it "infers dimensions from hash keys" do
|
|
22
|
-
_(sales.dimensions).must_equal [:product, :quarter, :price, :quantity]
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
describe "#coordinates" do
|
|
27
|
-
it "extracts unique values for each dimension" do
|
|
28
|
-
_(sales.coordinates).must_equal ({
|
|
29
|
-
product: ['Widget', 'Gadget'],
|
|
30
|
-
quarter: ['Q1', 'Q2'],
|
|
31
|
-
price: [10.0, 25.0],
|
|
32
|
-
quantity: [100, 150, 40, 60]
|
|
33
|
-
})
|
|
34
|
-
_(sales.coordinates[:product]).must_equal ['Widget', 'Gadget']
|
|
35
|
-
_(sales.coordinates[:quarter]).must_equal ['Q1', 'Q2']
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
describe "#[]" do
|
|
40
|
-
context "selection" do
|
|
41
|
-
it "selects by single coordinate" do
|
|
42
|
-
result = sales[product: 'Widget']
|
|
43
|
-
_(result.coordinates[:product]).must_equal ['Widget']
|
|
44
|
-
_(result.to_a.count).must_equal 2
|
|
45
|
-
_(result.to_a.map{|row| row[:product]}).must_equal ['Widget', 'Widget']
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
it "selects by array of coordinates" do
|
|
49
|
-
result = sales[quarter: ['Q1']]
|
|
50
|
-
_(result.coordinates[:quarter]).must_equal ['Q1']
|
|
51
|
-
_(result.to_a.count).must_equal 2
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
it "selects by multiple dimensions" do
|
|
55
|
-
result = sales[product: 'Widget', quarter: 'Q1']
|
|
56
|
-
_(result.to_a.count).must_equal 1
|
|
57
|
-
_(result.to_a).must_equal [{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100}]
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
it "returns a Namo instance" do
|
|
61
|
-
result = sales[product: 'Widget']
|
|
62
|
-
_(result.coordinates[:product]).must_equal ['Widget']
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
context "projection" do
|
|
67
|
-
it "projects to named dimensions" do
|
|
68
|
-
result = sales[:product, :price]
|
|
69
|
-
_(result.dimensions).must_equal [:product, :price]
|
|
70
|
-
_(result.coordinates[:product]).must_equal ['Widget', 'Gadget']
|
|
71
|
-
_(result.to_a.count).must_equal 4
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
context "selection and projection" do
|
|
76
|
-
it "can use them together" do
|
|
77
|
-
result = sales[:price, product: 'Widget']
|
|
78
|
-
_(result.to_a.count).must_equal 2
|
|
79
|
-
_(result.to_a).must_equal [{price: 10.0}, {price: 10.0}]
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
it "can chain them" do
|
|
83
|
-
result = sales[product: 'Widget'][:price]
|
|
84
|
-
_(result.to_a.count).must_equal 2
|
|
85
|
-
_(result.to_a).must_equal [{price: 10.0}, {price: 10.0}]
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
context "contraction" do
|
|
90
|
-
it "removes named dimensions" do
|
|
91
|
-
result = sales[-:price, -:quantity]
|
|
92
|
-
_(result.dimensions).must_equal [:product, :quarter]
|
|
93
|
-
_(result.to_a.count).must_equal 4
|
|
94
|
-
_(result.to_a.first).must_equal({product: 'Widget', quarter: 'Q1'})
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
it "removes a single dimension" do
|
|
98
|
-
result = sales[-:price]
|
|
99
|
-
_(result.dimensions).must_equal [:product, :quarter, :quantity]
|
|
100
|
-
_(result.to_a.count).must_equal 4
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
it "raises when mixing projection and contraction" do
|
|
104
|
-
_ { sales[:product, -:price] }.must_raise ArgumentError
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
it "carries formulae through contraction" do
|
|
108
|
-
sales[:label] = proc{|r| "#{r[:product]}-#{r[:quarter]}"}
|
|
109
|
-
result = sales[-:price, -:quantity]
|
|
110
|
-
_(result.map{|row| row[:label]}).must_equal [
|
|
111
|
-
'Widget-Q1', 'Widget-Q2', 'Gadget-Q1', 'Gadget-Q2'
|
|
112
|
-
]
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
context "selection and contraction" do
|
|
117
|
-
it "can use them together" do
|
|
118
|
-
result = sales[-:price, -:quantity, product: 'Widget']
|
|
119
|
-
_(result.to_a.count).must_equal 2
|
|
120
|
-
_(result.to_a).must_equal [
|
|
121
|
-
{product: 'Widget', quarter: 'Q1'},
|
|
122
|
-
{product: 'Widget', quarter: 'Q2'}
|
|
123
|
-
]
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
it "can chain them" do
|
|
127
|
-
result = sales[product: 'Widget'][-:price, -:quantity]
|
|
128
|
-
_(result.to_a.count).must_equal 2
|
|
129
|
-
_(result.to_a).must_equal [
|
|
130
|
-
{product: 'Widget', quarter: 'Q1'},
|
|
131
|
-
{product: 'Widget', quarter: 'Q2'}
|
|
132
|
-
]
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
describe "#[]= formulae" do
|
|
138
|
-
it "defines a formula" do
|
|
139
|
-
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
140
|
-
_(sales[:product, :quarter, :revenue].to_a).must_equal [
|
|
141
|
-
{product: "Widget", quarter: "Q1", revenue: 1000.0},
|
|
142
|
-
{product: "Widget", quarter: "Q2", revenue: 1500.0},
|
|
143
|
-
{product: "Gadget", quarter: "Q1", revenue: 1000.0},
|
|
144
|
-
{product: "Gadget", quarter: "Q2", revenue: 1500.0}
|
|
145
|
-
]
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
it "composes formulae" do
|
|
149
|
-
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
150
|
-
sales[:cost] = proc{|r| r[:quantity] * 4.0}
|
|
151
|
-
sales[:profit] = proc{|r| r[:revenue] - r[:cost]}
|
|
152
|
-
_(sales[:product, :quarter, :profit].to_a).must_equal [
|
|
153
|
-
{product: "Widget", quarter: "Q1", profit: 600.0},
|
|
154
|
-
{product: "Widget", quarter: "Q2", profit: 900.0},
|
|
155
|
-
{product: "Gadget", quarter: "Q1", profit: 840.0},
|
|
156
|
-
{product: "Gadget", quarter: "Q2", profit: 1260.0}
|
|
157
|
-
]
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
it "works with chained selection and projection" do
|
|
161
|
-
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
162
|
-
result = sales[product: 'Widget'][:product, :quarter, :revenue]
|
|
163
|
-
_(result.to_a).must_equal [
|
|
164
|
-
{product: "Widget", quarter: "Q1", revenue: 1000.0},
|
|
165
|
-
{product: "Widget", quarter: "Q2", revenue: 1500.0}
|
|
166
|
-
]
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
it "works with single-call selection and projection" do
|
|
170
|
-
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
171
|
-
result = sales[:product, :revenue, product: 'Widget']
|
|
172
|
-
_(result.to_a).must_equal [
|
|
173
|
-
{product: "Widget", revenue: 1000.0},
|
|
174
|
-
{product: "Widget", revenue: 1500.0}
|
|
175
|
-
]
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
it "carries formulae through selection" do
|
|
179
|
-
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
180
|
-
widgets = sales[product: 'Widget']
|
|
181
|
-
_(widgets[:revenue].to_a).must_equal [{revenue: 1000.0}, {revenue: 1500.0}]
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
it "projects formula with context dimensions" do
|
|
185
|
-
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
186
|
-
result = sales[:product, :quarter, :revenue]
|
|
187
|
-
_(result.to_a).must_equal [
|
|
188
|
-
{product: 'Widget', quarter: 'Q1', revenue: 1000.0},
|
|
189
|
-
{product: 'Widget', quarter: 'Q2', revenue: 1500.0},
|
|
190
|
-
{product: 'Gadget', quarter: 'Q1', revenue: 1000.0},
|
|
191
|
-
{product: 'Gadget', quarter: 'Q2', revenue: 1500.0}
|
|
192
|
-
]
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
describe "#each" do
|
|
197
|
-
it "yields Row objects" do
|
|
198
|
-
rows = []
|
|
199
|
-
sales.each{|row| rows << row}
|
|
200
|
-
_(rows.first).must_be_kind_of Namo::Row
|
|
201
|
-
_(rows.length).must_equal 4
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
it "yields rows with access to data" do
|
|
205
|
-
products = sales.map{|row| row[:product]}
|
|
206
|
-
_(products).must_equal ['Widget', 'Widget', 'Gadget', 'Gadget']
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
it "yields rows with access to formulae" do
|
|
210
|
-
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
211
|
-
revenues = sales.map{|row| row[:revenue]}
|
|
212
|
-
_(revenues).must_equal [1000.0, 1500.0, 1000.0, 1500.0]
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
it "returns an enumerator without a block" do
|
|
216
|
-
_(sales.each).must_be_kind_of Enumerator
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
describe "Enumerable" do
|
|
221
|
-
it "supports reduce" do
|
|
222
|
-
total_quantity = sales.reduce(0){|sum, row| sum + row[:quantity]}
|
|
223
|
-
_(total_quantity).must_equal 350
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
it "supports reduce with selection" do
|
|
227
|
-
widget_quantity = sales[product: 'Widget'].reduce(0){|sum, row| sum + row[:quantity]}
|
|
228
|
-
_(widget_quantity).must_equal 250
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
it "supports reduce with formulae" do
|
|
232
|
-
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
233
|
-
total_revenue = sales.reduce(0){|sum, row| sum + row[:revenue]}
|
|
234
|
-
_(total_revenue).must_equal 5000.0
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
it "supports reduce with selection and formulae" do
|
|
238
|
-
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
239
|
-
widget_revenue = sales[product: 'Widget'].reduce(0){|sum, row| sum + row[:revenue]}
|
|
240
|
-
_(widget_revenue).must_equal 2500.0
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
it "supports min_by" do
|
|
244
|
-
cheapest = sales.min_by{|row| row[:price]}
|
|
245
|
-
_(cheapest[:product]).must_equal 'Widget'
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
it "supports flat_map" do
|
|
249
|
-
prices = sales.flat_map{|row| [row[:price]]}
|
|
250
|
-
_(prices).must_equal [10.0, 10.0, 25.0, 25.0]
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
describe "#+" do
|
|
255
|
-
let(:more_data) do
|
|
256
|
-
[
|
|
257
|
-
{product: 'Widget', quarter: 'Q3', price: 10.0, quantity: 200},
|
|
258
|
-
{product: 'Gadget', quarter: 'Q3', price: 25.0, quantity: 80}
|
|
259
|
-
]
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
let(:more_sales) do
|
|
263
|
-
Namo.new(more_data)
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
it "concatenates rows" do
|
|
267
|
-
result = sales + more_sales
|
|
268
|
-
_(result.to_a.count).must_equal 6
|
|
269
|
-
_(result.to_a).must_equal(sample_data + more_data)
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
it "preserves dimensions" do
|
|
273
|
-
result = sales + more_sales
|
|
274
|
-
_(result.dimensions).must_equal [:product, :quarter, :price, :quantity]
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
it "carries formulae through from self" do
|
|
278
|
-
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
279
|
-
result = sales + more_sales
|
|
280
|
-
_(result.map{|row| row[:revenue]}).must_equal [1000.0, 1500.0, 1000.0, 1500.0, 2000.0, 2000.0]
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
it "merges formulae from other" do
|
|
284
|
-
more_sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
285
|
-
result = sales + more_sales
|
|
286
|
-
_(result.map{|row| row[:revenue]}).must_equal [1000.0, 1500.0, 1000.0, 1500.0, 2000.0, 2000.0]
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
it "prefers self's formulae on conflict" do
|
|
290
|
-
sales[:label] = proc{|r| "self: #{r[:product]}"}
|
|
291
|
-
more_sales[:label] = proc{|r| "other: #{r[:product]}"}
|
|
292
|
-
result = sales + more_sales
|
|
293
|
-
_(result.map{|row| row[:label]}).must_equal [
|
|
294
|
-
'self: Widget', 'self: Widget', 'self: Gadget', 'self: Gadget',
|
|
295
|
-
'self: Widget', 'self: Gadget'
|
|
296
|
-
]
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
it "raises when dimensions differ" do
|
|
300
|
-
other = Namo.new([{product: 'Widget', quarter: 'Q1'}])
|
|
301
|
-
_ { sales + other }.must_raise ArgumentError
|
|
302
|
-
end
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
describe "#-" do
|
|
306
|
-
let(:to_remove) do
|
|
307
|
-
Namo.new([
|
|
308
|
-
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
309
|
-
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
310
|
-
])
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
it "removes matching rows" do
|
|
314
|
-
result = sales - to_remove
|
|
315
|
-
_(result.to_a.count).must_equal 2
|
|
316
|
-
_(result.to_a).must_equal [
|
|
317
|
-
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
|
|
318
|
-
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40}
|
|
319
|
-
]
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
it "preserves non-matching rows" do
|
|
323
|
-
other = Namo.new([{product: 'Thingo', quarter: 'Q4', price: 99.0, quantity: 1}])
|
|
324
|
-
result = sales - other
|
|
325
|
-
_(result.to_a).must_equal sample_data
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
it "carries formulae through from self" do
|
|
329
|
-
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
330
|
-
result = sales - to_remove
|
|
331
|
-
_(result.map{|row| row[:revenue]}).must_equal [1500.0, 1000.0]
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
it "raises when dimensions differ" do
|
|
335
|
-
other = Namo.new([{product: 'Widget', quarter: 'Q1'}])
|
|
336
|
-
_ { sales - other }.must_raise ArgumentError
|
|
337
|
-
end
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
describe "#to_a" do
|
|
341
|
-
it "returns the data as an array of hashes" do
|
|
342
|
-
_(sales.to_a).must_equal sample_data
|
|
343
|
-
end
|
|
344
|
-
end
|
|
345
|
-
end
|