namo 0.30.1 → 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 +24 -0
- data/README.md +98 -66
- data/lib/Namo/Collection.rb +4 -0
- data/lib/Namo/Formulae.rb +4 -0
- data/lib/Namo/Row.rb +23 -0
- data/lib/Namo/VERSION.rb +1 -1
- data/lib/namo.rb +41 -0
- data/test/Namo/Collection_test.rb +13 -0
- data/test/Namo/Formulae_test.rb +15 -0
- data/test/Namo/Row_test.rb +25 -0
- data/test/console_test.rb +42 -0
- data/test/demo_test.rb +88 -0
- data/test/namo_test.rb +63 -0
- 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,29 @@
|
|
|
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
|
+
|
|
14
|
+
## 20260822
|
|
15
|
+
|
|
16
|
+
0.31.0: + Namo#inspect, Namo::Collection#inspect, Namo::Row#inspect, and Namo::Formulae#inspect.
|
|
17
|
+
|
|
18
|
+
1. + Namo#inspect: the class, the name where there is one, the stored rows, and the derived dimension names — the form the README has shown since 0.0.0 and the code has never produced. Rows beyond Namo::INSPECTED_ROWS are elided with a count of what is left, so the output no longer grows with the data.
|
|
19
|
+
2. + Namo::Row#inspect: the row and its formula names. It had been rendering @namo, so asking a 1,000-row Namo for one row emitted more output than inspecting the whole Namo.
|
|
20
|
+
3. + Namo::Collection#inspect: the member names and the row count, the members being the substance and the data view derived from them.
|
|
21
|
+
4. + Namo::Formulae#inspect: the formula names, never the callables.
|
|
22
|
+
5. + README.md: an Inspection output section, covering the rendered form, the naming of derived dimensions, and the row cap.
|
|
23
|
+
6. ~ README.md: the inspect-output blocks requoted from single to double quotes, as Hash#inspect emits, and the two projections that carry formulae given their derived: suffix. The blocks had been hand-written before there was an inspect to copy from.
|
|
24
|
+
7. ~ Namo::VERSION: /0.30.1/0.31.0/
|
|
25
|
+
|
|
26
|
+
|
|
3
27
|
## 20260819
|
|
4
28
|
|
|
5
29
|
0.30.1: ~ namo.gemspec: spec.files reordered, the globs first.
|
data/README.md
CHANGED
|
@@ -66,42 +66,42 @@ Select by named dimension using keyword arguments:
|
|
|
66
66
|
# Single value
|
|
67
67
|
sales[product: 'Widget']
|
|
68
68
|
# => #<Namo [
|
|
69
|
-
# {product:
|
|
70
|
-
# {product:
|
|
69
|
+
# {product: "Widget", quarter: "Q1", price: 10.0, quantity: 100},
|
|
70
|
+
# {product: "Widget", quarter: "Q2", price: 10.0, quantity: 150}
|
|
71
71
|
# ]>
|
|
72
72
|
|
|
73
73
|
# Multiple dimensions
|
|
74
74
|
sales[product: 'Widget', quarter: 'Q1']
|
|
75
75
|
# => #<Namo [
|
|
76
|
-
# {product:
|
|
76
|
+
# {product: "Widget", quarter: "Q1", price: 10.0, quantity: 100}
|
|
77
77
|
# ]>
|
|
78
78
|
|
|
79
79
|
# Range
|
|
80
80
|
sales[price: 10.0..20.0]
|
|
81
81
|
# => #<Namo [
|
|
82
|
-
# {product:
|
|
83
|
-
# {product:
|
|
82
|
+
# {product: "Widget", quarter: "Q1", price: 10.0, quantity: 100},
|
|
83
|
+
# {product: "Widget", quarter: "Q2", price: 10.0, quantity: 150}
|
|
84
84
|
# ]>
|
|
85
85
|
|
|
86
86
|
# Array of values
|
|
87
87
|
sales[quarter: ['Q1']]
|
|
88
88
|
# => #<Namo [
|
|
89
|
-
# {product:
|
|
90
|
-
# {product:
|
|
89
|
+
# {product: "Widget", quarter: "Q1", price: 10.0, quantity: 100},
|
|
90
|
+
# {product: "Gadget", quarter: "Q1", price: 25.0, quantity: 40}
|
|
91
91
|
# ]>
|
|
92
92
|
|
|
93
93
|
# Proc predicate
|
|
94
94
|
sales[price: ->(v){v < 20.0}]
|
|
95
95
|
# => #<Namo [
|
|
96
|
-
# {product:
|
|
97
|
-
# {product:
|
|
96
|
+
# {product: "Widget", quarter: "Q1", price: 10.0, quantity: 100},
|
|
97
|
+
# {product: "Widget", quarter: "Q2", price: 10.0, quantity: 150}
|
|
98
98
|
# ]>
|
|
99
99
|
|
|
100
100
|
# Regex predicate
|
|
101
101
|
sales[product: /^W/]
|
|
102
102
|
# => #<Namo [
|
|
103
|
-
# {product:
|
|
104
|
-
# {product:
|
|
103
|
+
# {product: "Widget", quarter: "Q1", price: 10.0, quantity: 100},
|
|
104
|
+
# {product: "Widget", quarter: "Q2", price: 10.0, quantity: 150}
|
|
105
105
|
# ]>
|
|
106
106
|
```
|
|
107
107
|
|
|
@@ -110,7 +110,7 @@ Procs receive the dimension value and select the row when they return truthy. Th
|
|
|
110
110
|
```ruby
|
|
111
111
|
sales[price: ->(v){v < 20.0}, quantity: ->(v){v > 100}]
|
|
112
112
|
# => #<Namo [
|
|
113
|
-
# {product:
|
|
113
|
+
# {product: "Widget", quarter: "Q2", price: 10.0, quantity: 150}
|
|
114
114
|
# ]>
|
|
115
115
|
```
|
|
116
116
|
|
|
@@ -131,10 +131,10 @@ Project to specific dimensions:
|
|
|
131
131
|
```ruby
|
|
132
132
|
sales[:product, :price]
|
|
133
133
|
# => #<Namo [
|
|
134
|
-
# {product:
|
|
135
|
-
# {product:
|
|
136
|
-
# {product:
|
|
137
|
-
# {product:
|
|
134
|
+
# {product: "Widget", price: 10.0},
|
|
135
|
+
# {product: "Widget", price: 10.0},
|
|
136
|
+
# {product: "Gadget", price: 25.0},
|
|
137
|
+
# {product: "Gadget", price: 25.0}
|
|
138
138
|
# ]>
|
|
139
139
|
```
|
|
140
140
|
|
|
@@ -143,8 +143,8 @@ Selection and projection can be chained:
|
|
|
143
143
|
```ruby
|
|
144
144
|
sales[product: 'Widget'][:quarter, :price]
|
|
145
145
|
# => #<Namo [
|
|
146
|
-
# {quarter:
|
|
147
|
-
# {quarter:
|
|
146
|
+
# {quarter: "Q1", price: 10.0},
|
|
147
|
+
# {quarter: "Q2", price: 10.0}
|
|
148
148
|
# ]>
|
|
149
149
|
```
|
|
150
150
|
|
|
@@ -153,8 +153,8 @@ Or combined in a single call (names before selectors):
|
|
|
153
153
|
```ruby
|
|
154
154
|
sales[:quarter, :price, product: 'Widget']
|
|
155
155
|
# => #<Namo [
|
|
156
|
-
# {quarter:
|
|
157
|
-
# {quarter:
|
|
156
|
+
# {quarter: "Q1", price: 10.0},
|
|
157
|
+
# {quarter: "Q2", price: 10.0}
|
|
158
158
|
# ]>
|
|
159
159
|
```
|
|
160
160
|
|
|
@@ -165,10 +165,10 @@ Contraction is the complement of projection. Projection says "keep these dimensi
|
|
|
165
165
|
```ruby
|
|
166
166
|
sales[-:price, -:quantity]
|
|
167
167
|
# => #<Namo [
|
|
168
|
-
# {product:
|
|
169
|
-
# {product:
|
|
170
|
-
# {product:
|
|
171
|
-
# {product:
|
|
168
|
+
# {product: "Widget", quarter: "Q1"},
|
|
169
|
+
# {product: "Widget", quarter: "Q2"},
|
|
170
|
+
# {product: "Gadget", quarter: "Q1"},
|
|
171
|
+
# {product: "Gadget", quarter: "Q2"}
|
|
172
172
|
# ]>
|
|
173
173
|
```
|
|
174
174
|
|
|
@@ -183,8 +183,8 @@ Selection and contraction can be chained:
|
|
|
183
183
|
```ruby
|
|
184
184
|
sales[product: 'Widget'][-:price, -:quantity]
|
|
185
185
|
# => #<Namo [
|
|
186
|
-
# {product:
|
|
187
|
-
# {product:
|
|
186
|
+
# {product: "Widget", quarter: "Q1"},
|
|
187
|
+
# {product: "Widget", quarter: "Q2"}
|
|
188
188
|
# ]>
|
|
189
189
|
```
|
|
190
190
|
|
|
@@ -193,8 +193,8 @@ Or combined in a single call (names before selectors):
|
|
|
193
193
|
```ruby
|
|
194
194
|
sales[-:price, -:quantity, product: 'Widget']
|
|
195
195
|
# => #<Namo [
|
|
196
|
-
# {product:
|
|
197
|
-
# {product:
|
|
196
|
+
# {product: "Widget", quarter: "Q1"},
|
|
197
|
+
# {product: "Widget", quarter: "Q2"}
|
|
198
198
|
# ]>
|
|
199
199
|
```
|
|
200
200
|
|
|
@@ -219,10 +219,10 @@ q2_sales = Namo.new([
|
|
|
219
219
|
|
|
220
220
|
all_sales = q1_sales + q2_sales
|
|
221
221
|
# => #<Namo [
|
|
222
|
-
# {product:
|
|
223
|
-
# {product:
|
|
224
|
-
# {product:
|
|
225
|
-
# {product:
|
|
222
|
+
# {product: "Widget", quarter: "Q1", price: 10.0, quantity: 100},
|
|
223
|
+
# {product: "Gadget", quarter: "Q1", price: 25.0, quantity: 40},
|
|
224
|
+
# {product: "Widget", quarter: "Q2", price: 10.0, quantity: 150},
|
|
225
|
+
# {product: "Gadget", quarter: "Q2", price: 25.0, quantity: 60}
|
|
226
226
|
# ]>
|
|
227
227
|
```
|
|
228
228
|
|
|
@@ -247,8 +247,8 @@ discontinued = Namo.new([
|
|
|
247
247
|
|
|
248
248
|
sales - discontinued
|
|
249
249
|
# => #<Namo [
|
|
250
|
-
# {product:
|
|
251
|
-
# {product:
|
|
250
|
+
# {product: "Widget", quarter: "Q1", price: 10.0, quantity: 100},
|
|
251
|
+
# {product: "Widget", quarter: "Q2", price: 10.0, quantity: 150}
|
|
252
252
|
# ]>
|
|
253
253
|
```
|
|
254
254
|
|
|
@@ -273,8 +273,8 @@ confirmed = Namo.new([
|
|
|
273
273
|
|
|
274
274
|
sales & confirmed
|
|
275
275
|
# => #<Namo [
|
|
276
|
-
# {product:
|
|
277
|
-
# {product:
|
|
276
|
+
# {product: "Widget", quarter: "Q1", price: 10.0, quantity: 100},
|
|
277
|
+
# {product: "Gadget", quarter: "Q2", price: 25.0, quantity: 60}
|
|
278
278
|
# ]>
|
|
279
279
|
```
|
|
280
280
|
|
|
@@ -297,9 +297,9 @@ all_sales = Namo.new([
|
|
|
297
297
|
|
|
298
298
|
q1_sales | all_sales
|
|
299
299
|
# => #<Namo [
|
|
300
|
-
# {product:
|
|
301
|
-
# {product:
|
|
302
|
-
# {product:
|
|
300
|
+
# {product: "Widget", quarter: "Q1", price: 10.0, quantity: 100},
|
|
301
|
+
# {product: "Gadget", quarter: "Q1", price: 25.0, quantity: 40},
|
|
302
|
+
# {product: "Thingo", quarter: "Q3", price: 5.0, quantity: 10}
|
|
303
303
|
# ]>
|
|
304
304
|
```
|
|
305
305
|
|
|
@@ -322,8 +322,8 @@ set_b = Namo.new([
|
|
|
322
322
|
|
|
323
323
|
set_a ^ set_b
|
|
324
324
|
# => #<Namo [
|
|
325
|
-
# {product:
|
|
326
|
-
# {product:
|
|
325
|
+
# {product: "Gadget", quarter: "Q1", price: 25.0, quantity: 40},
|
|
326
|
+
# {product: "Thingo", quarter: "Q3", price: 5.0, quantity: 10}
|
|
327
327
|
# ]>
|
|
328
328
|
```
|
|
329
329
|
|
|
@@ -346,8 +346,8 @@ fundamentals = Namo.new([
|
|
|
346
346
|
|
|
347
347
|
ohlcv * fundamentals
|
|
348
348
|
# => #<Namo [
|
|
349
|
-
# {symbol:
|
|
350
|
-
# {symbol:
|
|
349
|
+
# {symbol: "BHP", date: "2025-01-01", close: 42.5, pe: 14.5},
|
|
350
|
+
# {symbol: "RIO", date: "2025-01-01", close: 118.3, pe: 9.2}
|
|
351
351
|
# ]>
|
|
352
352
|
```
|
|
353
353
|
|
|
@@ -376,8 +376,8 @@ prices.*(quarterly) do |row, candidates|
|
|
|
376
376
|
candidates[quarter_end: ->(qe){qe <= row[:date]}].sort_by{|f| f[:quarter_end]}.last(1)
|
|
377
377
|
end
|
|
378
378
|
# => #<Namo [
|
|
379
|
-
# {symbol:
|
|
380
|
-
# {symbol:
|
|
379
|
+
# {symbol: "BHP", date: "2025-02-15", close: 42.5, quarter_end: "2024-12-31", eps: 1.0},
|
|
380
|
+
# {symbol: "BHP", date: "2025-05-20", close: 44.0, quarter_end: "2025-03-31", eps: 1.2}
|
|
381
381
|
# ]>
|
|
382
382
|
```
|
|
383
383
|
|
|
@@ -395,10 +395,10 @@ quarters = Namo.new([{quarter: 'Q1'}, {quarter: 'Q2'}])
|
|
|
395
395
|
|
|
396
396
|
products ** quarters
|
|
397
397
|
# => #<Namo [
|
|
398
|
-
# {product:
|
|
399
|
-
# {product:
|
|
400
|
-
# {product:
|
|
401
|
-
# {product:
|
|
398
|
+
# {product: "Widget", quarter: "Q1"},
|
|
399
|
+
# {product: "Widget", quarter: "Q2"},
|
|
400
|
+
# {product: "Gadget", quarter: "Q1"},
|
|
401
|
+
# {product: "Gadget", quarter: "Q2"}
|
|
402
402
|
# ]>
|
|
403
403
|
```
|
|
404
404
|
|
|
@@ -429,9 +429,9 @@ orders.**(tiers) do |row, candidates|
|
|
|
429
429
|
candidates[max_weight: ->(w){w >= row[:weight]}]
|
|
430
430
|
end
|
|
431
431
|
# => #<Namo [
|
|
432
|
-
# {order:
|
|
433
|
-
# {order:
|
|
434
|
-
# {order:
|
|
432
|
+
# {order: "A", weight: 5, tier: "light", max_weight: 10},
|
|
433
|
+
# {order: "A", weight: 5, tier: "heavy", max_weight: 20},
|
|
434
|
+
# {order: "B", weight: 15, tier: "heavy", max_weight: 20}
|
|
435
435
|
# ]>
|
|
436
436
|
```
|
|
437
437
|
|
|
@@ -454,8 +454,8 @@ fundamentals = Namo.new([
|
|
|
454
454
|
|
|
455
455
|
combined / fundamentals
|
|
456
456
|
# => #<Namo [
|
|
457
|
-
# {date:
|
|
458
|
-
# {date:
|
|
457
|
+
# {date: "2025-01-01", close: 42.5},
|
|
458
|
+
# {date: "2025-01-01", close: 118.3}
|
|
459
459
|
# ]>
|
|
460
460
|
```
|
|
461
461
|
|
|
@@ -616,10 +616,10 @@ sales[:revenue] = proc{|row| row[:price] * row[:quantity]}
|
|
|
616
616
|
|
|
617
617
|
sales[:product, :quarter, :revenue]
|
|
618
618
|
# => #<Namo [
|
|
619
|
-
# {product:
|
|
620
|
-
# {product:
|
|
621
|
-
# {product:
|
|
622
|
-
# {product:
|
|
619
|
+
# {product: "Widget", quarter: "Q1", revenue: 1000.0},
|
|
620
|
+
# {product: "Widget", quarter: "Q2", revenue: 1500.0},
|
|
621
|
+
# {product: "Gadget", quarter: "Q1", revenue: 1000.0},
|
|
622
|
+
# {product: "Gadget", quarter: "Q2", revenue: 1500.0}
|
|
623
623
|
# ]>
|
|
624
624
|
```
|
|
625
625
|
|
|
@@ -633,11 +633,11 @@ sales[:profit] = proc{|row| row[:revenue] - row[:cost]}
|
|
|
633
633
|
|
|
634
634
|
sales[:product, :quarter, :profit]
|
|
635
635
|
# => #<Namo [
|
|
636
|
-
# {product:
|
|
637
|
-
# {product:
|
|
638
|
-
# {product:
|
|
639
|
-
# {product:
|
|
640
|
-
# ]>
|
|
636
|
+
# {product: "Widget", quarter: "Q1", profit: 600.0},
|
|
637
|
+
# {product: "Widget", quarter: "Q2", profit: 900.0},
|
|
638
|
+
# {product: "Gadget", quarter: "Q1", profit: 840.0},
|
|
639
|
+
# {product: "Gadget", quarter: "Q2", profit: 1260.0}
|
|
640
|
+
# ] derived: [:revenue, :cost]>
|
|
641
641
|
```
|
|
642
642
|
|
|
643
643
|
Formulae work with selection and projection:
|
|
@@ -645,9 +645,9 @@ Formulae work with selection and projection:
|
|
|
645
645
|
```ruby
|
|
646
646
|
sales[product: 'Widget'][:revenue, :quarter]
|
|
647
647
|
# => #<Namo [
|
|
648
|
-
# {revenue: 1000.0, quarter:
|
|
649
|
-
# {revenue: 1500.0, quarter:
|
|
650
|
-
# ]>
|
|
648
|
+
# {revenue: 1000.0, quarter: "Q1"},
|
|
649
|
+
# {revenue: 1500.0, quarter: "Q2"}
|
|
650
|
+
# ] derived: [:cost, :profit]>
|
|
651
651
|
```
|
|
652
652
|
|
|
653
653
|
Formulae carry through selection — a filtered Namo instance remembers its formulae.
|
|
@@ -970,6 +970,38 @@ sales[:product, :quarter, :revenue].to_h
|
|
|
970
970
|
# }
|
|
971
971
|
```
|
|
972
972
|
|
|
973
|
+
### Inspection output
|
|
974
|
+
|
|
975
|
+
`inspect` renders the class, the stored rows with any derived dimensions among them, and the names of those derived dimensions:
|
|
976
|
+
|
|
977
|
+
```ruby
|
|
978
|
+
sales.inspect
|
|
979
|
+
# => #<Namo [
|
|
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
|
+
# ] derived: [:revenue]>
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
A name, where the Namo carries one, follows the class: `#<Namo :sales [`. A subclass reports its own name in place of `Namo`.
|
|
988
|
+
|
|
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.
|
|
992
|
+
|
|
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`.
|
|
994
|
+
|
|
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.
|
|
996
|
+
|
|
997
|
+
```ruby
|
|
998
|
+
sales.first
|
|
999
|
+
# => #<Namo::Row {product: "Widget", quarter: "Q1", price: 10.0, quantity: 100, revenue: 1000.0} derived: [:revenue]>
|
|
1000
|
+
|
|
1001
|
+
sales.group_by(:quarter)
|
|
1002
|
+
# => #<Namo::Collection members: ["Q1", "Q2"], 4 rows>
|
|
1003
|
+
```
|
|
1004
|
+
|
|
973
1005
|
### Named Namos
|
|
974
1006
|
|
|
975
1007
|
A Namo can carry a name, passed by the `name:` keyword and read or set through the `name` accessor:
|
data/lib/Namo/Collection.rb
CHANGED
|
@@ -53,6 +53,10 @@ class Namo
|
|
|
53
53
|
self
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
def inspect
|
|
57
|
+
"#<#{self.class}#{inspected_name} members: #{@members.map(&:name).inspect}, #{@data.length} rows#{inspected_derived}>"
|
|
58
|
+
end
|
|
59
|
+
|
|
56
60
|
private
|
|
57
61
|
|
|
58
62
|
def initialize(positional_data = nil, data: [], formulae: {}, name: nil)
|
data/lib/Namo/Formulae.rb
CHANGED
data/lib/Namo/Row.rb
CHANGED
|
@@ -43,8 +43,31 @@ class Namo
|
|
|
43
43
|
@row
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
def inspect
|
|
47
|
+
"#<#{self.class} #{inspected_row}#{inspected_derived}>"
|
|
48
|
+
end
|
|
49
|
+
|
|
46
50
|
private
|
|
47
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
|
+
|
|
67
|
+
def inspected_derived
|
|
68
|
+
@formulae.keys.empty? ? '' : " derived: #{@formulae.keys.inspect}"
|
|
69
|
+
end
|
|
70
|
+
|
|
48
71
|
def initialize(row, formulae, namo = nil)
|
|
49
72
|
@row = row
|
|
50
73
|
@formulae = formulae
|
data/lib/Namo/VERSION.rb
CHANGED
data/lib/namo.rb
CHANGED
|
@@ -17,6 +17,8 @@ class Namo
|
|
|
17
17
|
attr_accessor :formulae
|
|
18
18
|
attr_accessor :name
|
|
19
19
|
|
|
20
|
+
INSPECTED_ROWS = 10
|
|
21
|
+
|
|
20
22
|
def dimensions
|
|
21
23
|
data_dimensions + derived_dimensions
|
|
22
24
|
end
|
|
@@ -247,6 +249,10 @@ class Namo
|
|
|
247
249
|
end
|
|
248
250
|
end
|
|
249
251
|
|
|
252
|
+
def inspect
|
|
253
|
+
"#<#{self.class}#{inspected_name}#{inspected_rows}#{inspected_derived}>"
|
|
254
|
+
end
|
|
255
|
+
|
|
250
256
|
protected
|
|
251
257
|
|
|
252
258
|
def row_multiset
|
|
@@ -265,6 +271,41 @@ class Namo
|
|
|
265
271
|
|
|
266
272
|
private
|
|
267
273
|
|
|
274
|
+
def inspected_name
|
|
275
|
+
@name.nil? ? '' : " #{@name.inspect}"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# The rows as they are stored, never the derived values: inspect is called
|
|
279
|
+
# for every result in a console, and evaluating a formula there would cost a
|
|
280
|
+
# pass over the data per access and raise whatever the formula raises.
|
|
281
|
+
def inspected_rows
|
|
282
|
+
return ' []' if @data.empty?
|
|
283
|
+
shown = @data.first(INSPECTED_ROWS).map{|row| " #{inspected_row(row)}"}.join(",\n")
|
|
284
|
+
shown += "\n ... #{@data.length - INSPECTED_ROWS} more rows" if @data.length > INSPECTED_ROWS
|
|
285
|
+
" [\n#{shown}\n]"
|
|
286
|
+
end
|
|
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
|
+
|
|
305
|
+
def inspected_derived
|
|
306
|
+
derived_dimensions.empty? ? '' : " derived: #{derived_dimensions.inspect}"
|
|
307
|
+
end
|
|
308
|
+
|
|
268
309
|
def initialize(positional_data = nil, data: [], formulae: {}, name: nil)
|
|
269
310
|
@data = positional_data || data
|
|
270
311
|
@formulae = formulae.is_a?(Formulae) ? formulae : Formulae.new(formulae)
|
|
@@ -346,4 +346,17 @@ describe Namo::Collection do
|
|
|
346
346
|
_(car.detail.values(:assembly)).must_equal [:powertrain, :powertrain, :chassis, :body]
|
|
347
347
|
end
|
|
348
348
|
end
|
|
349
|
+
|
|
350
|
+
describe "#inspect" do
|
|
351
|
+
it "renders the member names and the row count" do
|
|
352
|
+
collection = Namo::Collection.new
|
|
353
|
+
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>"
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
it "renders an empty Collection" do
|
|
358
|
+
_(Namo::Collection.new.inspect).must_equal "#<Namo::Collection members: [], 0 rows>"
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
349
362
|
end
|
data/test/Namo/Formulae_test.rb
CHANGED
|
@@ -440,4 +440,19 @@ describe Namo::Formulae do
|
|
|
440
440
|
end
|
|
441
441
|
end
|
|
442
442
|
end
|
|
443
|
+
|
|
444
|
+
describe "#inspect" do
|
|
445
|
+
it "renders the formula names" do
|
|
446
|
+
formulae = Namo::Formulae.new
|
|
447
|
+
formulae[:revenue] = proc{|row| row[:price]}
|
|
448
|
+
_(formulae.inspect).must_equal "#<Namo::Formulae [:revenue]>"
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
it "does not render the callables" do
|
|
452
|
+
formulae = Namo::Formulae.new
|
|
453
|
+
formulae[:revenue] = proc{|row| row[:price]}
|
|
454
|
+
_(formulae.inspect).wont_match(/Proc/)
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
443
458
|
end
|
data/test/Namo/Row_test.rb
CHANGED
|
@@ -459,4 +459,29 @@ describe Namo::Row do
|
|
|
459
459
|
_([a, b, duplicate_of_a].uniq.length).must_equal 2
|
|
460
460
|
end
|
|
461
461
|
end
|
|
462
|
+
|
|
463
|
+
describe "#inspect" do
|
|
464
|
+
it "renders the row data" do
|
|
465
|
+
_(Namo::Row.new({a: 1}, Namo::Formulae.new).inspect).must_equal "#<Namo::Row {a: 1}>"
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
it "shows a derived dimension with its value, in among the stored ones" do
|
|
469
|
+
formulae = Namo::Formulae.new
|
|
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'}
|
|
477
|
+
_(Namo::Row.new({a: 1}, formulae).inspect).must_equal "#<Namo::Row {a: 1} derived: [:b]>"
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
it "does not render the Namo the row came from" do
|
|
481
|
+
namo = Namo.new((1..1000).map{|i| {a: i}})
|
|
482
|
+
_(namo.first.inspect).wont_match(/\{a: 2\}/)
|
|
483
|
+
_(namo.first.inspect.length).must_be :<, 100
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
462
487
|
end
|
|
@@ -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
|
@@ -3221,4 +3221,67 @@ describe Namo do
|
|
|
3221
3221
|
end
|
|
3222
3222
|
end
|
|
3223
3223
|
end
|
|
3224
|
+
|
|
3225
|
+
describe "#inspect" do
|
|
3226
|
+
it "renders the class and the rows" do
|
|
3227
|
+
_(Namo.new([{a: 1}]).inspect).must_equal "#<Namo [\n {a: 1}\n]>"
|
|
3228
|
+
end
|
|
3229
|
+
|
|
3230
|
+
it "renders an empty Namo without a row block" do
|
|
3231
|
+
_(Namo.new([]).inspect).must_equal "#<Namo []>"
|
|
3232
|
+
end
|
|
3233
|
+
|
|
3234
|
+
it "shows a derived dimension with its value, in among the stored ones" do
|
|
3235
|
+
namo = Namo.new([{a: 1}])
|
|
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}
|
|
3255
|
+
_(namo.inspect).must_equal "#<Namo [\n {a: 1}\n] derived: [:b]>"
|
|
3256
|
+
end
|
|
3257
|
+
|
|
3258
|
+
it "includes the name when there is one" do
|
|
3259
|
+
_(Namo.new([{a: 1}], name: :sales).inspect).must_match(/\A#<Namo :sales \[/)
|
|
3260
|
+
end
|
|
3261
|
+
|
|
3262
|
+
it "omits the name when there is none" do
|
|
3263
|
+
_(Namo.new([{a: 1}]).inspect).must_match(/\A#<Namo \[/)
|
|
3264
|
+
end
|
|
3265
|
+
|
|
3266
|
+
it "truncates beyond INSPECTED_ROWS and says how many are left" do
|
|
3267
|
+
namo = Namo.new((1..25).map{|i| {a: i}})
|
|
3268
|
+
_(namo.inspect.scan(/\{a: \d+\}/).length).must_equal Namo::INSPECTED_ROWS
|
|
3269
|
+
_(namo.inspect).must_match(/\.\.\. 15 more rows/)
|
|
3270
|
+
end
|
|
3271
|
+
|
|
3272
|
+
it "does not truncate at exactly INSPECTED_ROWS" do
|
|
3273
|
+
namo = Namo.new((1..Namo::INSPECTED_ROWS).map{|i| {a: i}})
|
|
3274
|
+
_(namo.inspect).wont_match(/more rows/)
|
|
3275
|
+
end
|
|
3276
|
+
|
|
3277
|
+
it "reports the subclass rather than Namo" do
|
|
3278
|
+
Object.const_set(:InspectedSubAssembly, Class.new(Namo)) unless defined?(InspectedSubAssembly)
|
|
3279
|
+
_(InspectedSubAssembly.new([{a: 1}]).inspect).must_match(/\A#<InspectedSubAssembly \[/)
|
|
3280
|
+
end
|
|
3281
|
+
|
|
3282
|
+
it "does not grow with the data beyond the truncation point" do
|
|
3283
|
+
_(Namo.new((1..10_000).map{|i| {a: i}}).inspect.length).must_be :<, 500
|
|
3284
|
+
end
|
|
3285
|
+
end
|
|
3286
|
+
|
|
3224
3287
|
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.
|
|
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:
|