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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35ca3cdc2c06b2da96ad636fa5ac1a593204a335ea93ff3994fc8bf187629aa5
4
- data.tar.gz: 29639054075323f96fd0b6fe76bd733265ea155949965ec552c13ea30995efb8
3
+ metadata.gz: 07d3c62244d72800b8f533d4618c8f51b4161338c37dc439ad26f67557a7315f
4
+ data.tar.gz: 7b8c6c808068b97555bb8f98a1d1ecb55a399e16fc51e475b61f13cbad4f0cef
5
5
  SHA512:
6
- metadata.gz: 8e0be2ae49cd9cc493e580174b3edf8ac874592b45bd3de08ab1b5db3e7a83daa6d6f9c8efb97089b1653d61a92248a0ec7de60c7ab26beef98c0e9b3fab7a0f
7
- data.tar.gz: 49a00bf247a8e286ee45e8619253d36ae8f77d2def369d377d5c46fa8c9ebad734513333d23472d9b8c074800f49a42292458505ad34281466079a73096e33aa
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
- ## Why?
1043
+ #### Partitioning with `group_by`
1044
1044
 
1045
- Every other multi-dimensional array library requires you to pre-shape your data before you can work with it. Namo takes it in the form it likely already comes in.
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
 
@@ -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
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.19.0'
5
+ VERSION = '0.21.0'
6
6
  end
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).must_equal({})
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).must_equal({})
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.19.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.14
100
+ rubygems_version: 4.0.10
99
101
  specification_version: 4
100
102
  summary: Named dimensional data for Ruby.
101
103
  test_files: []