namo 0.14.0 → 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 +35 -0
- data/README.md +28 -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 +9 -9
- data/test/Namo/Row_test.rb +62 -0
- data/test/namo_test.rb +120 -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,41 @@
|
|
|
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
|
+
|
|
4
39
|
20260608
|
|
5
40
|
0.14.0: + blocks on the composition operators (*, **) for custom match refinement.
|
|
6
41
|
|
data/README.md
CHANGED
|
@@ -644,6 +644,34 @@ sales[product: 'Widget'][:revenue, :quarter]
|
|
|
644
644
|
|
|
645
645
|
Formulae carry through selection — a filtered Namo instance remembers its formulae.
|
|
646
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
|
+
|
|
647
675
|
### Polymorphic `[]=`
|
|
648
676
|
|
|
649
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
|
@@ -124,7 +124,7 @@ class Namo
|
|
|
124
124
|
matched = other.data.select{|right_row| shared.all?{|dim| left_row[dim] == right_row[dim]}}
|
|
125
125
|
if block
|
|
126
126
|
candidates = other.class.new(matched, formulae: other.formulae.dup)
|
|
127
|
-
chosen = block.call(Row.new(left_row, @formulae), candidates)
|
|
127
|
+
chosen = block.call(Row.new(left_row, @formulae, self), candidates)
|
|
128
128
|
chosen.data.each{|right_row| combined_data << left_row.merge(right_row)}
|
|
129
129
|
else
|
|
130
130
|
matched.each{|right_row| combined_data << left_row.merge(right_row)}
|
|
@@ -140,7 +140,7 @@ class Namo
|
|
|
140
140
|
@data.each do |left_row|
|
|
141
141
|
if block
|
|
142
142
|
candidates = other.class.new(other.data, formulae: other.formulae.dup)
|
|
143
|
-
chosen = block.call(Row.new(left_row, @formulae), candidates)
|
|
143
|
+
chosen = block.call(Row.new(left_row, @formulae, self), candidates)
|
|
144
144
|
chosen.data.each{|right_row| combined_data << left_row.merge(right_row)}
|
|
145
145
|
else
|
|
146
146
|
other.data.each{|right_row| combined_data << left_row.merge(right_row)}
|
|
@@ -229,11 +229,17 @@ class Namo
|
|
|
229
229
|
|
|
230
230
|
private
|
|
231
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
|
+
|
|
232
238
|
def values_for(dim)
|
|
233
239
|
if data_dimensions.include?(dim)
|
|
234
240
|
@data.map{|row_data| row_data[dim]}
|
|
235
241
|
else
|
|
236
|
-
@data.map{|row_data| Row.new(row_data, @formulae)[dim]}
|
|
242
|
+
@data.map{|row_data| Row.new(row_data, @formulae, self)[dim]}
|
|
237
243
|
end
|
|
238
244
|
end
|
|
239
245
|
|
|
@@ -260,10 +266,4 @@ class Namo
|
|
|
260
266
|
raise ArgumentError, "dimensions in common, need no common dimensions: #{data_dimensions} vs #{other.data_dimensions}"
|
|
261
267
|
end
|
|
262
268
|
end
|
|
263
|
-
|
|
264
|
-
def initialize(positional_data = nil, data: [], formulae: {}, name: nil)
|
|
265
|
-
@data = positional_data || data
|
|
266
|
-
@formulae = formulae
|
|
267
|
-
@name = name
|
|
268
|
-
end
|
|
269
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 = []
|
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: []
|