namo 0.26.1 → 0.27.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: 40ae1191c5889356a1f8442673b3f44588521aa07f3f379fe58c9afa85b574c8
4
- data.tar.gz: c38c7996e0a31e6050adcd9162dbbd6bde3b1c8b12cbaf0b1ab6a7848eeffb78
3
+ metadata.gz: 5649b6afb8e1272273a87286f053376ad42caddec81eed0610a672f26bc7e66a
4
+ data.tar.gz: 77a5e0c6499c3d600b76753e28c5e600b0dcd6dc0968d93069a56e3b10fc7652
5
5
  SHA512:
6
- metadata.gz: 3bf0fb1cf44efc3096388e5adda7503802003e3b49a6539404cefed47ea03af1ca2414089e2599d0d7e0e1676f1a674f3bfadc80131ca4c88401d8c87d38b4c7
7
- data.tar.gz: f8ea345eee0bca6cf22ea50f470874becce72795649a0e28bba8df3027c70511634accf961f61aa0647177214e18ad139bb24a5870a534ec9e0de096023b67bc
6
+ metadata.gz: 33c03e6c39cee1242a576a8355928b041464c9049a5bbb54900b7654dc36a9dec44741edaf28a8408eb293f4fcf4b702736841e8cf3b3bae7ce43e6684b098f1
7
+ data.tar.gz: d0ad82d6b415a09ba086a9d6a7b0f06c24d1dfad8bb3eaad72e1a40c878f9a431ccfb093f781836a94d0d4f4ab6bdf8b7917ddb61123012bf1fd0455168e3e05
data/CHANGELOG CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## 20260710
4
4
 
5
+ 0.27.0: ~ Namo::Collection#summary/#as_summary: + an optional block for a per-member reduction beyond a single named reducer.
6
+
7
+ 1. ~ Namo::Collection#summary(dimension = nil, by:, reducer:, &block): dimension goes optional; with a block, maps each member through block.call(member).merge(by => member.name), the member label merged last so it wins; given neither a dimension nor a block, raises ArgumentError; the no-block path is byte-identical. as_summary forwards &block to summary and persists the result, so the block form lives in one place.
8
+ 2. ~ test/Namo/Collection_test.rb: + block-form summary returns one row per member merged with the member label; the member label wins over a by key the block returns; the neither-arg ArgumentError; the map/flat_map cardinality pin (summary one row per member, detail all rows per member); as_summary with a block sets the data and delegates. ~ the non-block custom-by test also pins the reduced value carried alongside the relabelled column.
9
+ 3. ~ README.md: + an "A block for per-member reductions" note under the Collection view methods (the one-row-per-member contract, the member-label-wins merge, an argmax example, the neither-arg raise); + a bare-collection call-site by: example beside the class-default prose; the summary/as_summary signatures gain (dimension = nil, ..., &block).
10
+ 4. ~ ROADMAP.md: + the 0.27.0 release entry (the block warrant from 0.14.0, summarise considered-and-refused, the pinned decisions, the transient-block serialisation-independence, the deferred group-scoped formula); current-state bumped to 0.27.0; the block form folded into the Summary vocabulary and the 1.0.0 feature list.
11
+ 5. ~ docs/: regenerate the print-ready PDFs.
12
+ 6. ~ Namo::VERSION: /0.26.1/0.27.0/
13
+
5
14
  0.26.1: Refactor Namo::Formulae#attach through a private #bind, and document mutability and the corrected collision-resolution idioms.
6
15
 
7
16
  1. ~ lib/Namo/Formulae.rb — attach binds each of a formulary's public method names through a new private bind(name, modul): marker guard, host.extend(modul), then self[name] = modul.instance_method(name).bind(host). attach and detach share one private raise_unless_formulary marker guard. bind stays private — attach is its only caller.
data/README.md CHANGED
@@ -1046,7 +1046,15 @@ gt.detail.values(:weight).sum # total weight by summing every line
1046
1046
  # => 490
1047
1047
  ```
1048
1048
 
1049
- `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.
1049
+ `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:
1050
+
1051
+ ```ruby
1052
+ parts = Namo::Collection.new
1053
+ parts << [powertrain, chassis, body]
1054
+
1055
+ parts.summary(:weight).values(:member) # default label => [:powertrain, :chassis, :body]
1056
+ parts.summary(:weight, by: :assembly).values(:assembly) # call-site by: => [:powertrain, :chassis, :body]
1057
+ ```
1050
1058
 
1051
1059
  #### Lazy detail, behaving as its line items
1052
1060
 
@@ -1066,8 +1074,8 @@ Detail is the lazy view because a Collection's rows simply *are* its members' ro
1066
1074
 
1067
1075
  The views come in a non-mutating pair and a mutating pair:
1068
1076
 
1069
- - `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.
1070
- - `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)`.)
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)`.)
1071
1079
 
1072
1080
  ```ruby
1073
1081
  gt.summary(:cost, reducer: :mean) # a fresh Namo; gt is unchanged
@@ -1077,6 +1085,23 @@ gt.as_detail(:assembly) # gt's data becomes the detail; retur
1077
1085
 
1078
1086
  `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`).
1079
1087
 
