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 +4 -4
- data/CHANGELOG +13 -0
- data/README.md +15 -1
- data/lib/Namo/VERSION.rb +1 -1
- data/lib/namo.rb +59 -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,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
|
|
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
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
|
[
|