diff --git a/review.md b/review.md new file mode 100644 index 0000000000..bfb437fcec --- /dev/null +++ b/review.md @@ -0,0 +1,75 @@ +# Review — `40a6aebf9 [PERf] evaluation: arg definitions at compile time` + +### Commit + +- Subject tag typo: `[PERf]` → `[PERF]` (Odoo convention is upper-case). + +### Findings + +#### tests/evaluation/evaluation.test.ts:1440,1457,1483 — blocker + +Three `test.only(...)` calls were left in. They suppress every other test in the file. Replace with `test(...)`. + +#### src/formulas/compiler.ts:514 — nit (latent footgun) + +The generated `; // FOO` is safe today only because every FUNCALL arg is reduced to a bare variable via `assignResultToVariable()` at line 505, and the outer use at line 429 wraps it as `return ${expr};` on its own line. If a future change ever inlines a FUNCALL's `returnExpression` into another expression (e.g. `f(${child.returnExpression} + 1)`), the comment will swallow the rest of the parent line and produce invalid JS. Either move the comment to a separate `code.append(...)` line, or leave a short note next to line 514 calling out the invariant. + +#### tests/test_helpers/helpers.ts:91 — nit (naming) + +`const functionMap = functionRegistry.content;` is now identical to `functionsContent` declared one line above — `functionMap` is a leftover from the removed `.mapping` field. Delete `functionMap`/`functionMapRestore` (and the matching restore logic), or rename to something that explains why a second alias exists. + +#### src/plugins/ui_core_views/cell_evaluation/compilation_parameters.ts:48 — nit + +The `as EvalContext` cast replaces what used to be a prototype-chained lookup via `functionMap`. Worth a quick grep for `evalContext[` / `this[` over a function name to confirm no caller still expects that chain. + +--- + +### Naming — alternatives by lens + +#### `PreparedComputeFunction` (`src/types/functions.ts:36`) + +| Lens | Name | Rationale | +| ----------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------- | +| What changed vs. old type | **`ComputeFunction`** (keep the name, replace the signature) | The old `this`-based shape no longer exists — one name, no migration tax. | +| What the type IS structurally | `CtxComputeFunction` | Says exactly what differs: `ctx` is a real parameter, not `this`. | +| What's been done to it | `SpecializedComputeFunction` | It's specialized for a known arg count. | +| Caller's POV (compiled code) | `CompiledCallable` / `EvalCallable` | From the generated formula's POV, it's just a thing you call with `(ctx, …args)`. | +| Lifetime/origin | `RegisteredFunctionImpl` | The actual JS implementation stored per function definition. | + +**Recommendation:** drop the alias and reuse `ComputeFunction`. + +#### `funCallIndex` (`src/formulas/compiler.ts:512`) + +| Lens | Name | +| ----------------------------- | ------------------------------------ | +| What it indexes into | `preparedFunctionIndex` | +| Full word, matches neighbours | `functionCallIndex` | +| Generated-code POV | **`functionsSlot`** / `slotIndex` | +| Minimal | `i` (only used twice within 5 lines) | + +**Recommendation:** `functionsSlot` — the rest of the line (`functions[functionsSlot](ctx, …)`) then reads as one coherent picture. + +#### `preparedFunctions` (array on `CompiledFormula`) + +| Lens | Name | +| ------------------- | ------------------------------------------------------------ | +| Contents | `computeFunctions` | +| Role at runtime | `runtimeFunctions` / `boundFunctions` | +| Relationship to AST | **`callSites`** (one entry per FUNCALL node in source order) | +| Compiler-author POV | `functionTable` | + +**Recommendation:** `callSites` — `preparedFunctions` invites the wrong mental model. The array has one entry per FUNCALL in the source, not one per distinct function: `SUM(SUM(SUM(...)))` produces 3 entries, not 1. + +#### `matrixOnlyIndices` (`src/functions/create_compute_function.ts`) + +| Lens | Name | +| ---------------- | ------------------------------------- | +| What it stores | `matrixOnlyArgIndices` | +| What it's for | `argsRequiringMatrix` | +| Loop readability | keep the name, fix the loop variables | + +**Recommendation:** keep `matrixOnlyIndices`; replace the `k` / `i = matrixOnlyIndices[k]` pair with `for (const argIndex of matrixOnlyIndices) { if (!isMatrix(args[argIndex])) … }`. + +#### `applyVectorization` — optional `argDefinitions` parameter (`src/functions/create_compute_function.ts:57`) + +The new optional `argDefinitions` short-circuits the function's own derivation when passed. The hot path (`vectorizedCompute`) always passes it; the slow path never does. Either make it required (and have the slow-path caller in `module_math.ts`/SUBTOTAL build it once) or rename to **`precomputedArgDefinitions`** to signal the intent. diff --git a/src/formulas/code_builder.ts b/src/formulas/code_builder.ts index ccc925ec3c..fab886699d 100644 --- a/src/formulas/code_builder.ts +++ b/src/formulas/code_builder.ts @@ -3,6 +3,7 @@ */ export interface FunctionCode { readonly returnExpression: JsString; + readonly returnARange: boolean; /** * Return the same function code but with the return expression assigned to a variable. */ @@ -68,11 +69,11 @@ export class FunctionCodeBuilder { } } - return(expression: JsString): FunctionCode { + return(expression: JsString, returnARange: boolean = false): FunctionCode { if (!isSafeJsValue(expression)) { throw new Error(`Expected JsString, got ${expression}`); } - return new FunctionCodeImpl(this.scope, this.code, expression); + return new FunctionCodeImpl(this.scope, this.code, expression, returnARange); } toString(): string { @@ -84,7 +85,8 @@ class FunctionCodeImpl implements FunctionCode { constructor( private readonly scope: Scope, readonly code: JsString[], - readonly returnExpression: JsString + readonly returnExpression: JsString, + readonly returnARange: boolean ) {} assignResultToVariable(): FunctionCode { diff --git a/src/formulas/compiler.ts b/src/formulas/compiler.ts index 11ed3cef06..daf578e7f5 100644 --- a/src/formulas/compiler.ts +++ b/src/formulas/compiler.ts @@ -1,4 +1,8 @@ import { argTargeting } from "../functions/arguments"; +import { + createComputeFunction, + createVectorizedComputeFunction, +} from "../functions/create_compute_function"; import { functionRegistry } from "../functions/function_registry"; import { canBeNamedRangeToken } from "../helpers/formulas"; import { concat, unquote } from "../helpers/misc"; @@ -6,6 +10,7 @@ import { parseNumber } from "../helpers/numbers"; import { _t } from "../translation"; import { CoreGetters } from "../types/core_getters"; import { BadExpressionError, EvaluationError, UnknownFunctionError } from "../types/errors"; +import { PreparedComputeFunction } from "../types/functions"; import { DEFAULT_LOCALE } from "../types/locale"; import { ApplyRangeChange, @@ -55,6 +60,7 @@ export const UNARY_OPERATOR_MAP = { interface ICompiledFormula { execute: FormulaToExecute; + preparedFunctions: PreparedComputeFunction[]; tokens: Token[]; dependencies: string[]; isBadExpression: boolean; @@ -71,6 +77,10 @@ const NO_REAL_VALUE = "__NO_REAL_VALUE__"; // It is only exported for testing purposes export const functionCache: { [key: string]: FormulaToExecute } = {}; +export const preparedFunctionsCache: { + [key: string]: PreparedComputeFunction[]; +} = {}; + const collator = new Intl.Collator("en", { sensitivity: "accent" }); /** @@ -91,7 +101,8 @@ export class CompiledFormula implements Omit 0; this.tokens.forEach((t) => { @@ -227,7 +238,8 @@ export class CompiledFormula implements Omit getters.getRangeFromSheetXC(sheetId, xc)), params.isBadExpression, params.normalizedFormula, - params.execute + params.execute, + params.preparedFunctions ); } } @@ -362,6 +378,8 @@ export type SerializedCompiledFormula = { normalizedFormula: string; }; +type CompiledArg = { argAST: FunctionCode; toVectorize: boolean }; + // ----------------------------------------------------------------------------- // COMPILER // ----------------------------------------------------------------------------- @@ -381,6 +399,7 @@ function compileTokens(tokens: Token[]): ICompiledFormula { execute: function () { return error; }, + preparedFunctions: [], isBadExpression: true, normalizedFormula: tokens.map((t) => t.value).join(""), }; @@ -397,6 +416,7 @@ function compileTokensOrThrow(tokens: Token[]): ICompiledFormula { let stringCount = 0; let numberCount = 0; let dependencyCount = 0; + const preparedFunctions: PreparedComputeFunction[] = []; if (ast.type === "BIN_OPERATION" && ast.value === ":") { throw new BadExpressionError(_t("Invalid formula")); @@ -414,11 +434,13 @@ function compileTokensOrThrow(tokens: Token[]): ICompiledFormula { "range", // same as above, but guarantee that the result is in the form of a range "getSymbolValue", "ctx", + "functions", code.toString() ); // @ts-ignore functionCache[cacheKey] = baseFunction; + preparedFunctionsCache[cacheKey] = preparedFunctions; /** * This function compile the function arguments. It is mostly straightforward, @@ -428,7 +450,7 @@ function compileTokensOrThrow(tokens: Token[]): ICompiledFormula { * the cell value into a range. This allow the grid model to differentiate * between a cell value and a non cell value. */ - function compileFunctionArgs(ast: ASTFuncall): FunctionCode[] { + function compileFunctionArgs(ast: ASTFuncall): CompiledArg[] { const { args } = ast; const functionName = ast.value.toUpperCase(); const functionDefinition = functions[functionName]; @@ -439,18 +461,25 @@ function compileTokensOrThrow(tokens: Token[]): ICompiledFormula { assertEnoughArgs(ast); - const compiledArgs: FunctionCode[] = []; + const compiledArgs: CompiledArg[] = []; const argToFocus = argTargeting(functionDefinition, args.length); for (let i = 0; i < args.length; i++) { const argDefinition = functionDefinition.args[argToFocus[i].index]; const currentArg = args[i]; - const argTypes = argDefinition.type || []; - - const hasRange = argTypes.some((t) => isRangeType(t)); - - compiledArgs.push(compileAST(currentArg, hasRange)); + const argAST = compileAST(currentArg, argDefinition.acceptMatrix); + if (argDefinition.acceptMatrixOnly && !argAST.returnARange) { + throw new BadExpressionError( + _t( + "Function %s expects the parameter '%s' to be reference to a cell or range.", + functionName, + (i + 1).toString() + ) + ); + } + const toVectorize = !argDefinition.acceptMatrix && argAST.returnARange; + compiledArgs.push({ argAST, toVectorize }); } return compiledArgs; @@ -461,7 +490,7 @@ function compileTokensOrThrow(tokens: Token[]): ICompiledFormula { * executable code for the evaluation of the cells content. It uses a cache to * not reevaluate identical code structures. */ - function compileAST(ast: AST, hasRange = false): FunctionCode { + function compileAST(ast: AST, acceptMatrix = false): FunctionCode { const code = new FunctionCodeBuilder(scope); if (ast.debug) { code.append(jsStr`debugger;`); @@ -475,62 +504,61 @@ function compileTokensOrThrow(tokens: Token[]): ICompiledFormula { case "STRING": return code.return(jsStr`this.literalValues.strings[${stringCount++}]`); case "REFERENCE": + const isRange = ast.value.includes(":") || acceptMatrix; return code.return( - jsStr`${ - ast.value.includes(":") || hasRange ? jsStr`range` : jsStr`ref` - }(deps[${dependencyCount++}])` + jsStr`${isRange ? jsStr`range` : jsStr`ref`}(deps[${dependencyCount++}])`, + isRange ); case "FUNCALL": - const args = compileFunctionArgs(ast).map((arg) => arg.assignResultToVariable()); + const compiledArgs = compileFunctionArgs(ast); + const args = compiledArgs.map((compiledArg) => + compiledArg.argAST.assignResultToVariable() + ); code.append(...args); const fnName = ast.value.toUpperCase(); if (!Object.hasOwn(functions, fnName)) { throw new Error(`Unknown function: "${fnName}"`); } const jsFnName = dangerouslyCreateJsStr(fnName); // validated with known functions - return code.return(jsStr`ctx['${jsFnName}'](${args.map((arg) => arg.returnExpression)})`); + const funCallIndex = preparedFunctions.length; + const argsToVectorize = compiledArgs.map((compiledArg) => compiledArg.toVectorize); + if (argsToVectorize.some((toVectorize) => toVectorize)) { + // TODO give argsToVectorize to createVectorizedComputeFunction + preparedFunctions.push(createVectorizedComputeFunction(functions[fnName], args.length)); + } else { + preparedFunctions.push(createComputeFunction(functions[fnName], args.length)); + } + const returnARange = functions[fnName].computeArray !== undefined; + const comment = jsStr`// ${jsFnName}`; + if (args.length === 0) { + return code.return(jsStr`functions[${funCallIndex}](ctx); ${comment}`, returnARange); + } + const compiledArgExpressions = args.map((arg) => arg.returnExpression); + return code.return( + jsStr`functions[${funCallIndex}](ctx,${compiledArgExpressions}); ${comment}`, + returnARange + ); case "ARRAY": { // a literal array is compiled into function calls - const arrayFunctionCall: ASTFuncall = { - type: "FUNCALL", - value: "ARRAY.LITERAL", - args: ast.value.map((row) => ({ - type: "FUNCALL", - value: "ARRAY.ROW", - args: row, - tokenStartIndex: 0, - tokenEndIndex: 0, - })), - tokenStartIndex: 0, - tokenEndIndex: 0, - }; - return compileAST(arrayFunctionCall); + return compileAST( + toFunCallAst( + "ARRAY.LITERAL", + ast.value.map((row) => toFunCallAst("ARRAY.ROW", row)) + ) + ); } case "UNARY_OPERATION": { - if (!Object.hasOwn(UNARY_OPERATOR_MAP, ast.value)) { - throw new Error(`Unknown operator: "${ast.value}"`); - } - const fnName = dangerouslyCreateJsStr(UNARY_OPERATOR_MAP[ast.value]); // validated with known operators - const operand = compileAST(ast.operand, ast.value === "#").assignResultToVariable(); // hasRange is true to avoid vectorization of SPILLED.RANGE - code.append(operand); - return code.return(jsStr`ctx['${fnName}'](${operand.returnExpression})`); + return compileAST(toFunCallAst(UNARY_OPERATOR_MAP[ast.value], [ast.operand])); } case "BIN_OPERATION": { - if (!Object.hasOwn(OPERATOR_MAP, ast.value)) { - throw new Error(`Unknown operator: "${ast.value}"`); - } - const fnName = dangerouslyCreateJsStr(OPERATOR_MAP[ast.value]); // validated with known operators - const left = compileAST(ast.left, false).assignResultToVariable(); - const right = compileAST(ast.right, false).assignResultToVariable(); - code.append(left); - code.append(right); - return code.return( - jsStr`ctx['${fnName}'](${left.returnExpression}, ${right.returnExpression})` - ); + return compileAST(toFunCallAst(OPERATOR_MAP[ast.value], [ast.left, ast.right])); } case "SYMBOL": const symbolIndex = symbols.indexOf(ast.value); - return code.return(jsStr`getSymbolValue(this.symbols[${symbolIndex}], ${hasRange})`); + return code.return( + jsStr`getSymbolValue(this.symbols[${symbolIndex}], ${acceptMatrix})`, + true + ); case "EMPTY": return code.return(jsStr`undefined`); } @@ -538,6 +566,7 @@ function compileTokensOrThrow(tokens: Token[]): ICompiledFormula { } const compiledFormula: ICompiledFormula = { execute: functionCache[cacheKey], + preparedFunctions: preparedFunctionsCache[cacheKey], dependencies, literalValues, symbols, @@ -684,6 +713,12 @@ function assertEnoughArgs(ast: ASTFuncall) { } } -function isRangeType(type: string) { - return type.startsWith("RANGE"); +function toFunCallAst(fnName: string, args: AST[]): ASTFuncall { + return { + type: "FUNCALL", + value: fnName, + args: args, + tokenStartIndex: 0, + tokenEndIndex: 0, + }; } diff --git a/src/functions/create_compute_function.ts b/src/functions/create_compute_function.ts index 11de7cec87..7f775b6117 100644 --- a/src/functions/create_compute_function.ts +++ b/src/functions/create_compute_function.ts @@ -1,16 +1,16 @@ -import { CellValue } from "../types/cells"; import { BadExpressionError, EvaluationError, NotAvailableError } from "../types/errors"; +import { range } from "../helpers/misc"; import { _t } from "../translation"; import { ArgDefinition, - ComputeFunction, EvalContext, FunctionDescription, + PreparedComputeFunction, } from "../types/functions"; import { Arg, FunctionResultObject, isMatrix, Matrix } from "../types/misc"; import { argTargeting } from "./arguments"; -import { isEvaluationError, matrixForEach, matrixMap } from "./helpers"; +import { isEvaluationError, matrixForEach } from "./helpers"; type VectorArgType = "horizontal" | "vertical" | "matrix"; @@ -50,11 +50,12 @@ type VectorArgType = "horizontal" | "vertical" | "matrix"; * - For special behaviors (e.g. the `IF` function), you may declare all arguments * as ranges and invoke this helper directly within your `compute` implementation. */ -export function applyVectorization( +function applyVectorization( context: EvalContext, descr: FunctionDescription, args: Arg[], - acceptToVectorize: boolean[] | undefined = undefined + vectorizableIndices: number[] = range(0, args.length), // if not specified, all arguments are vectorizable + argDefinitions?: ArgDefinition[] ): FunctionResultObject | Matrix { let countVectorizedCol = 1; let countVectorizedRow = 1; @@ -63,39 +64,42 @@ export function applyVectorization( let vectorArgsType: VectorArgType[] | undefined = undefined; - for (let i = 0; i < args.length; i++) { - const arg = args[i]; + for (let k = 0; k < vectorizableIndices.length; k++) { + const argIndex = vectorizableIndices[k]; + const arg = args[argIndex]; - if (isMatrix(arg) && (acceptToVectorize === undefined || acceptToVectorize[i])) { + if (isMatrix(arg)) { const nColumns = arg.length; const nRows = arg[0].length; if (nColumns !== 1 || nRows !== 1) { vectorArgsType ??= new Array(args.length); if (nColumns !== 1 && nRows !== 1) { - vectorArgsType[i] = "matrix"; + vectorArgsType[argIndex] = "matrix"; countVectorizedCol = Math.max(countVectorizedCol, nColumns); countVectorizedRow = Math.max(countVectorizedRow, nRows); vectorizedColLimit = Math.min(vectorizedColLimit, nColumns); vectorizedRowLimit = Math.min(vectorizedRowLimit, nRows); } else if (nColumns !== 1) { - vectorArgsType[i] = "horizontal"; + vectorArgsType[argIndex] = "horizontal"; countVectorizedCol = Math.max(countVectorizedCol, nColumns); vectorizedColLimit = Math.min(vectorizedColLimit, nColumns); } else if (nRows !== 1) { - vectorArgsType[i] = "vertical"; + vectorArgsType[argIndex] = "vertical"; countVectorizedRow = Math.max(countVectorizedRow, nRows); vectorizedRowLimit = Math.min(vectorizedRowLimit, nRows); } } else { - args[i] = arg[0][0]; + args[argIndex] = arg[0][0]; } } } - const argsToFocus = argTargeting(descr, args.length); - const argDefinitions: ArgDefinition[] = new Array(args.length); - for (let k = 0; k < args.length; k++) { - argDefinitions[k] = descr.args[argsToFocus[k].index]; + if (!argDefinitions) { + const argsToFocus = argTargeting(descr, args.length); + argDefinitions = new Array(args.length); + for (let k = 0; k < args.length; k++) { + argDefinitions[k] = descr.args[argsToFocus[k].index]; + } } if (countVectorizedCol === 1 && countVectorizedRow === 1) { @@ -141,9 +145,7 @@ export function applyVectorization( result[col] = column; for (let row = 0; row < countVectorizedRow; row++) { if (col > vectorizedColLimit - 1 || row > vectorizedRowLimit - 1) { - column[row] = new NotAvailableError( - _t("Array arguments to [[FUNCTION_NAME]] are of different size.") - ); + column[row] = new NotAvailableError(_t("Array arguments are of different size.")); continue; } for (let k = 0; k < nbVectorized; k++) { @@ -172,45 +174,6 @@ export function applyVectorization( return result; } -function computeFunctionToObject( - descr: FunctionDescription, - context: EvalContext, - args: Arg[] -): FunctionResultObject | Matrix { - if (context.debug) { - // eslint-disable-next-line no-debugger - debugger; - context.debug = false; - } - // Specialize the call for common arities for performance reasons - const compute = descr.compute; - let result: FunctionResultObject | Matrix | CellValue | Matrix; - switch (args.length) { - case 1: - result = compute.call(context, args[0]); - break; - case 2: - result = compute.call(context, args[0], args[1]); - break; - case 3: - result = compute.call(context, args[0], args[1], args[2]); - break; - default: - // fallback to a generic apply for functions with more than 3 arguments - result = compute.apply(context, args); - } - - if (!isMatrix(result)) { - return isFunctionResultObject(result) ? result : { value: result }; - } - - if (isFunctionResultObject(result[0][0])) { - return result as Matrix; - } - - return matrixMap(result as Matrix, (row) => ({ value: row })); -} - function errorHandlingCompute( descr: FunctionDescription, context: EvalContext, @@ -219,45 +182,61 @@ function errorHandlingCompute( ): Matrix | FunctionResultObject { for (let i = 0; i < args.length; i++) { const arg = args[i]; + const argDefinition = argDefinitions[i]; // Early exit if the argument is an error and the function does not accept errors. // We only check scalar arguments, not matrix arguments for performance reasons. // Casting helpers are responsible for handling errors in matrix arguments. - if (!argDefinitions[i].acceptErrors && !isMatrix(arg) && isEvaluationError(arg?.value)) { + if (!argDefinition.acceptErrors && !isMatrix(arg) && isEvaluationError(arg?.value)) { return arg; } } try { - return computeFunctionToObject(descr, context, args); + const computeFormula = descr.compute !== undefined ? descr.compute : descr.computeArray; + let result: FunctionResultObject | Matrix; + switch (args.length) { + case 1: + result = computeFormula.call(context, args[0]); + break; + case 2: + result = computeFormula.call(context, args[0], args[1]); + break; + case 3: + result = computeFormula.call(context, args[0], args[1], args[2]); + break; + default: + // fallback to a generic apply for functions with more than 3 arguments + result = computeFormula.apply(context, args); + } + return result; } catch (e) { return handleError(e, descr.name); } } -function isFunctionResultObject(obj: unknown): obj is FunctionResultObject { - return typeof obj === "object" && obj !== null && "value" in obj; -} - export function createComputeFunction( - descr: FunctionDescription -): ComputeFunction | FunctionResultObject> { - function vectorizedCompute( - this: EvalContext, - ...args: Arg[] - ): FunctionResultObject | Matrix { + descr: FunctionDescription, + argCount: number +): PreparedComputeFunction { + const functionName = descr.name; + const argsToFocus = argTargeting(descr, argCount); + const argDefinitions: ArgDefinition[] = new Array(argCount); + const matrixOnlyIndices: number[] = []; + for (let i = 0; i < argCount; i++) { + const def = descr.args[argsToFocus[i].index]; + argDefinitions[i] = def; + if (def.acceptMatrixOnly) { + matrixOnlyIndices.push(i); + } + } + function computeFn(evalContext: EvalContext, ...args: Arg[]) { let start = 0; - if (this.__timingEntries) { + if (evalContext.__timingEntries) { start = performance.now(); } - const acceptToVectorize: boolean[] = []; - - const argsToFocus = argTargeting(descr, args.length); - //#region Compute vectorisation limits - for (let i = 0; i < args.length; i++) { - const argIndex = argsToFocus[i].index; - const argDefinition = descr.args[argIndex]; - const arg = args[i]; - if (!isMatrix(arg) && argDefinition.acceptMatrixOnly) { + for (let k = 0; k < matrixOnlyIndices.length; k++) { + const i = matrixOnlyIndices[k]; + if (!isMatrix(args[i])) { throw new BadExpressionError( _t( "Function %s expects the parameter '%s' to be reference to a cell or range.", @@ -266,30 +245,72 @@ export function createComputeFunction( ) ); } - acceptToVectorize.push(!argDefinition.acceptMatrix); } - const result = replaceErrorPlaceholderInResult( - applyVectorization(this, descr, args, acceptToVectorize) + errorHandlingCompute(descr, evalContext, args, argDefinitions), + descr ); - if (this.__timingEntries && this.__originCellPosition) { + if (evalContext.__timingEntries && evalContext.__originCellPosition) { const end = performance.now(); - this.__timingEntries.push({ - functionName: descr.name, - position: this.__originCellPosition, + evalContext.__timingEntries.push({ + functionName, + position: evalContext.__originCellPosition, time: end - start, }); } return result; } - function replaceErrorPlaceholderInResult( - result: FunctionResultObject | Matrix - ): FunctionResultObject | Matrix { - if (!isMatrix(result)) { - replaceFunctionNamePlaceholder(result, descr.name); - } else { - matrixForEach(result, (result) => replaceFunctionNamePlaceholder(result, descr.name)); + return computeFn; +} + +export function createVectorizedComputeFunction( + descr: FunctionDescription, + argCount: number +): PreparedComputeFunction { + const functionName = descr.name; + const argsToFocus = argTargeting(descr, argCount); + const argDefinitions: ArgDefinition[] = new Array(argCount); + const vectorizableIndices: number[] = []; + const matrixOnlyIndices: number[] = []; + for (let i = 0; i < argCount; i++) { + const def = descr.args[argsToFocus[i].index]; + argDefinitions[i] = def; + if (!def.acceptMatrix) { + vectorizableIndices.push(i); + } + if (def.acceptMatrixOnly) { + matrixOnlyIndices.push(i); + } + } + function vectorizedCompute(evalContext: EvalContext, ...args: Arg[]) { + let start = 0; + if (evalContext.__timingEntries) { + start = performance.now(); + } + for (let k = 0; k < matrixOnlyIndices.length; k++) { + const i = matrixOnlyIndices[k]; + if (!isMatrix(args[i])) { + throw new BadExpressionError( + _t( + "Function %s expects the parameter '%s' to be reference to a cell or range.", + descr.name, + (i + 1).toString() + ) + ); + } + } + const result = replaceErrorPlaceholderInResult( + applyVectorization(evalContext, descr, args, vectorizableIndices, argDefinitions), + descr + ); + if (evalContext.__timingEntries && evalContext.__originCellPosition) { + const end = performance.now(); + evalContext.__timingEntries.push({ + functionName, + position: evalContext.__originCellPosition, + time: end - start, + }); } return result; } @@ -297,6 +318,18 @@ export function createComputeFunction( return vectorizedCompute; } +function replaceErrorPlaceholderInResult( + result: FunctionResultObject | Matrix, + descr: FunctionDescription +): FunctionResultObject | Matrix { + if (!isMatrix(result)) { + replaceFunctionNamePlaceholder(result, descr.name); + } else { + matrixForEach(result, (result) => replaceFunctionNamePlaceholder(result, descr.name)); + } + return result; +} + export function handleError(e: unknown, functionName: string): FunctionResultObject { // the error could be a user error (instance of EvaluationError) // or a javascript error (instance of Error) diff --git a/src/functions/function_registry.ts b/src/functions/function_registry.ts index 4236a401bc..647a2876aa 100644 --- a/src/functions/function_registry.ts +++ b/src/functions/function_registry.ts @@ -1,18 +1,11 @@ import { Registry } from "../registry"; -import { AddFunctionDescription, ComputeFunction, FunctionDescription } from "../types/functions"; -import { FunctionResultObject, Matrix } from "../types/misc"; -import { addMetaInfoFromArg, validateArguments } from "./arguments"; -import { createComputeFunction } from "./create_compute_function"; - import { _t } from "../translation"; +import { AddFunctionDescription, FunctionDescription } from "../types/functions"; +import { addMetaInfoFromArg, validateArguments } from "./arguments"; const functionNameRegex = /^[A-Z0-9\_\.]+$/; export class FunctionRegistry extends Registry { - mapping: { - [key: string]: ComputeFunction | FunctionResultObject>; - } = {}; - add(name: string, addDescr: AddFunctionDescription) { name = name.toUpperCase(); if (name in this.content) { @@ -33,7 +26,6 @@ export class FunctionRegistry extends Registry { } const descr = addMetaInfoFromArg(name, addDescr); validateArguments(descr); - this.mapping[name] = createComputeFunction(descr); super.replace(name, descr); return this; } diff --git a/src/functions/module_array.ts b/src/functions/module_array.ts index fd1d737829..e517c3febb 100644 --- a/src/functions/module_array.ts +++ b/src/functions/module_array.ts @@ -10,6 +10,7 @@ import { flattenRowFirst, generateMatrix, isEvaluationError, + matrixMap, toBoolean, toInteger, toMatrix, @@ -101,7 +102,7 @@ export const ARRAY_CONSTRAIN = { arg("rows (number)", _t("The number of rows in the constrained array.")), arg("columns (number)", _t("The number of columns in the constrained array.")), ], - compute: function ( + computeArray: function ( array: Arg, rows: Maybe, columns: Maybe @@ -137,7 +138,7 @@ export const ARRAY_LITERAL = { "Appends ranges vertically and in sequence to return a larger array. All ranges must have the same number of columns." ), args: [arg("range (any, range, repeating)", _t("The range to be appended."))], - compute: function (...ranges: Arg[]) { + computeArray: function (...ranges: Arg[]) { return stackVertically(ranges, { requireSameColCount: true }); }, isExported: false, @@ -152,7 +153,7 @@ export const ARRAY_ROW = { "Appends ranges horizontally and in sequence to return a larger array. All ranges must have the same number of rows." ), args: [arg("range (any, range, repeating)", _t("The range to be appended."))], - compute: function (...ranges: Arg[]) { + computeArray: function (...ranges: Arg[]) { return stackHorizontally(ranges, { requireSameRowCount: true }); }, isExported: false, @@ -171,7 +172,7 @@ export const CHOOSECOLS = { _t("The column index of the column to be returned.") ), ], - compute: function (array: Arg, ...columns: Arg[]) { + computeArray: function (array: Arg, ...columns: Arg[]) { const _array = toMatrix(array); const _columns = flattenRowFirst(columns, (item) => toInteger(item?.value, this.locale)); @@ -213,7 +214,7 @@ export const CHOOSEROWS = { _t("The row index of the row to be returned.") ), ], - compute: function (array: Arg, ...rows: Arg[]) { + computeArray: function (array: Arg, ...rows: Arg[]) { const _array = toMatrix(array); const _rows = flattenRowFirst(rows, (item) => toInteger(item?.value, this.locale)); const _nbColumns = _array.length; @@ -257,7 +258,7 @@ export const EXPAND = { ), arg("pad_with (any, default=0)", _t("The value with which to pad.")), // @compatibility: on Excel, pad with #N/A ], - compute: function ( + computeArray: function ( arg: Arg, rows: Maybe, columns?: Maybe, @@ -299,7 +300,7 @@ export const EXPAND = { export const FLATTEN = { description: _t("Flattens all the values from one or more ranges into a single column."), args: [arg("range (any, range, repeating)", _t("The range to flatten."))], - compute: function (...ranges: Arg[]): Matrix { + computeArray: function (...ranges: Arg[]): Matrix { return [flattenRowFirst(ranges, (val) => (val === undefined ? { value: "" } : val))]; }, isExported: false, @@ -314,10 +315,10 @@ export const FREQUENCY = { arg("data (range)", _t("The array of ranges containing the values to be counted.")), arg("classes (number, range)", _t("The range containing the set of classes.")), ], - compute: function ( + computeArray: function ( data: Matrix, classes: Matrix - ): Matrix { + ) { const _data = flattenRowFirst([data], (data) => data.value).filter( (val): val is number => typeof val === "number" ); @@ -359,7 +360,7 @@ export const FREQUENCY = { const result = sortedClasses .sort((a, b) => a.initialIndex - b.initialIndex) - .map((val) => val.count); + .map((val) => ({ value: val.count })); return [result]; }, isExported: true, @@ -371,7 +372,7 @@ export const FREQUENCY = { export const HSTACK = { description: _t("Appends ranges horizontally and in sequence to return a larger array."), args: [arg("range (any, range, repeating)", _t("The range to be appended."))], - compute: function (...ranges: Arg[]) { + computeArray: function (...ranges: Arg[]) { return stackHorizontally(ranges); }, isExported: true, @@ -397,7 +398,7 @@ export const MDETERM = { _t("The argument square_matrix must have the same number of columns and rows.") ); } - return invertMatrix(_matrix).determinant; + return { value: invertMatrix(_matrix).determinant }; }, isExported: true, } satisfies AddFunctionDescription; @@ -415,7 +416,7 @@ export const MINVERSE = { ) ), ], - compute: function (matrix: Arg) { + computeArray: function (matrix: Arg) { const _matrix = toNumberMatrix(matrix, "square_matrix"); if (!isSquareMatrix(_matrix)) { return new EvaluationError( @@ -426,7 +427,7 @@ export const MINVERSE = { if (!inverted) { return new EvaluationError(_t("The matrix is not invertible.")); } - return inverted; + return matrixMap(inverted, (value) => ({ value })); }, isExported: true, } satisfies AddFunctionDescription; @@ -446,7 +447,7 @@ export const MMULT = { _t("The second matrix in the matrix multiplication operation.") ), ], - compute: function (matrix1: Arg, matrix2: Arg) { + computeArray: function (matrix1: Arg, matrix2: Arg) { const _matrix1 = toNumberMatrix(matrix1, "matrix1"); const _matrix2 = toNumberMatrix(matrix2, "matrix2"); @@ -467,7 +468,7 @@ export const MMULT = { ); } - return multiplyMatrices(_matrix1, _matrix2); + return matrixMap(multiplyMatrices(_matrix1, _matrix2), (value) => ({ value })); }, isExported: true, } satisfies AddFunctionDescription; @@ -505,7 +506,7 @@ export const SUMPRODUCT = { result += product; } } - return result; + return { value: result }; }, isExported: true, } satisfies AddFunctionDescription; @@ -570,7 +571,11 @@ export const SUMX2MY2 = { ), ], compute: function (arrayX: Arg, arrayY: Arg) { - return getSumXAndY(arrayX, arrayY, (x, y) => x ** 2 - y ** 2); + const result = getSumXAndY(arrayX, arrayY, (x, y) => x ** 2 - y ** 2); + if (result instanceof EvaluationError) { + return result; + } + return { value: result }; }, isExported: true, } satisfies AddFunctionDescription; @@ -595,7 +600,11 @@ export const SUMX2PY2 = { ), ], compute: function (arrayX: Arg, arrayY: Arg) { - return getSumXAndY(arrayX, arrayY, (x, y) => x ** 2 + y ** 2); + const result = getSumXAndY(arrayX, arrayY, (x, y) => x ** 2 + y ** 2); + if (result instanceof EvaluationError) { + return result; + } + return { value: result }; }, isExported: true, } satisfies AddFunctionDescription; @@ -620,7 +629,11 @@ export const SUMXMY2 = { ), ], compute: function (arrayX: Arg, arrayY: Arg) { - return getSumXAndY(arrayX, arrayY, (x, y) => (x - y) ** 2); + const result = getSumXAndY(arrayX, arrayY, (x, y) => (x - y) ** 2); + if (result instanceof EvaluationError) { + return result; + } + return { value: result }; }, isExported: true, } satisfies AddFunctionDescription; @@ -672,7 +685,7 @@ function shouldKeepValue(ignore: number): (data: FunctionResultObject) => boolea export const TOCOL = { description: _t("Transforms a range of cells into a single column."), args: TO_COL_ROW_ARGS, - compute: function ( + computeArray: function ( array: Arg, ignore: Maybe = { value: TO_COL_ROW_DEFAULT_IGNORE }, scanByColumn: Maybe = { value: TO_COL_ROW_DEFAULT_SCAN } @@ -698,7 +711,7 @@ export const TOCOL = { export const TOROW = { description: _t("Transforms a range of cells into a single row."), args: TO_COL_ROW_ARGS, - compute: function ( + computeArray: function ( array: Arg, ignore: Maybe = { value: TO_COL_ROW_DEFAULT_IGNORE }, scanByColumn: Maybe = { value: TO_COL_ROW_DEFAULT_SCAN } @@ -725,7 +738,7 @@ export const TOROW = { export const TRANSPOSE = { description: _t("Transposes the rows and columns of a range."), args: [arg("range (any, range)", _t("The range to be transposed."))], - compute: function (arg: Arg): Matrix { + computeArray: function (arg: Arg): Matrix { const _array = toMatrix(arg); const nbColumns = _array[0].length; const nbRows = _array.length; @@ -741,7 +754,7 @@ export const TRANSPOSE = { export const VSTACK = { description: _t("Appends ranges vertically and in sequence to return a larger array."), args: [arg("range (any, range, repeating)", _t("The range to be appended."))], - compute: function (...ranges: Arg[]) { + computeArray: function (...ranges: Arg[]) { return stackVertically(ranges); }, isExported: true, @@ -765,7 +778,7 @@ export const WRAPCOLS = { _t("The value with which to fill the extra cells in the range.") ), ], - compute: function ( + computeArray: function ( range: Arg, wrapCount: Maybe, padWith: Maybe = { value: 0 } @@ -806,7 +819,7 @@ export const WRAPROWS = { _t("The value with which to fill the extra cells in the range.") ), ], - compute: function ( + computeArray: function ( range: Arg, wrapCount: Maybe, padWith: Maybe = { value: 0 } @@ -853,7 +866,7 @@ export const ARRAYTOTEXT = { const _format = toNumber(format, this.locale); const _array = toMatrix(array); if (_format === 1) { - return evaluationResultToDisplayString(_array, "", this.locale); + return { value: evaluationResultToDisplayString(_array, "", this.locale) }; } else if (_format === 0) { const rowSeparator = this.locale.decimalSeparator === "," ? "/" : ","; const arrayStr = transposeMatrix(_array) @@ -863,7 +876,7 @@ export const ARRAYTOTEXT = { }) ) .join(rowSeparator); - return arrayStr; + return { value: arrayStr }; } else { return new EvaluationError(_t("Format must be 0 or 1")); } diff --git a/src/functions/module_database.ts b/src/functions/module_database.ts index 45cca015b5..c8aa0e4cd8 100644 --- a/src/functions/module_database.ts +++ b/src/functions/module_database.ts @@ -184,7 +184,7 @@ export const DCOUNT = { database: Matrix, field: Maybe, criteria: Matrix - ): number { + ) { const cells = getMatchingCells(database, field, criteria, this.locale); return COUNT.compute.bind(this)([cells]); }, @@ -201,7 +201,7 @@ export const DCOUNTA = { database: Matrix, field: Maybe, criteria: Matrix - ): number { + ) { const cells = getMatchingCells(database, field, criteria, this.locale); return COUNTA.compute.bind(this)([cells]); }, @@ -289,7 +289,7 @@ export const DSTDEV = { database: Matrix, field: Maybe, criteria: Matrix - ): number { + ) { const cells = getMatchingCells(database, field, criteria, this.locale); return STDEV.compute.bind(this)([cells]); }, @@ -306,7 +306,7 @@ export const DSTDEVP = { database: Matrix, field: Maybe, criteria: Matrix - ): number { + ) { const cells = getMatchingCells(database, field, criteria, this.locale); return STDEVP.compute.bind(this)([cells]); }, @@ -340,7 +340,7 @@ export const DVAR = { database: Matrix, field: Maybe, criteria: Matrix - ): number { + ) { const cells = getMatchingCells(database, field, criteria, this.locale); return VAR.compute.bind(this)([cells]); }, @@ -357,7 +357,7 @@ export const DVARP = { database: Matrix, field: Maybe, criteria: Matrix - ): number { + ) { const cells = getMatchingCells(database, field, criteria, this.locale); return VARP.compute.bind(this)([cells]); }, diff --git a/src/functions/module_date.ts b/src/functions/module_date.ts index ab8ee841fc..a150b1cf06 100644 --- a/src/functions/module_date.ts +++ b/src/functions/module_date.ts @@ -149,17 +149,18 @@ export const DATEDIF = { } switch (_unit) { case TIME_UNIT.WHOLE_YEARS: - return getTimeDifferenceInWholeYears(jsStartDate, jsEndDate); + return { value: getTimeDifferenceInWholeYears(jsStartDate, jsEndDate) }; case TIME_UNIT.WHOLE_MONTHS: - return getTimeDifferenceInWholeMonths(jsStartDate, jsEndDate); + return { value: getTimeDifferenceInWholeMonths(jsStartDate, jsEndDate) }; case TIME_UNIT.WHOLE_DAYS: { - return getTimeDifferenceInWholeDays(jsStartDate, jsEndDate); + return { value: getTimeDifferenceInWholeDays(jsStartDate, jsEndDate) }; } case TIME_UNIT.MONTH_WITHOUT_WHOLE_YEARS: { - return ( - getTimeDifferenceInWholeMonths(jsStartDate, jsEndDate) - - getTimeDifferenceInWholeYears(jsStartDate, jsEndDate) * 12 - ); + return { + value: + getTimeDifferenceInWholeMonths(jsStartDate, jsEndDate) - + getTimeDifferenceInWholeYears(jsStartDate, jsEndDate) * 12, + }; } case TIME_UNIT.DAYS_WITHOUT_WHOLE_MONTHS: // Using "MD" may get incorrect result in Excel @@ -174,10 +175,10 @@ export const DATEDIF = { const daysInMonthBeforeEndMonth = getDaysInMonth(monthBeforeEndMonth); days = daysInMonthBeforeEndMonth - Math.abs(days); } - return days; + return { value: days }; case TIME_UNIT.DAYS_BETWEEN_NO_MORE_THAN_ONE_YEAR: { if (areTwoDatesWithinOneYear(_startDate, _endDate)) { - return getTimeDifferenceInWholeDays(jsStartDate, jsEndDate); + return { value: getTimeDifferenceInWholeDays(jsStartDate, jsEndDate) }; } const endDateWithinOneYear = new DateTime( jsStartDate.getFullYear(), @@ -189,7 +190,7 @@ export const DATEDIF = { endDateWithinOneYear.setFullYear(jsStartDate.getFullYear() + 1); days = getTimeDifferenceInWholeDays(jsStartDate, endDateWithinOneYear); } - return days; + return { value: days }; } } }, @@ -212,7 +213,7 @@ export const DATEVALUE = { ); } - return Math.trunc(internalDate!.value); + return { value: Math.trunc(internalDate!.value) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -223,8 +224,8 @@ export const DATEVALUE = { export const DAY = { description: _t("Day of the month that a specific date falls on."), args: [arg("date (string)", _t("The date from which to extract the day."))], - compute: function (date: Maybe): number { - return toJsDate(date, this.locale).getDate(); + compute: function (date: Maybe) { + return { value: toJsDate(date, this.locale).getDate() }; }, isExported: true, } satisfies AddFunctionDescription; @@ -239,14 +240,11 @@ export const DAYS = { arg("end_date (date)", _t("The end of the date range.")), arg("start_date (date)", _t("The start of the date range.")), ], - compute: function ( - endDate: Maybe, - startDate: Maybe - ): number { + compute: function (endDate: Maybe, startDate: Maybe) { const _endDate = toJsDate(endDate, this.locale); const _startDate = toJsDate(startDate, this.locale); const dateDif = _endDate.getTime() - _startDate.getTime(); - return Math.round(dateDif / MS_PER_DAY); + return { value: Math.round(dateDif / MS_PER_DAY) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -273,13 +271,13 @@ export const DAYS360 = { startDate: Maybe, endDate: Maybe, method: Maybe = { value: DEFAULT_DAY_COUNT_METHOD } - ): number { + ) { const _startDate = Math.trunc(toNumber(startDate, this.locale)); const _endDate = Math.trunc(toNumber(endDate, this.locale)); const dayCountConvention = toBoolean(method) ? 4 : 0; const yearFrac = getYearFrac(_startDate, _endDate, dayCountConvention); - return Math.sign(_endDate - _startDate) * Math.round(yearFrac * 360); + return { value: Math.sign(_endDate - _startDate) * Math.round(yearFrac * 360) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -296,10 +294,7 @@ export const EDATE = { _t("The number of months before (negative) or after (positive) 'start_date' to calculate.") ), ], - compute: function ( - startDate: Maybe, - months: Maybe - ): FunctionResultNumber { + compute: function (startDate: Maybe, months: Maybe) { const _startDate = toJsDate(startDate, this.locale); const _months = Math.trunc(toNumber(months, this.locale)); @@ -324,10 +319,7 @@ export const EOMONTH = { _t("The number of months before (negative) or after (positive) 'start_date' to consider.") ), ], - compute: function ( - startDate: Maybe, - months: Maybe - ): FunctionResultNumber { + compute: function (startDate: Maybe, months: Maybe) { const _startDate = toJsDate(startDate, this.locale); const _months = Math.trunc(toNumber(months, this.locale)); @@ -348,8 +340,8 @@ export const EOMONTH = { export const HOUR = { description: _t("Hour component of a specific time."), args: [arg("time (date)", _t("The time from which to calculate the hour component."))], - compute: function (date: Maybe): number { - return toJsDate(date, this.locale).getHours(); + compute: function (date: Maybe) { + return { value: toJsDate(date, this.locale).getHours() }; }, isExported: true, } satisfies AddFunctionDescription; @@ -367,7 +359,7 @@ export const ISOWEEKNUM = { ) ), ], - compute: function (date: Maybe): number { + compute: function (date: Maybe) { const _date = toJsDate(date, this.locale); const y = _date.getFullYear(); @@ -437,7 +429,7 @@ export const ISOWEEKNUM = { } const diff = (_date.getTime() - firstDay!.getTime()) / MS_PER_DAY; - return Math.floor(diff / 7) + 1; + return { value: Math.floor(diff / 7) + 1 }; }, isExported: true, } satisfies AddFunctionDescription; @@ -448,8 +440,8 @@ export const ISOWEEKNUM = { export const MINUTE = { description: _t("Minute component of a specific time."), args: [arg("time (date)", _t("The time from which to calculate the minute component."))], - compute: function (date: Maybe): number { - return toJsDate(date, this.locale).getMinutes(); + compute: function (date: Maybe) { + return { value: toJsDate(date, this.locale).getMinutes() }; }, isExported: true, } satisfies AddFunctionDescription; @@ -460,8 +452,8 @@ export const MINUTE = { export const MONTH = { description: _t("Month of the year a specific date falls in"), args: [arg("date (date)", _t("The date from which to extract the month."))], - compute: function (date: Maybe): number { - return toJsDate(date, this.locale).getMonth() + 1; + compute: function (date: Maybe) { + return { value: toJsDate(date, this.locale).getMonth() + 1 }; }, isExported: true, } satisfies AddFunctionDescription; @@ -489,7 +481,7 @@ export const NETWORKDAYS = { startDate: Maybe, endDate: Maybe, holidays: Arg - ): number { + ) { return NETWORKDAYS_INTL.compute.bind(this)(startDate, endDate, { value: 1 }, holidays); }, isExported: true, @@ -612,7 +604,7 @@ export const NETWORKDAYS_INTL = { endDate: Maybe, weekend: Maybe = { value: DEFAULT_WEEKEND }, holidays: Arg - ): number { + ) { const _startDate = toJsDate(startDate, this.locale); const _endDate = toJsDate(endDate, this.locale); const daysWeekend = weekendToDayNumber(weekend); @@ -640,7 +632,7 @@ export const NETWORKDAYS_INTL = { timeStepDate = stepDate.getTime(); } - return invertDate ? -netWorkingDay : netWorkingDay; + return { value: invertDate ? -netWorkingDay : netWorkingDay }; }, isExported: true, } satisfies AddFunctionDescription; @@ -670,8 +662,8 @@ export const NOW = { export const SECOND = { description: _t("Minute component of a specific time."), args: [arg("time (date)", _t("The time from which to calculate the second component."))], - compute: function (date: Maybe): number { - return toJsDate(date, this.locale).getSeconds(); + compute: function (date: Maybe) { + return { value: toJsDate(date, this.locale).getSeconds() }; }, isExported: true, } satisfies AddFunctionDescription; @@ -732,7 +724,7 @@ export const TIMEVALUE = { } const result = internalDate!.value - Math.trunc(internalDate!.value); - return result < 0 ? 1 + result : result; + return { value: result < 0 ? 1 + result : result }; }, isExported: true, } satisfies AddFunctionDescription; @@ -800,16 +792,16 @@ export const WEEKDAY = { } switch (_type) { case 1: - return m + 1; + return { value: m + 1 }; case 2: - return m === 0 ? 7 : m; + return { value: m === 0 ? 7 : m }; case 3: - return m === 0 ? 6 : m - 1; + return { value: m === 0 ? 6 : m - 1 }; } const delta = _type - 10; const result = (m + 1 - delta + 7) % 7; // +7 to avoid applying modulo on negative numbers - return result === 0 ? 7 : result; + return { value: result === 0 ? 7 : result }; }, isExported: true, } satisfies AddFunctionDescription; @@ -878,9 +870,9 @@ export const WEEKNUM = { const dif = (_date.getTime() - startDayOfFirstWeek.getTime()) / MS_PER_DAY; if (dif < 0) { - return 1; + return { value: 1 }; } - return Math.floor(dif / 7) + (dayStart === 1 ? 1 : 2); + return { value: Math.floor(dif / 7) + (dayStart === 1 ? 1 : 2) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -984,8 +976,8 @@ export const WORKDAY_INTL = { export const YEAR = { description: _t("Year specified by a given date."), args: [arg("date (date)", _t("The date from which to extract the year."))], - compute: function (date: Maybe): number { - return toJsDate(date, this.locale).getFullYear(); + compute: function (date: Maybe) { + return { value: toJsDate(date, this.locale).getFullYear() }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1036,7 +1028,7 @@ export const YEARFRAC = { ); } - return getYearFrac(_startDate, _endDate, _dayCountConvention); + return { value: getYearFrac(_startDate, _endDate, _dayCountConvention) }; }, } satisfies AddFunctionDescription; @@ -1064,7 +1056,7 @@ export const MONTH_START = { export const MONTH_END = { description: _t("Last day of the month following a date."), args: [arg("date (date)", _t("The date from which to calculate the result."))], - compute: function (date: Maybe): FunctionResultNumber { + compute: function (date: Maybe) { return EOMONTH.compute.bind(this)(date, { value: 0 }); }, } satisfies AddFunctionDescription; @@ -1075,8 +1067,8 @@ export const MONTH_END = { export const QUARTER = { description: _t("Quarter of the year a specific date falls in"), args: [arg("date (date)", _t("The date from which to extract the quarter."))], - compute: function (date: Maybe): number { - return Math.ceil((toJsDate(date, this.locale).getMonth() + 1) / 3); + compute: function (date: Maybe) { + return { value: Math.ceil((toJsDate(date, this.locale).getMonth() + 1) / 3) }; }, } satisfies AddFunctionDescription; @@ -1086,9 +1078,9 @@ export const QUARTER = { export const QUARTER_START = { description: _t("First day of the quarter of the year a specific date falls in."), args: [arg("date (date)", _t("The date from which to calculate the start of quarter."))], - compute: function (date: Maybe): FunctionResultNumber { - const quarter = QUARTER.compute.bind(this)(date); - const year = YEAR.compute.bind(this)(date); + compute: function (date: Maybe) { + const quarter = QUARTER.compute.bind(this)(date).value; + const year = YEAR.compute.bind(this)(date).value; const jsDate = new DateTime(year, (quarter - 1) * 3, 1); return { value: jsDateToRoundNumber(jsDate), @@ -1104,8 +1096,8 @@ export const QUARTER_END = { description: _t("Last day of the quarter of the year a specific date falls in."), args: [arg("date (date)", _t("The date from which to calculate the end of quarter."))], compute: function (date: Maybe): FunctionResultNumber { - const quarter = QUARTER.compute.bind(this)(date); - const year = YEAR.compute.bind(this)(date); + const quarter = QUARTER.compute.bind(this)(date).value; + const year = YEAR.compute.bind(this)(date).value; const jsDate = new DateTime(year, quarter * 3, 0); return { value: jsDateToRoundNumber(jsDate), @@ -1121,7 +1113,7 @@ export const YEAR_START = { description: _t("First day of the year a specific date falls in."), args: [arg("date (date)", _t("The date from which to calculate the start of the year."))], compute: function (date: Maybe): FunctionResultNumber { - const year = YEAR.compute.bind(this)(date); + const year = YEAR.compute.bind(this)(date).value; const jsDate = new DateTime(year, 0, 1); return { value: jsDateToRoundNumber(jsDate), @@ -1137,7 +1129,7 @@ export const YEAR_END = { description: _t("Last day of the year a specific date falls in."), args: [arg("date (date)", _t("The date from which to calculate the end of the year."))], compute: function (date: Maybe): FunctionResultNumber { - const year = YEAR.compute.bind(this)(date); + const year = YEAR.compute.bind(this)(date).value; const jsDate = new DateTime(year + 1, 0, 0); return { value: jsDateToRoundNumber(jsDate), diff --git a/src/functions/module_engineering.ts b/src/functions/module_engineering.ts index 88e289e0b4..d97238217f 100644 --- a/src/functions/module_engineering.ts +++ b/src/functions/module_engineering.ts @@ -18,10 +18,10 @@ export const DELTA = { compute: function ( number1: Maybe, number2: Maybe = { value: DEFAULT_DELTA_ARG } - ): number { + ) { const _number1 = toNumber(number1, this.locale); const _number2 = toNumber(number2, this.locale); - return _number1 === _number2 ? 1 : 0; + return { value: _number1 === _number2 ? 1 : 0 }; }, isExported: true, } satisfies AddFunctionDescription; diff --git a/src/functions/module_filter.ts b/src/functions/module_filter.ts index 1ae312d103..75340f3ddb 100644 --- a/src/functions/module_filter.ts +++ b/src/functions/module_filter.ts @@ -106,7 +106,7 @@ export const FILTER = { _t("Column or row containing true or false values corresponding to the range.") ), ], - compute: function (range: Arg, ...conditions: Arg[]) { + computeArray: function (range: Arg, ...conditions: Arg[]) { let _array = toMatrix(range); const _conditionsMatrices = conditions.map((cond) => matrixMap(toMatrix(cond), (data) => data.value) @@ -170,7 +170,7 @@ export const SORT: AddFunctionDescription = { ] ), ], - compute: function ( + computeArray: function ( range: Matrix, ...sortingCriteria: Arg[] ): Matrix { @@ -315,7 +315,7 @@ export const UNIQUE = { ] ), ], - compute: function ( + computeArray: function ( range: Arg = { value: "" }, byColumn: Maybe, exactlyOnce: Maybe diff --git a/src/functions/module_financial.ts b/src/functions/module_financial.ts index 1f8ea05597..38b01de691 100644 --- a/src/functions/module_financial.ts +++ b/src/functions/module_financial.ts @@ -215,7 +215,7 @@ export const ACCRINTM = { } const yearFrac = getYearFrac(start, end, _dayCountConvention); - return _redemption * _rate * yearFrac; + return { value: _redemption * _rate * yearFrac }; }, isExported: true, } satisfies AddFunctionDescription; @@ -310,9 +310,9 @@ export const AMORLINC = { const valueAtPeriod = _cost - firstDeprec - deprec * roundedPeriod; if (valueAtPeriod >= _salvage) { - return roundedPeriod === 0 ? firstDeprec : deprec; + return { value: roundedPeriod === 0 ? firstDeprec : deprec }; } - return _salvage - valueAtPeriod < deprec ? deprec - (_salvage - valueAtPeriod) : 0; + return { value: _salvage - valueAtPeriod < deprec ? deprec - (_salvage - valueAtPeriod) : 0 }; }, isExported: true, } satisfies AddFunctionDescription; @@ -359,11 +359,11 @@ export const COUPDAYS = { frequency, dayCountConvention ).value; - return toNumber(after, this.locale) - toNumber(before, this.locale); + return { value: toNumber(after, this.locale) - toNumber(before, this.locale) }; } const daysInYear = _dayCountConvention === 3 ? 365 : 360; - return daysInYear / _frequency; + return { value: daysInYear / _frequency }; }, isExported: true, } satisfies AddFunctionDescription; @@ -404,12 +404,12 @@ export const COUPDAYBS = { ).value; const _couponBeforeStart = toNumber(couponBeforeStart, this.locale); if ([1, 2, 3].includes(_dayCountConvention)) { - return start - _couponBeforeStart; + return { value: start - _couponBeforeStart }; } if (_dayCountConvention === 4) { const yearFrac = getYearFrac(_couponBeforeStart, start, _dayCountConvention); - return Math.round(yearFrac * 360); + return { value: Math.round(yearFrac * 360) }; } const startDate = toJsDate(start, this.locale); @@ -446,7 +446,7 @@ export const COUPDAYBS = { d1 = 30; } - return (y2 - y1) * 360 + (m2 - m1) * 30 + (d2 - d1); + return { value: (y2 - y1) * 360 + (m2 - m1) * 30 + (d2 - d1) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -487,12 +487,12 @@ export const COUPDAYSNC = { ).value; const _couponAfterStart = toNumber(couponAfterStart, this.locale); if ([1, 2, 3].includes(_dayCountConvention)) { - return _couponAfterStart - start; + return { value: _couponAfterStart - start }; } if (_dayCountConvention === 4) { const yearFrac = getYearFrac(start, _couponAfterStart, _dayCountConvention); - return Math.round(yearFrac * 360); + return { value: Math.round(yearFrac * 360) }; } const coupDayBs = COUPDAYBS.compute.bind(this)( @@ -507,7 +507,7 @@ export const COUPDAYSNC = { frequency, dayCountConvention ); - return toNumber(coupDays, this.locale) - toNumber(coupDayBs, this.locale); + return { value: toNumber(coupDays, this.locale) - toNumber(coupDayBs, this.locale) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -594,7 +594,7 @@ export const COUPNUM = { ); num++; } - return num - 1; + return { value: num - 1 }; }, isExported: true, } satisfies AddFunctionDescription; @@ -629,7 +629,12 @@ export const COUPPCD = { const monthsPerPeriod = 12 / _frequency; - const coupNum = COUPNUM.compute.bind(this)(settlement, maturity, frequency, dayCountConvention); + const coupNum = COUPNUM.compute.bind(this)( + settlement, + maturity, + frequency, + dayCountConvention + ).value; const date = addMonthsToDate(toJsDate(end, this.locale), -coupNum * monthsPerPeriod, true); return { value: jsDateToRoundNumber(date), @@ -704,7 +709,7 @@ export const CUMIPMT = { cumSum += impt(r, i, n, pv, 0, type); } - return cumSum; + return { value: cumSum }; }, isExported: true, } satisfies AddFunctionDescription; @@ -774,7 +779,7 @@ export const CUMPRINC = { cumSum += ppmt(r, i, n, pv, 0, type); } - return cumSum; + return { value: cumSum }; }, isExported: true, } satisfies AddFunctionDescription; @@ -997,7 +1002,7 @@ export const DISC = { * redemption DSM */ const yearsFrac = getYearFrac(_settlement, _maturity, _dayCountConvention); - return (_redemption - _price) / _redemption / yearsFrac; + return { value: (_redemption - _price) / _redemption / yearsFrac }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1030,7 +1035,7 @@ export const DOLLARDE = { const frac = 10 ** Math.ceil(Math.log10(_unit)) / _unit; - return truncatedPrice + priceFractionalPart * frac; + return { value: truncatedPrice + priceFractionalPart * frac }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1060,7 +1065,7 @@ export const DOLLARFR = { const frac = _unit / 10 ** Math.ceil(Math.log10(_unit)); - return truncatedPrice + priceFractionalPart * frac; + return { value: truncatedPrice + priceFractionalPart * frac }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1145,7 +1150,7 @@ export const DURATION = { count += presentValuePerPeriod; } - return count === 0 ? 0 : sum / count; + return { value: count === 0 ? 0 : sum / count }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1174,7 +1179,7 @@ export const EFFECT = { } // https://en.wikipedia.org/wiki/Nominal_interest_rate#Nominal_versus_effective_interest_rate - return Math.pow(1 + nominal / periods, periods) - 1; + return { value: Math.pow(1 + nominal / periods, periods) - 1 }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1244,11 +1249,13 @@ export const FVSCHEDULE = { ], compute: function (principalAmount: Maybe, rateSchedule: Arg) { const principal = toNumber(principalAmount, this.locale); - return reduceAny( - [rateSchedule], - (acc, rate) => acc * (1 + toNumber(rate, this.locale)), - principal - ); + return { + value: reduceAny( + [rateSchedule], + (acc, rate) => acc * (1 + toNumber(rate, this.locale)), + principal + ), + }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1313,7 +1320,7 @@ export const INTRATE = { * YEARFRAC(settlement, maturity, basis) */ const yearFrac = getYearFrac(_settlement, _maturity, _dayCountConvention); - return (_redemption - _investment) / _investment / yearFrac; + return { value: (_redemption - _investment) / _investment / yearFrac }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1483,7 +1490,7 @@ export const ISPMT = { } const currentInvestment = investment - investment * (period / nOfPeriods); - return -1 * currentInvestment * interestRate; + return { value: -1 * currentInvestment * interestRate }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1534,7 +1541,7 @@ export const MDURATION = { ); const y = toNumber(securityYield, this.locale); const k = Math.trunc(toNumber(frequency, this.locale)); - return toNumber(duration, this.locale) / (1 + y / k); + return { value: toNumber(duration, this.locale) / (1 + y / k) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1606,7 +1613,7 @@ export const MIRR = { } const exponent = 1 / (n - 1); - return (-fv / pv) ** exponent - 1; + return { value: (-fv / pv) ** exponent - 1 }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1635,7 +1642,7 @@ export const NOMINAL = { } // https://en.wikipedia.org/wiki/Nominal_interest_rate#Nominal_versus_effective_interest_rate - return (Math.pow(effective + 1, 1 / periods) - 1) * periods; + return { value: (Math.pow(effective + 1, 1 / periods) - 1) * periods }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1689,10 +1696,10 @@ export const NPER = { * <=> log[(C - fv) / (pv + C)] = N * log(R) */ if (r === 0) { - return -(fv + pv) / p; + return { value: -(fv + pv) / p }; } const c = (p * (1 + r * t)) / r; - return Math.log((c - fv) / (pv + c)) / Math.log(1 + r); + return { value: Math.log((c - fv) / (pv + c)) / Math.log(1 + r) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1767,7 +1774,7 @@ export const PDURATION = { return new EvaluationError(expectFutureValueStrictlyPositive(_futureValue)); } - return (Math.log(_futureValue) - Math.log(_presentValue)) / Math.log(1 + _rate); + return { value: (Math.log(_futureValue) - Math.log(_presentValue)) / Math.log(1 + _rate) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -2019,10 +2026,11 @@ export const PRICE = { const cashFlowFromCoupon = (100 * _rate) / _frequency; if (nbrFullCoupons === 1) { - return ( - (cashFlowFromCoupon + _redemption) / ((timeFirstCoupon * _yield) / _frequency + 1) - - cashFlowFromCoupon * (1 - timeFirstCoupon) - ); + return { + value: + (cashFlowFromCoupon + _redemption) / ((timeFirstCoupon * _yield) / _frequency + 1) - + cashFlowFromCoupon * (1 - timeFirstCoupon), + }; } let cashFlowsPresentValue = 0; @@ -2034,9 +2042,10 @@ export const PRICE = { const redemptionPresentValue = _redemption / yieldFactorPerPeriod ** (nbrFullCoupons - 1 + timeFirstCoupon); - return ( - redemptionPresentValue + cashFlowsPresentValue - cashFlowFromCoupon * (1 - timeFirstCoupon) - ); + return { + value: + redemptionPresentValue + cashFlowsPresentValue - cashFlowFromCoupon * (1 - timeFirstCoupon), + }; }, isExported: true, } satisfies AddFunctionDescription; @@ -2104,7 +2113,7 @@ export const PRICEDISC = { * PRICEDISC = redemption - discount * redemption * (DSM/B) */ const yearsFrac = getYearFrac(_settlement, _maturity, _dayCountConvention); - return _redemption - _discount * _redemption * yearsFrac; + return { value: _redemption - _discount * _redemption * yearsFrac }; }, isExported: true, } satisfies AddFunctionDescription; @@ -2203,7 +2212,7 @@ export const PRICEMAT = { const numerator = 100 + issueToMaturity * _rate * 100; const denominator = 1 + settlementToMaturity * _yield; const term2 = issueToSettlement * _rate * 100; - return numerator / denominator - term2; + return { value: numerator / denominator - term2 }; }, isExported: true, } satisfies AddFunctionDescription; @@ -2355,7 +2364,7 @@ export const RECEIVED = { * The ratio DSM/B can be computed with the YEARFRAC function to take the dayCountConvention into account. */ const yearsFrac = getYearFrac(_settlement, _maturity, _dayCountConvention); - return _investment / (1 - _discount * yearsFrac); + return { value: _investment / (1 - _discount * yearsFrac) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -2390,7 +2399,7 @@ export const RRI = { * * RRI = (future value / present value) ^ (1 / number of periods) - 1 */ - return (fv / pv) ** (1 / n) - 1; + return { value: (fv / pv) ** (1 / n) - 1 }; }, isExported: true, } satisfies AddFunctionDescription; @@ -2535,7 +2544,7 @@ export const TBILLPRICE = { return new EvaluationError(expectDiscountStrictlySmallerThanOne(disc)); } - return tBillPrice(start, end, disc); + return { value: tBillPrice(start, end, disc) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -2607,9 +2616,9 @@ export const TBILLEQ = { * */ - const nDays = DAYS.compute.bind(this)({ value: end }, { value: start }); + const nDays = DAYS.compute.bind(this)({ value: end }, { value: start }).value; if (nDays <= 182) { - return (365 * disc) / (360 - disc * nDays); + return { value: (365 * disc) / (360 - disc * nDays) }; } const p = tBillPrice(start, end, disc) / 100; @@ -2619,7 +2628,7 @@ export const TBILLEQ = { const num = -2 * x + 2 * Math.sqrt(x ** 2 - (2 * x - 1) * (1 - 1 / p)); const denom = 2 * x - 1; - return num / denom; + return { value: num / denom }; }, isExported: true, } satisfies AddFunctionDescription; @@ -2675,7 +2684,7 @@ export const TBILLYIELD = { */ const yearFrac = getYearFrac(start, end, 2); - return ((100 - p) / p) * (1 / yearFrac); + return { value: ((100 - p) / p) * (1 / yearFrac) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -2757,15 +2766,15 @@ export const VDB = { } if (_cost === 0) { - return 0; + return { value: 0 }; } if (_salvage >= _cost) { - return _startPeriod < 1 ? _cost - _salvage : 0; + return { value: _startPeriod < 1 ? _cost - _salvage : 0 }; } const doubleDeprecFactor = _factor / _life; if (doubleDeprecFactor >= 1) { - return _startPeriod < 1 ? _cost - _salvage : 0; + return { value: _startPeriod < 1 ? _cost - _salvage : 0 }; } let previousCost = _cost; @@ -2795,7 +2804,7 @@ export const VDB = { previousCost = nextCost; } - return resultDeprec; + return { value: resultDeprec }; }, isExported: true, } satisfies AddFunctionDescription; @@ -2895,7 +2904,7 @@ export const XIRR = { return previousFallback / 10 - 0.9; }; - return newtonMethod(func, derivFunc, guess, 40, 1e-5, nanFallback); + return { value: newtonMethod(func, derivFunc, guess, 40, 1e-5, nanFallback) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -2942,7 +2951,7 @@ export const XNPV = { } if (_cashFlows.length === 1) { - return _cashFlows[0]; + return { value: _cashFlows[0] }; } // aggregate values of the same date @@ -2977,7 +2986,7 @@ export const XNPV = { const dateDiff = (dates[0] - dates[i]) / 365; pv += values[i] * (1 + rate) ** dateDiff; } - return pv; + return { value: pv }; }, isExported: true, } satisfies AddFunctionDescription; @@ -3061,10 +3070,11 @@ export const YIELD = { if (nbrFullCoupons === 1) { const subPart = _price + cashFlowFromCoupon * (1 - timeFirstCoupon); - return ( - ((_redemption + cashFlowFromCoupon - subPart) * _frequency * (1 / timeFirstCoupon)) / - subPart - ); + return { + value: + ((_redemption + cashFlowFromCoupon - subPart) * _frequency * (1 / timeFirstCoupon)) / + subPart, + }; } // The result of YIELD function is the yield at which the PRICE function will return the given price. @@ -3129,7 +3139,7 @@ export const YIELD = { const initYieldFactorPerPeriod = 1 + initYield / _frequency; const methodResult = newtonMethod(func, derivFunc, initYieldFactorPerPeriod, 100, 1e-5); - return (methodResult - 1) * _frequency; + return { value: (methodResult - 1) * _frequency }; }, isExported: true, } satisfies AddFunctionDescription; @@ -3195,7 +3205,7 @@ export const YIELDDISC = { * YEARFRAC(settlement, maturity, basis) */ const yearFrac = getYearFrac(_settlement, _maturity, _dayCountConvention); - return (_redemption / _price - 1) / yearFrac; + return { value: (_redemption / _price - 1) / yearFrac }; }, isExported: true, } satisfies AddFunctionDescription; @@ -3267,7 +3277,7 @@ export const YIELDMAT = { const numerator = (100 * (1 + _rate * issueToMaturity)) / (_price + 100 * _rate * issueToSettlement) - 1; - return numerator / settlementToMaturity; + return { value: numerator / settlementToMaturity }; }, isExported: true, } satisfies AddFunctionDescription; diff --git a/src/functions/module_info.ts b/src/functions/module_info.ts index eb7b53951a..dc3599fcfd 100644 --- a/src/functions/module_info.ts +++ b/src/functions/module_info.ts @@ -57,34 +57,36 @@ export const CELL = { this.__originSheetId === position.sheetId ? "" : this.getters.getSheetName(position.sheetId) + "!"; - return sheetName + toXC(position.col, position.row, { colFixed: true, rowFixed: true }); + return { + value: sheetName + toXC(position.col, position.row, { colFixed: true, rowFixed: true }), + }; case "col": - return position.col + 1; + return { value: position.col + 1 }; case "contents": { - return firstReference.value; + return { value: firstReference.value }; } case "format": { - return firstReference.format || ""; + return { value: firstReference.format || "" }; } case "row": - return position.row + 1; + return { value: position.row + 1 }; case "type": { // take the same logic as `_createEvaluatedCell` function if (firstReference.value === null) { - return "b"; // blank + return { value: "b" }; // blank } if (isTextFormat(firstReference.format)) { - return "l"; // label + return { value: "l" }; // label } if (typeof firstReference.value === "number" || typeof firstReference.value === "boolean") { - return "v"; // value + return { value: "v" }; // value } - return "l"; // label + return { value: "l" }; // label } } - return ""; + return { value: "" }; }, isExported: true, } satisfies AddFunctionDescription; @@ -95,9 +97,9 @@ export const CELL = { export const ISERR = { description: _t("Whether a value is an error other than #N/A."), args: [arg("value (any)", _t("The value to be verified as an error type."))], - compute: function (data: Maybe): boolean { + compute: function (data: Maybe) { const value = data?.value; - return isEvaluationError(value) && value !== CellErrorType.NotAvailable; + return { value: isEvaluationError(value) && value !== CellErrorType.NotAvailable }; }, isExported: true, } satisfies AddFunctionDescription; @@ -108,9 +110,9 @@ export const ISERR = { export const ISERROR = { description: _t("Whether a value is an error."), args: [arg("value (any)", _t("The value to be verified as an error type."))], - compute: function (data: Maybe): boolean { + compute: function (data: Maybe) { const value = data?.value; - return isEvaluationError(value); + return { value: isEvaluationError(value) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -121,8 +123,8 @@ export const ISERROR = { export const ISLOGICAL = { description: _t("Whether a value is `true` or `false`."), args: [arg("value (any)", _t("The value to be verified as a logical TRUE or FALSE."))], - compute: function (value: Maybe): boolean { - return typeof value?.value === "boolean"; + compute: function (value: Maybe) { + return { value: typeof value?.value === "boolean" }; }, isExported: true, } satisfies AddFunctionDescription; @@ -133,8 +135,8 @@ export const ISLOGICAL = { export const ISNA = { description: _t("Whether a value is the error #N/A."), args: [arg("value (any)", _t("The value to be verified as an error type."))], - compute: function (data: Maybe): boolean { - return data?.value === CellErrorType.NotAvailable; + compute: function (data: Maybe) { + return { value: data?.value === CellErrorType.NotAvailable }; }, isExported: true, } satisfies AddFunctionDescription; @@ -145,8 +147,8 @@ export const ISNA = { export const ISNONTEXT = { description: _t("Whether a value is non-textual."), args: [arg("value (any)", _t("The value to be checked."))], - compute: function (value: Maybe): boolean { - return !ISTEXT.compute.bind(this)(value); + compute: function (value: Maybe) { + return { value: !ISTEXT.compute.bind(this)(value).value }; }, isExported: true, } satisfies AddFunctionDescription; @@ -158,8 +160,8 @@ export const ISNONTEXT = { export const ISNUMBER = { description: _t("Whether a value is a number."), args: [arg("value (any)", _t("The value to be verified as a number."))], - compute: function (value: Maybe): boolean { - return typeof value?.value === "number"; + compute: function (value: Maybe) { + return { value: typeof value?.value === "number" }; }, isExported: true, } satisfies AddFunctionDescription; @@ -170,8 +172,8 @@ export const ISNUMBER = { export const ISTEXT = { description: _t("Whether a value is text."), args: [arg("value (any)", _t("The value to be verified as text."))], - compute: function (value: Maybe): boolean { - return typeof value?.value === "string" && isEvaluationError(value?.value) === false; + compute: function (value: Maybe) { + return { value: typeof value?.value === "string" && isEvaluationError(value?.value) === false }; }, isExported: true, } satisfies AddFunctionDescription; @@ -182,8 +184,8 @@ export const ISTEXT = { export const ISBLANK = { description: _t("Whether the referenced cell is empty"), args: [arg("value (any)", _t("Reference to the cell that will be checked for emptiness."))], - compute: function (value: Maybe): boolean { - return value?.value === null; + compute: function (value: Maybe) { + return { value: value?.value === null }; }, isExported: true, } satisfies AddFunctionDescription; @@ -194,7 +196,7 @@ export const ISBLANK = { export const NA = { description: _t("Returns the error value #N/A."), args: [], - compute: function (): FunctionResultObject { + compute: function () { return { value: CellErrorType.NotAvailable }; }, isExported: true, @@ -213,7 +215,7 @@ export const ISFORMULA = { return new InvalidReferenceError(expectReferenceError); } const cell = this.getters.getCell(cellReference.position); - return cell?.isFormula ?? false; + return { value: cell?.isFormula ?? false }; }, isExported: true, } satisfies AddFunctionDescription; diff --git a/src/functions/module_logical.ts b/src/functions/module_logical.ts index 2fce223b14..09a41321e4 100644 --- a/src/functions/module_logical.ts +++ b/src/functions/module_logical.ts @@ -3,8 +3,7 @@ import { CellErrorType, EvaluationError } from "../types/errors"; import { AddFunctionDescription } from "../types/functions"; import { Arg, FunctionResultObject, Maybe } from "../types/misc"; import { arg } from "./arguments"; -import { applyVectorization } from "./create_compute_function"; -import { functionRegistry } from "./function_registry"; +import { createVectorizedComputeFunction } from "./create_compute_function"; import { boolAnd, boolOr } from "./helper_logical"; import { isMultipleElementMatrix, toScalar } from "./helper_matrices"; import { @@ -33,7 +32,7 @@ export const AND = { if (!foundBoolean) { return new EvaluationError(noValidInputErrorMessage); } - return result; + return { value: result }; }, isExported: true, } satisfies AddFunctionDescription; @@ -44,8 +43,8 @@ export const AND = { export const FALSE: AddFunctionDescription = { description: _t("Logical value `false`."), args: [], - compute: function (): boolean { - return false; + compute: function () { + return { value: false }; }, isExported: true, }; @@ -71,13 +70,14 @@ export const IF = { _t("The value the function returns if logical_expression is FALSE.") ), ], - compute: function (logicalExpression: Arg, valueIfTrue: Arg, valueIfFalse: Arg) { + computeArray: function (logicalExpression: Arg, valueIfTrue: Arg, valueIfFalse: Arg) { if (isMultipleElementMatrix(logicalExpression)) { - return applyVectorization(this, functionRegistry.get("IF"), [ + return createVectorizedComputeFunction(IF, 3)( + this, logicalExpression, valueIfTrue, - valueIfFalse, - ]); + valueIfFalse + ); } const result = toBoolean(toScalar(logicalExpression)) ? valueIfTrue : valueIfFalse; return result ?? { value: 0 }; @@ -97,9 +97,9 @@ export const IFERROR = { _t("The value the function returns if value is an error.") ), ], - compute: function (value: Arg, valueIfError: Arg) { + computeArray: function (value: Arg, valueIfError: Arg) { if (isMultipleElementMatrix(value)) { - return applyVectorization(this, functionRegistry.get("IFERROR"), [value, valueIfError]); + return createVectorizedComputeFunction(IFERROR, 2)(this, value, valueIfError); } const result = isEvaluationError(toScalar(value)?.value) ? valueIfError : value; return result ?? { value: 0 }; @@ -119,9 +119,9 @@ export const IFNA = { _t("The value the function returns if value is an #N/A error.") ), ], - compute: function (value: Arg, valueIfError: Arg) { + computeArray: function (value: Arg, valueIfError: Arg) { if (isMultipleElementMatrix(value)) { - return applyVectorization(this, functionRegistry.get("IFNA"), [value, valueIfError]); + return createVectorizedComputeFunction(IFNA, 2)(this, value, valueIfError); } const result = toScalar(value)?.value === CellErrorType.NotAvailable ? valueIfError : value; return result ?? { value: 0 }; @@ -146,7 +146,7 @@ export const IFS = { _t("The value to be returned if its corresponding condition is TRUE.") ), ], - compute: function (...values: Arg[]) { + computeArray: function (...values: Arg[]) { if (values.length % 2 !== 0) { return new EvaluationError( _t("Wrong number of arguments. Expected an even number of arguments.") @@ -154,7 +154,7 @@ export const IFS = { } while (values.length > 0) { if (isMultipleElementMatrix(values[0])) { - return applyVectorization(this, functionRegistry.get("IFS"), values); + return createVectorizedComputeFunction(IFS, values.length)(this, ...values); } const condition = toBoolean(toScalar(values.shift())); const valueIfTrue = values.shift(); @@ -180,8 +180,8 @@ export const NOT = { ) ), ], - compute: function (logicalExpression: Maybe): boolean { - return !toBoolean(logicalExpression); + compute: function (logicalExpression: Maybe) { + return { value: !toBoolean(logicalExpression) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -204,7 +204,7 @@ export const OR = { if (!foundBoolean) { return new EvaluationError(noValidInputErrorMessage); } - return result; + return { value: result }; }, isExported: true, } satisfies AddFunctionDescription; @@ -257,8 +257,8 @@ export const SWITCH = { export const TRUE: AddFunctionDescription = { description: _t("Logical value `true`."), args: [], - compute: function (): boolean { - return true; + compute: function () { + return { value: true }; }, isExported: true, }; @@ -287,7 +287,7 @@ export const XOR = { if (!foundBoolean) { return new EvaluationError(noValidInputErrorMessage); } - return acc; + return { value: acc }; }, isExported: true, } satisfies AddFunctionDescription; diff --git a/src/functions/module_lookup.ts b/src/functions/module_lookup.ts index 2d6256aabd..d9118f67ec 100644 --- a/src/functions/module_lookup.ts +++ b/src/functions/module_lookup.ts @@ -118,9 +118,9 @@ export const ADDRESS = { cellReference = rowPart + colPart; } if (sheet !== undefined) { - return getFullReference(toString(sheet), cellReference); + return { value: getFullReference(toString(sheet), cellReference) }; } - return cellReference; + return { value: cellReference }; }, isExported: true, } satisfies AddFunctionDescription; @@ -139,7 +139,7 @@ export const COLUMN = { ) ), ], - compute: function (cellReference: Arg) { + computeArray: function (cellReference: Arg) { if (cellReference === undefined) { if (this.__originCellPosition === undefined) { return new EvaluationError( @@ -148,7 +148,7 @@ export const COLUMN = { ) ); } - return this.__originCellPosition.col + 1; + return { value: this.__originCellPosition.col + 1 }; } const _cellReference = toMatrix(cellReference); const firstCell = _cellReference[0][0]; @@ -161,7 +161,7 @@ export const COLUMN = { } const left = firstCell.position.col; if (_cellReference.length === 1) { - return left + 1; + return { value: left + 1 }; } return generateMatrix(_cellReference.length, 1, (col) => ({ value: left + col + 1 })); }, @@ -183,7 +183,7 @@ export const COLUMNS = { if (_range[0][0].value === CellErrorType.InvalidReference) { return _range[0][0]; } - return _range.length; + return { value: _range.length }; }, isExported: true, } satisfies AddFunctionDescription; @@ -269,7 +269,7 @@ export const INDEX: AddFunctionDescription = { _t("The index of the column to be returned from within the reference range of cells.") ), ], - compute: function ( + computeArray: function ( reference: Arg, row: Maybe = { value: 0 }, column: Maybe = { value: 0 } @@ -314,7 +314,7 @@ export const INDIRECT: AddFunctionDescription = { A1_NOTATION_OPTIONS ), ], - compute: function ( + computeArray: function ( reference: Maybe, useA1Notation: Maybe = { value: true } ): FunctionResultObject | Matrix { @@ -517,7 +517,7 @@ export const MATCH = { ) { return valueNotAvailable(searchKey); } - return index + 1; + return { value: index + 1 }; }, isExported: true, } satisfies AddFunctionDescription; @@ -536,7 +536,7 @@ export const ROW = { ) ), ], - compute: function (cellReference: Arg) { + computeArray: function (cellReference: Arg) { if (cellReference === undefined) { if (this.__originCellPosition?.row === undefined) { return new EvaluationError( @@ -545,7 +545,7 @@ export const ROW = { ) ); } - return this.__originCellPosition.row + 1; + return { value: this.__originCellPosition.row + 1 }; } const _cellReference = toMatrix(cellReference); const firstCell = _cellReference[0][0]; @@ -558,7 +558,7 @@ export const ROW = { } const top = firstCell.position.row; if (_cellReference[0].length === 1) { - return top + 1; + return { value: top + 1 }; } return generateMatrix(1, _cellReference[0].length, (col, row) => ({ value: top + row + 1 })); }, @@ -580,7 +580,7 @@ export const ROWS = { if (_range[0][0].value === CellErrorType.InvalidReference) { return _range[0][0]; } - return _range[0].length; + return { value: _range[0].length }; }, isExported: true, } satisfies AddFunctionDescription; @@ -706,7 +706,7 @@ export const XLOOKUP = { ] ), ], - compute: function ( + computeArray: function ( searchKey: Maybe, lookupRange: Arg, returnRange: Arg, @@ -913,7 +913,7 @@ export const PIVOT = { _t("Whether to include the measure titles row or not.") ), ], - compute: function ( + computeArray: function ( pivotFormulaId: Maybe, rowCount: Maybe, includeTotal: Maybe, @@ -1022,7 +1022,7 @@ export const OFFSET = { _t("The number of columns of the range to return starting at the offset target.") ), ], - compute: function ( + computeArray: function ( cellReference: Arg, offsetRows: Maybe, offsetColumns: Maybe, @@ -1123,7 +1123,7 @@ export const CHOOSE = { _t("A potential value to return. May be a reference to a cell or an individual value.") ), ], - compute: function (index: Maybe, ...choices: Arg[]) { + computeArray: function (index: Maybe, ...choices: Arg[]) { const _index = Math.floor(toNumber(index, this.locale)) - 1; if (_index < 0 || _index >= choices.length) { return new EvaluationError( @@ -1156,7 +1156,7 @@ export const DROP = { _t("The number of columns to exclude. A negative value drops from the end of the array.") ), ], - compute: function ( + computeArray: function ( array: Matrix<{ value: string }>, rows: Maybe, columns: Maybe @@ -1207,7 +1207,7 @@ export const TAKE = { _t("The number of columns to take. A negative value takes from the end of the array.") ), ], - compute: function ( + computeArray: function ( array: Matrix<{ value: string }>, rows: Maybe, columns: Maybe @@ -1254,7 +1254,7 @@ export const FORMULATEXT = { } const cell = this.getters.getCell(cellReference.position); if (cell?.isFormula) { - return cell.compiledFormula.toFormulaString(this.getters); + return { value: cell.compiledFormula.toFormulaString(this.getters) }; } else { return new NotAvailableError(_t("The cell does not contain a formula.")); } diff --git a/src/functions/module_math.ts b/src/functions/module_math.ts index 25513fd13e..401b493b08 100644 --- a/src/functions/module_math.ts +++ b/src/functions/module_math.ts @@ -11,6 +11,8 @@ import { isMatrix, } from "../types/misc"; import { arg } from "./arguments"; +import { createComputeFunction } from "./create_compute_function"; +import { functionRegistry } from "./function_registry"; import { assertNotZero } from "./helper_assert"; import { countUnique, sum } from "./helper_math"; import { getUnitMatrix } from "./helper_matrices"; @@ -20,6 +22,7 @@ import { inferFormat, isDataNonEmpty, isEvaluationError, + matrixMap, reduceAny, strictToNumber, toBoolean, @@ -43,8 +46,8 @@ const DECIMAL_REPRESENTATION = /^-?[a-z0-9]+$/i; export const ABS = { description: _t("Absolute value of a number."), args: [arg("value (number)", _t("The number of which to return the absolute value."))], - compute: function (value: Maybe): number { - return Math.abs(toNumber(value, this.locale)); + compute: function (value: Maybe) { + return { value: Math.abs(toNumber(value, this.locale)) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -67,7 +70,7 @@ export const ACOS = { if (Math.abs(_value) > 1) { return new EvaluationError(_t("The value (%s) must be between -1 and 1 inclusive.", _value)); } - return Math.acos(_value); + return { value: Math.acos(_value) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -90,7 +93,7 @@ export const ACOSH = { if (_value < 1) { return new EvaluationError(_t("The value (%s) must be greater than or equal to 1.", _value)); } - return Math.acosh(_value); + return { value: Math.acosh(_value) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -101,13 +104,13 @@ export const ACOSH = { export const ACOT = { description: _t("Inverse cotangent of a value."), args: [arg("value (number)", _t("The value for which to calculate the inverse cotangent."))], - compute: function (value: Maybe): number { + compute: function (value: Maybe) { const _value = toNumber(value, this.locale); const sign = Math.sign(_value) || 1; // ACOT has two possible configurations: // @compatibility Excel: return Math.PI / 2 - Math.atan(toNumber(_value, this.locale)); // @compatibility Google: return sign * Math.PI / 2 - Math.atan(toNumber(_value, this.locale)); - return (sign * Math.PI) / 2 - Math.atan(_value); + return { value: (sign * Math.PI) / 2 - Math.atan(_value) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -132,7 +135,7 @@ export const ACOTH = { _t("The value (%s) cannot be between -1 and 1 inclusive.", _value) ); } - return Math.log((_value + 1) / (_value - 1)) / 2; + return { value: Math.log((_value + 1) / (_value - 1)) / 2 }; }, isExported: true, } satisfies AddFunctionDescription; @@ -153,7 +156,7 @@ export const ASIN = { if (Math.abs(_value) > 1) { return new EvaluationError(_t("The value (%s) must be between -1 and 1 inclusive.", _value)); } - return Math.asin(_value); + return { value: Math.asin(_value) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -166,8 +169,8 @@ export const ASINH = { args: [ arg("value (number)", _t("The value for which to calculate the inverse hyperbolic sine.")), ], - compute: function (value: Maybe): number { - return Math.asinh(toNumber(value, this.locale)); + compute: function (value: Maybe) { + return { value: Math.asinh(toNumber(value, this.locale)) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -178,8 +181,8 @@ export const ASINH = { export const ATAN = { description: _t("Inverse tangent of a value, in radians."), args: [arg("value (number)", _t("The value for which to calculate the inverse tangent."))], - compute: function (value: Maybe): number { - return Math.atan(toNumber(value, this.locale)); + compute: function (value: Maybe) { + return { value: Math.atan(toNumber(value, this.locale)) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -211,7 +214,7 @@ export const ATAN2 = { _t("Function [[FUNCTION_NAME]] caused a divide by zero error.") ); } - return Math.atan2(_y, _x); + return { value: Math.atan2(_y, _x) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -234,7 +237,7 @@ export const ATANH = { if (Math.abs(_value) >= 1) { return new EvaluationError(_t("The value (%s) must be between -1 and 1 exclusive.", _value)); } - return Math.atanh(_value); + return { value: Math.atanh(_value) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -359,8 +362,8 @@ export const CEILING_PRECISE = { export const COS = { description: _t("Cosine of an angle provided in radians."), args: [arg("angle (number)", _t("The angle to find the cosine of, in radians."))], - compute: function (angle: Maybe): number { - return Math.cos(toNumber(angle, this.locale)); + compute: function (angle: Maybe) { + return { value: Math.cos(toNumber(angle, this.locale)) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -371,8 +374,8 @@ export const COS = { export const COSH = { description: _t("Hyperbolic cosine of any real number."), args: [arg("value (number)", _t("Any real value to calculate the hyperbolic cosine of."))], - compute: function (value: Maybe): number { - return Math.cosh(toNumber(value, this.locale)); + compute: function (value: Maybe) { + return { value: Math.cosh(toNumber(value, this.locale)) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -390,7 +393,7 @@ export const COT = { _t("Function [[FUNCTION_NAME]] caused a divide by zero error.") ); } - return 1 / Math.tan(_angle); + return { value: 1 / Math.tan(_angle) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -408,7 +411,7 @@ export const COTH = { _t("Function [[FUNCTION_NAME]] caused a divide by zero error.") ); } - return 1 / Math.tanh(_value); + return { value: 1 / Math.tanh(_value) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -424,23 +427,25 @@ export const COUNTBLANK = { _t("Value or range in which to count the number of blanks.") ), ], - compute: function (...args: Arg[]): number { - return reduceAny( - args, - (acc, a) => { - if (a === undefined) { - return acc + 1; - } - if (a.value === null) { - return acc + 1; - } - if (a.value === "") { - return acc + 1; - } - return acc; - }, - 0 - ); + compute: function (...args: Arg[]) { + return { + value: reduceAny( + args, + (acc, a) => { + if (a === undefined) { + return acc + 1; + } + if (a.value === null) { + return acc + 1; + } + if (a.value === "") { + return acc + 1; + } + return acc; + }, + 0 + ), + }; }, isExported: true, } satisfies AddFunctionDescription; @@ -454,7 +459,7 @@ export const COUNTIF = { arg("range (range)", _t("The range that is tested against criterion.")), arg("criterion (string)", _t("The pattern or test to apply to range.")), ], - compute: function (...args: Arg[]): number { + compute: function (...args: Arg[]) { let count = 0; visitMatchingRanges( args, @@ -463,7 +468,7 @@ export const COUNTIF = { }, this.locale ); - return count; + return { value: count }; }, isExported: true, } satisfies AddFunctionDescription; @@ -477,7 +482,7 @@ export const COUNTIFS = { arg("criteria_range (any, range, repeating)", _t("Range over which to evaluate criteria.")), arg("criterion (string, repeating)", _t("Criteria to check.")), ], - compute: function (...args: Arg[]): number { + compute: function (...args: Arg[]) { let count = 0; visitMatchingRanges( args, @@ -486,7 +491,7 @@ export const COUNTIFS = { }, this.locale ); - return count; + return { value: count }; }, isExported: true, } satisfies AddFunctionDescription; @@ -498,8 +503,8 @@ export const COUNTIFS = { export const COUNTUNIQUE = { description: _t("Counts number of unique values in a range."), args: [arg("value (any, range, repeating)", _t("Value or range to consider for uniqueness."))], - compute: function (...args: Arg[]): number { - return countUnique(args); + compute: function (...args: Arg[]) { + return { value: countUnique(args) }; }, } satisfies AddFunctionDescription; @@ -517,7 +522,7 @@ export const COUNTUNIQUEIFS = { arg("criteria_range (any, range, repeating)", _t("Range over which to evaluate criteria.")), arg("criterion (string, repeating)", _t("Criteria to check.")), ], - compute: function (range: Matrix, ...args: Arg[]): number { + compute: function (range: Matrix, ...args: Arg[]) { const uniqueValues = new Set(); visitMatchingRanges( args, @@ -529,7 +534,7 @@ export const COUNTUNIQUEIFS = { }, this.locale ); - return uniqueValues.size; + return { value: uniqueValues.size }; }, } satisfies AddFunctionDescription; @@ -546,7 +551,7 @@ export const CSC = { _t("Function [[FUNCTION_NAME]] caused a divide by zero error.") ); } - return 1 / Math.sin(_angle); + return { value: 1 / Math.sin(_angle) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -564,7 +569,7 @@ export const CSCH = { _t("Function [[FUNCTION_NAME]] caused a divide by zero error.") ); } - return 1 / Math.sinh(_value); + return { value: 1 / Math.sinh(_value) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -588,7 +593,7 @@ export const DECIMAL = { const _value = toString(value); if (_value === "") { - return 0; + return { value: 0 }; } /** @@ -608,7 +613,7 @@ export const DECIMAL = { _t("The value (%s) must be a valid base %s representation.", _value, _base) ); } - return deci; + return { value: deci }; }, isExported: true, } satisfies AddFunctionDescription; @@ -619,8 +624,8 @@ export const DECIMAL = { export const DEGREES = { description: _t("Converts an angle value in radians to degrees."), args: [arg("angle (number)", _t("The angle to convert from radians to degrees."))], - compute: function (angle: Maybe): number { - return (toNumber(angle, this.locale) * 180) / Math.PI; + compute: function (angle: Maybe) { + return { value: (toNumber(angle, this.locale) * 180) / Math.PI }; }, isExported: true, } satisfies AddFunctionDescription; @@ -631,8 +636,8 @@ export const DEGREES = { export const EXP = { description: _t("Euler's number, e (~2.718) raised to a power."), args: [arg("value (number)", _t("The exponent to raise e."))], - compute: function (value: Maybe): number { - return Math.exp(toNumber(value, this.locale)); + compute: function (value: Maybe) { + return { value: Math.exp(toNumber(value, this.locale)) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -757,10 +762,10 @@ export const FLOOR_PRECISE = { export const ISEVEN = { description: _t("Whether the provided value is even."), args: [arg("value (number)", _t("The value to be verified as even."))], - compute: function (value: Maybe): boolean { + compute: function (value: Maybe) { const _value = strictToNumber(value, this.locale); - return Math.floor(Math.abs(_value)) & 1 ? false : true; + return { value: Math.floor(Math.abs(_value)) & 1 ? false : true }; }, isExported: true, } satisfies AddFunctionDescription; @@ -800,10 +805,10 @@ export const ISO_CEILING = { export const ISODD = { description: _t("Whether the provided value is even."), args: [arg("value (number)", _t("The value to be verified as even."))], - compute: function (value: Maybe): boolean { + compute: function (value: Maybe) { const _value = strictToNumber(value, this.locale); - return Math.floor(Math.abs(_value)) & 1 ? true : false; + return { value: Math.floor(Math.abs(_value)) & 1 ? true : false }; }, isExported: true, } satisfies AddFunctionDescription; @@ -819,7 +824,7 @@ export const LN = { if (_value <= 0) { return new EvaluationError(_t("The value (%s) must be strictly positive.", _value)); } - return Math.log(_value); + return { value: Math.log(_value) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -848,7 +853,7 @@ export const LOG = { if (_base === 1) { return new EvaluationError(_t("The base must be different from 1.")); } - return Math.log10(_value) / Math.log10(_base); + return { value: Math.log10(_value) / Math.log10(_base) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -897,12 +902,12 @@ export const MUNIT = { _t("An integer specifying the dimension size of the unit matrix. It must be positive.") ), ], - compute: function (n: Maybe) { + computeArray: function (n: Maybe) { const _n = toInteger(n, this.locale); if (_n < 1) { return new EvaluationError(_t("The argument dimension must be positive")); } - return getUnitMatrix(_n); + return matrixMap(getUnitMatrix(_n), (value) => ({ value })); }, isExported: true, } satisfies AddFunctionDescription; @@ -932,8 +937,8 @@ export const ODD = { export const PI = { description: _t("The number pi."), args: [], - compute: function (): number { - return Math.PI; + compute: function () { + return { value: Math.PI }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1007,8 +1012,8 @@ export const PRODUCT = { export const RAND = { description: _t("A random number between 0 inclusive and 1 exclusive."), args: [], - compute: function (): number { - return Math.random(); + compute: function () { + return { value: Math.random() }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1028,7 +1033,7 @@ export const RANDARRAY = { { value: true, label: _t("Integer") }, ]), ], - compute: function ( + computeArray: function ( rows: Maybe = { value: 1 }, columns: Maybe = { value: 1 }, min: Maybe = { value: 0 }, @@ -1075,7 +1080,7 @@ export const RANDARRAY = { } } } - return result; + return matrixMap(result, (value) => ({ value })); }, isExported: true, } satisfies AddFunctionDescription; @@ -1233,8 +1238,8 @@ export const ROUNDUP = { export const SEC = { description: _t("Secant of an angle provided in radians."), args: [arg("angle (number)", _t("The angle to find the secant of, in radians."))], - compute: function (angle: Maybe): number { - return 1 / Math.cos(toNumber(angle, this.locale)); + compute: function (angle: Maybe) { + return { value: 1 / Math.cos(toNumber(angle, this.locale)) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1245,8 +1250,8 @@ export const SEC = { export const SECH = { description: _t("Hyperbolic secant of any real number."), args: [arg("value (number)", _t("Any real value to calculate the hyperbolic secant of."))], - compute: function (value: Maybe): number { - return 1 / Math.cosh(toNumber(value, this.locale)); + compute: function (value: Maybe) { + return { value: 1 / Math.cosh(toNumber(value, this.locale)) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1265,7 +1270,7 @@ export const SEQUENCE = { _t("The amount to increment each value in the sequence") ), ], - compute: function ( + computeArray: function ( rows: Maybe, columns: FunctionResultObject = { value: 1 }, start: FunctionResultObject = { value: 1 }, @@ -1296,8 +1301,8 @@ export const SEQUENCE = { export const SIN = { description: _t("Sine of an angle provided in radians."), args: [arg("angle (number)", _t("The angle to find the sine of, in radians."))], - compute: function (angle: Maybe): number { - return Math.sin(toNumber(angle, this.locale)); + compute: function (angle: Maybe) { + return { value: Math.sin(toNumber(angle, this.locale)) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1308,8 +1313,8 @@ export const SIN = { export const SINH = { description: _t("Hyperbolic sine of any real number."), args: [arg("value (number)", _t("Any real value to calculate the hyperbolic sine of."))], - compute: function (value: Maybe): number { - return Math.sinh(toNumber(value, this.locale)); + compute: function (value: Maybe) { + return { value: Math.sinh(toNumber(value, this.locale)) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1375,7 +1380,8 @@ export const SUBTOTAL = { _t("Range or reference for which you want the subtotal.") ), ], - compute: function (functionCode: Maybe, ...refs: Arg[]) { + // LUL: not sure about this + computeArray: function (functionCode: Maybe, ...refs: Arg[]) { let code = toInteger(functionCode, this.locale); let acceptHiddenCells = true; if (code > 100) { @@ -1417,7 +1423,8 @@ export const SUBTOTAL = { } } - return this[subtotalFunctionAggregateByCode[code]].apply(this, [[functionResults]]); + const aggregateName = subtotalFunctionAggregateByCode[code]; + return createComputeFunction(functionRegistry.get(aggregateName), 1)(this, [functionResults]); }, isExported: true, } satisfies AddFunctionDescription; @@ -1455,7 +1462,7 @@ export const SUMIF = { criteriaRange: Matrix, criterion: Maybe, sumRange: Matrix - ): number { + ) { if (sumRange === undefined) { sumRange = criteriaRange; } @@ -1471,7 +1478,7 @@ export const SUMIF = { }, this.locale ); - return sum; + return { value: sum }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1486,7 +1493,7 @@ export const SUMIFS = { arg("criteria_range (any, range, repeating)", _t("Range to check.")), arg("criterion (string, repeating)", _t("Criteria to check.")), ], - compute: function (sumRange: Matrix, ...criters: Arg[]): number { + compute: function (sumRange: Matrix, ...criters: Arg[]) { let sum = 0; visitMatchingRanges( criters, @@ -1498,7 +1505,7 @@ export const SUMIFS = { }, this.locale ); - return sum; + return { value: sum }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1509,8 +1516,8 @@ export const SUMIFS = { export const TAN = { description: _t("Tangent of an angle provided in radians."), args: [arg("angle (number)", _t("The angle to find the tangent of, in radians."))], - compute: function (angle: Maybe): number { - return Math.tan(toNumber(angle, this.locale)); + compute: function (angle: Maybe) { + return { value: Math.tan(toNumber(angle, this.locale)) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1521,8 +1528,8 @@ export const TAN = { export const TANH = { description: _t("Hyperbolic tangent of any real number."), args: [arg("value (number)", _t("Any real value to calculate the hyperbolic tangent of."))], - compute: function (value: Maybe): number { - return Math.tanh(toNumber(value, this.locale)); + compute: function (value: Maybe) { + return { value: Math.tanh(toNumber(value, this.locale)) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1566,8 +1573,8 @@ export const TRUNC = { export const INT = { description: _t("Rounds a number down to the nearest integer that is less than or equal to it."), args: [arg("value (number)", _t("The number to round down to the nearest integer."))], - compute: function (value: Maybe): number { - return Math.floor(toNumber(value, this.locale)); + compute: function (value: Maybe) { + return { value: Math.floor(toNumber(value, this.locale)) }; }, isExported: true, } satisfies AddFunctionDescription; diff --git a/src/functions/module_operators.ts b/src/functions/module_operators.ts index 7c87e9f07a..36225c4383 100644 --- a/src/functions/module_operators.ts +++ b/src/functions/module_operators.ts @@ -42,11 +42,8 @@ export const CONCAT = { arg("value1 (string)", _t("The value to which value2 will be appended.")), arg("value2 (string)", _t("The value to append to value1.")), ], - compute: function ( - value1: Maybe, - value2: Maybe - ): string { - return toString(value1) + toString(value2); + compute: function (value1: Maybe, value2: Maybe) { + return { value: toString(value1) + toString(value2) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -308,7 +305,7 @@ export const POW = { export const SPILLED_RANGE = { description: _t("Gets the spilled range of an array formula."), args: [arg("ref (any, range)", _t("The reference to get the spilled range from."))], - compute: function (ref: Arg | undefined) { + computeArray: function (ref: Arg | undefined) { if (ref === undefined) { return new InvalidReferenceError(expectReferenceError); } @@ -385,8 +382,8 @@ export const UMINUS = { export const UNARY_PERCENT = { description: _t("Value interpreted as a percentage."), args: [arg("percentage (number)", _t("The value to interpret as a percentage."))], - compute: function (percentage: Maybe): number { - return toNumber(percentage, this.locale) / 100; + compute: function (percentage: Maybe) { + return { value: toNumber(percentage, this.locale) / 100 }; }, } satisfies AddFunctionDescription; diff --git a/src/functions/module_statistical.ts b/src/functions/module_statistical.ts index 84d508274c..061bd2378b 100644 --- a/src/functions/module_statistical.ts +++ b/src/functions/module_statistical.ts @@ -230,7 +230,9 @@ export const AVEDEV = { ); } const average = sum / count; - return reduceNumbers(values, (acc, a) => acc + Math.abs(average - a), 0, this.locale) / count; + return { + value: reduceNumbers(values, (acc, a) => acc + Math.abs(average - a), 0, this.locale) / count, + }; }, isExported: true, } satisfies AddFunctionDescription; @@ -396,7 +398,7 @@ export const AVERAGEIF = { _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.") ); } - return sum / count; + return { value: sum / count }; }, isExported: true, } satisfies AddFunctionDescription; @@ -431,7 +433,7 @@ export const AVERAGEIFS = { _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.") ); } - return sum / count; + return { value: sum / count }; }, isExported: true, } satisfies AddFunctionDescription; @@ -447,8 +449,8 @@ export const COUNT = { _t("Value or range to consider when counting.") ), ], - compute: function (...values: Arg[]): number { - return countNumbers(values, this.locale); + compute: function (...values: Arg[]) { + return { value: countNumbers(values, this.locale) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -459,8 +461,8 @@ export const COUNT = { export const COUNTA = { description: _t("The number of values in a dataset."), args: [arg("value (any, range, repeating)", _t("Value or range to consider when counting."))], - compute: function (...values: Arg[]): number { - return countAny(values); + compute: function (...values: Arg[]) { + return { value: countAny(values) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -480,8 +482,8 @@ export const COVAR = { _t("The range representing the array or matrix of independent data.") ), ], - compute: function (dataY: Arg, dataX: Arg): number { - return covariance(dataY, dataX, false); + compute: function (dataY: Arg, dataX: Arg) { + return { value: covariance(dataY, dataX, false) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -498,8 +500,8 @@ export const COVARIANCE_P = { _t("The range representing the array or matrix of independent data.") ), ], - compute: function (dataY: Arg, dataX: Arg): number { - return covariance(dataY, dataX, false); + compute: function (dataY: Arg, dataX: Arg) { + return { value: covariance(dataY, dataX, false) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -516,8 +518,8 @@ export const COVARIANCE_S = { _t("The range representing the array or matrix of independent data.") ), ], - compute: function (dataY: Arg, dataX: Arg): number { - return covariance(dataY, dataX, true); + compute: function (dataY: Arg, dataX: Arg) { + return { value: covariance(dataY, dataX, true) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -540,7 +542,7 @@ export const FORECAST: AddFunctionDescription = { _t("The range representing the array or matrix of independent data.") ), ], - compute: function ( + computeArray: function ( x: Arg, dataY: Matrix, dataX: Matrix @@ -550,11 +552,14 @@ export const FORECAST: AddFunctionDescription = { return new NotAvailableError(noValidInputErrorMessage); } - return predictLinearValues( - [flatDataY], - [flatDataX], - matrixMap(toMatrix(x), (value) => toNumber(value, this.locale)), - true + return matrixMap( + predictLinearValues( + [flatDataY], + [flatDataX], + matrixMap(toMatrix(x), (value) => toNumber(value, this.locale)), + true + ), + (value) => ({ value }) ); }, isExported: true, @@ -588,7 +593,7 @@ export const GROWTH: AddFunctionDescription = { CALCULATE_B_OPTIONS ), ], - compute: function ( + computeArray: function ( knownDataY: Matrix, knownDataX: Matrix = [[]], newDataX: Matrix = [[]], @@ -597,13 +602,16 @@ export const GROWTH: AddFunctionDescription = { if (knownDataY.length === 0 || knownDataY[0].length === 0) { return new EvaluationError(emptyDataErrorMessage("known_data_y")); } - return expM( - predictLinearValues( - logM(toNumberMatrix(knownDataY, "known_data_y")), - toNumberMatrix(knownDataX, "known_data_x"), - toNumberMatrix(newDataX, "new_data_y"), - toBoolean(b) - ) + return matrixMap( + expM( + predictLinearValues( + logM(toNumberMatrix(knownDataY, "known_data_y")), + toNumberMatrix(knownDataX, "known_data_x"), + toNumberMatrix(newDataX, "new_data_y"), + toBoolean(b) + ) + ), + (value) => ({ value }) ); }, }; @@ -629,7 +637,7 @@ export const INTERCEPT: AddFunctionDescription = { return new NotAvailableError(noValidInputErrorMessage); } const [[], [intercept]] = fullLinearRegression([flatDataX], [flatDataY]); - return intercept as number; + return { value: intercept as number }; }, isExported: true, }; @@ -709,7 +717,7 @@ export const LINEST: AddFunctionDescription = { RETURN_VERBOSE_OPTIONS ), ], - compute: function ( + computeArray: function ( dataY: Matrix, dataX: Matrix = [[]], calculateB: Maybe = { value: true }, @@ -718,11 +726,14 @@ export const LINEST: AddFunctionDescription = { if (dataY.length === 0 || dataY[0].length === 0) { return new EvaluationError(emptyDataErrorMessage("data_y")); } - return fullLinearRegression( - toNumberMatrix(dataX, "data_x"), - toNumberMatrix(dataY, "data_y"), - toBoolean(calculateB), - toBoolean(verbose) + return matrixMap( + fullLinearRegression( + toNumberMatrix(dataX, "data_x"), + toNumberMatrix(dataY, "data_y"), + toBoolean(calculateB), + toBoolean(verbose) + ), + (value) => ({ value }) ); }, isExported: true, @@ -757,7 +768,7 @@ export const LOGEST: AddFunctionDescription = { RETURN_VERBOSE_OPTIONS ), ], - compute: function ( + computeArray: function ( dataY: Matrix, dataX: Matrix = [[]], calculateB: Maybe = { value: true }, @@ -775,7 +786,7 @@ export const LOGEST: AddFunctionDescription = { for (let i = 0; i < coeffs.length; i++) { coeffs[i][0] = Math.exp(coeffs[i][0] as number); } - return coeffs; + return matrixMap(coeffs, (value) => ({ value })); }, isExported: true, }; @@ -819,10 +830,11 @@ export const MATTHEWS: AddFunctionDescription = { } } } - return ( - (trueP * trueN - falseP * falseN) / - Math.sqrt((trueP + falseP) * (trueP + falseN) * (trueN + falseP) * (trueN + falseN)) - ); + return { + value: + (trueP * trueN - falseP * falseN) / + Math.sqrt((trueP + falseP) * (trueP + falseN) * (trueN + falseP) * (trueN + falseN)), + }; }, isExported: false, }; @@ -879,7 +891,7 @@ export const MAXIFS = { arg("criteria_range (any, range, repeating)", _t("Range to evaluate criteria.")), arg("criterion (string, repeating)", _t("Criteria to check.")), ], - compute: function (range: Matrix, ...args: Arg[]): number { + compute: function (range: Matrix, ...args: Arg[]) { let result = -Infinity; visitMatchingRanges( args, @@ -891,7 +903,7 @@ export const MAXIFS = { }, this.locale ); - return result === -Infinity ? 0 : result; + return { value: result === -Infinity ? 0 : result }; }, isExported: true, } satisfies AddFunctionDescription; @@ -976,7 +988,7 @@ export const MINIFS = { arg("criteria_range (any, range, repeating)", _t("Range to evaluate criteria.")), arg("criterion (string, repeating)", _t("Criterion to check.")), ], - compute: function (range: Matrix, ...args: Arg[]): number { + compute: function (range: Matrix, ...args: Arg[]) { let result = Infinity; visitMatchingRanges( args, @@ -988,7 +1000,7 @@ export const MINIFS = { }, this.locale ); - return result === Infinity ? 0 : result; + return { value: result === Infinity ? 0 : result }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1036,11 +1048,12 @@ export const PEARSON: AddFunctionDescription = { _t("The range representing the array or matrix of independent data.") ), ], - compute: function ( - dataY: Matrix, - dataX: Matrix - ): number | NotAvailableError { - return pearson(dataY, dataX); + compute: function (dataY: Matrix, dataX: Matrix) { + const result = pearson(dataY, dataX); + if (result instanceof NotAvailableError) { + return result; + } + return { value: result }; }, isExported: true, }; @@ -1136,7 +1149,7 @@ export const POLYFIT_COEFFS: AddFunctionDescription = { COMPUTE_INTERCEPT_OPTIONS ), ], - compute: function ( + computeArray: function ( dataY: Matrix, dataX: Matrix, order: Maybe, @@ -1146,11 +1159,14 @@ export const POLYFIT_COEFFS: AddFunctionDescription = { if (flatDataX.length === 0 || flatDataY.length === 0) { return new NotAvailableError(noValidInputErrorMessage); } - return polynomialRegression( - flatDataY, - flatDataX, - toNumber(order, this.locale), - toBoolean(intercept) + return matrixMap( + polynomialRegression( + flatDataY, + flatDataX, + toNumber(order, this.locale), + toBoolean(intercept) + ), + (value) => ({ value }) ); }, isExported: false, @@ -1182,7 +1198,7 @@ export const POLYFIT_FORECAST: AddFunctionDescription = { COMPUTE_INTERCEPT_OPTIONS ), ], - compute: function ( + computeArray: function ( x: Arg, dataY: Matrix, dataX: Matrix, @@ -1195,9 +1211,9 @@ export const POLYFIT_FORECAST: AddFunctionDescription = { return new NotAvailableError(noValidInputErrorMessage); } const coeffs = polynomialRegression(flatDataY, flatDataX, _order, toBoolean(intercept)).flat(); - return matrixMap(toMatrix(x), (xij) => - evaluatePolynomial(coeffs, toNumber(xij, this.locale), _order) - ); + return matrixMap(toMatrix(x), (xij) => ({ + value: evaluatePolynomial(coeffs, toNumber(xij, this.locale), _order), + })); }, isExported: false, }; @@ -1301,7 +1317,7 @@ export const RANK: AddFunctionDescription = { if (!found) { return new NotAvailableError(_t("Value not found in the given data.")); } - return rank; + return { value: rank }; }, isExported: true, }; @@ -1323,15 +1339,12 @@ export const RSQ: AddFunctionDescription = { _t("The range representing the array or matrix of independent data.") ), ], - compute: function ( - dataY: Matrix, - dataX: Matrix - ): number { + compute: function (dataY: Matrix, dataX: Matrix) { const value = pearson(dataY, dataX); if (value instanceof Error) { throw value; } - return Math.pow(value as number, 2.0); + return { value: Math.pow(value as number, 2.0) }; }, isExported: true, }; @@ -1357,7 +1370,7 @@ export const SLOPE: AddFunctionDescription = { return new NotAvailableError(noValidInputErrorMessage); } const [[slope]] = fullLinearRegression([flatDataX], [flatDataY]); - return slope as number; + return { value: slope as number }; }, isExported: true, }; @@ -1442,7 +1455,7 @@ export const SPEARMAN: AddFunctionDescription = { for (let i = 0; i < n; ++i) { sum += (order[i][0] - i) ** 2; } - return 1 - (6 * sum) / (n ** 3 - n); + return { value: 1 - (6 * sum) / (n ** 3 - n) }; }, isExported: false, }; @@ -1455,8 +1468,8 @@ export const STDEV = { args: [ arg("value (number, range, repeating)", _t("Value or range to include in the sample.")), ], - compute: function (...args: Arg[]): number { - return Math.sqrt(VAR.compute.bind(this)(...args)); + compute: function (...args: Arg[]) { + return { value: Math.sqrt(VAR.compute.bind(this)(...args).value) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1472,8 +1485,8 @@ export const STDEV_P = { _t("Value or range to include in the population.") ), ], - compute: function (...args: Arg[]): number { - return Math.sqrt(VAR_P.compute.bind(this)(...args)); + compute: function (...args: Arg[]) { + return { value: Math.sqrt(VAR_P.compute.bind(this)(...args).value) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1486,8 +1499,8 @@ export const STDEV_S = { args: [ arg("value (number, range, repeating)", _t("Value or range to include in the sample.")), ], - compute: function (...args: Arg[]): number { - return Math.sqrt(VAR_S.compute.bind(this)(...args)); + compute: function (...args: Arg[]) { + return { value: Math.sqrt(VAR_S.compute.bind(this)(...args).value) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1500,8 +1513,8 @@ export const STDEVA = { args: [ arg("value (number, range, repeating)", _t("Value or range to include in the sample.")), ], - compute: function (...args: Arg[]): number { - return Math.sqrt(VARA.compute.bind(this)(...args)); + compute: function (...args: Arg[]) { + return { value: Math.sqrt(VARA.compute.bind(this)(...args).value) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1517,8 +1530,8 @@ export const STDEVP = { _t("Value or range to include in the population.") ), ], - compute: function (...args: Arg[]): number { - return Math.sqrt(VARP.compute.bind(this)(...args)); + compute: function (...args: Arg[]) { + return { value: Math.sqrt(VARP.compute.bind(this)(...args).value) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1534,8 +1547,8 @@ export const STDEVPA = { _t("Value or range to include in the population.") ), ], - compute: function (...args: Arg[]): number { - return Math.sqrt(VARPA.compute.bind(this)(...args)); + compute: function (...args: Arg[]) { + return { value: Math.sqrt(VARPA.compute.bind(this)(...args).value) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1563,7 +1576,7 @@ export const STEYX: AddFunctionDescription = { return new NotAvailableError(noValidInputErrorMessage); } const data = fullLinearRegression([flatDataX], [flatDataY], true, true); - return data[1][2] as number; + return { value: data[1][2] as number }; }, isExported: true, }; @@ -1596,7 +1609,7 @@ export const TREND: AddFunctionDescription = { CALCULATE_B_OPTIONS ), ], - compute: function ( + computeArray: function ( knownDataY: Matrix, knownDataX: Matrix = [[]], newDataX: Matrix = [[]], @@ -1605,11 +1618,14 @@ export const TREND: AddFunctionDescription = { if (knownDataY.length === 0 || knownDataY[0].length === 0) { return new EvaluationError(emptyDataErrorMessage("known_data_y")); } - return predictLinearValues( - toNumberMatrix(knownDataY, "known_data_y"), - toNumberMatrix(knownDataX, "known_data_x"), - toNumberMatrix(newDataX, "new_data_y"), - toBoolean(b) + return matrixMap( + predictLinearValues( + toNumberMatrix(knownDataY, "known_data_y"), + toNumberMatrix(knownDataX, "known_data_x"), + toNumberMatrix(newDataX, "new_data_y"), + toBoolean(b) + ), + (value) => ({ value }) ); }, }; @@ -1622,8 +1638,8 @@ export const VAR = { args: [ arg("value (number, range, repeating)", _t("Value or range to include in the sample.")), ], - compute: function (...args: Arg[]): number { - return variance(args, true, false, this.locale); + compute: function (...args: Arg[]) { + return { value: variance(args, true, false, this.locale) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1639,8 +1655,8 @@ export const VAR_P = { _t("Value or range to include in the population.") ), ], - compute: function (...args: Arg[]): number { - return variance(args, false, false, this.locale); + compute: function (...args: Arg[]) { + return { value: variance(args, false, false, this.locale) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1653,8 +1669,8 @@ export const VAR_S = { args: [ arg("value (number, range, repeating)", _t("Value or range to include in the sample.")), ], - compute: function (...args: Arg[]): number { - return variance(args, true, false, this.locale); + compute: function (...args: Arg[]) { + return { value: variance(args, true, false, this.locale) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1667,8 +1683,8 @@ export const VARA = { args: [ arg("value (number, range, repeating)", _t("Value or range to include in the sample.")), ], - compute: function (...args: Arg[]): number { - return variance(args, true, true, this.locale); + compute: function (...args: Arg[]) { + return { value: variance(args, true, true, this.locale) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1684,8 +1700,8 @@ export const VARP = { _t("Value or range to include in the population.") ), ], - compute: function (...args: Arg[]): number { - return variance(args, false, false, this.locale); + compute: function (...args: Arg[]) { + return { value: variance(args, false, false, this.locale) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1701,8 +1717,8 @@ export const VARPA = { _t("Value or range to include in the population.") ), ], - compute: function (...args: Arg[]): number { - return variance(args, false, true, this.locale); + compute: function (...args: Arg[]) { + return { value: variance(args, false, true, this.locale) }; }, isExported: true, } satisfies AddFunctionDescription; diff --git a/src/functions/module_text.ts b/src/functions/module_text.ts index b0bffefba1..b76500e00f 100644 --- a/src/functions/module_text.ts +++ b/src/functions/module_text.ts @@ -5,7 +5,15 @@ import { CellErrorType, EvaluationError, NotAvailableError } from "../types/erro import { AddFunctionDescription } from "../types/functions"; import { Arg, FunctionResultNumber, FunctionResultObject, Maybe } from "../types/misc"; import { arg } from "./arguments"; -import { reduceAny, toBoolean, toMatrix, toNumber, toString, transposeMatrix } from "./helpers"; +import { + matrixMap, + reduceAny, + toBoolean, + toMatrix, + toNumber, + toString, + transposeMatrix, +} from "./helpers"; const DEFAULT_STARTING_AT = 1; @@ -37,7 +45,7 @@ export const CHAR = { if (_tableNumber < 1) { return new EvaluationError(_t("The table_number (%s) is out of range.", _tableNumber)); } - return String.fromCharCode(_tableNumber); + return { value: String.fromCharCode(_tableNumber) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -48,7 +56,7 @@ export const CHAR = { export const CLEAN = { description: _t("Remove non-printable characters from a piece of text."), args: [arg("text (string)", _t("The text whose non-printable characters are to be removed."))], - compute: function (text: Maybe): string { + compute: function (text: Maybe) { const _text = toString(text); let cleanedStr = ""; for (const char of _text) { @@ -56,7 +64,7 @@ export const CLEAN = { cleanedStr += char; } } - return cleanedStr; + return { value: cleanedStr }; }, isExported: true, } satisfies AddFunctionDescription; @@ -67,8 +75,8 @@ export const CLEAN = { export const CONCATENATE = { description: _t("Appends strings to one another."), args: [arg("string (string, range, repeating)", _t("String to append in sequence."))], - compute: function (...datas: Arg[]): string { - return reduceAny(datas, (acc, a) => acc + toString(a), ""); + compute: function (...datas: Arg[]) { + return { value: reduceAny(datas, (acc, a) => acc + toString(a), "") }; }, isExported: true, } satisfies AddFunctionDescription; @@ -82,11 +90,8 @@ export const EXACT = { arg("string1 (string)", _t("The first string to compare.")), arg("string2 (string)", _t("The second string to compare.")), ], - compute: function ( - string1: Maybe, - string2: Maybe - ): boolean { - return toString(string1) === toString(string2); + compute: function (string1: Maybe, string2: Maybe) { + return { value: toString(string1) === toString(string2) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -137,7 +142,7 @@ export const FIND = { ); } - return result + 1; + return { value: result + 1 }; }, isExported: true, } satisfies AddFunctionDescription; @@ -185,9 +190,11 @@ export const JOIN = { _t("Value to be appended using delimiter.") ), ], - compute: function (delimiter: Maybe, ...valuesOrArrays: Arg[]): string { + compute: function (delimiter: Maybe, ...valuesOrArrays: Arg[]) { const _delimiter = toString(delimiter); - return reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toString(a), ""); + return { + value: reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toString(a), ""), + }; }, } satisfies AddFunctionDescription; @@ -211,7 +218,7 @@ export const LEFT = { _t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters) ); } - return toString(text).substring(0, _numberOfCharacters); + return { value: toString(text).substring(0, _numberOfCharacters) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -222,8 +229,8 @@ export const LEFT = { export const LEN = { description: _t("Length of a string."), args: [arg("text (string)", _t("The string whose length will be returned."))], - compute: function (text: Maybe): number { - return toString(text).length; + compute: function (text: Maybe) { + return { value: toString(text).length }; }, isExported: true, } satisfies AddFunctionDescription; @@ -234,8 +241,8 @@ export const LEN = { export const LOWER = { description: _t("Converts a specified string to lowercase."), args: [arg("text (string)", _t("The string to convert to lowercase."))], - compute: function (text: Maybe): string { - return toString(text).toLowerCase(); + compute: function (text: Maybe) { + return { value: toString(text).toLowerCase() }; }, isExported: true, } satisfies AddFunctionDescription; @@ -278,7 +285,7 @@ export const MID = { ); } - return _text.slice(_starting_at - 1, _starting_at + _extract_length - 1); + return { value: _text.slice(_starting_at - 1, _starting_at + _extract_length - 1) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -296,11 +303,13 @@ export const PROPER = { ) ), ], - compute: function (text: Maybe): string { + compute: function (text: Maybe) { const _text = toString(text); - return _text.replace(wordRegex, (word): string => { - return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); - }); + return { + value: _text.replace(wordRegex, (word): string => { + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + }), + }; }, isExported: true, } satisfies AddFunctionDescription; @@ -336,10 +345,10 @@ export const REGEXTEST = { const _caseSensitivity = toNumber(newText, this.locale); if (_pattern === "") { - return true; + return { value: true }; } if (_text === "") { - return false; + return { value: false }; } if (_caseSensitivity !== 0 && _caseSensitivity !== 1) { return new EvaluationError(_t("The case_sensitivity (%s) must be 0 or 1.", _caseSensitivity)); @@ -353,7 +362,7 @@ export const REGEXTEST = { return new EvaluationError(_t("Invalid regular expression")); } - return regex.test(_text); + return { value: regex.test(_text) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -386,7 +395,7 @@ export const REGEXEXTRACT = { ] ), ], - compute: function ( + computeArray: function ( text: Maybe, pattern: Maybe, return_mode: Maybe = { value: REGEXEXTRACT_DEFAULT_MODE }, @@ -421,14 +430,14 @@ export const REGEXEXTRACT = { } if (_returnMode === 0) { - return matches[0][0]; + return { value: matches[0][0] }; } else if (_returnMode === 1) { - return matches.map((match) => [match[0]]); + return matches.map((match) => [{ value: match[0] }]); } else { if (matches[0].length < 2) { return new EvaluationError(_t("No capturing groups found.")); } - return matches[0].slice(1).map((s) => [s]); + return matches[0].slice(1).map((s) => [{ value: s }]); } }, isExported: true, @@ -485,14 +494,16 @@ export const REGEXREPLACE = { if (_occurence !== 0) { const matches = [..._text.matchAll(regex)]; if (matches.length === 0 || Math.abs(_occurence) > matches.length) { - return _text; + return { value: _text }; } const i = _occurence > 0 ? _occurence - 1 : matches.length + _occurence; const length = matches[i][0].length; const position = matches[i].index; - return _text.substring(0, position) + _replacement + _text.substring(position + length); + return { + value: _text.substring(0, position) + _replacement + _text.substring(position + length), + }; } - return _text.replace(regex, _replacement); + return { value: _text.replace(regex, _replacement) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -527,7 +538,10 @@ export const REPLACE = { const _text = toString(text); const _length = toNumber(length, this.locale); const _newText = toString(newText); - return _text.substring(0, _position - 1) + _newText + _text.substring(_position - 1 + _length); + return { + value: + _text.substring(0, _position - 1) + _newText + _text.substring(_position - 1 + _length), + }; }, isExported: true, } satisfies AddFunctionDescription; @@ -553,7 +567,7 @@ export const RIGHT = { } const _text = toString(text); const stringLength = _text.length; - return _text.substring(stringLength - _numberOfCharacters, stringLength); + return { value: _text.substring(stringLength - _numberOfCharacters, stringLength) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -633,7 +647,7 @@ export const SPLIT = { ) ), ], - compute: function ( + computeArray: function ( text: Maybe, delimiter: Maybe, splitByEach: Maybe = { value: SPLIT_DEFAULT_SPLIT_BY_EACH }, @@ -655,7 +669,7 @@ export const SPLIT = { result = result.filter((text) => text !== ""); } - return transposeMatrix([result]); + return matrixMap(transposeMatrix([result]), (value) => ({ value })); }, isExported: false, } satisfies AddFunctionDescription; @@ -693,17 +707,21 @@ export const SUBSTITUTE = { const _textToSearch = toString(textToSearch); const _searchFor = toString(searchFor); if (_searchFor === "") { - return _textToSearch; + return { value: _textToSearch }; } const _replaceWith = toString(replaceWith); const reg = new RegExp(escapeRegExp(_searchFor), "g"); if (_occurrenceNumber === 0) { - return _textToSearch.replace(reg, _replaceWith); + return { value: _textToSearch.replace(reg, _replaceWith) }; } let n = 0; - return _textToSearch.replace(reg, (text) => (++n === _occurrenceNumber ? _replaceWith : text)); + return { + value: _textToSearch.replace(reg, (text) => + ++n === _occurrenceNumber ? _replaceWith : text + ), + }; }, isExported: true, } satisfies AddFunctionDescription; @@ -739,16 +757,18 @@ export const TEXTJOIN = { delimiter: Maybe, ignoreEmpty: Maybe = { value: TEXTJOIN_DEFAULT_IGNORE_EMPTY }, ...textsOrArrays: Arg[] - ): string { + ) { const _delimiter = toString(delimiter); const _ignoreEmpty = toBoolean(ignoreEmpty); let n = 0; - return reduceAny( - textsOrArrays, - (acc, a) => - !(_ignoreEmpty && toString(a) === "") ? (n++ ? acc + _delimiter : "") + toString(a) : acc, - "" - ); + return { + value: reduceAny( + textsOrArrays, + (acc, a) => + !(_ignoreEmpty && toString(a) === "") ? (n++ ? acc + _delimiter : "") + toString(a) : acc, + "" + ), + }; }, isExported: true, } satisfies AddFunctionDescription; @@ -787,7 +807,7 @@ export const TEXTSPLIT = { _t("The value to use for padding empty cells.") ), ], - compute: function ( + computeArray: function ( text: FunctionResultObject, colDelimiter: Arg, rowDelimiter: Arg, @@ -865,8 +885,8 @@ export const TRIM = { args: [ arg("text (string)", _t("The text or reference to a cell containing text to be trimmed.")), ], - compute: function (text: Maybe): string { - return trimContent(toString(text)); + compute: function (text: Maybe) { + return { value: trimContent(toString(text)) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -877,8 +897,8 @@ export const TRIM = { export const UPPER = { description: _t("Converts a specified string to uppercase."), args: [arg("text (string)", _t("The string to convert to uppercase."))], - compute: function (text: Maybe): string { - return toString(text).toUpperCase(); + compute: function (text: Maybe) { + return { value: toString(text).toUpperCase() }; }, isExported: true, } satisfies AddFunctionDescription; @@ -897,12 +917,9 @@ export const TEXT = { ) ), ], - compute: function ( - number: Maybe, - format: Maybe - ): string { + compute: function (number: Maybe, format: Maybe) { const _number = toNumber(number, this.locale); - return formatValue(_number, { format: toString(format), locale: this.locale }); + return { value: formatValue(_number, { format: toString(format), locale: this.locale }) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -913,8 +930,8 @@ export const TEXT = { export const VALUE = { description: _t("Converts a string to a numeric value."), args: [arg("value (number)", _t("the string to be converted"))], - compute: function (value: Maybe): number { - return toNumber(value, this.locale); + compute: function (value: Maybe) { + return { value: toNumber(value, this.locale) }; }, isExported: true, } satisfies AddFunctionDescription; diff --git a/src/functions/module_web.ts b/src/functions/module_web.ts index 5ad9232487..09f78c3236 100644 --- a/src/functions/module_web.ts +++ b/src/functions/module_web.ts @@ -17,16 +17,13 @@ export const HYPERLINK = { _t("The text to display in the cell, enclosed in quotation marks.") ), ], - compute: function ( - url: Maybe, - linkLabel: Maybe - ): string { + compute: function (url: Maybe, linkLabel: Maybe) { const processedUrl = toString(url).trim(); const processedLabel = toString(linkLabel) || processedUrl; if (processedUrl === "") { - return processedLabel; + return { value: processedLabel }; } - return markdownLink(processedLabel, processedUrl); + return { value: markdownLink(processedLabel, processedUrl) }; }, isExported: true, } satisfies AddFunctionDescription; diff --git a/src/plugins/ui_core_views/cell_evaluation/compilation_parameters.ts b/src/plugins/ui_core_views/cell_evaluation/compilation_parameters.ts index 9fb4c857ce..25a67d8fa6 100644 --- a/src/plugins/ui_core_views/cell_evaluation/compilation_parameters.ts +++ b/src/plugins/ui_core_views/cell_evaluation/compilation_parameters.ts @@ -1,4 +1,3 @@ -import { functionRegistry } from "../../../functions/function_registry"; import { intersection, isZoneValid } from "../../../helpers/zones"; import { _t } from "../../../translation"; import { EvaluatedCell } from "../../../types/cells"; @@ -20,7 +19,6 @@ export type CompilationParameters = { ensureRange: EnsureRange; evalContext: EvalContext; }; -const functionMap = functionRegistry.mapping; /** * Return all functions necessary to properly evaluate a formula: @@ -47,11 +45,11 @@ class CompilationParametersBuilder { private getters: Getters, private computeCell: (position: CellPosition) => EvaluatedCell ) { - this.evalContext = Object.assign(Object.create(functionMap), context, { + this.evalContext = Object.assign({}, context, { getters: this.getters, locale: this.getters.getLocale(), getFormulaResult: this.getFormulaResult.bind(this), - }); + }) as EvalContext; } getParameters(): CompilationParameters { diff --git a/src/plugins/ui_core_views/cell_evaluation/evaluator.ts b/src/plugins/ui_core_views/cell_evaluation/evaluator.ts index 71e9631d9c..22a33f14ed 100644 --- a/src/plugins/ui_core_views/cell_evaluation/evaluator.ts +++ b/src/plugins/ui_core_views/cell_evaluation/evaluator.ts @@ -771,7 +771,8 @@ export function updateEvalContextAndExecute( compilationParams.referenceDenormalizer, compilationParams.ensureRange, getSymbolValue, - evalContext + evalContext, + compiledFormula.preparedFunctions ); evalContext.__originCellPosition = currentCellPosition; evalContext.__originSheetId = currentSheetId; diff --git a/src/types/functions.ts b/src/types/functions.ts index afece78c2a..d7aef8c740 100644 --- a/src/types/functions.ts +++ b/src/types/functions.ts @@ -33,18 +33,31 @@ export interface ArgDefinition { export type ArgProposal = { value: CellValue; label?: string }; -export type ComputeFunction = (this: EvalContext, ...args: Arg[]) => R; +export type PreparedComputeFunction = ( + ctx: EvalContext, + ...args: Arg[] +) => FunctionResultObject | Matrix; -export interface AddFunctionDescription { - compute: ComputeFunction< - FunctionResultObject | Matrix | CellValue | Matrix - >; +export type BaseFunctionDescription = { description: string; category?: string; args: ArgDefinition[]; isExported?: boolean; hidden?: boolean; -} +}; + +export type ComputeFunction = (this: EvalContext, ...args: Arg[]) => FunctionResultObject; + +export type ComputeArrayFunction = ( + this: EvalContext, + ...args: Arg[] +) => FunctionResultObject | Matrix; + +type ComputeVariant = + | { compute: ComputeFunction; computeArray?: undefined } + | { compute?: undefined; computeArray: ComputeArrayFunction }; + +export type AddFunctionDescription = BaseFunctionDescription & ComputeVariant; export type FunctionDescription = AddFunctionDescription & { name: string; diff --git a/src/types/misc.ts b/src/types/misc.ts index 6d77d3ee99..f5738412a1 100644 --- a/src/types/misc.ts +++ b/src/types/misc.ts @@ -6,6 +6,7 @@ import { CellValue, EvaluatedCell } from "./cells"; import { CompiledFormula } from "../formulas/compiler"; import { CommandResult } from "./commands"; import { Format } from "./format"; +import { PreparedComputeFunction } from "./functions"; import { Range } from "./range"; /** @@ -172,6 +173,12 @@ export type ReferenceDenormalizer = (range: Range) => FunctionResultObject; export type EnsureRange = (range: Range) => Matrix; +// export type VectorizedCompute = ( +// formula: (...args: Arg[]) => FunctionResultObject, +// args: Arg[], +// acceptToVectorize?: boolean[] | undefined +// ) => Matrix | FunctionResultObject; + export type GetSymbolValue = (symbolName: string, isRange: boolean) => Arg; export type FormulaToExecute = ( @@ -179,7 +186,8 @@ export type FormulaToExecute = ( refFn: ReferenceDenormalizer, range: EnsureRange, getSymbolValue: GetSymbolValue, - ctx: object + ctx: object, + functions: PreparedComputeFunction[] ) => Matrix | FunctionResultObject; export interface LiteralValues { diff --git a/tests/autofill/autofill_plugin.test.ts b/tests/autofill/autofill_plugin.test.ts index d63b3ab022..eafac88d8a 100644 --- a/tests/autofill/autofill_plugin.test.ts +++ b/tests/autofill/autofill_plugin.test.ts @@ -758,10 +758,11 @@ describe("Autofill", () => { addToRegistry(functionRegistry, "SPREAD.EMPTY", { description: "spreads empty values", args: [], - compute: function (): null[][] { + computeArray: function () { + const value = { value: null }; return [ - [null, null, null], // return 2 col, 3 row matrix - [null, null, null], + [value, value, value], // return 2 col, 3 row matrix + [value, value, value], ]; }, isExported: false, diff --git a/tests/bottom_bar/aggregate_statistics_store.test.ts b/tests/bottom_bar/aggregate_statistics_store.test.ts index ec8e9a8982..beea906973 100644 --- a/tests/bottom_bar/aggregate_statistics_store.test.ts +++ b/tests/bottom_bar/aggregate_statistics_store.test.ts @@ -158,7 +158,7 @@ describe("Aggregate statistic functions", () => { addToRegistry(functionRegistry, "TWOARGSNEEDED", { description: "any function", compute: () => { - return true; + return { value: true }; }, args: [ { name: "arg1", description: "", type: ["ANY"] }, diff --git a/tests/collaborative/collaborative.test.ts b/tests/collaborative/collaborative.test.ts index 895bb78413..e4d2c74f56 100644 --- a/tests/collaborative/collaborative.test.ts +++ b/tests/collaborative/collaborative.test.ts @@ -709,7 +709,7 @@ describe("Multi users synchronisation", () => { let value: string | number = "LOADING..."; addToRegistry(functionRegistry, "GET.ASYNC.VALUE", { description: "Get value", - compute: () => value, + compute: () => ({ value }), args: [], }); const firstSheetId = alice.getters.getActiveSheetId(); @@ -733,7 +733,7 @@ describe("Multi users synchronisation", () => { let value: string | number = "LOADING..."; addToRegistry(functionRegistry, "GET.ASYNC.VALUE", { description: "Get value", - compute: () => value, + compute: () => ({ value }), args: [], }); const firstSheetId = alice.getters.getActiveSheetId(); diff --git a/tests/composer/autocomplete_dropdown_component.test.ts b/tests/composer/autocomplete_dropdown_component.test.ts index 7bda3b4a60..9229f3f9ca 100644 --- a/tests/composer/autocomplete_dropdown_component.test.ts +++ b/tests/composer/autocomplete_dropdown_component.test.ts @@ -47,22 +47,22 @@ beforeEach(() => { .add("IF", { description: "do if", args: [], - compute: () => 1, + compute: () => ({ value: 1 }), }) .add("SUM", { description: "do sum", args: [], - compute: () => 1, + compute: () => ({ value: 1 }), }) .add("SZZ", { description: "do something", args: [], - compute: () => 1, + compute: () => ({ value: 1 }), }) .add("HIDDEN", { description: "do something", args: [], - compute: () => 1, + compute: () => ({ value: 1 }), hidden: true, }); }); @@ -189,7 +189,7 @@ describe("Functions autocomplete", () => { addToRegistry(functionRegistry, `SUM${i + 1}`, { description: "do sum", args: [], - compute: () => 1, + compute: () => ({ value: 1 }), }); } @@ -217,7 +217,7 @@ describe("Functions autocomplete", () => { addToRegistry(functionRegistry, f, { description: "", args: [], - compute: () => 1, + compute: () => ({ value: 1 }), }); } await typeInComposer("=FUZZY"); @@ -233,7 +233,7 @@ describe("Functions autocomplete", () => { addToRegistry(functionRegistry, "FUZZY", { description: "", args: [], - compute: () => 1, + compute: () => ({ value: 1 }), }); await typeInComposer("=FUZZY"); expect(fixture.querySelectorAll(".o-autocomplete-value")).toHaveLength(0); @@ -661,12 +661,12 @@ describe("autocomplete boolean functions", () => { addToRegistry(functionRegistry, "TRUE", { description: "TRUE", args: [], - compute: () => true, + compute: () => ({ value: true }), }); addToRegistry(functionRegistry, "FALSE", { description: "FALSE", args: [], - compute: () => false, + compute: () => ({ value: false }), }); ({ model, fixture, parent } = await mountComposerWrapper()); parent.startComposition(); @@ -703,37 +703,37 @@ describe("composer entries", () => { .add("SEC", { description: "", args: [], - compute: () => 1, + compute: () => ({ value: 1 }), }) .add("SUPER", { description: "", args: [], - compute: () => 1, + compute: () => ({ value: 1 }), }) .add("SIN", { description: "", args: [], - compute: () => 1, + compute: () => ({ value: 1 }), }) .add("SLNT", { description: "", args: [], - compute: () => 1, + compute: () => ({ value: 1 }), }) .add("SECQ", { description: "", args: [], - compute: () => 1, + compute: () => ({ value: 1 }), }) .add("SAPER", { description: "", args: [], - compute: () => 1, + compute: () => ({ value: 1 }), }) .add("SLN", { description: "", args: [], - compute: () => 1, + compute: () => ({ value: 1 }), }); }); test("Autocomplente entries are sorted by length and then alphanumerically", async () => { diff --git a/tests/composer/formula_assistant_component.test.ts b/tests/composer/formula_assistant_component.test.ts index 231ce24e13..ab1fc64569 100644 --- a/tests/composer/formula_assistant_component.test.ts +++ b/tests/composer/formula_assistant_component.test.ts @@ -62,7 +62,7 @@ describe("formula assistant", () => { addToRegistry(functionRegistry, "FUNC0", { description: "func without args", args: [], - compute: () => 1, + compute: () => ({ value: 1 }), }); setTranslationMethod( (str, ...values) => str, @@ -71,18 +71,18 @@ describe("formula assistant", () => { addToRegistry(functionRegistry, "FUNC1", { description: "func1 def", args: [arg("f1ArgA (any)", "f1 ArgA def"), arg("f1ArgB (any)", _t("f1 ArgB def"))], - compute: () => 1, + compute: () => ({ value: 1 }), }); setTranslationMethod((str, ...values) => str); addToRegistry(functionRegistry, "FUNC2", { description: "func2 def", args: [arg("f2ArgA (any)", "f2 ArgA def"), arg("f2ArgB (any, default=TRUE)", "f2 ArgB def")], - compute: () => 1, + compute: () => ({ value: 1 }), }); addToRegistry(functionRegistry, "FUNC3", { description: "func3 def", args: [arg("f3ArgA (any)", "f3 ArgA def"), arg("f3ArgB (any, repeating)", "f3 ArgB def")], - compute: () => 1, + compute: () => ({ value: 1 }), }); addToRegistry(functionRegistry, "FUNC3BIS", { description: "func3bis def", @@ -90,7 +90,7 @@ describe("formula assistant", () => { arg("f3bisArgA (any)", "f3bis ArgA def"), arg("f3bisArgB (any, repeating, optional)", "f3bis ArgB def"), ], - compute: () => 1, + compute: () => ({ value: 1 }), }); addToRegistry(functionRegistry, "UPTOWNFUNC", { description: "a Bruno Mars song ?", @@ -99,7 +99,7 @@ describe("formula assistant", () => { arg("f4ArgB (any, repeating)", "f4 ArgB def"), arg("f4ArgC (any, repeating)", "f4 ArgC def"), ], - compute: () => 1, + compute: () => ({ value: 1 }), }); addToRegistry(functionRegistry, "FUNC5", { description: "a function with one optional argument defined after two repeating argument", @@ -109,7 +109,7 @@ describe("formula assistant", () => { arg("f5ArgC (any, repeating)", "f5 ArgC def"), arg("f5ArgD (any, optional)", "f5 ArgD def"), ], - compute: () => 1, + compute: () => ({ value: 1 }), }); addToRegistry(functionRegistry, "FUNC6", { description: "a function with one optional argument defined after three repeating arguments", @@ -120,7 +120,7 @@ describe("formula assistant", () => { arg("f6ArgD (any, repeating)", "f6 ArgD def"), arg("f6ArgE (any, optional)", "f6 ArgE def"), ], - compute: () => 1, + compute: () => ({ value: 1 }), }); addToRegistry(functionRegistry, "FUNC7", { description: "a function with two optional arguments defined after three repeating arguments", @@ -132,7 +132,7 @@ describe("formula assistant", () => { arg("f7ArgE (any, optional)", "f7 ArgE def"), arg("f7ArgF (any, optional)", "f7 ArgF def"), ], - compute: () => 1, + compute: () => ({ value: 1 }), }); }); @@ -676,12 +676,12 @@ describe("formula assistant for boolean functions", () => { addToRegistry(functionRegistry, "TRUE", { description: "TRUE", args: [], - compute: () => true, + compute: () => ({ value: true }), }); addToRegistry(functionRegistry, "FALSE", { description: "FALSE", args: [], - compute: () => false, + compute: () => ({ value: false }), }); }); diff --git a/tests/evaluation/__snapshots__/compiler.test.ts.snap b/tests/evaluation/__snapshots__/compiler.test.ts.snap index 6aff383781..a495090eed 100644 --- a/tests/evaluation/__snapshots__/compiler.test.ts.snap +++ b/tests/evaluation/__snapshots__/compiler.test.ts.snap @@ -1,294 +1,294 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`compile functions same symbol twice 1`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = getSymbolValue(this.symbols[0], false); const _2 = getSymbolValue(this.symbols[0], false); -return ctx['ADD'](_1, _2); +return functions[0](ctx,_1,_2); // ADD; }" `; exports[`compile functions simple in a function 1`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = getSymbolValue(this.symbols[1], true); -return ctx['SUM'](_1); +return functions[0](ctx,_1); // SUM; }" `; exports[`compile functions simple symbol 1`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { return getSymbolValue(this.symbols[0], false); }" `; exports[`compile functions symbol with optional single quotes 1`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { return getSymbolValue(this.symbols[0], false); }" `; exports[`compile functions symbol with space and with single quotes 1`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { return getSymbolValue(this.symbols[0], false); }" `; exports[`compile functions two different symbols 1`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = getSymbolValue(this.symbols[0], false); const _2 = getSymbolValue(this.symbols[1], false); -return ctx['ADD'](_1, _2); +return functions[0](ctx,_1,_2); // ADD; }" `; exports[`expression compiler array literal expression 1`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = this.literalValues.numbers[0]; const _2 = this.literalValues.numbers[1]; -const _5 = ctx['ARRAY.ROW'](_1,_2); +const _5 = functions[0](ctx,_1,_2); // ARRAY.ROW; const _3 = this.literalValues.numbers[2]; const _4 = this.literalValues.numbers[3]; -const _6 = ctx['ARRAY.ROW'](_3,_4); -return ctx['ARRAY.LITERAL'](_5,_6); +const _6 = functions[1](ctx,_3,_4); // ARRAY.ROW; +return functions[2](ctx,_5,_6); // ARRAY.LITERAL; }" `; exports[`expression compiler array literal expression with one column only 1`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = this.literalValues.numbers[0]; -const _5 = ctx['ARRAY.ROW'](_1); +const _5 = functions[0](ctx,_1); // ARRAY.ROW; const _2 = this.literalValues.numbers[1]; -const _6 = ctx['ARRAY.ROW'](_2); +const _6 = functions[1](ctx,_2); // ARRAY.ROW; const _3 = this.literalValues.numbers[2]; -const _7 = ctx['ARRAY.ROW'](_3); +const _7 = functions[2](ctx,_3); // ARRAY.ROW; const _4 = this.literalValues.numbers[3]; -const _8 = ctx['ARRAY.ROW'](_4); -return ctx['ARRAY.LITERAL'](_5,_6,_7,_8); +const _8 = functions[3](ctx,_4); // ARRAY.ROW; +return functions[4](ctx,_5,_6,_7,_8); // ARRAY.LITERAL; }" `; exports[`expression compiler array literal expression with one row only 1`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = this.literalValues.numbers[0]; const _2 = this.literalValues.numbers[1]; const _3 = this.literalValues.numbers[2]; const _4 = this.literalValues.numbers[3]; -const _5 = ctx['ARRAY.ROW'](_1,_2,_3,_4); -return ctx['ARRAY.LITERAL'](_5); +const _5 = functions[0](ctx,_1,_2,_3,_4); // ARRAY.ROW; +return functions[1](ctx,_5); // ARRAY.LITERAL; }" `; exports[`expression compiler cells are converted to ranges if function require a range 1`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = range(deps[0]); -return ctx['SUM'](_1); +return functions[0](ctx,_1); // SUM; }" `; exports[`expression compiler expression with $ref 1`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = ref(deps[0]); const _2 = ref(deps[1]); -const _3 = ctx['ADD'](_1, _2); +const _3 = functions[0](ctx,_1,_2); // ADD; const _4 = ref(deps[2]); -return ctx['ADD'](_3, _4); +return functions[1](ctx,_3,_4); // ADD; }" `; exports[`expression compiler expression with cell# 1`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = range(deps[0]); -return ctx['SPILLED.RANGE'](_1); +return functions[0](ctx,_1); // SPILLED.RANGE; }" `; exports[`expression compiler expression with range# 1`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = range(deps[0]); -return ctx['SPILLED.RANGE'](_1); +return functions[0](ctx,_1); // SPILLED.RANGE; }" `; exports[`expression compiler expression with references with a sheet 1`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { return ref(deps[0]); }" `; exports[`expression compiler expressions with a debugger 1`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { debugger; ctx["debug"] = true; const _1 = ref(deps[0]); const _2 = this.literalValues.numbers[0]; -return ctx['DIVIDE'](_1, _2); +return functions[0](ctx,_1,_2); // DIVIDE; }" `; exports[`expression compiler read some values and functions 1`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { -const _1 = ref(deps[0]); -const _2 = range(deps[1]); -const _3 = ctx['SUM'](_2); -return ctx['ADD'](_1, _3); +const _2 = ref(deps[0]); +const _1 = range(deps[1]); +const _3 = functions[0](ctx,_1); // SUM; +return functions[1](ctx,_2,_3); // ADD; }" `; exports[`expression compiler some arithmetic expressions 1`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { return this.literalValues.numbers[0]; }" `; exports[`expression compiler some arithmetic expressions 2`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { return { value: true }; }" `; exports[`expression compiler some arithmetic expressions 3`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { return this.literalValues.strings[0]; }" `; exports[`expression compiler some arithmetic expressions 4`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = this.literalValues.numbers[0]; const _2 = this.literalValues.numbers[1]; -return ctx['ADD'](_1, _2); +return functions[0](ctx,_1,_2); // ADD; }" `; exports[`expression compiler some arithmetic expressions 5`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = this.literalValues.numbers[0]; const _2 = this.literalValues.numbers[1]; -return ctx['MULTIPLY'](_1, _2); +return functions[0](ctx,_1,_2); // MULTIPLY; }" `; exports[`expression compiler some arithmetic expressions 6`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = this.literalValues.numbers[0]; const _2 = this.literalValues.numbers[1]; -return ctx['MINUS'](_1, _2); +return functions[0](ctx,_1,_2); // MINUS; }" `; exports[`expression compiler some arithmetic expressions 7`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = this.literalValues.numbers[0]; const _2 = this.literalValues.numbers[1]; -return ctx['DIVIDE'](_1, _2); +return functions[0](ctx,_1,_2); // DIVIDE; }" `; exports[`expression compiler some arithmetic expressions 8`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = this.literalValues.numbers[0]; -return ctx['UMINUS'](_1); +return functions[0](ctx,_1); // UMINUS; }" `; exports[`expression compiler some arithmetic expressions 9`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = this.literalValues.numbers[0]; const _2 = this.literalValues.numbers[1]; -const _3 = ctx['ADD'](_1, _2); -const _4 = this.literalValues.numbers[2]; -const _5 = ctx['UMINUS'](_4); -const _6 = this.literalValues.numbers[3]; -const _7 = ctx['ADD'](_5, _6); -return ctx['MULTIPLY'](_3, _7); +const _6 = functions[0](ctx,_1,_2); // ADD; +const _3 = this.literalValues.numbers[2]; +const _4 = functions[1](ctx,_3); // UMINUS; +const _5 = this.literalValues.numbers[3]; +const _7 = functions[2](ctx,_4,_5); // ADD; +return functions[3](ctx,_6,_7); // MULTIPLY; }" `; exports[`expression compiler some arithmetic expressions 10`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = this.literalValues.numbers[0]; const _2 = this.literalValues.numbers[1]; -return ctx['SUM'](_1,_2); +return functions[0](ctx,_1,_2); // SUM; }" `; exports[`expression compiler some arithmetic expressions 11`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = { value: true }; const _2 = this.literalValues.strings[0]; -return ctx['SUM'](_1,_2); +return functions[0](ctx,_1,_2); // SUM; }" `; exports[`expression compiler some arithmetic expressions 12`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = this.literalValues.numbers[0]; const _2 = undefined; const _3 = this.literalValues.numbers[1]; -return ctx['SUM'](_1,_2,_3); +return functions[0](ctx,_1,_2,_3); // SUM; }" `; exports[`expression compiler some arithmetic expressions 13`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = this.literalValues.numbers[0]; -return ctx['UNARY.PERCENT'](_1); +return functions[0](ctx,_1); // UNARY.PERCENT; }" `; exports[`expression compiler some arithmetic expressions 14`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = this.literalValues.numbers[0]; const _2 = this.literalValues.numbers[1]; -const _3 = ctx['ADD'](_1, _2); -return ctx['UNARY.PERCENT'](_3); +const _3 = functions[0](ctx,_1,_2); // ADD; +return functions[1](ctx,_3); // UNARY.PERCENT; }" `; exports[`expression compiler some arithmetic expressions 15`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = ref(deps[0]); -return ctx['UNARY.PERCENT'](_1); +return functions[0](ctx,_1); // UNARY.PERCENT; }" `; exports[`expression compiler with the same reference multiple times 1`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,functions ) { const _1 = range(deps[0]); const _2 = range(deps[1]); const _3 = range(deps[2]); -return ctx['SUM'](_1,_2,_3); +return functions[0](ctx,_1,_2,_3); // SUM; }" `; diff --git a/tests/evaluation/compiler.test.ts b/tests/evaluation/compiler.test.ts index 0b5f97fd19..cacb326570 100644 --- a/tests/evaluation/compiler.test.ts +++ b/tests/evaluation/compiler.test.ts @@ -107,7 +107,7 @@ describe("compile functions", () => { addToRegistry(functionRegistry, "ANYFUNCTION", { description: "any function", compute: () => { - return true; + return { value: true }; }, args: [ { name: "arg1", description: "", type: ["ANY"] }, @@ -124,7 +124,7 @@ describe("compile functions", () => { addToRegistry(functionRegistry, "OPTIONAL", { description: "function with optional argument", compute: () => { - return true; + return { value: true }; }, args: [ { name: "arg1", description: "", type: ["ANY"] }, @@ -140,7 +140,7 @@ describe("compile functions", () => { addToRegistry(functionRegistry, "USEDEFAULTARG", { description: "function with a default argument", compute: () => { - return true; + return { value: true }; }, args: [ { name: "arg1", description: "", type: ["ANY"] }, @@ -156,7 +156,7 @@ describe("compile functions", () => { addToRegistry(functionRegistry, "REPEATABLE", { description: "function with repeatable argument", compute: (arg) => { - return true; + return { value: true }; }, args: [ { name: "arg1", description: "", type: ["ANY"] }, @@ -172,7 +172,7 @@ describe("compile functions", () => { addToRegistry(functionRegistry, "REPEATABLES", { description: "any function", compute: (arg) => { - return true; + return { value: true }; }, args: [ { name: "arg1", description: "", type: ["ANY"] }, @@ -191,7 +191,7 @@ describe("compile functions", () => { addToRegistry(functionRegistry, "REPEATABLE_AND_OPTIONAL", { description: "function with repeatable argument", compute: (arg) => { - return true; + return { value: true }; }, args: [ { name: "arg1", description: "", type: ["ANY"] }, @@ -209,7 +209,7 @@ describe("compile functions", () => { addToRegistry(functionRegistry, "REPEATABLES_AND_OPTIONALS", { description: "any function", compute: (arg) => { - return true; + return { value: true }; }, args: [ { name: "arg1", description: "", type: ["ANY"] }, diff --git a/tests/evaluation/evaluation.test.ts b/tests/evaluation/evaluation.test.ts index 9718016b21..32733339e4 100644 --- a/tests/evaluation/evaluation.test.ts +++ b/tests/evaluation/evaluation.test.ts @@ -321,7 +321,7 @@ describe("evaluateCells", () => { addToRegistry(functionRegistry, "RANGE.COUNT.FUNCTION", { description: "any function", compute: function (range) { - return toMatrix(range).flat().length; + return { value: toMatrix(range).flat().length }; }, args: [{ name: "range", description: "", type: ["RANGE"], acceptMatrix: true }], }); @@ -1399,7 +1399,7 @@ describe("evaluate formula getter", () => { let value = 1; addToRegistry(functionRegistry, "GETVALUE", { description: "Get value", - compute: () => value, + compute: () => ({ value }), args: [], }); setCellContent(model, "A1", "=GETVALUE()"); @@ -1420,7 +1420,7 @@ describe("evaluate formula getter", () => { }); test("cells are re-evaluated if one of their dependency changes", () => { - const mockCompute = jest.fn().mockReturnValue("Hi"); + const mockCompute = jest.fn().mockReturnValue({ value: "Hi" }); addToRegistry(functionRegistry, "GETVALUE", { description: "Get value", @@ -1431,17 +1431,17 @@ describe("evaluate formula getter", () => { expect(getCellContent(model, "A1")).toBe("Hi"); expect(mockCompute).toHaveBeenCalledTimes(1); resetAllMocks(); - mockCompute.mockReturnValue("Hello"); + mockCompute.mockReturnValue({ value: "Hello" }); setCellContent(model, "A2", "1"); expect(getCellContent(model, "A1")).toBe("Hello"); expect(mockCompute).toHaveBeenCalledTimes(1); }); - test("cells in error are correctly reset", () => { + test.only("cells in error are correctly reset", () => { let value: string | number = "LOADING..."; addToRegistry(functionRegistry, "GETVALUE", { description: "Get value", - compute: () => value, + compute: () => ({ value }), args: [], }); setCellContent(model, "A1", "=SUM(A2)"); @@ -1454,11 +1454,11 @@ describe("evaluate formula getter", () => { expect(getEvaluatedCell(model, "A2").value).toBe(-2); }); - test("cells in error and in another sheet are correctly reset", () => { + test.only("cells in error and in another sheet are correctly reset", () => { let value: string | number = "LOADING..."; addToRegistry(functionRegistry, "GETVALUE", { description: "Get value", - compute: () => value, + compute: () => ({ value }), args: [], }); createSheet(model, { sheetId: "sheet2" }); @@ -1480,7 +1480,7 @@ describe("evaluate formula getter", () => { expect(getEvaluatedCell(model, "A3", firstSheetId).value).toBe(5); }); - test("cells with two consecutive error are correctly evaluated", () => { + test.only("cells with two consecutive error are correctly evaluated", () => { let value: number = 1; addToRegistry(functionRegistry, "GETVALUE", { description: "Get value", diff --git a/tests/evaluation/evaluation_formula_array.test.ts b/tests/evaluation/evaluation_formula_array.test.ts index cf9c4b1682..3d1d4583a2 100644 --- a/tests/evaluation/evaluation_formula_array.test.ts +++ b/tests/evaluation/evaluation_formula_array.test.ts @@ -37,11 +37,13 @@ describe("evaluate formulas that use/return an array", () => { arg("m (number)", "number of row of the matrix"), arg("v (number)", "value to fill matrix"), ], - compute: function (n, m, v): number[][] { + computeArray: function (n, m, v) { const _n = toNumber(toScalar(n), DEFAULT_LOCALE); const _m = toNumber(toScalar(m), DEFAULT_LOCALE); const _v = toNumber(toScalar(v), DEFAULT_LOCALE); - return Array.from({ length: _n }, (_, i) => Array.from({ length: _m }, (_, j) => _v)); + return Array.from({ length: _n }, (_, i) => + Array.from({ length: _m }, (_, j) => ({ value: _v })) + ); }, }); }); @@ -114,7 +116,7 @@ describe("evaluate formulas that use/return an array", () => { test("can interpolate function name when error is returned", () => { addToRegistry(functionRegistry, "GETERR", { description: "Get error", - compute: () => { + computeArray: () => { const error = { value: "#SPILL!", message: "Function [[FUNCTION_NAME]] failed", @@ -156,7 +158,7 @@ describe("evaluate formulas that use/return an array", () => { addToRegistry(functionRegistry, "MATRIX.2.2", { description: "Return an 2*2 matrix with some values", args: [], - compute: function () { + computeArray: function () { return [ [{ value: 1, format: "0.00" }, { value: 2 }], [{ value: 3, format: "0.00" }, { value: 4 }], @@ -178,7 +180,7 @@ describe("evaluate formulas that use/return an array", () => { addToRegistry(functionRegistry, "MATRIX", { description: "Return the matrix passed as argument", args: [arg("matrix (range)", "a matrix")], - compute: function (matrix) { + computeArray: function (matrix) { return toMatrix(matrix); }, }); @@ -645,7 +647,7 @@ describe("evaluate formulas that use/return an array", () => { args: [arg("range (any, range)", "The matrix to be transposed.")], compute: function (values) { c++; - return 5; + return { value: 5 }; }, isExported: true, }); @@ -705,7 +707,7 @@ describe("evaluate formulas that use/return an array", () => { args: [arg("range (any, range)", "")], compute: function () { c++; - return 5; + return { value: 5 }; }, isExported: false, }); @@ -725,7 +727,7 @@ describe("evaluate formulas that use/return an array", () => { args: [arg("range (any, range)", "")], compute: function () { c++; - return 5; + return { value: 5 }; }, isExported: false, }); diff --git a/tests/find_and_replace/find_and_replace_store.test.ts b/tests/find_and_replace/find_and_replace_store.test.ts index 3a2fe8ab8f..7e6cde1fe5 100644 --- a/tests/find_and_replace/find_and_replace_store.test.ts +++ b/tests/find_and_replace/find_and_replace_store.test.ts @@ -238,7 +238,7 @@ describe("basic search", () => { let value = "3"; addToRegistry(functionRegistry, "GETVALUE", { description: "Get value", - compute: () => value, + compute: () => ({ value }), args: [], }); setCellContent(model, "A1", "hello"); diff --git a/tests/functions/arguments.test.ts b/tests/functions/arguments.test.ts index e56048fab7..0a33714a12 100644 --- a/tests/functions/arguments.test.ts +++ b/tests/functions/arguments.test.ts @@ -1,4 +1,4 @@ -import { AddFunctionDescription } from "../../src"; +import { AddFunctionDescription, BaseFunctionDescription, ComputeFunction } from "../../src"; import { addMetaInfoFromArg, arg, @@ -126,9 +126,12 @@ describe("args", () => { }); describe("arguments validation", () => { - const aRandomFunction: Omit = { + const aRandomFunction: Omit< + BaseFunctionDescription & { compute: ComputeFunction; computeZone?: undefined }, + "args" + > = { description: "a random function", - compute: () => 0, + compute: () => ({ value: 0 }), }; function validateArgsDefinition(definitions: string[]) { @@ -179,7 +182,7 @@ describe("function addMetaInfoFromArg", () => { const basicFunction = { description: "basic function", compute: () => { - return true; + return { value: true }; }, args: [ { name: "arg1", description: "", type: ["ANY"] }, @@ -202,7 +205,7 @@ describe("function addMetaInfoFromArg", () => { const useOptional = { description: "function with optional argument", compute: (arg) => { - return true; + return { value: true }; }, args: [ { name: "arg1", description: "", type: ["ANY"] }, @@ -228,7 +231,7 @@ describe("function addMetaInfoFromArg", () => { const useRepeatable = { description: "function with repeatable argument", compute: (arg) => { - return true; + return { value: true }; }, args: [ { name: "arg1", description: "", type: ["ANY"] }, @@ -259,7 +262,7 @@ describe("function addMetaInfoFromArg", () => { const useRepeatables = { description: "function with many repeatable argument", compute: (arg) => { - return true; + return { value: true }; }, args: [ { name: "arg1", description: "", type: ["ANY"] }, @@ -291,7 +294,7 @@ describe("function addMetaInfoFromArg", () => { const useRepeatables = { description: "function with many repeatable argument", compute: (arg) => { - return true; + return { value: true }; }, args: [ { name: "arg1", description: "", type: ["ANY"] }, @@ -339,7 +342,7 @@ describe("function addMetaInfoFromArg", () => { const useRepeatables = { description: "function with many repeatable argument", compute: (arg) => { - return true; + return { value: true }; }, args: [ { name: "arg1", description: "", type: ["ANY"] }, @@ -389,7 +392,7 @@ describe("function addMetaInfoFromArg", () => { const useRepeatables = { description: "function with many repeatable argument", compute: (arg) => { - return true; + return { value: true }; }, args: [ { name: "arg1", description: "", type: ["ANY"] }, @@ -435,7 +438,7 @@ describe("function addMetaInfoFromArg", () => { const useRepeatables = { description: "function with many repeatable argument", compute: (arg) => { - return true; + return { value: true }; }, args: [ { name: "arg1", description: "", type: ["ANY"] }, @@ -472,7 +475,7 @@ describe("function addMetaInfoFromArg", () => { const useRepeatables = { description: "function with many repeatable argument", compute: (arg) => { - return true; + return { value: true }; }, args: [ { name: "arg1", description: "", type: ["ANY"], optional: true }, diff --git a/tests/functions/functions.test.ts b/tests/functions/functions.test.ts index 27d7f14ee7..4b8b2d8a4b 100644 --- a/tests/functions/functions.test.ts +++ b/tests/functions/functions.test.ts @@ -15,7 +15,7 @@ describe("functions", () => { addToRegistry(functionRegistry, "DOUBLEDOUBLE", { description: "Double the first argument", compute: function (arg) { - return 2 * toNumber(toScalar(arg), DEFAULT_LOCALE); + return { value: 2 * toNumber(toScalar(arg), DEFAULT_LOCALE) }; }, args: [arg("number (number)", "my number")], }); @@ -26,7 +26,7 @@ describe("functions", () => { const createBadFunction = () => { addToRegistry(functionRegistry, "TEST*FUNCTION", { description: "Double the first argument", - compute: () => 0, + compute: () => ({ value: 0 }), args: [], }); }; @@ -39,7 +39,7 @@ describe("functions", () => { const createBadFunction = () => { addToRegistry(functionRegistry, "TEST_FUNCTION", { description: "Double the first argument", - compute: () => 0, + compute: () => ({ value: 0 }), args: [], }); }; @@ -51,7 +51,7 @@ describe("functions", () => { addToRegistry(functionRegistry, "RETURN.VALUE.DEPENDING.ON.INPUT.VALUE", { description: "return value depending on input value", compute: function (arg) { - return toNumber(toScalar(arg), DEFAULT_LOCALE) * 2; + return { value: toNumber(toScalar(arg), DEFAULT_LOCALE) * 2 }; }, args: [arg("number (number)", "blabla")], }); @@ -68,7 +68,7 @@ describe("functions", () => { addToRegistry(functionRegistry, "RETURN.VALUE.DEPENDING.ON.INPUT.ERROR", { description: "return value depending on input error", compute: function (arg: Arg) { - return isEvaluationError(toScalar(arg)?.value); + return { value: isEvaluationError(toScalar(arg)?.value) }; }, args: [arg("arg (any)", "blabla")], }); @@ -85,7 +85,7 @@ describe("functions", () => { description: "return value depending on input error", compute: function (arg) { const error = new EvaluationError("Les calculs sont pas bons KEVIN !"); - return toBoolean(toScalar(arg)) ? error : "ceci n'est pas une erreur"; + return toBoolean(toScalar(arg)) ? error : { value: "ceci n'est pas une erreur" }; }, args: [arg("arg (any)", "blabla")], }); @@ -101,9 +101,12 @@ describe("functions", () => { addToRegistry(functionRegistry, "RETURN.ERROR.DEPENDING.ON.INPUT.ERROR", { description: "return value depending on input error", compute: function (arg) { - return toScalar(arg)?.value === CellErrorType.BadExpression - ? CellErrorType.CircularDependency - : CellErrorType.InvalidReference; + return { + value: + toScalar(arg)?.value === CellErrorType.BadExpression + ? CellErrorType.CircularDependency + : CellErrorType.InvalidReference, + }; }, args: [arg("arg (any)", "blabla")], }); @@ -160,7 +163,7 @@ describe("functions", () => { addToRegistry(functionRegistry, "GET.VALUE", { args: [arg("cell (any)", "blabla")], description: "Get the value of a cell", - compute: function (arg) { + computeArray: function (arg) { return arg || { value: 0 }; }, }); @@ -231,7 +234,7 @@ describe("functions", () => { addToRegistry(functionRegistry, "GETCOUCOU", { description: "Get coucou's name", compute: function () { - return (this as any).coucou; + return { value: (this as any).coucou }; }, args: [], }); @@ -245,7 +248,7 @@ describe("functions", () => { description: "Get the number of columns", compute: function () { const sheetId = (this as any).getters.getActiveSheetId(); - return (this as any).getters.getNumberCols(sheetId); + return { value: (this as any).getters.getNumberCols(sheetId) }; }, args: [], }); @@ -259,7 +262,7 @@ describe("functions", () => { description: "undefined", // @ts-expect-error can happen in a vanilla javascript code base compute: function () { - return undefined; + return { value: undefined }; }, args: [], }); @@ -271,15 +274,15 @@ describe("functions", () => { addToRegistry(functionRegistry, "RANGEEXPECTED", { description: "function expect number in 1st arg", compute: (arg) => { - return true; + return { value: true }; }, args: [arg("arg1 (range)", "1st argument")], }); addToRegistry(functionRegistry, "FORMULA_RETURNING_RANGE", { description: "function returning range", - compute: () => { - return [["cucumber"]]; + computeArray: () => { + return [[{ value: "cucumber" }]]; }, args: [], }); @@ -287,7 +290,7 @@ describe("functions", () => { addToRegistry(functionRegistry, "FORMULA_NOT_RETURNING_RANGE", { description: "function returning range", compute: () => { - return "cucumber"; + return { value: "cucumber" }; }, args: [], }); @@ -295,7 +298,7 @@ describe("functions", () => { addToRegistry(functionRegistry, "FORMULA_RETURNING_ERROR", { description: "function returning ERROR", compute: () => { - return "#ERROR"; + return { value: "#ERROR" }; }, args: [], }); @@ -310,8 +313,8 @@ describe("functions", () => { addToRegistry(functionRegistry, "FORMULA_RETURNING_RANGE_WITH_ERROR", { description: "function returning range", - compute: () => { - return [["#ERROR"]]; + computeArray: () => { + return [[{ value: "#ERROR" }]]; }, args: [], }); @@ -380,7 +383,7 @@ describe("functions", () => { addToRegistry(functionRegistry, "SIMPLE_VALUE_EXPECTED", { description: "does not accept a range", compute: (arg) => { - return true; + return { value: true }; }, args: [{ name: "arg1", description: "", type: ["NUMBER"] }], }); diff --git a/tests/functions/module_logical.test.ts b/tests/functions/module_logical.test.ts index 05bdc9d0c8..eae99b09e4 100644 --- a/tests/functions/module_logical.test.ts +++ b/tests/functions/module_logical.test.ts @@ -63,7 +63,7 @@ describe("FALSE formula", () => { }); }); -describe("IF formula", () => { +describe.skip("IF formula", () => { test("functional tests on simple arguments", () => { expect(evaluateCell("A1", { A1: "=IF( , , )" })).toBe(0); // @compatibility: on google sheets, return empty string "" expect(evaluateCell("A1", { A1: "=IF( , 1, 2)" })).toBe(2); @@ -187,7 +187,7 @@ describe("IF formula", () => { }); }); -describe("IFERROR formula", () => { +describe.skip("IFERROR formula", () => { test("functional tests on simple arguments", () => { expect(evaluateCell("A1", { A1: "=IFERROR( )" })).toBe("#BAD_EXPR"); // @compatibility: on google sheets, return #VALUE! expect(evaluateCell("A1", { A1: "=IFERROR( , )" })).toBe(0); // @compatibility: on google sheets, return empty string "" @@ -290,7 +290,7 @@ describe("IFERROR formula", () => { }); }); -describe("IFNA formula", () => { +describe.skip("IFNA formula", () => { test("functional tests on simple arguments", () => { expect(evaluateCell("A1", { A1: "=IFNA( )" })).toBe("#BAD_EXPR"); // @compatibility: on google sheets, return #N/A expect(evaluateCell("A1", { A1: "=IFNA( , )" })).toBe(0); // @compatibility: on google sheets, return empty string "" @@ -357,7 +357,7 @@ describe("IFNA formula", () => { }); }); -describe("IFS formula", () => { +describe.skip("IFS formula", () => { test("functional tests on simple arguments", () => { expect(evaluateCell("A1", { A1: "=IFS( , )" })).toBe("#ERROR"); // @compatibility: on google sheets, return #N/A expect(evaluateCell("A1", { A1: "=IFS( , 1)" })).toBe("#ERROR"); // @compatibility: on google sheets, return #N/A diff --git a/tests/functions/vectorization.test.ts b/tests/functions/vectorization.test.ts index 12b7ce8d80..f1aacff979 100644 --- a/tests/functions/vectorization.test.ts +++ b/tests/functions/vectorization.test.ts @@ -27,15 +27,15 @@ describe("vectorization", () => { { name: "arg2", description: "", type: ["ANY"] }, ], compute: function (arg1, arg2) { - return toString(toScalar(arg1)) + toString(toScalar(arg2)); + return { value: toString(toScalar(arg1)) + toString(toScalar(arg2)) }; }, }); addToRegistry(functionRegistry, "FUNCTION.THAT.SPREADS", { description: "a function that spreads a matrix", args: [{ name: "arg1", description: "", type: ["ANY"] }], - compute: function (arg1) { - const value = toString(toScalar(arg1)); + computeArray: function (arg1) { + const value = { value: toString(toScalar(arg1)) }; return [ [value, value], [value, value], @@ -126,17 +126,13 @@ describe("vectorization", () => { const model = createModelFromGrid(grid); setCellContent(model, "D1", "=FUNCTION.WITHOUT.RANGE.ARGS(A1:B1, A2:C2)"); expect(getRangeValuesAsMatrix(model, "D1:F1")).toEqual([["A1A2", "B1B2", "#N/A"]]); - expect(getEvaluatedCell(model, "F1").message).toBe( - "Array arguments to FUNCTION.WITHOUT.RANGE.ARGS are of different size." - ); + expect(getEvaluatedCell(model, "F1").message).toBe("Array arguments are of different size."); expect(checkFunctionDoesntSpreadBeyondRange(model, "D1:F1")).toBeTruthy(); setCellContent(model, "D2", "=FUNCTION.WITHOUT.RANGE.ARGS(A1:A2, B1:B3)"); expect(getRangeValuesAsMatrix(model, "D2:D4")).toEqual([["A1B1"], ["A2B2"], ["#N/A"]]); expect(checkFunctionDoesntSpreadBeyondRange(model, "D2:D4")).toBeTruthy(); - expect(getEvaluatedCell(model, "D4").message).toBe( - "Array arguments to FUNCTION.WITHOUT.RANGE.ARGS are of different size." - ); + expect(getEvaluatedCell(model, "D4").message).toBe("Array arguments are of different size."); }); test("vectorization of array formula will only return the first value of the array", () => { diff --git a/tests/menus/menu_items_registry.test.ts b/tests/menus/menu_items_registry.test.ts index e28b6aa3ff..7ef74fe80a 100644 --- a/tests/menus/menu_items_registry.test.ts +++ b/tests/menus/menu_items_registry.test.ts @@ -1041,7 +1041,7 @@ describe("Menu Item actions", () => { test("Insert -> Function -> All includes new functions", () => { addToRegistry(functionRegistry, "TEST.FUNC", { args: [], - compute: () => 42, + compute: () => ({ value: 42 }), description: "Test function", }); const env = makeTestEnv(); @@ -1065,7 +1065,7 @@ describe("Menu Item actions", () => { clearFunctions(); addToRegistry(functionRegistry, "HIDDEN.FUNC", { args: [], - compute: () => 42, + compute: () => ({ value: 42 }), description: "Test function", hidden: true, category: "hidden", diff --git a/tests/spreadsheet/spreadsheet_component.test.ts b/tests/spreadsheet/spreadsheet_component.test.ts index 67c6d1a728..ccb0151e42 100644 --- a/tests/spreadsheet/spreadsheet_component.test.ts +++ b/tests/spreadsheet/spreadsheet_component.test.ts @@ -96,7 +96,7 @@ describe("Simple Spreadsheet Component", () => { description: "Get the name of the current sheet", compute: function () { env = this.env; - return "Sheet"; + return { value: "Sheet" }; }, args: [], }); diff --git a/tests/test_helpers/helpers.ts b/tests/test_helpers/helpers.ts index ccca8815bf..02181f4e2d 100644 --- a/tests/test_helpers/helpers.ts +++ b/tests/test_helpers/helpers.ts @@ -52,6 +52,7 @@ import { UID, Zone, } from "../../src"; +import { preparedFunctionsCache } from "../../src/formulas/compiler"; import { getItemId } from "../../src/helpers/data_normalization"; import { detectDateFormat } from "../../src/helpers/format/format"; import { topbarMenuRegistry } from "../../src/registries/menus/topbar_menu_registry"; @@ -87,7 +88,7 @@ import { DOMTarget, click, getTarget, getTextNodes, keyDown, keyUp } from "./dom import { getCellContent, getEvaluatedCell } from "./getters_helpers"; const functionsContent = functionRegistry.content; -const functionMap = functionRegistry.mapping; +const functionMap = functionRegistry.content; const functionsContentRestore = { ...functionsContent }; const functionMapRestore = { ...functionMap }; @@ -128,6 +129,7 @@ export function addToRegistry(registry: Registry, key: string, value: T) { registry.add(key, value); registerCleanup(() => registry.remove(key)); } + registerCleanup(restoreDefaultFunctions); } const realTimeSetTimeout = window.setTimeout.bind(window); @@ -637,6 +639,9 @@ function _clearFunctions() { } export function restoreDefaultFunctions() { + for (const f in preparedFunctionsCache) { + delete preparedFunctionsCache[f]; + } for (const f in functionCache) { delete functionCache[f]; } diff --git a/tests/xlsx/xlsx_export.test.ts b/tests/xlsx/xlsx_export.test.ts index cb04126177..1567b2fb91 100644 --- a/tests/xlsx/xlsx_export.test.ts +++ b/tests/xlsx/xlsx_export.test.ts @@ -1095,26 +1095,26 @@ describe("Test XLSX export", () => { beforeEach(() => { addToRegistry(functionRegistry, "NOW", { ...NOW, - compute: () => 1, + compute: () => ({ value: 1 }), }); addToRegistry(functionRegistry, "RAND", { ...RAND, - compute: () => 1, + compute: () => ({ value: 1 }), }); addToRegistry(functionRegistry, "TODAY", { ...TODAY, - compute: () => 1, + compute: () => ({ value: 1 }), }); addToRegistry(functionRegistry, "RANDARRAY", { ...RANDARRAY, - compute: () => [ - [1, 1], - [1, 1], + computeArray: () => [ + [{ value: 1 }, { value: 1 }], + [{ value: 1 }, { value: 1 }], ], }); addToRegistry(functionRegistry, "RANDBETWEEN", { ...RANDBETWEEN, - compute: () => 1, + compute: () => ({ value: 1 }), }); }); @@ -1167,7 +1167,7 @@ describe("Test XLSX export", () => { addToRegistry(functionRegistry, "NON.EXPORTABLE.ARRAY.FORMULA", { description: "a non exportable formula that spread", args: [], - compute: function () { + computeArray: function () { return [ [ { value: 1, format: "0.00%" },