From 932e8c0fd80c0173f8eeb52774e1761fc6ced0c7 Mon Sep 17 00:00:00 2001 From: Selim Youssry Date: Sat, 15 Jun 2024 13:39:11 +0000 Subject: [PATCH 01/45] 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 7fe8943b57..26f51ace0c 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/45] 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 1ff9896484..c96292badd 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 54fd85082f..36406c12d5 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 354e0fa1fe..1c4937f0d1 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 2f01c82714..1572200d28 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 7b31d38456..e287a751f3 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 b7fdc7735d..b851a3e890 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 d3721471db..82a2de12fd 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 8bc529d76c..538795a7ea 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 43950a0ef5..40972ee0fe 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 2df0ca0b8a..480af8bca1 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 14800e6c59..d1434094f0 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 8ce39a6d91..706db6caaa 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 2c41911a90..73786288bb 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 9f4fbc3cd0..a534784536 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 bd49af1397..d6276be2d8 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 f6f2ca32bb..2c7e8b285b 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/45] 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 0000000000..af14b84689 --- /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 03617b9876..877cae7266 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 0000000000..7d97c2d94b --- /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/45] 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 af14b84689..40713fdc44 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 7d97c2d94b..00c401f4d3 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/45] 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 0b33a89cf9..e48e1bec92 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 40713fdc44..0000000000 --- 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 877cae7266..03617b9876 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 00c401f4d3..a269828ac3 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/45] 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 e48e1bec92..de0af8dbe3 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 a269828ac3..56fafd1619 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/45] 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 de0af8dbe3..c757ad9cb5 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 56fafd1619..a92b456810 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/45] 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 26f51ace0c..37afeea2fd 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 c757ad9cb5..b160d269b7 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/45] 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 b160d269b7..5a9a19e9ce 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/45] 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 0000000000..824fd929b4 --- /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/45] 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 824fd929b4..a35cced321 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/45] 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 5a9a19e9ce..a667a37403 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 cdf9948f97..0f90a96647 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/45] 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 a667a37403..38e077efe7 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/45] 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 38e077efe7..23d721296f 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 0f90a96647..9dba56a212 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/45] 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 23d721296f..55b1022de4 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/45] 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 55b1022de4..09809facc2 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 9dba56a212..2bc97be502 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/45] 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 2bc97be502..035d3a3a62 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/45] 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 09809facc2..6a473d1f4a 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 035d3a3a62..3aa5ab497f 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/45] 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 6a473d1f4a..a7aa586e34 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 3aa5ab497f..8b0c4a2068 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/45] 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 a7aa586e34..d02433fc06 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 8b0c4a2068..a56c709f70 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/45] 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 d02433fc06..6f493e7029 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 a56c709f70..6bc7b15f85 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 From d045b8199887fec404df3695820803648ca68b6d Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Tue, 17 Dec 2024 19:36:49 +0100 Subject: [PATCH 22/45] Make XLOOKUP handle sorted ranges --- src/interpreter/plugin/LookupPlugin.ts | 33 ++++++++++------------- test/interpreter/function-xlookup.spec.ts | 1 + 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index 6f493e7029..c69a82dcd3 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -86,7 +86,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech return new CellError(ErrorType.REF, ErrorMessage.IndexLarge) } - return this.doVlookup(zeroIfEmpty(key), rangeValue, index - 1, sorted) + return this.doVlookup(zeroIfEmpty(key), rangeValue, index - 1, { ordering: sorted ? 'asc' : 'none' }) }) } @@ -109,7 +109,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech return new CellError(ErrorType.REF, ErrorMessage.IndexLarge) } - return this.doHlookup(zeroIfEmpty(key), rangeValue, index - 1, sorted) + return this.doHlookup(zeroIfEmpty(key), rangeValue, index - 1, { ordering: sorted ? 'asc' : 'none' }) }) } @@ -135,16 +135,11 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech return new CellError(ErrorType.VALUE, ErrorMessage.BadMode) } - if (searchMode !== 1) { - // not supported yet - // TODO: Implement search mode - return new CellError(ErrorType.VALUE, ErrorMessage.BadMode) - } - const lookupRange = lookupRangeValue instanceof SimpleRangeValue ? lookupRangeValue : SimpleRangeValue.fromScalar(lookupRangeValue) const returnRange = returnRangeValue instanceof SimpleRangeValue ? returnRangeValue : SimpleRangeValue.fromScalar(returnRangeValue) + const searchOptions: SearchOptions = { ordering: searchMode === 2 ? 'asc' : searchMode === -2 ? 'desc' : 'none' } - return this.doXlookup(zeroIfEmpty(key), lookupRange, returnRange, ifNotFound, matchMode, searchMode) + return this.doXlookup(zeroIfEmpty(key), lookupRange, returnRange, ifNotFound, matchMode, searchOptions) }) } @@ -185,21 +180,21 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech }) } - protected searchInRange(key: RawNoErrorScalarValue, range: SimpleRangeValue, sorted: boolean, searchStrategy: SearchStrategy): number { + protected searchInRange(key: RawNoErrorScalarValue, range: SimpleRangeValue, searchOptions: SearchOptions, searchStrategy: SearchStrategy): number { // for sorted option: use findInOrderedArray - if (!sorted && typeof key === 'string' && this.arithmeticHelper.requiresRegex(key)) { + if (searchOptions.ordering === 'none' && typeof key === 'string' && this.arithmeticHelper.requiresRegex(key)) { return searchStrategy.advancedFind( this.arithmeticHelper.eqMatcherFunction(key), range ) } else { - const searchOptions: SearchOptions = sorted ? { ordering: 'asc' } : { ordering: 'none', matchExactly: true } - return searchStrategy.find(key, range, searchOptions) + const newSearchOptions: SearchOptions = searchOptions.ordering === 'none' ? { ordering: 'none', matchExactly: true } : searchOptions + return searchStrategy.find(key, range, newSearchOptions) } } - private doVlookup(key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, index: number, sorted: boolean): InternalScalarValue { + private doVlookup(key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, index: number, searchOptions: SearchOptions): InternalScalarValue { this.dependencyGraph.stats.start(StatType.VLOOKUP) const range = rangeValue.range let searchedRange @@ -208,7 +203,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech } else { searchedRange = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(range.start, 1, range.height()), this.dependencyGraph) } - const rowIndex = this.searchInRange(key, searchedRange, sorted, this.columnSearch) + const rowIndex = this.searchInRange(key, searchedRange, searchOptions, this.columnSearch) this.dependencyGraph.stats.end(StatType.VLOOKUP) @@ -230,7 +225,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech return value } - private doHlookup(key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, index: number, sorted: boolean): InternalScalarValue { + private doHlookup(key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, index: number, searchOptions: SearchOptions): InternalScalarValue { const range = rangeValue.range let searchedRange if (range === undefined) { @@ -238,7 +233,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech } else { searchedRange = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(range.start, range.width(), 1), this.dependencyGraph) } - const colIndex = this.searchInRange(key, searchedRange, sorted, this.rowSearch) + const colIndex = this.searchInRange(key, searchedRange, searchOptions, this.rowSearch) if (colIndex === -1) { return new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) @@ -258,7 +253,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech return value } - private doXlookup(key: RawNoErrorScalarValue, lookupRange: SimpleRangeValue, returnRange: SimpleRangeValue, ifNotFound: any, matchMode: number, searchMode: number): InterpreterValue { + private doXlookup(key: RawNoErrorScalarValue, lookupRange: SimpleRangeValue, returnRange: SimpleRangeValue, ifNotFound: any, matchMode: number, searchOptions: SearchOptions): InterpreterValue { const isVerticalSearch = lookupRange.width() === 1 && returnRange.height() === lookupRange.height() const isHorizontalSearch = lookupRange.height() === 1 && returnRange.width() === lookupRange.width() @@ -267,7 +262,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech } const searchStrategy = isVerticalSearch ? this.columnSearch : this.rowSearch - const indexFound = this.searchInRange(key, lookupRange, false, searchStrategy) + const indexFound = this.searchInRange(key, lookupRange, searchOptions, searchStrategy) if (indexFound === -1) { return (ifNotFound == ErrorType.NA) ? new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) : ifNotFound diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 6bc7b15f85..3241f13730 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -398,5 +398,6 @@ describe('Function XLOOKUP', () => { }) // TODO: +// - test with different row/column search strategies (columnindex etc) // - implement modes // - debugger From 19a4d8e64382dcf3be6638bf56e68ab727a34cc9 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Wed, 18 Dec 2024 17:20:51 +0100 Subject: [PATCH 23/45] Return NotFound if there is no match in sprted ranges --- src/interpreter/plugin/LookupPlugin.ts | 4 +- test/interpreter/function-xlookup.spec.ts | 53 +++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index c69a82dcd3..971cbbfc7d 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -125,7 +125,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech return new CellError(ErrorType.VALUE, ErrorMessage.BadMode) } - if (![1, -1, 1, 2].includes(searchMode)) { + if (![1, -1, 2, -2].includes(searchMode)) { return new CellError(ErrorType.VALUE, ErrorMessage.BadMode) } @@ -137,7 +137,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech const lookupRange = lookupRangeValue instanceof SimpleRangeValue ? lookupRangeValue : SimpleRangeValue.fromScalar(lookupRangeValue) const returnRange = returnRangeValue instanceof SimpleRangeValue ? returnRangeValue : SimpleRangeValue.fromScalar(returnRangeValue) - const searchOptions: SearchOptions = { ordering: searchMode === 2 ? 'asc' : searchMode === -2 ? 'desc' : 'none' } + const searchOptions: SearchOptions = { ordering: searchMode === 2 ? 'asc' : searchMode === -2 ? 'desc' : 'none', matchExactly: true } return this.doXlookup(zeroIfEmpty(key), lookupRange, returnRange, ifNotFound, matchMode, searchOptions) }) diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 3241f13730..77785f8256 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -238,6 +238,19 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual(2) }) + it('1, returns "NotFound" if there is no match', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(5, A2:A6, B2:B6, "NotFound", 0, 1)'], + [2, 1], + [1, 2], + [3, 3], + [1, 4], + [4, 5] + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') + }) + it('-1, finds the last match in unsorted horizontal range', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(1, A2:E2, A3:E3, "NotFound", 0, -1)'], @@ -261,6 +274,19 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual(4) }) + it('-1, returns "NotFound" if there is no match', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(5, A2:A6, B2:B6, "NotFound", 0, -1)'], + [2, 1], + [1, 2], + [3, 3], + [1, 4], + [4, 5] + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') + }) + it('2, finds the value in horizontal range sorted ascending', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(2, A2:E2, A2:E2, "NotFound", 0, 2)'], @@ -283,6 +309,19 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual(2) }) + it('2, returns "NotFound" if there is no match in a range sorted ascending', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", 0, 2)'], + [1], + [2], + [2], + [5], + [5], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') + }) + it('-2, finds the value in horizontal range sorted descending', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(2, A2:E2, A2:E2, "NotFound", 0, -2)'], @@ -304,6 +343,19 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual(2) }) + + it('-2, returns "NotFound" if there is no match in a range sorted descending', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", 0, -2)'], + [5], + [2], + [2], + [1], + [1], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') + }) }) describe('acts similar to Microsoft Excel', () => { @@ -398,6 +450,7 @@ describe('Function XLOOKUP', () => { }) // TODO: +// test notfound if sorted // - test with different row/column search strategies (columnindex etc) // - implement modes // - debugger From f25b62386ebb46478c25c4b91e41185ab0742df5 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Wed, 18 Dec 2024 17:50:23 +0100 Subject: [PATCH 24/45] Finish implementing searchMode for XLOOKUP --- src/Lookup/AdvancedFind.ts | 17 +++++++++-------- src/Lookup/ColumnIndex.ts | 19 +++++++++++-------- src/Lookup/SearchStrategy.ts | 1 + src/interpreter/plugin/LookupPlugin.ts | 15 ++++++++++----- test/interpreter/function-xlookup.spec.ts | 5 ++--- 5 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/Lookup/AdvancedFind.ts b/src/Lookup/AdvancedFind.ts index 50fe3b5fdb..0443563542 100644 --- a/src/Lookup/AdvancedFind.ts +++ b/src/Lookup/AdvancedFind.ts @@ -40,16 +40,16 @@ export abstract class AdvancedFind { /* * WARNING: Finding lower/upper bounds in unordered ranges is not supported. When ordering === 'none', assumes matchExactly === true */ - protected basicFind(searchKey: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, searchCoordinate: 'col' | 'row', { ordering, matchExactly }: SearchOptions): number { + protected basicFind(searchKey: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, searchCoordinate: 'col' | 'row', { ordering, matchExactly, returnOccurence }: SearchOptions): number { const normalizedSearchKey = typeof searchKey === 'string' ? forceNormalizeString(searchKey) : searchKey const range = rangeValue.range if (range === undefined) { - return this.findNormalizedValue(normalizedSearchKey, rangeValue.valuesFromTopLeftCorner()) + return this.findNormalizedValue(normalizedSearchKey, rangeValue.valuesFromTopLeftCorner(), returnOccurence) } if (ordering === 'none') { - return this.findNormalizedValue(normalizedSearchKey, this.dependencyGraph.computeListOfValuesInRange(range)) + return this.findNormalizedValue(normalizedSearchKey, this.dependencyGraph.computeListOfValuesInRange(range), returnOccurence) } return findLastOccurrenceInOrderedRange( @@ -60,10 +60,11 @@ export abstract class AdvancedFind { ) } - protected findNormalizedValue(searchKey: RawNoErrorScalarValue, searchArray: InternalScalarValue[]): number { - return searchArray - .map(getRawValue) - .map(val => typeof val === 'string' ? forceNormalizeString(val) : val) - .indexOf(searchKey) + protected findNormalizedValue(searchKey: RawNoErrorScalarValue, searchArray: InternalScalarValue[], returnOccurence: 'first' | 'last' = 'first'): number { + const normalizedArray = searchArray + .map(getRawValue) + .map(val => typeof val === 'string' ? forceNormalizeString(val) : val) + + return returnOccurence === 'first' ? normalizedArray.indexOf(searchKey) : normalizedArray.lastIndexOf(searchKey) } } diff --git a/src/Lookup/ColumnIndex.ts b/src/Lookup/ColumnIndex.ts index 6082b78cac..bace0a35e6 100644 --- a/src/Lookup/ColumnIndex.ts +++ b/src/Lookup/ColumnIndex.ts @@ -110,13 +110,16 @@ export class ColumnIndex implements ColumnSearchStrategy { /* * WARNING: Finding lower/upper bounds in unordered ranges is not supported. When ordering === 'none', assumes matchExactly === true */ - public find(searchKey: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, { ordering, matchExactly }: SearchOptions): number { - const handlingDuplicates = matchExactly === true ? 'findFirst' : 'findLast' - const resultUsingColumnIndex = this.findUsingColumnIndex(searchKey, rangeValue, handlingDuplicates) - return resultUsingColumnIndex !== undefined ? resultUsingColumnIndex : this.binarySearchStrategy.find(searchKey, rangeValue, { ordering, matchExactly }) + public find(searchKey: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, { ordering, matchExactly, returnOccurence }: SearchOptions): number { + if (returnOccurence == null) { + returnOccurence = matchExactly === true ? 'first' : 'last' + } + + const resultUsingColumnIndex = this.findUsingColumnIndex(searchKey, rangeValue, returnOccurence) + return resultUsingColumnIndex !== undefined ? resultUsingColumnIndex : this.binarySearchStrategy.find(searchKey, rangeValue, { ordering, matchExactly, returnOccurence }) } - private findUsingColumnIndex(key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, handlingDuplicates: 'findFirst' | 'findLast'): Maybe { + private findUsingColumnIndex(key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, returnOccurence: 'first' | 'last'): Maybe { const range = rangeValue.range if (range === undefined) { return undefined @@ -135,15 +138,15 @@ export class ColumnIndex implements ColumnSearchStrategy { return undefined } - const rowNumber = ColumnIndex.findRowBelongingToRange(valueIndexForTheKey, range, handlingDuplicates) + const rowNumber = ColumnIndex.findRowBelongingToRange(valueIndexForTheKey, range, returnOccurence) return rowNumber !== undefined ? rowNumber - range.start.row : undefined } - private static findRowBelongingToRange(valueIndex: ValueIndex, range: AbsoluteCellRange, handlingDuplicates: 'findFirst' | 'findLast'): Maybe { + private static findRowBelongingToRange(valueIndex: ValueIndex, range: AbsoluteCellRange, returnOccurence: 'first' | 'last'): Maybe { const start = range.start.row const end = range.end.row - const positionInIndex = handlingDuplicates === 'findFirst' + const positionInIndex = returnOccurence === 'first' ? findInOrderedArray(start, valueIndex.index, 'upperBound') : findInOrderedArray(end, valueIndex.index, 'lowerBound') diff --git a/src/Lookup/SearchStrategy.ts b/src/Lookup/SearchStrategy.ts index 96202f9487..4ff4b89379 100644 --- a/src/Lookup/SearchStrategy.ts +++ b/src/Lookup/SearchStrategy.ts @@ -17,6 +17,7 @@ import {ColumnIndex} from './ColumnIndex' export interface SearchOptions { ordering: 'asc' | 'desc' | 'none', matchExactly?: boolean, + returnOccurence?: 'first' | 'last', } export interface SearchStrategy { diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index 971cbbfc7d..304d9b8524 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -137,7 +137,11 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech const lookupRange = lookupRangeValue instanceof SimpleRangeValue ? lookupRangeValue : SimpleRangeValue.fromScalar(lookupRangeValue) const returnRange = returnRangeValue instanceof SimpleRangeValue ? returnRangeValue : SimpleRangeValue.fromScalar(returnRangeValue) - const searchOptions: SearchOptions = { ordering: searchMode === 2 ? 'asc' : searchMode === -2 ? 'desc' : 'none', matchExactly: true } + const searchOptions: SearchOptions = { + ordering: searchMode === 2 ? 'asc' : searchMode === -2 ? 'desc' : 'none', + returnOccurence: searchMode === -1 ? 'last' : 'first', + matchExactly: true, + } return this.doXlookup(zeroIfEmpty(key), lookupRange, returnRange, ifNotFound, matchMode, searchOptions) }) @@ -181,16 +185,17 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech } protected searchInRange(key: RawNoErrorScalarValue, range: SimpleRangeValue, searchOptions: SearchOptions, searchStrategy: SearchStrategy): number { - // for sorted option: use findInOrderedArray - if (searchOptions.ordering === 'none' && typeof key === 'string' && this.arithmeticHelper.requiresRegex(key)) { return searchStrategy.advancedFind( this.arithmeticHelper.eqMatcherFunction(key), range ) } else { - const newSearchOptions: SearchOptions = searchOptions.ordering === 'none' ? { ordering: 'none', matchExactly: true } : searchOptions - return searchStrategy.find(key, range, newSearchOptions) + if (searchOptions.ordering === 'none') { + searchOptions.matchExactly = true; + } + + return searchStrategy.find(key, range, searchOptions) } } diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 77785f8256..6baaddc062 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -450,7 +450,6 @@ describe('Function XLOOKUP', () => { }) // TODO: -// test notfound if sorted // - test with different row/column search strategies (columnindex etc) -// - implement modes -// - debugger +// - implement matchMode +// - setup debugger \ No newline at end of file From 5c4b5a23296136528cfa969fd1ebc1818edf507a Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Wed, 18 Dec 2024 21:25:38 +0100 Subject: [PATCH 25/45] Add unit tests for ColumnIndex column search strategy --- test/interpreter/function-xlookup.spec.ts | 173 ++++++++++++++++++++-- 1 file changed, 158 insertions(+), 15 deletions(-) diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 6baaddc062..43504ab4b9 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -214,13 +214,13 @@ describe('Function XLOOKUP', () => { }) }) - describe('when provided with searchMode = ', () => { + describe('with BinarySearch column search strategy, 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], - ]) + ], { useColumnIndex: false }) expect(engine.getCellValue(adr('A1'))).toEqual(2) }) @@ -233,7 +233,7 @@ describe('Function XLOOKUP', () => { [3, 3], [1, 4], [4, 5] - ]) + ], { useColumnIndex: false }) expect(engine.getCellValue(adr('A1'))).toEqual(2) }) @@ -246,7 +246,7 @@ describe('Function XLOOKUP', () => { [3, 3], [1, 4], [4, 5] - ]) + ], { useColumnIndex: false }) expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') }) @@ -256,7 +256,7 @@ describe('Function XLOOKUP', () => { ['=XLOOKUP(1, A2:E2, A3:E3, "NotFound", 0, -1)'], [2, 1, 3, 1, 4], [1, 2, 3, 4, 5], - ]) + ], { useColumnIndex: false }) expect(engine.getCellValue(adr('A1'))).toEqual(4) }) @@ -269,7 +269,7 @@ describe('Function XLOOKUP', () => { [3, 3], [1, 4], [4, 5] - ]) + ], { useColumnIndex: false }) expect(engine.getCellValue(adr('A1'))).toEqual(4) }) @@ -282,7 +282,7 @@ describe('Function XLOOKUP', () => { [3, 3], [1, 4], [4, 5] - ]) + ], { useColumnIndex: false }) expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') }) @@ -291,7 +291,7 @@ describe('Function XLOOKUP', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(2, A2:E2, A2:E2, "NotFound", 0, 2)'], [1, 2, 2, 5, 5], - ]) + ], { useColumnIndex: false }) expect(engine.getCellValue(adr('A1'))).toEqual(2) }) @@ -304,7 +304,7 @@ describe('Function XLOOKUP', () => { [2], [5], [5], - ]) + ], { useColumnIndex: false }) expect(engine.getCellValue(adr('A1'))).toEqual(2) }) @@ -317,7 +317,7 @@ describe('Function XLOOKUP', () => { [2], [5], [5], - ]) + ], { useColumnIndex: false }) expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') }) @@ -326,7 +326,7 @@ describe('Function XLOOKUP', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(2, A2:E2, A2:E2, "NotFound", 0, -2)'], [5, 2, 2, 1, 1], - ]) + ], { useColumnIndex: false }) expect(engine.getCellValue(adr('A1'))).toEqual(2) }) @@ -339,7 +339,7 @@ describe('Function XLOOKUP', () => { [2], [1], [1], - ]) + ], { useColumnIndex: false }) expect(engine.getCellValue(adr('A1'))).toEqual(2) }) @@ -352,7 +352,151 @@ describe('Function XLOOKUP', () => { [2], [1], [1], - ]) + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') + }) + }) + + describe('with ColumnIndex column search strategy, 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], + ], { useColumnIndex: true }) + + 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] + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + }) + + it('1, returns "NotFound" if there is no match', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(5, A2:A6, B2:B6, "NotFound", 0, 1)'], + [2, 1], + [1, 2], + [3, 3], + [1, 4], + [4, 5] + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') + }) + + 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], + ], { useColumnIndex: true }) + + 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] + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(4) + }) + + it('-1, returns "NotFound" if there is no match', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(5, A2:A6, B2:B6, "NotFound", 0, -1)'], + [2, 1], + [1, 2], + [3, 3], + [1, 4], + [4, 5] + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') + }) + + 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], + ], { useColumnIndex: true }) + + 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], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + }) + + it('2, returns "NotFound" if there is no match in a range sorted ascending', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", 0, 2)'], + [1], + [2], + [2], + [5], + [5], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') + }) + + 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], + ], { useColumnIndex: true }) + + 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], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + }) + + it('-2, returns "NotFound" if there is no match in a range sorted descending', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", 0, -2)'], + [5], + [2], + [2], + [1], + [1], + ], { useColumnIndex: true }) expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') }) @@ -450,6 +594,5 @@ describe('Function XLOOKUP', () => { }) // TODO: -// - test with different row/column search strategies (columnindex etc) +// - setup debugger!!! last day of cursor // - implement matchMode -// - setup debugger \ No newline at end of file From 5829994df1327138f6d155a5500315a606a7277d Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Fri, 20 Dec 2024 18:34:27 +0100 Subject: [PATCH 26/45] Add unt tests for matchMode --- src/interpreter/plugin/LookupPlugin.ts | 6 -- test/interpreter/function-xlookup.spec.ts | 104 ++++++++++++++++++++++ 2 files changed, 104 insertions(+), 6 deletions(-) diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index 304d9b8524..5512fdcf62 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -129,12 +129,6 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech return new CellError(ErrorType.VALUE, ErrorMessage.BadMode) } - if (matchMode !== 0) { - // not supported yet - // TODO: Implement match mode - return new CellError(ErrorType.VALUE, ErrorMessage.BadMode) - } - const lookupRange = lookupRangeValue instanceof SimpleRangeValue ? lookupRangeValue : SimpleRangeValue.fromScalar(lookupRangeValue) const returnRange = returnRangeValue instanceof SimpleRangeValue ? returnRangeValue : SimpleRangeValue.fromScalar(returnRangeValue) const searchOptions: SearchOptions = { diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 43504ab4b9..b0c7e2edb8 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -502,6 +502,110 @@ describe('Function XLOOKUP', () => { }) }) + describe('with BinarySearch column search strategy, when provided with matchMode = ', () => { + describe('-1, returns a lower bound if the search key is not present', () => { + it('in unsorted horizontal range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:E2, A3:E3, "NotFound", -1, 1)'], + [2, 1, 4, 2, 5], + [1, 2, 3, 4, 5], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(1) + }) + + it('in unsorted vertical range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:A6, B2:B6, "NotFound", -1, 1)'], + [2, 1], + [1, 2], + [4, 3], + [2, 4], + [5, 5] + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(1) + }) + + it('in sorted horizontal range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:E2, A2:E2, "NotFound", -1, 2)'], + [1, 2, 2, 5, 6], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + }) + + it('in sorted vertical range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", -1, 2)'], + [1], + [2], + [4], + [5], + [6] + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + }) + }) + + describe('1, returns an upper bound if the search key is not present', () => { + it('in unsorted horizontal range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:E2, A3:E3, "NotFound", 1, 1)'], + [2, 1, 4, 4, 5], + [1, 2, 3, 4, 5], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(3) + }) + + it('in unsorted vertical range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:A6, B2:B6, "NotFound", 1, 1)'], + [2, 1], + [1, 2], + [4, 3], + [4, 4], + [5, 5] + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(3) + }) + + it('in sorted horizontal range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:E2, A2:E2, "NotFound", 1, 2)'], + [1, 2, 4, 5, 6], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(4) + }) + + it('in sorted vertical range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", 1, 2)'], + [1], + [2], + [4], + [5], + [6] + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(4) + }) + }) + + describe('2, performs a wildcard match', () => { + // TODO + }) + }) + + describe('with ColumnIndex column search strategy, when provided with matchMode = ', () => { + // TODO + }) + describe('acts similar to Microsoft Excel', () => { /** * Examples from From fd1001f7c8f6909f232fe32c43f0b7c4437efd2a Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Sun, 22 Dec 2024 17:28:20 +0100 Subject: [PATCH 27/45] Make searchStrategy.find() able to return lower/upper bound if there is no exact match --- src/Lookup/AdvancedFind.ts | 4 +- src/Lookup/ColumnIndex.ts | 6 +- src/Lookup/SearchStrategy.ts | 2 +- src/interpreter/binarySearch.ts | 17 +- src/interpreter/plugin/LookupPlugin.ts | 44 +++-- test/interpreter/binary-search.spec.ts | 22 +-- test/interpreter/function-xlookup.spec.ts | 211 +++++++++++++--------- 7 files changed, 182 insertions(+), 124 deletions(-) diff --git a/src/Lookup/AdvancedFind.ts b/src/Lookup/AdvancedFind.ts index 0443563542..09b6498708 100644 --- a/src/Lookup/AdvancedFind.ts +++ b/src/Lookup/AdvancedFind.ts @@ -40,7 +40,7 @@ export abstract class AdvancedFind { /* * WARNING: Finding lower/upper bounds in unordered ranges is not supported. When ordering === 'none', assumes matchExactly === true */ - protected basicFind(searchKey: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, searchCoordinate: 'col' | 'row', { ordering, matchExactly, returnOccurence }: SearchOptions): number { + protected basicFind(searchKey: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, searchCoordinate: 'col' | 'row', { ordering, ifNoMatch, returnOccurence }: SearchOptions): number { const normalizedSearchKey = typeof searchKey === 'string' ? forceNormalizeString(searchKey) : searchKey const range = rangeValue.range @@ -55,7 +55,7 @@ export abstract class AdvancedFind { return findLastOccurrenceInOrderedRange( normalizedSearchKey, range, - { searchCoordinate, orderingDirection: ordering, matchExactly }, + { searchCoordinate, orderingDirection: ordering, ifNoMatch }, this.dependencyGraph ) } diff --git a/src/Lookup/ColumnIndex.ts b/src/Lookup/ColumnIndex.ts index bace0a35e6..d6420d8d16 100644 --- a/src/Lookup/ColumnIndex.ts +++ b/src/Lookup/ColumnIndex.ts @@ -110,13 +110,13 @@ export class ColumnIndex implements ColumnSearchStrategy { /* * WARNING: Finding lower/upper bounds in unordered ranges is not supported. When ordering === 'none', assumes matchExactly === true */ - public find(searchKey: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, { ordering, matchExactly, returnOccurence }: SearchOptions): number { + public find(searchKey: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, { ordering, ifNoMatch, returnOccurence }: SearchOptions): number { if (returnOccurence == null) { - returnOccurence = matchExactly === true ? 'first' : 'last' + returnOccurence = ifNoMatch === 'returnNotFound' ? 'first' : 'last' } const resultUsingColumnIndex = this.findUsingColumnIndex(searchKey, rangeValue, returnOccurence) - return resultUsingColumnIndex !== undefined ? resultUsingColumnIndex : this.binarySearchStrategy.find(searchKey, rangeValue, { ordering, matchExactly, returnOccurence }) + return resultUsingColumnIndex !== undefined ? resultUsingColumnIndex : this.binarySearchStrategy.find(searchKey, rangeValue, { ordering, ifNoMatch, returnOccurence }) } private findUsingColumnIndex(key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, returnOccurence: 'first' | 'last'): Maybe { diff --git a/src/Lookup/SearchStrategy.ts b/src/Lookup/SearchStrategy.ts index 4ff4b89379..4965c17f24 100644 --- a/src/Lookup/SearchStrategy.ts +++ b/src/Lookup/SearchStrategy.ts @@ -16,7 +16,7 @@ import {ColumnIndex} from './ColumnIndex' export interface SearchOptions { ordering: 'asc' | 'desc' | 'none', - matchExactly?: boolean, + ifNoMatch: 'returnLowerBound' | 'returnUpperBound' | 'returnNotFound', returnOccurence?: 'first' | 'last', } diff --git a/src/interpreter/binarySearch.ts b/src/interpreter/binarySearch.ts index c5c0dc1058..b5d35f5f49 100644 --- a/src/interpreter/binarySearch.ts +++ b/src/interpreter/binarySearch.ts @@ -29,7 +29,7 @@ const NOT_FOUND = -1 export function findLastOccurrenceInOrderedRange( searchKey: RawNoErrorScalarValue, range: AbsoluteCellRange, - { searchCoordinate, orderingDirection, matchExactly }: { searchCoordinate: 'row' | 'col', orderingDirection: 'asc' | 'desc', matchExactly?: boolean }, + { searchCoordinate, orderingDirection, ifNoMatch }: { searchCoordinate: 'row' | 'col', orderingDirection: 'asc' | 'desc', ifNoMatch: 'returnLowerBound' | 'returnUpperBound' | 'returnNotFound' }, dependencyGraph: DependencyGraph, ): number { const start = range.start[searchCoordinate] @@ -50,11 +50,20 @@ export function findLastOccurrenceInOrderedRange( return NOT_FOUND } - if (matchExactly && foundValue !== searchKey) { - return NOT_FOUND + if (foundValue === searchKey) { + return foundIndex - start + } + + if (ifNoMatch === 'returnLowerBound') { + return foundIndex - start } - return foundIndex - start + if (ifNoMatch === 'returnUpperBound') { + const upperBoundIndex = foundIndex+1 + return upperBoundIndex <= end ? upperBoundIndex - start : NOT_FOUND + } + + return NOT_FOUND } /* diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index 5512fdcf62..12c443cd7d 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -17,6 +17,10 @@ import { SimpleRangeValue } from '../../SimpleRangeValue' import { FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions } from './FunctionPlugin' import { ArraySize } from '../../ArraySize' +// enum MatchMode { +// matchExactly +// } + export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypecheck { public static implementedFunctions: ImplementedFunctions = { 'VLOOKUP': { @@ -79,14 +83,21 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech if (range === undefined) { return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) } + if (index < 1) { return new CellError(ErrorType.VALUE, ErrorMessage.LessThanOne) } + if (index > range.width()) { return new CellError(ErrorType.REF, ErrorMessage.IndexLarge) } - return this.doVlookup(zeroIfEmpty(key), rangeValue, index - 1, { ordering: sorted ? 'asc' : 'none' }) + const searchOptions: SearchOptions = { + ordering: sorted ? 'asc' : 'none', + ifNoMatch: sorted ? 'returnLowerBound' : 'returnNotFound' + } + + return this.doVlookup(zeroIfEmpty(key), rangeValue, index - 1, searchOptions) }) } @@ -102,14 +113,21 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech if (range === undefined) { return new CellError(ErrorType.VALUE, ErrorMessage.WrongType) } + if (index < 1) { return new CellError(ErrorType.VALUE, ErrorMessage.LessThanOne) } + if (index > range.height()) { return new CellError(ErrorType.REF, ErrorMessage.IndexLarge) } - return this.doHlookup(zeroIfEmpty(key), rangeValue, index - 1, { ordering: sorted ? 'asc' : 'none' }) + const searchOptions: SearchOptions = { + ordering: sorted ? 'asc' : 'none', + ifNoMatch: sorted ? 'returnLowerBound' : 'returnNotFound' + } + + return this.doHlookup(zeroIfEmpty(key), rangeValue, index - 1, searchOptions) }) } @@ -120,7 +138,7 @@ 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) => { + return this.runFunction(ast.args, state, this.metadata('XLOOKUP'), (key: RawNoErrorScalarValue, lookupRangeValue: SimpleRangeValue, returnRangeValue: SimpleRangeValue, notFoundFlag: any, matchMode: number, searchMode: number) => { if (![0, -1, 1, 2].includes(matchMode)) { return new CellError(ErrorType.VALUE, ErrorMessage.BadMode) } @@ -134,10 +152,14 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech const searchOptions: SearchOptions = { ordering: searchMode === 2 ? 'asc' : searchMode === -2 ? 'desc' : 'none', returnOccurence: searchMode === -1 ? 'last' : 'first', - matchExactly: true, + ifNoMatch: matchMode === -1 + ? 'returnLowerBound' + : matchMode === 1 + ? 'returnUpperBound' + : 'returnNotFound' } - return this.doXlookup(zeroIfEmpty(key), lookupRange, returnRange, ifNotFound, matchMode, searchOptions) + return this.doXlookup(zeroIfEmpty(key), lookupRange, returnRange, notFoundFlag, matchMode, searchOptions) }) } @@ -184,13 +206,9 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech this.arithmeticHelper.eqMatcherFunction(key), range ) - } else { - if (searchOptions.ordering === 'none') { - searchOptions.matchExactly = true; - } - - return searchStrategy.find(key, range, searchOptions) } + + return searchStrategy.find(key, range, searchOptions) } private doVlookup(key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, index: number, searchOptions: SearchOptions): InternalScalarValue { @@ -282,8 +300,8 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech const searchStrategy = rangeValue.width() === 1 ? this.columnSearch : this.rowSearch const searchOptions: SearchOptions = type === 0 - ? { ordering: 'none', matchExactly: true } - : { ordering: type === -1 ? 'desc' : 'asc' } + ? { ordering: 'none', ifNoMatch: 'returnNotFound' } + : { ordering: type === -1 ? 'desc' : 'asc', ifNoMatch: 'returnLowerBound' } const index = searchStrategy.find(key, rangeValue, searchOptions) if (index === -1) { diff --git a/test/interpreter/binary-search.spec.ts b/test/interpreter/binary-search.spec.ts index 989f2f585d..799143aa1b 100644 --- a/test/interpreter/binary-search.spec.ts +++ b/test/interpreter/binary-search.spec.ts @@ -3,29 +3,29 @@ import {EmptyValue} from '../../src/interpreter/InterpreterValue' import {CellError, ErrorType} from '../../src' describe('findLastOccurrenceInOrderedArray', () => { - it('should return -1 when empty array', () => { + it('returns -1 when empty array', () => { const values: number[] = [] expect(findLastOccurrenceInOrderedArray(1, values)).toBe(-1) }) - it('should work for one element', () => { + it('works for one element', () => { const values: number[] = [1] expect(findLastOccurrenceInOrderedArray(1, values)).toBe(0) }) - it('should return -1 when all elements are greater', () => { + it('returns -1 when all elements are greater', () => { const values: number[] = [3, 5, 10] expect(findLastOccurrenceInOrderedArray(1, values)).toBe(-1) }) - it('should find index of element in values of odd length', () => { + it('finds index of element in values of odd length', () => { const values: number[] = [3, 5, 10] expect(findLastOccurrenceInOrderedArray(3, values)).toBe(0) expect(findLastOccurrenceInOrderedArray(5, values)).toBe(1) expect(findLastOccurrenceInOrderedArray(10, values)).toBe(2) }) - it('should find index of element in values of even length', () => { + it('finds index of element in values of even length', () => { const values: number[] = [3, 5, 10, 11] expect(findLastOccurrenceInOrderedArray(3, values)).toBe(0) expect(findLastOccurrenceInOrderedArray(5, values)).toBe(1) @@ -33,23 +33,23 @@ describe('findLastOccurrenceInOrderedArray', () => { expect(findLastOccurrenceInOrderedArray(11, values)).toBe(3) }) - it('should find index of lower bound', () => { + it('finds index of lower bound', () => { const values: number[] = [1, 2, 3, 7] expect(findLastOccurrenceInOrderedArray(5, values)).toBe(2) expect(findLastOccurrenceInOrderedArray(10, values)).toBe(3) }) - it('should work for strings', () => { + it('works for strings', () => { const values: string[] = ['aaaa', 'bar', 'foo', 'xyz'] expect(findLastOccurrenceInOrderedArray('foo', values)).toBe(2) }) - it('should work for bools', () => { + it('works for bools', () => { const values: boolean[] = [false, false, false, true, true] expect(findLastOccurrenceInOrderedArray(true, values)).toBe(4) }) - it('should work for different types in array', () => { + it('works for different types in array', () => { const values = [3, 5, 7, 'aaaa', 'bar', 'foo', false, false, true] expect(findLastOccurrenceInOrderedArray(5, values)).toBe(1) expect(findLastOccurrenceInOrderedArray('foo', values)).toBe(5) @@ -58,12 +58,12 @@ describe('findLastOccurrenceInOrderedArray', () => { expect(findLastOccurrenceInOrderedArray('xyz', values)).toBe(5) }) - it('should return the last occurence', () => { + it('returns the last occurence', () => { const values = [1, 2, 2, 2, 2, 2, 3, 3, 3] expect(findLastOccurrenceInOrderedArray(2, values)).toBe(5) }) - it('should work for arrays ordered descending', () => { + it('works for arrays ordered descending', () => { const values: number[] = [11, 10, 5, 3] expect(findLastOccurrenceInOrderedArray(3, values, 'desc')).toBe(3) expect(findLastOccurrenceInOrderedArray(5, values, 'desc')).toBe(2) diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index b0c7e2edb8..65eb3174f5 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -503,99 +503,130 @@ describe('Function XLOOKUP', () => { }) describe('with BinarySearch column search strategy, when provided with matchMode = ', () => { - describe('-1, returns a lower bound if the search key is not present', () => { - it('in unsorted horizontal range', () => { - const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(3, A2:E2, A3:E3, "NotFound", -1, 1)'], - [2, 1, 4, 2, 5], - [1, 2, 3, 4, 5], - ], { useColumnIndex: false }) - - expect(engine.getCellValue(adr('A1'))).toEqual(1) - }) - - it('in unsorted vertical range', () => { - const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(3, A2:A6, B2:B6, "NotFound", -1, 1)'], - [2, 1], - [1, 2], - [4, 3], - [2, 4], - [5, 5] - ], { useColumnIndex: false }) - - expect(engine.getCellValue(adr('A1'))).toEqual(1) - }) - - it('in sorted horizontal range', () => { - const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(3, A2:E2, A2:E2, "NotFound", -1, 2)'], - [1, 2, 2, 5, 6], - ], { useColumnIndex: false }) - - expect(engine.getCellValue(adr('A1'))).toEqual(2) + describe('-1 (looking for a lower bound', () => { + describe('in array ordered ascending', () => { + it('returns exact match if exists', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", -1, 2)'], + [1, 2, 42, 50, 51], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(42) + }) + + it('returns a lower bound when there is no exact match', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", -1, 2)'], + [1, 2, 40, 50, 51], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(40) + }) + + it.todo('returns a lower bound when all elements are smaller than the key') + it.todo('returns -1 when all elements are greater than the key') }) - it('in sorted vertical range', () => { - const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", -1, 2)'], - [1], - [2], - [4], - [5], - [6] - ], { useColumnIndex: false }) - - expect(engine.getCellValue(adr('A1'))).toEqual(2) + describe('in array ordered descending', () => { }) }) - describe('1, returns an upper bound if the search key is not present', () => { - it('in unsorted horizontal range', () => { - const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(3, A2:E2, A3:E3, "NotFound", 1, 1)'], - [2, 1, 4, 4, 5], - [1, 2, 3, 4, 5], - ], { useColumnIndex: false }) - - expect(engine.getCellValue(adr('A1'))).toEqual(3) - }) - - it('in unsorted vertical range', () => { - const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(3, A2:A6, B2:B6, "NotFound", 1, 1)'], - [2, 1], - [1, 2], - [4, 3], - [4, 4], - [5, 5] - ], { useColumnIndex: false }) - - expect(engine.getCellValue(adr('A1'))).toEqual(3) - }) - - it('in sorted horizontal range', () => { - const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(3, A2:E2, A2:E2, "NotFound", 1, 2)'], - [1, 2, 4, 5, 6], - ], { useColumnIndex: false }) - - expect(engine.getCellValue(adr('A1'))).toEqual(4) - }) - - it('in sorted vertical range', () => { - const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", 1, 2)'], - [1], - [2], - [4], - [5], - [6] - ], { useColumnIndex: false }) - - expect(engine.getCellValue(adr('A1'))).toEqual(4) - }) - }) + describe('when looking for an upper bound', () => { + }) + + // describe('-1, returns a lower bound if the search key is not present', () => { + // it('in unsorted horizontal range', () => { + // const engine = HyperFormula.buildFromArray([ + // ['=XLOOKUP(3, A2:E2, A3:E3, "NotFound", -1, 1)'], + // [2, 1, 4, 2, 5], + // [1, 2, 3, 4, 5], + // ], { useColumnIndex: false }) + + // expect(engine.getCellValue(adr('A1'))).toEqual(1) + // }) + + // it('in unsorted vertical range', () => { + // const engine = HyperFormula.buildFromArray([ + // ['=XLOOKUP(3, A2:A6, B2:B6, "NotFound", -1, 1)'], + // [2, 1], + // [1, 2], + // [4, 3], + // [2, 4], + // [5, 5] + // ], { useColumnIndex: false }) + + // expect(engine.getCellValue(adr('A1'))).toEqual(1) + // }) + + // it('in sorted horizontal range', () => { + // const engine = HyperFormula.buildFromArray([ + // ['=XLOOKUP(3, A2:E2, A2:E2, "NotFound", -1, 2)'], + // [1, 2, 2, 5, 6], + // ], { useColumnIndex: false }) + + // expect(engine.getCellValue(adr('A1'))).toEqual(2) + // }) + + // it('in sorted vertical range', () => { + // const engine = HyperFormula.buildFromArray([ + // ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", -1, 2)'], + // [1], + // [2], + // [4], + // [5], + // [6] + // ], { useColumnIndex: false }) + + // expect(engine.getCellValue(adr('A1'))).toEqual(2) + // }) + // }) + + // describe('1, returns an upper bound if the search key is not present', () => { + // it('in unsorted horizontal range', () => { + // const engine = HyperFormula.buildFromArray([ + // ['=XLOOKUP(3, A2:E2, A3:E3, "NotFound", 1, 1)'], + // [2, 1, 4, 4, 5], + // [1, 2, 3, 4, 5], + // ], { useColumnIndex: false }) + + // expect(engine.getCellValue(adr('A1'))).toEqual(3) + // }) + + // it('in unsorted vertical range', () => { + // const engine = HyperFormula.buildFromArray([ + // ['=XLOOKUP(3, A2:A6, B2:B6, "NotFound", 1, 1)'], + // [2, 1], + // [1, 2], + // [4, 3], + // [4, 4], + // [5, 5] + // ], { useColumnIndex: false }) + + // expect(engine.getCellValue(adr('A1'))).toEqual(3) + // }) + + // it('in sorted horizontal range', () => { + // const engine = HyperFormula.buildFromArray([ + // ['=XLOOKUP(3, A2:E2, A2:E2, "NotFound", 1, 2)'], + // [1, 2, 4, 5, 6], + // ], { useColumnIndex: false }) + + // expect(engine.getCellValue(adr('A1'))).toEqual(4) + // }) + + // it('in sorted vertical range', () => { + // const engine = HyperFormula.buildFromArray([ + // ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", 1, 2)'], + // [1], + // [2], + // [4], + // [5], + // [6] + // ], { useColumnIndex: false }) + + // expect(engine.getCellValue(adr('A1'))).toEqual(4) + // }) + // }) describe('2, performs a wildcard match', () => { // TODO @@ -698,5 +729,5 @@ describe('Function XLOOKUP', () => { }) // TODO: -// - setup debugger!!! last day of cursor -// - implement matchMode +// TODO: test returnUpperBound +// - setup debugger, upgrade to premium From 2c4967b67ee368c13a00119776e562bda77c3d04 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Sun, 22 Dec 2024 18:52:02 +0100 Subject: [PATCH 28/45] Add unit tests for matchMode -1 --- src/interpreter/binarySearch.ts | 29 ++++++++++-- src/interpreter/plugin/LookupPlugin.ts | 6 +-- test/interpreter/function-xlookup.spec.ts | 54 ++++++++++++++++++++++- 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/src/interpreter/binarySearch.ts b/src/interpreter/binarySearch.ts index b5d35f5f49..e3dcd54283 100644 --- a/src/interpreter/binarySearch.ts +++ b/src/interpreter/binarySearch.ts @@ -46,7 +46,19 @@ export function findLastOccurrenceInOrderedRange( const foundIndex = findLastMatchingIndex(index => compareFn(searchKey, getValueFromIndexFn(index)) >= 0, start, end) const foundValue = getValueFromIndexFn(foundIndex) - if (foundIndex === NOT_FOUND || typeof foundValue !== typeof searchKey) { + if (foundIndex === NOT_FOUND) { + if (ifNoMatch === 'returnLowerBound' && orderingDirection === 'desc') { + return 0 + } + + if (ifNoMatch === 'returnUpperBound' && orderingDirection === 'asc') { + return 0 + } + + return NOT_FOUND + } + + if (typeof foundValue !== typeof searchKey) { return NOT_FOUND } @@ -55,12 +67,21 @@ export function findLastOccurrenceInOrderedRange( } if (ifNoMatch === 'returnLowerBound') { - return foundIndex - start + if (orderingDirection === 'asc') { + return foundIndex - start + } + + const nextIndex = foundIndex+1 + return nextIndex <= end ? nextIndex - start : NOT_FOUND } if (ifNoMatch === 'returnUpperBound') { - const upperBoundIndex = foundIndex+1 - return upperBoundIndex <= end ? upperBoundIndex - start : NOT_FOUND + if (orderingDirection === 'desc') { + return foundIndex - start + } + + const nextIndex = foundIndex+1 + return nextIndex <= end ? nextIndex - start : NOT_FOUND } return NOT_FOUND diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index 12c443cd7d..6ec0c65b83 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -159,7 +159,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech : 'returnNotFound' } - return this.doXlookup(zeroIfEmpty(key), lookupRange, returnRange, notFoundFlag, matchMode, searchOptions) + return this.doXlookup(zeroIfEmpty(key), lookupRange, returnRange, notFoundFlag, searchOptions) }) } @@ -270,7 +270,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech return value } - private doXlookup(key: RawNoErrorScalarValue, lookupRange: SimpleRangeValue, returnRange: SimpleRangeValue, ifNotFound: any, matchMode: number, searchOptions: SearchOptions): InterpreterValue { + private doXlookup(key: RawNoErrorScalarValue, lookupRange: SimpleRangeValue, returnRange: SimpleRangeValue, notFoundFlag: any, searchOptions: SearchOptions): InterpreterValue { const isVerticalSearch = lookupRange.width() === 1 && returnRange.height() === lookupRange.height() const isHorizontalSearch = lookupRange.height() === 1 && returnRange.width() === lookupRange.width() @@ -282,7 +282,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech const indexFound = this.searchInRange(key, lookupRange, searchOptions, searchStrategy) if (indexFound === -1) { - return (ifNotFound == ErrorType.NA) ? new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) : ifNotFound + return (notFoundFlag == ErrorType.NA) ? new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) : notFoundFlag } const returnValues: InternalScalarValue[][] = isVerticalSearch ? [returnRange.data[indexFound]] : returnRange.data.map((row) => [row[indexFound]]) diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 65eb3174f5..37d23f9d5f 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -523,11 +523,61 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual(40) }) - it.todo('returns a lower bound when all elements are smaller than the key') - it.todo('returns -1 when all elements are greater than the key') + it('returns a lower bound when all elements are smaller than the key', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", -1, 2)'], + [1, 2, 3, 4, 5], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(5) + }) + + it('returns NotFound when all elements are greater than the key', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", -1, 2)'], + [43, 44, 45, 46, 47], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual("NotFound") + }) }) describe('in array ordered descending', () => { + it('returns exact match if exists', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", -1, -2)'], + [55, 54, 42, 2, 1], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(42) + }) + + it('returns a lower bound when there is no exact match', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", -1, -2)'], + [55, 54, 40, 2, 1], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(40) + }) + + it('returns a lower bound when all elements are smaller than the key', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", -1, -2)'], + [5, 4, 3, 2, 1], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(5) + }) + + it('returns NotFound when all elements are greater than the key', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", -1, -2)'], + [100, 90, 80, 70, 60], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual("NotFound") + }) }) }) From 47adb9bdc48b1c5d622070813717fcf69564f32a Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Sun, 22 Dec 2024 21:08:44 +0100 Subject: [PATCH 29/45] Improve AdvancedFind.basicFind() to handle lower and upper bounds when searching in unordered range --- src/Lookup/AdvancedFind.ts | 49 ++++++-- src/interpreter/binarySearch.ts | 8 +- test/interpreter/function-xlookup.spec.ts | 139 +++++++--------------- 3 files changed, 87 insertions(+), 109 deletions(-) diff --git a/src/Lookup/AdvancedFind.ts b/src/Lookup/AdvancedFind.ts index 09b6498708..276fcd3c6f 100644 --- a/src/Lookup/AdvancedFind.ts +++ b/src/Lookup/AdvancedFind.ts @@ -13,7 +13,9 @@ import { import {SimpleRangeValue} from '../SimpleRangeValue' import {SearchOptions} from './SearchStrategy' import {forceNormalizeString} from '../interpreter/ArithmeticHelper' -import {findLastOccurrenceInOrderedRange} from '../interpreter/binarySearch' +import {compare, findLastOccurrenceInOrderedRange} from '../interpreter/binarySearch' + +const NOT_FOUND = -1 export abstract class AdvancedFind { protected constructor( @@ -34,22 +36,19 @@ export abstract class AdvancedFind { return i } } - return -1 + return NOT_FOUND } - /* - * WARNING: Finding lower/upper bounds in unordered ranges is not supported. When ordering === 'none', assumes matchExactly === true - */ protected basicFind(searchKey: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, searchCoordinate: 'col' | 'row', { ordering, ifNoMatch, returnOccurence }: SearchOptions): number { const normalizedSearchKey = typeof searchKey === 'string' ? forceNormalizeString(searchKey) : searchKey const range = rangeValue.range if (range === undefined) { - return this.findNormalizedValue(normalizedSearchKey, rangeValue.valuesFromTopLeftCorner(), returnOccurence) + return this.findNormalizedValue(normalizedSearchKey, rangeValue.valuesFromTopLeftCorner(), ifNoMatch, returnOccurence) } if (ordering === 'none') { - return this.findNormalizedValue(normalizedSearchKey, this.dependencyGraph.computeListOfValuesInRange(range), returnOccurence) + return this.findNormalizedValue(normalizedSearchKey, this.dependencyGraph.computeListOfValuesInRange(range), ifNoMatch, returnOccurence) } return findLastOccurrenceInOrderedRange( @@ -60,11 +59,43 @@ export abstract class AdvancedFind { ) } - protected findNormalizedValue(searchKey: RawNoErrorScalarValue, searchArray: InternalScalarValue[], returnOccurence: 'first' | 'last' = 'first'): number { + protected findNormalizedValue(searchKey: RawNoErrorScalarValue, searchArray: InternalScalarValue[], ifNoMatch: 'returnLowerBound' | 'returnUpperBound' | 'returnNotFound' = 'returnNotFound', returnOccurence: 'first' | 'last' = 'first'): number { const normalizedArray = searchArray .map(getRawValue) .map(val => typeof val === 'string' ? forceNormalizeString(val) : val) - return returnOccurence === 'first' ? normalizedArray.indexOf(searchKey) : normalizedArray.lastIndexOf(searchKey) + if (ifNoMatch === 'returnNotFound') { + return returnOccurence === 'first' ? normalizedArray.indexOf(searchKey) : normalizedArray.lastIndexOf(searchKey) + } + + const compareFn = ifNoMatch === 'returnLowerBound' + ? (left: RawNoErrorScalarValue, right: RawInterpreterValue) => compare(left, right) + : (left: RawNoErrorScalarValue, right: RawInterpreterValue) => -compare(left, right) + + let bestValue: RawNoErrorScalarValue = ifNoMatch === 'returnLowerBound' ? -Infinity : Infinity + let bestIndex = NOT_FOUND + + const initialIterationIndex = returnOccurence === 'first' ? 0 : normalizedArray.length-1 + const iterationCondition = returnOccurence === 'first' ? (i: number) => i < normalizedArray.length : (i: number) => i >= 0 + const incrementIndex = returnOccurence === 'first' ? (i: number) => i+1 : (i: number) => i-1 + + for (let i = initialIterationIndex; iterationCondition(i); i = incrementIndex(i)) { + const value = normalizedArray[i] as RawNoErrorScalarValue + + if (value === searchKey) { + return i + } + + if (compareFn(value, searchKey) > 0) { + continue + } + + if (compareFn(bestValue, value) < 0) { + bestValue = value + bestIndex = i + } + } + + return bestIndex } } diff --git a/src/interpreter/binarySearch.ts b/src/interpreter/binarySearch.ts index e3dcd54283..79bbe12736 100644 --- a/src/interpreter/binarySearch.ts +++ b/src/interpreter/binarySearch.ts @@ -16,13 +16,9 @@ const NOT_FOUND = -1 * Options: * - searchCoordinate - must be set to either 'row' or 'col' to indicate the dimension of the search, * - orderingDirection - must be set to either 'asc' or 'desc' to indicate the ordering direction for the search range, - * - matchExactly - when set to false, searches for the lower/upper bound. + * - ifNoMatch - must be set to 'returnLowerBound', 'returnUpperBound' or 'returnNotFound' * - * Semantics: - * - If orderingDirection === 'asc', searches for the lower bound for the searchKey value (unless marchExactly === true). - * - If orderingDirection === 'desc', searches for the upper bound for the searchKey value (unless marchExactly === true). - * - If the search range contains duplicates, returns the last matching value. - * - If no value in the range satisfies the above, returns -1. + * If the search range contains duplicates, returns the last matching value. If no value found in the range satisfies the above, returns -1. * * Note: this function does not normalize input strings. */ diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 37d23f9d5f..86f30dfa0b 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -579,105 +579,56 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual("NotFound") }) }) + + it('works for unsorted horizontal range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:E2, A3:E3, "NotFound", -1, 1)'], + [2, 1, 4, 2, 5], + [1, 2, 3, 4, 5], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(1) + }) + + it('works for unsorted vertical range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:A6, B2:B6, "NotFound", -1, 1)'], + [2, 1], + [1, 2], + [4, 3], + [2, 4], + [5, 5] + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(1) + }) + + it('works for sorted horizontal range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:E2, A2:E2, "NotFound", -1, 2)'], + [1, 2, 2, 5, 6], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + }) + + it('works for sorted vertical range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", -1, 2)'], + [1], + [2], + [4], + [5], + [6] + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + }) }) describe('when looking for an upper bound', () => { }) - // describe('-1, returns a lower bound if the search key is not present', () => { - // it('in unsorted horizontal range', () => { - // const engine = HyperFormula.buildFromArray([ - // ['=XLOOKUP(3, A2:E2, A3:E3, "NotFound", -1, 1)'], - // [2, 1, 4, 2, 5], - // [1, 2, 3, 4, 5], - // ], { useColumnIndex: false }) - - // expect(engine.getCellValue(adr('A1'))).toEqual(1) - // }) - - // it('in unsorted vertical range', () => { - // const engine = HyperFormula.buildFromArray([ - // ['=XLOOKUP(3, A2:A6, B2:B6, "NotFound", -1, 1)'], - // [2, 1], - // [1, 2], - // [4, 3], - // [2, 4], - // [5, 5] - // ], { useColumnIndex: false }) - - // expect(engine.getCellValue(adr('A1'))).toEqual(1) - // }) - - // it('in sorted horizontal range', () => { - // const engine = HyperFormula.buildFromArray([ - // ['=XLOOKUP(3, A2:E2, A2:E2, "NotFound", -1, 2)'], - // [1, 2, 2, 5, 6], - // ], { useColumnIndex: false }) - - // expect(engine.getCellValue(adr('A1'))).toEqual(2) - // }) - - // it('in sorted vertical range', () => { - // const engine = HyperFormula.buildFromArray([ - // ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", -1, 2)'], - // [1], - // [2], - // [4], - // [5], - // [6] - // ], { useColumnIndex: false }) - - // expect(engine.getCellValue(adr('A1'))).toEqual(2) - // }) - // }) - - // describe('1, returns an upper bound if the search key is not present', () => { - // it('in unsorted horizontal range', () => { - // const engine = HyperFormula.buildFromArray([ - // ['=XLOOKUP(3, A2:E2, A3:E3, "NotFound", 1, 1)'], - // [2, 1, 4, 4, 5], - // [1, 2, 3, 4, 5], - // ], { useColumnIndex: false }) - - // expect(engine.getCellValue(adr('A1'))).toEqual(3) - // }) - - // it('in unsorted vertical range', () => { - // const engine = HyperFormula.buildFromArray([ - // ['=XLOOKUP(3, A2:A6, B2:B6, "NotFound", 1, 1)'], - // [2, 1], - // [1, 2], - // [4, 3], - // [4, 4], - // [5, 5] - // ], { useColumnIndex: false }) - - // expect(engine.getCellValue(adr('A1'))).toEqual(3) - // }) - - // it('in sorted horizontal range', () => { - // const engine = HyperFormula.buildFromArray([ - // ['=XLOOKUP(3, A2:E2, A2:E2, "NotFound", 1, 2)'], - // [1, 2, 4, 5, 6], - // ], { useColumnIndex: false }) - - // expect(engine.getCellValue(adr('A1'))).toEqual(4) - // }) - - // it('in sorted vertical range', () => { - // const engine = HyperFormula.buildFromArray([ - // ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", 1, 2)'], - // [1], - // [2], - // [4], - // [5], - // [6] - // ], { useColumnIndex: false }) - - // expect(engine.getCellValue(adr('A1'))).toEqual(4) - // }) - // }) - describe('2, performs a wildcard match', () => { // TODO }) From 71cec9874bcaab83423ae607843bda18f80d5fd8 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Sun, 22 Dec 2024 21:30:33 +0100 Subject: [PATCH 30/45] Implement matchMode -1 and 1 for XLOOKUP --- src/Lookup/ColumnIndex.ts | 3 - test/interpreter/function-xlookup.spec.ts | 372 +++++++++++++++++++++- 2 files changed, 370 insertions(+), 5 deletions(-) diff --git a/src/Lookup/ColumnIndex.ts b/src/Lookup/ColumnIndex.ts index d6420d8d16..0dc0971349 100644 --- a/src/Lookup/ColumnIndex.ts +++ b/src/Lookup/ColumnIndex.ts @@ -107,9 +107,6 @@ export class ColumnIndex implements ColumnSearchStrategy { } } - /* - * WARNING: Finding lower/upper bounds in unordered ranges is not supported. When ordering === 'none', assumes matchExactly === true - */ public find(searchKey: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, { ordering, ifNoMatch, returnOccurence }: SearchOptions): number { if (returnOccurence == null) { returnOccurence = ifNoMatch === 'returnNotFound' ? 'first' : 'last' diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 86f30dfa0b..3d2ffbede8 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -626,7 +626,127 @@ describe('Function XLOOKUP', () => { }) }) - describe('when looking for an upper bound', () => { + describe('1 (looking for a upper bound', () => { + describe('in array ordered ascending', () => { + it('returns exact match if exists', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", 1, 2)'], + [1, 2, 42, 50, 51], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(42) + }) + + it('returns an upper bound when there is no exact match', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", 1, 2)'], + [1, 2, 44, 50, 51], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(44) + }) + + it('returns NotFound when all elements are smaller than the key', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", 1, 2)'], + [1, 2, 3, 4, 5], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') + }) + + it('returns an upper bound when all elements are greater than the key', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", 1, 2)'], + [43, 44, 45, 46, 47], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(43) + }) + }) + + describe('in array ordered descending', () => { + it('returns exact match if exists', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", 1, -2)'], + [55, 54, 42, 2, 1], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(42) + }) + + it('returns an upper bound when there is no exact match', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", 1, -2)'], + [55, 54, 44, 2, 1], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(44) + }) + + it('returns NotFound when all elements are smaller than the key', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", 1, -2)'], + [5, 4, 3, 2, 1], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') + }) + + it('returns an upper bound when all elements are greater than the key', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", 1, -2)'], + [100, 90, 80, 70, 60], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(60) + }) + }) + + it('works for unsorted horizontal range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:E2, A3:E3, "NotFound", 1, 1)'], + [2, 1, 4, 2, 5], + [1, 2, 3, 4, 5], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(3) + }) + + it('works for unsorted vertical range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:A6, B2:B6, "NotFound", 1, 1)'], + [2, 1], + [1, 2], + [4, 3], + [2, 4], + [5, 5] + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(3) + }) + + it('works for sorted horizontal range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:E2, A2:E2, "NotFound", 1, 2)'], + [1, 2, 5, 5, 6], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(5) + }) + + it('works for sorted vertical range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", 1, 2)'], + [1], + [2], + [4], + [5], + [6] + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual(4) + }) }) describe('2, performs a wildcard match', () => { @@ -635,7 +755,255 @@ describe('Function XLOOKUP', () => { }) describe('with ColumnIndex column search strategy, when provided with matchMode = ', () => { - // TODO + describe('-1 (looking for a lower bound', () => { + describe('in array ordered ascending', () => { + it('returns exact match if exists', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", -1, 2)'], + [1, 2, 42, 50, 51], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(42) + }) + + it('returns a lower bound when there is no exact match', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", -1, 2)'], + [1, 2, 40, 50, 51], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(40) + }) + + it('returns a lower bound when all elements are smaller than the key', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", -1, 2)'], + [1, 2, 3, 4, 5], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(5) + }) + + it('returns NotFound when all elements are greater than the key', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", -1, 2)'], + [43, 44, 45, 46, 47], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual("NotFound") + }) + }) + + describe('in array ordered descending', () => { + it('returns exact match if exists', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", -1, -2)'], + [55, 54, 42, 2, 1], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(42) + }) + + it('returns a lower bound when there is no exact match', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", -1, -2)'], + [55, 54, 40, 2, 1], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(40) + }) + + it('returns a lower bound when all elements are smaller than the key', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", -1, -2)'], + [5, 4, 3, 2, 1], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(5) + }) + + it('returns NotFound when all elements are greater than the key', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", -1, -2)'], + [100, 90, 80, 70, 60], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual("NotFound") + }) + }) + + it('works for unsorted horizontal range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:E2, A3:E3, "NotFound", -1, 1)'], + [2, 1, 4, 2, 5], + [1, 2, 3, 4, 5], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(1) + }) + + it('works for unsorted vertical range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:A6, B2:B6, "NotFound", -1, 1)'], + [2, 1], + [1, 2], + [4, 3], + [2, 4], + [5, 5] + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(1) + }) + + it('works for sorted horizontal range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:E2, A2:E2, "NotFound", -1, 2)'], + [1, 2, 2, 5, 6], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + }) + + it('works for sorted vertical range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", -1, 2)'], + [1], + [2], + [4], + [5], + [6] + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(2) + }) + }) + + describe('1 (looking for a upper bound', () => { + describe('in array ordered ascending', () => { + it('returns exact match if exists', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", 1, 2)'], + [1, 2, 42, 50, 51], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(42) + }) + + it('returns an upper bound when there is no exact match', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", 1, 2)'], + [1, 2, 44, 50, 51], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(44) + }) + + it('returns NotFound when all elements are smaller than the key', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", 1, 2)'], + [1, 2, 3, 4, 5], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') + }) + + it('returns an upper bound when all elements are greater than the key', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", 1, 2)'], + [43, 44, 45, 46, 47], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(43) + }) + }) + + describe('in array ordered descending', () => { + it('returns exact match if exists', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", 1, -2)'], + [55, 54, 42, 2, 1], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(42) + }) + + it('returns an upper bound when there is no exact match', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", 1, -2)'], + [55, 54, 44, 2, 1], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(44) + }) + + it('returns NotFound when all elements are smaller than the key', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", 1, -2)'], + [5, 4, 3, 2, 1], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') + }) + + it('returns an upper bound when all elements are greater than the key', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(42, A2:E2, A2:E2, "NotFound", 1, -2)'], + [100, 90, 80, 70, 60], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(60) + }) + }) + + it('works for unsorted horizontal range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:E2, A3:E3, "NotFound", 1, 1)'], + [2, 1, 4, 2, 5], + [1, 2, 3, 4, 5], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(3) + }) + + it('works for unsorted vertical range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:A6, B2:B6, "NotFound", 1, 1)'], + [2, 1], + [1, 2], + [4, 3], + [2, 4], + [5, 5] + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(3) + }) + + it('works for sorted horizontal range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:E2, A2:E2, "NotFound", 1, 2)'], + [1, 2, 5, 5, 6], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(5) + }) + + it('works for sorted vertical range', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", 1, 2)'], + [1], + [2], + [4], + [5], + [6] + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual(4) + }) + }) + + describe('2, performs a wildcard match', () => { + // TODO + }) }) describe('acts similar to Microsoft Excel', () => { From c8f7c119ffca17fd0a0926f59207ff18aa812617 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Sun, 22 Dec 2024 21:45:56 +0100 Subject: [PATCH 31/45] Fix the issue with MATCH function --- src/Lookup/ColumnIndex.ts | 4 ++++ src/interpreter/binarySearch.ts | 4 ++++ src/interpreter/plugin/LookupPlugin.ts | 2 +- test/column-index.spec.ts | 14 +++++++------- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Lookup/ColumnIndex.ts b/src/Lookup/ColumnIndex.ts index 0dc0971349..bb0fafd641 100644 --- a/src/Lookup/ColumnIndex.ts +++ b/src/Lookup/ColumnIndex.ts @@ -108,6 +108,10 @@ export class ColumnIndex implements ColumnSearchStrategy { } public find(searchKey: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, { ordering, ifNoMatch, returnOccurence }: SearchOptions): number { + if (ifNoMatch == null) { + ifNoMatch = 'returnNotFound' + } + if (returnOccurence == null) { returnOccurence = ifNoMatch === 'returnNotFound' ? 'first' : 'last' } diff --git a/src/interpreter/binarySearch.ts b/src/interpreter/binarySearch.ts index 79bbe12736..d95f19d7c5 100644 --- a/src/interpreter/binarySearch.ts +++ b/src/interpreter/binarySearch.ts @@ -28,6 +28,10 @@ export function findLastOccurrenceInOrderedRange( { searchCoordinate, orderingDirection, ifNoMatch }: { searchCoordinate: 'row' | 'col', orderingDirection: 'asc' | 'desc', ifNoMatch: 'returnLowerBound' | 'returnUpperBound' | 'returnNotFound' }, dependencyGraph: DependencyGraph, ): number { + if (ifNoMatch == null) { + ifNoMatch = 'returnNotFound' + } + const start = range.start[searchCoordinate] const end = searchCoordinate === 'col' ? range.effectiveEndColumn(dependencyGraph) : range.effectiveEndRow(dependencyGraph) diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index 6ec0c65b83..187860e883 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -301,7 +301,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech const searchStrategy = rangeValue.width() === 1 ? this.columnSearch : this.rowSearch const searchOptions: SearchOptions = type === 0 ? { ordering: 'none', ifNoMatch: 'returnNotFound' } - : { ordering: type === -1 ? 'desc' : 'asc', ifNoMatch: 'returnLowerBound' } + : { ordering: type === -1 ? 'desc' : 'asc', ifNoMatch: type === -1 ? 'returnUpperBound' : 'returnLowerBound' } const index = searchStrategy.find(key, rangeValue, searchOptions) if (index === -1) { diff --git a/test/column-index.spec.ts b/test/column-index.spec.ts index 92ac6fec47..780dc94b82 100644 --- a/test/column-index.spec.ts +++ b/test/column-index.spec.ts @@ -331,7 +331,7 @@ describe('ColumnIndex#find', () => { const index = buildEmptyIndex(transformService, new Config(), stats) index.add(1, adr('A2')) - const row = index.find(1, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('A1'), adr('A3')), undefined!), { ordering: 'asc' }) + const row = index.find(1, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('A1'), adr('A3')), undefined!), { ordering: 'asc', ifNoMatch: 'returnNotFound' }) expect(row).toBe(1) }) @@ -341,7 +341,7 @@ describe('ColumnIndex#find', () => { index.add(1, adr('A4')) index.add(1, adr('A10')) - const row = index.find(1, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('A1'), adr('A20')), undefined!), { ordering: 'none', matchExactly: true }) + const row = index.find(1, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('A1'), adr('A20')), undefined!), { ordering: 'none', ifNoMatch: 'returnNotFound' }) expect(row).toBe(3) }) @@ -351,7 +351,7 @@ describe('ColumnIndex#find', () => { index.add(1, adr('A4')) index.add(1, adr('A10')) - const row = index.find(1, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('A1'), adr('A20')), undefined!), { ordering: 'asc' }) + const row = index.find(1, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('A1'), adr('A20')), undefined!), { ordering: 'asc', ifNoMatch: 'returnNotFound' }) expect(row).toBe(9) }) @@ -550,12 +550,12 @@ describe('ColumnIndex - lazy crud operations', () => { transformService.addTransformation(new AddRowsTransformer(RowsSpan.fromNumberOfRows(0, 0, 1))) - const rowA = index.find(1, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('A1'), adr('A2')), undefined!), { ordering: 'asc' }) + const rowA = index.find(1, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('A1'), adr('A2')), undefined!), { ordering: 'asc', ifNoMatch: 'returnNotFound' }) expect(rowA).toEqual(1) expect(index.getValueIndex(0, 0, 1).index).toEqual([1]) expect(index.getValueIndex(0, 1, 1).index).toEqual([0]) - const rowB = index.find(1, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('B1'), adr('B2')), undefined!), { ordering: 'asc' }) + const rowB = index.find(1, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('B1'), adr('B2')), undefined!), { ordering: 'asc', ifNoMatch: 'returnNotFound' }) expect(rowB).toEqual(1) expect(index.getValueIndex(0, 0, 1).index).toEqual([1]) expect(index.getValueIndex(0, 1, 1).index).toEqual([1]) @@ -570,12 +570,12 @@ describe('ColumnIndex - lazy crud operations', () => { transformService.addTransformation(new AddRowsTransformer(RowsSpan.fromNumberOfRows(0, 0, 1))) - const row1 = index.find(1, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('A1'), adr('A3')), undefined!), { ordering: 'asc' }) + const row1 = index.find(1, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('A1'), adr('A3')), undefined!), { ordering: 'asc', ifNoMatch: 'returnNotFound' }) expect(row1).toEqual(1) expect(index.getValueIndex(0, 0, 1).index).toEqual([1]) expect(index.getValueIndex(0, 0, 2).index).toEqual([1]) - const row2 = index.find(2, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('A1'), adr('A3')), undefined!), { ordering: 'asc' }) + const row2 = index.find(2, SimpleRangeValue.onlyRange(new AbsoluteCellRange(adr('A1'), adr('A3')), undefined!), { ordering: 'asc', ifNoMatch: 'returnNotFound' }) expect(row2).toEqual(2) expect(index.getValueIndex(0, 0, 1).index).toEqual([1]) expect(index.getValueIndex(0, 0, 2).index).toEqual([2]) From b6a84497fb3ae78e1cf0268cb3c510c0079660b2 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Sun, 22 Dec 2024 21:52:59 +0100 Subject: [PATCH 32/45] Fixx ColumnIndex.find() issue --- src/Lookup/ColumnIndex.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Lookup/ColumnIndex.ts b/src/Lookup/ColumnIndex.ts index bb0fafd641..f0726b5cf6 100644 --- a/src/Lookup/ColumnIndex.ts +++ b/src/Lookup/ColumnIndex.ts @@ -113,7 +113,7 @@ export class ColumnIndex implements ColumnSearchStrategy { } if (returnOccurence == null) { - returnOccurence = ifNoMatch === 'returnNotFound' ? 'first' : 'last' + returnOccurence = ordering === 'none' ? 'first' : 'last' } const resultUsingColumnIndex = this.findUsingColumnIndex(searchKey, rangeValue, returnOccurence) From 8f7bdd42ea26e1f76e06b1f0e3802195a3316537 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Mon, 23 Dec 2024 17:09:17 +0100 Subject: [PATCH 33/45] Implement wildcard match for XLOOKUP --- src/Lookup/AdvancedFind.ts | 20 ++++--- src/Lookup/ColumnIndex.ts | 6 +- src/Lookup/SearchStrategy.ts | 6 +- src/interpreter/plugin/LookupPlugin.ts | 19 +++--- test/interpreter/function-xlookup.spec.ts | 72 +++++++++++++++++++++-- 5 files changed, 97 insertions(+), 26 deletions(-) diff --git a/src/Lookup/AdvancedFind.ts b/src/Lookup/AdvancedFind.ts index 276fcd3c6f..359c5a7ea2 100644 --- a/src/Lookup/AdvancedFind.ts +++ b/src/Lookup/AdvancedFind.ts @@ -11,7 +11,7 @@ import { RawNoErrorScalarValue } from '../interpreter/InterpreterValue' import {SimpleRangeValue} from '../SimpleRangeValue' -import {SearchOptions} from './SearchStrategy' +import {AdvancedFindOptions, SearchOptions} from './SearchStrategy' import {forceNormalizeString} from '../interpreter/ArithmeticHelper' import {compare, findLastOccurrenceInOrderedRange} from '../interpreter/binarySearch' @@ -23,15 +23,17 @@ export abstract class AdvancedFind { ) { } - public advancedFind(keyMatcher: (arg: RawInterpreterValue) => boolean, rangeValue: SimpleRangeValue): number { - let values: InternalScalarValue[] + public advancedFind(keyMatcher: (arg: RawInterpreterValue) => boolean, rangeValue: SimpleRangeValue, { returnOccurence }: AdvancedFindOptions = { returnOccurence: 'first' }): number { const range = rangeValue.range - if (range === undefined) { - values = rangeValue.valuesFromTopLeftCorner() - } else { - values = this.dependencyGraph.computeListOfValuesInRange(range) - } - for (let i = 0; i < values.length; i++) { + const values: InternalScalarValue[] = (range === undefined) + ? rangeValue.valuesFromTopLeftCorner() + : this.dependencyGraph.computeListOfValuesInRange(range) + + const initialIterationIndex = returnOccurence === 'first' ? 0 : values.length-1 + const iterationCondition = returnOccurence === 'first' ? (i: number) => i < values.length : (i: number) => i >= 0 + const incrementIndex = returnOccurence === 'first' ? (i: number) => i+1 : (i: number) => i-1 + + for (let i = initialIterationIndex; iterationCondition(i); i = incrementIndex(i)) { if (keyMatcher(getRawValue(values[i]))) { return i } diff --git a/src/Lookup/ColumnIndex.ts b/src/Lookup/ColumnIndex.ts index f0726b5cf6..b70e5767bc 100644 --- a/src/Lookup/ColumnIndex.ts +++ b/src/Lookup/ColumnIndex.ts @@ -23,7 +23,7 @@ import {LazilyTransformingAstService} from '../LazilyTransformingAstService' import {ColumnsSpan, RowsSpan} from '../Span' import {Statistics, StatType} from '../statistics' import {ColumnBinarySearch} from './ColumnBinarySearch' -import {ColumnSearchStrategy, SearchOptions} from './SearchStrategy' +import {AdvancedFindOptions, ColumnSearchStrategy, SearchOptions} from './SearchStrategy' import {Maybe} from '../Maybe' import {AbsoluteCellRange} from '../AbsoluteCellRange' @@ -162,8 +162,8 @@ export class ColumnIndex implements ColumnSearchStrategy { } - public advancedFind(keyMatcher: (arg: RawInterpreterValue) => boolean, range: SimpleRangeValue): number { - return this.binarySearchStrategy.advancedFind(keyMatcher, range) + public advancedFind(keyMatcher: (arg: RawInterpreterValue) => boolean, range: SimpleRangeValue, options: AdvancedFindOptions = { returnOccurence: 'first' }): number { + return this.binarySearchStrategy.advancedFind(keyMatcher, range, options) } public addColumns(columnsSpan: ColumnsSpan) { diff --git a/src/Lookup/SearchStrategy.ts b/src/Lookup/SearchStrategy.ts index 4965c17f24..9e6ab7a0f9 100644 --- a/src/Lookup/SearchStrategy.ts +++ b/src/Lookup/SearchStrategy.ts @@ -20,13 +20,17 @@ export interface SearchOptions { returnOccurence?: 'first' | 'last', } +export interface AdvancedFindOptions { + returnOccurence?: 'first' | 'last' +} + export interface SearchStrategy { /* * WARNING: Finding lower/upper bounds in unordered ranges is not supported. When ordering === 'none', assumes matchExactly === true */ find(searchKey: RawNoErrorScalarValue, range: SimpleRangeValue, options: SearchOptions): number, - advancedFind(keyMatcher: (arg: RawInterpreterValue) => boolean, range: SimpleRangeValue): number, + advancedFind(keyMatcher: (arg: RawInterpreterValue) => boolean, range: SimpleRangeValue, options: AdvancedFindOptions): number, } export interface ColumnSearchStrategy extends SearchStrategy { diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index 187860e883..32c2d47bff 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -149,6 +149,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech const lookupRange = lookupRangeValue instanceof SimpleRangeValue ? lookupRangeValue : SimpleRangeValue.fromScalar(lookupRangeValue) const returnRange = returnRangeValue instanceof SimpleRangeValue ? returnRangeValue : SimpleRangeValue.fromScalar(returnRangeValue) + const isWildcardMatchMode = matchMode === 2 const searchOptions: SearchOptions = { ordering: searchMode === 2 ? 'asc' : searchMode === -2 ? 'desc' : 'none', returnOccurence: searchMode === -1 ? 'last' : 'first', @@ -159,7 +160,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech : 'returnNotFound' } - return this.doXlookup(zeroIfEmpty(key), lookupRange, returnRange, notFoundFlag, searchOptions) + return this.doXlookup(zeroIfEmpty(key), lookupRange, returnRange, notFoundFlag, isWildcardMatchMode, searchOptions) }) } @@ -200,11 +201,13 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech }) } - protected searchInRange(key: RawNoErrorScalarValue, range: SimpleRangeValue, searchOptions: SearchOptions, searchStrategy: SearchStrategy): number { - if (searchOptions.ordering === 'none' && typeof key === 'string' && this.arithmeticHelper.requiresRegex(key)) { + protected searchInRange(key: RawNoErrorScalarValue, range: SimpleRangeValue, isWildcardMatchMode: boolean, searchOptions: SearchOptions, searchStrategy: SearchStrategy): number { + // if (searchOptions.ordering === 'none' && typeof key === 'string' && this.arithmeticHelper.requiresRegex(key)) { + if (isWildcardMatchMode && typeof key === 'string' && this.arithmeticHelper.requiresRegex(key)) { return searchStrategy.advancedFind( this.arithmeticHelper.eqMatcherFunction(key), - range + range, + { returnOccurence: searchOptions.returnOccurence } ) } @@ -220,7 +223,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech } else { searchedRange = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(range.start, 1, range.height()), this.dependencyGraph) } - const rowIndex = this.searchInRange(key, searchedRange, searchOptions, this.columnSearch) + const rowIndex = this.searchInRange(key, searchedRange, searchOptions.ordering === 'none', searchOptions, this.columnSearch) this.dependencyGraph.stats.end(StatType.VLOOKUP) @@ -250,7 +253,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech } else { searchedRange = SimpleRangeValue.onlyRange(AbsoluteCellRange.spanFrom(range.start, range.width(), 1), this.dependencyGraph) } - const colIndex = this.searchInRange(key, searchedRange, searchOptions, this.rowSearch) + const colIndex = this.searchInRange(key, searchedRange, searchOptions.ordering === 'none', searchOptions, this.rowSearch) if (colIndex === -1) { return new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) @@ -270,7 +273,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech return value } - private doXlookup(key: RawNoErrorScalarValue, lookupRange: SimpleRangeValue, returnRange: SimpleRangeValue, notFoundFlag: any, searchOptions: SearchOptions): InterpreterValue { + private doXlookup(key: RawNoErrorScalarValue, lookupRange: SimpleRangeValue, returnRange: SimpleRangeValue, notFoundFlag: any, isWildcardMatchMode: boolean, searchOptions: SearchOptions): InterpreterValue { const isVerticalSearch = lookupRange.width() === 1 && returnRange.height() === lookupRange.height() const isHorizontalSearch = lookupRange.height() === 1 && returnRange.width() === lookupRange.width() @@ -279,7 +282,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech } const searchStrategy = isVerticalSearch ? this.columnSearch : this.rowSearch - const indexFound = this.searchInRange(key, lookupRange, searchOptions, searchStrategy) + const indexFound = this.searchInRange(key, lookupRange, isWildcardMatchMode, searchOptions, searchStrategy) if (indexFound === -1) { return (notFoundFlag == ErrorType.NA) ? new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) : notFoundFlag diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 3d2ffbede8..50c9d9446d 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -172,6 +172,15 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual('b') }) + it('should not perform the wildcard match unless matchMode=2', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("a?b*", A2:E2, A2:E2)'], + ["a", "axxb", "a1b111", "a2b222", "x"], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.ValueNotFound)) + }) + describe('when lookupArray is a single-cell range', () => { it('works when returnArray is also a single-cell range', () => { const engine = HyperFormula.buildFromArray([ @@ -749,8 +758,63 @@ describe('Function XLOOKUP', () => { }) }) - describe('2, performs a wildcard match', () => { - // TODO + describe('2 (wildcard match)', () => { + describe('for a horizontal range', () => { + it('when searchMode = 1, returns the first matching item', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("a?b*", A2:E2, A2:E2, "NotFound", 2, 1)'], + ["a", "axxb", "a1b111", "a2b222", "x"], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual("a1b111") + }) + + it('when searchMode = 2, returns the first matching item', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("a?b*", A2:E2, A2:E2, "NotFound", 2, 2)'], + ["a", "axxb", "a1b111", "a2b222", "x"], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual("a1b111") + }) + + it('when searchMode = -2, returns the first matching item', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("a?b*", A2:E2, A2:E2, "NotFound", 2, -2)'], + ["a", "axxb", "a1b111", "a2b222", "x"], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual("a1b111") + }) + + it('when searchMode = -1, returns the last matching item', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("a?b*", A2:E2, A2:E2, "NotFound", 2, -1)'], + ["a", "axxb", "a1b111", "a2b222", "x"], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual("a2b222") + }) + + it('when there are no matching items, returns NotFound (all searchModes)', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("t?b*", A5:E5, A5:E5, "NotFound", 2, 1)'], + ['=XLOOKUP("t?b*", A5:E5, A5:E5, "NotFound", 2, -1)'], + ['=XLOOKUP("t?b*", A5:E5, A5:E5, "NotFound", 2, 2)'], + ['=XLOOKUP("t?b*", A5:E5, A5:E5, "NotFound", 2, -2)'], + ["a", "axxb", "a1b111", "a2b222", "x"], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual("NotFound") + expect(engine.getCellValue(adr('A2'))).toEqual("NotFound") + expect(engine.getCellValue(adr('A3'))).toEqual("NotFound") + expect(engine.getCellValue(adr('A4'))).toEqual("NotFound") + }) + }) + + describe('for a vertical range', () => { + // TODO + }) }) }) @@ -1097,6 +1161,4 @@ describe('Function XLOOKUP', () => { }) }) -// TODO: -// TODO: test returnUpperBound -// - setup debugger, upgrade to premium +// TODO: setup debugger, upgrade to premium From 56b4d832020229497044a8adb2acc7350ebced52 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Thu, 2 Jan 2025 11:40:52 +0100 Subject: [PATCH 34/45] Add more unit tests --- package.json | 2 +- test/interpreter/function-xlookup.spec.ts | 109 +++++++++++++++++----- 2 files changed, 89 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 9640b05203..e436919b2c 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "verify:typings": "tsc --noEmit", "test": "npm-run-all lint test:unit test:browser", "test:unit": "cross-env NODE_ICU_DATA=node_modules/full-icu jest", - "test:watch": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --watch", + "test:watch": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --watch xlookup", "test:coverage": "npm run test:unit -- --coverage", "test:logMemory": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --runInBand --logHeapUsage", "test:unit.ci": "cross-env NODE_ICU_DATA=node_modules/full-icu node --expose-gc ./node_modules/jest/bin/jest --forceExit", diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 50c9d9446d..c12d36a7ad 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -175,7 +175,7 @@ describe('Function XLOOKUP', () => { it('should not perform the wildcard match unless matchMode=2', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP("a?b*", A2:E2, A2:E2)'], - ["a", "axxb", "a1b111", "a2b222", "x"], + ['a', 'axxb', 'a1b111', 'a2b222', 'x'], ]) expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.ValueNotFound)) @@ -547,7 +547,7 @@ describe('Function XLOOKUP', () => { [43, 44, 45, 46, 47], ], { useColumnIndex: false }) - expect(engine.getCellValue(adr('A1'))).toEqual("NotFound") + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') }) }) @@ -585,7 +585,7 @@ describe('Function XLOOKUP', () => { [100, 90, 80, 70, 60], ], { useColumnIndex: false }) - expect(engine.getCellValue(adr('A1'))).toEqual("NotFound") + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') }) }) @@ -763,37 +763,37 @@ describe('Function XLOOKUP', () => { it('when searchMode = 1, returns the first matching item', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP("a?b*", A2:E2, A2:E2, "NotFound", 2, 1)'], - ["a", "axxb", "a1b111", "a2b222", "x"], + ['a', 'axxb', 'a1b111', 'a2b222', 'x'], ], { useColumnIndex: false }) - expect(engine.getCellValue(adr('A1'))).toEqual("a1b111") + expect(engine.getCellValue(adr('A1'))).toEqual('a1b111') }) it('when searchMode = 2, returns the first matching item', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP("a?b*", A2:E2, A2:E2, "NotFound", 2, 2)'], - ["a", "axxb", "a1b111", "a2b222", "x"], + ['a', 'axxb', 'a1b111', 'a2b222', 'x'], ], { useColumnIndex: false }) - expect(engine.getCellValue(adr('A1'))).toEqual("a1b111") + expect(engine.getCellValue(adr('A1'))).toEqual('a1b111') }) it('when searchMode = -2, returns the first matching item', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP("a?b*", A2:E2, A2:E2, "NotFound", 2, -2)'], - ["a", "axxb", "a1b111", "a2b222", "x"], + ['a', 'axxb', 'a1b111', 'a2b222', 'x'], ], { useColumnIndex: false }) - expect(engine.getCellValue(adr('A1'))).toEqual("a1b111") + expect(engine.getCellValue(adr('A1'))).toEqual('a1b111') }) it('when searchMode = -1, returns the last matching item', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP("a?b*", A2:E2, A2:E2, "NotFound", 2, -1)'], - ["a", "axxb", "a1b111", "a2b222", "x"], + ['a', 'axxb', 'a1b111', 'a2b222', 'x'], ], { useColumnIndex: false }) - expect(engine.getCellValue(adr('A1'))).toEqual("a2b222") + expect(engine.getCellValue(adr('A1'))).toEqual('a2b222') }) it('when there are no matching items, returns NotFound (all searchModes)', () => { @@ -802,18 +802,87 @@ describe('Function XLOOKUP', () => { ['=XLOOKUP("t?b*", A5:E5, A5:E5, "NotFound", 2, -1)'], ['=XLOOKUP("t?b*", A5:E5, A5:E5, "NotFound", 2, 2)'], ['=XLOOKUP("t?b*", A5:E5, A5:E5, "NotFound", 2, -2)'], - ["a", "axxb", "a1b111", "a2b222", "x"], + ['a', 'axxb', 'a1b111', 'a2b222', 'x'], ], { useColumnIndex: false }) - expect(engine.getCellValue(adr('A1'))).toEqual("NotFound") - expect(engine.getCellValue(adr('A2'))).toEqual("NotFound") - expect(engine.getCellValue(adr('A3'))).toEqual("NotFound") - expect(engine.getCellValue(adr('A4'))).toEqual("NotFound") + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') + expect(engine.getCellValue(adr('A2'))).toEqual('NotFound') + expect(engine.getCellValue(adr('A3'))).toEqual('NotFound') + expect(engine.getCellValue(adr('A4'))).toEqual('NotFound') }) }) describe('for a vertical range', () => { - // TODO + it('when searchMode = 1, returns the first matching item', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("a?b*", A2:A6, A2:A6, "NotFound", 2, 1)'], + ['a'], + ['axxb'], + ['a1b111'], + ['a2b222'], + ['x'], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual('a1b111') + }) + + it('when searchMode = 2, returns the first matching item', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("a?b*", A2:A6, A2:A6, "NotFound", 2, 2)'], + ['a'], + ['axxb'], + ['a1b111'], + ['a2b222'], + ['x'], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual('a1b111') + }) + + it('when searchMode = -2, returns the first matching item', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("a?b*", A2:A6, A2:A6, "NotFound", 2, -2)'], + ['a'], + ['axxb'], + ['a1b111'], + ['a2b222'], + ['x'], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual('a1b111') + }) + + it('when searchMode = -1, returns the last matching item', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("a?b*", A2:A6, A2:A6, "NotFound", 2, -1)'], + ['a'], + ['axxb'], + ['a1b111'], + ['a2b222'], + ['x'], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual('a2b222') + }) + + it('when there are no matching items, returns NotFound (all searchModes)', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("t?b*", A5:A9, A5:A9, "NotFound", 2, 1)'], + ['=XLOOKUP("t?b*", A5:A9, A5:A9, "NotFound", 2, -1)'], + ['=XLOOKUP("t?b*", A5:A9, A5:A9, "NotFound", 2, 2)'], + ['=XLOOKUP("t?b*", A5:A9, A5:A9, "NotFound", 2, -2)'], + ['a'], + ['axxb'], + ['a1b111'], + ['a2b222'], + ['x'], + ], { useColumnIndex: false }) + + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') + expect(engine.getCellValue(adr('A2'))).toEqual('NotFound') + expect(engine.getCellValue(adr('A3'))).toEqual('NotFound') + expect(engine.getCellValue(adr('A4'))).toEqual('NotFound') + }) }) }) }) @@ -854,7 +923,7 @@ describe('Function XLOOKUP', () => { [43, 44, 45, 46, 47], ], { useColumnIndex: true }) - expect(engine.getCellValue(adr('A1'))).toEqual("NotFound") + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') }) }) @@ -892,7 +961,7 @@ describe('Function XLOOKUP', () => { [100, 90, 80, 70, 60], ], { useColumnIndex: true }) - expect(engine.getCellValue(adr('A1'))).toEqual("NotFound") + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') }) }) @@ -1160,5 +1229,3 @@ describe('Function XLOOKUP', () => { }) }) }) - -// TODO: setup debugger, upgrade to premium From 4ccbca47cecf7eda179c338c965785454571192a Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Thu, 2 Jan 2025 11:44:27 +0100 Subject: [PATCH 35/45] Add unit tests for wildcard match with ColumnIndex column search strategy --- test/interpreter/function-xlookup.spec.ts | 128 +++++++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index c12d36a7ad..99957d5bfc 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -1134,8 +1134,132 @@ describe('Function XLOOKUP', () => { }) }) - describe('2, performs a wildcard match', () => { - // TODO + describe('2 (wildcard match)', () => { + describe('for a horizontal range', () => { + it('when searchMode = 1, returns the first matching item', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("a?b*", A2:E2, A2:E2, "NotFound", 2, 1)'], + ['a', 'axxb', 'a1b111', 'a2b222', 'x'], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual('a1b111') + }) + + it('when searchMode = 2, returns the first matching item', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("a?b*", A2:E2, A2:E2, "NotFound", 2, 2)'], + ['a', 'axxb', 'a1b111', 'a2b222', 'x'], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual('a1b111') + }) + + it('when searchMode = -2, returns the first matching item', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("a?b*", A2:E2, A2:E2, "NotFound", 2, -2)'], + ['a', 'axxb', 'a1b111', 'a2b222', 'x'], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual('a1b111') + }) + + it('when searchMode = -1, returns the last matching item', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("a?b*", A2:E2, A2:E2, "NotFound", 2, -1)'], + ['a', 'axxb', 'a1b111', 'a2b222', 'x'], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual('a2b222') + }) + + it('when there are no matching items, returns NotFound (all searchModes)', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("t?b*", A5:E5, A5:E5, "NotFound", 2, 1)'], + ['=XLOOKUP("t?b*", A5:E5, A5:E5, "NotFound", 2, -1)'], + ['=XLOOKUP("t?b*", A5:E5, A5:E5, "NotFound", 2, 2)'], + ['=XLOOKUP("t?b*", A5:E5, A5:E5, "NotFound", 2, -2)'], + ['a', 'axxb', 'a1b111', 'a2b222', 'x'], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') + expect(engine.getCellValue(adr('A2'))).toEqual('NotFound') + expect(engine.getCellValue(adr('A3'))).toEqual('NotFound') + expect(engine.getCellValue(adr('A4'))).toEqual('NotFound') + }) + }) + + describe('for a vertical range', () => { + it('when searchMode = 1, returns the first matching item', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("a?b*", A2:A6, A2:A6, "NotFound", 2, 1)'], + ['a'], + ['axxb'], + ['a1b111'], + ['a2b222'], + ['x'], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual('a1b111') + }) + + it('when searchMode = 2, returns the first matching item', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("a?b*", A2:A6, A2:A6, "NotFound", 2, 2)'], + ['a'], + ['axxb'], + ['a1b111'], + ['a2b222'], + ['x'], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual('a1b111') + }) + + it('when searchMode = -2, returns the first matching item', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("a?b*", A2:A6, A2:A6, "NotFound", 2, -2)'], + ['a'], + ['axxb'], + ['a1b111'], + ['a2b222'], + ['x'], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual('a1b111') + }) + + it('when searchMode = -1, returns the last matching item', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("a?b*", A2:A6, A2:A6, "NotFound", 2, -1)'], + ['a'], + ['axxb'], + ['a1b111'], + ['a2b222'], + ['x'], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual('a2b222') + }) + + it('when there are no matching items, returns NotFound (all searchModes)', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("t?b*", A5:A9, A5:A9, "NotFound", 2, 1)'], + ['=XLOOKUP("t?b*", A5:A9, A5:A9, "NotFound", 2, -1)'], + ['=XLOOKUP("t?b*", A5:A9, A5:A9, "NotFound", 2, 2)'], + ['=XLOOKUP("t?b*", A5:A9, A5:A9, "NotFound", 2, -2)'], + ['a'], + ['axxb'], + ['a1b111'], + ['a2b222'], + ['x'], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqual('NotFound') + expect(engine.getCellValue(adr('A2'))).toEqual('NotFound') + expect(engine.getCellValue(adr('A3'))).toEqual('NotFound') + expect(engine.getCellValue(adr('A4'))).toEqual('NotFound') + }) + }) }) }) From 1fedb19f7e6bc5b7e828aa52075d156311394c05 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Thu, 2 Jan 2025 12:00:52 +0100 Subject: [PATCH 36/45] Improve descriptions of some unit tests --- package.json | 3 +- test/interpreter/function-xlookup.spec.ts | 123 ++++------------------ 2 files changed, 24 insertions(+), 102 deletions(-) diff --git a/package.json b/package.json index e436919b2c..06f85405a2 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,8 @@ "verify:typings": "tsc --noEmit", "test": "npm-run-all lint test:unit test:browser", "test:unit": "cross-env NODE_ICU_DATA=node_modules/full-icu jest", - "test:watch": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --watch xlookup", + "test:watch": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --watch", + "test:watch-tmp": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --watch xlookup", "test:coverage": "npm run test:unit -- --coverage", "test:logMemory": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --runInBand --logHeapUsage", "test:unit.ci": "cross-env NODE_ICU_DATA=node_modules/full-icu node --expose-gc ./node_modules/jest/bin/jest --forceExit", diff --git a/test/interpreter/function-xlookup.spec.ts b/test/interpreter/function-xlookup.spec.ts index 99957d5bfc..f1e0f30767 100644 --- a/test/interpreter/function-xlookup.spec.ts +++ b/test/interpreter/function-xlookup.spec.ts @@ -143,7 +143,7 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A4'))).toEqual('not found') }) - it('works when returnArray is shifted (verical search)', () => { + it('works when returnArray is shifted (vertical search)', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(2, B1:B3, C11:C13)', 1], ['', 2], @@ -181,8 +181,17 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.ValueNotFound)) }) + it('should not perform the wildcard match unless matchMode=2 (ColumnIndex)', () => { + const engine = HyperFormula.buildFromArray([ + ['=XLOOKUP("a?b*", A2:E2, A2:E2)'], + ['a', 'axxb', 'a1b111', 'a2b222', 'x'], + ], { useColumnIndex: true }) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.ValueNotFound)) + }) + describe('when lookupArray is a single-cell range', () => { - it('works when returnArray is also a single-cell range', () => { + it('returns single cell, when returnArray is also a single-cell range', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(1, B1:B1, C1:C1)', 1, 'a'], ]) @@ -190,7 +199,7 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual('a') }) - it('works when returnArray is a vertical range', () => { + it('returns a vertical range, when returnArray is a vertical range', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(1, B1:B1, A3:A4)', 1], [], @@ -202,7 +211,7 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A2'))).toEqual('c') }) - it('works when returnArray is a horizontal range', () => { + it('returns a horizontal range, when returnArray is a horizontal range', () => { const engine = HyperFormula.buildFromArray([ [1, 'b', 'c'], ['=XLOOKUP(1, A1:A1, B1:C1)'], @@ -512,7 +521,7 @@ describe('Function XLOOKUP', () => { }) describe('with BinarySearch column search strategy, when provided with matchMode = ', () => { - describe('-1 (looking for a lower bound', () => { + describe('-1 (looking for a lower bound)', () => { describe('in array ordered ascending', () => { it('returns exact match if exists', () => { const engine = HyperFormula.buildFromArray([ @@ -589,7 +598,7 @@ describe('Function XLOOKUP', () => { }) }) - it('works for unsorted horizontal range', () => { + it('returns a lower bound if there is no match in unsorted horizontal range', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(3, A2:E2, A3:E3, "NotFound", -1, 1)'], [2, 1, 4, 2, 5], @@ -599,7 +608,7 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual(1) }) - it('works for unsorted vertical range', () => { + it('returns a lower bound if there is no match in unsorted vertical range', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(3, A2:A6, B2:B6, "NotFound", -1, 1)'], [2, 1], @@ -611,28 +620,6 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual(1) }) - - it('works for sorted horizontal range', () => { - const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(3, A2:E2, A2:E2, "NotFound", -1, 2)'], - [1, 2, 2, 5, 6], - ], { useColumnIndex: false }) - - expect(engine.getCellValue(adr('A1'))).toEqual(2) - }) - - it('works for sorted vertical range', () => { - const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", -1, 2)'], - [1], - [2], - [4], - [5], - [6] - ], { useColumnIndex: false }) - - expect(engine.getCellValue(adr('A1'))).toEqual(2) - }) }) describe('1 (looking for a upper bound', () => { @@ -712,7 +699,7 @@ describe('Function XLOOKUP', () => { }) }) - it('works for unsorted horizontal range', () => { + it('returns an upper bound if there is no match in unsorted horizontal range', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(3, A2:E2, A3:E3, "NotFound", 1, 1)'], [2, 1, 4, 2, 5], @@ -722,7 +709,7 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual(3) }) - it('works for unsorted vertical range', () => { + it('returns an upper bound if there is no match in unsorted vertical range', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(3, A2:A6, B2:B6, "NotFound", 1, 1)'], [2, 1], @@ -734,28 +721,6 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual(3) }) - - it('works for sorted horizontal range', () => { - const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(3, A2:E2, A2:E2, "NotFound", 1, 2)'], - [1, 2, 5, 5, 6], - ], { useColumnIndex: false }) - - expect(engine.getCellValue(adr('A1'))).toEqual(5) - }) - - it('works for sorted vertical range', () => { - const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", 1, 2)'], - [1], - [2], - [4], - [5], - [6] - ], { useColumnIndex: false }) - - expect(engine.getCellValue(adr('A1'))).toEqual(4) - }) }) describe('2 (wildcard match)', () => { @@ -965,7 +930,7 @@ describe('Function XLOOKUP', () => { }) }) - it('works for unsorted horizontal range', () => { + it('returns a lower bound if there is no match in unsorted horizontal range', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(3, A2:E2, A3:E3, "NotFound", -1, 1)'], [2, 1, 4, 2, 5], @@ -975,7 +940,7 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual(1) }) - it('works for unsorted vertical range', () => { + it('returns a lower bound if there is no match in unsorted vertical range', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(3, A2:A6, B2:B6, "NotFound", -1, 1)'], [2, 1], @@ -987,28 +952,6 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual(1) }) - - it('works for sorted horizontal range', () => { - const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(3, A2:E2, A2:E2, "NotFound", -1, 2)'], - [1, 2, 2, 5, 6], - ], { useColumnIndex: true }) - - expect(engine.getCellValue(adr('A1'))).toEqual(2) - }) - - it('works for sorted vertical range', () => { - const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", -1, 2)'], - [1], - [2], - [4], - [5], - [6] - ], { useColumnIndex: true }) - - expect(engine.getCellValue(adr('A1'))).toEqual(2) - }) }) describe('1 (looking for a upper bound', () => { @@ -1088,7 +1031,7 @@ describe('Function XLOOKUP', () => { }) }) - it('works for unsorted horizontal range', () => { + it('returns an upper bound if there is no match in unsorted horizontal range', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(3, A2:E2, A3:E3, "NotFound", 1, 1)'], [2, 1, 4, 2, 5], @@ -1098,7 +1041,7 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual(3) }) - it('works for unsorted vertical range', () => { + it('returns an upper bound if there is no match in unsorted vertical range', () => { const engine = HyperFormula.buildFromArray([ ['=XLOOKUP(3, A2:A6, B2:B6, "NotFound", 1, 1)'], [2, 1], @@ -1110,28 +1053,6 @@ describe('Function XLOOKUP', () => { expect(engine.getCellValue(adr('A1'))).toEqual(3) }) - - it('works for sorted horizontal range', () => { - const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(3, A2:E2, A2:E2, "NotFound", 1, 2)'], - [1, 2, 5, 5, 6], - ], { useColumnIndex: true }) - - expect(engine.getCellValue(adr('A1'))).toEqual(5) - }) - - it('works for sorted vertical range', () => { - const engine = HyperFormula.buildFromArray([ - ['=XLOOKUP(3, A2:A6, A2:A6, "NotFound", 1, 2)'], - [1], - [2], - [4], - [5], - [6] - ], { useColumnIndex: true }) - - expect(engine.getCellValue(adr('A1'))).toEqual(4) - }) }) describe('2 (wildcard match)', () => { From a6d3049827f18e323419a1fea01a6a43fe81afef Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Thu, 2 Jan 2025 12:04:17 +0100 Subject: [PATCH 37/45] Fix lint error --- src/Lookup/SearchStrategy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Lookup/SearchStrategy.ts b/src/Lookup/SearchStrategy.ts index 9e6ab7a0f9..23e1d51348 100644 --- a/src/Lookup/SearchStrategy.ts +++ b/src/Lookup/SearchStrategy.ts @@ -21,7 +21,7 @@ export interface SearchOptions { } export interface AdvancedFindOptions { - returnOccurence?: 'first' | 'last' + returnOccurence?: 'first' | 'last', } export interface SearchStrategy { From 814049bb2308dd3647dc2fe88f8c6490665d68e5 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Thu, 2 Jan 2025 12:14:48 +0100 Subject: [PATCH 38/45] Add changelog entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8436d09fb..79722d3f84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- Added a new function: XLOOKUP. [#1458](https://github.com/handsontable/hyperformula/issues/1458) + ### Changed - **Breaking change**: Changed ES module build to use `mjs` files and `exports` property in `package.json` to make importing language files possible in Node environment. [#1344](https://github.com/handsontable/hyperformula/issues/1344) From db6e4b354d2ff152c262d82cb7e55cadf875d2ed Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Thu, 2 Jan 2025 12:20:54 +0100 Subject: [PATCH 39/45] Describe XOOKUP in the built-in functions guide --- docs/guide/built-in-functions.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/guide/built-in-functions.md b/docs/guide/built-in-functions.md index 01945e5e67..27c4bcb680 100644 --- a/docs/guide/built-in-functions.md +++ b/docs/guide/built-in-functions.md @@ -212,22 +212,22 @@ Total number of functions: **{{ $page.functionsCount }}** ### Lookup and reference -| Function ID | Description | Syntax | -|:------------|:-----------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------| -| ADDRESS | Returns a cell reference as a string. | ADDRESS(Row, Column[, AbsoluteRelativeMode[, UseA1Notation[, Sheet]]]) | -| CHOOSE | Uses an index to return a value from a list of values. | CHOOSE(Index, Value1, Value2, ...ValueN) | -| COLUMN | Returns column number of a given reference or formula reference if argument not provided. | COLUMNS([Reference]) | -| COLUMNS | Returns the number of columns in the given reference. | COLUMNS(Array) | -| FORMULATEXT | Returns a formula in a given cell as a string. | FORMULATEXT(Reference) | -| HLOOKUP | Searches horizontally with reference to adjacent cells to the bottom. | HLOOKUP(Search_Criterion, Array, Index, Sort_Order) | -| HYPERLINK | Stores the url in the cell's metadata. It can be read using method [`getCellHyperlink`](../api/classes/hyperformula.md#getcellhyperlink) | HYPERLINK(Url[, LinkLabel]) | -| INDEX | Returns the contents of a cell specified by row and column number. The column number is optional and defaults to 1. | INDEX(Range, Row [, Column]) | -| MATCH | Returns the relative position of an item in an array that matches a specified value. | MATCH(Searchcriterion, Lookuparray [, MatchType]) | -| OFFSET | Returns the value of a cell offset by a certain number of rows and columns from a given reference point. | OFFSET(Reference, Rows, Columns, Height, Width) | -| 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. 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]) | +| Function ID | Description | Syntax | +|:------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------| +| ADDRESS | Returns a cell reference as a string. | ADDRESS(Row, Column[, AbsoluteRelativeMode[, UseA1Notation[, Sheet]]]) | +| CHOOSE | Uses an index to return a value from a list of values. | CHOOSE(Index, Value1, Value2, ...ValueN) | +| COLUMN | Returns column number of a given reference or formula reference if argument not provided. | COLUMNS([Reference]) | +| COLUMNS | Returns the number of columns in the given reference. | COLUMNS(Array) | +| FORMULATEXT | Returns a formula in a given cell as a string. | FORMULATEXT(Reference) | +| HLOOKUP | Searches horizontally with reference to adjacent cells to the bottom. | HLOOKUP(Search_Criterion, Array, Index, Sort_Order) | +| HYPERLINK | Stores the url in the cell's metadata. It can be read using method [`getCellHyperlink`](../api/classes/hyperformula.md#getcellhyperlink) | HYPERLINK(Url[, LinkLabel]) | +| INDEX | Returns the contents of a cell specified by row and column number. The column number is optional and defaults to 1. | INDEX(Range, Row [, Column]) | +| MATCH | Returns the relative position of an item in an array that matches a specified value. | MATCH(Searchcriterion, LookupArray [, MatchType]) | +| OFFSET | Returns the value of a cell offset by a certain number of rows and columns from a given reference point. | OFFSET(Reference, Rows, Columns, Height, Width) | +| 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 match it finds. If no match exists, then XLOOKUP can return the closest (approximate) match. | XLOOKUP(LookupValue, LookupArray, ReturnArray, [IfNotFound], [MatchMode], [SearchMode]) | ### Math and trigonometry From c42091825d360a31de22b53799ee95ef12edfd2b Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Thu, 2 Jan 2025 12:23:52 +0100 Subject: [PATCH 40/45] Rephrase XLOOKUP description --- docs/guide/built-in-functions.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/guide/built-in-functions.md b/docs/guide/built-in-functions.md index 27c4bcb680..73f9cb2432 100644 --- a/docs/guide/built-in-functions.md +++ b/docs/guide/built-in-functions.md @@ -212,22 +212,22 @@ Total number of functions: **{{ $page.functionsCount }}** ### Lookup and reference -| Function ID | Description | Syntax | -|:------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------| -| ADDRESS | Returns a cell reference as a string. | ADDRESS(Row, Column[, AbsoluteRelativeMode[, UseA1Notation[, Sheet]]]) | -| CHOOSE | Uses an index to return a value from a list of values. | CHOOSE(Index, Value1, Value2, ...ValueN) | -| COLUMN | Returns column number of a given reference or formula reference if argument not provided. | COLUMNS([Reference]) | -| COLUMNS | Returns the number of columns in the given reference. | COLUMNS(Array) | -| FORMULATEXT | Returns a formula in a given cell as a string. | FORMULATEXT(Reference) | -| HLOOKUP | Searches horizontally with reference to adjacent cells to the bottom. | HLOOKUP(Search_Criterion, Array, Index, Sort_Order) | -| HYPERLINK | Stores the url in the cell's metadata. It can be read using method [`getCellHyperlink`](../api/classes/hyperformula.md#getcellhyperlink) | HYPERLINK(Url[, LinkLabel]) | -| INDEX | Returns the contents of a cell specified by row and column number. The column number is optional and defaults to 1. | INDEX(Range, Row [, Column]) | -| MATCH | Returns the relative position of an item in an array that matches a specified value. | MATCH(Searchcriterion, LookupArray [, MatchType]) | -| OFFSET | Returns the value of a cell offset by a certain number of rows and columns from a given reference point. | OFFSET(Reference, Rows, Columns, Height, Width) | -| 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 match it finds. If no match exists, then XLOOKUP can return the closest (approximate) match. | XLOOKUP(LookupValue, LookupArray, ReturnArray, [IfNotFound], [MatchMode], [SearchMode]) | +| Function ID | Description | Syntax | +|:------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------| +| ADDRESS | Returns a cell reference as a string. | ADDRESS(Row, Column[, AbsoluteRelativeMode[, UseA1Notation[, Sheet]]]) | +| CHOOSE | Uses an index to return a value from a list of values. | CHOOSE(Index, Value1, Value2, ...ValueN) | +| COLUMN | Returns column number of a given reference or formula reference if argument not provided. | COLUMNS([Reference]) | +| COLUMNS | Returns the number of columns in the given reference. | COLUMNS(Array) | +| FORMULATEXT | Returns a formula in a given cell as a string. | FORMULATEXT(Reference) | +| HLOOKUP | Searches horizontally with reference to adjacent cells to the bottom. | HLOOKUP(Search_Criterion, Array, Index, Sort_Order) | +| HYPERLINK | Stores the url in the cell's metadata. It can be read using method [`getCellHyperlink`](../api/classes/hyperformula.md#getcellhyperlink) | HYPERLINK(Url[, LinkLabel]) | +| INDEX | Returns the contents of a cell specified by row and column number. The column number is optional and defaults to 1. | INDEX(Range, Row [, Column]) | +| MATCH | Returns the relative position of an item in an array that matches a specified value. | MATCH(Searchcriterion, LookupArray [, MatchType]) | +| OFFSET | Returns the value of a cell offset by a certain number of rows and columns from a given reference point. | OFFSET(Reference, Rows, Columns, Height, Width) | +| 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 | Searches for a key in a range and returns the item corresponding to the match it finds. If no match exists, then XLOOKUP can return the closest (approximate) match. | XLOOKUP(LookupValue, LookupArray, ReturnArray, [IfNotFound], [MatchMode], [SearchMode]) | ### Math and trigonometry From e70a56b2b53b9aeb9498e5a6520dec982fd9e982 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Thu, 2 Jan 2025 12:39:36 +0100 Subject: [PATCH 41/45] Remove redundant\ comments --- src/interpreter/plugin/LookupPlugin.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index 32c2d47bff..2963e9bb37 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -17,10 +17,6 @@ import { SimpleRangeValue } from '../../SimpleRangeValue' import { FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions } from './FunctionPlugin' import { ArraySize } from '../../ArraySize' -// enum MatchMode { -// matchExactly -// } - export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypecheck { public static implementedFunctions: ImplementedFunctions = { 'VLOOKUP': { @@ -202,7 +198,6 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech } protected searchInRange(key: RawNoErrorScalarValue, range: SimpleRangeValue, isWildcardMatchMode: boolean, searchOptions: SearchOptions, searchStrategy: SearchStrategy): number { - // if (searchOptions.ordering === 'none' && typeof key === 'string' && this.arithmeticHelper.requiresRegex(key)) { if (isWildcardMatchMode && typeof key === 'string' && this.arithmeticHelper.requiresRegex(key)) { return searchStrategy.advancedFind( this.arithmeticHelper.eqMatcherFunction(key), From a398fb1b0bc2d15efe10c0e2edc9f7df36aa27cf Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Thu, 2 Jan 2025 17:03:53 +0100 Subject: [PATCH 42/45] Refactor findLastOccurrenceInOrderedRange function --- src/interpreter/binarySearch.ts | 39 +++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/interpreter/binarySearch.ts b/src/interpreter/binarySearch.ts index d95f19d7c5..cc6f579689 100644 --- a/src/interpreter/binarySearch.ts +++ b/src/interpreter/binarySearch.ts @@ -46,44 +46,51 @@ export function findLastOccurrenceInOrderedRange( const foundIndex = findLastMatchingIndex(index => compareFn(searchKey, getValueFromIndexFn(index)) >= 0, start, end) const foundValue = getValueFromIndexFn(foundIndex) - if (foundIndex === NOT_FOUND) { - if (ifNoMatch === 'returnLowerBound' && orderingDirection === 'desc') { - return 0 - } - - if (ifNoMatch === 'returnUpperBound' && orderingDirection === 'asc') { - return 0 - } - - return NOT_FOUND - } - - if (typeof foundValue !== typeof searchKey) { - return NOT_FOUND - } - if (foundValue === searchKey) { return foundIndex - start } if (ifNoMatch === 'returnLowerBound') { + if (foundIndex === NOT_FOUND) { + return orderingDirection === 'asc' ? NOT_FOUND : 0 + } + + // czy to sie zdarza? + if (typeof foundValue !== typeof searchKey) { + return NOT_FOUND + } + + // here: foundValue !== searchKey if (orderingDirection === 'asc') { return foundIndex - start } + // orderingDirection === 'desc' const nextIndex = foundIndex+1 return nextIndex <= end ? nextIndex - start : NOT_FOUND } if (ifNoMatch === 'returnUpperBound') { + if (foundIndex === NOT_FOUND) { + return orderingDirection === 'asc' ? 0 : NOT_FOUND + } + + // czy to sie zdarza? + if (typeof foundValue !== typeof searchKey) { + return NOT_FOUND + } + + // here: foundValue !== searchKey if (orderingDirection === 'desc') { return foundIndex - start } + // orderingDirection === 'asc' const nextIndex = foundIndex+1 return nextIndex <= end ? nextIndex - start : NOT_FOUND } + // ifNoMatch === 'returnNotFound' return NOT_FOUND } From 24f7d043968bd284d3c0ecf21ba6e33ff6e7c547 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Thu, 2 Jan 2025 17:06:12 +0100 Subject: [PATCH 43/45] Fix misspelled word occurrence --- src/DependencyGraph/TopSort.ts | 2 +- src/Lookup/AdvancedFind.ts | 24 ++++++++++---------- src/Lookup/ColumnIndex.ts | 20 ++++++++-------- src/Lookup/SearchStrategy.ts | 4 ++-- src/interpreter/plugin/LookupPlugin.ts | 4 ++-- test/interpreter/binary-search.spec.ts | 2 +- test/interpreter/function-substitute.spec.ts | 2 +- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/DependencyGraph/TopSort.ts b/src/DependencyGraph/TopSort.ts index c8df450fe4..c04bcde473 100644 --- a/src/DependencyGraph/TopSort.ts +++ b/src/DependencyGraph/TopSort.ts @@ -39,7 +39,7 @@ export class TopSort { * Returns vertices in order of topological sort, but vertices that are on cycles are kept separate. * * @param modifiedNodes - seed for computation. During engine init run, all of the vertices of grap. In recomputation run, changed vertices. - * @param operatingFunction - recomputes value of a node, and returns whether a change occured + * @param operatingFunction - recomputes value of a node, and returns whether a change occurred * @param onCycle - action to be performed when node is on cycle */ public getTopSortedWithSccSubgraphFrom( diff --git a/src/Lookup/AdvancedFind.ts b/src/Lookup/AdvancedFind.ts index 359c5a7ea2..f855cd224c 100644 --- a/src/Lookup/AdvancedFind.ts +++ b/src/Lookup/AdvancedFind.ts @@ -23,15 +23,15 @@ export abstract class AdvancedFind { ) { } - public advancedFind(keyMatcher: (arg: RawInterpreterValue) => boolean, rangeValue: SimpleRangeValue, { returnOccurence }: AdvancedFindOptions = { returnOccurence: 'first' }): number { + public advancedFind(keyMatcher: (arg: RawInterpreterValue) => boolean, rangeValue: SimpleRangeValue, { returnOccurrence }: AdvancedFindOptions = { returnOccurrence: 'first' }): number { const range = rangeValue.range const values: InternalScalarValue[] = (range === undefined) ? rangeValue.valuesFromTopLeftCorner() : this.dependencyGraph.computeListOfValuesInRange(range) - const initialIterationIndex = returnOccurence === 'first' ? 0 : values.length-1 - const iterationCondition = returnOccurence === 'first' ? (i: number) => i < values.length : (i: number) => i >= 0 - const incrementIndex = returnOccurence === 'first' ? (i: number) => i+1 : (i: number) => i-1 + const initialIterationIndex = returnOccurrence === 'first' ? 0 : values.length-1 + const iterationCondition = returnOccurrence === 'first' ? (i: number) => i < values.length : (i: number) => i >= 0 + const incrementIndex = returnOccurrence === 'first' ? (i: number) => i+1 : (i: number) => i-1 for (let i = initialIterationIndex; iterationCondition(i); i = incrementIndex(i)) { if (keyMatcher(getRawValue(values[i]))) { @@ -41,16 +41,16 @@ export abstract class AdvancedFind { return NOT_FOUND } - protected basicFind(searchKey: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, searchCoordinate: 'col' | 'row', { ordering, ifNoMatch, returnOccurence }: SearchOptions): number { + protected basicFind(searchKey: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, searchCoordinate: 'col' | 'row', { ordering, ifNoMatch, returnOccurrence }: SearchOptions): number { const normalizedSearchKey = typeof searchKey === 'string' ? forceNormalizeString(searchKey) : searchKey const range = rangeValue.range if (range === undefined) { - return this.findNormalizedValue(normalizedSearchKey, rangeValue.valuesFromTopLeftCorner(), ifNoMatch, returnOccurence) + return this.findNormalizedValue(normalizedSearchKey, rangeValue.valuesFromTopLeftCorner(), ifNoMatch, returnOccurrence) } if (ordering === 'none') { - return this.findNormalizedValue(normalizedSearchKey, this.dependencyGraph.computeListOfValuesInRange(range), ifNoMatch, returnOccurence) + return this.findNormalizedValue(normalizedSearchKey, this.dependencyGraph.computeListOfValuesInRange(range), ifNoMatch, returnOccurrence) } return findLastOccurrenceInOrderedRange( @@ -61,13 +61,13 @@ export abstract class AdvancedFind { ) } - protected findNormalizedValue(searchKey: RawNoErrorScalarValue, searchArray: InternalScalarValue[], ifNoMatch: 'returnLowerBound' | 'returnUpperBound' | 'returnNotFound' = 'returnNotFound', returnOccurence: 'first' | 'last' = 'first'): number { + protected findNormalizedValue(searchKey: RawNoErrorScalarValue, searchArray: InternalScalarValue[], ifNoMatch: 'returnLowerBound' | 'returnUpperBound' | 'returnNotFound' = 'returnNotFound', returnOccurrence: 'first' | 'last' = 'first'): number { const normalizedArray = searchArray .map(getRawValue) .map(val => typeof val === 'string' ? forceNormalizeString(val) : val) if (ifNoMatch === 'returnNotFound') { - return returnOccurence === 'first' ? normalizedArray.indexOf(searchKey) : normalizedArray.lastIndexOf(searchKey) + return returnOccurrence === 'first' ? normalizedArray.indexOf(searchKey) : normalizedArray.lastIndexOf(searchKey) } const compareFn = ifNoMatch === 'returnLowerBound' @@ -77,9 +77,9 @@ export abstract class AdvancedFind { let bestValue: RawNoErrorScalarValue = ifNoMatch === 'returnLowerBound' ? -Infinity : Infinity let bestIndex = NOT_FOUND - const initialIterationIndex = returnOccurence === 'first' ? 0 : normalizedArray.length-1 - const iterationCondition = returnOccurence === 'first' ? (i: number) => i < normalizedArray.length : (i: number) => i >= 0 - const incrementIndex = returnOccurence === 'first' ? (i: number) => i+1 : (i: number) => i-1 + const initialIterationIndex = returnOccurrence === 'first' ? 0 : normalizedArray.length-1 + const iterationCondition = returnOccurrence === 'first' ? (i: number) => i < normalizedArray.length : (i: number) => i >= 0 + const incrementIndex = returnOccurrence === 'first' ? (i: number) => i+1 : (i: number) => i-1 for (let i = initialIterationIndex; iterationCondition(i); i = incrementIndex(i)) { const value = normalizedArray[i] as RawNoErrorScalarValue diff --git a/src/Lookup/ColumnIndex.ts b/src/Lookup/ColumnIndex.ts index b70e5767bc..8d9877471b 100644 --- a/src/Lookup/ColumnIndex.ts +++ b/src/Lookup/ColumnIndex.ts @@ -107,20 +107,20 @@ export class ColumnIndex implements ColumnSearchStrategy { } } - public find(searchKey: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, { ordering, ifNoMatch, returnOccurence }: SearchOptions): number { + public find(searchKey: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, { ordering, ifNoMatch, returnOccurrence }: SearchOptions): number { if (ifNoMatch == null) { ifNoMatch = 'returnNotFound' } - if (returnOccurence == null) { - returnOccurence = ordering === 'none' ? 'first' : 'last' + if (returnOccurrence == null) { + returnOccurrence = ordering === 'none' ? 'first' : 'last' } - const resultUsingColumnIndex = this.findUsingColumnIndex(searchKey, rangeValue, returnOccurence) - return resultUsingColumnIndex !== undefined ? resultUsingColumnIndex : this.binarySearchStrategy.find(searchKey, rangeValue, { ordering, ifNoMatch, returnOccurence }) + const resultUsingColumnIndex = this.findUsingColumnIndex(searchKey, rangeValue, returnOccurrence) + return resultUsingColumnIndex !== undefined ? resultUsingColumnIndex : this.binarySearchStrategy.find(searchKey, rangeValue, { ordering, ifNoMatch, returnOccurrence }) } - private findUsingColumnIndex(key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, returnOccurence: 'first' | 'last'): Maybe { + private findUsingColumnIndex(key: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, returnOccurrence: 'first' | 'last'): Maybe { const range = rangeValue.range if (range === undefined) { return undefined @@ -139,15 +139,15 @@ export class ColumnIndex implements ColumnSearchStrategy { return undefined } - const rowNumber = ColumnIndex.findRowBelongingToRange(valueIndexForTheKey, range, returnOccurence) + const rowNumber = ColumnIndex.findRowBelongingToRange(valueIndexForTheKey, range, returnOccurrence) return rowNumber !== undefined ? rowNumber - range.start.row : undefined } - private static findRowBelongingToRange(valueIndex: ValueIndex, range: AbsoluteCellRange, returnOccurence: 'first' | 'last'): Maybe { + private static findRowBelongingToRange(valueIndex: ValueIndex, range: AbsoluteCellRange, returnOccurrence: 'first' | 'last'): Maybe { const start = range.start.row const end = range.end.row - const positionInIndex = returnOccurence === 'first' + const positionInIndex = returnOccurrence === 'first' ? findInOrderedArray(start, valueIndex.index, 'upperBound') : findInOrderedArray(end, valueIndex.index, 'lowerBound') @@ -162,7 +162,7 @@ export class ColumnIndex implements ColumnSearchStrategy { } - public advancedFind(keyMatcher: (arg: RawInterpreterValue) => boolean, range: SimpleRangeValue, options: AdvancedFindOptions = { returnOccurence: 'first' }): number { + public advancedFind(keyMatcher: (arg: RawInterpreterValue) => boolean, range: SimpleRangeValue, options: AdvancedFindOptions = { returnOccurrence: 'first' }): number { return this.binarySearchStrategy.advancedFind(keyMatcher, range, options) } diff --git a/src/Lookup/SearchStrategy.ts b/src/Lookup/SearchStrategy.ts index 23e1d51348..2184c233a4 100644 --- a/src/Lookup/SearchStrategy.ts +++ b/src/Lookup/SearchStrategy.ts @@ -17,11 +17,11 @@ import {ColumnIndex} from './ColumnIndex' export interface SearchOptions { ordering: 'asc' | 'desc' | 'none', ifNoMatch: 'returnLowerBound' | 'returnUpperBound' | 'returnNotFound', - returnOccurence?: 'first' | 'last', + returnOccurrence?: 'first' | 'last', } export interface AdvancedFindOptions { - returnOccurence?: 'first' | 'last', + returnOccurrence?: 'first' | 'last', } export interface SearchStrategy { diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index 2963e9bb37..93f064905a 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -148,7 +148,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech const isWildcardMatchMode = matchMode === 2 const searchOptions: SearchOptions = { ordering: searchMode === 2 ? 'asc' : searchMode === -2 ? 'desc' : 'none', - returnOccurence: searchMode === -1 ? 'last' : 'first', + returnOccurrence: searchMode === -1 ? 'last' : 'first', ifNoMatch: matchMode === -1 ? 'returnLowerBound' : matchMode === 1 @@ -202,7 +202,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech return searchStrategy.advancedFind( this.arithmeticHelper.eqMatcherFunction(key), range, - { returnOccurence: searchOptions.returnOccurence } + { returnOccurrence: searchOptions.returnOccurrence } ) } diff --git a/test/interpreter/binary-search.spec.ts b/test/interpreter/binary-search.spec.ts index 799143aa1b..5a7a757ddc 100644 --- a/test/interpreter/binary-search.spec.ts +++ b/test/interpreter/binary-search.spec.ts @@ -58,7 +58,7 @@ describe('findLastOccurrenceInOrderedArray', () => { expect(findLastOccurrenceInOrderedArray('xyz', values)).toBe(5) }) - it('returns the last occurence', () => { + it('returns the last occurrence', () => { const values = [1, 2, 2, 2, 2, 2, 3, 3, 3] expect(findLastOccurrenceInOrderedArray(2, values)).toBe(5) }) diff --git a/test/interpreter/function-substitute.spec.ts b/test/interpreter/function-substitute.spec.ts index 7890bd4535..b72c0f1f60 100644 --- a/test/interpreter/function-substitute.spec.ts +++ b/test/interpreter/function-substitute.spec.ts @@ -39,7 +39,7 @@ describe('Function SUBSTITUTE', () => { expect(engine.getCellValue(adr('A4'))).toEqual('fofofofufo') }) - it('should return the original text if there are not enough occurences of the search string', () => { + it('should return the original text if there are not enough occurrences of the search string', () => { const engine = HyperFormula.buildFromArray([ ['=SUBSTITUTE("foobar", "o", "BAZ", 3)'], ]) From e6d801358248b72797ea55afd7f8dc6d088ac8f0 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Thu, 2 Jan 2025 17:07:29 +0100 Subject: [PATCH 44/45] Remove redundant comment --- src/interpreter/binarySearch.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/interpreter/binarySearch.ts b/src/interpreter/binarySearch.ts index cc6f579689..d53eddc676 100644 --- a/src/interpreter/binarySearch.ts +++ b/src/interpreter/binarySearch.ts @@ -55,7 +55,6 @@ export function findLastOccurrenceInOrderedRange( return orderingDirection === 'asc' ? NOT_FOUND : 0 } - // czy to sie zdarza? if (typeof foundValue !== typeof searchKey) { return NOT_FOUND } @@ -75,7 +74,6 @@ export function findLastOccurrenceInOrderedRange( return orderingDirection === 'asc' ? 0 : NOT_FOUND } - // czy to sie zdarza? if (typeof foundValue !== typeof searchKey) { return NOT_FOUND } From 52f540f7ebc658cb54018e3ee522f8546761a111 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Fri, 3 Jan 2025 11:46:05 +0100 Subject: [PATCH 45/45] Remove redundant guards --- src/Lookup/ColumnIndex.ts | 4 ---- src/interpreter/binarySearch.ts | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/Lookup/ColumnIndex.ts b/src/Lookup/ColumnIndex.ts index 8d9877471b..8cfa680164 100644 --- a/src/Lookup/ColumnIndex.ts +++ b/src/Lookup/ColumnIndex.ts @@ -108,10 +108,6 @@ export class ColumnIndex implements ColumnSearchStrategy { } public find(searchKey: RawNoErrorScalarValue, rangeValue: SimpleRangeValue, { ordering, ifNoMatch, returnOccurrence }: SearchOptions): number { - if (ifNoMatch == null) { - ifNoMatch = 'returnNotFound' - } - if (returnOccurrence == null) { returnOccurrence = ordering === 'none' ? 'first' : 'last' } diff --git a/src/interpreter/binarySearch.ts b/src/interpreter/binarySearch.ts index d53eddc676..cef984b642 100644 --- a/src/interpreter/binarySearch.ts +++ b/src/interpreter/binarySearch.ts @@ -28,10 +28,6 @@ export function findLastOccurrenceInOrderedRange( { searchCoordinate, orderingDirection, ifNoMatch }: { searchCoordinate: 'row' | 'col', orderingDirection: 'asc' | 'desc', ifNoMatch: 'returnLowerBound' | 'returnUpperBound' | 'returnNotFound' }, dependencyGraph: DependencyGraph, ): number { - if (ifNoMatch == null) { - ifNoMatch = 'returnNotFound' - } - const start = range.start[searchCoordinate] const end = searchCoordinate === 'col' ? range.effectiveEndColumn(dependencyGraph) : range.effectiveEndRow(dependencyGraph)