namo 0.4.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 +10 -0
- data/README.md +75 -0
- data/lib/Namo/VERSION.rb +1 -1
- data/lib/namo.rb +21 -0
- data/test/Namo_test.rb +129 -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,16 @@
|
|
|
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
|
+
|
|
4
14
|
20260415
|
|
5
15
|
0.4.0: + concatenation (+) and row removal (-)
|
|
6
16
|
|
data/README.md
CHANGED
|
@@ -205,6 +205,81 @@ sales - discontinued
|
|
|
205
205
|
|
|
206
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
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
|
+
|
|
208
283
|
### Formulae
|
|
209
284
|
|
|
210
285
|
Define computed dimensions using `[]=`:
|
data/lib/Namo/VERSION.rb
CHANGED
data/lib/namo.rb
CHANGED
|
@@ -70,6 +70,27 @@ class Namo
|
|
|
70
70
|
self.class.new(@data - other.data, formulae: @formulae.dup)
|
|
71
71
|
end
|
|
72
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
|
+
|
|
73
94
|
def to_a
|
|
74
95
|
@data.map do |row|
|
|
75
96
|
row.keys.each_with_object({}) do |key, hash|
|
data/test/Namo_test.rb
CHANGED
|
@@ -337,6 +337,135 @@ describe Namo do
|
|
|
337
337
|
end
|
|
338
338
|
end
|
|
339
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
|
+
|
|
340
469
|
describe "#to_a" do
|
|
341
470
|
it "returns the data as an array of hashes" do
|
|
342
471
|
_(sales.to_a).must_equal sample_data
|