From 7acd8f3a43c9e94165f986ae11c3089947617913 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Wed, 14 Oct 2026 23:50:43 +0000 Subject: [PATCH 1/7] Add TYPE and ERROR.TYPE functions Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 4 + docs/guide/list-of-differences.md | 1 + src/i18n/languages/csCZ.ts | 2 + src/i18n/languages/daDK.ts | 2 + src/i18n/languages/deDE.ts | 2 + src/i18n/languages/enGB.ts | 2 + src/i18n/languages/esES.ts | 2 + src/i18n/languages/fiFI.ts | 2 + src/i18n/languages/frFR.ts | 2 + src/i18n/languages/huHU.ts | 2 + src/i18n/languages/idID.ts | 2 + src/i18n/languages/itIT.ts | 2 + src/i18n/languages/nbNO.ts | 2 + src/i18n/languages/nlNL.ts | 2 + src/i18n/languages/plPL.ts | 2 + src/i18n/languages/ptPT.ts | 2 + src/i18n/languages/ruRU.ts | 2 + src/i18n/languages/svSE.ts | 2 + src/i18n/languages/trTR.ts | 2 + .../categories/information.ts | 14 ++ src/interpreter/plugin/InformationPlugin.ts | 127 +++++++++++++++++- src/license/functionCapabilities.ts | 3 +- 22 files changed, 181 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f97c660139..115a5d8e4c 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 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. + ### Fixed - Fixed the `EDATE` function returning the 28th of February instead of the 29th when the shifted date falls in February of a leap year (e.g. `=EDATE(DATE(2028, 1, 31), 1)` now returns `2028-02-29` instead of `2028-02-28`). [#1781](https://github.com/handsontable/hyperformula/pull/1781) diff --git a/docs/guide/list-of-differences.md b/docs/guide/list-of-differences.md index 660fda0f98..a142db351e 100644 --- a/docs/guide/list-of-differences.md +++ b/docs/guide/list-of-differences.md @@ -53,6 +53,7 @@ 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. | +| ERROR.TYPE function | =ERROR.TYPE(#NULL!) | HyperFormula has no `#NULL!` error, so the code 1 is never returned and the formula does not parse. `#CYCLE!`, which Excel does not have, has no code and gives `#N/A`. | ## Built-in functions diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index fea286083c..294edcfbb1 100644 --- a/src/i18n/languages/csCZ.ts +++ b/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/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index 9264551249..682768c817 100644 --- a/src/i18n/languages/daDK.ts +++ b/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/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index 4d3d39319b..3135a1a157 100644 --- a/src/i18n/languages/deDE.ts +++ b/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/src/i18n/languages/enGB.ts b/src/i18n/languages/enGB.ts index d271b732cd..0b1ff8d0ac 100644 --- a/src/i18n/languages/enGB.ts +++ b/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/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index 9c6b817b98..3206eadb5e 100644 --- a/src/i18n/languages/esES.ts +++ b/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/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index 99ff05708f..dffc59fab1 100644 --- a/src/i18n/languages/fiFI.ts +++ b/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/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index f7b9be45f7..1d02c27f50 100644 --- a/src/i18n/languages/frFR.ts +++ b/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/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index e40219553a..0faede3bea 100644 --- a/src/i18n/languages/huHU.ts +++ b/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/src/i18n/languages/idID.ts b/src/i18n/languages/idID.ts index 719cb11db6..33e2375302 100644 --- a/src/i18n/languages/idID.ts +++ b/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/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 1a494628f8..0efe41f2e3 100644 --- a/src/i18n/languages/itIT.ts +++ b/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/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index ff15415a98..43a1821e67 100644 --- a/src/i18n/languages/nbNO.ts +++ b/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/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index c491e2f44b..2974a3ce9c 100644 --- a/src/i18n/languages/nlNL.ts +++ b/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/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 6fa4f016b4..42829a8852 100644 --- a/src/i18n/languages/plPL.ts +++ b/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/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index 6c1e68c328..a235c025d4 100644 --- a/src/i18n/languages/ptPT.ts +++ b/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/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index 6b35c1ce2f..0813ca0e1a 100644 --- a/src/i18n/languages/ruRU.ts +++ b/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/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index 5f87580087..7c0b0ee350 100644 --- a/src/i18n/languages/svSE.ts +++ b/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/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index 93c2176d37..de0e6b04d6 100644 --- a/src/i18n/languages/trTR.ts +++ b/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/src/interpreter/functionMetadata/categories/information.ts b/src/interpreter/functionMetadata/categories/information.ts index fd18cd79b6..788f78c96e 100644 --- a/src/interpreter/functionMetadata/categories/information.ts +++ b/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! and 7 for #N/A, or #N/A for a value that is not an error.', + 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/src/interpreter/plugin/InformationPlugin.ts b/src/interpreter/plugin/InformationPlugin.ts index 8c10ac05ed..13644326a5 100644 --- a/src/interpreter/plugin/InformationPlugin.ts +++ b/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 value.numberOfElements() === 1 ? valueTypeCode(value.data[0][0]) : 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.COLUMN_RANGE || arg.type === AstNodeType.ROW_RANGE) { + return true + } + if (arg.type === AstNodeType.CELL_RANGE) { + try { + return AbsoluteCellRange.fromAst(arg, state.formulaAddress).size() > 1 + } catch (e) { + return false + } } + return false } /** diff --git a/src/license/functionCapabilities.ts b/src/license/functionCapabilities.ts index a26733ec5f..148827e21b 100644 --- a/src/license/functionCapabilities.ts +++ b/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', ] From c0c23930ba6c7f55d351992b8466e06543026fe5 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Wed, 14 Oct 2026 23:53:43 +0000 Subject: [PATCH 2/7] docs: link the pull request in the changelog Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 115a5d8e4c..169586d3ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added -- 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. +- 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 From 958e49a2223a92f048802f6128cdbef786cf5638 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 15 Oct 2026 00:12:36 +0000 Subject: [PATCH 3/7] Simplify TYPE for arrays and drop an unreachable guard Co-Authored-By: Claude Sonnet 5.5 --- src/interpreter/plugin/InformationPlugin.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/interpreter/plugin/InformationPlugin.ts b/src/interpreter/plugin/InformationPlugin.ts index 13644326a5..dd1758b024 100644 --- a/src/interpreter/plugin/InformationPlugin.ts +++ b/src/interpreter/plugin/InformationPlugin.ts @@ -238,7 +238,7 @@ export class InformationPlugin extends FunctionPlugin implements FunctionPluginT } const value = this.evaluateAst(ast.args[0], state) if (value instanceof SimpleRangeValue) { - return value.numberOfElements() === 1 ? valueTypeCode(value.data[0][0]) : TYPE_CODE_ARRAY + return TYPE_CODE_ARRAY } return valueTypeCode(value) } @@ -268,11 +268,7 @@ export class InformationPlugin extends FunctionPlugin implements FunctionPluginT return true } if (arg.type === AstNodeType.CELL_RANGE) { - try { - return AbsoluteCellRange.fromAst(arg, state.formulaAddress).size() > 1 - } catch (e) { - return false - } + return AbsoluteCellRange.fromAst(arg, state.formulaAddress).size() > 1 } return false } From 3c09924413f6936bbc753b1c154c6b82a85ad969 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 15 Oct 2026 02:09:50 +0000 Subject: [PATCH 4/7] Give the new rows of the differences list all five columns and add the measured Excel results Co-Authored-By: Claude Sonnet 5.5 --- docs/guide/list-of-differences.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/guide/list-of-differences.md b/docs/guide/list-of-differences.md index a142db351e..75e77d10d0 100644 --- a/docs/guide/list-of-differences.md +++ b/docs/guide/list-of-differences.md @@ -53,7 +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. | -| ERROR.TYPE function | =ERROR.TYPE(#NULL!) | HyperFormula has no `#NULL!` error, so the code 1 is never returned and the formula does not parse. `#CYCLE!`, which Excel does not have, has no code and gives `#N/A`. | +| TYPE of a computed array | =TYPE(A1:A2+1)
=TYPE(ROW(A1:A2)+0) | Only a bare `ROW(range)` or `COLUMN(range)` is recognised as an array, so `TYPE(ROW(A1:A2)+0)` gives `1`. `TYPE(A1:A2+1)` and `TYPE(IF(TRUE(), A1:A2))` give `16` (`64` with `useArrayArithmetic`). | Returns `64` for every array, including the three examples. | Not checked. | +| 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`. | Returns `1` for `#NULL!`. | Not checked. | ## Built-in functions From 4be683344aa062bf2c48fafd7e55ae823435baab Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 23 Oct 2026 00:13:49 +0000 Subject: [PATCH 5/7] HF-488: Put the measured Excel results of the TYPE and ERROR.TYPE rows in the Excel column The two new rows of the list of differences had the Excel results in the Google Sheets column. The TYPE row now also says that, without array arithmetic, a computed range is reduced to the cell in the formula's row, so TYPE gives 1 or 16 depending on where the formula sits. The ERROR.TYPE description lists 9 for #SPILL!. Co-Authored-By: Claude Opus 5.5 --- docs/guide/list-of-differences.md | 4 ++-- .../interpreter/functionMetadata/categories/information.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guide/list-of-differences.md b/docs/guide/list-of-differences.md index 75e77d10d0..a4bf9720dd 100644 --- a/docs/guide/list-of-differences.md +++ b/docs/guide/list-of-differences.md @@ -53,8 +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(A1:A2+1)
=TYPE(ROW(A1:A2)+0) | Only a bare `ROW(range)` or `COLUMN(range)` is recognised as an array, so `TYPE(ROW(A1:A2)+0)` gives `1`. `TYPE(A1:A2+1)` and `TYPE(IF(TRUE(), A1:A2))` give `16` (`64` with `useArrayArithmetic`). | Returns `64` for every array, including the three examples. | Not checked. | -| 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`. | Returns `1` for `#NULL!`. | Not checked. | +| 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: `TYPE` gives `1` in rows 1 and 2, and `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/interpreter/functionMetadata/categories/information.ts b/hyperformula/src/interpreter/functionMetadata/categories/information.ts index 788f78c96e..62cfe3cf00 100644 --- a/hyperformula/src/interpreter/functionMetadata/categories/information.ts +++ b/hyperformula/src/interpreter/functionMetadata/categories/information.ts @@ -13,7 +13,7 @@ import {FunctionDoc} from '../FunctionDescription' 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! and 7 for #N/A, or #N/A for a value that is not an error.', + 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())'], From 796a8c108a22851d0ad4ed7a29bec8cbbec31d8c Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 23 Oct 2026 00:22:04 +0000 Subject: [PATCH 6/7] HF-488: Say that TYPE classifies the intersected cell's value, not always a number Co-Authored-By: Claude Opus 5.5 --- docs/guide/list-of-differences.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/list-of-differences.md b/docs/guide/list-of-differences.md index a4bf9720dd..37e5b45e26 100644 --- a/docs/guide/list-of-differences.md +++ b/docs/guide/list-of-differences.md @@ -53,7 +53,7 @@ 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: `TYPE` gives `1` in rows 1 and 2, and `16` (`#VALUE!`) in any other row. With `useArrayArithmetic` set to `true`, both give `64`. | Not checked. | Returns `64` for all three examples. | +| 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 From 47010903e0299e312bda571a871aa86d842ba32d Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 23 Oct 2026 00:43:18 +0000 Subject: [PATCH 7/7] HF-488: Return 16 from TYPE for ROW or COLUMN of a range across sheets instead of throwing Building the range of a sheet-spanning reference threw SheetsNotEqual out of evaluation. Such a reference makes ROW and COLUMN return #REF!, so TYPE now classifies it as an error. Co-Authored-By: Claude Opus 5.5 --- .../src/interpreter/plugin/InformationPlugin.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/hyperformula/src/interpreter/plugin/InformationPlugin.ts b/hyperformula/src/interpreter/plugin/InformationPlugin.ts index dd1758b024..ad6d0d5faf 100644 --- a/hyperformula/src/interpreter/plugin/InformationPlugin.ts +++ b/hyperformula/src/interpreter/plugin/InformationPlugin.ts @@ -264,13 +264,17 @@ export class InformationPlugin extends FunctionPlugin implements FunctionPluginT if (arg === undefined) { return false } - if (arg.type === AstNodeType.COLUMN_RANGE || arg.type === AstNodeType.ROW_RANGE) { - return true + 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 false + return true } /**