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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2959666eaa2d603b1877d42863ddadae9e021aceb753a60009579a2dbe2d7aa4
4
- data.tar.gz: ac865863705a2ca58c815eed073227a39ebb479358fea0590e4f76a5f6b0813c
3
+ metadata.gz: 40c2256efac2663b7593bb1f7e4c3ca08fa07e6e225c75fcef3d6d8658f3794b
4
+ data.tar.gz: abd7bd9076d7019c245d942425232dcf0966edb076cf6d50fdedf7b74efea90f
5
5
  SHA512:
6
- metadata.gz: '09cc42eba9f306b4316a70140bb9642dc025c39d4bc3a502b21bee28058709b8b55609093d9979e27e2799d1bdad1efd60d829a71931502abc8e7cde71f1a1c7'
7
- data.tar.gz: 3952f6470d2f5dc8aeb733034d8c7cbf6214b909453f3b26e7ac29480388b79d345efba1ff925ce4617c64c7a9c2f1e578f2e8938f2e51def0d378109f65d276
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, so analytical pipelines close; and the formula mechanism is type-agnostic — strings, dates, booleans, and arbitrary Ruby objects work as readily as numbers.
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
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.9.2'
5
+ VERSION = '0.11.0'
6
6
  end
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)
@@ -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
  [
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.9.2
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran