namo 0.22.0 → 0.23.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 +11 -0
- data/README.md +59 -3
- data/lib/Namo/Formulae.rb +16 -0
- data/lib/Namo/Formulary.rb +6 -0
- data/lib/Namo/VERSION.rb +1 -1
- data/lib/namo.rb +21 -2
- data/test/Namo/Formulae_test.rb +142 -0
- data/test/namo_test.rb +259 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d07dec8cde3d61366ec26720be42067c0fa10deb6aa1fd37977bf69ba16aa802
|
|
4
|
+
data.tar.gz: c355e980ff842ddc90d6a3873e98bb7185fb788f44f610c18ace363059136e6f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 17ac2640764b18b17521af477f71b0e5419d24d68a35ce0a7fc79acd963f5e9210331b39ca04a242780037e1a39558bd8fd7079146331b13068f80f811da6fd9
|
|
7
|
+
data.tar.gz: b4ad7c11375029c45e9b00f2769368c8b48082a965105c95a9b4d23c86a81a24af15107695535a08ea78fa548fc733c84a6ac0df13635a089c4da1297cee1ed2
|
data/CHANGELOG
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
CHANGELOG
|
|
2
2
|
_________
|
|
3
3
|
|
|
4
|
+
20260627
|
|
5
|
+
0.23.0: + Namo::Formulary — A formulary is a reusable body of derived dimensions a Namo instance may draw upon.
|
|
6
|
+
|
|
7
|
+
1. + lib/Namo/Formulary.rb: + Namo::Formulary, an empty marker module. A module includes it to brand itself a formulary; attach raises ArgumentError for an untagged module, so an ordinary module of helpers can't be mistaken for a body of derivations.
|
|
8
|
+
2. ~ lib/Namo/Formulae.rb: + attach(modul), aliased as << — after the marker guard, extends a single memoised inert host (@host ||= Object.new) with the module and stores each of its public_instance_methods(false) as a Method bound to that host, then returns self. One host per Formulae across all attaches, so the bound Methods share a self-free receiver and a formulary method can't shadow Namo's API. No other change: derive, key?, required_parameter_count, keys, merge/dup/reject, and ==/eql?/hash are the 0.22.0 store-only methods, and they handle the formulary methods because the formulary methods are now store entries.
|
|
9
|
+
3. ~ lib/namo.rb: + require of Namo::Formulary; + Namo#attach(modul), aliased as << — raises ArgumentError naming the collision if a formulary method's name collides with a data column (intent/blast-radius: you didn't type the module's names, and it could clear several columns), else forwards to @formulae.attach and returns self. + the class-include channel: initialize calls a private attach_included_formularies that scans self.class.ancestors.reverse and routes each tagged module through attach (same guard, same host; reverse so most-recent-include wins, matching MRO). Collection#initialize calls super, so it participates harmlessly. Collection defines its own <<, so its member-add is unaffected and a plain Namo's << attaches a formulary. Separately and additively, []='s formula branch widens from when Proc to value.respond_to?(:call), so a directly-assigned Method or lambda registers as a formula rather than broadcasting per-row; arrays are unaffected. derived_dimensions, ===/eql?/hash, and requires_arguments? are unchanged from 0.22.0 — formulary methods are store keys, so the existing keys-based forms already count them.
|
|
10
|
+
4. + test/Namo/Formulae_test.rb: + a "formulary tier" describe — attach returns self, rejects an untagged module, and copies the public methods into the store (excluding a private helper); a single shared host across attaches (bound Methods share a receiver); << attaches as attach does; key?, required_parameter_count, and derive resolve the copied methods; most-recent-wins on re-attachment; carry-through through dup/reject/merge.
|
|
11
|
+
5. ~ test/namo_test.rb: + a "formularies" describe (runtime attach) — attach (returns the Namo, rejects untagged); derived_dimensions (copied names, private helper absent, vanilla Namo empty); resolution through values/Row/selection; two-arity windowing and the missing-namo raise; a parameterised method omitted from bulk and raising bare, materialising through a row-scoped wrapper; re-attachment; an instance []= formula shadowing a formulary method; attach raising on a formulary/data-name collision; carry-through through selection, projection, and concatenation; projection materialising-and-dropping a formulary name; === against a vanilla Namo and against the same names via []=; << attaching as attach does. + a "formularies via class include" describe — included methods as derived dimensions (private absent), resolution, most-recent-include wins (MRO), raise at construction on a data collision, and the build-time-vs-after contract (an instance built before a later include sees it only via attach). ~ the #[]= polymorphic dispatch describe gains a Method and a lambda registering as formulae (the array-broadcast regression already present).
|
|
12
|
+
6. ~ README.md: + a "Formularies" subsection under Formulae (attach and the marker, private-hides-helpers, the copy-into-the-store / snapshot model, the raise on a data collision, materialise-and-drop, carry-through, the bare-name/memoisation forward-note); the Polymorphic []= section now says "a callable" (proc, lambda, or Method) rather than "a proc". ~ COMPARISON.md: + a "Formularies" row. ~ ROADMAP.md: + the 0.23.0 release entry; current-state bumped to 0.23.0; the 2.x "module-based formula libraries", "Namo#=== revision", and marker-vs-implicit notes annotated as shipped/decided at 0.23.0, with bare-name bodies remaining 2.x.
|
|
13
|
+
7. ~ Namo::VERSION: /0.22.0/0.23.0/
|
|
14
|
+
|
|
4
15
|
20260626
|
|
5
16
|
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
17
|
|
data/README.md
CHANGED
|
@@ -716,9 +716,65 @@ prices[:sma_close_20] = proc{|row| row[:sma, :close, 20]}
|
|
|
716
716
|
prices[:date, :sma_close_20] # materialises per the usual projection rule
|
|
717
717
|
```
|
|
718
718
|
|
|
719
|
+
#### Formularies
|
|
720
|
+
|
|
721
|
+
A *formulary* is a reusable body of derived dimensions — a module whose public instance methods are formulae. A Namo attaches a formulary, and from then on those methods resolve as derived dimensions through the same interface as `[]=` formulae. The methods take `row` (and `namo`, and any further parameters) exactly as a `[]=` formula does:
|
|
722
|
+
|
|
723
|
+
```ruby
|
|
724
|
+
module OrderFlow
|
|
725
|
+
include Namo::Formulary
|
|
726
|
+
|
|
727
|
+
def signed_volume(row)
|
|
728
|
+
row[:buys] - row[:sells]
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def cum_delta(row, namo)
|
|
732
|
+
namo[date: ..row[:date]].values(:signed_volume).sum
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
private
|
|
736
|
+
|
|
737
|
+
def net(row)
|
|
738
|
+
row[:buys] + row[:sells]
|
|
739
|
+
end
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
flows = Namo.new([
|
|
743
|
+
{date: 1, buys: 60, sells: 40},
|
|
744
|
+
{date: 2, buys: 80, sells: 120},
|
|
745
|
+
{date: 3, buys: 75, sells: 75}
|
|
746
|
+
])
|
|
747
|
+
|
|
748
|
+
flows.attach(OrderFlow)
|
|
749
|
+
flows.values(:signed_volume) # => [20, -40, 0]
|
|
750
|
+
flows.values(:cum_delta) # => [20, -20, -20]
|
|
751
|
+
flows.derived_dimensions # => [:signed_volume, :cum_delta]
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
`<<` is the operator form of `attach`, so `flows << OrderFlow` reads the same and chains: `flows << OrderFlow << Scoring`.
|
|
755
|
+
|
|
756
|
+
A module brands itself a formulary by including `Namo::Formulary`. The marker is mandatory — `attach` raises `ArgumentError` for an untagged module, so an ordinary module of helpers can't be mistaken for a body of derivations. Within a formulary, the public methods are the derivations and `private` methods are helpers: `net` above never appears in `derived_dimensions` and never resolves as a dimension. The separation needs no per-method declaration — public or private says it.
|
|
757
|
+
|
|
758
|
+
Attaching **copies** the formulary's public methods into the Namo's formula collection — each becomes a stored formula, indistinguishable from one assigned through `[]=`. From there they are `[]=` formulae in everything but how they were defined: they resolve through the same path, carry through the same operators, and obey the same exclusivity rule.
|
|
759
|
+
|
|
760
|
+
The copy is a *snapshot* taken at the moment of attachment, and this is where a formulary departs from ordinary Ruby. A real `include` (or `extend`) is a live link: its methods resolve through the ancestor chain, so a method added to the module later, a redefinition, or a further module mixed in afterwards are all seen by objects that already include it. `attach` forgoes that — it copies the method set once, so later changes to the module are not reflected on an already-attached Namo. Re-attach to pick them up. (A live-link mechanism may come in a later release; it is not in 0.23.0.)
|
|
761
|
+
|
|
762
|
+
**Two ways to give a Namo a formulary.** `attach` (and `<<`) adds one to an individual Namo at runtime, as above. Alternatively, `include` the formulary into a `Namo` subclass — `class TradingAnalysis < Namo; include OrderFlow; end` — and every instance picks it up when it is built. The rule that separates them is simple: *the class `include` is honoured once, when each instance is constructed; everything after that is `attach`.* Both copy (snapshot), so a formulary `include`d into the class **after** an instance exists won't reach that instance — `attach` to it, or build a fresh one. A one-off plain `Namo` has no class to `include` into per-instance, so it always takes `attach`. Where both apply, ordinary last-write-wins holds: a runtime `attach` (or `[]=`) overrides a class-included formula of the same name on that instance, and the most-recently `include`d formulary wins among the class's own.
|
|
763
|
+
|
|
764
|
+
Because each method is copied into the store, the rules already governing `[]=` apply:
|
|
765
|
+
|
|
766
|
+
- **Most-recent wins.** Attaching a second formulary that defines a colliding name overwrites the first, exactly as a second `[]=` would. Equally, `[]=` and `attach` interleave as plain last-write-wins on a shared name — whichever was applied last governs.
|
|
767
|
+
- **Data collisions raise.** A name is data or derived, never both. If a formulary method's name collides with an existing data column, attaching raises an `ArgumentError` naming the collision rather than silently destroying the data. The reason it raises where `[]=` silently clears: `[]=` names one column at the call site, so replacing it is your explicit intent; a formulary names a whole *module*, so a data collision is a name you never typed — and possibly several columns at once — that you most likely didn't mean to destroy. The same raise guards the class-`include` channel, where it fires at construction. Resolve it explicitly first — contract the column away (`namo[-:signed_volume]`) or rename — then attach.
|
|
768
|
+
- **Materialise-and-drop on projection.** Naming a formulary method in a projection snapshots its values into a data column *and drops the formula*, exactly as for a `[]=` formula: `flows[:date, :signed_volume]` returns a Namo reporting `:signed_volume` as data, not derived, with all access paths agreeing on the stored snapshot.
|
|
769
|
+
- **Carry-through.** Selection, the set and composition operators, and a projection that *omits* the name all keep the formula live, computing against the result's own rows — the carry-through is free, because the formulae are ordinary store entries.
|
|
770
|
+
|
|
771
|
+
Two Namos that expose the same names — one via `[]=`, one via a formulary — are `===`; a Namo with a formulary attached is not `===` to a vanilla Namo of the same data dimensions.
|
|
772
|
+
|
|
773
|
+
Formulary methods take `row` explicitly and index it (`row[:buys]`), the same as `[]=` formulae. Resolving bare names inside a method body (`buys` for `row[:buys]`) and memoising derived values on a frozen Namo arrive in a later release; until then a formulary method is a plain function of its `row` (and `namo`).
|
|
774
|
+
|
|
719
775
|
### Polymorphic `[]=`
|
|
720
776
|
|
|
721
|
-
`[]=` dispatches on the
|
|
777
|
+
`[]=` dispatches on the value assigned. A callable — anything that responds to `call`, so a proc, a lambda, or a `Method` — registers a formula, as above. Anything else broadcasts the value to every row:
|
|
722
778
|
|
|
723
779
|
```ruby
|
|
724
780
|
sales[:status] = 'active'
|
|
@@ -732,7 +788,7 @@ sales.values(:revenue)
|
|
|
732
788
|
|
|
733
789
|
The two branches mirror the polymorphism `[]` already has on the selection side, where a single bracket call dispatches over exact values, arrays, ranges, procs, and regexes. Rather than introduce a separate `broadcast` or `set_all` method for the scalar case, `[]=` reads the same way for both: `sales[:status] = 'active'` says "set status to active across this Namo," and `sales[:revenue] = proc{…}` says "derive revenue from each row."
|
|
734
790
|
|
|
735
|
-
The two branches enforce **exclusive storage**: a name is either a data dimension or a derived dimension, never both. Assigning a
|
|
791
|
+
The two branches enforce **exclusive storage**: a name is either a data dimension or a derived dimension, never both. Assigning a callable clears any data column of that name; assigning anything else clears any formula of that name. The last write wins, and there is no shadowing:
|
|
736
792
|
|
|
737
793
|
```ruby
|
|
738
794
|
sales[:x] = 5 # :x is a broadcast data value
|
|
@@ -744,7 +800,7 @@ sales[:x] = 5 # :x is now a broadcast data value — the f
|
|
|
744
800
|
|
|
745
801
|
Exclusivity ties directly to the inspection vocabulary: a name assigned a scalar shows up in `data_dimensions`; a name assigned a proc shows up in `derived_dimensions`; never in both, so it appears in `dimensions` exactly once.
|
|
746
802
|
|
|
747
|
-
Only a
|
|
803
|
+
Only a callable takes the formula branch. An array doesn't respond to `call`, so it broadcasts as the per-row value rather than registering as a formula:
|
|
748
804
|
|
|
749
805
|
```ruby
|
|
750
806
|
sales[:weights] = [1, 2, 3]
|
data/lib/Namo/Formulae.rb
CHANGED
|
@@ -13,6 +13,18 @@ class Namo
|
|
|
13
13
|
@store[name] = callable
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
def attach(modul)
|
|
17
|
+
unless modul.include?(Namo::Formulary)
|
|
18
|
+
raise ArgumentError, "not a Namo::Formulary: #{modul}"
|
|
19
|
+
end
|
|
20
|
+
host.extend(modul)
|
|
21
|
+
modul.public_instance_methods(false).each do |name|
|
|
22
|
+
@store[name] = host.method(name)
|
|
23
|
+
end
|
|
24
|
+
self
|
|
25
|
+
end
|
|
26
|
+
alias_method :<<, :attach
|
|
27
|
+
|
|
16
28
|
def derive(name, row, namo, *arguments)
|
|
17
29
|
formula = self[name]
|
|
18
30
|
if collection_scoped?(name)
|
|
@@ -83,6 +95,10 @@ class Namo
|
|
|
83
95
|
@store = store
|
|
84
96
|
end
|
|
85
97
|
|
|
98
|
+
def host
|
|
99
|
+
@host ||= Object.new
|
|
100
|
+
end
|
|
101
|
+
|
|
86
102
|
def collection_scoped?(name)
|
|
87
103
|
required_parameter_count(name) >= 2
|
|
88
104
|
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/Formulary'
|
|
5
6
|
require_relative './Namo/Formulae'
|
|
6
7
|
require_relative './Namo/Row'
|
|
7
8
|
require_relative './Namo/Collection'
|
|
@@ -78,8 +79,7 @@ class Namo
|
|
|
78
79
|
end
|
|
79
80
|
|
|
80
81
|
def []=(name, value)
|
|
81
|
-
|
|
82
|
-
when Proc
|
|
82
|
+
if value.respond_to?(:call)
|
|
83
83
|
@data.each{|row| row.delete(name)} if @data.first&.key?(name)
|
|
84
84
|
@formulae[name] = value
|
|
85
85
|
else
|
|
@@ -88,6 +88,17 @@ class Namo
|
|
|
88
88
|
end
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
+
def attach(modul)
|
|
92
|
+
collisions = modul.public_instance_methods(false) & data_dimensions
|
|
93
|
+
unless collisions.empty?
|
|
94
|
+
raise ArgumentError, "formulary methods collide with data dimensions: #{collisions.inspect}"
|
|
95
|
+
end
|
|
96
|
+
@formulae.attach(modul)
|
|
97
|
+
self
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
alias_method :<<, :attach
|
|
101
|
+
|
|
91
102
|
def +(other)
|
|
92
103
|
raise_unless_namo(other)
|
|
93
104
|
raise_unless_matching_data_dimensions(other)
|
|
@@ -238,6 +249,14 @@ class Namo
|
|
|
238
249
|
@data = positional_data || data
|
|
239
250
|
@formulae = formulae.is_a?(Formulae) ? formulae : Formulae.new(formulae)
|
|
240
251
|
@name = name
|
|
252
|
+
attach_included_formularies
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def attach_included_formularies
|
|
256
|
+
self.class.ancestors.reverse.each do |modul|
|
|
257
|
+
next if modul.is_a?(Class) || !modul.include?(Namo::Formulary)
|
|
258
|
+
attach(modul)
|
|
259
|
+
end
|
|
241
260
|
end
|
|
242
261
|
|
|
243
262
|
def values_for(dim)
|
data/test/Namo/Formulae_test.rb
CHANGED
|
@@ -208,4 +208,146 @@ describe Namo::Formulae do
|
|
|
208
208
|
_(Namo::Formulae.new.keys).must_equal []
|
|
209
209
|
end
|
|
210
210
|
end
|
|
211
|
+
|
|
212
|
+
describe "formulary tier" do
|
|
213
|
+
let(:delta) do
|
|
214
|
+
Module.new do
|
|
215
|
+
include Namo::Formulary
|
|
216
|
+
def signed_volume(row); row[:buys] - row[:sells]; end
|
|
217
|
+
def echo_namo(row, namo); namo; end
|
|
218
|
+
def scaled(row, namo, factor); row[:buys] * factor; end
|
|
219
|
+
def windowed(row, namo, *rest); rest; end
|
|
220
|
+
private
|
|
221
|
+
def helper(row); row[:buys]; end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
let(:untagged) do
|
|
226
|
+
Module.new do
|
|
227
|
+
def signed_volume(row); 0; end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
let(:attached) do
|
|
232
|
+
Namo::Formulae.new.attach(delta)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
describe "#attach" do
|
|
236
|
+
it "returns self" do
|
|
237
|
+
formulae = Namo::Formulae.new
|
|
238
|
+
_(formulae.attach(delta)).must_be_same_as formulae
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
it "raises ArgumentError for a module not tagged Namo::Formulary" do
|
|
242
|
+
_(proc{Namo::Formulae.new.attach(untagged)}).must_raise ArgumentError
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it "copies the formulary's public methods into the callable store" do
|
|
246
|
+
_(attached.keys.sort).must_equal [:echo_namo, :scaled, :signed_volume, :windowed]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it "excludes a private helper" do
|
|
250
|
+
_(attached.keys).wont_include :helper
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it "binds every attached formulary to a single shared host" do
|
|
254
|
+
other = Module.new do
|
|
255
|
+
include Namo::Formulary
|
|
256
|
+
def momentum(row); row[:buys]; end
|
|
257
|
+
end
|
|
258
|
+
formulae = Namo::Formulae.new.attach(delta).attach(other)
|
|
259
|
+
_(formulae[:signed_volume].receiver).must_be_same_as formulae[:momentum].receiver
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
describe "#<<" do
|
|
264
|
+
it "attaches a formulary, the same as #attach" do
|
|
265
|
+
formulae = Namo::Formulae.new
|
|
266
|
+
formulae << delta
|
|
267
|
+
_(formulae.keys).must_include :signed_volume
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
it "returns self for chaining" do
|
|
271
|
+
formulae = Namo::Formulae.new
|
|
272
|
+
_(formulae << delta).must_be_same_as formulae
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
describe "#key?" do
|
|
277
|
+
it "is true for a formulary method name" do
|
|
278
|
+
_(attached.key?(:signed_volume)).must_equal true
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
it "is false for a private helper" do
|
|
282
|
+
_(attached.key?(:helper)).must_equal false
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
it "is false for an absent name" do
|
|
286
|
+
_(attached.key?(:missing)).must_equal false
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
describe "#required_parameter_count" do
|
|
291
|
+
it "counts a row-scoped formulary method" do
|
|
292
|
+
_(attached.required_parameter_count(:signed_volume)).must_equal 1
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
it "counts a two-arity formulary method" do
|
|
296
|
+
_(attached.required_parameter_count(:echo_namo)).must_equal 2
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
it "counts a parameterised formulary method" do
|
|
300
|
+
_(attached.required_parameter_count(:scaled)).must_equal 3
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
it "excludes the splat for a variadic formulary method" do
|
|
304
|
+
_(attached.required_parameter_count(:windowed)).must_equal 2
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
describe "#derive" do
|
|
309
|
+
it "resolves a row-scoped formulary method to its value" do
|
|
310
|
+
_(attached.derive(:signed_volume, {buys: 60, sells: 40}, nil)).must_equal 20
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
it "resolves a two-arity formulary method with the namo context" do
|
|
314
|
+
namo = Object.new
|
|
315
|
+
_(attached.derive(:echo_namo, {buys: 60, sells: 40}, namo)).must_be_same_as namo
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
it "raises when a two-arity formulary method has no namo context" do
|
|
319
|
+
error = _(proc{attached.derive(:echo_namo, {buys: 60, sells: 40}, nil)}).must_raise ArgumentError
|
|
320
|
+
_(error.message).must_match(/echo_namo/)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
it "resolves the most-recently attached formulary on a name collision" do
|
|
324
|
+
alt = Module.new do
|
|
325
|
+
include Namo::Formulary
|
|
326
|
+
def signed_volume(row); 999; end
|
|
327
|
+
end
|
|
328
|
+
formulae = Namo::Formulae.new.attach(delta).attach(alt)
|
|
329
|
+
_(formulae.derive(:signed_volume, {buys: 60, sells: 40}, nil)).must_equal 999
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
describe "carry-through" do
|
|
334
|
+
it "preserves attached formularies through #dup" do
|
|
335
|
+
_(attached.dup.keys).must_include :signed_volume
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
it "preserves attached formularies through #reject" do
|
|
339
|
+
_(attached.reject{|_name, _| false}.keys).must_include :signed_volume
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
it "carries both sides' formularies through #merge" do
|
|
343
|
+
other_delta = Module.new do
|
|
344
|
+
include Namo::Formulary
|
|
345
|
+
def momentum(row); row[:buys]; end
|
|
346
|
+
end
|
|
347
|
+
merged = attached.merge(Namo::Formulae.new.attach(other_delta))
|
|
348
|
+
_(merged.keys).must_include :momentum
|
|
349
|
+
_(merged.keys).must_include :signed_volume
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
211
353
|
end
|
data/test/namo_test.rb
CHANGED
|
@@ -639,6 +639,24 @@ describe Namo do
|
|
|
639
639
|
_(namo.values(:weights)).must_equal [[1, 2, 3], [1, 2, 3]]
|
|
640
640
|
end
|
|
641
641
|
|
|
642
|
+
it "registers a formula when assigned a Method (responds to call)" do
|
|
643
|
+
calculator = Object.new
|
|
644
|
+
def calculator.revenue(row); row[:price] * row[:quantity]; end
|
|
645
|
+
namo = Namo.new([{price: 10.0, quantity: 100}, {price: 5.0, quantity: 200}])
|
|
646
|
+
namo[:revenue] = calculator.method(:revenue)
|
|
647
|
+
_(namo.derived_dimensions).must_include :revenue
|
|
648
|
+
_(namo.data_dimensions).wont_include :revenue
|
|
649
|
+
_(namo.values(:revenue)).must_equal [1000.0, 1000.0]
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
it "registers a formula when assigned a lambda (responds to call)" do
|
|
653
|
+
namo = Namo.new([{x: 1}, {x: 2}])
|
|
654
|
+
namo[:doubled] = ->(r){r[:x] * 2}
|
|
655
|
+
_(namo.derived_dimensions).must_include :doubled
|
|
656
|
+
_(namo.data_dimensions).wont_include :doubled
|
|
657
|
+
_(namo.values(:doubled)).must_equal [2, 4]
|
|
658
|
+
end
|
|
659
|
+
|
|
642
660
|
it "is last-write-wins: scalar then proc leaves a formula only" do
|
|
643
661
|
namo = Namo.new([{y: 7}])
|
|
644
662
|
namo[:x] = 5
|
|
@@ -688,6 +706,247 @@ describe Namo do
|
|
|
688
706
|
end
|
|
689
707
|
end
|
|
690
708
|
|
|
709
|
+
describe "formularies" do
|
|
710
|
+
let(:flow_data) do
|
|
711
|
+
[
|
|
712
|
+
{date: 1, buys: 60, sells: 40},
|
|
713
|
+
{date: 2, buys: 80, sells: 120},
|
|
714
|
+
{date: 3, buys: 75, sells: 75},
|
|
715
|
+
]
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
let(:delta) do
|
|
719
|
+
Module.new do
|
|
720
|
+
include Namo::Formulary
|
|
721
|
+
def signed_volume(row); row[:buys] - row[:sells]; end
|
|
722
|
+
def cum_delta(row, namo); namo[date: ..row[:date]].values(:signed_volume).sum; end
|
|
723
|
+
def scaled_volume(row, namo, factor); (row[:buys] + row[:sells]) * factor; end
|
|
724
|
+
private
|
|
725
|
+
def helper(row); row[:buys]; end
|
|
726
|
+
end
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
let(:untagged) do
|
|
730
|
+
Module.new do
|
|
731
|
+
def signed_volume(row); 0; end
|
|
732
|
+
end
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
let(:flows) do
|
|
736
|
+
Namo.new(flow_data).attach(delta)
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
describe "#attach" do
|
|
740
|
+
it "returns the Namo" do
|
|
741
|
+
namo = Namo.new(flow_data)
|
|
742
|
+
_(namo.attach(delta)).must_be_same_as namo
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
it "raises ArgumentError for a module not tagged Namo::Formulary" do
|
|
746
|
+
_(proc{Namo.new(flow_data).attach(untagged)}).must_raise ArgumentError
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
describe "#<<" do
|
|
751
|
+
it "attaches a formulary, the same as #attach" do
|
|
752
|
+
namo = Namo.new(flow_data)
|
|
753
|
+
namo << delta
|
|
754
|
+
_(namo.values(:signed_volume)).must_equal [20, -40, 0]
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
it "returns the Namo for chaining" do
|
|
758
|
+
namo = Namo.new(flow_data)
|
|
759
|
+
_(namo << delta).must_be_same_as namo
|
|
760
|
+
end
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
describe "#derived_dimensions" do
|
|
764
|
+
it "lists the formulary's public methods alongside store keys, unique" do
|
|
765
|
+
_(flows.derived_dimensions.sort).must_equal [:cum_delta, :scaled_volume, :signed_volume]
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
it "does not surface a private helper" do
|
|
769
|
+
_(flows.derived_dimensions).wont_include :helper
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
it "is empty for a vanilla Namo — no Namo public API leaks as a dimension" do
|
|
773
|
+
_(Namo.new(flow_data).derived_dimensions).must_equal []
|
|
774
|
+
end
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
describe "resolution" do
|
|
778
|
+
it "resolves a row-scoped formulary method through values" do
|
|
779
|
+
_(flows.values(:signed_volume)).must_equal [20, -40, 0]
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
it "resolves a row-scoped formulary method through a Row" do
|
|
783
|
+
_(flows.entries.first[:signed_volume]).must_equal 20
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
it "selects on a formulary-defined dimension" do
|
|
787
|
+
_(flows[signed_volume: 20].values(:date)).must_equal [1]
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
it "windows a two-arity formulary method over the yielding Namo" do
|
|
791
|
+
_(flows.values(:cum_delta)).must_equal [20, -20, -20]
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
it "raises the collection-scoped-context error on a Row built without a Namo" do
|
|
795
|
+
row = Namo::Row.new(flow_data.first, flows.formulae)
|
|
796
|
+
_(proc{row[:cum_delta]}).must_raise ArgumentError
|
|
797
|
+
end
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
describe "parameterised formulary method" do
|
|
801
|
+
it "is omitted from no-arg bulk views" do
|
|
802
|
+
_(flows.values.keys).wont_include :scaled_volume
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
it "raises when named directly with no argument" do
|
|
806
|
+
_(proc{flows.values(:scaled_volume)}).must_raise ArgumentError
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
it "materialises through a row-scoped wrapper that supplies the argument" do
|
|
810
|
+
flows[:double_flow] = ->(r){r[:scaled_volume, 2]}
|
|
811
|
+
_(flows.values(:double_flow)).must_equal [200, 400, 300]
|
|
812
|
+
end
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
describe "live re-attachment" do
|
|
816
|
+
it "resolves to the most-recently attached formulary on a name collision" do
|
|
817
|
+
alt = Module.new do
|
|
818
|
+
include Namo::Formulary
|
|
819
|
+
def signed_volume(row); row[:buys]; end
|
|
820
|
+
end
|
|
821
|
+
namo = Namo.new(flow_data).attach(delta).attach(alt)
|
|
822
|
+
_(namo.values(:signed_volume)).must_equal [60, 80, 75]
|
|
823
|
+
end
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
describe "precedence" do
|
|
827
|
+
it "lets an instance []= formula shadow a formulary method of the same name" do
|
|
828
|
+
flows[:signed_volume] = ->(r){0}
|
|
829
|
+
_(flows.values(:signed_volume)).must_equal [0, 0, 0]
|
|
830
|
+
end
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
describe "carry-through" do
|
|
834
|
+
it "carries the formulary live through selection" do
|
|
835
|
+
_(flows.select{|row| row[:date] >= 2}.values(:signed_volume)).must_equal [-40, 0]
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
it "carries the formulary live through projection by selection" do
|
|
839
|
+
_(flows[date: 1..2].values(:signed_volume)).must_equal [20, -40]
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
it "carries the formulary through concatenation" do
|
|
843
|
+
_((flows + Namo.new(flow_data)).derived_dimensions).must_include :signed_volume
|
|
844
|
+
end
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
describe "materialising a formulary dimension by projection" do
|
|
848
|
+
it "lists the name once, as data, not derived" do
|
|
849
|
+
proj = flows[:date, :signed_volume]
|
|
850
|
+
_(proj.data_dimensions).must_include :signed_volume
|
|
851
|
+
_(proj.derived_dimensions).wont_include :signed_volume
|
|
852
|
+
_(proj.dimensions.count(:signed_volume)).must_equal 1
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
it "reads the snapshot, not a recomputation, on per-Row access" do
|
|
856
|
+
proj = flows[:date, :signed_volume]
|
|
857
|
+
_(proj.entries.first[:signed_volume]).must_equal 20
|
|
858
|
+
_(proj.values(:signed_volume)).must_equal [20, -40, 0]
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
it "raises when a formulary method collides with a data dimension" do
|
|
862
|
+
widened = Namo.new(flow_data.map{|row| row.merge(signed_volume: 999)})
|
|
863
|
+
error = _(proc{widened.attach(delta)}).must_raise ArgumentError
|
|
864
|
+
_(error.message).must_match(/signed_volume/)
|
|
865
|
+
end
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
describe "#===" do
|
|
869
|
+
it "is not === to a vanilla Namo of the same data dimensions" do
|
|
870
|
+
_(flows === Namo.new(flow_data)).must_equal false
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
it "is === to a Namo exposing the same names via []=" do
|
|
874
|
+
other = Namo.new(flow_data)
|
|
875
|
+
other[:signed_volume] = ->(r){0}
|
|
876
|
+
other[:cum_delta] = ->(r, n){0}
|
|
877
|
+
other[:scaled_volume] = ->(r, n, f){0}
|
|
878
|
+
_(flows === other).must_equal true
|
|
879
|
+
end
|
|
880
|
+
end
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
describe "formularies via class include" do
|
|
884
|
+
let(:flow_data) do
|
|
885
|
+
[{buys: 60, sells: 40}]
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
let(:indicators) do
|
|
889
|
+
Module.new do
|
|
890
|
+
include Namo::Formulary
|
|
891
|
+
def signed_volume(row); row[:buys] - row[:sells]; end
|
|
892
|
+
def label(row); "indicators"; end
|
|
893
|
+
private
|
|
894
|
+
def helper(row); row[:buys]; end
|
|
895
|
+
end
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
let(:scoring) do
|
|
899
|
+
Module.new do
|
|
900
|
+
include Namo::Formulary
|
|
901
|
+
def momentum(row); row[:buys] + row[:sells]; end
|
|
902
|
+
def label(row); "scoring"; end
|
|
903
|
+
end
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
let(:analysis_class) do
|
|
907
|
+
klass = Class.new(Namo)
|
|
908
|
+
klass.include(indicators)
|
|
909
|
+
klass.include(scoring)
|
|
910
|
+
klass
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
it "lists an included formulary's public methods as derived dimensions" do
|
|
914
|
+
_(analysis_class.new(flow_data).derived_dimensions.sort).must_equal [:label, :momentum, :signed_volume]
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
it "excludes a private helper" do
|
|
918
|
+
_(analysis_class.new(flow_data).derived_dimensions).wont_include :helper
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
it "resolves an included formulary method" do
|
|
922
|
+
_(analysis_class.new(flow_data).values(:signed_volume)).must_equal [20]
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
it "lets the most-recently included formulary win on a name collision" do
|
|
926
|
+
_(analysis_class.new(flow_data).values(:label)).must_equal ["scoring"]
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
it "raises at construction when an included formulary collides with a data dimension" do
|
|
930
|
+
clash = Module.new do
|
|
931
|
+
include Namo::Formulary
|
|
932
|
+
def buys(row); 0; end
|
|
933
|
+
end
|
|
934
|
+
klass = Class.new(Namo)
|
|
935
|
+
klass.include(clash)
|
|
936
|
+
_(proc{klass.new([{buys: 5}])}).must_raise ArgumentError
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
it "reaches an instance built before a later include only through attach" do
|
|
940
|
+
klass = Class.new(Namo)
|
|
941
|
+
instance = klass.new(flow_data)
|
|
942
|
+
klass.include(scoring)
|
|
943
|
+
_(instance.derived_dimensions).wont_include :momentum
|
|
944
|
+
_(klass.new(flow_data).derived_dimensions).must_include :momentum
|
|
945
|
+
instance.attach(scoring)
|
|
946
|
+
_(instance.derived_dimensions).must_include :momentum
|
|
947
|
+
end
|
|
948
|
+
end
|
|
949
|
+
|
|
691
950
|
describe "#[]= two-arity formulae" do
|
|
692
951
|
let(:price_data) do
|
|
693
952
|
[
|
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.23.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- thoran
|
|
@@ -67,6 +67,7 @@ files:
|
|
|
67
67
|
- lib/Namo/Collection.rb
|
|
68
68
|
- lib/Namo/Enumerable.rb
|
|
69
69
|
- lib/Namo/Formulae.rb
|
|
70
|
+
- lib/Namo/Formulary.rb
|
|
70
71
|
- lib/Namo/NegatedDimension.rb
|
|
71
72
|
- lib/Namo/Row.rb
|
|
72
73
|
- lib/Namo/VERSION.rb
|