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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11155d79565895733cba5947380c1995003ab8e3916dcc2a5b22a55053d804a7
4
- data.tar.gz: 5e983ddf2f3881195692df604fa9d61005f817cec18c87c960b837353fc094be
3
+ metadata.gz: e5235177577fcae1dd01a8a3172fc3933e77a5483f123ba6f436bd58a6eca68e
4
+ data.tar.gz: b2c023063fa95cca3f6ec38f324a116bf013339003d4051338a7c84b57251944
5
5
  SHA512:
6
- metadata.gz: 2143589ce12079aab10ee8e84bf2c77e94db8459bace1c2c1d45e71c538e8328fd088c3b8ae08490d292968890fec3b8e16d5384173374b3668b1acbeeb4ca49
7
- data.tar.gz: 2113f5063b2ae16863e4672bf8ec69bdc99d33ad561000068c19099950d7a25a3f4aecabbfed68f444dfe14677d61076793ed13754f8c48180e56f32ec8d56c5
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: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
70
- # {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
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: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100}
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: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
83
- # {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
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: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
90
- # {product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40}
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: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
97
- # {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
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: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
104
- # {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
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: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
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: 'Widget', price: 10.0},
135
- # {product: 'Widget', price: 10.0},
136
- # {product: 'Gadget', price: 25.0},
137
- # {product: 'Gadget', price: 25.0}
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: 'Q1', price: 10.0},
147
- # {quarter: 'Q2', price: 10.0}
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: 'Q1', price: 10.0},
157
- # {quarter: 'Q2', price: 10.0}
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: 'Widget', quarter: 'Q1'},
169
- # {product: 'Widget', quarter: 'Q2'},
170
- # {product: 'Gadget', quarter: 'Q1'},
171
- # {product: 'Gadget', quarter: 'Q2'}
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: 'Widget', quarter: 'Q1'},
187
- # {product: 'Widget', quarter: 'Q2'}
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: 'Widget', quarter: 'Q1'},
197
- # {product: 'Widget', quarter: 'Q2'}
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: '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}
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: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
251
- # {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
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: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
277
- # {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
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: '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}
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: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
326
- # {product: 'Thingo', quarter: 'Q3', price: 5.0, quantity: 10}
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: 'BHP', date: '2025-01-01', close: 42.5, pe: 14.5},
350
- # {symbol: 'RIO', date: '2025-01-01', close: 118.3, pe: 9.2}
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: '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}
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: 'Widget', quarter: 'Q1'},
399
- # {product: 'Widget', quarter: 'Q2'},
400
- # {product: 'Gadget', quarter: 'Q1'},
401
- # {product: 'Gadget', quarter: 'Q2'}
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: '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}
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: '2025-01-01', close: 42.5},
458
- # {date: '2025-01-01', close: 118.3}
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: '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}
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: '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
- # ]>
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: 'Q1'},
649
- # {revenue: 1500.0, quarter: 'Q2'}
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:
@@ -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
@@ -97,6 +97,10 @@ class Namo
97
97
  keys.sort.hash
98
98
  end
99
99
 
100
+ def inspect
101
+ "#<#{self.class} #{@store.keys.inspect}>"
102
+ end
103
+
100
104
  private
101
105
 
102
106
  def initialize(store = {})
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
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.30.1'
5
+ VERSION = '0.31.1'
6
6
  end
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
@@ -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
@@ -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.30.1
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: