From 838a193a228e14f531e64f23edd687fb3daaa55c Mon Sep 17 00:00:00 2001 From: iyxan23 Date: Mon, 16 Sep 2024 14:25:29 +0700 Subject: [PATCH] feat: impl builtin functions --- src/sheet/functions.ts | 225 +++++++++++++++++++++++++++++++++++ src/sheet/sheet-templater.ts | 6 +- 2 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 src/sheet/functions.ts diff --git a/src/sheet/functions.ts b/src/sheet/functions.ts new file mode 100644 index 0000000..999df6f --- /dev/null +++ b/src/sheet/functions.ts @@ -0,0 +1,225 @@ +// Default built-in functions + +import { z } from "zod"; +import { callLambda, createTemplaterFunction } from "./templater-function"; +import { createTemplaterNoArgsFunction } from "./templater-function"; +import { success } from "./expression/result"; + +// == array + +// .rest() is not possible yet +// const array = createTemplaterFunction(z.tuple([]).rest(z.any()), (...rest) => +// success([...rest]), +// ); +const arrayEmpty = createTemplaterNoArgsFunction(() => []); +const arrayAppend = createTemplaterFunction( + z.tuple([z.any().array(), z.any()]), + (arr, item) => success([...arr, item]), +); +const length = createTemplaterFunction(z.tuple([z.array(z.any())]), (a) => + success(a.length), +); +const keys = createTemplaterFunction(z.tuple([z.any()]), (obj) => + success(Object.keys(obj)), +); +const values = createTemplaterFunction(z.tuple([z.any()]), (obj) => + success(Object.values(obj)), +); +const entries = createTemplaterFunction(z.tuple([z.any()]), (obj) => + success(Object.entries(obj)), +); + +// == composable functions + +const flatten = createTemplaterFunction( + z.tuple([z.array(z.array(z.any()))]), + (arr) => success(arr.flat()), +); + +// usage: +// +// [reduce [:array] item acc { [...] } 0] +// ------- ---- --- --------- - +// array | | | L initial value +// | | lambda +// item local variable name | with item +// | & acc +// acc local variable name -+ +const reduce = createTemplaterFunction( + z.tuple([ + z.array(z.any()), + z.string(), + z.string(), + z.function(), + z.any().optional(), + ]), + (arr, itemName, accName, fn, init) => { + let result = init; + const issues = []; + const callFn = callLambda(fn); + + for (const item of arr) { + const callResult = callFn({ + variables: { + [itemName]: item, + [accName]: result, + }, + }); + + if (callResult.status === "failed") return callResult; + issues.push(...callResult.issues); + + result = callResult.result; + } + + return success(result, issues); + }, +); + +// usage: +// +// [map [:array] item { [...] }] +// -------- ---- -------- +// array | L lambda with item, +// | returns mapped item +// item local variable name +const map = createTemplaterFunction( + z.tuple([z.array(z.any()), z.string(), z.function()]), + (arr, idxName, fn) => { + const resultArr = []; + const issues = []; + const callFn = callLambda(fn); + + for (let i = 0; i < arr.length; i++) { + const result = callFn({ + variables: { + [idxName]: i, + }, + }); + + if (result.status === "failed") return result; + + issues.push(...result.issues); + resultArr.push(result.result); + } + + return success(resultArr, issues); + }, +); +const unique = createTemplaterFunction(z.tuple([z.array(z.any())]), (a) => + success([...new Set(a)]), +); + +// usage: +// +// [filter [:array] item { [...] }] +// -------- ---- -------- +// array | L lambda with item, +// | returns boolean +// item local variable name +const filter = createTemplaterFunction( + z.tuple([z.array(z.any()), z.string(), z.function()]), + (arr, idxName, fn) => { + const resultArr = []; + const issues = []; + const callFn = callLambda(fn); + + for (const item of arr) { + const result = callFn({ + variables: { + [idxName]: item, + }, + }); + + if (result.status === "failed") return result; + + issues.push(...result.issues); + if (result.result) resultArr.push(item); + } + + return success(resultArr, issues); + }, +); + +// == arithmetic + +const add = createTemplaterFunction( + z.tuple([z.coerce.number(), z.coerce.number()]), + (a, b) => success(a + b), +); +const subtract = createTemplaterFunction( + z.tuple([z.coerce.number(), z.coerce.number()]), + (a, b) => success(a - b), +); +const multiply = createTemplaterFunction( + z.tuple([z.coerce.number(), z.coerce.number()]), + (a, b) => success(a * b), +); +const divide = createTemplaterFunction( + z.tuple([z.coerce.number(), z.coerce.number()]), + (a, b) => success(a / b), +); +const round = createTemplaterFunction( + z.tuple([z.coerce.number(), z.coerce.number().optional()]), + (num, round) => + success( + round ? Math.round(num * (round * 10)) / (round * 10) : Math.round(num), + ), +); + +// == string + +const concat = createTemplaterFunction( + z.tuple([z.coerce.string(), z.coerce.string()]), + (a, b) => success(a + b), +); +const join = createTemplaterFunction( + z.tuple([z.array(z.string()), z.string().optional()]), + (arr, sep = "") => success(arr.join(sep)), +); +const jsonStringify = createTemplaterFunction(z.tuple([z.any()]), (a) => + success(JSON.stringify(a)), +); +const jsonParse = createTemplaterFunction(z.tuple([z.string()]), (a) => + success(JSON.parse(a)), +); + +// == conditionals + +const if_ = createTemplaterFunction( + z.tuple([z.coerce.boolean(), z.any(), z.any()]), + (condition, ifTrue, ifFalse) => (condition ? ifTrue : ifFalse), +); +const ifUndefined = createTemplaterFunction( + z.tuple([z.any(), z.string()]), + (a, b) => (a === undefined ? success(b) : success(a)), +); + +export const builtinFunctions = { + arrayEmpty, + arrayAppend, + flatten, + reduce, + map, + unique, + filter, + + length, + keys, + values, + entries, + + add, + subtract, + multiply, + divide, + round, + + concat, + join, + jsonStringify, + jsonParse, + + if: if_, + ifUndefined, +}; diff --git a/src/sheet/sheet-templater.ts b/src/sheet/sheet-templater.ts index 0f41e80..72d6fab 100644 --- a/src/sheet/sheet-templater.ts +++ b/src/sheet/sheet-templater.ts @@ -9,6 +9,7 @@ import { import { resultSymbol, success } from "./expression/result"; import { Result } from "./expression/result"; import deepmerge from "deepmerge"; +import { builtinFunctions } from "./functions"; export interface TemplatableCell { getTextContent(): string; @@ -56,10 +57,7 @@ export class SheetTemplater { private sheet: Sheet; private functions: Record> = { - helloWorld: createTemplaterNoArgsFunction(() => "hello world"), - testLambda: createTemplaterFunction(z.tuple([z.function()]), (a) => - success(a()), - ), + ...builtinFunctions, }; constructor(