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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0aa48a727f9cc67fae8eea94dcfa8227c66a34aa8918ac65e30b33b7685c82b
4
- data.tar.gz: c8f634b5c41f35337fb843d286edf80cbdabef951b076dd36533f5a2531c3715
3
+ metadata.gz: 86f572755292e2bac9808f09adb3169cec06066a5687ec2ea6bd9354f97d592a
4
+ data.tar.gz: edafcc183a1a9e5fb1212bb66a8b6906b94bf8e37a7375d3f9d304e5632ddba3
5
5
  SHA512:
6
- metadata.gz: 9b196446a0d02dc61790a25ea416bfebdd591020cfd6ef89430f5e8b9d8bd1239cc265bca83f0cd6dde4d42ce3c3927074ac69e1d4e79ae88db667b95caeadf3
7
- data.tar.gz: 3680f44daa8e1e78921d1d286c7f436947b6e436c9308d62038c17a1f9658ae4ccc23196afb2eb44e6b3efb41398ab1b39d8cf3bf813f87c24df12c7cb975586
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. ~ 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.
18
- 12. ~ Namo::VERSION: /0.6.0/0.7.0/
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
@@ -16,6 +16,10 @@ class Namo
16
16
  case coordinate
17
17
  when Array, Range
18
18
  coordinate.include?(self[dimension])
19
+ when Proc
20
+ coordinate.call(self[dimension])
21
+ when Regexp
22
+ coordinate.match?(self[dimension].to_s)
19
23
  else
20
24
  self[dimension] == coordinate
21
25
  end
data/lib/Namo/VERSION.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.7.0'
5
+ VERSION = '0.8.0'
6
6
  end
@@ -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
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.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran