namo 0.13.2 → 0.14.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 +31 -0
- data/README.md +59 -0
- data/lib/Namo/VERSION.rb +1 -1
- data/lib/namo.rb +15 -8
- data/test/namo_test.rb +204 -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: 78880ca991afa35b970cc09625c4ca632db3a3ff69e7b4c69fda2fa3b536bd08
|
|
4
|
+
data.tar.gz: b85bfe8ef28cf45880e8ebda997d209e7a4f6d545710bad52ec4f6e5c790e30f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c61419caff79be760cc24d3cd67eb008107061a65041853bd5108e51c70bf0fb100e50c899241b2ae45de59d92c9f7bcd6173eb2af279543c9057c186c3ec4ba
|
|
7
|
+
data.tar.gz: 5b9fe7311512c5a59f6528d70a82724f35323769267a804784d14a83351da08cb62000feb6f676e396ecd45c79397413f754cb09c599e5204f28212e95caa872
|
data/CHANGELOG
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
CHANGELOG
|
|
2
2
|
_________
|
|
3
3
|
|
|
4
|
+
20260608
|
|
5
|
+
0.14.0: + blocks on the composition operators (*, **) for custom match refinement.
|
|
6
|
+
|
|
7
|
+
1. ~ lib/namo.rb: Namo#* takes an optional block. Without a block, behaviour is unchanged.
|
|
8
|
+
With a block, the right rows matched on the shared data dimensions are passed as a Namo
|
|
9
|
+
(candidates, carrying other's formulae) alongside the left Row (carrying self's
|
|
10
|
+
formulae); the block returns a Namo of the rows to pair, each merged into the left row.
|
|
11
|
+
An empty returned Namo drops the left row (inner-join semantics). Result formulae are
|
|
12
|
+
other.formulae.merge(@formulae) as before — the block does not affect result formulae.
|
|
13
|
+
The shared-dimension precondition still applies with a block present.
|
|
14
|
+
2. ~ lib/namo.rb: Namo#** takes an optional block, same contract, with candidates being all
|
|
15
|
+
of other's rows (no shared-dimension pre-filter). The disjoint-dimensions precondition
|
|
16
|
+
still applies with a block present.
|
|
17
|
+
3. ~ test/namo_test.rb: + "with a block" context under #* (single-match one-row return, empty-return
|
|
18
|
+
drop, multi-row return, selection on other's derived dimension, reference to self's
|
|
19
|
+
derived dimension, result-formulae parity with no-block, derived-not-stored-but-resolves,
|
|
20
|
+
subclass type, preconditions still raise) and under #** (selector filtering one-to-many,
|
|
21
|
+
empty-return drop, full-candidates reproduces no-block, selection on other's derived
|
|
22
|
+
dimension, result-formulae parity, disjoint precondition still raises, subclass type).
|
|
23
|
+
Existing no-block #* and #** tests unchanged and green.
|
|
24
|
+
4. ~ README.md: + block subsections under Composition and Cartesian product, with the
|
|
25
|
+
price/quarterly matching and orders/tiers worked examples and the (row, candidates) ->
|
|
26
|
+
Namo contract.
|
|
27
|
+
5. ~ ROADMAP.md: Promote 0.14.0 to shipped (composition blocks only); Current state -> 0.14.0;
|
|
28
|
+
Summary folds in composition blocks; next phase -> 0.15.0. + the governing principle
|
|
29
|
+
(a block form is warranted iff the operation gives consideration to a dimension in
|
|
30
|
+
isolation), tied to the orthogonality/efficiency rationale for why set-operator/`/`
|
|
31
|
+
blocks were not added. The now-redundant upcoming 0.14.0 section removed.
|
|
32
|
+
6. ~ COMPARISON.md: "Conditional join with block" -> shipped (0.14.0).
|
|
33
|
+
7. ~ Namo::VERSION: /0.13.2/0.14.0/
|
|
34
|
+
|
|
4
35
|
20260608
|
|
5
36
|
0.13.2: Narrow the planned 0.14.0 block-form scope to the composition operators and document the rationale.
|
|
6
37
|
|
data/README.md
CHANGED
|
@@ -355,6 +355,36 @@ Inner-join semantics: unmatched rows from either side are dropped. Output dimens
|
|
|
355
355
|
|
|
356
356
|
The two Namos must have at least one shared data dimension. No overlap raises an `ArgumentError` — the asymmetry with `**` is deliberate, and falling through to a Cartesian product would silently turn a logic error into a large pile of nonsense rows. Formulae merge from both sides; the left-hand side wins on conflict.
|
|
357
357
|
|
|
358
|
+
#### Conditional join
|
|
359
|
+
|
|
360
|
+
`*` takes an optional block that decides which matched rows to pair with each left row. Without a block, every shared-dimension match pairs, as above. With one, the block is handed the current left row and the right rows already matched on the shared dimensions, and returns the subset to pair — the refinement plain `*` can't express, because it pairs every match.
|
|
361
|
+
|
|
362
|
+
The canonical case is matching each daily price to a single quarterly report — the most recent one dated on or before it. Plain `*` pairs *every* matching quarter; the block narrows that to the one the matching rule picks.
|
|
363
|
+
|
|
364
|
+
```ruby
|
|
365
|
+
prices = Namo.new([
|
|
366
|
+
{symbol: 'BHP', date: '2025-02-15', close: 42.5},
|
|
367
|
+
{symbol: 'BHP', date: '2025-05-20', close: 44.0}
|
|
368
|
+
])
|
|
369
|
+
|
|
370
|
+
quarterly = Namo.new([
|
|
371
|
+
{symbol: 'BHP', quarter_end: '2024-12-31', eps: 1.0},
|
|
372
|
+
{symbol: 'BHP', quarter_end: '2025-03-31', eps: 1.2}
|
|
373
|
+
])
|
|
374
|
+
|
|
375
|
+
prices.*(quarterly) do |row, candidates|
|
|
376
|
+
candidates[quarter_end: ->(qe){qe <= row[:date]}].sort_by{|f| f[:quarter_end]}.last(1)
|
|
377
|
+
end
|
|
378
|
+
# => #<Namo [
|
|
379
|
+
# {symbol: 'BHP', date: '2025-02-15', close: 42.5, quarter_end: '2024-12-31', eps: 1.0},
|
|
380
|
+
# {symbol: 'BHP', date: '2025-05-20', close: 44.0, quarter_end: '2025-03-31', eps: 1.2}
|
|
381
|
+
# ]>
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
`row` is the `Row` for the current left row, carrying self's formulae, so `row[:date]` and any self formula resolve inside the block. `candidates` is a Namo of the shared-dimension matches, carrying other's formulae, so the block can select on other's derived dimensions too. The block returns a Namo of the rows to pair: one row for the single-match rule above, though it may return zero, one, or many — it's a selector, not a reducer. An empty returned Namo pairs nothing, so that left row is dropped, preserving inner-join semantics. The block can also be passed as a named proc, `prices.*(quarterly, &most_recent_quarter)`.
|
|
385
|
+
|
|
386
|
+
The block changes only which rows pair. Formulae carry through exactly as in the no-block form — other's merged under self's, self winning on conflict — and the rows the block returns contribute data only.
|
|
387
|
+
|
|
358
388
|
### Cartesian product
|
|
359
389
|
|
|
360
390
|
`**` is the Cartesian product. Every row from the left paired with every row from the right:
|
|
@@ -378,6 +408,35 @@ The two Namos must have **no** shared data dimensions — the precondition is th
|
|
|
378
408
|
|
|
379
409
|
The visual relationship is intentional: `*` is the filtered version, `**` is the explosive version — more sigil, more output.
|
|
380
410
|
|
|
411
|
+
#### Conditional product
|
|
412
|
+
|
|
413
|
+
`**` takes an optional block on the same contract. Where `*`'s block receives the rows pre-matched on the shared dimensions, `**`'s receives *all* of other's rows — there are no shared dimensions to match on — and returns the subset to pair with each left row.
|
|
414
|
+
|
|
415
|
+
This expresses a conditional product: pair each order with only the shipping tiers that can carry it.
|
|
416
|
+
|
|
417
|
+
```ruby
|
|
418
|
+
orders = Namo.new([
|
|
419
|
+
{order: 'A', weight: 5},
|
|
420
|
+
{order: 'B', weight: 15}
|
|
421
|
+
])
|
|
422
|
+
|
|
423
|
+
tiers = Namo.new([
|
|
424
|
+
{tier: 'light', max_weight: 10},
|
|
425
|
+
{tier: 'heavy', max_weight: 20}
|
|
426
|
+
])
|
|
427
|
+
|
|
428
|
+
orders.**(tiers) do |row, candidates|
|
|
429
|
+
candidates[max_weight: ->(w){w >= row[:weight]}]
|
|
430
|
+
end
|
|
431
|
+
# => #<Namo [
|
|
432
|
+
# {order: 'A', weight: 5, tier: 'light', max_weight: 10},
|
|
433
|
+
# {order: 'A', weight: 5, tier: 'heavy', max_weight: 20},
|
|
434
|
+
# {order: 'B', weight: 15, tier: 'heavy', max_weight: 20}
|
|
435
|
+
# ]>
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
The contract matches `*`'s: `row` carries self's formulae, `candidates` carries other's, the block returns a Namo of rows to pair, and an empty return drops the left row. A block that returns its `candidates` unchanged reproduces the no-block product exactly — `**` is its own block form with the identity selector, just as `*` is `**` with the shared-dimension match applied first.
|
|
439
|
+
|
|
381
440
|
### Decomposition
|
|
382
441
|
|
|
383
442
|
`/` removes from the left Namo the dimensions that are also in the right, then dedupes the projected rows. It's the inverse of `*` and `**`:
|
data/lib/Namo/VERSION.rb
CHANGED
data/lib/namo.rb
CHANGED
|
@@ -115,28 +115,35 @@ class Namo
|
|
|
115
115
|
self.class.new((@data - other.data) + (other.data - @data), formulae: other.formulae.merge(@formulae))
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
def *(other)
|
|
118
|
+
def *(other, &block)
|
|
119
119
|
raise_unless_namo(other)
|
|
120
120
|
raise_unless_shared_data_dimensions(other)
|
|
121
121
|
shared = data_dimensions & other.data_dimensions
|
|
122
122
|
combined_data = []
|
|
123
123
|
@data.each do |left_row|
|
|
124
|
-
other.data.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
matched = other.data.select{|right_row| shared.all?{|dim| left_row[dim] == right_row[dim]}}
|
|
125
|
+
if block
|
|
126
|
+
candidates = other.class.new(matched, formulae: other.formulae.dup)
|
|
127
|
+
chosen = block.call(Row.new(left_row, @formulae), candidates)
|
|
128
|
+
chosen.data.each{|right_row| combined_data << left_row.merge(right_row)}
|
|
129
|
+
else
|
|
130
|
+
matched.each{|right_row| combined_data << left_row.merge(right_row)}
|
|
128
131
|
end
|
|
129
132
|
end
|
|
130
133
|
self.class.new(combined_data, formulae: other.formulae.merge(@formulae))
|
|
131
134
|
end
|
|
132
135
|
|
|
133
|
-
def **(other)
|
|
136
|
+
def **(other, &block)
|
|
134
137
|
raise_unless_namo(other)
|
|
135
138
|
raise_unless_disjoint_data_dimensions(other)
|
|
136
139
|
combined_data = []
|
|
137
140
|
@data.each do |left_row|
|
|
138
|
-
|
|
139
|
-
|
|
141
|
+
if block
|
|
142
|
+
candidates = other.class.new(other.data, formulae: other.formulae.dup)
|
|
143
|
+
chosen = block.call(Row.new(left_row, @formulae), candidates)
|
|
144
|
+
chosen.data.each{|right_row| combined_data << left_row.merge(right_row)}
|
|
145
|
+
else
|
|
146
|
+
other.data.each{|right_row| combined_data << left_row.merge(right_row)}
|
|
140
147
|
end
|
|
141
148
|
end
|
|
142
149
|
self.class.new(combined_data, formulae: other.formulae.merge(@formulae))
|
data/test/namo_test.rb
CHANGED
|
@@ -1450,6 +1450,126 @@ describe Namo do
|
|
|
1450
1450
|
b = Namo.new([{symbol: 'BHP', pe: 14.5}])
|
|
1451
1451
|
_((a * b).class).must_equal subclass
|
|
1452
1452
|
end
|
|
1453
|
+
|
|
1454
|
+
context "with a block" do
|
|
1455
|
+
let(:prices) do
|
|
1456
|
+
Namo.new([
|
|
1457
|
+
{symbol: 'BHP', date: '2025-02-15', close: 42.5},
|
|
1458
|
+
{symbol: 'BHP', date: '2025-05-20', close: 44.0}
|
|
1459
|
+
])
|
|
1460
|
+
end
|
|
1461
|
+
|
|
1462
|
+
let(:quarterly) do
|
|
1463
|
+
Namo.new([
|
|
1464
|
+
{symbol: 'BHP', quarter_end: '2024-12-31', eps: 1.0},
|
|
1465
|
+
{symbol: 'BHP', quarter_end: '2025-03-31', eps: 1.2}
|
|
1466
|
+
])
|
|
1467
|
+
end
|
|
1468
|
+
|
|
1469
|
+
let(:most_recent_quarter) do
|
|
1470
|
+
proc do |row, candidates|
|
|
1471
|
+
candidates[quarter_end: ->(qe){qe <= row[:date]}].sort_by{|f| f[:quarter_end]}.last(1)
|
|
1472
|
+
end
|
|
1473
|
+
end
|
|
1474
|
+
|
|
1475
|
+
it "pairs each left row with the single match the block returns" do
|
|
1476
|
+
result = prices.*(quarterly, &most_recent_quarter)
|
|
1477
|
+
_(result.to_a).must_equal [
|
|
1478
|
+
{symbol: 'BHP', date: '2025-02-15', close: 42.5, quarter_end: '2024-12-31', eps: 1.0},
|
|
1479
|
+
{symbol: 'BHP', date: '2025-05-20', close: 44.0, quarter_end: '2025-03-31', eps: 1.2}
|
|
1480
|
+
]
|
|
1481
|
+
end
|
|
1482
|
+
|
|
1483
|
+
it "drops a left row whose block returns an empty Namo" do
|
|
1484
|
+
early = Namo.new([
|
|
1485
|
+
{symbol: 'BHP', date: '2024-06-01', close: 40.0},
|
|
1486
|
+
{symbol: 'BHP', date: '2025-05-20', close: 44.0}
|
|
1487
|
+
])
|
|
1488
|
+
result = early.*(quarterly, &most_recent_quarter)
|
|
1489
|
+
_(result.to_a).must_equal [
|
|
1490
|
+
{symbol: 'BHP', date: '2025-05-20', close: 44.0, quarter_end: '2025-03-31', eps: 1.2}
|
|
1491
|
+
]
|
|
1492
|
+
end
|
|
1493
|
+
|
|
1494
|
+
it "pairs each row when the block returns a multi-row Namo (selector, not reducer)" do
|
|
1495
|
+
left = Namo.new([{symbol: 'BHP', date: '2025-05-20', close: 44.0}])
|
|
1496
|
+
result = left.*(quarterly){|row, candidates| candidates}
|
|
1497
|
+
_(result.to_a).must_equal [
|
|
1498
|
+
{symbol: 'BHP', date: '2025-05-20', close: 44.0, quarter_end: '2024-12-31', eps: 1.0},
|
|
1499
|
+
{symbol: 'BHP', date: '2025-05-20', close: 44.0, quarter_end: '2025-03-31', eps: 1.2}
|
|
1500
|
+
]
|
|
1501
|
+
end
|
|
1502
|
+
|
|
1503
|
+
it "selects on a derived dimension of other inside the block" do
|
|
1504
|
+
prices = Namo.new([
|
|
1505
|
+
{symbol: 'BHP', close: 42.5},
|
|
1506
|
+
{symbol: 'RIO', close: 118.3}
|
|
1507
|
+
])
|
|
1508
|
+
funds = Namo.new([
|
|
1509
|
+
{symbol: 'BHP', price: 42.5, eps: 3.0},
|
|
1510
|
+
{symbol: 'RIO', price: 118.3, eps: 13.0}
|
|
1511
|
+
])
|
|
1512
|
+
funds[:pe] = proc{|f| f[:price] / f[:eps]}
|
|
1513
|
+
result = prices.*(funds){|row, candidates| candidates[pe: ->(v){v && v < 10}]}
|
|
1514
|
+
_(result.values(:symbol)).must_equal ['RIO']
|
|
1515
|
+
_(result.values(:close)).must_equal [118.3]
|
|
1516
|
+
end
|
|
1517
|
+
|
|
1518
|
+
it "resolves a derived dimension of self referenced in the block" do
|
|
1519
|
+
dated = Namo.new([{symbol: 'BHP', date: '2025-05-20', close: 44.0}])
|
|
1520
|
+
dated[:cutoff] = proc{|r| r[:date]}
|
|
1521
|
+
result = dated.*(quarterly){|row, candidates| candidates[quarter_end: ->(qe){qe <= row[:cutoff]}].sort_by{|f| f[:quarter_end]}.last(1)}
|
|
1522
|
+
_(result.to_a).must_equal [
|
|
1523
|
+
{symbol: 'BHP', date: '2025-05-20', close: 44.0, quarter_end: '2025-03-31', eps: 1.2}
|
|
1524
|
+
]
|
|
1525
|
+
end
|
|
1526
|
+
|
|
1527
|
+
it "produces the same result formulae as the no-block form" do
|
|
1528
|
+
ohlcv = Namo.new([
|
|
1529
|
+
{symbol: 'BHP', close: 42.5},
|
|
1530
|
+
{symbol: 'RIO', close: 118.3}
|
|
1531
|
+
])
|
|
1532
|
+
fundamentals = Namo.new([
|
|
1533
|
+
{symbol: 'BHP', pe: 14.5},
|
|
1534
|
+
{symbol: 'RIO', pe: 9.2}
|
|
1535
|
+
])
|
|
1536
|
+
ohlcv[:label] = proc{|r| "#{r[:symbol]}-self"}
|
|
1537
|
+
fundamentals[:flag] = proc{|r| "pe=#{r[:pe]}"}
|
|
1538
|
+
blocked = ohlcv.*(fundamentals){|row, candidates| candidates}
|
|
1539
|
+
plain = ohlcv * fundamentals
|
|
1540
|
+
_(blocked.derived_dimensions).must_equal plain.derived_dimensions
|
|
1541
|
+
_(blocked.values(:label)).must_equal plain.values(:label)
|
|
1542
|
+
_(blocked.values(:flag)).must_equal plain.values(:flag)
|
|
1543
|
+
end
|
|
1544
|
+
|
|
1545
|
+
it "leaves other's derived dimension unstored but resolvable on the result" do
|
|
1546
|
+
prices = Namo.new([{symbol: 'BHP', close: 42.5}])
|
|
1547
|
+
funds = Namo.new([{symbol: 'BHP', price: 42.5, eps: 3.0}])
|
|
1548
|
+
funds[:pe] = proc{|f| f[:price] / f[:eps]}
|
|
1549
|
+
result = prices.*(funds){|row, candidates| candidates}
|
|
1550
|
+
_(result.data_dimensions).wont_include :pe
|
|
1551
|
+
_(result.derived_dimensions).must_include :pe
|
|
1552
|
+
_(result.values(:pe)).must_equal [42.5 / 3.0]
|
|
1553
|
+
end
|
|
1554
|
+
|
|
1555
|
+
it "returns an instance of self's class" do
|
|
1556
|
+
subclass = Class.new(Namo)
|
|
1557
|
+
a = subclass.new([{symbol: 'BHP', close: 42.5}])
|
|
1558
|
+
b = Namo.new([{symbol: 'BHP', pe: 14.5}])
|
|
1559
|
+
result = a.*(b){|row, candidates| candidates}
|
|
1560
|
+
_(result.class).must_equal subclass
|
|
1561
|
+
end
|
|
1562
|
+
|
|
1563
|
+
it "still raises ArgumentError on no shared dimensions" do
|
|
1564
|
+
a = Namo.new([{symbol: 'BHP'}])
|
|
1565
|
+
b = Namo.new([{quarter: 'Q1'}])
|
|
1566
|
+
_ { a.*(b){|row, candidates| candidates} }.must_raise ArgumentError
|
|
1567
|
+
end
|
|
1568
|
+
|
|
1569
|
+
it "still raises TypeError on a non-Namo operand" do
|
|
1570
|
+
_ { ohlcv.*([{symbol: 'BHP'}]){|row, candidates| candidates} }.must_raise TypeError
|
|
1571
|
+
end
|
|
1572
|
+
end
|
|
1453
1573
|
end
|
|
1454
1574
|
|
|
1455
1575
|
describe "#**" do
|
|
@@ -1529,6 +1649,90 @@ describe Namo do
|
|
|
1529
1649
|
b = Namo.new([{quarter: 'Q1'}])
|
|
1530
1650
|
_((a ** b).class).must_equal subclass
|
|
1531
1651
|
end
|
|
1652
|
+
|
|
1653
|
+
context "with a block" do
|
|
1654
|
+
let(:orders) do
|
|
1655
|
+
Namo.new([
|
|
1656
|
+
{order: 'A', weight: 5},
|
|
1657
|
+
{order: 'B', weight: 15}
|
|
1658
|
+
])
|
|
1659
|
+
end
|
|
1660
|
+
|
|
1661
|
+
let(:tiers) do
|
|
1662
|
+
Namo.new([
|
|
1663
|
+
{tier: 'light', max_weight: 10},
|
|
1664
|
+
{tier: 'heavy', max_weight: 20}
|
|
1665
|
+
])
|
|
1666
|
+
end
|
|
1667
|
+
|
|
1668
|
+
it "filters pairings by the selector block (one-to-many)" do
|
|
1669
|
+
result = orders.**(tiers){|row, candidates| candidates[max_weight: ->(w){w >= row[:weight]}]}
|
|
1670
|
+
_(result.to_a).must_equal [
|
|
1671
|
+
{order: 'A', weight: 5, tier: 'light', max_weight: 10},
|
|
1672
|
+
{order: 'A', weight: 5, tier: 'heavy', max_weight: 20},
|
|
1673
|
+
{order: 'B', weight: 15, tier: 'heavy', max_weight: 20}
|
|
1674
|
+
]
|
|
1675
|
+
end
|
|
1676
|
+
|
|
1677
|
+
it "drops a left row whose block returns an empty Namo" do
|
|
1678
|
+
with_unservable = Namo.new([
|
|
1679
|
+
{order: 'C', weight: 50},
|
|
1680
|
+
{order: 'A', weight: 5}
|
|
1681
|
+
])
|
|
1682
|
+
result = with_unservable.**(tiers){|row, candidates| candidates[max_weight: ->(w){w >= row[:weight]}]}
|
|
1683
|
+
_(result.to_a).must_equal [
|
|
1684
|
+
{order: 'A', weight: 5, tier: 'light', max_weight: 10},
|
|
1685
|
+
{order: 'A', weight: 5, tier: 'heavy', max_weight: 20}
|
|
1686
|
+
]
|
|
1687
|
+
end
|
|
1688
|
+
|
|
1689
|
+
it "reproduces the no-block product when the block returns all candidates" do
|
|
1690
|
+
blocked = products.**(quarters){|row, candidates| candidates}
|
|
1691
|
+
plain = products ** quarters
|
|
1692
|
+
_(blocked.to_a).must_equal plain.to_a
|
|
1693
|
+
end
|
|
1694
|
+
|
|
1695
|
+
it "selects on a derived dimension of other inside the block" do
|
|
1696
|
+
weighted_tiers = Namo.new([
|
|
1697
|
+
{tier: 'light', max_weight: 10},
|
|
1698
|
+
{tier: 'heavy', max_weight: 20}
|
|
1699
|
+
])
|
|
1700
|
+
weighted_tiers[:premium] = proc{|t| t[:max_weight] > 15}
|
|
1701
|
+
result = orders.**(weighted_tiers){|row, candidates| candidates[premium: ->(v){v}]}
|
|
1702
|
+
_(result.to_a).must_equal [
|
|
1703
|
+
{order: 'A', weight: 5, tier: 'heavy', max_weight: 20},
|
|
1704
|
+
{order: 'B', weight: 15, tier: 'heavy', max_weight: 20}
|
|
1705
|
+
]
|
|
1706
|
+
end
|
|
1707
|
+
|
|
1708
|
+
it "produces the same result formulae as the no-block form" do
|
|
1709
|
+
products[:plabel] = proc{|r| "p=#{r[:product]}"}
|
|
1710
|
+
quarters[:qlabel] = proc{|r| "q=#{r[:quarter]}"}
|
|
1711
|
+
blocked = products.**(quarters){|row, candidates| candidates}
|
|
1712
|
+
plain = products ** quarters
|
|
1713
|
+
_(blocked.derived_dimensions).must_equal plain.derived_dimensions
|
|
1714
|
+
_(blocked.values(:plabel)).must_equal plain.values(:plabel)
|
|
1715
|
+
_(blocked.values(:qlabel)).must_equal plain.values(:qlabel)
|
|
1716
|
+
end
|
|
1717
|
+
|
|
1718
|
+
it "returns an instance of self's class" do
|
|
1719
|
+
subclass = Class.new(Namo)
|
|
1720
|
+
a = subclass.new([{product: 'Widget'}])
|
|
1721
|
+
b = Namo.new([{quarter: 'Q1'}])
|
|
1722
|
+
result = a.**(b){|row, candidates| candidates}
|
|
1723
|
+
_(result.class).must_equal subclass
|
|
1724
|
+
end
|
|
1725
|
+
|
|
1726
|
+
it "still raises ArgumentError when a dimension is shared" do
|
|
1727
|
+
a = Namo.new([{symbol: 'BHP', close: 42.5}])
|
|
1728
|
+
b = Namo.new([{symbol: 'RIO', pe: 14.5}])
|
|
1729
|
+
_ { a.**(b){|row, candidates| candidates} }.must_raise ArgumentError
|
|
1730
|
+
end
|
|
1731
|
+
|
|
1732
|
+
it "still raises TypeError on a non-Namo operand" do
|
|
1733
|
+
_ { products.**([{quarter: 'Q1'}]){|row, candidates| candidates} }.must_raise TypeError
|
|
1734
|
+
end
|
|
1735
|
+
end
|
|
1532
1736
|
end
|
|
1533
1737
|
|
|
1534
1738
|
describe "#/" do
|