From a237cf5c745c13b428196b88de0d8fb793ddadec Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Mon, 13 May 2024 11:33:53 +0300 Subject: [PATCH] Finetune .swi format ref #236 --- src/__tests__/interval.spec.ts | 88 +++++++++++++++++++++++++++- src/cli.ts | 2 +- src/interval.ts | 40 ++++++++++++- src/monzo.ts | 101 ++++++++++++++++++++++++++++++++- src/utils.ts | 2 + 5 files changed, 226 insertions(+), 7 deletions(-) diff --git a/src/__tests__/interval.spec.ts b/src/__tests__/interval.spec.ts index 3fa0b266..52d6aed8 100644 --- a/src/__tests__/interval.spec.ts +++ b/src/__tests__/interval.spec.ts @@ -1,5 +1,5 @@ import {describe, it, expect} from 'vitest'; -import {TimeMonzo} from '../monzo'; +import {TimeMonzo, TimeReal} from '../monzo'; import {Interval, intervalValueAs} from '../interval'; import {FractionLiteral, NedjiLiteral} from '../expression'; import {sw} from '../parser'; @@ -44,6 +44,92 @@ describe('Idempontent formatting', () => { }); }); +describe('Interchange format', () => { + it('uses plain monzos up to 23-limit', () => { + const interval = new Interval(TimeMonzo.fromFraction('23/16'), 'linear'); + interval.node = interval.asMonzoLiteral(true); + expect(interval.toString()).toBe('[-4 0 0 0 0 0 0 0 1>'); + }); + + it('uses plain monzos up to 23-limit (poor internal value)', () => { + const interval = new Interval(TimeMonzo.fromFraction('5/4', 2), 'linear'); + interval.node = interval.asMonzoLiteral(true); + expect(interval.toString()).toBe('[-2 0 1>'); + }); + + it('switches to subgroup monzos for 29-limit', () => { + const interval = new Interval( + TimeMonzo.fromFraction('29/16', 20), + 'logarithmic' + ); + interval.node = interval.asMonzoLiteral(true); + expect(interval.toString()).toBe('[-4 1>@2.29'); + }); + + it('uses explicit basis with the absolute echelon', () => { + const interval = new Interval( + TimeMonzo.fromFractionalFrequency(440), + 'linear' + ); + interval.node = interval.asMonzoLiteral(true); + expect(interval.toString()).toBe('[1 3 1 1>@Hz.2.5.11'); + }); + + it('uses explicit basis with steps', () => { + const interval = new Interval( + TimeMonzo.fromFraction('5/3'), + 'logarithmic', + -3 + ); + interval.node = interval.asMonzoLiteral(true); + expect(interval.toString()).toBe('[-3 -1 1>@1°.3.5'); + }); + + it('uses increasing non-fractional basis', () => { + const interval = new Interval(TimeMonzo.fromFraction('103/101'), 'linear'); + interval.node = interval.asMonzoLiteral(true); + expect(interval.toString()).toBe('[-1 1>@101.103'); + }); + + it('has an expression for rational unity', () => { + const interval = new Interval(TimeMonzo.fromFraction(1), 'linear'); + interval.node = interval.asMonzoLiteral(true); + expect(interval.toString()).toBe('[>'); + }); + + it('has an expression for rational zero', () => { + const interval = new Interval(TimeMonzo.fromFraction(0), 'linear'); + interval.node = interval.asMonzoLiteral(true); + expect(interval.toString()).toBe('[1>@0'); + }); + + it('has an expression for rational -2', () => { + const interval = new Interval(TimeMonzo.fromFraction(-2), 'linear'); + interval.node = interval.asMonzoLiteral(true); + expect(interval.toString()).toBe('[1 1>@-1.2'); + }); + + it('has an expression for real unity', () => { + const interval = new Interval(TimeReal.fromValue(1), 'linear'); + interval.node = interval.asMonzoLiteral(true); + expect(interval.toString()).toBe('[0.>@rc'); + }); + + // Real zero skipped + + it('has an expression for real -2', () => { + const interval = new Interval(TimeReal.fromValue(-2), 'linear'); + interval.node = interval.asMonzoLiteral(true); + expect(interval.toString()).toBe('[1 1200.>@-1.rc'); + }); + + it('has an expression for real 256Hz', () => { + const interval = new Interval(TimeReal.fromFrequency(256), 'linear'); + interval.node = interval.asMonzoLiteral(true); + expect(interval.toString()).toBe('[1. 9600.>@Hz.rc'); + }); +}); + const SERIALIZED = '["hello",{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":2,"d":1},{"n":1,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},"domain":"linear","steps":0,"label":"","node":{"type":"IntegerLiteral","value":"12"},"trackingIds":[]},12,{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":1,"d":1},{"n":1,"d":1},{"n":-1,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},"domain":"linear","steps":0,"label":"","node":{"type":"DecimalLiteral","sign":"","whole":"1","fractional":"2","exponent":null,"flavor":"e"},"trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":0,"d":1},{"n":-1,"d":1},{"n":1,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},"domain":"linear","steps":0,"label":"","node":{"type":"FractionLiteral","numerator":"5","denominator":"3"},"trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":1,"d":3},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},"domain":"linear","steps":0,"label":"","node":{"type":"RadicalLiteral","argument":{"n":2,"d":1},"exponent":{"n":1,"d":3}},"trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":46797,"d":80000}],"residual":{"n":1,"d":1}},"domain":"logarithmic","steps":0,"label":"","node":{"type":"CentsLiteral","sign":"","whole":"701","fractional":"955","exponent":null,"real":false},"trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":-4,"d":1},{"n":4,"d":1},{"n":-1,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},"domain":"logarithmic","steps":0,"label":"","node":{"type":"SquareSuperparticular","start":"9","end":null},"trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[],"residual":{"n":1,"d":1}},"domain":"logarithmic","steps":3,"label":"","node":{"type":"StepLiteral","count":3},"trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":0,"d":1},{"n":6,"d":13},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},"domain":"logarithmic","steps":0,"label":"","node":{"type":"NedjiLiteral","numerator":6,"denominator":13,"equaveNumerator":3,"equaveDenominator":null},"trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":-2,"d":1},{"n":0,"d":1},{"n":1,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},"domain":"logarithmic","steps":0,"label":"","node":{"ups":0,"lifts":0,"type":"FJS","pythagorean":{"type":"Pythagorean","quality":{"fraction":"","quality":"M"},"degree":{"negative":false,"base":3,"octaves":0,"imperfect":true}},"superscripts":[[5,""]],"subscripts":[]},"trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":1,"d":1},{"n":-2,"d":1},{"n":0,"d":1},{"n":1,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},"domain":"logarithmic","steps":0,"label":"","node":{"ups":0,"lifts":0,"type":"AbsoluteFJS","pitch":{"type":"AbsolutePitch","nominal":"A","accidentals":[{"fraction":"","accidental":"b"}],"octave":4},"superscripts":[[7,""]],"subscripts":[]},"trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":-1,"d":1},"primeExponents":[{"n":0,"d":1},{"n":1,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":37,"d":1}},"domain":"linear","steps":0,"label":"","trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":1,"d":1},"primeExponents":[{"n":-3,"d":1},{"n":1,"d":1},{"n":-3,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},"domain":"linear","steps":0,"label":"","trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":-3,"d":1},{"n":1,"d":1},{"n":1,"d":1}],"residual":{"n":1,"d":1}},"domain":"logarithmic","steps":0,"label":"","node":{"ups":0,"lifts":0,"type":"MonzoLiteral","components":[{"sign":"-","left":3,"separator":"","right":"","exponent":null},{"sign":"","left":1,"separator":"","right":"","exponent":null},{"sign":"","left":1,"separator":"","right":"","exponent":null}],"basis":[]},"trackingIds":[]}]'; diff --git a/src/cli.ts b/src/cli.ts index dabd5b85..4b191793 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -76,7 +76,7 @@ export function toSonicWeaveInterchange(source: string) { } for (const interval of visitor.currentScale) { const universal = interval.shallowClone(); - universal.node = universal.asMonzoLiteral(); + universal.node = universal.asMonzoLiteral(true); let line = universal.toString(context); if (line.startsWith('(') && line.endsWith(')')) { line = line.slice(1, -1); diff --git a/src/interval.ts b/src/interval.ts index abed7570..67833f06 100644 --- a/src/interval.ts +++ b/src/interval.ts @@ -31,7 +31,13 @@ import { import {TimeMonzo, TimeReal} from './monzo'; import {asAbsoluteFJS, asFJS} from './fjs'; import {type RootContext} from './context'; -import {ONE, ZERO, countUpsAndLifts, setUnion} from './utils'; +import { + NUM_INTERCHANGE_COMPONENTS, + ONE, + ZERO, + countUpsAndLifts, + setUnion, +} from './utils'; import {Fraction, FractionValue} from 'xen-dev-utils'; /** @@ -992,6 +998,10 @@ export class Interval { ); } + /** + * Return `true` if this interval is only composed of abstract edosteps. + * @returns `true` if the interval is a unit scalar, possibly with edosteps, `false` otherwise. + */ isPureSteps() { return this.value.isScalar() && this.value.isUnity(); } @@ -1063,8 +1073,32 @@ export class Interval { return this.node; } - asMonzoLiteral(): MonzoLiteral { - const node = this.value.asMonzoLiteral(); + /** + * Convert the interval to a virtual AST node representing the universal type. + * @param interchange Boolean flag to format everything explicitly. + * @returns A virtual monzo literal. + */ + asMonzoLiteral(interchange = false): MonzoLiteral { + let node: MonzoLiteral; + if ( + interchange && + this.value instanceof TimeMonzo && + !this.value.residual.isUnity() + ) { + const clone = this.value.clone(); + clone.numberOfComponents = NUM_INTERCHANGE_COMPONENTS; + node = clone.asMonzoLiteral(); + } else { + node = this.value.asMonzoLiteral(); + } + if ( + interchange && + (node.basis.length || + node.components.length > NUM_INTERCHANGE_COMPONENTS || + this.steps) + ) { + node = this.value.asInterchangeLiteral(); + } if (this.steps) { if (!node.basis.length && node.components.length) { node.basis.push({numerator: 2, denominator: null, radical: false}); diff --git a/src/monzo.ts b/src/monzo.ts index b59b81c1..8fc1656f 100644 --- a/src/monzo.ts +++ b/src/monzo.ts @@ -22,6 +22,7 @@ import { FRACTION_PRIMES, HALF, NEGATIVE_ONE, + NUM_INTERCHANGE_COMPONENTS, ONE, TWO, ZERO, @@ -59,7 +60,7 @@ export type EqualTemperament = { equave: Fraction; }; -let NUMBER_OF_COMPONENTS = 9; // Primes 2, 3, 5, 7, 11, 13, 17, 19 and 23 +let NUMBER_OF_COMPONENTS = NUM_INTERCHANGE_COMPONENTS; // Primes 2, 3, 5, 7, 11, 13, 17, 19 and 23 /** * Set the default number of components in the vector part of time monzos. @@ -741,6 +742,49 @@ export class TimeReal { return {type: 'MonzoLiteral', components, ups: 0, lifts: 0, basis}; } + /** + * Obtain an AST node representing the time monzo as a monzo literal suitable for interchange between programs. + * @returns Monzo literal. + */ + asInterchangeLiteral(): MonzoLiteral { + const components: VectorComponent[] = []; + const basis: BasisElement[] = []; + if (this.timeExponent) { + basis.push('Hz'); + const {sign, whole, fractional, exponent} = numberToDecimalLiteral( + -this.timeExponent, + 'r' + ); + components.push({ + sign, + left: Number(whole), + separator: '.', + right: fractional, + exponent, + }); + } + if (this.value < 0) { + basis.push({numerator: -1, denominator: null, radical: false}); + components.push({sign: '', left: 1, right: '', exponent: null}); + } else if (this.value === 0) { + basis.push({numerator: 0, denominator: null, radical: false}); + components.push({sign: '', left: 1, right: '', exponent: null}); + } + basis.push('rc'); + const {sign, whole, fractional, exponent} = numberToDecimalLiteral( + this.totalCents(true), + 'r' + ); + components.push({ + sign, + left: Number(whole), + separator: '.', + right: fractional, + exponent, + }); + return {type: 'MonzoLiteral', components, ups: 0, lifts: 0, basis}; + } + /** * Faithful string representation of the time real. * @param domain Domain of representation. @@ -2612,8 +2656,61 @@ export class TimeMonzo { return {type: 'MonzoLiteral', components, ups: 0, lifts: 0, basis}; } + /** + * Obtain an AST node representing the time monzo as a monzo literal suitable for interchange between programs. + * @returns Monzo literal. + */ + asInterchangeLiteral(): MonzoLiteral { + const components: VectorComponent[] = []; + const basis: BasisElement[] = []; + if (this.timeExponent.n) { + basis.push('Hz'); + components.push( + fractionToVectorComponent(this.timeExponent.inverse().neg()) + ); + } + if (this.residual.s < 0) { + basis.push({numerator: -1, denominator: null, radical: false}); + components.push({sign: '', left: 1, right: '', exponent: null}); + } else if (!this.residual.s) { + basis.push({numerator: 0, denominator: null, radical: false}); + components.push({sign: '', left: 1, right: '', exponent: null}); + } + for (let i = 0; i < this.primeExponents.length; ++i) { + const component = this.primeExponents[i]; + if (component.n) { + basis.push({numerator: PRIMES[i], denominator: null, radical: false}); + components.push(fractionToVectorComponent(component)); + } + } + if (this.residual.n > 1) { + basis.push({ + numerator: this.residual.n, + denominator: null, + radical: false, + }); + components.push({sign: '', left: 1, right: '', exponent: null}); + } + if (this.residual.d > 1) { + basis.push({ + numerator: this.residual.d, + denominator: null, + radical: false, + }); + components.push({sign: '-', left: 1, right: '', exponent: null}); + if (this.residual.n > this.residual.d) { + basis.push(basis.pop()!, basis.pop()!); + components.push(components.pop()!, components.pop()!); + } + } + return {type: 'MonzoLiteral', components, ups: 0, lifts: 0, basis}; + } + + /** + * Obtain an AST node representing the time monzo as a val literal. + * @returns Val literal. + */ asValLiteral(): ValLiteral { - // TODO: Check that the basis is legal. return {...this.asMonzoLiteral(false), type: 'ValLiteral'} as ValLiteral; } diff --git a/src/utils.ts b/src/utils.ts index cc8ad4f4..876f9349 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,6 +2,8 @@ import {Fraction, PRIMES} from 'xen-dev-utils'; +export const NUM_INTERCHANGE_COMPONENTS = 9; + export function F(n: number, d?: number) { return Object.freeze(new Fraction(n, d)); }