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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11011fc9115f460be75cc35c2f4a99f7d695a53a0ef362b714b4e564cef682ee
4
- data.tar.gz: 9bf58afbf1f52f800c12cad38963253ed7cc371fd966b45f98131ca154595d98
3
+ metadata.gz: 6841b3921358ce7eefdbfbdba3a62eafd74857223d9df86038f9cc8087bbb947
4
+ data.tar.gz: bf05398ce78ea49683fbb7adb0b9989c6d6df86864a9bdf2373b156b14d8d5e7
5
5
  SHA512:
6
- metadata.gz: fb32fce7accb48566f6610254315ce068111bd7b582cff01e7e890dcf8308ac12882ffd06a4fc01a889ba58df9481ebb79a1541bac6f710530cb3271d960973b
7
- data.tar.gz: 3a4330c0e98f19ad5a4937ebc01ba4725658da0a6c3f330969683b3f68b54611d37a26c0a42b68761c4ac2f15db7fd362868ed28be4006167fb37964812a3665
6
+ metadata.gz: 2887c9dee76341b5e4ee75d50a1416475bd8c579d52117f67dcab81c9fe7c8cb1c50c08be7295db74aaf1f2d8727aa678b95d231d4c2e1d82a25b1ca050de61e
7
+ data.tar.gz: d5c5a35ac078d60dcd1baf43e73b5be1aa1bf02ed27a65071fd146223402a812a8b9ff9b88e6be5e4e62b5b47966460df613994d79215405439cde96c1a63278
data/CHANGELOG CHANGED
@@ -1,14 +1,40 @@
1
1
  CHANGELOG
2
- _______
2
+ _________
3
3
 
4
- 2026-03-15
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
- 2026-03-15
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
- prices = Namo.new([
27
- {date: '2025-01-01', symbol: 'BHP', close: 42.5},
28
- {date: '2025-01-01', symbol: 'RIO', close: 118.3},
29
- {date: '2025-01-02', symbol: 'BHP', close: 43.1},
30
- {date: '2025-01-02', symbol: 'RIO', close: 117.8}
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
- prices.dimensions
38
- # => [:date, :symbol, :close]
37
+ sales.dimensions
38
+ # => [:product, :quarter, :price, :quantity]
39
39
 
40
- prices.coordinates[:date]
41
- # => ['2025-01-01', '2025-01-02']
40
+ sales.coordinates[:product]
41
+ # => ['Widget', 'Gadget']
42
42
 
43
- prices.coordinates[:symbol]
44
- # => ['BHP', 'RIO']
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
- prices[symbol: 'BHP']
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
- prices[date: '2025-01-01', symbol: 'BHP']
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
- prices[close: 42.0..43.0]
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
- prices[symbol: ['BHP', 'RIO']]
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
- # All data
63
- prices[]
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
- Selection always returns a new Namo. Omitting a dimension means "all values along that dimension."
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: na(med) (di)m(ensi)o(ns). A companion to Numo (numeric arrays for Ruby).
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
@@ -1,6 +1,6 @@
1
1
  # Namo/VERSION.rb
2
2
  # Namo::VERSION
3
3
 
4
- module Namo
5
- VERSION = '0.0.1'
4
+ class Namo
5
+ VERSION = '0.2.0'
6
6
  end
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
- selected = (
19
- @data.select do |row|
20
- selections.all? do |dimension, coordinate|
21
- case coordinate
22
- when Array, Range
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(selected)
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
- {date: '2025-01-01', symbol: 'BHP', close: 42.5},
13
- {date: '2025-01-01', symbol: 'RIO', close: 118.3},
14
- {date: '2025-01-02', symbol: 'BHP', close: 43.1},
15
- {date: '2025-01-02', symbol: 'RIO', close: 117.8}
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(:namo){ Namo.new(sample_data) }
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
- describe '#dimensions' do
22
- it 'infers dimensions from hash keys' do
23
- _(namo.dimensions).must_equal [:date, :symbol, :close]
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 '#coordinates' do
28
- it 'extracts unique values per dimension' do
29
- _(namo.coordinates[:date]).must_equal ['2025-01-01', '2025-01-02']
30
- _(namo.coordinates[:symbol]).must_equal ['BHP', 'RIO']
31
- _(namo.coordinates[:close]).must_equal [42.5, 118.3, 43.1, 117.8]
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 '#[]' do
36
- it 'selects by single value' do
37
- result = namo[symbol: 'BHP']
38
- _(result.coordinates[:date]).must_equal ['2025-01-01', '2025-01-02']
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 'selects by multiple dimensions' do
44
- result = namo[date: '2025-01-01', symbol: 'BHP']
45
- _(result.coordinates[:date]).must_equal ['2025-01-01']
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 'selects by range' do
51
- result = namo[close: 42.0..43.0]
52
- _(result.coordinates[:close]).must_equal [42.5]
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 'selects by array of values' do
56
- result = namo[symbol: ['BHP', 'RIO']]
57
- _(result.coordinates[:symbol]).must_equal ['BHP', 'RIO']
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 'returns a Namo' do
61
- result = namo[symbol: 'BHP']
62
- _(result).must_be_kind_of Namo
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 'returns all data when no selections given' do
66
- result = namo[]
67
- _(result.coordinates).must_equal namo.coordinates
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.1
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: 2026-03-15 00:00:00.000000000 Z
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.7
89
+ rubygems_version: 4.0.10
76
90
  specification_version: 4
77
91
  summary: Named dimensional data for Ruby.
78
92
  test_files: []