namo 0.9.2 → 0.11.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 +24 -0
- data/README.md +27 -1
- data/lib/Namo/Row.rb +12 -0
- data/lib/Namo/VERSION.rb +1 -1
- data/lib/namo.rb +59 -0
- data/test/Namo/Row_test.rb +77 -0
- data/test/namo_test.rb +402 -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: 40c2256efac2663b7593bb1f7e4c3ca08fa07e6e225c75fcef3d6d8658f3794b
|
|
4
|
+
data.tar.gz: abd7bd9076d7019c245d942425232dcf0966edb076cf6d50fdedf7b74efea90f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2b5045c4160d8812fa3f04f10dc027bf8c6cfaf937405cec0570a31af52babdc1f2dc6e25db249a9b9dea81afc9806f617203ae71dded576974fe9512bd4ec00
|
|
7
|
+
data.tar.gz: a553ef0ae27d67bf28d8d91ba9e2e71f217f8ee23ff3d177ad207dd520f30971730774a51660830d98d79a6189cee16f1227dbd168282f3a27c4316912a6f29f
|
data/CHANGELOG
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
CHANGELOG
|
|
2
2
|
_________
|
|
3
3
|
|
|
4
|
+
20260531
|
|
5
|
+
0.11.0: ~ Subset Enumerable methods (select, reject, sort_by, first, last, take, drop, take_while, drop_while, uniq, partition) return Namos
|
|
6
|
+
|
|
7
|
+
1. + Namo#select, Namo#reject, Namo#sort_by, Namo#take_while, Namo#drop_while: Predicate and ordering subset methods that wrap the @data Array result in `self.class.new(..., formulae: @formulae.dup)`, shadowing Enumerable's Array-returning defaults. Blocks receive a Row, so formulae resolve inside the predicate, and formulae carry through to the returned Namo. Namo#select is aliased as filter and find_all (the only overridden method with Enumerable aliases), so the aliases return Namos too.
|
|
8
|
+
2. + Namo#first, Namo#last: With an argument, return a Namo of the first/last n rows. Without an argument, return a single Row (or nil on an empty Namo), per Ruby's Enumerable#first / Array#last convention. first(0) returns an empty Namo (n is truthy). last(n) reads @data.last(n) directly — the efficient path, no Enumerable materialise-then-slice fall-through.
|
|
9
|
+
3. + Namo#take, Namo#drop: Leading subset and its complement, wrapping @data.take(n) / @data.drop(n).
|
|
10
|
+
4. + Namo#uniq: Full-row dedupe. The no-block path dedupes raw @data hashes via Array#uniq, which uses eql?/hash — matching Row#eql?/Row#hash from 0.10.0, so numeric types stay distinct ({n: 1} and {n: 1.0} are both kept) and Row allocations are avoided. With a block, dedupes on the block's return value, per Enumerable#uniq.
|
|
11
|
+
5. + Namo#partition: Returns [Namo, Namo] — matches and non-matches — each wrapped via self.class.new with duped formulae. map, flat_map, reduce, sum, min_by, max_by, count, and each are deliberately left unchanged (transformed values, scalars, or already-correct). group_by is structurally blocked — it needs Namo::Collection (0.17.0) and lands at 0.18.0.
|
|
12
|
+
6. ~ test/namo_test.rb: + describe blocks for each new method — return-as-Namo, formula references in blocks, formula carry-through, empty-Namo edges, n=0 and n>length boundaries, uniq full-row dedupe plus numeric strictness plus block form, partition summing to the original, and subclass type preservation via Class.new(Namo). + Tests that select's filter and find_all aliases return Namos. + Guard tests that map/flat_map/reduce keep their original return types.
|
|
13
|
+
7. ~ README.md: + Note in the Enumerable section that the subset-returning methods return Namos, with a chaining example and a first/last/uniq semantics paragraph. ~ Overview design-stance sentence to fold the Enumerable subset methods into "Namos in, Namos out".
|
|
14
|
+
8. ~ ROADMAP.md: Promote 0.11.0 from upcoming to shipped under "Current state: 0.11.0"; fold the subset Enumerable methods into the Summary's completed vocabulary and point "next phase" at 0.12.0+; bump Date to 20260531.
|
|
15
|
+
9. ~ Namo::VERSION: /0.10.0/0.11.0/
|
|
16
|
+
|
|
17
|
+
20260528
|
|
18
|
+
0.10.0: + Row value semantics: ==, eql?, hash
|
|
19
|
+
|
|
20
|
+
1. + Namo::Row#==: Data equality. Two Rows are equal iff the other is a Row and their `@row` hashes are `==`. Class identity not gated (Row has no subclass hierarchy with included modules); formulae ignored (a formula is a property of the surrounding Namo, not of the row's identity). Mirrors `Namo#==` one level down.
|
|
21
|
+
2. + Namo::Row#eql?: Strict data equality. Same shape as `==` but uses `Hash#eql?` on `@row`, matching Ruby's numeric-type-strict convention (`{n: 1}` Row is `eql?` to another `{n: 1}` Row, but not to a `{n: 1.0}` Row, just as `1.eql?(1.0)` is false).
|
|
22
|
+
3. + Namo::Row#hash: Returns `@row.hash`. Consistent with `eql?` — Rows that are `eql?` produce equal hashes, so Rows participate correctly in Hash keys, Set members, and Array#uniq.
|
|
23
|
+
4. ~ test/Namo/Row_test.rb: + `#==` tests (equal/different `@row`, non-Row operand, formulae ignored). + `#eql?` tests (equal `@row`, non-Row operand, `Hash#eql?` numeric-type strictness vs `==`, formulae ignored). + `#hash` tests (hash equality for `eql?` Rows, Rows as Hash keys, Rows as Set members deduplicating equal Rows).
|
|
24
|
+
5. ~ README.md: + Note in the Equality section that Row participates in value semantics (`==`, `eql?`, `hash`) on the same data-only basis as `Namo#==`, ignoring formulae.
|
|
25
|
+
6. ~ ROADMAP.md: Promote 0.10.0 from upcoming to shipped under "Current state: 0.10.0"; revise Summary to point "next phase" at 0.11.0+ and note Row value semantics among the algebra-completing pieces.
|
|
26
|
+
7. ~ Namo::VERSION: /0.9.2/0.10.0/
|
|
27
|
+
|
|
4
28
|
20260525
|
|
5
29
|
0.9.2: ~ Auto-load Namo::VERSION
|
|
6
30
|
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Named dimensional data for Ruby.
|
|
|
4
4
|
|
|
5
5
|
Namo is a Ruby library for working with multi-dimensional data using named dimensions. It infers dimensions and coordinates from plain arrays of hashes — the same shape you get from databases, CSV files, JSON, and YAML — so there's no reshaping step.
|
|
6
6
|
|
|
7
|
-
The design rests on a few stances: every hash key is a dimension and none is privileged as a coordinate or value; formulae attach to a Namo alongside data and re-evaluate on each access, appearing as derived dimensions alongside the data dimensions; operators that combine Namos all take Namos and return Namos
|
|
7
|
+
The design rests on a few stances: every hash key is a dimension and none is privileged as a coordinate or value; formulae attach to a Namo alongside data and re-evaluate on each access, appearing as derived dimensions alongside the data dimensions; operators that combine Namos all take Namos and return Namos — as do the subset-returning Enumerable methods (`select`, `reject`, `sort_by`, `uniq`, and the rest) — so analytical pipelines close; and the formula mechanism is type-agnostic — strings, dates, booleans, and arbitrary Ruby objects work as readily as numbers.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
@@ -486,6 +486,18 @@ The two `:revenue` procs are independently-written and not the same object — `
|
|
|
486
486
|
|
|
487
487
|
Each comparison operator answers a distinct question: `eql?` is strictest (class + data + formula names); `==` is data identity; `===` is analytical identity; the subset operators are data containment.
|
|
488
488
|
|
|
489
|
+
Rows participate in value semantics on the same data-only basis. `Row#==`, `Row#eql?`, and `Row#hash` compare the underlying row hash and ignore the surrounding Namo's formulae — two Rows with the same data are equal regardless of which Namo yielded them. This makes Rows usable as Hash keys and Set members, and underwrites whole-row deduplication on the Enumerable side:
|
|
490
|
+
|
|
491
|
+
```ruby
|
|
492
|
+
a = Namo.new([{x: 1}]).first
|
|
493
|
+
b = Namo.new([{x: 1}]).first
|
|
494
|
+
a == b # => true
|
|
495
|
+
a.eql?(b) # => true
|
|
496
|
+
{a => :found}[b] # => :found
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
The omission of `Row#===` and Row-level `<`/`<=`/`>`/`>=` is deliberate: a Row is a record, not a collection, so the set-theoretic operators don't translate. The value-semantics trio (`==`, `eql?`, `hash`) is what a hash-shaped value needs to behave correctly in Ruby's collection machinery; that's the whole Row-comparison story.
|
|
500
|
+
|
|
489
501
|
### Subset and Superset
|
|
490
502
|
|
|
491
503
|
`<`, `<=`, `>`, `>=` are multiset subset and superset relations on rows.
|
|
@@ -664,6 +676,20 @@ sales.flat_map{|row| [row[:price]]}
|
|
|
664
676
|
# => [10.0, 10.0, 25.0, 25.0]
|
|
665
677
|
```
|
|
666
678
|
|
|
679
|
+
The subset-returning Enumerable methods — `select`, `reject`, `sort_by`, `first(n)`, `last(n)`, `take`, `drop`, `take_while`, `drop_while`, `uniq`, and `partition` — return Namos rather than Arrays (`partition` returns `[Namo, Namo]`), carrying formulae through. This keeps the analytical chain closed: the result of a filter is still selectable, projectable, and operable, exactly like the operators that combine Namos:
|
|
680
|
+
|
|
681
|
+
```ruby
|
|
682
|
+
sales.select{|row| row[:price] < 20.0}.values(:price).sum
|
|
683
|
+
# => 20.0
|
|
684
|
+
|
|
685
|
+
sales.select{|row| row[:price] < 20.0}[product: 'Widget'][:quarter, :revenue].to_a
|
|
686
|
+
# => [{quarter: 'Q1', revenue: 1000.0}, {quarter: 'Q2', revenue: 1500.0}]
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
Without an argument, `first` and `last` return a single `Row` (or `nil` on an empty Namo), following Ruby's convention; with an argument they return a Namo of that many rows. `uniq` dedupes on full-row equality (`Row#==`), or on a block's return value when given one. `select`'s aliases `filter` and `find_all` follow the override and return Namos too.
|
|
690
|
+
|
|
691
|
+
The transforming and reducing methods are deliberately left as Enumerable's defaults, because their results aren't row-shaped and so can't be a Namo: `map`/`collect` and `flat_map` return Arrays of whatever the block produces; `reduce`/`inject`, `sum`, `count`, `min_by`, and `max_by` return scalars. `each` is unchanged — it yields Rows, or returns an Enumerator with no block.
|
|
692
|
+
|
|
667
693
|
### Extracting data
|
|
668
694
|
|
|
669
695
|
`to_a` returns an array of hashes — the row-oriented form:
|
data/lib/Namo/Row.rb
CHANGED
|
@@ -11,6 +11,18 @@ class Namo
|
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
def ==(other)
|
|
15
|
+
other.is_a?(Row) && @row == other.to_h
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def eql?(other)
|
|
19
|
+
other.is_a?(Row) && @row.eql?(other.to_h)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def hash
|
|
23
|
+
@row.hash
|
|
24
|
+
end
|
|
25
|
+
|
|
14
26
|
def match?(selections)
|
|
15
27
|
selections.all? do |dimension, coordinate|
|
|
16
28
|
case coordinate
|
data/lib/Namo/VERSION.rb
CHANGED
data/lib/namo.rb
CHANGED
|
@@ -81,6 +81,65 @@ class Namo
|
|
|
81
81
|
@data.each{|row_data| block.call(Row.new(row_data, @formulae))}
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
def select(&block)
|
|
85
|
+
self.class.new(@data.select{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
|
|
86
|
+
end
|
|
87
|
+
alias_method :filter, :select
|
|
88
|
+
alias_method :find_all, :select
|
|
89
|
+
|
|
90
|
+
def reject(&block)
|
|
91
|
+
self.class.new(@data.reject{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def sort_by(&block)
|
|
95
|
+
self.class.new(@data.sort_by{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def first(n = nil)
|
|
99
|
+
if n
|
|
100
|
+
self.class.new(@data.first(n), formulae: @formulae.dup)
|
|
101
|
+
else
|
|
102
|
+
@data.first ? Row.new(@data.first, @formulae) : nil
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def last(n = nil)
|
|
107
|
+
if n
|
|
108
|
+
self.class.new(@data.last(n), formulae: @formulae.dup)
|
|
109
|
+
else
|
|
110
|
+
@data.last ? Row.new(@data.last, @formulae) : nil
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def take(n)
|
|
115
|
+
self.class.new(@data.take(n), formulae: @formulae.dup)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def drop(n)
|
|
119
|
+
self.class.new(@data.drop(n), formulae: @formulae.dup)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def take_while(&block)
|
|
123
|
+
self.class.new(@data.take_while{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def drop_while(&block)
|
|
127
|
+
self.class.new(@data.drop_while{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def uniq(&block)
|
|
131
|
+
rows = block ? @data.uniq{|row| block.call(Row.new(row, @formulae))} : @data.uniq
|
|
132
|
+
self.class.new(rows, formulae: @formulae.dup)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def partition(&block)
|
|
136
|
+
matches, non_matches = @data.partition{|row| block.call(Row.new(row, @formulae))}
|
|
137
|
+
[
|
|
138
|
+
self.class.new(matches, formulae: @formulae.dup),
|
|
139
|
+
self.class.new(non_matches, formulae: @formulae.dup),
|
|
140
|
+
]
|
|
141
|
+
end
|
|
142
|
+
|
|
84
143
|
def +(other)
|
|
85
144
|
raise_unless_namo(other)
|
|
86
145
|
raise_unless_matching_data_dimensions(other)
|
data/test/Namo/Row_test.rb
CHANGED
|
@@ -215,4 +215,81 @@ describe Namo::Row do
|
|
|
215
215
|
_(row.to_h).must_equal row_data
|
|
216
216
|
end
|
|
217
217
|
end
|
|
218
|
+
|
|
219
|
+
describe "#==" do
|
|
220
|
+
it "is true for two Rows with equal @row" do
|
|
221
|
+
a = Namo::Row.new({product: 'Widget', price: 10.0}, {})
|
|
222
|
+
b = Namo::Row.new({product: 'Widget', price: 10.0}, {})
|
|
223
|
+
_(a == b).must_equal true
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it "is false for two Rows with different @row" do
|
|
227
|
+
a = Namo::Row.new({product: 'Widget', price: 10.0}, {})
|
|
228
|
+
b = Namo::Row.new({product: 'Gadget', price: 10.0}, {})
|
|
229
|
+
_(a == b).must_equal false
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it "is false for a non-Row operand" do
|
|
233
|
+
a = Namo::Row.new({product: 'Widget'}, {})
|
|
234
|
+
_(a == {product: 'Widget'}).must_equal false
|
|
235
|
+
_(a == 'Widget').must_equal false
|
|
236
|
+
_(a == nil).must_equal false
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
it "ignores formulae" do
|
|
240
|
+
a = Namo::Row.new({price: 10.0, quantity: 100}, {})
|
|
241
|
+
b = Namo::Row.new({price: 10.0, quantity: 100}, {revenue: proc{|r| r[:price] * r[:quantity]}})
|
|
242
|
+
_(a == b).must_equal true
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
describe "#eql?" do
|
|
247
|
+
it "is true for two Rows with eql? @row" do
|
|
248
|
+
a = Namo::Row.new({product: 'Widget', price: 10.0}, {})
|
|
249
|
+
b = Namo::Row.new({product: 'Widget', price: 10.0}, {})
|
|
250
|
+
_(a.eql?(b)).must_equal true
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it "is false for a non-Row operand" do
|
|
254
|
+
a = Namo::Row.new({product: 'Widget'}, {})
|
|
255
|
+
_(a.eql?({product: 'Widget'})).must_equal false
|
|
256
|
+
_(a.eql?(nil)).must_equal false
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
it "distinguishes numeric types the way Hash#eql? does" do
|
|
260
|
+
a = Namo::Row.new({n: 1}, {})
|
|
261
|
+
b = Namo::Row.new({n: 1.0}, {})
|
|
262
|
+
_(a == b).must_equal true
|
|
263
|
+
_(a.eql?(b)).must_equal false
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
it "ignores formulae" do
|
|
267
|
+
a = Namo::Row.new({price: 10.0, quantity: 100}, {})
|
|
268
|
+
b = Namo::Row.new({price: 10.0, quantity: 100}, {revenue: proc{|r| r[:price] * r[:quantity]}})
|
|
269
|
+
_(a.eql?(b)).must_equal true
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
describe "#hash" do
|
|
274
|
+
it "is equal for two Rows that are eql?" do
|
|
275
|
+
a = Namo::Row.new({product: 'Widget', price: 10.0}, {})
|
|
276
|
+
b = Namo::Row.new({product: 'Widget', price: 10.0}, {})
|
|
277
|
+
_(a.hash).must_equal b.hash
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
it "lets Rows work as Hash keys" do
|
|
281
|
+
a = Namo::Row.new({product: 'Widget'}, {})
|
|
282
|
+
b = Namo::Row.new({product: 'Gadget'}, {})
|
|
283
|
+
lookup = Namo::Row.new({product: 'Widget'}, {})
|
|
284
|
+
h = {a => :x, b => :y}
|
|
285
|
+
_(h[lookup]).must_equal :x
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
it "lets Array#uniq dedupe equal Rows" do
|
|
289
|
+
a = Namo::Row.new({product: 'Widget'}, {})
|
|
290
|
+
b = Namo::Row.new({product: 'Gadget'}, {})
|
|
291
|
+
duplicate_of_a = Namo::Row.new({product: 'Widget'}, {})
|
|
292
|
+
_([a, b, duplicate_of_a].uniq.length).must_equal 2
|
|
293
|
+
end
|
|
294
|
+
end
|
|
218
295
|
end
|
data/test/namo_test.rb
CHANGED
|
@@ -511,6 +511,408 @@ describe Namo do
|
|
|
511
511
|
end
|
|
512
512
|
end
|
|
513
513
|
|
|
514
|
+
describe "#select" do
|
|
515
|
+
it "returns a Namo of matching rows" do
|
|
516
|
+
result = sales.select{|row| row[:price] < 20.0}
|
|
517
|
+
_(result).must_be_kind_of Namo
|
|
518
|
+
_(result.to_a).must_equal [
|
|
519
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
520
|
+
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
|
|
521
|
+
]
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
it "selects using formula references in the block" do
|
|
525
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
526
|
+
result = sales.select{|row| row[:revenue] >= 1500.0}
|
|
527
|
+
_(result.to_a).must_equal [
|
|
528
|
+
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
|
|
529
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
530
|
+
]
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
it "preserves formulae through to the returned Namo" do
|
|
534
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
535
|
+
result = sales.select{|row| row[:price] < 20.0}
|
|
536
|
+
_(result.values(:revenue)).must_equal [1000.0, 1500.0]
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
it "returns an empty Namo when nothing matches" do
|
|
540
|
+
result = sales.select{|row| row[:price] > 1000.0}
|
|
541
|
+
_(result).must_be_kind_of Namo
|
|
542
|
+
_(result.to_a).must_equal []
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
it "returns an instance of self's class" do
|
|
546
|
+
subclass = Class.new(Namo)
|
|
547
|
+
result = subclass.new(sample_data).select{|row| row[:price] < 20.0}
|
|
548
|
+
_(result.class).must_equal subclass
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
it "is aliased as filter, returning a Namo" do
|
|
552
|
+
result = sales.filter{|row| row[:price] < 20.0}
|
|
553
|
+
_(result).must_be_kind_of Namo
|
|
554
|
+
_(result.values(:product)).must_equal ['Widget', 'Widget']
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
it "is aliased as find_all, returning a Namo" do
|
|
558
|
+
result = sales.find_all{|row| row[:price] < 20.0}
|
|
559
|
+
_(result).must_be_kind_of Namo
|
|
560
|
+
_(result.values(:product)).must_equal ['Widget', 'Widget']
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
describe "#reject" do
|
|
565
|
+
it "returns the complement of select" do
|
|
566
|
+
result = sales.reject{|row| row[:price] < 20.0}
|
|
567
|
+
_(result).must_be_kind_of Namo
|
|
568
|
+
_(result.to_a).must_equal [
|
|
569
|
+
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
|
|
570
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
571
|
+
]
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
it "together with select sums to the original" do
|
|
575
|
+
selected = sales.select{|row| row[:price] < 20.0}
|
|
576
|
+
rejected = sales.reject{|row| row[:price] < 20.0}
|
|
577
|
+
_((selected.to_a + rejected.to_a).length).must_equal sample_data.length
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
it "preserves formulae through to the returned Namo" do
|
|
581
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
582
|
+
result = sales.reject{|row| row[:price] < 20.0}
|
|
583
|
+
_(result.values(:revenue)).must_equal [1000.0, 1500.0]
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
it "returns an instance of self's class" do
|
|
587
|
+
subclass = Class.new(Namo)
|
|
588
|
+
result = subclass.new(sample_data).reject{|row| row[:price] < 20.0}
|
|
589
|
+
_(result.class).must_equal subclass
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
describe "#sort_by" do
|
|
594
|
+
it "returns rows in the specified order" do
|
|
595
|
+
result = sales.sort_by{|row| row[:quantity]}
|
|
596
|
+
_(result).must_be_kind_of Namo
|
|
597
|
+
_(result.values(:quantity)).must_equal [40, 60, 100, 150]
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
it "sorts using formula references in the block" do
|
|
601
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
602
|
+
result = sales.sort_by{|row| row[:revenue]}
|
|
603
|
+
_(result.values(:revenue)).must_equal [1000.0, 1000.0, 1500.0, 1500.0]
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
it "returns an instance of self's class" do
|
|
607
|
+
subclass = Class.new(Namo)
|
|
608
|
+
result = subclass.new(sample_data).sort_by{|row| row[:quantity]}
|
|
609
|
+
_(result.class).must_equal subclass
|
|
610
|
+
end
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
describe "#first" do
|
|
614
|
+
it "with an argument returns a Namo of the first n rows" do
|
|
615
|
+
result = sales.first(2)
|
|
616
|
+
_(result).must_be_kind_of Namo
|
|
617
|
+
_(result.to_a).must_equal [
|
|
618
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
619
|
+
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
|
|
620
|
+
]
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
it "with an argument of 0 returns an empty Namo" do
|
|
624
|
+
result = sales.first(0)
|
|
625
|
+
_(result).must_be_kind_of Namo
|
|
626
|
+
_(result.to_a).must_equal []
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
it "without an argument returns a Row" do
|
|
630
|
+
result = sales.first
|
|
631
|
+
_(result).must_be_kind_of Namo::Row
|
|
632
|
+
_(result[:product]).must_equal 'Widget'
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
it "without an argument on an empty Namo returns nil" do
|
|
636
|
+
_(Namo.new.first).must_be_nil
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
it "preserves formulae through to the returned Namo" do
|
|
640
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
641
|
+
_(sales.first(2).values(:revenue)).must_equal [1000.0, 1500.0]
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
it "returns an instance of self's class with an argument" do
|
|
645
|
+
subclass = Class.new(Namo)
|
|
646
|
+
_(subclass.new(sample_data).first(2).class).must_equal subclass
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
describe "#last" do
|
|
651
|
+
it "with an argument returns a Namo of the last n rows" do
|
|
652
|
+
result = sales.last(2)
|
|
653
|
+
_(result).must_be_kind_of Namo
|
|
654
|
+
_(result.to_a).must_equal [
|
|
655
|
+
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
|
|
656
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
657
|
+
]
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
it "with an argument of 0 returns an empty Namo" do
|
|
661
|
+
result = sales.last(0)
|
|
662
|
+
_(result).must_be_kind_of Namo
|
|
663
|
+
_(result.to_a).must_equal []
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
it "without an argument returns a Row" do
|
|
667
|
+
result = sales.last
|
|
668
|
+
_(result).must_be_kind_of Namo::Row
|
|
669
|
+
_(result[:product]).must_equal 'Gadget'
|
|
670
|
+
_(result[:quarter]).must_equal 'Q2'
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
it "without an argument on an empty Namo returns nil" do
|
|
674
|
+
_(Namo.new.last).must_be_nil
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
it "preserves formulae through to the returned Namo" do
|
|
678
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
679
|
+
_(sales.last(2).values(:revenue)).must_equal [1000.0, 1500.0]
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
it "returns an instance of self's class with an argument" do
|
|
683
|
+
subclass = Class.new(Namo)
|
|
684
|
+
_(subclass.new(sample_data).last(2).class).must_equal subclass
|
|
685
|
+
end
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
describe "#take" do
|
|
689
|
+
it "returns a Namo of the first n rows" do
|
|
690
|
+
result = sales.take(2)
|
|
691
|
+
_(result).must_be_kind_of Namo
|
|
692
|
+
_(result.values(:quantity)).must_equal [100, 150]
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
it "returns an empty Namo for n of 0" do
|
|
696
|
+
_(sales.take(0).to_a).must_equal []
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
it "returns all rows when n exceeds the length" do
|
|
700
|
+
_(sales.take(10).to_a).must_equal sample_data
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
it "returns an instance of self's class" do
|
|
704
|
+
subclass = Class.new(Namo)
|
|
705
|
+
_(subclass.new(sample_data).take(2).class).must_equal subclass
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
describe "#drop" do
|
|
710
|
+
it "returns a Namo of all rows past the first n" do
|
|
711
|
+
result = sales.drop(2)
|
|
712
|
+
_(result).must_be_kind_of Namo
|
|
713
|
+
_(result.values(:quantity)).must_equal [40, 60]
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
it "returns all rows for n of 0" do
|
|
717
|
+
_(sales.drop(0).to_a).must_equal sample_data
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
it "returns an empty Namo when n exceeds the length" do
|
|
721
|
+
_(sales.drop(10).to_a).must_equal []
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
it "returns an instance of self's class" do
|
|
725
|
+
subclass = Class.new(Namo)
|
|
726
|
+
_(subclass.new(sample_data).drop(2).class).must_equal subclass
|
|
727
|
+
end
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
describe "#take_while" do
|
|
731
|
+
it "returns a Namo of leading rows while the predicate holds" do
|
|
732
|
+
result = sales.take_while{|row| row[:price] < 20.0}
|
|
733
|
+
_(result).must_be_kind_of Namo
|
|
734
|
+
_(result.values(:product)).must_equal ['Widget', 'Widget']
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
it "evaluates the predicate against formula references" do
|
|
738
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
739
|
+
result = sales.take_while{|row| row[:revenue] < 1500.0}
|
|
740
|
+
_(result.values(:revenue)).must_equal [1000.0]
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
it "returns an instance of self's class" do
|
|
744
|
+
subclass = Class.new(Namo)
|
|
745
|
+
_(subclass.new(sample_data).take_while{|row| row[:price] < 20.0}.class).must_equal subclass
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
describe "#drop_while" do
|
|
750
|
+
it "returns a Namo of rows from the first predicate failure" do
|
|
751
|
+
result = sales.drop_while{|row| row[:price] < 20.0}
|
|
752
|
+
_(result).must_be_kind_of Namo
|
|
753
|
+
_(result.values(:product)).must_equal ['Gadget', 'Gadget']
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
it "evaluates the predicate against formula references" do
|
|
757
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
758
|
+
result = sales.drop_while{|row| row[:revenue] < 1500.0}
|
|
759
|
+
_(result.values(:revenue)).must_equal [1500.0, 1000.0, 1500.0]
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
it "returns an instance of self's class" do
|
|
763
|
+
subclass = Class.new(Namo)
|
|
764
|
+
_(subclass.new(sample_data).drop_while{|row| row[:price] < 20.0}.class).must_equal subclass
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
describe "#uniq" do
|
|
769
|
+
let(:dup_data) do
|
|
770
|
+
[
|
|
771
|
+
{product: 'Widget', quarter: 'Q1'},
|
|
772
|
+
{product: 'Widget', quarter: 'Q1'},
|
|
773
|
+
{product: 'Gadget', quarter: 'Q1'},
|
|
774
|
+
{product: 'Widget', quarter: 'Q2'}
|
|
775
|
+
]
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
it "without a block dedupes rows on full-row equality" do
|
|
779
|
+
result = Namo.new(dup_data).uniq
|
|
780
|
+
_(result).must_be_kind_of Namo
|
|
781
|
+
_(result.to_a).must_equal [
|
|
782
|
+
{product: 'Widget', quarter: 'Q1'},
|
|
783
|
+
{product: 'Gadget', quarter: 'Q1'},
|
|
784
|
+
{product: 'Widget', quarter: 'Q2'}
|
|
785
|
+
]
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
it "distinguishes numeric types, matching Row#eql? semantics" do
|
|
789
|
+
result = Namo.new([{n: 1}, {n: 1.0}]).uniq
|
|
790
|
+
_(result.to_a).must_equal [{n: 1}, {n: 1.0}]
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
it "with a block dedupes on the block's return value" do
|
|
794
|
+
result = Namo.new(dup_data).uniq{|row| row[:product]}
|
|
795
|
+
_(result.to_a).must_equal [
|
|
796
|
+
{product: 'Widget', quarter: 'Q1'},
|
|
797
|
+
{product: 'Gadget', quarter: 'Q1'}
|
|
798
|
+
]
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
it "preserves formulae through to the returned Namo" do
|
|
802
|
+
namo = Namo.new(dup_data)
|
|
803
|
+
namo[:label] = proc{|r| "#{r[:product]}-#{r[:quarter]}"}
|
|
804
|
+
result = namo.uniq
|
|
805
|
+
_(result.values(:label)).must_equal ['Widget-Q1', 'Gadget-Q1', 'Widget-Q2']
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
it "returns an instance of self's class" do
|
|
809
|
+
subclass = Class.new(Namo)
|
|
810
|
+
_(subclass.new(dup_data).uniq.class).must_equal subclass
|
|
811
|
+
end
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
describe "#partition" do
|
|
815
|
+
it "returns a two-element Array of Namos" do
|
|
816
|
+
result = sales.partition{|row| row[:price] < 20.0}
|
|
817
|
+
_(result).must_be_kind_of Array
|
|
818
|
+
_(result.length).must_equal 2
|
|
819
|
+
_(result[0]).must_be_kind_of Namo
|
|
820
|
+
_(result[1]).must_be_kind_of Namo
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
it "splits into matches and non-matches summing to the original" do
|
|
824
|
+
matches, non_matches = sales.partition{|row| row[:price] < 20.0}
|
|
825
|
+
_(matches.to_a).must_equal [
|
|
826
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
827
|
+
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
|
|
828
|
+
]
|
|
829
|
+
_(non_matches.to_a).must_equal [
|
|
830
|
+
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
|
|
831
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
832
|
+
]
|
|
833
|
+
_((matches.to_a + non_matches.to_a).length).must_equal sample_data.length
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
it "partitions using formula references in the block" do
|
|
837
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
838
|
+
matches, non_matches = sales.partition{|row| row[:revenue] >= 1500.0}
|
|
839
|
+
_(matches.values(:revenue)).must_equal [1500.0, 1500.0]
|
|
840
|
+
_(non_matches.values(:revenue)).must_equal [1000.0, 1000.0]
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
it "preserves formulae through to both returned Namos" do
|
|
844
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
845
|
+
matches, non_matches = sales.partition{|row| row[:price] < 20.0}
|
|
846
|
+
_(matches.values(:revenue)).must_equal [1000.0, 1500.0]
|
|
847
|
+
_(non_matches.values(:revenue)).must_equal [1000.0, 1500.0]
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
it "returns instances of self's class" do
|
|
851
|
+
subclass = Class.new(Namo)
|
|
852
|
+
matches, non_matches = subclass.new(sample_data).partition{|row| row[:price] < 20.0}
|
|
853
|
+
_(matches.class).must_equal subclass
|
|
854
|
+
_(non_matches.class).must_equal subclass
|
|
855
|
+
end
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
describe "subset methods on an empty Namo" do
|
|
859
|
+
let(:empty) { Namo.new }
|
|
860
|
+
|
|
861
|
+
it "select returns an empty Namo" do
|
|
862
|
+
_(empty.select{|row| true}.to_a).must_equal []
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
it "reject returns an empty Namo" do
|
|
866
|
+
_(empty.reject{|row| true}.to_a).must_equal []
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
it "sort_by returns an empty Namo" do
|
|
870
|
+
_(empty.sort_by{|row| row[:x]}.to_a).must_equal []
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
it "first(n) returns an empty Namo" do
|
|
874
|
+
_(empty.first(2).to_a).must_equal []
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
it "last(n) returns an empty Namo" do
|
|
878
|
+
_(empty.last(2).to_a).must_equal []
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
it "take and drop return empty Namos" do
|
|
882
|
+
_(empty.take(2).to_a).must_equal []
|
|
883
|
+
_(empty.drop(2).to_a).must_equal []
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
it "take_while and drop_while return empty Namos" do
|
|
887
|
+
_(empty.take_while{|row| true}.to_a).must_equal []
|
|
888
|
+
_(empty.drop_while{|row| true}.to_a).must_equal []
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
it "uniq returns an empty Namo" do
|
|
892
|
+
_(empty.uniq.to_a).must_equal []
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
it "partition returns two empty Namos" do
|
|
896
|
+
matches, non_matches = empty.partition{|row| true}
|
|
897
|
+
_(matches.to_a).must_equal []
|
|
898
|
+
_(non_matches.to_a).must_equal []
|
|
899
|
+
end
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
describe "unchanged Enumerable methods" do
|
|
903
|
+
it "map still returns an Array" do
|
|
904
|
+
_(sales.map{|row| row[:product]}).must_be_kind_of Array
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
it "flat_map still returns an Array" do
|
|
908
|
+
_(sales.flat_map{|row| [row[:price]]}).must_be_kind_of Array
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
it "reduce still returns a scalar" do
|
|
912
|
+
_(sales.reduce(0){|sum, row| sum + row[:quantity]}).must_equal 350
|
|
913
|
+
end
|
|
914
|
+
end
|
|
915
|
+
|
|
514
916
|
describe "#+" do
|
|
515
917
|
let(:more_data) do
|
|
516
918
|
[
|