namo 0.20.0 → 0.22.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: ae1731b55ec8d0de8dedfb35fc643f6fb6a68e72bb3464c4139242c1bc6bb91e
4
- data.tar.gz: 161a1842a4daf8a4365a8420600f2824f5f15fd2ea3951e18d00d811eec22010
3
+ metadata.gz: e6342f1cab3b5811a97d92be8ef7ba34a81191bd97a6ee2c606f8adc7d9a0d47
4
+ data.tar.gz: ce7db23d2907e33a80d1e263b4b9987645a6c1c47979b7f3311bf519d8f9e738
5
5
  SHA512:
6
- metadata.gz: 4a563c97ca83b91e578375af09e533f633d2ed2d6bca1152297d3bf4b24dc60c5f25a5679e0e87023e3031f5019b3c32e825f35f23b75b0259cee6fb765a02b5
7
- data.tar.gz: 140f9b808a8f406ca1ca16611ab776544c332830353f1d9dba58d4cf3578ff746d2ff7788671fd9eda0c082d6cc3f1457f29ee38473d88ec2c1f0aa457a0dace
6
+ metadata.gz: 5c6c26eda0a637af215eef9cab0fe369769af3e945b3a1cf980788bd34be97abeefb3cf29fb208012556c5df9e46c212c9476dc222b0f3917404c7db3003f21a
7
+ data.tar.gz: f71fd3ed7b8e8068b55fbb8379cd361c91c84ca1cb4fe823ff72af9fe1c9b8e1247468fb9fd818aa865cac709aa2d22e63f2c866030cacee22bf25c5930e7758
data/CHANGELOG CHANGED
@@ -1,6 +1,57 @@
1
1
  CHANGELOG
2
2
  _________
3
3
 
4
+ 20260626
5
+ 0.22.0: + Formulae#derive — move formula invocation out of Row#[] and into Formulae. [] keeps its meaning (the stored callable, as at 0.21.0); the new derive(name, row, namo, *arguments) resolves a named formula to its value. derive over call: the receiver is the collection, not the callable, so the verb says the collection derives a value via the named rule rather than borrowing Proc#call's "invoke the receiver"; and the result is whatever the proc returns, not necessarily numeric, which a neutral verb leaves open. Indexing a Formulae is storage access, not a Namo operation, so [] returning the callable carries no inconsistency with the dimension-value [] of Namo and Row — different domains, same spelling. Behaviour-preserving at the usage level: row[:revenue], namo[:x] = formula, values, and selections are unchanged.
6
+
7
+ 1. ~ lib/Namo/Formulae.rb: + derive(name, row, namo, *arguments), the formula resolver —
8
+ invocation mechanics lifted whole from Row's formula branch (collection-scoped formulae
9
+ are called (row, namo, *arguments), row-only formulae (row)). + required_parameter_count(name),
10
+ public, a property of the stored callable. collection_scoped? and raise_unless_namo_context
11
+ move in as private; the latter takes namo as a parameter rather than closing over Row's
12
+ @namo. [] and []= are untouched — [] still returns the stored callable.
13
+ 2. ~ lib/Namo/Row.rb: #[] delegates formula resolution to
14
+ @formulae.derive(name, self, @namo, *arguments); Row keeps only the fail-fast argument
15
+ guard, the formula-vs-data dispatch (@formulae.key?), and the data fallback (@row[name]).
16
+ collection_scoped?, required_parameter_count, and raise_unless_namo_context are gone
17
+ (now Formulae's). expected_argument_counts sources the count from
18
+ @formulae.required_parameter_count and the fixed-vs-variadic check from
19
+ @formulae[name].arity.
20
+ 3. + test/Namo/Formulae_test.rb: + #derive tests (row-only to value, collection-scoped with
21
+ namo context, missing-namo-context raise, parameterised with arguments) and +
22
+ #required_parameter_count tests (row-only, collection-scoped, parameterised, variadic).
23
+ The #[] and #[]= blocks are unchanged — [] still returns the callable.
24
+ 4. ~ test/Namo/Row_test.rb: the formulae fixture is a Namo::Formulae, not a bare {} — Row
25
+ now delegates formula resolution to its collaborator (call, required_parameter_count),
26
+ which production always wraps (lib/namo.rb initialize). The data-only equality/match
27
+ tests keep their {} literals: they never resolve a formula, so Hash#key? alone answers.
28
+ 5. ~ Namo::VERSION: /0.21.0/0.22.0/
29
+
30
+ 20260625
31
+ 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.
32
+
33
+ 1. + lib/Namo/Formulae.rb: + Namo::Formulae, a thin class wrapping a Hash @store.
34
+ Responds to exactly the subset of Hash that lib/namo.rb and Row send the formula
35
+ collection: [], []=, keys, key?, empty?, each, delete, merge, reject, dup; includes
36
+ Enumerable (each yields [name, callable] pairs, giving map/select/etc. for free).
37
+ Three deliberate choices: to_h returns a copy (@store.dup) so callers cannot mutate
38
+ internals; ==/eql?/hash are names-only (keys.sort), matching Namo's own formula
39
+ equality where two sets with the same names but different proc objects are equivalent;
40
+ merge/reject/dup return a Formulae, not a Hash.
41
+ 2. ~ lib/namo.rb: + the require; initialize now normalises the incoming formulae to a
42
+ Formulae (Formulae.new(formulae) unless already one). This is the single behavioural
43
+ seam — every existing internal construction passes a Hash and is wrapped, while
44
+ operator results that now pass a Formulae (merge/dup return Formulae) pass through.
45
+ No other call site changed: each sends a message Formulae answers.
46
+ 3. ~ test/namo_test.rb: the two assertions comparing the public #formulae accessor to a
47
+ literal {} now assert emptiness (must_be_empty). The accessor returns a Formulae, not a
48
+ Hash; the formula set being empty is the unchanged behaviour these tests pin. + two
49
+ construction tests pinning the normalisation seam: a Hash given by keyword is wrapped in
50
+ a Formulae, and a Formulae given by keyword passes through unchanged (same object).
51
+ 4. + test/Namo/Formulae_test.rb: + unit test pinning the three deliberate choices (to_h
52
+ copy, names-only ==/eql?/hash, the Hash-compatible surface plus Enumerable map/select).
53
+ 5. ~ Namo::VERSION: /0.20.0/0.21.0/
54
+
4
55
  20260615