1088
+ #### A block for per-member reductions
1089
+
1090
+ The single-`dimension`, single-`reducer` form reduces one column with one named method. Per-member work that wants several columns at once, or a reduction that isn't a single-column `send` — notably a *pick-by-extremum* ("the component of the heaviest part", an argmax that reduces no one column) — is supplied by an optional block. The block receives each member and returns the hash for that member's summary row; `summary` labels it with the member name and collects one row per member:
1091
+
1092
+ ```ruby
1093
+ gt.summary(by: :assembly) do |assembly|
1094
+ heaviest = assembly.max_by{|part| part[:weight]}
1095
+ {
1096
+ part_count: assembly.values(:weight).size,
1097
+ heaviest_part: heaviest[:component],
1098
+ total_cost: assembly.values(:cost).sum,
1099
+ }
1100
+ end
1101
+ ```
1102
+
1103
+ The contract is one row per member: `summary` maps members to rows, where `detail` flat-maps them to line items — the two views have opposite cardinality. The member label merges last, so a block returning a hash that carries the `by` key cannot override the group's identity — `by` is `summary`'s to set, not the block's. `as_summary` takes the same block and persists its result, delegating to `summary`. A `summary` given neither a `dimension` nor a block raises an `ArgumentError`.
1104
+
1080
1105
  #### Inject-iff-absent
1081
1106
 
1082
1107
  `detail(by:)` unions the members' rows and labels each with its origin, but only when that label isn't already present:
@@ -23,10 +23,14 @@ class Namo
23
23
  @members.find{|member| member.name == name} unless name.nil?
24
24
  end
25
25
 
26
- def summary(dimension, by: :member, reducer: :sum)
27
- rows = @members.map do |member|
28
- {by => member.name, dimension => member.values(dimension).send(reducer)}
29
- end
26
+ def summary(dimension = nil, by: :member, reducer: :sum, &block)
27
+ raise ArgumentError, "summary needs a dimension or a block" unless dimension || block
28
+ rows =
29
+ if block
30
+ @members.map{|member| block.call(member).merge(by => member.name)}
31
+ else
32
+ @members.map{|member| {by => member.name, dimension => member.values(dimension).send(reducer)}}
33
+ end
30
34
  Namo.new(rows)
31
35
  end
32
36
 
@@ -37,8 +41,8 @@ class Namo
37
41
  Namo.new(rows)
38
42
  end
39
43
 
40
- def as_summary(dimension, by: :member, reducer: :sum)
41
- @data = summary(dimension, by: by, reducer: reducer).data
44
+ def as_summary(dimension = nil, by: :member, reducer: :sum, &block)
45
+ @data = summary(dimension, by: by, reducer: reducer, &block).data
42
46
  self
43
47
  end
44
48
 
data/lib/Namo/VERSION.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.26.1'
5
+ VERSION = '0.27.0'
6
6
  end
@@ -181,9 +181,10 @@ describe Namo::Collection do
181
181
  _(summary.values(:weight)).must_equal [280, 150, 60]
182
182
  end
183
183
 
184
- it "labels with a custom by dimension" do
184
+ it "labels with a custom by dimension, carrying the reduced value alongside" do
185
185
  summary = collection.summary(:weight, by: :assembly)
186
186
  _(summary.values(:assembly)).must_equal [:powertrain, :chassis, :body]
187
+ _(summary.values(:weight)).must_equal [280, 150, 60]
187
188
  end
188
189
 
189
190
  it "reduces with a custom reducer" do
@@ -195,6 +196,32 @@ describe Namo::Collection do
195
196
  collection.summary(:weight)
196
197
  _(collection.values(:weight)).must_equal [200, 80, 150, 60]
197
198
  end
199
+
200
+ it "with a block returns one row per member, the block's hash merged with the member label" do
201
+ result = collection.summary do |member|
202
+ {heaviest: member.max_by{|row| row[:weight]}[:component]}
203
+ end
204
+ _(result.values(:member)).must_equal [:powertrain, :chassis, :body]
205
+ _(result.values(:heaviest)).must_equal ['engine', 'frame', 'panels']
206
+ end
207
+
208
+ it "with a block, the member label wins over a by key the block returns" do
209
+ result = collection.summary(by: :assembly) do |member|
210
+ {assembly: :overridden, total: member.values(:weight).sum}
211
+ end
212
+ _(result.values(:assembly)).must_equal [:powertrain, :chassis, :body]
213
+ _(result.values(:total)).must_equal [280, 150, 60]
214
+ end
215
+
216
+ it "raises an ArgumentError when given neither a dimension nor a block" do
217
+ error = _(proc{collection.summary}).must_raise ArgumentError
218
+ _(error.message).must_match(/dimension or a block/)
219
+ end
220
+
221
+ it "yields one row per member (map), where detail yields all rows per member (flat_map)" do
222
+ _(collection.summary(:weight).data.size).must_equal collection.members.size
223
+ _(collection.detail.data.size).must_equal collection.members.sum{|member| member.data.size}
224
+ end
198
225
  end
199
226
 
200
227
  describe "#detail" do
@@ -251,6 +278,13 @@ describe Namo::Collection do
251
278
  collection.as_summary(:weight)
252
279
  _(collection.dimensions.sort).must_equal [:member, :weight].sort
253
280
  end
281
+
282
+ it "as_summary with a block sets the data to the block summary and returns self" do
283
+ result = collection.as_summary(by: :assembly){|member| {count: member.values(:weight).size}}
284
+ _(result).must_be_same_as collection
285
+ _(collection.values(:assembly)).must_equal [:powertrain, :chassis, :body]
286
+ _(collection.values(:count)).must_equal [2, 1, 1]
287
+ end
254
288
  end
255
289
 
256
290
  describe "as_* view lifetime (rebuild-on-<<: persists until the next <<)" do
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.26.1
4
+ version: 0.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran