feat(base-formula): add date and coercion functions, wire registry

This commit is contained in:
Philipinho
2026-04-24 00:03:40 +01:00
parent 0174fada5f
commit ea0dc2b56b
5 changed files with 67 additions and 6 deletions
@@ -45,8 +45,7 @@ describe("evaluate", () => {
});
});
// TODO: unskip after Task 15 wires the function registry with round/concat.
describe.skip("evaluate with registered functions", () => {
describe("evaluate with registered functions", () => {
it("invokes registered functions", () => {
expect(run('round(1.6)', {})).toBe(2);
expect(run('concat("a", "b", "c")', {})).toBe("abc");
@@ -1,4 +1,3 @@
// TODO: unskip after Task 15 lands date functions and populates the registry.
import { parseRaw, resolve, typecheck, registry } from "@docmost/base-formula/server";
import type { FormulaAST } from "@docmost/base-formula/server";
@@ -9,7 +8,7 @@ const mk = (src: string, propTypes: Record<string, "number" | "string" | "boolea
return { ast: resolved.ast, typeMap };
};
describe.skip("typecheck", () => {
describe("typecheck", () => {
it("infers number for arithmetic", () => {
const { ast, typeMap } = mk('prop("Price") * 2', { Price: "number" });
expect(typecheck(ast, typeMap, registry).resultType).toBe("number");
@@ -0,0 +1,54 @@
// packages/base-formula/src/functions/date.ts
import { register } from "./registry";
import { makeErrorCell } from "../error";
const toDate = (v: unknown): Date | null => {
if (v == null) return null;
const d = new Date(String(v));
return isNaN(d.getTime()) ? null : d;
};
register({
name: "now", arity: { min: 0, max: 0 }, paramTypes: [], returnType: "date",
eval: () => new Date().toISOString(),
doc: "Current timestamp.", category: "date",
});
register({
name: "today", arity: { min: 0, max: 0 }, paramTypes: [], returnType: "date",
eval: () => {
const d = new Date(); d.setUTCHours(0, 0, 0, 0); return d.toISOString();
},
doc: "Midnight UTC of today.", category: "date",
});
register({
name: "dateAdd", arity: { min: 3, max: 3 }, paramTypes: ["date", "number", "string"], returnType: "date",
eval: ([base, amt, unit]) => {
const d = toDate(base);
if (!d) return makeErrorCell("DATE_INVALID", "invalid date");
const n = Number(amt);
const u = String(unit);
const r = new Date(d);
if (u === "days") r.setUTCDate(r.getUTCDate() + n);
else if (u === "hours") r.setUTCHours(r.getUTCHours() + n);
else if (u === "minutes") r.setUTCMinutes(r.getUTCMinutes() + n);
else if (u === "months") r.setUTCMonth(r.getUTCMonth() + n);
else if (u === "years") r.setUTCFullYear(r.getUTCFullYear() + n);
else return makeErrorCell("TYPE_MISMATCH", `unknown unit ${u}`);
return r.toISOString();
},
doc: "Adds a duration to a date. Units: days, hours, minutes, months, years.", category: "date",
});
register({
name: "dateBetween", arity: { min: 3, max: 3 }, paramTypes: ["date", "date", "string"], returnType: "number",
eval: ([a, b, unit]) => {
const da = toDate(a), db = toDate(b);
if (!da || !db) return makeErrorCell("DATE_INVALID", "invalid date");
const ms = db.getTime() - da.getTime();
const u = String(unit);
if (u === "days") return Math.floor(ms / 86_400_000);
if (u === "hours") return Math.floor(ms / 3_600_000);
if (u === "minutes") return Math.floor(ms / 60_000);
return makeErrorCell("TYPE_MISMATCH", `unknown unit ${u}`);
},
doc: "Difference between two dates in a given unit.", category: "date",
});
@@ -0,0 +1,8 @@
// packages/base-formula/src/functions/index.ts
import "./logic";
import "./math";
import "./string";
import "./date";
import "./coercion";
export { registry, register } from "./registry";
export type { FormulaFn } from "./registry";
+3 -2
View File
@@ -7,7 +7,8 @@ export * from "./parser";
export * from "./resolver";
export * from "./typecheck";
export * from "./format";
export { registry, register } from "./functions/registry";
export type { FormulaFn } from "./functions/registry";
import "./functions/index"; // side-effect: populate registry
export { registry, register } from "./functions/index";
export type { FormulaFn } from "./functions/index";
export * from "./graph";
export * from "./eval";