namo 0.11.0 → 0.12.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: 40c2256efac2663b7593bb1f7e4c3ca08fa07e6e225c75fcef3d6d8658f3794b
4
- data.tar.gz: abd7bd9076d7019c245d942425232dcf0966edb076cf6d50fdedf7b74efea90f
3
+ metadata.gz: 4a7dd953b56fb4f692b0e92822cb6d5dcf9a38f8645a913aedc06333441ad305
4
+ data.tar.gz: b492295ead6b4a9ea5647290eef7d008e86e6cbc0275176f45f2ea76b1692854
5
5
  SHA512:
6
- metadata.gz: 2b5045c4160d8812fa3f04f10dc027bf8c6cfaf937405cec0570a31af52babdc1f2dc6e25db249a9b9dea81afc9806f617203ae71dded576974fe9512bd4ec00
7
- data.tar.gz: a553ef0ae27d67bf28d8d91ba9e2e71f217f8ee23ff3d177ad207dd520f30971730774a51660830d98d79a6189cee16f1227dbd168282f3a27c4316912a6f29f
6
+ metadata.gz: 35a4a0acebfa4f01b83948c7a1639606cb58f9de7c0e75c8cf5886654128526cbe5554e4670503b74b5687674da00929a1b3252ee7bf30d2ecb7db976b580de5
7
+ data.tar.gz: ce62a04e0e1a7db2e1ec1a5f20674e000baadf3de64186ea1afe2718d8acdd36189e612aa14c6ca71a8e6330009aa0bfa380b797020ee9fbb69a6a49e9390e90
data/CHANGELOG CHANGED
@@ -1,6 +1,25 @@
1
1
  CHANGELOG
2
2
  _________
3
3
 
4
+ 20260601
5
+ 0.12.0: Constructor widening — keyword data: and name:.
6
+
7
+ 1. ~ lib/namo.rb: Namo#initialize widened from `(data = [], formulae: {})` to `(positional_data = nil, data: [], formulae: {}, name: nil)`. Data may now be passed positionally or by the `data:` keyword; positional wins when both are given (`@data = positional_data || data`). The positional default changes from `[]` to `nil` so it acts as a "not supplied" sentinel that lets `|| data` fall through to the keyword path; an explicit `Namo.new([])` still yields `@data == []` because the truthy empty array short-circuits. + `name:` keyword, stored in `@name`. Every pre-existing call site (positional data, positional data + keyword formulae, no-arg, formulae-only, and the operators' internal `self.class.new(rows, formulae: ...)`) is unaffected.
8
+ 2. + lib/namo.rb: `attr_accessor :name`, alongside the existing `:data` and `:formulae` accessors. Gives `name` reading and `name=` post-construction mutation. Operator results carry `name == nil` (the operators construct without `name:`), establishing the subclass guard convention: subclasses guard `initialize` side effects with `return unless name`, so operator-derived instances skip them and only explicitly-named constructions fire them.
9
+ 3. ~ test/namo_test.rb: + "construction" describe (positional data, positional + keyword formulae, no-arg empty, formulae-only, explicit `[]` honoured over the nil sentinel, keyword `data:`, positional-wins-over-keyword, and round-trips through a set operator and an Enumerable method). + "#name" describe (stored from keyword, defaults to nil, settable post-construction, nil on set-operator and Enumerable-derived results). + "subclass side-effect guard" describe — an anonymous Class.new(Namo) with a `return unless name`-guarded counter, asserting it fires for a named construction and stays untouched for an unnamed construction and an operator result.
10
+ 4. ~ README.md: + note in the Usage section that data may be passed positionally or by `data:`, with positional winning when both are given. + "Named Namos" section: the `name:` keyword, the `name`/`name=` accessor, nil-on-derivation for operator results, and the subclass guard convention with the TradingAnalysis / argument-less `super` example.
11
+ 5. ~ ROADMAP.md: Promote 0.12.0 from upcoming to shipped — `## Current state` bumped to 0.12.0, a `### 0.12.0 (2026-06-01)` entry added in the shipped section (written in shipped voice), and the now-redundant `## 0.12.0` upcoming section removed; fold the widened constructor into the Summary's completed vocabulary and point "next phase" at 0.13.0+.
12
+ 6. ~ Namo::VERSION: /0.11.1/0.12.0/
13
+
14
+ 20260601
15
+ 0.11.1: Extract the subset Enumerable methods into a Namo::Enumerable module.
16
+
17
+ 1. + lib/Namo/Enumerable.rb: New module Namo::Enumerable holding each and the subset-returning Enumerable methods (select plus its filter/find_all aliases, reject, sort_by, first, last, take, drop, take_while, drop_while, uniq, partition), moved verbatim from Namo. The module does `include ::Enumerable` — the leading :: is required, because under the nested `class Namo; module Enumerable` scope a bare `Enumerable` resolves to Namo::Enumerable itself and raises a cyclic-include error.
18
+ 2. ~ lib/namo.rb: Remove the inline each and the eleven subset methods; remove `include Enumerable`; + `require_relative './Namo/Enumerable'` and `include Namo::Enumerable` (which transitively brings stdlib Enumerable into the ancestor chain below the module, so the overrides win and map/reduce/etc. still fall through). Pure reorganisation — no behaviour change.
19
+ 3. ~ test/namo_test.rb: + Namo::Enumerable module tests — it is a Module, Namo includes it and stdlib Enumerable, and it precedes ::Enumerable in Namo.ancestors so the overrides resolve first. The existing Enumerable subset tests are unchanged and remain green, evidencing the move is behaviour-preserving.
20
+ 4. ~ ROADMAP.md: + note in the 0.11.0 section that, as of 0.11.1, these methods live in the Namo::Enumerable module; bump Date to 20260601.
21
+ 5. ~ Namo::VERSION: /0.11.0/0.11.1/
22
+
4
23
  20260531
