Formulas & Evaluation
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Baseline date: 2026-10-09. This page was written against Apache POI 5.5.1 (the latest release on that date; the 6.0 line is announced but has not been released), Java 21 and later, Spring Boot 4.x on Spring Framework 7 and Jakarta EE 11. Everything marked as verified was run against the 5.5.1 jars on that date; the 6.0 notes are expectations taken from the project website, not tested behaviour.
A formula is the one thing in a spreadsheet that has two lives: a text that a person or a program writes
(SUM(D2:D4)) and a result that something has to calculate. POI treats the two very differently, and most formula
bugs come from confusing them. POI stores and parses formula text for you, but it only calculates a result when you ask
the FormulaEvaluator to do it, and its calculation engine is a Java re-implementation that knows part of Excel’s
function library, not Excel itself. This page covers writing formulas, reading them, calculating them, extending the
function library, and the limits you will meet. It uses the Bookshelf scenario from the
section landing page: the books-inventory.xlsx catalogue with a stock value per
row and a totals row. Cell basics are in Spreadsheet Usermodel Basics,
number formats in Dates, Numbers & DataFormatter and
the component and jar overview in Introduction & Architecture.
The model: text, tokens, cached value
Three things are attached to a formula cell, and it helps to name them before looking at the API:
| Part | What it is | Where it lives in the file |
|---|---|---|
Formula text |
The expression as a string, without a leading equals sign, in English function names and comma argument separators:
|
|
Token list (RPN) |
The formula parsed into Ptg objects ("parsed things") in reverse Polish (postfix) order: operands first, then the operator that consumes them. This is what Excel’s binary format stores and what the evaluator walks. |
In memory only for |
Cached value |
The result the formula had when the file was last calculated: a number, text, boolean or error code. |
|
The Excel application recalculates formulas when it opens a file that asks for it. POI does not recalculate anything when you set or write a formula: the cell gets a formula and no cached value until you evaluate it. That has two consequences that the rest of the page keeps returning to:
-
A file written by POI that holds formulas without cached values looks right in Excel (which calculates on load) and can look empty or zero in everything that only reads
<v>: previewers, other libraries, SAX importers, aDataFormatterthat does not evaluate. -
Reading a formula cell gives you the formula or the cached value, never a freshly calculated number, unless you call the evaluator.
The path from formula text to a result, as POI implements it:
The interesting detail is in the two failure branches: a syntax error is reported when you set the formula, but an unknown function name is not a syntax error. It parses as a call to a user-defined function and fails only when the evaluator runs (see What the evaluator does not know).
Writing formulas with setCellFormula
Cell.setCellFormula(String) parses the text immediately and switches the cell type to FORMULA. Passing null
removes the formula but keeps its cached result as a plain value; to empty the cell call setBlank(). The rules are the ones of the file format, not of your locale:
| Rule | Consequence |
|---|---|
No leading |
|
English function names |
|
Comma argument separators |
|
Decimal point |
Numeric literals use a point: |
A1-style references |
|
Absolute and relative references |
Written exactly as in the grid. POI does not rewrite references when you copy a row: you build the strings yourself (the |
1-based rows in text, 0-based in the API |
|
Length and nesting |
Excel’s documented limits apply to the file: 8,192 characters per formula, 255 arguments per function, 64 nesting levels. POI itself does not enforce all of them, Excel will repair or reject the file. |
Quotes inside text |
A string literal uses double quotes, and a double quote inside it is doubled: |
A complete export of the catalogue with a stock value per book, a status column and a totals row:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFFormulaEvaluator;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
public class InventoryFormulas {
record Book(String title, double price, int stock) { }
public static void main(String[] args) throws IOException {
List<Book> books = List.of(
new Book("Dune", 12.50, 4),
new Book("Emma", 8.00, 0),
new Book("Ulysses", 15.25, 7));
Path target = Path.of("books-inventory.xlsx");
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("Inventory");
Row header = sheet.createRow(0);
String[] titles = {"Title", "Price", "Stock", "Stock value", "Status"};
for (int c = 0; c < titles.length; c++) {
header.createCell(c).setCellValue(titles[c]);
}
for (int i = 0; i < books.size(); i++) {
Book book = books.get(i);
Row row = sheet.createRow(i + 1);
int excelRow = i + 2; // formulas use 1-based rows; row 1 is the header
row.createCell(0).setCellValue(book.title());
row.createCell(1).setCellValue(book.price());
row.createCell(2).setCellValue(book.stock());
// No leading '=', English function names, comma separators
row.createCell(3).setCellFormula("B" + excelRow + "*C" + excelRow);
row.createCell(4).setCellFormula(
"IF(C" + excelRow + "=0,\"Out of stock\",\"Available\")");
}
int last = books.size() + 1; // last data row, 1-based
Row totals = sheet.createRow(last);
totals.createCell(0).setCellValue("Total");
totals.createCell(3).setCellFormula("SUM(D2:D" + last + ")");
Cell available = totals.createCell(4);
available.setCellFormula("COUNTIF(E2:E" + last + ",\"Available\")");
// Store cached results so that viewers that never calculate still show numbers
XSSFFormulaEvaluator.evaluateAllFormulaCells(workbook);
// Ask Excel to recalculate everything anyway when the file is opened
workbook.setForceFormulaRecalculation(true);
try (var out = Files.newOutputStream(target)) {
workbook.write(out);
}
}
}
}
Source: https://poi.apache.org/components/spreadsheet/quick-guide.html#FormulaEvaluator and https://poi.apache.org/apidocs/dev/org/apache/poi/ss/usermodel/Cell.html#setCellFormula(java.lang.String)
What the file contains for the status cell of the first book (the <f> element is the formula text, <v> the cached
value that evaluateAllFormulaCells stored):
<c r="E2" t="str"><f>IF(C2=0,"Out of stock","Available")</f><v>Available</v></c>
Source: https://poi.apache.org/components/spreadsheet/quick-guide.html and https://ecma-international.org/publications-and-standards/standards/ecma-376/
What a formula can reference
| Reference | Example | Notes |
|---|---|---|
Cell and range on the same sheet |
|
Always works. |
Another sheet of the same workbook |
|
Setting a formula that names a missing sheet succeeds in XSSF (verified) and the evaluation then fails with |
Defined name |
|
The name must exist in the workbook when the formula is set, otherwise the parser throws |
Table structured reference |
|
Parsed by XSSF; evaluation support is partial, so verify the specific form you use. |
Another workbook |
|
Needs an explicit link, see External workbooks. |
Whole column or row |
|
Allowed; expensive when used inside functions evaluated for every row. |
Array formulas
An array formula calculates over arrays and either fills a range with one result each, or returns a single value
computed from arrays. In the file the formula is stored once, on the first cell of the range, and flagged as an array
formula. Sheet.setArrayFormula(String, CellRangeAddress) creates it and returns a CellRange of the cells; the
related methods are Sheet.removeArrayFormula(Cell) and Cell.isPartOfArrayFormulaGroup().
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFFormulaEvaluator;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
public class ArrayFormulaExample {
public static void main(String[] args) throws IOException {
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
XSSFSheet sheet = workbook.createSheet("Inventory");
double[] prices = {12.50, 8.00, 15.25};
int[] stock = {4, 0, 7};
for (int i = 0; i < prices.length; i++) {
Row row = sheet.createRow(i);
row.createCell(0).setCellValue(prices[i]);
row.createCell(1).setCellValue(stock[i]);
}
// Multi-cell array formula: the same formula is stored once for C1:C3
// (the formula is entered in the first cell of the range)
sheet.setArrayFormula("A1:A3*B1:B3", new CellRangeAddress(0, 2, 2, 2));
// Single-cell array formula: total stock value in D1
sheet.setArrayFormula("SUM(A1:A3*B1:B3)", new CellRangeAddress(0, 0, 3, 3));
XSSFFormulaEvaluator.evaluateAllFormulaCells(workbook);
System.out.println(sheet.getRow(1).getCell(2).getNumericCellValue()); // 0.0
System.out.println(sheet.getRow(0).getCell(3).getNumericCellValue()); // 156.75
try (var out = Files.newOutputStream(Path.of("array-formulas.xlsx"))) {
workbook.write(out);
}
}
}
}
Source: https://poi.apache.org/components/spreadsheet/quick-guide.html#CellArrays and https://poi.apache.org/apidocs/dev/org/apache/poi/ss/usermodel/Sheet.html#setArrayFormula(java.lang.String,org.apache.poi.ss.util.CellRangeAddress)
Pitfalls specific to array formulas:
-
You cannot edit one cell of the range afterwards: Excel and POI both refuse to change part of an array. Remove the whole array first.
-
The range must not overlap another array formula or a merged region.
-
These are the classic legacy array formulas (Ctrl+Shift+Enter in Excel). Excel 365 "dynamic arrays" that spill into neighbouring cells, and functions that exist only for them (
FILTER,SORT,UNIQUE,LET), are not supported:setCellFormula("FILTER(A2:A4,C2:C4>0)")fails with aFormulaParseExceptionin 5.5.1 (verified), with a misleading "too many arguments" message.
Calculating formulas with FormulaEvaluator
FormulaEvaluator is created from the workbook (workbook.getCreationHelper().createFormulaEvaluator()) and is bound
to that one workbook. It has more entry points than most people notice, and they differ in what they do to the cell:
| Method | Returns | Effect on the workbook |
|---|---|---|
|
A |
None. The cell keeps its formula and its old cached value. Use it to read a result. |
|
The |
Stores the new result as the cell’s cached value; the formula stays. Use it to prepare a file for readers that do not calculate. |
|
The same |
Destroys the formula: the cell becomes a number, string, boolean or error. Never use it on a workbook you still want to keep formulas in. |
|
nothing |
Runs |
|
nothing |
Static convenience: creates an evaluator for the workbook and runs |
The four calls on the inventory file written above:
import java.io.File;
import java.io.IOException;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.CellValue;
import org.apache.poi.ss.usermodel.FormulaEvaluator;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
public class ReadFormulaCells {
public static void main(String[] args) throws IOException {
File file = new File("books-inventory.xlsx");
// read-only: steps 3 and 4 change cells, and close() must not save them into the file
try (Workbook workbook = WorkbookFactory.create(file, null, true)) {
FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
Sheet sheet = workbook.getSheet("Inventory");
Row totals = sheet.getRow(4);
// 1. Cached value stored in the file: no calculation, no evaluator needed
Cell total = totals.getCell(3);
if (total.getCellType() == CellType.FORMULA) {
System.out.println("Formula: " + total.getCellFormula());
System.out.println("Cached type: " + total.getCachedFormulaResultType());
System.out.println("Cached value: " + total.getNumericCellValue());
}
// 2. evaluate(): calculates, returns a CellValue, the cell is NOT modified
CellValue value = evaluator.evaluate(total);
switch (value.getCellType()) {
case NUMERIC -> System.out.println("Number: " + value.getNumberValue());
case STRING -> System.out.println("Text: " + value.getStringValue());
case BOOLEAN -> System.out.println("Boolean: " + value.getBooleanValue());
case ERROR -> System.out.println("Error code: " + value.getErrorValue());
default -> System.out.println("Other: " + value.getCellType());
}
// 3. evaluateFormulaCell(): calculates and stores the result as the cell's cached value,
// the formula stays; the return value is the type of the result
CellType resultType = evaluator.evaluateFormulaCell(totals.getCell(4));
System.out.println("Result type: " + resultType
+ ", cached: " + totals.getCell(4).getNumericCellValue());
// 4. evaluateInCell(): calculates and REPLACES the formula by its result
Cell status = sheet.getRow(1).getCell(4);
evaluator.evaluateInCell(status);
System.out.println(status.getCellType() + ": " + status.getStringCellValue());
// 5. evaluateAll(): every formula cell of the workbook gets a fresh cached value
evaluator.evaluateAll();
}
}
}
Source: https://poi.apache.org/components/spreadsheet/eval.html and https://poi.apache.org/apidocs/dev/org/apache/poi/ss/usermodel/FormulaEvaluator.html
Notes on the result handling:
-
A
CellValueis a detached value, not a cell. Switch ongetCellType()and call the matching getter:getNumberValue,getStringValue,getBooleanValue,getErrorValue.CellValue.formatAsString()gives a debug string. -
Cell.getCachedFormulaResultType()returns the type of the stored result; in POI 5.x it is aCellTypeenum. CallinggetNumericCellValue()on a formula cell returns the cached number, which is0.0when the file had none. -
evaluatethrows unchecked exceptions:NotImplementedExceptionfor a function POI does not implement, andIllegalArgumentException("Invalid sheetIndex: -1", verified) for a reference to a sheet that does not exist. Catch them around the evaluation of a cell, not around the whole export. -
The evaluator is not thread-safe, and neither is the workbook. One evaluator per workbook, used by one thread.
Cached values, recalculation and what Excel does on opening
After POI writes a formula, there are three ways to make readers see a number:
| Approach | What it does | When to choose it |
|---|---|---|
|
POI calculates and writes |
Your users or downstream tools read the file without Excel (previews, importers, other libraries). Costs CPU on the server. |
|
Sets |
Always a good complement, because the POI evaluator may differ from Excel for rare functions. Costs nothing on the server. Does not help non Excel readers. |
|
The same request limited to one sheet. |
When only some sheets need the guarantee. |
Nothing |
The cell has |
Excel computes on load, so interactive users are fine; every other consumer sees blanks. |
The recommended combination for exports whose files may be read by programs is: evaluate on the server, and set the force flag. The first makes the cached values present, the second makes Excel trust nothing it did not calculate itself.
Evaluating on the server and writing the result gives you the POI engine’s answer, which is the same as
Excel’s for the common functions and may be different (or missing) for the rest. When an export contains a function POI
does not implement, do not evaluate that cell; rely on fullCalcOnLoad and document that non Excel readers will not
see the number.
Error values
A formula that fails in Excel’s own terms (division by zero, bad reference, wrong type) does not throw in POI: the result is an error value, exactly as the user would see in Excel.
| Excel shows | FormulaError/code |
Typical cause |
|---|---|---|
|
|
Intersection of two ranges that do not intersect. |
|
|
Division by zero; |
|
|
Wrong operand type, for example text in an arithmetic operation. |
|
|
A reference to a deleted or invalid cell or sheet. |
|
|
Unknown defined name or function name. |
|
|
Invalid numeric argument. |
|
|
Value not available, for example a lookup that finds nothing. |
Read the code from CellValue.getErrorValue() (a byte) or Cell.getErrorCellValue() and translate it with
FormulaError.forInt(code). In an import page you would turn those into validation messages; in an export, decide
whether to show #N/A or to wrap the formula in IFERROR(...,"").
External workbooks
A formula can read another workbook: '[loans-report.xlsx]Loans'!A1 (workbook file name in brackets, then the sheet).
The file stores a cached copy of the values it read, so Excel can show them without opening the other file. POI needs
two things to evaluate such a reference: the workbook has to be linked under that name, and the evaluator needs to
know the evaluator of the other workbook.
import java.io.IOException;
import java.util.Map;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellValue;
import org.apache.poi.ss.usermodel.FormulaEvaluator;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
public class ExternalWorkbook {
public static void main(String[] args) throws IOException {
try (XSSFWorkbook inventory = new XSSFWorkbook();
XSSFWorkbook loans = new XSSFWorkbook()) {
// The referenced workbook: loans-report.xlsx, sheet "Loans", A1 = number of open loans
loans.createSheet("Loans").createRow(0).createCell(0).setCellValue(42);
// Register the external workbook under the file name used in the formula
inventory.linkExternalWorkbook("loans-report.xlsx", loans);
Sheet sheet = inventory.createSheet("Inventory");
Cell open = sheet.createRow(0).createCell(0);
open.setCellFormula("'[loans-report.xlsx]Loans'!A1");
FormulaEvaluator mine = inventory.getCreationHelper().createFormulaEvaluator();
FormulaEvaluator other = loans.getCreationHelper().createFormulaEvaluator();
// Tell the evaluator which evaluator belongs to which workbook name (include itself)
mine.setupReferencedWorkbooks(Map.of(
"books-inventory.xlsx", mine,
"loans-report.xlsx", other));
CellValue value = mine.evaluate(open);
System.out.println(value.getNumberValue()); // 42.0
}
}
}
Source: https://poi.apache.org/components/spreadsheet/eval.html#Evaluating+workbook+with+references+to+other+workbooks and https://poi.apache.org/apidocs/dev/org/apache/poi/ss/usermodel/FormulaEvaluator.html#setupReferencedWorkbooks(java.util.Map)
-
The map keys must match the names used in the formula, and the map must contain the evaluator of the workbook being evaluated as well (verified: the example works with both entries).
-
setIgnoreMissingWorkbooks(true)makes the evaluator use the cached value stored in the file when a referenced workbook is not in the map, instead of throwing. This is the practical choice for a server, which has no reason to open the other file. -
Do not build external references from user input. A server that resolves
'[...]'names against its own files is a path-disclosure risk; keep external references to a fixed, controlled set. -
linkExternalWorkbookis an XSSF method (XSSFWorkbook.linkExternalWorkbook). For the binary format see Legacy .xls with HSSF.
What the evaluator does not know
POI’s function library is a table of Java implementations in org.apache.poi.ss.formula.functions. Excel’s library is
larger. You can ask POI which names it covers:
Collection<String> supported = WorkbookEvaluator.getSupportedFunctionNames();
Collection<String> missing = WorkbookEvaluator.getNotSupportedFunctionNames();
System.out.println(supported.size() + " implemented, " + missing.size() + " not"); // 252 and 161 in 5.5.1
System.out.println(supported.contains("XLOOKUP")); // true
Source: https://poi.apache.org/apidocs/dev/org/apache/poi/ss/formula/WorkbookEvaluator.html and https://poi.apache.org/components/spreadsheet/eval-devguide.html
The counts above are what the 5.5.1 jar reported on 2026-10-09; run the two calls against your own version, they change between releases. A sample of what 5.5.1 reports as not implemented, because these are the ones a report is likely to use:
| Area | Not implemented (5.5.1, verified) |
|---|---|
Dates and information |
|
Financial |
|
Statistics |
|
Engineering and math |
|
Cube and add-in |
all |
Dynamic arrays |
|
On the other side, functions that are newer than the old classic set exist: XLOOKUP, TEXTJOIN and IFS are in the
supported list in 5.5.1 (verified). Always test a function before building an export on it.
Behaviour when a function is missing:
-
Parsing: a name the parser does not know is treated as a user-defined function.
setCellFormula("SUMA(A1)")succeeds (verified), so a typo or a localised name is not caught when you write it. -
Evaluating: the evaluator throws
NotImplementedException("Error evaluating cell Inventory!B1"), with aNotImplementedFunctionExceptionnaming the function as its cause (verified). The exception is thrown, not turned into#NAME?. -
Writing without evaluating: the file is perfectly valid. Excel knows its own functions and calculates the cell.
Strategies, in order of preference: do not evaluate cells that use unsupported functions; evaluate everything else and
set fullCalcOnLoad; register your own implementation as a user-defined function (next section); or catch the exception
per cell and leave the cached value empty.
User-defined functions
A user-defined function (UDF) is a Java implementation of a formula function that POI does not implement. The extension
point is the FreeRefFunction interface, grouped into a UDFFinder that you add to the workbook as a toolpack.
DefaultUDFFinder is the simple finder built from two parallel arrays; AggregatingUDFFinder combines several.
import java.io.IOException;
import org.apache.poi.ss.formula.OperationEvaluationContext;
import org.apache.poi.ss.formula.eval.BoolEval;
import org.apache.poi.ss.formula.eval.EvaluationException;
import org.apache.poi.ss.formula.eval.OperandResolver;
import org.apache.poi.ss.formula.eval.ValueEval;
import org.apache.poi.ss.formula.functions.FreeRefFunction;
import org.apache.poi.ss.formula.udf.DefaultUDFFinder;
import org.apache.poi.ss.formula.udf.UDFFinder;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellValue;
import org.apache.poi.ss.usermodel.FormulaEvaluator;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
public class IsbnUdf {
/** ISBN_VALID(text): TRUE when the text has 13 digits once hyphens are removed. */
static final FreeRefFunction ISBN_VALID = (ValueEval[] args, OperationEvaluationContext ctx) -> {
try {
ValueEval single = OperandResolver.getSingleValue(args[0], ctx.getRowIndex(), ctx.getColumnIndex());
String digits = OperandResolver.coerceValueToString(single).replace("-", "");
return BoolEval.valueOf(digits.length() == 13 && digits.chars().allMatch(Character::isDigit));
} catch (EvaluationException e) {
return e.getErrorEval(); // propagate #VALUE!, #REF!, ...
}
};
public static void main(String[] args) throws IOException {
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
// Register the function BEFORE parsing formulas that use it and before creating the evaluator
UDFFinder toolPack = new DefaultUDFFinder(
new String[] {"ISBN_VALID"}, new FreeRefFunction[] {ISBN_VALID});
workbook.addToolPack(toolPack);
Sheet sheet = workbook.createSheet("Inventory");
sheet.createRow(0).createCell(0).setCellValue("978-3-16-148410-0");
Cell check = sheet.getRow(0).createCell(1);
check.setCellFormula("ISBN_VALID(A1)");
FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
CellValue result = evaluator.evaluate(check);
System.out.println(result.getBooleanValue()); // true
}
}
}
Source: https://poi.apache.org/components/spreadsheet/user-defined-functions.html and https://poi.apache.org/apidocs/dev/org/apache/poi/ss/formula/udf/DefaultUDFFinder.html
-
The function receives its arguments as
ValueEvalobjects (NumberEval,StringEval,BoolEval,ErrorEval,AreaEval,RefEval). Resolve a reference withOperandResolver.getSingleValue, coerce withOperandResolver.coerceValueToString/coerceValueToDouble, and return aValueEval. ThrowEvaluationException(or return anErrorEval) to produce an Excel error. -
The
OperationEvaluationContextgives the position of the calling cell (getRowIndex,getColumnIndex) and the workbook, which is how a UDF reads other cells. -
Register the toolpack on the
Workbook(addToolPack) before creating the evaluator; an evaluator created earlier does not see it. The toolpack belongs to the workbook object, not to the file: reopen the file and register again. -
A UDF exists only in the Java process. The saved
.xlsxcontains a call to an unknown function, which Excel shows as#NAME?unless the user has an add-in with the same name. Combine a UDF with cached values only for files that are consumed by programs, or implement the same logic as native Excel functions in the formula. -
Names are case insensitive in the formula. Keep them upper case and without dots to avoid ambiguity with namespaces.
The evaluator cache
The evaluator remembers every cell it calculates, so a formula that is referenced a thousand times is calculated once. That is why evaluation is fast, and also why it can return stale answers: the cache does not know that you changed a cell. Tell it:
import java.io.IOException;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.FormulaEvaluator;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
public class EvaluatorCache {
public static void main(String[] args) throws IOException {
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("Inventory");
Row row = sheet.createRow(0);
Cell price = row.createCell(0);
price.setCellValue(12.50);
Cell stock = row.createCell(1);
stock.setCellValue(4);
Cell value = row.createCell(2);
value.setCellFormula("A1*B1");
FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
System.out.println(evaluator.evaluate(value).getNumberValue()); // 50.0
// The evaluator cached the intermediate results. Changing an input cell is not noticed...
stock.setCellValue(10);
System.out.println(evaluator.evaluate(value).getNumberValue()); // still 50.0 (stale)
// ...unless the evaluator is told about the change
evaluator.notifyUpdateCell(stock);
System.out.println(evaluator.evaluate(value).getNumberValue()); // 125.0
// Changing a formula: notifySetFormula; replacing many cells: clear the whole cache
value.setCellFormula("A1*B1*2");
evaluator.notifySetFormula(value);
System.out.println(evaluator.evaluate(value).getNumberValue()); // 250.0
evaluator.clearAllCachedResultValues();
}
}
}
Source: https://poi.apache.org/components/spreadsheet/eval.html and https://poi.apache.org/apidocs/dev/org/apache/poi/ss/usermodel/FormulaEvaluator.html#notifyUpdateCell(org.apache.poi.ss.usermodel.Cell)
| Method | When to call it |
|---|---|
|
After changing the value of a cell that formulas depend on. |
|
After changing the formula of a cell. |
|
After removing a cell or clearing its content. |
|
After bulk changes, or when in doubt. The next evaluation recalculates everything. |
|
Prints the evaluation steps of the next evaluation to the standard output. A debugging aid only; do not leave it on in a server. |
|
Use cached values for external workbooks that were not supplied. |
The simplest safe pattern for an export is to build the whole workbook first, then create the evaluator and evaluate once. Alternating "change a cell, evaluate, change a cell, evaluate" is where stale values come from.
Looking inside: Ptg tokens
The parsed form of a formula is a public, inspectable list. This is rarely needed in application code, but it explains the evaluator’s behaviour and helps when a formula fails to parse.
import java.io.IOException;
import org.apache.poi.ss.formula.FormulaParser;
import org.apache.poi.ss.formula.FormulaRenderer;
import org.apache.poi.ss.formula.FormulaType;
import org.apache.poi.ss.formula.ptg.Ptg;
import org.apache.poi.xssf.usermodel.XSSFEvaluationWorkbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
public class FormulaParts {
public static void main(String[] args) throws IOException {
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
workbook.createSheet("Inventory");
XSSFEvaluationWorkbook evaluationWorkbook = XSSFEvaluationWorkbook.create(workbook);
// Parse the text into the postfix (RPN) token list that the evaluator walks
Ptg[] tokens = FormulaParser.parse(
"SUM(B2:B4)*2", evaluationWorkbook, FormulaType.CELL, 0);
for (Ptg token : tokens) {
System.out.println(token.getClass().getSimpleName());
}
// AreaPtg, AttrPtg (the optimised one-argument SUM), IntPtg, MultiplyPtg
// and back to text
System.out.println(FormulaRenderer.toFormulaString(evaluationWorkbook, tokens)); // SUM(B2:B4)*2
}
}
}
Source: https://poi.apache.org/components/spreadsheet/eval-devguide.html and https://poi.apache.org/apidocs/dev/org/apache/poi/ss/formula/FormulaParser.html
The output shows the postfix order: the range B2:B4, then the SUM marker, then the literal 2, then the
multiplication that consumes the two values on the operand stack. Notice that a one-argument SUM is stored as an
AttrPtg flagged "optimised SUM", not as a function call; this is an Excel file-format detail that POI preserves.
FormulaRenderer.toFormulaString turns the tokens back into text, so a parse followed by a render also normalises the
formula (spacing, case of function names). FormulaParser and the Ptg classes are public API but are described in the
developer guide rather than in a tutorial; the Javadoc is the reference.
Formulas in other POI APIs
| API | Formula support |
|---|---|
HSSF ( |
|
SXSSF (streaming write) |
You can write formulas with |
SAX / event reading |
There is no evaluator. The handler receives the cached value (and, if you ask for it with the
|
Templates |
Opening a template, changing inputs and writing it back keeps the formulas but leaves stale cached values; evaluate or force recalculation. See Templates & Modifying Workbooks. |
Data validation and conditional formatting |
Rules can contain formulas (a list validation |
ExcelAnt
POI also ships ExcelAnt, an Apache Ant integration for testing spreadsheets with POI’s own evaluator from a build
script. It is documented on the project website (see the link below) and is not part of the poi-ooxml jar you use in a
server; check the page for its current packaging and syntax. It is mentioned here only because the
official spreadsheet documentation lists it; most Java servers will use JUnit and the evaluator directly.
Memory and performance
| Aspect | Cost and advice |
|---|---|
Evaluation cache |
One entry per evaluated cell, held for the life of the evaluator. For a workbook with a million formula cells that is
significant; call |
|
Evaluates every formula cell and everything they depend on. Cost grows with the number of formulas times the size of the ranges they read. Evaluate once, at the end. |
Range functions repeated per row |
|
SXSSF |
Formulas are cheap to write; evaluating them is not available beyond the window. Prefer writing computed values or leaving the calculation to Excel. |
Excel on load |
|
Security
-
Formula injection. A user-supplied string that begins with
=,+,-or@can run as a formula when the file is exported to CSV and opened in Excel, andsetCellFormulaon user input creates a live formula in the.xlsxitself.setCellValue(String)always stores text, so it is safe in a native workbook; the risk is the later CSV export and any code that passes user text tosetCellFormula. Never build formula text from request data. The OWASP page below describes the attack and the usual mitigation of prefixing the value; the helper below applies it for text that will also be exported to CSV. -
Formulas in uploaded files. A workbook uploaded by a user may hold formulas such as
HYPERLINKor external references that make a client open a URL or read a file when the file is opened. POI keeps them if you re-export the workbook. Treat formulas in uploaded files as untrusted content and see Introduction & Architecture for the general posture. -
External references and UDFs are local capabilities: keep the set of linked workbooks and registered toolpacks fixed.
import java.io.IOException;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
public class SafeText {
/** Prefixes values that a spreadsheet application could interpret as a formula (needed for CSV export). */
static String neutralise(String text) {
if (text != null && !text.isEmpty() && "=+-@\t\r".indexOf(text.charAt(0)) >= 0) {
return "'" + text;
}
return text;
}
public static void main(String[] args) throws IOException {
String titleFromUser = "=HYPERLINK(\"http://example.org\",\"Dune\")";
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("Inventory");
Row row = sheet.createRow(0);
// setCellValue(String) always stores TEXT, never a formula
Cell text = row.createCell(0);
text.setCellValue(neutralise(titleFromUser));
System.out.println(text.getCellType() == CellType.STRING); // true
// Never do this with user input: setCellFormula(userInput) creates a live formula
// row.createCell(1).setCellFormula(titleFromUser.substring(1));
}
}
}
Pitfalls
| Symptom | Cause and fix |
|---|---|
|
Remove the leading |
|
Use commas. POI never reads the user’s locale. |
File opens in Excel with |
A localised or misspelled function name. POI accepted it as a UDF; use the English name. |
Numbers are |
No cached values. Evaluate on the server, or accept that only Excel calculates. |
|
A function missing from POI. Skip the cell, add a UDF, or catch per cell. |
Result does not change after editing an input |
Stale evaluator cache; call |
Formulas disappeared after the export |
|
Off-by-one in references |
The API is 0-based, formula text is 1-based. |
Different result than Excel |
POI re-implements the functions. Report it with a minimal workbook; see the evaluator section of the official site for known gaps. |
Changes between 4.x, 5.x and 6.0
| Version | Change relevant to formulas |
|---|---|
3.x to 4.x |
Enum based cell types were introduced next to the old |
5.0 |
The old |
5.x |
Function coverage grows with minor releases ( |
6.0 (planned, not released on 2026-10-09) |
The project announces removal of deprecated APIs, |
What POI does not provide
-
Not Excel. The evaluator is an independent implementation of part of Excel’s function library; 161 functions are not implemented in 5.5.1.
-
No dynamic arrays or spill ranges. Only classic array formulas are supported.
-
No macros. VBA is neither executed nor created; a macro-enabled workbook can be read and written but its macros are not run.
-
No recalculation engine inside the file. POI does not track dependencies when you edit cells, apart from the explicit evaluator notifications.
-
No locale handling. Function names and separators are always the file-format ones.
Where the Bookshelf export must display a number that POI cannot compute, compute it in Java and write it as a value, or write the formula and rely on Excel. The next pages continue with layout, validation, tables and charts: Sheet Layout & Printing and Data Validation & Conditional Formatting. The cell formatting that makes formula results readable is in Cell Styles, Fonts & Formats, and the other pages of the section are listed on the landing page.
References
-
Spreadsheet Quick Guide, formulas and evaluator: https://poi.apache.org/components/spreadsheet/quick-guide.html
-
Formula support: https://poi.apache.org/components/spreadsheet/formula.html
-
Formula evaluation: https://poi.apache.org/components/spreadsheet/eval.html
-
Formula evaluation developer guide: https://poi.apache.org/components/spreadsheet/eval-devguide.html
-
User-defined functions: https://poi.apache.org/components/spreadsheet/user-defined-functions.html
-
ExcelAnt: https://poi.apache.org/components/spreadsheet/excelant.html
-
Spreadsheet limitations: https://poi.apache.org/components/spreadsheet/limitations.html
-
Javadoc,
FormulaEvaluator: https://poi.apache.org/apidocs/dev/org/apache/poi/ss/usermodel/FormulaEvaluator.html -
Javadoc,
WorkbookEvaluator: https://poi.apache.org/apidocs/dev/org/apache/poi/ss/formula/WorkbookEvaluator.html -
Javadoc,
FreeRefFunction: https://poi.apache.org/apidocs/dev/org/apache/poi/ss/formula/functions/FreeRefFunction.html -
Official examples: https://github.com/apache/poi/tree/trunk/poi-examples/src/main/java/org/apache/poi/examples/
-
Microsoft Excel specifications and limits: https://support.microsoft.com/en-us/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3
-
OWASP CSV Injection: https://owasp.org/www-community/attacks/CSV_Injection
-
ECMA-376: https://ecma-international.org/publications-and-standards/standards/ecma-376/