diff --git a/CHANGELOG.md b/CHANGELOG.md
index 323c746b2d..11cbf80716 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,9 @@ 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 `TAKE` dynamic-array function. [#1722](https://github.com/handsontable/hyperformula/pull/1722)
+- Added the `DROP` dynamic-array function. [#1796](https://github.com/handsontable/hyperformula/pull/1796)
+- Added the `TRIMRANGE` dynamic-array function. [#1842](https://github.com/handsontable/hyperformula/pull/1842)
### Fixed
diff --git a/docs/guide/list-of-differences.md b/docs/guide/list-of-differences.md
index 660fda0f98..b17705f7e5 100644
--- a/docs/guide/list-of-differences.md
+++ b/docs/guide/list-of-differences.md
@@ -46,6 +46,8 @@ See a full list of differences between HyperFormula, Microsoft Excel, and Google
| Ranges created with `:` | A1:A2
A$1:$A$2
A:C
1:2
Sheet1!A1:A2 | Allowed ranges consist of two addresses (A1:B5), columns (A:C) or rows (3:5).
They cannot be mixed or contain named expressions. | Everything allowed. | Same as Google Sheets. |
| Formatting inside the TEXT function | TEXT(A1,"dd-mm-yy")
TEXT(A1,"###.###”) | To support all date, time and currency formats, set [`stringifyDateTime`](compatibility-with-microsoft-excel.md#date-and-time-formats) and [`stringifyCurrency`](currency-handling.md) configuration options. | A wide variety of options for string formatting is supported. | Same as Google Sheets. |
| Cell references inside inline arrays | ={A1, A2} | The array's value is calculated but not updated when the cells' values change. | The array's value is calculated and updated when the cells' values change. | ERROR: invalid array |
+| Errors in array results | =DROP(Data!A1:C3, NA()) | The error fills the whole area reserved for the array (here 3x3), because the array's size is predicted before the formula is evaluated. | No such function. | A single N/A error. |
+| Array size that depends on cell values | =DROP(Data!A1:C3, Counts!A1) in A1,
where Counts!A1 = 1 and A3 holds a value | Returns SPILL: the whole source (3x3) is reserved, because the count is read after the array's size is set. | No such function. | Spills the two remaining rows into A1:C2. |
| SPLIT function | =SPLIT("Lorem ipsum dolor", 0) | This function works differently from Google Sheets version but should be sufficient to achieve the same functionality in most scenarios. Read SPLIT function description on [the Built-in Functions page](built-in-functions.md#text). | Different syntax and return value. | No such function. |
| DATEVALUE function | =DATEVALUE("25/02/1991") | Type of the returned value: `CellValueDetailedType.NUMBER_DATE` (compliant with the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard) | Cell auto-formatted as **regular number** | Cell auto-formatted as **regular number** |
| TIMEVALUE function | =TIMEVALUE("14:31") | Type of the returned value: `CellValueDetailedType.NUMBER_TIME` (compliant with the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard) | Cell auto-formatted as **regular number** | Cell auto-formatted as **regular number** |
@@ -117,6 +119,18 @@ To remove the differences, create [custom implementations](custom-functions.md)
| NORMSDIST | =NORMSDIST(0, TRUE()) | 0.5 | Wrong number | Wrong number |
| ADDRESS | =ADDRESS(1,1,4, TRUE(), "") | !A1 | ''!A1 | !A1 |
| SEQUENCE | =SEQUENCE(0) | VALUE | N/A | CALC |
+| TAKE | =TAKE(A1:A3, 0) | N/A | No such function. | CALC |
+| TAKE | =TAKE(Data!A:A, , 1) | Spills to the source sheet's current height from row 1; returns SPILL below row 1. | No such function. | Spills all 1,048,576 rows from row 1, with blank source cells as 0; returns SPILL below row 1. |
+| DROP | =DROP(A1:A3, 3) | N/A | No such function. | CALC |
+| DROP | =DROP(Data!A:A, 1) | Spills to the source sheet's current height minus one row; returns SPILL below row 2. | No such function. | Spills all 1,048,575 remaining rows, with blank source cells as 0; returns SPILL below row 2. |
+| DROP | =DROP(Data!A:A, B1) | Spills from row 1 only; returns SPILL below row 1, because the count is read after the result size is set. | No such function. | Spills 1,048,576 minus B1 rows when entered in row B1+1 or above; returns SPILL below. |
+| DROP | =DROP(A1:C3, {1,2}) | Uses the first count and drops one row. | No such function. | Returns a 1x2 array of VALUE errors. |
+| DROP | =ISREF(DROP(Data!A1:C3, 1)) | FALSE (returns values, not a reference) | No such function. | TRUE |
+| TRIMRANGE | =TRIMRANGE(A1:C10) | Reserves the whole range (10x3): the trimmed values fill its top-left corner, the other cells stay blank, and a value in one of them makes the formula return SPILL. | No such function. | Spills only the trimmed rows and columns, and follows the data. |
+| TRIMRANGE | =TRIMRANGE(Data!A:A) | Spills to the source sheet's current height from row 1; returns SPILL below row 1. | No such function. | Spills the trimmed rows from any row. |
+| TRIMRANGE | =TRIMRANGE(A1:C3, {0,3}) | VALUE | No such function. | Returns a 1x2 array of VALUE errors. |
+| TRIMRANGE | =ISREF(TRIMRANGE(A1:C3)) | FALSE (returns values, not a reference) | No such function. | TRUE |
+| TRIMRANGE | =A1:.C10 | ERROR: the trim reference operators (`.:`, `:.`, `.:.`) are not supported. | ERROR: formula parse error. | Same as =TRIMRANGE(A1:C10, 2, 2). |
| INT | =INT(-8.9) | -8 | -9 | -9 |
| ISEVEN | =ISEVEN(2.5) | FALSE | TRUE | TRUE |
| ISODD | =ISODD(3.5) | FALSE | TRUE | TRUE |
diff --git a/hyperformula/src/error-message.ts b/hyperformula/src/error-message.ts
index d42040901c..e4eb42e8f7 100644
--- a/hyperformula/src/error-message.ts
+++ b/hyperformula/src/error-message.ts
@@ -68,6 +68,7 @@ export class ErrorMessage {
public static IncorrectDateTime = 'String does not represent correct DateTime.'
public static CharacterCodeBounds = 'Character code out of bounds.'
public static NonZero = 'Argument cannot be 0.'
+ public static ZeroRowOrColumnCount = 'Row or column count cannot be zero.'
public static LessThanOne = 'Argument cannot be less than 1.'
public static WeekendString = 'Incorrect weekend bitmask string.'
public static InvalidRoman = 'Invalid roman numeral.'
diff --git a/hyperformula/src/i18n/languages/csCZ.ts b/hyperformula/src/i18n/languages/csCZ.ts
index fea286083c..151283aeaf 100644
--- a/hyperformula/src/i18n/languages/csCZ.ts
+++ b/hyperformula/src/i18n/languages/csCZ.ts
@@ -98,6 +98,7 @@ const dictionary: RawTranslationPackage = {
DELTA: 'DELTA',
DOLLARDE: 'DOLLARDE',
DOLLARFR: 'DOLLARFR',
+ DROP: 'ZAHODIT',
EDATE: 'EDATE',
EFFECT: "EFFECT",
EOMONTH: 'EOMONTH',
@@ -225,6 +226,7 @@ const dictionary: RawTranslationPackage = {
SWITCH: '',
SYD: 'ODPIS.NELIN',
T: 'T',
+ TAKE: 'VZÍT',
TAN: 'TG',
TANH: 'TGH',
TBILLEQ: 'TBILLEQ',
@@ -237,6 +239,7 @@ const dictionary: RawTranslationPackage = {
TODAY: 'DNES',
TRANSPOSE: 'TRANSPOZICE',
TRIM: 'PROČISTIT',
+ TRIMRANGE: 'OŘÍZNOUTOBLAST',
TRUE: 'PRAVDA',
TRUNC: 'USEKNOUT',
UNICHAR: 'UNICHAR',
diff --git a/hyperformula/src/i18n/languages/daDK.ts b/hyperformula/src/i18n/languages/daDK.ts
index 9264551249..86ac6eee50 100644
--- a/hyperformula/src/i18n/languages/daDK.ts
+++ b/hyperformula/src/i18n/languages/daDK.ts
@@ -98,6 +98,7 @@ const dictionary: RawTranslationPackage = {
DELTA: 'DELTA',
DOLLARDE: 'KR.DECIMAL',
DOLLARFR: 'KR.BRØK',
+ DROP: 'UDELAD',
EDATE: 'EDATO',
EFFECT: "EFFEKTIV.RENTE",
EOMONTH: 'SLUT.PÅ.MÅNED',
@@ -225,6 +226,7 @@ const dictionary: RawTranslationPackage = {
SWITCH: '',
SYD: 'ÅRSAFSKRIVNING',
T: 'T',
+ TAKE: 'TAG',
TAN: 'TAN',
TANH: 'TANH',
TBILLEQ: 'STATSOBLIGATION',
@@ -237,6 +239,7 @@ const dictionary: RawTranslationPackage = {
TODAY: 'IDAG',
TRANSPOSE: 'TRANSPONER',
TRIM: 'FJERN.OVERFLØDIGE.BLANKE',
+ TRIMRANGE: 'TRIMOMRÅDE',
TRUE: 'TRUE',
TRUNC: 'AFKORT',
UNICHAR: 'UNICHAR',
diff --git a/hyperformula/src/i18n/languages/deDE.ts b/hyperformula/src/i18n/languages/deDE.ts
index 4d3d39319b..4d08a74dca 100644
--- a/hyperformula/src/i18n/languages/deDE.ts
+++ b/hyperformula/src/i18n/languages/deDE.ts
@@ -98,6 +98,7 @@ const dictionary: RawTranslationPackage = {
DELTA: 'DELTA',
DOLLARDE: 'NOTIERUNGDEZ',
DOLLARFR: 'NOTIERUNGBRU',
+ DROP: 'WEGLASSEN',
EDATE: 'EDATUM',
EFFECT: "EFFEKTIV",
EOMONTH: 'MONATSENDE',
@@ -225,6 +226,7 @@ const dictionary: RawTranslationPackage = {
SWITCH: '',
SYD: 'DIA',
T: 'T',
+ TAKE: 'ÜBERNEHMEN',
TAN: 'TAN',
TANH: 'TANHYP',
TBILLEQ: 'TBILLÄQUIV',
@@ -237,6 +239,7 @@ const dictionary: RawTranslationPackage = {
TODAY: 'HEUTE',
TRANSPOSE: 'MTRANS',
TRIM: 'GLÄTTEN',
+ TRIMRANGE: 'ABSCHNBEREICH',
TRUE: 'WAHR',
TRUNC: 'KÜRZEN',
UNICHAR: 'UNIZEICHEN',
diff --git a/hyperformula/src/i18n/languages/enGB.ts b/hyperformula/src/i18n/languages/enGB.ts
index d271b732cd..78de70b809 100644
--- a/hyperformula/src/i18n/languages/enGB.ts
+++ b/hyperformula/src/i18n/languages/enGB.ts
@@ -99,6 +99,7 @@ const dictionary: RawTranslationPackage = {
DELTA: 'DELTA',
DOLLARDE: 'DOLLARDE',
DOLLARFR: 'DOLLARFR',
+ DROP: 'DROP',
EDATE: 'EDATE',
EFFECT: 'EFFECT',
EOMONTH: 'EOMONTH',
@@ -227,6 +228,7 @@ const dictionary: RawTranslationPackage = {
SWITCH: 'SWITCH',
SYD: 'SYD',
T: 'T',
+ TAKE: 'TAKE',
TAN: 'TAN',
TANH: 'TANH',
TBILLEQ: 'TBILLEQ',
@@ -239,6 +241,7 @@ const dictionary: RawTranslationPackage = {
TODAY: 'TODAY',
TRANSPOSE: 'TRANSPOSE',
TRIM: 'TRIM',
+ TRIMRANGE: 'TRIMRANGE',
TRUE: 'TRUE',
TRUNC: 'TRUNC',
UNICHAR: 'UNICHAR',
diff --git a/hyperformula/src/i18n/languages/esES.ts b/hyperformula/src/i18n/languages/esES.ts
index 9c6b817b98..db4f3da7e6 100644
--- a/hyperformula/src/i18n/languages/esES.ts
+++ b/hyperformula/src/i18n/languages/esES.ts
@@ -98,6 +98,7 @@ export const dictionary: RawTranslationPackage = {
DELTA: 'DELTA',
DOLLARDE: 'MONEDA.DEC',
DOLLARFR: 'MONEDA.FRAC',
+ DROP: 'EXCLUIR',
EDATE: 'FECHA.MES',
EFFECT: "INT.EFECTIVO",
EOMONTH: 'FIN.MES',
@@ -225,6 +226,7 @@ export const dictionary: RawTranslationPackage = {
SWITCH: '',
SYD: 'SYD',
T: 'T',
+ TAKE: 'TOMAR',
TAN: 'TAN',
TANH: 'TANH',
TBILLEQ: 'LETRA.DE.TEST.EQV.A.BONO',
@@ -237,6 +239,7 @@ export const dictionary: RawTranslationPackage = {
TODAY: 'HOY',
TRANSPOSE: 'TRANSPONER',
TRIM: 'ESPACIOS',
+ TRIMRANGE: 'RECORTAR.RANGO',
TRUE: 'VERDADERO',
TRUNC: 'TRUNCAR',
UNICHAR: 'UNICHAR',
diff --git a/hyperformula/src/i18n/languages/fiFI.ts b/hyperformula/src/i18n/languages/fiFI.ts
index 99ff05708f..6eff376beb 100644
--- a/hyperformula/src/i18n/languages/fiFI.ts
+++ b/hyperformula/src/i18n/languages/fiFI.ts
@@ -98,6 +98,7 @@ const dictionary: RawTranslationPackage = {
DELTA: 'SAMA.ARVO',
DOLLARDE: 'VALUUTTA.DES',
DOLLARFR: 'VALUUTTA.MURTO',
+ DROP: 'HYLKÄÄ',
EDATE: 'PÄIVÄ.KUUKAUSI',
EFFECT: "KORKO.EFEKT",
EOMONTH: 'KUUKAUSI.LOPPU',
@@ -225,6 +226,7 @@ const dictionary: RawTranslationPackage = {
SWITCH: '',
SYD: 'VUOSIPOISTO',
T: 'T',
+ TAKE: 'OTA',
TAN: 'TAN',
TANH: 'TANH',
TBILLEQ: 'OBLIG.TUOTTOPROS',
@@ -237,6 +239,7 @@ const dictionary: RawTranslationPackage = {
TODAY: 'TÄMÄ.PÄIVÄ',
TRANSPOSE: 'TRANSPONOI',
TRIM: 'POISTA.VÄLIT',
+ TRIMRANGE: 'RAJAAALUE',
TRUE: 'TOSI',
TRUNC: 'KATKAISE',
UNICHAR: 'UNICODEMERKKI',
diff --git a/hyperformula/src/i18n/languages/frFR.ts b/hyperformula/src/i18n/languages/frFR.ts
index f7b9be45f7..1f79cc9b5a 100644
--- a/hyperformula/src/i18n/languages/frFR.ts
+++ b/hyperformula/src/i18n/languages/frFR.ts
@@ -98,6 +98,7 @@ const dictionary: RawTranslationPackage = {
DELTA: 'DELTA',
DOLLARDE: 'PRIX.DEC',
DOLLARFR: 'PRIX.FRAC',
+ DROP: 'EXCLURE',
EDATE: 'MOIS.DECALER',
EFFECT: "TAUX.EFFECTIF",
EOMONTH: 'FIN.MOIS',
@@ -225,6 +226,7 @@ const dictionary: RawTranslationPackage = {
SWITCH: '',
SYD: 'SYD',
T: 'T',
+ TAKE: 'PRENDRE',
TAN: 'TAN',
TANH: 'TANH',
TBILLEQ: 'TAUX.ESCOMPTE.R',
@@ -237,6 +239,7 @@ const dictionary: RawTranslationPackage = {
TODAY: 'AUJOURDHUI',
TRANSPOSE: 'TRANSPOSE',
TRIM: 'SUPPRESPACE',
+ TRIMRANGE: 'SUPPR.PLAGE',
TRUE: 'VRAI',
TRUNC: 'TRONQUE',
UNICHAR: 'UNICAR',
diff --git a/hyperformula/src/i18n/languages/huHU.ts b/hyperformula/src/i18n/languages/huHU.ts
index e40219553a..541a13f704 100644
--- a/hyperformula/src/i18n/languages/huHU.ts
+++ b/hyperformula/src/i18n/languages/huHU.ts
@@ -98,6 +98,7 @@ const dictionary: RawTranslationPackage = {
DELTA: 'DELTA',
DOLLARDE: 'FORINT.DEC',
DOLLARFR: 'FORINT.TÖRT',
+ DROP: 'ELTÁVOLÍT',
EDATE: 'KALK.DÁTUM',
EFFECT: "TÉNYLEGES",
EOMONTH: 'HÓNAP.UTOLSÓ.NAP',
@@ -225,6 +226,7 @@ const dictionary: RawTranslationPackage = {
SWITCH: '',
SYD: 'ÉSZÖ',
T: 'T',
+ TAKE: 'ÁTHELYEZ',
TAN: 'TAN',
TANH: 'TANH',
TBILLEQ: 'KJEGY.EGYENÉRT',
@@ -237,6 +239,7 @@ const dictionary: RawTranslationPackage = {
TODAY: 'MA',
TRANSPOSE: 'TRANSZPONÁLÁS',
TRIM: 'KIMETSZ',
+ TRIMRANGE: 'KIMETSZÉSITARTOMÁNY',
TRUE: 'IGAZ',
TRUNC: 'CSONK',
UNICHAR: 'UNIKARAKTER',
diff --git a/hyperformula/src/i18n/languages/idID.ts b/hyperformula/src/i18n/languages/idID.ts
index 719cb11db6..d967f57da4 100644
--- a/hyperformula/src/i18n/languages/idID.ts
+++ b/hyperformula/src/i18n/languages/idID.ts
@@ -99,6 +99,7 @@ const dictionary: RawTranslationPackage = {
DELTA: 'DELTA',
DOLLARDE: 'DOLAR.DES',
DOLLARFR: 'DOLAR.PEC',
+ DROP: 'DROP',
EDATE: 'EDATE',
EFFECT: 'EFEKTIF',
EOMONTH: 'AKHIR.BULAN',
@@ -227,6 +228,7 @@ const dictionary: RawTranslationPackage = {
SWITCH: 'BERALIH',
SYD: 'SYD',
T: 'T',
+ TAKE: 'TAKE',
TAN: 'TAN',
TANH: 'TANH',
TBILLEQ: 'TBILL.SAMA',
@@ -239,6 +241,7 @@ const dictionary: RawTranslationPackage = {
TODAY: 'HARI.INI',
TRANSPOSE: 'TRANSPOS',
TRIM: 'HAPUS.SPASI',
+ TRIMRANGE: 'TRIMRANGE',
TRUE: 'BENAR',
TRUNC: 'POTONG',
UNICHAR: 'UNIKARAKTER',
diff --git a/hyperformula/src/i18n/languages/itIT.ts b/hyperformula/src/i18n/languages/itIT.ts
index 1a494628f8..bac1abd64e 100644
--- a/hyperformula/src/i18n/languages/itIT.ts
+++ b/hyperformula/src/i18n/languages/itIT.ts
@@ -98,6 +98,7 @@ const dictionary: RawTranslationPackage = {
DELTA: 'DELTA',
DOLLARDE: 'VALUTA.DEC',
DOLLARFR: 'VALUTA.FRAZ',
+ DROP: 'ESCLUDI',
EDATE: 'DATA.MESE',
EFFECT: "EFFETTIVO",
EOMONTH: 'FINE.MESE',
@@ -225,6 +226,7 @@ const dictionary: RawTranslationPackage = {
SWITCH: '',
SYD: 'AMMORT.ANNUO',
T: 'T',
+ TAKE: 'INCLUDI',
TAN: 'TAN',
TANH: 'TANH',
TBILLEQ: 'BOT.EQUIV',
@@ -237,6 +239,7 @@ const dictionary: RawTranslationPackage = {
TODAY: 'OGGI',
TRANSPOSE: 'MATR.TRASPOSTA',
TRIM: 'ANNULLA.SPAZI',
+ TRIMRANGE: 'ESCLUDI.VUOTE',
TRUE: 'VERO',
TRUNC: 'TRONCA',
UNICHAR: 'CARATT.UNI',
diff --git a/hyperformula/src/i18n/languages/nbNO.ts b/hyperformula/src/i18n/languages/nbNO.ts
index ff15415a98..75c59a1995 100644
--- a/hyperformula/src/i18n/languages/nbNO.ts
+++ b/hyperformula/src/i18n/languages/nbNO.ts
@@ -98,6 +98,7 @@ const dictionary: RawTranslationPackage = {
DELTA: 'DELTA',
DOLLARDE: 'DOLLARDE',
DOLLARFR: 'DOLLARBR',
+ DROP: 'UTELAT',
EDATE: 'DAG.ETTER',
EFFECT: "EFFEKTIV.RENTE",
EOMONTH: 'MÅNEDSSLUTT',
@@ -225,6 +226,7 @@ const dictionary: RawTranslationPackage = {
SWITCH: '',
SYD: 'ÅRSAVS',
T: 'T',
+ TAKE: 'TA',
TAN: 'TAN',
TANH: 'TANH',
TBILLEQ: 'TBILLEKV',
@@ -237,6 +239,7 @@ const dictionary: RawTranslationPackage = {
TODAY: 'IDAG',
TRANSPOSE: 'TRANSPONER',
TRIM: 'TRIMME',
+ TRIMRANGE: 'TRIMMEOMRÅDE',
TRUE: 'SANN',
TRUNC: 'AVKORT',
UNICHAR: 'UNICODETEGN',
diff --git a/hyperformula/src/i18n/languages/nlNL.ts b/hyperformula/src/i18n/languages/nlNL.ts
index c491e2f44b..f2db79ee21 100644
--- a/hyperformula/src/i18n/languages/nlNL.ts
+++ b/hyperformula/src/i18n/languages/nlNL.ts
@@ -98,6 +98,7 @@ const dictionary: RawTranslationPackage = {
DELTA: 'DELTA',
DOLLARDE: 'EURO.DE',
DOLLARFR: 'EURO.BR',
+ DROP: 'WEGLATEN',
EDATE: 'ZELFDE.DAG',
EFFECT: "EFFECT.RENTE",
EOMONTH: 'LAATSTE.DAG',
@@ -225,6 +226,7 @@ const dictionary: RawTranslationPackage = {
SWITCH: '',
SYD: 'SYD',
T: 'T',
+ TAKE: 'NEMEN',
TAN: 'TAN',
TANH: 'TANH',
TBILLEQ: 'SCHATK.OBL',
@@ -237,6 +239,7 @@ const dictionary: RawTranslationPackage = {
TODAY: 'VANDAAG',
TRANSPOSE: 'TRANSPONEREN',
TRIM: 'SPATIES.WISSEN',
+ TRIMRANGE: 'TRIMBEREIK',
TRUE: 'WAAR',
TRUNC: 'GEHEEL',
UNICHAR: 'UNITEKEN',
diff --git a/hyperformula/src/i18n/languages/plPL.ts b/hyperformula/src/i18n/languages/plPL.ts
index 6fa4f016b4..b13cd1f60f 100644
--- a/hyperformula/src/i18n/languages/plPL.ts
+++ b/hyperformula/src/i18n/languages/plPL.ts
@@ -98,6 +98,7 @@ const dictionary: RawTranslationPackage = {
DELTA: 'DELTA',
DOLLARDE: 'CENA.DZIES',
DOLLARFR: 'CENA.UŁAM',
+ DROP: 'POMIŃ',
EDATE: 'NR.SER.DATY',
EFFECT: "EFEKTYWNA",
EOMONTH: 'NR.SER.OST.DN.MIEŚ',
@@ -225,6 +226,7 @@ const dictionary: RawTranslationPackage = {
SWITCH: 'PRZEŁĄCZ',
SYD: 'SYD',
T: 'T',
+ TAKE: 'WYCINEK',
TAN: 'TAN',
TANH: 'TANH',
TBILLEQ: 'RENT.EKW.BS',
@@ -237,6 +239,7 @@ const dictionary: RawTranslationPackage = {
TODAY: 'DZIŚ',
TRANSPOSE: 'TRANSPONUJ',
TRIM: 'USUŃ.ZBĘDNE.ODSTĘPY',
+ TRIMRANGE: 'PRZYTNIJZAKRES',
TRUE: 'PRAWDA',
TRUNC: 'LICZBA.CAŁK',
UNICHAR: 'ZNAK.UNICODE',
diff --git a/hyperformula/src/i18n/languages/ptPT.ts b/hyperformula/src/i18n/languages/ptPT.ts
index 6c1e68c328..17a2dd4a82 100644
--- a/hyperformula/src/i18n/languages/ptPT.ts
+++ b/hyperformula/src/i18n/languages/ptPT.ts
@@ -98,6 +98,7 @@ const dictionary: RawTranslationPackage = {
DELTA: 'DELTA',
DOLLARDE: 'MOEDADEC',
DOLLARFR: 'MOEDAFRA',
+ DROP: 'EXCLUIR',
EDATE: 'DATAM',
EFFECT: "EFETIVA",
EOMONTH: 'FIMMÊS',
@@ -225,6 +226,7 @@ const dictionary: RawTranslationPackage = {
SWITCH: '',
SYD: 'SDA',
T: 'T',
+ TAKE: 'INCLUIR',
TAN: 'TAN',
TANH: 'TANH',
TBILLEQ: 'OTN',
@@ -237,6 +239,7 @@ const dictionary: RawTranslationPackage = {
TODAY: 'HOJE',
TRANSPOSE: 'TRANSPOR',
TRIM: 'ARRUMAR',
+ TRIMRANGE: 'APARARINTERVALO',
TRUE: 'VERDADEIRO',
TRUNC: 'TRUNCAR',
UNICHAR: 'CARACTUNI',
diff --git a/hyperformula/src/i18n/languages/ruRU.ts b/hyperformula/src/i18n/languages/ruRU.ts
index 6b35c1ce2f..4da13f847e 100644
--- a/hyperformula/src/i18n/languages/ruRU.ts
+++ b/hyperformula/src/i18n/languages/ruRU.ts
@@ -98,6 +98,7 @@ const dictionary: RawTranslationPackage = {
DELTA: 'ДЕЛЬТА',
DOLLARDE: 'РУБЛЬ.ДЕС',
DOLLARFR: 'РУБЛЬ.ДРОБЬ',
+ DROP: 'СБРОСИТЬ',
EDATE: 'ДАТАМЕС',
EFFECT: "ЭФФЕКТ",
EOMONTH: 'КОНМЕСЯЦА',
@@ -225,6 +226,7 @@ const dictionary: RawTranslationPackage = {
SWITCH: '',
SYD: 'АСЧ',
T: 'Т',
+ TAKE: 'ВЗЯТЬ',
TAN: 'TAN',
TANH: 'TANH',
TBILLEQ: 'РАВНОКЧЕК',
@@ -237,6 +239,7 @@ const dictionary: RawTranslationPackage = {
TODAY: 'СЕГОДНЯ',
TRANSPOSE: 'ТРАНСП',
TRIM: 'СЖПРОБЕЛЫ',
+ TRIMRANGE: 'УРЕЗДИАПАЗОН',
TRUE: 'ИСТИНА',
TRUNC: 'ОТБР',
UNICHAR: 'ЮНИСИМВ',
diff --git a/hyperformula/src/i18n/languages/svSE.ts b/hyperformula/src/i18n/languages/svSE.ts
index 5f87580087..f7043eff0a 100644
--- a/hyperformula/src/i18n/languages/svSE.ts
+++ b/hyperformula/src/i18n/languages/svSE.ts
@@ -98,6 +98,7 @@ const dictionary: RawTranslationPackage = {
DELTA: 'DELTA',
DOLLARDE: 'DECTAL',
DOLLARFR: 'BRÅK',
+ DROP: 'UTESLUT',
EDATE: 'EDATUM',
EFFECT: "EFFRÄNTA",
EOMONTH: 'SLUTMÅNAD',
@@ -225,6 +226,7 @@ const dictionary: RawTranslationPackage = {
SWITCH: '',
SYD: 'ÅRSAVSKR',
T: 'T',
+ TAKE: 'TA',
TAN: 'TAN',
TANH: 'TANH',
TBILLEQ: 'SSVXEKV',
@@ -237,6 +239,7 @@ const dictionary: RawTranslationPackage = {
TODAY: 'IDAG',
TRANSPOSE: 'TRANSPONERA',
TRIM: 'RENSA',
+ TRIMRANGE: 'KLIPPOMRÅDE',
TRUE: 'SANT',
TRUNC: 'AVKORTA',
UNICHAR: 'UNICHAR',
diff --git a/hyperformula/src/i18n/languages/trTR.ts b/hyperformula/src/i18n/languages/trTR.ts
index 93c2176d37..53fe0c0959 100644
--- a/hyperformula/src/i18n/languages/trTR.ts
+++ b/hyperformula/src/i18n/languages/trTR.ts
@@ -98,6 +98,7 @@ const dictionary: RawTranslationPackage = {
DELTA: 'DELTA',
DOLLARDE: 'LİRAON',
DOLLARFR: 'LİRAKES',
+ DROP: 'BIRAK',
EDATE: 'SERİTARİH',
EFFECT: "ETKİN",
EOMONTH: 'SERİAY',
@@ -225,6 +226,7 @@ const dictionary: RawTranslationPackage = {
SWITCH: '',
SYD: 'YAT',
T: 'T',
+ TAKE: 'AL',
TAN: 'TAN',
TANH: 'TANH',
TBILLEQ: 'HTAHEŞ',
@@ -237,6 +239,7 @@ const dictionary: RawTranslationPackage = {
TODAY: 'BUGÜN',
TRANSPOSE: 'DEVRİK_DÖNÜŞÜM',
TRIM: 'KIRP',
+ TRIMRANGE: 'KIRPARALIĞI',
TRUE: 'DOĞRU',
TRUNC: 'NSAT',
UNICHAR: 'UNICODEKARAKTERİ',
diff --git a/hyperformula/src/interpreter/Interpreter.ts b/hyperformula/src/interpreter/Interpreter.ts
index 6444332537..73115a4bcc 100644
--- a/hyperformula/src/interpreter/Interpreter.ts
+++ b/hyperformula/src/interpreter/Interpreter.ts
@@ -31,6 +31,7 @@ import {
import {CriterionBuilder} from './Criterion'
import {FunctionRegistry} from './FunctionRegistry'
import {InterpreterState} from './InterpreterState'
+import {FunctionPluginDefinition} from './plugin/FunctionPlugin'
import {
cloneNumber,
EmptyValue,
@@ -65,6 +66,18 @@ export class Interpreter {
this.functionCallLicenseGate = new FunctionCallLicenseGate(config, functionRegistry)
}
+ /**
+ * Checks whether a function is implemented by a specific plugin in this interpreter's engine instance.
+ *
+ * @param {string} functionId - The canonical function identifier.
+ * @param {FunctionPluginDefinition} plugin - The plugin class to compare with the registered implementation.
+ * @returns {boolean} `true` when the function is implemented by the given plugin class.
+ * @internal
+ */
+ public isFunctionImplementedBy(functionId: string, plugin: FunctionPluginDefinition): boolean {
+ return this.functionRegistry.getFunctionPlugin(functionId) === plugin
+ }
+
public evaluateAst(ast: Ast, state: InterpreterState): InterpreterValue {
let val = this.evaluateAstWithoutPostprocessing(ast, state)
if (isExtendedNumber(val)) {
diff --git a/hyperformula/src/interpreter/functionMetadata/categories/lookup-and-reference.ts b/hyperformula/src/interpreter/functionMetadata/categories/lookup-and-reference.ts
index ef6b848d7f..f72310db63 100644
--- a/hyperformula/src/interpreter/functionMetadata/categories/lookup-and-reference.ts
+++ b/hyperformula/src/interpreter/functionMetadata/categories/lookup-and-reference.ts
@@ -39,6 +39,13 @@ export const LOOKUP_AND_REFERENCE_DOCS: Record = {
documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html',
examples: ['=COLUMNS(A1:C5)', '=COLUMNS(A1:F1)'],
},
+ DROP: {
+ category: 'Lookup and reference',
+ shortDescription: 'Removes specified rows or columns from the beginning or end of an array.',
+ parameters: [{name: 'array', description: 'The array or range from which to remove rows or columns.'}, {name: 'rows', description: 'The number of rows to remove; a negative value removes rows from the end. Zero or an empty argument removes no rows.'}, {name: 'columns', description: 'The number of columns to remove; a negative value removes columns from the end. When omitted, empty, or zero, no columns are removed.'}],
+ documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html',
+ examples: ['=DROP(A1:C5, 2)', '=DROP(A1:C5, -1, -1)', '=DROP(A1:C5, , 1)'],
+ },
FILTER: {
category: 'Lookup and reference',
shortDescription: 'Filters an array, based on multiple conditions (boolean arrays).',
@@ -121,6 +128,13 @@ export const LOOKUP_AND_REFERENCE_DOCS: Record = {
documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html',
examples: ['=SORT(A1:A10)', '=SORT(A1:B10, 2, -1)'],
},
+ TAKE: {
+ category: 'Lookup and reference',
+ shortDescription: 'Returns specified rows or columns from the beginning or end of an array.',
+ parameters: [{name: 'array', description: 'The array or range from which to take rows or columns.'}, {name: 'rows', description: 'The number of rows to take; a negative value takes rows from the end. An empty argument keeps all rows.'}, {name: 'columns', description: 'The number of columns to take; a negative value takes columns from the end. When omitted or empty, all columns are kept.'}],
+ documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html',
+ examples: ['=TAKE(A1:C5, 2)', '=TAKE(A1:C5, -2, -1)', '=TAKE(A1:C5, , 2)'],
+ },
TRANSPOSE: {
category: 'Lookup and reference',
shortDescription: 'Transposes the rows and columns of an array.',
@@ -128,6 +142,13 @@ export const LOOKUP_AND_REFERENCE_DOCS: Record = {
documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html',
examples: ['=TRANSPOSE(A1:C2)', '=TRANSPOSE(A1:A5)'],
},
+ TRIMRANGE: {
+ category: 'Lookup and reference',
+ shortDescription: 'Returns a range without the blank rows and columns at its edges. The result takes up the size of the range, or of its sheet\'s used area for a whole column or row; the cells the result does not fill stay blank.',
+ parameters: [{name: 'range', description: 'The range or array to trim. In a range, a cell is blank only when it is empty, so a formula is kept even when its value is blank; in an array, an element is blank when it has no value. When every row or column is trimmed away, the result is a REF error.'}, {name: 'trim_rows', description: 'Which blank rows to remove: 0 none, 1 leading, 2 trailing, 3 both. Any other value is a VALUE error. When omitted or empty, 3.'}, {name: 'trim_cols', description: 'Which blank columns to remove, with the same codes as trim_rows. When omitted or empty, 3.'}],
+ documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html',
+ examples: ['=TRIMRANGE(A1:C10)', '=TRIMRANGE(A1:C10, 2, 0)', '=TRIMRANGE(A1:C10, , 1)'],
+ },
UNIQUE: {
category: 'Lookup and reference',
shortDescription: 'Returns the unique rows or columns of an array.',
diff --git a/hyperformula/src/interpreter/plugin/ArrayPlugin.ts b/hyperformula/src/interpreter/plugin/ArrayPlugin.ts
index 27b47096e0..c8b2ae11f9 100644
--- a/hyperformula/src/interpreter/plugin/ArrayPlugin.ts
+++ b/hyperformula/src/interpreter/plugin/ArrayPlugin.ts
@@ -3,17 +3,284 @@
* Copyright (c) 2025 Handsoncode. All rights reserved.
*/
+import {AbsoluteCellRange} from '../../AbsoluteCellRange'
import {ArraySize} from '../../ArraySize'
-import {CellError, ErrorType} from '../../Cell'
+import {CellError, ErrorType, SimpleCellAddress} from '../../Cell'
+import {ArrayFormulaVertex, EmptyCellVertex} from '../../DependencyGraph'
import {ErrorMessage} from '../../error-message'
-import {AstNodeType, ProcedureAst} from '../../parser'
+import {Ast, AstNodeType, ProcedureAst} from '../../parser'
import {coerceScalarToBoolean} from '../ArithmeticHelper'
import {InterpreterState} from '../InterpreterState'
-import {InternalScalarValue, InterpreterValue} from '../InterpreterValue'
+import {EmptyValue, getRawValue, InternalScalarValue, InterpreterValue} from '../InterpreterValue'
import {SimpleRangeValue} from '../../SimpleRangeValue'
+import {BooleanPlugin} from './BooleanPlugin'
import {FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions} from './FunctionPlugin'
+/**
+ * Classifies a count or code expression of TAKE, DROP or TRIMRANGE whose value may be known before evaluation.
+ *
+ * @internal
+ */
+type LiteralCount =
+ | {kind: 'value', value: number}
+ | {kind: 'invalid'}
+ | {kind: 'unresolved'}
+
+/**
+ * Distinguishes an omitted or empty TAKE count from an expression whose value
+ * cannot be resolved during static result-size prediction.
+ *
+ * @internal
+ */
+type TakeLiteralDimension = LiteralCount | {kind: 'unbounded'}
+
+/**
+ * The rows and columns of a TRIMRANGE source that hold content, relative to the source.
+ *
+ * @internal
+ */
+interface ContentBounds {
+ top: number,
+ bottom: number,
+ left: number,
+ right: number,
+}
+
+/**
+ * TRIMRANGE trim codes: 0 trims nothing, 1 leading blank lines, 2 trailing blank lines, 3 both.
+ */
+const TRIM_LEADING = 1
+const TRIM_TRAILING = 2
+const TRIM_BOTH = 3
+
export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypecheck {
+ /**
+ * Evaluates the dependency-free subset of TAKE and DROP count expressions,
+ * and of TRIMRANGE code expressions, used for static result-size prediction.
+ *
+ * @param {Ast | undefined} argument - The count expression to inspect before evaluation.
+ * @returns {LiteralCount} The constant value, an invalid-literal marker, or an unresolved marker.
+ * @internal
+ */
+ private parseLiteralCount(argument: Ast | undefined): LiteralCount {
+ if (argument?.type === AstNodeType.NUMBER) {
+ return {kind: 'value', value: argument.value}
+ }
+
+ if (argument?.type === AstNodeType.STRING) {
+ const coercedValue = this.arithmeticHelper.coerceToMaybeNumber(argument.value)
+ if (coercedValue === undefined) {
+ return {kind: 'invalid'}
+ }
+ return {kind: 'value', value: getRawValue(coercedValue)}
+ }
+
+ if (argument?.type === AstNodeType.ERROR || argument?.type === AstNodeType.ERROR_WITH_RAW_INPUT) {
+ return {kind: 'invalid'}
+ }
+
+ if (argument?.type === AstNodeType.PLUS_UNARY_OP || argument?.type === AstNodeType.MINUS_UNARY_OP) {
+ const dimension = this.parseLiteralCount(argument.value)
+ if (dimension.kind !== 'value') {
+ return dimension
+ }
+ return {kind: 'value', value: argument.type === AstNodeType.MINUS_UNARY_OP ? -dimension.value : dimension.value}
+ }
+
+ if (argument?.type === AstNodeType.PARENTHESIS) {
+ return this.parseLiteralCount(argument.expression)
+ }
+
+ if (argument?.type === AstNodeType.PERCENT_OP) {
+ const dimension = this.parseLiteralCount(argument.value)
+ if (dimension.kind !== 'value') {
+ return dimension
+ }
+ return {kind: 'value', value: getRawValue(this.arithmeticHelper.unaryPercent(dimension.value))}
+ }
+
+ if (
+ argument?.type === AstNodeType.FUNCTION_CALL
+ && argument.args.length === 0
+ && (argument.procedureName === 'TRUE' || argument.procedureName === 'FALSE')
+ && this.interpreter.isFunctionImplementedBy(argument.procedureName, BooleanPlugin)
+ ) {
+ return {kind: 'value', value: argument.procedureName === 'TRUE' ? 1 : 0}
+ }
+
+ if (
+ argument?.type === AstNodeType.PLUS_OP
+ || argument?.type === AstNodeType.MINUS_OP
+ || argument?.type === AstNodeType.TIMES_OP
+ || argument?.type === AstNodeType.DIV_OP
+ || argument?.type === AstNodeType.POWER_OP
+ ) {
+ const left = this.parseLiteralCount(argument.left)
+ const right = this.parseLiteralCount(argument.right)
+ if (left.kind === 'invalid' || right.kind === 'invalid') {
+ return {kind: 'invalid'}
+ }
+ if (left.kind === 'value' && right.kind === 'value') {
+ let value: number
+ switch (argument.type) {
+ case AstNodeType.PLUS_OP:
+ value = this.arithmeticHelper.addWithEpsilonRaw(left.value, right.value)
+ break
+ case AstNodeType.MINUS_OP:
+ value = getRawValue(this.arithmeticHelper.subtract(left.value, right.value))
+ break
+ case AstNodeType.TIMES_OP:
+ value = getRawValue(this.arithmeticHelper.multiply(left.value, right.value))
+ break
+ case AstNodeType.DIV_OP: {
+ const quotient = this.arithmeticHelper.divide(left.value, right.value)
+ if (quotient instanceof CellError) {
+ return {kind: 'invalid'}
+ }
+ value = getRawValue(quotient)
+ break
+ }
+ case AstNodeType.POWER_OP:
+ value = this.arithmeticHelper.pow(left.value, right.value)
+ break
+ }
+ return Number.isFinite(value) ? {kind: 'value', value} : {kind: 'invalid'}
+ }
+ }
+
+ return {kind: 'unresolved'}
+ }
+
+ /**
+ * Converts a statically resolved TAKE count into its output dimension.
+ *
+ * @param {Ast | undefined} argument - The count expression to classify.
+ * @returns {TakeLiteralDimension} The non-negative truncated dimension or its invalid, unresolved, or unbounded classification.
+ * @internal
+ */
+ private parseTakeLiteralDimension(argument: Ast | undefined): TakeLiteralDimension {
+ if (argument === undefined || argument.type === AstNodeType.EMPTY) {
+ return {kind: 'unbounded'}
+ }
+
+ const dimension = this.parseLiteralCount(argument)
+ return dimension.kind === 'value'
+ ? {kind: 'value', value: Math.abs(Math.trunc(dimension.value))}
+ : dimension
+ }
+
+ /**
+ * Resolves a direct TAKE, DROP or TRIMRANGE source reference without evaluating its values.
+ * The range supplies materialized dimensions only; spill placement never
+ * depends on its sheet.
+ *
+ * @param {Ast} argument - The source expression to inspect.
+ * @param {InterpreterState} state - The formula state used to resolve relative addresses.
+ * @returns {AbsoluteCellRange | undefined} The source range, or `undefined` for a computed array.
+ * @internal
+ */
+ private directSourceRange(argument: Ast, state: InterpreterState): AbsoluteCellRange | undefined {
+ if (argument.type === AstNodeType.PARENTHESIS) {
+ return this.directSourceRange(argument.expression, state)
+ }
+ if (argument.type === AstNodeType.CELL_RANGE || argument.type === AstNodeType.COLUMN_RANGE || argument.type === AstNodeType.ROW_RANGE) {
+ return AbsoluteCellRange.fromAstOrUndef(argument, state.formulaAddress)
+ }
+ return undefined
+ }
+
+ /**
+ * Classifies a DROP count before evaluation. An omitted or empty count
+ * drops nothing, so it resolves to zero.
+ *
+ * @param {Ast | undefined} argument - The count expression to classify.
+ * @returns {LiteralCount} The constant count, an invalid-literal marker, or an unresolved marker.
+ * @internal
+ */
+ private parseDropLiteralCount(argument: Ast | undefined): LiteralCount {
+ if (argument === undefined || argument.type === AstNodeType.EMPTY) {
+ return {kind: 'value', value: 0}
+ }
+ return this.parseLiteralCount(argument)
+ }
+
+ /**
+ * Predicts how many rows or columns of the source remain after DROP.
+ *
+ * An unresolved count may drop nothing, so the source dimension is the upper
+ * bound. An unbounded source (a whole column or row) is measured by its
+ * effective size. In a spreadsheet such a result reaches the sheet edge, so
+ * it fits only when the formula starts no further from the first row or
+ * column than the number of rows or columns dropped; otherwise the dimension
+ * stays unbounded, which makes array-space validation report a spill error.
+ * An unresolved count drops nothing for this rule too, so its result fits
+ * only in the first row or column.
+ *
+ * @param {number} sourceDimension - The predicted source height or width, possibly unbounded.
+ * @param {LiteralCount} count - The classified count; must not be invalid.
+ * @param {number} anchor - The formula's row or column index along the same axis.
+ * @param {number} effectiveDimension - The effective source height or width along the same axis.
+ * @returns {number} The remaining dimension, zero when everything is dropped, or `Infinity` for a spill.
+ * @internal
+ */
+ private predictDropDimension(sourceDimension: number, count: LiteralCount, anchor: number, effectiveDimension: number): number {
+ const requestedDrop = count.kind === 'value' ? Math.abs(Math.trunc(count.value)) : 0
+
+ if (!Number.isFinite(sourceDimension) && anchor > requestedDrop) {
+ return Number.POSITIVE_INFINITY
+ }
+
+ const boundedDimension = Number.isFinite(sourceDimension) ? sourceDimension : effectiveDimension
+ return boundedDimension - Math.min(requestedDrop, boundedDimension)
+ }
+
+ /**
+ * Measures a TAKE, DROP or TRIMRANGE source whose predicted size is unbounded. A direct
+ * whole-column or whole-row reference is measured by its effective size; a
+ * computed array falls back to the configured sheet limits.
+ *
+ * @param {Ast} argument - The source expression to measure.
+ * @param {InterpreterState} state - The formula state used to resolve relative addresses.
+ * @returns {ArraySize} The effective source size.
+ * @internal
+ */
+ private effectiveSourceSize(argument: Ast, state: InterpreterState): ArraySize {
+ const sourceRange = this.directSourceRange(argument, state)
+ return new ArraySize(
+ sourceRange?.effectiveWidth(this.dependencyGraph) ?? this.config.maxColumns,
+ sourceRange?.effectiveHeight(this.dependencyGraph) ?? this.config.maxRows,
+ )
+ }
+
+ /**
+ * Returns a block of a source array as values. An address-backed source is
+ * read only inside the block, so the cost is proportional to the result
+ * rather than to the source. The block is returned as values rather than as
+ * a range so that it coerces to a scalar like any other array result.
+ *
+ * @param {SimpleRangeValue} source - The evaluated source array.
+ * @param {number} startRow - The first row of the block, relative to the source.
+ * @param {number} startColumn - The first column of the block, relative to the source.
+ * @param {number} height - The number of rows in the block.
+ * @param {number} width - The number of columns in the block.
+ * @returns {SimpleRangeValue} The block's values.
+ * @internal
+ */
+ private sliceSourceArray(source: SimpleRangeValue, startRow: number, startColumn: number, height: number, width: number): SimpleRangeValue {
+ const sourceRange = source.range
+
+ if (sourceRange !== undefined) {
+ const blockRange = AbsoluteCellRange.spanFrom(sourceRange.getAddress(startColumn, startRow), width, height)
+ return SimpleRangeValue.onlyValues(SimpleRangeValue.onlyRange(blockRange, this.dependencyGraph).data)
+ }
+
+ const block = source.data
+ .slice(startRow, startRow + height)
+ .map(row => row.slice(startColumn, startColumn + width))
+
+ return SimpleRangeValue.onlyValues(block)
+ }
+
public static implementedFunctions: ImplementedFunctions = {
'ARRAYFORMULA': {
method: 'arrayformula',
@@ -43,6 +310,41 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche
],
repeatLastArgs: 1,
},
+ 'DROP': {
+ method: 'drop',
+ sizeOfResultArrayMethod: 'dropArraySize',
+ enableArrayArithmeticForArguments: true,
+ parameters: [
+ {argumentType: FunctionArgumentType.RANGE},
+ {argumentType: FunctionArgumentType.NUMBER},
+ {argumentType: FunctionArgumentType.NUMBER, optionalArg: true, defaultValue: 0},
+ ],
+ vectorizationForbidden: true,
+ },
+ 'TAKE': {
+ method: 'take',
+ sizeOfResultArrayMethod: 'takeArraySize',
+ enableArrayArithmeticForArguments: true,
+ parameters: [
+ {argumentType: FunctionArgumentType.RANGE},
+ {argumentType: FunctionArgumentType.NUMBER, defaultValue: Number.POSITIVE_INFINITY, emptyAsDefault: true},
+ {argumentType: FunctionArgumentType.NUMBER, optionalArg: true, defaultValue: Number.POSITIVE_INFINITY, emptyAsDefault: true},
+ ],
+ vectorizationForbidden: true,
+ },
+ 'TRIMRANGE': {
+ method: 'trimrange',
+ sizeOfResultArrayMethod: 'trimrangeArraySize',
+ enableArrayArithmeticForArguments: true,
+ parameters: [
+ {argumentType: FunctionArgumentType.RANGE},
+ // Coerced in the method: an empty text is #VALUE!, and both codes are coerced before either is range-checked.
+ // SCALAR, so that an error in either code reaches the method and the codes are checked in order.
+ {argumentType: FunctionArgumentType.SCALAR, optionalArg: true, defaultValue: TRIM_BOTH, emptyAsDefault: true},
+ {argumentType: FunctionArgumentType.SCALAR, optionalArg: true, defaultValue: TRIM_BOTH, emptyAsDefault: true},
+ ],
+ vectorizationForbidden: true,
+ },
'VSTACK': {
method: 'vstack',
sizeOfResultArrayMethod: 'vstackArraySize',
@@ -166,6 +468,503 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche
return new ArraySize(width, height)
}
+ /**
+ * Corresponds to TAKE(array, rows, [columns]).
+ *
+ * Returns rows and columns from the beginning or end of the source array.
+ * Syntactically empty dimensions keep all rows or columns. Counts that
+ * evaluate to zero return a #N/A error.
+ *
+ * @param {ProcedureAst} ast - The parsed TAKE call.
+ * @param {InterpreterState} state - The current formula evaluation state.
+ * @returns {InterpreterValue} The selected source values or a spreadsheet error.
+ */
+ public take(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
+ // The default supports TAKE(array, , columns), but the rows argument
+ // position must still be present.
+ if (ast.args.length < 2) {
+ return new CellError(ErrorType.NA, ErrorMessage.WrongArgNumber)
+ }
+
+ return this.runFunction(ast.args, state, this.metadata('TAKE'),
+ (range: SimpleRangeValue, rows: number, columns: number) => {
+ const sourceHeight = range.height()
+ const sourceWidth = range.width()
+ const requestedRows = Math.trunc(rows)
+ const requestedColumns = Math.trunc(columns)
+
+ if (requestedRows === 0 || requestedColumns === 0) {
+ return new CellError(ErrorType.NA, ErrorMessage.ZeroRowOrColumnCount)
+ }
+
+ if (sourceHeight === 0 || sourceWidth === 0) {
+ return new CellError(ErrorType.NA, ErrorMessage.EmptyRange)
+ }
+
+ const rowsToTake = Math.min(Math.abs(requestedRows), sourceHeight)
+ const columnsToTake = Math.min(Math.abs(requestedColumns), sourceWidth)
+ const startRow = requestedRows > 0 ? 0 : sourceHeight - rowsToTake
+ const startColumn = requestedColumns > 0 ? 0 : sourceWidth - columnsToTake
+
+ return this.sliceSourceArray(range, startRow, startColumn, rowsToTake, columnsToTake)
+ }
+ )
+ }
+
+ /**
+ * Corresponds to DROP(array, rows, [columns]).
+ *
+ * Removes rows and columns from the beginning (positive counts) or the end
+ * (negative counts) of the source array and returns the rest. Counts are
+ * truncated toward zero; a zero, omitted, or empty count drops nothing.
+ * Dropping every row or every column returns a #N/A error.
+ *
+ * @param {ProcedureAst} ast - The parsed DROP call.
+ * @param {InterpreterState} state - The current formula evaluation state.
+ * @returns {InterpreterValue} The remaining source values or a spreadsheet error.
+ */
+ public drop(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
+ return this.runFunction(ast.args, state, this.metadata('DROP'),
+ (range: SimpleRangeValue, rows: number, columns: number) => {
+ const rowsToDrop = Math.min(Math.abs(Math.trunc(rows)), range.height())
+ const columnsToDrop = Math.min(Math.abs(Math.trunc(columns)), range.width())
+ const remainingRows = range.height() - rowsToDrop
+ const remainingColumns = range.width() - columnsToDrop
+
+ if (remainingRows === 0 || remainingColumns === 0) {
+ return new CellError(ErrorType.NA, ErrorMessage.EmptyArray)
+ }
+
+ const startRow = rows > 0 ? rowsToDrop : 0
+ const startColumn = columns > 0 ? columnsToDrop : 0
+
+ return this.sliceSourceArray(range, startRow, startColumn, remainingRows, remainingColumns)
+ }
+ )
+ }
+
+ /**
+ * Calculates the spilled array size of DROP. Statically known counts give
+ * the exact size; counts that depend on other cells use the source size as
+ * the upper bound. Dropping every row or column from a known count is
+ * reported as an invalid size, so the formula evaluates to its #N/A error
+ * instead of reserving space.
+ *
+ * @param {ProcedureAst} ast - The parsed DROP call.
+ * @param {InterpreterState} state - The formula state whose address anchors the spill.
+ * @returns {ArraySize} The predicted result dimensions or an invalid size.
+ */
+ public dropArraySize(ast: ProcedureAst, state: InterpreterState): ArraySize {
+ if (ast.args.length < 2 || ast.args.length > 3) {
+ return ArraySize.error()
+ }
+
+ const metadata = this.metadata('DROP')
+ const sourceSize = this.arraySizeForAst(
+ ast.args[0],
+ new InterpreterState(state.formulaAddress, state.arraysFlag || (metadata?.enableArrayArithmeticForArguments ?? false)),
+ )
+ const rowCount = this.parseDropLiteralCount(ast.args[1])
+ const columnCount = this.parseDropLiteralCount(ast.args[2])
+
+ if (rowCount.kind === 'invalid' || columnCount.kind === 'invalid') {
+ return ArraySize.error()
+ }
+
+ const effectiveSourceSize = this.effectiveSourceSize(ast.args[0], state)
+ const height = this.predictDropDimension(sourceSize.height, rowCount, state.formulaAddress.row, effectiveSourceSize.height)
+ const width = this.predictDropDimension(sourceSize.width, columnCount, state.formulaAddress.col, effectiveSourceSize.width)
+
+ if (height < 1 || width < 1) {
+ return ArraySize.error()
+ }
+
+ return new ArraySize(width, height)
+ }
+
+ /**
+ * Calculates the spilled array size of TAKE using the source dimensions as
+ * the upper bound. Finite counts are capped at the configured sheet limits.
+ * Direct references that remain unbounded use their effective dimensions,
+ * while computed sources use the configured limit. Unbounded dimensions are
+ * valid only at the corresponding output-sheet edge.
+ *
+ * An unbounded result anchored away from the required sheet edge retains its
+ * unbounded predicted dimension so array-space validation returns a spill
+ * error without evaluating TAKE as a scalar formula.
+ *
+ * @param {ProcedureAst} ast - The parsed TAKE call.
+ * @param {InterpreterState} state - The formula state whose address anchors the spill.
+ * @returns {ArraySize} The predicted result dimensions or an invalid size.
+ */
+ public takeArraySize(ast: ProcedureAst, state: InterpreterState): ArraySize {
+ if (ast.args.length < 2 || ast.args.length > 3) {
+ return ArraySize.error()
+ }
+
+ const metadata = this.metadata('TAKE')
+ const sourceSize = this.arraySizeForAst(
+ ast.args[0],
+ new InterpreterState(state.formulaAddress, state.arraysFlag || (metadata?.enableArrayArithmeticForArguments ?? false)),
+ )
+
+ const rowDimension = this.parseTakeLiteralDimension(ast.args[1])
+ const columnDimension = this.parseTakeLiteralDimension(ast.args[2])
+
+ const hasZeroDimension = (rowDimension.kind === 'value' && rowDimension.value === 0)
+ || (columnDimension.kind === 'value' && columnDimension.value === 0)
+
+ if (rowDimension.kind === 'invalid' || columnDimension.kind === 'invalid' || hasZeroDimension) {
+ return ArraySize.error()
+ }
+
+ const height = rowDimension.kind === 'value'
+ ? Math.min(sourceSize.height, rowDimension.value, this.config.maxRows)
+ : sourceSize.height
+ const width = columnDimension.kind === 'value'
+ ? Math.min(sourceSize.width, columnDimension.value, this.config.maxColumns)
+ : sourceSize.width
+
+ return this.sizeAtSheetEdge(
+ ast.args[0], state, width, height, columnDimension.kind === 'unbounded', rowDimension.kind === 'unbounded',
+ )
+ }
+
+ /**
+ * Finishes the predicted size of a result that may span a whole column or
+ * row. A spreadsheet result of that kind reaches the sheet edge, so it fits
+ * only when it starts in the first row (column); elsewhere its unbounded
+ * dimension is kept, so that array-space validation reports a spill error.
+ * In the first row (column), or for a dimension the rule does not apply to,
+ * an unbounded dimension is replaced by the source's effective size.
+ *
+ * @param {Ast} source - The source expression, measured when a dimension is unbounded.
+ * @param {InterpreterState} state - The formula state whose address anchors the spill.
+ * @param {number} width - The predicted width, possibly unbounded.
+ * @param {number} height - The predicted height, possibly unbounded.
+ * @param {boolean} widthReachesSheetEdge - Whether an unbounded width must start in the first column.
+ * @param {boolean} heightReachesSheetEdge - Whether an unbounded height must start in the first row.
+ * @returns {ArraySize} The predicted size, an unbounded one that will not fit, or an invalid size when empty.
+ * @internal
+ */
+ private sizeAtSheetEdge(
+ source: Ast,
+ state: InterpreterState,
+ width: number,
+ height: number,
+ widthReachesSheetEdge: boolean,
+ heightReachesSheetEdge: boolean,
+ ): ArraySize {
+ const startsBelowFirstRow = heightReachesSheetEdge && !Number.isFinite(height) && state.formulaAddress.row !== 0
+ const startsRightOfFirstColumn = widthReachesSheetEdge && !Number.isFinite(width) && state.formulaAddress.col !== 0
+
+ if (startsBelowFirstRow || startsRightOfFirstColumn) {
+ return new ArraySize(width, height)
+ }
+
+ const effectiveSourceSize = this.effectiveSourceSize(source, state)
+ const effectiveHeight = Number.isFinite(height) ? height : effectiveSourceSize.height
+ const effectiveWidth = Number.isFinite(width) ? width : effectiveSourceSize.width
+
+ if (effectiveHeight < 1 || effectiveWidth < 1) {
+ return ArraySize.error()
+ }
+
+ // A fresh size: a reference's size would make the formula a single cell.
+ return new ArraySize(effectiveWidth, effectiveHeight)
+ }
+
+ /**
+ * Corresponds to TRIMRANGE(range, [trim_rows], [trim_cols]).
+ *
+ * Removes the blank rows and columns at the edges of the range and returns
+ * the rest as values. Each code is 0 (trim nothing), 1 (leading blank lines),
+ * 2 (trailing blank lines) or 3 (both, the default); any other value is a
+ * #VALUE! error. A cell of a range is blank when it is empty: a formula is
+ * content even when its value is blank. A source with nothing left to keep
+ * returns a #REF! error.
+ *
+ * @param {ProcedureAst} ast - The parsed TRIMRANGE call.
+ * @param {InterpreterState} state - The current formula evaluation state.
+ * @returns {InterpreterValue} The trimmed source values or a spreadsheet error.
+ */
+ public trimrange(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
+ // An empty range argument would otherwise evaluate to a blank cell and be trimmed away.
+ if (ast.args[0]?.type === AstNodeType.EMPTY) {
+ return new CellError(ErrorType.VALUE, ErrorMessage.EmptyArg)
+ }
+
+ return this.runFunction(ast.args, state, this.metadata('TRIMRANGE'),
+ (range: SimpleRangeValue, trimRows: InternalScalarValue, trimColumns: InternalScalarValue) => {
+ // A spreadsheet lifts an array of codes into an array of results; a function returning an array cannot be
+ // lifted here, so a code larger than one cell is an error.
+ if (this.isMultiCellArgument(ast.args[1], state) || this.isMultiCellArgument(ast.args[2], state)) {
+ return new CellError(ErrorType.VALUE, ErrorMessage.ScalarExpected)
+ }
+
+ const rowMode = this.coerceTrimCode(trimRows)
+ if (rowMode instanceof CellError) {
+ return rowMode
+ }
+ const columnMode = this.coerceTrimCode(trimColumns)
+ if (columnMode instanceof CellError) {
+ return columnMode
+ }
+ if (!this.isTrimMode(rowMode) || !this.isTrimMode(columnMode)) {
+ return new CellError(ErrorType.VALUE, ErrorMessage.BadMode)
+ }
+
+ const height = range.height()
+ const width = range.width()
+ const bounds = this.contentBounds(range, range.range ?? this.directSourceCell(ast.args[0], state))
+
+ if (bounds === undefined) {
+ return rowMode === 0 && columnMode === 0
+ ? this.sliceSourceArray(range, 0, 0, height, width)
+ : new CellError(ErrorType.REF, ErrorMessage.EmptyRange)
+ }
+
+ const top = (rowMode & TRIM_LEADING) !== 0 ? bounds.top : 0
+ const bottom = (rowMode & TRIM_TRAILING) !== 0 ? bounds.bottom : height - 1
+ const left = (columnMode & TRIM_LEADING) !== 0 ? bounds.left : 0
+ const right = (columnMode & TRIM_TRAILING) !== 0 ? bounds.right : width - 1
+
+ return this.sliceSourceArray(range, top, left, bottom - top + 1, right - left + 1)
+ }
+ )
+ }
+
+ /**
+ * Calculates the spilled array size of TRIMRANGE. Which rows and columns are
+ * blank is known only after evaluation, so the untrimmed source is reserved:
+ * the result fills its top-left corner and the rest stays blank. A
+ * whole-column or whole-row source follows the same sheet-edge rule as TAKE
+ * (see `sizeAtSheetEdge`). A trim code that is certainly
+ * invalid is reported as an invalid size, so the formula is a single error
+ * cell instead of reserving space.
+ *
+ * @param {ProcedureAst} ast - The parsed TRIMRANGE call.
+ * @param {InterpreterState} state - The formula state whose address anchors the spill.
+ * @returns {ArraySize} The predicted result dimensions or an invalid size.
+ */
+ public trimrangeArraySize(ast: ProcedureAst, state: InterpreterState): ArraySize {
+ if (ast.args.length < 1 || ast.args.length > 3) {
+ return ArraySize.error()
+ }
+
+ if (
+ !this.isPossibleTrimCode(ast.args[1])
+ || !this.isPossibleTrimCode(ast.args[2])
+ || this.isMultiCellArgument(ast.args[1], state)
+ || this.isMultiCellArgument(ast.args[2], state)
+ ) {
+ return ArraySize.error()
+ }
+
+ const metadata = this.metadata('TRIMRANGE')
+ const sourceSize = this.arraySizeForAst(
+ ast.args[0],
+ new InterpreterState(state.formulaAddress, state.arraysFlag || (metadata?.enableArrayArithmeticForArguments ?? false)),
+ )
+
+ if (sourceSize.isScalar() && !sourceSize.isRef) {
+ return ArraySize.scalar()
+ }
+
+ return this.sizeAtSheetEdge(ast.args[0], state, sourceSize.width, sourceSize.height, true, true)
+ }
+
+ /**
+ * Coerces a TRIMRANGE trim code to a number. An error is returned as it is,
+ * and an empty text is a #VALUE! error rather than 0; numbers, numeric text,
+ * booleans and blank cells coerce as numbers.
+ *
+ * @param {InternalScalarValue} value - The code as passed to the function.
+ * @returns {number | CellError} The code as a number, not yet range-checked, or an error.
+ */
+ private coerceTrimCode(value: InternalScalarValue): number | CellError {
+ if (value instanceof CellError) {
+ return value
+ }
+ if (value === '') {
+ return new CellError(ErrorType.VALUE, ErrorMessage.NumberCoercion)
+ }
+ const coercedValue = this.arithmeticHelper.coerceScalarToNumberOrError(value)
+ return coercedValue instanceof CellError ? coercedValue : getRawValue(coercedValue)
+ }
+
+ /**
+ * Checks that a number is a TRIMRANGE trim code: an integer from 0 to 3.
+ * Fractions are rejected, not truncated.
+ *
+ * @param {number} value - The coerced code.
+ * @returns {boolean} Whether the code is valid.
+ */
+ private isTrimMode(value: number): boolean {
+ return Number.isInteger(value) && value >= 0 && value <= TRIM_BOTH
+ }
+
+ /**
+ * Checks a TRIMRANGE trim code before evaluation. Omitted, empty and
+ * unresolved codes may be valid; a literal is valid only when it is a trim
+ * code, and an empty text never is.
+ *
+ * @param {Ast | undefined} argument - The code expression to inspect.
+ * @returns {boolean} False when the code is certainly invalid.
+ */
+ private isPossibleTrimCode(argument: Ast | undefined): boolean {
+ if (argument === undefined || argument.type === AstNodeType.EMPTY) {
+ return true
+ }
+ if (argument.type === AstNodeType.STRING && argument.value === '') {
+ return false
+ }
+
+ const code = this.parseLiteralCount(argument)
+ if (code.kind === 'invalid') {
+ return false
+ }
+ return code.kind === 'unresolved' || this.isTrimMode(code.value)
+ }
+
+ /**
+ * Checks whether an argument is predicted to be larger than one cell: an
+ * array constant, a multi-cell range or an array-returning function.
+ *
+ * @param {Ast | undefined} argument - The argument expression to inspect.
+ * @param {InterpreterState} state - The formula state used to resolve relative addresses.
+ * @returns {boolean} True when the argument is predicted to have more than one cell.
+ */
+ private isMultiCellArgument(argument: Ast | undefined, state: InterpreterState): boolean {
+ if (argument === undefined || argument.type === AstNodeType.EMPTY) {
+ return false
+ }
+ const size = this.arraySizeForAst(argument, new InterpreterState(state.formulaAddress, true))
+ return size.width > 1 || size.height > 1
+ }
+
+ /**
+ * Resolves a TRIMRANGE source written as a single cell reference to a
+ * one-cell range, so that the cell is judged by its content like any cell of
+ * a range. A single cell reference evaluates to its value, which carries no
+ * range.
+ *
+ * @param {Ast | undefined} argument - The source expression to inspect.
+ * @param {InterpreterState} state - The formula state used to resolve relative addresses.
+ * @returns {AbsoluteCellRange | undefined} The one-cell range, or `undefined` for any other source.
+ */
+ private directSourceCell(argument: Ast | undefined, state: InterpreterState): AbsoluteCellRange | undefined {
+ if (argument?.type === AstNodeType.PARENTHESIS) {
+ return this.directSourceCell(argument.expression, state)
+ }
+ if (argument?.type !== AstNodeType.CELL_REFERENCE) {
+ return undefined
+ }
+ return AbsoluteCellRange.spanFrom(argument.reference.toSimpleCellAddress(state.formulaAddress), 1, 1)
+ }
+
+ /**
+ * Finds the rows and columns of a TRIMRANGE source that hold content. A
+ * range is read cell by cell, only inside the part of it that lies within
+ * its sheet's current height and width, since nothing beyond holds content.
+ * A computed array is read element by element, a blank element being an
+ * empty value. Both are scanned inward from the edges.
+ *
+ * @param {SimpleRangeValue} source - The evaluated source.
+ * @param {AbsoluteCellRange | undefined} sourceRange - The cells the source was read from, or `undefined` for a computed array.
+ * @returns {ContentBounds | undefined} The bounds relative to the source, or `undefined` when every cell is blank.
+ */
+ private contentBounds(source: SimpleRangeValue, sourceRange: AbsoluteCellRange | undefined): ContentBounds | undefined {
+ if (sourceRange === undefined) {
+ const data = source.data
+ return this.scanContentBounds(data.length, source.width(), (row, column) => data[row][column] !== EmptyValue)
+ }
+
+ const address: SimpleCellAddress = {sheet: sourceRange.sheet, row: 0, col: 0}
+ const height = Math.min(sourceRange.end.row, this.dependencyGraph.getSheetHeight(sourceRange.sheet) - 1) - sourceRange.start.row + 1
+ const width = Math.min(sourceRange.end.col, this.dependencyGraph.getSheetWidth(sourceRange.sheet) - 1) - sourceRange.start.col + 1
+ return this.scanContentBounds(height, width, (row, column) => {
+ address.row = sourceRange.start.row + row
+ address.col = sourceRange.start.col + column
+ return this.isContentCell(address)
+ })
+ }
+
+ /**
+ * Scans a block inward from each edge for the first row and column that
+ * hold content: rows from the top and the bottom, then columns from the left
+ * and the right within those rows. The cost is the blank border plus one
+ * line per edge, not the whole block, unless the block is blank.
+ *
+ * @param {number} height - The number of rows to scan; zero or less means none.
+ * @param {number} width - The number of columns to scan; zero or less means none.
+ * @param {(row: number, column: number) => boolean} hasContent - Whether the cell at a block position holds content.
+ * @returns {ContentBounds | undefined} The bounds within the block, or `undefined` when every cell is blank.
+ */
+ private scanContentBounds(height: number, width: number, hasContent: (row: number, column: number) => boolean): ContentBounds | undefined {
+ const rowHasContent = (row: number) => {
+ for (let column = 0; column < width; column++) {
+ if (hasContent(row, column)) {
+ return true
+ }
+ }
+ return false
+ }
+ const columnHasContent = (column: number, top: number, bottom: number) => {
+ for (let row = top; row <= bottom; row++) {
+ if (hasContent(row, column)) {
+ return true
+ }
+ }
+ return false
+ }
+
+ let top = 0
+ while (top < height && !rowHasContent(top)) {
+ top++
+ }
+ if (top >= height) {
+ return undefined
+ }
+ let bottom = height - 1
+ while (bottom > top && !rowHasContent(bottom)) {
+ bottom--
+ }
+ let left = 0
+ while (!columnHasContent(left, top, bottom)) {
+ left++
+ }
+ let right = width - 1
+ while (right > left && !columnHasContent(right, top, bottom)) {
+ right--
+ }
+
+ return {top, bottom, left, right}
+ }
+
+ /**
+ * Checks whether a cell holds content for TRIMRANGE. An empty cell is blank;
+ * any formula is content, even one whose value is blank, including the cell
+ * that holds an array formula; any other cell of a spilled array is content
+ * when its value is not blank, so the blank padding of another array result
+ * counts as blank.
+ *
+ * @param {SimpleCellAddress} address - The cell to inspect.
+ * @returns {boolean} True when the cell holds content.
+ */
+ private isContentCell(address: SimpleCellAddress): boolean {
+ const vertex = this.dependencyGraph.getCell(address)
+ if (vertex === undefined || vertex instanceof EmptyCellVertex) {
+ return false
+ }
+ if (vertex instanceof ArrayFormulaVertex) {
+ const formulaAddress = vertex.getAddress(this.dependencyGraph.lazilyTransformingAstService)
+ const holdsFormula = formulaAddress.sheet === address.sheet && formulaAddress.row === address.row && formulaAddress.col === address.col
+ return holdsFormula || this.dependencyGraph.getCellValue(address) !== EmptyValue
+ }
+ return true
+ }
+
/**
* Corresponds to VSTACK(array1, [array2], ...)
*
diff --git a/hyperformula/src/license/functionCapabilities.ts b/hyperformula/src/license/functionCapabilities.ts
index a26733ec5f..5cc76f6834 100644
--- a/hyperformula/src/license/functionCapabilities.ts
+++ b/hyperformula/src/license/functionCapabilities.ts
@@ -82,7 +82,7 @@ const UNGROUPED_FUNCTIONS = [
'CONFIDENCE.NORM', 'CONFIDENCE.T', 'CORREL', 'COSH', 'COT', 'COTH', 'COUNTUNIQUE', 'COVARIANCE.P',
'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',
+ 'DOLLARFR', 'DPRODUCT', 'DROP', 'DSTDEV', 'DSTDEVP', 'DSUM', 'DVAR', 'DVARP', 'EFFECT', 'ERF', 'ERFC',
'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 +95,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',
+ 'TAKE', 'TANH', 'TBILLEQ', 'TBILLPRICE', 'TBILLYIELD', 'TDIST', 'TIMEVALUE', 'TRIMRANGE', 'UNICODE', 'VARA', 'VARPA',
'WEIBULL.DIST', 'WORKDAY.INTL', 'Z.TEST',
]