namo 0.6.0 → 0.7.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: 9d29843b2d9895ba401fa013ea83753f548458bc09bcd8e61218400f81950e33
4
- data.tar.gz: 6b8772e13cd773d41ae4cabddb019bddb533cbc9a24ece7729977543e462a30c
3
+ metadata.gz: c0aa48a727f9cc67fae8eea94dcfa8227c66a34aa8918ac65e30b33b7685c82b
4
+ data.tar.gz: c8f634b5c41f35337fb843d286edf80cbdabef951b076dd36533f5a2531c3715
5
5
  SHA512:
6
- metadata.gz: 22c4c03943617b1ec56e45ace4722019e6d54bb12f9b30fb3e2b85eebce132477a380ccde5529ea65782be03f060abd7507ebbfbe8b06d3c8ad10d7b90319061
7
- data.tar.gz: 17e0163a3353024bec9826d5fc95773893d8195dbb7efe87ff7e814da7c74ca1391f168ca10025a6deb9fcd44e7ecae8fc3a10c44f29f9ef1e2018a0de96aa97
6
+ metadata.gz: 9b196446a0d02dc61790a25ea416bfebdd591020cfd6ef89430f5e8b9d8bd1239cc265bca83f0cd6dde4d42ce3c3927074ac69e1d4e79ae88db667b95caeadf3
7
+ data.tar.gz: 3680f44daa8e1e78921d1d286c7f436947b6e436c9308d62038c17a1f9658ae4ccc23196afb2eb44e6b3efb41398ab1b39d8cf3bf813f87c24df12c7cb975586
data/CHANGELOG CHANGED
@@ -1,6 +1,22 @@
1
1
  CHANGELOG
2
2
  _________
3
3
 
4
+ 20260520
5
+ 0.7.0: + derived-dimension surfacing, lazy single-column access, live views
6
+
7
+ 1. + Namo#data_dimensions: Returns the storage dimensions as a plain Array (keys of the first row).
8
+ 2. + Namo#derived_dimensions: Returns the formula names as a plain Array.
9
+ 3. + Namo#values(*dims): Per-dimension full sequences (duplicates preserved, in row order). With no args, returns a Hash {dim => sequence} across the queryable namespace. With one arg, lazily computes and returns just that column as an Array. With multiple args, returns a subset Hash containing each requested dimension. Unknown dimensions propagate as nil per row, matching Row#[] and Namo#[] selection conventions — values(:unknown) returns an Array of nils; values(:known, :unknown) returns {known: [...], unknown: [nil, ...]}.
10
+ 4. + Namo#coordinates(*dims): Per-dimension unique-value sets. Same argument shape as #values; coordinates(dim) == values(dim).uniq. Unknown dimensions therefore appear as [nil].
11
+ 5. + Namo#to_h: Alias for the full values Hash.
12
+ 6. ~ Namo#dimensions: Now covers the queryable namespace (storage + derived) instead of storage-only. Return type stays a plain Array. Memoisation removed: every call recomputes from current state (live view).
13
+ 7. ~ Namo#coordinates: Memoisation removed (was @coordinates ||= ...). Now covers the queryable namespace; coordinates(:derived_dim) and coordinates[:derived_dim] work, evaluating the formula across all rows. New positional-args API supports lazy single-column access.
14
+ 8. ~ Namo#canonical_data: Sorts by data_dimensions to preserve 0.6.0 row-equality semantics under the broader dimensions definition.
15
+ 9. /raise_unless_matching_dimensions/raise_unless_matching_data_dimensions/: Private helper renamed to reflect what it actually compares.
16
+ 10. ~ test/namo_test.rb: + Tests for #data_dimensions, #derived_dimensions, the no-arg/single-arg/multi-arg forms of #values and #coordinates, derived-dimension surfacing in #dimensions, #to_h, the coordinates(dim) == values(dim).uniq consistency property, and live-view semantics (added rows / formulae reflected on next call).
17
+ 11. ~ Rakefile: + -V mainfont=Charter -V monofont=Menlo on pandoc invocation in docs:md2pdf, for a cleaner serif body font and so code spans containing Unicode math glyphs (e.g. ∅) render correctly under xelatex.
18
+ 12. ~ Namo::VERSION: /0.6.0/0.7.0/
19
+
4
20
  20260511
