diff --git a/src/__tests__/util.spec.ts b/src/__tests__/util.spec.ts index 8a5a25ca..583195b0 100644 --- a/src/__tests__/util.spec.ts +++ b/src/__tests__/util.spec.ts @@ -5,7 +5,9 @@ import { formatExponential, formatHertz, gapKeyColors, + parseChordInput, } from "../utils"; +import { DEFAULT_NUMBER_OF_COMPONENTS } from "../constants"; function naiveExponential(x: number, fractionDigits = 3) { if (Math.abs(x) < 10000) { @@ -97,3 +99,23 @@ describe("Gap key color algorithm", () => { ); }); }); + +describe("Chord input parser", () => { + it("parses many types of intervals with many separators supported", () => { + const text = "3:2400.&11/3|1\\5;[-1,1> [0 0 1>-4/1"; + const intervals = parseChordInput(text); + expect(intervals[0].monzo.vector.length).toBe(DEFAULT_NUMBER_OF_COMPONENTS); + expect(intervals[0].type).toBe("ratio"); + expect(intervals[1].type).toBe("cents"); + expect(intervals[2].type).toBe("ratio"); + expect(intervals[3].type).toBe("equal temperament"); + expect(intervals[4].type).toBe("monzo"); + + expect(intervals[0].totalCents()).toBeCloseTo(1901.955); + expect(intervals[1].totalCents()).toBeCloseTo(2400); + expect(intervals[2].totalCents()).toBeCloseTo(2249.36); + expect(intervals[3].totalCents()).toBeCloseTo(240); + expect(intervals[4].totalCents()).toBeCloseTo(701.955); + expect(intervals[5].totalCents()).toBeCloseTo(386.31); + }); +}); diff --git a/src/components/modals/generation/EqualTemperament.vue b/src/components/modals/generation/EqualTemperament.vue index 4b0931fe..cb12f5f2 100644 --- a/src/components/modals/generation/EqualTemperament.vue +++ b/src/components/modals/generation/EqualTemperament.vue @@ -3,9 +3,9 @@ import { DEFAULT_NUMBER_OF_COMPONENTS } from "@/constants"; import { computed, ref, watch } from "vue"; import Modal from "@/components/ModalDialog.vue"; import ScaleLineInput from "@/components/ScaleLineInput.vue"; -import { splitText } from "@/components/modals/tempering-state"; import { clamp } from "xen-dev-utils"; import { ExtendedMonzo, Interval, Scale } from "scale-workshop-core"; +import { splitText } from "@/utils"; const props = defineProps<{ centsFractionDigits: number; diff --git a/src/components/modals/modification/TemperScale.vue b/src/components/modals/modification/TemperScale.vue index 4b460f73..ad8979dd 100644 --- a/src/components/modals/modification/TemperScale.vue +++ b/src/components/modals/modification/TemperScale.vue @@ -6,7 +6,7 @@ import { import { Mapping, stretchToEdo, toPrimeMapping } from "@/tempering"; import { computed, ref, watch } from "vue"; import Modal from "@/components/ModalDialog.vue"; -import { makeState, splitText } from "@/components/modals/tempering-state"; +import { makeState } from "@/components/modals/tempering-state"; import { add, Fraction, PRIME_CENTS } from "xen-dev-utils"; import { mapByVal, resolveMonzo, tenneyVals, vanishCommas } from "temperaments"; import { @@ -15,6 +15,7 @@ import { Scale, type IntervalOptions, } from "scale-workshop-core"; +import { splitText } from "@/utils"; const props = defineProps<{ scale: Scale; @@ -46,6 +47,7 @@ const constraintsString = state.constraintsString; // === Computed state === const vals = state.vals; +const rawCommas = state.rawCommas; const commas = state.commas; const subgroup = state.subgroup; const options = state.options; @@ -168,7 +170,7 @@ function modify() { options.value.temperEquaves && tempering.value !== "CTE"; const weights = options.value.weights; const jip = subgroup.value.jip(); - const commaMonzos = commas.value.map( + const commaMonzos = rawCommas.value.map( (comma) => subgroup.value.toMonzoAndResidual(comma)[0] ); const mappingVector = vanishCommas( diff --git a/src/components/modals/tempering-state.ts b/src/components/modals/tempering-state.ts index 70e88542..5988190f 100644 --- a/src/components/modals/tempering-state.ts +++ b/src/components/modals/tempering-state.ts @@ -1,19 +1,15 @@ // Component state used by RankOne, RankTwo and TemperScale -import { computedAndError } from "@/utils"; +import { computedAndError, parseChordInput, splitText } from "@/utils"; import { fractionToString } from "scale-workshop-core"; import { Subgroup, type TuningOptions } from "temperaments"; import { computed, ref, watch, type Ref } from "vue"; -// Split text values separated by whitespace, pipes, amps, semicolons or commas. -export function splitText(text: string) { - return text - .replace(/\s/g, ",") - .replace(/\|/g, ",") - .replace(/&/g, ",") - .replace(/;/g, ",") - .split(",") - .filter((token) => token.length); +// Split text into (non-extended) monzos +function splitCommas(text: string) { + return parseChordInput(text).map((interval) => + interval.monzo.vector.map((component) => component.valueOf()) + ); } export function makeState(method: Ref, subgroupStringDefault = "") { @@ -32,7 +28,8 @@ export function makeState(method: Ref, subgroupStringDefault = "") { // === Computed state === const vals = computed(() => splitText(valsString.value)); - const commas = computed(() => splitText(commasString.value)); + const rawCommas = computed(() => splitText(commasString.value)); + const commas = computed(() => splitCommas(commasString.value)); const subgroupDefault = new Subgroup(subgroupStringDefault || []); const [subgroup, subgroupError] = computedAndError(() => { @@ -105,6 +102,7 @@ export function makeState(method: Ref, subgroupStringDefault = "") { tempering, constraintsString, vals, + rawCommas, commas, subgroup, weights, diff --git a/src/utils.ts b/src/utils.ts index 0cadfdc9..cd9621b6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,9 +3,15 @@ import { computed, type ComputedRef } from "vue"; import { gcd, mmod } from "xen-dev-utils"; import { DEFAULT_NUMBER_OF_COMPONENTS } from "./constants"; +// Split at whitespace, pipes, amps, colons, semicolons and commas +export const SEPARATOR_RE = /\s|\||&|:|;|,/; + +export function splitText(text: string) { + return text.split(SEPARATOR_RE).filter((token) => token.length); +} + export function parseChordInput(input: string) { - const separator = input.includes(":") ? ":" : /\s/; - return parseChord(input, DEFAULT_NUMBER_OF_COMPONENTS, separator); + return parseChord(input, DEFAULT_NUMBER_OF_COMPONENTS, SEPARATOR_RE); } export function debounce(func: (...args: any[]) => void, timeout = 300) {