namo 0.22.0 → 0.24.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 +24 -0
- data/README.md +78 -3
- data/lib/Namo/Collection.rb +14 -4
- 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 +33 -2
- data/test/Namo/Collection_test.rb +42 -0
- data/test/Namo/Formulae_test.rb +142 -0
- data/test/namo_test.rb +295 -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: 9e1ecaf29846a7876a59dae0d82c6c9d4e1fddd84b0c6a101941214ed6544517
|
|
4
|
+
data.tar.gz: 4245b13c3485a504ab5c365803de282cb508579b2ca9aec0799df0f1ce653d68
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 41156dc96b98f59f8ff0dcf0d78f3b80ed895f402e11af9a53ba2e6cdb13d8bf39f1a38f48f2457bfe8bcf9082d146b73dafa4663e068816b2339d6888fd25af
|
|
7
|
+
data.tar.gz: e9c1f033e17fd2b10ffab984ea6349b7641de8abf0f04f23f85c893793242abd9e795847244855ad18b8c3f74b5876cce16dd212b2d1d2a6d327998657c9a4dc
|
data/CHANGELOG
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
CHANGELOG
|
|
2
2
|
_________
|
|
3
3
|
|
|
4
|
+
20260629
|
|
5
|
+
0.24.0: ~ Namo#<<, Namo::Collection#<< — widen from a formulary-only alias into a polymorphic "append a constituent" operator.
|
|
6
|
+
|
|
7
|
+
A base Namo's constituents are formularies and rows: << dispatches a Module to attach (0.23.0 behaviour, unchanged), a Hash to a data-row append, and a Row to a row append of its underlying hash. It does not take a bare callable (formula registration stays with []=) or a whole Namo (combining is +'s job) — both raise. Collection#<< is reconciled to the same verb over its own constituents: a Namo is a member, a Module attaches, and a loose Hash/Row raises (a Collection's rows are derived from its members, so a loose row has no durable home). This Hash/Row divergence — a base Namo appends the row, a Collection refuses it — is deliberate, not an oversight.
|
|
8
|
+
|
|
9
|
+
1. ~ lib/namo.rb: ~ Namo#<< — replace the alias_method :<<, :attach with a dispatching << that routes Module→attach, Row→add_row(row.to_h), Hash→add_row(row), else→TypeError. attach stays Module-only and unchanged, so the 0.23.0 collision-guard and formulary semantics carry over verbatim and a Module's << still reaches it. + private add_row(row), appending to @data and returning self (so << chains). A whole Namo falls to the else and raises (it is not a Module/Row/Hash) — combining is +. Row-append is a data mutation that does not pass through []=, so it does not trigger formula-eviction; a row carrying a formula-named key surfaces live through the formula on access, per existing precedence (not guarded — the exclusivity invariant is []='s).
|
|
10
|
+
2. ~ lib/Namo/Collection.rb: ~ Collection#<< — keep the splat + flatten (a member or an array of them), but dispatch each constituent: Namo→add_member, Module→attach (inherited from Namo — a Collection carries formulae over its detail view), Hash/Row→ArgumentError redirecting to member-add, else→TypeError. + private add_member(member), the prior reject-by-name + append body extracted verbatim. The @data = detail.data rebuild after the loop is preserved, so member-adds re-materialise detail as before and a Module-only << rebuilds harmlessly. Module routes to the inherited Namo#attach; member-add behaviour is byte-identical to 0.23.0 for the Namo/array cases.
|
|
11
|
+
3. ~ test/namo_test.rb: ~ the formularies "#<<" describe gains the row arms — a Hash appends as a data row (and returns the Namo), a Row appends its underlying hash, a whole Namo raises TypeError (+'s job), a bare callable raises TypeError ([]='s job), and a mixed chain interleaves rows and a formulary. The two 0.23.0 Module-arm tests are unchanged.
|
|
12
|
+
4. ~ test/Namo/Collection_test.rb: ~ the "#<<" describe gains a formulary attach whose row-scoped method resolves over the detail rows, a collection-scoped method (the namo_collection convention) reaching members, a loose Hash and a loose Row each raising ArgumentError with the member-redirect message, and a chain mixing member-adds with a formulary attach. The 0.23.0 member-add tests are unchanged.
|
|
13
|
+
5. ~ README.md: ~ the Formularies << note — << now appends a constituent (Module, Hash, or Row) to a base Namo, chains, and raises on a bare callable or a whole Namo; the bare-Hash-is-always-a-row and row-append-does-not-evict notes added. ~ the Collection "<< and unnamed members" section — Collection#<< dispatches member/Module/raise, with the deliberate Hash/Row divergence spelled out. + a "Collection formulae" subsection — the namo_collection convention (an ordinary two-arity formulary method reaching a collection view in its body), no marker and no second store.
|
|
14
|
+
6. ~ ROADMAP.md: + the 0.24.0 release entry; current-state bumped to 0.24.0; pivot/reshaping recorded as a decision (long-form aggregation is in-grain future work, widening-to-a-grid is presentational and belongs at an output edge, not the algebra), and collection formulae noted as needing no new mechanism.
|
|
15
|
+
7. ~ Namo::VERSION: /0.23.0/0.24.0/
|
|
16
|
+
|
|
17
|
+
20260627
|
|
18
|
+
0.23.0: + Namo::Formulary — A formulary is a reusable body of derived dimensions a Namo instance may draw upon.
|
|
19
|
+
|
|
20
|
+
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.
|
|
21
|
+
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.
|
|
22
|
+
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.
|
|
23
|
+
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.
|
|
24
|
+
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).
|
|
25
|
+
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.
|
|
26
|
+
7. ~ Namo::VERSION: /0.22.0/0.23.0/
|
|
27
|
+
|
|
4
28
|
20260626
|
|
5
29
|
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
30
|
|
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
|
+
`<<` appends a *constituent*. For a base Namo the constituents are formularies and rows: a **module** attaches (the operator form of `attach`, so `flows << OrderFlow` reads the same as `flows.attach(OrderFlow)`); a **Hash** appends as a data row; a **Row** (one drawn from another Namo) appends its underlying hash as a row. It returns the Namo, so the arms chain freely: `flows << OrderFlow << {date: 4, buys: 10, sells: 5} << Scoring`. Two things `<<` deliberately does not take: a bare callable (formula *registration* stays with `[]=`, which dispatches callable-vs-scalar and enforces data/formula exclusivity) and a whole Namo (combining collections is `+`'s job) — both raise. A bare Hash is therefore always a row, never a body of formulae. Appending a row is a data mutation that does not pass through `[]=`, so it does not trigger formula-eviction; a row carrying a formula-named key is your data, surfacing live through the formula on access per the usual precedence.
|
|
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]
|
|
@@ -1017,6 +1073,8 @@ gt << SubAssembly.new(name: :powertrain, data: [...]) # replaces the existing
|
|
|
1017
1073
|
gt << [front_suspension, rear_suspension] # adds each
|
|
1018
1074
|
```
|
|
1019
1075
|
|
|
1076
|
+
On a Collection `<<` dispatches to the *constituent* appropriate to a collection: a **member** (a Namo, or an array of them) is added as above; a **module** attaches a formulary (a Collection is a Namo, so it carries formulae over its detail view — see below); a loose **Hash or Row raises** an `ArgumentError` that redirects to member-add. This is the one place `Namo#<<` and `Collection#<<` deliberately diverge: a base Namo appends a loose row, a Collection refuses it — a Collection's rows are *derived* from its members, so a loose row has no durable home (the next member-add rebuilds the data view and would erase it). The honest response is to refuse and point at member-add. A whole Namo arriving at a Collection is unambiguously a member, never a row-merge, because `<<` has no whole-Namo arm (that is `+`).
|
|
1077
|
+
|
|
1020
1078
|
There is no insertion-time guard against unnamed members. An unnamed member is simply appended (no name to collide on) and is unfindable by `find` — the honest consequence of having no name, not an error. `find(name)` returns the member with that name, or `nil`:
|
|
1021
1079
|
|
|
1022
1080
|
```ruby
|
|
@@ -1026,6 +1084,23 @@ gt.find(:missing) # => nil
|
|
|
1026
1084
|
|
|
1027
1085
|
(`find(name)` is member lookup; it shadows `Enumerable#find` on Collections. Predicate search over rows remains available as `detect`.)
|
|
1028
1086
|
|
|
1087
|
+
#### Collection formulae
|
|
1088
|
+
|
|
1089
|
+
A formulary attached to a Collection resolves over its detail rows like any other — a Collection is a Namo, and its data view is the materialised detail. A *collection formula* needs no new mechanism: it is an ordinary two-arity formulary method whose `namo` argument is the Collection, reaching for a collection view (`summary`, `detail`, `members`) in its body. It resolves through the same `derive` path as every other formula; member-awareness lives in the method body, not in any marker or second store. By convention, name that argument `namo_collection` to signal it expects a Collection — a plain Namo lacking `members`/`summary` will `NoMethodError`, which is honest self-enforcement, no guard needed.
|
|
1090
|
+
|
|
1091
|
+
```ruby
|
|
1092
|
+
module FleetMetrics
|
|
1093
|
+
include Namo::Formulary
|
|
1094
|
+
|
|
1095
|
+
def member_count(row, namo_collection)
|
|
1096
|
+
namo_collection.members.size
|
|
1097
|
+
end
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
gt << FleetMetrics
|
|
1101
|
+
gt.values(:member_count) # => one entry per detail row, each the member count
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1029
1104
|
#### View lifetime and liveness
|
|
1030
1105
|
|
|
1031
1106
|
Materialisation is pure-live: the Collection rebuilds its data view from the current members on every `<<`, with no memoisation. So a mutation is reflected immediately — add a member, then summarise or detail, and the new member is included.
|
data/lib/Namo/Collection.rb
CHANGED
|
@@ -5,10 +5,15 @@ class Namo
|
|
|
5
5
|
class Collection < Namo
|
|
6
6
|
attr_reader :members
|
|
7
7
|
|
|
8
|
-
def <<(*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
def <<(*constituents)
|
|
9
|
+
constituents.flatten.each do |constituent|
|
|
10
|
+
case constituent
|
|
11
|
+
when Namo then add_member(constituent)
|
|
12
|
+
when Module then attach(constituent)
|
|
13
|
+
when Hash, Row
|
|
14
|
+
raise ArgumentError, "a Collection's rows come from its members; add a member (a named Namo), not a loose row"
|
|
15
|
+
else raise TypeError, "can't append #{constituent.class} to a Collection"
|
|
16
|
+
end
|
|
12
17
|
end
|
|
13
18
|
@data = detail.data
|
|
14
19
|
self
|
|
@@ -48,5 +53,10 @@ class Namo
|
|
|
48
53
|
@members = []
|
|
49
54
|
super
|
|
50
55
|
end
|
|
56
|
+
|
|
57
|
+
def add_member(member)
|
|
58
|
+
@members.reject!{|existing| existing.name == member.name} unless member.name.nil?
|
|
59
|
+
@members << member
|
|
60
|
+
end
|
|
51
61
|
end
|
|
52
62
|
end
|
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,24 @@ 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
|
+
def <<(constituent)
|
|
101
|
+
case constituent
|
|
102
|
+
when Module then attach(constituent)
|
|
103
|
+
when Row then add_row(constituent.to_h)
|
|
104
|
+
when Hash then add_row(constituent)
|
|
105
|
+
else raise TypeError, "can't append #{constituent.class} to a Namo; expected a Module (formulary), a Hash, or a Row"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
91
109
|
def +(other)
|
|
92
110
|
raise_unless_namo(other)
|
|
93
111
|
raise_unless_matching_data_dimensions(other)
|
|
@@ -238,6 +256,19 @@ class Namo
|
|
|
238
256
|
@data = positional_data || data
|
|
239
257
|
@formulae = formulae.is_a?(Formulae) ? formulae : Formulae.new(formulae)
|
|
240
258
|
@name = name
|
|
259
|
+
attach_included_formularies
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def add_row(row)
|
|
263
|
+
@data << row
|
|
264
|
+
self
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def attach_included_formularies
|
|
268
|
+
self.class.ancestors.reverse.each do |modul|
|
|
269
|
+
next if modul.is_a?(Class) || !modul.include?(Namo::Formulary)
|
|
270
|
+
attach(modul)
|
|
271
|
+
end
|
|
241
272
|
end
|
|
242
273
|
|
|
243
274
|
def values_for(dim)
|
|
@@ -45,6 +45,20 @@ describe Namo::Collection do
|
|
|
45
45
|
Namo::Collection.new.tap{|c| c << [powertrain, chassis, body]}
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
let(:component_pricing) do
|
|
49
|
+
Module.new do
|
|
50
|
+
include Namo::Formulary
|
|
51
|
+
def cost_per_kg(row); row[:cost] / row[:weight]; end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
let(:fleet_metrics) do
|
|
56
|
+
Module.new do
|
|
57
|
+
include Namo::Formulary
|
|
58
|
+
def member_count(row, namo_collection); namo_collection.members.size; end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
48
62
|
describe "construction" do
|
|
49
63
|
it "starts with empty members" do
|
|
50
64
|
_(Namo::Collection.new.members).must_equal []
|
|
@@ -101,6 +115,34 @@ describe Namo::Collection do
|
|
|
101
115
|
collection = Namo::Collection.new
|
|
102
116
|
_(collection << powertrain).must_be_same_as collection
|
|
103
117
|
end
|
|
118
|
+
|
|
119
|
+
it "attaches a formulary whose row-scoped method resolves over the detail rows" do
|
|
120
|
+
collection << component_pricing
|
|
121
|
+
_(collection.values(:cost_per_kg)).must_equal [250, 250, 200, 250]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "attaches a formulary whose collection-scoped method reaches members" do
|
|
125
|
+
collection << fleet_metrics
|
|
126
|
+
_(collection.values(:member_count)).must_equal [3, 3, 3, 3]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "raises an ArgumentError on a loose Hash, redirecting to member-add" do
|
|
130
|
+
error = _(proc{collection << {component: 'bolt', weight: 1, cost: 5}}).must_raise ArgumentError
|
|
131
|
+
_(error.message).must_match(/member/)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "raises an ArgumentError on a loose Row, redirecting to member-add" do
|
|
135
|
+
row = Namo.new([{component: 'bolt', weight: 1, cost: 5}]).entries.first
|
|
136
|
+
error = _(proc{collection << row}).must_raise ArgumentError
|
|
137
|
+
_(error.message).must_match(/member/)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it "chains member-adds and a formulary attach" do
|
|
141
|
+
collection = Namo::Collection.new
|
|
142
|
+
collection << powertrain << chassis << component_pricing
|
|
143
|
+
_(collection.members.size).must_equal 2
|
|
144
|
+
_(collection.values(:cost_per_kg)).must_equal [250, 250, 200]
|
|
145
|
+
end
|
|
104
146
|
end
|
|
105
147
|
|
|
106
148
|
describe "#find" do
|
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,283 @@ 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
|
+
|
|
762
|
+
it "appends a Hash as a data row" do
|
|
763
|
+
namo = Namo.new(flow_data)
|
|
764
|
+
namo << {date: 4, buys: 10, sells: 5}
|
|
765
|
+
_(namo.values(:date)).must_equal [1, 2, 3, 4]
|
|
766
|
+
_(namo.values(:buys)).must_equal [60, 80, 75, 10]
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
it "returns the Namo when appending a row" do
|
|
770
|
+
namo = Namo.new(flow_data)
|
|
771
|
+
_(namo << {date: 4, buys: 10, sells: 5}).must_be_same_as namo
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
it "appends a Row's underlying hash as a data row" do
|
|
775
|
+
namo = Namo.new(flow_data)
|
|
776
|
+
row = Namo.new([{date: 4, buys: 10, sells: 5}]).entries.first
|
|
777
|
+
namo << row
|
|
778
|
+
_(namo.values(:date)).must_equal [1, 2, 3, 4]
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
it "raises a TypeError on a whole Namo, which is +'s job" do
|
|
782
|
+
namo = Namo.new(flow_data)
|
|
783
|
+
_(proc{namo << Namo.new(flow_data)}).must_raise TypeError
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
it "raises a TypeError on a bare callable, which is []='s job" do
|
|
787
|
+
namo = Namo.new(flow_data)
|
|
788
|
+
_(proc{namo << ->(row){row[:buys]}}).must_raise TypeError
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
it "chains mixed arms: rows and a formulary" do
|
|
792
|
+
namo = Namo.new(flow_data)
|
|
793
|
+
namo << {date: 4, buys: 10, sells: 5} << delta << {date: 5, buys: 0, sells: 0}
|
|
794
|
+
_(namo.values(:date)).must_equal [1, 2, 3, 4, 5]
|
|
795
|
+
_(namo.values(:signed_volume)).must_equal [20, -40, 0, 5, 0]
|
|
796
|
+
end
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
describe "#derived_dimensions" do
|
|
800
|
+
it "lists the formulary's public methods alongside store keys, unique" do
|
|
801
|
+
_(flows.derived_dimensions.sort).must_equal [:cum_delta, :scaled_volume, :signed_volume]
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
it "does not surface a private helper" do
|
|
805
|
+
_(flows.derived_dimensions).wont_include :helper
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
it "is empty for a vanilla Namo — no Namo public API leaks as a dimension" do
|
|
809
|
+
_(Namo.new(flow_data).derived_dimensions).must_equal []
|
|
810
|
+
end
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
describe "resolution" do
|
|
814
|
+
it "resolves a row-scoped formulary method through values" do
|
|
815
|
+
_(flows.values(:signed_volume)).must_equal [20, -40, 0]
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
it "resolves a row-scoped formulary method through a Row" do
|
|
819
|
+
_(flows.entries.first[:signed_volume]).must_equal 20
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
it "selects on a formulary-defined dimension" do
|
|
823
|
+
_(flows[signed_volume: 20].values(:date)).must_equal [1]
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
it "windows a two-arity formulary method over the yielding Namo" do
|
|
827
|
+
_(flows.values(:cum_delta)).must_equal [20, -20, -20]
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
it "raises the collection-scoped-context error on a Row built without a Namo" do
|
|
831
|
+
row = Namo::Row.new(flow_data.first, flows.formulae)
|
|
832
|
+
_(proc{row[:cum_delta]}).must_raise ArgumentError
|
|
833
|
+
end
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
describe "parameterised formulary method" do
|
|
837
|
+
it "is omitted from no-arg bulk views" do
|
|
838
|
+
_(flows.values.keys).wont_include :scaled_volume
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
it "raises when named directly with no argument" do
|
|
842
|
+
_(proc{flows.values(:scaled_volume)}).must_raise ArgumentError
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
it "materialises through a row-scoped wrapper that supplies the argument" do
|
|
846
|
+
flows[:double_flow] = ->(r){r[:scaled_volume, 2]}
|
|
847
|
+
_(flows.values(:double_flow)).must_equal [200, 400, 300]
|
|
848
|
+
end
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
describe "live re-attachment" do
|
|
852
|
+
it "resolves to the most-recently attached formulary on a name collision" do
|
|
853
|
+
alt = Module.new do
|
|
854
|
+
include Namo::Formulary
|
|
855
|
+
def signed_volume(row); row[:buys]; end
|
|
856
|
+
end
|
|
857
|
+
namo = Namo.new(flow_data).attach(delta).attach(alt)
|
|
858
|
+
_(namo.values(:signed_volume)).must_equal [60, 80, 75]
|
|
859
|
+
end
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
describe "precedence" do
|
|
863
|
+
it "lets an instance []= formula shadow a formulary method of the same name" do
|
|
864
|
+
flows[:signed_volume] = ->(r){0}
|
|
865
|
+
_(flows.values(:signed_volume)).must_equal [0, 0, 0]
|
|
866
|
+
end
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
describe "carry-through" do
|
|
870
|
+
it "carries the formulary live through selection" do
|
|
871
|
+
_(flows.select{|row| row[:date] >= 2}.values(:signed_volume)).must_equal [-40, 0]
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
it "carries the formulary live through projection by selection" do
|
|
875
|
+
_(flows[date: 1..2].values(:signed_volume)).must_equal [20, -40]
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
it "carries the formulary through concatenation" do
|
|
879
|
+
_((flows + Namo.new(flow_data)).derived_dimensions).must_include :signed_volume
|
|
880
|
+
end
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
describe "materialising a formulary dimension by projection" do
|
|
884
|
+
it "lists the name once, as data, not derived" do
|
|
885
|
+
proj = flows[:date, :signed_volume]
|
|
886
|
+
_(proj.data_dimensions).must_include :signed_volume
|
|
887
|
+
_(proj.derived_dimensions).wont_include :signed_volume
|
|
888
|
+
_(proj.dimensions.count(:signed_volume)).must_equal 1
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
it "reads the snapshot, not a recomputation, on per-Row access" do
|
|
892
|
+
proj = flows[:date, :signed_volume]
|
|
893
|
+
_(proj.entries.first[:signed_volume]).must_equal 20
|
|
894
|
+
_(proj.values(:signed_volume)).must_equal [20, -40, 0]
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
it "raises when a formulary method collides with a data dimension" do
|
|
898
|
+
widened = Namo.new(flow_data.map{|row| row.merge(signed_volume: 999)})
|
|
899
|
+
error = _(proc{widened.attach(delta)}).must_raise ArgumentError
|
|
900
|
+
_(error.message).must_match(/signed_volume/)
|
|
901
|
+
end
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
describe "#===" do
|
|
905
|
+
it "is not === to a vanilla Namo of the same data dimensions" do
|
|
906
|
+
_(flows === Namo.new(flow_data)).must_equal false
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
it "is === to a Namo exposing the same names via []=" do
|
|
910
|
+
other = Namo.new(flow_data)
|
|
911
|
+
other[:signed_volume] = ->(r){0}
|
|
912
|
+
other[:cum_delta] = ->(r, n){0}
|
|
913
|
+
other[:scaled_volume] = ->(r, n, f){0}
|
|
914
|
+
_(flows === other).must_equal true
|
|
915
|
+
end
|
|
916
|
+
end
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
describe "formularies via class include" do
|
|
920
|
+
let(:flow_data) do
|
|
921
|
+
[{buys: 60, sells: 40}]
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
let(:indicators) do
|
|
925
|
+
Module.new do
|
|
926
|
+
include Namo::Formulary
|
|
927
|
+
def signed_volume(row); row[:buys] - row[:sells]; end
|
|
928
|
+
def label(row); "indicators"; end
|
|
929
|
+
private
|
|
930
|
+
def helper(row); row[:buys]; end
|
|
931
|
+
end
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
let(:scoring) do
|
|
935
|
+
Module.new do
|
|
936
|
+
include Namo::Formulary
|
|
937
|
+
def momentum(row); row[:buys] + row[:sells]; end
|
|
938
|
+
def label(row); "scoring"; end
|
|
939
|
+
end
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
let(:analysis_class) do
|
|
943
|
+
klass = Class.new(Namo)
|
|
944
|
+
klass.include(indicators)
|
|
945
|
+
klass.include(scoring)
|
|
946
|
+
klass
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
it "lists an included formulary's public methods as derived dimensions" do
|
|
950
|
+
_(analysis_class.new(flow_data).derived_dimensions.sort).must_equal [:label, :momentum, :signed_volume]
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
it "excludes a private helper" do
|
|
954
|
+
_(analysis_class.new(flow_data).derived_dimensions).wont_include :helper
|
|
955
|
+
end
|
|
956
|
+
|
|
957
|
+
it "resolves an included formulary method" do
|
|
958
|
+
_(analysis_class.new(flow_data).values(:signed_volume)).must_equal [20]
|
|
959
|
+
end
|
|
960
|
+
|
|
961
|
+
it "lets the most-recently included formulary win on a name collision" do
|
|
962
|
+
_(analysis_class.new(flow_data).values(:label)).must_equal ["scoring"]
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
it "raises at construction when an included formulary collides with a data dimension" do
|
|
966
|
+
clash = Module.new do
|
|
967
|
+
include Namo::Formulary
|
|
968
|
+
def buys(row); 0; end
|
|
969
|
+
end
|
|
970
|
+
klass = Class.new(Namo)
|
|
971
|
+
klass.include(clash)
|
|
972
|
+
_(proc{klass.new([{buys: 5}])}).must_raise ArgumentError
|
|
973
|
+
end
|
|
974
|
+
|
|
975
|
+
it "reaches an instance built before a later include only through attach" do
|
|
976
|
+
klass = Class.new(Namo)
|
|
977
|
+
instance = klass.new(flow_data)
|
|
978
|
+
klass.include(scoring)
|
|
979
|
+
_(instance.derived_dimensions).wont_include :momentum
|
|
980
|
+
_(klass.new(flow_data).derived_dimensions).must_include :momentum
|
|
981
|
+
instance.attach(scoring)
|
|
982
|
+
_(instance.derived_dimensions).must_include :momentum
|
|
983
|
+
end
|
|
984
|
+
end
|
|
985
|
+
|
|
691
986
|
describe "#[]= two-arity formulae" do
|
|
692
987
|
let(:price_data) do
|
|
693
988
|
[
|
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.24.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
|