namo 0.31.3 → 0.31.4

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: 611590edfde52f5713b8fe41daf28b5986a26f4df3a5c28631dace995c31b98c
4
- data.tar.gz: f098e612be772d3232598020963f6b3ed5c9a786f4827de25312e90aff23bc48
3
+ metadata.gz: 3a5b005cb03bb42a9a9319632833f382a8ae2480ce04160f326a831f534da50a
4
+ data.tar.gz: f344e285a0d457af4ad633c98a33aab02cefe49fd273251ad9bdc7bdd3d98afa
5
5
  SHA512:
6
- metadata.gz: a28b1d267f69eb14a9ee8d7a9cbdba9de57132be4577a39892e3f679e0ccee293a88a2d526ceda0ff6232317bdb8028ae3f04858160c7a38cbc4cb640c5fff16
7
- data.tar.gz: e615d6be239ccd8a563922e3b51f5044a745733a4ef3f120b78ab5b07f84a4ff5be37373386853fb9d1484146471fc7034d58af9aee9172e4ae967e341ba0c39
6
+ metadata.gz: 8025bdb29748fc3076649dc59a38139075c03a9692c5c684295cb5060334e8e8c0c45dea9001dd942d68d0e3302910464c9141157a42d61af85eb751420bf0d8
7
+ data.tar.gz: 8fc50fc83aab71b68cb6e70dc4178e95dc8e2f3e6983480ccbcc4553915b61dde52f00cf8d2f1570988d5129df7e0534b13db8b3dc7413602d1fefc64b694613
data/CHANGELOG CHANGED
@@ -1,5 +1,16 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 20260826
4
+
5
+ 0.31.4: ~ Namo::Collection#inspect: the members are rendered, nested under their names.
6
+
7
+ 1. ~ Namo::Collection#inspect: the members are rendered nested under their names rather than listed as bare labels beside a row count, the members being the substance and the data view derived from them. The Namo::INSPECTED_ROWS budget is spread across the members shown — one member renders ten of its rows, five render two apiece, ten render one each and are put on one line — and members beyond it are elided with a count, as rows are. The member list had no cap at all: 2,641 members emitted 22,704 characters on a single line, which is the failure INSPECTED_ROWS was introduced at 0.31.0 to end. A member renders nested as it renders alone, its derived values among its stored ones and its own derived: suffix naming whatever none of its rendered rows could materialise. The detail view's row count closes the block, and a Collection's own formulae are named after it, being a namespace its members cannot resolve.
8
+ 2. + Namo#inspected_data: the rendered rows, the count of those not shown, and the derived dimensions none of them carries — protected, so Namo::Collection can render its members' rows without going through Namo#inspect and repeating each member's name, and still report what a member reports of itself.
9
+ 3. ~ README.md: the Inspection output section, and the two group_by examples whose rendering changes.
10
+ 4. ~ ROADMAP.md: + 0.31.4.
11
+ 5. ~ Namo::VERSION: /0.31.3/0.31.4/
12
+
13
+
3
14
  ## 20260824
4
15
 
5
16
  0.31.3: ~ Namo#to_a: derived dimensions are carried alongside the stored ones.
data/README.md CHANGED
@@ -996,14 +996,27 @@ Evaluation is bounded by the row cap rather than by the data — at most ten row
996
996
 
997
997
  Rows beyond `Namo::INSPECTED_ROWS` — ten — are elided with a count of the remainder, so the output stays bounded however large the data: a thousand-row Namo renders its first ten rows and then `... 990 more rows`.
998
998
 
999
- A `Row` renders itself, its derived values and its formula names in every case, having no `derived_dimensions` of its own to be asked, and never the Namo it came from. A `Collection` renders its member names and its row count, the members being its substance and the data view derived from them.
999
+ A `Row` renders itself, its derived values and its formula names in every case, having no `derived_dimensions` of its own to be asked, and never the Namo it came from.
1000
+
1001
+ A `Collection` renders its members, nested under their names, because the members are its substance and `@data` is a view derived from them. The `INSPECTED_ROWS` budget is spread across the members shown rather than spent on one: a single member renders ten of its rows, five members render two apiece, ten render one each and are put on one line. Members beyond that are elided with a count, as rows are, so a Collection of thousands renders in a dozen lines. The total row count of the detail view closes the block.
1002
+
1003
+ A member renders nested as it would render alone: its derived values among its stored ones, and its own `derived:` suffix naming whatever it cannot materialise. A Collection's own formulae are a separate namespace — it cannot resolve its members' and they cannot resolve its — so they are named in a suffix of the Collection's own, after the closing bracket.
1000
1004
 
