summarum/src/engine/engine.test.ts
2026-06-23 23:56:13 +03:00

254 lines
8.4 KiB
TypeScript

import { describe, it, expect, beforeAll } from "vitest";
import { SumEngine } from "./index";
let eng: SumEngine;
beforeAll(() => {
eng = new SumEngine();
// deterministic rates for tests
eng.setRates({ USD: 1, EUR: 0.5, RUB: 100, GBP: 0.25, BTC: 0.00001 });
});
function calc(expr: string): string | null {
const r = eng.evaluateDocument(expr);
return r[r.length - 1].text;
}
function calcDoc(doc: string): (string | null)[] {
return eng.evaluateDocument(doc).map((r) => r.text);
}
describe("arithmetic", () => {
it("basic ops", () => {
expect(calc("2 + 2")).toBe("4");
expect(calc("7 - 10")).toBe("-3");
expect(calc("6 * 7")).toBe("42");
expect(calc("1 / 8")).toBe("0.13");
expect(calc("2 ^ 10")).toBe("1,024");
expect(calc("10 mod 3")).toBe("1");
expect(calc("5!")).toBe("120");
});
it("decimal precision (no float artifacts)", () => {
expect(calc("0.1 + 0.2")).toBe("0.3");
});
it("parens and precedence", () => {
expect(calc("2 + 3 * 4")).toBe("14");
expect(calc("(2 + 3) * 4")).toBe("20");
expect(calc("2(3+4)")).toBe("14");
});
it("word operators", () => {
expect(calc("5 plus 3")).toBe("8");
expect(calc("10 minus 4")).toBe("6");
expect(calc("3 times 4")).toBe("12");
expect(calc("20 divided by 4")).toBe("5");
expect(calc("5 и 3")).toBe("8"); // ru plus
});
it("thousands separators in input", () => {
expect(calc("1,000 + 234")).toBe("1,234");
});
it("scales", () => {
expect(calc("1.5 million")).toBe("1,500,000");
expect(calc("5k")).toBe("5,000");
expect(calc("2 trillion")).toBe("2,000,000,000,000");
expect(calc("1.5 трлн")).toBe("1,500,000,000,000");
expect(calc("5к")).toBe("5,000");
expect(calc("2 dozen")).toBe("2"); // dozen is not in the vocab — noise
});
it("free text around math is ignored", () => {
expect(calc("I spent 20 on pizza plus 5 on coffee")).toBe("25");
});
it("constants", () => {
expect(calc("pi * 2")).toBe("6.28");
});
it("functions", () => {
expect(calc("sqrt(16)")).toBe("4");
expect(calc("sqrt 16")).toBe("4");
expect(calc("square root of 16")).toBe("4");
expect(calc("cbrt(27)")).toBe("3");
expect(calc("abs(-5)")).toBe("5");
expect(calc("round(2.6)")).toBe("3");
expect(calc("log(1000)")).toBe("3");
expect(calc("ln(1)")).toBe("0");
});
});
describe("percent", () => {
it("sample: 8 / (45 - 20%)", () => {
expect(calc("8 / (45 - 20%)")).toBe("0.22");
});
it("plus/minus percent", () => {
expect(calc("100 + 20%")).toBe("120");
expect(calc("100 - 20%")).toBe("80");
});
it("of / off / on", () => {
expect(calc("20% of 100")).toBe("20");
expect(calc("20% off 100")).toBe("80");
expect(calc("20% on 100")).toBe("120");
});
it("as a % of", () => {
expect(calc("5 as a % of 25")).toBe("20%");
});
it("of what is", () => {
expect(calc("20% of what is 30")).toBe("150");
expect(calc("25% off what is 60")).toBe("80");
});
it("percent arithmetic", () => {
expect(calc("5% + 5%")).toBe("10%");
expect(calc("100 * 20%")).toBe("20");
});
});
describe("units", () => {
it("sample: 5 inches in cm", () => {
expect(calc("5 inches in cm")).toBe("12.7 cm");
});
it("length", () => {
expect(calc("1 mile in km")).toBe("1.61 km");
expect(calc("100 km in miles")).toBe("62.14 mi.");
expect(calc("6 feet in meters")).toBe("1.83 m");
});
it("prefixed symbols", () => {
expect(calc("10 mm in cm")).toBe("1 cm");
expect(calc("1 km in m")).toBe("1,000 m");
});
it("weight", () => {
expect(calc("1 kg in pounds")).toBe("2.2 lb");
expect(calc("16 oz in pounds")).toBe("1 lb");
});
it("temperature", () => {
expect(calc("100 C in F")).toBe("212°F");
expect(calc("32 F in C")).toBe("0°C");
expect(calc("0 C in K")).toBe("273.15 K");
});
it("volume", () => {
expect(calc("1 gallon in liters")).toBe("3.79 L");
});
it("area", () => {
expect(calc("1 hectare in acres")).toBe("2.47 acre");
expect(calc("2 m * 3 m")).toBe("6 m²");
});
it("data with SI and IEC prefixes", () => {
expect(calc("1 GB in MB")).toBe("1,000 MB");
expect(calc("1 GiB in MiB")).toBe("1,024 MiB");
expect(calc("1 GB in MiB")).toBe("953.67 MiB");
expect(calc("1 byte in bits")).toBe("8 b");
});
it("css", () => {
expect(calc("32 px in em")).toBe("2 em");
expect(calc("1 inch in px")).toBe("96 px");
});
it("time units", () => {
expect(calc("90 minutes in hours")).toBe("1.5 h");
expect(calc("2 hours 30 minutes in minutes")).toBe("150 min");
});
it("unit math keeps the left unit", () => {
expect(calc("1 m + 50 cm")).toBe("1.5 m");
expect(calc("5 ft 4 in in cm")).toBe("162.56 cm");
});
it("angle trig", () => {
expect(calc("sin(90 degrees)")).toBe("1");
});
it("russian unit phrases", () => {
expect(calc("5 метров в см")).toBe("500 cm");
});
});
describe("currency", () => {
it("sample: $9 in Euro (test rate 1 USD = 0.5 EUR)", () => {
expect(calc("$9 in Euro")).toBe("€ 4.5");
});
it("postfix and codes", () => {
expect(calc("100 USD in RUB")).toBe("10,000 ₽");
expect(calc("100 рублей в долларах")).toBe("$1");
});
it("currency arithmetic", () => {
expect(calc("$10 + $5")).toBe("$15");
expect(calc("$10 + 20%")).toBe("$12");
});
it("GBP↔RUB via natural phrases", () => {
// "sterling" and "quid" as English shortcuts (bare "pounds" is weight, not GBP)
expect(calc("100 sterling in RUB")).toBe("40,000 ₽");
expect(calc("100 quid in rubles")).toBe("40,000 ₽");
// Russian: "стерлинг" (bare "фунт" is weight unit, not GBP)
expect(calc("100 стерлингов в рублях")).toBe("40,000 ₽");
// existing multi-word forms still work
expect(calc("100 GBP in RUB")).toBe("40,000 ₽");
expect(calc("£100 in RUB")).toBe("40,000 ₽");
});
});
describe("numeral systems", () => {
it("input", () => {
expect(calc("0x1F")).toBe("0x1F");
expect(calc("0b101 in decimal")).toBe("5");
});
it("conversion", () => {
expect(calc("255 in hex")).toBe("0xFF");
expect(calc("5 in binary")).toBe("0b101");
expect(calc("8 in octal")).toBe("0o10");
});
it("hex arithmetic keeps repr of the left operand", () => {
expect(calc("0x10 + 0x10 in decimal")).toBe("32");
});
});
describe("variables and document context", () => {
it("assignment and reuse", () => {
expect(calcDoc("x = 5\nx * 2")).toEqual(["5", "10"]);
});
it("is-assignment", () => {
expect(calcDoc("price is 100\nprice + 20%")).toEqual(["100", "120"]);
});
it("sum / total", () => {
expect(calcDoc("10\n20\n30\nsum")).toEqual(["10", "20", "30", "60"]);
expect(calcDoc("10\n20\n\n30\nsum")).toEqual(["10", "20", null, "30", "30"]);
});
it("sum with units", () => {
expect(calcDoc("$10\n$20\ntotal")).toEqual(["$10", "$20", "$30"]);
});
it("avg", () => {
expect(calcDoc("10\n20\naverage")).toEqual(["10", "20", "15"]);
});
it("prev", () => {
expect(calcDoc("42\nprev + 1")).toEqual(["42", "43"]);
});
it("headers split blocks and produce no result", () => {
expect(calcDoc("# Food\n10\n20\nsum")).toEqual([null, "10", "20", "30"]);
});
it("comments", () => {
expect(calcDoc("// just a comment\n5 + 5 // inline")).toEqual([null, "10"]);
});
it("label lines produce no result", () => {
expect(calc("Monthly costs")).toBe(null);
});
});
describe("dates", () => {
it("today + 2 weeks lands 14 days ahead", () => {
const r = eng.evaluateDocument("today + 2 weeks - today");
// (today + 2w) - today = 14 days
expect(r[0].text).toBe("14 day");
});
it("tomorrow - today = 1 day", () => {
expect(calc("tomorrow - today")).toBe("1 day");
});
it("time in a timezone returns a formatted time", () => {
const t = calc("time in Tokyo");
expect(t).toMatch(/^\d{2}:\d{2}$/);
});
});
describe("extension API", () => {
it("addUnit (horse from Sample.js)", () => {
eng.addUnit({ id: "horse", phrases: "horse, horses, hrs", baseUnitId: "meter", format: "hrs", ratio: 2.4 });
expect(calc("2 horses in meters")).toBe("4.8 m");
});
it("addFunction (zum from Sample.js)", () => {
eng.addFunction({ id: "zum", phrases: "zum" }, (values) => ({ double: values[0].double + values[1].double }));
expect(calc("zum(2;3)")).toBe("5");
});
it("setVariable", () => {
eng.setVariable("myvar", { double: 5, unitId: "USD" });
expect(calc("myvar * 2")).toBe("$10");
});
});