namo 0.7.0 → 0.8.0
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 +18 -2
- data/README.md +33 -0
- data/lib/Namo/Row.rb +4 -0
- data/lib/Namo/VERSION.rb +1 -1
- data/test/Namo/Row_test.rb +149 -0
- data/test/namo_test.rb +104 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 86f572755292e2bac9808f09adb3169cec06066a5687ec2ea6bd9354f97d592a
|
|
4
|
+
data.tar.gz: edafcc183a1a9e5fb1212bb66a8b6906b94bf8e37a7375d3f9d304e5632ddba3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2b2fad7c20e6cc9909b6d770a94627a071626de5cec9b402074ccda919295781dd18c38411604e5d0adf3d2f5e80833de830be37892a8e0a5cea5323bcef1a66
|
|
7
|
+
data.tar.gz: d0e9d4ae5bbe7a4dfc217614f0fb1442750313e9db734b622b831b15873e1ce6a3a5d2e91b01c450ab795476f00b94e29aae12be0dea4aad3d37e46369b5628e
|
data/CHANGELOG
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
CHANGELOG
|
|
2
2
|
_________
|
|
3
3
|
|
|
4
|
+
20260521
|
|
5
|
+
0.8.0: + proc and regex-based selection
|
|
6
|
+
|
|
7
|
+
1. ~ Namo::Row#match?: + `when Proc` branch — calls the proc with the dimension value; truthy result selects the row. Predicate receives nil for missing or nil-valued dimensions and decides.
|
|
8
|
+
2. ~ Namo::Row#match?: + `when Regexp` branch — matches against row[dimension].to_s. nil becomes "" (matches //, not /./); Integer/Float/Date/Symbol coerce via to_s.
|
|
9
|
+
3. ~ test/Namo/Row_test.rb: + Proc-predicate tests (true/false/nil/truthy non-boolean returns, nil dimension values, composition with exact/array/range/regex, multiple proc predicates across dimensions, carry-through to formula-defined dimensions). + Regexp-predicate tests (match/no-match/case-insensitive on Strings, to_s coercion of Integer/Float/Date/Symbol/nil, composition with exact/array/range/proc, multiple regex predicates across dimensions, carry-through to formula-defined dimensions).
|
|
10
|
+
4. ~ test/namo_test.rb: + End-to-end tests for proc selection, regex selection, and mixed proc/regex selection, including composition with projection and contraction in a single call and selection on formula-defined dimensions.
|
|
11
|
+
5. ~ README.md: + Proc and regex examples in the Selection section; + paragraphs on proc semantics (truthy/nil-aware, composes with everything) and regex semantics (.to_s coercion across nil/String/Symbol/Integer/Float/Date).
|
|
12
|
+
6. ~ ROADMAP.md: Promote 0.8.0 from upcoming to shipped under "Current state: 0.8.0"; revise Summary to include proc/regex in the selection vocabulary and point "next phase" at 0.9.0+.
|
|
13
|
+
7. ~ CHANGELOG: Update with 0.8.0's changes and retroactively log the 0.7.0 changes to README (+ Coordinates and values section), COMPARISON.md, and EXAMPLES.md (2 sections relocated from EXAMPLES.md to COMPARISON.md).
|
|
14
|
+
8. ~ Namo::VERSION: /0.7.0/0.8.0/
|
|
15
|
+
|
|
4
16
|
20260520
|
|
5
17
|
0.7.0: + derived-dimension surfacing, lazy single-column access, live views
|
|
6
18
|
|
|
@@ -14,8 +26,12 @@ _________
|
|
|
14
26
|
8. ~ Namo#canonical_data: Sorts by data_dimensions to preserve 0.6.0 row-equality semantics under the broader dimensions definition.
|
|
15
27
|
9. /raise_unless_matching_dimensions/raise_unless_matching_data_dimensions/: Private helper renamed to reflect what it actually compares.
|
|
16
28
|
10. ~ test/namo_test.rb: + Tests for #data_dimensions, #derived_dimensions, the no-arg/single-arg/multi-arg forms of #values and #coordinates, derived-dimension surfacing in #dimensions, #to_h, the coordinates(dim) == values(dim).uniq consistency property, and live-view semantics (added rows / formulae reflected on next call).
|
|
17
|
-
11. ~
|
|
18
|
-
12. ~
|
|
29
|
+
11. ~ README.md: + Coordinates and values section covering #values, #coordinates(*dims), #data_dimensions, #derived_dimensions, and #to_h.
|
|
30
|
+
12. ~ COMPARISON.md: + "Worked example: comparing yesterday's screen to today's" under the set-operators section; + "Schema dispatch on incoming data feeds" under the equality/=== section. Date bumped to 20260520.
|
|
31
|
+
13. ~ EXAMPLES.md: - "Schema dispatch on incoming data feeds", - "Comparing yesterday's screen to today's" — both relocated to COMPARISON.md as worked examples under the matching feature sections. Date bumped to 20260520.
|
|
32
|
+
14. ~ ROADMAP.md: Promote 0.7.0 from upcoming to shipped under "Current state: 0.7.0"; point "next phase" at 0.8.0+.
|
|
33
|
+
15. ~ Rakefile: + -V mainfont=Charter -V monofont=Menlo on pandoc invocation in docs:md2pdf, for a cleaner serif body font and so code spans containing Unicode math glyphs (e.g. ∅) render correctly under xelatex.
|
|
34
|
+
16. ~ Namo::VERSION: /0.6.0/0.7.0/
|
|
19
35
|
|
|
20
36
|
20260511
|
|
21
37
|
0.6.0: + equality, pattern-match, and subset/superset operators
|
data/README.md
CHANGED
|
@@ -79,8 +79,41 @@ sales[quarter: ['Q1']]
|
|
|
79
79
|
# {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
80
80
|
# {product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40}
|
|
81
81
|
# ]>
|
|
82
|
+
|
|
83
|
+
# Proc predicate
|
|
84
|
+
sales[price: ->(v){v < 20.0}]
|
|
85
|
+
# => #<Namo [
|
|
86
|
+
# {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
87
|
+
# {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
|
|
88
|
+
# ]>
|
|
89
|
+
|
|
90
|
+
# Regex predicate
|
|
91
|
+
sales[product: /^W/]
|
|
92
|
+
# => #<Namo [
|
|
93
|
+
# {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
94
|
+
# {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
|
|
95
|
+
# ]>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Procs receive the dimension value and select the row when they return truthy. They handle arbitrary predicates — multi-condition tests, nil-aware checks, anything Ruby can express — and compose with everything else:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
sales[price: ->(v){v < 20.0}, quantity: ->(v){v > 100}]
|
|
102
|
+
# => #<Namo [
|
|
103
|
+
# {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
|
|
104
|
+
# ]>
|
|
82
105
|
```
|
|
83
106
|
|
|
107
|
+
Regexes match against the dimension value coerced with `to_s`, so they work against strings, symbols, numbers, dates, or anything else with a sensible string form. `nil` becomes `""` — `//` matches it, `/./` doesn't.
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
sales[product: /widget/i] # case-insensitive
|
|
111
|
+
sales[product: /Widget|Gadget/] # alternation
|
|
112
|
+
sales[product: /^W/, quarter: 'Q1'] # mixed with exact
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Procs and regexes mix freely with exact values, arrays, ranges, projection, and contraction in the same `[]` call.
|
|
116
|
+
|
|
84
117
|
### Projection
|
|
85
118
|
|
|
86
119
|
Project to specific dimensions:
|
data/lib/Namo/Row.rb
CHANGED
data/lib/Namo/VERSION.rb
CHANGED
data/test/Namo/Row_test.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require 'date'
|
|
1
2
|
require 'minitest/autorun'
|
|
2
3
|
require 'minitest-spec-context'
|
|
3
4
|
|
|
@@ -59,6 +60,154 @@ describe Namo::Row do
|
|
|
59
60
|
_(row.match?(product: 'Widget', quarter: 'Q1')).must_equal true
|
|
60
61
|
_(row.match?(product: 'Widget', quarter: 'Q2')).must_equal false
|
|
61
62
|
end
|
|
63
|
+
|
|
64
|
+
describe "Proc predicates" do
|
|
65
|
+
it "matches when the proc returns true" do
|
|
66
|
+
_(row.match?(price: ->(v){v < 15.0})).must_equal true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "doesn't match when the proc returns false" do
|
|
70
|
+
_(row.match?(price: ->(v){v > 100.0})).must_equal false
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "doesn't match when the proc returns nil" do
|
|
74
|
+
_(row.match?(price: ->(v){nil})).must_equal false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "matches when the proc returns a truthy non-boolean" do
|
|
78
|
+
_(row.match?(price: ->(v){"truthy"})).must_equal true
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "passes nil to the proc when the dimension is missing" do
|
|
82
|
+
seen = nil
|
|
83
|
+
row.match?(missing: ->(v){seen = v; true})
|
|
84
|
+
_(seen).must_be_nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "lets the proc decide what to do with a nil value" do
|
|
88
|
+
_(row.match?(missing: ->(v){v.nil?})).must_equal true
|
|
89
|
+
_(row.match?(missing: ->(v){!v.nil?})).must_equal false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "composes with an exact value on another dimension" do
|
|
93
|
+
_(row.match?(price: ->(v){v < 15.0}, product: 'Widget')).must_equal true
|
|
94
|
+
_(row.match?(price: ->(v){v < 15.0}, product: 'Gadget')).must_equal false
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "composes with an array on another dimension" do
|
|
98
|
+
_(row.match?(price: ->(v){v < 15.0}, product: ['Widget', 'Gadget'])).must_equal true
|
|
99
|
+
_(row.match?(price: ->(v){v < 15.0}, product: ['Gadget'])).must_equal false
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "composes with a range on another dimension" do
|
|
103
|
+
_(row.match?(price: ->(v){v < 15.0}, quantity: 50..150)).must_equal true
|
|
104
|
+
_(row.match?(price: ->(v){v < 15.0}, quantity: 200..300)).must_equal false
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "composes with a regex on another dimension" do
|
|
108
|
+
_(row.match?(price: ->(v){v < 15.0}, product: /^W/)).must_equal true
|
|
109
|
+
_(row.match?(price: ->(v){v < 15.0}, product: /^G/)).must_equal false
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "composes multiple proc predicates across dimensions" do
|
|
113
|
+
_(row.match?(
|
|
114
|
+
price: ->(v){v < 15.0},
|
|
115
|
+
quantity: ->(v){v >= 100}
|
|
116
|
+
)).must_equal true
|
|
117
|
+
_(row.match?(
|
|
118
|
+
price: ->(v){v < 15.0},
|
|
119
|
+
quantity: ->(v){v >= 200}
|
|
120
|
+
)).must_equal false
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "carries through to a formula-defined dimension" do
|
|
124
|
+
formulae[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
125
|
+
_(row.match?(revenue: ->(v){v == 1000.0})).must_equal true
|
|
126
|
+
_(row.match?(revenue: ->(v){v > 5000.0})).must_equal false
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
describe "Regexp predicates" do
|
|
131
|
+
it "matches against a String value" do
|
|
132
|
+
_(row.match?(product: /Widget/)).must_equal true
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "doesn't match when the regex doesn't apply" do
|
|
136
|
+
_(row.match?(product: /Gadget/)).must_equal false
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "supports case-insensitive matching" do
|
|
140
|
+
_(row.match?(product: /widget/i)).must_equal true
|
|
141
|
+
_(row.match?(product: /widget/)).must_equal false
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "supports anchored patterns" do
|
|
145
|
+
_(row.match?(product: /^Wid/)).must_equal true
|
|
146
|
+
_(row.match?(product: /^Gad/)).must_equal false
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it "coerces Integer values via to_s" do
|
|
150
|
+
_(row.match?(quantity: /100/)).must_equal true
|
|
151
|
+
_(row.match?(quantity: /^1/)).must_equal true
|
|
152
|
+
_(row.match?(quantity: /^9/)).must_equal false
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "coerces Float values via to_s" do
|
|
156
|
+
_(row.match?(price: /^10\./)).must_equal true
|
|
157
|
+
_(row.match?(price: /\.0$/)).must_equal true
|
|
158
|
+
_(row.match?(price: /^99/)).must_equal false
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "coerces Date values via to_s" do
|
|
162
|
+
row_data[:date] = Date.new(2026, 5, 21)
|
|
163
|
+
_(row.match?(date: /^2026/)).must_equal true
|
|
164
|
+
_(row.match?(date: /-05-/)).must_equal true
|
|
165
|
+
_(row.match?(date: /^2025/)).must_equal false
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "coerces Symbol values via to_s" do
|
|
169
|
+
row_data[:tag] = :priority
|
|
170
|
+
_(row.match?(tag: /priority/)).must_equal true
|
|
171
|
+
_(row.match?(tag: /^pri/)).must_equal true
|
|
172
|
+
_(row.match?(tag: /xyz/)).must_equal false
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "coerces nil to an empty string" do
|
|
176
|
+
_(row.match?(missing: //)).must_equal true
|
|
177
|
+
_(row.match?(missing: /./)).must_equal false
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it "composes with an exact value on another dimension" do
|
|
181
|
+
_(row.match?(product: /^W/, quarter: 'Q1')).must_equal true
|
|
182
|
+
_(row.match?(product: /^W/, quarter: 'Q2')).must_equal false
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it "composes with an array on another dimension" do
|
|
186
|
+
_(row.match?(product: /^W/, quarter: ['Q1', 'Q2'])).must_equal true
|
|
187
|
+
_(row.match?(product: /^W/, quarter: ['Q3'])).must_equal false
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it "composes with a range on another dimension" do
|
|
191
|
+
_(row.match?(product: /^W/, price: 5.0..15.0)).must_equal true
|
|
192
|
+
_(row.match?(product: /^W/, price: 20.0..30.0)).must_equal false
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it "composes with a proc on another dimension" do
|
|
196
|
+
_(row.match?(product: /^W/, quantity: ->(v){v >= 100})).must_equal true
|
|
197
|
+
_(row.match?(product: /^W/, quantity: ->(v){v >= 200})).must_equal false
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "composes multiple regex predicates across dimensions" do
|
|
201
|
+
_(row.match?(product: /^W/, quarter: /^Q/)).must_equal true
|
|
202
|
+
_(row.match?(product: /^W/, quarter: /^X/)).must_equal false
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it "carries through to a formula-defined dimension" do
|
|
206
|
+
formulae[:label] = proc{|r| "#{r[:product]}-#{r[:quarter]}"}
|
|
207
|
+
_(row.match?(label: /Widget-Q1/)).must_equal true
|
|
208
|
+
_(row.match?(label: /Gadget/)).must_equal false
|
|
209
|
+
end
|
|
210
|
+
end
|
|
62
211
|
end
|
|
63
212
|
|
|
64
213
|
describe "#to_h" do
|
data/test/namo_test.rb
CHANGED
|
@@ -272,6 +272,110 @@ describe Namo do
|
|
|
272
272
|
]
|
|
273
273
|
end
|
|
274
274
|
end
|
|
275
|
+
|
|
276
|
+
context "proc selection" do
|
|
277
|
+
it "selects rows where the proc returns truthy" do
|
|
278
|
+
result = sales[price: ->(v){v < 15.0}]
|
|
279
|
+
_(result.to_a.count).must_equal 2
|
|
280
|
+
_(result.to_a.map{|row| row[:product]}).must_equal ['Widget', 'Widget']
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
it "selects on multiple proc dimensions" do
|
|
284
|
+
result = sales[price: ->(v){v < 30.0}, quantity: ->(v){v > 50}]
|
|
285
|
+
_(result.to_a).must_equal [
|
|
286
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
287
|
+
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
|
|
288
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
289
|
+
]
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
it "composes with projection in a single call" do
|
|
293
|
+
result = sales[:product, :price, price: ->(v){v < 15.0}]
|
|
294
|
+
_(result.to_a).must_equal [
|
|
295
|
+
{product: 'Widget', price: 10.0},
|
|
296
|
+
{product: 'Widget', price: 10.0}
|
|
297
|
+
]
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
it "composes with contraction in a single call" do
|
|
301
|
+
result = sales[-:quantity, price: ->(v){v < 15.0}]
|
|
302
|
+
_(result.to_a).must_equal [
|
|
303
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0},
|
|
304
|
+
{product: 'Widget', quarter: 'Q2', price: 10.0}
|
|
305
|
+
]
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
it "selects on a formula-defined dimension" do
|
|
309
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
310
|
+
result = sales[revenue: ->(v){v >= 1500.0}]
|
|
311
|
+
_(result.to_a).must_equal [
|
|
312
|
+
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
|
|
313
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
314
|
+
]
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
context "regex selection" do
|
|
319
|
+
it "selects by regex against String values" do
|
|
320
|
+
result = sales[product: /^W/]
|
|
321
|
+
_(result.to_a.count).must_equal 2
|
|
322
|
+
_(result.to_a.map{|row| row[:product]}).must_equal ['Widget', 'Widget']
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
it "supports case-insensitive matching" do
|
|
326
|
+
result = sales[product: /widget/i]
|
|
327
|
+
_(result.to_a.count).must_equal 2
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
it "supports alternation" do
|
|
331
|
+
result = sales[product: /Widget|Gadget/]
|
|
332
|
+
_(result.to_a.count).must_equal 4
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
it "coerces non-String values via to_s" do
|
|
336
|
+
result = sales[quantity: /^1/]
|
|
337
|
+
_(result.to_a.map{|row| row[:quantity]}).must_equal [100, 150]
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
it "composes with an exact value on another dimension" do
|
|
341
|
+
result = sales[product: /^W/, quarter: 'Q1']
|
|
342
|
+
_(result.to_a).must_equal [
|
|
343
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100}
|
|
344
|
+
]
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
it "composes with projection in a single call" do
|
|
348
|
+
result = sales[:product, :quarter, product: /^W/]
|
|
349
|
+
_(result.to_a).must_equal [
|
|
350
|
+
{product: 'Widget', quarter: 'Q1'},
|
|
351
|
+
{product: 'Widget', quarter: 'Q2'}
|
|
352
|
+
]
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
it "composes with contraction in a single call" do
|
|
356
|
+
result = sales[-:price, -:quantity, product: /^W/]
|
|
357
|
+
_(result.to_a).must_equal [
|
|
358
|
+
{product: 'Widget', quarter: 'Q1'},
|
|
359
|
+
{product: 'Widget', quarter: 'Q2'}
|
|
360
|
+
]
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
it "selects on a formula-defined dimension" do
|
|
364
|
+
sales[:label] = proc{|r| "#{r[:product]}-#{r[:quarter]}"}
|
|
365
|
+
result = sales[label: /Widget/]
|
|
366
|
+
_(result.to_a.count).must_equal 2
|
|
367
|
+
_(result.map{|row| row[:label]}).must_equal ['Widget-Q1', 'Widget-Q2']
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
context "mixed proc and regex selection" do
|
|
372
|
+
it "combines a proc and a regex across dimensions" do
|
|
373
|
+
result = sales[product: /^W/, quantity: ->(v){v > 100}]
|
|
374
|
+
_(result.to_a).must_equal [
|
|
375
|
+
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
|
|
376
|
+
]
|
|
377
|
+
end
|
|
378
|
+
end
|
|
275
379
|
end
|
|
276
380
|
|
|
277
381
|
describe "#[]= formulae" do
|