1001
1005
  ```ruby
1002
1006
  sales.first
1003
1007
  # => #<Namo::Row {product: "Widget", quarter: "Q1", price: 10.0, quantity: 100, revenue: 1000.0} derived: [:revenue]>
1004
1008
 
1005
1009
  sales.group_by(:quarter)
1006
- # => #<Namo::Collection members: ["Q1", "Q2"], 4 rows>
1010
+ # => #<Namo::Collection [
1011
+ # "Q1" => [
1012
+ # {product: "Widget", quarter: "Q1", price: 10.0, quantity: 100, revenue: 1000.0},
1013
+ # {product: "Gadget", quarter: "Q1", price: 25.0, quantity: 40, revenue: 1000.0}
1014
+ # ],
1015
+ # "Q2" => [
1016
+ # {product: "Widget", quarter: "Q2", price: 10.0, quantity: 150, revenue: 1500.0},
1017
+ # {product: "Gadget", quarter: "Q2", price: 25.0, quantity: 60, revenue: 1500.0}
1018
+ # ]
1019
+ # ] 4 rows>
1007
1020
  ```
1008
1021
 
1009
1022
  ### Named Namos
@@ -1204,7 +1217,14 @@ Freeze-gated memoisation is a 2.x optimisation — opt-in via `freeze`, transpar
1204
1217
 
1205
1218
  ```ruby
1206
1219
  prices.group_by(:symbol)
1207
- # => #<Namo::Collection members: [:BHP, :RIO, :CBA]>
1220
+ # => #<Namo::Collection [
1221
+ # :BHP => [
1222
+ # {symbol: :BHP, date: "2025-01-02", close: 42.5},
1223
+ # {symbol: :BHP, date: "2025-01-03", close: 43.0}
1224
+ # ],
1225
+ # :RIO => [{symbol: :RIO, date: "2025-01-02", close: 118.0}],
1226
+ # :CBA => [{symbol: :CBA, date: "2025-01-02", close: 96.4}]
1227
+ # ] 4 rows>
1208
1228
 
1209
1229
  prices.group_by(:symbol).summary(:close, reducer: :mean)
1210
1230
  # => Namo with {member:, close:} rows — mean close per symbol
@@ -25,20 +25,23 @@ class Namo
25
25
 
26
26
  def summary(dimension = nil, by: :member, reducer: :sum, &block)
27
27
  raise ArgumentError, "summary needs a dimension or a block" unless dimension || block
28
- rows =
28
+ rows = (
29
29
  if block
30
30
  @members.map{|member| block.call(member).merge(by => member.name)}
31
31
  else
32
32
  @members.map{|member| {by => member.name, dimension => member.values(dimension).send(reducer)}}
33
33
  end
34
+ )
34
35
  Namo.new(rows)
35
36
  end
36
37
 
37
38
  def detail(positional_by = nil, by: :member)
38
39
  by = positional_by || by
39
- rows = @members.flat_map do |member|
40
- member.data.map{|row| row.key?(by) ? row : row.merge(by => member.name)}
41
- end
40
+ rows = (
41
+ @members.flat_map do |member|
42
+ member.data.map{|row| row.key?(by) ? row : row.merge(by => member.name)}
43
+ end
44
+ )
42
45
  Namo.new(rows)
43
46
  end
44
47
 
@@ -53,12 +56,46 @@ class Namo
53
56
  self
54
57
  end
55
58
 
59
+ # The members are the substance, so they are what is rendered. A list of names
60
+ # and a row count described the derived view and called it the object, and the
61
+ # list was unbounded besides: 2,641 members emitted 22,704 characters on one
62
+ # line, which is the failure INSPECTED_ROWS was introduced to end.
63
+ #
64
+ # That budget is now spread across the members shown rather than spent on one
65
+ # member's rows: one member renders ten of its rows, ten members render one
66
+ # apiece, five render two.
56
67
  def inspect
57
- "#<#{self.class}#{inspected_name} members: #{@members.map(&:name).inspect}, #{@data.length} rows#{inspected_derived}>"
68
+ "#<#{self.class}#{inspected_name} #{inspected_members} #{@data.length} rows#{inspected_derived}>"
58
69
  end
59
70
 
60
71
  private
61
72
 
73
+ def inspected_members
74
+ return '[]' if @members.empty?
75
+ shown = [@members.length, INSPECTED_ROWS].min
76
+ each = [INSPECTED_ROWS / shown, 1].max
77
+ rendered = @members.first(shown).map{|member| " #{member.name.inspect} => #{inspected_member(member, each)}"}.join(",\n")
78
+ rendered += "\n ... #{@members.length - shown} more members" if @members.length > shown
79
+ "[\n#{rendered}\n]"
80
+ end
81
+
82
+ # A member showing a single row is rendered on one line: three lines of brackets
83
+ # to carry one row is the shape a Collection of many members lands in, and it
84
+ # cost more than the rows did. On what is rendered rather than on the limit,
85
+ # so a member holding one row of a larger allowance reads the same way.
86
+ def inspected_member(member, limit)
87
+ rows, remainder, unshown = member.inspected_data(limit)
88
+ elided = remainder > 0
89
+ suffix = inspected_derived_names(unshown)
90
+ if rows.length == 1
91
+ "[#{rows.join}#{elided ? ", ... #{remainder} more rows" : ''}]#{suffix}"
92
+ else
93
+ body = rows.map{|row| " #{row}"}.join(",\n")
94
+ body += "\n ... #{remainder} more rows" if elided
95
+ "[\n#{body}\n ]#{suffix}"
96
+ end
97
+ end
98
+
62
99
  def initialize(positional_data = nil, data: [], formulae: {}, name: nil)
63
100
  @members = []
64
101
  super
data/lib/Namo/VERSION.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.31.3'
5
+ VERSION = '0.31.4'
6
6
  end
data/lib/namo.rb CHANGED
@@ -262,6 +262,16 @@ class Namo
262
262
 
263
263
  protected
264
264
 
265
+ # The rendered rows, the count of those not shown, and the derived dimensions
266
+ # none of them carries. Shared with Collection, which renders its members' rows
267
+ # rather than its own and must report what a member would report of itself.
268
+ def inspected_data(limit)
269
+ rendered = @data.first(limit).map{|row| [row, inspected_derivations(row)]}
270
+ [rendered.map{|row, derived| row.merge(derived).inspect},
271
+ @data.length - limit,
272
+ unshown_derived(rendered)]
273
+ end
274
+
265
275
  def row_multiset
266
276
  @data.tally
267
277
  end
@@ -278,6 +288,13 @@ class Namo
278
288
 
279
289
  private
280
290
 
291
+ def initialize(positional_data = nil, data: [], formulae: {}, name: nil)
292
+ @data = positional_data || data
293
+ @formulae = formulae.is_a?(Formulae) ? formulae : Formulae.new(formulae)
294
+ @name = name
295
+ attach_included_formularies
296
+ end
297
+
281
298
  def inspected_name
282
299
  @name.nil? ? '' : " #{@name.inspect}"
283
300
  end
@@ -308,19 +325,19 @@ class Namo
308
325
  # Only those the rows above do not already carry. A formula which raises, and a
309
326
  # parameterised one which cannot be materialised without its arguments, render
310
327
  # no value and would otherwise be nowhere in the output at all. A Collection
311
- # renders no rows and so passes none, naming every one of them.
328
+ # renders no rows of its own and so passes none, naming every one of them.
312
329
  def inspected_derived(rendered = [])
313
- unshown = derived_dimensions.reject do |dimension|
314
- rendered.any? && rendered.all?{|_, derived| derived.key?(dimension)}
315
- end
330
+ inspected_derived_names(unshown_derived(rendered))
331
+ end
332
+
333
+ def inspected_derived_names(unshown)
316
334
  unshown.empty? ? '' : " derived: #{unshown.inspect}"
317
335
  end
318
336
 
319
- def initialize(positional_data = nil, data: [], formulae: {}, name: nil)
320
- @data = positional_data || data
321
- @formulae = formulae.is_a?(Formulae) ? formulae : Formulae.new(formulae)
322
- @name = name
323
- attach_included_formularies
337
+ def unshown_derived(rendered)
338
+ derived_dimensions.reject do |dimension|
339
+ rendered.any? && rendered.all?{|_, derived| derived.key?(dimension)}
340
+ end
324
341
  end
325
342
 
326
343
  def add_row(row)
@@ -348,14 +348,104 @@ describe Namo::Collection do
348
348
  end
349
349
 
350
350
  describe "#inspect" do
351
- it "renders the member names and the row count" do
351
+ def collection_of(members, rows_each)
352
+ rows = (1..members).flat_map{|member| (1..rows_each).map{|row| {group: "g#{member}", n: row}}}
353
+ Namo.new(rows).group_by(:group)
354
+ end
355
+
356
+ it "renders each member nested, under its name" do
352
357
  collection = Namo::Collection.new
353
358
  collection << Namo.new([{a: 1}], name: :first) << Namo.new([{a: 2}], name: :second)
354
- _(collection.inspect).must_equal "#<Namo::Collection members: [:first, :second], 2 rows>"
359
+ _(collection.inspect).must_equal <<~INSPECT.chomp
360
+ #<Namo::Collection [
361
+ :first => [{a: 1}],
362
+ :second => [{a: 2}]
363
+ ] 2 rows>
364
+ INSPECT
355
365
  end
356
366
 
357
367
  it "renders an empty Collection" do
358
- _(Namo::Collection.new.inspect).must_equal "#<Namo::Collection members: [], 0 rows>"
368
+ _(Namo::Collection.new.inspect).must_equal "#<Namo::Collection [] 0 rows>"
369
+ end
370
+
371
+ # The budget is INSPECTED_ROWS spread across the members shown, so the output
372
+ # is bounded by what was asked for rather than by how the data is shaped.
373
+ it "gives a lone member the whole row budget" do
374
+ rendered = collection_of(1, 50).inspect
375
+ _(rendered.scan(/\{group:/).length).must_equal Namo::INSPECTED_ROWS
376
+ _(rendered).must_include "... 40 more rows"
377
+ end
378
+
379
+ it "gives five members two rows apiece" do
380
+ rendered = collection_of(5, 9).inspect
381
+ _(rendered.scan(/\{group:/).length).must_equal Namo::INSPECTED_ROWS
382
+ _(rendered).must_include "... 7 more rows"
383
+ end
384
+
385
+ it "gives ten members one row apiece, on one line each" do
386
+ rendered = collection_of(10, 4).inspect
387
+ _(rendered.scan(/\{group:/).length).must_equal Namo::INSPECTED_ROWS
388
+ _(rendered).must_include %q{"g1" => [{group: "g1", n: 1}, ... 3 more rows]}
389
+ end
390
+
391
+ it "elides the members beyond the budget, with a count" do
392
+ rendered = collection_of(2641, 130).inspect
393
+ _(rendered).must_include "... 2631 more members"
394
+ _(rendered.lines.length).must_equal 13
395
+ end
396
+
397
+ # 2,641 members emitted 22,704 characters on a single line before the budget.
398
+ it "stays bounded as the members multiply" do
399
+ _(collection_of(2641, 130).inspect.length).must_be :<, collection_of(10, 130).inspect.length * 2
400
+ end
401
+
402
+ it "reports the total row count of the detail view" do
403
+ _(collection_of(3, 7).inspect).must_include "] 21 rows>"
404
+ end
405
+
406
+ # Where the two rules meet: 0.31.2 names only what the rendered rows do not
407
+ # carry. A member's formula is carried, since its rows are what is rendered; a
408
+ # Collection's own applies to the data view, which the nested form never shows,
409
+ # so naming it is the only way to learn it is there.
410
+ it "names its own derived dimensions while showing its members' inline" do
411
+ member = Namo.new([{part: "block", weight: 90}], name: :powertrain)
412
+ member[:light] = proc{|row| row[:weight] < 50}
413
+ collection = Namo::Collection.new
414
+ collection << member
415
+ collection[:heavy] = proc{|row| row[:weight] > 50}
416
+ _(collection.derived_dimensions).must_equal [:heavy]
417
+ _(member.derived_dimensions).must_equal [:light]
418
+ _(collection.inspect).must_include "{part: \"block\", weight: 90, light: false}"
419
+ _(collection.inspect).must_include "] 1 rows derived: [:heavy]>"
420
+ end
421
+
422
+ # Nested, a member must say what it says of itself: a formula it cannot
423
+ # materialise is named in its own inspect, and vanishing here would make the
424
+ # nested rendering quieter than the parts it is made of.
425
+ it "carries a member's own suffix for what that member cannot materialise" do
426
+ member = Namo.new([{part: "block", weight: 90}], name: :powertrain)
427
+ member[:light] = proc{|row| row[:weight] < 50}
428
+ member[:scaled] = proc{|row, namo, factor| row[:weight] * factor}
429
+ collection = Namo::Collection.new
430
+ collection << member
431
+ _(member.inspect).must_include "derived: [:scaled]"
432
+ _(collection.inspect).must_include ":powertrain => [{part: \"block\", weight: 90, light: false}] derived: [:scaled]"
433
+ end
434
+
435
+ it "carries no suffix when only its members hold formulae" do
436
+ member = Namo.new([{part: "block", weight: 90}], name: :powertrain)
437
+ member[:light] = proc{|row| row[:weight] < 50}
438
+ collection = Namo::Collection.new
439
+ collection << member
440
+ _(collection.inspect).wont_include "derived:"
441
+ end
442
+
443
+ it "renders a member's derived dimensions among its stored ones" do
444
+ member = Namo.new([{a: 1}], name: :first)
445
+ member[:b] = proc{|row| row[:a] + 1}
446
+ collection = Namo::Collection.new
447
+ collection << member
448
+ _(collection.inspect).must_include "{a: 1, b: 2}"
359
449
  end
360
450
  end
361
451
 
@@ -0,0 +1,42 @@
1
+ # test/sizing_test.rb
2
+
3
+ require 'minitest/autorun'
4
+ require 'minitest-spec-context'
5
+
6
+ require_relative '../lib/namo'
7
+
8
+ # script/sizing produces the figures in ROADMAP.md's "What size it is for". A
9
+ # measurement whose script has rotted is a measurement nobody can repeat, which is
10
+ # the condition the script exists to end — so assert it still runs, at sizes small
11
+ # enough not to make the suite a benchmark.
12
+
13
+ describe 'script/sizing' do
14
+ def sizing(*arguments)
15
+ root = File.expand_path('..', __dir__)
16
+ IO.popen([File.join(root, 'script', 'sizing'), *arguments],
17
+ chdir: root, err: [:child, :out]){|io| io.read}
18
+ end
19
+
20
+ it "reports a row per size asked for" do
21
+ output = sizing('2000', '4000')
22
+ _($?.success?).must_equal true
23
+ _(output).must_match(/^\| 2,000 \| \d+ ms \|/)
24
+ _(output).must_match(/^\| 4,000 \| \d+ ms \|/)
25
+ end
26
+
27
+ it "renders as the table the ROADMAP carries" do
28
+ _(sizing('2000').lines.first).must_equal "| rows | group_by | per row | bytes/row |\n"
29
+ end
30
+
31
+ it "refuses a size it cannot measure" do
32
+ sizing('0')
33
+ _($?.success?).must_equal false
34
+ end
35
+
36
+ it "needs nothing but the gem's own dependencies" do
37
+ root = File.expand_path('..', __dir__)
38
+ output = IO.popen({'RUBYLIB' => nil}, [File.join(root, 'script', 'sizing'), '2000'],
39
+ chdir: root, err: [:child, :out]){|io| io.read}
40
+ _(output).wont_match(/LoadError/)
41
+ end
42
+ 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.31.3
4
+ version: 0.31.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran
@@ -82,6 +82,7 @@ files:
82
82
  - test/console_test.rb
83
83
  - test/demo_test.rb
84
84
  - test/namo_test.rb
85
+ - test/sizing_test.rb
85
86
  homepage: https://github.com/thoran/namo
86
87
  licenses:
87
88
  - MIT
@@ -100,7 +101,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
100
101
  - !ruby/object:Gem::Version
101
102
  version: '0'
102
103
  requirements: []
103
- rubygems_version: 4.0.18
104
+ rubygems_version: 4.0.19
104
105
  specification_version: 4
105
106
  summary: Named dimensional data for Ruby.
106
107
  test_files: []