diff --git a/src/__tests__/scale.spec.ts b/src/__tests__/scale.spec.ts index 69f7d18..3b81a36 100644 --- a/src/__tests__/scale.spec.ts +++ b/src/__tests__/scale.spec.ts @@ -95,6 +95,19 @@ describe('Scale', () => { expect(scale.getFrequency(3)).toBeCloseTo(1875); expect(scale.getFrequency(4)).toBeCloseTo(2000); }); + it('can be sorted (subunity lines)', () => { + const intervals = [ + new Interval(ExtendedMonzo.fromFraction(new Fraction(2, 3), 3), 'ratio'), + new Interval(ExtendedMonzo.fromFraction(new Fraction(3, 2), 3), 'ratio'), + new Interval(ExtendedMonzo.fromFraction(new Fraction(5, 3), 3), 'ratio'), + new Interval(ExtendedMonzo.fromFraction(new Fraction(2, 1), 3), 'ratio'), + new Interval(ExtendedMonzo.fromFraction(new Fraction(5, 2), 3), 'ratio'), + new Interval(ExtendedMonzo.fromFraction(new Fraction(9, 5), 3), 'ratio'), + ]; + const baseFrequency = 1000; + const scale = Scale.fromIntervalArray(intervals, baseFrequency).sorted(); + expect(scale.getFrequency(0)).toBeCloseTo(baseFrequency); + }); it('can be octave reduced', () => { const intervals = [ new Interval(ExtendedMonzo.fromFraction(new Fraction(3, 5), 3), 'ratio'), diff --git a/src/scale.ts b/src/scale.ts index d342417..459f187 100644 --- a/src/scale.ts +++ b/src/scale.ts @@ -670,10 +670,27 @@ export class Scale { /** * Sort the scale in-place. + * @param preserveUnity Keep the first entry in place and verify that it's 0 cents. * @returns The scale with intervals sorted from smallest to largest without touching the equave. */ - sortInPlace() { + sortInPlace(preserveUnity = true) { + let unity: Interval | undefined; + if (preserveUnity) { + unity = this.intervals.shift(); + if (!unity) { + // Seems like an empty scale. + return this; + } + if (unity.totalCents()) { + throw new Error( + 'Unrooted scales cannot be sorted while preserving unity.' + ); + } + } this.intervals.sort((a, b) => a.compare(b)); + if (unity) { + this.intervals.unshift(unity); + } return this; } @@ -689,16 +706,33 @@ export class Scale { /** * Construct a sorted copy of the scale. scale in-place. * @param deep Create new copies of the intervals instead just a new array with the old instances. + * @param preserveUnity Keep the first entry in place and verify that it's 0 cents. * @returns The scale with intervals sorted from smallest to largest without touching the equave. */ - sorted(deep = false) { + sorted(deep = false, preserveUnity = true) { let intervals: Interval[]; if (deep) { intervals = this.intervals.map(interval => interval.clone()); } else { intervals = [...this.intervals]; } + let unity: Interval | undefined; + if (preserveUnity) { + unity = intervals.shift(); + if (!unity) { + // Seems like an empty scale. + return this.variant(intervals); + } + if (unity.totalCents()) { + throw new Error( + 'Unrooted scales cannot be sorted while preserving unity.' + ); + } + } intervals.sort((a, b) => a.compare(b)); + if (unity) { + intervals.unshift(unity); + } return this.variant(intervals); } @@ -939,7 +973,7 @@ export class Scale { * @returns A new scale with intervals from both without duplicates. */ merge(other: Scale) { - return this.concat(other).removeDuplicatesInPlace().sortInPlace(); + return this.concat(other).removeDuplicatesInPlace().sortInPlace(false); } /**