namo 0.17.0 → 0.18.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 +40 -0
- data/README.md +115 -0
- data/lib/Namo/Collection.rb +52 -0
- data/lib/Namo/VERSION.rb +1 -1
- data/lib/namo.rb +1 -0
- data/test/Namo/Collection_test.rb +250 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ed38a77d14075cbe3c28ff53097d539c2fefe4a91ff515e165e02c280311fd3
|
|
4
|
+
data.tar.gz: ba009835a59174f2e60327f3583753d2e6c2b0e81b06f23de4a15dd792981526
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7328696a6b4032561fa1b68c0eb4d3e7d3582ee4d5fc24926779112413867f1713100d9c21a3aa2f18c299e50802c866b5c95e874e569f4ef41f40e7b3924f58
|
|
7
|
+
data.tar.gz: 8028776be37f1f183ca2cefc3c9de7229d6918cf37d05b5f925675dd2ffd7d05603cc21ba53665f74094991b0c8d08f22479de2f50ee35e36ba772777bdbfb6e
|
data/CHANGELOG
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
CHANGELOG
|
|
2
2
|
_________
|
|
3
3
|
|
|
4
|
+
20260613
|
|
5
|
+
0.18.0: + Namo::Collection — hierarchical aggregate of named Namos with summary/detail views.
|
|
6
|
+
|
|
7
|
+
1. + lib/Namo/Collection.rb: Namo::Collection < Namo, the first family member beyond Namo
|
|
8
|
+
itself. Holds @members (an Array of named Namos, attr_reader :members) whose substance is
|
|
9
|
+
the collection; the inherited @data is a derived view rebuilt from the members. << accepts a
|
|
10
|
+
member or an array (via flatten), replaces by name (last-write-wins, unless member.name.nil?
|
|
11
|
+
so nil-named members always append), rebuilds @data = detail.data, and returns self. find(name)
|
|
12
|
+
returns the named member or nil (find(nil) is nil, and an unnamed member never matches),
|
|
13
|
+
shadowing Enumerable#find on Collections. summary(dimension, by: :member, reducer: :sum)
|
|
14
|
+
reduces each member to a {by => member.name, dimension => reduced} row; detail(by: :member)
|
|
15
|
+
unions the members' rows under the inject-iff-absent conditional
|
|
16
|
+
(row.key?(by) ? row : row.merge(by => member.name)); both return a fresh Namo (non-mutating).
|
|
17
|
+
as_summary(...) and as_detail(by = :member) set @data to the chosen view and return self
|
|
18
|
+
(mutating); the view persists until the next << re-materialises detail. Private initialize
|
|
19
|
+
sets @members = [] before super.
|
|
20
|
+
2. ~ lib/namo.rb: + require_relative './Namo/Collection'.
|
|
21
|
+
3. + test/Namo/Collection_test.rb: empty members; lazy detail materialisation and selection;
|
|
22
|
+
pure-live reflection of <<; <<' add/replace-by-name/array/unnamed/returns-self; find by name,
|
|
23
|
+
absent, nil, and unnamed; summary's labelled rows, custom by:, :mean reducer (local Array#mean),
|
|
24
|
+
and non-mutation; detail's plain-Namo return, inject-iff-absent both ways, and non-mutation;
|
|
25
|
+
live recomputation after <<; as_summary/as_detail mutate and return self and surface the
|
|
26
|
+
summary's columns in dimensions; as_* view persistence until the next <<; assembly round-trip
|
|
27
|
+
(as_detail(:assembly) injects and is retained, removed only by contraction); subclass default
|
|
28
|
+
by: via a Car < Namo::Collection overriding summary/detail.
|
|
29
|
+
4. ~ README.md: + Collections section after Named Namos — the GT-budget example (Car/SubAssembly,
|
|
30
|
+
<<, summary, detail), lazy detail materialisation, the four view methods and the
|
|
31
|
+
non-mutating/mutating split, the inject-iff-absent rule, << replace-by-name with use-site
|
|
32
|
+
handling of unnamed members, the rebuild-on-<< view lifetime, and forward-notes to group_by
|
|
33
|
+
(0.19.0) and freeze-gated memoisation (2.x).
|
|
34
|
+
5. ~ ROADMAP.md: Promote 0.18.0 to shipped, recording the resolved materialisation model
|
|
35
|
+
(rebuild-on-<<, as_* persists until the next <<, diverging from the drafted pure-live-per-access
|
|
36
|
+
/ transient wording) and the find/Enumerable#find shadowing; Current state -> 0.18.0; Summary
|
|
37
|
+
folds in Namo::Collection; next phase -> group_by (0.19.0). The upcoming 0.18.0 design block
|
|
38
|
+
folded into the shipped entry.
|
|
39
|
+
6. ~ COMPARISON.md: Aggregation entry repointed — Namo::Collection shipped (0.18.0), group_by
|
|
40
|
+
constructor still planned (0.19.0); persistent-named-collection retained as Namo's novelty
|
|
41
|
+
versus the other tools' transient group-by intermediates.
|
|
42
|
+
7. ~ Namo::VERSION: /0.17.0/0.18.0/
|
|
43
|
+
|
|
4
44
|
20260613
|
|
5
45
|
0.17.0: + parameterised formulae — formulae with required parameters beyond (row, namo) receive arguments at access time through Row#[].
|
|
6
46
|
|
data/README.md
CHANGED
|
@@ -925,6 +925,121 @@ end
|
|
|
925
925
|
|
|
926
926
|
`super` with no parentheses forwards every argument — positional and keyword — to `Namo#initialize` unchanged. The `return unless name` guard means a subclass need not override every operator to stop the result of `*` or `select` from re-running its construction side effects: it guards on `name` instead.
|
|
927
927
|
|
|
928
|
+
### Collections
|
|
929
|
+
|
|
930
|
+
`Namo::Collection` is a hierarchical aggregate — a Namo that holds an Array of named Namos (its `members`) and exposes summary and detail views across them. It is the first member of the Namo family beyond `Namo` itself.
|
|
931
|
+
|
|
932
|
+
The motivating case is a hierarchical budget. Each sub-assembly of a car (`powertrain`, `chassis`, `body`, ...) is a Namo with shared columns; the whole car is a `Collection` of those sub-assemblies, queryable both at summary level ("weight by assembly") and detail level ("every line item across all assemblies"):
|
|
933
|
+
|
|
934
|
+
```ruby
|
|
935
|
+
class Car < Namo::Collection
|
|
936
|
+
def summary(dimension, by: :assembly, reducer: :sum)
|
|
937
|
+
super
|
|
938
|
+
end
|
|
939
|
+
|
|
940
|
+
def detail(by: :assembly)
|
|
941
|
+
super
|
|
942
|
+
end
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
class SubAssembly < Namo; end
|
|
946
|
+
|
|
947
|
+
powertrain = SubAssembly.new(name: :powertrain, data: [
|
|
948
|
+
{component: 'engine', weight: 200, cost: 50000},
|
|
949
|
+
{component: 'gearbox', weight: 80, cost: 20000}
|
|
950
|
+
])
|
|
951
|
+
chassis = SubAssembly.new(name: :chassis, data: [{component: 'frame', weight: 150, cost: 30000}])
|
|
952
|
+
body = SubAssembly.new(name: :body, data: [{component: 'panels', weight: 60, cost: 15000}])
|
|
953
|
+
|
|
954
|
+
gt = Car.new
|
|
955
|
+
gt << [powertrain, chassis, body]
|
|
956
|
+
|
|
957
|
+
gt.summary(:weight).to_a
|
|
958
|
+
# => [
|
|
959
|
+
# {assembly: :powertrain, weight: 280},
|
|
960
|
+
# {assembly: :chassis, weight: 150},
|
|
961
|
+
# {assembly: :body, weight: 60}
|
|
962
|
+
# ]
|
|
963
|
+
|
|
964
|
+
gt.summary(:weight).values(:weight).sum # total weight by summing the assembly summaries
|
|
965
|
+
# => 490
|
|
966
|
+
|
|
967
|
+
gt.detail.values(:weight).sum # total weight by summing every line item
|
|
968
|
+
# => 490
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
`Car` overrides `summary`/`detail` only to set `by: :assembly` as the per-class default and then calls `super`. A bare `Namo::Collection.new` works equally well, defaulting `by:` to `:member` and taking it at the call site.
|
|
972
|
+
|
|
973
|
+
#### Lazy detail, behaving as its line items
|
|
974
|
+
|
|
975
|
+
A Collection's substance is its `members`; the inherited `@data` is a *derived view* of them. Any inherited row-operation — selection, projection, `each`, `values`, the set and composition operators — reads that view, so a Collection transparently behaves as its **detail** (the lossless union of its members' rows). Nothing has to be called first:
|
|
976
|
+
|
|
977
|
+
```ruby
|
|
978
|
+
gt.values(:weight)
|
|
979
|
+
# => [200, 80, 150, 60]
|
|
980
|
+
|
|
981
|
+
gt[component: 'engine'].values(:cost)
|
|
982
|
+
# => [50000]
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
Detail is the lazy view because a Collection's rows simply *are* its members' rows; a summary is a reduction you pose against them, so it is never reached by accident — only through `summary` or `as_summary`.
|
|
986
|
+
|
|
987
|
+
#### Four view methods
|
|
988
|
+
|
|
989
|
+
The views come in a non-mutating pair and a mutating pair:
|
|
990
|
+
|
|
991
|
+
- `summary(dimension, by:, reducer:)` and `detail(by:)` are **non-mutating** — each returns a fresh `Namo` derived from the members, leaving the Collection untouched. Use these when you want a view to keep: assign the result to a variable and operate on it independently.
|
|
992
|
+
- `as_summary(dimension, by:, reducer:)` and `as_detail(by)` are **mutating** — each sets the Collection's data to the chosen view and returns `self`, for a fluent step. (`as_detail` carries no `dimension`, so its label argument is positional: `as_detail(:assembly)`.)
|
|
993
|
+
|
|
994
|
+
```ruby
|
|
995
|
+
gt.summary(:cost, reducer: :mean) # a fresh Namo; gt is unchanged
|
|
996
|
+
gt.as_summary(:weight) # gt's data becomes the summary; returns gt
|
|
997
|
+
gt.as_detail(:assembly) # gt's data becomes the detail; returns gt
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
`reducer:` is any method the member's column responds to — `:sum` (the default) and `:mean` are typical (`:mean` via a statistics gem that adds `Array#mean`).
|
|
1001
|
+
|
|
1002
|
+
#### Inject-iff-absent
|
|
1003
|
+
|
|
1004
|
+
`detail(by:)` unions the members' rows and labels each with its origin, but only when that label isn't already present:
|
|
1005
|
+
|
|
1006
|
+
- If `by` is **already a dimension** in a member's rows, the row passes through untouched — the dimension is intrinsic.
|
|
1007
|
+
- If `by` is **not** present, `detail` injects it (`row.merge(by => member.name)`), promoting the member's name into a dimension.
|
|
1008
|
+
|
|
1009
|
+
This single conditional is where assembly (`<<`, members named extrinsically) and partition (`group_by`, members named by an intrinsic value — 0.19.0) meet. For an assembled Collection, `as_detail(:assembly)` is the dimension-creating step: it promotes the member name into real data and **retains** it. From then on the structure is intrinsic and round-trips are exact; the promoted dimension is removed only by explicit contraction (`gt[-:assembly]`), never automatically.
|
|
1010
|
+
|
|
1011
|
+
#### `<<` and unnamed members
|
|
1012
|
+
|
|
1013
|
+
`<<` accepts a single member or an array of them. A member whose `name` collides with an existing member's **replaces** it (last-write-wins), making the name → member mapping a dictionary rather than a multimap:
|
|
1014
|
+
|
|
1015
|
+
```ruby
|
|
1016
|
+
gt << SubAssembly.new(name: :powertrain, data: [...]) # replaces the existing :powertrain
|
|
1017
|
+
gt << [front_suspension, rear_suspension] # adds each
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
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
|
+
|
|
1022
|
+
```ruby
|
|
1023
|
+
gt.find(:chassis) # => the chassis SubAssembly
|
|
1024
|
+
gt.find(:missing) # => nil
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
(`find(name)` is member lookup; it shadows `Enumerable#find` on Collections. Predicate search over rows remains available as `detect`.)
|
|
1028
|
+
|
|
1029
|
+
#### View lifetime and liveness
|
|
1030
|
+
|
|
1031
|
+
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.
|
|
1032
|
+
|
|
1033
|
+
A mutating `as_summary`/`as_detail` view **persists until the next `<<`**, which re-materialises detail. So `as_summary` is for "be the summary for this immediate chain":
|
|
1034
|
+
|
|
1035
|
+
```ruby
|
|
1036
|
+
gt.as_summary(:weight).values(:weight) # => [280, 150, 60] (the summary)
|
|
1037
|
+
gt << front_suspension # re-materialises detail
|
|
1038
|
+
gt.values(:weight) # => [200, 80, 150, 60, ...] (line items again)
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
Freeze-gated memoisation is a 2.x optimisation — opt-in via `freeze`, transparent, and never changing this observable behaviour. `group_by` (0.19.0) is the partition-side constructor for the same type: it splits a Namo into a `Collection`, the mirror of assembling one with `<<`.
|
|
1042
|
+
|
|
928
1043
|
## Why?
|
|
929
1044
|
|
|
930
1045
|
Every other multi-dimensional array library requires you to pre-shape your data before you can work with it. Namo takes it in the form it likely already comes in.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Namo/Collection.rb
|
|
2
|
+
# Namo::Collection
|
|
3
|
+
|
|
4
|
+
class Namo
|
|
5
|
+
class Collection < Namo
|
|
6
|
+
attr_reader :members
|
|
7
|
+
|
|
8
|
+
def <<(*members)
|
|
9
|
+
members.flatten.each do |member|
|
|
10
|
+
@members.reject!{|existing| existing.name == member.name} unless member.name.nil?
|
|
11
|
+
@members << member
|
|
12
|
+
end
|
|
13
|
+
@data = detail.data
|
|
14
|
+
self
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def find(name)
|
|
18
|
+
@members.find{|member| member.name == name} unless name.nil?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def summary(dimension, by: :member, reducer: :sum)
|
|
22
|
+
rows = @members.map do |member|
|
|
23
|
+
{by => member.name, dimension => member.values(dimension).send(reducer)}
|
|
24
|
+
end
|
|
25
|
+
Namo.new(rows)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def detail(by: :member)
|
|
29
|
+
rows = @members.flat_map do |member|
|
|
30
|
+
member.data.map{|row| row.key?(by) ? row : row.merge(by => member.name)}
|
|
31
|
+
end
|
|
32
|
+
Namo.new(rows)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def as_summary(dimension, by: :member, reducer: :sum)
|
|
36
|
+
@data = summary(dimension, by: by, reducer: reducer).data
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def as_detail(by = :member)
|
|
41
|
+
@data = detail(by: by).data
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def initialize(positional_data = nil, data: [], formulae: {}, name: nil)
|
|
48
|
+
@members = []
|
|
49
|
+
super
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/Namo/VERSION.rb
CHANGED
data/lib/namo.rb
CHANGED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
require 'minitest/autorun'
|
|
2
|
+
require 'minitest-spec-context'
|
|
3
|
+
|
|
4
|
+
require_relative '../../lib/namo'
|
|
5
|
+
|
|
6
|
+
class Array
|
|
7
|
+
def mean
|
|
8
|
+
sum.to_f / size
|
|
9
|
+
end
|
|
10
|
+
end unless [].respond_to?(:mean)
|
|
11
|
+
|
|
12
|
+
class SubAssembly < Namo; end
|
|
13
|
+
|
|
14
|
+
class Car < Namo::Collection
|
|
15
|
+
def summary(dimension, by: :assembly, reducer: :sum)
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def detail(by: :assembly)
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe Namo::Collection do
|
|
25
|
+
let(:powertrain) do
|
|
26
|
+
SubAssembly.new(name: :powertrain, data: [
|
|
27
|
+
{component: 'engine', weight: 200, cost: 50000},
|
|
28
|
+
{component: 'gearbox', weight: 80, cost: 20000},
|
|
29
|
+
])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
let(:chassis) do
|
|
33
|
+
SubAssembly.new(name: :chassis, data: [{component: 'frame', weight: 150, cost: 30000}])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
let(:body) do
|
|
37
|
+
SubAssembly.new(name: :body, data: [{component: 'panels', weight: 60, cost: 15000}])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
let(:wheels) do
|
|
41
|
+
SubAssembly.new(name: :wheels, data: [{component: 'tyres', weight: 40, cost: 8000}])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
let(:collection) do
|
|
45
|
+
Namo::Collection.new.tap{|c| c << [powertrain, chassis, body]}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe "construction" do
|
|
49
|
+
it "starts with empty members" do
|
|
50
|
+
_(Namo::Collection.new.members).must_equal []
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe "lazy detail materialisation" do
|
|
55
|
+
it "materialises detail on a bare row-operation without a prior as_detail" do
|
|
56
|
+
_(collection.values(:weight)).must_equal [200, 80, 150, 60]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "supports selection against the materialised detail" do
|
|
60
|
+
_(collection[component: 'engine'].values(:component)).must_equal ['engine']
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "reflects a newly added member on the next operation" do
|
|
64
|
+
before = collection.values(:weight)
|
|
65
|
+
collection << wheels
|
|
66
|
+
_(collection.values(:weight)).must_equal before + [40]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe "#<<" do
|
|
71
|
+
it "adds a member" do
|
|
72
|
+
collection = Namo::Collection.new
|
|
73
|
+
collection << powertrain
|
|
74
|
+
_(collection.members.size).must_equal 1
|
|
75
|
+
_(collection.find(:powertrain)).must_be_same_as powertrain
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "replaces a member with a colliding name (last-write-wins)" do
|
|
79
|
+
collection = Namo::Collection.new
|
|
80
|
+
collection << powertrain
|
|
81
|
+
replacement = SubAssembly.new(name: :powertrain, data: [{component: 'hybrid', weight: 250, cost: 70000}])
|
|
82
|
+
collection << replacement
|
|
83
|
+
_(collection.members.size).must_equal 1
|
|
84
|
+
_(collection.find(:powertrain)).must_be_same_as replacement
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "adds each member from an array" do
|
|
88
|
+
collection = Namo::Collection.new
|
|
89
|
+
collection << [powertrain, chassis]
|
|
90
|
+
_(collection.members.size).must_equal 2
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "appends an unnamed member, which is unfindable by name" do
|
|
94
|
+
collection = Namo::Collection.new
|
|
95
|
+
collection << SubAssembly.new(data: [{component: 'misc', weight: 5, cost: 100}])
|
|
96
|
+
collection << SubAssembly.new(data: [{component: 'other', weight: 6, cost: 200}])
|
|
97
|
+
_(collection.members.size).must_equal 2
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "returns self" do
|
|
101
|
+
collection = Namo::Collection.new
|
|
102
|
+
_(collection << powertrain).must_be_same_as collection
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
describe "#find" do
|
|
107
|
+
it "returns the member with the given name" do
|
|
108
|
+
_(collection.find(:chassis)).must_be_same_as chassis
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "returns nil for an absent name" do
|
|
112
|
+
_(collection.find(:engine)).must_be_nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "returns nil for find(nil)" do
|
|
116
|
+
_(collection.find(nil)).must_be_nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "never matches an unnamed member" do
|
|
120
|
+
collection = Namo::Collection.new
|
|
121
|
+
collection << SubAssembly.new(data: [{component: 'misc', weight: 5, cost: 100}])
|
|
122
|
+
_(collection.find(nil)).must_be_nil
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
describe "#summary" do
|
|
127
|
+
it "reduces each member to a labelled row" do
|
|
128
|
+
summary = collection.summary(:weight)
|
|
129
|
+
_(summary.values(:member)).must_equal [:powertrain, :chassis, :body]
|
|
130
|
+
_(summary.values(:weight)).must_equal [280, 150, 60]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "labels with a custom by dimension" do
|
|
134
|
+
summary = collection.summary(:weight, by: :assembly)
|
|
135
|
+
_(summary.values(:assembly)).must_equal [:powertrain, :chassis, :body]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "reduces with a custom reducer" do
|
|
139
|
+
summary = collection.summary(:weight, reducer: :mean)
|
|
140
|
+
_(summary.values(:weight)).must_equal [140.0, 150.0, 60.0]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "is non-mutating — leaves the collection's data untouched" do
|
|
144
|
+
collection.summary(:weight)
|
|
145
|
+
_(collection.values(:weight)).must_equal [200, 80, 150, 60]
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
describe "#detail" do
|
|
150
|
+
it "returns a plain Namo" do
|
|
151
|
+
_(collection.detail).must_be_instance_of Namo
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it "unions the members' rows, injecting the by dimension when absent" do
|
|
155
|
+
detail = collection.detail
|
|
156
|
+
_(detail.values(:member)).must_equal [:powertrain, :powertrain, :chassis, :body]
|
|
157
|
+
_(detail.values(:weight)).must_equal [200, 80, 150, 60]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it "does not inject when the by dimension is already present" do
|
|
161
|
+
collection = Namo::Collection.new
|
|
162
|
+
collection << SubAssembly.new(name: :ignored, data: [{member: :preexisting, weight: 5}])
|
|
163
|
+
_(collection.detail.values(:member)).must_equal [:preexisting]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it "is non-mutating — leaves the collection's data untouched" do
|
|
167
|
+
collection.detail(by: :assembly)
|
|
168
|
+
_(collection.dimensions).wont_include :assembly
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
describe "live recomputation (no memoisation in 1.x)" do
|
|
173
|
+
it "reflects a mutation on the next detail call" do
|
|
174
|
+
before = collection.detail.values(:weight).size
|
|
175
|
+
collection << wheels
|
|
176
|
+
_(collection.detail.values(:weight).size).must_equal before + 1
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it "reflects a mutation on the next summary call" do
|
|
180
|
+
collection << wheels
|
|
181
|
+
_(collection.summary(:weight).values(:member)).must_equal [:powertrain, :chassis, :body, :wheels]
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
describe "#as_summary / #as_detail" do
|
|
186
|
+
it "as_summary sets the data to the summary view and returns self" do
|
|
187
|
+
result = collection.as_summary(:weight)
|
|
188
|
+
_(result).must_be_same_as collection
|
|
189
|
+
_(collection.values(:weight)).must_equal [280, 150, 60]
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it "as_detail sets the data to the detail view and returns self" do
|
|
193
|
+
collection.as_summary(:weight)
|
|
194
|
+
result = collection.as_detail
|
|
195
|
+
_(result).must_be_same_as collection
|
|
196
|
+
_(collection.values(:weight)).must_equal [200, 80, 150, 60]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it "exposes the summary's columns in dimensions immediately after as_summary" do
|
|
200
|
+
collection.as_summary(:weight)
|
|
201
|
+
_(collection.dimensions.sort).must_equal [:member, :weight].sort
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
describe "as_* view lifetime (rebuild-on-<<: persists until the next <<)" do
|
|
206
|
+
it "keeps the summary view across a bare row-operation" do
|
|
207
|
+
collection.as_summary(:weight)
|
|
208
|
+
_(collection.values(:weight)).must_equal [280, 150, 60]
|
|
209
|
+
_(collection[member: :powertrain].values(:weight)).must_equal [280]
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it "re-materialises detail on the next <<" do
|
|
213
|
+
collection.as_summary(:weight)
|
|
214
|
+
collection << wheels
|
|
215
|
+
_(collection.values(:weight)).must_equal [200, 80, 150, 60, 40]
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
describe "assembly round-trip" do
|
|
220
|
+
it "injects :assembly via as_detail and retains it through a later << and as_detail" do
|
|
221
|
+
collection.as_detail(:assembly)
|
|
222
|
+
_(collection.dimensions).must_include :assembly
|
|
223
|
+
_(collection.coordinates(:assembly)).must_equal [:powertrain, :chassis, :body]
|
|
224
|
+
collection << wheels
|
|
225
|
+
collection.as_detail(:assembly)
|
|
226
|
+
_(collection.dimensions).must_include :assembly
|
|
227
|
+
_(collection.coordinates(:assembly)).must_equal [:powertrain, :chassis, :body, :wheels]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
it "removes :assembly only by explicit contraction" do
|
|
231
|
+
collection.as_detail(:assembly)
|
|
232
|
+
contracted = collection[-:assembly]
|
|
233
|
+
_(contracted.dimensions).wont_include :assembly
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
describe "subclass with a default by:" do
|
|
238
|
+
let(:car) do
|
|
239
|
+
Car.new.tap{|c| c << [powertrain, chassis, body]}
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
it "uses the subclass default :assembly for summary" do
|
|
243
|
+
_(car.summary(:weight).values(:assembly)).must_equal [:powertrain, :chassis, :body]
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
it "uses the subclass default :assembly for detail" do
|
|
247
|
+
_(car.detail.values(:assembly)).must_equal [:powertrain, :powertrain, :chassis, :body]
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: namo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.18.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- thoran
|
|
@@ -64,6 +64,7 @@ files:
|
|
|
64
64
|
- LICENSE
|
|
65
65
|
- README.md
|
|
66
66
|
- Rakefile
|
|
67
|
+
- lib/Namo/Collection.rb
|
|
67
68
|
- lib/Namo/Enumerable.rb
|
|
68
69
|
- lib/Namo/NegatedDimension.rb
|
|
69
70
|
- lib/Namo/Row.rb
|
|
@@ -71,6 +72,7 @@ files:
|
|
|
71
72
|
- lib/Symbol.rb
|
|
72
73
|
- lib/namo.rb
|
|
73
74
|
- namo.gemspec
|
|
75
|
+
- test/Namo/Collection_test.rb
|
|
74
76
|
- test/Namo/NegatedDimension_test.rb
|
|
75
77
|
- test/Namo/Row_test.rb
|
|
76
78
|
- test/Symbol_test.rb
|