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 +4 -4
- data/CHANGELOG +21 -0
- data/README.md +27 -5
- data/lib/Namo/Collection.rb +42 -5
- data/lib/Namo/VERSION.rb +1 -1
- data/lib/namo.rb +35 -12
- data/test/Namo/Collection_test.rb +93 -3
- data/test/console_test.rb +1 -1
- data/test/namo_test.rb +45 -10
- data/test/sizing_test.rb +42 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3a5b005cb03bb42a9a9319632833f382a8ae2480ce04160f326a831f534da50a
|
|
4
|
+
data.tar.gz: f344e285a0d457af4ad633c98a33aab02cefe49fd273251ad9bdc7bdd3d98afa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 `
|
|
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.
|
|
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
|
|
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
|
|
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
|
data/lib/Namo/Collection.rb
CHANGED
|
@@ -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 =
|
|
40
|
-
|
|
41
|
-
|
|
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}
|
|
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
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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].
|
|
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
|
data/test/sizing_test.rb
ADDED
|
@@ -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.
|
|
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.
|
|
104
|
+
rubygems_version: 4.0.19
|
|
104
105
|
specification_version: 4
|
|
105
106
|
summary: Named dimensional data for Ruby.
|
|
106
107
|
test_files: []
|