namo 0.3.0 → 0.5.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 +19 -0
- data/README.md +127 -0
- data/lib/Namo/VERSION.rb +1 -1
- data/lib/namo.rb +35 -0
- data/test/Namo_test.rb +215 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f1cf8cf203da06b319112fec69ad8fae3dddc73ef850d3f84de4d13df2c51249
|
|
4
|
+
data.tar.gz: 1e80df8ee9c7be401c3f9cbf9625eceef0a87b6136a9d8b3825379b32eecff3f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 712227a916253708637a1ca8c1f177e1ab36daba694df13edc2918943779751ba89e71f8d0f578b44d285abf522a869d5f16c3b0f8377899fa7818bbd6009d41
|
|
7
|
+
data.tar.gz: 7c50400df852f31b3a6a12011486cd72f32c946b94c6f7e71a9d83358a50bda4eec9b8854442181112101d82b4b154e61c6159746c697d0adec192f33d99f5a3
|
data/CHANGELOG
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
CHANGELOG
|
|
2
2
|
_________
|
|
3
3
|
|
|
4
|
+
20260416
|
|
5
|
+
0.5.0: + row-axis set operations: intersection (&), union (|), symmetric difference (^)
|
|
6
|
+
|
|
7
|
+
1. + Namo#&: Intersection. Returns rows present in both Namo objects. Requires matching dimensions.
|
|
8
|
+
2. + Namo#|: Union. Returns all rows from both sides, deduplicated. Requires matching dimensions.
|
|
9
|
+
3. + Namo#^: Symmetric difference. Returns rows in one side but not both. Requires matching dimensions.
|
|
10
|
+
4. ~ Namo_test.rb: Add tests for #&, #|, and #^.
|
|
11
|
+
5. ~ README.md: + Intersection section, + Union section, + Symmetric difference section.
|
|
12
|
+
6. ~ Namo::VERSION: /0.4.0/0.5.0/
|
|
13
|
+
|
|
14
|
+
20260415
|
|
15
|
+
0.4.0: + concatenation (+) and row removal (-)
|
|
16
|
+
|
|
17
|
+
1. + Namo#+: Concatenate two Namo instances with matching dimensions. Appends rows from the second to the first. Raises ArgumentError when dimensions differ.
|
|
18
|
+
2. + Namo#-: Remove rows from the first Namo that appear exactly in the second. Whole-row set difference. Raises ArgumentError when dimensions differ.
|
|
19
|
+
3. ~ Namo_test.rb: Add tests for #+ and #-.
|
|
20
|
+
4. ~ README.md: + Concatenation section, + Row removal section.
|
|
21
|
+
5. ~ Namo::VERSION: /0.3.0/0.4.0/
|
|
22
|
+
|
|
4
23
|
20260415
|
|
5
24
|
0.3.0: + contraction
|
|
6
25
|
|
data/README.md
CHANGED
|
@@ -153,6 +153,133 @@ sales[-:price, -:quantity, product: 'Widget']
|
|
|
153
153
|
|
|
154
154
|
Selection, projection, and contraction always return a new Namo instance, so everything chains.
|
|
155
155
|
|
|
156
|
+
### Concatenation
|
|
157
|
+
|
|
158
|
+
`+` combines two Namo objects that share the same dimensions by appending the rows of the second to the first:
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
q1_sales = Namo.new([
|
|
162
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
163
|
+
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40}
|
|
164
|
+
])
|
|
165
|
+
|
|
166
|
+
q2_sales = Namo.new([
|
|
167
|
+
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
|
|
168
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
169
|
+
])
|
|
170
|
+
|
|
171
|
+
all_sales = q1_sales + q2_sales
|
|
172
|
+
# => #<Namo [
|
|
173
|
+
# {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
174
|
+
# {product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
|
|
175
|
+
# {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
|
|
176
|
+
# {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
177
|
+
# ]>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The dimensions must match — concatenating Namo objects with different dimensions raises an `ArgumentError`. Formulae carry through from the left-hand side.
|
|
181
|
+
|
|
182
|
+
### Row Removal
|
|
183
|
+
|
|
184
|
+
`-` removes from the first Namo any row that appears exactly in the second:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
sales = Namo.new([
|
|
188
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
189
|
+
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
|
|
190
|
+
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
|
|
191
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
192
|
+
])
|
|
193
|
+
|
|
194
|
+
discontinued = Namo.new([
|
|
195
|
+
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
|
|
196
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
197
|
+
])
|
|
198
|
+
|
|
199
|
+
sales - discontinued
|
|
200
|
+
# => #<Namo [
|
|
201
|
+
# {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
202
|
+
# {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
|
|
203
|
+
# ]>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Removal is exact — every dimension, every value must match. The dimensions must match; different dimensions raise an `ArgumentError`. Formulae carry through from the left-hand side.
|
|
207
|
+
|
|
208
|
+
### Intersection
|
|
209
|
+
|
|
210
|
+
`&` returns the rows present in both Namo objects, like `Array#&`:
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
sales = Namo.new([
|
|
214
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
215
|
+
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
|
|
216
|
+
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
|
|
217
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
218
|
+
])
|
|
219
|
+
|
|
220
|
+
confirmed = Namo.new([
|
|
221
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
222
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
223
|
+
])
|
|
224
|
+
|
|
225
|
+
sales & confirmed
|
|
226
|
+
# => #<Namo [
|
|
227
|
+
# {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
228
|
+
# {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
229
|
+
# ]>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The dimensions must match; different dimensions raise an `ArgumentError`. Formulae carry through from the left-hand side.
|
|
233
|
+
|
|
234
|
+
### Union
|
|
235
|
+
|
|
236
|
+
`|` returns all rows from both sides, deduplicated, like `Array#|`:
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
q1_sales = Namo.new([
|
|
240
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
241
|
+
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40}
|
|
242
|
+
])
|
|
243
|
+
|
|
244
|
+
all_sales = Namo.new([
|
|
245
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
246
|
+
{product: 'Thingo', quarter: 'Q3', price: 5.0, quantity: 10}
|
|
247
|
+
])
|
|
248
|
+
|
|
249
|
+
q1_sales | all_sales
|
|
250
|
+
# => #<Namo [
|
|
251
|
+
# {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
252
|
+
# {product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
|
|
253
|
+
# {product: 'Thingo', quarter: 'Q3', price: 5.0, quantity: 10}
|
|
254
|
+
# ]>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The dimensions must match; different dimensions raise an `ArgumentError`. Formulae merge from both sides; the left-hand side's formulae take precedence on conflict.
|
|
258
|
+
|
|
259
|
+
### Symmetric Difference
|
|
260
|
+
|
|
261
|
+
`^` returns rows that appear in one side but not both:
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
set_a = Namo.new([
|
|
265
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
266
|
+
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40}
|
|
267
|
+
])
|
|
268
|
+
|
|
269
|
+
set_b = Namo.new([
|
|
270
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
271
|
+
{product: 'Thingo', quarter: 'Q3', price: 5.0, quantity: 10}
|
|
272
|
+
])
|
|
273
|
+
|
|
274
|
+
set_a ^ set_b
|
|
275
|
+
# => #<Namo [
|
|
276
|
+
# {product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
|
|
277
|
+
# {product: 'Thingo', quarter: 'Q3', price: 5.0, quantity: 10}
|
|
278
|
+
# ]>
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The dimensions must match; different dimensions raise an `ArgumentError`. Formulae merge from both sides; the left-hand side's formulae take precedence on conflict.
|
|
282
|
+
|
|
156
283
|
### Formulae
|
|
157
284
|
|
|
158
285
|
Define computed dimensions using `[]=`:
|
data/lib/Namo/VERSION.rb
CHANGED
data/lib/namo.rb
CHANGED
|
@@ -56,6 +56,41 @@ class Namo
|
|
|
56
56
|
@data.each{|row_data| block.call(Row.new(row_data, @formulae))}
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
def +(other)
|
|
60
|
+
unless dimensions == other.dimensions
|
|
61
|
+
raise ArgumentError, "dimensions do not match"
|
|
62
|
+
end
|
|
63
|
+
self.class.new(@data + other.data, formulae: other.formulae.merge(@formulae))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def -(other)
|
|
67
|
+
unless dimensions == other.dimensions
|
|
68
|
+
raise ArgumentError, "dimensions do not match"
|
|
69
|
+
end
|
|
70
|
+
self.class.new(@data - other.data, formulae: @formulae.dup)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def &(other)
|
|
74
|
+
unless dimensions == other.dimensions
|
|
75
|
+
raise ArgumentError, "dimensions do not match"
|
|
76
|
+
end
|
|
77
|
+
self.class.new(@data & other.data, formulae: @formulae.dup)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def |(other)
|
|
81
|
+
unless dimensions == other.dimensions
|
|
82
|
+
raise ArgumentError, "dimensions do not match"
|
|
83
|
+
end
|
|
84
|
+
self.class.new((@data | other.data), formulae: other.formulae.merge(@formulae))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def ^(other)
|
|
88
|
+
unless dimensions == other.dimensions
|
|
89
|
+
raise ArgumentError, "dimensions do not match"
|
|
90
|
+
end
|
|
91
|
+
self.class.new((@data - other.data) + (other.data - @data), formulae: other.formulae.merge(@formulae))
|
|
92
|
+
end
|
|
93
|
+
|
|
59
94
|
def to_a
|
|
60
95
|
@data.map do |row|
|
|
61
96
|
row.keys.each_with_object({}) do |key, hash|
|
data/test/Namo_test.rb
CHANGED
|
@@ -251,6 +251,221 @@ describe Namo do
|
|
|
251
251
|
end
|
|
252
252
|
end
|
|
253
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 "#&" do
|
|
341
|
+
let(:other) do
|
|
342
|
+
Namo.new([
|
|
343
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
344
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60},
|
|
345
|
+
{product: 'Thingo', quarter: 'Q3', price: 5.0, quantity: 10}
|
|
346
|
+
])
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
it "returns rows present in both" do
|
|
350
|
+
result = sales & other
|
|
351
|
+
_(result.to_a).must_equal [
|
|
352
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
353
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
354
|
+
]
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
it "returns empty when nothing overlaps" do
|
|
358
|
+
other = Namo.new([{product: 'Thingo', quarter: 'Q3', price: 5.0, quantity: 10}])
|
|
359
|
+
result = sales & other
|
|
360
|
+
_(result.to_a).must_equal []
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
it "preserves dimensions" do
|
|
364
|
+
result = sales & other
|
|
365
|
+
_(result.dimensions).must_equal [:product, :quarter, :price, :quantity]
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
it "carries formulae through from self" do
|
|
369
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
370
|
+
result = sales & other
|
|
371
|
+
_(result.map{|row| row[:revenue]}).must_equal [1000.0, 1500.0]
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
it "raises when dimensions differ" do
|
|
375
|
+
other = Namo.new([{product: 'Widget', quarter: 'Q1'}])
|
|
376
|
+
_ { sales & other }.must_raise ArgumentError
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
describe "#|" do
|
|
381
|
+
let(:other) do
|
|
382
|
+
Namo.new([
|
|
383
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
384
|
+
{product: 'Thingo', quarter: 'Q3', price: 5.0, quantity: 10}
|
|
385
|
+
])
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
it "returns all rows deduplicated" do
|
|
389
|
+
result = sales | other
|
|
390
|
+
_(result.to_a).must_equal [
|
|
391
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
392
|
+
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
|
|
393
|
+
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
|
|
394
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60},
|
|
395
|
+
{product: 'Thingo', quarter: 'Q3', price: 5.0, quantity: 10}
|
|
396
|
+
]
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
it "preserves dimensions" do
|
|
400
|
+
result = sales | other
|
|
401
|
+
_(result.dimensions).must_equal [:product, :quarter, :price, :quantity]
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
it "merges formulae from other" do
|
|
405
|
+
other[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
406
|
+
result = sales | other
|
|
407
|
+
_(result.map{|row| row[:revenue]}).must_equal [1000.0, 1500.0, 1000.0, 1500.0, 50.0]
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
it "prefers self's formulae on conflict" do
|
|
411
|
+
sales[:label] = proc{|r| "self: #{r[:product]}"}
|
|
412
|
+
other[:label] = proc{|r| "other: #{r[:product]}"}
|
|
413
|
+
result = sales | other
|
|
414
|
+
_(result.map{|row| row[:label]}).must_equal [
|
|
415
|
+
'self: Widget', 'self: Widget', 'self: Gadget', 'self: Gadget', 'self: Thingo'
|
|
416
|
+
]
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
it "raises when dimensions differ" do
|
|
420
|
+
other = Namo.new([{product: 'Widget', quarter: 'Q1'}])
|
|
421
|
+
_ { sales | other }.must_raise ArgumentError
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
describe "#^" do
|
|
426
|
+
let(:other) do
|
|
427
|
+
Namo.new([
|
|
428
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
429
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60},
|
|
430
|
+
{product: 'Thingo', quarter: 'Q3', price: 5.0, quantity: 10}
|
|
431
|
+
])
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
it "returns rows in one but not both" do
|
|
435
|
+
result = sales ^ other
|
|
436
|
+
_(result.to_a).must_equal [
|
|
437
|
+
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
|
|
438
|
+
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
|
|
439
|
+
{product: 'Thingo', quarter: 'Q3', price: 5.0, quantity: 10}
|
|
440
|
+
]
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
it "returns empty when both are identical" do
|
|
444
|
+
result = sales ^ sales
|
|
445
|
+
_(result.to_a).must_equal []
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
it "returns all rows when nothing overlaps" do
|
|
449
|
+
other = Namo.new([{product: 'Thingo', quarter: 'Q3', price: 5.0, quantity: 10}])
|
|
450
|
+
result = sales ^ other
|
|
451
|
+
_(result.to_a.count).must_equal 5
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
it "merges formulae with self winning on conflict" do
|
|
455
|
+
sales[:label] = proc{|r| "self: #{r[:product]}"}
|
|
456
|
+
other[:label] = proc{|r| "other: #{r[:product]}"}
|
|
457
|
+
result = sales ^ other
|
|
458
|
+
_(result.map{|row| row[:label]}).must_equal [
|
|
459
|
+
'self: Widget', 'self: Gadget', 'self: Thingo'
|
|
460
|
+
]
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
it "raises when dimensions differ" do
|
|
464
|
+
other = Namo.new([{product: 'Widget', quarter: 'Q1'}])
|
|
465
|
+
_ { sales ^ other }.must_raise ArgumentError
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
254
469
|
describe "#to_a" do
|
|
255
470
|
it "returns the data as an array of hashes" do
|
|
256
471
|
_(sales.to_a).must_equal sample_data
|