namo 0.5.0 → 0.6.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: f1cf8cf203da06b319112fec69ad8fae3dddc73ef850d3f84de4d13df2c51249
4
- data.tar.gz: 1e80df8ee9c7be401c3f9cbf9625eceef0a87b6136a9d8b3825379b32eecff3f
3
+ metadata.gz: 9d29843b2d9895ba401fa013ea83753f548458bc09bcd8e61218400f81950e33
4
+ data.tar.gz: 6b8772e13cd773d41ae4cabddb019bddb533cbc9a24ece7729977543e462a30c
5
5
  SHA512:
6
- metadata.gz: 712227a916253708637a1ca8c1f177e1ab36daba694df13edc2918943779751ba89e71f8d0f578b44d285abf522a869d5f16c3b0f8377899fa7818bbd6009d41
7
- data.tar.gz: 7c50400df852f31b3a6a12011486cd72f32c946b94c6f7e71a9d83358a50bda4eec9b8854442181112101d82b4b154e61c6159746c697d0adec192f33d99f5a3
6
+ metadata.gz: 22c4c03943617b1ec56e45ace4722019e6d54bb12f9b30fb3e2b85eebce132477a380ccde5529ea65782be03f060abd7507ebbfbe8b06d3c8ad10d7b90319061
7
+ data.tar.gz: 17e0163a3353024bec9826d5fc95773893d8195dbb7efe87ff7e814da7c74ca1391f168ca10025a6deb9fcd44e7ecae8fc3a10c44f29f9ef1e2018a0de96aa97
data/CHANGELOG CHANGED
@@ -1,6 +1,21 @@
1
1
  CHANGELOG
2
2
  _________
3
3
 