5
56
  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.
6
57
 
@@ -0,0 +1,96 @@
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 derive(name, row, namo, *arguments)
17
+ formula = self[name]
18
+ if collection_scoped?(name)
19
+ raise_unless_namo_context(name, namo)
20
+ formula.call(row, namo, *arguments)
21
+ else
22
+ formula.call(row)
23
+ end
24
+ end
25
+
26
+ def required_parameter_count(name)
27
+ formula = self[name]
28
+ formula.arity >= 0 ? formula.arity : -formula.arity - 1
29
+ end
30
+
31
+ def keys
32
+ @store.keys
33
+ end
34
+
35
+ def key?(name)
36
+ @store.key?(name)
37
+ end
38
+
39
+ def empty?
40
+ @store.empty?
41
+ end
42
+
43
+ def each(&block)
44
+ return enum_for(:each) unless block_given?
45
+ @store.each(&block)
46
+ end
47
+
48
+ def delete(name)
49
+ @store.delete(name)
50
+ end
51
+
52
+ def merge(other)
53
+ self.class.new(@store.merge(other.to_h))
54
+ end
55
+
56
+ def reject(&block)
57
+ self.class.new(@store.reject(&block))
58
+ end
59
+
60
+ def dup
61
+ self.class.new(@store.dup)
62
+ end
63
+
64
+ def to_h
65
+ @store.dup
66
+ end
67
+
68
+ def ==(other)
69
+ other.is_a?(Formulae) && keys.sort == other.keys.sort
70
+ end
71
+
72
+ def eql?(other)
73
+ self == other
74
+ end
75
+
76
+ def hash
77
+ keys.sort.hash
78
+ end
79
+
80
+ private
81
+
82
+ def initialize(store = {})
83
+ @store = store
84
+ end
85
+
86
+ def collection_scoped?(name)
87
+ required_parameter_count(name) >= 2
88
+ end
89
+
90
+ def raise_unless_namo_context(name, namo)
91
+ unless namo
92
+ raise ArgumentError, "collection-scoped formula #{name.inspect} requires a Namo context, but this Row has none"
93
+ end
94
+ end
95
+ end
96
+ end
data/lib/Namo/Row.rb CHANGED
@@ -6,13 +6,7 @@ class Namo
6
6
  def [](name, *arguments)
7
7
  raise_unless_expected_arguments(name, arguments)
8
8
  if @formulae.key?(name)
9
- formula = @formulae[name]
10
- if collection_scoped?(formula)
11
- raise_unless_namo_context(name)
12
- formula.call(self, @namo, *arguments)
13
- else
14
- formula.call(self)
15
- end
9
+ @formulae.derive(name, self, @namo, *arguments)
16
10
  else
