mirror of
https://github.com/docmost/docmost.git
synced 2026-06-15 22:48:42 +08:00
feat(base-formula): add date and coercion functions, wire registry
This commit is contained in:
@@ -45,8 +45,7 @@ describe("evaluate", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: unskip after Task 15 wires the function registry with round/concat.
|
describe("evaluate with registered functions", () => {
|
||||||
describe.skip("evaluate with registered functions", () => {
|
|
||||||
it("invokes registered functions", () => {
|
it("invokes registered functions", () => {
|
||||||
expect(run('round(1.6)', {})).toBe(2);
|
expect(run('round(1.6)', {})).toBe(2);
|
||||||
expect(run('concat("a", "b", "c")', {})).toBe("abc");
|
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 { parseRaw, resolve, typecheck, registry } from "@docmost/base-formula/server";
|
||||||
import type { FormulaAST } 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 };
|
return { ast: resolved.ast, typeMap };
|
||||||
};
|
};
|
||||||
|
|
||||||
describe.skip("typecheck", () => {
|
describe("typecheck", () => {
|
||||||
it("infers number for arithmetic", () => {
|
it("infers number for arithmetic", () => {
|
||||||
const { ast, typeMap } = mk('prop("Price") * 2', { Price: "number" });
|
const { ast, typeMap } = mk('prop("Price") * 2', { Price: "number" });
|
||||||
expect(typecheck(ast, typeMap, registry).resultType).toBe("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";
|
||||||
@@ -7,7 +7,8 @@ export * from "./parser";
|
|||||||
export * from "./resolver";
|
export * from "./resolver";
|
||||||
export * from "./typecheck";
|
export * from "./typecheck";
|
||||||
export * from "./format";
|
export * from "./format";
|
||||||
export { registry, register } from "./functions/registry";
|
import "./functions/index"; // side-effect: populate registry
|
||||||
export type { FormulaFn } from "./functions/registry";
|
export { registry, register } from "./functions/index";
|
||||||
|
export type { FormulaFn } from "./functions/index";
|
||||||
export * from "./graph";
|
export * from "./graph";
|
||||||
export * from "./eval";
|
export * from "./eval";
|
||||||
|
|||||||
Reference in New Issue
Block a user