namo 0.31.4 → 0.31.6

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: 3a5b005cb03bb42a9a9319632833f382a8ae2480ce04160f326a831f534da50a
4
- data.tar.gz: f344e285a0d457af4ad633c98a33aab02cefe49fd273251ad9bdc7bdd3d98afa
3
+ metadata.gz: 062a5ecf9fba2666e680bc2842d7569bd07a2e00f521cb6ad583a2b13f22d5c4
4
+ data.tar.gz: 7333a5d541ee742f49796d09949b7b2c89ac056635449ce9f1c1e0733ef757b0
5
5
  SHA512:
6
- metadata.gz: 8025bdb29748fc3076649dc59a38139075c03a9692c5c684295cb5060334e8e8c0c45dea9001dd942d68d0e3302910464c9141157a42d61af85eb751420bf0d8
7
- data.tar.gz: 8fc50fc83aab71b68cb6e70dc4178e95dc8e2f3e6983480ccbcc4553915b61dde52f00cf8d2f1570988d5129df7e0534b13db8b3dc7413602d1fefc64b694613
6
+ metadata.gz: 705357a13d1495c3a96fbc470fb4f796f7d8eb9a40a82552708801c3969972dc2cf59255f64a8d7ad0d3479442980f3e5a6748d9b4e16992e70a5258291a787b
7
+ data.tar.gz: 911df517eaeafb86233af02c3d56a92dfc27afa4cc7cadc8e8b4b02e6f16e52df8ee683a79de7be57f89480f8c9420c02b2d8a8bd3b8a4e75606561d5ce3850a
data/CHANGELOG CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## 20260826
4
4
 
5
+ 0.31.6: ~ Namo::Collection: a row operation yields a Namo rather than a memberless Collection.
6
+
7
+ 1. + Namo#return_class: the class an operation returns, protected, returning self.class. The twenty-two sites which built what they return — projection and contraction, the five set operators, the two composition operators, and the subset Enumerable methods in Namo::Enumerable — now ask for it rather than naming self.class themselves.
8
+ 2. ~ Namo::Collection#return_class: returns Namo. A Collection's kind is its members and a row operation carries none across, so what came back was a Collection with none: it answered summary with an empty Namo rather than raising, and rendered as [] beside a row count. 0.18.0 has these operations read the detail view and behave as the detail, and the detail is a Namo; this is the code meeting that sentence.
9
+ 3. ~ README.md: the four views, on what a row operation on a Collection returns.
10
+ 4. ~ ROADMAP.md: + 0.31.6.
11
+ 5. ~ Namo::VERSION: /0.31.5/0.31.6/
12
+
13
+
14
+ 0.31.5: ~ Namo::Collection#detail, #as_detail: the detail view carries the members' formulae.
15
+
16
+ 1. ~ Namo::Collection#detail: the returned Namo carries the members' formulae, folded in member order so that a later member wins a name collision, as a later member wins a collision of names in <<. It had been built with none, so the view documented at 0.18.0 as the lossless union of the members' rows dropped every derived dimension: group_by hands each member the source's formulae, and detail handed them back stripped.
17
+ 2. ~ Namo::Collection#as_detail: merges the detail view's formulae into the Collection's own, the Collection having become that view. Without it the round trip still lost them, as_detail having taken only the data.
18
+ 3. ~ script/demo: the collections section checks the round trip with === rather than ==, == comparing row multisets alone and so unable to see the formulae go missing, which is why this stood.
19
+ 4. ~ README.md: the four views, on which of them carries formulae and why.
20
+ 5. ~ ROADMAP.md: + 0.31.5.
21
+ 6. ~ Namo::VERSION: /0.31.4/0.31.5/
22
+
23
+
5
24
  0.31.4: ~ Namo::Collection#inspect: the members are rendered, nested under their names.
6
25
 
7
26
  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.
data/README.md CHANGED
@@ -1119,8 +1119,12 @@ gt[component: 'engine'].values(:cost)
1119
1119
 
1120
1120
  Detail is the lazy view because a Collection's rows simply *are* its members' rows; a summary is a reduction you pose against them, so it is never reached by accident — only through `summary` or `as_summary`.
1121
1121
 
1122
+ An inherited row-operation on a Collection — selection, projection, `select`, `sort_by`, the set and composition operators — returns a `Namo`, not a Collection. It reads the detail view and behaves as the detail, and the detail is a Namo. A Collection is defined by its members, and a row operation carries none of them across, so a Collection is not what the result is: asking one for a `summary` should say there is none to give rather than answer for no members. A subclass of `Namo` is unaffected — a projection of a `PriceData` is a `PriceData`.
1123
+
1122
1124
  #### Four view methods
