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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0dbfe699c8bbb334e0f72bbfd1859b0eee910be2c72eaab988da633b58b438e6
4
- data.tar.gz: 40238ef4593d7820bddfb6cb708823fa9c269a51def947e89cdb43e4b69cbae0
3
+ metadata.gz: f1cf8cf203da06b319112fec69ad8fae3dddc73ef850d3f84de4d13df2c51249
4
+ data.tar.gz: 1e80df8ee9c7be401c3f9cbf9625eceef0a87b6136a9d8b3825379b32eecff3f
5
5
  SHA512:
6
- metadata.gz: 9e6dfd0bcf4b8d370373a43e507a4d208e4ccb4478e044eb138bf5f2bfa61612cbb4f10735be691c852d2f8bf672eca70720cc6a5c35cd5fb02fae224388e010
7
- data.tar.gz: 2b147c6e7faf7b9f92e8e2457fcd657734a35946c326857e3b7c09177ceaa7f3ce4f0892bdcbb68ec251d673e1b298c1ed06416fe43918656734a58bd0c639ea
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
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.4.0'
5
+ VERSION = '0.5.0'
6
6
  end
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
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.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran