tuning-core 0.1.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.
@@ -0,0 +1,728 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import Fraction from 'fraction.js';
3
+ import { IntervalSet } from '../classes';
4
+
5
+ describe('IntervalSet', () => {
6
+ describe('constructor', () => {
7
+ test('creates empty set with no arguments', () => {
8
+ const set = new IntervalSet();
9
+ expect(set.size).toBe(0);
10
+ expect(set.isEmpty()).toBe(true);
11
+ });
12
+
13
+ test('creates set from array of Fractions', () => {
14
+ const set = new IntervalSet([
15
+ new Fraction(1),
16
+ new Fraction(3, 2),
17
+ new Fraction(2),
18
+ ]);
19
+ expect(set.size).toBe(3);
20
+ });
21
+
22
+ test('creates set from array of numbers', () => {
23
+ const set = new IntervalSet([1, 1.5, 2]);
24
+ expect(set.size).toBe(3);
25
+ });
26
+
27
+ test('creates set from array of strings', () => {
28
+ const set = new IntervalSet(['1', '3/2', '2']);
29
+ expect(set.size).toBe(3);
30
+ });
31
+
32
+ test('creates set from mixed types', () => {
33
+ const set = new IntervalSet([1, '3/2', new Fraction(2)]);
34
+ expect(set.size).toBe(3);
35
+ });
36
+
37
+ test('handles duplicate ratios in constructor', () => {
38
+ const set = new IntervalSet([1, '3/2', 1.5, new Fraction(3, 2)]);
39
+ expect(set.size).toBe(2); // 1 and 3/2 (1.5 and Fraction(3,2) are duplicates)
40
+ });
41
+
42
+ test('simplifies fractions automatically', () => {
43
+ const set = new IntervalSet(['6/4', '3/2']);
44
+ expect(set.size).toBe(1); // Both simplify to 3/2
45
+ });
46
+ });
47
+
48
+ describe('add', () => {
49
+ test('adds Fraction to set', () => {
50
+ const set = new IntervalSet();
51
+ set.add(new Fraction(3, 2));
52
+ expect(set.size).toBe(1);
53
+ expect(set.has('3/2')).toBe(true);
54
+ });
55
+
56
+ test('adds number to set', () => {
57
+ const set = new IntervalSet();
58
+ set.add(1.5);
59
+ expect(set.size).toBe(1);
60
+ expect(set.has('3/2')).toBe(true);
61
+ });
62
+
63
+ test('adds string to set', () => {
64
+ const set = new IntervalSet();
65
+ set.add('5/4');
66
+ expect(set.size).toBe(1);
67
+ expect(set.has('5/4')).toBe(true);
68
+ });
69
+
70
+ test('replaces existing ratio', () => {
71
+ const set = new IntervalSet(['3/2']);
72
+ set.add(1.5); // Same ratio, different representation
73
+ expect(set.size).toBe(1);
74
+ });
75
+
76
+ test('adds multiple distinct ratios', () => {
77
+ const set = new IntervalSet();
78
+ set.add(1);
79
+ set.add('5/4');
80
+ set.add('3/2');
81
+ expect(set.size).toBe(3);
82
+ });
83
+ });
84
+
85
+ describe('has', () => {
86
+ test('returns true for existing ratio (Fraction)', () => {
87
+ const set = new IntervalSet(['3/2']);
88
+ expect(set.has(new Fraction(3, 2))).toBe(true);
89
+ });
90
+
91
+ test('returns true for existing ratio (number)', () => {
92
+ const set = new IntervalSet(['3/2']);
93
+ expect(set.has(1.5)).toBe(true);
94
+ });
95
+
96
+ test('returns true for existing ratio (string)', () => {
97
+ const set = new IntervalSet([1.5]);
98
+ expect(set.has('3/2')).toBe(true);
99
+ });
100
+
101
+ test('returns false for non-existent ratio', () => {
102
+ const set = new IntervalSet([1, 2]);
103
+ expect(set.has('3/2')).toBe(false);
104
+ });
105
+
106
+ test('handles equivalent fractions', () => {
107
+ const set = new IntervalSet(['3/2']);
108
+ expect(set.has('6/4')).toBe(true);
109
+ });
110
+ });
111
+
112
+ describe('delete', () => {
113
+ test('removes existing ratio', () => {
114
+ const set = new IntervalSet([1, '3/2', 2]);
115
+ const result = set.delete('3/2');
116
+ expect(result).toBe(set); // Returns this for chaining
117
+ expect(set.size).toBe(2);
118
+ expect(set.has('3/2')).toBe(false);
119
+ });
120
+
121
+ test('returns this for chaining', () => {
122
+ const set = new IntervalSet([1, 2]);
123
+ const result = set.delete('3/2');
124
+ expect(result).toBe(set);
125
+ expect(set.size).toBe(2);
126
+ });
127
+
128
+ test('handles equivalent fractions', () => {
129
+ const set = new IntervalSet(['3/2']);
130
+ set.delete('6/4');
131
+ expect(set.has('3/2')).toBe(false);
132
+ });
133
+ });
134
+
135
+ describe('get', () => {
136
+ test('retrieves existing ratio', () => {
137
+ const set = new IntervalSet(['3/2']);
138
+ const ratio = set.get('3/2');
139
+ expect(ratio).toBeDefined();
140
+ expect(ratio?.toFraction()).toBe('3/2');
141
+ });
142
+
143
+ test('returns undefined for non-existent ratio', () => {
144
+ const set = new IntervalSet([1]);
145
+ const ratio = set.get('3/2');
146
+ expect(ratio).toBeUndefined();
147
+ });
148
+
149
+ test('handles equivalent fractions', () => {
150
+ const set = new IntervalSet(['3/2']);
151
+ const ratio = set.get(1.5);
152
+ expect(ratio?.toFraction()).toBe('3/2');
153
+ });
154
+ });
155
+
156
+ describe('size and isEmpty', () => {
157
+ test('size returns correct count', () => {
158
+ const set = new IntervalSet();
159
+ expect(set.size).toBe(0);
160
+ set.add(1);
161
+ expect(set.size).toBe(1);
162
+ set.add(2);
163
+ expect(set.size).toBe(2);
164
+ });
165
+
166
+ test('isEmpty returns true for empty set', () => {
167
+ const set = new IntervalSet();
168
+ expect(set.isEmpty()).toBe(true);
169
+ });
170
+
171
+ test('isEmpty returns false for non-empty set', () => {
172
+ const set = new IntervalSet([1]);
173
+ expect(set.isEmpty()).toBe(false);
174
+ });
175
+ });
176
+
177
+ describe('getRatios', () => {
178
+ test('returns all ratios as array', () => {
179
+ const set = new IntervalSet([1, '3/2', 2]);
180
+ const ratios = set.getRatios();
181
+ expect(ratios.length).toBe(3);
182
+ expect(ratios.every(r => r instanceof Fraction)).toBe(true);
183
+ });
184
+
185
+ test('returns empty array for empty set', () => {
186
+ const set = new IntervalSet();
187
+ expect(set.getRatios()).toEqual([]);
188
+ });
189
+
190
+ test('returns ratios sorted in ascending order', () => {
191
+ const set = new IntervalSet([2, '5/4', '3/2', 1]);
192
+ const sorted = set.getRatios();
193
+
194
+ expect(sorted[0]?.toFraction()).toBe('1');
195
+ expect(sorted[1]?.toFraction()).toBe('5/4');
196
+ expect(sorted[2]?.toFraction()).toBe('3/2');
197
+ expect(sorted[3]?.toFraction()).toBe('2');
198
+ });
199
+
200
+ test('handles single ratio', () => {
201
+ const set = new IntervalSet([1]);
202
+ const sorted = set.getRatios();
203
+ expect(sorted.length).toBe(1);
204
+ });
205
+
206
+ test('returns empty array for empty set', () => {
207
+ const set = new IntervalSet();
208
+ expect(set.getRatios()).toEqual([]);
209
+ });
210
+
211
+ test('returns ratio at index in sorted order', () => {
212
+ const set = new IntervalSet([2, 1, '3/2']);
213
+ const ratios = set.getRatios();
214
+ expect(ratios[0]?.toFraction()).toBe('1');
215
+ expect(ratios[1]?.toFraction()).toBe('3/2');
216
+ expect(ratios[2]?.toFraction()).toBe('2');
217
+ });
218
+
219
+ test('returns undefined for out of bounds index', () => {
220
+ const set = new IntervalSet([1, 2]);
221
+ const ratios = set.getRatios();
222
+ expect(ratios[5]).toBeUndefined();
223
+ expect(ratios[-1]).toBeUndefined();
224
+ });
225
+
226
+ test('returns undefined for empty set', () => {
227
+ const set = new IntervalSet();
228
+ const ratios = set.getRatios();
229
+ expect(ratios[0]).toBeUndefined();
230
+ });
231
+ });
232
+
233
+ describe('min and max', () => {
234
+ test('min returns smallest ratio', () => {
235
+ const set = new IntervalSet([2, '5/4', '3/2']);
236
+ const min = set.min();
237
+ expect(min?.toFraction()).toBe('5/4');
238
+ });
239
+
240
+ test('max returns largest ratio', () => {
241
+ const set = new IntervalSet(['5/4', 1, '3/2']);
242
+ const max = set.max();
243
+ expect(max?.toFraction()).toBe('3/2');
244
+ });
245
+
246
+ test('min returns undefined for empty set', () => {
247
+ const set = new IntervalSet();
248
+ expect(set.min()).toBeUndefined();
249
+ });
250
+
251
+ test('max returns undefined for empty set', () => {
252
+ const set = new IntervalSet();
253
+ expect(set.max()).toBeUndefined();
254
+ });
255
+
256
+ test('min and max work with single element', () => {
257
+ const set = new IntervalSet(['3/2']);
258
+ expect(set.min()?.toFraction()).toBe('3/2');
259
+ expect(set.max()?.toFraction()).toBe('3/2');
260
+ });
261
+ });
262
+
263
+ describe('clear', () => {
264
+ test('removes all ratios', () => {
265
+ const set = new IntervalSet([1, '3/2', 2]);
266
+ set.clear();
267
+ expect(set.size).toBe(0);
268
+ expect(set.isEmpty()).toBe(true);
269
+ });
270
+
271
+ test('works on empty set', () => {
272
+ const set = new IntervalSet();
273
+ expect(() => set.clear()).not.toThrow();
274
+ expect(set.size).toBe(0);
275
+ });
276
+ });
277
+
278
+ describe('clone', () => {
279
+ test('creates independent copy', () => {
280
+ const set1 = new IntervalSet([1, '3/2', 2]);
281
+ const set2 = set1.clone();
282
+
283
+ expect(set2.size).toBe(set1.size);
284
+ expect(set2.equals(set1)).toBe(true);
285
+ });
286
+
287
+ test('clone is independent from original', () => {
288
+ const set1 = new IntervalSet([1, '3/2']);
289
+ const set2 = set1.clone();
290
+
291
+ set2.add(2);
292
+
293
+ expect(set1.size).toBe(2);
294
+ expect(set2.size).toBe(3);
295
+ });
296
+
297
+ test('clones empty set', () => {
298
+ const set1 = new IntervalSet();
299
+ const set2 = set1.clone();
300
+ expect(set2.size).toBe(0);
301
+ });
302
+ });
303
+
304
+ describe('forEach', () => {
305
+ test('iterates over all ratios', () => {
306
+ const set = new IntervalSet([1, '3/2']);
307
+ const keys: string[] = [];
308
+
309
+ set.forEach((ratio: Fraction, key: string) => {
310
+ keys.push(key);
311
+ });
312
+
313
+ expect(keys.length).toBe(2);
314
+ expect(keys).toContain('1');
315
+ expect(keys).toContain('3/2');
316
+ });
317
+
318
+ test('provides Fraction objects in callback', () => {
319
+ const set = new IntervalSet(['5/4']);
320
+
321
+ set.forEach((ratio: Fraction) => {
322
+ expect(ratio).toBeInstanceOf(Fraction);
323
+ expect(ratio.toFraction()).toBe('5/4');
324
+ });
325
+ });
326
+ });
327
+
328
+ describe('toSpectrum', () => {
329
+ test('converts to spectrum with amplitude function', () => {
330
+ const set = new IntervalSet([1, '3/2', 2]);
331
+ const spectrum = set.toSpectrum((ratio: Fraction, index: number) => {
332
+ return 1 / (index + 1);
333
+ });
334
+
335
+ expect(spectrum.size).toBe(3);
336
+ expect(spectrum.get(1)?.amplitude).toBe(1);
337
+ expect(spectrum.get('3/2')?.amplitude).toBe(0.5);
338
+ expect(spectrum.get(2)?.amplitude).toBeCloseTo(0.333, 3);
339
+ });
340
+
341
+ test('converts to spectrum with constant amplitude', () => {
342
+ const set = new IntervalSet([1, 2]);
343
+ const spectrum = set.toSpectrum(() => 0.5);
344
+
345
+ expect(spectrum.get(1)?.amplitude).toBe(0.5);
346
+ expect(spectrum.get(2)?.amplitude).toBe(0.5);
347
+ });
348
+
349
+ test('converts ratios to frequencies in spectrum', () => {
350
+ const set = new IntervalSet([1, '3/2', 2]);
351
+ const spectrum = set.toSpectrum(() => 1);
352
+
353
+ const frequencies = spectrum.getFrequenciesAsNumbers();
354
+ expect(frequencies).toEqual([1, 1.5, 2]);
355
+ });
356
+
357
+ test('handles empty set', () => {
358
+ const set = new IntervalSet();
359
+ const spectrum = set.toSpectrum(() => 1);
360
+ expect(spectrum.size).toBe(0);
361
+ });
362
+ });
363
+
364
+ describe('toStrings', () => {
365
+ test('returns sorted array of fraction strings', () => {
366
+ const set = new IntervalSet([2, 1, '3/2']);
367
+ const strings = set.toStrings();
368
+
369
+ expect(strings).toEqual(['1', '3/2', '2']);
370
+ });
371
+
372
+ test('returns empty array for empty set', () => {
373
+ const set = new IntervalSet();
374
+ expect(set.toStrings()).toEqual([]);
375
+ });
376
+ });
377
+
378
+ describe('toNumbers', () => {
379
+ test('returns sorted array of decimal numbers', () => {
380
+ const set = new IntervalSet([2, 1, '3/2']);
381
+ const numbers = set.toNumbers();
382
+
383
+ expect(numbers).toEqual([1, 1.5, 2]);
384
+ });
385
+
386
+ test('returns empty array for empty set', () => {
387
+ const set = new IntervalSet();
388
+ expect(set.toNumbers()).toEqual([]);
389
+ });
390
+
391
+ test('converts complex fractions to decimals', () => {
392
+ const set = new IntervalSet(['5/4', '7/4']);
393
+ const numbers = set.toNumbers();
394
+
395
+ expect(numbers[0]).toBe(1.25);
396
+ expect(numbers[1]).toBe(1.75);
397
+ });
398
+ });
399
+
400
+ describe('equals', () => {
401
+ test('returns true for equal sets', () => {
402
+ const set1 = new IntervalSet([1, '3/2', 2]);
403
+ const set2 = new IntervalSet([1, '3/2', 2]);
404
+
405
+ expect(set1.equals(set2)).toBe(true);
406
+ });
407
+
408
+ test('returns true regardless of insertion order', () => {
409
+ const set1 = new IntervalSet([1, '3/2', 2]);
410
+ const set2 = new IntervalSet([2, 1, '3/2']);
411
+
412
+ expect(set1.equals(set2)).toBe(true);
413
+ });
414
+
415
+ test('returns false for sets with different sizes', () => {
416
+ const set1 = new IntervalSet([1, '3/2']);
417
+ const set2 = new IntervalSet([1, '3/2', 2]);
418
+
419
+ expect(set1.equals(set2)).toBe(false);
420
+ });
421
+
422
+ test('returns false for sets with different ratios', () => {
423
+ const set1 = new IntervalSet([1, '3/2']);
424
+ const set2 = new IntervalSet([1, '5/4']);
425
+
426
+ expect(set1.equals(set2)).toBe(false);
427
+ });
428
+
429
+ test('handles equivalent fractions', () => {
430
+ const set1 = new IntervalSet(['3/2']);
431
+ const set2 = new IntervalSet(['6/4']);
432
+
433
+ expect(set1.equals(set2)).toBe(true);
434
+ });
435
+
436
+ test('returns true for empty sets', () => {
437
+ const set1 = new IntervalSet();
438
+ const set2 = new IntervalSet();
439
+
440
+ expect(set1.equals(set2)).toBe(true);
441
+ });
442
+ });
443
+
444
+
445
+ describe('edge cases', () => {
446
+ test('handles very small ratios', () => {
447
+ const set = new IntervalSet(['1/1000']);
448
+ expect(set.has('1/1000')).toBe(true);
449
+ expect(set.getRatios()[0]?.valueOf()).toBe(0.001);
450
+ });
451
+
452
+ test('handles very large ratios', () => {
453
+ const set = new IntervalSet(['1000/1']);
454
+ expect(set.has(1000)).toBe(true);
455
+ });
456
+
457
+ test('handles many ratios', () => {
458
+ const ratios: number[] = [];
459
+ for (let i = 1; i <= 100; i++) {
460
+ ratios.push(i);
461
+ }
462
+ const set = new IntervalSet(ratios);
463
+ expect(set.size).toBe(100);
464
+ });
465
+
466
+ test('handles irrational approximations', () => {
467
+ const set = new IntervalSet([Math.PI]);
468
+ const ratio = set.getRatios()[0];
469
+ expect(ratio?.valueOf()).toBeCloseTo(Math.PI, 5);
470
+ });
471
+ });
472
+
473
+ describe('IntervalSet static mathods', () => {
474
+ describe('range', () => {
475
+ test('generates ratios between 1 and 2 with denominator limit', () => {
476
+ const set = IntervalSet.range(1, 2, 5);
477
+
478
+ expect(set.size).toBeGreaterThan(0);
479
+ expect(set.has(1)).toBe(true);
480
+ expect(set.has(2)).toBe(true);
481
+
482
+ // Should include simple ratios like 5/4, 4/3, 3/2
483
+ expect(set.has('5/4')).toBe(true);
484
+ expect(set.has('4/3')).toBe(true);
485
+ expect(set.has('3/2')).toBe(true);
486
+ });
487
+
488
+ test('respects minimum bound', () => {
489
+ const set = IntervalSet.range(1.5, 2, 10);
490
+ const ratios = set.toNumbers();
491
+
492
+ expect(ratios.every(r => r >= 1.5)).toBe(true);
493
+ expect(set.has(1)).toBe(false);
494
+ expect(set.has('5/4')).toBe(false);
495
+ });
496
+
497
+ test('respects maximum bound', () => {
498
+ const set = IntervalSet.range(1, 1.5, 10);
499
+ const ratios = set.toNumbers();
500
+
501
+ expect(ratios.every(r => r <= 1.5)).toBe(true);
502
+ expect(set.has(2)).toBe(false);
503
+ });
504
+
505
+ test('includes boundary values', () => {
506
+ const set = IntervalSet.range(1, 2, 5);
507
+
508
+ expect(set.has(1)).toBe(true);
509
+ expect(set.has(2)).toBe(true);
510
+ });
511
+
512
+ test('higher denominator limit produces more ratios', () => {
513
+ const set1 = IntervalSet.range(1, 2, 5);
514
+ const set2 = IntervalSet.range(1, 2, 10);
515
+
516
+ expect(set2.size).toBeGreaterThan(set1.size);
517
+ });
518
+
519
+ test('generates all ratios with denominator 1', () => {
520
+ const set = IntervalSet.range(1, 3, 1);
521
+
522
+ expect(set.size).toBe(3); // 1/1, 2/1, 3/1
523
+ expect(set.has(1)).toBe(true);
524
+ expect(set.has(2)).toBe(true);
525
+ expect(set.has(3)).toBe(true);
526
+ });
527
+
528
+ test('handles fractional bounds (Fraction)', () => {
529
+ const set = IntervalSet.range(new Fraction(1), new Fraction(2), 8);
530
+
531
+ expect(set.has(1)).toBe(true);
532
+ expect(set.has(2)).toBe(true);
533
+ expect(set.size).toBeGreaterThan(0);
534
+ });
535
+
536
+ test('handles fractional bounds (string)', () => {
537
+ const set = IntervalSet.range('1', '2', 8);
538
+
539
+ expect(set.has(1)).toBe(true);
540
+ expect(set.has(2)).toBe(true);
541
+ });
542
+
543
+ test('handles non-integer bounds', () => {
544
+ const set = IntervalSet.range('5/4', '3/2', 6);
545
+
546
+ expect(set.has('5/4')).toBe(true);
547
+ expect(set.has('3/2')).toBe(true);
548
+ expect(set.has(1)).toBe(false);
549
+ expect(set.has(2)).toBe(false);
550
+ });
551
+
552
+ test('generates single ratio when min equals max', () => {
553
+ const set = IntervalSet.range(1, 1, 10);
554
+
555
+ expect(set.size).toBe(1);
556
+ expect(set.has(1)).toBe(true);
557
+ });
558
+
559
+ test('throws error when min > max', () => {
560
+ expect(() => IntervalSet.range(2, 1, 5)).toThrow('min must be less than or equal to max');
561
+ });
562
+
563
+ test('throws error for maxDenominator < 1', () => {
564
+ expect(() => IntervalSet.range(1, 2, 0)).toThrow('maxDenominator must be at least 1');
565
+ expect(() => IntervalSet.range(1, 2, -5)).toThrow('maxDenominator must be at least 1');
566
+ });
567
+
568
+ test('automatically simplifies fractions', () => {
569
+ const set = IntervalSet.range(1, 2, 10);
570
+
571
+ // 6/4 should be simplified to 3/2, so only one entry
572
+ const count3_2 = set.getRatios().filter(r => r.toFraction() === '3/2').length;
573
+ expect(count3_2).toBe(1);
574
+ });
575
+
576
+ test.only('handles large denominator limits', () => {
577
+ const set = IntervalSet.range(1, 2, 100);
578
+
579
+ expect(set.size).toEqual(3045); // Many ratios
580
+ expect(set.has('199/100')).toBe(true);
581
+ expect(set.has('100/100')).toBe(true); // Simplifies to 1
582
+ });
583
+
584
+ test('generates correct ratios for octave with limit 5', () => {
585
+ const set = IntervalSet.range(1, 2, 5);
586
+
587
+ // Should include these common ratios
588
+ expect(set.has('6/5')).toBe(true); // Minor third
589
+ expect(set.has('5/4')).toBe(true); // Major third
590
+ expect(set.has('4/3')).toBe(true); // Perfect fourth
591
+ expect(set.has('3/2')).toBe(true); // Perfect fifth
592
+ expect(set.has('8/5')).toBe(true); // Minor sixth
593
+ expect(set.has('5/3')).toBe(true); // Major sixth
594
+ });
595
+
596
+ test('all generated ratios are within bounds', () => {
597
+ const set = IntervalSet.range(1, 2, 20);
598
+ const ratios = set.toNumbers();
599
+
600
+ expect(ratios.every(r => r >= 1 && r <= 2)).toBe(true);
601
+ });
602
+
603
+ test('handles very small ranges', () => {
604
+ const set = IntervalSet.range(1, '101/100', 100);
605
+
606
+ expect(set.size).toBeGreaterThan(0);
607
+ expect(set.min()?.compare(1)).toBeGreaterThanOrEqual(0);
608
+ expect(set.max()?.compare(new Fraction(101, 100))).toBeLessThanOrEqual(0);
609
+ });
610
+
611
+ test('generates ratios across multiple octaves', () => {
612
+ const set = IntervalSet.range(1, 4, 5);
613
+
614
+ expect(set.has(1)).toBe(true);
615
+ expect(set.has(2)).toBe(true);
616
+ expect(set.has(3)).toBe(true);
617
+ expect(set.has(4)).toBe(true);
618
+ });
619
+
620
+ test('fine divisions for 100-step octave', () => {
621
+ const set = IntervalSet.range(1, 2, 100);
622
+
623
+ // Should have many fine divisions
624
+ expect(set.size).toBeGreaterThan(100);
625
+
626
+ // Check it includes some specific cents-like divisions
627
+ expect(set.has('100/99')).toBe(true);
628
+ expect(set.has('99/98')).toBe(true);
629
+ });
630
+ });
631
+
632
+ describe('musical examples', () => {
633
+ test('generates 5-limit just intonation intervals', () => {
634
+ const set = IntervalSet.range(1, 2, 5);
635
+
636
+ // 5-limit intervals in one octave
637
+ expect(set.has('6/5')).toBe(true); // Minor third
638
+ expect(set.has('5/4')).toBe(true); // Major third
639
+ expect(set.has('4/3')).toBe(true); // Perfect fourth
640
+ expect(set.has('3/2')).toBe(true); // Perfect fifth
641
+ expect(set.has('8/5')).toBe(true); // Minor sixth
642
+ expect(set.has('5/3')).toBe(true); // Major sixth
643
+ expect(set.has('9/5')).toBe(true); // Minor seventh
644
+ });
645
+
646
+ test('generates 7-limit intervals', () => {
647
+ const set = IntervalSet.range(1, 2, 7);
648
+
649
+ expect(set.has('7/4')).toBe(true); // Harmonic seventh
650
+ expect(set.has('7/5')).toBe(true); // Tritone
651
+ expect(set.has('7/6')).toBe(true); // Septimal minor third
652
+ });
653
+ });
654
+
655
+ describe('edge cases and validation', () => {
656
+ test('handles maxDenominator of 1', () => {
657
+ const set = IntervalSet.range(1, 5, 1);
658
+
659
+ // Should only include integers
660
+ expect(set.toStrings()).toEqual(['1', '2', '3', '4', '5']);
661
+ });
662
+
663
+ test('handles very large ranges', () => {
664
+ const set = IntervalSet.range(0.25, 10, 5);
665
+
666
+ expect(set.size).toBeGreaterThan(10);
667
+ expect(set.has(0.25)).toBe(true);
668
+ expect(set.has(10)).toBe(true);
669
+ });
670
+
671
+ test('all ratios respect denominator limit', () => {
672
+ const maxDenom = 7;
673
+ const set = IntervalSet.range(1, 2, maxDenom);
674
+
675
+ // Check that all ratios have denominators <= maxDenom (after simplification)
676
+ set.forEach(ratio => {
677
+ expect(ratio.d).toBeLessThanOrEqual(maxDenom);
678
+ });
679
+ });
680
+
681
+ test('handles irrational bounds approximated as fractions', () => {
682
+ const set = IntervalSet.range(1, Math.PI, 10);
683
+
684
+ expect(set.size).toBeGreaterThan(0);
685
+ expect(set.max()?.valueOf()).toBeLessThanOrEqual(Math.PI);
686
+ });
687
+
688
+ test('consistent results for equivalent bound representations', () => {
689
+ const set1 = IntervalSet.range(1, 2, 8);
690
+ const set2 = IntervalSet.range('1', '2', 8);
691
+ const set3 = IntervalSet.range(new Fraction(1), new Fraction(2), 8);
692
+
693
+ expect(set1.equals(set2)).toBe(true);
694
+ expect(set2.equals(set3)).toBe(true);
695
+ });
696
+
697
+ test('handles bounds with complex fractions', () => {
698
+ const set = IntervalSet.range('7/6', '11/6', 12);
699
+
700
+ expect(set.has('7/6')).toBe(true);
701
+ expect(set.has('11/6')).toBe(true);
702
+ expect(set.min()?.toFraction()).toBe('7/6');
703
+ expect(set.max()?.toFraction()).toBe('11/6');
704
+ });
705
+ });
706
+
707
+ describe('performance considerations', () => {
708
+ test('handles reasonable large denominator limits efficiently', () => {
709
+ const startTime = Date.now();
710
+ const set = IntervalSet.range(1, 2, 100);
711
+ const endTime = Date.now();
712
+
713
+ expect(set.size).toBeGreaterThan(0);
714
+ expect(endTime - startTime).toBeLessThan(1000); // Should complete in under 1 second
715
+ });
716
+
717
+ test('deduplication works correctly', () => {
718
+ const set = IntervalSet.range(1, 2, 20);
719
+
720
+ // Count how many times we'd see 3/2 if we didn't deduplicate
721
+ // (e.g., 3/2, 6/4, 9/6, 12/8, 15/10, 18/12 all simplify to 3/2)
722
+ // But the set should only have one
723
+ const count = set.getRatios().filter(r => r.toFraction() === '3/2').length;
724
+ expect(count).toBe(1);
725
+ });
726
+ });
727
+ });
728
+ });