namo 0.3.0 → 0.4.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: a0cb29939a05de5108211f0d9f89b8c6b3cd0826433199f3f4014f1201a28fee
4
- data.tar.gz: d628a872b860561da5c9f02867123864508c34c3311e7d9962457146226d1071
3
+ metadata.gz: 0dbfe699c8bbb334e0f72bbfd1859b0eee910be2c72eaab988da633b58b438e6
4
+ data.tar.gz: 40238ef4593d7820bddfb6cb708823fa9c269a51def947e89cdb43e4b69cbae0
5
5
  SHA512:
6
- metadata.gz: 72b7db10583b8316b0e727b4af8404c2069533049c40e83a3974841b8a1822139898922492f721e31328632b32582c8795a50fb9a53262bee04fa5bc75672713
7
- data.tar.gz: b269559c27dd1999ba2c3fb638c85abc0ee22530d984ffe620b2ac9e3fa1726655ac7c7d26c96273baf1bd1396df6ff0eeaeaf7c4e71753aee588eabdb09417f
6
+ metadata.gz: 9e6dfd0bcf4b8d370373a43e507a4d208e4ccb4478e044eb138bf5f2bfa61612cbb4f10735be691c852d2f8bf672eca70720cc6a5c35cd5fb02fae224388e010
7
+ data.tar.gz: 2b147c6e7faf7b9f92e8e2457fcd657734a35946c326857e3b7c09177ceaa7f3ce4f0892bdcbb68ec251d673e1b298c1ed06416fe43918656734a58bd0c639ea
data/CHANGELOG CHANGED
@@ -1,6 +1,15 @@
1
1
  CHANGELOG
2
2
  _________
3
3
 
4
+ 20260415
5
+ 0.4.0: + concatenation (+) and row removal (-)
6
+
7
+ 1. + Namo#+: Concatenate two Namo instances with matching dimensions. Appends rows from the second to the first. Raises ArgumentError when dimensions differ.
8
+ 2. + Namo#-: Remove rows from the first Namo that appear exactly in the second. Whole-row set difference. Raises ArgumentError when dimensions differ.
9
+ 3. ~ Namo_test.rb: Add tests for #+ and #-.
10
+ 4. ~ README.md: + Concatenation section, + Row removal section.
11
+ 5. ~ Namo::VERSION: /0.3.0/0.4.0/
12
+
4
13
  20260415
5
14
  0.3.0: + contraction
6
15
 
data/README.md CHANGED
@@ -153,6 +153,58 @@ sales[-:price, -:quantity, product: 'Widget']
153
153
 
154
154
  Selection, projection, and contraction always return a new Namo instance, so everything chains.
155
155
 
156
+ ### Concatenation
157
+
158
+ `+` combines two Namo objects that share the same dimensions by appending the rows of the second to the first:
159
+
160
+ ```ruby
161
+ q1_sales = Namo.new([
162
+ {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
163
+ {product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40}
164
+ ])
165
+
166
+ q2_sales = Namo.new([
167
+ {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
168
+ {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
169
+ ])
170
+
171
+ all_sales = q1_sales + q2_sales
172
+ # => #<Namo [
173
+ # {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
174
+ # {product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
175
+ # {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
176
+ # {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
177
+ # ]>
178
+ ```
179
+
180
+ The dimensions must match — concatenating Namo objects with different dimensions raises an `ArgumentError`. Formulae carry through from the left-hand side.
181
+
182
+ ### Row Removal
183
+
184
+ `-` removes from the first Namo any row that appears exactly in the second:
185
+
186
+ ```ruby
187
+ sales = Namo.new([
188
+ {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
189
+ {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
190
+ {product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
191
+ {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
192
+ ])
193
+
194
+ discontinued = Namo.new([
195
+ {product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
196
+ {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
197
+ ])
198
+
199
+ sales - discontinued
200
+ # => #<Namo [
201
+ # {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
202
+ # {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
203
+ # ]>
204
+ ```
205
+
206
+ Removal is exact — every dimension, every value must match. The dimensions must match; different dimensions raise an `ArgumentError`. Formulae carry through from the left-hand side.
207
+
156
208
  ### Formulae
157
209
 
158
210
  Define computed dimensions using `[]=`:
data/lib/Namo/VERSION.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.3.0'
5
+ VERSION = '0.4.0'
6
6
  end
