UNPKG

hyperformula

Version:

HyperFormula is a JavaScript engine for efficient processing of spreadsheet-like data and formulas

222 lines 8.22 kB
/** * @license * Copyright (c) 2025 Handsoncode. All rights reserved. */ import { CellError, ErrorType } from "../../Cell.mjs"; import { ErrorMessage } from "../../error-message.mjs"; import { Condition, CriterionFunctionCompute } from "../CriterionFunctionCompute.mjs"; import { getRawValue, isExtendedNumber } from "../InterpreterValue.mjs"; import { FunctionArgumentType, FunctionPlugin } from "./FunctionPlugin.mjs"; class AverageResult { constructor(sum, count) { this.sum = sum; this.count = count; } static single(arg) { return new AverageResult(arg, 1); } compose(other) { return new AverageResult(this.sum + other.sum, this.count + other.count); } averageValue() { if (this.count > 0) { return this.sum / this.count; } else { return undefined; } } } AverageResult.empty = new AverageResult(0, 0); /** Computes key for criterion function cache */ function conditionalAggregationFunctionCacheKey(functionName) { return conditions => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const conditionsStrings = conditions.map(c => `${c.conditionRange.range.sheet},${c.conditionRange.range.start.col},${c.conditionRange.range.start.row}`); return [functionName, ...conditionsStrings].join(','); }; } function zeroForInfinite(value) { if (isExtendedNumber(value) && !Number.isFinite(getRawValue(value))) { return 0; } else { return value; } } function mapToRawScalarValue(arg) { if (arg instanceof CellError) { return arg; } if (isExtendedNumber(arg)) { return getRawValue(arg); } return undefined; } export class ConditionalAggregationPlugin extends FunctionPlugin { /** * Corresponds to SUMIF(Range, Criterion, SumRange) * * Range is the range to which criterion is to be applied. * Criterion is the criteria used to choose which cells will be included in sum. * SumRange is the range on which adding will be performed. * * @param ast * @param state */ sumif(ast, state) { const functionName = 'SUMIF'; const computeFn = (conditionRange, criterion, values) => this.computeConditionalAggregationFunction(values !== null && values !== void 0 ? values : conditionRange, [conditionRange, criterion], functionName, 0, (left, right) => this.arithmeticHelper.nonstrictadd(left, right), mapToRawScalarValue); return this.runFunction(ast.args, state, this.metadata(functionName), computeFn); } sumifs(ast, state) { const functionName = 'SUMIFS'; const computeFn = (values, ...args) => this.computeConditionalAggregationFunction(values, args, functionName, 0, (left, right) => this.arithmeticHelper.nonstrictadd(left, right), mapToRawScalarValue); return this.runFunction(ast.args, state, this.metadata(functionName), computeFn); } averageif(ast, state) { const functionName = 'AVERAGEIF'; const computeFn = (conditionRange, criterion, values) => { const averageResult = this.computeConditionalAggregationFunction(values !== null && values !== void 0 ? values : conditionRange, [conditionRange, criterion], functionName, AverageResult.empty, (left, right) => left.compose(right), arg => isExtendedNumber(arg) ? AverageResult.single(getRawValue(arg)) : AverageResult.empty); if (averageResult instanceof CellError) { return averageResult; } else { return averageResult.averageValue() || new CellError(ErrorType.DIV_BY_ZERO); } }; return this.runFunction(ast.args, state, this.metadata(functionName), computeFn); } /** * Corresponds to COUNTIF(Range, Criterion) * * Range is the range to which criterion is to be applied. * Criterion is the criteria used to choose which cells will be included in sum. * * Returns number of cells on which criteria evaluate to true. * * @param ast * @param state */ countif(ast, state) { const functionName = 'COUNTIF'; const computeFn = (conditionRange, criterion) => this.computeConditionalAggregationFunction(conditionRange, [conditionRange, criterion], functionName, 0, (left, right) => left + right, () => 1); return this.runFunction(ast.args, state, this.metadata(functionName), computeFn); } countifs(ast, state) { const functionName = 'COUNTIFS'; const computeFn = (...args) => this.computeConditionalAggregationFunction(args[0], args, functionName, 0, (left, right) => left + right, () => 1); return this.runFunction(ast.args, state, this.metadata(functionName), computeFn); } minifs(ast, state) { const functionName = 'MINIFS'; const composeFunction = (left, right) => { if (right === undefined || left === undefined) { return right === undefined ? left : right; } return Math.min(left, right); }; const computeFn = (values, ...args) => { const minResult = this.computeConditionalAggregationFunction(values, args, functionName, Number.POSITIVE_INFINITY, composeFunction, mapToRawScalarValue); return zeroForInfinite(minResult); }; return this.runFunction(ast.args, state, this.metadata(functionName), computeFn); } maxifs(ast, state) { const functionName = 'MAXIFS'; const composeFunction = (left, right) => { if (right === undefined || left === undefined) { return right === undefined ? left : right; } return Math.max(left, right); }; const computeFn = (values, ...args) => { const maxResult = this.computeConditionalAggregationFunction(values, args, functionName, Number.NEGATIVE_INFINITY, composeFunction, mapToRawScalarValue); return zeroForInfinite(maxResult); }; return this.runFunction(ast.args, state, this.metadata(functionName), computeFn); } computeConditionalAggregationFunction(valuesRange, conditionArgs, functionName, reduceInitialValue, composeFunction, mapFunction) { const conditions = []; for (let i = 0; i < conditionArgs.length; i += 2) { const conditionArg = conditionArgs[i]; const criterionPackage = this.interpreter.criterionBuilder.fromCellValue(conditionArgs[i + 1], this.arithmeticHelper); if (criterionPackage === undefined) { return new CellError(ErrorType.VALUE, ErrorMessage.BadCriterion); } conditions.push(new Condition(conditionArg, criterionPackage)); } return new CriterionFunctionCompute(this.interpreter, conditionalAggregationFunctionCacheKey(functionName), reduceInitialValue, composeFunction, mapFunction).compute(valuesRange, conditions); } } ConditionalAggregationPlugin.implementedFunctions = { SUMIF: { method: 'sumif', parameters: [{ argumentType: FunctionArgumentType.RANGE }, { argumentType: FunctionArgumentType.NOERROR }, { argumentType: FunctionArgumentType.RANGE, optionalArg: true }] }, COUNTIF: { method: 'countif', parameters: [{ argumentType: FunctionArgumentType.RANGE }, { argumentType: FunctionArgumentType.NOERROR }] }, AVERAGEIF: { method: 'averageif', parameters: [{ argumentType: FunctionArgumentType.RANGE }, { argumentType: FunctionArgumentType.NOERROR }, { argumentType: FunctionArgumentType.RANGE, optionalArg: true }] }, SUMIFS: { method: 'sumifs', parameters: [{ argumentType: FunctionArgumentType.RANGE }, { argumentType: FunctionArgumentType.RANGE }, { argumentType: FunctionArgumentType.NOERROR }], repeatLastArgs: 2 }, COUNTIFS: { method: 'countifs', parameters: [{ argumentType: FunctionArgumentType.RANGE }, { argumentType: FunctionArgumentType.NOERROR }], repeatLastArgs: 2 }, MINIFS: { method: 'minifs', parameters: [{ argumentType: FunctionArgumentType.RANGE }, { argumentType: FunctionArgumentType.RANGE }, { argumentType: FunctionArgumentType.NOERROR }], repeatLastArgs: 2 }, MAXIFS: { method: 'maxifs', parameters: [{ argumentType: FunctionArgumentType.RANGE }, { argumentType: FunctionArgumentType.RANGE }, { argumentType: FunctionArgumentType.NOERROR }], repeatLastArgs: 2 } };