5
21
  0.6.0: + equality, pattern-match, and subset/superset operators
6
22
 
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; formulae attach to a Namo alongside stored data and re-evaluate on each access; the operators that combine Namos all take Namos and return Namos, so analytical pipelines close; and the formula mechanism is type-agnostic — strings, dates, booleans, and arbitrary Ruby objects work as readily as numbers.
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, 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
 
@@ -383,7 +383,7 @@ sales[:product, :quarter, :revenue]
383
383
  # ]>
384
384
  ```
385
385
 
386
- Formulae aren't materialised into stored columns — they re-evaluate on every access. A `:revenue` value reflects the current `:price` and `:quantity` at the moment you ask for it, so derived values stay in sync with whatever the underlying data is doing.
386
+ Formulae aren't materialised into row data — they re-evaluate on every access. A `:revenue` value reflects the current `:price` and `:quantity` at the moment you ask for it, so derived values stay in sync with whatever the underlying data is doing.
387
387
 
388
388
  Formulae compose:
389
389
 
@@ -412,6 +412,78 @@ sales[product: 'Widget'][:revenue, :quarter]
412
412
 
413
413
  Formulae carry through selection — a filtered Namo instance remembers its formulae.
414
414
 
415
+ ### Coordinates and values
416
+
417
+ `dimensions` covers the *queryable namespace* — every name you can ask for, whether it lives in the row data or is computed by a formula. Once formulae are defined, they appear alongside data dimensions:
418
+
419
+ ```ruby
420
+ sales[:revenue] = proc{|row| row[:price] * row[:quantity]}
421
+
422
+ sales.dimensions
423
+ # => [:product, :quarter, :price, :quantity, :revenue]
424
+
425
+ sales.data_dimensions
426
+ # => [:product, :quarter, :price, :quantity]
427
+
428
+ sales.derived_dimensions
429
+ # => [:revenue]
430
+ ```
431
+
432
+ `coordinates` gives the unique values per dimension, including derived ones:
433
+
434
+ ```ruby
435
+ sales.coordinates[:product]
436
+ # => ['Widget', 'Gadget']
437
+
438
+ sales.coordinates[:revenue]
439
+ # => [1000.0, 1500.0]
440
+ ```
441
+
442
+ `values` gives the full per-row sequence — duplicates preserved, row order preserved:
443
+
444
+ ```ruby
445
+ sales.values[:product]
446
+ # => ['Widget', 'Widget', 'Gadget', 'Gadget']
447
+
448
+ sales.values[:revenue]
449
+ # => [1000.0, 1500.0, 1000.0, 1500.0]
450
+ ```
451
+
452
+ Both `coordinates` and `values` accept positional arguments. With no args they return a Hash across the queryable namespace; with one arg they lazily compute and return just that column as an Array; with multiple args they return a subset Hash containing just the requested columns:
453
+
454
+ ```ruby
455
+ sales.values(:product)
456
+ # => ['Widget', 'Widget', 'Gadget', 'Gadget']
457
+
458
+ sales.values(:product, :quarter)
459
+ # => {
460
+ # product: ['Widget', 'Widget', 'Gadget', 'Gadget'],
461
+ # quarter: ['Q1', 'Q2', 'Q1', 'Q2']
462
+ # }
463
+
464
+ sales.coordinates(:revenue)
465
+ # => [1000.0, 1500.0]
466
+ ```
467
+
468
+ Single-arg access is lazy: `sales.values(:revenue)` evaluates the formula only across the rows of `:revenue`, without materialising the other columns. The bracket form (`sales.values[:revenue]`) still works through ordinary Hash lookup but pays for the full materialisation up front.
469
+
470
+ `coordinates` is `values` with `.uniq` applied per column — `coordinates(dim) == values(dim).uniq` holds for every dimension.
471
+
472
+ `to_h` is the Ruby-conventional alias for the full `values` Hash:
473
+
474
+ ```ruby
475
+ sales.to_h
476
+ # => {
477
+ # product: ['Widget', 'Widget', 'Gadget', 'Gadget'],
478
+ # quarter: ['Q1', 'Q2', 'Q1', 'Q2'],
479
+ # price: [10.0, 10.0, 25.0, 25.0],
480
+ # quantity: [100, 150, 40, 60],
481
+ # revenue: [1000.0, 1500.0, 1000.0, 1500.0]
482
+ # }
483
+ ```
484
+
485
+ Unknown dimensions propagate `nil` per row — `values(:missing)` returns `[nil, nil, ...]` rather than raising or returning a sentinel, matching the convention used by `Row#[]` and `[]` selection. Use `dimensions.include?(:dim)` if you need to check membership directly.
486
+
415
487
  ### Enumerable
416
488
 
417
489
  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:
@@ -443,7 +515,7 @@ sales.flat_map{|row| [row[:price]]}
443
515
 
444
516
  ### Extracting data
445
517
 
446
- `to_a` returns an array of hashes:
518
+ `to_a` returns an array of hashes — the row-oriented form:
447
519
 
448
520
  ```ruby
449
521
  sales[:product, :quarter, :revenue].to_a
@@ -455,6 +527,17 @@ sales[:product, :quarter, :revenue].to_a
455
527
  # ]
456
528
  ```
457
529
 
530
+ `to_h` returns a hash of arrays — the columnar form (see [Coordinates and values](#coordinates-and-values) above):
531
+
532
+ ```ruby
533
+ sales[:product, :quarter, :revenue].to_h
534
+ # => {
535
+ # product: ['Widget', 'Widget', 'Gadget', 'Gadget'],
536
+ # quarter: ['Q1', 'Q2', 'Q1', 'Q2'],
537
+ # revenue: [1000.0, 1500.0, 1000.0, 1500.0]
538
+ # }
539
+ ```
540
+
458
541
  ## Why?
459
542
 
460
543
  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.
data/Rakefile CHANGED
@@ -21,7 +21,7 @@ namespace :docs do
21
21
  task :md2pdf => :md4print do
22
22
  Dir.glob('docs/*.print.md').each do |f|
23
23
  pdf = f.sub(/\.md$/, '.pdf')
24
- sh "pandoc #{f} --pdf-engine=xelatex -V geometry:margin=1in -o #{pdf}"
24
+ sh "pandoc #{f} --pdf-engine=xelatex -V geometry:margin=1in -V mainfont=Charter -V monofont=Menlo -o #{pdf}"
25
25
  end
26
26
  end
27
27
 
data/lib/Namo/VERSION.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.6.0'
5
+ VERSION = '0.7.0'
6
6
  end
data/lib/namo.rb CHANGED
@@ -12,15 +12,39 @@ class Namo
12
12
  attr_accessor :formulae
13
13
 
14
14
  def dimensions
15
- @dimensions ||= @data.first.keys
15
+ @data.first.keys + @formulae.keys
16
16
  end
17
17
 
18
- def coordinates
19
- @coordinates ||= (
20
- dimensions.each_with_object({}) do |dimension, hash|
21
- hash[dimension] = @data.map{|row| row[dimension]}.uniq
22
- end
23
- )
18
+ def data_dimensions
19
+ @data.first.keys
20
+ end
21
+
22
+ def derived_dimensions
23
+ @formulae.keys
24
+ end
25
+
26
+ def values(*dims)
27
+ if dims.empty?
28
+ dimensions.each_with_object({}){|dim, hash| hash[dim] = values_for(dim)}
29
+ elsif dims.length == 1
30
+ values_for(dims.first)
31
+ else
32
+ dims.each_with_object({}){|dim, hash| hash[dim] = values_for(dim)}
33
+ end
34
+ end
35
+
36
+ def coordinates(*dims)
37
+ if dims.empty?
38
+ values.transform_values(&:uniq)
39
+ elsif dims.length == 1
40
+ values(dims.first).uniq
41
+ else
42
+ dims.each_with_object({}){|dim, hash| hash[dim] = values(dim).uniq}
43
+ end
44
+ end
45
+
46
+ def to_h
47
+ values
24
48
  end
25
49
 
26
50
  def [](*names, **selections)
@@ -32,7 +56,7 @@ class Namo
32
56
  projected = (
33
57
  if negated.any?
34
58
  excluded = negated.map(&:name)
35
- kept = dimensions - excluded
59
+ kept = data_dimensions - excluded
36
60
  rows.map do |row|
37
61
  kept.each_with_object({}){|name, hash| hash[name] = row[name]}
38
62
  end
@@ -58,31 +82,31 @@ class Namo
58
82
 
59
83
  def +(other)
60
84
  raise_unless_namo(other)
61
- raise_unless_matching_dimensions(other)
85
+ raise_unless_matching_data_dimensions(other)
62
86
  self.class.new(@data + other.data, formulae: other.formulae.merge(@formulae))
63
87
  end
64
88
 
65
89
  def -(other)
66
90
  raise_unless_namo(other)
67
- raise_unless_matching_dimensions(other)
91
+ raise_unless_matching_data_dimensions(other)
68
92
  self.class.new(@data - other.data, formulae: @formulae.dup)
69
93
  end
70
94
 
71
95
  def &(other)
72
96
  raise_unless_namo(other)
73
- raise_unless_matching_dimensions(other)
97
+ raise_unless_matching_data_dimensions(other)
74
98
  self.class.new(@data & other.data, formulae: @formulae.dup)
75
99
  end
76
100
 
77
101
  def |(other)
78
102
  raise_unless_namo(other)
79
- raise_unless_matching_dimensions(other)
103
+ raise_unless_matching_data_dimensions(other)
80
104
  self.class.new((@data | other.data), formulae: other.formulae.merge(@formulae))
81
105
  end
82
106
 
83
107
  def ^(other)
84
108
  raise_unless_namo(other)
85
- raise_unless_matching_dimensions(other)
109
+ raise_unless_matching_data_dimensions(other)
86
110
  self.class.new((@data - other.data) + (other.data - @data), formulae: other.formulae.merge(@formulae))
87
111
  end
88
112
 
@@ -109,25 +133,25 @@ class Namo
109
133
 
110
134
  def <(other)
111
135
  raise_unless_namo(other)
112
- raise_unless_matching_dimensions(other)
136
+ raise_unless_matching_data_dimensions(other)
113
137
  proper_subset_of_rows?(other)
114
138
  end
115
139
 
116
140
  def <=(other)
117
141
  raise_unless_namo(other)
118
- raise_unless_matching_dimensions(other)
142
+ raise_unless_matching_data_dimensions(other)
119
143
  subset_of_rows?(other)
120
144
  end
121
145
 
122
146
  def >(other)
123
147
  raise_unless_namo(other)
124
- raise_unless_matching_dimensions(other)
148
+ raise_unless_matching_data_dimensions(other)
125
149
  other.proper_subset_of_rows?(self)
126
150
  end
127
151
 
128
152
  def >=(other)
129
153
  raise_unless_namo(other)
130
- raise_unless_matching_dimensions(other)
154
+ raise_unless_matching_data_dimensions(other)
131
155
  other.subset_of_rows?(self)
132
156
  end
133
157
 
@@ -142,7 +166,7 @@ class Namo
142
166
  protected
143
167
 
144
168
  def canonical_data
145
- @data.sort_by{|row| row.values_at(*dimensions.sort)}
169
+ @data.sort_by{|row| row.values_at(*data_dimensions.sort)}
146
170
  end
147
171
 
148
172
  def subset_of_rows?(other)
@@ -157,15 +181,23 @@ class Namo
157
181
 
158
182
  private
159
183
 
184
+ def values_for(dim)
185
+ if data_dimensions.include?(dim)
186
+ @data.map{|row_data| row_data[dim]}
187
+ else
188
+ @data.map{|row_data| Row.new(row_data, @formulae)[dim]}
189
+ end
190
+ end
191
+
160
192
  def raise_unless_namo(other)
161
193
  unless other.is_a?(Namo)
162
194
  raise TypeError, "can't compare Namo with #{other.class}"
163
195
  end
164
196
  end
165
197
 
166
- def raise_unless_matching_dimensions(other)
167
- unless dimensions == other.dimensions
168
- raise ArgumentError, "dimensions don't match: #{dimensions} vs #{other.dimensions}"
198
+ def raise_unless_matching_data_dimensions(other)
199
+ unless data_dimensions == other.data_dimensions
200
+ raise ArgumentError, "dimensions don't match: #{data_dimensions} vs #{other.data_dimensions}"
169
201
  end
170
202
  end
171
203
 
data/test/namo_test.rb CHANGED
@@ -21,19 +21,159 @@ describe Namo do
21
21
  it "infers dimensions from hash keys" do
22
22
  _(sales.dimensions).must_equal [:product, :quarter, :price, :quantity]
23
23
  end
24
+
25
+ it "includes derived dimensions after storage dimensions" do
26
+ sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
27
+ sales[:label] = proc{|r| "#{r[:product]}-#{r[:quarter]}"}
28
+ _(sales.dimensions).must_equal [:product, :quarter, :price, :quantity, :revenue, :label]
29
+ end
30
+
31
+ it "reflects mutation on the next call" do
32
+ sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
33
+ _(sales.dimensions).must_include :revenue
34
+ sales.formulae.delete(:revenue)
35
+ _(sales.dimensions).wont_include :revenue
36
+ end
37
+ end
38
+
39
+ describe "#data_dimensions" do
40
+ it "returns only the storage keys" do
41
+ sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
42
+ _(sales.data_dimensions).must_equal [:product, :quarter, :price, :quantity]
43
+ end
44
+ end
45
+
46
+ describe "#derived_dimensions" do
47
+ it "returns only the formula keys" do
48
+ sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
49
+ _(sales.derived_dimensions).must_equal [:revenue]
50
+ end
51
+
52
+ it "is empty when no formulae are defined" do
53
+ _(sales.derived_dimensions).must_equal []
54
+ end
24
55
  end
25
56
 
26
57
  describe "#coordinates" do
27
- it "extracts unique values for each dimension" do
58
+ it "with no args returns a Hash of unique values for each dimension" do
28
59
  _(sales.coordinates).must_equal ({
29
60
  product: ['Widget', 'Gadget'],
30
61
  quarter: ['Q1', 'Q2'],
31
62
  price: [10.0, 25.0],
32
63
  quantity: [100, 150, 40, 60]
33
64
  })
65
+ end
66
+
67
+ it "0.6.0-style indexing still works" do
34
68
  _(sales.coordinates[:product]).must_equal ['Widget', 'Gadget']
35
69
  _(sales.coordinates[:quarter]).must_equal ['Q1', 'Q2']
36
70
  end
71
+
72
+ it "with one arg returns just that column's unique values as an Array" do
73
+ _(sales.coordinates(:product)).must_equal ['Widget', 'Gadget']
74
+ end
75
+
76
+ it "with one arg returns [nil] for an unknown dimension (nil values uniqued)" do
77
+ _(sales.coordinates(:missing)).must_equal [nil]
78
+ end
79
+
80
+ it "with one arg evaluates a derived dimension" do
81
+ sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
82
+ _(sales.coordinates(:revenue)).must_equal [1000.0, 1500.0]
83
+ end
84
+
85
+ it "with multiple args returns a subset Hash" do
86
+ _(sales.coordinates(:product, :quarter)).must_equal({
87
+ product: ['Widget', 'Gadget'],
88
+ quarter: ['Q1', 'Q2']
89
+ })
90
+ end
91
+
92
+ it "with multiple args includes unknown dimensions as [nil]" do
93
+ _(sales.coordinates(:product, :missing)).must_equal({
94
+ product: ['Widget', 'Gadget'],
95
+ missing: [nil]
96
+ })
97
+ end
98
+
99
+ it "covers derived dimensions in the no-arg form" do
100
+ sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
101
+ _(sales.coordinates[:revenue]).must_equal [1000.0, 1500.0]
102
+ end
103
+ end
104
+
105
+ describe "#values" do
106
+ it "with no args returns a Hash of full sequences for each dimension" do
107
+ _(sales.values).must_equal({
108
+ product: ['Widget', 'Widget', 'Gadget', 'Gadget'],
109
+ quarter: ['Q1', 'Q2', 'Q1', 'Q2'],
110
+ price: [10.0, 10.0, 25.0, 25.0],
111
+ quantity: [100, 150, 40, 60]
112
+ })
113
+ end
114
+
115
+ it "with one arg returns just that column as an Array, preserving duplicates and order" do
116
+ _(sales.values(:product)).must_equal ['Widget', 'Widget', 'Gadget', 'Gadget']
117
+ _(sales.values(:price)).must_equal [10.0, 10.0, 25.0, 25.0]
118
+ end
119
+
120
+ it "with one arg returns an Array of nils for an unknown dimension (one nil per row)" do
121
+ _(sales.values(:missing)).must_equal [nil, nil, nil, nil]
122
+ end
123
+
124
+ it "with one arg evaluates a derived dimension across all rows" do
125
+ sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
126
+ _(sales.values(:revenue)).must_equal [1000.0, 1500.0, 1000.0, 1500.0]
127
+ end
128
+
129
+ it "with multiple args returns a subset Hash" do
130
+ _(sales.values(:product, :quarter)).must_equal({
131
+ product: ['Widget', 'Widget', 'Gadget', 'Gadget'],
132
+ quarter: ['Q1', 'Q2', 'Q1', 'Q2']
133
+ })
134
+ end
135
+
136
+ it "with multiple args includes unknown dimensions as Arrays of nils" do
137
+ _(sales.values(:product, :missing)).must_equal({
138
+ product: ['Widget', 'Widget', 'Gadget', 'Gadget'],
139
+ missing: [nil, nil, nil, nil]
140
+ })
141
+ end
142
+
143
+ it "covers derived dimensions in the no-arg form" do
144
+ sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
145
+ _(sales.values[:revenue]).must_equal [1000.0, 1500.0, 1000.0, 1500.0]
146
+ end
147
+ end
148
+
149
+ describe "#to_h" do
150
+ it "returns the full values Hash" do
151
+ _(sales.to_h).must_equal sales.values
152
+ end
153
+ end
154
+
155
+ describe "aspect consistency" do
156
+ it "satisfies coordinates(dim) == values(dim).uniq for each dimension" do
157
+ sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
158
+ sales.dimensions.each do |dim|
159
+ _(sales.coordinates(dim)).must_equal sales.values(dim).uniq
160
+ end
161
+ end
162
+ end
163
+
164
+ describe "live-view semantics" do
165
+ it "reflects added rows on next call" do
166
+ _(sales.values(:product)).must_equal ['Widget', 'Widget', 'Gadget', 'Gadget']
167
+ sales.data << {product: 'Thingo', quarter: 'Q3', price: 5.0, quantity: 10}
168
+ _(sales.values(:product)).must_equal ['Widget', 'Widget', 'Gadget', 'Gadget', 'Thingo']
169
+ end
170
+
171
+ it "reflects added formulae on next call" do
172
+ _(sales.derived_dimensions).must_equal []
173
+ sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
174
+ _(sales.derived_dimensions).must_equal [:revenue]
175
+ _(sales.coordinates(:revenue)).must_equal [1000.0, 1500.0]
176
+ end
37
177
  end
38
178
 
39
179
  describe "#[]" do
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.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran