From 423d22d46244494a086d79a1cbbba538b897f162 Mon Sep 17 00:00:00 2001 From: Horizontal42 <75383519+Horizontal42@users.noreply.github.com> Date: Sun, 9 Aug 2026 15:59:13 +0300 Subject: [PATCH] add evaluator quantity conversion tests --- src/engine/evaluator.test.ts | 90 ++++++++++++++++++++++++++++++++++++ src/engine/evaluator.ts | 2 +- 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/engine/evaluator.test.ts diff --git a/src/engine/evaluator.test.ts b/src/engine/evaluator.test.ts new file mode 100644 index 0000000..53fbfe6 --- /dev/null +++ b/src/engine/evaluator.test.ts @@ -0,0 +1,90 @@ +import { describe, it, expect } from "vitest"; +import { toBase, fromBase, convertQ, attachUnit } from "./evaluator"; +import { qty, pct, EvalError, Decimal } from "./types"; +import { buildRegistry } from "./registry"; + +const reg = buildRegistry(); +const getUnit = (id: string) => { + const u = reg.unitsById.get(id); + if (!u) throw new Error(`unit ${id} not found`); + return u; +}; + +describe("quantity conversion", () => { + it("toBase works", () => { + // 1 km = 1000 m + const km = getUnit("kilo:meter"); + const res1 = toBase(qty(2, km)); + expect(res1.toNumber()).toBe(2000); + + // 0 C = 273.15 K + const c = getUnit("celsius"); + const res2 = toBase(qty(0, c)); + expect(res2.toNumber()).toBe(273.15); + + // 10 mpg + const mpg = getUnit("mpg"); + const res3 = toBase(qty(10, mpg)); + expect(res3.toNumber()).toBeCloseTo(23.5215, 4); + + // error on zero for reciprocal + expect(() => toBase(qty(0, mpg))).toThrow(EvalError); + }); + + it("fromBase works", () => { + const km = getUnit("kilo:meter"); + const res1 = fromBase(new Decimal(2000), km); + expect(res1.toNumber()).toBe(2); + + const c = getUnit("celsius"); + const res2 = fromBase(new Decimal(273.15), c); + expect(res2.toNumber()).toBe(0); + + const mpg = getUnit("mpg"); + const res3 = fromBase(new Decimal(23.5215), mpg); + expect(res3.toNumber()).toBeCloseTo(10, 4); + + expect(() => fromBase(new Decimal(0), mpg)).toThrow(EvalError); + }); + + it("convertQ works", () => { + const m = getUnit("meter"); + const km = getUnit("kilo:meter"); + + // 2000m -> km + const q1 = convertQ(qty(2000, m), km); + expect(q1.value.toNumber()).toBe(2); + expect(q1.unit?.id).toBe("kilo:meter"); + + // c -> f + const c = getUnit("celsius"); + const f = getUnit("fahrenheit"); + const q2 = convertQ(qty(0, c), f); + expect(q2.value.toNumber()).toBeCloseTo(32); + + // fail on mismatched dimensions + const kg = getUnit("kilo:gram"); + expect(() => convertQ(qty(1, m), kg)).toThrow(EvalError); + }); + + it("attachUnit works", () => { + const m = getUnit("meter"); + const km = getUnit("kilo:meter"); + const kg = getUnit("kilo:gram"); + + // attaching to dimensionless + const r1 = attachUnit(qty(2), m); + expect(r1).toMatchObject({ kind: "quantity", unit: { id: "meter" } }); + + // attaching matching dimension does conversion + const r2 = attachUnit(qty(2000, m), km); + expect((r2 as any).value.toNumber()).toBe(2); + expect((r2 as any).unit?.id).toBe("kilo:meter"); + + // attaching mismatched dimension throws + expect(() => attachUnit(qty(1, m), kg)).toThrow(EvalError); + + // attaching to non-quantity throws + expect(() => attachUnit(pct(50), m)).toThrow(EvalError); + }); +}); diff --git a/src/engine/evaluator.ts b/src/engine/evaluator.ts index 18d8d77..848ed72 100644 --- a/src/engine/evaluator.ts +++ b/src/engine/evaluator.ts @@ -80,7 +80,7 @@ function asScalar(v: Value): Decimal { throw new EvalError("expected a number"); } -function attachUnit(v: Value, unit: Unit): Value { +export function attachUnit(v: Value, unit: Unit): Value { if (v.kind === "quantity") { if (v.unit && v.unit.dimension === unit.dimension) return convertQ(v, unit); if (v.unit) throw new EvalError("unit mismatch");