diff --git a/CHANGELOG.md b/CHANGELOG.md index 25ecbeb4ec..60362a5858 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Added support for array arithmetic. (#628) - Accepting time in JS Date() objects on the input. (#648) ## [0.6.0] - 2021-04-27 diff --git a/src/Config.ts b/src/Config.ts index e6f00e46ab..d5723fc851 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -28,6 +28,7 @@ type GPUMode = 'gpu' | 'cpu' | 'dev' const PossibleGPUModeString: GPUMode[] = ['gpu', 'cpu', 'dev'] export interface ConfigParams { + arrays: boolean, //FIXME /** * Specifies if the string comparison is accent sensitive or not. * Applies to comparison operators only. @@ -380,6 +381,7 @@ type ConfigParamsList = keyof ConfigParams export class Config implements ConfigParams, ParserConfig { public static defaultConfig: ConfigParams = { + arrays: false, accentSensitive: false, caseSensitive: false, caseFirst: 'lower', @@ -420,6 +422,7 @@ export class Config implements ConfigParams, ParserConfig { currencySymbol: ['$'], } + public readonly arrays: boolean /** @inheritDoc */ public readonly caseSensitive: boolean /** @inheritDoc */ @@ -524,6 +527,7 @@ export class Config implements ConfigParams, ParserConfig { constructor( { + arrays, accentSensitive, caseSensitive, caseFirst, @@ -564,6 +568,7 @@ export class Config implements ConfigParams, ParserConfig { currencySymbol, }: Partial = {}, ) { + this.arrays = this.valueFromParam(arrays, 'boolean', 'arrays') this.accentSensitive = this.valueFromParam(accentSensitive, 'boolean', 'accentSensitive') this.caseSensitive = this.valueFromParam(caseSensitive, 'boolean', 'caseSensitive') this.caseFirst = this.valueFromParam(caseFirst, ['upper', 'lower', 'false'], 'caseFirst') diff --git a/src/Lookup/AdvancedFind.ts b/src/Lookup/AdvancedFind.ts index 090c49136e..dd08a5dfef 100644 --- a/src/Lookup/AdvancedFind.ts +++ b/src/Lookup/AdvancedFind.ts @@ -5,7 +5,8 @@ import {AbsoluteCellRange} from '../AbsoluteCellRange' import {DependencyGraph} from '../DependencyGraph' -import {getRawValue, RawInterpreterValue} from '../interpreter/InterpreterValue' +import {getRawValue, InternalScalarValue, RawInterpreterValue} from '../interpreter/InterpreterValue' +import {SimpleRangeValue} from '../interpreter/SimpleRangeValue' export abstract class AdvancedFind { protected constructor( @@ -13,11 +14,17 @@ export abstract class AdvancedFind { ) { } - public advancedFind(keyMatcher: (arg: RawInterpreterValue) => boolean, range: AbsoluteCellRange): number { - const values = this.dependencyGraph.computeListOfValuesInRange(range) + public advancedFind(keyMatcher: (arg: RawInterpreterValue) => boolean, rangeValue: SimpleRangeValue): number { + let values: InternalScalarValue[] + const range = rangeValue.range() + if(range === undefined) { + values = rangeValue.valuesFromTopLeftCorner() + } else { + values = this.dependencyGraph.computeListOfValuesInRange(range) + } for (let i = 0; i < values.length; i++) { if (keyMatcher(getRawValue(values[i]))) { - return i + range.start.col + return i } } return -1 diff --git a/src/Lookup/ColumnBinarySearch.ts b/src/Lookup/ColumnBinarySearch.ts index fb148b4c84..182a86c63b 100644 --- a/src/Lookup/ColumnBinarySearch.ts +++ b/src/Lookup/ColumnBinarySearch.ts @@ -10,6 +10,7 @@ import {DependencyGraph} from '../DependencyGraph' import {forceNormalizeString} from '../interpreter/ArithmeticHelper' import {rangeLowerBound} from '../interpreter/binarySearch' import {getRawValue, RawNoErrorScalarValue, RawScalarValue} from '../interpreter/InterpreterValue' +import {SimpleRangeValue} from '../interpreter/SimpleRangeValue' import {Matrix} from '../Matrix' import {ColumnsSpan} from '../Span' import {AdvancedFind} from './AdvancedFind' @@ -42,16 +43,17 @@ export class ColumnBinarySearch extends AdvancedFind implements ColumnSearchStra public destroy(): void {} - public find(key: RawNoErrorScalarValue, range: AbsoluteCellRange, sorted: boolean): number { + public find(key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, sorted: boolean): number { if(typeof key === 'string') { key = forceNormalizeString(key) } - if (range.height() < this.config.binarySearchThreshold || !sorted) { - const values = this.dependencyGraph.computeListOfValuesInRange(range).map(getRawValue).map(arg => + const range = rangeValue.range() + if(range === undefined) { + return rangeValue.valuesFromTopLeftCorner().map(getRawValue).map(arg => (typeof arg === 'string') ? forceNormalizeString(arg) : arg - ) - const index = values.indexOf(key) - return index < 0 ? index : index + range.start.row + ).indexOf(key) + } else if (range.height() < this.config.binarySearchThreshold || !sorted) { + return this.dependencyGraph.computeListOfValuesInRange(range).map(getRawValue).indexOf(key) } else { return rangeLowerBound(range, key, this.dependencyGraph, 'row') } diff --git a/src/Lookup/ColumnIndex.ts b/src/Lookup/ColumnIndex.ts index b34183c8cb..4fe563a2ed 100644 --- a/src/Lookup/ColumnIndex.ts +++ b/src/Lookup/ColumnIndex.ts @@ -90,7 +90,11 @@ export class ColumnIndex implements ColumnSearchStrategy { } } - public find(key: RawNoErrorScalarValue, range: AbsoluteCellRange, sorted: boolean): number { + public find(key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, sorted: boolean): number { + const range = rangeValue.range() + if(range === undefined) { + return this.binarySearchStrategy.find(key, rangeValue, sorted) + } this.ensureRecentData(range.sheet, range.start.col, key) const columnMap = this.getColumnMap(range.sheet, range.start.col) @@ -104,15 +108,15 @@ export class ColumnIndex implements ColumnSearchStrategy { const valueIndex = columnMap.get(key) if (!valueIndex) { - return this.binarySearchStrategy.find(key, range, sorted) + return this.binarySearchStrategy.find(key, rangeValue, sorted) } const index = upperBound(valueIndex.index, range.start.row) const rowNumber = valueIndex.index[index] - return rowNumber <= range.end.row ? rowNumber : this.binarySearchStrategy.find(key, range, sorted) + return rowNumber <= range.end.row ? rowNumber - range.start.row : this.binarySearchStrategy.find(key, rangeValue, sorted) } - public advancedFind(keyMatcher: (arg: RawInterpreterValue) => boolean, range: AbsoluteCellRange): number { + public advancedFind(keyMatcher: (arg: RawInterpreterValue) => boolean, range: SimpleRangeValue): number { return this.binarySearchStrategy.advancedFind(keyMatcher, range) } diff --git a/src/Lookup/RowSearchStrategy.ts b/src/Lookup/RowSearchStrategy.ts index dacfdd75c1..6b3a6071b9 100644 --- a/src/Lookup/RowSearchStrategy.ts +++ b/src/Lookup/RowSearchStrategy.ts @@ -9,6 +9,7 @@ import {DependencyGraph} from '../DependencyGraph' import {forceNormalizeString} from '../interpreter/ArithmeticHelper' import {rangeLowerBound} from '../interpreter/binarySearch' import {getRawValue, RawNoErrorScalarValue} from '../interpreter/InterpreterValue' +import {SimpleRangeValue} from '../interpreter/SimpleRangeValue' import {AdvancedFind} from './AdvancedFind' import {SearchStrategy} from './SearchStrategy' @@ -20,16 +21,17 @@ export class RowSearchStrategy extends AdvancedFind implements SearchStrategy { super(dependencyGraph) } - public find(key: RawNoErrorScalarValue, range: AbsoluteCellRange, sorted: boolean): number { + public find(key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, sorted: boolean): number { if(typeof key === 'string') { key = forceNormalizeString(key) } - if (range.width() < this.config.binarySearchThreshold || !sorted) { - const values = this.dependencyGraph.computeListOfValuesInRange(range).map(getRawValue).map(arg => + const range = rangeValue.range() + if(range === undefined) { + return rangeValue.valuesFromTopLeftCorner().map(getRawValue).indexOf(key) + } else if (range.width() < this.config.binarySearchThreshold || !sorted) { + return this.dependencyGraph.computeListOfValuesInRange(range).map(getRawValue).map(arg => (typeof arg === 'string') ? forceNormalizeString(arg) : arg - ) - const index = values.indexOf(key) - return index < 0 ? index : index + range.start.col + ).indexOf(key) } else { return rangeLowerBound(range, key, this.dependencyGraph, 'col') } diff --git a/src/Lookup/SearchStrategy.ts b/src/Lookup/SearchStrategy.ts index 5a1fd207f3..0dab0a869b 100644 --- a/src/Lookup/SearchStrategy.ts +++ b/src/Lookup/SearchStrategy.ts @@ -8,6 +8,7 @@ import {SimpleCellAddress} from '../Cell' import {Config} from '../Config' import {DependencyGraph} from '../DependencyGraph' import {RawInterpreterValue, RawNoErrorScalarValue, RawScalarValue} from '../interpreter/InterpreterValue' +import {SimpleRangeValue} from '../interpreter/SimpleRangeValue' import {Matrix} from '../Matrix' import {ColumnsSpan} from '../Span' import {Statistics} from '../statistics/Statistics' @@ -15,9 +16,9 @@ import {ColumnBinarySearch} from './ColumnBinarySearch' import {ColumnIndex} from './ColumnIndex' export interface SearchStrategy { - find(key: RawNoErrorScalarValue, range: AbsoluteCellRange, sorted: boolean): number, + find(key: RawNoErrorScalarValue, range: SimpleRangeValue, sorted: boolean): number, - advancedFind(keyMatcher: (arg: RawInterpreterValue) => boolean, range: AbsoluteCellRange): number, + advancedFind(keyMatcher: (arg: RawInterpreterValue) => boolean, range: SimpleRangeValue): number, } export interface ColumnSearchStrategy extends SearchStrategy { diff --git a/src/Matrix.ts b/src/Matrix.ts index 2c933a8e64..5b12bcbfde 100644 --- a/src/Matrix.ts +++ b/src/Matrix.ts @@ -9,6 +9,9 @@ import {ErrorMessage} from './error-message' import {Ast, AstNodeType} from './parser' export class MatrixSize { + public static fromMatrix(matrix: T[][]): MatrixSize { + return new MatrixSize(matrix.length > 0 ? matrix[0].length : 0, matrix.length) + } constructor( public width: number, public height: number, diff --git a/src/interpreter/ArithmeticHelper.ts b/src/interpreter/ArithmeticHelper.ts index 0111802399..a2fce01929 100644 --- a/src/interpreter/ArithmeticHelper.ts +++ b/src/interpreter/ArithmeticHelper.ts @@ -212,9 +212,8 @@ export class ArithmeticHelper { return cloneNumber(arg, -getRawValue(arg)) } - public unaryPlus = (arg: ExtendedNumber): ExtendedNumber => { - return arg - } + public unaryPlus = (arg: InternalScalarValue): InternalScalarValue => arg + public unaryPercent = (arg: ExtendedNumber): ExtendedNumber => { return new PercentNumber(getRawValue(arg)/100) diff --git a/src/interpreter/Interpreter.ts b/src/interpreter/Interpreter.ts index 0a453ae89b..574b49d468 100644 --- a/src/interpreter/Interpreter.ts +++ b/src/interpreter/Interpreter.ts @@ -3,6 +3,7 @@ * Copyright (c) 2021 Handsoncode. All rights reserved. */ +import {GPU} from 'gpu.js' import {AbsoluteCellRange, AbsoluteColumnRange, AbsoluteRowRange} from '../AbsoluteCellRange' import {CellError, ErrorType, invalidSimpleCellAddress, SimpleCellAddress} from '../Cell' import {Config} from '../Config' @@ -12,7 +13,6 @@ import {ErrorMessage} from '../error-message' import {LicenseKeyValidityState} from '../helpers/licenseKeyValidator' import {ColumnSearchStrategy} from '../Lookup/SearchStrategy' import {Matrix, NotComputedMatrix} from '../Matrix' -import {Maybe} from '../Maybe' import {NamedExpressions} from '../NamedExpressions' import {NumberLiteralHelper} from '../NumberLiteralHelper' // noinspection TypeScriptPreferShortImport @@ -26,11 +26,10 @@ import { cloneNumber, EmptyValue, getRawValue, - InternalNoErrorScalarValue, + InternalScalarValue, + InterpreterValue, isExtendedNumber, } from './InterpreterValue' -import {InterpreterValue} from './InterpreterValue' -import type {GPU} from 'gpu.js' import {SimpleRangeValue} from './SimpleRangeValue' export class Interpreter { @@ -91,123 +90,74 @@ export class Interpreter { case AstNodeType.CONCATENATE_OP: { const leftResult = this.evaluateAst(ast.left, formulaAddress) const rightResult = this.evaluateAst(ast.right, formulaAddress) - return passErrors(leftResult, rightResult) ?? - wrapperBinary(this.arithmeticHelper.concat, - coerceScalarToString(leftResult as InternalNoErrorScalarValue), - coerceScalarToString(rightResult as InternalNoErrorScalarValue) - ) + return this.binaryRangeWrapper(this.concatOp, leftResult, rightResult) } case AstNodeType.EQUALS_OP: { const leftResult = this.evaluateAst(ast.left, formulaAddress) const rightResult = this.evaluateAst(ast.right, formulaAddress) - return passErrors(leftResult, rightResult) ?? - this.arithmeticHelper.eq(leftResult as InternalNoErrorScalarValue, rightResult as InternalNoErrorScalarValue) + return this.binaryRangeWrapper(this.equalOp, leftResult, rightResult) } case AstNodeType.NOT_EQUAL_OP: { const leftResult = this.evaluateAst(ast.left, formulaAddress) const rightResult = this.evaluateAst(ast.right, formulaAddress) - return passErrors(leftResult, rightResult) ?? - this.arithmeticHelper.neq(leftResult as InternalNoErrorScalarValue, rightResult as InternalNoErrorScalarValue) + return this.binaryRangeWrapper(this.notEqualOp, leftResult, rightResult) } case AstNodeType.GREATER_THAN_OP: { const leftResult = this.evaluateAst(ast.left, formulaAddress) const rightResult = this.evaluateAst(ast.right, formulaAddress) - return passErrors(leftResult, rightResult) ?? - this.arithmeticHelper.gt(leftResult as InternalNoErrorScalarValue, rightResult as InternalNoErrorScalarValue) + return this.binaryRangeWrapper(this.greaterThanOp, leftResult, rightResult) } case AstNodeType.LESS_THAN_OP: { const leftResult = this.evaluateAst(ast.left, formulaAddress) const rightResult = this.evaluateAst(ast.right, formulaAddress) - return passErrors(leftResult, rightResult) ?? - this.arithmeticHelper.lt(leftResult as InternalNoErrorScalarValue, rightResult as InternalNoErrorScalarValue) + return this.binaryRangeWrapper(this.lessThanOp, leftResult, rightResult) } case AstNodeType.GREATER_THAN_OR_EQUAL_OP: { const leftResult = this.evaluateAst(ast.left, formulaAddress) const rightResult = this.evaluateAst(ast.right, formulaAddress) - return passErrors(leftResult, rightResult) ?? - this.arithmeticHelper.geq(leftResult as InternalNoErrorScalarValue, rightResult as InternalNoErrorScalarValue) + return this.binaryRangeWrapper(this.greaterThanOrEqualOp, leftResult, rightResult) } case AstNodeType.LESS_THAN_OR_EQUAL_OP: { const leftResult = this.evaluateAst(ast.left, formulaAddress) const rightResult = this.evaluateAst(ast.right, formulaAddress) - return passErrors(leftResult, rightResult) ?? - this.arithmeticHelper.leq(leftResult as InternalNoErrorScalarValue, rightResult as InternalNoErrorScalarValue) + return this.binaryRangeWrapper(this.lessThanOrEqualOp, leftResult, rightResult) } case AstNodeType.PLUS_OP: { const leftResult = this.evaluateAst(ast.left, formulaAddress) const rightResult = this.evaluateAst(ast.right, formulaAddress) - return passErrors(leftResult, rightResult) ?? - wrapperBinary(this.arithmeticHelper.addWithEpsilon, - this.arithmeticHelper.coerceScalarToNumberOrError(leftResult as InternalNoErrorScalarValue), - this.arithmeticHelper.coerceScalarToNumberOrError(rightResult as InternalNoErrorScalarValue) - ) + return this.binaryRangeWrapper(this.plusOp, leftResult, rightResult) } case AstNodeType.MINUS_OP: { const leftResult = this.evaluateAst(ast.left, formulaAddress) const rightResult = this.evaluateAst(ast.right, formulaAddress) - return passErrors(leftResult, rightResult) ?? - wrapperBinary(this.arithmeticHelper.subtract, - this.arithmeticHelper.coerceScalarToNumberOrError(leftResult as InternalNoErrorScalarValue), - this.arithmeticHelper.coerceScalarToNumberOrError(rightResult as InternalNoErrorScalarValue) - ) + return this.binaryRangeWrapper(this.minusOp, leftResult, rightResult) } case AstNodeType.TIMES_OP: { const leftResult = this.evaluateAst(ast.left, formulaAddress) const rightResult = this.evaluateAst(ast.right, formulaAddress) - return passErrors(leftResult, rightResult) ?? - wrapperBinary( - this.arithmeticHelper.multiply, - this.arithmeticHelper.coerceScalarToNumberOrError(leftResult as InternalNoErrorScalarValue), - this.arithmeticHelper.coerceScalarToNumberOrError(rightResult as InternalNoErrorScalarValue) - ) + return this.binaryRangeWrapper(this.timesOp, leftResult, rightResult) } case AstNodeType.POWER_OP: { const leftResult = this.evaluateAst(ast.left, formulaAddress) const rightResult = this.evaluateAst(ast.right, formulaAddress) - return passErrors(leftResult, rightResult) ?? - wrapperBinary( - this.arithmeticHelper.pow, - this.arithmeticHelper.coerceScalarToNumberOrError(leftResult as InternalNoErrorScalarValue), - this.arithmeticHelper.coerceScalarToNumberOrError(rightResult as InternalNoErrorScalarValue) - ) + return this.binaryRangeWrapper(this.powerOp, leftResult, rightResult) } case AstNodeType.DIV_OP: { const leftResult = this.evaluateAst(ast.left, formulaAddress) const rightResult = this.evaluateAst(ast.right, formulaAddress) - return passErrors(leftResult, rightResult) ?? - wrapperBinary( - this.arithmeticHelper.divide, - this.arithmeticHelper.coerceScalarToNumberOrError(leftResult as InternalNoErrorScalarValue), - this.arithmeticHelper.coerceScalarToNumberOrError(rightResult as InternalNoErrorScalarValue) - ) + return this.binaryRangeWrapper(this.divOp, leftResult, rightResult) } case AstNodeType.PLUS_UNARY_OP: { const result = this.evaluateAst(ast.value, formulaAddress) - if (result instanceof SimpleRangeValue) { - return new CellError(ErrorType.VALUE, ErrorMessage.ScalarExpected) - } else if(isExtendedNumber(result)) { - return this.arithmeticHelper.unaryPlus(result) - } else { - return result - } + return this.unaryRangeWrapper(this.unaryPlusOp, result) } case AstNodeType.MINUS_UNARY_OP: { const result = this.evaluateAst(ast.value, formulaAddress) - if (result instanceof SimpleRangeValue) { - return new CellError(ErrorType.VALUE, ErrorMessage.ScalarExpected) - } else { - return wrapperUnary(this.arithmeticHelper.unaryMinus, - this.arithmeticHelper.coerceScalarToNumberOrError(result)) - } + return this.unaryRangeWrapper(this.unaryMinusOp, result) } case AstNodeType.PERCENT_OP: { const result = this.evaluateAst(ast.value, formulaAddress) - if (result instanceof SimpleRangeValue) { - return new CellError(ErrorType.VALUE, ErrorMessage.ScalarExpected) - } else { - return wrapperUnary(this.arithmeticHelper.unaryPercent, - this.arithmeticHelper.coerceScalarToNumberOrError(result)) - } + return this.unaryRangeWrapper(this.percentOp, result) } case AstNodeType.FUNCTION_CALL: { if (this.config.licenseKeyValidityState !== LicenseKeyValidityState.VALID && !FunctionRegistry.functionIsProtected(ast.procedureName)) { @@ -298,37 +248,147 @@ export class Interpreter { private rangeSpansOneSheet(ast: CellRangeAst | ColumnRangeAst | RowRangeAst): boolean { return ast.start.sheet === ast.end.sheet } -} -function passErrors(left: InterpreterValue, right: InterpreterValue): Maybe { - if (left instanceof CellError) { - return left - } else if (left instanceof SimpleRangeValue) { - return new CellError(ErrorType.VALUE, ErrorMessage.ScalarExpected) - } else if (right instanceof CellError) { - return right - } else if (right instanceof SimpleRangeValue) { - return new CellError(ErrorType.VALUE, ErrorMessage.ScalarExpected) - } else { - return undefined + private equalOp = (arg1: InternalScalarValue, arg2: InternalScalarValue): InternalScalarValue => + binaryErrorWrapper(this.arithmeticHelper.eq, arg1, arg2) + + private notEqualOp = (arg1: InternalScalarValue, arg2: InternalScalarValue): InternalScalarValue => + binaryErrorWrapper(this.arithmeticHelper.neq, arg1, arg2) + + private greaterThanOp = (arg1: InternalScalarValue, arg2: InternalScalarValue): InternalScalarValue => + binaryErrorWrapper(this.arithmeticHelper.gt, arg1, arg2) + + private lessThanOp = (arg1: InternalScalarValue, arg2: InternalScalarValue): InternalScalarValue => + binaryErrorWrapper(this.arithmeticHelper.lt, arg1, arg2) + + private greaterThanOrEqualOp = (arg1: InternalScalarValue, arg2: InternalScalarValue): InternalScalarValue => + binaryErrorWrapper(this.arithmeticHelper.geq, arg1, arg2) + + private lessThanOrEqualOp = (arg1: InternalScalarValue, arg2: InternalScalarValue): InternalScalarValue => + binaryErrorWrapper(this.arithmeticHelper.leq, arg1, arg2) + + private concatOp = (arg1: InternalScalarValue, arg2: InternalScalarValue): InternalScalarValue => + binaryErrorWrapper(this.arithmeticHelper.concat, + coerceScalarToString(arg1), + coerceScalarToString(arg2) + ) + + private plusOp = (arg1: InternalScalarValue, arg2: InternalScalarValue): InternalScalarValue => + binaryErrorWrapper(this.arithmeticHelper.addWithEpsilon, + this.arithmeticHelper.coerceScalarToNumberOrError(arg1), + this.arithmeticHelper.coerceScalarToNumberOrError(arg2) + ) + + private minusOp = (arg1: InternalScalarValue, arg2: InternalScalarValue): InternalScalarValue => + binaryErrorWrapper(this.arithmeticHelper.subtract, + this.arithmeticHelper.coerceScalarToNumberOrError(arg1), + this.arithmeticHelper.coerceScalarToNumberOrError(arg2) + ) + + private timesOp = (arg1: InternalScalarValue, arg2: InternalScalarValue): InternalScalarValue => + binaryErrorWrapper( + this.arithmeticHelper.multiply, + this.arithmeticHelper.coerceScalarToNumberOrError(arg1), + this.arithmeticHelper.coerceScalarToNumberOrError(arg2) + ) + + private powerOp = (arg1: InternalScalarValue, arg2: InternalScalarValue): InternalScalarValue => + binaryErrorWrapper( + this.arithmeticHelper.pow, + this.arithmeticHelper.coerceScalarToNumberOrError(arg1), + this.arithmeticHelper.coerceScalarToNumberOrError(arg2) + ) + + private divOp = (arg1: InternalScalarValue, arg2: InternalScalarValue): InternalScalarValue => + binaryErrorWrapper( + this.arithmeticHelper.divide, + this.arithmeticHelper.coerceScalarToNumberOrError(arg1), + this.arithmeticHelper.coerceScalarToNumberOrError(arg2) + ) + + private unaryMinusOp = (arg: InternalScalarValue): InternalScalarValue => + unaryErrorWrapper(this.arithmeticHelper.unaryMinus, + this.arithmeticHelper.coerceScalarToNumberOrError(arg)) + + private percentOp = (arg: InternalScalarValue): InternalScalarValue => + unaryErrorWrapper(this.arithmeticHelper.unaryPercent, + this.arithmeticHelper.coerceScalarToNumberOrError(arg)) + + private unaryPlusOp = (arg: InternalScalarValue): InternalScalarValue => this.arithmeticHelper.unaryPlus(arg) + + private unaryRangeWrapper(op: (arg: InternalScalarValue) => InternalScalarValue, arg: InterpreterValue): InterpreterValue { + if (arg instanceof CellError) { + return arg + } else if(arg instanceof SimpleRangeValue) { + if(!this.config.arrays) { + return new CellError(ErrorType.VALUE, ErrorMessage.ScalarExpected) + } + const newRaw = arg.raw().map( + (row) => row.map(op) + ) + return SimpleRangeValue.onlyValues(newRaw) + } else { + return op(arg) + } + } + + private binaryRangeWrapper(op: (arg1: InternalScalarValue, arg2: InternalScalarValue) => InternalScalarValue, arg1: InterpreterValue, arg2: InterpreterValue): InterpreterValue { + if (arg1 instanceof CellError) { + return arg1 + } else if(arg1 instanceof SimpleRangeValue && !this.config.arrays) { + return new CellError(ErrorType.VALUE, ErrorMessage.ScalarExpected) + } else if (arg2 instanceof CellError) { + return arg2 + } else if(arg2 instanceof SimpleRangeValue && !this.config.arrays) { + return new CellError(ErrorType.VALUE, ErrorMessage.ScalarExpected) + } else if(arg1 instanceof SimpleRangeValue || arg2 instanceof SimpleRangeValue) { + if(!(arg1 instanceof SimpleRangeValue)) { + arg1 = SimpleRangeValue.fromScalar(arg1) + } + if(!(arg2 instanceof SimpleRangeValue)) { + arg2 = SimpleRangeValue.fromScalar(arg2) + } + const width = Math.max(arg1.width(), arg2.width()) + const height = Math.max(arg1.height(), arg2.height()) + const ret: InternalScalarValue[][] = Array(height) + for(let i=0;i(op: (a: T) => InterpreterValue, a: T | CellError): InterpreterValue { - if (a instanceof CellError) { - return a +function unaryErrorWrapper(op: (arg: T) => InternalScalarValue, arg: T | CellError): InternalScalarValue { + if (arg instanceof CellError) { + return arg } else { - return op(a) + return op(arg) } } -function wrapperBinary(op: (a: T, b: T) => InterpreterValue, a: T | CellError, b: T | CellError): InterpreterValue { - if (a instanceof CellError) { - return a - } else if (b instanceof CellError) { - return b +function binaryErrorWrapper(op: (arg1: T, arg2: T) => InternalScalarValue, arg1: T | CellError, arg2: T | CellError): InternalScalarValue { + if (arg1 instanceof CellError) { + return arg1 + } else if (arg2 instanceof CellError) { + return arg2 } else { - return op(a, b) + return op(arg1, arg2) } } @@ -338,3 +398,4 @@ function wrapperForAddress(val: InterpreterValue, adr: SimpleCellAddress): Inter } return val } + diff --git a/src/interpreter/SimpleRangeValue.ts b/src/interpreter/SimpleRangeValue.ts index eabea21449..447ffd8a18 100644 --- a/src/interpreter/SimpleRangeValue.ts +++ b/src/interpreter/SimpleRangeValue.ts @@ -191,6 +191,10 @@ export class SimpleRangeValue { return new SimpleRangeValue(new ArrayData(size, data, true)) } + public static onlyValues(data: InternalScalarValue[][]): SimpleRangeValue { + return new SimpleRangeValue(new ArrayData(MatrixSize.fromMatrix(data), data, false)) //FIXME test for _hasOnlyNumbers + } + public static onlyRange(range: AbsoluteCellRange, dependencyGraph: DependencyGraph): SimpleRangeValue { return new SimpleRangeValue(new OnlyRangeData({ width: range.width(), diff --git a/src/interpreter/binarySearch.ts b/src/interpreter/binarySearch.ts index 243bb189b0..3a615478e4 100644 --- a/src/interpreter/binarySearch.ts +++ b/src/interpreter/binarySearch.ts @@ -30,7 +30,7 @@ export function rangeLowerBound(range: AbsoluteCellRange, key: RawNoErrorScalarV centerValueFn = (center: number) => getRawValue(dependencyGraph.getCellValue(simpleCellAddress(range.sheet, center, range.start.row))) } - return lowerBound(centerValueFn, key, start, end) + return lowerBound(centerValueFn, key, start, end) - start } /* diff --git a/src/interpreter/plugin/InformationPlugin.ts b/src/interpreter/plugin/InformationPlugin.ts index 96632bafc5..f95a360807 100644 --- a/src/interpreter/plugin/InformationPlugin.ts +++ b/src/interpreter/plugin/InformationPlugin.ts @@ -386,7 +386,7 @@ export class InformationPlugin extends FunctionPlugin { } const range = rangeValue.range() if (range === undefined) { - return rangeValue.topLeftCornerValue() ?? new CellError(ErrorType.VALUE, ErrorMessage.CellRangeExpected) + return rangeValue?.raw()?.[row-1]?.[col-1] ?? rangeValue.topLeftCornerValue() ?? new CellError(ErrorType.VALUE, ErrorMessage.CellRangeExpected) } const address = range.getAddress(col - 1, row - 1) return this.dependencyGraph.getScalarValue(address) diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index 8253834bbd..99a9198d82 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -67,7 +67,7 @@ export class LookupPlugin extends FunctionPlugin { return new CellError(ErrorType.REF, ErrorMessage.IndexLarge) } - return this.doVlookup(zeroIfEmpty(key), range, index - 1, sorted) + return this.doVlookup(zeroIfEmpty(key), rangeValue, index - 1, sorted) }) } @@ -90,25 +90,25 @@ export class LookupPlugin extends FunctionPlugin { return new CellError(ErrorType.REF, ErrorMessage.IndexLarge) } - return this.doHlookup(zeroIfEmpty(key), range, index - 1, sorted) + return this.doHlookup(zeroIfEmpty(key), rangeValue, index - 1, sorted) }) } public match(ast: ProcedureAst, formulaAddress: SimpleCellAddress): InternalScalarValue { return this.runFunction(ast.args, formulaAddress, this.metadata('MATCH'), (key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, sorted: number) => { - const range = rangeValue.range() - if (range === undefined) { - return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) - } - - return this.doMatch(zeroIfEmpty(key), range, sorted) + return this.doMatch(zeroIfEmpty(key), rangeValue, sorted) }) } - private doVlookup(key: RawNoErrorScalarValue, range: AbsoluteCellRange, index: number, sorted: boolean): InternalScalarValue { + private doVlookup(key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, index: number, sorted: boolean): InternalScalarValue { this.dependencyGraph.stats.start(StatType.VLOOKUP) - - const searchedRange = AbsoluteCellRange.spanFrom(range.start, 1, range.height()) + const range = rangeValue.range() + let searchedRange + if(range === undefined) { + searchedRange = SimpleRangeValue.onlyValues(rangeValue.raw().map((arg) => [arg[0]])) + } else { + searchedRange = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(range.start, 1, range.height()), this.dependencyGraph) + } const rowIndex = this.searchInRange(key, searchedRange, sorted, this.columnSearch) this.dependencyGraph.stats.end(StatType.VLOOKUP) @@ -117,8 +117,13 @@ export class LookupPlugin extends FunctionPlugin { return new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) } - const address = simpleCellAddress(range.sheet, range.start.col + index, rowIndex) - const value = this.dependencyGraph.getCellValue(address) + let value + if(range === undefined) { + value = rangeValue.raw()[rowIndex][index] + } else { + const address = simpleCellAddress(range.sheet, range.start.col + index, range.start.row + rowIndex) + value = this.dependencyGraph.getCellValue(address) + } if (value instanceof SimpleRangeValue) { return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) @@ -126,17 +131,28 @@ export class LookupPlugin extends FunctionPlugin { return value } - private doHlookup(key: RawNoErrorScalarValue, range: AbsoluteCellRange, index: number, sorted: boolean): InternalScalarValue { - const searchedRange = AbsoluteCellRange.spanFrom(range.start, range.width(), 1) + private doHlookup(key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, index: number, sorted: boolean): InternalScalarValue { + const range = rangeValue.range() + let searchedRange + if(range === undefined) { + searchedRange = SimpleRangeValue.onlyValues([rangeValue.raw()[0]]) + } else { + searchedRange = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(range.start, range.width(), 1), this.dependencyGraph) + } const colIndex = this.searchInRange(key, searchedRange, sorted, this.rowSearch) if (colIndex === -1) { return new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) } - const address = simpleCellAddress(range.sheet, colIndex, range.start.row + index) - const value = this.dependencyGraph.getCellValue(address) + let value + if(range === undefined) { + value = rangeValue.raw()[index][colIndex] + } else { + const address = simpleCellAddress(range.sheet, range.start.col + colIndex, range.start.row + index) + value = this.dependencyGraph.getCellValue(address) + } if (value instanceof SimpleRangeValue) { return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) @@ -144,26 +160,26 @@ export class LookupPlugin extends FunctionPlugin { return value } - private doMatch(key: RawNoErrorScalarValue, range: AbsoluteCellRange, sorted: number): InternalScalarValue { - if (range.width() > 1 && range.height() > 1) { + private doMatch(key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, sorted: number): InternalScalarValue { + if (rangeValue.width() > 1 && rangeValue.height() > 1) { return new CellError(ErrorType.NA) } - if (range.width() === 1) { - const index = this.columnSearch.find(key, range, sorted !== 0) + if (rangeValue.width() === 1) { + const index = this.columnSearch.find(key, rangeValue, sorted !== 0) if (index === -1) { return new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) } - return index - range.start.row + 1 + return index + 1 } else { - const index = this.rowSearch.find(key, range, sorted !== 0) + const index = this.rowSearch.find(key, rangeValue, sorted !== 0) if (index === -1) { return new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) } - return index - range.start.col + 1 + return index + 1 } } - protected searchInRange(key: RawNoErrorScalarValue, range: AbsoluteCellRange, sorted: boolean, searchStrategy: SearchStrategy): number { + protected searchInRange(key: RawNoErrorScalarValue, range: SimpleRangeValue, sorted: boolean, searchStrategy: SearchStrategy): number { if(!sorted && typeof key === 'string' && this.interpreter.arithmeticHelper.requiresRegex(key)) { return searchStrategy.advancedFind( this.interpreter.arithmeticHelper.eqMatcherFunction(key), diff --git a/test/arrays-integration.spec.ts b/test/arrays-integration.spec.ts new file mode 100644 index 0000000000..95ec6eb894 --- /dev/null +++ b/test/arrays-integration.spec.ts @@ -0,0 +1,29 @@ +import {HyperFormula} from '../src' +import {adr} from './testUtils' + +describe('integration test', () => { + it('should work', () => { + const engine = HyperFormula.buildFromSheets({ + 'Output':[['=INDEX(LookupRange,MATCH(1,(Lookup!A1:A8<=Inputs!A1)*(Lookup!B1:B8>=Inputs!A1)*(Lookup!C1:C8=Inputs!B1), 0), 4)']], + 'Inputs':[[23, 'B']], + 'Lookup':[ + [11, 15, 'A', 66], + [11, 15, 'B', 77], + [16, 20, 'A', 88], + [16, 20, 'B', 99], + [21, 25, 'A', 110], + [21, 25, 'B', 121], + [26, 30, 'A', 132], + [26, 30, 'B', 143], + ] + }, {arrays: true}) //flag that enables ArrayFormula() everywhere + + engine.addNamedExpression('LookupRange', '=Lookup!$A$1:Lookup!$D$8') + + expect(engine.getCellValue(adr('A1'))).toEqual(121) + + engine.setCellContents(adr('B1', engine.getSheetId('Inputs')), 'A') + + expect(engine.getCellValue(adr('A1'))).toEqual(110) + }) +}) diff --git a/test/arrays.spec.ts b/test/arrays.spec.ts new file mode 100644 index 0000000000..3a378adbc7 --- /dev/null +++ b/test/arrays.spec.ts @@ -0,0 +1,46 @@ +import {ErrorType, HyperFormula} from '../src' +import {adr, detailedErrorWithOrigin} from './testUtils' + +describe('OPs', () => { + it('unary op', () => { + const engine = HyperFormula.buildFromArray([[1, 2, 3], ['=SUM(-A1:C1)']], {arrays: true}) + expect(engine.getCellValue(adr('A2'))).toEqual(-6) + }) + + it('binary op', () => { + const engine = HyperFormula.buildFromArray([[1, 2, 3], [4, 5, 6], ['=SUM(2*A1:C1+A2:C2)']], {arrays: true}) + expect(engine.getCellValue(adr('A3'))).toEqual(27) + }) + + it('index', () => { + const engine = HyperFormula.buildFromArray([[1, 2, 3], ['=INDEX(2*A1:C1+3,1,1)']], {arrays: true}) + expect(engine.getCellValue(adr('A2'))).toEqual(5) + }) + + it('binary op + index', () => { + const engine = HyperFormula.buildFromArray([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ['=INDEX(A1:C2+A1:B3,1,1)', '=INDEX(A1:C2+A1:B3,1,2)', '=INDEX(A1:C2+A1:B3,1,3)'], + ['=INDEX(A1:C2+A1:B3,2,1)', '=INDEX(A1:C2+A1:B3,2,2)', '=INDEX(A1:C2+A1:B3,2,3)'], + ['=INDEX(A1:C2+A1:B3,3,1)', '=INDEX(A1:C2+A1:B3,3,2)', '=INDEX(A1:C2+A1:B3,3,3)'], + ], {arrays: true}) + expect(engine.getSheetValues(0)).toEqual( + [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [2, 4, detailedErrorWithOrigin(ErrorType.NA, 'Sheet1!C4')], + [8, 10, detailedErrorWithOrigin(ErrorType.NA, 'Sheet1!C5')], + [detailedErrorWithOrigin(ErrorType.NA, 'Sheet1!A6'), detailedErrorWithOrigin(ErrorType.NA, 'Sheet1!B6'), detailedErrorWithOrigin(ErrorType.NA, 'Sheet1!C6')]]) + }) + + it('match', () => { + const engine = HyperFormula.buildFromArray([ + ['=MATCH(10,2*A2:E2)'], + [1, 2, 3, 4, 5], + ], {arrays: true}) + expect(engine.getCellValue(adr('A1'))).toEqual(5) + }) +}) diff --git a/test/column-index.spec.ts b/test/column-index.spec.ts index 7e63482c02..c1e29ea156 100644 --- a/test/column-index.spec.ts +++ b/test/column-index.spec.ts @@ -301,7 +301,7 @@ describe('ColumnIndex#find', () => { const index = buildEmptyIndex(transformService, new Config(), stats) index.add(1, adr('A2')) - const row = index.find(1, new AbsoluteCellRange(adr('A1'), adr('A3')), true) + const row = index.find(1, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('A1'), adr('A3')), undefined!), true) expect(row).toBe(1) }) @@ -311,7 +311,7 @@ describe('ColumnIndex#find', () => { index.add(1, adr('A4')) index.add(1, adr('A10')) - const row = index.find(1, new AbsoluteCellRange(adr('A1'), adr('A20')), true) + const row = index.find(1, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('A1'), adr('A20')), undefined!), true) expect(row).toBe(3) }) @@ -510,12 +510,12 @@ describe('ColumnIndex - lazy cruds', () => { transformService.addTransformation(new AddRowsTransformer(RowsSpan.fromNumberOfRows(0, 0, 1))) - const rowA = index.find(1, new AbsoluteCellRange(adr('A1'), adr('A2')), true) + const rowA = index.find(1, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('A1'), adr('A2')), undefined!), true) expect(rowA).toEqual(1) expect(index.getValueIndex(0, 0, 1).index).toEqual([1]) expect(index.getValueIndex(0, 1, 1).index).toEqual([0]) - const rowB = index.find(1, new AbsoluteCellRange(adr('B1'), adr('B2')), true) + const rowB = index.find(1, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('B1'), adr('B2')), undefined!), true) expect(rowB).toEqual(1) expect(index.getValueIndex(0, 0, 1).index).toEqual([1]) expect(index.getValueIndex(0, 1, 1).index).toEqual([1]) @@ -530,12 +530,12 @@ describe('ColumnIndex - lazy cruds', () => { transformService.addTransformation(new AddRowsTransformer(RowsSpan.fromNumberOfRows(0, 0, 1))) - const row1 = index.find(1, new AbsoluteCellRange(adr('A1'), adr('A3')), true) + const row1 = index.find(1, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('A1'), adr('A3')), undefined!), true) expect(row1).toEqual(1) expect(index.getValueIndex(0, 0, 1).index).toEqual([1]) expect(index.getValueIndex(0, 0, 2).index).toEqual([1]) - const row2 = index.find(2, new AbsoluteCellRange(adr('A1'), adr('A3')), true) + const row2 = index.find(2, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('A1'), adr('A3')), undefined!), true) expect(row2).toEqual(2) expect(index.getValueIndex(0, 0, 1).index).toEqual([1]) expect(index.getValueIndex(0, 0, 2).index).toEqual([2]) diff --git a/test/interpreter/function-match.spec.ts b/test/interpreter/function-match.spec.ts index 649ea6bbb3..21065c403e 100644 --- a/test/interpreter/function-match.spec.ts +++ b/test/interpreter/function-match.spec.ts @@ -22,12 +22,12 @@ describe('Function MATCH', () => { expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) }) - it('validates that 2nd argument is range', () => { + it('2nd argument can be a scalar', () => { const engine = HyperFormula.buildFromArray([ - ['=MATCH(1, 42)'], + ['=MATCH(42, 42)'], ]) - expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) + expect(engine.getCellValue(adr('A1'))).toEqual(1) }) it('validates that 3rd argument is number', () => {