diff --git a/packages/base-formula/src/functions/math.ts b/packages/base-formula/src/functions/math.ts index d0922435c..279d9c6ac 100644 --- a/packages/base-formula/src/functions/math.ts +++ b/packages/base-formula/src/functions/math.ts @@ -1,12 +1,19 @@ // packages/base-formula/src/functions/math.ts import { register } from "./registry"; +import { makeErrorCell } from "../error"; const num = (v: unknown): number | null => v == null ? null : Number(v); register({ - name: "round", arity: { min: 1, max: 1 }, paramTypes: ["number"], returnType: "number", - eval: ([v]) => { const n = num(v); return n == null ? null : Math.round(n); }, - doc: "Rounds to the nearest integer.", category: "math", + name: "round", arity: { min: 1, max: 2 }, paramTypes: ["number", "number"], returnType: "number", + eval: ([v, places]) => { + const n = num(v); + if (n == null) return null; + const p = places == null ? 0 : Math.trunc(Number(places)); + const factor = Math.pow(10, p); + return Math.round(n * factor) / factor; + }, + doc: "Rounds to the nearest integer, or to `places` decimals if given.", category: "math", }); register({ name: "floor", arity: { min: 1, max: 1 }, paramTypes: ["number"], returnType: "number", @@ -44,8 +51,104 @@ register({ eval: ([a, b]) => { const na = num(a), nb = num(b); if (na == null || nb == null) return null; - if (nb === 0) throw new Error("modulo by zero"); + if (nb === 0) return makeErrorCell("DIV_BY_ZERO", "modulo by zero"); return na % nb; }, doc: "Remainder after division.", category: "math", }); +register({ + name: "add", arity: { min: 2, max: 2 }, paramTypes: ["number", "number"], returnType: "number", + eval: ([a, b]) => { + const na = num(a), nb = num(b); + return na == null || nb == null ? null : na + nb; + }, + doc: "Sum of two numbers.", category: "math", +}); +register({ + name: "subtract", arity: { min: 2, max: 2 }, paramTypes: ["number", "number"], returnType: "number", + eval: ([a, b]) => { + const na = num(a), nb = num(b); + return na == null || nb == null ? null : na - nb; + }, + doc: "Difference of two numbers.", category: "math", +}); +register({ + name: "multiply", arity: { min: 2, max: 2 }, paramTypes: ["number", "number"], returnType: "number", + eval: ([a, b]) => { + const na = num(a), nb = num(b); + return na == null || nb == null ? null : na * nb; + }, + doc: "Product of two numbers.", category: "math", +}); +register({ + name: "divide", arity: { min: 2, max: 2 }, paramTypes: ["number", "number"], returnType: "number", + eval: ([a, b]) => { + const na = num(a), nb = num(b); + if (na == null || nb == null) return null; + if (nb === 0) return makeErrorCell("DIV_BY_ZERO", "division by zero"); + return na / nb; + }, + doc: "Quotient of two numbers.", category: "math", +}); +register({ + name: "pow", arity: { min: 2, max: 2 }, paramTypes: ["number", "number"], returnType: "number", + eval: ([a, b]) => { + const na = num(a), nb = num(b); + return na == null || nb == null ? null : Math.pow(na, nb); + }, + doc: "Base raised to an exponent.", category: "math", +}); +register({ + name: "sqrt", arity: { min: 1, max: 1 }, paramTypes: ["number"], returnType: "number", + eval: ([v]) => { + const n = num(v); + if (n == null) return null; + if (n < 0) return makeErrorCell("TYPE_MISMATCH", "sqrt of negative number"); + return Math.sqrt(n); + }, + doc: "Positive square root.", category: "math", +}); +register({ + name: "sum", arity: { min: 1, max: null }, paramTypes: "variadic-any", returnType: "number", + eval: (args) => { + // Null propagates as 0 so `sum(prop("A"), prop("B"))` still works when + // some cells are empty — matches Airtable/Notion semantics. + let total = 0; + for (const v of args) { + const n = num(v); + if (n != null && Number.isFinite(n)) total += n; + } + return total; + }, + doc: "Sum of the arguments.", category: "math", +}); +register({ + name: "mean", arity: { min: 1, max: null }, paramTypes: "variadic-any", returnType: "number", + eval: (args) => { + const nums: number[] = []; + for (const v of args) { + const n = num(v); + if (n != null && Number.isFinite(n)) nums.push(n); + } + if (nums.length === 0) return null; + return nums.reduce((a, b) => a + b, 0) / nums.length; + }, + doc: "Arithmetic average of the arguments.", category: "math", +}); +register({ + name: "median", arity: { min: 1, max: null }, paramTypes: "variadic-any", returnType: "number", + eval: (args) => { + const nums: number[] = []; + for (const v of args) { + const n = num(v); + if (n != null && Number.isFinite(n)) nums.push(n); + } + if (nums.length === 0) return null; + nums.sort((a, b) => a - b); + const mid = Math.floor(nums.length / 2); + return nums.length % 2 === 0 + ? (nums[mid - 1] + nums[mid]) / 2 + : nums[mid]; + }, + doc: "Middle value of the arguments.", category: "math", +});