17
11
  @row[name]
18
12
  end
@@ -57,19 +51,12 @@ class Namo
57
51
  @namo = namo
58
52
  end
59
53
 
60
- def collection_scoped?(formula)
61
- required_parameter_count(formula) >= 2
62
- end
63
-
64
- def required_parameter_count(formula)
65
- formula.arity >= 0 ? formula.arity : -formula.arity - 1
66
- end
67
-
68
54
  def expected_argument_counts(name)
69
- formula = @formulae[name]
70
- return [0, 0] unless formula && collection_scoped?(formula)
71
- minimum = required_parameter_count(formula) - 2
72
- maximum = formula.arity >= 0 ? minimum : nil
55
+ return [0, 0] unless @formulae.key?(name)
56
+ count = @formulae.required_parameter_count(name)
57
+ return [0, 0] unless count >= 2
58
+ minimum = count - 2
59
+ maximum = @formulae[name].arity >= 0 ? minimum : nil
73
60
  [minimum, maximum]
74
61
  end
75
62
 
@@ -79,11 +66,5 @@ class Namo
79
66
  expected = maximum.nil? ? "#{minimum}+" : minimum.to_s
80
67
  raise ArgumentError, "wrong number of arguments for #{name.inspect} (given #{arguments.length}, expected #{expected})"
81
68
  end
82
-
83
- def raise_unless_namo_context(name)
84
- unless @namo
85
- raise ArgumentError, "collection-scoped formula #{name.inspect} requires a Namo context, but this Row has none"
86
- end
87
- end
88
69
  end
89
70
  end
