namo 0.26.1 → 0.28.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: cf138a52a691a4d50f093e506e5018d652a66ab23846593e2622cbe23710c8ef
4
+ data.tar.gz: a8bbdc8cdf8af2b8c3df9881d468cc0445ffca7dd1f4523c614a2e71a8acc4c5
5
5
  SHA512:
6
- metadata.gz: 3bf0fb1cf44efc3096388e5adda7503802003e3b49a6539404cefed47ea03af1ca2414089e2599d0d7e0e1676f1a674f3bfadc80131ca4c88401d8c87d38b4c7
7
- data.tar.gz: f8ea345eee0bca6cf22ea50f470874becce72795649a0e28bba8df3027c70511634accf961f61aa0647177214e18ad139bb24a5870a534ec9e0de096023b67bc
6
+ metadata.gz: 58c3cb00aee14efb44155a9a4a8e5eaa748aa51389f0ac19c27f4d8a077970cac3b605e7d1ffad2c95329782fdf04755ee1e8975b9e2d34209705a1e8f780014
7
+ data.tar.gz: 7492096f83494f4c5a976b602796e3611c4eef1e920cc6b6179ff1b63e7f7fcd7a7f1e12ab2d637d3a1ef8fb4a0eeeb0364a0068ff3cbe12332b8e4b0e0d20c2
data/CHANGELOG CHANGED
@@ -1,7 +1,29 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 20260711
4
+
5
+ 0.28.0: ~ Namo::Collection#detail/#as_detail: Accept the by label positionally or by keyword.
6
+
7
+ Accept the by label positionally or by keyword (positional winning), per the constructor precedent — resolving the detail/as_detail calling-convention asymmetry.
8
+
9
+ 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.
10
+ 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.
11
+ 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.
12
+ 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.
13
+ 5. ~ docs/: regenerate the print-ready PDFs.
14
+ 6. ~ Namo::VERSION: /0.27.0/0.28.0/
15
+
3
16
  ## 20260710
4
17
 
18
+ 0.27.0: ~ Namo::Collection#summary/#as_summary: + an optional block for a per-member reduction beyond a single named reducer.
19
+
20
+ 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.
21
+ 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.
22
+ 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).
23
+ 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.
24
+ 5. ~ docs/: regenerate the print-ready PDFs.
25
+ 6. ~ Namo::VERSION: /0.26.1/0.27.0/
26
+
5
27
  0.26.1: Refactor Namo::Formulae#attach through a private #bind, and document mutability and the corrected collision-resolution idioms.
6
28
 
7
29
  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 = :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.)
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,27 +23,33 @@ 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
 
33
- def detail(by: :member)
37
+ def detail(positional_by = nil, by: :member)
38
+ by = positional_by || by
34
39
  rows = @members.flat_map do |member|
35
40
  member.data.map{|row| row.key?(by) ? row : row.merge(by => member.name)}
36
41
  end
37
42
  Namo.new(rows)
38
43
  end
39
44
 
40
- def as_summary(dimension, by: :member, reducer: :sum)
41
- @data = summary(dimension, by: by, reducer: reducer).data
45
+ def as_summary(dimension = nil, by: :member, reducer: :sum, &block)
46
+ @data = summary(dimension, by: by, reducer: reducer, &block).data
42
47
  self
43
48
  end
44
49
 
45
- def as_detail(by = :member)
46
- @data = detail(by: by).data
50
+ def as_detail(positional_by = nil, by: :member)
51
+ by = positional_by || by
52
+ @data = detail(by).data
47
53
  self
48
54
  end
49
55
 
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.28.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
@@ -218,6 +245,12 @@ describe Namo::Collection do
218
245
  collection.detail(by: :assembly)
219
246
  _(collection.dimensions).wont_include :assembly
220
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
221
254
  end
222
255
 
223
256
  describe "live recomputation (no memoisation in 1.x)" do
@@ -251,6 +284,21 @@ describe Namo::Collection do
251
284
  collection.as_summary(:weight)
252
285
  _(collection.dimensions.sort).must_equal [:member, :weight].sort
253
286
  end
287
+
288
+ it "as_summary with a block sets the data to the block summary and returns self" do
289
+ result = collection.as_summary(by: :assembly){|member| {count: member.values(:weight).size}}
290
+ _(result).must_be_same_as collection
291
+ _(collection.values(:assembly)).must_equal [:powertrain, :chassis, :body]
292
+ _(collection.values(:count)).must_equal [2, 1, 1]
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
254
302
  end
255
303
 
256
304
  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.28.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: []