namo 0.10.0 → 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: ff324711fe932145ecc01cedadee6453f80fe6610d162783a7dea053d2f35fc0
4
- data.tar.gz: ec993e96d3ac5f1053eaa0f6c933347914209ed84423e78e3c4e5a4843c22915
3
+ metadata.gz: 40c2256efac2663b7593bb1f7e4c3ca08fa07e6e225c75fcef3d6d8658f3794b
4
+ data.tar.gz: abd7bd9076d7019c245d942425232dcf0966edb076cf6d50fdedf7b74efea90f
5
5
  SHA512:
6
- metadata.gz: 863aa8aeb8432d3fed7da33218ce14790a24a5e4a4b1c70a09b44aa495ef7ebe1bba36954622a1629990ff61cd892904725ee2293613e63c51ca48e0e6d05c18
7
- data.tar.gz: 343949804d61f4dd93a636a961d6c097f9f1c20f7010044d67fbab34aaba75f1b3c9b33cd42f2f74f1ce189fd9324e6a18398b7b595fda11ca2fc44cfbcfc1c0
6
+ metadata.gz: 2b5045c4160d8812fa3f04f10dc027bf8c6cfaf937405cec0570a31af52babdc1f2dc6e25db249a9b9dea81afc9806f617203ae71dded576974fe9512bd4ec00
7
+ data.tar.gz: a553ef0ae27d67bf28d8d91ba9e2e71f217f8ee23ff3d177ad207dd520f30971730774a51660830d98d79a6189cee16f1227dbd168282f3a27c4316912a6f29f
data/CHANGELOG CHANGED
@@ -1,6 +1,19 @@
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
+
4
17
  20260528
5
18
  0.10.0: + Row value semantics: ==, eql?, hash
6
19
 
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
 
@@ -676,6 +676,20 @@ sales.flat_map{|row| [row[:price]]}
676
676
  # => [10.0, 10.0, 25.0, 25.0]
677
677
  ```
678
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
+
679
693
  ### Extracting data
680
694
 
681
695
  `to_a` returns an array of hashes — the row-oriented form:
data/lib/Namo/VERSION.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.10.0'
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)
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.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran