namo 0.31.2 → 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: 820a3c18db30a81cef333ad6b6bd95cd3129610d8d237a0a4f14554d23bdd2e9
4
- data.tar.gz: 8b0c94985ae46c47e8b129eecd7fe7eba7d30f499aea917b2c9e80b1edf2ad7c
3
+ metadata.gz: 3a5b005cb03bb42a9a9319632833f382a8ae2480ce04160f326a831f534da50a
4
+ data.tar.gz: f344e285a0d457af4ad633c98a33aab02cefe49fd273251ad9bdc7bdd3d98afa
5
5
  SHA512:
6
- metadata.gz: a5631205eb63c196e4ba9d6b8cf5bf54807e61a462e55645e216aeb5515d767934413c226a0cb567ae0637002e6c3a80a3490c6db7c94397ae17da2c42ae8c8f
7
- data.tar.gz: 829d09221f044a4572ff7b97bc76c3a84cd7c8ce8e9e2ee8656b2ac5c82a3753d2f793223bdf3125a0e22d925bf86e19078680b78d6dcde7077248aef7488740
6
+ metadata.gz: 8025bdb29748fc3076649dc59a38139075c03a9692c5c684295cb5060334e8e8c0c45dea9001dd942d68d0e3302910464c9141157a42d61af85eb751420bf0d8
7
+ data.tar.gz: 8fc50fc83aab71b68cb6e70dc4178e95dc8e2f3e6983480ccbcc4553915b61dde52f00cf8d2f1570988d5129df7e0534b13db8b3dc7413602d1fefc64b694613
data/CHANGELOG CHANGED
@@ -1,7 +1,28 @@
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
 
16
+ 0.31.3: ~ Namo#to_a: derived dimensions are carried alongside the stored ones.
17
+
18
+ 1. ~ Namo#to_a: each row carries the derived dimensions which can be materialised without arguments, in among its stored keys, so to_a agrees with to_h and with dimensions about what the Namo holds. A formula wanting arguments is omitted, the rule values, coordinates and to_h already follow. Namo#data is unchanged and remains the stored rows; Namo::Row#to_h is unchanged, Row#==, Row#eql? and Namo#<< all reading through it.
19
+ 2. ~ README.md: the Extracting data section, and the parameterised-formula rule which now names to_a.
20
+ 3. ~ ROADMAP.md: + 0.31.3; the open question on which conversions materialise closes for to_a and stays open for Namo::Row.
21
+ 4. ~ Namo::VERSION: /0.31.2/0.31.3/
22
+
23
+ Rebuilding a Namo from to_a now carries the derived dimensions in as stored data. Namo.new(other.to_a) followed by attaching the formulary which produced them raises, the 0.24.1 collision guard refusing the name twice over; []= accepts it, evicting the data. Where the stored rows are what is meant, data is the accessor for them. A formula whose inputs a projection has cut raises through to_a as it has always raised through to_h and values, which 0.16.0 settled as the caller's own choice.
24
+
25
+
5
26
  0.31.2: ~ Namo#inspect: the derived: suffix names only what the rows do not carry.
6
27
 
7
28
  1. ~ Namo#inspect: the derived: suffix names only the derived dimensions the rendered rows do not already carry, and is omitted where they all do. A formula which raises, a parameterised one which cannot be materialised without its arguments, and any formula on a Namo with no rows to render, are named as before, being nowhere else in the output. Namo::Collection renders no rows and so names every one of its own. Namo::Row is unchanged, naming its formulae in every case.
data/README.md CHANGED
@@ -717,7 +717,7 @@ prices.last[:sma] # ArgumentError: wrong number of arguments for :sma
717
717
  prices.last[:close, 20] # ArgumentError: wrong number of arguments for :close (given 1, expected 0)
718
718
  ```
719
719
 
720
- A formula that requires arguments can't be materialised without them. `values(:sma)`, `coordinates(:sma)`, naming `:sma` in a projection, and selecting on it all raise the same `ArgumentError`; the no-argument `values`, `coordinates`, and `to_h` omit the dimension, returning everything that can be materialised. `dimensions` and `derived_dimensions` still list it — it is queryable, with arguments. To materialise particular values, bind the arguments in a one-arity wrapper and ask for that:
720
+ A formula that requires arguments can't be materialised without them. `values(:sma)`, `coordinates(:sma)`, naming `:sma` in a projection, and selecting on it all raise the same `ArgumentError`; the no-argument `values`, `coordinates`, `to_h` and `to_a` omit the dimension, returning everything that can be materialised. `dimensions` and `derived_dimensions` still list it — it is queryable, with arguments. To materialise particular values, bind the arguments in a one-arity wrapper and ask for that:
721
721
 
722
722
  ```ruby
723
723
  prices[:sma_close_20] = proc{|row| row[:sma, :close, 20]}
@@ -947,7 +947,7 @@ The transforming and reducing methods are deliberately left as Enumerable's defa
947
947
 
948
948
  ### Extracting data
949
949
 
950
- `to_a` returns an array of hashes — the row-oriented form:
950
+ `to_a` returns an array of hashes — the row-oriented form, carrying derived dimensions alongside the stored ones, so it agrees with `to_h` and with `dimensions` about what the Namo holds:
951
951
 
952
952
  ```ruby
953
953
  sales[:product, :quarter, :revenue].to_a
@@ -959,6 +959,8 @@ sales[:product, :quarter, :revenue].to_a
959
959
  # ]
960
960
  ```
961
961
 
962
+ `data` is the stored rows, without derived dimensions and without copying — it is the accessor for what the Namo was built from, where `to_a` is the conversion of what it can answer. A formula whose inputs a projection has cut raises through `to_a` as it does through `to_h` and `values`, which is [the caller's own choice](#projection-of-derived-dimensions) rather than a failure of the conversion.
963
+
962
964
  `to_h` returns a hash of arrays — the columnar form (see [Coordinates and values](#coordinates-and-values) above):
963
965
 
964
966
  ```ruby
@@ -994,14 +996,27 @@ Evaluation is bounded by the row cap rather than by the data — at most ten row
994
996
 
995
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`.
996
998
 
997
- 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.
998
1004
 
999
1005
  ```ruby
1000
1006
  sales.first
1001
1007
  # => #<Namo::Row {product: "Widget", quarter: "Q1", price: 10.0, quantity: 100, revenue: 1000.0} derived: [:revenue]>
1002
1008
 
1003
1009
  sales.group_by(:quarter)
1004
- # => #<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>
1005
1020
  ```
1006
1021
 
1007
1022
  ### Named Namos
@@ -1202,7 +1217,14 @@ Freeze-gated memoisation is a 2.x optimisation — opt-in via `freeze`, transpar
1202
1217
 
1203
1218
  ```ruby
1204
1219
  prices.group_by(:symbol)
1205
- # => #<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>
1206
1228
 
1207
1229
  prices.group_by(:symbol).summary(:close, reducer: :mean)
1208
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.2'
5
+ VERSION = '0.31.4'
6
6
  end
data/lib/namo.rb CHANGED
@@ -241,10 +241,16 @@ class Namo
241
241
  other.subset_of_rows?(self)
242
242
  end
243
243
 
244
+ # Each row's own keys, in their own order, and then the derived dimensions which
245
+ # can be materialised without arguments — the rule values, coordinates and to_h
246
+ # already follow. data is the stored rows, and stays so.
244
247
  def to_a
245
- @data.map do |row|
246
- row.keys.each_with_object({}) do |key, hash|
247
- hash[key] = row[key]
248
+ materialising do
249
+ derived = materialisable_dimensions - data_dimensions
250
+ @data.map do |row|
251
+ subject = Row.new(row, @formulae, self)
252
+ stored = row.keys.each_with_object({}){|key, hash| hash[key] = row[key]}
253
+ derived.each_with_object(stored){|dimension, hash| hash[dimension] = subject[dimension]}
248
254
  end
249
255
  end
250
256
  end
@@ -256,6 +262,16 @@ class Namo
256
262
 
257
263
  protected
258
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
+
259
275
  def row_multiset
260
276
  @data.tally
261
277
  end
@@ -272,6 +288,13 @@ class Namo
272
288
 
273
289
  private
274
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
+
275
298
  def inspected_name
276
299
  @name.nil? ? '' : " #{@name.inspect}"
277
300
  end
@@ -302,19 +325,19 @@ class Namo
302
325
  # Only those the rows above do not already carry. A formula which raises, and a
303
326
  # parameterised one which cannot be materialised without its arguments, render
304
327
  # no value and would otherwise be nowhere in the output at all. A Collection
305
- # 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.
306
329
  def inspected_derived(rendered = [])
307
- unshown = derived_dimensions.reject do |dimension|
308
- rendered.any? && rendered.all?{|_, derived| derived.key?(dimension)}
309
- end
330
+ inspected_derived_names(unshown_derived(rendered))
331
+ end
332
+
333
+ def inspected_derived_names(unshown)
310
334
  unshown.empty? ? '' : " derived: #{unshown.inspect}"
311
335
  end
312
336
 
313
- def initialize(positional_data = nil, data: [], formulae: {}, name: nil)
314
- @data = positional_data || data
315
- @formulae = formulae.is_a?(Formulae) ? formulae : Formulae.new(formulae)
316
- @name = name
317
- 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
318
341
  end
319
342
 
320
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
 
data/test/console_test.rb CHANGED
@@ -33,7 +33,7 @@ describe 'script/console' do
33
33
 
34
34
  it "holds what the fixtures hold, so it cannot drift from the demo" do
35
35
  expected = eval(Fixtures.readings)
36
- _(console('readings.to_a == ' + expected.inspect)).must_match(/true/)
36
+ _(console('readings.data == ' + expected.inspect)).must_match(/true/)
37
37
  end
38
38
 
39
39
  it "starts without echoing its own source" do
data/test/namo_test.rb CHANGED
@@ -488,8 +488,8 @@ describe Namo do
488
488
  sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
489
489
  result = sales[revenue: ->(v){v >= 1500.0}]
490
490
  _(result.to_a).must_equal [
491
- {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
492
- {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
491
+ {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150, revenue: 1500.0},
492
+ {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60, revenue: 1500.0}
493
493
  ]
494
494
  end
495
495
  end
@@ -571,8 +571,8 @@ describe Namo do
571
571
  sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
572
572
  result = sales[revenue: 1200.0..]
573
573
  _(result.to_a).must_equal [
574
- {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
575
- {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
574
+ {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150, revenue: 1500.0},
575
+ {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60, revenue: 1500.0}
576
576
  ]
577
577
  end
578
578
  end
@@ -602,7 +602,7 @@ describe Namo do
602
602
  sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
603
603
  sales[:cost] = proc{|r| r[:quantity] * 4.0}
604
604
  sales[:profit] = proc{|r| r[:revenue] - r[:cost]}
605
- _(sales[:product, :quarter, :profit].to_a).must_equal [
605
+ _(sales[:product, :quarter, :profit].data).must_equal [
606
606
  {product: "Widget", quarter: "Q1", profit: 600.0},
607
607
  {product: "Widget", quarter: "Q2", profit: 900.0},
608
608
  {product: "Gadget", quarter: "Q1", profit: 840.0},
@@ -1657,8 +1657,8 @@ describe Namo do
1657
1657
  sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
1658
1658
  result = sales.select{|row| row[:revenue] >= 1500.0}
1659
1659
  _(result.to_a).must_equal [
1660
- {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
1661
- {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
1660
+ {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150, revenue: 1500.0},
1661
+ {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60, revenue: 1500.0}
1662
1662
  ]
1663
1663
  end
1664
1664
 
@@ -2458,7 +2458,7 @@ describe Namo do
2458
2458
  dated[:cutoff] = proc{|r| r[:date]}
2459
2459
  result = dated.*(quarterly){|row, candidates| candidates[quarter_end: ->(qe){qe <= row[:cutoff]}].sort_by{|f| f[:quarter_end]}.last(1)}
2460
2460
  _(result.to_a).must_equal [
2461
- {symbol: 'BHP', date: '2025-05-20', close: 44.0, quarter_end: '2025-03-31', eps: 1.2}
2461
+ {symbol: 'BHP', date: '2025-05-20', close: 44.0, quarter_end: '2025-03-31', eps: 1.2, cutoff: '2025-05-20'}
2462
2462
  ]
2463
2463
  end
2464
2464
 
@@ -2638,8 +2638,8 @@ describe Namo do
2638
2638
  weighted_tiers[:premium] = proc{|t| t[:max_weight] > 15}
2639
2639
  result = orders.**(weighted_tiers){|row, candidates| candidates[premium: ->(v){v}]}
2640
2640
  _(result.to_a).must_equal [
2641
- {order: 'A', weight: 5, tier: 'heavy', max_weight: 20},
2642
- {order: 'B', weight: 15, tier: 'heavy', max_weight: 20}
2641
+ {order: 'A', weight: 5, tier: 'heavy', max_weight: 20, premium: true},
2642
+ {order: 'B', weight: 15, tier: 'heavy', max_weight: 20, premium: true}
2643
2643
  ]
2644
2644
  end
2645
2645
 
@@ -3067,6 +3067,41 @@ describe Namo do
3067
3067
  it "returns the data as an array of hashes" do
3068
3068
  _(sales.to_a).must_equal sample_data
3069
3069
  end
3070
+
3071
+ it "carries a derived dimension alongside the stored ones" do
3072
+ sales[:revenue] = proc{|row| row[:price] * row[:quantity]}
3073
+ _(sales.to_a.first).must_equal(
3074
+ {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100, revenue: 1000.0})
3075
+ end
3076
+
3077
+ it "agrees with to_h about which dimensions there are" do
3078
+ sales[:revenue] = proc{|row| row[:price] * row[:quantity]}
3079
+ _(sales.to_a.first.keys).must_equal sales.to_h.keys
3080
+ _(sales.to_a.first.keys).must_equal sales.dimensions
3081
+ end
3082
+
3083
+ it "omits a formula which cannot be materialised without its arguments" do
3084
+ sales[:revenue] = proc{|row| row[:price] * row[:quantity]}
3085
+ sales[:scaled] = proc{|row, namo, factor| row[:price] * factor}
3086
+ _(sales.to_a.first.keys).wont_include :scaled
3087
+ _(sales.to_a.first.keys).must_include :revenue
3088
+ _(sales.derived_dimensions).must_include :scaled
3089
+ end
3090
+
3091
+ it "raises where a carried formula's inputs were projected away, as 0.16.0 settled" do
3092
+ sales[:revenue] = proc{|row| row[:price] * row[:quantity]}
3093
+ _(->{sales[:product, :quarter].to_a}).must_raise NoMethodError
3094
+ end
3095
+
3096
+ it "leaves data as the stored rows" do
3097
+ sales[:revenue] = proc{|row| row[:price] * row[:quantity]}
3098
+ _(sales.data).must_equal sample_data
3099
+ _(sales.data.first.keys).wont_include :revenue
3100
+ end
3101
+
3102
+ it "returns fresh rows rather than the stored ones" do
3103
+ _(sales.to_a.first).wont_be_same_as sales.data.first
3104
+ end
3070
3105
  end
3071
3106
 
3072
3107
  describe "#group_by" do
@@ -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.2
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: []