diff --git a/CHANGELOG.md b/CHANGELOG.md index 323c746b2d..1bf49f3af8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added - Added a message after `npm i hyperformula` that points AI assistants to the SKILLS file. The message needs `--foreground-scripts` to be visible in npm, and pnpm 10 needs the `postinstall` script to be approved first. [#1829](https://github.com/handsontable/hyperformula/pull/1829) +- Added the `TYPE` and `ERROR.TYPE` information functions: `TYPE` returns the code of a value's type (number, text, boolean, error, array), and `ERROR.TYPE` returns the code of an error, or `#N/A` for any other value. [#1813](https://github.com/handsontable/hyperformula/pull/1813) ### Fixed diff --git a/docs/guide/list-of-differences.md b/docs/guide/list-of-differences.md index 660fda0f98..37e5b45e26 100644 --- a/docs/guide/list-of-differences.md +++ b/docs/guide/list-of-differences.md @@ -53,6 +53,8 @@ See a full list of differences between HyperFormula, Microsoft Excel, and Google | EOMONTH function | =EOMONTH(DATE(2019, 7, 31), 1) | Type of the returned value: `CellValueDetailedType.NUMBER_DATE`. This is non-compliant with the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard, which defines the return type as a Number, while describing it as a Date serial number through the function summary. | Cell auto-formatted as **date** | Cell auto-formatted as **regular number** | | EDATE and EOMONTH results in February 1900 | =EDATE(DATE(1900, 1, 31), 1)

=EOMONTH(DATE(1900, 1, 31), 1) | With the [`leapYear1900`](/api/interfaces/configparams.md#leapyear1900) option set to `true`, February 1900 has 29 days, so both examples return 29 February 1900 (serial `60` with `nullDate` set to 1899-12-31). With the default `false`, both return 28 February 1900. | 28 February 1900 for both examples. Google Sheets does not treat 1900 as a leap year. | 28 February 1900 (serial `59`) for both examples. Excel keeps the 1900 leap day only in the serial-to-date mapping (`DAY(60)` is `29`); its month arithmetic treats February 1900 as having 28 days, so no `EDATE` or `EOMONTH` result is ever serial `60`. | | Empty cells in lookup search | =XLOOKUP(3, A1:A4, A1:A4, "NF", 0, 2)
where A1:A4 = 1, 2, (empty), 3 | Empty cells are skipped during the search. A value that is present is found even when it sits past an interspersed empty cell (exact match is gap-independent), and approximate `MATCH`/`VLOOKUP`/`HLOOKUP`/`XLOOKUP` skip empty cells — but not empty strings — when finding the lower/upper bound. Returns `3`. On an all-empty range in a binary search mode, HyperFormula returns the `if_not_found` result (never row 1). | Skips empty cells in approximate search (parity with HyperFormula). | With binary search modes (`search_mode` ±2), a range with interspersed empty cells is not strictly sorted; per Excel's documentation the result may be invalid, so a value past an empty cell is not reliably found. On an all-empty range in a binary mode, Excel returns the first row's value. | +| TYPE of a computed array | =TYPE(ROW(A1:A2)+0)
=TYPE(A1:A2+1)
=TYPE(IF(TRUE(), A1:A2)) | `ROW` and `COLUMN` over a multi-cell range count as arrays only when passed to `TYPE` directly, so `TYPE(ROW(A1:A2)+0)` gives `1`. With [`useArrayArithmetic`](/api/interfaces/configparams.md#usearrayarithmetic) set to `false` (the default), `A1:A2+1` and `IF(TRUE(), A1:A2)` are reduced to the cell in the formula's row, so `TYPE` classifies that cell's value (`1` for a number) in rows 1 and 2, and gives `16` (`#VALUE!`) in any other row. With `useArrayArithmetic` set to `true`, both give `64`. | Not checked. | Returns `64` for all three examples. | +| ERROR.TYPE function | =ERROR.TYPE(#NULL!) | HyperFormula has no `#NULL!` error, so the code 1 is never returned and the literal does not parse. `#CYCLE!`, which Excel does not have, has no code and gives `#N/A`. | Not checked. | Returns `1` for `#NULL!`. | ## Built-in functions diff --git a/hyperformula/src/i18n/languages/csCZ.ts b/hyperformula/src/i18n/languages/csCZ.ts index fea286083c..294edcfbb1 100644 --- a/hyperformula/src/i18n/languages/csCZ.ts +++ b/hyperformula/src/i18n/languages/csCZ.ts @@ -103,6 +103,7 @@ const dictionary: RawTranslationPackage = { EOMONTH: 'EOMONTH', ERF: 'ERF', ERFC: 'ERFC', + 'ERROR.TYPE': 'CHYBA.TYP', EVEN: 'ZAOKROUHLIT.NA.SUDÉ', EXACT: 'STEJNÉ', EXP: 'EXP', @@ -239,6 +240,7 @@ const dictionary: RawTranslationPackage = { TRIM: 'PROČISTIT', TRUE: 'PRAVDA', TRUNC: 'USEKNOUT', + TYPE: 'TYP', UNICHAR: 'UNICHAR', UNICODE: 'UNICODE', UNIQUE: 'UNIQUE', diff --git a/hyperformula/src/i18n/languages/daDK.ts b/hyperformula/src/i18n/languages/daDK.ts index 9264551249..682768c817 100644 --- a/hyperformula/src/i18n/languages/daDK.ts +++ b/hyperformula/src/i18n/languages/daDK.ts @@ -103,6 +103,7 @@ const dictionary: RawTranslationPackage = { EOMONTH: 'SLUT.PÅ.MÅNED', ERF: 'FEJLFUNK', ERFC: 'FEJLFUNK.KOMP', + 'ERROR.TYPE': 'FEJLTYPE', EVEN: 'LIGE', EXACT: 'EKSAKT', EXP: 'EKSP', @@ -239,6 +240,7 @@ const dictionary: RawTranslationPackage = { TRIM: 'FJERN.OVERFLØDIGE.BLANKE', TRUE: 'TRUE', TRUNC: 'AFKORT', + TYPE: 'VÆRDITYPE', UNICHAR: 'UNICHAR', UNICODE: 'UNICODE', UNIQUE: 'ENTYDIGE', diff --git a/hyperformula/src/i18n/languages/deDE.ts b/hyperformula/src/i18n/languages/deDE.ts index 4d3d39319b..3135a1a157 100644 --- a/hyperformula/src/i18n/languages/deDE.ts +++ b/hyperformula/src/i18n/languages/deDE.ts @@ -103,6 +103,7 @@ const dictionary: RawTranslationPackage = { EOMONTH: 'MONATSENDE', ERF: 'GAUSSFEHLER', ERFC: 'GAUSSFKOMPL', + 'ERROR.TYPE': 'FEHLER.TYP', EVEN: 'GERADE', EXACT: 'IDENTISCH', EXP: 'EXP', @@ -239,6 +240,7 @@ const dictionary: RawTranslationPackage = { TRIM: 'GLÄTTEN', TRUE: 'WAHR', TRUNC: 'KÜRZEN', + TYPE: 'TYP', UNICHAR: 'UNIZEICHEN', UNICODE: 'UNICODE', UNIQUE: 'EINDEUTIG', diff --git a/hyperformula/src/i18n/languages/enGB.ts b/hyperformula/src/i18n/languages/enGB.ts index d271b732cd..0b1ff8d0ac 100644 --- a/hyperformula/src/i18n/languages/enGB.ts +++ b/hyperformula/src/i18n/languages/enGB.ts @@ -104,6 +104,7 @@ const dictionary: RawTranslationPackage = { EOMONTH: 'EOMONTH', ERF: 'ERF', ERFC: 'ERFC', + 'ERROR.TYPE': 'ERROR.TYPE', EVEN: 'EVEN', EXACT: 'EXACT', EXP: 'EXP', @@ -241,6 +242,7 @@ const dictionary: RawTranslationPackage = { TRIM: 'TRIM', TRUE: 'TRUE', TRUNC: 'TRUNC', + TYPE: 'TYPE', UNICHAR: 'UNICHAR', UNICODE: 'UNICODE', UNIQUE: 'UNIQUE', diff --git a/hyperformula/src/i18n/languages/esES.ts b/hyperformula/src/i18n/languages/esES.ts index 9c6b817b98..3206eadb5e 100644 --- a/hyperformula/src/i18n/languages/esES.ts +++ b/hyperformula/src/i18n/languages/esES.ts @@ -103,6 +103,7 @@ export const dictionary: RawTranslationPackage = { EOMONTH: 'FIN.MES', ERF: 'FUN.ERROR', ERFC: 'FUN.ERROR.COMPL', + 'ERROR.TYPE': 'TIPO.DE.ERROR', EVEN: 'REDONDEA.PAR', EXACT: 'IGUAL', EXP: 'EXP', @@ -239,6 +240,7 @@ export const dictionary: RawTranslationPackage = { TRIM: 'ESPACIOS', TRUE: 'VERDADERO', TRUNC: 'TRUNCAR', + TYPE: 'TIPO', UNICHAR: 'UNICHAR', UNICODE: 'UNICODE', UNIQUE: 'UNICOS', diff --git a/hyperformula/src/i18n/languages/fiFI.ts b/hyperformula/src/i18n/languages/fiFI.ts index 99ff05708f..dffc59fab1 100644 --- a/hyperformula/src/i18n/languages/fiFI.ts +++ b/hyperformula/src/i18n/languages/fiFI.ts @@ -103,6 +103,7 @@ const dictionary: RawTranslationPackage = { EOMONTH: 'KUUKAUSI.LOPPU', ERF: 'VIRHEFUNKTIO', ERFC: 'VIRHEFUNKTIO.KOMPLEMENTTI', + 'ERROR.TYPE': 'VIRHEEN.LAJI', EVEN: 'PARILLINEN', EXACT: 'VERTAA', EXP: 'EKSPONENTTI', @@ -239,6 +240,7 @@ const dictionary: RawTranslationPackage = { TRIM: 'POISTA.VÄLIT', TRUE: 'TOSI', TRUNC: 'KATKAISE', + TYPE: 'TYYPPI', UNICHAR: 'UNICODEMERKKI', UNICODE: 'UNICODE', UNIQUE: 'AINUTKERTAISET.ARVOT', diff --git a/hyperformula/src/i18n/languages/frFR.ts b/hyperformula/src/i18n/languages/frFR.ts index f7b9be45f7..1d02c27f50 100644 --- a/hyperformula/src/i18n/languages/frFR.ts +++ b/hyperformula/src/i18n/languages/frFR.ts @@ -103,6 +103,7 @@ const dictionary: RawTranslationPackage = { EOMONTH: 'FIN.MOIS', ERF: 'ERF', ERFC: 'ERFC', + 'ERROR.TYPE': 'TYPE.ERREUR', EVEN: 'PAIR', EXACT: 'EXACT', EXP: 'EXP', @@ -239,6 +240,7 @@ const dictionary: RawTranslationPackage = { TRIM: 'SUPPRESPACE', TRUE: 'VRAI', TRUNC: 'TRONQUE', + TYPE: 'TYPE', UNICHAR: 'UNICAR', UNICODE: 'UNICODE', UNIQUE: 'UNIQUE', diff --git a/hyperformula/src/i18n/languages/huHU.ts b/hyperformula/src/i18n/languages/huHU.ts index e40219553a..0faede3bea 100644 --- a/hyperformula/src/i18n/languages/huHU.ts +++ b/hyperformula/src/i18n/languages/huHU.ts @@ -103,6 +103,7 @@ const dictionary: RawTranslationPackage = { EOMONTH: 'HÓNAP.UTOLSÓ.NAP', ERF: 'HIBAF', ERFC: 'HIBAF.KOMPLEMENTER', + 'ERROR.TYPE': 'HIBA.TÍPUS', EVEN: 'PÁROS', EXACT: 'AZONOS', EXP: 'KITEVŐ', @@ -239,6 +240,7 @@ const dictionary: RawTranslationPackage = { TRIM: 'KIMETSZ', TRUE: 'IGAZ', TRUNC: 'CSONK', + TYPE: 'TÍPUS', UNICHAR: 'UNIKARAKTER', UNICODE: 'UNICODE', UNIQUE: 'EGYEDI', diff --git a/hyperformula/src/i18n/languages/idID.ts b/hyperformula/src/i18n/languages/idID.ts index 719cb11db6..33e2375302 100644 --- a/hyperformula/src/i18n/languages/idID.ts +++ b/hyperformula/src/i18n/languages/idID.ts @@ -104,6 +104,7 @@ const dictionary: RawTranslationPackage = { EOMONTH: 'AKHIR.BULAN', ERF: 'ERF', ERFC: 'ERFC', + 'ERROR.TYPE': 'ERROR.TYPE', EVEN: 'GENAP', EXACT: 'IDENTIK', EXP: 'EXP', @@ -241,6 +242,7 @@ const dictionary: RawTranslationPackage = { TRIM: 'HAPUS.SPASI', TRUE: 'BENAR', TRUNC: 'POTONG', + TYPE: 'TYPE', UNICHAR: 'UNIKARAKTER', UNICODE: 'UNICODE', UNIQUE: 'UNIQUE', diff --git a/hyperformula/src/i18n/languages/itIT.ts b/hyperformula/src/i18n/languages/itIT.ts index 1a494628f8..0efe41f2e3 100644 --- a/hyperformula/src/i18n/languages/itIT.ts +++ b/hyperformula/src/i18n/languages/itIT.ts @@ -103,6 +103,7 @@ const dictionary: RawTranslationPackage = { EOMONTH: 'FINE.MESE', ERF: 'FUNZ.ERRORE', ERFC: 'FUNZ.ERRORE.COMP', + 'ERROR.TYPE': 'ERRORE.TIPO', EVEN: 'PARI', EXACT: 'IDENTICO', EXP: 'EXP', @@ -239,6 +240,7 @@ const dictionary: RawTranslationPackage = { TRIM: 'ANNULLA.SPAZI', TRUE: 'VERO', TRUNC: 'TRONCA', + TYPE: 'TIPO', UNICHAR: 'CARATT.UNI', UNICODE: 'UNICODE', UNIQUE: 'UNICI', diff --git a/hyperformula/src/i18n/languages/nbNO.ts b/hyperformula/src/i18n/languages/nbNO.ts index ff15415a98..43a1821e67 100644 --- a/hyperformula/src/i18n/languages/nbNO.ts +++ b/hyperformula/src/i18n/languages/nbNO.ts @@ -103,6 +103,7 @@ const dictionary: RawTranslationPackage = { EOMONTH: 'MÅNEDSSLUTT', ERF: 'FEILF', ERFC: 'FEILFK', + 'ERROR.TYPE': 'FEIL.TYPE', EVEN: 'AVRUND.TIL.PARTALL', EXACT: 'EKSAKT', EXP: 'EKSP', @@ -239,6 +240,7 @@ const dictionary: RawTranslationPackage = { TRIM: 'TRIMME', TRUE: 'SANN', TRUNC: 'AVKORT', + TYPE: 'VERDITYPE', UNICHAR: 'UNICODETEGN', UNICODE: 'UNICODE', UNIQUE: 'UNIK', diff --git a/hyperformula/src/i18n/languages/nlNL.ts b/hyperformula/src/i18n/languages/nlNL.ts index c491e2f44b..2974a3ce9c 100644 --- a/hyperformula/src/i18n/languages/nlNL.ts +++ b/hyperformula/src/i18n/languages/nlNL.ts @@ -103,6 +103,7 @@ const dictionary: RawTranslationPackage = { EOMONTH: 'LAATSTE.DAG', ERF: 'FOUTFUNCTIE', ERFC: 'FOUT.COMPLEMENT', + 'ERROR.TYPE': 'TYPE.FOUT', EVEN: 'EVEN', EXACT: 'GELIJK', EXP: 'EXP', @@ -239,6 +240,7 @@ const dictionary: RawTranslationPackage = { TRIM: 'SPATIES.WISSEN', TRUE: 'WAAR', TRUNC: 'GEHEEL', + TYPE: 'TYPE', UNICHAR: 'UNITEKEN', UNICODE: 'UNICODE', UNIQUE: 'UNIEK', diff --git a/hyperformula/src/i18n/languages/plPL.ts b/hyperformula/src/i18n/languages/plPL.ts index 6fa4f016b4..42829a8852 100644 --- a/hyperformula/src/i18n/languages/plPL.ts +++ b/hyperformula/src/i18n/languages/plPL.ts @@ -103,6 +103,7 @@ const dictionary: RawTranslationPackage = { EOMONTH: 'NR.SER.OST.DN.MIEŚ', ERF: 'FUNKCJA.BŁ', ERFC: 'KOMP.FUNKCJA.BŁ', + 'ERROR.TYPE': 'NR.BŁĘDU', EVEN: 'ZAOKR.DO.PARZ', EXACT: 'PORÓWNAJ', EXP: 'EXP', @@ -239,6 +240,7 @@ const dictionary: RawTranslationPackage = { TRIM: 'USUŃ.ZBĘDNE.ODSTĘPY', TRUE: 'PRAWDA', TRUNC: 'LICZBA.CAŁK', + TYPE: 'TYP', UNICHAR: 'ZNAK.UNICODE', UNICODE: 'UNICODE', UNIQUE: 'UNIKATOWE', diff --git a/hyperformula/src/i18n/languages/ptPT.ts b/hyperformula/src/i18n/languages/ptPT.ts index 6c1e68c328..a235c025d4 100644 --- a/hyperformula/src/i18n/languages/ptPT.ts +++ b/hyperformula/src/i18n/languages/ptPT.ts @@ -103,6 +103,7 @@ const dictionary: RawTranslationPackage = { EOMONTH: 'FIMMÊS', ERF: 'FUNERRO', ERFC: 'FUNERROCOMPL', + 'ERROR.TYPE': 'TIPO.ERRO', EVEN: 'PAR', EXACT: 'EXATO', EXP: 'EXP', @@ -239,6 +240,7 @@ const dictionary: RawTranslationPackage = { TRIM: 'ARRUMAR', TRUE: 'VERDADEIRO', TRUNC: 'TRUNCAR', + TYPE: 'TIPO', UNICHAR: 'CARACTUNI', UNICODE: 'UNICODE', UNIQUE: 'EXCLUSIVOS', diff --git a/hyperformula/src/i18n/languages/ruRU.ts b/hyperformula/src/i18n/languages/ruRU.ts index 6b35c1ce2f..0813ca0e1a 100644 --- a/hyperformula/src/i18n/languages/ruRU.ts +++ b/hyperformula/src/i18n/languages/ruRU.ts @@ -103,6 +103,7 @@ const dictionary: RawTranslationPackage = { EOMONTH: 'КОНМЕСЯЦА', ERF: 'ФОШ', ERFC: 'ДФОШ', + 'ERROR.TYPE': 'ТИП.ОШИБКИ', EVEN: 'ЧЁТН', EXACT: 'СОВПАД', EXP: 'EXP', @@ -239,6 +240,7 @@ const dictionary: RawTranslationPackage = { TRIM: 'СЖПРОБЕЛЫ', TRUE: 'ИСТИНА', TRUNC: 'ОТБР', + TYPE: 'ТИП', UNICHAR: 'ЮНИСИМВ', UNICODE: 'UNICODE', UNIQUE: 'УНИК', diff --git a/hyperformula/src/i18n/languages/svSE.ts b/hyperformula/src/i18n/languages/svSE.ts index 5f87580087..7c0b0ee350 100644 --- a/hyperformula/src/i18n/languages/svSE.ts +++ b/hyperformula/src/i18n/languages/svSE.ts @@ -103,6 +103,7 @@ const dictionary: RawTranslationPackage = { EOMONTH: 'SLUTMÅNAD', ERF: 'FELF', ERFC: 'FELFK', + 'ERROR.TYPE': 'FEL.TYP', EVEN: 'JÄMN', EXACT: 'EXAKT', EXP: 'EXP', @@ -239,6 +240,7 @@ const dictionary: RawTranslationPackage = { TRIM: 'RENSA', TRUE: 'SANT', TRUNC: 'AVKORTA', + TYPE: 'VÄRDETYP', UNICHAR: 'UNICHAR', UNICODE: 'UNICODE', UNIQUE: 'UNIK', diff --git a/hyperformula/src/i18n/languages/trTR.ts b/hyperformula/src/i18n/languages/trTR.ts index 93c2176d37..de0e6b04d6 100644 --- a/hyperformula/src/i18n/languages/trTR.ts +++ b/hyperformula/src/i18n/languages/trTR.ts @@ -103,6 +103,7 @@ const dictionary: RawTranslationPackage = { EOMONTH: 'SERİAY', ERF: 'HATAİŞLEV', ERFC: 'TÜMHATAİŞLEV', + 'ERROR.TYPE': 'HATA.TİPİ', EVEN: 'ÇİFT', EXACT: 'ÖZDEŞ', EXP: 'ÜS', @@ -239,6 +240,7 @@ const dictionary: RawTranslationPackage = { TRIM: 'KIRP', TRUE: 'DOĞRU', TRUNC: 'NSAT', + TYPE: 'TÜR', UNICHAR: 'UNICODEKARAKTERİ', UNICODE: 'UNICODE', UNIQUE: 'BENZERSİZ', diff --git a/hyperformula/src/interpreter/functionMetadata/categories/information.ts b/hyperformula/src/interpreter/functionMetadata/categories/information.ts index fd18cd79b6..62cfe3cf00 100644 --- a/hyperformula/src/interpreter/functionMetadata/categories/information.ts +++ b/hyperformula/src/interpreter/functionMetadata/categories/information.ts @@ -11,6 +11,13 @@ import {FunctionDoc} from '../FunctionDescription' * `VERSION` function, whose structural metadata lives in `protectedFunctionMetadata.ts` rather than in a plugin. */ export const INFORMATION_DOCS: Record = { + 'ERROR.TYPE': { + category: 'Information', + shortDescription: 'Returns the number of an error type: 2 for #DIV/0!, 3 for #VALUE!, 4 for #REF!, 5 for #NAME?, 6 for #NUM!, 7 for #N/A and 9 for #SPILL!, or #N/A for a value that is not an error or an error without a number.', + parameters: [{name: 'value', description: 'The value or cell reference to inspect; any value that is not an error, a number or text included, results in #N/A.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=ERROR.TYPE(1/0)', '=ERROR.TYPE(A1)', '=ERROR.TYPE(NA())'], + }, ISBINARY: { category: 'Information', shortDescription: 'Returns TRUE if provided value is a valid binary number.', @@ -130,6 +137,13 @@ export const INFORMATION_DOCS: Record = { documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', examples: ['=SHEETS()', '=SHEETS(A1:B3)'], }, + TYPE: { + category: 'Information', + shortDescription: 'Returns the type of a value: 1 for a number or an empty cell, 2 for text, 4 for a logical value, 16 for an error and 64 for an array or a multi-cell range.', + parameters: [{name: 'value', description: 'The value or reference to classify; an error is reported as 16 instead of being propagated, and a single-cell range is classified by the value it holds.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=TYPE(A1)', '=TYPE("text")', '=TYPE(A1:B2)'], + }, // VERSION is a protected function (its plugin, VersionPlugin, is kept out of the general registry so it can // never be unregistered; see `FunctionRegistry._protectedPlugins`). Its structural metadata (parameters, // repeatLastArgs) is authored separately in `PROTECTED_FUNCTION_METADATA`. diff --git a/hyperformula/src/interpreter/plugin/InformationPlugin.ts b/hyperformula/src/interpreter/plugin/InformationPlugin.ts index 8c10ac05ed..ad6d0d5faf 100644 --- a/hyperformula/src/interpreter/plugin/InformationPlugin.ts +++ b/hyperformula/src/interpreter/plugin/InformationPlugin.ts @@ -3,15 +3,53 @@ * Copyright (c) 2025 Handsoncode. All rights reserved. */ +import {AbsoluteCellRange} from '../../AbsoluteCellRange' import {CellError, ErrorType, SimpleCellAddress} from '../../Cell' import {FormulaVertex} from '../../DependencyGraph/FormulaVertex' import {ErrorMessage} from '../../error-message' -import {AstNodeType, ProcedureAst} from '../../parser' +import {Ast, AstNodeType, ProcedureAst} from '../../parser' import {InterpreterState} from '../InterpreterState' import {EmptyValue, InternalScalarValue, InterpreterValue, isExtendedNumber} from '../InterpreterValue' import {SimpleRangeValue} from '../../SimpleRangeValue' import {FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions} from './FunctionPlugin' +const TYPE_CODE_NUMBER = 1 +const TYPE_CODE_TEXT = 2 +const TYPE_CODE_BOOLEAN = 4 +const TYPE_CODE_ERROR = 16 +const TYPE_CODE_ARRAY = 64 + +/** + * Codes returned by ERROR.TYPE. #NULL! (code 1) has no counterpart in HyperFormula, and neither has a code for #CYCLE!. + */ +const ERROR_TYPE_CODES: Partial> = { + [ErrorType.DIV_BY_ZERO]: 2, + [ErrorType.VALUE]: 3, + [ErrorType.REF]: 4, + [ErrorType.NAME]: 5, + [ErrorType.NUM]: 6, + [ErrorType.NA]: 7, + [ErrorType.SPILL]: 9, +} + +/** + * Returns the TYPE code of a single (non-array) value. + * + * @param value + */ +function valueTypeCode(value: InterpreterValue): number { + if (value instanceof CellError) { + return TYPE_CODE_ERROR + } + if (typeof value === 'string') { + return TYPE_CODE_TEXT + } + if (typeof value === 'boolean') { + return TYPE_CODE_BOOLEAN + } + return TYPE_CODE_NUMBER +} + /** * Interpreter plugin containing information functions */ @@ -35,6 +73,12 @@ export class InformationPlugin extends FunctionPlugin implements FunctionPluginT doesNotNeedArgumentsToBeComputed: true, vectorizationForbidden: true, }, + 'ERROR.TYPE': { + method: 'errortype', + parameters: [ + {argumentType: FunctionArgumentType.SCALAR} + ] + }, 'ISBINARY': { method: 'isbinary', parameters: [ @@ -149,7 +193,88 @@ export class InformationPlugin extends FunctionPlugin implements FunctionPluginT ], doesNotNeedArgumentsToBeComputed: true, vectorizationForbidden: true, + }, + 'TYPE': { + method: 'type', + parameters: [ + {argumentType: FunctionArgumentType.ANY} + ], + vectorizationForbidden: true, + } + } + + /** + * Corresponds to ERROR.TYPE(value) + * + * Returns the number of an error type (2-7 and 9), or #N/A when the value is not an error or the error has no code + * + * @param ast + * @param state + */ + public errortype(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('ERROR.TYPE'), (arg: InternalScalarValue) => { + if (!(arg instanceof CellError)) { + return new CellError(ErrorType.NA) + } + return ERROR_TYPE_CODES[arg.type] ?? new CellError(ErrorType.NA) + }) + } + + /** + * Corresponds to TYPE(value) + * + * Returns the type of a value: 1 for numbers and empty cells, 2 for text, 4 for booleans, 16 for errors + * and 64 for arrays and multi-cell ranges. Errors are classified, not propagated. + * + * @param ast + * @param state + */ + public type(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + if (ast.args.length !== 1) { + return new CellError(ErrorType.NA, ErrorMessage.WrongArgNumber) + } + if (this.isRowOrColumnOfMultiCellRange(ast.args[0], state)) { + return TYPE_CODE_ARRAY + } + const value = this.evaluateAst(ast.args[0], state) + if (value instanceof SimpleRangeValue) { + return TYPE_CODE_ARRAY + } + return valueTypeCode(value) + } + + /** + * ROW and COLUMN evaluate to a scalar here, while Excel returns an array when they are given a multi-cell range. + * TYPE reports the Excel classification. + * + * @param ast + * @param state + */ + private isRowOrColumnOfMultiCellRange(ast: Ast, state: InterpreterState): boolean { + while (ast.type === AstNodeType.PARENTHESIS) { + ast = ast.expression + } + if (ast.type !== AstNodeType.FUNCTION_CALL || (ast.procedureName !== 'ROW' && ast.procedureName !== 'COLUMN')) { + return false + } + let arg = ast.args[0] + while (arg !== undefined && arg.type === AstNodeType.PARENTHESIS) { + arg = arg.expression + } + if (arg === undefined) { + return false + } + if (arg.type !== AstNodeType.CELL_RANGE && arg.type !== AstNodeType.COLUMN_RANGE && arg.type !== AstNodeType.ROW_RANGE) { + return false + } + if (arg.start.sheet !== arg.end.sheet) { + // A range across sheets makes ROW and COLUMN return #REF!, which TYPE classifies as an error. + return false + } + if (arg.type === AstNodeType.CELL_RANGE) { + return AbsoluteCellRange.fromAst(arg, state.formulaAddress).size() > 1 } + return true } /** diff --git a/hyperformula/src/license/functionCapabilities.ts b/hyperformula/src/license/functionCapabilities.ts index a26733ec5f..148827e21b 100644 --- a/hyperformula/src/license/functionCapabilities.ts +++ b/hyperformula/src/license/functionCapabilities.ts @@ -83,6 +83,7 @@ const UNGROUPED_FUNCTIONS = [ 'COVARIANCE.S', 'CSC', 'CSCH', 'CUMIPMT', 'CUMPRINC', 'DAVERAGE', 'DB', 'DCOUNT', 'DCOUNTA', 'DDB', 'DEC2BIN', 'DEC2OCT', 'DECIMAL', 'DEGREES', 'DELTA', 'DEVSQ', 'DGET', 'DMAX', 'DMIN', 'DOLLARDE', 'DOLLARFR', 'DPRODUCT', 'DSTDEV', 'DSTDEVP', 'DSUM', 'DVAR', 'DVARP', 'EFFECT', 'ERF', 'ERFC', + 'ERROR.TYPE', 'EXPON.DIST', 'F.DIST', 'F.DIST.RT', 'F.INV', 'F.INV.RT', 'F.TEST', 'FACT', 'FACTDOUBLE', 'FISHER', 'FISHERINV', 'FLOOR.MATH', 'FLOOR.PRECISE', 'FORMULATEXT', 'FVSCHEDULE', 'GAMMA', 'GAMMA.DIST', 'GAMMA.INV', 'GAMMALN', 'GAUSS', 'GCD', 'GEOMEAN', 'HARMEAN', 'HEX2BIN', 'HEX2OCT', 'HYPGEOM.DIST', @@ -95,7 +96,7 @@ const UNGROUPED_FUNCTIONS = [ 'PHI', 'POISSON.DIST', 'QUARTILE.EXC', 'QUARTILE.INC', 'RADIANS', 'ROMAN', 'RRI', 'RSQ', 'SEC', 'SECH', 'SERIESSUM', 'SHEET', 'SHEETS', 'SINH', 'SKEW', 'SKEW.P', 'SLOPE', 'SPLIT', 'SQRTPI', 'STANDARDIZE', 'STEYX', 'SUMX2MY2', 'SUMX2PY2', 'SYD', 'T.DIST', 'T.DIST.2T', 'T.DIST.RT', 'T.INV', 'T.INV.2T', 'T.TEST', - 'TANH', 'TBILLEQ', 'TBILLPRICE', 'TBILLYIELD', 'TDIST', 'TIMEVALUE', 'UNICODE', 'VARA', 'VARPA', + 'TANH', 'TBILLEQ', 'TBILLPRICE', 'TBILLYIELD', 'TDIST', 'TIMEVALUE', 'TYPE', 'UNICODE', 'VARA', 'VARPA', 'WEIBULL.DIST', 'WORKDAY.INTL', 'Z.TEST', ]