data/lib/namo.rb CHANGED
@@ -56,6 +56,20 @@ class Namo
56
56
  @data.each{|row_data| block.call(Row.new(row_data, @formulae))}
57
57
  end
58
58
 
59
+ def +(other)
60
+ unless dimensions == other.dimensions
61
+ raise ArgumentError, "dimensions do not match"
62
+ end
63
+ self.class.new(@data + other.data, formulae: other.formulae.merge(@formulae))
64
+ end
65
+
66
+ def -(other)
67
+ unless dimensions == other.dimensions
68
+ raise ArgumentError, "dimensions do not match"
69
+ end
70
+ self.class.new(@data - other.data, formulae: @formulae.dup)
71
+ end
72
+
59
73
  def to_a
60
74
  @data.map do |row|
61
75
  row.keys.each_with_object({}) do |key, hash|
data/test/Namo_test.rb CHANGED
@@ -251,6 +251,92 @@ describe Namo do
251
251
  end
252
252
  end
253
253
 
254
+ describe "#+" do
255
+ let(:more_data) do
256
+ [
257
+ {product: 'Widget', quarter: 'Q3', price: 10.0, quantity: 200},
258
+ {product: 'Gadget', quarter: 'Q3', price: 25.0, quantity: 80}
259
+ ]
260
+ end
261
+
262
+ let(:more_sales) do
263
+ Namo.new(more_data)
264
+ end
265
+
266
+ it "concatenates rows" do
267
+ result = sales + more_sales
268
+ _(result.to_a.count).must_equal 6
269
+ _(result.to_a).must_equal(sample_data + more_data)
270
+ end
271
+
272
+ it "preserves dimensions" do
273
+ result = sales + more_sales
274
+ _(result.dimensions).must_equal [:product, :quarter, :price, :quantity]
275
+ end
276
+
277
+ it "carries formulae through from self" do
278
+ sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
279
+ result = sales + more_sales
280
+ _(result.map{|row| row[:revenue]}).must_equal [1000.0, 1500.0, 1000.0, 1500.0, 2000.0, 2000.0]
281
+ end
282
+
283
+ it "merges formulae from other" do
284
+ more_sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
285
+ result = sales + more_sales
286
+ _(result.map{|row| row[:revenue]}).must_equal [1000.0, 1500.0, 1000.0, 1500.0, 2000.0, 2000.0]
287
+ end
288
+
289
+ it "prefers self's formulae on conflict" do
290
+ sales[:label] = proc{|r| "self: #{r[:product]}"}
291
+ more_sales[:label] = proc{|r| "other: #{r[:product]}"}
292
+ result = sales + more_sales
293
+ _(result.map{|row| row[:label]}).must_equal [
294
+ 'self: Widget', 'self: Widget', 'self: Gadget', 'self: Gadget',
295
+ 'self: Widget', 'self: Gadget'
296
+ ]
297
+ end
298
+
299
+ it "raises when dimensions differ" do
300
+ other = Namo.new([{product: 'Widget', quarter: 'Q1'}])
301
+ _ { sales + other }.must_raise ArgumentError
302
+ end
303
+ end
304
+
305
+ describe "#-" do
306
+ let(:to_remove) do
307
+ Namo.new([
308
+ {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
309
+ {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
310
+ ])
311
+ end
312
+
313
+ it "removes matching rows" do
314
+ result = sales - to_remove
315
+ _(result.to_a.count).must_equal 2
316
+ _(result.to_a).must_equal [
317
+ {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
318
+ {product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40}
319
+ ]
320
+ end
321
+
322
+ it "preserves non-matching rows" do
323
+ other = Namo.new([{product: 'Thingo', quarter: 'Q4', price: 99.0, quantity: 1}])
324
+ result = sales - other
325
+ _(result.to_a).must_equal sample_data
326
+ end
327
+
328
+ it "carries formulae through from self" do
329
+ sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
330
+ result = sales - to_remove
331
+ _(result.map{|row| row[:revenue]}).must_equal [1500.0, 1000.0]
332
+ end
333
+
334
+ it "raises when dimensions differ" do
335
+ other = Namo.new([{product: 'Widget', quarter: 'Q1'}])
336
+ _ { sales - other }.must_raise ArgumentError
337
+ end
338
+ end
339
+
254
340
  describe "#to_a" do
255
341
  it "returns the data as an array of hashes" do
256
342
  _(sales.to_a).must_equal sample_data
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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran