namo 0.0.1 → 0.2.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 +29 -3
- data/README.md +155 -21
- data/lib/Namo/VERSION.rb +2 -2
- data/lib/namo.rb +65 -12
- data/namo.gemspec +1 -1
- data/test/namo_test.rb +183 -41
- metadata +17 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6841b3921358ce7eefdbfbdba3a62eafd74857223d9df86038f9cc8087bbb947
|
|
4
|
+
data.tar.gz: bf05398ce78ea49683fbb7adb0b9989c6d6df86864a9bdf2373b156b14d8d5e7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2887c9dee76341b5e4ee75d50a1416475bd8c579d52117f67dcab81c9fe7c8cb1c50c08be7295db74aaf1f2d8727aa678b95d231d4c2e1d82a25b1ca050de61e
|
|
7
|
+
data.tar.gz: d5c5a35ac078d60dcd1baf43e73b5be1aa1bf02ed27a65071fd146223402a812a8b9ff9b88e6be5e4e62b5b47966460df613994d79215405439cde96c1a63278
|
data/CHANGELOG
CHANGED
|
@@ -1,14 +1,40 @@
|
|
|
1
1
|
CHANGELOG
|
|
2
|
-
|
|
2
|
+
_________
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
20260414
|
|
5
|
+
0.2.0: Include Enumerable.
|
|
6
|
+
|
|
7
|
+
1. ~ Namo: include Enumerable
|
|
8
|
+
2. + Namo#each: Yield Row objects wrapping each data row with formulae. Returns an Enumerator when no block is given.
|
|
9
|
+
3. ~ Namo#[]: Refactor to use Enumerable#select and Row#match? instead of select_rows.
|
|
10
|
+
4. - Namo#select_rows: Replaced by Row#match?.
|
|
11
|
+
5. + Namo::Row#match?: Selection logic moved from Namo to Row.
|
|
12
|
+
6. + Namo::Row#to_h: Return the underlying row hash.
|
|
13
|
+
7. ~ namo_test.rb: Add tests for #each and Enumerable methods (map, reduce, min_by, flat_map).
|
|
14
|
+
8. ~ README.md: + Enumerable section
|
|
15
|
+
9. ~ Namo::VERSION: /0.1.0/0.2.0/; /module/class/ to match Namo's definition.
|
|
16
|
+
|
|
17
|
+
20260328
|
|
18
|
+
0.1.0: Add formulae via []=. Formulae are procs which receive a Row object, compose via named references, and carry through selection.
|
|
19
|
+
|
|
20
|
+
1. ~ Namo#[]: Refactor to accept *names for projection alongside **selections. Always returns a Namo instance.
|
|
21
|
+
2. + Namo#[]=: Define named formulae as procs.
|
|
22
|
+
3. + Namo#to_a: Extract data as an array of hashes.
|
|
23
|
+
4. + Namo::Row: Resolve names against row data or formulae.
|
|
24
|
+
5. ~ Namo#initialize: Accept optional formulae: keyword argument.
|
|
25
|
+
6. + attr_accessor :data, :formulae
|
|
26
|
+
7. ~ README.md: + Projection section, + Formulae section, + Extracting data section
|
|
27
|
+
8. ~ namo_test.rb: Add tests for projection, formulae, composition, chained and single-call selection with projection.
|
|
28
|
+
9. ~ Namo::VERSION: /0.0.1/0.1.0/
|
|
29
|
+
|
|
30
|
+
20260315
|
|
5
31
|
0.0.1: Fix README.md and CHANGELOG.
|
|
6
32
|
|
|
7
33
|
1. ~ README.md: Fix Contributing secton.
|
|
8
34
|
2. ~ CHANGELOG: Use the same description in the summary as in the commit message.
|
|
9
35
|
3. ~ Namo::VERSION: /0.0.0/0.0.1/
|
|
10
36
|
|
|
11
|
-
|
|
37
|
+
20260315
|
|
12
38
|
0.0.0: The initial release has instantiation from hashes, automatic inferral of dimension names and co-ordinates, and selection of data by a single value, an array, or a range.
|
|
13
39
|
|
|
14
40
|
1. + Namo
|
data/README.md
CHANGED
|
@@ -18,60 +18,194 @@ gem 'namo'
|
|
|
18
18
|
|
|
19
19
|
## Usage
|
|
20
20
|
|
|
21
|
-
Create a Namo from an array of hashes:
|
|
21
|
+
Create a Namo instance from an array of hashes:
|
|
22
22
|
|
|
23
23
|
```ruby
|
|
24
24
|
require 'namo'
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
{
|
|
28
|
-
{
|
|
29
|
-
{
|
|
30
|
-
{
|
|
26
|
+
sales = Namo.new([
|
|
27
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
28
|
+
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
|
|
29
|
+
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
|
|
30
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
31
31
|
])
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Dimensions and coordinates are inferred:
|
|
35
35
|
|
|
36
36
|
```ruby
|
|
37
|
-
|
|
38
|
-
# => [:
|
|
37
|
+
sales.dimensions
|
|
38
|
+
# => [:product, :quarter, :price, :quantity]
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
# => ['
|
|
40
|
+
sales.coordinates[:product]
|
|
41
|
+
# => ['Widget', 'Gadget']
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
# => ['
|
|
43
|
+
sales.coordinates[:quarter]
|
|
44
|
+
# => ['Q1', 'Q2']
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
### Selection
|
|
48
|
+
|
|
47
49
|
Select by named dimension using keyword arguments:
|
|
48
50
|
|
|
49
51
|
```ruby
|
|
50
52
|
# Single value
|
|
51
|
-
|
|
53
|
+
sales[product: 'Widget']
|
|
54
|
+
# => #<Namo [
|
|
55
|
+
# {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
56
|
+
# {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
|
|
57
|
+
# ]>
|
|
52
58
|
|
|
53
59
|
# Multiple dimensions
|
|
54
|
-
|
|
60
|
+
sales[product: 'Widget', quarter: 'Q1']
|
|
61
|
+
# => #<Namo [
|
|
62
|
+
# {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100}
|
|
63
|
+
# ]>
|
|
55
64
|
|
|
56
65
|
# Range
|
|
57
|
-
|
|
66
|
+
sales[price: 10.0..20.0]
|
|
67
|
+
# => #<Namo [
|
|
68
|
+
# {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
69
|
+
# {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
|
|
70
|
+
# ]>
|
|
58
71
|
|
|
59
72
|
# Array of values
|
|
60
|
-
|
|
73
|
+
sales[quarter: ['Q1']]
|
|
74
|
+
# => #<Namo [
|
|
75
|
+
# {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
76
|
+
# {product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40}
|
|
77
|
+
# ]>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Projection
|
|
81
|
+
|
|
82
|
+
Project to specific dimensions:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
sales[:product, :price]
|
|
86
|
+
# => #<Namo [
|
|
87
|
+
# {product: 'Widget', price: 10.0},
|
|
88
|
+
# {product: 'Widget', price: 10.0},
|
|
89
|
+
# {product: 'Gadget', price: 25.0},
|
|
90
|
+
# {product: 'Gadget', price: 25.0}
|
|
91
|
+
# ]>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Selection and projection can be chained:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
sales[product: 'Widget'][:quarter, :price]
|
|
98
|
+
# => #<Namo [
|
|
99
|
+
# {quarter: 'Q1', price: 10.0},
|
|
100
|
+
# {quarter: 'Q2', price: 10.0}
|
|
101
|
+
# ]>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Or combined in a single call (names before selectors):
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
sales[:quarter, :price, product: 'Widget']
|
|
108
|
+
# => #<Namo [
|
|
109
|
+
# {quarter: 'Q1', price: 10.0},
|
|
110
|
+
# {quarter: 'Q2', price: 10.0}
|
|
111
|
+
# ]>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Selection and projection always return a new Namo instance, so everything chains.
|
|
115
|
+
|
|
116
|
+
### Formulae
|
|
117
|
+
|
|
118
|
+
Define computed dimensions using `[]=`:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
sales[:revenue] = proc{|row| row[:price] * row[:quantity]}
|
|
122
|
+
|
|
123
|
+
sales[:product, :quarter, :revenue]
|
|
124
|
+
# => #<Namo [
|
|
125
|
+
# {product: 'Widget', quarter: 'Q1', revenue: 1000.0},
|
|
126
|
+
# {product: 'Widget', quarter: 'Q2', revenue: 1500.0},
|
|
127
|
+
# {product: 'Gadget', quarter: 'Q1', revenue: 1000.0},
|
|
128
|
+
# {product: 'Gadget', quarter: 'Q2', revenue: 1500.0}
|
|
129
|
+
# ]>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Formulae compose:
|
|
61
133
|
|
|
62
|
-
|
|
63
|
-
|
|
134
|
+
```ruby
|
|
135
|
+
sales[:cost] = proc{|row| row[:quantity] * 4.0}
|
|
136
|
+
sales[:profit] = proc{|row| row[:revenue] - row[:cost]}
|
|
137
|
+
|
|
138
|
+
sales[:product, :quarter, :profit]
|
|
139
|
+
# => #<Namo [
|
|
140
|
+
# {product: 'Widget', quarter: 'Q1', profit: 600.0},
|
|
141
|
+
# {product: 'Widget', quarter: 'Q2', profit: 900.0},
|
|
142
|
+
# {product: 'Gadget', quarter: 'Q1', profit: 840.0},
|
|
143
|
+
# {product: 'Gadget', quarter: 'Q2', profit: 1260.0}
|
|
144
|
+
# ]>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Formulae work with selection and projection:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
sales[product: 'Widget'][:revenue, :quarter]
|
|
151
|
+
# => #<Namo [
|
|
152
|
+
# {revenue: 1000.0, quarter: 'Q1'},
|
|
153
|
+
# {revenue: 1500.0, quarter: 'Q2'}
|
|
154
|
+
# ]>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Formulae carry through selection — a filtered Namo instance remembers its formulae.
|
|
158
|
+
|
|
159
|
+
### Enumerable
|
|
160
|
+
|
|
161
|
+
Namo includes `Enumerable`, so `each`, `reduce`, `map`, `select`, `min_by`, and all the rest work out of the box. Rows are yielded as `Row` objects, so formulae are accessible during enumeration:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
sales.reduce(0){|sum, row| sum + row[:quantity]}
|
|
165
|
+
# => 350
|
|
166
|
+
|
|
167
|
+
sales[product: 'Widget'].reduce(0){|sum, row| sum + row[:quantity]}
|
|
168
|
+
# => 250
|
|
169
|
+
|
|
170
|
+
sales[:revenue] = proc{|row| row[:price] * row[:quantity]}
|
|
171
|
+
|
|
172
|
+
sales.reduce(0){|sum, row| sum + row[:revenue]}
|
|
173
|
+
# => 5000.0
|
|
174
|
+
|
|
175
|
+
sales[product: 'Widget'].reduce(0){|sum, row| sum + row[:revenue]}
|
|
176
|
+
# => 2500.0
|
|
177
|
+
|
|
178
|
+
sales.map{|row| row[:product]}
|
|
179
|
+
# => ['Widget', 'Widget', 'Gadget', 'Gadget']
|
|
180
|
+
|
|
181
|
+
sales.min_by{|row| row[:price]}[:product]
|
|
182
|
+
# => 'Widget'
|
|
183
|
+
|
|
184
|
+
sales.flat_map{|row| [row[:price]]}
|
|
185
|
+
# => [10.0, 10.0, 25.0, 25.0]
|
|
64
186
|
```
|
|
65
187
|
|
|
66
|
-
|
|
188
|
+
### Extracting data
|
|
189
|
+
|
|
190
|
+
`to_a` returns an array of hashes:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
sales[:product, :quarter, :revenue].to_a
|
|
194
|
+
# => [
|
|
195
|
+
# {product: 'Widget', quarter: 'Q1', revenue: 1000.0},
|
|
196
|
+
# {product: 'Widget', quarter: 'Q2', revenue: 1500.0},
|
|
197
|
+
# {product: 'Gadget', quarter: 'Q1', revenue: 1000.0},
|
|
198
|
+
# {product: 'Gadget', quarter: 'Q2', revenue: 1500.0}
|
|
199
|
+
# ]
|
|
200
|
+
```
|
|
67
201
|
|
|
68
202
|
## Why?
|
|
69
203
|
|
|
70
|
-
Every other multi-dimensional array library requires you to pre-shape your data before you can work with it. Namo takes it in the form it already comes in.
|
|
204
|
+
Every other multi-dimensional array library requires you to pre-shape your data before you can work with it. Namo takes it in the form it likely already comes in.
|
|
71
205
|
|
|
72
206
|
## Name
|
|
73
207
|
|
|
74
|
-
Namo:
|
|
208
|
+
Namo: nam(ed) (dimensi)o(ns). A companion to Numo (numeric arrays for Ruby). And in Aussie culture 'o' gets added to the end of names.
|
|
75
209
|
|
|
76
210
|
## Contributing
|
|
77
211
|
|
data/lib/Namo/VERSION.rb
CHANGED
data/lib/namo.rb
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
# Namo
|
|
3
3
|
|
|
4
4
|
class Namo
|
|
5
|
+
include Enumerable
|
|
6
|
+
|
|
7
|
+
class Row
|
|
8
|
+
def [](name)
|
|
9
|
+
if @formulae.key?(name)
|
|
10
|
+
@formulae[name].call(self)
|
|
11
|
+
else
|
|
12
|
+
@row[name]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def match?(selections)
|
|
17
|
+
selections.all? do |dimension, coordinate|
|
|
18
|
+
case coordinate
|
|
19
|
+
when Array, Range
|
|
20
|
+
coordinate.include?(self[dimension])
|
|
21
|
+
else
|
|
22
|
+
self[dimension] == coordinate
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_h
|
|
28
|
+
@row
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def initialize(row, formulae)
|
|
34
|
+
@row = row
|
|
35
|
+
@formulae = formulae
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
attr_accessor :data
|
|
40
|
+
attr_accessor :formulae
|
|
41
|
+
|
|
5
42
|
def dimensions
|
|
6
43
|
@dimensions ||= @data.first.keys
|
|
7
44
|
end
|
|
@@ -14,25 +51,41 @@ class Namo
|
|
|
14
51
|
)
|
|
15
52
|
end
|
|
16
53
|
|
|
17
|
-
def [](**selections)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
coordinate.include?(row[dimension])
|
|
24
|
-
else
|
|
25
|
-
coordinate == row[dimension]
|
|
26
|
-
end
|
|
54
|
+
def [](*names, **selections)
|
|
55
|
+
rows = selections.any? ? select{|row| row.match?(selections)} : entries
|
|
56
|
+
data = (
|
|
57
|
+
if names.any?
|
|
58
|
+
rows.map do |row|
|
|
59
|
+
names.each_with_object({}){|name, hash| hash[name] = row[name]}
|
|
27
60
|
end
|
|
61
|
+
else
|
|
62
|
+
rows.map(&:to_h)
|
|
28
63
|
end
|
|
29
64
|
)
|
|
30
|
-
self.class.new(
|
|
65
|
+
self.class.new(data, formulae: @formulae.dup)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def []=(name, proc)
|
|
69
|
+
@formulae[name] = proc
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def each(&block)
|
|
73
|
+
return enum_for(:each) unless block_given?
|
|
74
|
+
@data.each{|row_data| block.call(Row.new(row_data, @formulae))}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def to_a
|
|
78
|
+
@data.map do |row|
|
|
79
|
+
row.keys.each_with_object({}) do |key, hash|
|
|
80
|
+
hash[key] = row[key]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
31
83
|
end
|
|
32
84
|
|
|
33
85
|
private
|
|
34
86
|
|
|
35
|
-
def initialize(data)
|
|
87
|
+
def initialize(data = nil, formulae: {})
|
|
36
88
|
@data = data
|
|
89
|
+
@formulae = formulae
|
|
37
90
|
end
|
|
38
91
|
end
|
data/namo.gemspec
CHANGED
|
@@ -2,7 +2,6 @@ require_relative './lib/Namo/VERSION'
|
|
|
2
2
|
|
|
3
3
|
Gem::Specification.new do |spec|
|
|
4
4
|
spec.name = 'namo'
|
|
5
|
-
spec.date = '2026-03-15'
|
|
6
5
|
spec.version = Namo::VERSION
|
|
7
6
|
|
|
8
7
|
spec.summary = "Named dimensional data for Ruby."
|
|
@@ -28,5 +27,6 @@ Gem::Specification.new do |spec|
|
|
|
28
27
|
].flatten
|
|
29
28
|
|
|
30
29
|
spec.add_development_dependency 'minitest'
|
|
30
|
+
spec.add_development_dependency 'minitest-spec-context'
|
|
31
31
|
spec.add_development_dependency 'rake'
|
|
32
32
|
end
|
data/test/namo_test.rb
CHANGED
|
@@ -1,70 +1,212 @@
|
|
|
1
|
-
# namo_test.rb
|
|
2
|
-
|
|
3
|
-
# 20260314
|
|
4
|
-
# 0.0.0
|
|
5
|
-
|
|
6
1
|
require 'minitest/autorun'
|
|
2
|
+
require 'minitest-spec-context'
|
|
3
|
+
|
|
7
4
|
require_relative '../lib/namo'
|
|
8
5
|
|
|
9
6
|
describe Namo do
|
|
10
7
|
let(:sample_data) do
|
|
11
8
|
[
|
|
12
|
-
{
|
|
13
|
-
{
|
|
14
|
-
{
|
|
15
|
-
{
|
|
9
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
10
|
+
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
|
|
11
|
+
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
|
|
12
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
16
13
|
]
|
|
17
14
|
end
|
|
18
15
|
|
|
19
|
-
let(:
|
|
16
|
+
let(:sales) do
|
|
17
|
+
Namo.new(sample_data)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe "#dimensions" do
|
|
21
|
+
it "infers dimensions from hash keys" do
|
|
22
|
+
_(sales.dimensions).must_equal [:product, :quarter, :price, :quantity]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe "#coordinates" do
|
|
27
|
+
it "extracts unique values for each dimension" do
|
|
28
|
+
_(sales.coordinates).must_equal ({
|
|
29
|
+
product: ['Widget', 'Gadget'],
|
|
30
|
+
quarter: ['Q1', 'Q2'],
|
|
31
|
+
price: [10.0, 25.0],
|
|
32
|
+
quantity: [100, 150, 40, 60]
|
|
33
|
+
})
|
|
34
|
+
_(sales.coordinates[:product]).must_equal ['Widget', 'Gadget']
|
|
35
|
+
_(sales.coordinates[:quarter]).must_equal ['Q1', 'Q2']
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe "#[]" do
|
|
40
|
+
context "selection" do
|
|
41
|
+
it "selects by single coordinate" do
|
|
42
|
+
result = sales[product: 'Widget']
|
|
43
|
+
_(result.coordinates[:product]).must_equal ['Widget']
|
|
44
|
+
_(result.to_a.count).must_equal 2
|
|
45
|
+
_(result.to_a.map{|row| row[:product]}).must_equal ['Widget', 'Widget']
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "selects by array of coordinates" do
|
|
49
|
+
result = sales[quarter: ['Q1']]
|
|
50
|
+
_(result.coordinates[:quarter]).must_equal ['Q1']
|
|
51
|
+
_(result.to_a.count).must_equal 2
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "selects by multiple dimensions" do
|
|
55
|
+
result = sales[product: 'Widget', quarter: 'Q1']
|
|
56
|
+
_(result.to_a.count).must_equal 1
|
|
57
|
+
_(result.to_a).must_equal [{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100}]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "returns a Namo instance" do
|
|
61
|
+
result = sales[product: 'Widget']
|
|
62
|
+
_(result.coordinates[:product]).must_equal ['Widget']
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
context "projection" do
|
|
67
|
+
it "projects to named dimensions" do
|
|
68
|
+
result = sales[:product, :price]
|
|
69
|
+
_(result.dimensions).must_equal [:product, :price]
|
|
70
|
+
_(result.coordinates[:product]).must_equal ['Widget', 'Gadget']
|
|
71
|
+
_(result.to_a.count).must_equal 4
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context "selection and projection" do
|
|
76
|
+
it "can use them together" do
|
|
77
|
+
result = sales[:price, product: 'Widget']
|
|
78
|
+
_(result.to_a.count).must_equal 2
|
|
79
|
+
_(result.to_a).must_equal [{price: 10.0}, {price: 10.0}]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "can chain them" do
|
|
83
|
+
result = sales[product: 'Widget'][:price]
|
|
84
|
+
_(result.to_a.count).must_equal 2
|
|
85
|
+
_(result.to_a).must_equal [{price: 10.0}, {price: 10.0}]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe "#[]= formulae" do
|
|
91
|
+
it "defines a formula" do
|
|
92
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
93
|
+
_(sales[:product, :quarter, :revenue].to_a).must_equal [
|
|
94
|
+
{product: "Widget", quarter: "Q1", revenue: 1000.0},
|
|
95
|
+
{product: "Widget", quarter: "Q2", revenue: 1500.0},
|
|
96
|
+
{product: "Gadget", quarter: "Q1", revenue: 1000.0},
|
|
97
|
+
{product: "Gadget", quarter: "Q2", revenue: 1500.0}
|
|
98
|
+
]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "composes formulae" do
|
|
102
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
103
|
+
sales[:cost] = proc{|r| r[:quantity] * 4.0}
|
|
104
|
+
sales[:profit] = proc{|r| r[:revenue] - r[:cost]}
|
|
105
|
+
_(sales[:product, :quarter, :profit].to_a).must_equal [
|
|
106
|
+
{product: "Widget", quarter: "Q1", profit: 600.0},
|
|
107
|
+
{product: "Widget", quarter: "Q2", profit: 900.0},
|
|
108
|
+
{product: "Gadget", quarter: "Q1", profit: 840.0},
|
|
109
|
+
{product: "Gadget", quarter: "Q2", profit: 1260.0}
|
|
110
|
+
]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "works with chained selection and projection" do
|
|
114
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
115
|
+
result = sales[product: 'Widget'][:product, :quarter, :revenue]
|
|
116
|
+
_(result.to_a).must_equal [
|
|
117
|
+
{product: "Widget", quarter: "Q1", revenue: 1000.0},
|
|
118
|
+
{product: "Widget", quarter: "Q2", revenue: 1500.0}
|
|
119
|
+
]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "works with single-call selection and projection" do
|
|
123
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
124
|
+
result = sales[:product, :revenue, product: 'Widget']
|
|
125
|
+
_(result.to_a).must_equal [
|
|
126
|
+
{product: "Widget", revenue: 1000.0},
|
|
127
|
+
{product: "Widget", revenue: 1500.0}
|
|
128
|
+
]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "carries formulae through selection" do
|
|
132
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
133
|
+
widgets = sales[product: 'Widget']
|
|
134
|
+
_(widgets[:revenue].to_a).must_equal [{revenue: 1000.0}, {revenue: 1500.0}]
|
|
135
|
+
end
|
|
20
136
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
137
|
+
it "projects formula with context dimensions" do
|
|
138
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
139
|
+
result = sales[:product, :quarter, :revenue]
|
|
140
|
+
_(result.to_a).must_equal [
|
|
141
|
+
{product: 'Widget', quarter: 'Q1', revenue: 1000.0},
|
|
142
|
+
{product: 'Widget', quarter: 'Q2', revenue: 1500.0},
|
|
143
|
+
{product: 'Gadget', quarter: 'Q1', revenue: 1000.0},
|
|
144
|
+
{product: 'Gadget', quarter: 'Q2', revenue: 1500.0}
|
|
145
|
+
]
|
|
24
146
|
end
|
|
25
147
|
end
|
|
26
148
|
|
|
27
|
-
describe
|
|
28
|
-
it
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
_(
|
|
149
|
+
describe "#each" do
|
|
150
|
+
it "yields Row objects" do
|
|
151
|
+
rows = []
|
|
152
|
+
sales.each{|row| rows << row}
|
|
153
|
+
_(rows.first).must_be_kind_of Namo::Row
|
|
154
|
+
_(rows.length).must_equal 4
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it "yields rows with access to data" do
|
|
158
|
+
products = sales.map{|row| row[:product]}
|
|
159
|
+
_(products).must_equal ['Widget', 'Widget', 'Gadget', 'Gadget']
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it "yields rows with access to formulae" do
|
|
163
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
164
|
+
revenues = sales.map{|row| row[:revenue]}
|
|
165
|
+
_(revenues).must_equal [1000.0, 1500.0, 1000.0, 1500.0]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "returns an enumerator without a block" do
|
|
169
|
+
_(sales.each).must_be_kind_of Enumerator
|
|
32
170
|
end
|
|
33
171
|
end
|
|
34
172
|
|
|
35
|
-
describe
|
|
36
|
-
it
|
|
37
|
-
|
|
38
|
-
_(
|
|
39
|
-
_(result.coordinates[:symbol]).must_equal ['BHP']
|
|
40
|
-
_(result.coordinates[:close]).must_equal [42.5, 43.1]
|
|
173
|
+
describe "Enumerable" do
|
|
174
|
+
it "supports reduce" do
|
|
175
|
+
total_quantity = sales.reduce(0){|sum, row| sum + row[:quantity]}
|
|
176
|
+
_(total_quantity).must_equal 350
|
|
41
177
|
end
|
|
42
178
|
|
|
43
|
-
it
|
|
44
|
-
|
|
45
|
-
_(
|
|
46
|
-
_(result.coordinates[:symbol]).must_equal ['BHP']
|
|
47
|
-
_(result.coordinates[:close]).must_equal [42.5]
|
|
179
|
+
it "supports reduce with selection" do
|
|
180
|
+
widget_quantity = sales[product: 'Widget'].reduce(0){|sum, row| sum + row[:quantity]}
|
|
181
|
+
_(widget_quantity).must_equal 250
|
|
48
182
|
end
|
|
49
183
|
|
|
50
|
-
it
|
|
51
|
-
|
|
52
|
-
|
|
184
|
+
it "supports reduce with formulae" do
|
|
185
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
186
|
+
total_revenue = sales.reduce(0){|sum, row| sum + row[:revenue]}
|
|
187
|
+
_(total_revenue).must_equal 5000.0
|
|
53
188
|
end
|
|
54
189
|
|
|
55
|
-
it
|
|
56
|
-
|
|
57
|
-
|
|
190
|
+
it "supports reduce with selection and formulae" do
|
|
191
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
192
|
+
widget_revenue = sales[product: 'Widget'].reduce(0){|sum, row| sum + row[:revenue]}
|
|
193
|
+
_(widget_revenue).must_equal 2500.0
|
|
58
194
|
end
|
|
59
195
|
|
|
60
|
-
it
|
|
61
|
-
|
|
62
|
-
_(
|
|
196
|
+
it "supports min_by" do
|
|
197
|
+
cheapest = sales.min_by{|row| row[:price]}
|
|
198
|
+
_(cheapest[:product]).must_equal 'Widget'
|
|
63
199
|
end
|
|
64
200
|
|
|
65
|
-
it
|
|
66
|
-
|
|
67
|
-
_(
|
|
201
|
+
it "supports flat_map" do
|
|
202
|
+
prices = sales.flat_map{|row| [row[:price]]}
|
|
203
|
+
_(prices).must_equal [10.0, 10.0, 25.0, 25.0]
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
describe "#to_a" do
|
|
208
|
+
it "returns the data as an array of hashes" do
|
|
209
|
+
_(sales.to_a).must_equal sample_data
|
|
68
210
|
end
|
|
69
211
|
end
|
|
70
212
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: namo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- thoran
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: minitest
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: minitest-spec-context
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
26
40
|
- !ruby/object:Gem::Dependency
|
|
27
41
|
name: rake
|
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -72,7 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
72
86
|
- !ruby/object:Gem::Version
|
|
73
87
|
version: '0'
|
|
74
88
|
requirements: []
|
|
75
|
-
rubygems_version: 4.0.
|
|
89
|
+
rubygems_version: 4.0.10
|
|
76
90
|
specification_version: 4
|
|
77
91
|
summary: Named dimensional data for Ruby.
|
|
78
92
|
test_files: []
|