namo 0.27.0 → 0.29.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 +28 -0
- data/README.md +2 -2
- data/lib/Namo/Collection.rb +5 -3
- data/lib/Namo/VERSION.rb +1 -1
- data/lib/namo.rb +25 -11
- data/test/Namo/Collection_test.rb +14 -0
- data/test/namo_test.rb +38 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5e56f3159063720b0e933e9c346ddad824831a51dac7a576da6d0cf841fd28ea
|
|
4
|
+
data.tar.gz: 8aee98bb89f8c7ab978c0ce459cc90c8259123123c13f5158a3e16dbb93922c7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16e3d16921ee88ae7d5d76c79227efb035128ac46f17d93fa61c7445434bb1c475c597ff7e2478fe19bbaf8f53b73ed366fee5ed1e514ad99a423a030db0d46c
|
|
7
|
+
data.tar.gz: 7e0045e6e6a842be576e171c9c31bde901f7ade581e5dce694e853a868f832c358e5eb7329ce9fc14efdc13a20663f4877135895f99328d646bb8717d177a2af
|
data/CHANGELOG
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 20260715
|
|
4
|
+
|
|
5
|
+
0.29.0: Per access indexed materialisation.
|
|
6
|
+
|
|
7
|
+
Materialise each column once per top-level access, collapsing an O(n²) parent-scanning formula to O(n), with no value retained across accesses.
|
|
8
|
+
|
|
9
|
+
1. ~ Namo#values: wrap the body in a private materialising scope.
|
|
10
|
+
2. ~ Namo#values_for: memoise its column build into @materialised[dim] ||= (the data/derived branch that was its body).
|
|
11
|
+
3. + Namo#materialising (a per-access scope that sets @materialised, yields, and clears it in an ensure; a nested values call reuses the open scope rather than starting another). A formula that scans its parent asks for the same column once per row it visits; the scope materialises that column once and reuses it for the rest of the access, then discards it — so every access still recomputes from current state and nothing can go stale. Enumerable is untouched: iteration paths (each/select/map/group_by) do not yet open the scope.
|
|
12
|
+
4. ~ test/namo_test.rb: + an "access-scoped materialisation" describe — a referenced column materialises once per access, not once per referencing row; nothing is retained across accesses (a later access recomputes from current state); the scope clears on a raising materialisation, leaving later accesses working.
|
|
13
|
+
5. ~ README.md: ~ the cross-row liveness note — "with no caching" becomes "with no caching across accesses", the within-access materialisation being a loop-invariant hoist, not a cache.
|
|
14
|
+
6. ~ ROADMAP.md: + the 0.29.0 entry; current-state bumped to 0.29.0; the 1.x pure-live passages refined from "no memoisation anywhere" to the invariant "no derived value outlives the access that produced it"; the Live-data-and-cache-invalidation open question gains the access-scoped resolution; + forward notes on streaming performance vs incremental maintenance (a commercial-tier candidate) and on formula opacity as the shared wall behind ===, dependency-aware projection, serialisation, and incremental streaming.
|
|
15
|
+
7. ~ docs/: regenerate the print-ready PDFs.
|
|
16
|
+
8. ~ Namo::VERSION: /0.28.0/0.29.0/
|
|
17
|
+
|
|
18
|
+
## 20260713
|
|
19
|
+
|
|
20
|
+
0.28.0: ~ Namo::Collection#detail/#as_detail: Accept the by label positionally or by keyword.
|
|
21
|
+
|
|
22
|
+
Accept the by label positionally or by keyword (positional winning), per the constructor precedent — resolving the detail/as_detail calling-convention asymmetry.
|
|
23
|
+
|
|
24
|
+
1. ~ Namo::Collection#detail(positional_by = nil, by: :member), #as_detail(positional_by = nil, by: :member): resolve by = positional_by || by, mirroring the constructor's @data = positional_data || data; positional wins. summary/as_summary unchanged — dimension stays their positional, by: the keyword modifier.
|
|
25
|
+
2. ~ test/Namo/Collection_test.rb: + detail and as_detail each take the by label positionally or by keyword, positional winning over a conflicting keyword.
|
|
26
|
+
3. ~ README.md: ~ the view-methods note — detail/as_detail take the label positionally or by keyword (the constructor's shape); replaces the "carries no dimension, so positional" rationale.
|
|
27
|
+
4. ~ ROADMAP.md: + the 0.28.0 entry (twin asymmetry, constructor precedent, primary-dual/modifier-keyword scope); current-state bumped to 0.28.0; a forward-reference on the 0.18.0 note superseding its positional rationale.
|
|
28
|
+
5. ~ docs/: regenerate the print-ready PDFs.
|
|
29
|
+
6. ~ Namo::VERSION: /0.27.0/0.28.0/
|
|
30
|
+
|
|
3
31
|
## 20260710
|
|
4
32
|
|
|
5
33
|
0.27.0: ~ Namo::Collection#summary/#as_summary: + an optional block for a per-member reduction beyond a single named reducer.
|
data/README.md
CHANGED
|
@@ -688,7 +688,7 @@ prices.values(:sma)
|
|
|
688
688
|
# => [10.0, 15.0, 20.0]
|
|
689
689
|
```
|
|
690
690
|
|
|
691
|
-
`namo` is the Namo that yielded the row, live — so the window always reflects the current state of the object you ask through. A filtered Namo's rows window over the filtered rows; an operator result's rows window over the result. Appending a row changes every cross-row value on the next access, with no caching.
|
|
691
|
+
`namo` is the Namo that yielded the row, live — so the window always reflects the current state of the object you ask through. A filtered Namo's rows window over the filtered rows; an operator result's rows window over the result. Appending a row changes every cross-row value on the next access, with no caching across accesses.
|
|
692
692
|
|
|
693
693
|
One-arity formulae are unchanged, and the two forms mix freely — a one-arity formula can reference a two-arity one, and a two-arity formula can reference a one-arity one, by name.
|
|
694
694
|
|
|
@@ -1075,7 +1075,7 @@ Detail is the lazy view because a Collection's rows simply *are* its members' ro
|
|
|
1075
1075
|
The views come in a non-mutating pair and a mutating pair:
|
|
1076
1076
|
|
|
1077
1077
|
- `summary(dimension = nil, by:, reducer:, &block)` 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.
|
|
1078
|
-
- `as_summary(dimension = nil, by:, reducer:, &block)` 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`
|
|
1078
|
+
- `as_summary(dimension = nil, by:, reducer:, &block)` and `as_detail(by = :member)` are **mutating** — each sets the Collection's data to the chosen view and returns `self`, for a fluent step. (`detail` and `as_detail` take the origin label either positionally or by the `by:` keyword, positional winning — the same shape the constructor gives `data` — so `as_detail(:assembly)` and `as_detail(by: :assembly)` are one call. `summary`/`as_summary` keep `by:` a keyword beside their `dimension` positional.)
|
|
1079
1079
|
|
|
1080
1080
|
```ruby
|
|
1081
1081
|
gt.summary(:cost, reducer: :mean) # a fresh Namo; gt is unchanged
|
data/lib/Namo/Collection.rb
CHANGED
|
@@ -34,7 +34,8 @@ class Namo
|
|
|
34
34
|
Namo.new(rows)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
def detail(by: :member)
|
|
37
|
+
def detail(positional_by = nil, by: :member)
|
|
38
|
+
by = positional_by || by
|
|
38
39
|
rows = @members.flat_map do |member|
|
|
39
40
|
member.data.map{|row| row.key?(by) ? row : row.merge(by => member.name)}
|
|
40
41
|
end
|
|
@@ -46,8 +47,9 @@ class Namo
|
|
|
46
47
|
self
|
|
47
48
|
end
|
|
48
49
|
|
|
49
|
-
def as_detail(
|
|
50
|
-
|
|
50
|
+
def as_detail(positional_by = nil, by: :member)
|
|
51
|
+
by = positional_by || by
|
|
52
|
+
@data = detail(by).data
|
|
51
53
|
self
|
|
52
54
|
end
|
|
53
55
|
|
data/lib/Namo/VERSION.rb
CHANGED
data/lib/namo.rb
CHANGED
|
@@ -30,12 +30,14 @@ class Namo
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def values(*dims)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
materialising do
|
|
34
|
+
if dims.empty?
|
|
35
|
+
materialisable_dimensions.each_with_object({}){|dim, hash| hash[dim] = values_for(dim)}
|
|
36
|
+
elsif dims.length == 1
|
|
37
|
+
values_for(dims.first)
|
|
38
|
+
else
|
|
39
|
+
dims.each_with_object({}){|dim, hash| hash[dim] = values_for(dim)}
|
|
40
|
+
end
|
|
39
41
|
end
|
|
40
42
|
end
|
|
41
43
|
|
|
@@ -290,14 +292,26 @@ class Namo
|
|
|
290
292
|
end
|
|
291
293
|
end
|
|
292
294
|
|
|
293
|
-
def
|
|
294
|
-
if
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
def materialising
|
|
296
|
+
return yield if @materialised
|
|
297
|
+
@materialised = {}
|
|
298
|
+
begin
|
|
299
|
+
yield
|
|
300
|
+
ensure
|
|
301
|
+
@materialised = nil
|
|
298
302
|
end
|
|
299
303
|
end
|
|
300
304
|
|
|
305
|
+
def values_for(dim)
|
|
306
|
+
@materialised[dim] ||= (
|
|
307
|
+
if data_dimensions.include?(dim)
|
|
308
|
+
@data.map{|row_data| row_data[dim]}
|
|
309
|
+
else
|
|
310
|
+
@data.map{|row_data| Row.new(row_data, @formulae, self)[dim]}
|
|
311
|
+
end
|
|
312
|
+
)
|
|
313
|
+
end
|
|
314
|
+
|
|
301
315
|
def materialisable_dimensions
|
|
302
316
|
dimensions.reject{|dim| requires_arguments?(dim)}
|
|
303
317
|
end
|
|
@@ -245,6 +245,12 @@ describe Namo::Collection do
|
|
|
245
245
|
collection.detail(by: :assembly)
|
|
246
246
|
_(collection.dimensions).wont_include :assembly
|
|
247
247
|
end
|
|
248
|
+
|
|
249
|
+
it "takes the by label positionally or by keyword, positional winning" do
|
|
250
|
+
_(collection.detail(:assembly).values(:assembly)).must_equal [:powertrain, :powertrain, :chassis, :body]
|
|
251
|
+
_(collection.detail(by: :assembly).values(:assembly)).must_equal [:powertrain, :powertrain, :chassis, :body]
|
|
252
|
+
_(collection.detail(:assembly, by: :ignored).values(:assembly)).must_equal [:powertrain, :powertrain, :chassis, :body]
|
|
253
|
+
end
|
|
248
254
|
end
|
|
249
255
|
|
|
250
256
|
describe "live recomputation (no memoisation in 1.x)" do
|
|
@@ -285,6 +291,14 @@ describe Namo::Collection do
|
|
|
285
291
|
_(collection.values(:assembly)).must_equal [:powertrain, :chassis, :body]
|
|
286
292
|
_(collection.values(:count)).must_equal [2, 1, 1]
|
|
287
293
|
end
|
|
294
|
+
|
|
295
|
+
it "as_detail takes the by label positionally or by keyword, positional winning" do
|
|
296
|
+
positional = Namo::Collection.new.tap{|c| c << [powertrain, chassis]}.as_detail(:assembly)
|
|
297
|
+
keyword = Namo::Collection.new.tap{|c| c << [powertrain, chassis]}.as_detail(by: :assembly)
|
|
298
|
+
_(positional.values(:assembly)).must_equal [:powertrain, :powertrain, :chassis]
|
|
299
|
+
_(keyword.values(:assembly)).must_equal positional.values(:assembly)
|
|
300
|
+
_(Namo::Collection.new.tap{|c| c << powertrain}.as_detail(:assembly, by: :ignored).values(:assembly)).must_equal [:powertrain, :powertrain]
|
|
301
|
+
end
|
|
288
302
|
end
|
|
289
303
|
|
|
290
304
|
describe "as_* view lifetime (rebuild-on-<<: persists until the next <<)" do
|
data/test/namo_test.rb
CHANGED
|
@@ -287,6 +287,44 @@ describe Namo do
|
|
|
287
287
|
end
|
|
288
288
|
end
|
|
289
289
|
|
|
290
|
+
describe "access-scoped materialisation" do
|
|
291
|
+
it "materialises a referenced column once per access, not once per referencing row" do
|
|
292
|
+
calls = Hash.new(0)
|
|
293
|
+
namo = Namo.new([{n: 1}, {n: 2}, {n: 3}])
|
|
294
|
+
namo[:base] = proc{|row| calls[:base] += 1; row[:n] * 10}
|
|
295
|
+
namo[:sum_base] = proc{|row, namo| namo.values(:base).sum}
|
|
296
|
+
_(namo.values(:sum_base)).must_equal [60, 60, 60]
|
|
297
|
+
_(calls[:base]).must_equal 3
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
it "retains nothing across accesses — a later access recomputes from current state" do
|
|
301
|
+
namo = Namo.new([{n: 1}, {n: 2}])
|
|
302
|
+
namo[:total] = proc{|row, namo| namo.values(:n).sum}
|
|
303
|
+
_(namo.values(:total)).must_equal [3, 3]
|
|
304
|
+
namo.data << {n: 4}
|
|
305
|
+
_(namo.values(:total)).must_equal [7, 7, 7]
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
it "clears the access scope when materialisation raises, leaving later accesses working" do
|
|
309
|
+
namo = Namo.new([{n: 1}])
|
|
310
|
+
namo[:boom] = proc{|row| raise 'materialisation blew up'}
|
|
311
|
+
_(->{namo.values(:boom)}).must_raise RuntimeError
|
|
312
|
+
namo[:ok] = proc{|row| row[:n]}
|
|
313
|
+
_(namo.values(:ok)).must_equal [1]
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
it "keeps one scope when a formula's nested values call runs during a multi-dimension access" do
|
|
317
|
+
calls = 0
|
|
318
|
+
namo = Namo.new([{x: 1}, {x: 2}, {x: 3}])
|
|
319
|
+
namo[:base] = proc{|row| calls += 1; row[:x] * 2}
|
|
320
|
+
namo[:agg] = proc{|row, namo| namo.values(:base).first}
|
|
321
|
+
# :agg's formula calls values(:base) mid-materialisation; that nested call must
|
|
322
|
+
# reuse the open scope, not reset it, so the :x column that follows still builds.
|
|
323
|
+
_(namo.values(:agg, :x)).must_equal(agg: [2, 2, 2], x: [1, 2, 3])
|
|
324
|
+
_(calls).must_equal 3
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
290
328
|
describe "#to_h" do
|
|
291
329
|
it "returns the full values Hash" do
|
|
292
330
|
_(sales.to_h).must_equal sales.values
|
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.29.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- thoran
|
|
@@ -98,7 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
98
98
|
- !ruby/object:Gem::Version
|
|
99
99
|
version: '0'
|
|
100
100
|
requirements: []
|
|
101
|
-
rubygems_version: 4.0.
|
|
101
|
+
rubygems_version: 4.0.16
|
|
102
102
|
specification_version: 4
|
|
103
103
|
summary: Named dimensional data for Ruby.
|
|
104
104
|
test_files: []
|