From 932e8c0fd80c0173f8eeb52774e1761fc6ced0c7 Mon Sep 17 00:00:00 2001 From: Selim Youssry Date: Sat, 15 Jun 2024 13:39:11 +0000 Subject: [PATCH 01/21] ISS-1 sapiologie/hyperformula XLOOKUP in built-in-functions.md --- docs/guide/built-in-functions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/guide/built-in-functions.md b/docs/guide/built-in-functions.md index 7fe8943b5..26f51ace0 100644 --- a/docs/guide/built-in-functions.md +++ b/docs/guide/built-in-functions.md @@ -227,6 +227,7 @@ Total number of functions: **{{ $page.functionsCount }}** | ROW | Returns row number of a given reference or formula reference if argument not provided. | ROW([Reference]) | | ROWS | Returns the number of rows in the given reference. | ROWS(Array) | | VLOOKUP | Searches vertically with reference to adjacent cells to the right. | VLOOKUP(Search_Criterion, Array, Index, Sort_Order) | +| XLOOKUP | The XLOOKUP function searches a range or an array, and then returns the item corresponding to the first match it finds. If no match exists, then XLOOKUP can return the closest (approximate) match. | XLOOKUP(lookup_value, lookup_array, return_array, [if_not_found], [match_mode], [search_mode]) | ### Math and trigonometry From f0f954526b69579909f08617d1d050c5acbf0e2c Mon Sep 17 00:00:00 2001 From: Selim Youssry Date: Sat, 15 Jun 2024 14:07:46 +0000 Subject: [PATCH 02/21] ISS-1 sapiologie/hyperformula add translations --- src/i18n/languages/csCZ.ts | 1 + src/i18n/languages/daDK.ts | 1 + src/i18n/languages/deDE.ts | 1 + src/i18n/languages/enGB.ts | 1 + src/i18n/languages/esES.ts | 1 + src/i18n/languages/fiFI.ts | 1 + src/i18n/languages/frFR.ts | 1 + src/i18n/languages/huHU.ts | 1 + src/i18n/languages/itIT.ts | 1 + src/i18n/languages/nbNO.ts | 1 + src/i18n/languages/nlNL.ts | 1 + src/i18n/languages/plPL.ts | 1 + src/i18n/languages/ptPT.ts | 1 + src/i18n/languages/ruRU.ts | 1 + src/i18n/languages/svSE.ts | 1 + src/i18n/languages/trTR.ts | 1 + 16 files changed, 16 insertions(+) diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index 1ff989648..c96292bad 100644 --- a/src/i18n/languages/csCZ.ts +++ b/src/i18n/languages/csCZ.ts @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = { WEEKNUM: 'WEEKNUM', WORKDAY: 'WORKDAY', 'WORKDAY.INTL': 'WORKDAY.INTL', + XLOOKUP: 'XVYHLEDAT', XNPV: 'XNPV', XOR: 'XOR', YEAR: 'ROK', diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index 54fd85082..36406c12d 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = { WEEKNUM: 'UGE.NR', WORKDAY: 'ARBEJDSDAG', 'WORKDAY.INTL': 'ARBEJDSDAG.INTL', + XLOOKUP: 'XOPSLAG', XNPV: 'NETTO.NUTIDSVÆRDI', XOR: 'XELLER', YEAR: 'ÅR', diff --git a/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index 354e0fa1f..1c4937f0d 100644 --- a/src/i18n/languages/deDE.ts +++ b/src/i18n/languages/deDE.ts @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = { WEEKNUM: 'KALENDERWOCHE', WORKDAY: 'ARBEITSTAG', 'WORKDAY.INTL': 'ARBEITSTAG.INTL', + XLOOKUP: 'XVERWEIS', XNPV: 'XKAPITALWERT', XOR: 'XODER', YEAR: 'JAHR', diff --git a/src/i18n/languages/enGB.ts b/src/i18n/languages/enGB.ts index 2f01c8271..1572200d2 100644 --- a/src/i18n/languages/enGB.ts +++ b/src/i18n/languages/enGB.ts @@ -236,6 +236,7 @@ const dictionary: RawTranslationPackage = { 'WORKDAY.INTL': 'WORKDAY.INTL', XNPV: 'XNPV', XOR: 'XOR', + XLOOKUP: 'XLOOKUP', YEAR: 'YEAR', YEARFRAC: 'YEARFRAC', 'HF.ADD': 'HF.ADD', diff --git a/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index 7b31d3845..e287a751f 100644 --- a/src/i18n/languages/esES.ts +++ b/src/i18n/languages/esES.ts @@ -232,6 +232,7 @@ export const dictionary: RawTranslationPackage = { WEEKNUM: 'NUM.DE.SEMANA', WORKDAY: 'DIA.LAB', 'WORKDAY.INTL': 'DIA.LAB.INTL', + XLOOKUP: 'BUSCARX', XNPV: 'VNA.NO.PER', XOR: 'XOR', YEAR: 'AÑO', diff --git a/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index b7fdc7735..b851a3e89 100644 --- a/src/i18n/languages/fiFI.ts +++ b/src/i18n/languages/fiFI.ts @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = { WEEKNUM: 'VIIKKO.NRO', WORKDAY: 'TYÖPÄIVÄ', 'WORKDAY.INTL': 'TYÖPÄIVÄ.KANSVÄL', + XLOOKUP: 'XHAKU', XNPV: 'NNA.JAKSOTON', XOR: 'EHDOTON.TAI', YEAR: 'VUOSI', diff --git a/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index d3721471d..82a2de12f 100644 --- a/src/i18n/languages/frFR.ts +++ b/src/i18n/languages/frFR.ts @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = { WEEKNUM: 'NO.SEMAINE', WORKDAY: 'SERIE.JOUR.OUVRE', 'WORKDAY.INTL': 'SERIE.JOUR.OUVRE.INTL', + XLOOKUP: 'RECHERCHEX', XNPV: 'VAN.PAIEMENTS', XOR: 'OUX', YEAR: 'ANNEE', diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index 8bc529d76..538795a7e 100644 --- a/src/i18n/languages/huHU.ts +++ b/src/i18n/languages/huHU.ts @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = { WEEKNUM: 'HÉT.SZÁMA', WORKDAY: 'KALK.MUNKANAP', 'WORKDAY.INTL': 'KALK.MUNKANAP.INTL', + XLOOKUP: 'XKERES', XNPV: 'XNJÉ', XOR: 'XVAGY', YEAR: 'ÉV', diff --git a/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 43950a0ef..40972ee0f 100644 --- a/src/i18n/languages/itIT.ts +++ b/src/i18n/languages/itIT.ts @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = { WEEKNUM: 'NUM.SETTIMANA', WORKDAY: 'GIORNO.LAVORATIVO', 'WORKDAY.INTL': 'GIORNO.LAVORATIVO.INTL', + XLOOKUP: 'CERCA.X', XNPV: 'VAN.X', XOR: 'XOR', YEAR: 'ANNO', diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index 2df0ca0b8..480af8bca 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = { WEEKNUM: 'UKENR', WORKDAY: 'ARBEIDSDAG', 'WORKDAY.INTL': 'ARBEIDSDAG.INTL', + XLOOKUP: 'XOPPSLAG', XNPV: 'XNNV', XOR: 'EKSKLUSIVELLER', YEAR: 'ÅR', diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index 14800e6c5..d1434094f 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = { WEEKNUM: 'WEEKNUMMER', WORKDAY: 'WERKDAG', 'WORKDAY.INTL': 'WERKDAG.INTL', + XLOOKUP: 'X.ZOEKEN', XNPV: 'NHW2', XOR: 'EX.OF', YEAR: 'JAAR', diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 8ce39a6d9..706db6caa 100644 --- a/src/i18n/languages/plPL.ts +++ b/src/i18n/languages/plPL.ts @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = { WEEKNUM: 'NUM.TYG', WORKDAY: 'DZIEŃ.ROBOCZY', 'WORKDAY.INTL': 'DZIEŃ.ROBOCZY.NIESTAND', + XLOOKUP: 'X.WYSZUKAJ', XNPV: 'XNPV', XOR: 'XOR', YEAR: 'ROK', diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index 2c41911a9..73786288b 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.ts @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = { WEEKNUM: 'NÚMSEMANA', WORKDAY: 'DIATRABALHO', 'WORKDAY.INTL': 'DIATRABALHO.INTL', + XLOOKUP: 'PROCX', XNPV: 'XVPL', XOR: 'OUEXCL', YEAR: 'ANO', diff --git a/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index 9f4fbc3cd..a53478453 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = { WEEKNUM: 'НОМНЕДЕЛИ', WORKDAY: 'РАБДЕНЬ', 'WORKDAY.INTL': 'РАБДЕНЬ.МЕЖД', + XLOOKUP: 'ПРОСМОТРХ', XNPV: 'ЧИСТНЗ', XOR: 'ИСКЛИЛИ', YEAR: 'ГОД', diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index bd49af139..d6276be2d 100644 --- a/src/i18n/languages/svSE.ts +++ b/src/i18n/languages/svSE.ts @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = { WEEKNUM: 'VECKONR', WORKDAY: 'ARBETSDAGAR', 'WORKDAY.INTL': 'ARBETSDAGAR.INT', + XLOOKUP: 'XLETAUPP', XNPV: 'XNUVÄRDE', XOR: 'XOR', YEAR: 'ÅR', diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index f6f2ca32b..2c7e8b285 100644 --- a/src/i18n/languages/trTR.ts +++ b/src/i18n/languages/trTR.ts @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = { WEEKNUM: 'HAFTASAY', WORKDAY: 'İŞGÜNÜ', 'WORKDAY.INTL': 'İŞGÜNÜ.ULUSL', + XLOOKUP: 'ÇAPRAZARA', XNPV: 'ANBD', XOR: 'ÖZELVEYA', YEAR: 'YIL', From 384bcb8403b783531ec71e78517ce230a00300bf Mon Sep 17 00:00:00 2001 From: Selim Youssry Date: Sat, 15 Jun 2024 14:26:49 +0000 Subject: [PATCH 03/21] ISS-1 sapiologie/hyperformula placeholder function and test --- src/interpreter/plugin/XlookupPlugin.ts | 27 +++++++++++++++++++++++ src/interpreter/plugin/index.ts | 1 + test/interpreter/function-xlookup.spec.ts | 6 +++++ 3 files changed, 34 insertions(+) create mode 100644 src/interpreter/plugin/XlookupPlugin.ts create mode 100644 test/interpreter/function-xlookup.spec.ts diff --git a/src/interpreter/plugin/XlookupPlugin.ts b/src/interpreter/plugin/XlookupPlugin.ts new file mode 100644 index 000000000..af14b8468 --- /dev/null +++ b/src/interpreter/plugin/XlookupPlugin.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright (c) 2024 Handsoncode. All rights reserved. + */ + +import { ProcedureAst } from '../../parser' +import { InterpreterState } from '../InterpreterState' +import { InterpreterValue } from '../InterpreterValue' +import { FunctionPlugin, FunctionPluginTypecheck } from './FunctionPlugin' + + +export class XlookupPlugin extends FunctionPlugin implements FunctionPluginTypecheck { + public static implementedFunctions = { + XLOOKUP: { + method: 'xlookup', + parameters: [ + // TODO @selim - add arguments + ], + repeatLastArgs: 2, + } + } + + public xlookup(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + // TODO @selim - implement + return 2 + } +} \ No newline at end of file diff --git a/src/interpreter/plugin/index.ts b/src/interpreter/plugin/index.ts index 03617b987..877cae726 100644 --- a/src/interpreter/plugin/index.ts +++ b/src/interpreter/plugin/index.ts @@ -46,3 +46,4 @@ export {StatisticalPlugin} from './StatisticalPlugin' export {MathPlugin} from './MathPlugin' export {ComplexPlugin} from './ComplexPlugin' export {StatisticalAggregationPlugin} from './StatisticalAggregationPlugin' +export {XlookupPlugin} from './XlookupPlugin' diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts new file mode 100644 index 000000000..7d97c2d94 --- /dev/null +++ b/test/interpreter/function-xlookup.spec.ts @@ -0,0 +1,6 @@ +describe('Function XLOOKUP', () => { + // TODO @selim - implement me + it('wrong number of arguments', () => { + expect(true).toEqual(true) + }) +}) \ No newline at end of file From eecce3a43a4c7fac98358134d08ec5479648accc Mon Sep 17 00:00:00 2001 From: Selim Youssry Date: Sun, 16 Jun 2024 15:39:02 +0000 Subject: [PATCH 04/21] ISS-1 sapiologie/hyperformula progress on Xlookup logic --- src/interpreter/plugin/XlookupPlugin.ts | 111 ++++++++++++++++++++-- test/interpreter/function-xlookup.spec.ts | 23 ++++- 2 files changed, 125 insertions(+), 9 deletions(-) diff --git a/src/interpreter/plugin/XlookupPlugin.ts b/src/interpreter/plugin/XlookupPlugin.ts index af14b8468..40713fdc4 100644 --- a/src/interpreter/plugin/XlookupPlugin.ts +++ b/src/interpreter/plugin/XlookupPlugin.ts @@ -1,27 +1,126 @@ /** * @license * Copyright (c) 2024 Handsoncode. All rights reserved. + * + * Documentation from + * https://support.microsoft.com/en-us/office/xlookup-function-b7fd680e-6d10-43e6-84f9-88eae8bf5929 */ +import { AbsoluteCellRange } from '../../AbsoluteCellRange' +import { CellError, ErrorType } from '../../Cell' +import { ErrorMessage } from '../../error-message' import { ProcedureAst } from '../../parser' import { InterpreterState } from '../InterpreterState' -import { InterpreterValue } from '../InterpreterValue' -import { FunctionPlugin, FunctionPluginTypecheck } from './FunctionPlugin' +import { RawNoErrorScalarValue, InterpreterValue } from '../InterpreterValue' +import { SimpleRangeValue } from '../../SimpleRangeValue' +import { FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck } from './FunctionPlugin' +import { zeroIfEmpty } from '../ArithmeticHelper' +import { InvalidArgumentsError } from '../../errors' +enum RangeShape { + Column = 1, + Row = 2, + Table = 3 +} export class XlookupPlugin extends FunctionPlugin implements FunctionPluginTypecheck { public static implementedFunctions = { XLOOKUP: { method: 'xlookup', parameters: [ - // TODO @selim - add arguments - ], - repeatLastArgs: 2, + // lookup_value + { argumentType: FunctionArgumentType.NOERROR }, + // lookup_array + { argumentType: FunctionArgumentType.RANGE }, + // return_array + { argumentType: FunctionArgumentType.RANGE }, + // [if_not_found] + { argumentType: FunctionArgumentType.STRING, optionalArg: true, defaultValue: ErrorType.NA }, + // [match_mode] + { argumentType: FunctionArgumentType.NUMBER, optionalArg: true, defaultValue: 0 }, + // [search_mode] + { argumentType: FunctionArgumentType.NUMBER, optionalArg: true, defaultValue: 1 }, + ] } } public xlookup(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - // TODO @selim - implement + return this.runFunction(ast.args, state, this.metadata('XLOOKUP'), (key: RawNoErrorScalarValue, lookupRangeValue: SimpleRangeValue, returnRangeValue: SimpleRangeValue, ifNotFound: any, matchMode: number, searchMode: number) => { + const lookupRange = lookupRangeValue.range + const returnRange = returnRangeValue.range + + if (lookupRange === undefined) { + return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) + } + if (returnRange === undefined) { + return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) + } + if (ifNotFound !== ErrorType.NA && !(ifNotFound instanceof String)) { + return new CellError(ErrorType.VALUE, ErrorMessage.NoConditionMet) + } + if (![0, -1, 1, 2].includes(matchMode)) { + return new CellError(ErrorType.VALUE, ErrorMessage.NoConditionMet) + } + if (![1, -1, 1, 2].includes(searchMode)) { + return new CellError(ErrorType.VALUE, ErrorMessage.NoConditionMet) + } + + // TODO - Implement all options - until then, return NotSupported + if (matchMode !== 0) { + return new CellError(ErrorType.NAME, ErrorMessage.FunctionName("XLOOKUP")) + } + if (searchMode !== 1) { + return new CellError(ErrorType.NAME, ErrorMessage.FunctionName("XLOOKUP")) + } + + return this.doXlookup(zeroIfEmpty(key), lookupRangeValue.range!, returnRangeValue.range!, ifNotFound, matchMode, searchMode) + }) + } + + private doXlookup(key: RawNoErrorScalarValue, lookupAbsRange: AbsoluteCellRange, absReturnRange: AbsoluteCellRange, ifNotFound: any, matchMode: number, searchMode: number): InterpreterValue { + console.log("key", key) + + console.log("lookupAbsRange", lookupAbsRange) + const rangeShape = XlookupPlugin.getRangeShape(lookupAbsRange) + + switch (rangeShape) { + case RangeShape.Column: { + break + } + case RangeShape.Row: { + const searchedRange = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(lookupAbsRange.start, lookupAbsRange.width(), 1), this.dependencyGraph) + // const colIndex = this.searchInRange(key, searchedRange, sorted, this.rowSearch) + break + } + case RangeShape.Table: { + return new CellError(ErrorType.VALUE, ErrorMessage.CellRangeExpected) + } + } + + /** + * Strategy + * 1. [x] Check if lookupRange is a vertical or horizontal range + * 2. [ ] If vertical, lookup row by row + * 3. [ ] If horizontal, lookup column by column + * 4. [ ] Find the cell that matches the condition and return its row and column + * 5. [ ] If vertical, use that row, and return the range in the returnRange from its first column to its last column + * 6. [ ] If horizontal, use that column, and return the range in the returnRange from its first row to its last row + */ + return 2 } + + + + + private static getRangeShape(absRange: AbsoluteCellRange): RangeShape { + + if (absRange.start.col === absRange.end.col && absRange.start.row <= absRange.end.row) { + return RangeShape.Column + } + if (absRange.start.row === absRange.end.row && absRange.start.col <= absRange.end.col) { + return RangeShape.Row + } + return RangeShape.Table + } } \ No newline at end of file diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 7d97c2d94..00c401f4d 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -1,6 +1,23 @@ +/** + * Examples from + * https://support.microsoft.com/en-us/office/xlookup-function-b7fd680e-6d10-43e6-84f9-88eae8bf5929 + */ + +import { HyperFormula } from './../../src' +import {adr} from '../testUtils' + + describe('Function XLOOKUP', () => { - // TODO @selim - implement me - it('wrong number of arguments', () => { - expect(true).toEqual(true) + it('should find value in range (official example 1)', () => { + const engine = HyperFormula.buildFromArray([ + ['China', 'CN', '+86'], + ['India', 'IN', '+91'], + ['United States', 'US', '+1'], + ['Indonesia', 'ID', '+62'], + ['France', 'FR', '+33'], + ['=XLOOKUP("Indonesia", A1:A5, C1:C5)'], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A6'))).toEqual('+62') }) }) \ No newline at end of file From e9cabd6279ed1c37e4cbf62ad49f88079e3fa2d9 Mon Sep 17 00:00:00 2001 From: Selim Youssry Date: Sun, 16 Jun 2024 16:56:09 +0000 Subject: [PATCH 05/21] ISS-1 sapiologie/hyperformula Working XLOOKUP without support for optional parameters --- src/interpreter/plugin/LookupPlugin.ts | 161 ++++++++++++++++++---- src/interpreter/plugin/XlookupPlugin.ts | 126 ----------------- src/interpreter/plugin/index.ts | 1 - test/interpreter/function-xlookup.spec.ts | 38 +++-- 4 files changed, 164 insertions(+), 162 deletions(-) delete mode 100644 src/interpreter/plugin/XlookupPlugin.ts diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index 0b33a89cf..e48e1bec9 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -3,45 +3,66 @@ * Copyright (c) 2024 Handsoncode. All rights reserved. */ -import {AbsoluteCellRange} from '../../AbsoluteCellRange' -import {CellError, ErrorType, simpleCellAddress} from '../../Cell' -import {ErrorMessage} from '../../error-message' -import {RowSearchStrategy} from '../../Lookup/RowSearchStrategy' -import {SearchOptions, SearchStrategy} from '../../Lookup/SearchStrategy' -import {ProcedureAst} from '../../parser' -import {StatType} from '../../statistics' -import {zeroIfEmpty} from '../ArithmeticHelper' -import {InterpreterState} from '../InterpreterState' -import {InternalScalarValue, InterpreterValue, RawNoErrorScalarValue} from '../InterpreterValue' -import {SimpleRangeValue} from '../../SimpleRangeValue' -import {FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions} from './FunctionPlugin' +import { AbsoluteCellRange } from '../../AbsoluteCellRange' +import { CellError, CellRange, ErrorType, simpleCellAddress } from '../../Cell' +import { ErrorMessage } from '../../error-message' +import { RowSearchStrategy } from '../../Lookup/RowSearchStrategy' +import { SearchOptions, SearchStrategy } from '../../Lookup/SearchStrategy' +import { ProcedureAst } from '../../parser' +import { StatType } from '../../statistics' +import { zeroIfEmpty } from '../ArithmeticHelper' +import { InterpreterState } from '../InterpreterState' +import { InternalScalarValue, InterpreterValue, RawNoErrorScalarValue } from '../InterpreterValue' +import { SimpleRangeValue } from '../../SimpleRangeValue' +import { FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions } from './FunctionPlugin' +import { ArraySize } from '../../ArraySize' + + export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypecheck { public static implementedFunctions: ImplementedFunctions = { 'VLOOKUP': { method: 'vlookup', parameters: [ - {argumentType: FunctionArgumentType.NOERROR}, - {argumentType: FunctionArgumentType.RANGE}, - {argumentType: FunctionArgumentType.NUMBER}, - {argumentType: FunctionArgumentType.BOOLEAN, defaultValue: true}, + { argumentType: FunctionArgumentType.NOERROR }, + { argumentType: FunctionArgumentType.RANGE }, + { argumentType: FunctionArgumentType.NUMBER }, + { argumentType: FunctionArgumentType.BOOLEAN, defaultValue: true }, ] }, 'HLOOKUP': { method: 'hlookup', parameters: [ - {argumentType: FunctionArgumentType.NOERROR}, - {argumentType: FunctionArgumentType.RANGE}, - {argumentType: FunctionArgumentType.NUMBER}, - {argumentType: FunctionArgumentType.BOOLEAN, defaultValue: true}, + { argumentType: FunctionArgumentType.NOERROR }, + { argumentType: FunctionArgumentType.RANGE }, + { argumentType: FunctionArgumentType.NUMBER }, + { argumentType: FunctionArgumentType.BOOLEAN, defaultValue: true }, + ] + }, + 'XLOOKUP': { + method: 'xlookup', + arraySizeMethod: 'xlookupArraySize', + parameters: [ + // lookup_value + { argumentType: FunctionArgumentType.NOERROR }, + // lookup_array + { argumentType: FunctionArgumentType.RANGE }, + // return_array + { argumentType: FunctionArgumentType.RANGE }, + // [if_not_found] + { argumentType: FunctionArgumentType.STRING, optionalArg: true, defaultValue: ErrorType.NA }, + // [match_mode] + { argumentType: FunctionArgumentType.NUMBER, optionalArg: true, defaultValue: 0 }, + // [search_mode] + { argumentType: FunctionArgumentType.NUMBER, optionalArg: true, defaultValue: 1 }, ] }, 'MATCH': { method: 'match', parameters: [ - {argumentType: FunctionArgumentType.NOERROR}, - {argumentType: FunctionArgumentType.RANGE}, - {argumentType: FunctionArgumentType.NUMBER, defaultValue: 1}, + { argumentType: FunctionArgumentType.NOERROR }, + { argumentType: FunctionArgumentType.RANGE }, + { argumentType: FunctionArgumentType.NUMBER, defaultValue: 1 }, ] }, } @@ -94,6 +115,62 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech }) } + /** + * Corresponds to XLOOKUP(lookup_value, lookup_array, return_array, [if_not_found], [match_mode], [search_mode]) + * + * @param ast + * @param state + */ + public xlookup(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('XLOOKUP'), (key: RawNoErrorScalarValue, lookupRangeValue: SimpleRangeValue, returnRangeValue: SimpleRangeValue, ifNotFound: any, matchMode: number, searchMode: number) => { + const lookupRange = lookupRangeValue.range + const returnRange = returnRangeValue.range + + if (lookupRange === undefined) { + return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) + } + if (returnRange === undefined) { + return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) + } + if (ifNotFound !== ErrorType.NA && !(ifNotFound instanceof String)) { + return new CellError(ErrorType.VALUE, ErrorMessage.NoConditionMet) + } + if (![0, -1, 1, 2].includes(matchMode)) { + return new CellError(ErrorType.VALUE, ErrorMessage.NoConditionMet) + } + if (![1, -1, 1, 2].includes(searchMode)) { + return new CellError(ErrorType.VALUE, ErrorMessage.NoConditionMet) + } + + // TODO - Implement all options - until then, return NotSupported + if (matchMode !== 0) { + return new CellError(ErrorType.NAME, ErrorMessage.FunctionName('XLOOKUP')) + } + if (searchMode !== 1) { + return new CellError(ErrorType.NAME, ErrorMessage.FunctionName('XLOOKUP')) + } + + return this.doXlookup(zeroIfEmpty(key), lookupRangeValue, returnRangeValue, ifNotFound, matchMode, searchMode) + }) + } + + public xlookupArraySize(ast: ProcedureAst, state: InterpreterState): ArraySize { + const lookupRangeValue = ast?.args?.[1] as CellRange + const returnRangeValue = ast?.args?.[2] as CellRange + const searchWidth = lookupRangeValue.end.col - lookupRangeValue.start.col + 1 + const searchHeight = lookupRangeValue.end.row - lookupRangeValue.start.row + 1 + + if (searchWidth === 1) { + // column search + const outputWidth = returnRangeValue.end.col - returnRangeValue.start.col + 1 + return new ArraySize(outputWidth, 1); + } else { + // row search + const outputHeight = returnRangeValue.end.row - returnRangeValue.start.row + 1 + return new ArraySize(1, outputHeight); + } + } + public match(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.runFunction(ast.args, state, this.metadata('MATCH'), (key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, type: number) => { return this.doMatch(zeroIfEmpty(key), rangeValue, type) @@ -171,6 +248,44 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech return value } + private doXlookup(key: RawNoErrorScalarValue, lookupAbsRangeValue: SimpleRangeValue, absReturnRangeValue: SimpleRangeValue, ifNotFound: any, matchMode: number, searchMode: number): InterpreterValue { + const lookupAbsRange = lookupAbsRangeValue.range + const absReturnRange = absReturnRangeValue.range + if (lookupAbsRange === undefined || absReturnRange === undefined) { + return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) + } + + if (lookupAbsRange.start.col === lookupAbsRange.end.col && lookupAbsRange.start.row <= lookupAbsRange.end.row) { + // single column + const searchedRange = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(lookupAbsRange.start, 1, lookupAbsRange.height()), this.dependencyGraph) + const rowIndex = this.searchInRange(key, searchedRange, false, this.columnSearch) + if (rowIndex === -1) { + return new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) + } + const topLeft = { sheet: absReturnRange.sheet, col: absReturnRange.start.col, row: absReturnRange.start.row + rowIndex } + const width = absReturnRange.end.col - absReturnRange.start.col + 1 + + const ret = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(topLeft, width, 1), this.dependencyGraph) + return ret + } else if (lookupAbsRange.start.row === lookupAbsRange.end.row && lookupAbsRange.start.col <= lookupAbsRange.end.col) { + // single row + const searchedRange = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(lookupAbsRange.start, lookupAbsRange.width(), 1), this.dependencyGraph) + const colIndex = this.searchInRange(key, searchedRange, false, this.rowSearch) + + if (colIndex === -1) { + return (ifNotFound == ErrorType.NA) ? new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) : ifNotFound + } + + const topLeft = { sheet: absReturnRange.sheet, col: absReturnRange.start.col + colIndex, row: absReturnRange.start.row } + const height = absReturnRange.end.row - absReturnRange.start.row + 1 + return SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(topLeft, 1, height), this.dependencyGraph) + } else { + // multiple rows and tables - not supported + return new CellError(ErrorType.VALUE, ErrorMessage.CellRangeExpected) + } + } + + private doMatch(key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, type: number): InternalScalarValue { if (![-1, 0, 1].includes(type)) { return new CellError(ErrorType.VALUE, ErrorMessage.BadMode) diff --git a/src/interpreter/plugin/XlookupPlugin.ts b/src/interpreter/plugin/XlookupPlugin.ts deleted file mode 100644 index 40713fdc4..000000000 --- a/src/interpreter/plugin/XlookupPlugin.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * @license - * Copyright (c) 2024 Handsoncode. All rights reserved. - * - * Documentation from - * https://support.microsoft.com/en-us/office/xlookup-function-b7fd680e-6d10-43e6-84f9-88eae8bf5929 - */ - -import { AbsoluteCellRange } from '../../AbsoluteCellRange' -import { CellError, ErrorType } from '../../Cell' -import { ErrorMessage } from '../../error-message' -import { ProcedureAst } from '../../parser' -import { InterpreterState } from '../InterpreterState' -import { RawNoErrorScalarValue, InterpreterValue } from '../InterpreterValue' -import { SimpleRangeValue } from '../../SimpleRangeValue' -import { FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck } from './FunctionPlugin' -import { zeroIfEmpty } from '../ArithmeticHelper' -import { InvalidArgumentsError } from '../../errors' - -enum RangeShape { - Column = 1, - Row = 2, - Table = 3 -} - -export class XlookupPlugin extends FunctionPlugin implements FunctionPluginTypecheck { - public static implementedFunctions = { - XLOOKUP: { - method: 'xlookup', - parameters: [ - // lookup_value - { argumentType: FunctionArgumentType.NOERROR }, - // lookup_array - { argumentType: FunctionArgumentType.RANGE }, - // return_array - { argumentType: FunctionArgumentType.RANGE }, - // [if_not_found] - { argumentType: FunctionArgumentType.STRING, optionalArg: true, defaultValue: ErrorType.NA }, - // [match_mode] - { argumentType: FunctionArgumentType.NUMBER, optionalArg: true, defaultValue: 0 }, - // [search_mode] - { argumentType: FunctionArgumentType.NUMBER, optionalArg: true, defaultValue: 1 }, - ] - } - } - - public xlookup(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('XLOOKUP'), (key: RawNoErrorScalarValue, lookupRangeValue: SimpleRangeValue, returnRangeValue: SimpleRangeValue, ifNotFound: any, matchMode: number, searchMode: number) => { - const lookupRange = lookupRangeValue.range - const returnRange = returnRangeValue.range - - if (lookupRange === undefined) { - return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) - } - if (returnRange === undefined) { - return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) - } - if (ifNotFound !== ErrorType.NA && !(ifNotFound instanceof String)) { - return new CellError(ErrorType.VALUE, ErrorMessage.NoConditionMet) - } - if (![0, -1, 1, 2].includes(matchMode)) { - return new CellError(ErrorType.VALUE, ErrorMessage.NoConditionMet) - } - if (![1, -1, 1, 2].includes(searchMode)) { - return new CellError(ErrorType.VALUE, ErrorMessage.NoConditionMet) - } - - // TODO - Implement all options - until then, return NotSupported - if (matchMode !== 0) { - return new CellError(ErrorType.NAME, ErrorMessage.FunctionName("XLOOKUP")) - } - if (searchMode !== 1) { - return new CellError(ErrorType.NAME, ErrorMessage.FunctionName("XLOOKUP")) - } - - return this.doXlookup(zeroIfEmpty(key), lookupRangeValue.range!, returnRangeValue.range!, ifNotFound, matchMode, searchMode) - }) - } - - private doXlookup(key: RawNoErrorScalarValue, lookupAbsRange: AbsoluteCellRange, absReturnRange: AbsoluteCellRange, ifNotFound: any, matchMode: number, searchMode: number): InterpreterValue { - console.log("key", key) - - console.log("lookupAbsRange", lookupAbsRange) - const rangeShape = XlookupPlugin.getRangeShape(lookupAbsRange) - - switch (rangeShape) { - case RangeShape.Column: { - break - } - case RangeShape.Row: { - const searchedRange = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(lookupAbsRange.start, lookupAbsRange.width(), 1), this.dependencyGraph) - // const colIndex = this.searchInRange(key, searchedRange, sorted, this.rowSearch) - break - } - case RangeShape.Table: { - return new CellError(ErrorType.VALUE, ErrorMessage.CellRangeExpected) - } - } - - /** - * Strategy - * 1. [x] Check if lookupRange is a vertical or horizontal range - * 2. [ ] If vertical, lookup row by row - * 3. [ ] If horizontal, lookup column by column - * 4. [ ] Find the cell that matches the condition and return its row and column - * 5. [ ] If vertical, use that row, and return the range in the returnRange from its first column to its last column - * 6. [ ] If horizontal, use that column, and return the range in the returnRange from its first row to its last row - */ - - return 2 - } - - - - - private static getRangeShape(absRange: AbsoluteCellRange): RangeShape { - - if (absRange.start.col === absRange.end.col && absRange.start.row <= absRange.end.row) { - return RangeShape.Column - } - if (absRange.start.row === absRange.end.row && absRange.start.col <= absRange.end.col) { - return RangeShape.Row - } - return RangeShape.Table - } -} \ No newline at end of file diff --git a/src/interpreter/plugin/index.ts b/src/interpreter/plugin/index.ts index 877cae726..03617b987 100644 --- a/src/interpreter/plugin/index.ts +++ b/src/interpreter/plugin/index.ts @@ -46,4 +46,3 @@ export {StatisticalPlugin} from './StatisticalPlugin' export {MathPlugin} from './MathPlugin' export {ComplexPlugin} from './ComplexPlugin' export {StatisticalAggregationPlugin} from './StatisticalAggregationPlugin' -export {XlookupPlugin} from './XlookupPlugin' diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 00c401f4d..a269828ac 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -4,20 +4,34 @@ */ import { HyperFormula } from './../../src' -import {adr} from '../testUtils' +import { adr } from '../testUtils' +import { AbsoluteCellRange } from '../../src/AbsoluteCellRange' describe('Function XLOOKUP', () => { - it('should find value in range (official example 1)', () => { - const engine = HyperFormula.buildFromArray([ - ['China', 'CN', '+86'], - ['India', 'IN', '+91'], - ['United States', 'US', '+1'], - ['Indonesia', 'ID', '+62'], - ['France', 'FR', '+33'], - ['=XLOOKUP("Indonesia", A1:A5, C1:C5)'], - ], { useColumnIndex: false }) + it('should find value in simple column range (official example 1)', () => { + const engine = HyperFormula.buildFromArray([ + ['China', 'CN'], + ['India', 'IN'], + ['United States', 'US'], + ['Indonesia', 'ID'], + ['France', 'FR'], + ['=XLOOKUP("Indonesia", A1:A5, B1:B5)'], + ], { useColumnIndex: false }) - expect(engine.getCellValue(adr('A6'))).toEqual('+62') - }) + expect(engine.getCellValue(adr('A6'))).toEqual('ID') + }) + + it('should find range in table (official example 2)', () => { + const engine = HyperFormula.buildFromArray([ + ['8389', 'Dianne Pugh', 'Finance'], + ['4390', 'Ned Lanning', 'Marketing'], + ['8604', 'Margo Hendrix', 'Sales'], + ['8389', 'Dianne Pugh', 'Finance'], + ['4937', 'Earlene McCarty', 'Accounting'], + ['=XLOOKUP(A1, A2:A5, B2:C5)'], + ], { useColumnIndex: false }) + + expect(engine.getRangeValues(AbsoluteCellRange.spanFrom(adr('A6'), 2, 1))).toEqual([['Dianne Pugh', 'Finance']]) + }) }) \ No newline at end of file From 83be86cd4ed16dcbb7bcce60d99a3688d8ef6c80 Mon Sep 17 00:00:00 2001 From: Selim Youssry Date: Sun, 16 Jun 2024 17:02:52 +0000 Subject: [PATCH 06/21] ISS-1 sapiologie/hyperformula override default if_not_found --- src/interpreter/plugin/LookupPlugin.ts | 4 ++-- test/interpreter/function-xlookup.spec.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index e48e1bec9..de0af8dbe 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -132,7 +132,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech if (returnRange === undefined) { return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) } - if (ifNotFound !== ErrorType.NA && !(ifNotFound instanceof String)) { + if (ifNotFound !== ErrorType.NA && typeof ifNotFound !== 'string') { return new CellError(ErrorType.VALUE, ErrorMessage.NoConditionMet) } if (![0, -1, 1, 2].includes(matchMode)) { @@ -260,7 +260,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech const searchedRange = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(lookupAbsRange.start, 1, lookupAbsRange.height()), this.dependencyGraph) const rowIndex = this.searchInRange(key, searchedRange, false, this.columnSearch) if (rowIndex === -1) { - return new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) + return (ifNotFound == ErrorType.NA) ? new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) : ifNotFound } const topLeft = { sheet: absReturnRange.sheet, col: absReturnRange.start.col, row: absReturnRange.start.row + rowIndex } const width = absReturnRange.end.col - absReturnRange.start.col + 1 diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index a269828ac..56fafd161 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -34,4 +34,17 @@ describe('Function XLOOKUP', () => { expect(engine.getRangeValues(AbsoluteCellRange.spanFrom(adr('A6'), 2, 1))).toEqual([['Dianne Pugh', 'Finance']]) }) + + it('should find use if_not_found argument if not found (official example 3)', () => { + const engine = HyperFormula.buildFromArray([ + ['1234', 'Dianne Pugh', 'Finance'], + ['4390', 'Ned Lanning', 'Marketing'], + ['8604', 'Margo Hendrix', 'Sales'], + ['8389', 'Dianne Pugh', 'Finance'], + ['4937', 'Earlene McCarty', 'Accounting'], + ['=XLOOKUP(A1, A2:A5, B2:C5, "ID not found")'], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A6'))).toEqual('ID not found') + }) }) \ No newline at end of file From 5101a9059e34e675ab5c1f455f5c95ffaa210ba4 Mon Sep 17 00:00:00 2001 From: Selim Youssry Date: Sun, 16 Jun 2024 17:53:33 +0000 Subject: [PATCH 07/21] ISS-1 sapiologie/hyperformula add commented out range function return test --- src/interpreter/plugin/LookupPlugin.ts | 8 ++-- test/interpreter/function-xlookup.spec.ts | 51 ++++++++++++++++++++++- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index de0af8dbe..c757ad9cb 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -158,7 +158,10 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech const lookupRangeValue = ast?.args?.[1] as CellRange const returnRangeValue = ast?.args?.[2] as CellRange const searchWidth = lookupRangeValue.end.col - lookupRangeValue.start.col + 1 - const searchHeight = lookupRangeValue.end.row - lookupRangeValue.start.row + 1 + + if (returnRangeValue?.start == null || returnRangeValue?.end == null) { + return ArraySize.scalar(); + } if (searchWidth === 1) { // column search @@ -265,8 +268,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech const topLeft = { sheet: absReturnRange.sheet, col: absReturnRange.start.col, row: absReturnRange.start.row + rowIndex } const width = absReturnRange.end.col - absReturnRange.start.col + 1 - const ret = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(topLeft, width, 1), this.dependencyGraph) - return ret + return SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(topLeft, width, 1), this.dependencyGraph) } else if (lookupAbsRange.start.row === lookupAbsRange.end.row && lookupAbsRange.start.col <= lookupAbsRange.end.col) { // single row const searchedRange = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(lookupAbsRange.start, lookupAbsRange.width(), 1), this.dependencyGraph) diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 56fafd161..a92b45681 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -22,7 +22,7 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A6'))).toEqual('ID') }) - it('should find range in table (official example 2)', () => { + it('should find row range in table (official example 2)', () => { const engine = HyperFormula.buildFromArray([ ['8389', 'Dianne Pugh', 'Finance'], ['4390', 'Ned Lanning', 'Marketing'], @@ -35,6 +35,18 @@ describe('Function XLOOKUP', () => { expect(engine.getRangeValues(AbsoluteCellRange.spanFrom(adr('A6'), 2, 1))).toEqual([['Dianne Pugh', 'Finance']]) }) + it('should find column range in table (official example 2, transposed)', () => { + const engine = HyperFormula.buildFromArray([ + ['8389', '4390', '8604', '8389', '4937'], + ['Dianne Pugh', 'Ned Lanning', 'Margo Hendrix', 'Dianne Pugh', 'Earlene McCarty'], + ['Finance', 'Marketing', 'Sales', 'Finance', 'Accounting'], + ['=XLOOKUP(A1, B1:E1, B2:E3)'], + [] + ], { useColumnIndex: false }) + + expect(engine.getRangeValues(AbsoluteCellRange.spanFrom(adr('A4'), 1, 2))).toEqual([['Dianne Pugh'], ['Finance']]) + }) + it('should find use if_not_found argument if not found (official example 3)', () => { const engine = HyperFormula.buildFromArray([ ['1234', 'Dianne Pugh', 'Finance'], @@ -47,4 +59,41 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A6'))).toEqual('ID not found') }) + + it('nested xlookup function to perform both a vertical and horizontal match (official example 5)', () => { + const engine = HyperFormula.buildFromArray([ + ['Quarter', 'Gross profit', 'Net profit', 'Profit %'], + ['Qtr1', '=XLOOKUP(B1, $A4:$A12, XLOOKUP($A2, $B3:$F3, $B4:$F12))', '19342', '29.3'], + ['Income statement', 'Qtr1', 'Qtr2', 'Qtr3', 'Qtr4', 'Total'], + ['Total sales', '50000', '78200', '89500', '91200', '308950'], + ['Cost of sales', '25000', '42050', '59450', '60450', '186950'], + ['Gross profit', '25000', '36150', '30050', '30800', '122000'], + ['Depreciation', '899', '791', '202', '412', '2304'], + ['Interest', '513', '853', '150', '956', '2472'], + ['Earnings before tax', '23588', '34506', '29698', '29432', '117224'], + ['Tax', '4246', '6211', '5346', '5298', '21100'], + ['Net profit', '19342', '28295', '24352', '24134', '96124'], + ['Profit %', '29.3', '27.8', '23.4', '27.6', '26.9'], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('B2'))).toEqual(25000) + }) + + // TODO - Uncomment this function when functions are allowed to return ranges in Hyperformula + // it('two nested xlookup + sum (official example 6)', () => { + // const engine = HyperFormula.buildFromArray([ + // ['Start', 'End', 'Total'], + // ['Grape', 'Banana', '=SUM(XLOOKUP(A2, A4:A8, D4:D8):XLOOKUP(B2, A4:A8, D4:D8))'], + // ['Product', 'Qty', 'Price', 'Total'], + // ['Apple', '23', '0.52', '11.90'], + // ['Grape', '98', '0.77', '75.28'], + // ['Pear', '75', '0.24', '18.16'], + // ['Banana', '95', '0.18', '17.25'], + // ['Cherry', '42', '0.16', '6.80'] + // ], + // { useColumnIndex: false } + // ) + + // expect(engine.getCellValue(adr('C2'))).toEqual(110.70) + // }) }) \ No newline at end of file From 08445d10b882027817be0cf8f89bf5e820e784ee Mon Sep 17 00:00:00 2001 From: Selim Youssry Date: Sun, 16 Jun 2024 17:55:12 +0000 Subject: [PATCH 08/21] ISS-1 sapiologie/hyperformula update the doc to reflect limitations --- docs/guide/built-in-functions.md | 2 +- src/interpreter/plugin/LookupPlugin.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guide/built-in-functions.md b/docs/guide/built-in-functions.md index 26f51ace0..37afeea2f 100644 --- a/docs/guide/built-in-functions.md +++ b/docs/guide/built-in-functions.md @@ -227,7 +227,7 @@ Total number of functions: **{{ $page.functionsCount }}** | ROW | Returns row number of a given reference or formula reference if argument not provided. | ROW([Reference]) | | ROWS | Returns the number of rows in the given reference. | ROWS(Array) | | VLOOKUP | Searches vertically with reference to adjacent cells to the right. | VLOOKUP(Search_Criterion, Array, Index, Sort_Order) | -| XLOOKUP | The XLOOKUP function searches a range or an array, and then returns the item corresponding to the first match it finds. If no match exists, then XLOOKUP can return the closest (approximate) match. | XLOOKUP(lookup_value, lookup_array, return_array, [if_not_found], [match_mode], [search_mode]) | +| XLOOKUP | The XLOOKUP function searches a range or an array, and then returns the item corresponding to the first match it finds. If no match exists, then XLOOKUP can return the closest (approximate) match. Current limitations: only default match_mode and search_mode are supported, a range of value is returned, not a range, so having XLOOKUP(...):XLOOKUP(...) will not work. | XLOOKUP(lookup_value, lookup_array, return_array, [if_not_found], [match_mode], [search_mode]) | ### Math and trigonometry diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index c757ad9cb..b160d269b 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -160,17 +160,17 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech const searchWidth = lookupRangeValue.end.col - lookupRangeValue.start.col + 1 if (returnRangeValue?.start == null || returnRangeValue?.end == null) { - return ArraySize.scalar(); + return ArraySize.scalar() } if (searchWidth === 1) { // column search const outputWidth = returnRangeValue.end.col - returnRangeValue.start.col + 1 - return new ArraySize(outputWidth, 1); + return new ArraySize(outputWidth, 1) } else { // row search const outputHeight = returnRangeValue.end.row - returnRangeValue.start.row + 1 - return new ArraySize(1, outputHeight); + return new ArraySize(1, outputHeight) } } From 9ece67c174744da355307d85e97d6d250751dcb4 Mon Sep 17 00:00:00 2001 From: Selim Youssry Date: Mon, 17 Jun 2024 11:17:33 +0000 Subject: [PATCH 09/21] ISS-1 sapiologie/hyperformula Set vectorizationForbidden: true on XLOOKUP --- src/interpreter/plugin/LookupPlugin.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index b160d269b..5a9a19e9c 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -42,6 +42,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech 'XLOOKUP': { method: 'xlookup', arraySizeMethod: 'xlookupArraySize', + vectorizationForbidden: true, parameters: [ // lookup_value { argumentType: FunctionArgumentType.NOERROR }, From 9c448c259180c96264a3e28630d93ceb4e7f4dca Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Thu, 5 Dec 2024 11:39:44 +0100 Subject: [PATCH 10/21] Add test suite for the XLOOKUP function --- test/interpreter/function-xlookup.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 test/interpreter/function-xlookup.spec.ts diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts new file mode 100644 index 000000000..824fd929b --- /dev/null +++ b/test/interpreter/function-xlookup.spec.ts @@ -0,0 +1,8 @@ +import {HyperFormula} from '../../src' +import {ErrorType} from '../../src' +import {ErrorMessage} from '../../src/error-message' +import {adr, detailedError} from '../testUtils' + +describe('Function XLOOKUP', () => { + +}) From 167cac8934b5e2842f81e5652bc2437d2479dcdf Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Thu, 5 Dec 2024 12:10:23 +0100 Subject: [PATCH 11/21] Plan basic tests for XLOOKUP --- test/interpreter/function-xlookup.spec.ts | 45 ++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 824fd929b..a35cced32 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -4,5 +4,48 @@ import {ErrorMessage} from '../../src/error-message' import {adr, detailedError} from '../testUtils' describe('Function XLOOKUP', () => { - + describe('validates arguments', () => { + it('returns error when less than 3 arguments', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(1, A2:B3)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.WrongArgNumber)) + }) + + it('returns error when less more than 3 arguments', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(1, A2:B3, C4:D5, "foo")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.WrongArgNumber)) + }) + + // arg types validation + + it('returns error when lookupArray and returnArray are not of the same shape', () => { + + }) + + it('propagates errors properly', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(1/0, B1:B1, 1)'], + ['=XLOOKUP(1, B1:B1, 1/0)'], + ['=XLOOKUP(1, A10:A11, NA())'] + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.DIV_BY_ZERO)) + expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.DIV_BY_ZERO)) + expect(engine.getCellValue(adr('A3'))).toEqualError(detailedError(ErrorType.NA)) + }) + }) + + describe('looks up values', () => { + // sorted column, NA if not found + // sorted row, NA if not found + // unsorted column, NA if not found + // unsorted row, NA if not found + }) + + // different modes }) From 8699958e1ffa7818f6d9fe7832296a24d0cfd5d5 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Thu, 5 Dec 2024 15:10:46 +0100 Subject: [PATCH 12/21] Make XLOOKUP work in basic mode --- src/interpreter/plugin/LookupPlugin.ts | 34 ++++++----------------- test/interpreter/function-xlookup.spec.ts | 12 ++++---- 2 files changed, 15 insertions(+), 31 deletions(-) diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index 5a9a19e9c..a667a3740 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -15,9 +15,6 @@ import { InterpreterState } from '../InterpreterState' import { InternalScalarValue, InterpreterValue, RawNoErrorScalarValue } from '../InterpreterValue' import { SimpleRangeValue } from '../../SimpleRangeValue' import { FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions } from './FunctionPlugin' -import { ArraySize } from '../../ArraySize' - - export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypecheck { public static implementedFunctions: ImplementedFunctions = { @@ -41,7 +38,6 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech }, 'XLOOKUP': { method: 'xlookup', - arraySizeMethod: 'xlookupArraySize', vectorizationForbidden: true, parameters: [ // lookup_value @@ -130,24 +126,32 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech if (lookupRange === undefined) { return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) } + if (returnRange === undefined) { return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) } + if (ifNotFound !== ErrorType.NA && typeof ifNotFound !== 'string') { return new CellError(ErrorType.VALUE, ErrorMessage.NoConditionMet) } + if (![0, -1, 1, 2].includes(matchMode)) { return new CellError(ErrorType.VALUE, ErrorMessage.NoConditionMet) } + if (![1, -1, 1, 2].includes(searchMode)) { return new CellError(ErrorType.VALUE, ErrorMessage.NoConditionMet) } - // TODO - Implement all options - until then, return NotSupported if (matchMode !== 0) { + // not supported yet + // TODO: Implement match mode return new CellError(ErrorType.NAME, ErrorMessage.FunctionName('XLOOKUP')) } + if (searchMode !== 1) { + // not supported yet + // TODO: Implement search mode return new CellError(ErrorType.NAME, ErrorMessage.FunctionName('XLOOKUP')) } @@ -155,26 +159,6 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech }) } - public xlookupArraySize(ast: ProcedureAst, state: InterpreterState): ArraySize { - const lookupRangeValue = ast?.args?.[1] as CellRange - const returnRangeValue = ast?.args?.[2] as CellRange - const searchWidth = lookupRangeValue.end.col - lookupRangeValue.start.col + 1 - - if (returnRangeValue?.start == null || returnRangeValue?.end == null) { - return ArraySize.scalar() - } - - if (searchWidth === 1) { - // column search - const outputWidth = returnRangeValue.end.col - returnRangeValue.start.col + 1 - return new ArraySize(outputWidth, 1) - } else { - // row search - const outputHeight = returnRangeValue.end.row - returnRangeValue.start.row + 1 - return new ArraySize(1, outputHeight) - } - } - public match(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.runFunction(ast.args, state, this.metadata('MATCH'), (key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, type: number) => { return this.doMatch(zeroIfEmpty(key), rangeValue, type) diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index cdf9948f9..0f90a9664 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -1,7 +1,7 @@ -import {HyperFormula} from '../../src' -import {ErrorType} from '../../src' -import {ErrorMessage} from '../../src/error-message' -import {adr, detailedError} from '../testUtils' +import { HyperFormula, ErrorType } from '../../src' +import { ErrorMessage } from '../../src/error-message' +import { adr, detailedError } from '../testUtils' +import { AbsoluteCellRange } from '../../src/AbsoluteCellRange' describe('Function XLOOKUP', () => { describe('validates arguments', () => { @@ -13,9 +13,9 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.WrongArgNumber)) }) - it('returns error when less more than 3 arguments', () => { + it('returns error when less more than 5 arguments', () => { const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(1, A2:B3, C4:D5, "foo")'], + ['=XLOOKUP(1, A2:B3, C4:D5, "foo", 0, 1, 42)'], ]) expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.WrongArgNumber)) From 13b9cc0b2b72db85af2ef3c56a545fe1bf6a7918 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Thu, 5 Dec 2024 15:13:56 +0100 Subject: [PATCH 13/21] Make XLOOKUP return a range --- src/interpreter/plugin/LookupPlugin.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index a667a3740..38e077efe 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -15,6 +15,7 @@ import { InterpreterState } from '../InterpreterState' import { InternalScalarValue, InterpreterValue, RawNoErrorScalarValue } from '../InterpreterValue' import { SimpleRangeValue } from '../../SimpleRangeValue' import { FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions } from './FunctionPlugin' +import { ArraySize } from '../../ArraySize' export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypecheck { public static implementedFunctions: ImplementedFunctions = { @@ -38,6 +39,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech }, 'XLOOKUP': { method: 'xlookup', + arraySizeMethod: 'xlookupArraySize', vectorizationForbidden: true, parameters: [ // lookup_value @@ -159,6 +161,26 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech }) } + public xlookupArraySize(ast: ProcedureAst, state: InterpreterState): ArraySize { + const lookupRangeValue = ast?.args?.[1] as CellRange + const returnRangeValue = ast?.args?.[2] as CellRange + const searchWidth = lookupRangeValue.end.col - lookupRangeValue.start.col + 1 + + if (returnRangeValue?.start == null || returnRangeValue?.end == null) { + return ArraySize.scalar() + } + + if (searchWidth === 1) { + // column search + const outputWidth = returnRangeValue.end.col - returnRangeValue.start.col + 1 + return new ArraySize(outputWidth, 1) + } else { + // row search + const outputHeight = returnRangeValue.end.row - returnRangeValue.start.row + 1 + return new ArraySize(1, outputHeight) + } + } + public match(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.runFunction(ast.args, state, this.metadata('MATCH'), (key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, type: number) => { return this.doMatch(zeroIfEmpty(key), rangeValue, type) From d3c6a2652654e224f389dbacb64196dc25e98c7e Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Thu, 5 Dec 2024 17:40:27 +0100 Subject: [PATCH 14/21] Add unit tests about the types of argument --- src/interpreter/plugin/LookupPlugin.ts | 41 ++++++----- test/interpreter/function-xlookup.spec.ts | 84 ++++++++++++++++++++--- 2 files changed, 94 insertions(+), 31 deletions(-) diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index 38e077efe..23d721296 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -49,7 +49,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech // return_array { argumentType: FunctionArgumentType.RANGE }, // [if_not_found] - { argumentType: FunctionArgumentType.STRING, optionalArg: true, defaultValue: ErrorType.NA }, + { argumentType: FunctionArgumentType.SCALAR, optionalArg: true, defaultValue: ErrorType.NA }, // [match_mode] { argumentType: FunctionArgumentType.NUMBER, optionalArg: true, defaultValue: 0 }, // [search_mode] @@ -121,40 +121,25 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech * @param state */ public xlookup(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('XLOOKUP'), (key: RawNoErrorScalarValue, lookupRangeValue: SimpleRangeValue, returnRangeValue: SimpleRangeValue, ifNotFound: any, matchMode: number, searchMode: number) => { - const lookupRange = lookupRangeValue.range - const returnRange = returnRangeValue.range - - if (lookupRange === undefined) { - return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) - } - - if (returnRange === undefined) { - return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) - } - - if (ifNotFound !== ErrorType.NA && typeof ifNotFound !== 'string') { - return new CellError(ErrorType.VALUE, ErrorMessage.NoConditionMet) - } - + return this.runFunction(ast.args, state, this.metadata('XLOOKUP'), (key: RawNoErrorScalarValue, lookupRangeValue: SimpleRangeValue, returnRangeValue: SimpleRangeValue, ifNotFound: any, matchMode: number, searchMode: number) => { if (![0, -1, 1, 2].includes(matchMode)) { - return new CellError(ErrorType.VALUE, ErrorMessage.NoConditionMet) + return new CellError(ErrorType.VALUE, ErrorMessage.BadMode) } if (![1, -1, 1, 2].includes(searchMode)) { - return new CellError(ErrorType.VALUE, ErrorMessage.NoConditionMet) + return new CellError(ErrorType.VALUE, ErrorMessage.BadMode) } if (matchMode !== 0) { // not supported yet // TODO: Implement match mode - return new CellError(ErrorType.NAME, ErrorMessage.FunctionName('XLOOKUP')) + return new CellError(ErrorType.VALUE, ErrorMessage.BadMode) } if (searchMode !== 1) { // not supported yet // TODO: Implement search mode - return new CellError(ErrorType.NAME, ErrorMessage.FunctionName('XLOOKUP')) + return new CellError(ErrorType.VALUE, ErrorMessage.BadMode) } return this.doXlookup(zeroIfEmpty(key), lookupRangeValue, returnRangeValue, ifNotFound, matchMode, searchMode) @@ -162,8 +147,22 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech } public xlookupArraySize(ast: ProcedureAst, state: InterpreterState): ArraySize { + if (ast?.args?.length !== 5) { + return ArraySize.error() + } + const lookupRangeValue = ast?.args?.[1] as CellRange const returnRangeValue = ast?.args?.[2] as CellRange + + if ([ + lookupRangeValue.start, + returnRangeValue.start, + lookupRangeValue.end, + returnRangeValue.end + ].some((val) => val === undefined)) { + return ArraySize.error() + } + const searchWidth = lookupRangeValue.end.col - lookupRangeValue.start.col + 1 if (returnRangeValue?.start == null || returnRangeValue?.end == null) { diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 0f90a9664..9dba56a21 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -13,18 +13,82 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.WrongArgNumber)) }) - it('returns error when less more than 5 arguments', () => { + it('returns error when more than 5 arguments', () => { const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(1, A2:B3, C4:D5, "foo", 0, 1, 42)'], + ['=XLOOKUP(1, A2:A3, B2:B3, "foo", 0, 1, 42)'], ]) expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.WrongArgNumber)) }) - // arg types validation + it('returns error when lookupArray is not a range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(1, B1, C1:C1)'], + ['=XLOOKUP(1, 42, C1:C1)'], + ['=XLOOKUP(1, "string", C1:C1)'], + ['=XLOOKUP(1, TRUE(), C1:C1)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) + expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) + expect(engine.getCellValue(adr('A3'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) + expect(engine.getCellValue(adr('A4'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) + }) - it('returns error when lookupArray and returnArray are not of the same shape', () => { + it('returns error when returnArray is not a range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(1, C1:C1, B1)'], + ['=XLOOKUP(1, C1:C1, 42)'], + ['=XLOOKUP(1, C1:C1, "string")'], + ['=XLOOKUP(1, C1:C1, TRUE())'], + ]) + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) + expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) + expect(engine.getCellValue(adr('A3'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) + expect(engine.getCellValue(adr('A4'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) + }) + + // it('returns error when lookupArray and returnArray are not of the same shape', () => { + // const engine = HyperFormula.buildFromArray([ + // ['=XLOOKUP(1, B1:B2, C1:C3)'], + // ]) + + // expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) + // }) + + it('returns error when matchMode is of wrong type', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(1, B1:B2, C1:C2, 0, -2)'], + ['=XLOOKUP(1, B1:B2, C1:C2, 0, 3)'], + ['=XLOOKUP(1, B1:B2, C1:C2, 0, 0.5)'], + ['=XLOOKUP(1, B1:B2, C1:C2, 0, "string")'], + ['=XLOOKUP(1, B1:B2, C1:C2, 0, B1:B2)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.BadMode)) + expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.BadMode)) + expect(engine.getCellValue(adr('A3'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.BadMode)) + expect(engine.getCellValue(adr('A4'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.NumberCoercion)) + expect(engine.getCellValue(adr('A5'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) + }) + + it('returns error when searchMode is of wrong type', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(1, B1:B2, C1:C2, 0, 0, -3)'], + ['=XLOOKUP(1, B1:B2, C1:C2, 0, 0, 3)'], + ['=XLOOKUP(1, B1:B2, C1:C2, 0, 0, 0)'], + ['=XLOOKUP(1, B1:B2, C1:C2, 0, 0, 0.5)'], + ['=XLOOKUP(1, B1:B2, C1:C2, 0, 0, "string")'], + ['=XLOOKUP(1, B1:B2, C1:C2, 0, 0, D1:D2)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.BadMode)) + expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.BadMode)) + expect(engine.getCellValue(adr('A3'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.BadMode)) + expect(engine.getCellValue(adr('A4'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.BadMode)) + expect(engine.getCellValue(adr('A5'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.NumberCoercion)) + expect(engine.getCellValue(adr('A6'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) }) it('propagates errors properly', () => { @@ -63,7 +127,7 @@ describe('Function XLOOKUP', () => { ['Indonesia', 'ID'], ['France', 'FR'], ['=XLOOKUP("Indonesia", A1:A5, B1:B5)'], - ], { useColumnIndex: false }) + ]) expect(engine.getCellValue(adr('A6'))).toEqual('ID') }) @@ -76,7 +140,7 @@ describe('Function XLOOKUP', () => { ['8389', 'Dianne Pugh', 'Finance'], ['4937', 'Earlene McCarty', 'Accounting'], ['=XLOOKUP(A1, A2:A5, B2:C5)'], - ], { useColumnIndex: false }) + ]) expect(engine.getRangeValues(AbsoluteCellRange.spanFrom(adr('A6'), 2, 1))).toEqual([['Dianne Pugh', 'Finance']]) }) @@ -88,7 +152,7 @@ describe('Function XLOOKUP', () => { ['Finance', 'Marketing', 'Sales', 'Finance', 'Accounting'], ['=XLOOKUP(A1, B1:E1, B2:E3)'], [] - ], { useColumnIndex: false }) + ]) expect(engine.getRangeValues(AbsoluteCellRange.spanFrom(adr('A4'), 1, 2))).toEqual([['Dianne Pugh'], ['Finance']]) }) @@ -100,8 +164,8 @@ describe('Function XLOOKUP', () => { ['8604', 'Margo Hendrix', 'Sales'], ['8389', 'Dianne Pugh', 'Finance'], ['4937', 'Earlene McCarty', 'Accounting'], - ['=XLOOKUP(A1, A2:A5, B2:C5, "ID not found")'], - ], { useColumnIndex: false }) + ['=XLOOKUP(A1, A2:A5, B2:B5, "ID not found")'], + ]) expect(engine.getCellValue(adr('A6'))).toEqual('ID not found') }) @@ -120,7 +184,7 @@ describe('Function XLOOKUP', () => { ['Tax', '4246', '6211', '5346', '5298', '21100'], ['Net profit', '19342', '28295', '24352', '24134', '96124'], ['Profit %', '29.3', '27.8', '23.4', '27.6', '26.9'], - ], { useColumnIndex: false }) + ]) expect(engine.getCellValue(adr('B2'))).toEqual(25000) }) From af993655d011c6a244b9471a51a7c3f2c955eedd Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Thu, 5 Dec 2024 18:06:41 +0100 Subject: [PATCH 15/21] Fix XLOOKUP for scenario with 2D returnArray --- src/interpreter/plugin/LookupPlugin.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index 23d721296..55b1022de 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -147,19 +147,16 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech } public xlookupArraySize(ast: ProcedureAst, state: InterpreterState): ArraySize { - if (ast?.args?.length !== 5) { - return ArraySize.error() - } - const lookupRangeValue = ast?.args?.[1] as CellRange const returnRangeValue = ast?.args?.[2] as CellRange - if ([ - lookupRangeValue.start, - returnRangeValue.start, - lookupRangeValue.end, - returnRangeValue.end - ].some((val) => val === undefined)) { + if (lookupRangeValue == null + || lookupRangeValue.start == null + || lookupRangeValue.end == null + || returnRangeValue == null + || returnRangeValue.start == null + || returnRangeValue.end == null + ) { return ArraySize.error() } From 052e30c95e4d7cc8c4f9c62ae7fd677abcd48728 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Sat, 7 Dec 2024 12:20:05 +0100 Subject: [PATCH 16/21] Add unit tests for argument validation --- src/interpreter/plugin/LookupPlugin.ts | 67 +++++++++++++++----- test/interpreter/function-xlookup.spec.ts | 76 +++++++++++++++++++++-- 2 files changed, 120 insertions(+), 23 deletions(-) diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index 55b1022de..09809facc 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -160,6 +160,10 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech return ArraySize.error() } + if (!this.areRangesShapeValidForXlookup(lookupRangeValue, returnRangeValue)) { + return ArraySize.error() + } + const searchWidth = lookupRangeValue.end.col - lookupRangeValue.start.col + 1 if (returnRangeValue?.start == null || returnRangeValue?.end == null) { @@ -177,6 +181,21 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech } } + private areRangesShapeValidForXlookup(lookupRange: CellRange, returnRange: CellRange): boolean { + const isVerticalSearch = lookupRange.start.col === lookupRange.end.col && lookupRange.start.row <= lookupRange.end.row + const isHorizontalSearch = lookupRange.start.row === lookupRange.end.row && lookupRange.start.col <= lookupRange.end.col + + if (isVerticalSearch) { + return lookupRange.end.row - lookupRange.start.row === returnRange.end.row - returnRange.start.row + } + + if (isHorizontalSearch) { + return lookupRange.end.col - lookupRange.start.col === returnRange.end.col - returnRange.start.col + } + + return false + } + public match(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.runFunction(ast.args, state, this.metadata('MATCH'), (key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, type: number) => { return this.doMatch(zeroIfEmpty(key), rangeValue, type) @@ -254,40 +273,54 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech return value } - private doXlookup(key: RawNoErrorScalarValue, lookupAbsRangeValue: SimpleRangeValue, absReturnRangeValue: SimpleRangeValue, ifNotFound: any, matchMode: number, searchMode: number): InterpreterValue { - const lookupAbsRange = lookupAbsRangeValue.range - const absReturnRange = absReturnRangeValue.range - if (lookupAbsRange === undefined || absReturnRange === undefined) { + private doXlookup(key: RawNoErrorScalarValue, lookupRangeValue: SimpleRangeValue, returnRangeValue: SimpleRangeValue, ifNotFound: any, matchMode: number, searchMode: number): InterpreterValue { + const lookupRange = lookupRangeValue.range + const returnRange = returnRangeValue.range + + // handle single cell ranges + + if (lookupRange === undefined || returnRange === undefined) { return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) } - if (lookupAbsRange.start.col === lookupAbsRange.end.col && lookupAbsRange.start.row <= lookupAbsRange.end.row) { - // single column - const searchedRange = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(lookupAbsRange.start, 1, lookupAbsRange.height()), this.dependencyGraph) + const isVerticalSearch = lookupRange.start.col === lookupRange.end.col && lookupRange.start.row <= lookupRange.end.row + const isHorizontalSearch = lookupRange.start.row === lookupRange.end.row && lookupRange.start.col <= lookupRange.end.col + + if (isVerticalSearch) { + if(lookupRange.height() !== returnRange.height()) { + return new CellError(ErrorType.VALUE, ErrorMessage.WrongDimension) + } + + const searchedRange = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(lookupRange.start, 1, lookupRange.height()), this.dependencyGraph) const rowIndex = this.searchInRange(key, searchedRange, false, this.columnSearch) if (rowIndex === -1) { return (ifNotFound == ErrorType.NA) ? new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) : ifNotFound } - const topLeft = { sheet: absReturnRange.sheet, col: absReturnRange.start.col, row: absReturnRange.start.row + rowIndex } - const width = absReturnRange.end.col - absReturnRange.start.col + 1 + const topLeft = { sheet: returnRange.sheet, col: returnRange.start.col, row: returnRange.start.row + rowIndex } + const width = returnRange.end.col - returnRange.start.col + 1 return SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(topLeft, width, 1), this.dependencyGraph) - } else if (lookupAbsRange.start.row === lookupAbsRange.end.row && lookupAbsRange.start.col <= lookupAbsRange.end.col) { - // single row - const searchedRange = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(lookupAbsRange.start, lookupAbsRange.width(), 1), this.dependencyGraph) + } + + if (isHorizontalSearch) { + if(lookupRange.width() !== returnRange.width()) { + return new CellError(ErrorType.VALUE, ErrorMessage.WrongDimension) + } + + const searchedRange = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(lookupRange.start, lookupRange.width(), 1), this.dependencyGraph) const colIndex = this.searchInRange(key, searchedRange, false, this.rowSearch) if (colIndex === -1) { return (ifNotFound == ErrorType.NA) ? new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) : ifNotFound } - const topLeft = { sheet: absReturnRange.sheet, col: absReturnRange.start.col + colIndex, row: absReturnRange.start.row } - const height = absReturnRange.end.row - absReturnRange.start.row + 1 + const topLeft = { sheet: returnRange.sheet, col: returnRange.start.col + colIndex, row: returnRange.start.row } + const height = returnRange.end.row - returnRange.start.row + 1 return SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(topLeft, 1, height), this.dependencyGraph) - } else { - // multiple rows and tables - not supported - return new CellError(ErrorType.VALUE, ErrorMessage.CellRangeExpected) } + + + return new CellError(ErrorType.VALUE, ErrorMessage.WrongDimension) } diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 9dba56a21..2bc97be50 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -49,13 +49,29 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A4'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) }) - // it('returns error when lookupArray and returnArray are not of the same shape', () => { - // const engine = HyperFormula.buildFromArray([ - // ['=XLOOKUP(1, B1:B2, C1:C3)'], - // ]) + it('returns error when shapes of lookupArray and returnArray are incompatible', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(1, B1:B10, C1:C9)'], // returnArray too short + ['=XLOOKUP(1, B1:B10, C1:C11)'], // returnArray too long + ['=XLOOKUP(1, B1:B10, C1:D5)'], // returnArray too short + ['=XLOOKUP(1, B1:E1, B2:D2)'], // returnArray too short + ['=XLOOKUP(1, B1:E1, B2:F2)'], // returnArray too long + ['=XLOOKUP(1, B1:E1, B2:C3)'], // returnArray too short + ['=XLOOKUP(1, B1:B3, C1:E1)'], // transposed + ['=XLOOKUP(1, C1:E1, B1:B3)'], // transposed + ['=XLOOKUP(1, B1:C2, D3:E4)'], // lookupArray: 2d range + ]) - // expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) - // }) + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongDimension)) + expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongDimension)) + expect(engine.getCellValue(adr('A3'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongDimension)) + expect(engine.getCellValue(adr('A4'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongDimension)) + expect(engine.getCellValue(adr('A5'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongDimension)) + expect(engine.getCellValue(adr('A6'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongDimension)) + expect(engine.getCellValue(adr('A7'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongDimension)) + expect(engine.getCellValue(adr('A8'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongDimension)) + expect(engine.getCellValue(adr('A9'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongDimension)) + }) it('returns error when matchMode is of wrong type', () => { const engine = HyperFormula.buildFromArray([ @@ -109,6 +125,54 @@ describe('Function XLOOKUP', () => { // sorted row, NA if not found // unsorted column, NA if not found // unsorted row, NA if not found + + it('works when returnArray is shifted (verical search)', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(2, B1:B3, C11:C13)', 1], + ['', 2], + ['', 3], + [], + [], + [], + [], + [], + [], + [], + ['', '', 'a'], + ['', '', 'b'], + ['', '', 'c'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual('b') + }) + + it('works when returnArray is shifted (horizontal search)', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(2, B1:D1, C2:E2)', '1', '2', '3'], + ['', '', 'a', 'b', 'c'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual('b') + }) + + // TODO + xit('works when lookupArray is a single cell', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(1, B1:B1, C1:C1)', 1, 'a', 'horizontal'], // lookupArray: single cell, returnArray: single cell + ['=XLOOKUP(1, B1, C1:C1)', '', 'vertical'], // lookupArray: single cell, returnArray: single cell + ['=XLOOKUP(1, B1:B1, C1:C2)'], // lookupArray: single cell, returnArray: vertical range + ['=XLOOKUP(1, B1, C1:C2)'], // lookupArray: single cell, returnArray: vertical range + ['=XLOOKUP(1, B1:B1, C1:D1)'], // lookupArray: single cell, returnArray: horizontal range + ['=XLOOKUP(1, B1, C1:D1)'], // lookupArray: single cell, returnArray: horizontal range + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + expect(engine.getCellValue(adr('A2'))).toEqual(2) + expect(engine.getCellValue(adr('A3'))).toEqual([['a'], ['vertical']]) + expect(engine.getCellValue(adr('A4'))).toEqual([['a'], ['vertical']]) + expect(engine.getCellValue(adr('A5'))).toEqual(['a', 'horizontal']) + expect(engine.getCellValue(adr('A6'))).toEqual(['a', 'horizontal']) + }) }) // different modes From e1cd83b6210ce15ee58d78842e1907f064a5fbcd Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Sat, 7 Dec 2024 14:43:32 +0100 Subject: [PATCH 17/21] Add official Exel example 4 --- test/interpreter/function-xlookup.spec.ts | 75 ++++++++++++++++++++--- 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 2bc97be50..035d3a3a6 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -121,10 +121,55 @@ describe('Function XLOOKUP', () => { }) describe('looks up values', () => { - // sorted column, NA if not found - // sorted row, NA if not found - // unsorted column, NA if not found - // unsorted row, NA if not found + it('finds value in a sorted row', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(2, B1:D1, B1:D1)', 1, 2, 3], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + }) + + it('finds value in an unsorted row', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(2, B1:D1, B1:D1)', 4, 2, 3], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + }) + + it('finds value in a sorted column', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(2, B1:B3, B1:B3)', 1], + ['', 2], + ['', 3], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + }) + + it('finds value in an unsorted column', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(2, B1:B3, B1:B3)', 4], + ['', 2], + ['', 3], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + }) + + it('when key is not found, returns ifNotFound value or NA error', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(2, B1:B3, B1:B3)'], + ['=XLOOKUP(2, B1:D1, B1:D1)'], + ['=XLOOKUP(2, B1:B3, B1:B3, "not found")'], + ['=XLOOKUP(2, B1:D1, B1:D1, "not found")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.ValueNotFound)) + expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.ValueNotFound)) + expect(engine.getCellValue(adr('A3'))).toEqual("not found") + expect(engine.getCellValue(adr('A4'))).toEqual("not found") + }) it('works when returnArray is shifted (verical search)', () => { const engine = HyperFormula.buildFromArray([ @@ -155,8 +200,7 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual('b') }) - // TODO - xit('works when lookupArray is a single cell', () => { + it('works when lookupArray is a single cell', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(1, B1:B1, C1:C1)', 1, 'a', 'horizontal'], // lookupArray: single cell, returnArray: single cell ['=XLOOKUP(1, B1, C1:C1)', '', 'vertical'], // lookupArray: single cell, returnArray: single cell @@ -175,8 +219,6 @@ describe('Function XLOOKUP', () => { }) }) - // different modes - describe('acts similar to Microsoft Excel', () => { /** * Examples from @@ -234,6 +276,19 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A6'))).toEqual('ID not found') }) + xit('example 4', () => { + const engine = HyperFormula.buildFromArray([ + ['10', 'a'], + ['20', 'b'], + ['30', 'c'], + ['40', 'd'], + ['50', 'e'], + ['=XLOOKUP(25, A1:A5, B1:B5, 0, 1, 1)'], + ]) + + expect(engine.getCellValue(adr('A6'))).toEqual('c') + }) + it('nested xlookup function to perform both a vertical and horizontal match (official example 5)', () => { const engine = HyperFormula.buildFromArray([ ['Quarter', 'Gross profit', 'Net profit', 'Profit %'], @@ -254,3 +309,7 @@ describe('Function XLOOKUP', () => { }) }) }) + +// TODO: +// - single cell +// - modes \ No newline at end of file From 3135c288904d89227d2d12465bcee5742a3e273b Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Sun, 8 Dec 2024 14:53:24 +0100 Subject: [PATCH 18/21] Implement XLOOKUP for single-cell ranges --- src/interpreter/plugin/LookupPlugin.ts | 58 ++++++----------------- test/interpreter/function-xlookup.spec.ts | 51 +++++++------------- 2 files changed, 32 insertions(+), 77 deletions(-) diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index 09809facc..6a473d1f4 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -142,7 +142,10 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech return new CellError(ErrorType.VALUE, ErrorMessage.BadMode) } - return this.doXlookup(zeroIfEmpty(key), lookupRangeValue, returnRangeValue, ifNotFound, matchMode, searchMode) + const lookupRange = lookupRangeValue instanceof SimpleRangeValue ? lookupRangeValue : SimpleRangeValue.fromScalar(lookupRangeValue) + const returnRange = returnRangeValue instanceof SimpleRangeValue ? returnRangeValue : SimpleRangeValue.fromScalar(returnRangeValue) + + return this.doXlookup(zeroIfEmpty(key), lookupRange, returnRange, ifNotFound, matchMode, searchMode) }) } @@ -273,54 +276,23 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech return value } - private doXlookup(key: RawNoErrorScalarValue, lookupRangeValue: SimpleRangeValue, returnRangeValue: SimpleRangeValue, ifNotFound: any, matchMode: number, searchMode: number): InterpreterValue { - const lookupRange = lookupRangeValue.range - const returnRange = returnRangeValue.range - - // handle single cell ranges + private doXlookup(key: RawNoErrorScalarValue, lookupRange: SimpleRangeValue, returnRange: SimpleRangeValue, ifNotFound: any, matchMode: number, searchMode: number): InterpreterValue { + const isVerticalSearch = lookupRange.width() === 1 && returnRange.height() === lookupRange.height() + const isHorizontalSearch = lookupRange.height() === 1 && returnRange.width() === lookupRange.width() - if (lookupRange === undefined || returnRange === undefined) { - return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) + if (!isVerticalSearch && !isHorizontalSearch) { + return new CellError(ErrorType.VALUE, ErrorMessage.WrongDimension) } - const isVerticalSearch = lookupRange.start.col === lookupRange.end.col && lookupRange.start.row <= lookupRange.end.row - const isHorizontalSearch = lookupRange.start.row === lookupRange.end.row && lookupRange.start.col <= lookupRange.end.col - - if (isVerticalSearch) { - if(lookupRange.height() !== returnRange.height()) { - return new CellError(ErrorType.VALUE, ErrorMessage.WrongDimension) - } - - const searchedRange = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(lookupRange.start, 1, lookupRange.height()), this.dependencyGraph) - const rowIndex = this.searchInRange(key, searchedRange, false, this.columnSearch) - if (rowIndex === -1) { - return (ifNotFound == ErrorType.NA) ? new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) : ifNotFound - } - const topLeft = { sheet: returnRange.sheet, col: returnRange.start.col, row: returnRange.start.row + rowIndex } - const width = returnRange.end.col - returnRange.start.col + 1 - - return SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(topLeft, width, 1), this.dependencyGraph) - } - - if (isHorizontalSearch) { - if(lookupRange.width() !== returnRange.width()) { - return new CellError(ErrorType.VALUE, ErrorMessage.WrongDimension) - } - - const searchedRange = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(lookupRange.start, lookupRange.width(), 1), this.dependencyGraph) - const colIndex = this.searchInRange(key, searchedRange, false, this.rowSearch) - - if (colIndex === -1) { - return (ifNotFound == ErrorType.NA) ? new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) : ifNotFound - } + const searchStrategy = isVerticalSearch ? this.columnSearch : this.rowSearch + const indexFound = this.searchInRange(key, lookupRange, false, searchStrategy) - const topLeft = { sheet: returnRange.sheet, col: returnRange.start.col + colIndex, row: returnRange.start.row } - const height = returnRange.end.row - returnRange.start.row + 1 - return SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(topLeft, 1, height), this.dependencyGraph) + if (indexFound === -1) { + return (ifNotFound == ErrorType.NA) ? new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) : ifNotFound } - - return new CellError(ErrorType.VALUE, ErrorMessage.WrongDimension) + const returnValues: InternalScalarValue[][] = isVerticalSearch ? [returnRange.data[indexFound]] : returnRange.data.map((row) => [row[indexFound]]) + return SimpleRangeValue.onlyValues(returnValues) } diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 035d3a3a6..3aa5ab497 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -21,34 +21,6 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.WrongArgNumber)) }) - it('returns error when lookupArray is not a range', () => { - const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(1, B1, C1:C1)'], - ['=XLOOKUP(1, 42, C1:C1)'], - ['=XLOOKUP(1, "string", C1:C1)'], - ['=XLOOKUP(1, TRUE(), C1:C1)'], - ]) - - expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) - expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) - expect(engine.getCellValue(adr('A3'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) - expect(engine.getCellValue(adr('A4'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) - }) - - it('returns error when returnArray is not a range', () => { - const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(1, C1:C1, B1)'], - ['=XLOOKUP(1, C1:C1, 42)'], - ['=XLOOKUP(1, C1:C1, "string")'], - ['=XLOOKUP(1, C1:C1, TRUE())'], - ]) - - expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) - expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) - expect(engine.getCellValue(adr('A3'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) - expect(engine.getCellValue(adr('A4'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.WrongType)) - }) - it('returns error when shapes of lookupArray and returnArray are incompatible', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(1, B1:B10, C1:C9)'], // returnArray too short @@ -205,17 +177,28 @@ describe('Function XLOOKUP', () => { ['=XLOOKUP(1, B1:B1, C1:C1)', 1, 'a', 'horizontal'], // lookupArray: single cell, returnArray: single cell ['=XLOOKUP(1, B1, C1:C1)', '', 'vertical'], // lookupArray: single cell, returnArray: single cell ['=XLOOKUP(1, B1:B1, C1:C2)'], // lookupArray: single cell, returnArray: vertical range + [], ['=XLOOKUP(1, B1, C1:C2)'], // lookupArray: single cell, returnArray: vertical range + [], ['=XLOOKUP(1, B1:B1, C1:D1)'], // lookupArray: single cell, returnArray: horizontal range ['=XLOOKUP(1, B1, C1:D1)'], // lookupArray: single cell, returnArray: horizontal range ]) - expect(engine.getCellValue(adr('A1'))).toEqual(2) - expect(engine.getCellValue(adr('A2'))).toEqual(2) - expect(engine.getCellValue(adr('A3'))).toEqual([['a'], ['vertical']]) - expect(engine.getCellValue(adr('A4'))).toEqual([['a'], ['vertical']]) - expect(engine.getCellValue(adr('A5'))).toEqual(['a', 'horizontal']) - expect(engine.getCellValue(adr('A6'))).toEqual(['a', 'horizontal']) + expect(engine.getCellValue(adr('A1'))).toEqual('a') + expect(engine.getCellValue(adr('A2'))).toEqual('a') + expect(engine.getRangeValues(AbsoluteCellRange.spanFrom(adr('A3'), 1, 2))).toEqual([['a'], ['vertical']]) + expect(engine.getRangeValues(AbsoluteCellRange.spanFrom(adr('A5'), 1, 2))).toEqual([['a'], ['vertical']]) + expect(engine.getRangeValues(AbsoluteCellRange.spanFrom(adr('A7'), 2, 1))).toEqual([['a', 'horizontal']]) + expect(engine.getRangeValues(AbsoluteCellRange.spanFrom(adr('A8'), 2, 1))).toEqual([['a', 'horizontal']]) + }) + + it('finds an empty cell', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("", B1:D1, B2:D2)', 1, 2, ''], + ['', 'a', 'b', 'c'] + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual('c') }) }) From 4a15b9508ca64ed5eb2427d259a15730b1421946 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Mon, 9 Dec 2024 13:46:05 +0100 Subject: [PATCH 19/21] Refactor xlookupArraySize function --- src/interpreter/plugin/LookupPlugin.ts | 58 +++++++------------- test/interpreter/function-xlookup.spec.ts | 67 +++++++++++++++-------- 2 files changed, 65 insertions(+), 60 deletions(-) diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index 6a473d1f4..a7aa586e3 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -149,54 +149,37 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech }) } - public xlookupArraySize(ast: ProcedureAst, state: InterpreterState): ArraySize { - const lookupRangeValue = ast?.args?.[1] as CellRange - const returnRangeValue = ast?.args?.[2] as CellRange - - if (lookupRangeValue == null - || lookupRangeValue.start == null - || lookupRangeValue.end == null - || returnRangeValue == null - || returnRangeValue.start == null - || returnRangeValue.end == null - ) { - return ArraySize.error() - } + public xlookupArraySize(ast: ProcedureAst): ArraySize { + const lookupRange = ast?.args?.[1] as CellRange + const returnRange = ast?.args?.[2] as CellRange + + // co tu wpada jesli argumenty to single-cell range? - if (!this.areRangesShapeValidForXlookup(lookupRangeValue, returnRangeValue)) { + if (lookupRange?.start == null + || lookupRange?.end == null + || returnRange?.start == null + || returnRange?.end == null + ) { return ArraySize.error() } - const searchWidth = lookupRangeValue.end.col - lookupRangeValue.start.col + 1 + const lookupRangeHeight = lookupRange.end.row - lookupRange.start.row + 1 + const lookupRangeWidth = lookupRange.end.col - lookupRange.start.col + 1 + const returnRangeHeight = returnRange.end.row - returnRange.start.row + 1 + const returnRangeWidth = returnRange.end.col - returnRange.start.col + 1 - if (returnRangeValue?.start == null || returnRangeValue?.end == null) { - return ArraySize.scalar() - } + const isVerticalSearch = lookupRangeWidth === 1 && returnRangeHeight === lookupRangeHeight + const isHorizontalSearch = lookupRangeHeight === 1 && returnRangeWidth === lookupRangeWidth - if (searchWidth === 1) { - // column search - const outputWidth = returnRangeValue.end.col - returnRangeValue.start.col + 1 - return new ArraySize(outputWidth, 1) - } else { - // row search - const outputHeight = returnRangeValue.end.row - returnRangeValue.start.row + 1 - return new ArraySize(1, outputHeight) + if (!isVerticalSearch && !isHorizontalSearch) { + return ArraySize.error() } - } - - private areRangesShapeValidForXlookup(lookupRange: CellRange, returnRange: CellRange): boolean { - const isVerticalSearch = lookupRange.start.col === lookupRange.end.col && lookupRange.start.row <= lookupRange.end.row - const isHorizontalSearch = lookupRange.start.row === lookupRange.end.row && lookupRange.start.col <= lookupRange.end.col if (isVerticalSearch) { - return lookupRange.end.row - lookupRange.start.row === returnRange.end.row - returnRange.start.row + return new ArraySize(returnRangeWidth, 1) } - if (isHorizontalSearch) { - return lookupRange.end.col - lookupRange.start.col === returnRange.end.col - returnRange.start.col - } - - return false + return new ArraySize(1, returnRangeHeight) } public match(ast: ProcedureAst, state: InterpreterState): InterpreterValue { @@ -295,7 +278,6 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech return SimpleRangeValue.onlyValues(returnValues) } - private doMatch(key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, type: number): InternalScalarValue { if (![-1, 0, 1].includes(type)) { return new CellError(ErrorType.VALUE, ErrorMessage.BadMode) diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 3aa5ab497..8b0c4a206 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -139,8 +139,8 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.ValueNotFound)) expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.ValueNotFound)) - expect(engine.getCellValue(adr('A3'))).toEqual("not found") - expect(engine.getCellValue(adr('A4'))).toEqual("not found") + expect(engine.getCellValue(adr('A3'))).toEqual('not found') + expect(engine.getCellValue(adr('A4'))).toEqual('not found') }) it('works when returnArray is shifted (verical search)', () => { @@ -172,24 +172,45 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual('b') }) - it('works when lookupArray is a single cell', () => { - const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(1, B1:B1, C1:C1)', 1, 'a', 'horizontal'], // lookupArray: single cell, returnArray: single cell - ['=XLOOKUP(1, B1, C1:C1)', '', 'vertical'], // lookupArray: single cell, returnArray: single cell - ['=XLOOKUP(1, B1:B1, C1:C2)'], // lookupArray: single cell, returnArray: vertical range - [], - ['=XLOOKUP(1, B1, C1:C2)'], // lookupArray: single cell, returnArray: vertical range - [], - ['=XLOOKUP(1, B1:B1, C1:D1)'], // lookupArray: single cell, returnArray: horizontal range - ['=XLOOKUP(1, B1, C1:D1)'], // lookupArray: single cell, returnArray: horizontal range - ]) - - expect(engine.getCellValue(adr('A1'))).toEqual('a') - expect(engine.getCellValue(adr('A2'))).toEqual('a') - expect(engine.getRangeValues(AbsoluteCellRange.spanFrom(adr('A3'), 1, 2))).toEqual([['a'], ['vertical']]) - expect(engine.getRangeValues(AbsoluteCellRange.spanFrom(adr('A5'), 1, 2))).toEqual([['a'], ['vertical']]) - expect(engine.getRangeValues(AbsoluteCellRange.spanFrom(adr('A7'), 2, 1))).toEqual([['a', 'horizontal']]) - expect(engine.getRangeValues(AbsoluteCellRange.spanFrom(adr('A8'), 2, 1))).toEqual([['a', 'horizontal']]) + describe('when lookupArray is a single cell', () => { + it('works when returnArray is also a single cell', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(1, B1:B1, C1:C1)', 1, 'a'], + ['=XLOOKUP(1, B1, C1:C1)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual('a') + expect(engine.getCellValue(adr('A2'))).toEqual('a') + }) + + it('works when returnArray is a vertical range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(1, B1:B1, A5:A6)', 1], + [], + ['=XLOOKUP(1, B1, A5:A6)'], + [], + ['b'], + ['c'] + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual('b') + expect(engine.getCellValue(adr('A2'))).toEqual('c') + expect(engine.getCellValue(adr('A3'))).toEqual('b') + expect(engine.getCellValue(adr('A4'))).toEqual('c') + }) + + it('works when returnArray is a horizontal range', () => { + const engine = HyperFormula.buildFromArray([ + [1, 'b', 'c'], + ['=XLOOKUP(1, A1:A1, B1:C1)'], + ['=XLOOKUP(1, A1, B1:C1)'], + ]) + + expect(engine.getCellValue(adr('A2'))).toEqual('b') + expect(engine.getCellValue(adr('B2'))).toEqual('c') + expect(engine.getCellValue(adr('A3'))).toEqual('b') + expect(engine.getCellValue(adr('B3'))).toEqual('c') + }) }) it('finds an empty cell', () => { @@ -294,5 +315,7 @@ describe('Function XLOOKUP', () => { }) // TODO: -// - single cell -// - modes \ No newline at end of file +// - debugger +// - review arraysize function +// - fix single cell +// - implementmodes From 7cd0a99f2581e73addda637ac9ea4727375d5f55 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Mon, 9 Dec 2024 14:33:38 +0100 Subject: [PATCH 20/21] Fix the single-cell ranges tests --- src/interpreter/plugin/LookupPlugin.ts | 5 +---- test/interpreter/function-xlookup.spec.ts | 21 +++++---------------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index a7aa586e3..d02433fc0 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -26,7 +26,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech { argumentType: FunctionArgumentType.RANGE }, { argumentType: FunctionArgumentType.NUMBER }, { argumentType: FunctionArgumentType.BOOLEAN, defaultValue: true }, - ] + ], }, 'HLOOKUP': { method: 'hlookup', @@ -40,7 +40,6 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech 'XLOOKUP': { method: 'xlookup', arraySizeMethod: 'xlookupArraySize', - vectorizationForbidden: true, parameters: [ // lookup_value { argumentType: FunctionArgumentType.NOERROR }, @@ -153,8 +152,6 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech const lookupRange = ast?.args?.[1] as CellRange const returnRange = ast?.args?.[2] as CellRange - // co tu wpada jesli argumenty to single-cell range? - if (lookupRange?.start == null || lookupRange?.end == null || returnRange?.start == null diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 8b0c4a206..a56c709f7 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -172,22 +172,18 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual('b') }) - describe('when lookupArray is a single cell', () => { - it('works when returnArray is also a single cell', () => { + describe('when lookupArray is a single-cell range', () => { + it('works when returnArray is also a single-cell range', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(1, B1:B1, C1:C1)', 1, 'a'], - ['=XLOOKUP(1, B1, C1:C1)'], ]) expect(engine.getCellValue(adr('A1'))).toEqual('a') - expect(engine.getCellValue(adr('A2'))).toEqual('a') }) it('works when returnArray is a vertical range', () => { const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(1, B1:B1, A5:A6)', 1], - [], - ['=XLOOKUP(1, B1, A5:A6)'], + ['=XLOOKUP(1, B1:B1, A3:A4)', 1], [], ['b'], ['c'] @@ -195,21 +191,16 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual('b') expect(engine.getCellValue(adr('A2'))).toEqual('c') - expect(engine.getCellValue(adr('A3'))).toEqual('b') - expect(engine.getCellValue(adr('A4'))).toEqual('c') }) it('works when returnArray is a horizontal range', () => { const engine = HyperFormula.buildFromArray([ [1, 'b', 'c'], ['=XLOOKUP(1, A1:A1, B1:C1)'], - ['=XLOOKUP(1, A1, B1:C1)'], ]) expect(engine.getCellValue(adr('A2'))).toEqual('b') expect(engine.getCellValue(adr('B2'))).toEqual('c') - expect(engine.getCellValue(adr('A3'))).toEqual('b') - expect(engine.getCellValue(adr('B3'))).toEqual('c') }) }) @@ -280,7 +271,7 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A6'))).toEqual('ID not found') }) - xit('example 4', () => { + it('example 4', () => { const engine = HyperFormula.buildFromArray([ ['10', 'a'], ['20', 'b'], @@ -315,7 +306,5 @@ describe('Function XLOOKUP', () => { }) // TODO: +// - implement modes // - debugger -// - review arraysize function -// - fix single cell -// - implementmodes From bfac2965a8aa851fd1777f72203c19a49e3a54c7 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Mon, 9 Dec 2024 16:05:58 +0100 Subject: [PATCH 21/21] Add unit tests for non-default searchMode --- src/interpreter/plugin/LookupPlugin.ts | 2 + test/interpreter/function-xlookup.spec.ts | 94 ++++++++++++++++++++++- 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index d02433fc0..6f493e702 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -186,6 +186,8 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech } protected searchInRange(key: RawNoErrorScalarValue, range: SimpleRangeValue, sorted: boolean, searchStrategy: SearchStrategy): number { + // for sorted option: use findInOrderedArray + if (!sorted && typeof key === 'string' && this.arithmeticHelper.requiresRegex(key)) { return searchStrategy.advancedFind( this.arithmeticHelper.eqMatcherFunction(key), diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index a56c709f7..6bc7b15f8 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -92,7 +92,7 @@ describe('Function XLOOKUP', () => { }) }) - describe('looks up values', () => { + describe('with default matchMode and searchMode', () => { it('finds value in a sorted row', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(2, B1:D1, B1:D1)', 1, 2, 3], @@ -214,6 +214,98 @@ describe('Function XLOOKUP', () => { }) }) + describe('when provided with searchMode = ', () => { + it('1, finds the first match in unsorted horizontal range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(1, A2:E2, A3:E3, "NotFound", 0, 1)'], + [2, 1, 3, 1, 4], + [1, 2, 3, 4, 5], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + }) + + it('1, finds the first match in unsorted vertical range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(1, A2:A6, B2:B6, "NotFound", 0, 1)'], + [2, 1], + [1, 2], + [3, 3], + [1, 4], + [4, 5] + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + }) + + it('-1, finds the last match in unsorted horizontal range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(1, A2:E2, A3:E3, "NotFound", 0, -1)'], + [2, 1, 3, 1, 4], + [1, 2, 3, 4, 5], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual(4) + }) + + it('-1, finds the last match in unsorted vertical range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(1, A2:A6, B2:B6, "NotFound", 0, -1)'], + [2, 1], + [1, 2], + [3, 3], + [1, 4], + [4, 5] + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual(4) + }) + + it('2, finds the value in horizontal range sorted ascending', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(2, A2:E2, A2:E2, "NotFound", 0, 2)'], + [1, 2, 2, 5, 5], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + }) + + it('2, finds the value in vertical range sorted ascending', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(2, A2:A6, A2:A6, "NotFound", 0, 2)'], + [1], + [2], + [2], + [5], + [5], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + }) + + it('-2, finds the value in horizontal range sorted descending', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(2, A2:E2, A2:E2, "NotFound", 0, -2)'], + [5, 2, 2, 1, 1], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + }) + + it('-2, finds the value in vertical range sorted descending', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(2, A2:A6, A2:A6, "NotFound", 0, -2)'], + [5], + [2], + [2], + [1], + [1], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + }) + }) + describe('acts similar to Microsoft Excel', () => { /** * Examples from