From fc39bb88ea846c9be836fbf88912609cdaf102b6 Mon Sep 17 00:00:00 2001 From: Alexis Lacroix Date: Wed, 27 May 2026 16:28:13 +0200 Subject: [PATCH 1/6] [REF] compiler: operators as function Task: 6254300 --- src/formulas/compiler.ts | 50 +++++++++++++++------------------------- 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/src/formulas/compiler.ts b/src/formulas/compiler.ts index 11ed3cef06..7b0dd3e698 100644 --- a/src/formulas/compiler.ts +++ b/src/formulas/compiler.ts @@ -491,42 +491,18 @@ function compileTokensOrThrow(tokens: Token[]): ICompiledFormula { return code.return(jsStr`ctx['${jsFnName}'](${args.map((arg) => arg.returnExpression)})`); 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); @@ -684,6 +660,16 @@ function assertEnoughArgs(ast: ASTFuncall) { } } +function toFunCallAst(fnName: string, args: AST[]): ASTFuncall { + return { + type: "FUNCALL", + value: fnName, + args: args, + tokenStartIndex: 0, + tokenEndIndex: 0, + }; +} + function isRangeType(type: string) { return type.startsWith("RANGE"); } From 2c04e8cbd235ee985e25c3a9adc7ee3cb6a27510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Lef=C3=A8vre?= Date: Wed, 27 May 2026 12:18:06 +0200 Subject: [PATCH 2/6] [PERF] evaluation: arg definitions at compile time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A number of checks and pre/post processing is done for each executed function. Some of it can be known at compile time since it only depends on the function and the number of arguments. With this commit, we pre-compute a number of things only once at compile time to minimize what is done when executing the function. Bunch report ------------ cells imported in 09ccc2440: Mean: 2841.65 ms, StdErr: 3.91 ms c0ed960c1: Mean: 2758.60 ms, StdErr: 6.32 ms → 🟢 (vs prev: -3%, Δ=-83.05 ms, combined StdErr=7.43 ms, |Δ|/SE=-11.2x; n=20) evaluate all cells 09ccc2440: Mean: 2678.65 ms, StdErr: 8.38 ms c0ed960c1: Mean: 2458.64 ms, StdErr: 11.72 ms → 🟢 (vs prev: -8%, Δ=-220.02 ms, combined StdErr=14.41 ms, |Δ|/SE=-15.3x; n=20) Model created in 09ccc2440: Mean: 6470.76 ms, StdErr: 9.44 ms c0ed960c1: Mean: 6158.89 ms, StdErr: 14.90 ms → 🟢 (vs prev: -5%, Δ=-311.87 ms, combined StdErr=17.64 ms, |Δ|/SE=-17.7x; n=20) Large formula dataset --------------------- cells imported in 09ccc2440: Mean: 2703.84 ms, StdErr: 14.52 ms c0ed960c1: Mean: 2704.29 ms, StdErr: 14.18 ms → ⚫ (vs prev: +0%, Δ=+0.45 ms, combined StdErr=20.30 ms, |Δ|/SE=0.0x; n=20) evaluate all cells 09ccc2440: Mean: 400.51 ms, StdErr: 2.94 ms c0ed960c1: Mean: 375.49 ms, StdErr: 2.43 ms → 🟢 (vs prev: -6%, Δ=-25.02 ms, combined StdErr=3.81 ms, |Δ|/SE=-6.6x; n=20) Model created in 09ccc2440: Mean: 3441.85 ms, StdErr: 14.57 ms c0ed960c1: Mean: 3419.21 ms, StdErr: 14.61 ms → ⚫ (vs prev: -1%, Δ=-22.64 ms, combined StdErr=20.63 ms, |Δ|/SE=-1.1x; n=20) Other production spreadsheet (heavily vectorized) ------------------------------------------------- ⚫ no measureable change anywhere Yet another production spreadsheet (with some vectorization) ------------------------------------------------------------ cells imported in 09ccc2440: Mean: 491.04 ms, StdErr: 6.53 ms c0ed960c1: Mean: 493.73 ms, StdErr: 4.17 ms → ⚫ (vs prev: +1%, Δ=+2.69 ms, combined StdErr=7.75 ms, |Δ|/SE=0.3x; n=20) evaluate all cells 09ccc2440: Mean: 660.53 ms, StdErr: 3.85 ms c0ed960c1: Mean: 633.88 ms, StdErr: 3.46 ms → 🟢 (vs prev: -4%, Δ=-26.65 ms, combined StdErr=5.18 ms, |Δ|/SE=-5.1x; n=20) Model created in 09ccc2440: Mean: 1423.05 ms, StdErr: 9.20 ms c0ed960c1: Mean: 1396.71 ms, StdErr: 7.02 ms → 🟢 (vs prev: -2%, Δ=-26.33 ms, combined StdErr=11.57 ms, |Δ|/SE=-2.3x; n=20) Legend: ⚫: no measurable change |Δ|/SE < 2 🔴: slower Δ > 0 and |Δ|/SE >= 2 🟢: faster Δ < 0 and |Δ|/SE >= 2 Task: 6254300 --- src/formulas/compiler.ts | 44 +++++- src/functions/create_compute_function.ts | 69 ++++---- src/functions/function_registry.ts | 9 +- src/functions/module_logical.ts | 33 ++-- src/functions/module_math.ts | 5 +- .../cell_evaluation/compilation_parameters.ts | 5 +- .../cell_evaluation/evaluator.ts | 3 +- src/types/functions.ts | 6 +- src/types/misc.ts | 4 +- .../__snapshots__/compiler.test.ts.snap | 148 +++++++++--------- tests/test_helpers/helpers.ts | 14 +- 11 files changed, 195 insertions(+), 145 deletions(-) diff --git a/src/formulas/compiler.ts b/src/formulas/compiler.ts index 7b0dd3e698..6298191f13 100644 --- a/src/formulas/compiler.ts +++ b/src/formulas/compiler.ts @@ -1,4 +1,6 @@ +import { ComputeFunction, FunctionResultObject, Matrix } from ".."; import { argTargeting } from "../functions/arguments"; +import { createComputeFunction } from "../functions/create_compute_function"; import { functionRegistry } from "../functions/function_registry"; import { canBeNamedRangeToken } from "../helpers/formulas"; import { concat, unquote } from "../helpers/misc"; @@ -55,6 +57,7 @@ export const UNARY_OPERATOR_MAP = { interface ICompiledFormula { execute: FormulaToExecute; + computeFunctions: ComputeFunction>[]; tokens: Token[]; dependencies: string[]; isBadExpression: boolean; @@ -71,6 +74,10 @@ const NO_REAL_VALUE = "__NO_REAL_VALUE__"; // It is only exported for testing purposes export const functionCache: { [key: string]: FormulaToExecute } = {}; +export const computeFunctionsCache: { + [key: string]: ComputeFunction>[]; +} = {}; + const collator = new Intl.Collator("en", { sensitivity: "accent" }); /** @@ -91,7 +98,10 @@ export class CompiledFormula implements Omit + >[] ) { this.hasDependencies = dependencies?.length > 0; this.tokens.forEach((t) => { @@ -227,7 +237,8 @@ export class CompiledFormula implements Omit getters.getRangeFromSheetXC(sheetId, xc)), params.isBadExpression, params.normalizedFormula, - params.execute + params.execute, + params.computeFunctions ); } } @@ -381,6 +396,7 @@ function compileTokens(tokens: Token[]): ICompiledFormula { execute: function () { return error; }, + computeFunctions: [], isBadExpression: true, normalizedFormula: tokens.map((t) => t.value).join(""), }; @@ -397,6 +413,8 @@ function compileTokensOrThrow(tokens: Token[]): ICompiledFormula { let stringCount = 0; let numberCount = 0; let dependencyCount = 0; + const computeFunctions: ComputeFunction>[] = + []; if (ast.type === "BIN_OPERATION" && ast.value === ":") { throw new BadExpressionError(_t("Invalid formula")); @@ -414,11 +432,13 @@ function compileTokensOrThrow(tokens: Token[]): ICompiledFormula { "range", // same as above, but guarantee that the result is in the form of a range "getSymbolValue", "ctx", + "computeFunctions", code.toString() ); // @ts-ignore functionCache[cacheKey] = baseFunction; + computeFunctionsCache[cacheKey] = computeFunctions; /** * This function compile the function arguments. It is mostly straightforward, @@ -488,7 +508,16 @@ function compileTokensOrThrow(tokens: Token[]): ICompiledFormula { 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 = computeFunctions.length; + computeFunctions.push(createComputeFunction(functions[fnName], args.length)); + const comment = jsStr`// ${jsFnName}`; + if (args.length === 0) { + return code.return(jsStr`computeFunctions[${funCallIndex}](ctx); ${comment}`); + } + const compiledArgs = args.map((arg) => arg.returnExpression); + return code.return( + jsStr`computeFunctions[${funCallIndex}](ctx,${compiledArgs}); ${comment}` + ); case "ARRAY": { // a literal array is compiled into function calls return compileAST( @@ -514,6 +543,7 @@ function compileTokensOrThrow(tokens: Token[]): ICompiledFormula { } const compiledFormula: ICompiledFormula = { execute: functionCache[cacheKey], + computeFunctions: computeFunctionsCache[cacheKey], dependencies, literalValues, symbols, diff --git a/src/functions/create_compute_function.ts b/src/functions/create_compute_function.ts index 11de7cec87..a4580e1139 100644 --- a/src/functions/create_compute_function.ts +++ b/src/functions/create_compute_function.ts @@ -54,6 +54,7 @@ export function applyVectorization( context: EvalContext, descr: FunctionDescription, args: Arg[], + argDefinitions: ArgDefinition[], acceptToVectorize: boolean[] | undefined = undefined ): FunctionResultObject | Matrix { let countVectorizedCol = 1; @@ -92,12 +93,6 @@ export function applyVectorization( } } - 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 (countVectorizedCol === 1 && countVectorizedRow === 1) { // either this function is not vectorized or it ends up with a 1x1 dimension return errorHandlingCompute(descr, context, args, argDefinitions); @@ -238,45 +233,59 @@ function isFunctionResultObject(obj: unknown): obj is FunctionResultObject { return typeof obj === "object" && obj !== null && "value" in obj; } +export function getFunctionArgDefinitions( + descr: FunctionDescription, + argCount: number +): ArgDefinition[] { + const argDefs: ArgDefinition[] = []; + const argsToFocus = argTargeting(descr, argCount); + for (let i = 0; i < argCount; i++) { + argDefs.push(descr.args[argsToFocus[i].index]); + } + return argDefs; +} + export function createComputeFunction( - descr: FunctionDescription -): ComputeFunction | FunctionResultObject> { - function vectorizedCompute( - this: EvalContext, - ...args: Arg[] - ): FunctionResultObject | Matrix { + descr: FunctionDescription, + argCount: number +): ComputeFunction> { + const functionName = descr.name; + const argsToFocus = argTargeting(descr, argCount); + const argDefinitions: ArgDefinition[] = []; + const acceptToVectorize: boolean[] = []; + const matrixOnlyArgIndices: number[] = []; + for (let i = 0; i < argCount; i++) { + const def = descr.args[argsToFocus[i].index]; + argDefinitions.push(def); + acceptToVectorize.push(!def.acceptMatrix); + if (def.acceptMatrixOnly) { + matrixOnlyArgIndices.push(i); + } + } + function vectorizedCompute(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 (const argIndex of matrixOnlyArgIndices) { + if (!isMatrix(args[argIndex])) { throw new BadExpressionError( _t( "Function %s expects the parameter '%s' to be reference to a cell or range.", descr.name, - (i + 1).toString() + (argIndex + 1).toString() ) ); } - acceptToVectorize.push(!argDefinition.acceptMatrix); } - const result = replaceErrorPlaceholderInResult( - applyVectorization(this, descr, args, acceptToVectorize) + applyVectorization(evalContext, descr, args, argDefinitions, acceptToVectorize) ); - 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, }); } diff --git a/src/functions/function_registry.ts b/src/functions/function_registry.ts index c48a636951..2f3ff92de2 100644 --- a/src/functions/function_registry.ts +++ b/src/functions/function_registry.ts @@ -1,18 +1,12 @@ import { Registry } from "../registries/registry"; -import { AddFunctionDescription, ComputeFunction, FunctionDescription } from "../types/functions"; -import { FunctionResultObject, Matrix } from "../types/misc"; +import { AddFunctionDescription, FunctionDescription } from "../types/functions"; import { addMetaInfoFromArg, validateArguments } from "./arguments"; -import { createComputeFunction } from "./create_compute_function"; import { _t } from "../translation"; 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 +27,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_logical.ts b/src/functions/module_logical.ts index 2fce223b14..54c38b2870 100644 --- a/src/functions/module_logical.ts +++ b/src/functions/module_logical.ts @@ -3,7 +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 { applyVectorization, getFunctionArgDefinitions } from "./create_compute_function"; import { functionRegistry } from "./function_registry"; import { boolAnd, boolOr } from "./helper_logical"; import { isMultipleElementMatrix, toScalar } from "./helper_matrices"; @@ -73,11 +73,13 @@ export const IF = { ], compute: function (logicalExpression: Arg, valueIfTrue: Arg, valueIfFalse: Arg) { if (isMultipleElementMatrix(logicalExpression)) { - return applyVectorization(this, functionRegistry.get("IF"), [ - logicalExpression, - valueIfTrue, - valueIfFalse, - ]); + const IF = functionRegistry.get("IF"); + return applyVectorization( + this, + IF, + [logicalExpression, valueIfTrue, valueIfFalse], + getFunctionArgDefinitions(IF, 3) + ); } const result = toBoolean(toScalar(logicalExpression)) ? valueIfTrue : valueIfFalse; return result ?? { value: 0 }; @@ -99,7 +101,13 @@ export const IFERROR = { ], compute: function (value: Arg, valueIfError: Arg) { if (isMultipleElementMatrix(value)) { - return applyVectorization(this, functionRegistry.get("IFERROR"), [value, valueIfError]); + const IFERROR = functionRegistry.get("IFERROR"); + return applyVectorization( + this, + IFERROR, + [value, valueIfError], + getFunctionArgDefinitions(IFERROR, 2) + ); } const result = isEvaluationError(toScalar(value)?.value) ? valueIfError : value; return result ?? { value: 0 }; @@ -121,7 +129,13 @@ export const IFNA = { ], compute: function (value: Arg, valueIfError: Arg) { if (isMultipleElementMatrix(value)) { - return applyVectorization(this, functionRegistry.get("IFNA"), [value, valueIfError]); + const IFNA = functionRegistry.get("IFNA"); + return applyVectorization( + this, + IFNA, + [value, valueIfError], + getFunctionArgDefinitions(IFNA, 2) + ); } const result = toScalar(value)?.value === CellErrorType.NotAvailable ? valueIfError : value; return result ?? { value: 0 }; @@ -154,7 +168,8 @@ export const IFS = { } while (values.length > 0) { if (isMultipleElementMatrix(values[0])) { - return applyVectorization(this, functionRegistry.get("IFS"), values); + const IFS = functionRegistry.get("IFS"); + return applyVectorization(this, IFS, values, getFunctionArgDefinitions(IFS, values.length)); } const condition = toBoolean(toScalar(values.shift())); const valueIfTrue = values.shift(); diff --git a/src/functions/module_math.ts b/src/functions/module_math.ts index 25513fd13e..e29c747ace 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"; @@ -1417,7 +1419,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; 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..4ae245e080 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,10 +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), + __originSheetId: "", }); } diff --git a/src/plugins/ui_core_views/cell_evaluation/evaluator.ts b/src/plugins/ui_core_views/cell_evaluation/evaluator.ts index 71e9631d9c..e402032d66 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.computeFunctions ); evalContext.__originCellPosition = currentCellPosition; evalContext.__originSheetId = currentSheetId; diff --git a/src/types/functions.ts b/src/types/functions.ts index afece78c2a..870968a0ae 100644 --- a/src/types/functions.ts +++ b/src/types/functions.ts @@ -33,10 +33,12 @@ export interface ArgDefinition { export type ArgProposal = { value: CellValue; label?: string }; -export type ComputeFunction = (this: EvalContext, ...args: Arg[]) => R; +export type ComputeFunction = (ctx: EvalContext, ...args: Arg[]) => R; + +type BindedComputeFunction = (this: EvalContext, ...args: Arg[]) => R; export interface AddFunctionDescription { - compute: ComputeFunction< + compute: BindedComputeFunction< FunctionResultObject | Matrix | CellValue | Matrix >; description: string; diff --git a/src/types/misc.ts b/src/types/misc.ts index 7924b90050..5695efde29 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 { ComputeFunction } from "./functions"; import { Range } from "./range"; /** @@ -179,7 +180,8 @@ export type FormulaToExecute = ( refFn: ReferenceDenormalizer, range: EnsureRange, getSymbolValue: GetSymbolValue, - ctx: object + ctx: object, + functions: ComputeFunction>[] ) => Matrix | FunctionResultObject; export interface LiteralValues { diff --git a/tests/evaluation/__snapshots__/compiler.test.ts.snap b/tests/evaluation/__snapshots__/compiler.test.ts.snap index 6aff383781..831faf33f5 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,computeFunctions ) { const _1 = getSymbolValue(this.symbols[0], false); const _2 = getSymbolValue(this.symbols[0], false); -return ctx['ADD'](_1, _2); +return computeFunctions[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,computeFunctions ) { const _1 = getSymbolValue(this.symbols[1], true); -return ctx['SUM'](_1); +return computeFunctions[0](ctx,_1); // SUM; }" `; exports[`compile functions simple symbol 1`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,computeFunctions ) { 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,computeFunctions ) { 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,computeFunctions ) { 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,computeFunctions ) { const _1 = getSymbolValue(this.symbols[0], false); const _2 = getSymbolValue(this.symbols[1], false); -return ctx['ADD'](_1, _2); +return computeFunctions[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,computeFunctions ) { const _1 = this.literalValues.numbers[0]; const _2 = this.literalValues.numbers[1]; -const _5 = ctx['ARRAY.ROW'](_1,_2); +const _5 = computeFunctions[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 = computeFunctions[1](ctx,_3,_4); // ARRAY.ROW; +return computeFunctions[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,computeFunctions ) { const _1 = this.literalValues.numbers[0]; -const _5 = ctx['ARRAY.ROW'](_1); +const _5 = computeFunctions[0](ctx,_1); // ARRAY.ROW; const _2 = this.literalValues.numbers[1]; -const _6 = ctx['ARRAY.ROW'](_2); +const _6 = computeFunctions[1](ctx,_2); // ARRAY.ROW; const _3 = this.literalValues.numbers[2]; -const _7 = ctx['ARRAY.ROW'](_3); +const _7 = computeFunctions[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 = computeFunctions[3](ctx,_4); // ARRAY.ROW; +return computeFunctions[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,computeFunctions ) { 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 = computeFunctions[0](ctx,_1,_2,_3,_4); // ARRAY.ROW; +return computeFunctions[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,computeFunctions ) { const _1 = range(deps[0]); -return ctx['SUM'](_1); +return computeFunctions[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,computeFunctions ) { const _1 = ref(deps[0]); const _2 = ref(deps[1]); -const _3 = ctx['ADD'](_1, _2); +const _3 = computeFunctions[0](ctx,_1,_2); // ADD; const _4 = ref(deps[2]); -return ctx['ADD'](_3, _4); +return computeFunctions[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,computeFunctions ) { const _1 = range(deps[0]); -return ctx['SPILLED.RANGE'](_1); +return computeFunctions[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,computeFunctions ) { const _1 = range(deps[0]); -return ctx['SPILLED.RANGE'](_1); +return computeFunctions[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,computeFunctions ) { 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,computeFunctions ) { debugger; ctx["debug"] = true; const _1 = ref(deps[0]); const _2 = this.literalValues.numbers[0]; -return ctx['DIVIDE'](_1, _2); +return computeFunctions[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,computeFunctions ) { -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 = computeFunctions[0](ctx,_1); // SUM; +return computeFunctions[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,computeFunctions ) { 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,computeFunctions ) { return { value: true }; }" `; exports[`expression compiler some arithmetic expressions 3`] = ` -"function anonymous(deps,ref,range,getSymbolValue,ctx +"function anonymous(deps,ref,range,getSymbolValue,ctx,computeFunctions ) { 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,computeFunctions ) { const _1 = this.literalValues.numbers[0]; const _2 = this.literalValues.numbers[1]; -return ctx['ADD'](_1, _2); +return computeFunctions[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,computeFunctions ) { const _1 = this.literalValues.numbers[0]; const _2 = this.literalValues.numbers[1]; -return ctx['MULTIPLY'](_1, _2); +return computeFunctions[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,computeFunctions ) { const _1 = this.literalValues.numbers[0]; const _2 = this.literalValues.numbers[1]; -return ctx['MINUS'](_1, _2); +return computeFunctions[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,computeFunctions ) { const _1 = this.literalValues.numbers[0]; const _2 = this.literalValues.numbers[1]; -return ctx['DIVIDE'](_1, _2); +return computeFunctions[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,computeFunctions ) { const _1 = this.literalValues.numbers[0]; -return ctx['UMINUS'](_1); +return computeFunctions[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,computeFunctions ) { 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 = computeFunctions[0](ctx,_1,_2); // ADD; +const _3 = this.literalValues.numbers[2]; +const _4 = computeFunctions[1](ctx,_3); // UMINUS; +const _5 = this.literalValues.numbers[3]; +const _7 = computeFunctions[2](ctx,_4,_5); // ADD; +return computeFunctions[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,computeFunctions ) { const _1 = this.literalValues.numbers[0]; const _2 = this.literalValues.numbers[1]; -return ctx['SUM'](_1,_2); +return computeFunctions[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,computeFunctions ) { const _1 = { value: true }; const _2 = this.literalValues.strings[0]; -return ctx['SUM'](_1,_2); +return computeFunctions[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,computeFunctions ) { const _1 = this.literalValues.numbers[0]; const _2 = undefined; const _3 = this.literalValues.numbers[1]; -return ctx['SUM'](_1,_2,_3); +return computeFunctions[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,computeFunctions ) { const _1 = this.literalValues.numbers[0]; -return ctx['UNARY.PERCENT'](_1); +return computeFunctions[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,computeFunctions ) { 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 = computeFunctions[0](ctx,_1,_2); // ADD; +return computeFunctions[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,computeFunctions ) { const _1 = ref(deps[0]); -return ctx['UNARY.PERCENT'](_1); +return computeFunctions[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,computeFunctions ) { const _1 = range(deps[0]); const _2 = range(deps[1]); const _3 = range(deps[2]); -return ctx['SUM'](_1,_2,_3); +return computeFunctions[0](ctx,_1,_2,_3); // SUM; }" `; diff --git a/tests/test_helpers/helpers.ts b/tests/test_helpers/helpers.ts index a40884ee75..f386f39c8f 100644 --- a/tests/test_helpers/helpers.ts +++ b/tests/test_helpers/helpers.ts @@ -53,6 +53,7 @@ import { UID, Zone, } from "../../src"; +import { computeFunctionsCache } 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"; @@ -88,10 +89,8 @@ import { DOMTarget, click, getTarget, getTextNodes, keyDown, keyUp } from "./dom import { getCellContent, getEvaluatedCell } from "./getters_helpers"; const functionsContent = functionRegistry.content; -const functionMap = functionRegistry.mapping; const functionsContentRestore = { ...functionsContent }; -const functionMapRestore = { ...functionMap }; export function spyDispatch(parent: Spreadsheet): jest.SpyInstance { return jest.spyOn(parent["props"].model, "dispatch"); @@ -129,6 +128,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); @@ -625,23 +625,19 @@ export function clearFunctions() { } function _clearFunctions() { - Object.keys(functionMap).forEach((k) => { - delete functionMap[k]; - }); - Object.keys(functionsContent).forEach((k) => { delete functionsContent[k]; }); } export function restoreDefaultFunctions() { + for (const f in computeFunctionsCache) { + delete computeFunctionsCache[f]; + } for (const f in functionCache) { delete functionCache[f]; } _clearFunctions(); - Object.keys(functionMapRestore).forEach((k) => { - functionMap[k] = functionMapRestore[k]; - }); Object.keys(functionsContentRestore).forEach((k) => { functionsContent[k] = functionsContentRestore[k]; }); From b599dd55ff69bc6ea13eff1341748046cc78b069 Mon Sep 17 00:00:00 2001 From: Alexis Lacroix Date: Tue, 21 Apr 2026 12:14:42 +0200 Subject: [PATCH 3/6] [REF] create_compute_function: remove return value adapter layer Remove the layer that was adapting the return value of the compute formula by directly creating the values object within each compute function definition. This simplifies typing by reducing the number of different return cases that needed to be handled in the compute functions. Task: 6254300 --- src/functions/create_compute_function.ts | 63 ++---- src/functions/module_array.ts | 38 ++-- src/functions/module_database.ts | 12 +- src/functions/module_date.ts | 114 +++++----- src/functions/module_engineering.ts | 4 +- src/functions/module_financial.ts | 136 ++++++------ src/functions/module_info.ts | 58 ++--- src/functions/module_logical.ts | 18 +- src/functions/module_lookup.ts | 20 +- src/functions/module_math.ts | 169 +++++++-------- src/functions/module_operators.ts | 11 +- src/functions/module_statistical.ts | 198 ++++++++++-------- src/functions/module_text.ts | 131 +++++++----- src/functions/module_web.ts | 9 +- src/types/functions.ts | 4 +- tests/autofill/autofill_plugin.test.ts | 7 +- .../aggregate_statistics_store.test.ts | 2 +- tests/collaborative/collaborative.test.ts | 4 +- .../autocomplete_dropdown_component.test.ts | 32 +-- .../formula_assistant_component.test.ts | 22 +- tests/evaluation/compiler.test.ts | 14 +- tests/evaluation/evaluation.test.ts | 12 +- .../evaluation_formula_array.test.ts | 12 +- .../find_and_replace_store.test.ts | 2 +- tests/functions/arguments.test.ts | 20 +- tests/functions/functions.test.ts | 39 ++-- tests/functions/vectorization.test.ts | 4 +- tests/menus/menu_items_registry.test.ts | 4 +- .../spreadsheet/spreadsheet_component.test.ts | 2 +- tests/xlsx/xlsx_export.test.ts | 12 +- 30 files changed, 597 insertions(+), 576 deletions(-) diff --git a/src/functions/create_compute_function.ts b/src/functions/create_compute_function.ts index a4580e1139..b078b541e9 100644 --- a/src/functions/create_compute_function.ts +++ b/src/functions/create_compute_function.ts @@ -10,7 +10,7 @@ import { } 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"; @@ -167,45 +167,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, @@ -223,16 +184,28 @@ function errorHandlingCompute( } } try { - return computeFunctionToObject(descr, context, args); + 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); + } + 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 getFunctionArgDefinitions( descr: FunctionDescription, argCount: number diff --git a/src/functions/module_array.ts b/src/functions/module_array.ts index fd1d737829..fad92fd832 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, @@ -314,10 +315,7 @@ 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 ( - data: Matrix, - classes: Matrix - ): Matrix { + compute: function (data: Matrix, classes: Matrix) { const _data = flattenRowFirst([data], (data) => data.value).filter( (val): val is number => typeof val === "number" ); @@ -359,7 +357,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, @@ -397,7 +395,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; @@ -426,7 +424,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; @@ -467,7 +465,7 @@ export const MMULT = { ); } - return multiplyMatrices(_matrix1, _matrix2); + return matrixMap(multiplyMatrices(_matrix1, _matrix2), (value) => ({ value })); }, isExported: true, } satisfies AddFunctionDescription; @@ -505,7 +503,7 @@ export const SUMPRODUCT = { result += product; } } - return result; + return { value: result }; }, isExported: true, } satisfies AddFunctionDescription; @@ -570,7 +568,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 +597,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 +626,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; @@ -853,7 +863,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 +873,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_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 54c38b2870..206efbfc7d 100644 --- a/src/functions/module_logical.ts +++ b/src/functions/module_logical.ts @@ -33,7 +33,7 @@ export const AND = { if (!foundBoolean) { return new EvaluationError(noValidInputErrorMessage); } - return result; + return { value: result }; }, isExported: true, } satisfies AddFunctionDescription; @@ -44,8 +44,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, }; @@ -195,8 +195,8 @@ export const NOT = { ) ), ], - compute: function (logicalExpression: Maybe): boolean { - return !toBoolean(logicalExpression); + compute: function (logicalExpression: Maybe) { + return { value: !toBoolean(logicalExpression) }; }, isExported: true, } satisfies AddFunctionDescription; @@ -219,7 +219,7 @@ export const OR = { if (!foundBoolean) { return new EvaluationError(noValidInputErrorMessage); } - return result; + return { value: result }; }, isExported: true, } satisfies AddFunctionDescription; @@ -272,8 +272,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, }; @@ -302,7 +302,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..5db85adafc 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; @@ -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; @@ -517,7 +517,7 @@ export const MATCH = { ) { return valueNotAvailable(searchKey); } - return index + 1; + return { value: index + 1 }; }, isExported: true, } satisfies AddFunctionDescription; @@ -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; @@ -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 e29c747ace..cf9b8c5f58 100644 --- a/src/functions/module_math.ts +++ b/src/functions/module_math.ts @@ -22,6 +22,7 @@ import { inferFormat, isDataNonEmpty, isEvaluationError, + matrixMap, reduceAny, strictToNumber, toBoolean, @@ -45,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; @@ -69,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; @@ -92,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; @@ -103,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; @@ -134,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; @@ -155,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; @@ -168,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; @@ -180,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; @@ -213,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; @@ -236,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; @@ -361,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; @@ -373,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; @@ -392,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; @@ -410,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; @@ -426,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; @@ -456,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, @@ -465,7 +468,7 @@ export const COUNTIF = { }, this.locale ); - return count; + return { value: count }; }, isExported: true, } satisfies AddFunctionDescription; @@ -479,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, @@ -488,7 +491,7 @@ export const COUNTIFS = { }, this.locale ); - return count; + return { value: count }; }, isExported: true, } satisfies AddFunctionDescription; @@ -500,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; @@ -519,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, @@ -531,7 +534,7 @@ export const COUNTUNIQUEIFS = { }, this.locale ); - return uniqueValues.size; + return { value: uniqueValues.size }; }, } satisfies AddFunctionDescription; @@ -548,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; @@ -566,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; @@ -590,7 +593,7 @@ export const DECIMAL = { const _value = toString(value); if (_value === "") { - return 0; + return { value: 0 }; } /** @@ -610,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; @@ -621,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; @@ -633,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; @@ -759,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; @@ -802,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; @@ -821,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; @@ -850,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; @@ -904,7 +907,7 @@ export const MUNIT = { 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; @@ -934,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; @@ -1009,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; @@ -1077,7 +1080,7 @@ export const RANDARRAY = { } } } - return result; + return matrixMap(result, (value) => ({ value })); }, isExported: true, } satisfies AddFunctionDescription; @@ -1235,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; @@ -1247,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; @@ -1298,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; @@ -1310,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; @@ -1458,7 +1461,7 @@ export const SUMIF = { criteriaRange: Matrix, criterion: Maybe, sumRange: Matrix - ): number { + ) { if (sumRange === undefined) { sumRange = criteriaRange; } @@ -1474,7 +1477,7 @@ export const SUMIF = { }, this.locale ); - return sum; + return { value: sum }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1489,7 +1492,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, @@ -1501,7 +1504,7 @@ export const SUMIFS = { }, this.locale ); - return sum; + return { value: sum }; }, isExported: true, } satisfies AddFunctionDescription; @@ -1512,8 +1515,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; @@ -1524,8 +1527,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; @@ -1569,8 +1572,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..ba801cc11a 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; @@ -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..6fa87f24b5 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; @@ -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, @@ -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, }; @@ -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, @@ -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, }; @@ -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, @@ -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, }; @@ -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..7709fb3132 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; @@ -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; @@ -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; @@ -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/types/functions.ts b/src/types/functions.ts index 870968a0ae..7226661be7 100644 --- a/src/types/functions.ts +++ b/src/types/functions.ts @@ -38,9 +38,7 @@ export type ComputeFunction = (ctx: EvalContext, ...args: Arg[]) => R; type BindedComputeFunction = (this: EvalContext, ...args: Arg[]) => R; export interface AddFunctionDescription { - compute: BindedComputeFunction< - FunctionResultObject | Matrix | CellValue | Matrix - >; + compute: BindedComputeFunction>; description: string; category?: string; args: ArgDefinition[]; diff --git a/tests/autofill/autofill_plugin.test.ts b/tests/autofill/autofill_plugin.test.ts index d63b3ab022..e047a36f3d 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[][] { + compute: 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 4ec31cd132..00379e4c05 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/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..76471ae8dc 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,7 +1431,7 @@ 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); @@ -1441,7 +1441,7 @@ describe("evaluate formula getter", () => { let value: string | number = "LOADING..."; addToRegistry(functionRegistry, "GETVALUE", { description: "Get value", - compute: () => value, + compute: () => ({ value }), args: [], }); setCellContent(model, "A1", "=SUM(A2)"); @@ -1458,7 +1458,7 @@ describe("evaluate formula getter", () => { let value: string | number = "LOADING..."; addToRegistry(functionRegistry, "GETVALUE", { description: "Get value", - compute: () => value, + compute: () => ({ value }), args: [], }); createSheet(model, { sheetId: "sheet2" }); diff --git a/tests/evaluation/evaluation_formula_array.test.ts b/tests/evaluation/evaluation_formula_array.test.ts index cf9c4b1682..6569c92838 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[][] { + compute: 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 })) + ); }, }); }); @@ -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..b61125d398 100644 --- a/tests/functions/arguments.test.ts +++ b/tests/functions/arguments.test.ts @@ -128,7 +128,7 @@ describe("args", () => { describe("arguments validation", () => { const aRandomFunction: Omit = { description: "a random function", - compute: () => 0, + compute: () => ({ value: 0 }), }; function validateArgsDefinition(definitions: string[]) { @@ -179,7 +179,7 @@ describe("function addMetaInfoFromArg", () => { const basicFunction = { description: "basic function", compute: () => { - return true; + return { value: true }; }, args: [ { name: "arg1", description: "", type: ["ANY"] }, @@ -202,7 +202,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 +228,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 +259,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 +291,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 +339,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 +389,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 +435,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 +472,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..7d3be8a661 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")], }); @@ -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,7 +274,7 @@ 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")], }); @@ -279,7 +282,7 @@ describe("functions", () => { addToRegistry(functionRegistry, "FORMULA_RETURNING_RANGE", { description: "function returning range", compute: () => { - return [["cucumber"]]; + 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: [], }); @@ -311,7 +314,7 @@ describe("functions", () => { addToRegistry(functionRegistry, "FORMULA_RETURNING_RANGE_WITH_ERROR", { description: "function returning range", compute: () => { - return [["#ERROR"]]; + 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/vectorization.test.ts b/tests/functions/vectorization.test.ts index 12b7ce8d80..97a5f9af5b 100644 --- a/tests/functions/vectorization.test.ts +++ b/tests/functions/vectorization.test.ts @@ -27,7 +27,7 @@ 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)) }; }, }); @@ -35,7 +35,7 @@ describe("vectorization", () => { description: "a function that spreads a matrix", args: [{ name: "arg1", description: "", type: ["ANY"] }], compute: function (arg1) { - const value = toString(toScalar(arg1)); + const value = { value: toString(toScalar(arg1)) }; return [ [value, value], [value, value], diff --git a/tests/menus/menu_items_registry.test.ts b/tests/menus/menu_items_registry.test.ts index 753a5509df..b5dd07a8cb 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 835bbefc27..39e351e5b7 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/xlsx/xlsx_export.test.ts b/tests/xlsx/xlsx_export.test.ts index c25e456e2f..9059ebe5f9 100644 --- a/tests/xlsx/xlsx_export.test.ts +++ b/tests/xlsx/xlsx_export.test.ts @@ -1100,26 +1100,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], + [{ value: 1 }, { value: 1 }], + [{ value: 1 }, { value: 1 }], ], }); addToRegistry(functionRegistry, "RANDBETWEEN", { ...RANDBETWEEN, - compute: () => 1, + compute: () => ({ value: 1 }), }); }); From 4ffa66c7c1ba31a8fe099a8f5603a98a40758f93 Mon Sep 17 00:00:00 2001 From: Alexis Lacroix Date: Thu, 21 May 2026 11:02:01 +0200 Subject: [PATCH 4/6] [REF] functions: distinguish between scalar and array return type Introduce a type-level distinction between formulas that return a scalar value and formulas that return an array. Previously, all formulas shared the same return type signature, making it impossible to enforce correct usage at compile time. This distinction will help in future commits to know, at compile time, what can be vectorized and what cannot. Task: 6254300 --- src/formulas/compiler.ts | 13 +++---- src/functions/create_compute_function.ts | 12 +++--- src/functions/module_array.ts | 37 ++++++++++--------- src/functions/module_filter.ts | 6 +-- src/functions/module_logical.ts | 8 ++-- src/functions/module_lookup.ts | 20 +++++----- src/functions/module_math.ts | 9 +++-- src/functions/module_operators.ts | 2 +- src/functions/module_statistical.ts | 14 +++---- src/functions/module_text.ts | 6 +-- src/types/functions.ts | 25 ++++++++++--- src/types/misc.ts | 2 +- tests/autofill/autofill_plugin.test.ts | 2 +- .../evaluation_formula_array.test.ts | 8 ++-- tests/functions/arguments.test.ts | 2 +- tests/functions/functions.test.ts | 6 +-- tests/functions/vectorization.test.ts | 2 +- tests/xlsx/xlsx_export.test.ts | 4 +- 18 files changed, 96 insertions(+), 82 deletions(-) diff --git a/src/formulas/compiler.ts b/src/formulas/compiler.ts index 6298191f13..88ad780dc2 100644 --- a/src/formulas/compiler.ts +++ b/src/formulas/compiler.ts @@ -1,4 +1,3 @@ -import { ComputeFunction, FunctionResultObject, Matrix } from ".."; import { argTargeting } from "../functions/arguments"; import { createComputeFunction } from "../functions/create_compute_function"; import { functionRegistry } from "../functions/function_registry"; @@ -8,6 +7,7 @@ import { parseNumber } from "../helpers/numbers"; import { _t } from "../translation"; import { CoreGetters } from "../types/core_getters"; import { BadExpressionError, EvaluationError, UnknownFunctionError } from "../types/errors"; +import { ComputeFunction } from "../types/functions"; import { DEFAULT_LOCALE } from "../types/locale"; import { ApplyRangeChange, @@ -57,7 +57,7 @@ export const UNARY_OPERATOR_MAP = { interface ICompiledFormula { execute: FormulaToExecute; - computeFunctions: ComputeFunction>[]; + computeFunctions: ComputeFunction[]; tokens: Token[]; dependencies: string[]; isBadExpression: boolean; @@ -75,7 +75,7 @@ const NO_REAL_VALUE = "__NO_REAL_VALUE__"; export const functionCache: { [key: string]: FormulaToExecute } = {}; export const computeFunctionsCache: { - [key: string]: ComputeFunction>[]; + [key: string]: ComputeFunction[]; } = {}; const collator = new Intl.Collator("en", { sensitivity: "accent" }); @@ -99,9 +99,7 @@ export class CompiledFormula implements Omit - >[] + public readonly computeFunctions: ComputeFunction[] ) { this.hasDependencies = dependencies?.length > 0; this.tokens.forEach((t) => { @@ -413,8 +411,7 @@ function compileTokensOrThrow(tokens: Token[]): ICompiledFormula { let stringCount = 0; let numberCount = 0; let dependencyCount = 0; - const computeFunctions: ComputeFunction>[] = - []; + const computeFunctions: ComputeFunction[] = []; if (ast.type === "BIN_OPERATION" && ast.value === ":") { throw new BadExpressionError(_t("Invalid formula")); diff --git a/src/functions/create_compute_function.ts b/src/functions/create_compute_function.ts index b078b541e9..5a42bafd8b 100644 --- a/src/functions/create_compute_function.ts +++ b/src/functions/create_compute_function.ts @@ -184,21 +184,21 @@ function errorHandlingCompute( } } try { - const compute = descr.compute; + const computeFormula = descr.compute || descr.computeArray; let result: FunctionResultObject | Matrix | CellValue | Matrix; switch (args.length) { case 1: - result = compute.call(context, args[0]); + result = computeFormula.call(context, args[0]); break; case 2: - result = compute.call(context, args[0], args[1]); + result = computeFormula.call(context, args[0], args[1]); break; case 3: - result = compute.call(context, args[0], args[1], args[2]); + 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 = compute.apply(context, args); + result = computeFormula.apply(context, args); } return result; } catch (e) { @@ -221,7 +221,7 @@ export function getFunctionArgDefinitions( export function createComputeFunction( descr: FunctionDescription, argCount: number -): ComputeFunction> { +): ComputeFunction { const functionName = descr.name; const argsToFocus = argTargeting(descr, argCount); const argDefinitions: ArgDefinition[] = []; diff --git a/src/functions/module_array.ts b/src/functions/module_array.ts index fad92fd832..e517c3febb 100644 --- a/src/functions/module_array.ts +++ b/src/functions/module_array.ts @@ -102,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 @@ -138,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, @@ -153,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, @@ -172,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)); @@ -214,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; @@ -258,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, @@ -300,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, @@ -315,7 +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 (data: Matrix, classes: Matrix) { + computeArray: function ( + data: Matrix, + classes: Matrix + ) { const _data = flattenRowFirst([data], (data) => data.value).filter( (val): val is number => typeof val === "number" ); @@ -369,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, @@ -413,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( @@ -444,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"); @@ -682,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 } @@ -708,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 } @@ -735,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; @@ -751,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, @@ -775,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 } @@ -816,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 } 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_logical.ts b/src/functions/module_logical.ts index 206efbfc7d..77dd9dfc9c 100644 --- a/src/functions/module_logical.ts +++ b/src/functions/module_logical.ts @@ -71,7 +71,7 @@ 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)) { const IF = functionRegistry.get("IF"); return applyVectorization( @@ -99,7 +99,7 @@ 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)) { const IFERROR = functionRegistry.get("IFERROR"); return applyVectorization( @@ -127,7 +127,7 @@ 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)) { const IFNA = functionRegistry.get("IFNA"); return applyVectorization( @@ -160,7 +160,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.") diff --git a/src/functions/module_lookup.ts b/src/functions/module_lookup.ts index 5db85adafc..d9118f67ec 100644 --- a/src/functions/module_lookup.ts +++ b/src/functions/module_lookup.ts @@ -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( @@ -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 { @@ -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( @@ -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 diff --git a/src/functions/module_math.ts b/src/functions/module_math.ts index cf9b8c5f58..401b493b08 100644 --- a/src/functions/module_math.ts +++ b/src/functions/module_math.ts @@ -902,7 +902,7 @@ 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")); @@ -1033,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 }, @@ -1270,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 }, @@ -1380,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) { diff --git a/src/functions/module_operators.ts b/src/functions/module_operators.ts index ba801cc11a..36225c4383 100644 --- a/src/functions/module_operators.ts +++ b/src/functions/module_operators.ts @@ -305,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); } diff --git a/src/functions/module_statistical.ts b/src/functions/module_statistical.ts index 6fa87f24b5..061bd2378b 100644 --- a/src/functions/module_statistical.ts +++ b/src/functions/module_statistical.ts @@ -542,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 @@ -593,7 +593,7 @@ export const GROWTH: AddFunctionDescription = { CALCULATE_B_OPTIONS ), ], - compute: function ( + computeArray: function ( knownDataY: Matrix, knownDataX: Matrix = [[]], newDataX: Matrix = [[]], @@ -717,7 +717,7 @@ export const LINEST: AddFunctionDescription = { RETURN_VERBOSE_OPTIONS ), ], - compute: function ( + computeArray: function ( dataY: Matrix, dataX: Matrix = [[]], calculateB: Maybe = { value: true }, @@ -768,7 +768,7 @@ export const LOGEST: AddFunctionDescription = { RETURN_VERBOSE_OPTIONS ), ], - compute: function ( + computeArray: function ( dataY: Matrix, dataX: Matrix = [[]], calculateB: Maybe = { value: true }, @@ -1149,7 +1149,7 @@ export const POLYFIT_COEFFS: AddFunctionDescription = { COMPUTE_INTERCEPT_OPTIONS ), ], - compute: function ( + computeArray: function ( dataY: Matrix, dataX: Matrix, order: Maybe, @@ -1198,7 +1198,7 @@ export const POLYFIT_FORECAST: AddFunctionDescription = { COMPUTE_INTERCEPT_OPTIONS ), ], - compute: function ( + computeArray: function ( x: Arg, dataY: Matrix, dataX: Matrix, @@ -1609,7 +1609,7 @@ export const TREND: AddFunctionDescription = { CALCULATE_B_OPTIONS ), ], - compute: function ( + computeArray: function ( knownDataY: Matrix, knownDataX: Matrix = [[]], newDataX: Matrix = [[]], diff --git a/src/functions/module_text.ts b/src/functions/module_text.ts index 7709fb3132..b76500e00f 100644 --- a/src/functions/module_text.ts +++ b/src/functions/module_text.ts @@ -395,7 +395,7 @@ export const REGEXEXTRACT = { ] ), ], - compute: function ( + computeArray: function ( text: Maybe, pattern: Maybe, return_mode: Maybe = { value: REGEXEXTRACT_DEFAULT_MODE }, @@ -647,7 +647,7 @@ export const SPLIT = { ) ), ], - compute: function ( + computeArray: function ( text: Maybe, delimiter: Maybe, splitByEach: Maybe = { value: SPLIT_DEFAULT_SPLIT_BY_EACH }, @@ -807,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, diff --git a/src/types/functions.ts b/src/types/functions.ts index 7226661be7..9e8363fd37 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 = (ctx: EvalContext, ...args: Arg[]) => R; +export type ComputeFunction = ( + ctx: EvalContext, + ...args: Arg[] +) => FunctionResultObject | Matrix; -type BindedComputeFunction = (this: EvalContext, ...args: Arg[]) => R; - -export interface AddFunctionDescription { - compute: BindedComputeFunction>; +export type BaseFunctionDescription = { description: string; category?: string; args: ArgDefinition[]; isExported?: boolean; hidden?: boolean; -} +}; + +type ScalarComputeFunction = (this: EvalContext, ...args: Arg[]) => FunctionResultObject; + +type ArrayComputeFunction = ( + this: EvalContext, + ...args: Arg[] +) => FunctionResultObject | Matrix; + +type ComputeVariant = + | { compute: ScalarComputeFunction; computeArray?: never } + | { compute?: never; computeArray: ArrayComputeFunction }; + +export type AddFunctionDescription = BaseFunctionDescription & ComputeVariant; export type FunctionDescription = AddFunctionDescription & { name: string; diff --git a/src/types/misc.ts b/src/types/misc.ts index 5695efde29..6ed28b67da 100644 --- a/src/types/misc.ts +++ b/src/types/misc.ts @@ -181,7 +181,7 @@ export type FormulaToExecute = ( range: EnsureRange, getSymbolValue: GetSymbolValue, ctx: object, - functions: ComputeFunction>[] + functions: ComputeFunction[] ) => Matrix | FunctionResultObject; export interface LiteralValues { diff --git a/tests/autofill/autofill_plugin.test.ts b/tests/autofill/autofill_plugin.test.ts index e047a36f3d..eafac88d8a 100644 --- a/tests/autofill/autofill_plugin.test.ts +++ b/tests/autofill/autofill_plugin.test.ts @@ -758,7 +758,7 @@ describe("Autofill", () => { addToRegistry(functionRegistry, "SPREAD.EMPTY", { description: "spreads empty values", args: [], - compute: function () { + computeArray: function () { const value = { value: null }; return [ [value, value, value], // return 2 col, 3 row matrix diff --git a/tests/evaluation/evaluation_formula_array.test.ts b/tests/evaluation/evaluation_formula_array.test.ts index 6569c92838..3d1d4583a2 100644 --- a/tests/evaluation/evaluation_formula_array.test.ts +++ b/tests/evaluation/evaluation_formula_array.test.ts @@ -37,7 +37,7 @@ 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) { + 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); @@ -116,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", @@ -158,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 }], @@ -180,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); }, }); diff --git a/tests/functions/arguments.test.ts b/tests/functions/arguments.test.ts index b61125d398..d616398e9b 100644 --- a/tests/functions/arguments.test.ts +++ b/tests/functions/arguments.test.ts @@ -126,7 +126,7 @@ describe("args", () => { }); describe("arguments validation", () => { - const aRandomFunction: Omit = { + const aRandomFunction = { description: "a random function", compute: () => ({ value: 0 }), }; diff --git a/tests/functions/functions.test.ts b/tests/functions/functions.test.ts index 7d3be8a661..4b8b2d8a4b 100644 --- a/tests/functions/functions.test.ts +++ b/tests/functions/functions.test.ts @@ -163,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 }; }, }); @@ -281,7 +281,7 @@ describe("functions", () => { addToRegistry(functionRegistry, "FORMULA_RETURNING_RANGE", { description: "function returning range", - compute: () => { + computeArray: () => { return [[{ value: "cucumber" }]]; }, args: [], @@ -313,7 +313,7 @@ describe("functions", () => { addToRegistry(functionRegistry, "FORMULA_RETURNING_RANGE_WITH_ERROR", { description: "function returning range", - compute: () => { + computeArray: () => { return [[{ value: "#ERROR" }]]; }, args: [], diff --git a/tests/functions/vectorization.test.ts b/tests/functions/vectorization.test.ts index 97a5f9af5b..1541491005 100644 --- a/tests/functions/vectorization.test.ts +++ b/tests/functions/vectorization.test.ts @@ -34,7 +34,7 @@ describe("vectorization", () => { addToRegistry(functionRegistry, "FUNCTION.THAT.SPREADS", { description: "a function that spreads a matrix", args: [{ name: "arg1", description: "", type: ["ANY"] }], - compute: function (arg1) { + computeArray: function (arg1) { const value = { value: toString(toScalar(arg1)) }; return [ [value, value], diff --git a/tests/xlsx/xlsx_export.test.ts b/tests/xlsx/xlsx_export.test.ts index 9059ebe5f9..6a2bba6a81 100644 --- a/tests/xlsx/xlsx_export.test.ts +++ b/tests/xlsx/xlsx_export.test.ts @@ -1112,7 +1112,7 @@ describe("Test XLSX export", () => { }); addToRegistry(functionRegistry, "RANDARRAY", { ...RANDARRAY, - compute: () => [ + computeArray: () => [ [{ value: 1 }, { value: 1 }], [{ value: 1 }, { value: 1 }], ], @@ -1172,7 +1172,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%" }, From be44488a872a37d1362c1a2a41cdf88c75ff4799 Mon Sep 17 00:00:00 2001 From: Alexis Lacroix Date: Thu, 28 May 2026 16:55:52 +0200 Subject: [PATCH 5/6] [REF] compilation: manage vectorization during compilation It is no longer necessary to check at each compute function whether vectorization is required. Now, the compilation tells us whether a formula needs to be vectorized or not. Task: 6254300 --- src/formulas/code_builder.ts | 8 ++- src/formulas/compiler.ts | 60 ++++++++++++-------- src/functions/create_compute_function.ts | 72 +++++++++++------------- 3 files changed, 76 insertions(+), 64 deletions(-) diff --git a/src/formulas/code_builder.ts b/src/formulas/code_builder.ts index ccc925ec3c..0e97b9e618 100644 --- a/src/formulas/code_builder.ts +++ b/src/formulas/code_builder.ts @@ -3,6 +3,7 @@ */ export interface FunctionCode { readonly returnExpression: JsString; + readonly returnsMatrix: 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, returnsMatrix: 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, returnsMatrix); } 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 returnsMatrix: boolean ) {} assignResultToVariable(): FunctionCode { diff --git a/src/formulas/compiler.ts b/src/formulas/compiler.ts index 88ad780dc2..c93781e790 100644 --- a/src/formulas/compiler.ts +++ b/src/formulas/compiler.ts @@ -375,6 +375,8 @@ export type SerializedCompiledFormula = { normalizedFormula: string; }; +type CompiledArg = { argAST: FunctionCode; toVectorize: boolean }; + // ----------------------------------------------------------------------------- // COMPILER // ----------------------------------------------------------------------------- @@ -445,7 +447,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]; @@ -456,18 +458,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.returnsMatrix) { + 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.returnsMatrix; + compiledArgs.push({ argAST, toVectorize }); } return compiledArgs; @@ -478,7 +487,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;`); @@ -492,13 +501,14 @@ 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((compileArg) => compileArg.argAST.assignResultToVariable()); code.append(...args); const fnName = ast.value.toUpperCase(); if (!Object.hasOwn(functions, fnName)) { @@ -506,14 +516,19 @@ function compileTokensOrThrow(tokens: Token[]): ICompiledFormula { } const jsFnName = dangerouslyCreateJsStr(fnName); // validated with known functions const funCallIndex = computeFunctions.length; - computeFunctions.push(createComputeFunction(functions[fnName], args.length)); + const argsToVectorize = compiledArgs.map((compiledArg) => compiledArg.toVectorize); + + computeFunctions.push( + createComputeFunction(functions[fnName], args.length, argsToVectorize) + ); + + const returnsMatrix = + functions[fnName].computeArray !== undefined || argsToVectorize.includes(true); const comment = jsStr`// ${jsFnName}`; - if (args.length === 0) { - return code.return(jsStr`computeFunctions[${funCallIndex}](ctx); ${comment}`); - } - const compiledArgs = args.map((arg) => arg.returnExpression); + const parameters = [jsStr`ctx`, ...args.map((arg) => arg.returnExpression)]; return code.return( - jsStr`computeFunctions[${funCallIndex}](ctx,${compiledArgs}); ${comment}` + jsStr`computeFunctions[${funCallIndex}](${parameters}); ${comment}`, + returnsMatrix ); case "ARRAY": { // a literal array is compiled into function calls @@ -532,7 +547,10 @@ function compileTokensOrThrow(tokens: Token[]): ICompiledFormula { } 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`); } @@ -696,7 +714,3 @@ function toFunCallAst(fnName: string, args: AST[]): ASTFuncall { tokenEndIndex: 0, }; } - -function isRangeType(type: string) { - return type.startsWith("RANGE"); -} diff --git a/src/functions/create_compute_function.ts b/src/functions/create_compute_function.ts index 5a42bafd8b..f3690faa89 100644 --- a/src/functions/create_compute_function.ts +++ b/src/functions/create_compute_function.ts @@ -1,5 +1,4 @@ -import { CellValue } from "../types/cells"; -import { BadExpressionError, EvaluationError, NotAvailableError } from "../types/errors"; +import { EvaluationError, NotAvailableError } from "../types/errors"; import { _t } from "../translation"; import { @@ -175,17 +174,18 @@ 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 { const computeFormula = descr.compute || descr.computeArray; - let result: FunctionResultObject | Matrix | CellValue | Matrix; + let result: FunctionResultObject | Matrix; switch (args.length) { case 1: result = computeFormula.call(context, args[0]); @@ -220,40 +220,35 @@ export function getFunctionArgDefinitions( export function createComputeFunction( descr: FunctionDescription, - argCount: number + argCount: number, + argsToVectorize?: boolean[] ): ComputeFunction { const functionName = descr.name; const argsToFocus = argTargeting(descr, argCount); - const argDefinitions: ArgDefinition[] = []; - const acceptToVectorize: boolean[] = []; - const matrixOnlyArgIndices: number[] = []; + const argDefinitions: ArgDefinition[] = new Array(argCount); for (let i = 0; i < argCount; i++) { const def = descr.args[argsToFocus[i].index]; - argDefinitions.push(def); - acceptToVectorize.push(!def.acceptMatrix); - if (def.acceptMatrixOnly) { - matrixOnlyArgIndices.push(i); - } + argDefinitions[i] = def; } - function vectorizedCompute(evalContext: EvalContext, ...args: Arg[]) { + function computeFn(evalContext: EvalContext, ...args: Arg[]) { let start = 0; if (evalContext.__timingEntries) { start = performance.now(); } - for (const argIndex of matrixOnlyArgIndices) { - if (!isMatrix(args[argIndex])) { - throw new BadExpressionError( - _t( - "Function %s expects the parameter '%s' to be reference to a cell or range.", - descr.name, - (argIndex + 1).toString() - ) - ); - } + let result: FunctionResultObject | Matrix; + + if (argsToVectorize !== undefined && argsToVectorize.includes(true)) { + result = replaceErrorPlaceholderInResult( + applyVectorization(evalContext, descr, args, argDefinitions, argsToVectorize), + descr + ); + } else { + result = replaceErrorPlaceholderInResult( + errorHandlingCompute(descr, evalContext, args, argDefinitions), + descr + ); } - const result = replaceErrorPlaceholderInResult( - applyVectorization(evalContext, descr, args, argDefinitions, acceptToVectorize) - ); + if (evalContext.__timingEntries && evalContext.__originCellPosition) { const end = performance.now(); evalContext.__timingEntries.push({ @@ -265,18 +260,19 @@ export function createComputeFunction( 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 result; - } + return computeFn; +} - 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 { From 28808d6e235b988c430bf5ec984abf69af44a43a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Lef=C3=A8vre?= Date: Thu, 28 May 2026 17:54:06 +0200 Subject: [PATCH 6/6] [PERF] vectorization: use indices instead of boolean array for vectorized args Instead of storing a boolean array where each index indicates whether an argument should be vectorized, store only the indices of arguments that need vectorization. This avoids iterating over all arguments and directly targets only the relevant ones. Task: 6254300 --- src/formulas/compiler.ts | 12 ++++++---- src/functions/create_compute_function.ts | 30 ++++++++++-------------- src/functions/helper_matrices.ts | 10 ++++++++ src/functions/module_logical.ts | 19 +++++++++++---- src/functions/module_math.ts | 12 ++++++---- src/index.ts | 2 ++ 6 files changed, 55 insertions(+), 30 deletions(-) diff --git a/src/formulas/compiler.ts b/src/formulas/compiler.ts index c93781e790..12c0bf50a5 100644 --- a/src/formulas/compiler.ts +++ b/src/formulas/compiler.ts @@ -516,14 +516,18 @@ function compileTokensOrThrow(tokens: Token[]): ICompiledFormula { } const jsFnName = dangerouslyCreateJsStr(fnName); // validated with known functions const funCallIndex = computeFunctions.length; - const argsToVectorize = compiledArgs.map((compiledArg) => compiledArg.toVectorize); - + const vectorizedArgsIndices: number[] = []; + for (let i = 0; i < compiledArgs.length; i++) { + if (compiledArgs[i].toVectorize) { + vectorizedArgsIndices.push(i); + } + } computeFunctions.push( - createComputeFunction(functions[fnName], args.length, argsToVectorize) + createComputeFunction(functions[fnName], args.length, vectorizedArgsIndices) ); const returnsMatrix = - functions[fnName].computeArray !== undefined || argsToVectorize.includes(true); + functions[fnName].computeArray !== undefined || vectorizedArgsIndices.length > 0; const comment = jsStr`// ${jsFnName}`; const parameters = [jsStr`ctx`, ...args.map((arg) => arg.returnExpression)]; return code.return( diff --git a/src/functions/create_compute_function.ts b/src/functions/create_compute_function.ts index f3690faa89..2ca3e62157 100644 --- a/src/functions/create_compute_function.ts +++ b/src/functions/create_compute_function.ts @@ -54,7 +54,7 @@ export function applyVectorization( descr: FunctionDescription, args: Arg[], argDefinitions: ArgDefinition[], - acceptToVectorize: boolean[] | undefined = undefined + vectorizedArgsIndices: number[] ): FunctionResultObject | Matrix { let countVectorizedCol = 1; let countVectorizedRow = 1; @@ -63,31 +63,31 @@ export function applyVectorization( let vectorArgsType: VectorArgType[] | undefined = undefined; - for (let i = 0; i < args.length; i++) { - const arg = args[i]; + for (const argIndex of vectorizedArgsIndices) { + 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]; } } } @@ -103,23 +103,19 @@ export function applyVectorization( // Resolve each arg's access pattern once, outside the inner loop. type ArgGetter = (i: number, j: number) => Arg; const argGetters: ArgGetter[] = []; - const vectorizedIndices: number[] = []; // tracks which slots need updating each iteration. for (let k = 0; k < args.length; k++) { const arg = args[k]; switch (vectorArgsType?.[k]) { case "matrix": { argGetters.push((i, j) => arg![i][j]); - vectorizedIndices.push(k); break; } case "horizontal": { argGetters.push((i) => arg![i][0]); - vectorizedIndices.push(k); break; } case "vertical": { argGetters.push((_i, j) => arg![0][j]); - vectorizedIndices.push(k); break; } case undefined: @@ -127,7 +123,7 @@ export function applyVectorization( break; } } - const nbVectorized = vectorizedIndices.length; + const nbVectorized = vectorizedArgsIndices.length; const result: Matrix = new Array(countVectorizedCol); for (let col = 0; col < countVectorizedCol; col++) { @@ -141,7 +137,7 @@ export function applyVectorization( continue; } for (let k = 0; k < nbVectorized; k++) { - argsBuffer[vectorizedIndices[k]] = argGetters[k](col, row); + argsBuffer[vectorizedArgsIndices[k]] = argGetters[k](col, row); } const singleCellComputeResult = errorHandlingCompute( descr, @@ -221,7 +217,7 @@ export function getFunctionArgDefinitions( export function createComputeFunction( descr: FunctionDescription, argCount: number, - argsToVectorize?: boolean[] + vectorizedArgsIndices?: number[] ): ComputeFunction { const functionName = descr.name; const argsToFocus = argTargeting(descr, argCount); @@ -237,9 +233,9 @@ export function createComputeFunction( } let result: FunctionResultObject | Matrix; - if (argsToVectorize !== undefined && argsToVectorize.includes(true)) { + if (vectorizedArgsIndices !== undefined && vectorizedArgsIndices.length > 0) { result = replaceErrorPlaceholderInResult( - applyVectorization(evalContext, descr, args, argDefinitions, argsToVectorize), + applyVectorization(evalContext, descr, args, argDefinitions, vectorizedArgsIndices), descr ); } else { diff --git a/src/functions/helper_matrices.ts b/src/functions/helper_matrices.ts index 1b0d91190f..e96b902cdc 100644 --- a/src/functions/helper_matrices.ts +++ b/src/functions/helper_matrices.ts @@ -159,3 +159,13 @@ function isSingleElementMatrix(matrix: Matrix) { export function isMultipleElementMatrix(arg: any) { return isMatrix(arg) && !isSingleElementMatrix(arg); } + +export function getMatrixArgIndices(args: unknown[]): number[] { + const indices: number[] = []; + for (let i = 0; i < args.length; i++) { + if (isMultipleElementMatrix(args[i])) { + indices.push(i); + } + } + return indices; +} diff --git a/src/functions/module_logical.ts b/src/functions/module_logical.ts index 77dd9dfc9c..d95d8ce88a 100644 --- a/src/functions/module_logical.ts +++ b/src/functions/module_logical.ts @@ -6,7 +6,7 @@ import { arg } from "./arguments"; import { applyVectorization, getFunctionArgDefinitions } from "./create_compute_function"; import { functionRegistry } from "./function_registry"; import { boolAnd, boolOr } from "./helper_logical"; -import { isMultipleElementMatrix, toScalar } from "./helper_matrices"; +import { getMatrixArgIndices, isMultipleElementMatrix, toScalar } from "./helper_matrices"; import { conditionalVisitBoolean, isEvaluationError, @@ -78,7 +78,8 @@ export const IF = { this, IF, [logicalExpression, valueIfTrue, valueIfFalse], - getFunctionArgDefinitions(IF, 3) + getFunctionArgDefinitions(IF, 3), + getMatrixArgIndices([logicalExpression, valueIfTrue, valueIfFalse]) ); } const result = toBoolean(toScalar(logicalExpression)) ? valueIfTrue : valueIfFalse; @@ -106,7 +107,8 @@ export const IFERROR = { this, IFERROR, [value, valueIfError], - getFunctionArgDefinitions(IFERROR, 2) + getFunctionArgDefinitions(IFERROR, 2), + getMatrixArgIndices([value, valueIfError]) ); } const result = isEvaluationError(toScalar(value)?.value) ? valueIfError : value; @@ -134,7 +136,8 @@ export const IFNA = { this, IFNA, [value, valueIfError], - getFunctionArgDefinitions(IFNA, 2) + getFunctionArgDefinitions(IFNA, 2), + getMatrixArgIndices([value, valueIfError]) ); } const result = toScalar(value)?.value === CellErrorType.NotAvailable ? valueIfError : value; @@ -169,7 +172,13 @@ export const IFS = { while (values.length > 0) { if (isMultipleElementMatrix(values[0])) { const IFS = functionRegistry.get("IFS"); - return applyVectorization(this, IFS, values, getFunctionArgDefinitions(IFS, values.length)); + return applyVectorization( + this, + IFS, + values, + getFunctionArgDefinitions(IFS, values.length), + getMatrixArgIndices(values) + ); } const condition = toBoolean(toScalar(values.shift())); const valueIfTrue = values.shift(); diff --git a/src/functions/module_math.ts b/src/functions/module_math.ts index 401b493b08..e9b65c019e 100644 --- a/src/functions/module_math.ts +++ b/src/functions/module_math.ts @@ -15,7 +15,7 @@ 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"; +import { getUnitMatrix, toScalar } from "./helper_matrices"; import { expectReferenceError, generateMatrix, @@ -1380,8 +1380,7 @@ export const SUBTOTAL = { _t("Range or reference for which you want the subtotal.") ), ], - // LUL: not sure about this - computeArray: function (functionCode: Maybe, ...refs: Arg[]) { + compute: function (functionCode: Maybe, ...refs: Arg[]) { let code = toInteger(functionCode, this.locale); let acceptHiddenCells = true; if (code > 100) { @@ -1424,7 +1423,12 @@ export const SUBTOTAL = { } const aggregateName = subtotalFunctionAggregateByCode[code]; - return createComputeFunction(functionRegistry.get(aggregateName), 1)(this, [functionResults]); + const args = [functionResults]; + const result = createComputeFunction(functionRegistry.get(aggregateName), args.length)( + this, + args + ); + return toScalar(result); }, isExported: true, } satisfies AddFunctionDescription; diff --git a/src/index.ts b/src/index.ts index 7fdc7dfcb5..9e917fd422 100644 --- a/src/index.ts +++ b/src/index.ts @@ -339,6 +339,7 @@ import { Select } from "./components/select/select"; import { ChartRangeDataSourceComponent } from "./components/side_panel/chart/building_blocks/range_data_source/range_data_source"; import { TopBar } from "./components/top_bar/top_bar"; import { topBarToolBarRegistry } from "./components/top_bar/top_bar_tools_registry"; +import { createComputeFunction } from "./functions/create_compute_function"; import { parseFormat } from "./helpers/format/format_parser"; import { replaceSymbolInFormula } from "./helpers/formulas"; import { @@ -428,6 +429,7 @@ export const helpers = { getCanonicalSymbolName, fuzzyLookup, replaceSymbolInFormula, + createComputeFunction, }; export const links = {