On Spreadsheets Done Right Lucía Romero's KPI workbook was split into a raw Readings sheet, an input sheet, a Calc sheet and an output sheet. This page fills the Calc sheet. Every grid below was typed into LibreOffice Calc 25.2 and shows the values Calc displays; formulas are written in Excel's English notation, with commas between arguments, which Calc accepts in an English setup. The numbers are Kestrel Energy's September 2026 sample data, so they can be compared with the SQL results elsewhere in this topic.

What a formula is

A spreadsheet formula is an expression in a cell, starting with =, that calculates the cell's value from constants, references to other cells, operators and functions; the spreadsheet recalculates it whenever one of its inputs changes. The cell shows the result, and the formula bar shows the formula.

  | A              | B
1 | Item           | Value
2 | kWh (M-30002)  | 296.708
3 | Day price      | 0.312
4 | Cost           | =B2*B3
5 | Cost, rounded  | =ROUND(B4,2)
6 | Divide by zero | =B2/0
7 | Typo in name   | =SUMM(B2:B3)
8 | Text plus 1    | ="kWh"+1
  | A              | B
1 | Item           | Value
2 | kWh (M-30002)  | 296.708
3 | Day price      | 0.312
4 | Cost           | 92.572896
5 | Cost, rounded  | 92.57
6 | Divide by zero | #DIV/0!
7 | Typo in name   | #NAME?
8 | Text plus 1    | #VALUE!

Operators follow the usual precedence: ^ before * and /, before + and -, and & joins text. ROUND changes the stored value; formatting a cell to two decimals only changes what you see, and the hidden decimals still count in later sums. An error value is not a failure of the spreadsheet but a message, and it spreads: every formula that uses an error cell shows the same error.

Error Means Typical cause
#DIV/0! Division by zero An empty or zero cell in the denominator
#NAME? Unknown name A typo, a function your version does not have, or a localized function name
#VALUE! Wrong type of value Text used in a calculation
#N/A Value not available A lookup that found no match
#REF! Invalid reference A row, column or sheet the formula used was deleted
#SPILL! A result range is blocked (Excel) Cells below or right of a dynamic array formula are not empty

LibreOffice Calc shows some errors as a code instead, such as Err:508 for a missing bracket; the help lists what each code means.

Relative and absolute references

Formulas are written once and copied down a column. That only works because references move with the copy. A relative reference such as B4 is stored as a position relative to the formula's own cell ("two columns left, same row"), so copying the formula one row down turns it into B5. An absolute reference such as $B$1 points to a fixed cell and stays the same wherever the formula is copied. Mixed references fix only one part: $B4 fixes the column, B$1 the row. Pressing F4 in Excel, or Shift+F4 in LibreOffice Calc, while the cursor is on a reference cycles through the four forms.

A formula copied from row 4 to rows 5 and 6. With an absolute reference, B4 times dollar B dollar 1 becomes B5 times dollar B dollar 1 and B6 times dollar B dollar 1: all three point to the price in B1. With a relative reference, B4 times B1 becomes B5 times B2 and B6 times B3: the price reference slides down with the copy and points at an empty cell and a header. Absolute: =B4*$B$1 Relative: =B4*B1 B1: 0.27 =B4*$B$1 =B5*$B$1 =B6*$B$1 B1: 0.27 B2: empty B3: "kwh" =B4*B1 =B5*B2 =B6*B3 result 0 #VALUE!
Copied down, the absolute reference keeps pointing at the price; the relative one slides to cells that hold something else.

Here are the three fixed-tariff meters priced at €0.27 per kWh, once with the price reference fixed and once as it looks after a relative formula was copied down:

  | A        | B           | C           | D
1 | Price    | 0.27        |             |
3 | meter_id | kwh         | cost_right  | cost_wrong
4 | M-30001  | 334.990     | =B4*$B$1    | =B4*B1
5 | M-30008  | 463.290     | =B5*$B$1    | =B5*B2
6 | M-30010  | 275.960     | =B6*$B$1    | =B6*B3
7 | Total    | =SUM(B4:B6) | =SUM(C4:C6)
  | A        | B       | C          | D
