namo 0.13.2 → 0.15.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 +66 -0
- data/README.md +87 -0
- data/lib/Namo/Enumerable.rb +10 -10
- data/lib/Namo/Row.rb +15 -2
- data/lib/Namo/VERSION.rb +1 -1
- data/lib/namo.rb +22 -15
- data/test/Namo/Row_test.rb +62 -0
- data/test/namo_test.rb +324 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d2d5b27fc7c414edd9d13ac80d87bc6f99c87b2f94a42b8f192c3e5a4f08daa
|
|
4
|
+
data.tar.gz: 91ff974cb3c20802cf0afea77563211a1d29abda66bf9960ed18611fbaf09410
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f309aa497bde5bd05083f844f8d123257917da3e74abe07e4e18a8aff3dd362ab0e2c2b4b6df0791cb3a1b55ea53f9f7914a89eda74c2d158c0d7e5cf1e6b358
|
|
7
|
+
data.tar.gz: 62040149299672f673de4216cc7123e78e0135902d5a5ad3c948e74e7d2906b184b9ac6a6e84238f000d85a63b3ccc0b6dc894c3cdb74a65fb6ec1d7df0d6e33
|
data/CHANGELOG
CHANGED
|
@@ -1,6 +1,72 @@
|
|
|
1
1
|
CHANGELOG
|
|
2
2
|
_________
|
|
3
3
|
|
|
4
|
+
20260612
|
|
5
|
+
0.15.0: + two-arity formulae — procs with arity 2 receive (row, namo) for cross-row computation.
|
|
6
|
+
|
|
7
|
+
1. ~ lib/Namo/Row.rb: Row's constructor gains an optional third parameter, namo (default nil),
|
|
8
|
+
stored in @namo — the Namo that yielded the Row; the two-argument form keeps working.
|
|
9
|
+
Row#[] dispatches on formula arity: exactly 2 calls formula.call(self, @namo), raising
|
|
10
|
+
ArgumentError via the new private raise_unless_namo_context when @namo is nil; every
|
|
11
|
+
other arity (0, 1, negative) keeps the existing formula.call(self) unchanged.
|
|
12
|
+
2. ~ lib/Namo/Enumerable.rb: every Row construction (each, select and its aliases, reject,
|
|
13
|
+
sort_by, first, last, take_while, drop_while, uniq's block form, partition) passes self
|
|
14
|
+
as the Row's namo, so two-arity formulae resolve during enumeration and predicate
|
|
15
|
+
evaluation, with the yielding Namo as the window.
|
|
16
|
+
3. ~ lib/namo.rb: values_for's derived branch and the * and ** block paths pass self as the
|
|
17
|
+
Row's namo — values/coordinates/to_h resolve two-arity dimensions, and composition-block
|
|
18
|
+
rows can resolve self's two-arity formulae (extension of the 0.14.0 block contract; the
|
|
19
|
+
(Row, Namo) -> Namo contract and result-formulae rule are unchanged).
|
|
20
|
+
4. ~ test/Namo/Row_test.rb: + constructor third-argument and backward-compatibility tests,
|
|
21
|
+
arity-dispatch tests (1, 2, 0, negative), the nil-namo ArgumentError, and match? on a
|
|
22
|
+
two-arity derived dimension.
|
|
23
|
+
5. ~ test/namo_test.rb: + "#[]= two-arity formulae" describe — SMA cross-row resolution via
|
|
24
|
+
values/coordinates/to_h, selection and subset-predicate resolution, first/last Rows,
|
|
25
|
+
yielding-Namo (filtered-window) semantics, liveness, operator carry-through, composition-
|
|
26
|
+
block resolution, mixed-arity chains, and the empty-Namo case.
|
|
27
|
+
6. ~ README.md: + Cross-row formulae subsection under Formulae, with the SMA example, the
|
|
28
|
+
yielding-Namo semantic, and the no-context error.
|
|
29
|
+
7. ~ ROADMAP.md: Promote 0.15.0 to shipped; Current state -> 0.15.0; Summary folds in
|
|
30
|
+
two-arity formulae; the example's window.length corrected to window.count (Namo has
|
|
31
|
+
no length); "Live computation objects" phrasing updated. + upcoming 0.16.0 section,
|
|
32
|
+
Data/formula exclusivity — projection drops the formulae it materialises, * and **
|
|
33
|
+
raise on a data/formula name collision — with parameterised formulae renumbered to
|
|
34
|
+
0.17.0, Namo::Collection to 0.18.0, and group_by to 0.19.0; next phase -> 0.16.0.
|
|
35
|
+
8. ~ COMPARISON.md: Two-arity formulae -> shipped (0.15.0) with the same window.count
|
|
36
|
+
correction; Parameterised formulae repointed to planned (0.17.0). Date bumped.
|
|
37
|
+
9. ~ Namo::VERSION: /0.14.0/0.15.0/
|
|
38
|
+
|
|
39
|
+
20260608
|
|
40
|
+
0.14.0: + blocks on the composition operators (*, **) for custom match refinement.
|
|
41
|
+
|
|
42
|
+
1. ~ lib/namo.rb: Namo#* takes an optional block. Without a block, behaviour is unchanged.
|
|
43
|
+
With a block, the right rows matched on the shared data dimensions are passed as a Namo
|
|
44
|
+
(candidates, carrying other's formulae) alongside the left Row (carrying self's
|
|
45
|
+
formulae); the block returns a Namo of the rows to pair, each merged into the left row.
|
|
46
|
+
An empty returned Namo drops the left row (inner-join semantics). Result formulae are
|
|
47
|
+
other.formulae.merge(@formulae) as before — the block does not affect result formulae.
|
|
48
|
+
The shared-dimension precondition still applies with a block present.
|
|
49
|
+
2. ~ lib/namo.rb: Namo#** takes an optional block, same contract, with candidates being all
|
|
50
|
+
of other's rows (no shared-dimension pre-filter). The disjoint-dimensions precondition
|
|
51
|
+
still applies with a block present.
|
|
52
|
+
3. ~ test/namo_test.rb: + "with a block" context under #* (single-match one-row return, empty-return
|
|
53
|
+
drop, multi-row return, selection on other's derived dimension, reference to self's
|
|
54
|
+
derived dimension, result-formulae parity with no-block, derived-not-stored-but-resolves,
|
|
55
|
+
subclass type, preconditions still raise) and under #** (selector filtering one-to-many,
|
|
56
|
+
empty-return drop, full-candidates reproduces no-block, selection on other's derived
|
|
57
|
+
dimension, result-formulae parity, disjoint precondition still raises, subclass type).
|
|
58
|
+
Existing no-block #* and #** tests unchanged and green.
|
|
59
|
+
4. ~ README.md: + block subsections under Composition and Cartesian product, with the
|
|
60
|
+
price/quarterly matching and orders/tiers worked examples and the (row, candidates) ->
|
|
61
|
+
Namo contract.
|
|
62
|
+
5. ~ ROADMAP.md: Promote 0.14.0 to shipped (composition blocks only); Current state -> 0.14.0;
|
|
63
|
+
Summary folds in composition blocks; next phase -> 0.15.0. + the governing principle
|
|
64
|
+
(a block form is warranted iff the operation gives consideration to a dimension in
|
|
65
|
+
isolation), tied to the orthogonality/efficiency rationale for why set-operator/`/`
|
|
66
|
+
blocks were not added. The now-redundant upcoming 0.14.0 section removed.
|
|
67
|
+
6. ~ COMPARISON.md: "Conditional join with block" -> shipped (0.14.0).
|
|
68
|
+
7. ~ Namo::VERSION: /0.13.2/0.14.0/
|
|
69
|
+
|
|
4
70
|
20260608
|
|
5
71
|
0.13.2: Narrow the planned 0.14.0 block-form scope to the composition operators and document the rationale.
|
|
6
72
|
|
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 `**`:
|
|
@@ -585,6 +644,34 @@ sales[product: 'Widget'][:revenue, :quarter]
|
|
|
585
644
|
|
|
586
645
|
Formulae carry through selection — a filtered Namo instance remembers its formulae.
|
|
587
646
|
|
|
647
|
+
#### Cross-row formulae
|
|
648
|
+
|
|
649
|
+
A formula's arity selects its calling convention. A proc with **one** parameter receives the row, as above. A proc with **two** parameters receives `(row, namo)`, where `namo` is the Namo the row belongs to — so the formula can reach beyond the current row to the rest of the collection. That's what cross-row computation needs: moving windows, ranks, running totals, anything whose value depends on the row's neighbours.
|
|
650
|
+
|
|
651
|
+
A simple moving average reads the surrounding rows through `namo`:
|
|
652
|
+
|
|
653
|
+
```ruby
|
|
654
|
+
prices = Namo.new([
|
|
655
|
+
{symbol: 'AAA', date: 1, close: 10.0},
|
|
656
|
+
{symbol: 'AAA', date: 2, close: 20.0},
|
|
657
|
+
{symbol: 'AAA', date: 3, close: 30.0}
|
|
658
|
+
])
|
|
659
|
+
|
|
660
|
+
prices[:sma] = proc{|row, namo|
|
|
661
|
+
window = namo[symbol: row[:symbol], date: ->(d){d <= row[:date]}]
|
|
662
|
+
window.values(:close).sum / window.count.to_f
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
prices.values(:sma)
|
|
666
|
+
# => [10.0, 15.0, 20.0]
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
`namo` is the Namo that yielded the row, live — so the window always reflects the current state of the object you ask through. A filtered Namo's rows window over the filtered rows; an operator result's rows window over the result. Appending a row changes every cross-row value on the next access, with no caching.
|
|
670
|
+
|
|
671
|
+
One-arity formulae are unchanged, and the two forms mix freely — a one-arity formula can reference a two-arity one, and a two-arity formula can reference a one-arity one, by name.
|
|
672
|
+
|
|
673
|
+
Resolving a two-arity formula needs a Namo to window over. A `Row` constructed directly, without one, raises an `ArgumentError` naming the formula rather than letting the missing context surface as an unrelated error.
|
|
674
|
+
|
|
588
675
|
### Polymorphic `[]=`
|
|
589
676
|
|
|
590
677
|
`[]=` dispatches on the type of the value assigned. A proc registers a formula, as above. Anything else broadcasts the value to every row:
|
data/lib/Namo/Enumerable.rb
CHANGED
|
@@ -7,28 +7,28 @@ class Namo
|
|
|
7
7
|
|
|
8
8
|
def each(&block)
|
|
9
9
|
return enum_for(:each) unless block_given?
|
|
10
|
-
@data.each{|row_data| block.call(Row.new(row_data, @formulae))}
|
|
10
|
+
@data.each{|row_data| block.call(Row.new(row_data, @formulae, self))}
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def select(&block)
|
|
14
|
-
self.class.new(@data.select{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
|
|
14
|
+
self.class.new(@data.select{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
|
|
15
15
|
end
|
|
16
16
|
alias_method :filter, :select
|
|
17
17
|
alias_method :find_all, :select
|
|
18
18
|
|
|
19
19
|
def reject(&block)
|
|
20
|
-
self.class.new(@data.reject{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
|
|
20
|
+
self.class.new(@data.reject{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def sort_by(&block)
|
|
24
|
-
self.class.new(@data.sort_by{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
|
|
24
|
+
self.class.new(@data.sort_by{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def first(n = nil)
|
|
28
28
|
if n
|
|
29
29
|
self.class.new(@data.first(n), formulae: @formulae.dup)
|
|
30
30
|
else
|
|
31
|
-
@data.first ? Row.new(@data.first, @formulae) : nil
|
|
31
|
+
@data.first ? Row.new(@data.first, @formulae, self) : nil
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -36,7 +36,7 @@ class Namo
|
|
|
36
36
|
if n
|
|
37
37
|
self.class.new(@data.last(n), formulae: @formulae.dup)
|
|
38
38
|
else
|
|
39
|
-
@data.last ? Row.new(@data.last, @formulae) : nil
|
|
39
|
+
@data.last ? Row.new(@data.last, @formulae, self) : nil
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -49,20 +49,20 @@ class Namo
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def take_while(&block)
|
|
52
|
-
self.class.new(@data.take_while{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
|
|
52
|
+
self.class.new(@data.take_while{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def drop_while(&block)
|
|
56
|
-
self.class.new(@data.drop_while{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
|
|
56
|
+
self.class.new(@data.drop_while{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def uniq(&block)
|
|
60
|
-
rows = block ? @data.uniq{|row| block.call(Row.new(row, @formulae))} : @data.uniq
|
|
60
|
+
rows = block ? @data.uniq{|row| block.call(Row.new(row, @formulae, self))} : @data.uniq
|
|
61
61
|
self.class.new(rows, formulae: @formulae.dup)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def partition(&block)
|
|
65
|
-
matches, non_matches = @data.partition{|row| block.call(Row.new(row, @formulae))}
|
|
65
|
+
matches, non_matches = @data.partition{|row| block.call(Row.new(row, @formulae, self))}
|
|
66
66
|
[
|
|
67
67
|
self.class.new(matches, formulae: @formulae.dup),
|
|
68
68
|
self.class.new(non_matches, formulae: @formulae.dup),
|
data/lib/Namo/Row.rb
CHANGED
|
@@ -5,7 +5,13 @@ class Namo
|
|
|
5
5
|
class Row
|
|
6
6
|
def [](name)
|
|
7
7
|
if @formulae.key?(name)
|
|
8
|
-
@formulae[name].
|
|
8
|
+
case @formulae[name].arity
|
|
9
|
+
when 2
|
|
10
|
+
raise_unless_namo_context(name)
|
|
11
|
+
@formulae[name].call(self, @namo)
|
|
12
|
+
else
|
|
13
|
+
@formulae[name].call(self)
|
|
14
|
+
end
|
|
9
15
|
else
|
|
10
16
|
@row[name]
|
|
11
17
|
end
|
|
@@ -44,9 +50,16 @@ class Namo
|
|
|
44
50
|
|
|
45
51
|
private
|
|
46
52
|
|
|
47
|
-
def initialize(row, formulae)
|
|
53
|
+
def initialize(row, formulae, namo = nil)
|
|
48
54
|
@row = row
|
|
49
55
|
@formulae = formulae
|
|
56
|
+
@namo = namo
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def raise_unless_namo_context(name)
|
|
60
|
+
unless @namo
|
|
61
|
+
raise ArgumentError, "two-arity formula #{name.inspect} requires a Namo context, but this Row has none"
|
|
62
|
+
end
|
|
50
63
|
end
|
|
51
64
|
end
|
|
52
65
|
end
|
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, self), 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, self), 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))
|
|
@@ -222,11 +229,17 @@ class Namo
|
|
|
222
229
|
|
|
223
230
|
private
|
|
224
231
|
|
|
232
|
+
def initialize(positional_data = nil, data: [], formulae: {}, name: nil)
|
|
233
|
+
@data = positional_data || data
|
|
234
|
+
@formulae = formulae
|
|
235
|
+
@name = name
|
|
236
|
+
end
|
|
237
|
+
|
|
225
238
|
def values_for(dim)
|
|
226
239
|
if data_dimensions.include?(dim)
|
|
227
240
|
@data.map{|row_data| row_data[dim]}
|
|
228
241
|
else
|
|
229
|
-
@data.map{|row_data| Row.new(row_data, @formulae)[dim]}
|
|
242
|
+
@data.map{|row_data| Row.new(row_data, @formulae, self)[dim]}
|
|
230
243
|
end
|
|
231
244
|
end
|
|
232
245
|
|
|
@@ -253,10 +266,4 @@ class Namo
|
|
|
253
266
|
raise ArgumentError, "dimensions in common, need no common dimensions: #{data_dimensions} vs #{other.data_dimensions}"
|
|
254
267
|
end
|
|
255
268
|
end
|
|
256
|
-
|
|
257
|
-
def initialize(positional_data = nil, data: [], formulae: {}, name: nil)
|
|
258
|
-
@data = positional_data || data
|
|
259
|
-
@formulae = formulae
|
|
260
|
-
@name = name
|
|
261
|
-
end
|
|
262
269
|
end
|
data/test/Namo/Row_test.rb
CHANGED
|
@@ -40,6 +40,60 @@ describe Namo::Row do
|
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
describe "constructor" do
|
|
44
|
+
it "constructs from the two-argument form (namo defaults nil)" do
|
|
45
|
+
_(Namo::Row.new(row_data, formulae)).must_be_kind_of Namo::Row
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "accepts a third namo argument" do
|
|
49
|
+
namo = Namo.new([row_data])
|
|
50
|
+
_(Namo::Row.new(row_data, formulae, namo)).must_be_kind_of Namo::Row
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe "#[] arity dispatch" do
|
|
55
|
+
it "calls an arity-1 formula with the Row only" do
|
|
56
|
+
seen = nil
|
|
57
|
+
formulae[:dim] = ->(r){seen = r; 1}
|
|
58
|
+
row[:dim]
|
|
59
|
+
_(seen).must_be_same_as row
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "calls an arity-2 formula with the Row and the yielding Namo" do
|
|
63
|
+
namo = Namo.new([row_data])
|
|
64
|
+
row = Namo::Row.new(row_data, formulae, namo)
|
|
65
|
+
seen_row = nil
|
|
66
|
+
seen_namo = nil
|
|
67
|
+
formulae[:dim] = ->(r, n){seen_row = r; seen_namo = n; 1}
|
|
68
|
+
row[:dim]
|
|
69
|
+
_(seen_row).must_be_same_as row
|
|
70
|
+
_(seen_namo.equal?(namo)).must_equal true
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "takes the one-arity path for an arity-0 proc" do
|
|
74
|
+
formulae[:dim] = proc{42}
|
|
75
|
+
_(row[:dim]).must_equal 42
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "takes the one-arity path for a negative-arity proc" do
|
|
79
|
+
seen_rest = nil
|
|
80
|
+
formulae[:dim] = proc{|r, *rest| seen_rest = rest; 1}
|
|
81
|
+
row[:dim]
|
|
82
|
+
_(seen_rest).must_equal []
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "raises ArgumentError naming the formula when an arity-2 formula has no Namo context" do
|
|
86
|
+
formulae[:sma] = ->(r, n){n.count}
|
|
87
|
+
error = _(proc{row[:sma]}).must_raise ArgumentError
|
|
88
|
+
_(error.message).must_match(/sma/)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "resolves an arity-1 formula on a Row with no Namo context" do
|
|
92
|
+
formulae[:revenue] = ->(r){r[:price] * r[:quantity]}
|
|
93
|
+
_(row[:revenue]).must_equal 1000.0
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
43
97
|
describe "#match?" do
|
|
44
98
|
it "matches a single value" do
|
|
45
99
|
_(row.match?(product: 'Widget')).must_equal true
|
|
@@ -61,6 +115,14 @@ describe Namo::Row do
|
|
|
61
115
|
_(row.match?(product: 'Widget', quarter: 'Q2')).must_equal false
|
|
62
116
|
end
|
|
63
117
|
|
|
118
|
+
it "resolves a two-arity derived dimension when the Row carries a Namo" do
|
|
119
|
+
namo = Namo.new([row_data])
|
|
120
|
+
formulae[:row_count] = ->(r, n){n.count}
|
|
121
|
+
row = Namo::Row.new(row_data, formulae, namo)
|
|
122
|
+
_(row.match?(row_count: 1)).must_equal true
|
|
123
|
+
_(row.match?(row_count: 2)).must_equal false
|
|
124
|
+
end
|
|
125
|
+
|
|
64
126
|
describe "Proc predicates" do
|
|
65
127
|
it "matches when the proc returns true" do
|
|
66
128
|
_(row.match?(price: ->(v){v < 15.0})).must_equal true
|
data/test/namo_test.rb
CHANGED
|
@@ -647,6 +647,126 @@ describe Namo do
|
|
|
647
647
|
end
|
|
648
648
|
end
|
|
649
649
|
|
|
650
|
+
describe "#[]= two-arity formulae" do
|
|
651
|
+
let(:price_data) do
|
|
652
|
+
[
|
|
653
|
+
{symbol: 'AAA', date: 1, close: 10.0},
|
|
654
|
+
{symbol: 'AAA', date: 2, close: 20.0},
|
|
655
|
+
{symbol: 'AAA', date: 3, close: 30.0},
|
|
656
|
+
]
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
let(:prices) do
|
|
660
|
+
Namo.new(price_data)
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# A simple moving average: the mean close over the rows of the same symbol
|
|
664
|
+
# up to and including the current row's date, computed against the Namo the
|
|
665
|
+
# row belongs to.
|
|
666
|
+
let(:sma) do
|
|
667
|
+
->(row, namo){
|
|
668
|
+
window = namo[symbol: row[:symbol], date: ->(d){d <= row[:date]}]
|
|
669
|
+
window.values(:close).sum / window.count.to_f
|
|
670
|
+
}
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
it "resolves a cross-row SMA via values" do
|
|
674
|
+
prices[:sma] = sma
|
|
675
|
+
_(prices.values(:sma)).must_equal [10.0, 15.0, 20.0]
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
it "resolves through coordinates" do
|
|
679
|
+
prices[:sma] = sma
|
|
680
|
+
_(prices.coordinates(:sma)).must_equal [10.0, 15.0, 20.0]
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
it "resolves through the no-arg to_h" do
|
|
684
|
+
prices[:sma] = sma
|
|
685
|
+
_(prices.to_h[:sma]).must_equal [10.0, 15.0, 20.0]
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
it "selects on the two-arity dimension" do
|
|
689
|
+
prices[:sma] = sma
|
|
690
|
+
_(prices[sma: ->(v){v > 12.0}].values(:date)).must_equal [2, 3]
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
it "resolves a two-arity dimension in a subset-method predicate" do
|
|
694
|
+
prices[:sma] = sma
|
|
695
|
+
result = prices.select{|row| row[:sma] > 12.0}
|
|
696
|
+
_(result).must_be_kind_of Namo
|
|
697
|
+
_(result.values(:date)).must_equal [2, 3]
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
it "resolves the two-arity formula on the no-arg first Row" do
|
|
701
|
+
prices[:sma] = sma
|
|
702
|
+
_(prices.first[:sma]).must_equal 10.0
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
it "resolves the two-arity formula on the no-arg last Row" do
|
|
706
|
+
prices[:sma] = sma
|
|
707
|
+
_(prices.last[:sma]).must_equal 20.0
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
it "windows over the yielding Namo — a filtered Namo computes over the filtered rows only" do
|
|
711
|
+
prices[:sma] = sma
|
|
712
|
+
filtered = prices.select{|row| row[:date] >= 2}
|
|
713
|
+
# On the full Namo the date-2 SMA averages dates 1 and 2 (15.0); on the
|
|
714
|
+
# filtered Namo it sees only date 2, so the value differs.
|
|
715
|
+
_(prices.values(:sma)).must_equal [10.0, 15.0, 20.0]
|
|
716
|
+
_(filtered.values(:sma)).must_equal [20.0, 25.0]
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
it "is live: appending a row changes the two-arity result on next access" do
|
|
720
|
+
prices[:sma] = sma
|
|
721
|
+
_(prices.last[:sma]).must_equal 20.0
|
|
722
|
+
prices.data << {symbol: 'AAA', date: 4, close: 40.0}
|
|
723
|
+
_(prices.last[:sma]).must_equal 25.0
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
it "carries a two-arity formula through a set-operator result, windowing over the combined rows" do
|
|
727
|
+
a = Namo.new([{symbol: 'AAA', date: 1}, {symbol: 'AAA', date: 2}])
|
|
728
|
+
b = Namo.new([{symbol: 'AAA', date: 3}])
|
|
729
|
+
a[:peers] = ->(row, namo){namo.count}
|
|
730
|
+
_((a + b).values(:peers)).must_equal [3, 3, 3]
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
it "carries a merged two-arity formula through a composition result, windowing over the joined rows" do
|
|
734
|
+
left = Namo.new([{symbol: 'AAA', date: 1}, {symbol: 'AAA', date: 2}])
|
|
735
|
+
right = Namo.new([{symbol: 'AAA', sector: 'tech'}])
|
|
736
|
+
left[:peers] = ->(row, namo){namo.count}
|
|
737
|
+
result = left * right
|
|
738
|
+
_(result.values(:peers)).must_equal [2, 2]
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
it "resolves a two-arity formula of self inside a composition block" do
|
|
742
|
+
left = Namo.new([{symbol: 'AAA', date: 1}, {symbol: 'AAA', date: 2}])
|
|
743
|
+
right = Namo.new([{symbol: 'AAA', sector: 'tech'}])
|
|
744
|
+
left[:peers] = ->(row, namo){namo.count}
|
|
745
|
+
seen = nil
|
|
746
|
+
left.*(right){|row, candidates| seen = row[:peers]; candidates}
|
|
747
|
+
_(seen).must_equal 2
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
it "lets a one-arity formula reference a two-arity formula by name" do
|
|
751
|
+
prices[:sma] = sma
|
|
752
|
+
prices[:double_sma] = ->(row){row[:sma] * 2}
|
|
753
|
+
_(prices.values(:double_sma)).must_equal [20.0, 30.0, 40.0]
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
it "lets a two-arity formula reference a one-arity formula by name" do
|
|
757
|
+
prices[:tenth] = ->(row){row[:close] / 10.0}
|
|
758
|
+
prices[:tenth_plus_count] = ->(row, namo){row[:tenth] + namo.count}
|
|
759
|
+
_(prices.values(:tenth_plus_count)).must_equal [4.0, 5.0, 6.0]
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
it "returns [] for a two-arity dimension on an empty Namo without invoking the formula" do
|
|
763
|
+
invoked = false
|
|
764
|
+
empty = Namo.new([], formulae: {sma: ->(row, namo){invoked = true; 0}})
|
|
765
|
+
_(empty.values(:sma)).must_equal []
|
|
766
|
+
_(invoked).must_equal false
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
|
|
650
770
|
describe "#each" do
|
|
651
771
|
it "yields Row objects" do
|
|
652
772
|
rows = []
|
|
@@ -1450,6 +1570,126 @@ describe Namo do
|
|
|
1450
1570
|
b = Namo.new([{symbol: 'BHP', pe: 14.5}])
|
|
1451
1571
|
_((a * b).class).must_equal subclass
|
|
1452
1572
|
end
|
|
1573
|
+
|
|
1574
|
+
context "with a block" do
|
|
1575
|
+
let(:prices) do
|
|
1576
|
+
Namo.new([
|
|
1577
|
+
{symbol: 'BHP', date: '2025-02-15', close: 42.5},
|
|
1578
|
+
{symbol: 'BHP', date: '2025-05-20', close: 44.0}
|
|
1579
|
+
])
|
|
1580
|
+
end
|
|
1581
|
+
|
|
1582
|
+
let(:quarterly) do
|
|
1583
|
+
Namo.new([
|
|
1584
|
+
{symbol: 'BHP', quarter_end: '2024-12-31', eps: 1.0},
|
|
1585
|
+
{symbol: 'BHP', quarter_end: '2025-03-31', eps: 1.2}
|
|
1586
|
+
])
|
|
1587
|
+
end
|
|
1588
|
+
|
|
1589
|
+
let(:most_recent_quarter) do
|
|
1590
|
+
proc do |row, candidates|
|
|
1591
|
+
candidates[quarter_end: ->(qe){qe <= row[:date]}].sort_by{|f| f[:quarter_end]}.last(1)
|
|
1592
|
+
end
|
|
1593
|
+
end
|
|
1594
|
+
|
|
1595
|
+
it "pairs each left row with the single match the block returns" do
|
|
1596
|
+
result = prices.*(quarterly, &most_recent_quarter)
|
|
1597
|
+
_(result.to_a).must_equal [
|
|
1598
|
+
{symbol: 'BHP', date: '2025-02-15', close: 42.5, quarter_end: '2024-12-31', eps: 1.0},
|
|
1599
|
+
{symbol: 'BHP', date: '2025-05-20', close: 44.0, quarter_end: '2025-03-31', eps: 1.2}
|
|
1600
|
+
]
|
|
1601
|
+
end
|
|
1602
|
+
|
|
1603
|
+
it "drops a left row whose block returns an empty Namo" do
|
|
1604
|
+
early = Namo.new([
|
|
1605
|
+
{symbol: 'BHP', date: '2024-06-01', close: 40.0},
|
|
1606
|
+
{symbol: 'BHP', date: '2025-05-20', close: 44.0}
|
|
1607
|
+
])
|
|
1608
|
+
result = early.*(quarterly, &most_recent_quarter)
|
|
1609
|
+
_(result.to_a).must_equal [
|
|
1610
|
+
{symbol: 'BHP', date: '2025-05-20', close: 44.0, quarter_end: '2025-03-31', eps: 1.2}
|
|
1611
|
+
]
|
|
1612
|
+
end
|
|
1613
|
+
|
|
1614
|
+
it "pairs each row when the block returns a multi-row Namo (selector, not reducer)" do
|
|
1615
|
+
left = Namo.new([{symbol: 'BHP', date: '2025-05-20', close: 44.0}])
|
|
1616
|
+
result = left.*(quarterly){|row, candidates| candidates}
|
|
1617
|
+
_(result.to_a).must_equal [
|
|
1618
|
+
{symbol: 'BHP', date: '2025-05-20', close: 44.0, quarter_end: '2024-12-31', eps: 1.0},
|
|
1619
|
+
{symbol: 'BHP', date: '2025-05-20', close: 44.0, quarter_end: '2025-03-31', eps: 1.2}
|
|
1620
|
+
]
|
|
1621
|
+
end
|
|
1622
|
+
|
|
1623
|
+
it "selects on a derived dimension of other inside the block" do
|
|
1624
|
+
prices = Namo.new([
|
|
1625
|
+
{symbol: 'BHP', close: 42.5},
|
|
1626
|
+
{symbol: 'RIO', close: 118.3}
|
|
1627
|
+
])
|
|
1628
|
+
funds = Namo.new([
|
|
1629
|
+
{symbol: 'BHP', price: 42.5, eps: 3.0},
|
|
1630
|
+
{symbol: 'RIO', price: 118.3, eps: 13.0}
|
|
1631
|
+
])
|
|
1632
|
+
funds[:pe] = proc{|f| f[:price] / f[:eps]}
|
|
1633
|
+
result = prices.*(funds){|row, candidates| candidates[pe: ->(v){v && v < 10}]}
|
|
1634
|
+
_(result.values(:symbol)).must_equal ['RIO']
|
|
1635
|
+
_(result.values(:close)).must_equal [118.3]
|
|
1636
|
+
end
|
|
1637
|
+
|
|
1638
|
+
it "resolves a derived dimension of self referenced in the block" do
|
|
1639
|
+
dated = Namo.new([{symbol: 'BHP', date: '2025-05-20', close: 44.0}])
|
|
1640
|
+
dated[:cutoff] = proc{|r| r[:date]}
|
|
1641
|
+
result = dated.*(quarterly){|row, candidates| candidates[quarter_end: ->(qe){qe <= row[:cutoff]}].sort_by{|f| f[:quarter_end]}.last(1)}
|
|
1642
|
+
_(result.to_a).must_equal [
|
|
1643
|
+
{symbol: 'BHP', date: '2025-05-20', close: 44.0, quarter_end: '2025-03-31', eps: 1.2}
|
|
1644
|
+
]
|
|
1645
|
+
end
|
|
1646
|
+
|
|
1647
|
+
it "produces the same result formulae as the no-block form" do
|
|
1648
|
+
ohlcv = Namo.new([
|
|
1649
|
+
{symbol: 'BHP', close: 42.5},
|
|
1650
|
+
{symbol: 'RIO', close: 118.3}
|
|
1651
|
+
])
|
|
1652
|
+
fundamentals = Namo.new([
|
|
1653
|
+
{symbol: 'BHP', pe: 14.5},
|
|
1654
|
+
{symbol: 'RIO', pe: 9.2}
|
|
1655
|
+
])
|
|
1656
|
+
ohlcv[:label] = proc{|r| "#{r[:symbol]}-self"}
|
|
1657
|
+
fundamentals[:flag] = proc{|r| "pe=#{r[:pe]}"}
|
|
1658
|
+
blocked = ohlcv.*(fundamentals){|row, candidates| candidates}
|
|
1659
|
+
plain = ohlcv * fundamentals
|
|
1660
|
+
_(blocked.derived_dimensions).must_equal plain.derived_dimensions
|
|
1661
|
+
_(blocked.values(:label)).must_equal plain.values(:label)
|
|
1662
|
+
_(blocked.values(:flag)).must_equal plain.values(:flag)
|
|
1663
|
+
end
|
|
1664
|
+
|
|
1665
|
+
it "leaves other's derived dimension unstored but resolvable on the result" do
|
|
1666
|
+
prices = Namo.new([{symbol: 'BHP', close: 42.5}])
|
|
1667
|
+
funds = Namo.new([{symbol: 'BHP', price: 42.5, eps: 3.0}])
|
|
1668
|
+
funds[:pe] = proc{|f| f[:price] / f[:eps]}
|
|
1669
|
+
result = prices.*(funds){|row, candidates| candidates}
|
|
1670
|
+
_(result.data_dimensions).wont_include :pe
|
|
1671
|
+
_(result.derived_dimensions).must_include :pe
|
|
1672
|
+
_(result.values(:pe)).must_equal [42.5 / 3.0]
|
|
1673
|
+
end
|
|
1674
|
+
|
|
1675
|
+
it "returns an instance of self's class" do
|
|
1676
|
+
subclass = Class.new(Namo)
|
|
1677
|
+
a = subclass.new([{symbol: 'BHP', close: 42.5}])
|
|
1678
|
+
b = Namo.new([{symbol: 'BHP', pe: 14.5}])
|
|
1679
|
+
result = a.*(b){|row, candidates| candidates}
|
|
1680
|
+
_(result.class).must_equal subclass
|
|
1681
|
+
end
|
|
1682
|
+
|
|
1683
|
+
it "still raises ArgumentError on no shared dimensions" do
|
|
1684
|
+
a = Namo.new([{symbol: 'BHP'}])
|
|
1685
|
+
b = Namo.new([{quarter: 'Q1'}])
|
|
1686
|
+
_ { a.*(b){|row, candidates| candidates} }.must_raise ArgumentError
|
|
1687
|
+
end
|
|
1688
|
+
|
|
1689
|
+
it "still raises TypeError on a non-Namo operand" do
|
|
1690
|
+
_ { ohlcv.*([{symbol: 'BHP'}]){|row, candidates| candidates} }.must_raise TypeError
|
|
1691
|
+
end
|
|
1692
|
+
end
|
|
1453
1693
|
end
|
|
1454
1694
|
|
|
1455
1695
|
describe "#**" do
|
|
@@ -1529,6 +1769,90 @@ describe Namo do
|
|
|
1529
1769
|
b = Namo.new([{quarter: 'Q1'}])
|
|
1530
1770
|
_((a ** b).class).must_equal subclass
|
|
1531
1771
|
end
|
|
1772
|
+
|
|
1773
|
+
context "with a block" do
|
|
1774
|
+
let(:orders) do
|
|
1775
|
+
Namo.new([
|
|
1776
|
+
{order: 'A', weight: 5},
|
|
1777
|
+
{order: 'B', weight: 15}
|
|
1778
|
+
])
|
|
1779
|
+
end
|
|
1780
|
+
|
|
1781
|
+
let(:tiers) do
|
|
1782
|
+
Namo.new([
|
|
1783
|
+
{tier: 'light', max_weight: 10},
|
|
1784
|
+
{tier: 'heavy', max_weight: 20}
|
|
1785
|
+
])
|
|
1786
|
+
end
|
|
1787
|
+
|
|
1788
|
+
it "filters pairings by the selector block (one-to-many)" do
|
|
1789
|
+
result = orders.**(tiers){|row, candidates| candidates[max_weight: ->(w){w >= row[:weight]}]}
|
|
1790
|
+
_(result.to_a).must_equal [
|
|
1791
|
+
{order: 'A', weight: 5, tier: 'light', max_weight: 10},
|
|
1792
|
+
{order: 'A', weight: 5, tier: 'heavy', max_weight: 20},
|
|
1793
|
+
{order: 'B', weight: 15, tier: 'heavy', max_weight: 20}
|
|
1794
|
+
]
|
|
1795
|
+
end
|
|
1796
|
+
|
|
1797
|
+
it "drops a left row whose block returns an empty Namo" do
|
|
1798
|
+
with_unservable = Namo.new([
|
|
1799
|
+
{order: 'C', weight: 50},
|
|
1800
|
+
{order: 'A', weight: 5}
|
|
1801
|
+
])
|
|
1802
|
+
result = with_unservable.**(tiers){|row, candidates| candidates[max_weight: ->(w){w >= row[:weight]}]}
|
|
1803
|
+
_(result.to_a).must_equal [
|
|
1804
|
+
{order: 'A', weight: 5, tier: 'light', max_weight: 10},
|
|
1805
|
+
{order: 'A', weight: 5, tier: 'heavy', max_weight: 20}
|
|
1806
|
+
]
|
|
1807
|
+
end
|
|
1808
|
+
|
|
1809
|
+
it "reproduces the no-block product when the block returns all candidates" do
|
|
1810
|
+
blocked = products.**(quarters){|row, candidates| candidates}
|
|
1811
|
+
plain = products ** quarters
|
|
1812
|
+
_(blocked.to_a).must_equal plain.to_a
|
|
1813
|
+
end
|
|
1814
|
+
|
|
1815
|
+
it "selects on a derived dimension of other inside the block" do
|
|
1816
|
+
weighted_tiers = Namo.new([
|
|
1817
|
+
{tier: 'light', max_weight: 10},
|
|
1818
|
+
{tier: 'heavy', max_weight: 20}
|
|
1819
|
+
])
|
|
1820
|
+
weighted_tiers[:premium] = proc{|t| t[:max_weight] > 15}
|
|
1821
|
+
result = orders.**(weighted_tiers){|row, candidates| candidates[premium: ->(v){v}]}
|
|
1822
|
+
_(result.to_a).must_equal [
|
|
1823
|
+
{order: 'A', weight: 5, tier: 'heavy', max_weight: 20},
|
|
1824
|
+
{order: 'B', weight: 15, tier: 'heavy', max_weight: 20}
|
|
1825
|
+
]
|
|
1826
|
+
end
|
|
1827
|
+
|
|
1828
|
+
it "produces the same result formulae as the no-block form" do
|
|
1829
|
+
products[:plabel] = proc{|r| "p=#{r[:product]}"}
|
|
1830
|
+
quarters[:qlabel] = proc{|r| "q=#{r[:quarter]}"}
|
|
1831
|
+
blocked = products.**(quarters){|row, candidates| candidates}
|
|
1832
|
+
plain = products ** quarters
|
|
1833
|
+
_(blocked.derived_dimensions).must_equal plain.derived_dimensions
|
|
1834
|
+
_(blocked.values(:plabel)).must_equal plain.values(:plabel)
|
|
1835
|
+
_(blocked.values(:qlabel)).must_equal plain.values(:qlabel)
|
|
1836
|
+
end
|
|
1837
|
+
|
|
1838
|
+
it "returns an instance of self's class" do
|
|
1839
|
+
subclass = Class.new(Namo)
|
|
1840
|
+
a = subclass.new([{product: 'Widget'}])
|
|
1841
|
+
b = Namo.new([{quarter: 'Q1'}])
|
|
1842
|
+
result = a.**(b){|row, candidates| candidates}
|
|
1843
|
+
_(result.class).must_equal subclass
|
|
1844
|
+
end
|
|
1845
|
+
|
|
1846
|
+
it "still raises ArgumentError when a dimension is shared" do
|
|
1847
|
+
a = Namo.new([{symbol: 'BHP', close: 42.5}])
|
|
1848
|
+
b = Namo.new([{symbol: 'RIO', pe: 14.5}])
|
|
1849
|
+
_ { a.**(b){|row, candidates| candidates} }.must_raise ArgumentError
|
|
1850
|
+
end
|
|
1851
|
+
|
|
1852
|
+
it "still raises TypeError on a non-Namo operand" do
|
|
1853
|
+
_ { products.**([{quarter: 'Q1'}]){|row, candidates| candidates} }.must_raise TypeError
|
|
1854
|
+
end
|
|
1855
|
+
end
|
|
1532
1856
|
end
|
|
1533
1857
|
|
|
1534
1858
|
describe "#/" do
|
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.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- thoran
|
|
@@ -93,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
93
93
|
- !ruby/object:Gem::Version
|
|
94
94
|
version: '0'
|
|
95
95
|
requirements: []
|
|
96
|
-
rubygems_version: 4.0.
|
|
96
|
+
rubygems_version: 4.0.14
|
|
97
97
|
specification_version: 4
|
|
98
98
|
summary: Named dimensional data for Ruby.
|
|
99
99
|
test_files: []
|