5
24
  0.11.0: ~ Subset Enumerable methods (select, reject, sort_by, first, last, take, drop, take_while, drop_while, uniq, partition) return Namos
6
25
 
data/README.md CHANGED
@@ -33,6 +33,16 @@ sales = Namo.new([
33
33
  ])
34
34
  ```
35
35
 
36
+ Data may be passed positionally, as above, or by the `data:` keyword where that reads more explicitly:
37
+
38
+ ```ruby
39
+ sales = Namo.new(data: [
40
+ {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100}
41
+ ])
42
+ ```
43
+
44
+ When both are given, the positional argument wins and the keyword `data:` is ignored.
45
+
36
46
  Dimensions and coordinates are inferred:
37
47
 
38
48
  ```ruby
@@ -715,6 +725,39 @@ sales[:product, :quarter, :revenue].to_h
715
725
  # }
716
726
  ```
717
727
 
728
+ ### Named Namos
729
+
730
+ A Namo can carry a name, passed by the `name:` keyword and read or set through the `name` accessor:
731
+
732
+ ```ruby
733
+ sales = Namo.new(data: rows, name: :sales)
734
+ sales.name
735
+ # => :sales
736
+
737
+ sales.name = :renamed
738
+ ```
739
+
740
+ A name defaults to `nil`, and operator results are name-less by design — the result of `+`, `*`, `select`, and the rest is a derived object, not the original, so giving it the parent's name would mislead:
741
+
742
+ ```ruby
743
+ (sales + more).name
744
+ # => nil
745
+ ```
746
+
747
+ This `nil`-on-derivation behaviour is what lets subclasses with side effects in `initialize` guard those effects on the name. Operator-derived instances are name-less and skip the side effects; explicitly constructed instances pass `name:` and the side effects fire:
748
+
749
+ ```ruby
750
+ class TradingAnalysis < Namo
751
+ def initialize(positional_data = nil, data: [], formulae: {}, name: nil)
752
+ super
753
+ return unless name
754
+ register_indicators
755
+ end
756
+ end
757
+ ```
758
+
759
+ `super` with no parentheses forwards every argument — positional and keyword — to `Namo#initialize` unchanged. The `return unless name` guard means a subclass need not override every operator to stop the result of `*` or `select` from re-running its construction side effects: it guards on `name` instead.
760
+
718
761
  ## Why?
719
762
 
720
763
  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.
@@ -0,0 +1,72 @@
1
+ # Namo/Enumerable.rb
2
+ # Namo::Enumerable
3
+
4
+ class Namo
5
+ module Enumerable
6
+ include ::Enumerable
7
+
8
+ def each(&block)
9
+ return enum_for(:each) unless block_given?
10
+ @data.each{|row_data| block.call(Row.new(row_data, @formulae))}
11
+ end
12
+
13
+ def select(&block)
14
+ self.class.new(@data.select{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
15
+ end
16
+ alias_method :filter, :select
17
+ alias_method :find_all, :select
18
+
19
+ def reject(&block)
20
+ self.class.new(@data.reject{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
21
+ end
22
+
23
+ def sort_by(&block)
24
+ self.class.new(@data.sort_by{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
25
+ end
26
+
27
+ def first(n = nil)
28
+ if n
29
+ self.class.new(@data.first(n), formulae: @formulae.dup)
30
+ else
31
+ @data.first ? Row.new(@data.first, @formulae) : nil
32
+ end
33
+ end
34
+
35
+ def last(n = nil)
36
+ if n
37
+ self.class.new(@data.last(n), formulae: @formulae.dup)
38
+ else
39
+ @data.last ? Row.new(@data.last, @formulae) : nil
40
+ end
41
+ end
42
+
43
+ def take(n)
44
+ self.class.new(@data.take(n), formulae: @formulae.dup)
45
+ end
46
+
47
+ def drop(n)
48
+ self.class.new(@data.drop(n), formulae: @formulae.dup)
49
+ end
50
+
51
+ def take_while(&block)
52
+ self.class.new(@data.take_while{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
53
+ end
54
+
55
+ def drop_while(&block)
56
+ self.class.new(@data.drop_while{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
57
+ end
58
+
59
+ def uniq(&block)
60
+ rows = block ? @data.uniq{|row| block.call(Row.new(row, @formulae))} : @data.uniq
61
+ self.class.new(rows, formulae: @formulae.dup)
62
+ end
63
+
64
+ def partition(&block)
65
+ matches, non_matches = @data.partition{|row| block.call(Row.new(row, @formulae))}
66
+ [
67
+ self.class.new(matches, formulae: @formulae.dup),
68
+ self.class.new(non_matches, formulae: @formulae.dup),
69
+ ]
70
+ end
71
+ end
72
+ end
data/lib/Namo/VERSION.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.11.0'
5
+ VERSION = '0.12.0'
6
6
  end
data/lib/namo.rb CHANGED
@@ -3,14 +3,16 @@
3
3
 
4
4
  require_relative './Namo/NegatedDimension'
5
5
  require_relative './Namo/Row'
6
+ require_relative './Namo/Enumerable'
6
7
  require_relative './Namo/VERSION'
7
8
  require_relative './Symbol'
8
9
 
9
10
  class Namo
10
- include Enumerable
11
+ include Namo::Enumerable
11
12
 
12
13
  attr_accessor :data
13
14
  attr_accessor :formulae
15
+ attr_accessor :name
14
16
 
15
17
  def dimensions
16
18
  data_dimensions + derived_dimensions
@@ -76,70 +78,6 @@ class Namo
76
78
  @formulae[name] = proc
77
79
  end
78
80
 
79
- def each(&block)
80
- return enum_for(:each) unless block_given?
81
- @data.each{|row_data| block.call(Row.new(row_data, @formulae))}
82
- end
83
-
84
- def select(&block)
85
- self.class.new(@data.select{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
86
- end
87
- alias_method :filter, :select
88
- alias_method :find_all, :select
89
-
90
- def reject(&block)
91
- self.class.new(@data.reject{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
92
- end
93
-
94
- def sort_by(&block)
95
- self.class.new(@data.sort_by{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
96
- end
97
-
98
- def first(n = nil)
99
- if n
100
- self.class.new(@data.first(n), formulae: @formulae.dup)
101
- else
102
- @data.first ? Row.new(@data.first, @formulae) : nil
103
- end
104
- end
105
-
106
- def last(n = nil)
107
- if n
108
- self.class.new(@data.last(n), formulae: @formulae.dup)
109
- else
110
- @data.last ? Row.new(@data.last, @formulae) : nil
111
- end
112
- end
113
-
114
- def take(n)
115
- self.class.new(@data.take(n), formulae: @formulae.dup)
116
- end
117
-
118
- def drop(n)
119
- self.class.new(@data.drop(n), formulae: @formulae.dup)
120
- end
121
-
122
- def take_while(&block)
123
- self.class.new(@data.take_while{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
124
- end
125
-
126
- def drop_while(&block)
127
- self.class.new(@data.drop_while{|row| block.call(Row.new(row, @formulae))}, formulae: @formulae.dup)
128
- end
129
-
130
- def uniq(&block)
131
- rows = block ? @data.uniq{|row| block.call(Row.new(row, @formulae))} : @data.uniq
132
- self.class.new(rows, formulae: @formulae.dup)
133
- end
134
-
135
- def partition(&block)
136
- matches, non_matches = @data.partition{|row| block.call(Row.new(row, @formulae))}
137
- [
138
- self.class.new(matches, formulae: @formulae.dup),
139
- self.class.new(non_matches, formulae: @formulae.dup),
140
- ]
141
- end
142
-
143
81
  def +(other)
144
82
  raise_unless_namo(other)
145
83
  raise_unless_matching_data_dimensions(other)
@@ -309,8 +247,9 @@ class Namo
309
247
  end
310
248
  end
311
249
 
312
- def initialize(data = [], formulae: {})
313
- @data = data
250
+ def initialize(positional_data = nil, data: [], formulae: {}, name: nil)
251
+ @data = positional_data || data
314
252
  @formulae = formulae
253
+ @name = name
315
254
  end
316
255
  end
data/test/namo_test.rb CHANGED
@@ -33,6 +33,113 @@ describe Namo do
33
33
  end
34
34
  end
35
35
 
36
+ describe "construction" do
37
+ it "accepts positional data" do
38
+ _(Namo.new([{x: 1}]).data).must_equal [{x: 1}]
39
+ end
40
+
41
+ it "accepts positional data with keyword formulae" do
42
+ namo = Namo.new([{x: 1}], formulae: {y: proc{|r| r[:x] * 2}})
43
+ _(namo.data).must_equal [{x: 1}]
44
+ _(namo.values(:y)).must_equal [2]
45
+ end
46
+
47
+ it "produces an empty Namo with no arguments" do
48
+ namo = Namo.new
49
+ _(namo.data).must_equal []
50
+ _(namo.formulae).must_equal({})
51
+ end
52
+
53
+ it "accepts keyword formulae with no data" do
54
+ namo = Namo.new(formulae: {y: proc{|r| r[:x] * 2}})
55
+ _(namo.data).must_equal []
56
+ _(namo.derived_dimensions).must_equal [:y]
57
+ end
58
+
59
+ it "honours an explicit empty positional array over the nil sentinel" do
60
+ _(Namo.new([]).data).must_equal []
61
+ end
62
+
63
+ it "accepts data by keyword" do
64
+ _(Namo.new(data: [{x: 1}]).data).must_equal [{x: 1}]
65
+ end
66
+
67
+ it "lets positional data win when both positional and keyword data are given" do
68
+ _(Namo.new([{x: 1}], data: [{x: 2}]).data).must_equal [{x: 1}]
69
+ end
70
+
71
+ it "survives a round-trip through a set operator" do
72
+ a = Namo.new([{x: 1}])
73
+ b = Namo.new([{x: 2}])
74
+ _((a + b).data).must_equal [{x: 1}, {x: 2}]
75
+ end
76
+
77
+ it "survives a round-trip through an Enumerable method" do
78
+ namo = Namo.new([{x: 1}, {x: 2}])
79
+ _(namo.select{|row| row[:x] > 1}.data).must_equal [{x: 2}]
80
+ end
81
+ end
82
+
83
+ describe "#name" do
84
+ it "stores a name passed by keyword" do
85
+ _(Namo.new([{x: 1}], name: :foo).name).must_equal :foo
86
+ end
87
+
88
+ it "defaults to nil when no name is passed" do
89
+ _(Namo.new([{x: 1}]).name).must_be_nil
90
+ end
91
+
92
+ it "is settable post-construction" do
93
+ namo = Namo.new([{x: 1}])
94
+ namo.name = :bar
95
+ _(namo.name).must_equal :bar
96
+ end
97
+
98
+ it "is nil on a Namo derived from a set operator" do
99
+ a = Namo.new([{x: 1}], name: :a)
100
+ b = Namo.new([{x: 2}], name: :b)
101
+ _((a + b).name).must_be_nil
102
+ end
103
+
104
+ it "is nil on a Namo derived from an Enumerable method" do
105
+ namo = Namo.new([{x: 1}, {x: 2}], name: :original)
106
+ _(namo.select{|row| row[:x] > 1}.name).must_be_nil
107
+ end
108
+ end
109
+
110
+ describe "subclass side-effect guard" do
111
+ before do
112
+ @guard_class = Class.new(Namo) do
113
+ def self.fired
114
+ @fired ||= []
115
+ end
116
+ def initialize(positional_data = nil, data: [], formulae: {}, name: nil)
117
+ super
118
+ return unless name
119
+ self.class.fired << name
120
+ end
121
+ end
122
+ end
123
+
124
+ it "fires guarded side effects for an explicitly named construction" do
125
+ @guard_class.new(data: [{x: 1}], name: :foo)
126
+ _(@guard_class.fired).must_equal [:foo]
127
+ end
128
+
129
+ it "skips guarded side effects for an unnamed construction" do
130
+ @guard_class.new(data: [{x: 1}])
131
+ _(@guard_class.fired).must_equal []
132
+ end
133
+
134
+ it "skips guarded side effects for an operator result" do
135
+ a = @guard_class.new(data: [{x: 1}], name: :a)
136
+ b = @guard_class.new(data: [{x: 2}], name: :b)
137
+ @guard_class.fired.clear
138
+ _((a + b).name).must_be_nil
139
+ _(@guard_class.fired).must_equal []
140
+ end
141
+ end
142
+
36
143
  describe "#dimensions" do
37
144
  it "infers dimensions from hash keys" do
38
145
  _(sales.dimensions).must_equal [:product, :quarter, :price, :quantity]
@@ -913,6 +1020,22 @@ describe Namo do
913
1020
  end
914
1021
  end
915
1022
 
1023
+ describe "Namo::Enumerable module" do
1024
+ it "is a Module supplying the subset methods" do
1025
+ _(Namo::Enumerable).must_be_kind_of Module
1026
+ end
1027
+
1028
+ it "is included in Namo, transitively including stdlib Enumerable" do
1029
+ _(Namo.include?(Namo::Enumerable)).must_equal true
1030
+ _(Namo.include?(Enumerable)).must_equal true
1031
+ end
1032
+
1033
+ it "sits above stdlib Enumerable so its overrides win" do
1034
+ ancestors = Namo.ancestors
1035
+ _(ancestors.index(Namo::Enumerable) < ancestors.index(::Enumerable)).must_equal true
1036
+ end
1037
+ end
1038
+
916
1039
  describe "#+" do
917
1040
  let(:more_data) do
918
1041
  [
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.11.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran
@@ -64,6 +64,7 @@ files:
64
64
  - LICENSE
65
65
  - README.md
66
66
  - Rakefile
67
+ - lib/Namo/Enumerable.rb
67
68
  - lib/Namo/NegatedDimension.rb
68
69
  - lib/Namo/Row.rb
69
70
  - lib/Namo/VERSION.rb