namo 0.10.0 → 0.11.1

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: ad4994109308fd955b2f508afe1f07251bc4287406ed7f27eb8a484a61dfabcf
4
+ data.tar.gz: 04be0a1d0e8f97b86932a3e5e8953bfe3b0e51fe16bcb80b5dec9a5a73aecd36
5
5
  SHA512:
6
- metadata.gz: 863aa8aeb8432d3fed7da33218ce14790a24a5e4a4b1c70a09b44aa495ef7ebe1bba36954622a1629990ff61cd892904725ee2293613e63c51ca48e0e6d05c18
7
- data.tar.gz: 343949804d61f4dd93a636a961d6c097f9f1c20f7010044d67fbab34aaba75f1b3c9b33cd42f2f74f1ce189fd9324e6a18398b7b595fda11ca2fc44cfbcfc1c0
6
+ metadata.gz: 507a77ecfc3d23fe1ad4ef14783b2e4f86ff44b58431cb93b8e4aa41c12bf4778f03c7a3bdca7f8958620b3d2e3bba3292cdfbcd5636c8286661aed100398b30
7
+ data.tar.gz: fa6acfcf416108a871e0697ae998cef8701263446abee9089c62e1cfb24c102d22f002e643f89890a7c40ad2ccedca3dd7d58ebf04125fb4a87d17f58a908abd
data/CHANGELOG CHANGED
@@ -1,6 +1,28 @@
1
1
  CHANGELOG
2
2
  _________
3
3
 
4
+ 20260601
5
+ 0.11.1: Extract the subset Enumerable methods into a Namo::Enumerable module.
6
+
7
+ 1. + lib/Namo/Enumerable.rb: New module Namo::Enumerable holding each and the subset-returning Enumerable methods (select plus its filter/find_all aliases, reject, sort_by, first, last, take, drop, take_while, drop_while, uniq, partition), moved verbatim from Namo. The module does `include ::Enumerable` — the leading :: is required, because under the nested `class Namo; module Enumerable` scope a bare `Enumerable` resolves to Namo::Enumerable itself and raises a cyclic-include error.
8
+ 2. ~ lib/namo.rb: Remove the inline each and the eleven subset methods; remove `include Enumerable`; + `require_relative './Namo/Enumerable'` and `include Namo::Enumerable` (which transitively brings stdlib Enumerable into the ancestor chain below the module, so the overrides win and map/reduce/etc. still fall through). Pure reorganisation — no behaviour change.
9
+ 3. ~ test/namo_test.rb: + Namo::Enumerable module tests — it is a Module, Namo includes it and stdlib Enumerable, and it precedes ::Enumerable in Namo.ancestors so the overrides resolve first. The existing Enumerable subset tests are unchanged and remain green, evidencing the move is behaviour-preserving.
10
+ 4. ~ ROADMAP.md: + note in the 0.11.0 section that, as of 0.11.1, these methods live in the Namo::Enumerable module; bump Date to 20260601.
11
+ 5. ~ Namo::VERSION: /0.11.0/0.11.1/
12
+
13
+ 20260531
14
+ 0.11.0: ~ Subset Enumerable methods (select, reject, sort_by, first, last, take, drop, take_while, drop_while, uniq, partition) return Namos
15
+
16
+ 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.
17
+ 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.
18
+ 3. + Namo#take, Namo#drop: Leading subset and its complement, wrapping @data.take(n) / @data.drop(n).
19
+ 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.
20
+ 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.
21
+ 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.
22
+ 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".
23
+ 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.
24
+ 9. ~ Namo::VERSION: /0.10.0/0.11.0/
25
+
4
26
  20260528
5
27
  0.10.0: + Row value semantics: ==, eql?, hash
6
28
 
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:
@@ -0,0 +1,72 @@
1
+ # Namo/Enumerable.rb
2
+ # Namo::Enumerable
3
+
4
+ class Namo
5
+ module Enumerable
6
+ include ::Enumerable
7
+
8
+ def each(&block)
9
+ return enum_for(:each) unless block_given?
10
+ @data.each{|row_data| block.call(Row.new(row_data, @formulae))}
11
+ end
12
+
13
+ def select(&block)
14
+ self.class.new(@data.select{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
15
+ end
16
+ alias_method :filter, :select
17
+ alias_method :find_all, :select
18
+
19
+ def reject(&block)
20
+ self.class.new(@data.reject{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
21
+ end
22
+
23
+ def sort_by(&block)
24
+ self.class.new(@data.sort_by{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
25
+ end
26
+
27
+ def first(n = nil)
28
+ if n
29
+ self.class.new(@data.first(n), formulae: @formulae.dup)
30
+ else
31
+ @data.first ? Row.new(@data.first, @formulae) : nil
32
+ end
33
+ end
34
+
35
+ def last(n = nil)
36
+ if n
37
+ self.class.new(@data.last(n), formulae: @formulae.dup)
38
+ else
39
+ @data.last ? Row.new(@data.last, @formulae) : nil
40
+ end
41
+ end
42
+
43
+ def take(n)
44
+ self.class.new(@data.take(n), formulae: @formulae.dup)
45
+ end
46
+
47
+ def drop(n)
48
+ self.class.new(@data.drop(n), formulae: @formulae.dup)
49
+ end
50
+
51
+ def take_while(&block)
52
+ self.class.new(@data.take_while{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
53
+ end
54
+
55
+ def drop_while(&block)
56
+ self.class.new(@data.drop_while{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
57
+ end
58
+
59
+ def uniq(&block)
60
+ rows = block ? @data.uniq{|row| block.call(Row.new(row, @formulae))} : @data.uniq
61
+ self.class.new(rows, formulae: @formulae.dup)
62
+ end
63
+
64
+ def partition(&block)
65
+ matches, non_matches = @data.partition{|row| block.call(Row.new(row, @formulae))}
66
+ [
67
+ self.class.new(matches, formulae: @formulae.dup),
68
+ self.class.new(non_matches, formulae: @formulae.dup),
69
+ ]
70
+ end
71
+ end
72
+ end
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.1'
6
6
  end
data/lib/namo.rb CHANGED
@@ -3,11 +3,12 @@
3
3
 
4
4
  require_relative './Namo/NegatedDimension'
5
5
  require_relative './Namo/Row'
6
+ require_relative './Namo/Enumerable'
6
7
  require_relative './Namo/VERSION'
7
8
  require_relative './Symbol'
8
9
 
9
10
  class Namo
10
- include Enumerable
11
+ include Namo::Enumerable
11
12
 
12
13
  attr_accessor :data
13
14
  attr_accessor :formulae
@@ -76,11 +77,6 @@ class Namo
76
77
  @formulae[name] = proc
77
78
  end
78
79
 
79
- def each(&block)
80
- return enum_for(:each) unless block_given?
81
- @data.each{|row_data| block.call(Row.new(row_data, @formulae))}
82
- end
83
-
84
80
  def +(other)
85
81
  raise_unless_namo(other)
86
82
  raise_unless_matching_data_dimensions(other)
data/test/namo_test.rb CHANGED
@@ -511,6 +511,424 @@ 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
+
916
+ describe "Namo::Enumerable module" do
917
+ it "is a Module supplying the subset methods" do
918
+ _(Namo::Enumerable).must_be_kind_of Module
919
+ end
920
+
921
+ it "is included in Namo, transitively including stdlib Enumerable" do
922
+ _(Namo.include?(Namo::Enumerable)).must_equal true
923
+ _(Namo.include?(Enumerable)).must_equal true
924
+ end
925
+
926
+ it "sits above stdlib Enumerable so its overrides win" do
927
+ ancestors = Namo.ancestors
928
+ _(ancestors.index(Namo::Enumerable) < ancestors.index(::Enumerable)).must_equal true
929
+ end
930
+ end
931
+
514
932
  describe "#+" do
515
933
  let(:more_data) do
516
934
  [
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran
@@ -64,6 +64,7 @@ files:
64
64
  - LICENSE
65
65
  - README.md
66
66
  - Rakefile
67
+ - lib/Namo/Enumerable.rb
67
68
  - lib/Namo/NegatedDimension.rb
68
69
  - lib/Namo/Row.rb
69
70
  - lib/Namo/VERSION.rb