Dates, Numbers & DataFormatter
|
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. It
explains the two things that surprise every developer who moves values between Java and a spreadsheet: a cell has no
date type and no integer type, only a double that a number format decorates, and the text a user sees in a cell is
produced by that format and not stored anywhere. The page covers how dates and times are stored, the DateUtil and
LocalDateTime methods, DataFormatter (the class that renders a cell as Excel shows it), a table of format codes and
the text they produce, and the rules for reading numbers that arrive as text or lose precision. Number formats as a
styling tool (how to create and share them) are in
Cell Styles, Fonts & Formats; the cell types and the
ss.usermodel interfaces are in Spreadsheet Usermodel Basics;
formula results are in Formulas & Evaluation. The examples use the
Bookshelf scenario described in the section landing page.
How Excel stores dates, times and numbers
A spreadsheet cell holds one of a few raw values: a double, a string, a boolean, an error code, a formula, or nothing. There
is no date cell and no integer cell. A date is a double that counts days, and the cell only looks like a date because its
style carries a number format such as yyyy-mm-dd.
| Concept | How it is stored |
|---|---|
Serial date |
The integer part is the number of days since an epoch. |
Time of day |
The fractional part is the fraction of a day. |
Time zone |
None. A date in a workbook is a "local wall clock" value without an offset. |
Numbers |
A 64-bit IEEE 754 double, exactly as |
Integers |
Stored as doubles too. |
Text that looks like a number |
A string cell. Excel marks it with a green triangle (number stored as text). POI never converts it for you. |
The 1900 and 1904 date systems
An .xlsx workbook records which epoch it uses in the workbook properties (date1904).
| System | Serial 1 means | Where it comes from |
|---|---|---|
1900 (default) |
1 January 1900. Serial |
Excel for Windows and the default of every file POI creates. |
1904 |
1 January 1904 (serial |
Workbooks created in old Excel for Mac and some tools that export them. A date that is off by four years and one day is almost always a 1904 workbook read as 1900. |
The 1900 system has a deliberate historical bug that Excel keeps for compatibility with Lotus 1-2-3: it treats 1900 as a
leap year and has a serial 60 for the non-existent 29 February 1900. POI follows Excel, so serial numbers below 60 do not
match a real count of calendar days, and DateUtil.getLocalDateTime(60.0) returns 1 March 1900 (there is
no way to represent the phantom day in java.time). In practice any date after 1 March 1900 converts correctly and the
issue only matters for very old historical data.
POI handles the epoch for you whenever it can see the workbook: Cell.getDateCellValue() and
Cell.getLocalDateTimeCellValue() read the flag of the workbook the cell belongs to, and XSSFWorkbook.isDate1904()
exposes it. The static DateUtil methods have no workbook, so they take a use1904windowing boolean that you must pass
yourself (the overloads without it assume 1900).
Writing dates
Cell has overloads for the Java date types, and each one stores the matching serial number.
| Method | Behaviour |
|---|---|
|
Stores the date at midnight as a serial number. Preferred for dates. |
|
Stores date and time as a serial number with a fraction. Preferred for timestamps. |
|
Same, using the JVM default time zone to find the local date and time. Prefer the |
|
Same, using the calendar’s own fields. Kept for old code. |
|
Stores a raw serial number; use it only for a time of day (a fraction of a day) or when you convert yourself with
|
Writing the value is only half of the job: a date cell without a date number format shows the raw serial (45658).
Always set a style with a date format, and create that style once and reuse it for all cells, because an .xlsx
workbook is limited to 64,000 cell styles (the sharing rule is explained in
Cell Styles, Fonts & Formats).
The first half of the example below writes a loan sheet with a date, a timestamp and a time-only cell; the second half
reads the file back with the matching getters. It prints the serial number of each cell, whether POI considers it a date,
and the converted java.time value.
import java.io.IOException;
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.CreationHelper;
import org.apache.poi.ss.usermodel.DateUtil;
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;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
public class WriteDates {
public static void main(String[] args) throws IOException {
Path file = Path.of("books-dates.xlsx");
try (Workbook workbook = new XSSFWorkbook()) {
CreationHelper helper = workbook.getCreationHelper();
Sheet sheet = workbook.createSheet("Loans");
// One style per format, created once and reused (the style limit is 64,000 in .xlsx).
CellStyle dateStyle = workbook.createCellStyle();
dateStyle.setDataFormat(helper.createDataFormat().getFormat("yyyy-mm-dd"));
CellStyle dateTimeStyle = workbook.createCellStyle();
dateTimeStyle.setDataFormat(helper.createDataFormat().getFormat("yyyy-mm-dd hh:mm"));
CellStyle timeStyle = workbook.createCellStyle();
timeStyle.setDataFormat(helper.createDataFormat().getFormat("hh:mm"));
Row row = sheet.createRow(0);
Cell loanDate = row.createCell(0);
loanDate.setCellValue(LocalDate.of(2025, 1, 1)); // stored as the serial number 45658
loanDate.setCellStyle(dateStyle);
Cell returnBy = row.createCell(1);
returnBy.setCellValue(LocalDateTime.of(2025, 1, 15, 18, 0)); // 45672.75
returnBy.setCellStyle(dateTimeStyle);
Cell openingTime = row.createCell(2);
openingTime.setCellValue(LocalTime.of(9, 30).toSecondOfDay() / 86_400.0); // a time of day is a fraction of a day
openingTime.setCellStyle(timeStyle);
try (var out = java.nio.file.Files.newOutputStream(file)) {
workbook.write(out);
}
}
try (Workbook workbook = WorkbookFactory.create(file.toFile(), null, true)) {
Row row = workbook.getSheet("Loans").getRow(0);
for (Cell cell : row) {
System.out.println(cell.getAddress() + " serial=" + cell.getNumericCellValue()
+ " isDate=" + DateUtil.isCellDateFormatted(cell));
}
LocalDateTime returnBy = row.getCell(1).getLocalDateTimeCellValue();
System.out.println(returnBy.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
LocalDate loanDate = row.getCell(0).getLocalDateTimeCellValue().toLocalDate();
LocalTime opening = row.getCell(2).getLocalDateTimeCellValue().toLocalTime();
System.out.println(loanDate + " " + opening);
}
}
}
Running it prints the following. The three cells hold the numbers 45658, 45672.75 and 0.3958333..., and the
number format is the only reason they are dates:
A1 serial=45658.0 isDate=true
B1 serial=45672.75 isDate=true
C1 serial=0.3958333333333333 isDate=true
2025-01-15T18:00:00
2025-01-01 09:30
Reading dates and the DateUtil class
DateUtil is the static helper behind every date conversion. Its main members:
| Method | Purpose |
|---|---|
|
True if the cell is numeric and its number format is a date or time format. This is the test that tells a date from a
number before calling |
|
The same decision for a format that is not attached to a cell yet. |
|
True only for the built-in date formats (ids such as 14 or 22), not for custom ones. |
|
Java to serial number. The overloads with a |
|
Serial number to |
|
Serial number to the legacy |
|
False for negative numbers, which are not valid dates in the 1900 system. |
|
Controls a thread-local cache that speeds up the repeated date conversions of a big sheet. Leave it enabled (the default). |
Cell.getLocalDateTimeCellValue() returns null for a blank cell and throws IllegalStateException ("Cannot get a
NUMERIC value from a STRING cell") for a text cell, so an import has to look at the cell type first.
How POI decides that a cell is a date
isCellDateFormatted does not look at the value. It looks at the cell’s number format and applies a heuristic on the
format code: letters such as y, m, d, h, s outside quotes and brackets make it a date format, and so does the
elapsed-time code [h]:mm. Consequences:
-
A number with a style
Generalis never a date, even if the value is45658. There is no way to know that the author meant a date; that decision belongs to your import validation. -
A custom code that happens to contain date letters in a literal (for example
0.0 "d"for days) is protected by the quotes, but unquoted letters are read as date codes. -
The format of a cell can be changed by conditional formatting, and the same cell then shows as a date or as a number depending on its value. Use the
isCellDateFormatted(Cell, ConditionalFormattingEvaluator)overload if you want to respect it (the evaluator is described in Data Validation & Conditional Formatting). -
A string cell containing
2025-01-01is not a date for POI. Users paste ISO dates as text all the time.
Coercing real-world cells
An upload endpoint cannot trust the type of a cell: the same column holds a real date in one row, an ISO string in the
next, a formula in the third and an empty cell in the fourth. The example gathers that logic in two small methods that
return Optional so the caller can attach a row number to the validation message (the full import flow is in a
later page of the section). A formula cell is evaluated first with FormulaEvaluator.evaluateFormulaCell, which returns
the type of the result.
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Optional;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.DateUtil;
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 DateCoercion {
private static final DateTimeFormatter ISO = DateTimeFormatter.ISO_LOCAL_DATE;
/** Reads a "published on" cell that users fill as a real date or as ISO text. */
static Optional<LocalDate> readDate(Cell cell, FormulaEvaluator evaluator) {
if (cell == null) {
return Optional.empty();
}
CellType type = cell.getCellType() == CellType.FORMULA
? evaluator.evaluateFormulaCell(cell)
: cell.getCellType();
return switch (type) {
case NUMERIC -> DateUtil.isCellDateFormatted(cell)
? Optional.of(cell.getLocalDateTimeCellValue().toLocalDate())
: Optional.empty(); // a plain number, not a date: report it as invalid
case STRING -> {
try {
yield Optional.of(LocalDate.parse(cell.getStringCellValue().trim(), ISO));
} catch (DateTimeParseException e) {
yield Optional.empty();
}
}
default -> Optional.empty(); // blank, boolean, error
};
}
/** Reads a price cell as an exact decimal, whatever the cell type. */
static Optional<BigDecimal> readPrice(Cell cell, FormulaEvaluator evaluator) {
if (cell == null) {
return Optional.empty();
}
CellType type = cell.getCellType() == CellType.FORMULA
? evaluator.evaluateFormulaCell(cell)
: cell.getCellType();
try {
return switch (type) {
case NUMERIC -> Optional.of(BigDecimal.valueOf(cell.getNumericCellValue()));
case STRING -> Optional.of(new BigDecimal(cell.getStringCellValue().trim().replace(',', '.')));
default -> Optional.empty();
};
} catch (NumberFormatException e) {
return Optional.empty();
}
}
public static void main(String[] args) throws IOException {
try (Workbook workbook = WorkbookFactory.create(Path.of("books-import.xlsx").toFile(), null, true)) {
FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
Sheet sheet = workbook.getSheetAt(0);
for (Row row : sheet) {
System.out.println(readDate(row.getCell(1), evaluator) + " " + readPrice(row.getCell(3), evaluator));
}
}
}
}
Source: POI Javadoc, DateUtil
DataFormatter: the text Excel shows
DataFormatter turns a cell into the string the user sees in Excel: it applies the number format of the cell’s style,
including thousands separators, the number of decimals, percentages, currencies, dates and the colours and sections of a
format code. This is what you want when you read a spreadsheet as "a table of text", for example to import a catalogue,
to build a preview for the user or to write a CSV that matches the screen.
getNumericCellValue() and getStringCellValue() return the raw content; DataFormatter.formatCellValue(cell) returns
the displayed text. They give different answers whenever a style is involved:
-
A numeric
0.256with format0%is0.256raw and26%formatted. -
A numeric
42with format000000is42.0raw and000042formatted. This is how an identifier with leading zeros survives a round trip through a number cell. -
A date cell is a double for
getNumericCellValue()and2025-01-01forformatCellValue. -
A string cell is returned unchanged by both.
-
A boolean is
TRUEorFALSE, an error cell is the error text (#DIV/0!), and a blank cell is the empty string.
The formatCellValue overloads and the options
| Member | Behaviour |
|---|---|
|
Formats the cell. For a formula cell it returns the formula text (for example |
|
Evaluates the formula first and formats the result with the style of the cell. This is the overload to use on workbooks that contain formulas. |
|
The same, also applying number formats and colours that conditional formatting would give the cell. |
|
Formats with the symbols and names of the given locale: decimal and grouping separators, and month and day names. The default constructor uses the JVM default locale, which makes the output differ between a laptop and a container. |
|
With |
|
When true, a formula cell is formatted from the result cached in the file, without evaluating anything. Default false in 5.5.1. |
|
Turns the 2-digit years of built-in formats such as |
|
Register your own |
|
Formats a raw number with a format index and code, without a cell. Useful for tests. |
A DataFormatter caches the Format objects it creates, so create one and reuse it for the whole import of one file.
It keeps mutable state and is not documented as thread safe: do not share an instance between threads; one per request
(or one per import job) is cheap.
Locale: the same cell, different text
A format code such as mmmm yyyy is locale independent in the file, and the rendering is not. With the cell value
45658.5 the formatter returns January 2025 for Locale.US, Januar 2025 for Locale.GERMANY and janvier 2025
for Locale.FRANCE. Numbers follow the same rule: 1234.5 with #,##0.00 is 1,234.50 for the US and 1.234,50
for Germany. Pass the locale of the user who wrote the file (or a fixed Locale.ROOT/Locale.US for machine-to-machine
imports) and do not rely on the server default. The built-in formats with ids below 164 (such as id 14, shown as
m/d/yy) use fixed US-style patterns in POI and only the names are localised.
Formula cells, the evaluator and the cached value
A formula cell stores two things in the file: the formula text and the last result that Excel (or the writing library)
calculated, called the cached value. POI’s DataFormatter can show either.
| Call | POI 5.5.1 result for 0.1+0.2 |
Remarks |
|---|---|---|
|
|
Surprising to most users. This is the long-standing 5.x default. |
|
|
The evaluator calculates the formula now; slow for large sheets, fails on unsupported functions. |
|
The cached value, formatted |
Fast; correct only if the file has a cached value (a workbook written by POI has none until it is evaluated). |
The POI site announces that 6.0 will make DataFormatter use cached formula values by default. The 6.0 release has
not shipped (as of 2026-10-09) and its final behaviour must be checked against its release notes, but the practical
advice is the same in both lines: set the option explicitly, so your import behaves the same on 5.5.x and on 6.0.
Choose the cached value for uploads generated by Excel and the evaluator when the workbook was produced by a library
that does not write cached results. Evaluation, its limits and its cache are explained in
Formulas & Evaluation.
Format codes and the text they produce
The table shows what DataFormatter (Locale.US, POI 5.5.1) returns for a cell with each format code and value. It was
produced by running the formatter on a real workbook, not copied from documentation. A code with several sections
separated by semicolons applies the first section to positives, the second to negatives, the third to zero and the
fourth to text; a bracket such as [Red] selects a colour that DataFormatter ignores (it returns text, not colours).
| Format code | Cell value | Displayed value | Notes |
|---|---|---|---|
|
|
|
No grouping, up to 15 digits, no trailing zeros. |
|
|
|
Long integers fall into scientific notation: never store an ISBN-13 as a number. |
|
|
|
Rounds half away from zero, like Excel. |
|
|
|
|
|
|
|
Thousands separator of the locale. |
|
|
|
|
|
|
|
The value is multiplied by 100. |
|
|
|
|
|
|
|
Scientific. |
|
|
|
A quoted literal is a currency symbol; there is no Java currency conversion. |
|
|
|
Negative section; the red colour is lost. |
|
|
|
Zero padding for identifiers. |
|
|
|
Fraction. |
|
text |
|
Text format; the cell is a string cell. |
|
|
|
ISO date. |
|
|
|
|
|
|
|
Month name depends on the locale. |
|
|
|
|
|
|
|
Day of the week. |
|
|
|
|
|
|
|
12-hour clock. |
|
|
|
Elapsed time can exceed 24 hours. |
|
|
|
A time of day. |
built-in id 14 ( |
|
|
Fixed US-style pattern. |
built-in id 22 ( |
|
|
Two reminders when you write these formats rather than read them: create the code with
DataFormat.getFormat(String) and reuse the style, and remember that the code is stored in the file, so a decimal
separator in the code (0.00) is always a dot and the locale only decides how it is rendered.
Source: POI Javadoc, DataFormatter
Reading numbers: text, precision and BigDecimal
Numbers stored as text
A catalogue sheet usually contains ISBN, shelf code and price columns that users type by hand, so a number may arrive as a
string cell (0012345), as a numeric cell (12345) or as a formula result. A reliable rule is to read what the user
sees and then decide:
-
For identifiers (ISBN, inventory code, postal code) use
DataFormatter.formatCellValue. A string cell comes back unchanged, with its leading zeros; a numeric cell with format000000comes back padded. Do not usegetNumericCellValue()for them. -
For quantities and prices, branch on the cell type: use
getNumericCellValue()for numeric cells and parse the trimmed text for string cells, normalising the decimal separator according to the locale you expect. POI offers no locale-aware parser for text;java.text.NumberFormat.getInstance(Locale)orBigDecimaldo that work. -
Reject (and report with the row number) a cell whose text cannot be parsed. Never silently turn it into zero.
An ISBN-13 stored as the number 9780306406157 also shows the limit of General: it renders as 9.78031E+12, and
because a double has about 15 reliable digits, a longer number such as a 16-digit card or order number is already damaged
in the file. Write such identifiers as strings and apply the @ format so Excel keeps them as text.
Precision and BigDecimal
A double cannot represent most decimal fractions exactly: 0.1 + 0.2 is 0.30000000000000004, and a Java
double read from a cell has the same property. DataFormatter hides this because it rounds to the cell’s format
(0.3), getNumericCellValue() shows it. Rules for money and exact quantities:
-
Read with
BigDecimal.valueOf(double), which uses the shortest decimal text of the double (0.1stays0.1), and nevernew BigDecimal(double), which exposes the binary expansion. -
Round explicitly with
setScale(2, RoundingMode.HALF_UP)at the point where the business rule says so. Do not rely on the number format to round: it only changes the display, not the stored value. -
When you write,
cell.setCellValue(bigDecimal.doubleValue())is the only option (a cell is a double); keep the exact amount in the database and treat the workbook as a presentation. -
Totals calculated with a formula (
SUM) use doubles in Excel as well, so a sheet total and aBigDecimaltotal can differ in the last digit.
The next example reads the first two data rows of the catalogue, shows the identifier as the user sees it, formats the
price with a German locale and reads the exact value, whether the price is a typed number or a formula. It uses the
WorkbookFactory.create(File, String, boolean) overload with readOnly true so the source file is never modified
(see Spreadsheet Usermodel Basics for opening workbooks).
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Path;
import java.util.Locale;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.DataFormatter;
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 ReadBooks {
public static void main(String[] args) throws IOException {
// The user's locale decides the decimal separator and the month names, not the server's default.
DataFormatter formatter = new DataFormatter(Locale.GERMANY);
try (Workbook workbook = WorkbookFactory.create(Path.of("books-import.xlsx").toFile(), null, true)) {
FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
Sheet sheet = workbook.getSheetAt(0);
for (Row row : sheet) {
Cell isbnCell = row.getCell(0);
Cell priceCell = row.getCell(3);
if (isbnCell == null || priceCell == null) {
continue;
}
// 1. The text exactly as Excel shows it: leading zeros, "9.78E+12" and all.
String shown = formatter.formatCellValue(isbnCell);
// 2. A formula cell: with the evaluator the result is formatted, without it the formula text is returned.
String price = formatter.formatCellValue(priceCell, evaluator);
// 3. The exact numeric value, when the cell really holds a number.
BigDecimal exact = null;
if (priceCell.getCellType() == CellType.NUMERIC
|| priceCell.getCellType() == CellType.FORMULA
&& evaluator.evaluateFormulaCell(priceCell) == CellType.NUMERIC) {
exact = BigDecimal.valueOf(priceCell.getNumericCellValue());
}
System.out.println(shown + " | " + price + " | " + exact);
}
}
}
}
With a first row holding the text 0012345 and the formula 19.9*2, and a second row holding the number 9780306406157
and the value 1234.5, the program prints (German locale, price format #,##0.00):
0012345 | 39,80 | 39.8
9,78031E+12 | 1.234,50 | 1234.5
Source: POI Quick Guide, Cell contents and Formula evaluation
Memory, performance and threading
-
Date conversions and
DataFormatterare cheap compared with parsing the file, but a million-cell sheet calls them a million times. Reuse oneDataFormatterand oneFormulaEvaluatorper workbook and keep the default thread-local cache ofDateUtilenabled. -
formatCellValue(cell, evaluator)evaluates every formula it meets; on a big workbook prefer the cached value option, or evaluate only the columns you need. The event API (see Streaming Reads: Event & SAX APIs) formats cells with the sameDataFormatter, so the semantics on this page apply there too. -
Streaming writes keep the style limits of the file, so create the date, number and currency styles once, before the loop, even with SXSSF (see Streaming Writes with SXSSF).
-
DataFormatterinstances andFormulaEvaluatorinstances are not thread safe. One document, one evaluator, one formatter per thread.
Pitfalls
| Symptom | Cause and fix |
|---|---|
Dates show as |
The cell has no date format. Set a style with a date format on every date cell. |
Dates are off by four years and one day |
The workbook uses the 1904 system and a |
A date shifts by a day or several hours |
Conversion through |
|
The user typed text. Check |
|
No evaluator and cached values are off. Pass the evaluator, or call |
The same file imports differently on the laptop and in production |
|
|
The identifier was stored as a number. Write it as a string with the |
Money differs in the last cent |
Double arithmetic and |
A text date such as |
Text dates are ambiguous. Accept real date cells and ISO text ( |
What POI does not provide
-
No conversion of an Excel format code to a
java.time.format.DateTimeFormatterpattern, and no parser for text dates or locale-formatted numbers: the formatter goes from a value to text only. -
No time zone or offset in a cell. Excel has no concept for it.
-
No automatic detection of "this numeric column is really a date" when the style is
General. -
No fixed-point or decimal cell: every number is a double.
-
DataFormatterreturns text only; it drops the colours of[Red]-style sections (the colour matters for display tools and is not exposed by this class).
Changes between 4.x, 5.x and 6.0
| Line | Relevant changes |
|---|---|
3.x and earlier |
|
4.x |
The integer cell types were removed in 4.0 in favour of the |
5.x |
The |
6.0 (announced, not released) |
Per the POI site: |
References
-
Spreadsheet Quick Guide, date cells and cell contents: https://poi.apache.org/components/spreadsheet/quick-guide.html
-
Formula evaluation: https://poi.apache.org/components/spreadsheet/eval.html
-
Spreadsheet limitations: https://poi.apache.org/components/spreadsheet/limitations.html
-
DataFormatter Javadoc: https://poi.apache.org/apidocs/dev/org/apache/poi/ss/usermodel/DataFormatter.html
-
DateUtil Javadoc: https://poi.apache.org/apidocs/dev/org/apache/poi/ss/usermodel/DateUtil.html
-
Cell Javadoc: https://poi.apache.org/apidocs/dev/org/apache/poi/ss/usermodel/Cell.html
-
Apache POI versioning policy: https://poi.apache.org/versioning.html
-
Changes by release: https://poi.apache.org/changes.html
-
Official examples: https://github.com/apache/poi/tree/trunk/poi-examples/src/main/java/org/apache/poi/examples/
-
Excel specifications and limits: https://support.microsoft.com/en-us/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3
-
ECMA-376 (Office Open XML): https://ecma-international.org/publications-and-standards/standards/ecma-376/