1123
1125
 
1126
+ `detail` carries the members' formulae, its rows being the members' rows — folded in member order, so a later member wins a name collision as it wins a collision of names in `<<`. A `summary`'s rows are reductions, holding neither the dimensions those formulae read nor the rows they read them from, so it carries none.
1127
+
1124
1128
  The views come in a non-mutating pair and a mutating pair:
1125
1129
 
1126
1130
  - `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.
@@ -42,7 +42,7 @@ class Namo
42
42
  member.data.map{|row| row.key?(by) ? row : row.merge(by => member.name)}
43
43
  end
44
44
  )
45
- Namo.new(rows)
45
+ Namo.new(rows, formulae: member_formulae)
46
46
  end
47
47
 
48
48
  def as_summary(dimension = nil, by: :member, reducer: :sum, &block)
@@ -52,7 +52,9 @@ class Namo
52
52
 
53
53
  def as_detail(positional_by = nil, by: :member)
54
54
  by = positional_by || by
55
- @data = detail(by).data
55
+ view = detail(by)
56
+ @data = view.data
57
+ @formulae = @formulae.merge(view.formulae)
56
58
  self
57
59
  end
58
60
 
@@ -68,8 +70,27 @@ class Namo
68
70
  "#<#{self.class}#{inspected_name} #{inspected_members} #{@data.length} rows#{inspected_derived}>"
69
71
  end
70
72
 
73
+ protected
74
+
75
+ # A row operation on a Collection yields a Namo: 0.18.0 has it read the detail
76
+ # view and behave as the detail, and the detail is a Namo. Built as a
77
+ # Collection it would be one with no members, answering summary with an empty
78
+ # Namo rather than saying it cannot.
79
+ def return_class
80
+ Namo
81
+ end
82
+
71
83
  private
72
84
 
85
+ # The members' formulae, folded in member order so that a later member wins a
86
+ # name collision, as a later member wins a collision of names in <<. The
87
+ # detail view is the members' rows, so it must be able to answer what the
88
+ # members can answer of them; group_by hands every member the same formulae,
89
+ # so only an assembled Collection can have members which disagree.
90
+ def member_formulae
91
+ @members.map(&:formulae).reduce(Formulae.new){|merged, formulae| merged.merge(formulae)}
92
+ end
93
+
73
94
  def inspected_members
74
95
  return '[]' if @members.empty?
75
96
  shown = [@members.length, INSPECTED_ROWS].min
@@ -11,22 +11,22 @@ class Namo
11
11
  end
12
12
 
13
13
  def select(&block)
14
- self.class.new(@data.select{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
14
+ return_class.new(@data.select{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
15
15
  end
16
16
  alias_method :filter, :select
17
17
  alias_method :find_all, :select
18
18
 
19
19
  def reject(&block)
20
- self.class.new(@data.reject{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
20
+ return_class.new(@data.reject{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
21
21
  end
22
22
 
23
23
  def sort_by(&block)
24
- self.class.new(@data.sort_by{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
24
+ return_class.new(@data.sort_by{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
25
25
  end
26
26
 
27
27
  def first(n = nil)
28
28
  if n
29
- self.class.new(@data.first(n), formulae: @formulae.dup)
29
+ return_class.new(@data.first(n), formulae: @formulae.dup)
30
30
  else
31
31
  @data.first ? Row.new(@data.first, @formulae, self) : nil
32
32
  end
@@ -34,38 +34,38 @@ class Namo
34
34
 
35
35
  def last(n = nil)
36
36
  if n
37
- self.class.new(@data.last(n), formulae: @formulae.dup)
37
+ return_class.new(@data.last(n), formulae: @formulae.dup)
38
38
  else
39
39
  @data.last ? Row.new(@data.last, @formulae, self) : nil
40
40
  end
41
41
  end
42
42
 
43
43
  def take(n)
44
- self.class.new(@data.take(n), formulae: @formulae.dup)
44
+ return_class.new(@data.take(n), formulae: @formulae.dup)
45
45
  end
46
46
 
47
47
  def drop(n)
48
- self.class.new(@data.drop(n), formulae: @formulae.dup)
48
+ return_class.new(@data.drop(n), formulae: @formulae.dup)
49
49
  end
50
50
 
51
51
  def take_while(&block)
52
- self.class.new(@data.take_while{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
52
+ return_class.new(@data.take_while{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
53
53
  end
54
54
 
55
55
  def drop_while(&block)
56
- self.class.new(@data.drop_while{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
56
+ return_class.new(@data.drop_while{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
57
57
  end
58
58
 
59
59
  def uniq(&block)
60
60
  rows = block ? @data.uniq{|row| block.call(Row.new(row, @formulae, self))} : @data.uniq
61
- self.class.new(rows, formulae: @formulae.dup)
61
+ return_class.new(rows, formulae: @formulae.dup)
62
62
  end
63
63
 
64
64
  def partition(&block)
65
65
  matches, non_matches = @data.partition{|row| block.call(Row.new(row, @formulae, self))}
66
66
  [
67
- self.class.new(matches, formulae: @formulae.dup),
68
- self.class.new(non_matches, formulae: @formulae.dup),
67
+ return_class.new(matches, formulae: @formulae.dup),
68
+ return_class.new(non_matches, formulae: @formulae.dup),
69
69
  ]
70
70
  end
71
71
 
@@ -74,7 +74,7 @@ class Namo
74
74
  source = derived_dimensions.include?(dimension) ? self[*data_dimensions, dimension] : self
75
75
  members = (
76
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)
77
+ return_class.new(rows, formulae: source.formulae.dup, name: value)
78
78
  end
79
79
  )
80
80
  collection << members
data/lib/Namo/VERSION.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.31.4'
5
+ VERSION = '0.31.6'
6
6
  end
data/lib/namo.rb CHANGED
@@ -79,7 +79,7 @@ class Namo
79
79
  end
80
80
  )
81
81
  carried = positive.any? ? @formulae.reject{|name, _| positive.include?(name)} : @formulae.dup
82
- self.class.new(projected, formulae: carried)
82
+ return_class.new(projected, formulae: carried)
83
83
  end
84
84
 
85
85
  def []=(name, value)
@@ -124,31 +124,31 @@ class Namo
124
124
  def +(other)
125
125
  raise_unless_namo(other)
126
126
  raise_unless_matching_data_dimensions(other)
127
- self.class.new(@data + other.data, formulae: other.formulae.merge(@formulae))
127
+ return_class.new(@data + other.data, formulae: other.formulae.merge(@formulae))
128
128
  end
129
129
 
130
130
  def -(other)
131
131
  raise_unless_namo(other)
132
132
  raise_unless_matching_data_dimensions(other)
133
- self.class.new(@data - other.data, formulae: @formulae.dup)
133
+ return_class.new(@data - other.data, formulae: @formulae.dup)
134
134
  end
135
135
 
136
136
  def &(other)
137
137
  raise_unless_namo(other)
138
138
  raise_unless_matching_data_dimensions(other)
139
- self.class.new(@data & other.data, formulae: @formulae.dup)
139
+ return_class.new(@data & other.data, formulae: @formulae.dup)
140
140
  end
141
141
 
142
142
  def |(other)
143
143
  raise_unless_namo(other)
144
144
  raise_unless_matching_data_dimensions(other)
145
- self.class.new((@data | other.data), formulae: other.formulae.merge(@formulae))
145
+ return_class.new((@data | other.data), formulae: other.formulae.merge(@formulae))
146
146
  end
147
147
 
148
148
  def ^(other)
149
149
  raise_unless_namo(other)
150
150
  raise_unless_matching_data_dimensions(other)
151
- self.class.new((@data - other.data) + (other.data - @data), formulae: other.formulae.merge(@formulae))
151
+ return_class.new((@data - other.data) + (other.data - @data), formulae: other.formulae.merge(@formulae))
152
152
  end
153
153
 
154
154
  def *(other, &block)
@@ -167,7 +167,7 @@ class Namo
167
167
  matched.each{|right_row| combined_data << left_row.merge(right_row)}
168
168
  end
169
169
  end
170
- self.class.new(combined_data, formulae: other.formulae.merge(@formulae))
170
+ return_class.new(combined_data, formulae: other.formulae.merge(@formulae))
171
171
  end
172
172
 
173
173
  def **(other, &block)
@@ -184,7 +184,7 @@ class Namo
184
184
  other.data.each{|right_row| combined_data << left_row.merge(right_row)}
185
185
  end
186
186
  end
187
- self.class.new(combined_data, formulae: other.formulae.merge(@formulae))
187
+ return_class.new(combined_data, formulae: other.formulae.merge(@formulae))
188
188
  end
189
189
 
190
190
  def /(other)
@@ -193,7 +193,7 @@ class Namo
193
193
  projected = @data.map do |row|
194
194
  kept.each_with_object({}){|dim, hash| hash[dim] = row[dim]}
195
195
  end
196
- self.class.new(projected.uniq, formulae: @formulae.dup)
196
+ return_class.new(projected.uniq, formulae: @formulae.dup)
197
197
  end
198
198
 
199
199
  def ==(other)
@@ -262,6 +262,13 @@ class Namo
262
262
 
263
263
  protected
264
264
 
265
+ # The class an operation returns. A subclass is its own kind of thing, so a
266
+ # projection of a PriceData is a PriceData; Namo::Collection is not, its kind
267
+ # being its members, which a row operation carries none of.
268
+ def return_class
269
+ self.class
270
+ end
271
+
265
272
  # The rendered rows, the count of those not shown, and the derived dimensions
266
273
  # none of them carries. Shared with Collection, which renders its members' rows
267
274
  # rather than its own and must report what a member would report of itself.
@@ -347,6 +347,102 @@ describe Namo::Collection do
347
347
  end
348
348
  end
349
349
 
350
+ # 0.18.0 has an inherited row-operation read the detail view and "behave as the
351
+ # detail", and the detail is a Namo. Built as a Collection the result was one
352
+ # with no members: it answered summary with an empty Namo rather than saying it
353
+ # had no summary to give, and rendered as [] beside a row count.
354
+ describe "the class of a derived result" do
355
+ def grouped
356
+ Namo.new([
357
+ {station: "Melbourne", month: "2025-01", high_temp: 26.4},
358
+ {station: "Perth", month: "2025-01", high_temp: 32.0},
359
+ {station: "Perth", month: "2025-02", high_temp: 29.5}]).group_by(:month)
360
+ end
361
+
362
+ it "is a Namo for selection and projection" do
363
+ _(grouped[station: "Perth"]).must_be_instance_of Namo
364
+ _(grouped[:station, :high_temp]).must_be_instance_of Namo
365
+ end
366
+
367
+ it "is a Namo for the subset Enumerable methods" do
368
+ collection = grouped
369
+ _(collection.select{|row| row[:station] == "Perth"}).must_be_instance_of Namo
370
+ _(collection.sort_by{|row| row[:high_temp]}).must_be_instance_of Namo
371
+ _(collection.first(2)).must_be_instance_of Namo
372
+ _(collection.uniq).must_be_instance_of Namo
373
+ end
374
+
375
+ it "is a Namo for the set and composition operators" do
376
+ collection = grouped
377
+ _(collection + collection).must_be_instance_of Namo
378
+ _(collection - collection).must_be_instance_of Namo
379
+ _(collection & collection).must_be_instance_of Namo
380
+ end
381
+
382
+ it "carries the rows the operation selected" do
383
+ _(grouped[station: "Perth"].values(:high_temp)).must_equal [32.0, 29.5]
384
+ end
385
+
386
+ # The point of it: a summary of no members is not an empty summary.
387
+ it "has no summary to give, rather than an empty one" do
388
+ _{grouped[station: "Perth"].summary(:high_temp)}.must_raise NoMethodError
389
+ end
390
+
391
+ it "leaves a subclass its own kind" do
392
+ priced = Class.new(Namo)
393
+ instance = priced.new([{a: 1}, {a: 2}])
394
+ _(instance.select{|row| row[:a] > 1}).must_be_instance_of priced
395
+ _(instance[:a]).must_be_instance_of priced
396
+ end
397
+
398
+ it "leaves group_by returning a Collection" do
399
+ _(grouped).must_be_instance_of Namo::Collection
400
+ _(grouped.members.length).must_equal 2
401
+ end
402
+ end
403
+
404
+ describe "the detail view and its formulae" do
405
+ def readings
406
+ namo = Namo.new([
407
+ {station: "Melbourne", month: "2025-01", high_temp: 26.4, mean_temp: 25.9},
408
+ {station: "Perth", month: "2025-01", high_temp: 32.0, mean_temp: 30.0}])
409
+ namo[:anomaly] = proc{|row| (row[:high_temp] - row[:mean_temp]).round(1)}
410
+ namo
411
+ end
412
+
413
+ it "carries the members' formulae, the view being the members' rows" do
414
+ _(readings.group_by(:station).detail(:station).derived_dimensions).must_equal [:anomaly]
415
+ end
416
+
417
+ it "answers a derived dimension through the detail view" do
418
+ _(readings.group_by(:station).detail(:station).values(:anomaly)).must_equal [0.5, 2.0]
419
+ end
420
+
421
+ # == compares row multisets and cannot see a formula go missing, so it called
422
+ # the partition exact while the derived dimensions were being dropped. === is
423
+ # the operator which compares the formula names too.
424
+ it "inverts the partition exactly, formulae and all" do
425
+ source = readings
426
+ _(source.group_by(:station).as_detail(:station)).must_be :===, source
427
+ end
428
+
429
+ it "folds the members' formulae, a later member winning the name" do
430
+ first = Namo.new([{a: 1}], name: :first)
431
+ first[:b] = proc{|row| :from_first}
432
+ second = Namo.new([{a: 2}], name: :second)
433
+ second[:b] = proc{|row| :from_second}
434
+ collection = Namo::Collection.new
435
+ collection << first << second
436
+ _(collection.detail.values(:b)).must_equal [:from_second, :from_second]
437
+ end
438
+
439
+ # A summary's rows are reductions, holding neither the dimensions the members'
440
+ # formulae read nor the rows they read them from, so it carries none.
441
+ it "leaves the summary without them" do
442
+ _(readings.group_by(:station).summary(:high_temp).derived_dimensions).must_equal []
443
+ end
444
+ end
445
+
350
446
  describe "#inspect" do
351
447
  def collection_of(members, rows_each)
352
448
  rows = (1..members).flat_map{|member| (1..rows_each).map{|row| {group: "g#{member}", n: row}}}
data/test/console_test.rb CHANGED
@@ -4,17 +4,20 @@ require 'minitest/autorun'
4
4
  require 'minitest-spec-context'
5
5
 
6
6
  require_relative '../lib/namo'
7
- require_relative '../script/fixtures'
8
7
 
9
- # script/console exists so that a question asked at a prompt is asked of the same
10
- # data the demo put on the screen. That is only true while both read
11
- # script/fixtures.rb, so what is worth asserting is the agreement rather than the
12
- # session: the names it defines, and that they hold what the fixtures say.
8
+ # bin/console is the conventional gem prompt: irb with the library loaded and
9
+ # nothing else. It held the demo's fixtures until 20260826, so that a question
10
+ # asked here was asked of the data the demo had shown — a guarantee the demo's
11
+ # own `i` now keeps better, opening irb on the binding the run is using rather
12
+ # than on a second process holding equal values.
13
+ #
14
+ # What is left to assert is that it starts, that Namo is there, and that it needs
15
+ # nothing off the load path.
13
16
 
14
- describe 'script/console' do
17
+ describe 'bin/console' do
15
18
  def console(*expressions)
16
19
  root = File.expand_path('..', __dir__)
17
- IO.popen([File.join(root, 'script', 'console'), '--prompt', 'simple'],
20
+ IO.popen([File.join(root, 'bin', 'console'), '--prompt', 'simple'],
18
21
  'r+', chdir: root, err: [:child, :out]) do |io|
19
22
  io.puts(expressions, 'exit')
20
23
  io.close_write
@@ -22,21 +25,22 @@ describe 'script/console' do
22
25
  end
23
26
  end
24
27
 
25
- it "defines the names it says it does" do
26
- output = console('[readings, stations, months].map{|n| n.class}.inspect')
27
- _(output).must_match(/\[Namo, Namo, Namo\]/)
28
+ it "starts with Namo loaded" do
29
+ _(console('Namo.new([{a: 1}]).dimensions.inspect')).must_match(/\[:a\]/)
28
30
  end
29
31
 
30
- it "gives sales the revenue formula" do
31
- _(console('readings.derived_dimensions.inspect')).must_match(/\[:anomaly\]/)
32
- end
33
-
34
- it "holds what the fixtures hold, so it cannot drift from the demo" do
35
- expected = eval(Fixtures.readings)
36
- _(console('readings.data == ' + expected.inspect)).must_match(/true/)
32
+ it "starts without echoing its own source" do
33
+ _(console('1')).wont_match(/IRB\.start/)
37
34
  end
38
35
 
39
- it "starts without echoing its own source" do
40
- _(console('1')).wont_match(/binding\.irb/)
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, 'bin', 'console'), '--prompt', 'simple'],
39
+ 'r+', chdir: root, err: [:child, :out]) do |io|
40
+ io.puts('exit')
41
+ io.close_write
42
+ io.read
43
+ end
44
+ _(output).wont_match(/LoadError/)
41
45
  end
42
46
  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.4
4
+ version: 0.31.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran