namo 0.14.0 → 0.16.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 +80 -0
- data/README.md +44 -2
- 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 +18 -8
- data/test/Namo/Row_test.rb +62 -0
- data/test/namo_test.rb +320 -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: dcb7aafe4522ff115c12464016fef1a6909047975829523b095105f601a7ae60
|
|
4
|
+
data.tar.gz: 81f150f5f370f836734908a30e1ecd600b856f90c0151a698fa25bd117530d92
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6fd5a96f0fdf85e6ffb8a3cfa2d1a560d5da1d118072e95cd549af43149ff2a4c963de122b9026c29c0817e33fcfbd2ba3ece969ee1f6f5d92738c3d7f38b73a
|
|
7
|
+
data.tar.gz: 78ba07312d7e4f4d4ac85a6ca572852964ed7055d48687361d8a86f9cb998122f421e75418ba67bc978bf78d7b0cc22315ebb800ca734e8ade07817bae886736
|
data/CHANGELOG
CHANGED
|
@@ -1,6 +1,86 @@
|
|
|
1
1
|
CHANGELOG
|
|
2
2
|
_________
|
|
3
3
|
|
|
4
|
+
20260612
|
|
5
|
+
0.16.0: ~ data/formula exclusivity — projection drops the formulae it materialises; * and ** raise on a data/formula name collision.
|
|
6
|
+
|
|
7
|
+
1. ~ lib/namo.rb: Namo#[]'s positive-projection branch carries @formulae minus the projected
|
|
8
|
+
derived names — naming a derived dimension materialises it (stored values, computed against
|
|
9
|
+
the yielding Namo, windowed over any same-call selection) and drops the formula; omitted
|
|
10
|
+
formulae carry live and recompute from the result's own rows. Contraction and selection-only
|
|
11
|
+
calls are unchanged. The projection list is the materialise/live selector.
|
|
12
|
+
2. ~ lib/namo.rb: Namo#* and Namo#** raise ArgumentError via the new private
|
|
13
|
+
raise_unless_data_formula_exclusivity when one operand's data dimension is the other's
|
|
14
|
+
derived dimension, block and no-block forms alike. Formula-vs-formula stays left-wins; the
|
|
15
|
+
set operators need no guard (matching-data-dimensions blocks the asymmetric case); the
|
|
16
|
+
constructor stays unguarded.
|
|
17
|
+
3. ~ test/namo_test.rb: + "data/formula exclusivity" describe — access-path agreement on a
|
|
18
|
+
materialised dimension, dimension-listing, dependent-formula carry, omitted-formula
|
|
19
|
+
liveness, live-without-inputs caveat, two-arity windowing at materialisation,
|
|
20
|
+
contraction/selection unchanged, subclass type, composition collision raises (both
|
|
21
|
+
directions, both operators, block forms), left-wins formula merge, contraction-first
|
|
22
|
+
resolution.
|
|
23
|
+
4. ~ test/namo_test.rb: + "range selection" context under #[] — basic range, beginless and
|
|
24
|
+
endless forms, range composed with projection, range on a formula-defined dimension
|
|
25
|
+
(Row_test holds the predicate matrix; these pin the Namo-level wiring).
|
|
26
|
+
5. ~ README.md: + Projection of derived dimensions subsection under Formulae (naming
|
|
27
|
+
materialises, omitting carries live); data/formula collision sentences under Composition
|
|
28
|
+
and Cartesian product.
|
|
29
|
+
6. ~ ROADMAP.md: Promote 0.16.0 to shipped; Current state -> 0.16.0; Summary folds in
|
|
30
|
+
exclusivity; next phase -> 0.17.0. window.length -> window.count and n.length -> n.count
|
|
31
|
+
in the remaining future-release examples (0.17.0, 2.x, 4.x) — Namo has no #length,
|
|
32
|
+
extending the 0.15.0 correction.
|
|
33
|
+
7. ~ COMPARISON.md: Repoint the pre-renumbering planned markers to what shipped — proc-based,
|
|
34
|
+
regex-based, and mixed selection -> shipped (0.8.0); Enumerable methods return Namos ->
|
|
35
|
+
shipped (0.11.0), with the entry summary's parity sentence and the Sorting entry's
|
|
36
|
+
"as of 0.14.0" corrected to 0.11.0; values and to_h -> shipped (0.7.0). Aspect classes ->
|
|
37
|
+
not planned, the entry rewritten to record 0.7.0's plain-return-types decision (Namo#===
|
|
38
|
+
and subclassing cover case dispatch; a Matcher factory can serve a finer split later).
|
|
39
|
+
Aggregation repointed from 2.x to group_by returning a Namo::Collection at 0.19.0 (gated
|
|
40
|
+
on Collection at 0.18.0), with summary/members examples; bare names stay 2.x. Parameterised
|
|
41
|
+
formulae stays planned (0.17.0), its example's window.length -> window.count.
|
|
42
|
+
8. ~ EXAMPLES.md: + Epidemiology / public health section — a cross-row (two-arity) rolling
|
|
43
|
+
weekly average in the four-stage format (Polars, then Namo 1.x/2.x/3.x), with the
|
|
44
|
+
(row, namo) window over the yielding Namo and a one-arity formula referencing the
|
|
45
|
+
two-arity one. window.length -> window.count in the finance 1.x and 2.x stages. Date
|
|
46
|
+
bumped to 20260612.
|
|
47
|
+
9. ~ Namo::VERSION: /0.15.0/0.16.0/
|
|
48
|
+
|
|
49
|
+
20260612
|
|
50
|
+
0.15.0: + two-arity formulae — procs with arity 2 receive (row, namo) for cross-row computation.
|
|
51
|
+
|
|
52
|
+
1. ~ lib/Namo/Row.rb: Row's constructor gains an optional third parameter, namo (default nil),
|
|
53
|
+
stored in @namo — the Namo that yielded the Row; the two-argument form keeps working.
|
|
54
|
+
Row#[] dispatches on formula arity: exactly 2 calls formula.call(self, @namo), raising
|
|
55
|
+
ArgumentError via the new private raise_unless_namo_context when @namo is nil; every
|
|
56
|
+
other arity (0, 1, negative) keeps the existing formula.call(self) unchanged.
|
|
57
|
+
2. ~ lib/Namo/Enumerable.rb: every Row construction (each, select and its aliases, reject,
|
|
58
|
+
sort_by, first, last, take_while, drop_while, uniq's block form, partition) passes self
|
|
59
|
+
as the Row's namo, so two-arity formulae resolve during enumeration and predicate
|
|
60
|
+
evaluation, with the yielding Namo as the window.
|
|
61
|
+
3. ~ lib/namo.rb: values_for's derived branch and the * and ** block paths pass self as the
|
|
62
|
+
Row's namo — values/coordinates/to_h resolve two-arity dimensions, and composition-block
|
|
63
|
+
rows can resolve self's two-arity formulae (extension of the 0.14.0 block contract; the
|
|
64
|
+
(Row, Namo) -> Namo contract and result-formulae rule are unchanged).
|
|
65
|
+
4. ~ test/Namo/Row_test.rb: + constructor third-argument and backward-compatibility tests,
|
|
66
|
+
arity-dispatch tests (1, 2, 0, negative), the nil-namo ArgumentError, and match? on a
|
|
67
|
+
two-arity derived dimension.
|
|
68
|
+
5. ~ test/namo_test.rb: + "#[]= two-arity formulae" describe — SMA cross-row resolution via
|
|
69
|
+
values/coordinates/to_h, selection and subset-predicate resolution, first/last Rows,
|
|
70
|
+
yielding-Namo (filtered-window) semantics, liveness, operator carry-through, composition-
|
|
71
|
+
block resolution, mixed-arity chains, and the empty-Namo case.
|
|
72
|
+
6. ~ README.md: + Cross-row formulae subsection under Formulae, with the SMA example, the
|
|
73
|
+
yielding-Namo semantic, and the no-context error.
|
|
74
|
+
7. ~ ROADMAP.md: Promote 0.15.0 to shipped; Current state -> 0.15.0; Summary folds in
|
|
75
|
+
two-arity formulae; the example's window.length corrected to window.count (Namo has
|
|
76
|
+
no length); "Live computation objects" phrasing updated. + upcoming 0.16.0 section,
|
|
77
|
+
Data/formula exclusivity — projection drops the formulae it materialises, * and **
|
|
78
|
+
raise on a data/formula name collision — with parameterised formulae renumbered to
|
|
79
|
+
0.17.0, Namo::Collection to 0.18.0, and group_by to 0.19.0; next phase -> 0.16.0.
|
|
80
|
+
8. ~ COMPARISON.md: Two-arity formulae -> shipped (0.15.0) with the same window.count
|
|
81
|
+
correction; Parameterised formulae repointed to planned (0.17.0). Date bumped.
|
|
82
|
+
9. ~ Namo::VERSION: /0.14.0/0.15.0/
|
|
83
|
+
|
|
4
84
|
20260608
|
|
5
85
|
0.14.0: + blocks on the composition operators (*, **) for custom match refinement.
|
|
6
86
|
|
data/README.md
CHANGED
|
@@ -353,7 +353,7 @@ ohlcv * fundamentals
|
|
|
353
353
|
|
|
354
354
|
Inner-join semantics: unmatched rows from either side are dropped. Output dimensions are `self.data_dimensions` followed by `other.data_dimensions` exclusive to other. Duplicates on shared coordinates are preserved multiplicatively — output multiplicity is the product of input multiplicities on each matching key.
|
|
355
355
|
|
|
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.
|
|
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. A name that is data on one side and a formula on the other also raises an `ArgumentError` — the operands disagree about what the name means, with no last-write order to appeal to — so resolve before composing: `audited[-:margin] * modelled`.
|
|
357
357
|
|
|
358
358
|
#### Conditional join
|
|
359
359
|
|
|
@@ -404,7 +404,7 @@ products ** quarters
|
|
|
404
404
|
|
|
405
405
|
Output has `self.data.length * other.data.length` rows. Output dimensions are `self.data_dimensions + other.data_dimensions`, in operand order. Duplicates are preserved multiplicatively.
|
|
406
406
|
|
|
407
|
-
The two Namos must have **no** shared data dimensions — the precondition is the mirror image of `*`. Any overlap raises an `ArgumentError`; allowing it would produce rows with the same dimension named twice. Formulae merge from both sides; the left-hand side wins on conflict
|
|
407
|
+
The two Namos must have **no** shared data dimensions — the precondition is the mirror image of `*`. Any overlap raises an `ArgumentError`; allowing it would produce rows with the same dimension named twice. Formulae merge from both sides; the left-hand side wins on conflict, and a data/formula name collision between the operands raises, as for `*`.
|
|
408
408
|
|
|
409
409
|
The visual relationship is intentional: `*` is the filtered version, `**` is the explosive version — more sigil, more output.
|
|
410
410
|
|
|
@@ -644,6 +644,48 @@ sales[product: 'Widget'][:revenue, :quarter]
|
|
|
644
644
|
|
|
645
645
|
Formulae carry through selection — a filtered Namo instance remembers its formulae.
|
|
646
646
|
|
|
647
|
+
#### Projection of derived dimensions
|
|
648
|
+
|
|
649
|
+
Naming a derived dimension in a projection asks for its values: they are computed against the source and stored in the result's rows, and the formula is dropped — the name is a data dimension of the result. Omitting it carries the formula live, recomputing from the result's own rows on every access:
|
|
650
|
+
|
|
651
|
+
```ruby
|
|
652
|
+
sales[:price, :quantity, :revenue].derived_dimensions
|
|
653
|
+
# => [] — :revenue is stored values, a snapshot taken at projection
|
|
654
|
+
|
|
655
|
+
sales[:price, :quantity].derived_dimensions
|
|
656
|
+
# => [:revenue] — :revenue recomputes from the projected rows on every access
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
The projection list is the selector: name a derived dimension for a snapshot, omit it to keep it as computation. A carried formula whose inputs the projection dropped breaks on access — the same caveat as contracting away a formula's inputs.
|
|
660
|
+
|
|
661
|
+
#### Cross-row formulae
|
|
662
|
+
|
|
663
|
+
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.
|
|
664
|
+
|
|
665
|
+
A simple moving average reads the surrounding rows through `namo`:
|
|
666
|
+
|
|
667
|
+
```ruby
|
|
668
|
+
prices = Namo.new([
|
|
669
|
+
{symbol: 'AAA', date: 1, close: 10.0},
|
|
670
|
+
{symbol: 'AAA', date: 2, close: 20.0},
|
|
671
|
+
{symbol: 'AAA', date: 3, close: 30.0}
|
|
672
|
+
])
|
|
673
|
+
|
|
674
|
+
prices[:sma] = proc{|row, namo|
|
|
675
|
+
window = namo[symbol: row[:symbol], date: ->(d){d <= row[:date]}]
|
|
676
|
+
window.values(:close).sum / window.count.to_f
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
prices.values(:sma)
|
|
680
|
+
# => [10.0, 15.0, 20.0]
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
`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.
|
|
684
|
+
|
|
685
|
+
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.
|
|
686
|
+
|
|
687
|
+
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.
|
|
688
|
+
|
|
647
689
|
### Polymorphic `[]=`
|
|
648
690
|
|
|
649
691
|
`[]=` 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
|
@@ -71,7 +71,8 @@ class Namo
|
|
|
71
71
|
rows.map(&:to_h)
|
|
72
72
|
end
|
|
73
73
|
)
|
|
74
|
-
|
|
74
|
+
carried = positive.any? ? @formulae.reject{|name, _| positive.include?(name)} : @formulae.dup
|
|
75
|
+
self.class.new(projected, formulae: carried)
|
|
75
76
|
end
|
|
76
77
|
|
|
77
78
|
def []=(name, value)
|
|
@@ -118,13 +119,14 @@ class Namo
|
|
|
118
119
|
def *(other, &block)
|
|
119
120
|
raise_unless_namo(other)
|
|
120
121
|
raise_unless_shared_data_dimensions(other)
|
|
122
|
+
raise_unless_data_formula_exclusivity(other)
|
|
121
123
|
shared = data_dimensions & other.data_dimensions
|
|
122
124
|
combined_data = []
|
|
123
125
|
@data.each do |left_row|
|
|
124
126
|
matched = other.data.select{|right_row| shared.all?{|dim| left_row[dim] == right_row[dim]}}
|
|
125
127
|
if block
|
|
126
128
|
candidates = other.class.new(matched, formulae: other.formulae.dup)
|
|
127
|
-
chosen = block.call(Row.new(left_row, @formulae), candidates)
|
|
129
|
+
chosen = block.call(Row.new(left_row, @formulae, self), candidates)
|
|
128
130
|
chosen.data.each{|right_row| combined_data << left_row.merge(right_row)}
|
|
129
131
|
else
|
|
130
132
|
matched.each{|right_row| combined_data << left_row.merge(right_row)}
|
|
@@ -136,11 +138,12 @@ class Namo
|
|
|
136
138
|
def **(other, &block)
|
|
137
139
|
raise_unless_namo(other)
|
|
138
140
|
raise_unless_disjoint_data_dimensions(other)
|
|
141
|
+
raise_unless_data_formula_exclusivity(other)
|
|
139
142
|
combined_data = []
|
|
140
143
|
@data.each do |left_row|
|
|
141
144
|
if block
|
|
142
145
|
candidates = other.class.new(other.data, formulae: other.formulae.dup)
|
|
143
|
-
chosen = block.call(Row.new(left_row, @formulae), candidates)
|
|
146
|
+
chosen = block.call(Row.new(left_row, @formulae, self), candidates)
|
|
144
147
|
chosen.data.each{|right_row| combined_data << left_row.merge(right_row)}
|
|
145
148
|
else
|
|
146
149
|
other.data.each{|right_row| combined_data << left_row.merge(right_row)}
|
|
@@ -229,11 +232,17 @@ class Namo
|
|
|
229
232
|
|
|
230
233
|
private
|
|
231
234
|
|
|
235
|
+
def initialize(positional_data = nil, data: [], formulae: {}, name: nil)
|
|
236
|
+
@data = positional_data || data
|
|
237
|
+
@formulae = formulae
|
|
238
|
+
@name = name
|
|
239
|
+
end
|
|
240
|
+
|
|
232
241
|
def values_for(dim)
|
|
233
242
|
if data_dimensions.include?(dim)
|
|
234
243
|
@data.map{|row_data| row_data[dim]}
|
|
235
244
|
else
|
|
236
|
-
@data.map{|row_data| Row.new(row_data, @formulae)[dim]}
|
|
245
|
+
@data.map{|row_data| Row.new(row_data, @formulae, self)[dim]}
|
|
237
246
|
end
|
|
238
247
|
end
|
|
239
248
|
|
|
@@ -261,9 +270,10 @@ class Namo
|
|
|
261
270
|
end
|
|
262
271
|
end
|
|
263
272
|
|
|
264
|
-
def
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
273
|
+
def raise_unless_data_formula_exclusivity(other)
|
|
274
|
+
collisions = (data_dimensions & other.derived_dimensions) | (derived_dimensions & other.data_dimensions)
|
|
275
|
+
if collisions.any?
|
|
276
|
+
raise ArgumentError, "name collision between data and formulae: #{collisions.inspect}"
|
|
277
|
+
end
|
|
268
278
|
end
|
|
269
279
|
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
|
@@ -491,6 +491,36 @@ describe Namo do
|
|
|
491
491
|
end
|
|
492
492
|
end
|
|
493
493
|
|
|
494
|
+
context "range selection" do
|
|
495
|
+
it "selects rows whose value falls within the range" do
|
|
496
|
+
result = sales[price: 5.0..15.0]
|
|
497
|
+
_(result.to_a.count).must_equal 2
|
|
498
|
+
_(result.to_a.map{|row| row[:product]}).must_equal ['Widget', 'Widget']
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
it "supports beginless and endless ranges" do
|
|
502
|
+
_(sales[price: ..15.0].to_a.count).must_equal 2
|
|
503
|
+
_(sales[quantity: 100..].to_a.count).must_equal 2
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
it "composes with projection in a single call" do
|
|
507
|
+
result = sales[:product, :quantity, quantity: 50..120]
|
|
508
|
+
_(result.to_a).must_equal [
|
|
509
|
+
{product: 'Widget', quantity: 100},
|
|
510
|
+
{product: 'Gadget', quantity: 60}
|
|
511
|
+
]
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
it "selects on a formula-defined dimension" do
|
|
515
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
516
|
+
result = sales[revenue: 1200.0..]
|
|
517
|
+
_(result.to_a).must_equal [
|
|
518
|
+
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
|
|
519
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
520
|
+
]
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
494
524
|
context "mixed proc and regex selection" do
|
|
495
525
|
it "combines a proc and a regex across dimensions" do
|
|
496
526
|
result = sales[product: /^W/, quantity: ->(v){v > 100}]
|
|
@@ -647,6 +677,296 @@ describe Namo do
|
|
|
647
677
|
end
|
|
648
678
|
end
|
|
649
679
|
|
|
680
|
+
describe "#[]= two-arity formulae" do
|
|
681
|
+
let(:price_data) do
|
|
682
|
+
[
|
|
683
|
+
{symbol: 'AAA', date: 1, close: 10.0},
|
|
684
|
+
{symbol: 'AAA', date: 2, close: 20.0},
|
|
685
|
+
{symbol: 'AAA', date: 3, close: 30.0},
|
|
686
|
+
]
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
let(:prices) do
|
|
690
|
+
Namo.new(price_data)
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
# A simple moving average: the mean close over the rows of the same symbol
|
|
694
|
+
# up to and including the current row's date, computed against the Namo the
|
|
695
|
+
# row belongs to.
|
|
696
|
+
let(:sma) do
|
|
697
|
+
->(row, namo){
|
|
698
|
+
window = namo[symbol: row[:symbol], date: ->(d){d <= row[:date]}]
|
|
699
|
+
window.values(:close).sum / window.count.to_f
|
|
700
|
+
}
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
it "resolves a cross-row SMA via values" do
|
|
704
|
+
prices[:sma] = sma
|
|
705
|
+
_(prices.values(:sma)).must_equal [10.0, 15.0, 20.0]
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
it "resolves through coordinates" do
|
|
709
|
+
prices[:sma] = sma
|
|
710
|
+
_(prices.coordinates(:sma)).must_equal [10.0, 15.0, 20.0]
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
it "resolves through the no-arg to_h" do
|
|
714
|
+
prices[:sma] = sma
|
|
715
|
+
_(prices.to_h[:sma]).must_equal [10.0, 15.0, 20.0]
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
it "selects on the two-arity dimension" do
|
|
719
|
+
prices[:sma] = sma
|
|
720
|
+
_(prices[sma: ->(v){v > 12.0}].values(:date)).must_equal [2, 3]
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
it "resolves a two-arity dimension in a subset-method predicate" do
|
|
724
|
+
prices[:sma] = sma
|
|
725
|
+
result = prices.select{|row| row[:sma] > 12.0}
|
|
726
|
+
_(result).must_be_kind_of Namo
|
|
727
|
+
_(result.values(:date)).must_equal [2, 3]
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
it "resolves the two-arity formula on the no-arg first Row" do
|
|
731
|
+
prices[:sma] = sma
|
|
732
|
+
_(prices.first[:sma]).must_equal 10.0
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
it "resolves the two-arity formula on the no-arg last Row" do
|
|
736
|
+
prices[:sma] = sma
|
|
737
|
+
_(prices.last[:sma]).must_equal 20.0
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
it "windows over the yielding Namo — a filtered Namo computes over the filtered rows only" do
|
|
741
|
+
prices[:sma] = sma
|
|
742
|
+
filtered = prices.select{|row| row[:date] >= 2}
|
|
743
|
+
# On the full Namo the date-2 SMA averages dates 1 and 2 (15.0); on the
|
|
744
|
+
# filtered Namo it sees only date 2, so the value differs.
|
|
745
|
+
_(prices.values(:sma)).must_equal [10.0, 15.0, 20.0]
|
|
746
|
+
_(filtered.values(:sma)).must_equal [20.0, 25.0]
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
it "is live: appending a row changes the two-arity result on next access" do
|
|
750
|
+
prices[:sma] = sma
|
|
751
|
+
_(prices.last[:sma]).must_equal 20.0
|
|
752
|
+
prices.data << {symbol: 'AAA', date: 4, close: 40.0}
|
|
753
|
+
_(prices.last[:sma]).must_equal 25.0
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
it "carries a two-arity formula through a set-operator result, windowing over the combined rows" do
|
|
757
|
+
a = Namo.new([{symbol: 'AAA', date: 1}, {symbol: 'AAA', date: 2}])
|
|
758
|
+
b = Namo.new([{symbol: 'AAA', date: 3}])
|
|
759
|
+
a[:peers] = ->(row, namo){namo.count}
|
|
760
|
+
_((a + b).values(:peers)).must_equal [3, 3, 3]
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
it "carries a merged two-arity formula through a composition result, windowing over the joined rows" do
|
|
764
|
+
left = Namo.new([{symbol: 'AAA', date: 1}, {symbol: 'AAA', date: 2}])
|
|
765
|
+
right = Namo.new([{symbol: 'AAA', sector: 'tech'}])
|
|
766
|
+
left[:peers] = ->(row, namo){namo.count}
|
|
767
|
+
result = left * right
|
|
768
|
+
_(result.values(:peers)).must_equal [2, 2]
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
it "resolves a two-arity formula of self inside a composition block" do
|
|
772
|
+
left = Namo.new([{symbol: 'AAA', date: 1}, {symbol: 'AAA', date: 2}])
|
|
773
|
+
right = Namo.new([{symbol: 'AAA', sector: 'tech'}])
|
|
774
|
+
left[:peers] = ->(row, namo){namo.count}
|
|
775
|
+
seen = nil
|
|
776
|
+
left.*(right){|row, candidates| seen = row[:peers]; candidates}
|
|
777
|
+
_(seen).must_equal 2
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
it "lets a one-arity formula reference a two-arity formula by name" do
|
|
781
|
+
prices[:sma] = sma
|
|
782
|
+
prices[:double_sma] = ->(row){row[:sma] * 2}
|
|
783
|
+
_(prices.values(:double_sma)).must_equal [20.0, 30.0, 40.0]
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
it "lets a two-arity formula reference a one-arity formula by name" do
|
|
787
|
+
prices[:tenth] = ->(row){row[:close] / 10.0}
|
|
788
|
+
prices[:tenth_plus_count] = ->(row, namo){row[:tenth] + namo.count}
|
|
789
|
+
_(prices.values(:tenth_plus_count)).must_equal [4.0, 5.0, 6.0]
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
it "returns [] for a two-arity dimension on an empty Namo without invoking the formula" do
|
|
793
|
+
invoked = false
|
|
794
|
+
empty = Namo.new([], formulae: {sma: ->(row, namo){invoked = true; 0}})
|
|
795
|
+
_(empty.values(:sma)).must_equal []
|
|
796
|
+
_(invoked).must_equal false
|
|
797
|
+
end
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
describe "data/formula exclusivity" do
|
|
801
|
+
context "projection" do
|
|
802
|
+
let(:price_data) do
|
|
803
|
+
[
|
|
804
|
+
{symbol: 'AAA', date: 1, close: 10.0},
|
|
805
|
+
{symbol: 'AAA', date: 2, close: 20.0},
|
|
806
|
+
{symbol: 'AAA', date: 3, close: 30.0},
|
|
807
|
+
]
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
let(:prices) do
|
|
811
|
+
Namo.new(price_data)
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
let(:sma) do
|
|
815
|
+
->(row, namo){
|
|
816
|
+
window = namo[symbol: row[:symbol], date: ->(d){d <= row[:date]}]
|
|
817
|
+
window.values(:close).sum / window.count.to_f
|
|
818
|
+
}
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
it "agrees across all access paths on a materialised dimension" do
|
|
822
|
+
prices[:sma] = sma
|
|
823
|
+
projected = prices[:date, :sma]
|
|
824
|
+
_(projected.values(:sma)).must_equal [10.0, 15.0, 20.0]
|
|
825
|
+
_(projected.first[:sma]).must_equal projected.values(:sma).first
|
|
826
|
+
_(projected[sma: ->(v){v > 12.0}].values(:date)).must_equal [2, 3]
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
it "lists a materialised dimension as data, not derived, exactly once" do
|
|
830
|
+
prices[:sma] = sma
|
|
831
|
+
projected = prices[:date, :sma]
|
|
832
|
+
_(projected.data_dimensions).must_include :sma
|
|
833
|
+
_(projected.derived_dimensions).wont_include :sma
|
|
834
|
+
_(projected.dimensions.count(:sma)).must_equal 1
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
it "carries a dependent formula not named in the projection, resolving off the materialised column" do
|
|
838
|
+
prices[:sma] = sma
|
|
839
|
+
prices[:double_sma] = ->(row){row[:sma] * 2}
|
|
840
|
+
projected = prices[:date, :sma]
|
|
841
|
+
_(projected.derived_dimensions).must_equal [:double_sma]
|
|
842
|
+
_(projected.values(:double_sma)).must_equal [20.0, 30.0, 40.0]
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
it "carries an omitted formula live, recomputing from the result's own rows" do
|
|
846
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
847
|
+
projected = sales[:price, :quantity]
|
|
848
|
+
_(projected.derived_dimensions).must_equal [:revenue]
|
|
849
|
+
_(projected.values(:revenue)).must_equal [1000.0, 1500.0, 1000.0, 1500.0]
|
|
850
|
+
projected.data.first[:quantity] = 200
|
|
851
|
+
_(projected.values(:revenue).first).must_equal 2000.0
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
it "breaks on access when a carried formula's inputs were dropped (caveat emptor)" do
|
|
855
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
856
|
+
projected = sales[:product]
|
|
857
|
+
_(projected.derived_dimensions).must_equal [:revenue]
|
|
858
|
+
_ { projected.values(:revenue) }.must_raise NoMethodError
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
it "materialises a two-arity formula windowed over the yielding Namo" do
|
|
862
|
+
prices[:sma] = sma
|
|
863
|
+
_(prices[:date, :sma].values(:sma)).must_equal [10.0, 15.0, 20.0]
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
it "windows a two-arity materialisation over a same-call selection" do
|
|
867
|
+
prices[:sma] = sma
|
|
868
|
+
projected = prices[:date, :sma, date: 2..3]
|
|
869
|
+
_(projected.values(:sma)).must_equal [20.0, 25.0]
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
it "carries all formulae through a selection-only call" do
|
|
873
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
874
|
+
result = sales[price: ..15.0]
|
|
875
|
+
_(result.derived_dimensions).must_equal [:revenue]
|
|
876
|
+
_(result.values(:revenue)).must_equal [1000.0, 1500.0]
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
it "carries all formulae through contraction" do
|
|
880
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
881
|
+
result = sales[-:quarter]
|
|
882
|
+
_(result.derived_dimensions).must_equal [:revenue]
|
|
883
|
+
_(result.values(:revenue)).must_equal [1000.0, 1500.0, 1000.0, 1500.0]
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
it "returns pure materialised values and empty formulae when only derived names are projected" do
|
|
887
|
+
prices[:sma] = sma
|
|
888
|
+
projected = prices[:sma]
|
|
889
|
+
_(projected.to_a).must_equal [{sma: 10.0}, {sma: 15.0}, {sma: 20.0}]
|
|
890
|
+
_(projected.formulae).must_equal({})
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
it "returns an instance of self's class" do
|
|
894
|
+
subclass = Class.new(Namo)
|
|
895
|
+
namo = subclass.new([{x: 1}])
|
|
896
|
+
namo[:double] = ->(row){row[:x] * 2}
|
|
897
|
+
_(namo[:double].class).must_equal subclass
|
|
898
|
+
end
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
context "composition" do
|
|
902
|
+
let(:audited) do
|
|
903
|
+
Namo.new([
|
|
904
|
+
{symbol: 'BHP', margin: 0.3},
|
|
905
|
+
{symbol: 'RIO', margin: 0.25}
|
|
906
|
+
])
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
let(:modelled) do
|
|
910
|
+
namo = Namo.new([
|
|
911
|
+
{symbol: 'BHP', price: 10.0, cost: 6.0},
|
|
912
|
+
{symbol: 'RIO', price: 20.0, cost: 16.0}
|
|
913
|
+
])
|
|
914
|
+
namo[:margin] = proc{|r| (r[:price] - r[:cost]) / r[:price]}
|
|
915
|
+
namo
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
let(:audited_orders) do
|
|
919
|
+
Namo.new([{order: 'A', margin: 0.3}])
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
let(:modelled_tiers) do
|
|
923
|
+
namo = Namo.new([{tier: 'light', price: 10.0, cost: 6.0}])
|
|
924
|
+
namo[:margin] = proc{|r| (r[:price] - r[:cost]) / r[:price]}
|
|
925
|
+
namo
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
it "raises on * when self's data dimension is other's derived dimension" do
|
|
929
|
+
_ { audited * modelled }.must_raise ArgumentError
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
it "raises on * when self's derived dimension is other's data dimension" do
|
|
933
|
+
_ { modelled * audited }.must_raise ArgumentError
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
it "raises on ** when self's data dimension is other's derived dimension" do
|
|
937
|
+
_ { audited_orders ** modelled_tiers }.must_raise ArgumentError
|
|
938
|
+
end
|
|
939
|
+
|
|
940
|
+
it "raises on ** when self's derived dimension is other's data dimension" do
|
|
941
|
+
_ { modelled_tiers ** audited_orders }.must_raise ArgumentError
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
it "raises in the block forms of both operators" do
|
|
945
|
+
_ { audited.*(modelled){|row, candidates| candidates} }.must_raise ArgumentError
|
|
946
|
+
_ { audited_orders.**(modelled_tiers){|row, candidates| candidates} }.must_raise ArgumentError
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
it "names the colliding dimensions in the message" do
|
|
950
|
+
err = _ { audited * modelled }.must_raise ArgumentError
|
|
951
|
+
_(err.message).must_match(/name collision between data and formulae/)
|
|
952
|
+
_(err.message).must_include ':margin'
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
it "does not raise on a formula-vs-formula collision — left wins" do
|
|
956
|
+
left = Namo.new([{symbol: 'BHP', close: 42.5}])
|
|
957
|
+
right = Namo.new([{symbol: 'BHP', pe: 14.5}])
|
|
958
|
+
left[:margin] = proc{|r| :left}
|
|
959
|
+
right[:margin] = proc{|r| :right}
|
|
960
|
+
_((left * right).values(:margin)).must_equal [:left]
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
it "composes after explicit resolution by contraction" do
|
|
964
|
+
result = audited[-:margin] * modelled
|
|
965
|
+
_(result.values(:margin)).must_equal [0.4, 0.2]
|
|
966
|
+
end
|
|
967
|
+
end
|
|
968
|
+
end
|
|
969
|
+
|
|
650
970
|
describe "#each" do
|
|
651
971
|
it "yields Row objects" do
|
|
652
972
|
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.16.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: []
|