/*
Unit tests for RFC-0570 formula evaluation module — extractNumeric, scanFormulas, resolveFormula.
RFC-0729: pipe syntax, formatter registry, money formatter, REF-10 error.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-1311: Compass header sweep — declares the formula suite's contract for test-file scaffold coverage.
*/
import { describe, it, expect } from "vitest";
import {
extractNumeric,
scanFormulas,
resolveFormula,
registerPipeFormatter,
getPipeFormatter,
} from "../formula-eval.ts";
import type { ContentRefIndex, SourceRef } from "../../content/content-reference.ts";
import { EMPTY_CONTENT_REF_INDEX } from "../../content/content-reference.ts";
describe("extractNumeric", () => {
it("extracts number from string with currency suffix", () => {
expect(extractNumeric("200 €")).toBe(200);
});
it("extracts number with thin space thousands separator", () => {
expect(extractNumeric("1\u202f040 €")).toBe(1040);
});
it("extracts number with period thousands separator (German)", () => {
expect(extractNumeric("1.040 €")).toBe(1040);
});
it("extracts number with comma decimal separator (German)", () => {
expect(extractNumeric("70,50 €")).toBe(70.5);
});
it("extracts negative number", () => {
expect(extractNumeric("-200 €")).toBe(-200);
});
it("extracts number with unit suffix", () => {
expect(extractNumeric("70 €/Monat")).toBe(70);
});
it("extracts plain number string", () => {
expect(extractNumeric("1040")).toBe(1040);
});
it("extracts number with period decimal separator", () => {
expect(extractNumeric("70.50")).toBe(70.5);
});
it("extracts number with both period thousands and comma decimal (German)", () => {
expect(extractNumeric("1.040,50 €")).toBe(1040.5);
});
it("returns null for non-numeric string", () => {
expect(extractNumeric("no number")).toBeNull();
});
it("returns null for null", () => {
expect(extractNumeric(null)).toBeNull();
});
it("returns null for undefined", () => {
expect(extractNumeric(undefined)).toBeNull();
});
it("returns null for boolean", () => {
expect(extractNumeric(true)).toBeNull();
});
it("returns number for number input", () => {
expect(extractNumeric(200)).toBe(200);
});
it("returns null for NaN", () => {
expect(extractNumeric(Number.NaN)).toBeNull();
});
it("returns null for empty string", () => {
expect(extractNumeric("")).toBeNull();
});
});
describe("scanFormulas", () => {
it("returns empty array for text without formulas", () => {
expect(scanFormulas("no formula here")).toEqual([]);
});
it("returns empty array for text without =( prefix", () => {
expect(scanFormulas("(a + b) without prefix")).toEqual([]);
});
it("scans simple formula", () => {
const result = scanFormulas("=(a + b)");
expect(result).toHaveLength(1);
expect(result[0].expression).toBe("a + b");
expect(result[0].start).toBe(0);
expect(result[0].end).toBe(8);
});
it("scans formula with nested parentheses", () => {
const result = scanFormulas("=(a + (b * c))");
expect(result).toHaveLength(1);
expect(result[0].expression).toBe("a + (b * c)");
});
it("scans multiple formulas in text", () => {
const result = scanFormulas("text =(a) more =(b) end");
expect(result).toHaveLength(2);
expect(result[0].expression).toBe("a");
expect(result[1].expression).toBe("b");
});
it("scans formula with surrounding text", () => {
const result = scanFormulas("Total: =(a + b * 12) €");
expect(result).toHaveLength(1);
expect(result[0].expression).toBe("a + b * 12");
});
it("handles unbalanced parentheses gracefully", () => {
const result = scanFormulas("=(a + b");
expect(result).toEqual([]);
});
it("fast path: returns empty array when text lacks =( prefix", () => {
expect(scanFormulas("just some text without any formula")).toEqual([]);
});
});
describe("resolveFormula", () => {
const mockIndex: ContentRefIndex = {
version: 1,
generatedAt: "2026-01-01",
collections: ["business-profile"],
entries: {
"business-profile": {
"offerings/digital-foundation": {
de: {
presentation: {
price: {
setup: "200 €",
monthly: "70 €",
},
},
},
},
},
},
};
it("resolves formula with two references and arithmetic", () => {
const result = resolveFormula(
mockIndex,
"business-profile.offerings/digital-foundation.presentation.price.setup + business-profile.offerings/digital-foundation.presentation.price.monthly * 12",
"de",
"de",
);
expect(result.resolved).toBe(true);
expect(result.value).toBe("1040");
});
it("resolves formula with numeric literal and reference", () => {
const result = resolveFormula(
mockIndex,
"business-profile.offerings/digital-foundation.presentation.price.setup + 100",
"de",
"de",
);
expect(result.resolved).toBe(true);
expect(result.value).toBe("300");
});
it("resolves formula with nested parentheses", () => {
const result = resolveFormula(
mockIndex,
"(business-profile.offerings/digital-foundation.presentation.price.setup + business-profile.offerings/digital-foundation.presentation.price.monthly) * 2",
"de",
"de",
);
expect(result.resolved).toBe(true);
expect(result.value).toBe("540");
});
it("returns REF-06 for unresolved reference", () => {
const result = resolveFormula(
mockIndex,
"business-profile.offerings/nonexistent.presentation.price.setup + 100",
"de",
"de",
);
expect(result.resolved).toBe(false);
expect(result.error).toContain("REF-06");
});
it("returns REF-07 for non-numeric operand", () => {
const indexWithNonNumeric: ContentRefIndex = {
...mockIndex,
entries: {
"business-profile": {
"offerings/digital-foundation": {
de: {
presentation: {
price: {
setup: "not a number",
monthly: "70 €",
},
},
},
},
},
},
};
const result = resolveFormula(
indexWithNonNumeric,
"business-profile.offerings/digital-foundation.presentation.price.setup + 100",
"de",
"de",
);
expect(result.resolved).toBe(false);
expect(result.error).toContain("REF-07");
});
it("returns REF-08 for syntax error", () => {
const result = resolveFormula(mockIndex, "200 +", "de", "de");
expect(result.resolved).toBe(false);
expect(result.error).toContain("REF-08");
});
it("returns REF-09 for division by zero", () => {
const result = resolveFormula(mockIndex, "200 / 0", "de", "de");
expect(result.resolved).toBe(false);
expect(result.error).toContain("REF-09");
});
it("resolves formula with empty index (no refs, pure arithmetic)", () => {
const result = resolveFormula(EMPTY_CONTENT_REF_INDEX, "200 + 300", "de", "de");
expect(result.resolved).toBe(true);
expect(result.value).toBe("500");
});
it("RFC-0723: returns string value for single-ref expression with non-numeric value", () => {
const indexWithStringValue: ContentRefIndex = {
...mockIndex,
entries: {
"business-profile": {
"offerings/digital-foundation": {
de: {
presentation: {
price: {
setup: "200 €",
monthly: "70 €",
},
tagline: "Digitales Fundament",
},
},
},
},
},
};
const result = resolveFormula(
indexWithStringValue,
"business-profile.offerings/digital-foundation.presentation.tagline",
"de",
"de",
);
expect(result.resolved).toBe(true);
expect(result.value).toBe("Digitales Fundament");
});
it("RFC-0723: returns full string value for single-ref expression with numeric value", () => {
const result = resolveFormula(
mockIndex,
"business-profile.offerings/digital-foundation.presentation.price.setup",
"de",
"de",
);
expect(result.resolved).toBe(true);
expect(result.value).toBe("200 €");
});
it("RFC-0723: returns REF-07 for multi-ref expression with non-numeric operand", () => {
const indexWithNonNumeric: ContentRefIndex = {
...mockIndex,
entries: {
"business-profile": {
"offerings/digital-foundation": {
de: {
presentation: {
price: {
setup: "not a number",
monthly: "70 €",
},
},
},
},
},
},
};
const result = resolveFormula(
indexWithNonNumeric,
"business-profile.offerings/digital-foundation.presentation.price.setup + business-profile.offerings/digital-foundation.presentation.price.monthly",
"de",
"de",
);
expect(result.resolved).toBe(false);
expect(result.error).toContain("REF-07");
});
});
describe("RFC-0729: pipe syntax", () => {
const mockIndex: ContentRefIndex = {
version: 1,
generatedAt: "2026-01-01",
collections: ["business-profile"],
entries: {
"business-profile": {
"offerings/digital-foundation": {
de: {
presentation: {
price: {
setup: "200 €",
monthly: "70 €",
},
},
},
},
},
},
};
it("formats with money formatter in German locale", () => {
const result = resolveFormula(
mockIndex,
"business-profile.offerings/digital-foundation.presentation.price.monthly | money currency=EUR locale=de",
"de",
"de",
);
expect(result.resolved).toBe(true);
expect(result.value).toBe("70\u00A0€");
});
it("formats with money formatter in Ukrainian locale", () => {
const indexWithUk: ContentRefIndex = {
...mockIndex,
entries: {
"business-profile": {
"offerings/digital-foundation": {
de: {
presentation: {
price: { setup: "200 €", monthly: "70 €" },
},
},
uk: {
presentation: {
price: { setup: "200 €", monthly: "70 €" },
},
},
},
},
},
};
const result = resolveFormula(
indexWithUk,
"business-profile.offerings/digital-foundation.presentation.price.monthly | money currency=EUR locale=uk",
"uk",
"uk",
);
expect(result.resolved).toBe(true);
expect(result.value).toBe("70\u00A0€");
});
it("formats with currency conversion to UAH in German locale", () => {
const result = resolveFormula(
mockIndex,
"business-profile.offerings/digital-foundation.presentation.price.monthly | money currency=EUR locale=de targetCurrency=UAH rate=45",
"de",
"de",
);
expect(result.resolved).toBe(true);
expect(result.value).toBe("3.150\u00A0₴");
});
it("formats arithmetic result with pipe", () => {
const result = resolveFormula(
mockIndex,
"business-profile.offerings/digital-foundation.presentation.price.monthly * 12 | money currency=EUR locale=de",
"de",
"de",
);
expect(result.resolved).toBe(true);
expect(result.value).toBe("840\u00A0€");
});
it("preserves existing behavior for expressions without pipe", () => {
const result = resolveFormula(
mockIndex,
"business-profile.offerings/digital-foundation.presentation.price.monthly * 12",
"de",
"de",
);
expect(result.resolved).toBe(true);
expect(result.value).toBe("840");
});
it("preserves RFC-0723 single-ref string return without pipe", () => {
const indexWithString: ContentRefIndex = {
...mockIndex,
entries: {
"business-profile": {
"offerings/digital-foundation": {
de: {
presentation: {
price: {
setup: "200 €",
monthly: "70 €",
},
tagline: "Digitales Fundament",
},
},
},
},
},
};
const result = resolveFormula(
indexWithString,
"business-profile.offerings/digital-foundation.presentation.tagline",
"de",
"de",
);
expect(result.resolved).toBe(true);
expect(result.value).toBe("Digitales Fundament");
});
it("uses context.lang as default locale when no locale param", () => {
const result = resolveFormula(
mockIndex,
"business-profile.offerings/digital-foundation.presentation.price.monthly | money currency=EUR",
"de",
"de",
);
expect(result.resolved).toBe(true);
expect(result.value).toBe("70\u00A0€");
});
});
describe("RFC-0729: formatter registry", () => {
it("registers and retrieves a custom formatter", () => {
const customFn = (_value: number, _params: Record) => "custom";
registerPipeFormatter("custom-test", customFn);
expect(getPipeFormatter("custom-test")).toBe(customFn);
});
it("returns undefined for unregistered formatter", () => {
expect(getPipeFormatter("nonexistent-formatter")).toBeUndefined();
});
it("money formatter is registered by default", () => {
expect(getPipeFormatter("money")).toBeDefined();
});
});
describe("RFC-0729: REF-10 unknown formatter", () => {
const mockIndex: ContentRefIndex = {
version: 1,
generatedAt: "2026-01-01",
collections: ["business-profile"],
entries: {
"business-profile": {
"offerings/digital-foundation": {
de: {
presentation: {
price: {
setup: "200 €",
monthly: "70 €",
},
},
},
},
},
},
};
it("returns REF-10 for unknown formatter name", () => {
const result = resolveFormula(
mockIndex,
"business-profile.offerings/digital-foundation.presentation.price.monthly | unknownFormatter",
"de",
"de",
);
expect(result.resolved).toBe(false);
expect(result.error).toContain("REF-10");
expect(result.error).toContain("unknownFormatter");
});
it("returns REF-10 for empty formatter name", () => {
const result = resolveFormula(
mockIndex,
"business-profile.offerings/digital-foundation.presentation.price.monthly |",
"de",
"de",
);
expect(result.resolved).toBe(false);
expect(result.error).toContain("REF-10");
});
});
describe("RFC-0729: invalid rate param", () => {
const mockIndex: ContentRefIndex = {
version: 1,
generatedAt: "2026-01-01",
collections: ["business-profile"],
entries: {
"business-profile": {
"offerings/digital-foundation": {
de: {
presentation: {
price: {
setup: "200 €",
monthly: "70 €",
},
},
},
},
},
},
};
it("ignores conversion when rate is non-numeric", () => {
const result = resolveFormula(
mockIndex,
"business-profile.offerings/digital-foundation.presentation.price.monthly | money currency=EUR locale=de targetCurrency=UAH rate=abc",
"de",
"de",
);
expect(result.resolved).toBe(true);
expect(result.value).toBe("70\u00A0€");
});
});
describe("RFC-0731: this. self-reference in formulas", () => {
const mockIndex: ContentRefIndex = {
version: 1,
generatedAt: "2026-01-01",
collections: ["business-profile"],
entries: {
"business-profile": {
"offerings/digital-foundation": {
de: {
pricing: {
charges: {
monthlySubscription: { amount: { value: "70.00" } },
setup: { amount: { value: "200.00" } },
},
},
presentation: {
price: {
setup: "200 €",
monthly: "70 €",
},
},
},
},
},
},
};
const sourceRef: SourceRef = {
collection: "business-profile",
file: "offerings/digital-foundation",
};
it("resolves this. in arithmetic formula expression", () => {
const result = resolveFormula(
mockIndex,
"this.pricing.charges.monthlySubscription.amount.value + this.pricing.charges.setup.amount.value",
"de",
"de",
sourceRef,
);
expect(result.resolved).toBe(true);
expect(result.value).toBe("270");
});
it("resolves this. as single-ref string interpolation =(this.field)", () => {
const result = resolveFormula(
mockIndex,
"this.presentation.price.monthly",
"de",
"de",
sourceRef,
);
expect(result.resolved).toBe(true);
expect(result.value).toBe("70 €");
});
it("resolves this. with pipe formatter =(this.field | money)", () => {
const result = resolveFormula(
mockIndex,
"this.pricing.charges.monthlySubscription.amount.value | money currency=EUR locale=de",
"de",
"de",
sourceRef,
);
expect(result.resolved).toBe(true);
expect(result.value).toContain("70");
});
it("returns REF-12 when this. is used without sourceRef", () => {
const result = resolveFormula(
mockIndex,
"this.pricing.charges.monthlySubscription.amount.value",
"de",
"de",
);
expect(result.resolved).toBe(false);
expect(result.error).toContain("REF-12");
});
});