data/lib/Namo/VERSION.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.20.0'
5
+ VERSION = '0.22.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,211 @@
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 "#derive" do
30
+ let(:row) do
31
+ {price: 10.0, quantity: 100}
32
+ end
33
+
34
+ it "resolves a row-only formula to its value" do
35
+ _(formulae.derive(:revenue, row, nil)).must_equal 1000.0
36
+ end
37
+
38
+ it "resolves a collection-scoped formula with the namo context" do
39
+ namo = Object.new
40
+ formulae[:context] = ->(r, n){n}
41
+ _(formulae.derive(:context, row, namo)).must_be_same_as namo
42
+ end
43
+
44
+ it "raises ArgumentError naming the formula when a collection-scoped formula has no namo context" do
45
+ formulae[:context] = ->(r, n){n}
46
+ error = _(proc{formulae.derive(:context, row, nil)}).must_raise ArgumentError
47
+ _(error.message).must_match(/context/)
48
+ end
49
+
50
+ it "resolves a parameterised formula with arguments" do
51
+ formulae[:scaled] = ->(r, n, factor){r[:price] * factor}
52
+ _(formulae.derive(:scaled, row, Object.new, 3)).must_equal 30.0
53
+ end
54
+ end
55
+
56
+ describe "#required_parameter_count" do
57
+ it "counts a row-only formula" do
58
+ _(formulae.required_parameter_count(:revenue)).must_equal 1
59
+ end
60
+
61
+ it "counts a collection-scoped formula" do
62
+ formulae[:context] = ->(r, n){n}
63
+ _(formulae.required_parameter_count(:context)).must_equal 2
64
+ end
65
+
66
+ it "counts a parameterised formula" do
67
+ formulae[:scaled] = ->(r, n, factor){r[:price] * factor}
68
+ _(formulae.required_parameter_count(:scaled)).must_equal 3
69
+ end
70
+
71
+ it "excludes the splat when counting a variadic formula" do
72
+ formulae[:variadic] = ->(r, n, *rest){rest}
73
+ _(formulae.required_parameter_count(:variadic)).must_equal 2
74
+ end
75
+ end
76
+
77
+ describe "#[]=" do
78
+ it "stores a callable under a name" do
79
+ formulae[:cost] = cost
80
+ _(formulae[:cost]).must_be_same_as cost
81
+ end
82
+ end
83
+
84
+ describe "#keys" do
85
+ it "returns the stored names" do
86
+ _(formulae.keys).must_equal [:revenue]
87
+ end
88
+ end
89
+
90
+ describe "#key?" do
91
+ it "is true for a stored name" do
92
+ _(formulae.key?(:revenue)).must_equal true
93
+ end
94
+
95
+ it "is false for an absent name" do
96
+ _(formulae.key?(:missing)).must_equal false
97
+ end
98
+ end
99
+
100
+ describe "#empty?" do
101
+ it "is true with no formulae" do
102
+ _(Namo::Formulae.new).must_be_empty
103
+ end
104
+
105
+ it "is false with formulae" do
106
+ _(formulae.empty?).must_equal false
107
+ end
108
+ end
109
+
110
+ describe "#each" do
111
+ it "yields name/callable pairs" do
112
+ yielded = {}
113
+ formulae.each{|name, callable| yielded[name] = callable}
114
+ _(yielded).must_equal({revenue: revenue})
115
+ end
116
+
117
+ it "returns an enumerator with no block" do
118
+ _(formulae.each).must_be_kind_of Enumerator
119
+ end
120
+ end
121
+
122
+ describe "#delete" do
123
+ it "removes a name" do
124
+ formulae.delete(:revenue)
125
+ _(formulae.key?(:revenue)).must_equal false
126
+ end
127
+ end
128
+
129
+ describe "#merge" do
130
+ it "returns a Formulae combining both sets" do
131
+ merged = formulae.merge(Namo::Formulae.new({cost: cost}))
132
+ _(merged).must_be_kind_of Namo::Formulae
133
+ _(merged.keys.sort).must_equal [:cost, :revenue]
134
+ end
135
+
136
+ it "lets the argument win on a name conflict" do
137
+ other = proc{|r| 0}
138
+ merged = formulae.merge(Namo::Formulae.new({revenue: other}))
139
+ _(merged[:revenue]).must_be_same_as other
140
+ end
141
+
142
+ it "does not mutate the receiver" do
143
+ formulae.merge(Namo::Formulae.new({cost: cost}))
144
+ _(formulae.keys).must_equal [:revenue]
145
+ end
146
+ end
147
+
148
+ describe "#reject" do
149
+ it "returns a Formulae without the rejected names" do
150
+ formulae[:cost] = cost
151
+ rejected = formulae.reject{|name, _| name == :cost}
152
+ _(rejected).must_be_kind_of Namo::Formulae
153
+ _(rejected.keys).must_equal [:revenue]
154
+ end
155
+ end
156
+
157
+ describe "#dup" do
158
+ it "returns an independent Formulae" do
159
+ copy = formulae.dup
160
+ copy[:cost] = cost
161
+ _(formulae.key?(:cost)).must_equal false
162
+ end
163
+ end
164
+
165
+ describe "#to_h" do
166
+ it "returns a copy, not the live store" do
167
+ hash = formulae.to_h
168
+ hash[:cost] = cost
169
+ _(formulae.key?(:cost)).must_equal false
170
+ end
171
+ end
172
+
173
+ describe "Enumerable" do
174
+ it "supports map over name/callable pairs" do
175
+ _(formulae.map{|name, _| name}).must_equal [:revenue]
176
+ end
177
+
178
+ it "supports select returning name/callable pairs" do
179
+ formulae[:cost] = cost
180
+ _(formulae.select{|name, _| name == :cost}).must_equal [[:cost, cost]]
181
+ end
182
+ end
183
+
184
+ describe "value semantics" do
185
+ it "is == when names match, regardless of proc objects" do
186
+ _(Namo::Formulae.new({revenue: revenue})).must_equal Namo::Formulae.new({revenue: proc{|r| 0}})
187
+ end
188
+
189
+ it "is not == when names differ" do
190
+ _(formulae).wont_equal Namo::Formulae.new({cost: cost})
191
+ end
192
+
193
+ it "is not == to a bare Hash" do
194
+ _(formulae).wont_equal({revenue: revenue})
195
+ end
196
+
197
+ it "is eql? on matching names with differing procs" do
198
+ _(Namo::Formulae.new({revenue: revenue}).eql?(Namo::Formulae.new({revenue: proc{|r| 0}}))).must_equal true
199
+ end
200
+
201
+ it "hashes equal on matching names with differing procs" do
202
+ _(Namo::Formulae.new({revenue: revenue}).hash).must_equal Namo::Formulae.new({revenue: proc{|r| 0}}).hash
203
+ end
204
+ end
205
+
206
+ describe "constructor" do
207
+ it "defaults to an empty store" do
208
+ _(Namo::Formulae.new.keys).must_equal []
209
+ end
210
+ end
211
+ end
@@ -10,7 +10,7 @@ describe Namo::Row do
10
10
  end
11
11
 
12
12
  let(:formulae) do
13
- {}
13
+ Namo::Formulae.new
14
14
  end
15
15
 
16
16
  let(:row) do
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
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.20.0
4
+ version: 0.22.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: []