Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XLOOKUP function #1469

Draft
wants to merge 23 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
932e8c0
ISS-1 sapiologie/hyperformula XLOOKUP in built-in-functions.md
selimyoussry Jun 15, 2024
f0f9545
ISS-1 sapiologie/hyperformula add translations
selimyoussry Jun 15, 2024
384bcb8
ISS-1 sapiologie/hyperformula placeholder function and test
selimyoussry Jun 15, 2024
eecce3a
ISS-1 sapiologie/hyperformula progress on Xlookup logic
selimyoussry Jun 16, 2024
e9cabd6
ISS-1 sapiologie/hyperformula Working XLOOKUP without support for opt…
selimyoussry Jun 16, 2024
83be86c
ISS-1 sapiologie/hyperformula override default if_not_found
selimyoussry Jun 16, 2024
5101a90
ISS-1 sapiologie/hyperformula add commented out range function return…
selimyoussry Jun 16, 2024
08445d1
ISS-1 sapiologie/hyperformula update the doc to reflect limitations
selimyoussry Jun 16, 2024
9ece67c
ISS-1 sapiologie/hyperformula Set vectorizationForbidden: true on XLO…
selimyoussry Jun 17, 2024
9c448c2
Add test suite for the XLOOKUP function
sequba Dec 5, 2024
167cac8
Plan basic tests for XLOOKUP
sequba Dec 5, 2024
1f9d091
Merge branch 'iss-1-xlookup' of github.com:sapiologie/hyperformula in…
sequba Dec 5, 2024
0811866
Merge branch 'sapiologie-iss-1-xlookup' into feature/issue-1458
sequba Dec 5, 2024
8699958
Make XLOOKUP work in basic mode
sequba Dec 5, 2024
13b9cc0
Make XLOOKUP return a range
sequba Dec 5, 2024
d3c6a26
Add unit tests about the types of argument
sequba Dec 5, 2024
af99365
Fix XLOOKUP for scenario with 2D returnArray
sequba Dec 5, 2024
052e30c
Add unit tests for argument validation
sequba Dec 7, 2024
e1cd83b
Add official Exel example 4
sequba Dec 7, 2024
3135c28
Implement XLOOKUP for single-cell ranges
sequba Dec 8, 2024
4a15b95
Refactor xlookupArraySize function
sequba Dec 9, 2024
7cd0a99
Fix the single-cell ranges tests
sequba Dec 9, 2024
bfac296
Add unit tests for non-default searchMode
sequba Dec 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/guide/built-in-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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

Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/csCZ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = {
WEEKNUM: 'WEEKNUM',
WORKDAY: 'WORKDAY',
'WORKDAY.INTL': 'WORKDAY.INTL',
XLOOKUP: 'XVYHLEDAT',
XNPV: 'XNPV',
XOR: 'XOR',
YEAR: 'ROK',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/daDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/deDE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = {
WEEKNUM: 'KALENDERWOCHE',
WORKDAY: 'ARBEITSTAG',
'WORKDAY.INTL': 'ARBEITSTAG.INTL',
XLOOKUP: 'XVERWEIS',
XNPV: 'XKAPITALWERT',
XOR: 'XODER',
YEAR: 'JAHR',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/enGB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/esES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/fiFI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/frFR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/huHU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/itIT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/nbNO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = {
WEEKNUM: 'UKENR',
WORKDAY: 'ARBEIDSDAG',
'WORKDAY.INTL': 'ARBEIDSDAG.INTL',
XLOOKUP: 'XOPPSLAG',
XNPV: 'XNNV',
XOR: 'EKSKLUSIVELLER',
YEAR: 'ÅR',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/nlNL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/plPL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/ptPT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = {
WEEKNUM: 'NÚMSEMANA',
WORKDAY: 'DIATRABALHO',
'WORKDAY.INTL': 'DIATRABALHO.INTL',
XLOOKUP: 'PROCX',
XNPV: 'XVPL',
XOR: 'OUEXCL',
YEAR: 'ANO',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/ruRU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = {
WEEKNUM: 'НОМНЕДЕЛИ',
WORKDAY: 'РАБДЕНЬ',
'WORKDAY.INTL': 'РАБДЕНЬ.МЕЖД',
XLOOKUP: 'ПРОСМОТРХ',
XNPV: 'ЧИСТНЗ',
XOR: 'ИСКЛИЛИ',
YEAR: 'ГОД',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/svSE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ const dictionary: RawTranslationPackage = {
WEEKNUM: 'VECKONR',
WORKDAY: 'ARBETSDAGAR',
'WORKDAY.INTL': 'ARBETSDAGAR.INT',
XLOOKUP: 'XLETAUPP',
XNPV: 'XNUVÄRDE',
XOR: 'XOR',
YEAR: 'ÅR',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/trTR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
154 changes: 130 additions & 24 deletions src/interpreter/plugin/LookupPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,64 @@
* 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<LookupPlugin> {
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.SCALAR, 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 },
]
},
}
Expand Down Expand Up @@ -94,13 +113,81 @@ 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) => {
if (![0, -1, 1, 2].includes(matchMode)) {
return new CellError(ErrorType.VALUE, ErrorMessage.BadMode)
}

if (![1, -1, 1, 2].includes(searchMode)) {
return new CellError(ErrorType.VALUE, ErrorMessage.BadMode)
}

if (matchMode !== 0) {
// not supported yet
// TODO: Implement match mode
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)

return this.doXlookup(zeroIfEmpty(key), lookupRange, returnRange, ifNotFound, matchMode, searchMode)
})
}

public xlookupArraySize(ast: ProcedureAst): ArraySize {
const lookupRange = ast?.args?.[1] as CellRange
const returnRange = ast?.args?.[2] as CellRange

if (lookupRange?.start == null
|| lookupRange?.end == null
|| returnRange?.start == null
|| returnRange?.end == null
) {
return ArraySize.error()
}

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

const isVerticalSearch = lookupRangeWidth === 1 && returnRangeHeight === lookupRangeHeight
const isHorizontalSearch = lookupRangeHeight === 1 && returnRangeWidth === lookupRangeWidth

if (!isVerticalSearch && !isHorizontalSearch) {
return ArraySize.error()
}

if (isVerticalSearch) {
return new ArraySize(returnRangeWidth, 1)
}

return new ArraySize(1, returnRangeHeight)
}

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)
})
}

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),
Expand Down Expand Up @@ -171,6 +258,25 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech
return value
}

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 (!isVerticalSearch && !isHorizontalSearch) {
return new CellError(ErrorType.VALUE, ErrorMessage.WrongDimension)
}

const searchStrategy = isVerticalSearch ? this.columnSearch : this.rowSearch
const indexFound = this.searchInRange(key, lookupRange, false, searchStrategy)

if (indexFound === -1) {
return (ifNotFound == ErrorType.NA) ? new CellError(ErrorType.NA, ErrorMessage.ValueNotFound) : ifNotFound
}

const returnValues: InternalScalarValue[][] = isVerticalSearch ? [returnRange.data[indexFound]] : returnRange.data.map((row) => [row[indexFound]])
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)
Expand Down
Loading
Loading