diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cf79c677..d5aaff0f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Fixed +- Fixed the `IFS` function evaluating later condition/value pairs after the first true condition. - Fixed a memory leak in `LazilyTransformingAstService` where the transformations array grew unboundedly, causing increasing memory usage over time. [#1629](https://github.com/handsontable/hyperformula/issues/1629) - Fixed a memory leak in `UndoRedo` where `oldData` entries for evicted undo stack entries were never cleaned up, causing increasing memory usage over time. [#1629](https://github.com/handsontable/hyperformula/issues/1629) - Fixed the IRR function returning `#NUM!` error when the initial investment significantly exceeds the sum of returns. [#1628](https://github.com/handsontable/hyperformula/issues/1628) diff --git a/src/interpreter/plugin/BooleanPlugin.ts b/src/interpreter/plugin/BooleanPlugin.ts index a11c269b2..29742a97c 100644 --- a/src/interpreter/plugin/BooleanPlugin.ts +++ b/src/interpreter/plugin/BooleanPlugin.ts @@ -6,6 +6,7 @@ import {CellError, ErrorType} from '../../Cell' import {ErrorMessage} from '../../error-message' import {ProcedureAst} from '../../parser' +import {SimpleRangeValue} from '../../SimpleRangeValue' import {InterpreterState} from '../InterpreterState' import {InternalNoErrorScalarValue, InternalScalarValue, InterpreterValue} from '../InterpreterValue' import {FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions} from './FunctionPlugin' @@ -147,14 +148,58 @@ export class BooleanPlugin extends FunctionPlugin implements FunctionPluginTypec * @param state */ public ifs(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('IFS'), (...args) => { - for (let idx = 0; idx < args.length; idx += 2) { - if (args[idx]) { - return args[idx+1] + const metadata = this.metadata('IFS') + const argumentsMetadata = this.buildMetadataForEachArgumentValue(ast.args.length, metadata) + + if (!this.isNumberOfArgumentValuesValid(argumentsMetadata, ast.args.length)) { + return new CellError(ErrorType.NA, ErrorMessage.WrongArgNumber) + } + + for (let idx = 0; idx < ast.args.length; idx += 2) { + const condition = this.evaluateAst(ast.args[idx], state) + + if (condition instanceof SimpleRangeValue && state.arraysFlag) { + return this.runFunction(ast.args, state, metadata, (...args: InterpreterValue[]) => this.evaluateIfsArguments(args)) + } + + const coercedCondition = this.coerceToType(condition, argumentsMetadata[idx], state) + if (coercedCondition === undefined) { + return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) + } + if (coercedCondition instanceof CellError) { + return coercedCondition + } + if (coercedCondition) { + const value = this.evaluateAst(ast.args[idx + 1], state) + if (value instanceof SimpleRangeValue && state.arraysFlag) { + return this.runFunction(ast.args, state, metadata, (...args: InterpreterValue[]) => this.evaluateIfsArguments(args)) } + + const coercedValue = this.coerceToType(value, argumentsMetadata[idx + 1], state) + if (coercedValue === undefined) { + return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) + } + + return coercedValue as InterpreterValue } - return new CellError(ErrorType.NA, ErrorMessage.NoConditionMet) - }) + } + + return new CellError(ErrorType.NA, ErrorMessage.NoConditionMet) + } + + /** + * Preserves the previous vectorized IFS implementation for array arithmetic. + * + * @param {InterpreterValue[]} args evaluated IFS arguments + */ + private evaluateIfsArguments(args: InterpreterValue[]): InterpreterValue { + for (let idx = 0; idx < args.length; idx += 2) { + if (args[idx]) { + return args[idx + 1] + } + } + + return new CellError(ErrorType.NA, ErrorMessage.NoConditionMet) } /** diff --git a/test/smoke.spec.ts b/test/smoke.spec.ts index 28108ccbd..20ce73dfc 100644 --- a/test/smoke.spec.ts +++ b/test/smoke.spec.ts @@ -56,6 +56,16 @@ describe('HyperFormula', () => { hf.destroy() }) + it('should not evaluate IFS branches after the first true condition', () => { + const hf = HyperFormula.buildFromArray([ + ['=IFS(TRUE(), 1, 1/0, 2)'], + ], {licenseKey: 'gpl-v3'}) + + expect(hf.getCellValue(adr('A1'))).toBe(1) + + hf.destroy() + }) + it('should handle common spreadsheet functions', () => { const data = [ [1, 2, 3, 4, 5],