1 | Price    | 0.27    |            |
3 | meter_id | kwh     | cost_right | cost_wrong
4 | M-30001  | 334.99  | 90.4473    | 90.4473
5 | M-30008  | 463.29  | 125.0883   | 0
6 | M-30010  | 275.96  | 74.5092    | #VALUE!
7 | Total    | 1074.24 | 290.0448   |

The first row is right in both columns, which is exactly why the mistake survives: someone checks the first result and copies the formula down. Row 5 multiplies by an empty cell, which counts as 0, and gives a plausible-looking zero. Only row 6 shows an error, because B3 happens to hold text. Check a copied formula in the last row, not the first. A named input cell (=B4*FixedPrice, see named ranges) avoids the question altogether, because a name always points to the same place.

IF and IFS

IF(condition, value_if_true, value_if_false) chooses between two results. Several conditions used to mean nested IFs, which are hard to read; IFS (Excel 2019 and later, LibreOffice, Google Sheets) takes pairs of condition and result and returns the result of the first condition that is TRUE. A final TRUE condition acts as "everything else". This is the same idea as SQL's CASE expression.

  | A        | B         | C
1 | meter_id | kwh       | size
2 | M-30006  | 1554.960  | =IFS(B2>=1000,"large",B2>=300,"medium",TRUE,"small")
3 | M-30001  | 334.990   | =IFS(B3>=1000,"large",B3>=300,"medium",TRUE,"small")
4 | M-30009  | 168.004   | =IFS(B4>=1000,"large",B4>=300,"medium",TRUE,"small")
5 | M-30009  | -4.210    | =IF(B5<0,"check reading","ok")
  | A        | B       | C
1 | meter_id | kwh     | size
2 | M-30006  | 1554.96 | large
3 | M-30001  | 334.99  | medium
4 | M-30009  | 168.004 | small
5 | M-30009  | -4.21   | check reading

The order of the conditions matters: test the largest threshold first, or every large meter is labeled "medium". Without the final TRUE pair, a value that matches no condition gives #N/A. Row 5 uses a plain IF as a check column for the negative reading of meter M-30009 on 21 September, one of the planted problems from Data Types and Data Quality.

SUMIFS and COUNTIFS

SUMIFS adds up the cells of one range for the rows where every condition holds: SUMIFS(sum_range, criteria_range1, criteria1, …). Its partner COUNTIFS counts those rows, and AVERAGEIFS, MAXIFS and MINIFS work the same way. They are the workhorses of a KPI sheet. With September's import per meter on one sheet, kWh per tariff is one formula, copied down:

   | A        | B         | C        | D | E               | F
 1 | meter_id | tariff    | kwh      |   | tariff          | kwh
 2 | M-30001  | fixed     | 334.990  |   | day_night       | =SUMIFS(C$2:C$13,B$2:B$13,E2)
 3 | M-30002  | day_night | 296.708  |   | dynamic         | =SUMIFS(C$2:C$13,B$2:B$13,E3)
 4 | M-30004  | dynamic   | 271.212  |   | fixed           | =SUMIFS(C$2:C$13,B$2:B$13,E4)
 5 | M-30005  | variable  | 208.233  |   | variable        | =SUMIFS(C$2:C$13,B$2:B$13,E5)
 6 | M-30006  | dynamic   | 1554.960 |   | Total           | =SUM(F2:F5)
 7 | M-30007  | day_night | 336.270  |   | Meters >= 300   | =COUNTIFS(C$2:C$13,">=300")
 8 | M-30008  | fixed     | 463.290  |   | dynamic, >= 300 | =COUNTIFS(B$2:B$13,"dynamic",C$2:C$13,">=300")
 9 | M-30009  | variable  | 168.004
