namo 0.2.0 → 0.4.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 +20 -0
- data/README.md +93 -1
- data/lib/Namo/NegatedDimension.rb +14 -0
- data/lib/Namo/Row.rb +36 -0
- data/lib/Namo/VERSION.rb +1 -1
- data/lib/Symbol.rb +8 -0
- data/lib/namo.rb +32 -36
- data/namo.gemspec +14 -5
- data/test/Namo/NegatedDimension_test.rb +13 -0
- data/test/Namo/Row_test.rb +69 -0
- data/test/{namo_test.rb → Namo_test.rb} +133 -0
- data/test/Symbol_test.rb +16 -0
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0dbfe699c8bbb334e0f72bbfd1859b0eee910be2c72eaab988da633b58b438e6
|
|
4
|
+
data.tar.gz: 40238ef4593d7820bddfb6cb708823fa9c269a51def947e89cdb43e4b69cbae0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9e6dfd0bcf4b8d370373a43e507a4d208e4ccb4478e044eb138bf5f2bfa61612cbb4f10735be691c852d2f8bf672eca70720cc6a5c35cd5fb02fae224388e010
|
|
7
|
+
data.tar.gz: 2b147c6e7faf7b9f92e8e2457fcd657734a35946c326857e3b7c09177ceaa7f3ce4f0892bdcbb68ec251d673e1b298c1ed06416fe43918656734a58bd0c639ea
|
data/CHANGELOG
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
CHANGELOG
|
|
2
2
|
_________
|
|
3
3
|
|
|
4
|
+
20260415
|
|
5
|
+
0.4.0: + concatenation (+) and row removal (-)
|
|
6
|
+
|
|
7
|
+
1. + Namo#+: Concatenate two Namo instances with matching dimensions. Appends rows from the second to the first. Raises ArgumentError when dimensions differ.
|
|
8
|
+
2. + Namo#-: Remove rows from the first Namo that appear exactly in the second. Whole-row set difference. Raises ArgumentError when dimensions differ.
|
|
9
|
+
3. ~ Namo_test.rb: Add tests for #+ and #-.
|
|
10
|
+
4. ~ README.md: + Concatenation section, + Row removal section.
|
|
11
|
+
5. ~ Namo::VERSION: /0.3.0/0.4.0/
|
|
12
|
+
|
|
13
|
+
20260415
|
|
14
|
+
0.3.0: + contraction
|
|
15
|
+
|
|
16
|
+
1. + Namo::NegatedDimension: Wrapper for negated dimension names used in contraction.
|
|
17
|
+
2. + Symbol#-@: Produce a NegatedDimension, enabling -:date syntax.
|
|
18
|
+
3. ~ Namo#[]: Extend to recognise NegatedDimension for contraction (remove named dimensions, keep everything else). Raise ArgumentError when mixing projection and contraction.
|
|
19
|
+
4. > Namo::Row: Extract to Namo/Row.rb.
|
|
20
|
+
5. ~ namo_test.rb: Add tests for contraction, mixed-mode error, and contraction with selection and formulae. Split tests into one file per class: Row_test.rb, NegatedDimension_test.rb, Symbol_test.rb.
|
|
21
|
+
6. ~ README.md: + Contraction section
|
|
22
|
+
7. ~ Namo::VERSION: /0.2.0/0.3.0/
|
|
23
|
+
|
|
4
24
|
20260414
|
|
5
25
|
0.2.0: Include Enumerable.
|
|
6
26
|
|
data/README.md
CHANGED
|
@@ -111,7 +111,99 @@ sales[:quarter, :price, product: 'Widget']
|
|
|
111
111
|
# ]>
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
### Contraction
|
|
115
|
+
|
|
116
|
+
Contraction is the complement of projection. Projection says "keep these dimensions"; contraction says "remove these dimensions, keep everything else":
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
sales[-:price, -:quantity]
|
|
120
|
+
# => #<Namo [
|
|
121
|
+
# {product: 'Widget', quarter: 'Q1'},
|
|
122
|
+
# {product: 'Widget', quarter: 'Q2'},
|
|
123
|
+
# {product: 'Gadget', quarter: 'Q1'},
|
|
124
|
+
# {product: 'Gadget', quarter: 'Q2'}
|
|
125
|
+
# ]>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The `-:price` syntax uses unary minus on Symbol to produce a negated dimension. Mixing projection and contraction in the same call is an error — the two modes are mutually exclusive:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
sales[:product, -:price] # => ArgumentError
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Selection and contraction can be chained:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
sales[product: 'Widget'][-:price, -:quantity]
|
|
138
|
+
# => #<Namo [
|
|
139
|
+
# {product: 'Widget', quarter: 'Q1'},
|
|
140
|
+
# {product: 'Widget', quarter: 'Q2'}
|
|
141
|
+
# ]>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Or combined in a single call (names before selectors):
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
sales[-:price, -:quantity, product: 'Widget']
|
|
148
|
+
# => #<Namo [
|
|
149
|
+
# {product: 'Widget', quarter: 'Q1'},
|
|
150
|
+
# {product: 'Widget', quarter: 'Q2'}
|
|
151
|
+
# ]>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Selection, projection, and contraction always return a new Namo instance, so everything chains.
|
|
155
|
+
|
|
156
|
+
### Concatenation
|
|
157
|
+
|
|
158
|
+
`+` combines two Namo objects that share the same dimensions by appending the rows of the second to the first:
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
q1_sales = Namo.new([
|
|
162
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
163
|
+
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40}
|
|
164
|
+
])
|
|
165
|
+
|
|
166
|
+
q2_sales = Namo.new([
|
|
167
|
+
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
|
|
168
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
169
|
+
])
|
|
170
|
+
|
|
171
|
+
all_sales = q1_sales + q2_sales
|
|
172
|
+
# => #<Namo [
|
|
173
|
+
# {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
174
|
+
# {product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
|
|
175
|
+
# {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
|
|
176
|
+
# {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
177
|
+
# ]>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The dimensions must match — concatenating Namo objects with different dimensions raises an `ArgumentError`. Formulae carry through from the left-hand side.
|
|
181
|
+
|
|
182
|
+
### Row Removal
|
|
183
|
+
|
|
184
|
+
`-` removes from the first Namo any row that appears exactly in the second:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
sales = Namo.new([
|
|
188
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
189
|
+
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
|
|
190
|
+
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
|
|
191
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
192
|
+
])
|
|
193
|
+
|
|
194
|
+
discontinued = Namo.new([
|
|
195
|
+
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
|
|
196
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
197
|
+
])
|
|
198
|
+
|
|
199
|
+
sales - discontinued
|
|
200
|
+
# => #<Namo [
|
|
201
|
+
# {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
202
|
+
# {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
|
|
203
|
+
# ]>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Removal is exact — every dimension, every value must match. The dimensions must match; different dimensions raise an `ArgumentError`. Formulae carry through from the left-hand side.
|
|
115
207
|
|
|
116
208
|
### Formulae
|
|
117
209
|
|
data/lib/Namo/Row.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Namo/Row.rb
|
|
2
|
+
# Namo::Row
|
|
3
|
+
|
|
4
|
+
class Namo
|
|
5
|
+
class Row
|
|
6
|
+
def [](name)
|
|
7
|
+
if @formulae.key?(name)
|
|
8
|
+
@formulae[name].call(self)
|
|
9
|
+
else
|
|
10
|
+
@row[name]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def match?(selections)
|
|
15
|
+
selections.all? do |dimension, coordinate|
|
|
16
|
+
case coordinate
|
|
17
|
+
when Array, Range
|
|
18
|
+
coordinate.include?(self[dimension])
|
|
19
|
+
else
|
|
20
|
+
self[dimension] == coordinate
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_h
|
|
26
|
+
@row
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def initialize(row, formulae)
|
|
32
|
+
@row = row
|
|
33
|
+
@formulae = formulae
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/Namo/VERSION.rb
CHANGED
data/lib/Symbol.rb
ADDED
data/lib/namo.rb
CHANGED
|
@@ -1,41 +1,13 @@
|
|
|
1
1
|
# namo.rb
|
|
2
2
|
# Namo
|
|
3
3
|
|
|
4
|
+
require_relative 'Namo/NegatedDimension'
|
|
5
|
+
require_relative 'Namo/Row'
|
|
6
|
+
require_relative 'Symbol'
|
|
7
|
+
|
|
4
8
|
class Namo
|
|
5
9
|
include Enumerable
|
|
6
10
|
|
|
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
11
|
attr_accessor :data
|
|
40
12
|
attr_accessor :formulae
|
|
41
13
|
|
|
@@ -53,16 +25,26 @@ class Namo
|
|
|
53
25
|
|
|
54
26
|
def [](*names, **selections)
|
|
55
27
|
rows = selections.any? ? select{|row| row.match?(selections)} : entries
|
|
56
|
-
|
|
57
|
-
|
|
28
|
+
negated, positive = names.partition{|n| n.is_a?(NegatedDimension)}
|
|
29
|
+
if negated.any? && positive.any?
|
|
30
|
+
raise ArgumentError, "cannot mix projection and contraction in a single call"
|
|
31
|
+
end
|
|
32
|
+
projected = (
|
|
33
|
+
if negated.any?
|
|
34
|
+
excluded = negated.map(&:name)
|
|
35
|
+
kept = dimensions - excluded
|
|
36
|
+
rows.map do |row|
|
|
37
|
+
kept.each_with_object({}){|name, hash| hash[name] = row[name]}
|
|
38
|
+
end
|
|
39
|
+
elsif positive.any?
|
|
58
40
|
rows.map do |row|
|
|
59
|
-
|
|
41
|
+
positive.each_with_object({}){|name, hash| hash[name] = row[name]}
|
|
60
42
|
end
|
|
61
43
|
else
|
|
62
44
|
rows.map(&:to_h)
|
|
63
45
|
end
|
|
64
46
|
)
|
|
65
|
-
self.class.new(
|
|
47
|
+
self.class.new(projected, formulae: @formulae.dup)
|
|
66
48
|
end
|
|
67
49
|
|
|
68
50
|
def []=(name, proc)
|
|
@@ -74,6 +56,20 @@ class Namo
|
|
|
74
56
|
@data.each{|row_data| block.call(Row.new(row_data, @formulae))}
|
|
75
57
|
end
|
|
76
58
|
|
|
59
|
+
def +(other)
|
|
60
|
+
unless dimensions == other.dimensions
|
|
61
|
+
raise ArgumentError, "dimensions do not match"
|
|
62
|
+
end
|
|
63
|
+
self.class.new(@data + other.data, formulae: other.formulae.merge(@formulae))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def -(other)
|
|
67
|
+
unless dimensions == other.dimensions
|
|
68
|
+
raise ArgumentError, "dimensions do not match"
|
|
69
|
+
end
|
|
70
|
+
self.class.new(@data - other.data, formulae: @formulae.dup)
|
|
71
|
+
end
|
|
72
|
+
|
|
77
73
|
def to_a
|
|
78
74
|
@data.map do |row|
|
|
79
75
|
row.keys.each_with_object({}) do |key, hash|
|
data/namo.gemspec
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
require_relative './lib/Namo/VERSION'
|
|
2
2
|
|
|
3
|
+
class Gem::Specification
|
|
4
|
+
def development_dependencies=(gems)
|
|
5
|
+
gems.each{|gem| add_development_dependency(*gem)}
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
3
9
|
Gem::Specification.new do |spec|
|
|
4
10
|
spec.name = 'namo'
|
|
5
11
|
spec.version = Namo::VERSION
|
|
@@ -13,20 +19,23 @@ Gem::Specification.new do |spec|
|
|
|
13
19
|
spec.license = 'MIT'
|
|
14
20
|
|
|
15
21
|
spec.required_ruby_version = '>= 2.7'
|
|
22
|
+
|
|
16
23
|
spec.require_paths = ['lib']
|
|
17
24
|
|
|
18
25
|
spec.files = [
|
|
26
|
+
'namo.gemspec',
|
|
19
27
|
'CHANGELOG',
|
|
20
28
|
'Gemfile',
|
|
21
|
-
Dir['lib/**/*.rb'],
|
|
22
29
|
'LICENSE',
|
|
23
|
-
'namo.gemspec',
|
|
24
30
|
'Rakefile',
|
|
25
31
|
'README.md',
|
|
32
|
+
Dir['lib/**/*.rb'],
|
|
26
33
|
Dir['test/**/*.rb'],
|
|
27
34
|
].flatten
|
|
28
35
|
|
|
29
|
-
spec.
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
spec.development_dependencies = %w{
|
|
37
|
+
minitest
|
|
38
|
+
minitest-spec-context
|
|
39
|
+
rake
|
|
40
|
+
}
|
|
32
41
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require 'minitest/autorun'
|
|
2
|
+
require 'minitest-spec-context'
|
|
3
|
+
|
|
4
|
+
require_relative '../../lib/namo'
|
|
5
|
+
|
|
6
|
+
describe Namo::NegatedDimension do
|
|
7
|
+
describe "#name" do
|
|
8
|
+
it "returns the original symbol" do
|
|
9
|
+
nd = Namo::NegatedDimension.new(:price)
|
|
10
|
+
_(nd.name).must_equal :price
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require 'minitest/autorun'
|
|
2
|
+
require 'minitest-spec-context'
|
|
3
|
+
|
|
4
|
+
require_relative '../../lib/namo'
|
|
5
|
+
|
|
6
|
+
describe Namo::Row do
|
|
7
|
+
let(:row_data) do
|
|
8
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
let(:formulae) do
|
|
12
|
+
{}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
let(:row) do
|
|
16
|
+
Namo::Row.new(row_data, formulae)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe "#[]" do
|
|
20
|
+
it "returns raw data by dimension name" do
|
|
21
|
+
_(row[:product]).must_equal 'Widget'
|
|
22
|
+
_(row[:price]).must_equal 10.0
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "returns nil for missing dimensions" do
|
|
26
|
+
_(row[:missing]).must_be_nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "resolves formulae over raw data" do
|
|
30
|
+
formulae[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
31
|
+
_(row[:revenue]).must_equal 1000.0
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "composes formulae" do
|
|
35
|
+
formulae[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
36
|
+
formulae[:cost] = proc{|r| r[:quantity] * 4.0}
|
|
37
|
+
formulae[:profit] = proc{|r| r[:revenue] - r[:cost]}
|
|
38
|
+
_(row[:profit]).must_equal 600.0
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe "#match?" do
|
|
43
|
+
it "matches a single value" do
|
|
44
|
+
_(row.match?(product: 'Widget')).must_equal true
|
|
45
|
+
_(row.match?(product: 'Gadget')).must_equal false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "matches an array of values" do
|
|
49
|
+
_(row.match?(product: ['Widget', 'Gadget'])).must_equal true
|
|
50
|
+
_(row.match?(product: ['Gadget'])).must_equal false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "matches a range" do
|
|
54
|
+
_(row.match?(price: 5.0..15.0)).must_equal true
|
|
55
|
+
_(row.match?(price: 20.0..30.0)).must_equal false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "matches multiple dimensions" do
|
|
59
|
+
_(row.match?(product: 'Widget', quarter: 'Q1')).must_equal true
|
|
60
|
+
_(row.match?(product: 'Widget', quarter: 'Q2')).must_equal false
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe "#to_h" do
|
|
65
|
+
it "returns the underlying row hash" do
|
|
66
|
+
_(row.to_h).must_equal row_data
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -85,6 +85,53 @@ describe Namo do
|
|
|
85
85
|
_(result.to_a).must_equal [{price: 10.0}, {price: 10.0}]
|
|
86
86
|
end
|
|
87
87
|
end
|
|
88
|
+
|
|
89
|
+
context "contraction" do
|
|
90
|
+
it "removes named dimensions" do
|
|
91
|
+
result = sales[-:price, -:quantity]
|
|
92
|
+
_(result.dimensions).must_equal [:product, :quarter]
|
|
93
|
+
_(result.to_a.count).must_equal 4
|
|
94
|
+
_(result.to_a.first).must_equal({product: 'Widget', quarter: 'Q1'})
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "removes a single dimension" do
|
|
98
|
+
result = sales[-:price]
|
|
99
|
+
_(result.dimensions).must_equal [:product, :quarter, :quantity]
|
|
100
|
+
_(result.to_a.count).must_equal 4
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "raises when mixing projection and contraction" do
|
|
104
|
+
_ { sales[:product, -:price] }.must_raise ArgumentError
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "carries formulae through contraction" do
|
|
108
|
+
sales[:label] = proc{|r| "#{r[:product]}-#{r[:quarter]}"}
|
|
109
|
+
result = sales[-:price, -:quantity]
|
|
110
|
+
_(result.map{|row| row[:label]}).must_equal [
|
|
111
|
+
'Widget-Q1', 'Widget-Q2', 'Gadget-Q1', 'Gadget-Q2'
|
|
112
|
+
]
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
context "selection and contraction" do
|
|
117
|
+
it "can use them together" do
|
|
118
|
+
result = sales[-:price, -:quantity, product: 'Widget']
|
|
119
|
+
_(result.to_a.count).must_equal 2
|
|
120
|
+
_(result.to_a).must_equal [
|
|
121
|
+
{product: 'Widget', quarter: 'Q1'},
|
|
122
|
+
{product: 'Widget', quarter: 'Q2'}
|
|
123
|
+
]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "can chain them" do
|
|
127
|
+
result = sales[product: 'Widget'][-:price, -:quantity]
|
|
128
|
+
_(result.to_a.count).must_equal 2
|
|
129
|
+
_(result.to_a).must_equal [
|
|
130
|
+
{product: 'Widget', quarter: 'Q1'},
|
|
131
|
+
{product: 'Widget', quarter: 'Q2'}
|
|
132
|
+
]
|
|
133
|
+
end
|
|
134
|
+
end
|
|
88
135
|
end
|
|
89
136
|
|
|
90
137
|
describe "#[]= formulae" do
|
|
@@ -204,6 +251,92 @@ describe Namo do
|
|
|
204
251
|
end
|
|
205
252
|
end
|
|
206
253
|
|
|
254
|
+
describe "#+" do
|
|
255
|
+
let(:more_data) do
|
|
256
|
+
[
|
|
257
|
+
{product: 'Widget', quarter: 'Q3', price: 10.0, quantity: 200},
|
|
258
|
+
{product: 'Gadget', quarter: 'Q3', price: 25.0, quantity: 80}
|
|
259
|
+
]
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
let(:more_sales) do
|
|
263
|
+
Namo.new(more_data)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
it "concatenates rows" do
|
|
267
|
+
result = sales + more_sales
|
|
268
|
+
_(result.to_a.count).must_equal 6
|
|
269
|
+
_(result.to_a).must_equal(sample_data + more_data)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it "preserves dimensions" do
|
|
273
|
+
result = sales + more_sales
|
|
274
|
+
_(result.dimensions).must_equal [:product, :quarter, :price, :quantity]
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
it "carries formulae through from self" do
|
|
278
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
279
|
+
result = sales + more_sales
|
|
280
|
+
_(result.map{|row| row[:revenue]}).must_equal [1000.0, 1500.0, 1000.0, 1500.0, 2000.0, 2000.0]
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
it "merges formulae from other" do
|
|
284
|
+
more_sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
285
|
+
result = sales + more_sales
|
|
286
|
+
_(result.map{|row| row[:revenue]}).must_equal [1000.0, 1500.0, 1000.0, 1500.0, 2000.0, 2000.0]
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
it "prefers self's formulae on conflict" do
|
|
290
|
+
sales[:label] = proc{|r| "self: #{r[:product]}"}
|
|
291
|
+
more_sales[:label] = proc{|r| "other: #{r[:product]}"}
|
|
292
|
+
result = sales + more_sales
|
|
293
|
+
_(result.map{|row| row[:label]}).must_equal [
|
|
294
|
+
'self: Widget', 'self: Widget', 'self: Gadget', 'self: Gadget',
|
|
295
|
+
'self: Widget', 'self: Gadget'
|
|
296
|
+
]
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
it "raises when dimensions differ" do
|
|
300
|
+
other = Namo.new([{product: 'Widget', quarter: 'Q1'}])
|
|
301
|
+
_ { sales + other }.must_raise ArgumentError
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
describe "#-" do
|
|
306
|
+
let(:to_remove) do
|
|
307
|
+
Namo.new([
|
|
308
|
+
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
|
|
309
|
+
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
|
|
310
|
+
])
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
it "removes matching rows" do
|
|
314
|
+
result = sales - to_remove
|
|
315
|
+
_(result.to_a.count).must_equal 2
|
|
316
|
+
_(result.to_a).must_equal [
|
|
317
|
+
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
|
|
318
|
+
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40}
|
|
319
|
+
]
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
it "preserves non-matching rows" do
|
|
323
|
+
other = Namo.new([{product: 'Thingo', quarter: 'Q4', price: 99.0, quantity: 1}])
|
|
324
|
+
result = sales - other
|
|
325
|
+
_(result.to_a).must_equal sample_data
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
it "carries formulae through from self" do
|
|
329
|
+
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
330
|
+
result = sales - to_remove
|
|
331
|
+
_(result.map{|row| row[:revenue]}).must_equal [1500.0, 1000.0]
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
it "raises when dimensions differ" do
|
|
335
|
+
other = Namo.new([{product: 'Widget', quarter: 'Q1'}])
|
|
336
|
+
_ { sales - other }.must_raise ArgumentError
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
207
340
|
describe "#to_a" do
|
|
208
341
|
it "returns the data as an array of hashes" do
|
|
209
342
|
_(sales.to_a).must_equal sample_data
|
data/test/Symbol_test.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'minitest/autorun'
|
|
2
|
+
require 'minitest-spec-context'
|
|
3
|
+
|
|
4
|
+
require_relative '../lib/namo'
|
|
5
|
+
|
|
6
|
+
describe Symbol do
|
|
7
|
+
describe "#-@" do
|
|
8
|
+
it "returns a NegatedDimension" do
|
|
9
|
+
_(-:price).must_be_kind_of Namo::NegatedDimension
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "preserves the symbol name" do
|
|
13
|
+
_((-:price).name).must_equal :price
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
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.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- thoran
|
|
@@ -64,10 +64,16 @@ files:
|
|
|
64
64
|
- LICENSE
|
|
65
65
|
- README.md
|
|
66
66
|
- Rakefile
|
|
67
|
+
- lib/Namo/NegatedDimension.rb
|
|
68
|
+
- lib/Namo/Row.rb
|
|
67
69
|
- lib/Namo/VERSION.rb
|
|
70
|
+
- lib/Symbol.rb
|
|
68
71
|
- lib/namo.rb
|
|
69
72
|
- namo.gemspec
|
|
70
|
-
- test/
|
|
73
|
+
- test/Namo/NegatedDimension_test.rb
|
|
74
|
+
- test/Namo/Row_test.rb
|
|
75
|
+
- test/Namo_test.rb
|
|
76
|
+
- test/Symbol_test.rb
|
|
71
77
|
homepage: https://github.com/thoran/namo
|
|
72
78
|
licenses:
|
|
73
79
|
- MIT
|