Which LibreOffice version do you need for VSTACK, HSTACK, TEXTSPLIT, TAKE and DROP?
Excel’s array-shaping functions — VSTACK, HSTACK,
TEXTSPLIT, TAKE, DROP and their relatives — are the
functions most likely to decide whether a modern workbook survives a move to LibreOffice Calc. They
do not degrade gracefully. Either the release you are running has them, in which case our executed
runs show them computing the documented answer, or it does not, in which case every one of them
returns #NAME?: the same error you would get from a typo.
We ran the same corpus through four LibreOffice builds — 24.2.0.3, 24.8.7.2, 25.2.0.3 and
25.8.7.3 — and the results sort into three clean groups. Fourteen array-shaping and text-split
functions arrive in 25.8.7.3 and are #NAME? in everything older. Nine dynamic-array
functions arrived a wave earlier, in 24.8.7.2. And a further nine are still #NAME? in the
newest build we tested.
The surprise
25.2 is not enough. It is tempting to assume that a 2025 release of LibreOffice has the 2022-era
Excel array functions, but our 25.2.0.3 run returns #NAME? for VSTACK,
HSTACK, TEXTSPLIT, TAKE, DROP,
TOCOL, TOROW, WRAPROWS, WRAPCOLS,
CHOOSECOLS, CHOOSEROWS, TEXTBEFORE,
TEXTAFTER and EXPAND — all fourteen. They only start evaluating in our 25.8.7.3 run. The
gap between “current-ish” and “new enough” is one release, and it is the
difference between a working workbook and a sheet full of errors.
What this means for a migration
The practical rule from our runs is short. A workbook that uses any of the fourteen
functions in the first table below needs LibreOffice 25.8 or newer. On 24.2, 24.8 or 25.2
those formulas do not compute a different answer — they do not compute at all, and the
#NAME? flows downstream into every total, chart and lookup that reads the result.
A workbook that stops at the second table’s nine functions (XLOOKUP,
FILTER, SORT, UNIQUE, SEQUENCE,
LET, SORTBY, XMATCH, RANDARRAY) needs 24.8 or newer. A workbook that
uses anything in the third table has no LibreOffice release in our tested range that will run it,
and has to be rewritten.
If you are checking a single formula, paste it into the free compatibility checker linked at the foot of this page; if you are checking a whole file, the Migration Audit button below it takes an .xlsx and flags every formula whose function is missing or behaves differently in the target app. For the full per-function support table across all four builds, see LibreOffice Calc function support by version.
Wave one: fourteen functions that land in 25.8.7.3
Inputs, per case: CHOOSECOLS reads A1:C1 = 1, 2, 3;
CHOOSEROWS reads A1:A5 = 10, 20, 30, 40, 50; TAKE and
DROP read A1:C3 = 1…9 in reading order; HSTACK reads
A1:A2 = 1, 2 and B1:B2 = 3, 4; TOCOL, TOROW and
VSTACK read A1:B2 = 1, 2 / 3, 4; WRAPCOLS and
WRAPROWS read A1:D1 = 1, 2, 3, 4; EXPAND reads
A1:A2 = 5, 7 for its row-padding case and A1 = 5 for its column-padding
case; the text cases take string literals.
| Function | Formula | Excel, desktop (documented) | Google Sheets (executed 2026-08-29) | LibreOffice 24.2.0.3, 24.8.7.2 & 25.2.0.3 (executed) | LibreOffice 25.8.7.3 (executed) |
|---|---|---|---|---|---|
| CHOOSECOLS | =CHOOSECOLS(A1:C1,3,1) | 3, 1 | 3 — read-back 3, 1 | #NAME? | 3 — read-back 3, 1 (matches) |
| CHOOSEROWS | =CHOOSEROWS(A1:A5,4,2) | 40, 20 | 40 — read-back 40, 20 | #NAME? | 40 — read-back 40, 20 (matches) |
| DROP | =DROP(A1:C3,1,1) | 5, 6 / 8, 9 | #NAME? | #NAME? | 5 — read-back 5, 6, 8, 9 (matches) |
| EXPAND | =EXPAND(A1:A2,3,1,0) | 5, 7, 0 | #NAME? | #NAME? | 5 — read-back 5, 7, 0 (matches) |
| HSTACK | =HSTACK(A1:A2,B1:B2) | 1, 3 / 2, 4 | 1 — read-back 1, 3, 2, 4 | #NAME? | 1 — read-back 1, 3, 2, 4 (matches) |
| TAKE | =TAKE(A1:C3,2,2) | 1, 2 / 4, 5 | #NAME? | #NAME? | 1 — read-back 1, 2, 4, 5 (matches) |
| TEXTAFTER | =TEXTAFTER("a-b-c","-") | b-c | #NAME? | #NAME? | b-c |
| TEXTBEFORE | =TEXTBEFORE("a-b-c","-") | a | #NAME? | #NAME? | a |
| TEXTSPLIT | =TEXTSPLIT("a,b,c",",") | a, b, c | #NAME? | #NAME? | a — read-back a, b, c (matches) |
| TOCOL | =TOCOL(A1:B2) | 1, 2, 3, 4 | 1 — read-back 1, 2, 3, 4 | #NAME? | 1 — read-back 1, 2, 3, 4 (matches) |
| TOROW | =TOROW(A1:B2) | 1, 2, 3, 4 | 1 — read-back 1, 2, 3, 4 | #NAME? | 1 — read-back 1, 2, 3, 4 (matches) |
| VSTACK | =VSTACK(A1:B1,A2:B2) | 1, 2 / 3, 4 | 1 — read-back 1, 2, 3, 4 | #NAME? | 1 — read-back 1, 2, 3, 4 (matches) |
| WRAPCOLS | =WRAPCOLS(A1:D1,2) | 1, 3 / 2, 4 | 1 — read-back 1, 3, 2, 4 | #NAME? | 1 — read-back 1, 3, 2, 4 (matches) |
| WRAPROWS | =WRAPROWS(A1:D1,2) | 1, 2 / 3, 4 | 1 — read-back 1, 2, 3, 4 | #NAME? | 1 — read-back 1, 2, 3, 4 (matches) |
The #NAME? column is one column because all three older builds agreed: every one of
these cases returned #NAME? in 24.2.0.3, in 24.8.7.2 and in 25.2.0.3. Of the
fourteen, our function dataset records CHOOSECOLS, CHOOSEROWS,
HSTACK, TOCOL, TOROW, VSTACK,
WRAPCOLS and WRAPROWS as documented in Google Sheets’ function list;
it does not record DROP, TAKE, TEXTSPLIT,
TEXTBEFORE, TEXTAFTER or EXPAND there. We have now run all fourteen in Sheets, and the executed result matches that inventory
exactly: the eight it documents returned their values, and DROP, TAKE, TEXTSPLIT, TEXTBEFORE,
TEXTAFTER and EXPAND returned #NAME?.
Two caveats on the 25.8.7.3 column, both covered in detail elsewhere so we will not repeat the
tables here. TEXTAFTER and TEXTBEFORE evaluate in 25.8.7.3 but do not
honour the if_not_found fallback argument in our runs, and
FILTER’s empty-result case reports a different error than Excel documents —
both are measured and explained in
FILTER no match: Excel #CALC! vs LibreOffice #N/A.
For hand-rolled replacements of the two text functions, see
TEXTBEFORE/TEXTAFTER vs LEFT, MID and RIGHT.
Wave two: nine functions that landed in 24.8.7.2
These are the previous generation of dynamic-array functions. They are #NAME? in
24.2.0.3 only; from 24.8.7.2 onward our runs show them computing.
| Function | Formula | Excel, desktop (documented) | Google Sheets (executed 2026-08-29) | LibreOffice 24.2.0.3 (executed) | LibreOffice 24.8.7.2, 25.2.0.3 & 25.8.7.3 (executed) |
|---|---|---|---|---|---|
| FILTER | =FILTER(A1:A5,B1:B5>2) | c, d, e | c — read-back c, d, e (matches) | #NAME? | c — read-back c, d, e (matches) |
| LET | =LET(x,5,x*2) | 10 | 10 | #NAME? | 10 |
| RANDARRAY | =ROWS(RANDARRAY(2,3))*10+COLUMNS(RANDARRAY(2,3)) | 23 (shape check only — individual values are non-deterministic) | 23 | #NAME? | 23 |
| SEQUENCE | =SEQUENCE(5) | 1, 2, 3, 4, 5 | 1 — read-back 1, 2, 3, 4, 5 | #NAME? | 1 — read-back 1, 2, 3, 4, 5 (matches) |
| SORT | =SORT(A1:A5) | 1, 1, 3, 4, 5 | 1 — read-back 1, 1, 3, 4, 5 (matches) | #NAME? | 1 — read-back 1, 1, 3, 4, 5 (matches) |
| SORTBY | =SORTBY(A1:A3,B1:B3) | y, z, x | #NAME? | #NAME? | y — read-back y, z, x (matches) |
| UNIQUE | =UNIQUE(A1:A5) | 1, 2, 3 | 1 — read-back 1, 2, 3 | #NAME? | 1 — read-back 1, 2, 3 (matches) |
| XLOOKUP | =XLOOKUP("b",A1:A3,B1:B3) | 2 | 2 | #NAME? | 2 |
| XMATCH | =XMATCH("b",A1:A3) | 2 | 2 | #NAME? | 2 |
Inputs: FILTER reads A1:A5 = a…e with B1:B5 = 1…5;
SORT and UNIQUE read A1:A5 = 3, 1, 4, 1, 5 and
1, 2, 2, 3, 1 respectively; SORTBY reads A1:A3 = x, y, z with
B1:B3 = 3, 1, 2; XLOOKUP and XMATCH read
A1:A3 = a, b, c with B1:B3 = 1, 2, 3. Our dataset records
FILTER, LET, RANDARRAY, SEQUENCE, SORT,
UNIQUE, XLOOKUP and XMATCH as documented in Google Sheets, and does
not record SORTBY there. All nine have now been executed in Sheets, including a
follow-up run on 2026-08-29 that writes the corpus with plain (unprefixed) function names rather
than Excel’s _xlfn._xlws. storage form. LET, RANDARRAY, SEQUENCE, UNIQUE, XLOOKUP,
FILTER and SORT all returned their documented values — the plain-name
run resolved FILTER and SORT, which an earlier prefixed run had left inconclusive because Sheets’
importer did not map that storage form. Only SORTBY returned #NAME?, and that is a real
result: Google does not document SORTBY. The ninth, XMATCH, executed in Sheets and returned
2 — and it exposed an inventory gap on our side: this dataset had recorded XMATCH as
not documented in Sheets, because it is absent from the “List of Google Sheets
functions” table page we originally scraped. Google does in fact publish a dedicated XMATCH help
article, so on 2026-08-29 we fetched and verified it and corrected the inventory: XMATCH is now recorded
as documented in Sheets, which is what the executed value said all along.
Sheets semantic split: SORT’s third argument
Excel’s SORT(array, [sort_index], [sort_order], [by_col]) takes a signed
sort_order (1 ascending, -1 descending) as its third argument.
Google Sheets’ SORT(range, sort_column, is_ascending, …) uses that same position
for a boolean is_ascending flag instead. Our corpus’s =SORT(A1:A5,1,-1) case
(id SORT_descending) is written the Excel way, intending a descending sort; in the
plain-name Sheets run it returned 1 with a read-back of 1, 1, 3, 4, 5 —
the ascending result, because Sheets reads the nonzero -1 as truthy for
is_ascending rather than as “descending.” A formula copied verbatim from Excel
to sort descending will silently sort ascending in Sheets instead. Separately, a degenerate
=SORT(A1:A1) case over a single blank cell (id SORT_empty_range_error) returned a
blank cell in the Sheets run, not the 0 that a blank sorts to in numeric context and that
LibreOffice also returns — sorting a lone blank cell returns blank, not zero, in Sheets.
One wrinkle inside this wave is worth stating because it cuts against the tidy story. A separate
SORT case, =SORT(A1:A1) over a single blank cell, returned 0
in 24.8.7.2, #VALUE! in 25.2.0.3, and 0 again in 25.8.7.3. So “landed
in 24.8” describes the ordinary path; one degenerate input regressed for a release and came
back. It is a reminder that “supported” is per-case, not per-function.
RANDARRAY is volatile, so unlike the other rows in this table our case does not
check a literal returned value. The row above is a shape probe: =ROWS(RANDARRAY(2,3))*10+
COLUMNS(RANDARRAY(2,3)) encodes “2 rows, 3 columns” as 23, which is
deterministic even though the individual cells are not. A second case in our corpus,
=AND(MIN(RANDARRAY(10,10))>=0,MAX(RANDARRAY(10,10))<1), checks that every value in
a 10×10 spill lands in [0,1) rather than checking what any cell actually contains.
Both cases return #NAME? in 24.2.0.3 and their expected deterministic result
(23 and TRUE) in 24.8.7.2, 25.2.0.3 and 25.8.7.3.
Still unsupported in 25.8.7.3
Nine functions returned #NAME? in every build we ran, including the newest. There is
no version in our tested range where these start working, so a migration has to replace them rather
than wait for an upgrade.
| Function | Formula | Excel, desktop (documented) | Google Sheets (executed 2026-08-29) | LibreOffice 25.8.7.3 (executed) |
|---|---|---|---|---|
| ARRAYTOTEXT | =ARRAYTOTEXT({1,2,3}) | 1, 2, 3 | #NAME? | #NAME? |
| BYCOL | =BYCOL(A1:B2,LAMBDA(c,SUM(c))) | 4, 6 | 4 — read-back 4, 6 (matches) | #NAME? |
| BYROW | =BYROW(A1:B2,LAMBDA(r,SUM(r))) | 3, 7 | 3 — read-back 3, 7 (matches) | #NAME? |
| GROUPBY | =GROUPBY(A1:A4,B1:B4,SUM) | Documented; spilled layout varies with optional arguments, so our case is a presence probe with no fixed expected value | #NAME? | #NAME? |
| MAKEARRAY | =MAKEARRAY(2,2,LAMBDA(r,c,r*c)) | 1, 2 / 2, 4 | 1 — read-back 1, 2, 2, 4 (matches) | #NAME? |
| MAP | =MAP(A1:A3,LAMBDA(x,x*2)) | 2, 4, 6 | 2 — read-back 2, 4, 6 | #NAME? |
| PIVOTBY | =PIVOTBY(A1:A4,B1:B4,C1:C4,SUM) | Documented; spilled pivot layout varies with optional arguments, so our case is a presence probe with no fixed expected value | #NAME? | #NAME? |
| REDUCE | =REDUCE(0,A1:A3,LAMBDA(a,b,a+b)) | 6 | 6 | #NAME? |
| SCAN | =SCAN(0,A1:A3,LAMBDA(a,b,a+b)) | 1, 3, 6 | 1 — read-back 1, 3, 6 | #NAME? |
All nine — ARRAYTOTEXT, BYCOL, BYROW,
GROUPBY, MAKEARRAY, MAP, PIVOTBY,
REDUCE and SCAN — returned #NAME? in all four builds:
24.2.0.3, 24.8.7.2, 25.2.0.3 and 25.8.7.3. Unlike the two waves above, there is no version in our
tested range where any of them start working. Google Sheets tells a different story for six of the
nine: MAP, REDUCE and SCAN already executed and matched, and the
2026-08-29 plain-name run resolved BYCOL, BYROW and MAKEARRAY from
inconclusive to matched as well — all six run correctly in Sheets even though none of them run in
any LibreOffice build we tested. ARRAYTOTEXT, GROUPBY and PIVOTBY
remain #NAME? in Sheets too, and that is a real result: Google does not document any of
the three.
Six of the nine are the LAMBDA helper family, and they have their own writeup with the rewrite
recipes: see
LAMBDA, MAP & SCAN: Excel vs LibreOffice,
which also covers what happens to an inline =LAMBDA(x,x*2)(5) call in Calc.
How we measured the spilling cases
This matters for reading the tables honestly, because most of these functions return an array
rather than a single value. Each case is written into its own sheet with no cached result, and the
file is recalculated by LibreOffice from scratch; the formula sits in the top-left cell of a
check range recorded with the case. The single value shown in the LibreOffice columns is
that anchor cell — which is why =VSTACK(A1:B1,A2:B2) shows as 1 even
though its documented result is 1, 2 / 3, 4.
The harness also reads back every cell of the recorded check range and compares that list,
flattened in reading order, against the flattened expected array — that is the
“read-back” shown after each anchor value, and where it says matches, every
cell in the recorded range equalled the corresponding expected value. So for these cases the honest
claim is: the function evaluates (no #NAME?), the anchor cell matches, and the
cells inside the recorded check range match value-for-value in reading order. What we did
not verify is the shape of the spill — flattening makes a 2×2 block and a
1×4 row indistinguishable — nor anything outside the recorded range, so a result that
spilled further than expected would not be caught. For the scalar cases
(TEXTAFTER, TEXTBEFORE, LET, XLOOKUP,
XMATCH, RANDARRAY) there is no range: the single returned value is what was
compared.
How to migrate safely
Pin the version, if you can. The cheapest fix for wave one is not a rewrite but a requirement: install 25.8 or newer everywhere the workbook will be opened. Our runs show all fourteen functions evaluating there, so a workbook that only uses wave one and wave two functions needs no formula changes at all — it needs a supported release. Where you cannot control the target version, treat 24.8 as the floor and rewrite wave one.
Rewrite wave one with classic functions. =VSTACK(A1:B1,A2:B2) is a
literal block of cells, so reference the combined range directly or copy the two pieces into one
contiguous block. =HSTACK(A1:A2,B1:B2) is the same trick sideways.
=TAKE(A1:C3,2,2) and =DROP(A1:C3,1,1) are sub-ranges: use
=INDEX($A$1:$C$3,ROW()-ROW($E$1)+1,COLUMN()-COLUMN($E$1)+1) filled across the target
block, or simply point at A1:B2 and B2:C3 when the offsets are fixed.
=CHOOSECOLS(A1:C1,3,1) and =CHOOSEROWS(A1:A5,4,2) become one
=INDEX(range,row,col) per output cell. =TOCOL(A1:B2) and
=TOROW(A1:B2), and the WRAPROWS/WRAPCOLS pair, are index
arithmetic: =INDEX($A$1:$B$2,INT((ROW()-1)/2)+1,MOD(ROW()-1,2)+1) filled down reproduces
the flatten, with the divisor set to the block width. =TEXTSPLIT("a,b,c",",") becomes
LEFT/MID/RIGHT around FIND, or Calc’s
Text-to-Columns for a one-off.
Do not rely on catching #NAME?. Wrapping a missing function in
=IFERROR(VSTACK(...),"") hides the failure instead of fixing it: the workbook opens
clean and reports blanks where data should be, which is worse than a visible error. Fix the version
or fix the formula.
Check before you commit to a target. The two buttons below are the fast path: the checker answers “does this one formula survive?” and the Migration Audit answers “which formulas in this file do not?” across a whole .xlsx, in the browser.
Honest limits
The Excel column throughout is Microsoft’s documented behaviour as recorded in our test
corpus — we did not run desktop Excel, and nothing on this page is an Excel measurement. The Google Sheets column is executed output: the same .xlsx was imported to Google Drive on
2026-08-29, recalculated by Sheets, and exported back for readback. Sheets has no version to pin, so
that run is dated rather than versioned. An earlier pass stored these formulas with Excel’s
_xlfn._xlws./_xlfn. storage prefix, which Google’s importer did not map for
five cases (FILTER, SORT, BYCOL, BYROW, MAKEARRAY), so those cells came back inconclusive; a
follow-up run on 2026-08-29 that writes the same cases with plain, unprefixed function names
resolved all five, and the values shown above are from that run. Both runs are kept in our results
file, with per-function provenance recorded in subset_runs. The LibreOffice columns are executed output, and every version claim on this page traces to
one of four result files from LibreOffice Calc 24.2.0.3, 24.8.7.2, 25.2.0.3 and 25.8.7.3. Those are
the only four builds we tested: a function we describe as arriving in 25.8.7.3 may have arrived in
any release after 25.2.0.3, and we cannot narrow it further. See
our methodology for how recalculation is proven rather than
assumed.
Check before you migrate
A note on which Excel this is. The Excel column in the tables above is Microsoft’s documented behaviour for desktop Excel, as recorded in our test corpus — we do not run desktop Excel, and no value in that column is a measurement. Excel for the web is a different application with its own calculation engine, and that one we do run (recalculated on OneDrive, 2026-09-01). Its measured results are published on each function’s own page rather than in these guide tables. Because we have no desktop run to compare against, a disagreement between an Excel-web measurement and the documented column is genuinely ambiguous: it may mean the web engine diverges from the desktop one, or that the documentation is wrong about both. We do not claim to know which.