10 | M-30010  | fixed     | 275.960
11 | M-30011  | day_night | 1195.540
12 | M-30012  | dynamic   | 231.104
13 | M-30014  | variable  | 221.324
   | A        | B         | C       | E               | F
 1 | meter_id | tariff    | kwh     | tariff          | kwh
 2 | M-30001  | fixed     | 334.99  | day_night       | 1828.518
 3 | M-30002  | day_night | 296.708 | dynamic         | 2057.276
 4 | M-30004  | dynamic   | 271.212 | fixed           | 1074.24
 5 | M-30005  | variable  | 208.233 | variable        | 597.561
 6 | M-30006  | dynamic   | 1554.96 | Total           | 5557.595
 7 | M-30007  | day_night | 336.27  | Meters >= 300   | 5
 8 | M-30008  | fixed     | 463.29  | dynamic, >= 300 | 1
 9 | M-30009  | variable  | 168.004 |                 |
10 | M-30010  | fixed     | 275.96  |                 |
11 | M-30011  | day_night | 1195.54 |                 |
12 | M-30012  | dynamic   | 231.104 |                 |
13 | M-30014  | variable  | 221.324 |                 |

A few details make this formula robust. The ranges use $ on the row numbers (C$2:C$13) so they stay put when the formula is copied down, while the criterion E2 moves along. All ranges must have the same size. A criterion can be a value, a cell, or a comparison written as text (">=300", or ">="&H1 to compare with a cell). Text criteria ignore case, as the previous page showed. In an Excel table (say MeterTotals, with columns meter_id, tariff and kwh) the formula reads =SUMIFS(MeterTotals[kwh], MeterTotals[tariff], E2) and grows with the data.

The same totals in SQL are one GROUP BY. Run on the full daily readings in DuckDB, they match to the last decimal, and the "Total" check cell above matches the sum of all readings:

SELECT c.tariff, sum(r.kwh_import) AS kwh
FROM daily_readings r
JOIN meters m USING (meter_id)
JOIN contracts c USING (contract_id)
GROUP BY c.tariff
ORDER BY c.tariff;
┌───────────┬───────────────┐
│  tariff   │      kwh      │
│  varchar  │ decimal(38,3) │
├───────────┼───────────────┤
│ day_night │      1828.518 │
│ dynamic   │      2057.276 │
│ fixed     │      1074.240 │
│ variable  │       597.561 │
└───────────┴───────────────┘

Both still contain the raw data's problems (the duplicate reading, the reading ten times too high, the meter whose customer does not exist). A formula sums what it is given; whether the inputs are right is the question of Data Types and Data Quality.

Lookups

A lookup fetches a value from another table by matching a key: the customer's name for a contract, the tariff for a meter, the price for a date. It is the spreadsheet version of a left join, one cell at a time.

XLOOKUP

XLOOKUP searches one range for a value and returns the item in the same position of another range: XLOOKUP(lookup_value, lookup_array, return_array, [if_not_found], [match_mode], [search_mode]). It matches exactly by default, can look left as well as right, and takes a value to show when nothing is found. It is available in Excel 2021, 2024 and Microsoft 365, in Google Sheets, and in LibreOffice from version 24.8; not in Excel 2019 or 2016.

  | A           | B           | C                                                | D | E           | F
1 | contract_id | customer_id | name                                             |   | customer_id | name
2 | CT-20004    | C-10003     | =XLOOKUP(B2,E$2:E$4,F$2:F$4,"unknown customer") |   | C-10003     | Lotte Kortenhoeve
3 | CT-20006    | C-10005     | =XLOOKUP(B3,E$2:E$4,F$2:F$4,"unknown customer") |   | C-10005     | Bakkerij Korenschoof B.V.
4 | CT-20014    | C-10099     | =XLOOKUP(B4,E$2:E$4,F$2:F$4,"unknown customer") |   | C-10011     | Marit Wolterbeek
5 | CT-20014    | C-10099     | =XLOOKUP(B5,E$2:E$4,F$2:F$4)
6 | CT-20006    | C-10005     | =INDEX(F$2:F$4,MATCH(B6,E$2:E$4,0))
7 | CT-20006    | C-10005     | =VLOOKUP(B7,E$2:F$4,2,FALSE)
  | A           | B           | C                         | E           | F
