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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5649b6afb8e1272273a87286f053376ad42caddec81eed0610a672f26bc7e66a
4
- data.tar.gz: 77a5e0c6499c3d600b76753e28c5e600b0dcd6dc0968d93069a56e3b10fc7652
3
+ metadata.gz: 5e56f3159063720b0e933e9c346ddad824831a51dac7a576da6d0cf841fd28ea
4
+ data.tar.gz: 8aee98bb89f8c7ab978c0ce459cc90c8259123123c13f5158a3e16dbb93922c7
5
5
  SHA512:
6
- metadata.gz: 33c03e6c39cee1242a576a8355928b041464c9049a5bbb54900b7654dc36a9dec44741edaf28a8408eb293f4fcf4b702736841e8cf3b3bae7ce43e6684b098f1
7
- data.tar.gz: d0ad82d6b415a09ba086a9d6a7b0f06c24d1dfad8bb3eaad72e1a40c878f9a431ccfb093f781836a94d0d4f4ab6bdf8b7917ddb61123012bf1fd0455168e3e05
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` carries no `dimension`, so its label argument is positional: `as_detail(:assembly)`.)
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
@@ -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(by = :member)
50
- @data = detail(by: by).data
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
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.27.0'
5
+ VERSION = '0.29.0'
6
6
  end
data/lib/namo.rb CHANGED
@@ -30,12 +30,14 @@ class Namo
30
30
  end
31
31
 
32
32
  def values(*dims)
33
- if dims.empty?
34
- materialisable_dimensions.each_with_object({}){|dim, hash| hash[dim] = values_for(dim)}
35
- elsif dims.length == 1
36
- values_for(dims.first)
37
- else
38
- dims.each_with_object({}){|dim, hash| hash[dim] = values_for(dim)}
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 values_for(dim)
294
- if data_dimensions.include?(dim)
295
- @data.map{|row_data| row_data[dim]}
296
- else
297
- @data.map{|row_data| Row.new(row_data, @formulae, self)[dim]}
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.27.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.15
101
+ rubygems_version: 4.0.16
102
102
  specification_version: 4
103
103
  summary: Named dimensional data for Ruby.
104
104
  test_files: []