4
+ 20260511
5
+ 0.6.0: + equality, pattern-match, and subset/superset operators
6
+
7
+ 1. + Namo#==: Multiset equality on row data, ignoring class and formulae.
8
+ 2. + Namo#===: Analytical identity match — true iff other has the same dimensions and same formula names as self, ignoring rows and proc bodies. Returns false for non-Namo operands.
9
+ 3. + Namo#eql?: Strict equality requiring class match, multiset-equal data, and formula name match.
10
+ 4. + Namo#hash: Content-based hash, consistent with eql?.
11
+ 5. + Namo#<, #<=, #>, #>=: Multiset subset/superset relations on rows. Raise ArgumentError on mismatched dimensions, TypeError on non-Namo operand.
12
+ 6. ~ Namo#+, #-, #&, #|, #^: Error message on dimension mismatch updated to "dimensions don't match: X vs Y". Non-Namo operand now raises TypeError ("can't compare Namo with X") instead of NoMethodError.
13
+ 7. ~ lib/namo.rb, namo.gemspec: Minor cleanup (./-prefixed requires; gemspec whitespace).
14
+ 8. ~ test/namo_test.rb: Add tests for ==, ===, eql?, hash, <, <=, >, >=, equal?, and the new error message.
15
+ 9. ~ README.md: + Equality section, + Subset and superset section, + design-philosophy paragraph in the opening and one-line principle callouts in the dimensions, formulae, set-operator, and equality sections.
16
+ 10. + script/md4print, ~ Rakefile: + rake docs:print, docs:pdf, docs:all for regenerating docs/*.print.md and docs/*.print.pdf.
17
+ 11. ~ Namo::VERSION: /0.5.0/0.6.0/
18
+
4
19
  20260416
5
20
  0.5.0: + row-axis set operations: intersection (&), union (|), symmetric difference (^)
6
21
 
data/README.md CHANGED
@@ -4,6 +4,8 @@ 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.
8
+
7
9
  ## Installation
8
10
 
9
11
  ```
@@ -44,6 +46,8 @@ sales.coordinates[:quarter]
44
46
  # => ['Q1', 'Q2']
45
47
  ```
46
48
 
49
+ Every key is a dimension; every value is a coordinate. There's no schema declaration and no choosing which column is "the index" — `price` and `quantity` are no less first-class than `product` and `quarter`.
50
+
47
51
  ### Selection
48
52
 
49
53
  Select by named dimension using keyword arguments:
@@ -155,6 +159,8 @@ Selection, projection, and contraction always return a new Namo instance, so eve
155
159
 
156
160
  ### Concatenation
157
161
 
162
+ `+` is the first of Namo's binary operators: it takes a Namo on each side and returns a Namo. The same shape holds for `-`, `&`, `|`, `^`, `==`, `===`, `<`, `<=`, `>`, `>=` and (later) the composition operators — Namo in, Namo (or boolean) out — so analytical pipelines stay queryable end-to-end.
163
+
158
164
  `+` combines two Namo objects that share the same dimensions by appending the rows of the second to the first:
159
165
 
160
166
  ```ruby
@@ -280,6 +286,87 @@ set_a ^ set_b
280
286
 
281
287
  The dimensions must match; different dimensions raise an `ArgumentError`. Formulae merge from both sides; the left-hand side's formulae take precedence on conflict.
282
288
 
289
+ ### Equality
290
+
291
+ Comparison on Namos is **multiset-theoretic on rows**: row order is ignored (it's an accident of ingestion, not data), but row multiplicities count (they *are* data). The same stance carries across the equality, pattern-match, and subset/superset operators below.
292
+
293
+ `==` is multiset equality on rows. Class and formulae are ignored; row order is ignored; row multiplicities are not.
294
+
295
+ ```ruby
296
+ a = Namo.new([{x: 1}, {x: 2}])
297
+ b = Namo.new([{x: 2}, {x: 1}])
298
+
299
+ a == b
300
+ # => true
301
+
302
+ a == Namo.new([{x: 1}, {x: 1}, {x: 2}])
303
+ # => false
304
+ ```
305
+
306
+ `eql?` is stricter: it also requires the class to match and the formula names to match. Like `===`, it ignores proc bodies — proc identity isn't a meaningful equivalence in Ruby (`proc{...} == proc{...}` is false), so neither `===` nor `eql?` uses it.
307
+
308
+ `hash` is consistent with `eql?` and is content-based, so equal Namos hash equally and can be used as Hash keys:
309
+
310
+ ```ruby
311
+ h = {a => 'first'}
312
+ h[b]
313
+ # => 'first'
314
+ ```
315
+
316
+ `equal?` is unchanged from Ruby's default — it tests object identity.
317
+
318
+ `===` answers a different question: does the candidate have the same dimensions and the same formula names? Row data is ignored, and so are the proc bodies themselves — only the names matter. This is the `===` semantics that case statements use, so Namos can serve as templates for analytical shape:
319
+
320
+ ```ruby
321
+ sales_shape = Namo.new([{product: 'X', quarter: 'Q1', price: 0.0, quantity: 0}])
322
+ sales_shape[:revenue] = proc{|row| row[:price] * row[:quantity]}
323
+
324
+ q1 = Namo.new([{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100}])
325
+ q1[:revenue] = proc{|row| row[:price] * row[:quantity]}
326
+
327
+ sales_shape === q1
328
+ # => true (same dimensions, same formula name)
329
+
330
+ sales_shape == q1
331
+ # => false (different rows)
332
+ ```
333
+
334
+ The two `:revenue` procs are independently-written and not the same object — `proc{...} == proc{...}` is false in Ruby. But `===` doesn't compare proc identity; it asks "do these Namos have the same analytical shape?" and the shape is the set of dimensions plus the set of formula names.
335
+
336
+ Each comparison operator answers a distinct question: `eql?` is strictest (class + data + formula names); `==` is data identity; `===` is analytical identity; the subset operators are data containment.
337
+
338
+ ### Subset and Superset
339
+
340
+ `<`, `<=`, `>`, `>=` are multiset subset and superset relations on rows.
341
+
342
+ ```ruby
343
+ small = Namo.new([{x: 1}, {x: 2}])
344
+ large = Namo.new([{x: 1}, {x: 2}, {x: 3}])
345
+
346
+ small <= large
347
+ # => true
348
+
349
+ small < large
350
+ # => true
351
+
352
+ large > small
353
+ # => true
354
+ ```
355
+
356
+ Equal sets are `<=` and `>=` each other, but neither `<` nor `>`. Disjoint sets are none of the above — unless one side is empty, in which case it is a subset of (and disjoint with) the other.
357
+
358
+ Multiplicity matters: a single `{x: 1}` is a proper subset of two `{x: 1}`s.
359
+
360
+ ```ruby
361
+ one = Namo.new([{x: 1}])
362
+ two = Namo.new([{x: 1}, {x: 1}])
363
+
364
+ one < two
365
+ # => true
366
+ ```
367
+
368
+ The dimensions must match; different dimensions raise an `ArgumentError`. Comparing against a non-Namo raises a `TypeError`.
369
+
283
370
  ### Formulae
284
371
 
285
372
  Define computed dimensions using `[]=`:
@@ -296,6 +383,8 @@ sales[:product, :quarter, :revenue]
296
383
  # ]>
297
384
  ```
298
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.
387
+
299
388
  Formulae compose:
300
389
 
301
390
  ```ruby
data/Rakefile CHANGED
@@ -6,4 +6,37 @@ Rake::TestTask.new(:test) do |t|
6
6
  t.test_files = FileList['test/**/*_test.rb']
7
7
  end
8
8
 
9
+ namespace :docs do
10
+ SOURCE_DOCS = %w{COMPARISON EXAMPLES README ROADMAP}
11
+
12
+ desc "Strip syntax highlighting from code blocks for printing"
13
+ task :md4print do
14
+ SOURCE_DOCS.each do |name|
15
+ sh "script/md4print #{name}.md"
16
+ sh "mv #{name}.print.md docs/"
17
+ end
18
+ end
19
+
20
+ desc "Render print-ready markdown to PDF"
21
+ task :md2pdf => :md4print do
22
+ Dir.glob('docs/*.print.md').each do |f|
23
+ pdf = f.sub(/\.md$/, '.pdf')
24
+ sh "pandoc #{f} --pdf-engine=xelatex -V geometry:margin=1in -o #{pdf}"
25
+ end
26
+ end
27
+
28
+ desc "Remove intermediate .print.md files"
29
+ task :clean do
30
+ rm_f Dir.glob('docs/*.print.md')
31
+ end
32
+
33
+ desc "Remove all generated docs (intermediates and PDFs)"
34
+ task :clobber => :clean do
35
+ rm_f Dir.glob('docs/*.print.pdf')
36
+ end
37
+
38
+ desc "Regenerate all derived docs"
39
+ task :gen => [:md2pdf, :clean]
40
+ end
41
+
9
42
  task default: :test
data/lib/Namo/VERSION.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.5.0'
5
+ VERSION = '0.6.0'
6
6
  end
data/lib/namo.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  # namo.rb
2
2
  # Namo
3
3
 
4
- require_relative 'Namo/NegatedDimension'
5
- require_relative 'Namo/Row'
6
- require_relative 'Symbol'
4
+ require_relative './Namo/NegatedDimension'
5
+ require_relative './Namo/Row'
6
+ require_relative './Symbol'
7
7
 
8
8
  class Namo
9
9
  include Enumerable
@@ -57,40 +57,80 @@ class Namo
57
57
  end
58
58
 
59
59
  def +(other)
60
- unless dimensions == other.dimensions
61
- raise ArgumentError, "dimensions do not match"
62
- end
60
+ raise_unless_namo(other)
61
+ raise_unless_matching_dimensions(other)
63
62
  self.class.new(@data + other.data, formulae: other.formulae.merge(@formulae))
64
63
  end
65
64
 
66
65
  def -(other)
67
- unless dimensions == other.dimensions
68
- raise ArgumentError, "dimensions do not match"
69
- end
66
+ raise_unless_namo(other)
67
+ raise_unless_matching_dimensions(other)
70
68
  self.class.new(@data - other.data, formulae: @formulae.dup)
71
69
  end
72
70
 
73
71
  def &(other)
74
- unless dimensions == other.dimensions
75
- raise ArgumentError, "dimensions do not match"
76
- end
72
+ raise_unless_namo(other)
73
+ raise_unless_matching_dimensions(other)
77
74
  self.class.new(@data & other.data, formulae: @formulae.dup)
78
75
  end
79
76
 
80
77
  def |(other)
81
- unless dimensions == other.dimensions
82
- raise ArgumentError, "dimensions do not match"
83
- end
78
+ raise_unless_namo(other)
79
+ raise_unless_matching_dimensions(other)
84
80
  self.class.new((@data | other.data), formulae: other.formulae.merge(@formulae))
85
81
  end
86
82
 
87
83
  def ^(other)
88
- unless dimensions == other.dimensions
89
- raise ArgumentError, "dimensions do not match"
90
- end
84
+ raise_unless_namo(other)
85
+ raise_unless_matching_dimensions(other)
91
86
  self.class.new((@data - other.data) + (other.data - @data), formulae: other.formulae.merge(@formulae))
92
87
  end
93
88
 
89
+ def ==(other)
90
+ return false unless other.is_a?(Namo)
91
+ canonical_data == other.canonical_data
92
+ end
93
+
94
+ def ===(other)
95
+ return false unless other.is_a?(Namo)
96
+ dimensions.sort == other.dimensions.sort &&
97
+ @formulae.keys.sort == other.formulae.keys.sort
98
+ end
99
+
100
+ def eql?(other)
101
+ self.class == other.class &&
102
+ canonical_data == other.canonical_data &&
103
+ @formulae.keys.sort == other.formulae.keys.sort
104
+ end
105
+
106
+ def hash
107
+ [self.class, canonical_data, @formulae.keys.sort].hash
108
+ end
109
+
110
+ def <(other)
111
+ raise_unless_namo(other)
112
+ raise_unless_matching_dimensions(other)
113
+ proper_subset_of_rows?(other)
114
+ end
115
+
116
+ def <=(other)
117
+ raise_unless_namo(other)
118
+ raise_unless_matching_dimensions(other)
119
+ subset_of_rows?(other)
120
+ end
121
+
122
+ def >(other)
123
+ raise_unless_namo(other)
124
+ raise_unless_matching_dimensions(other)
125
+ other.proper_subset_of_rows?(self)
126
+ end
127
+
128
+ def >=(other)
129
+ raise_unless_namo(other)
130
+ raise_unless_matching_dimensions(other)
131
+ other.subset_of_rows?(self)
132
+ end
133
+
94
134
  def to_a
95
135
  @data.map do |row|
96
136
  row.keys.each_with_object({}) do |key, hash|
@@ -99,8 +139,36 @@ class Namo
99
139
  end
100
140
  end
101
141
 
142
+ protected
143
+
144
+ def canonical_data
145
+ @data.sort_by{|row| row.values_at(*dimensions.sort)}
146
+ end
147
+
148
+ def subset_of_rows?(other)
149
+ self_counts = canonical_data.tally
150
+ other_counts = other.canonical_data.tally
151
+ self_counts.all?{|row, count| (other_counts[row] || 0) >= count}
152
+ end
153
+
154
+ def proper_subset_of_rows?(other)
155
+ subset_of_rows?(other) && self != other
156
+ end
157
+
102
158
  private
103
159
 
160
+ def raise_unless_namo(other)
161
+ unless other.is_a?(Namo)
162
+ raise TypeError, "can't compare Namo with #{other.class}"
163
+ end
164
+ end
165
+
166
+ def raise_unless_matching_dimensions(other)
167
+ unless dimensions == other.dimensions
168
+ raise ArgumentError, "dimensions don't match: #{dimensions} vs #{other.dimensions}"
169
+ end
170
+ end
171
+
104
172
  def initialize(data = nil, formulae: {})
105
173
  @data = data
106
174
  @formulae = formulae
data/namo.gemspec CHANGED
@@ -19,7 +19,6 @@ Gem::Specification.new do |spec|
19
19
  spec.license = 'MIT'
20
20
 
21
21
  spec.required_ruby_version = '>= 2.7'
22
-
23
22
  spec.require_paths = ['lib']
24
23
 