1 | contract_id | customer_id | name                      | customer_id | name
2 | CT-20004    | C-10003     | Lotte Kortenhoeve         | C-10003     | Lotte Kortenhoeve
3 | CT-20006    | C-10005     | Bakkerij Korenschoof B.V. | C-10005     | Bakkerij Korenschoof B.V.
4 | CT-20014    | C-10099     | unknown customer          | C-10011     | Marit Wolterbeek
5 | CT-20014    | C-10099     | #N/A                      |             |
6 | CT-20006    | C-10005     | Bakkerij Korenschoof B.V. |             |
7 | CT-20006    | C-10005     | Bakkerij Korenschoof B.V. |             |

Contract CT-20014 belongs to customer C-10099, who does not exist: the orphan contract from the planted data. Without the fourth argument the lookup returns #N/A (row 5), which at least shows up. Be careful with "unknown customer" or an IFERROR around a lookup: they turn a visible error into a quiet label, and a SUMIFS on names will skip the row. That is fine when the label is reported, as Femke Jansen's definition of an active contract does by listing contracts without a known customer separately (see define the measure before you count), and harmful when it hides the problem.

INDEX and MATCH

Before XLOOKUP, the robust lookup was INDEX and MATCH: MATCH(value, range, 0) returns the position of an exact match in a range, and INDEX(range, position) returns the item at that position, so =INDEX(F$2:F$4,MATCH(B6,E$2:E$4,0)) (row 6 above) does what XLOOKUP does. It works in every version of Excel, LibreOffice and Google Sheets, which makes it the safe choice for a workbook that people open in older versions. Note the 0: MATCH's default is an approximate match.

The VLOOKUP traps

VLOOKUP(value, table, column_number, [range_lookup]) is still the most common lookup, and it has two traps. First, the fourth argument defaults to TRUE, an approximate match that assumes the first column is sorted. On an unsorted list it does not report a problem; it returns whatever its search lands on:

  | A        | B         | C | D                    | E
1 | meter_id | tariff    |   | Look up              | Result
2 | M-30010  | fixed     |   | M-30006, approximate | =VLOOKUP("M-30006",A2:B5,2)
3 | M-30002  | day_night |   | M-30006, exact       | =VLOOKUP("M-30006",A2:B5,2,FALSE)
4 | M-30008  | fixed     |   | M-30005, approximate | =VLOOKUP("M-30005",A2:B5,2)
5 | M-30005  | variable  |   | M-30005, exact       | =VLOOKUP("M-30005",A2:B5,2,FALSE)
  | A        | B         | D                    | E
1 | meter_id | tariff    | Look up              | Result
2 | M-30010  | fixed     | M-30006, approximate | day_night
3 | M-30002  | day_night | M-30006, exact       | #N/A
4 | M-30008  | fixed     | M-30005, approximate | day_night
5 | M-30005  | variable  | M-30005, exact       | variable

M-30006 is not in the list, yet the approximate lookup answers "day_night". M-30005 is in the list, and the approximate lookup still answers "day_night" instead of "variable". Which wrong answer you get depends on the program's search method, so another spreadsheet program can give a different wrong answer. Always pass FALSE (or 0) unless you mean an approximate match on sorted data. Second, column_number is a typed number: insert a column into the table and 2 now fetches the wrong column, without an error. XLOOKUP and INDEX/MATCH refer to the return range itself and do not have this problem.

Approximate match on purpose: prices by date

Sometimes an approximate match is exactly right. Kestrel's variable tariff cost €0.2500 per kWh from 1 January 2026 and €0.2600 from 1 August. The price for a month is the last price whose valid_from is on or before that month: an "exact or next smaller" match. XLOOKUP does it with match mode -1, and MATCH with match type 1:

  | A          | B                                | C                                   | D | E          | F
1 | period     | price (XLOOKUP)                  | price (INDEX/MATCH)                 |   | valid_from | price
2 | 2026-06-01 | =XLOOKUP(A2,E$2:E$3,F$2:F$3,,-1) | =INDEX(F$2:F$3,MATCH(A2,E$2:E$3,1)) |   | 2026-01-01 | 0.2500
3 | 2026-07-01 | =XLOOKUP(A3,E$2:E$3,F$2:F$3,,-1) | =INDEX(F$2:F$3,MATCH(A3,E$2:E$3,1)) |   | 2026-08-01 | 0.2600
4 | 2026-08-01 | =XLOOKUP(A4,E$2:E$3,F$2:F$3,,-1) | =INDEX(F$2:F$3,MATCH(A4,E$2:E$3,1))
5 | 2026-09-01 | =XLOOKUP(A5,E$2:E$3,F$2:F$3,,-1) | =INDEX(F$2:F$3,MATCH(A5,E$2:E$3,1))
6 | 2026-09-01 | =XLOOKUP(A6,E$2:E$3,F$2:F$3)     | =VLOOKUP(A6,E$2:F$3,2,TRUE)
  | A          | B               | C                   | E          | F
1 | period     | price (XLOOKUP) | price (INDEX/MATCH) | valid_from | price
2 | 2026-06-01 | 0.25            | 0.25                | 2026-01-01 | 0.25
3 | 2026-07-01 | 0.25            | 0.25                | 2026-08-01 | 0.26
4 | 2026-08-01 | 0.26            | 0.26                |            |
5 | 2026-09-01 | 0.26            | 0.26                |            |
6 | 2026-09-01 | #N/A            | 0.26                |            |

Row 6 shows both sides of the coin: an exact XLOOKUP finds no price "valid from 1 September" and gives #N/A, while VLOOKUP with TRUE, on this sorted table, is correct. In DuckDB the same lookup is an ASOF JOIN (see Analytical SQL). MATCH with type 1 and VLOOKUP with TRUE need the table sorted by valid_from. XLOOKUP's match mode -1 does not (only its binary search modes 2 and -2 do). Dates must be real dates in every case: typed as 2026-08-01 they are, but a date that arrived as text compares as text.

Text and date functions

Exports rarely arrive clean. A handful of text functions repair most problems in a check column, without touching the raw data: TRIM removes leading, trailing and doubled spaces, UPPER, LOWER and PROPER fix case, LEFT, RIGHT, MID and LEN cut and measure, and SUBSTITUTE replaces characters. NUMBERVALUE(text, decimal_separator) turns a number written as text with a decimal comma into a real number, which is the fix for a Dutch export opened in an English setup (see CSV dialects).

Dates are numbers: a serial count of days, shown in a date format. That makes date arithmetic easy (subtract two dates to get days) and has one surprise: a formula that returns a date may show the bare serial number until the cell is formatted as a date. EOMONTH(date, months) gives the last day of a month, WORKDAY(date, days) skips weekends (and optionally a list of holidays), and TEXT(value, format) turns a value into formatted text.

   | A                 | B
 1 | Raw city          | ' den haag
 2 | Cleaned           | =PROPER(TRIM(B1))
 3 | Raw amount (text) | '12,50
 4 | As a number       | =NUMBERVALUE(B3,",")
 5 | Report date       | 2026-10-06
 6 | Last month end    | =EOMONTH(B5,-1)
 7 | Shown as a date   | =TEXT(B6,"yyyy-mm-dd")
 8 | Month label       | =TEXT(B6,"yyyy-mm")
 9 | 5th working day   | =TEXT(WORKDAY(B6,5),"yyyy-mm-dd")
10 | Days to due date  | =DATE(2026,10,15)-B5
   | A                 | B
 1 | Raw city          |  den haag
 2 | Cleaned           | Den Haag
 3 | Raw amount (text) | 12,50
 4 | As a number       | 12.5
 5 | Report date       | 2026-10-06
 6 | Last month end    | 46295
 7 | Shown as a date   | 2026-09-30
 8 | Month label       | 2026-09
 9 | 5th working day   | 2026-10-07
10 | Days to due date  | 9

46295 is 30 September 2026 as a serial number; formatting B6 as a date would show it as one. The KPI pack is due on the fifth working day of the month, Wednesday 7 October, and September's invoices fall due on 15 October, nine days after the report date. Typing dates in ISO 8601 form (2026-10-06) is the safe habit: every locale reads it the same way, unlike 10/06/2026, which is 6 October in the United States and 10 June in the Netherlands and the UK (see dates and times).

Dynamic arrays: FILTER, UNIQUE and SORT

Classic formulas return one value. A dynamic array formula returns a whole range of values from a single formula: a filtered list, the distinct values of a column, a sorted copy. In Excel 2021, 2024 and Microsoft 365 the result spills: in Microsoft's words, "a formula has resulted in multiple values, and those values have been placed in the neighboring cells". The spill range is that block of cells, outlined when selected; you refer to all of it by the top cell followed by #, as in E2#. If any cell in the way is not empty, the formula shows #SPILL! until you clear it.

LibreOffice Calc has FILTER, UNIQUE, SORT, SORTBY and SEQUENCE since version 24.8, but version 25.2 does not spill a normal formula: select the output range and enter the formula with Ctrl+Shift+Enter as an array formula. (The grids on these pages are entered that way automatically, sized to the result.) The # reference is Excel syntax; in Calc refer to the range itself.

   | A          | B      | C      | D | E                             | F
 1 | invoice_id | amount | status |   | Paid in September             |
 2 | INV-40026  | 86.35  | open   |   | =FILTER(A2:B13,C2:C13="paid") |
 3 | INV-40027  | 88.10  | open
 4 | INV-40028  | 66.95  | open
 5 | INV-40029  | 62.30  | open   |   | status                        | amount
 6 | INV-40030  | 640.10 | paid   |   | =UNIQUE(C2:C13)               | =SUMIFS(B$2:B$13,C$2:C$13,E6)
 7 | INV-40031  | 95.60  | open   |   |                               | =SUMIFS(B$2:B$13,C$2:C$13,E7)
 8 | INV-40032  | 191.40 | paid
 9 | INV-40033  | 55.20  | open
10 | INV-40034  | 94.75  | open
11 | INV-40035  | 452.90 | open
12 | INV-40036  | 58.80  | open
13 | INV-40037  | 71.90  | open
   | A          | B      | C      | E                 | F
 1 | invoice_id | amount | status | Paid in September |
 2 | INV-40026  | 86.35  | open   | INV-40030         | 640.1
 3 | INV-40027  | 88.1   | open   | INV-40032         | 191.4
 4 | INV-40028  | 66.95  | open   |                   |
 5 | INV-40029  | 62.3   | open   | status            | amount
 6 | INV-40030  | 640.1  | paid   | open              | 1132.85
 7 | INV-40031  | 95.6   | open   | paid              | 831.5
 8 | INV-40032  | 191.4  | paid   |                   |
 9 | INV-40033  | 55.2   | open   |                   |
10 | INV-40034  | 94.75  | open   |                   |
11 | INV-40035  | 452.9  | open   |                   |
12 | INV-40036  | 58.8   | open   |                   |
13 | INV-40037  | 71.9   | open   |                   |

These are Kestrel's twelve September invoices. FILTER returns the two paid ones (the bakery and the bicycle repair shop pay by direct debit), with both columns. UNIQUE lists each status once, in the order they first appear, and SUMIFS next to it adds up each status: €1,132.85 open and €831.50 paid, the same totals as the GROUPING SETS query on Analytical SQL. In Excel you would write =SUMIFS(B2:B13,C2:C13,E6#) once and let it spill along with the UNIQUE list; in Calc 25.2 the SUMIFS is copied down by hand, and must be extended if a new status appears. FILTER also accepts a third argument with what to show when nothing matches; without it, an empty result is an error.

Excel-only functions

Microsoft 365 goes further. GROUPBY builds the whole summary above in one formula, with a total row (as of October 2026 only in Excel for Microsoft 365; it is explained with pivot tables on Pivot Tables and Summaries). It is not in LibreOffice or Google Sheets, so it cannot be run here:

=GROUPBY(B2:B13, C2:C13, SUM)        B = tariff, C = kwh, the meter table under SUMIFS above
day_night   1828.518
dynamic     2057.276
fixed        1074.24
variable     597.561
Total       5557.595

The Calc-compatible alternative is the pattern just shown: UNIQUE (or SORT of UNIQUE) for the groups and SUMIFS beside it. The same goes for TEXTSPLIT, VSTACK, TAKE and DROP: Excel 2024 and Microsoft 365 have them, LibreOffice only from version 25.8. In Calc 25.2, split text with LEFT, MID and FIND, or better, split columns when the data is imported.

LET: names inside a formula

Long formulas repeat themselves. The LET function assigns names to intermediate results inside one formula and then uses them: LET(name1, value1, name2, value2, …, calculation). Each value is calculated once, and the formula reads like a small program instead of a puzzle. LET is in Excel 2021 and later, Google Sheets, and LibreOffice from 24.8.

  | A           | B
1 | kwh         | 1074.240
2 | price       | 0.2700
3 | VAT rate    | 21%
4 | Without LET | =ROUND(B1*B2+B1*B2*B3,2)
5 | With LET    | =LET(net,B1*B2,vat,net*B3,ROUND(net+vat,2))
  | A           | B
1 | kwh         | 1074.24
2 | price       | 0.27
3 | VAT rate    | 21.00%
4 | Without LET | 350.95
5 | With LET    | 350.95

The fixed tariff's September energy at €0.27 per kWh plus 21% VAT, as a simplified illustration: Kestrel's real invoices also contain a standing charge and energy tax, which is why they do not reconcile with readings times price. Typing 21% stores 0.21 and formats the cell as a percentage. The names in LET exist only inside that one formula; for a name the whole workbook can use, define a named range. Giving a calculation a name is the same idea as abstraction in programming.

Excel 2024, Microsoft 365 and Google Sheets go one step further with LAMBDA, which turns a formula into a reusable function with parameters, usually saved under a name in Name Manager. LibreOffice has no LAMBDA in any release as of October 2026, so this is illustrative:

Name:       CostInclVAT
Refers to:  =LAMBDA(kwh, price, vat, ROUND(kwh*price*(1+vat), 2))

In a cell:  =CostInclVAT(B1, B2, B3)
350.95

Versions and languages

A formula that works on your machine can fail on Margriet's, for two reasons: her program may not have the function, or it may expect it in another language.

Function availability as of October 2026
Function Excel 365 Excel 2024 Excel 2021 Excel 2019 LibreOffice Google Sheets
IFS, SUMIFS, COUNTIFS, INDEX/MATCH Yes Yes Yes Yes Yes Yes
XLOOKUP, FILTER, UNIQUE, SORT, SEQUENCE, LET Yes Yes Yes No 24.8 and later Yes
XMATCH, SORTBY Yes Yes Yes No 24.8 and later No
TEXTSPLIT, VSTACK, HSTACK, TAKE, DROP Yes Yes No No 25.8 and later VSTACK and HSTACK only (SPLIT instead of TEXTSPLIT)
LAMBDA Yes Yes No No No Yes
GROUPBY, PIVOTBY Yes No No No No No

Microsoft's own XLOOKUP page lists Excel 2019 under "Applies to", but its text says the function is not available in Excel 2016 and Excel 2019; the text is right. The PIVOTBY page similarly lists Excel 2021 and 2024, which is almost certainly a mistake: treat PIVOTBY, like its twin GROUPBY, as Microsoft 365 only. When you share a workbook with someone on an older version, a missing function shows up as #NAME?.