namo 0.31.0 → 0.31.1
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 +11 -0
- data/README.md +10 -8
- data/lib/Namo/Row.rb +16 -1
- data/lib/Namo/VERSION.rb +1 -1
- data/lib/namo.rb +18 -1
- data/test/Namo/Row_test.rb +8 -2
- data/test/console_test.rb +42 -0
- data/test/demo_test.rb +88 -0
- data/test/namo_test.rb +20 -2
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e5235177577fcae1dd01a8a3172fc3933e77a5483f123ba6f436bd58a6eca68e
|
|
4
|
+
data.tar.gz: b2c023063fa95cca3f6ec38f324a116bf013339003d4051338a7c84b57251944
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3278fdcba0888a5899930abb399c3cc3b1aff49d7bb27ceefc223524c4247cb4c85ca0274ba58254965229e9cec131c014a544fce61444e8d6e34221896047e5
|
|
7
|
+
data.tar.gz: 94cdb60c0bc852b456ded744a63f8e7fda0dbf884d2ff085b0908cfb80d26f48588b4254b7d389cb68b4bce0f668d3a528ce5eeba22bd80ea4a50331c6ccb2e8
|
data/CHANGELOG
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 20260824
|
|
4
|
+
|
|
5
|
+
0.31.1: ~ Namo#inspect, Namo::Row#inspect: derived dimensions render their values.
|
|
6
|
+
|
|
7
|
+
1. ~ Namo#inspect: a derived dimension renders with its value, in among the stored ones, rather than being named only. Where there is no value to show it is omitted and the derived: suffix names it alone: a formula which raises, and a parameterised one which cannot be materialised without its arguments.
|
|
8
|
+
2. ~ Namo::Row#inspect: the same.
|
|
9
|
+
3. ~ README.md: the Inspection output section rewritten, and the two blocks whose rendering changes.
|
|
10
|
+
4. ~ ROADMAP.md: + 0.31.1.
|
|
11
|
+
5. ~ Namo::VERSION: /0.31.0/0.31.1/
|
|
12
|
+
|
|
13
|
+
|
|
3
14
|
## 20260822
|
|
4
15
|
|
|
5
16
|
0.31.0: + Namo#inspect, Namo::Collection#inspect, Namo::Row#inspect, and Namo::Formulae#inspect.
|
data/README.md
CHANGED
|
@@ -972,29 +972,31 @@ sales[:product, :quarter, :revenue].to_h
|
|
|
972
972
|
|
|
973
973
|
### Inspection output
|
|
974
974
|
|
|
975
|
-
`inspect` renders the class, the stored rows, and the names of
|
|
975
|
+
`inspect` renders the class, the stored rows with any derived dimensions among them, and the names of those derived dimensions:
|
|
976
976
|
|
|
977
977
|
```ruby
|
|
978
978
|
sales.inspect
|
|
979
979
|
# => #<Namo [
|
|
980
|
-
# {product: "Widget", quarter: "Q1", price: 10.0, quantity: 100},
|
|
981
|
-
# {product: "Widget", quarter: "Q2", price: 10.0, quantity: 150},
|
|
982
|
-
# {product: "Gadget", quarter: "Q1", price: 25.0, quantity: 40},
|
|
983
|
-
# {product: "Gadget", quarter: "Q2", price: 25.0, quantity: 60}
|
|
980
|
+
# {product: "Widget", quarter: "Q1", price: 10.0, quantity: 100, revenue: 1000.0},
|
|
981
|
+
# {product: "Widget", quarter: "Q2", price: 10.0, quantity: 150, revenue: 1500.0},
|
|
982
|
+
# {product: "Gadget", quarter: "Q1", price: 25.0, quantity: 40, revenue: 1000.0},
|
|
983
|
+
# {product: "Gadget", quarter: "Q2", price: 25.0, quantity: 60, revenue: 1500.0}
|
|
984
984
|
# ] derived: [:revenue]>
|
|
985
985
|
```
|
|
986
986
|
|
|
987
987
|
A name, where the Namo carries one, follows the class: `#<Namo :sales [`. A subclass reports its own name in place of `Namo`.
|
|
988
988
|
|
|
989
|
-
|
|
989
|
+
A derived dimension shows its value, in among the stored ones, and the `derived:` suffix names which of the keys are computed. Where there is no value to show it is omitted rather than the rendering failing: a formula which raises, and a parameterised one which cannot be materialised without its arguments, each render nothing and are named by the suffix alone.
|
|
990
|
+
|
|
991
|
+
Evaluation is bounded by the row cap rather than by the data — at most ten rows are rendered, so at most ten evaluations per formula. A collection-scoped formula makes each of those a pass over the Namo, so inspecting a large one that carries such a formula is not free.
|
|
990
992
|
|
|
991
993
|
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`.
|
|
992
994
|
|
|
993
|
-
A `Row` renders itself and its formula names, 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.
|
|
995
|
+
A `Row` renders itself, its derived values and its formula names, 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.
|
|
994
996
|
|
|
995
997
|
```ruby
|
|
996
998
|
sales.first
|
|
997
|
-
# => #<Namo::Row {product: "Widget", quarter: "Q1", price: 10.0, quantity: 100} derived: [:revenue]>
|
|
999
|
+
# => #<Namo::Row {product: "Widget", quarter: "Q1", price: 10.0, quantity: 100, revenue: 1000.0} derived: [:revenue]>
|
|
998
1000
|
|
|
999
1001
|
sales.group_by(:quarter)
|
|
1000
1002
|
# => #<Namo::Collection members: ["Q1", "Q2"], 4 rows>
|
data/lib/Namo/Row.rb
CHANGED
|
@@ -44,11 +44,26 @@ class Namo
|
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def inspect
|
|
47
|
-
"#<#{self.class} #{
|
|
47
|
+
"#<#{self.class} #{inspected_row}#{inspected_derived}>"
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
private
|
|
51
51
|
|
|
52
|
+
def inspected_row
|
|
53
|
+
@row.merge(inspected_derivations).inspect
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# As Namo#inspected_derivations: the value where there is one to show, and
|
|
57
|
+
# the derived list to name it either way.
|
|
58
|
+
def inspected_derivations
|
|
59
|
+
@formulae.keys.each_with_object({}) do |dimension, derived|
|
|
60
|
+
next if @formulae.required_parameter_count(dimension) > 2
|
|
61
|
+
derived[dimension] = self[dimension]
|
|
62
|
+
rescue StandardError
|
|
63
|
+
next
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
52
67
|
def inspected_derived
|
|
53
68
|
@formulae.keys.empty? ? '' : " derived: #{@formulae.keys.inspect}"
|
|
54
69
|
end
|
data/lib/Namo/VERSION.rb
CHANGED
data/lib/namo.rb
CHANGED
|
@@ -280,11 +280,28 @@ class Namo
|
|
|
280
280
|
# pass over the data per access and raise whatever the formula raises.
|
|
281
281
|
def inspected_rows
|
|
282
282
|
return ' []' if @data.empty?
|
|
283
|
-
shown = @data.first(INSPECTED_ROWS).map{|row| " #{row
|
|
283
|
+
shown = @data.first(INSPECTED_ROWS).map{|row| " #{inspected_row(row)}"}.join(",\n")
|
|
284
284
|
shown += "\n ... #{@data.length - INSPECTED_ROWS} more rows" if @data.length > INSPECTED_ROWS
|
|
285
285
|
" [\n#{shown}\n]"
|
|
286
286
|
end
|
|
287
287
|
|
|
288
|
+
def inspected_row(row)
|
|
289
|
+
row.merge(inspected_derivations(row)).inspect
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# A derived dimension is shown with its value, in among the stored ones. A
|
|
293
|
+
# formula wanting arguments has no value to show without them, and one which
|
|
294
|
+
# raises has none to show either; the derived list still names both.
|
|
295
|
+
def inspected_derivations(row)
|
|
296
|
+
subject = Row.new(row, @formulae, self)
|
|
297
|
+
derived_dimensions.each_with_object({}) do |dimension, derived|
|
|
298
|
+
next if @formulae.required_parameter_count(dimension) > 2
|
|
299
|
+
derived[dimension] = subject[dimension]
|
|
300
|
+
rescue StandardError
|
|
301
|
+
next
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
288
305
|
def inspected_derived
|
|
289
306
|
derived_dimensions.empty? ? '' : " derived: #{derived_dimensions.inspect}"
|
|
290
307
|
end
|
data/test/Namo/Row_test.rb
CHANGED
|
@@ -465,9 +465,15 @@ describe Namo::Row do
|
|
|
465
465
|
_(Namo::Row.new({a: 1}, Namo::Formulae.new).inspect).must_equal "#<Namo::Row {a: 1}>"
|
|
466
466
|
end
|
|
467
467
|
|
|
468
|
-
it "
|
|
468
|
+
it "shows a derived dimension with its value, in among the stored ones" do
|
|
469
469
|
formulae = Namo::Formulae.new
|
|
470
|
-
formulae[:b] = proc{|row|
|
|
470
|
+
formulae[:b] = proc{|row| row[:a] + 1}
|
|
471
|
+
_(Namo::Row.new({a: 1}, formulae).inspect).must_equal "#<Namo::Row {a: 1, b: 2} derived: [:b]>"
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
it "names a derived dimension whose formula raises, without a value" do
|
|
475
|
+
formulae = Namo::Formulae.new
|
|
476
|
+
formulae[:b] = proc{|row| raise 'no value to show'}
|
|
471
477
|
_(Namo::Row.new({a: 1}, formulae).inspect).must_equal "#<Namo::Row {a: 1} derived: [:b]>"
|
|
472
478
|
end
|
|
473
479
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# test/console_test.rb
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require 'minitest-spec-context'
|
|
5
|
+
|
|
6
|
+
require_relative '../lib/namo'
|
|
7
|
+
require_relative '../script/fixtures'
|
|
8
|
+
|
|
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.
|
|
13
|
+
|
|
14
|
+
describe 'script/console' do
|
|
15
|
+
def console(*expressions)
|
|
16
|
+
root = File.expand_path('..', __dir__)
|
|
17
|
+
IO.popen([File.join(root, 'script', 'console'), '--prompt', 'simple'],
|
|
18
|
+
'r+', chdir: root, err: [:child, :out]) do |io|
|
|
19
|
+
io.puts(expressions, 'exit')
|
|
20
|
+
io.close_write
|
|
21
|
+
io.read
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
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
|
+
end
|
|
29
|
+
|
|
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.to_a == ' + expected.inspect)).must_match(/true/)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "starts without echoing its own source" do
|
|
40
|
+
_(console('1')).wont_match(/binding\.irb/)
|
|
41
|
+
end
|
|
42
|
+
end
|
data/test/demo_test.rb
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# test/demo_test.rb
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require 'minitest-spec-context'
|
|
5
|
+
|
|
6
|
+
require_relative '../lib/namo'
|
|
7
|
+
|
|
8
|
+
# script/demo is 300-odd lines exercising selection, projection, formulae,
|
|
9
|
+
# group_by, summary, the operators and inspect, and nothing else runs it. These
|
|
10
|
+
# assert only that each section completes, which is the difference between a
|
|
11
|
+
# demonstration which rots quietly and one which fails the suite when the library
|
|
12
|
+
# moves under it.
|
|
13
|
+
#
|
|
14
|
+
# A process apiece, since the script is a program rather than a library and its
|
|
15
|
+
# sections share a binding within a run.
|
|
16
|
+
|
|
17
|
+
describe 'script/demo' do
|
|
18
|
+
def demo(*sections)
|
|
19
|
+
root = File.expand_path('..', __dir__)
|
|
20
|
+
IO.popen([File.join(root, 'script', 'demo'), *sections],
|
|
21
|
+
chdir: root, err: [:child, :out]){|io| io.read}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def sections
|
|
25
|
+
@sections ||= demo('--help').scan(/^ [* ] ([a-z_]+)$/).flatten
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def cut
|
|
29
|
+
@cut ||= demo('--help').scan(/^ \* ([a-z_]+)$/).flatten
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "lists its sections" do
|
|
33
|
+
_(sections).wont_be_empty
|
|
34
|
+
_(sections).must_include 'ingestion'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "runs every section without raising" do
|
|
38
|
+
failed = sections.reject do |section|
|
|
39
|
+
demo(section)
|
|
40
|
+
$?.success?
|
|
41
|
+
end
|
|
42
|
+
_(failed).must_be_empty
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "has a talk cut, and it is a subset of the sections" do
|
|
46
|
+
_(cut).wont_be_empty
|
|
47
|
+
_(cut - sections).must_be_empty
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "runs the talk cut" do
|
|
51
|
+
demo('talk')
|
|
52
|
+
_($?.success?).must_equal true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The padding is what holds a section title in one place on the screen, and it
|
|
56
|
+
# can only pad down to the constants. A section grown past them would silently
|
|
57
|
+
# start pushing the next one about. Rows rather than lines, since a line wider
|
|
58
|
+
# than the window costs more than one of them.
|
|
59
|
+
#
|
|
60
|
+
# The cut rather than every section: those are the ones which have to fit on the
|
|
61
|
+
# day, and the rest are reference, free to run long and be scrolled.
|
|
62
|
+
it "has no section in the talk cut taller than the slide height" do
|
|
63
|
+
source = File.read(File.join(File.expand_path('..', __dir__), 'script', 'demo'))
|
|
64
|
+
height, width = %w[SLIDE_HEIGHT SLIDE_WIDTH].map{|name| source[/^#{name} = (\d+)$/, 1].to_i}
|
|
65
|
+
_([height, width].min).must_be :>, 0
|
|
66
|
+
overlong = cut.select do |section|
|
|
67
|
+
demo(section).lines.sum{|line| [(line.chomp.size / width.to_f).ceil, 1].max} > height
|
|
68
|
+
end
|
|
69
|
+
_(overlong).must_be_empty
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "runs the whole script" do
|
|
73
|
+
demo
|
|
74
|
+
_($?.success?).must_equal true
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "refuses a section it does not have" do
|
|
78
|
+
demo('nonexistent')
|
|
79
|
+
_($?.success?).must_equal false
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "needs nothing but the gem's own dependencies" do
|
|
83
|
+
root = File.expand_path('..', __dir__)
|
|
84
|
+
output = IO.popen({'RUBYLIB' => nil}, [File.join(root, 'script', 'demo'), 'ingestion'],
|
|
85
|
+
chdir: root, err: [:child, :out]){|io| io.read}
|
|
86
|
+
_(output).wont_match(/LoadError/)
|
|
87
|
+
end
|
|
88
|
+
end
|
data/test/namo_test.rb
CHANGED
|
@@ -3231,9 +3231,27 @@ describe Namo do
|
|
|
3231
3231
|
_(Namo.new([]).inspect).must_equal "#<Namo []>"
|
|
3232
3232
|
end
|
|
3233
3233
|
|
|
3234
|
-
it "
|
|
3234
|
+
it "shows a derived dimension with its value, in among the stored ones" do
|
|
3235
3235
|
namo = Namo.new([{a: 1}])
|
|
3236
|
-
namo[:b] = proc{|row|
|
|
3236
|
+
namo[:b] = proc{|row| row[:a] + 1}
|
|
3237
|
+
_(namo.inspect).must_equal "#<Namo [\n {a: 1, b: 2}\n] derived: [:b]>"
|
|
3238
|
+
end
|
|
3239
|
+
|
|
3240
|
+
it "shows a collection-scoped derived value" do
|
|
3241
|
+
namo = Namo.new([{a: 1}, {a: 2}])
|
|
3242
|
+
namo[:total] = proc{|row, namo| namo.values(:a).sum}
|
|
3243
|
+
_(namo.inspect).must_equal "#<Namo [\n {a: 1, total: 3},\n {a: 2, total: 3}\n] derived: [:total]>"
|
|
3244
|
+
end
|
|
3245
|
+
|
|
3246
|
+
it "names a derived dimension whose formula raises, without a value" do
|
|
3247
|
+
namo = Namo.new([{a: 1}])
|
|
3248
|
+
namo[:b] = proc{|row| raise 'no value to show'}
|
|
3249
|
+
_(namo.inspect).must_equal "#<Namo [\n {a: 1}\n] derived: [:b]>"
|
|
3250
|
+
end
|
|
3251
|
+
|
|
3252
|
+
it "names a derived dimension whose formula wants arguments, without a value" do
|
|
3253
|
+
namo = Namo.new([{a: 1}])
|
|
3254
|
+
namo[:b] = proc{|row, namo, factor| row[:a] * factor}
|
|
3237
3255
|
_(namo.inspect).must_equal "#<Namo [\n {a: 1}\n] derived: [:b]>"
|
|
3238
3256
|
end
|
|
3239
3257
|
|
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.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- thoran
|
|
@@ -79,6 +79,8 @@ files:
|
|
|
79
79
|
- test/Namo/NegatedDimension_test.rb
|
|
80
80
|
- test/Namo/Row_test.rb
|
|
81
81
|
- test/Symbol_test.rb
|
|
82
|
+
- test/console_test.rb
|
|
83
|
+
- test/demo_test.rb
|
|
82
84
|
- test/namo_test.rb
|
|
83
85
|
homepage: https://github.com/thoran/namo
|
|
84
86
|
licenses:
|