25
24
  spec.files = [
@@ -466,6 +466,282 @@ describe Namo do
466
466
  end
467
467
  end
468
468
 
469
+ describe "#==" do
470
+ it "is true for same data, same order" do
471
+ a = Namo.new([{x: 1}, {x: 2}])
472
+ b = Namo.new([{x: 1}, {x: 2}])
473
+ _(a == b).must_equal true
474
+ end
475
+
476
+ it "is true for same data, different order" do
477
+ a = Namo.new([{x: 1}, {x: 2}])
478
+ b = Namo.new([{x: 2}, {x: 1}])
479
+ _(a == b).must_equal true
480
+ end
481
+
482
+ it "is false for different data" do
483
+ a = Namo.new([{x: 1}, {x: 2}])
484
+ b = Namo.new([{x: 1}, {x: 3}])
485
+ _(a == b).must_equal false
486
+ end
487
+
488
+ it "is multiset-aware: duplicates count" do
489
+ a = Namo.new([{x: 1}])
490
+ b = Namo.new([{x: 1}, {x: 1}])
491
+ _(a == b).must_equal false
492
+ end
493
+
494
+ it "is true across subclasses with same data" do
495
+ subclass = Class.new(Namo)
496
+ a = Namo.new([{x: 1}, {x: 2}])
497
+ b = subclass.new([{x: 1}, {x: 2}])
498
+ _(a == b).must_equal true
499
+ end
500
+
501
+ it "ignores formulae" do
502
+ a = Namo.new([{x: 1}, {x: 2}])
503
+ b = Namo.new([{x: 1}, {x: 2}])
504
+ b[:y] = proc{|row| row[:x] * 2}
505
+ _(a == b).must_equal true
506
+ end
507
+
508
+ it "is false against a non-Namo" do
509
+ a = Namo.new([{x: 1}, {x: 2}])
510
+ _(a == [{x: 1}, {x: 2}]).must_equal false
511
+ _(a == 'string').must_equal false
512
+ _(a == nil).must_equal false
513
+ end
514
+ end
515
+
516
+ describe "#===" do
517
+ it "is true when dimensions and formulae match, ignoring rows" do
518
+ a = Namo.new([{x: 1}])
519
+ b = Namo.new([{x: 2}, {x: 3}])
520
+ _(a === b).must_equal true
521
+ end
522
+
523
+ it "is false when formulae differ" do
524
+ a = Namo.new([{x: 1}])
525
+ b = Namo.new([{x: 1}])
526
+ b[:doubled] = proc{|row| row[:x] * 2}
527
+ _(a === b).must_equal false
528
+ end
529
+
530
+ it "is true when formulae have the same names, regardless of proc identity" do
531
+ a = Namo.new([{x: 1}])
532
+ a[:doubled] = proc{|row| row[:x] * 2}
533
+ b = Namo.new([{x: 1}])
534
+ b[:doubled] = proc{|row| row[:x] * 2}
535
+ _(a === b).must_equal true
536
+ end
537
+
538
+ it "is false when dimensions differ" do
539
+ a = Namo.new([{x: 1}])
540
+ b = Namo.new([{y: 1}])
541
+ _(a === b).must_equal false
542
+ end
543
+
544
+ it "is true when dimensions are in different order" do
545
+ a = Namo.new([{x: 1, y: 2}])
546
+ b = Namo.new([{y: 9, x: 8}])
547
+ _(a === b).must_equal true
548
+ end
549
+
550
+ it "is false for a non-Namo and does not raise" do
551
+ a = Namo.new([{x: 1}])
552
+ _(a === [{x: 1}]).must_equal false
553
+ _(a === 'string').must_equal false
554
+ _(a === nil).must_equal false
555
+ end
556
+
557
+ it "drives case statement dispatch on analytical type" do
558
+ template = Namo.new([{x: 0}])
559
+ candidate = Namo.new([{x: 5}, {x: 6}])
560
+ result = case candidate
561
+ when template; :matched
562
+ else; :not_matched
563
+ end
564
+ _(result).must_equal :matched
565
+ end
566
+ end
567
+
568
+ describe "#eql?" do
569
+ it "is true for same class, same data, no formulae" do
570
+ a = Namo.new([{x: 1}, {x: 2}])
571
+ b = Namo.new([{x: 1}, {x: 2}])
572
+ _(a.eql?(b)).must_equal true
573
+ end
574
+
575
+ it "is true for same class, same data, different order" do
576
+ a = Namo.new([{x: 1}, {x: 2}])
577
+ b = Namo.new([{x: 2}, {x: 1}])
578
+ _(a.eql?(b)).must_equal true
579
+ end
580
+
581
+ it "is true when formula names match, regardless of proc identity" do
582
+ a = Namo.new([{x: 1}, {x: 2}])
583
+ a[:y] = proc{|row| row[:x] * 2}
584
+ b = Namo.new([{x: 1}, {x: 2}])
585
+ b[:y] = proc{|row| row[:x] * 2}
586
+ _(a.eql?(b)).must_equal true
587
+ end
588
+
589
+ it "is false when formula names differ" do
590
+ a = Namo.new([{x: 1}, {x: 2}])
591
+ a[:doubled] = proc{|row| row[:x] * 2}
592
+ b = Namo.new([{x: 1}, {x: 2}])
593
+ b[:tripled] = proc{|row| row[:x] * 3}
594
+ _(a.eql?(b)).must_equal false
595
+ end
596
+
597
+ it "is false across different classes" do
598
+ subclass = Class.new(Namo)
599
+ a = Namo.new([{x: 1}, {x: 2}])
600
+ b = subclass.new([{x: 1}, {x: 2}])
601
+ _(a.eql?(b)).must_equal false
602
+ end
603
+ end
604
+
605
+ describe "#hash" do
606
+ it "is equal for set-equal Namos" do
607
+ a = Namo.new([{x: 1}, {x: 2}])
608
+ b = Namo.new([{x: 2}, {x: 1}])
609
+ _(a.hash).must_equal b.hash
610
+ end
611
+
612
+ it "differs when formula names differ" do
613
+ a = Namo.new([{x: 1}, {x: 2}])
614
+ b = Namo.new([{x: 1}, {x: 2}])
615
+ b[:y] = proc{|row| row[:x] * 2}
616
+ _(a.hash).wont_equal b.hash
617
+ end
618
+
619
+ it "is equal when formula names match, regardless of proc identity" do
620
+ a = Namo.new([{x: 1}, {x: 2}])
621
+ a[:y] = proc{|row| row[:x] * 2}
622
+ b = Namo.new([{x: 1}, {x: 2}])
623
+ b[:y] = proc{|row| row[:x] * 2}
624
+ _(a.hash).must_equal b.hash
625
+ end
626
+
627
+ it "differs across classes" do
628
+ subclass = Class.new(Namo)
629
+ a = Namo.new([{x: 1}, {x: 2}])
630
+ b = subclass.new([{x: 1}, {x: 2}])
631
+ _(a.hash).wont_equal b.hash
632
+ end
633
+
634
+ it "makes Namos usable as Hash keys" do
635
+ a = Namo.new([{x: 1}, {x: 2}])
636
+ b = Namo.new([{x: 2}, {x: 1}])
637
+ h = {a => 'first'}
638
+ _(h[b]).must_equal 'first'
639
+ end
640
+ end
641
+
642
+ describe "#<, #<=, #>, #>=" do
643
+ let(:small) { Namo.new([{x: 1}, {x: 2}]) }
644
+ let(:large) { Namo.new([{x: 1}, {x: 2}, {x: 3}]) }
645
+ let(:disjoint) { Namo.new([{x: 4}, {x: 5}]) }
646
+
647
+ it "recognises proper subset" do
648
+ _(small < large).must_equal true
649
+ _(small <= large).must_equal true
650
+ _(large > small).must_equal true
651
+ _(large >= small).must_equal true
652
+ end
653
+
654
+ it "treats equal sets as <= and >= but not < or >" do
655
+ copy = Namo.new([{x: 2}, {x: 1}])
656
+ _(small <= copy).must_equal true
657
+ _(small >= copy).must_equal true
658
+ _(small < copy).must_equal false
659
+ _(small > copy).must_equal false
660
+ end
661
+
662
+ it "treats disjoint sets as neither subset nor superset" do
663
+ _(small <= disjoint).must_equal false
664
+ _(small >= disjoint).must_equal false
665
+ _(small < disjoint).must_equal false
666
+ _(small > disjoint).must_equal false
667
+ end
668
+
669
+ it "is multiset-aware: a single row is a proper subset of two of the same row" do
670
+ one = Namo.new([{x: 1}])
671
+ two = Namo.new([{x: 1}, {x: 1}])
672
+ _(one < two).must_equal true
673
+ _(one <= two).must_equal true
674
+ _(two <= one).must_equal false
675
+ _(two < one).must_equal false
676
+ end
677
+
678
+ it "raises ArgumentError on mismatched dimensions" do
679
+ other = Namo.new([{y: 1}])
680
+ _ { small < other }.must_raise ArgumentError
681
+ _ { small <= other }.must_raise ArgumentError
682
+ _ { small > other }.must_raise ArgumentError
683
+ _ { small >= other }.must_raise ArgumentError
684
+ end
685
+
686
+ it "raises TypeError on non-Namo" do
687
+ _ { small < [{x: 1}] }.must_raise TypeError
688
+ _ { small <= 'string' }.must_raise TypeError
689
+ _ { small > nil }.must_raise TypeError
690
+ _ { small >= 42 }.must_raise TypeError
691
+ end
692
+ end
693
+
694
+ describe "#equal?" do
695
+ it "is false for distinct objects" do
696
+ a = Namo.new([{x: 1}])
697
+ b = Namo.new([{x: 1}])
698
+ _(a.equal?(b)).must_equal false
699
+ end
700
+
701
+ it "is true for the same object" do
702
+ a = Namo.new([{x: 1}])
703
+ _(a.equal?(a)).must_equal true
704
+ end
705
+ end
706
+
707
+ describe "dimension-mismatch error message" do
708
+ it "names both dimension lists" do
709
+ a = Namo.new([{x: 1}])
710
+ b = Namo.new([{y: 1}])
711
+ err = _ { a + b }.must_raise ArgumentError
712
+ _(err.message).must_match(/dimensions don't match/)
713
+ _(err.message).must_match(/\[:x\]/)
714
+ _(err.message).must_match(/\[:y\]/)
715
+ end
716
+ end
717
+
718
+ describe "non-Namo comparison error message" do
719
+ it "names the offending class" do
720
+ a = Namo.new([{x: 1}])
721
+ err = _ { a < 'string' }.must_raise TypeError
722
+ _(err.message).must_match(/can't compare Namo with/)
723
+ _(err.message).must_match(/String/)
724
+ end
725
+ end
726
+
727
+ describe "non-Namo set operation error message" do
728
+ it "raises TypeError on non-Namo for #+, #-, #&, #|, #^" do
729
+ a = Namo.new([{x: 1}])
730
+ _ { a + [{x: 1}] }.must_raise TypeError
731
+ _ { a - 'string' }.must_raise TypeError
732
+ _ { a & nil }.must_raise TypeError
733
+ _ { a | 42 }.must_raise TypeError
734
+ _ { a ^ :symbol }.must_raise TypeError
735
+ end
736
+
737
+ it "names the offending class" do
738
+ a = Namo.new([{x: 1}])
739
+ err = _ { a + 'string' }.must_raise TypeError
740
+ _(err.message).must_match(/can't compare Namo with/)
741
+ _(err.message).must_match(/String/)
742
+ end
743
+ end
744
+
469
745
  describe "#to_a" do
470
746
  it "returns the data as an array of hashes" do
471
747
  _(sales.to_a).must_equal sample_data
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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran
@@ -72,8 +72,8 @@ files:
72
72
  - namo.gemspec
73
73
  - test/Namo/NegatedDimension_test.rb
74
74
  - test/Namo/Row_test.rb
75
- - test/Namo_test.rb
76
75
  - test/Symbol_test.rb
76
+ - test/namo_test.rb
77
77
  homepage: https://github.com/thoran/namo
78
78
  licenses:
79
79
  - MIT
@@ -92,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
92
92
  - !ruby/object:Gem::Version
93
93
  version: '0'
94
94
  requirements: []
95
- rubygems_version: 4.0.10
95
+ rubygems_version: 4.0.11
96
96
  specification_version: 4
97
97
  summary: Named dimensional data for Ruby.
98
98
  test_files: []