namo 0.19.0 → 0.21.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 +60 -0
- data/README.md +27 -2
- data/lib/Namo/Enumerable.rb +9 -0
- data/lib/Namo/Formulae.rb +71 -0
- data/lib/Namo/VERSION.rb +1 -1
- data/lib/namo.rb +2 -1
- data/test/Namo/Formulae_test.rb +163 -0
- data/test/namo_test.rb +144 -2
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 07d3c62244d72800b8f533d4618c8f51b4161338c37dc439ad26f67557a7315f
|
|
4
|
+
data.tar.gz: 7b8c6c808068b97555bb8f98a1d1ecb55a399e16fc51e475b61f13cbad4f0cef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 58db7e089c9a6be6e9d050a549cf5df98983227d3a3d218d0d5f61ee2684009a1af5f4069cb16376fe8211425060fe2b1d609fb5b6770236726782fa57a35b20
|
|
7
|
+
data.tar.gz: a4327a3484ffc1b30755ed38f94c2e432f0d6e338f5348a75b854c1cdebcfedebab4a742441d5e542c832d4a0aa517dc67a51f8b926462f2c0ab4ecad4a990d8
|
data/CHANGELOG
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
1
|
CHANGELOG
|
|
2
2
|
_________
|
|
3
3
|
|
|
4
|
+
20260625
|
|
5
|
+
0.21.0: + Namo::Formulae; @formulae: /Hash/Namo::Formulae/. Behaviour-preserving: no new capability, just an object for the formula collection to accrete behaviour onto later, mirroring the 0.1.0 extraction of Row.
|
|
6
|
+
|
|
7
|
+
1. + lib/Namo/Formulae.rb: + Namo::Formulae, a thin class wrapping a Hash @store.
|
|
8
|
+
Responds to exactly the subset of Hash that lib/namo.rb and Row send the formula
|
|
9
|
+
collection: [], []=, keys, key?, empty?, each, delete, merge, reject, dup; includes
|
|
10
|
+
Enumerable (each yields [name, callable] pairs, giving map/select/etc. for free).
|
|
11
|
+
Three deliberate choices: to_h returns a copy (@store.dup) so callers cannot mutate
|
|
12
|
+
internals; ==/eql?/hash are names-only (keys.sort), matching Namo's own formula
|
|
13
|
+
equality where two sets with the same names but different proc objects are equivalent;
|
|
14
|
+
merge/reject/dup return a Formulae, not a Hash.
|
|
15
|
+
2. ~ lib/namo.rb: + the require; initialize now normalises the incoming formulae to a
|
|
16
|
+
Formulae (Formulae.new(formulae) unless already one). This is the single behavioural
|
|
17
|
+
seam — every existing internal construction passes a Hash and is wrapped, while
|
|
18
|
+
operator results that now pass a Formulae (merge/dup return Formulae) pass through.
|
|
19
|
+
No other call site changed: each sends a message Formulae answers.
|
|
20
|
+
3. ~ test/namo_test.rb: the two assertions comparing the public #formulae accessor to a
|
|
21
|
+
literal {} now assert emptiness (must_be_empty). The accessor returns a Formulae, not a
|
|
22
|
+
Hash; the formula set being empty is the unchanged behaviour these tests pin. + two
|
|
23
|
+
construction tests pinning the normalisation seam: a Hash given by keyword is wrapped in
|
|
24
|
+
a Formulae, and a Formulae given by keyword passes through unchanged (same object).
|
|
25
|
+
4. + test/Namo/Formulae_test.rb: + unit test pinning the three deliberate choices (to_h
|
|
26
|
+
copy, names-only ==/eql?/hash, the Hash-compatible surface plus Enumerable map/select).
|
|
27
|
+
5. ~ Namo::VERSION: /0.20.0/0.21.0/
|
|
28
|
+
|
|
29
|
+
20260615
|
|
30
|
+
0.20.0: + group_by(dimension) — splits a Namo into a Namo::Collection, one member per distinct value, completing the Enumerable coherence pass begun at 0.11.0.
|
|
31
|
+
|
|
32
|
+
1. ~ lib/Namo/Enumerable.rb: + Namo#group_by(dimension). Returns a Namo::Collection
|
|
33
|
+
whose members are the groups — one member per distinct value, each a Namo of the
|
|
34
|
+
matching rows, named by its group value, carrying the surviving formulae, preserving
|
|
35
|
+
subclass type via self.class.new. The grouping dimension is retained (the split is
|
|
36
|
+
along the axis, not consuming it), so namo.group_by(d).as_detail(d) inverts it. A
|
|
37
|
+
nil-valued group produces a nil-named member that holds its rows and round-trips, the
|
|
38
|
+
use-site naming decision (0.18.0) paying off. A ternary makes the materialise
|
|
39
|
+
decision: when dimension is derived, the group source is the projection
|
|
40
|
+
(self[*data_dimensions, dimension], 0.16.0) so the grouped-by formula becomes a stored
|
|
41
|
+
column and is dropped while every other formula carries through live; when data, the
|
|
42
|
+
source is self untouched. A parameterised formula raises the 0.17.0 ArgumentError at
|
|
43
|
+
the materialisation site, group_by needing no enforcement of its own.
|
|
44
|
+
2. ~ test/namo_test.rb: + "#group_by" describe — return type, one-member-per-value,
|
|
45
|
+
row partitioning, grouping-dimension retention, find-by-name, formula carry-through,
|
|
46
|
+
subclass preservation, non-mutation, empty-Namo; the uniform inversion law
|
|
47
|
+
(data: exact; formula: against the materialised form); grouping by a formula
|
|
48
|
+
(partition by value, materialise-and-drop, only-grouped-formula-altered with a live
|
|
49
|
+
dependent, parameterised raise); the nil-valued grouping dimension (nil-named member,
|
|
50
|
+
no rows dropped, round-trip); and consistency with assembly (summary/detail on a
|
|
51
|
+
partitioned Collection).
|
|
52
|
+
3. ~ README.md: + group_by subsection under Collections — the partition constructor as
|
|
53
|
+
the mirror of <<, the round-trip example, the materialise-on-formula behaviour, and
|
|
54
|
+
the cross-reference to the assembly side.
|
|
55
|
+
4. ~ ROADMAP.md: Promote 0.20.0 to shipped; Current state -> 0.20.0; + the materialisation
|
|
56
|
+
rationale (unified-treatment requires it; materialise-and-drop is the only invertible
|
|
57
|
+
choice; the uniform inversion law); Summary folds in group_by; next phase -> 1.0.0.
|
|
58
|
+
5. ~ COMPARISON.md: Aggregation entry — group_by repointed from planned (0.20.0) to
|
|
59
|
+
shipped, the partition constructor now realised; + the grouping-by-formula comparison
|
|
60
|
+
(computed-key grouping is parity, but only Namo partitions on a named, attached formula
|
|
61
|
+
through the data-dimension interface). Date bumped.
|
|
62
|
+
6. ~ Namo::VERSION: /0.19.0/0.20.0/
|
|
63
|
+
|
|
4
64
|
20260614
|
|
5
65
|
0.19.0: ~ row-multiset equality — replace the sorted canonical form with a row multiset, so ==, eql?, hash, and the subset operators compare without sorting: nil/NaN-safe, row- and dimension-order blind, and consistently type-strict.
|
|
6
66
|
|
data/README.md
CHANGED
|
@@ -1040,9 +1040,34 @@ gt.values(:weight) # => [200, 80, 150, 60, ...] (line
|
|
|
1040
1040
|
|
|
1041
1041
|
Freeze-gated memoisation is a 2.x optimisation — opt-in via `freeze`, transparent, and never changing this observable behaviour. `group_by` (0.20.0) is the partition-side constructor for the same type: it splits a Namo into a `Collection`, the mirror of assembling one with `<<`.
|
|
1042
1042
|
|
|
1043
|
-
|
|
1043
|
+
#### Partitioning with `group_by`
|
|
1044
1044
|
|
|
1045
|
-
|
|
1045
|
+
`group_by(dimension)` is the partition-side constructor for a `Collection` — the mirror of assembling one with `<<`. It splits a Namo into one member per distinct value of the dimension, each a Namo holding that group's rows, named by its group value:
|
|
1046
|
+
|
|
1047
|
+
```ruby
|
|
1048
|
+
prices.group_by(:symbol)
|
|
1049
|
+
# => #<Namo::Collection members: [:BHP, :RIO, :CBA]>
|
|
1050
|
+
|
|
1051
|
+
prices.group_by(:symbol).summary(:close, reducer: :mean)
|
|
1052
|
+
# => Namo with {member:, close:} rows — mean close per symbol
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
The grouping dimension is retained in every member — the split runs *along* the axis, it doesn't consume it — so the partition inverts exactly through `as_detail` on the same dimension:
|
|
1056
|
+
|
|
1057
|
+
```ruby
|
|
1058
|
+
prices.group_by(:symbol).as_detail(:symbol) == prices
|
|
1059
|
+
# => true
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
Because data and derived dimensions are treated alike, you can group by a formula as readily as by a stored column. Grouping by a derived dimension materialises it first — the grouped-by formula becomes a stored value in each member and is dropped, while every other formula carries through live:
|
|
1063
|
+
|
|
1064
|
+
```ruby
|
|
1065
|
+
prices[:value_score] = proc{|r| r[:pe] < 10 ? 2 : r[:pe] < 15 ? 1 : 0}
|
|
1066
|
+
prices.group_by(:value_score)
|
|
1067
|
+
# => one member per score; :value_score is now a data column in each
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
This gives a single inversion law over the whole namespace — `namo.group_by(d).as_detail(d) == namo[*namo.data_dimensions, d]` for any `d`, with the exact-original round-trip being the data-dimension instance of it. A nil-valued group produces a nil-named member, holding its rows and round-tripping like any other.
|
|
1046
1071
|
|
|
1047
1072
|
## Name
|
|
1048
1073
|
|
data/lib/Namo/Enumerable.rb
CHANGED
|
@@ -68,5 +68,14 @@ class Namo
|
|
|
68
68
|
self.class.new(non_matches, formulae: @formulae.dup),
|
|
69
69
|
]
|
|
70
70
|
end
|
|
71
|
+
|
|
72
|
+
def group_by(dimension)
|
|
73
|
+
collection = Collection.new
|
|
74
|
+
source = derived_dimensions.include?(dimension) ? self[*data_dimensions, dimension] : self
|
|
75
|
+
source.data.group_by{|row_data| row_data[dimension]}.each do |value, rows|
|
|
76
|
+
collection << self.class.new(rows, formulae: source.formulae.dup, name: value)
|
|
77
|
+
end
|
|
78
|
+
collection
|
|
79
|
+
end
|
|
71
80
|
end
|
|
72
81
|
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Namo/Formulae.rb
|
|
2
|
+
# Namo::Formulae
|
|
3
|
+
|
|
4
|
+
class Namo
|
|
5
|
+
class Formulae
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
def [](name)
|
|
9
|
+
@store[name]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def []=(name, callable)
|
|
13
|
+
@store[name] = callable
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def keys
|
|
17
|
+
@store.keys
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def key?(name)
|
|
21
|
+
@store.key?(name)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def empty?
|
|
25
|
+
@store.empty?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def each(&block)
|
|
29
|
+
return enum_for(:each) unless block_given?
|
|
30
|
+
@store.each(&block)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def delete(name)
|
|
34
|
+
@store.delete(name)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def merge(other)
|
|
38
|
+
self.class.new(@store.merge(other.to_h))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def reject(&block)
|
|
42
|
+
self.class.new(@store.reject(&block))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def dup
|
|
46
|
+
self.class.new(@store.dup)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_h
|
|
50
|
+
@store.dup
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def ==(other)
|
|
54
|
+
other.is_a?(Formulae) && keys.sort == other.keys.sort
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def eql?(other)
|
|
58
|
+
self == other
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def hash
|
|
62
|
+
keys.sort.hash
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def initialize(store = {})
|
|
68
|
+
@store = store
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/Namo/VERSION.rb
CHANGED
data/lib/namo.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# Namo
|
|
3
3
|
|
|
4
4
|
require_relative './Namo/NegatedDimension'
|
|
5
|
+
require_relative './Namo/Formulae'
|
|
5
6
|
require_relative './Namo/Row'
|
|
6
7
|
require_relative './Namo/Collection'
|
|
7
8
|
require_relative './Namo/Enumerable'
|
|
@@ -235,7 +236,7 @@ class Namo
|
|
|
235
236
|
|
|
236
237
|
def initialize(positional_data = nil, data: [], formulae: {}, name: nil)
|
|
237
238
|
@data = positional_data || data
|
|
238
|
-
@formulae = formulae
|
|
239
|
+
@formulae = formulae.is_a?(Formulae) ? formulae : Formulae.new(formulae)
|
|
239
240
|
@name = name
|
|
240
241
|
end
|
|
241
242
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
require 'minitest/autorun'
|
|
2
|
+
require 'minitest-spec-context'
|
|
3
|
+
|
|
4
|
+
require_relative '../../lib/namo'
|
|
5
|
+
|
|
6
|
+
describe Namo::Formulae do
|
|
7
|
+
let(:revenue) do
|
|
8
|
+
proc{|r| r[:price] * r[:quantity]}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
let(:cost) do
|
|
12
|
+
proc{|r| r[:quantity] * 4.0}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
let(:formulae) do
|
|
16
|
+
Namo::Formulae.new({revenue: revenue})
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe "#[]" do
|
|
20
|
+
it "returns the callable stored under a name" do
|
|
21
|
+
_(formulae[:revenue]).must_be_same_as revenue
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "returns nil for an absent name" do
|
|
25
|
+
_(formulae[:missing]).must_be_nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe "#[]=" do
|
|
30
|
+
it "stores a callable under a name" do
|
|
31
|
+
formulae[:cost] = cost
|
|
32
|
+
_(formulae[:cost]).must_be_same_as cost
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe "#keys" do
|
|
37
|
+
it "returns the stored names" do
|
|
38
|
+
_(formulae.keys).must_equal [:revenue]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe "#key?" do
|
|
43
|
+
it "is true for a stored name" do
|
|
44
|
+
_(formulae.key?(:revenue)).must_equal true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "is false for an absent name" do
|
|
48
|
+
_(formulae.key?(:missing)).must_equal false
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe "#empty?" do
|
|
53
|
+
it "is true with no formulae" do
|
|
54
|
+
_(Namo::Formulae.new).must_be_empty
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "is false with formulae" do
|
|
58
|
+
_(formulae.empty?).must_equal false
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe "#each" do
|
|
63
|
+
it "yields name/callable pairs" do
|
|
64
|
+
yielded = {}
|
|
65
|
+
formulae.each{|name, callable| yielded[name] = callable}
|
|
66
|
+
_(yielded).must_equal({revenue: revenue})
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "returns an enumerator with no block" do
|
|
70
|
+
_(formulae.each).must_be_kind_of Enumerator
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe "#delete" do
|
|
75
|
+
it "removes a name" do
|
|
76
|
+
formulae.delete(:revenue)
|
|
77
|
+
_(formulae.key?(:revenue)).must_equal false
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe "#merge" do
|
|
82
|
+
it "returns a Formulae combining both sets" do
|
|
83
|
+
merged = formulae.merge(Namo::Formulae.new({cost: cost}))
|
|
84
|
+
_(merged).must_be_kind_of Namo::Formulae
|
|
85
|
+
_(merged.keys.sort).must_equal [:cost, :revenue]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "lets the argument win on a name conflict" do
|
|
89
|
+
other = proc{|r| 0}
|
|
90
|
+
merged = formulae.merge(Namo::Formulae.new({revenue: other}))
|
|
91
|
+
_(merged[:revenue]).must_be_same_as other
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "does not mutate the receiver" do
|
|
95
|
+
formulae.merge(Namo::Formulae.new({cost: cost}))
|
|
96
|
+
_(formulae.keys).must_equal [:revenue]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe "#reject" do
|
|
101
|
+
it "returns a Formulae without the rejected names" do
|
|
102
|
+
formulae[:cost] = cost
|
|
103
|
+
rejected = formulae.reject{|name, _| name == :cost}
|
|
104
|
+
_(rejected).must_be_kind_of Namo::Formulae
|
|
105
|
+
_(rejected.keys).must_equal [:revenue]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe "#dup" do
|
|
110
|
+
it "returns an independent Formulae" do
|
|
111
|
+
copy = formulae.dup
|
|
112
|
+
copy[:cost] = cost
|
|
113
|
+
_(formulae.key?(:cost)).must_equal false
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
describe "#to_h" do
|
|
118
|
+
it "returns a copy, not the live store" do
|
|
119
|
+
hash = formulae.to_h
|
|
120
|
+
hash[:cost] = cost
|
|
121
|
+
_(formulae.key?(:cost)).must_equal false
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
describe "Enumerable" do
|
|
126
|
+
it "supports map over name/callable pairs" do
|
|
127
|
+
_(formulae.map{|name, _| name}).must_equal [:revenue]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "supports select returning name/callable pairs" do
|
|
131
|
+
formulae[:cost] = cost
|
|
132
|
+
_(formulae.select{|name, _| name == :cost}).must_equal [[:cost, cost]]
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
describe "value semantics" do
|
|
137
|
+
it "is == when names match, regardless of proc objects" do
|
|
138
|
+
_(Namo::Formulae.new({revenue: revenue})).must_equal Namo::Formulae.new({revenue: proc{|r| 0}})
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it "is not == when names differ" do
|
|
142
|
+
_(formulae).wont_equal Namo::Formulae.new({cost: cost})
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it "is not == to a bare Hash" do
|
|
146
|
+
_(formulae).wont_equal({revenue: revenue})
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it "is eql? on matching names with differing procs" do
|
|
150
|
+
_(Namo::Formulae.new({revenue: revenue}).eql?(Namo::Formulae.new({revenue: proc{|r| 0}}))).must_equal true
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "hashes equal on matching names with differing procs" do
|
|
154
|
+
_(Namo::Formulae.new({revenue: revenue}).hash).must_equal Namo::Formulae.new({revenue: proc{|r| 0}}).hash
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
describe "constructor" do
|
|
159
|
+
it "defaults to an empty store" do
|
|
160
|
+
_(Namo::Formulae.new.keys).must_equal []
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
data/test/namo_test.rb
CHANGED
|
@@ -47,7 +47,7 @@ describe Namo do
|
|
|
47
47
|
it "produces an empty Namo with no arguments" do
|
|
48
48
|
namo = Namo.new
|
|
49
49
|
_(namo.data).must_equal []
|
|
50
|
-
_(namo.formulae).
|
|
50
|
+
_(namo.formulae).must_be_empty
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
it "accepts keyword formulae with no data" do
|
|
@@ -56,6 +56,17 @@ describe Namo do
|
|
|
56
56
|
_(namo.derived_dimensions).must_equal [:y]
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
it "wraps keyword formulae given as a Hash in a Formulae" do
|
|
60
|
+
namo = Namo.new(formulae: {y: proc{|r| r[:x] * 2}})
|
|
61
|
+
_(namo.formulae).must_be_kind_of Namo::Formulae
|
|
62
|
+
_(namo.formulae.keys).must_equal [:y]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "passes keyword formulae already given as a Formulae through unchanged" do
|
|
66
|
+
formulae = Namo::Formulae.new({y: proc{|r| r[:x] * 2}})
|
|
67
|
+
_(Namo.new(formulae: formulae).formulae).must_be_same_as formulae
|
|
68
|
+
end
|
|
69
|
+
|
|
59
70
|
it "honours an explicit empty positional array over the nil sentinel" do
|
|
60
71
|
_(Namo.new([]).data).must_equal []
|
|
61
72
|
end
|
|
@@ -1010,7 +1021,7 @@ describe Namo do
|
|
|
1010
1021
|
prices[:sma] = sma
|
|
1011
1022
|
projected = prices[:sma]
|
|
1012
1023
|
_(projected.to_a).must_equal [{sma: 10.0}, {sma: 15.0}, {sma: 20.0}]
|
|
1013
|
-
_(projected.formulae).
|
|
1024
|
+
_(projected.formulae).must_be_empty
|
|
1014
1025
|
end
|
|
1015
1026
|
|
|
1016
1027
|
it "returns an instance of self's class" do
|
|
@@ -2573,4 +2584,135 @@ describe Namo do
|
|
|
2573
2584
|
_(sales.to_a).must_equal sample_data
|
|
2574
2585
|
end
|
|
2575
2586
|
end
|
|
2587
|
+
|
|
2588
|
+
describe "#group_by" do
|
|
2589
|
+
let(:price_data) do
|
|
2590
|
+
[
|
|
2591
|
+
{symbol: 'BHP', date: 1, close: 42.5, pe: 12.0},
|
|
2592
|
+
{symbol: 'BHP', date: 2, close: 43.0, pe: 12.0},
|
|
2593
|
+
{symbol: 'RIO', date: 1, close: 118.3, pe: 9.0},
|
|
2594
|
+
{symbol: 'CBA', date: 1, close: 100.0, pe: 22.0}
|
|
2595
|
+
]
|
|
2596
|
+
end
|
|
2597
|
+
let(:prices) { Namo.new(price_data) }
|
|
2598
|
+
|
|
2599
|
+
it "returns a Namo::Collection" do
|
|
2600
|
+
_(prices.group_by(:symbol)).must_be_kind_of Namo::Collection
|
|
2601
|
+
end
|
|
2602
|
+
|
|
2603
|
+
it "has one member per distinct value of the dimension" do
|
|
2604
|
+
collection = prices.group_by(:symbol)
|
|
2605
|
+
_(collection.members.length).must_equal 3
|
|
2606
|
+
_(collection.members.map(&:name)).must_equal ['BHP', 'RIO', 'CBA']
|
|
2607
|
+
end
|
|
2608
|
+
|
|
2609
|
+
it "gives each member exactly the rows matching its group value" do
|
|
2610
|
+
collection = prices.group_by(:symbol)
|
|
2611
|
+
_(collection.find('BHP').values(:date)).must_equal [1, 2]
|
|
2612
|
+
_(collection.find('RIO').values(:date)).must_equal [1]
|
|
2613
|
+
end
|
|
2614
|
+
|
|
2615
|
+
it "retains the grouping dimension in each member (it is not consumed)" do
|
|
2616
|
+
_(prices.group_by(:symbol).find('BHP').data_dimensions).must_include :symbol
|
|
2617
|
+
end
|
|
2618
|
+
|
|
2619
|
+
it "names each member by its group value, found via find" do
|
|
2620
|
+
_(prices.group_by(:symbol).find('CBA').values(:close)).must_equal [100.0]
|
|
2621
|
+
end
|
|
2622
|
+
|
|
2623
|
+
it "carries the parent's formulae into each member" do
|
|
2624
|
+
prices[:cheap] = proc{|r| r[:pe] < 15}
|
|
2625
|
+
_(prices.group_by(:symbol).find('BHP').values(:cheap)).must_equal [true, true]
|
|
2626
|
+
end
|
|
2627
|
+
|
|
2628
|
+
it "preserves the receiver's class in each member" do
|
|
2629
|
+
subclass = Class.new(Namo)
|
|
2630
|
+
collection = subclass.new(price_data).group_by(:symbol)
|
|
2631
|
+
_(collection.members.first).must_be_instance_of subclass
|
|
2632
|
+
end
|
|
2633
|
+
|
|
2634
|
+
it "does not mutate the receiver" do
|
|
2635
|
+
prices.group_by(:symbol)
|
|
2636
|
+
_(prices.data).must_equal price_data
|
|
2637
|
+
end
|
|
2638
|
+
|
|
2639
|
+
it "returns an empty Collection for an empty Namo" do
|
|
2640
|
+
collection = Namo.new.group_by(:symbol)
|
|
2641
|
+
_(collection).must_be_kind_of Namo::Collection
|
|
2642
|
+
_(collection.members).must_equal []
|
|
2643
|
+
end
|
|
2644
|
+
|
|
2645
|
+
context "the uniform inversion law" do
|
|
2646
|
+
it "round-trips exactly on a data dimension" do
|
|
2647
|
+
_(prices.group_by(:symbol).as_detail(:symbol)).must_equal prices
|
|
2648
|
+
end
|
|
2649
|
+
|
|
2650
|
+
it "round-trips against the materialised form on a formula dimension" do
|
|
2651
|
+
prices[:value_score] = proc{|r| r[:pe] < 10 ? 2 : r[:pe] < 15 ? 1 : 0}
|
|
2652
|
+
recovered = prices.group_by(:value_score).as_detail(:value_score)
|
|
2653
|
+
_(recovered).must_equal prices[*prices.data_dimensions, :value_score]
|
|
2654
|
+
end
|
|
2655
|
+
end
|
|
2656
|
+
|
|
2657
|
+
context "grouping by a formula" do
|
|
2658
|
+
before do
|
|
2659
|
+
prices[:value_score] = proc{|r| r[:pe] < 10 ? 2 : r[:pe] < 15 ? 1 : 0}
|
|
2660
|
+
end
|
|
2661
|
+
|
|
2662
|
+
it "partitions by the formula's value" do
|
|
2663
|
+
collection = prices.group_by(:value_score)
|
|
2664
|
+
_(collection.members.map(&:name).sort).must_equal [0, 1, 2]
|
|
2665
|
+
_(collection.find(1).values(:symbol)).must_equal ['BHP', 'BHP']
|
|
2666
|
+
end
|
|
2667
|
+
|
|
2668
|
+
it "materialises the grouped-by formula into a data column in each member" do
|
|
2669
|
+
member = prices.group_by(:value_score).find(1)
|
|
2670
|
+
_(member.data_dimensions).must_include :value_score
|
|
2671
|
+
_(member.derived_dimensions).wont_include :value_score
|
|
2672
|
+
end
|
|
2673
|
+
|
|
2674
|
+
it "alters only the grouped-by formula, carrying the rest through live" do
|
|
2675
|
+
prices[:tier] = proc{|r| r[:value_score] >= 1 ? 'good' : 'poor'}
|
|
2676
|
+
member = prices.group_by(:value_score).find(1)
|
|
2677
|
+
_(member.derived_dimensions).must_include :tier
|
|
2678
|
+
_(member.derived_dimensions).wont_include :value_score
|
|
2679
|
+
_(member.values(:tier)).must_equal ['good', 'good']
|
|
2680
|
+
end
|
|
2681
|
+
|
|
2682
|
+
it "raises for a parameterised formula it cannot materialise" do
|
|
2683
|
+
prices[:sma] = proc{|row, namo, field, period| 0}
|
|
2684
|
+
_(proc{prices.group_by(:sma)}).must_raise ArgumentError
|
|
2685
|
+
end
|
|
2686
|
+
end
|
|
2687
|
+
|
|
2688
|
+
context "a nil-valued grouping dimension" do
|
|
2689
|
+
let(:sector_data) do
|
|
2690
|
+
[
|
|
2691
|
+
{symbol: 'BHP', sector: 'Mining'},
|
|
2692
|
+
{symbol: 'RIO', sector: 'Mining'},
|
|
2693
|
+
{symbol: 'XYZ', sector: nil}
|
|
2694
|
+
]
|
|
2695
|
+
end
|
|
2696
|
+
let(:stocks) { Namo.new(sector_data) }
|
|
2697
|
+
|
|
2698
|
+
it "produces a nil-named member holding the nil-valued rows" do
|
|
2699
|
+
collection = stocks.group_by(:sector)
|
|
2700
|
+
nil_member = collection.members.find{|m| m.name.nil?}
|
|
2701
|
+
_(nil_member).wont_be_nil
|
|
2702
|
+
_(nil_member.values(:symbol)).must_equal ['XYZ']
|
|
2703
|
+
end
|
|
2704
|
+
|
|
2705
|
+
it "drops no rows and still round-trips" do
|
|
2706
|
+
_(stocks.group_by(:sector).as_detail(:sector)).must_equal stocks
|
|
2707
|
+
end
|
|
2708
|
+
end
|
|
2709
|
+
|
|
2710
|
+
context "consistency with assembly" do
|
|
2711
|
+
it "produces a Collection on which summary and detail behave as on an assembled one" do
|
|
2712
|
+
collection = prices.group_by(:symbol)
|
|
2713
|
+
_(collection.summary(:close, reducer: :sum).values(:member)).must_equal ['BHP', 'RIO', 'CBA']
|
|
2714
|
+
_(collection.detail.values(:close)).must_equal [42.5, 43.0, 118.3, 100.0]
|
|
2715
|
+
end
|
|
2716
|
+
end
|
|
2717
|
+
end
|
|
2576
2718
|
end
|
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.21.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- thoran
|
|
@@ -66,6 +66,7 @@ files:
|
|
|
66
66
|
- Rakefile
|
|
67
67
|
- lib/Namo/Collection.rb
|
|
68
68
|
- lib/Namo/Enumerable.rb
|
|
69
|
+
- lib/Namo/Formulae.rb
|
|
69
70
|
- lib/Namo/NegatedDimension.rb
|
|
70
71
|
- lib/Namo/Row.rb
|
|
71
72
|
- lib/Namo/VERSION.rb
|
|
@@ -73,6 +74,7 @@ files:
|
|
|
73
74
|
- lib/namo.rb
|
|
74
75
|
- namo.gemspec
|
|
75
76
|
- test/Namo/Collection_test.rb
|
|
77
|
+
- test/Namo/Formulae_test.rb
|
|
76
78
|
- test/Namo/NegatedDimension_test.rb
|
|
77
79
|
- test/Namo/Row_test.rb
|
|
78
80
|
- test/Symbol_test.rb
|
|
@@ -95,7 +97,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
95
97
|
- !ruby/object:Gem::Version
|
|
96
98
|
version: '0'
|
|
97
99
|
requirements: []
|
|
98
|
-
rubygems_version: 4.0.
|
|
100
|
+
rubygems_version: 4.0.10
|
|
99
101
|
specification_version: 4
|
|
100
102
|
summary: Named dimensional data for Ruby.
|
|
101
103
|
test_files: []
|