namo 0.1.0 → 0.3.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: b1bc0f90c2517b809ed9929ffcdf2fbc1e538974bc30320a3b603d93582f5200
4
- data.tar.gz: 70247c7e8fa7b48ac0192400016351c323f5abcd9ee1095653cc58d6edc524e4
3
+ metadata.gz: a0cb29939a05de5108211f0d9f89b8c6b3cd0826433199f3f4014f1201a28fee
4
+ data.tar.gz: d628a872b860561da5c9f02867123864508c34c3311e7d9962457146226d1071
5
5
  SHA512:
6
- metadata.gz: b7162863da3b04b989dd6206234094b160b0f32d36fee58f948608b85a3cd1c2dc09691c6e2c2c74d598942a0c15e44760f6b498a7bddbb1cc4bb2366b2996ac
7
- data.tar.gz: 5dd204de47dc6979753a0b7113765093222f5fab870bacd4034aada2f93288af6e872781f357704951b75d1e17ea9c6aefc8a625530ca2e6f253f695c5456fc2
6
+ metadata.gz: 72b7db10583b8316b0e727b4af8404c2069533049c40e83a3974841b8a1822139898922492f721e31328632b32582c8795a50fb9a53262bee04fa5bc75672713
7
+ data.tar.gz: b269559c27dd1999ba2c3fb638c85abc0ee22530d984ffe620b2ac9e3fa1726655ac7c7d26c96273baf1bd1396df6ff0eeaeaf7c4e71753aee588eabdb09417f
data/CHANGELOG CHANGED
@@ -1,6 +1,30 @@
1
1
  CHANGELOG
2
2
  _________
3
3
 
4
+ 20260415
5
+ 0.3.0: + contraction
6
+
7
+ 1. + Namo::NegatedDimension: Wrapper for negated dimension names used in contraction.
8
+ 2. + Symbol#-@: Produce a NegatedDimension, enabling -:date syntax.
9
+ 3. ~ Namo#[]: Extend to recognise NegatedDimension for contraction (remove named dimensions, keep everything else). Raise ArgumentError when mixing projection and contraction.
10
+ 4. > Namo::Row: Extract to Namo/Row.rb.
11
+ 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.
12
+ 6. ~ README.md: + Contraction section
13
+ 7. ~ Namo::VERSION: /0.2.0/0.3.0/
14
+
15
+ 20260414
16
+ 0.2.0: Include Enumerable.
17
+
18
+ 1. ~ Namo: include Enumerable
19
+ 2. + Namo#each: Yield Row objects wrapping each data row with formulae. Returns an Enumerator when no block is given.
20
+ 3. ~ Namo#[]: Refactor to use Enumerable#select and Row#match? instead of select_rows.
21
+ 4. - Namo#select_rows: Replaced by Row#match?.
22
+ 5. + Namo::Row#match?: Selection logic moved from Namo to Row.
23
+ 6. + Namo::Row#to_h: Return the underlying row hash.
24
+ 7. ~ namo_test.rb: Add tests for #each and Enumerable methods (map, reduce, min_by, flat_map).
25
+ 8. ~ README.md: + Enumerable section
26
+ 9. ~ Namo::VERSION: /0.1.0/0.2.0/; /module/class/ to match Namo's definition.
27
+
4
28
  20260328
5
29
  0.1.0: Add formulae via []=. Formulae are procs which receive a Row object, compose via named references, and carry through selection.
6
30
 
data/README.md CHANGED
@@ -111,7 +111,47 @@ sales[:quarter, :price, product: 'Widget']
111
111
  # ]>
112
112
  ```
113
113
 
114
- Selection and projection always return a new Namo instance, so everything chains.
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.
115
155
 
116
156
  ### Formulae
117
157
 
@@ -156,6 +196,35 @@ sales[product: 'Widget'][:revenue, :quarter]
156
196
 
157
197
  Formulae carry through selection — a filtered Namo instance remembers its formulae.
158
198
 
199
+ ### Enumerable
200
+
201
+ 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:
202
+
203
+ ```ruby
204
+ sales.reduce(0){|sum, row| sum + row[:quantity]}
205
+ # => 350
206
+
207
+ sales[product: 'Widget'].reduce(0){|sum, row| sum + row[:quantity]}
208
+ # => 250
209
+
210
+ sales[:revenue] = proc{|row| row[:price] * row[:quantity]}
211
+
212
+ sales.reduce(0){|sum, row| sum + row[:revenue]}
213
+ # => 5000.0
214
+
215
+ sales[product: 'Widget'].reduce(0){|sum, row| sum + row[:revenue]}
216
+ # => 2500.0
217
+
218
+ sales.map{|row| row[:product]}
219
+ # => ['Widget', 'Widget', 'Gadget', 'Gadget']
220
+
221
+ sales.min_by{|row| row[:price]}[:product]
222
+ # => 'Widget'
223
+
224
+ sales.flat_map{|row| [row[:price]]}
225
+ # => [10.0, 10.0, 25.0, 25.0]
226
+ ```
227
+
159
228
  ### Extracting data
160
229
 
161
230
  `to_a` returns an array of hashes:
@@ -0,0 +1,14 @@
1
+ # Namo/NegatedDimension.rb
2
+ # Namo::NegatedDimension
3
+
4
+ class Namo
5
+ class NegatedDimension
6
+ attr_reader :name
7
+
8
+ private
9
+
10
+ def initialize(name)
11
+ @name = name
12
+ end
13
+ end
14
+ end
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
@@ -1,6 +1,6 @@
1
1
  # Namo/VERSION.rb
2
2
  # Namo::VERSION
3
3
 
4
- module Namo
5
- VERSION = '0.1.0'
4
+ class Namo
5
+ VERSION = '0.3.0'
6
6
  end
data/lib/Symbol.rb ADDED
@@ -0,0 +1,8 @@
1
+ # Symbol.rb
2
+ # Symbol#-@
3
+
4
+ class Symbol
5
+ def -@
6
+ Namo::NegatedDimension.new(self)
7
+ end
8
+ end
data/lib/namo.rb CHANGED
@@ -1,23 +1,12 @@
1
1
  # namo.rb
2
2
  # Namo
3
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
- private
4
+ require_relative 'Namo/NegatedDimension'
5
+ require_relative 'Namo/Row'
6
+ require_relative 'Symbol'
15
7
 
16
- def initialize(row, formulae)
17
- @row = row
18
- @formulae = formulae
19
- end
20
- end
8
+ class Namo
9
+ include Enumerable
21
10
 
22
11
  attr_accessor :data
23
12
  attr_accessor :formulae
@@ -35,20 +24,38 @@ class Namo
35
24
  end
36
25
 
37
26
  def [](*names, **selections)
38
- data = selections.any? ? select_rows(selections) : @data
39
- if names.any?
40
- data = data.map do |row_data|
41
- row = Row.new(row_data, @formulae)
42
- names.each_with_object({}){|name, hash| hash[name] = row[name]}
43
- end
27
+ rows = selections.any? ? select{|row| row.match?(selections)} : entries
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"
44
31
  end
45
- self.class.new(data, formulae: @formulae.dup)
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?
40
+ rows.map do |row|
41
+ positive.each_with_object({}){|name, hash| hash[name] = row[name]}
42
+ end
43
+ else
44
+ rows.map(&:to_h)
45
+ end
46
+ )
47
+ self.class.new(projected, formulae: @formulae.dup)
46
48
  end
47
49
 
48
50
  def []=(name, proc)
49
51
  @formulae[name] = proc
50
52
  end
51
53
 
54
+ def each(&block)
55
+ return enum_for(:each) unless block_given?
56
+ @data.each{|row_data| block.call(Row.new(row_data, @formulae))}
57
+ end
58
+
52
59
  def to_a
53
60
  @data.map do |row|
54
61
  row.keys.each_with_object({}) do |key, hash|
@@ -63,17 +70,4 @@ class Namo
63
70
  @data = data
64
71
  @formulae = formulae
65
72
  end
66
-
67
- def select_rows(selections)
68
- @data.select do |row|
69
- selections.all? do |dimension, coordinate|
70
- case coordinate
71
- when Array, Range
72
- coordinate.include?(row[dimension])
73
- else
74
- row[dimension] == coordinate
75
- end
76
- end
77
- end
78
- end
79
73
  end
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.add_development_dependency 'minitest'
30
- spec.add_development_dependency 'minitest-spec-context'
31
- spec.add_development_dependency 'rake'
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
@@ -146,6 +193,64 @@ describe Namo do
146
193
  end
147
194
  end
148
195
 
196
+ describe "#each" do
197
+ it "yields Row objects" do
198
+ rows = []
199
+ sales.each{|row| rows << row}
200
+ _(rows.first).must_be_kind_of Namo::Row
201
+ _(rows.length).must_equal 4
202
+ end
203
+
204
+ it "yields rows with access to data" do
205
+ products = sales.map{|row| row[:product]}
206
+ _(products).must_equal ['Widget', 'Widget', 'Gadget', 'Gadget']
207
+ end
208
+
209
+ it "yields rows with access to formulae" do
210
+ sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
211
+ revenues = sales.map{|row| row[:revenue]}
212
+ _(revenues).must_equal [1000.0, 1500.0, 1000.0, 1500.0]
213
+ end
214
+
215
+ it "returns an enumerator without a block" do
216
+ _(sales.each).must_be_kind_of Enumerator
217
+ end
218
+ end
219
+
220
+ describe "Enumerable" do
221
+ it "supports reduce" do
222
+ total_quantity = sales.reduce(0){|sum, row| sum + row[:quantity]}
223
+ _(total_quantity).must_equal 350
224
+ end
225
+
226
+ it "supports reduce with selection" do
227
+ widget_quantity = sales[product: 'Widget'].reduce(0){|sum, row| sum + row[:quantity]}
228
+ _(widget_quantity).must_equal 250
229
+ end
230
+
231
+ it "supports reduce with formulae" do
232
+ sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
233
+ total_revenue = sales.reduce(0){|sum, row| sum + row[:revenue]}
234
+ _(total_revenue).must_equal 5000.0
235
+ end
236
+
237
+ it "supports reduce with selection and formulae" do
238
+ sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
239
+ widget_revenue = sales[product: 'Widget'].reduce(0){|sum, row| sum + row[:revenue]}
240
+ _(widget_revenue).must_equal 2500.0
241
+ end
242
+
243
+ it "supports min_by" do
244
+ cheapest = sales.min_by{|row| row[:price]}
245
+ _(cheapest[:product]).must_equal 'Widget'
246
+ end
247
+
248
+ it "supports flat_map" do
249
+ prices = sales.flat_map{|row| [row[:price]]}
250
+ _(prices).must_equal [10.0, 10.0, 25.0, 25.0]
251
+ end
252
+ end
253
+
149
254
  describe "#to_a" do
150
255
  it "returns the data as an array of hashes" do
151
256
  _(sales.to_a).must_equal sample_data
@@ -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.1.0
4
+ version: 0.3.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/namo_test.rb
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
@@ -86,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
86
92
  - !ruby/object:Gem::Version
87
93
  version: '0'
88
94
  requirements: []
89
- rubygems_version: 4.0.8
95
+ rubygems_version: 4.0.10
90
96
  specification_version: 4
91
97
  summary: Named dimensional data for Ruby.
92
98
  test_files: []