namo 0.28.0 → 0.30.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: cf138a52a691a4d50f093e506e5018d652a66ab23846593e2622cbe23710c8ef
4
- data.tar.gz: a8bbdc8cdf8af2b8c3df9881d468cc0445ffca7dd1f4523c614a2e71a8acc4c5
3
+ metadata.gz: e6f9ea9ed29bd86c309865e0c49d03122b7fa50460e34fd7a42a9a273bb220c0
4
+ data.tar.gz: 99d5e8191c3458b498c6fe297c2af8b94d8bb73aced942d9f3da67af94ace454
5
5
  SHA512:
6
- metadata.gz: 58c3cb00aee14efb44155a9a4a8e5eaa748aa51389f0ac19c27f4d8a077970cac3b605e7d1ffad2c95329782fdf04755ee1e8975b9e2d34209705a1e8f780014
7
- data.tar.gz: 7492096f83494f4c5a976b602796e3611c4eef1e920cc6b6179ff1b63e7f7fcd7a7f1e12ab2d637d3a1ef8fb4a0eeeb0364a0068ff3cbe12332b8e4b0e0d20c2
6
+ metadata.gz: 9e4aca2b13be76dc10a2fcf3eb7122045e600b1ed2410c2f6e82554c7dfc679249bf714fc52939fd4ee058fa6925b1958935bd91281dbf67f48634aec4cd4ea9
7
+ data.tar.gz: d3432cb0f092521adb1e2f60dccbc5b37aff2e4f91511189300b0693d374124e76cf1fde57d64709606ea504aae5db85d9c47e7aba99efcfdfa68b6fcbc38eef
data/CHANGELOG CHANGED
@@ -1,6 +1,33 @@
1
1
  # CHANGELOG
2
2
 
3
- ## 20260711
3
+ ## 20260715
4
+
5
+ 0.30.0: Assemble Collection objects in a single pass when doing Namo::Enumerable#group_by.
6
+
7
+ Appending one member per group re-materialised Collection#<<'s @data = detail.data over all members-so-far g times; group_by now appends all the members in one << operation and materialises once, causing it be performed in O(rows) instead of O(rows·groups).
8
+
9
+ 1. ~ Namo::Enumerable#group_by: map the groups to members, then append them all in one collection << members, rather than << per group — each of which rebuilt @data over every member-so-far.
10
+ 2. ~ test/namo_test.rb: + a single-pass assembly pin under #group_by — the assembly materialises the detail once whatever the number of groups (not once per group).
11
+ 3. ~ ROADMAP.md: + the 0.30.0 entry; current-state bumped to 0.30.0.
12
+ 4. ~ docs/: regenerate the print-ready PDFs.
13
+ 5. ~ Namo::VERSION: /0.29.0/0.30.0/
14
+
15
+ ## 20260715
16
+
17
+ 0.29.0: Per access indexed materialisation.
18
+
19
+ Materialise each column once per top-level access, collapsing an O(n²) parent-scanning formula to O(n), with no value retained across accesses.
20
+
21
+ 1. ~ Namo#values: wrap the body in a private materialising scope.
22
+ 2. ~ Namo#values_for: memoise its column build into @materialised[dim] ||= (the data/derived branch that was its body).
23
+ 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.
24
+ 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.
25
+ 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.
26
+ 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.
27
+ 7. ~ docs/: regenerate the print-ready PDFs.
28
+ 8. ~ Namo::VERSION: /0.28.0/0.29.0/
29
+
30
+ ## 20260713
4
31
 
5
32
  0.28.0: ~ Namo::Collection#detail/#as_detail: Accept the by label positionally or by keyword.
6
33
 
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
 
@@ -72,10 +72,12 @@ class Namo
72
72
  def group_by(dimension)
73
73
  collection = Collection.new
74
74
  source = derived_dimensions.include?(dimension) ? self[*data_dimensions, dimension] : self
75
- source.data.group_by{|row_data| row_data[dimension]}.each do |value, rows|
76
- collection << self.class.new(rows, formulae: source.formulae.dup, name: value)
77
- end
78
- collection
75
+ members = (
76
+ source.data.group_by{|row_data| row_data[dimension]}.map do |value, rows|
77
+ self.class.new(rows, formulae: source.formulae.dup, name: value)
78
+ end
79
+ )
80
+ collection << members
79
81
  end
80
82
  end
81
83
  end
data/lib/Namo/VERSION.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.28.0'
5
+ VERSION = '0.30.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
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
@@ -3160,5 +3198,27 @@ describe Namo do
3160
3198
  _(collection.detail.values(:close)).must_equal [42.5, 43.0, 118.3, 100.0]
3161
3199
  end
3162
3200
  end
3201
+
3202
+ context "single-pass assembly" do
3203
+ # Collection#<< rebuilds @data = detail.data over all members-so-far on every
3204
+ # append, so appending one member per group re-materialised the data g times
3205
+ # (O(rows·groups)). group_by now hands all members to a single <<: one
3206
+ # materialisation whatever the number of groups.
3207
+ def detail_calls_assembling(namo, dimension)
3208
+ calls = 0
3209
+ tracer = TracePoint.new(:call) do |tp|
3210
+ calls += 1 if tp.method_id == :detail && tp.defined_class == Namo::Collection
3211
+ end
3212
+ tracer.enable{namo.group_by(dimension)}
3213
+ calls
3214
+ end
3215
+
3216
+ it "materialises the assembled data once, whatever the number of groups" do
3217
+ few = Namo.new((1..3).map{|i| {symbol: "S#{i}", close: i.to_f}})
3218
+ many = Namo.new((1..30).map{|i| {symbol: "S#{i}", close: i.to_f}})
3219
+ _(detail_calls_assembling(few, :symbol)).must_equal 1
3220
+ _(detail_calls_assembling(many, :symbol)).must_equal 1
3221
+ end
3222
+ end
3163
3223
  end
3164
3224
  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.28.0
4
+ version: 0.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran