The format support table

When the same workbook is written to .xlsx, .ods and .numbers, what goes through unchanged, what changes shape, and what does not go through at all. Measured by SwiftSheets (the version is the one in the title; the numbers are the latest measurement on main).

As of 2026-08-31 / verified with: macOS 15, LibreOffice 26.2.3, numbers-parser 4.16.3, the OASIS ODF 1.3 schema, Numbers 15.3.1


How this table was made

A table that says "supported" is soon a lie. So this one was built by measurement, not guesswork. One workbook carries all 48 features the model can hold; it is written to each format, read back, and compared with the original. ○ is what came home, × what did not, △ what came home in another shape.

That measurement is itself a test in the repository (FormatSupportTests). It pins which of the 48 features survive and how many warnings each format returns, so if a codec changes and this table goes stale, the test goes red first.

41 / 48
Excel (.xlsx / .xlsm)
Every spreadsheet mechanism goes through. The 7 that do not are ideas that exist only in Numbers or OpenDocument.
43 / 48
ODS (OpenDocument)
One more than Excel. It drops 6 and picks up 7 — different ones, not better or worse.
23 / 48
Numbers
Values, formulas, stock functions, styles, conditional formatting, data validation, cell controls, pivot tables, several tables, pictures and shapes, a table's position, page orientation and headers, paper, title rows. Print areas and protection cannot be written (the reasons are below).

At a glance

The three formats are not dialects of one thing. Excel and ODS draw the same map in different words; Numbers draws a different map. The figure below shows the relationship.

The SwiftSheets model one format-neutral representation (48 features) Excel 41 / 48 values, formulas, styles, conditional formats validation, tables, filters, pivots protection, scenarios, print setup, names not carried: several tables, cell controls, the 4 ODF-only items (below) ODS 43 / 48 2 more than Excel (different ones) conditional formats: extension namespace pivot table = data pilot not carried: protected ranges, scenarios, cell controls, several tables Numbers 23 / 48 values, formulas, cell styles, number formats links, notes, merges, several tables conditional formats, pivots, validation, controls not carried: colour scales, data bars, print areas, defined names, filters …
The blue bar is what goes through unchanged. Numbers' bar is shorter not because it can do less, but because its format has no public specification.

The table

○ = round-trips unchanged / △ = goes through in another shape (read the note on the right) / × = does not go through. A × is not dropped in silence: every write returns it as a warning (see below).

FeatureExcelODSNumbersNote
Content
Values (number, text, date, boolean, duration, error)Numbers' decimals are decimal128; no digits are lost
Formulas ODS gets a translation to OpenFormula (of:=[.A1]). Numbers gets the formula itself (the expression tree becomes a reverse-Polish node list). Only shapes with no example in the local fixtures — references to another table, defined names, functions Numbers lacks, column ranges like A:C, the intersection and union operators — fall back to the cached value, and a warning says which and why
 └ Stock and currency functions (STOCK, CURRENCY and four others) Numbers-only functions that fetch a quote. To Numbers they are written as formulas and refreshed when the document opens. Excel and ODS have no function of that name, so the value that had been fetched is written, and the warning says so — the same substitution Numbers itself makes when it exports to Excel (measured; Appendix B.27)
Array formulas (with their range)× ODS uses table:number-matrix-*-spanned on the anchor cell. Numbers is read only: the expansion Numbers itself builds when it imports an Excel file (the formula on the anchor plus an internal function on every covered cell) is read back as a range. It is not written because that internal function produces no value on recalculation — even a Numbers-made specimen, disguised as an older version so that Numbers recalculates it, loses its values (measured; Appendix B.26)
Formatting within a cell (rich text) ODS uses text:span. Numbers holds it as the points where the character style changes
Hyperlinks A Numbers link is attached to a range of characters, so one cell can carry several (reading takes the first and warns)
Notes (cell comments) ODS uses office:annotation. Numbers keeps them in the table's comment list, with one shared "author" record per person who wrote one
Merged cells
Appearance
Cell style (font, bold, colour, borders) Numbers holds fonts by PostScript name, so they are converted both ways (Helvetica NeueHelveticaNeue)
Fill (solid, gradient) Neither ODF nor Numbers can hold more than one colour per cell, so a gradient becomes its first colour (with a warning)
Alignment and wrapping
Number format (#,##0.00 and the like) ODF has "data styles", Numbers a description (kind + decimal places + thousands separator). Neither can hold more than one way of showing a value, so Excel's multiple sections, colours and conditions are lost
Column width, row height, hidden rows and columns
Row and column grouping (outline)× ODF expresses the levels as nested table:table-row-group elements
Frozen panesIn Numbers the header rows and header columns play that part
Tab colour×ODF 1.3's table:tab-color, the same attribute LibreOffice writes; a theme colour is resolved to RGB before writing
Conditional formatting (the first priority of this revision)
Comparison, formula, text, top/bottom, above/below average, duplicate/unique, blank/error, time period ODS carries all 18 kinds. Numbers carries 14 — 8 comparisons (> ≧ < ≦ = ≠ between, not between), 4 text rules (contains, does not contain, begins with, ends with), duplicate and unique. The rest are the ones Numbers itself discards when it reads an Excel file, so they are discarded here too, with a warning
Colour scale (2- and 3-colour)×
Data bar×ODF has no "hide the value"
Icon set×
Mechanisms
Data validation (drop-down lists, range checks) ODF lists the rules at the head of the document and cells point at them by name. In Numbers only a list rule with its choices spelled out becomes a pop-up menu (Numbers itself makes the same substitution when it imports Excel). A list that references a range, and numeric conditions, are dropped with a warning
Cell controls (checkbox, stepper, slider, rating)×× Numbers-only. Read and written together with the control's range (minimum, maximum, step). Excel and ODS keep only the value, and the warning says what was dropped
Named tables× Becomes an ODF "database range". The name and the range go through; the banded style is lost
AutoFilter (the range)× Numbers itself discards Excel's filters on import (measured: neither the filter rules nor the hidden rows survive). There is no specimen to copy (Appendix B.26). The other way round — reading a Numbers-native filter — the hidden rows stay hidden and the rules are dropped with a warning: the same deal Numbers itself makes when it exports to Excel (Appendix B.29)
 └ Filter criteria and the recorded sort× Value lists and comparison criteria go through. Colour, icon, dynamic and date-group filters do not exist in ODF and are dropped with a warning. When a Numbers document is read, a sort arrives as rows already in sorted order (the warning names the columns of the rule), and a filter as rows that stay hidden (the warning counts the rules) — both the way Numbers itself hands them over when it exports to Excel (Appendix B.29)
Pivot tables Excel and ODS get the layout only; the application that opens the file computes the figures. Numbers gets a genuine Numbers pivot table (a live summary that Numbers recounts from the source rows). Several fields are allowed on both axes — with a subtotal row per group; measured to three levels of nesting (Appendix B.28). Only the summarised values are limited to one: the second and later ones are dropped with a warning, the first kept (measured: the value lanes of a rebuilt pivot table are drawn with a single shared marker, so the distinction cannot survive in principle). Read back, it is an ordinary table
Sheet protection×
Protected ranges (editable windows in a protected sheet)×× ODF protects the whole table and has no windows inside it
Scenarios×× An ODF scenario is a "shadow sheet", not a set of input values; writing one would add a sheet
Printing and the workbook
Header/footer Excel's &L&C&R, ODF's three regions and Numbers' three left/centre/right stores are converted both ways. Font directives are lost, and Numbers also drops every code other than &P (date, file name and so on), naming each
Orientation, paper, scale, margins, centeringODF has a master page per sheet. Numbers holds orientation, scale, margins and the first page number; the paper size is dropped and named
Print area, title rows/columnsNumbers carries title rows/columns that start at row 1 as "repeat the headers on every page"; the print area is dropped and named
Page breaks×ODF uses fo:break-before on the row and column styles
Defined names (workbook and sheet)×
Workbook protection (locking the sheet structure)×ODF uses table:structure-protected (the flag only; the key is not compatible with Excel's, so it is not carried)
Custom document properties (department, case number and the like)× ODF uses meta:user-defined. Only the link-to-a-defined-name type is flattened to text
Hidden sheets×Numbers has no idea of a hidden sheet
Several tables on one sheet×× The one place Numbers is stronger. Excel and ODS have one grid per sheet, so every table but the first is dropped with a warning
The other way round: what ODF has and Excel lacks
Label ranges (a heading used as is in a formula)×× The "売上" in =SUM(売上) is a column heading, not a defined name. Excel 2003 had this as "natural-language formulas", but today's Excel format has nowhere to put it
Consolidation definitions×× "Sum this range and this range into here", kept in the document. Excel's Consolidate is a one-off operation and the definition is not saved
Detective arrows (tracing precedents and dependents)×× Excel draws the same arrows, but they vanish when the file is closed. ODF keeps them in the file, so the audit trail arrives intact
Calculation settings (regular expressions, wildcards, case sensitivity, two-digit years)×× Settings that change the meaning of a search criterion. In Excel they are application settings and never enter the file
Date epoch Excel offers 1900 or 1904; ODF allows any date. Any other epoch is read as the 1900 family, with a warning saying so
Currency cell type ODF holds "which currency" on the cell as data. Excel and Numbers have it only inside the number format, so the symbol survives but the distinction "this is an amount of money" does not

Where the conditional formatting is written

Conditional formatting going through ODS is the biggest change in this revision. But the ODF 1.3 core specification has no proper place for it. All the core has is "attach one condition to a cell style" (style:map); there are no words for colour scales, data bars or icon sets. What real applications do is, all of them, use LibreOffice's extension namespace calcext:.

content.xml <table:table> … rows and columns … the cell contents live here <calcext:conditional-formats> the target range (CF.A1:CF.A20) the order of the conditions = the priority colour scales, data bars, icons = the place SwiftSheets treats as authoritative styles.xml <style:style name="ConditionalStyle1"> background #FFC7CE / text #9C0006 the style painted when a condition matches <style:page-layout> <style:master-page> margins, orientation, header/footer one pair per sheet referenced by name
The conditions go in content.xml; the style they paint goes in a named style in styles.xml. That split is ODF's rule.

Decision: the extension is authoritative; the core form is read only

  • Only calcext: is written. LibreOffice writes both, but writing both duplicates every rule.
  • Both are read. Only a file with no extension at all (written by a generator older than the extension) falls back to the core style:map.
  • Priority is document order. ODF has no priority attribute, so the rules are sorted by priority when written.

This spelling was not written from memory. SwiftSheets itself produced an xlsx "with all 18 kinds of rule", LibreOffice 26.2 converted it to ODS, and the XML that came out was used as the specification, as is. The reverse was checked too — when LibreOffice converts the ODS this library writes back to xlsx, all 24 rules are restored.

Why Numbers alone is so different

Numbers support is thin not because the features were never built. The Numbers file format has no public specification. Inside is a Snappy-compressed Protobuf object graph, and SwiftSheets uses a blueprint of it taken from numbers-parser (MIT), extracted by machine (not one line was copied by hand).

A blueprint is not enough on its own. What is missing is a judge. For Excel, openpyxl answers "is that a valid file"; for ODS, LibreOffice does. For Numbers there was for a long time only numbers-parser and LibreOffice's import, and Numbers.app itself was not on the development machine. It arrived on 2026-08-25 (Numbers 15.3.1).

What the fourth judge did first

The three judges until then (this library's own reader round-tripping, numbers-parser, LibreOffice) all said "readable". Numbers.app refused the same file: "can't be opened because it is damaged". One sheet with one table opened; add a second sheet or a second table and it did not.

The cause was how the package index was written for the duplicated parts. Numbers trusts the package index and goes looking for files; the other three look objects up by identifier, so they read the file happily even when the index is wrong. That is why all three missed it (the details and the fix are in the implementation spec, Appendix B.18). There is no way to find this other than having the target application open the file.

The criterion is always "can it be checked"

Features aimed at Numbers are treated differently from one another. The criterion is one — is there a real example, and can a judge check it. The two that were held back until Numbers.app arrived went in together once the judge was there.

FeatureWent in?Was there a way to check
Cell styles and number formatsWent in numbers-parser agrees on every cell of 11 fixtures (bold, italic, font, size, text colour, alignment). numbers-parser reads the written files without a warning, and LibreOffice opens them
Hyperlinks, formatting within a cell, notesWent in Numbers 15.3.1 was given an Excel file to read and save, and the placement was observed field by field. The cell's text moves into a "document engine container"; a link is a smart field on top of it, a formatting change a character style. Notes are a separate list, sharing one record per author
Writing formulasWent in The formula archives of the fixtures were read field by field, and the writer built from that. numbers-parser reads the SUM(B2:B3) in a written file as a formula (Appendix B.18)
Conditional formattingWent in Below. The values of the nameless integer predicate_type were observed in documents Numbers itself was made to write
Formulas referencing another tableWent in The reference is neither the table's UUID nor the base owner in the dependency records but the table's table_id string. Write either of the first two and Numbers reads #REF! — the kind of thing only the real application can tell you
Replacing the templateHeld back Swapping in an empty document made with Numbers 15 stops formulas from being calculated (measured). The current template was made by an older version, and Numbers recalculates the whole document when it opens it. A newer template can go in once the dependency records can be written by this library

The nameless integer, filled in by observation rather than guessing

It was in the blueprint (TST.ConditionalStyleSetArchive, list kind 9, cell-record flag 0x80). The one reason it could not be written: the integer that stands for the condition has no name. Apple holds the kinds of condition in a C enum, and all Protobuf keeps is a nameless int32 called predicate_type. Write a guessed integer and, at best, Numbers offers to repair the file; at worst you get a document that silently paints the wrong cells.

With Numbers.app available, Numbers itself was made to write it, and the result was read. An Excel file was built with one kind of rule per column, each rule given a value no other rule shares (11, 12, 13–14, 15–16, 17, 18, 19, 20, "pp", "qq", "rr", "ss"). Numbers read it and saved it as .numbers, and the rules that came out were matched back to their columns by value. Nothing relies on the order, so the conclusion holds even if the rules are reordered.

Build an Excel file one rule per column every value distinct Have Numbers read it and save it as is 14 of the 25 kinds survived Match back to the columns by value "the rule carrying 11" = greater than → predicate_type = 7 The 14 that survived: 8 comparisons, 4 text rules, duplicate, unique. The 11 that did not (colour scale, data bar, icons, top/bottom, average, blank, error, period, formula) are Numbers saying "I have no such rule", so this library does not write them either, and warns.
Numbers opens the written file, and the rules survive a re-save (proof that Numbers understood the conditions).

The specimen itself (the document Numbers 15.3.1 wrote) is at Tests/SwiftSheetsTests/Fixtures/numbers/conditional-formats-15.numbers, and the mapping is pinned by a test.

The other way round — what ODF has and Excel lacks

So far the question was "how far does an Excel feature go through the other formats". There is a reverse. OpenDocument has 6 things the Excel format does not, and all of them are read and written.

The clearest is the label range. If a table heading reads "売上", you can write =SUM(売上) without creating a defined name. Excel could do the same up to 2003, but today's Excel format has nowhere to write it. Consolidation definitions and detective arrows are of the same character: what is only an "operation" in Excel, ODF keeps as "document content".

The source is the official OASIS OpenDocument 1.3 schema. For conditional formatting there was no specification and the only way was to have the real application write it; here there is a formal specification, so every element and attribute name is taken from it. On top of that, the written ODS was re-saved by LibreOffice, and all 6 come back out intact.

What happens when written to Excel

They are lost, naturally. But not in silence — though the warning comes not from the Excel writer itself but from the shared layer in front of it. The Excel reader and writer were not to be changed at all in this revision, so they were left alone. Go through workbook.write(to:as:) and the warnings come back.

What is dropped is always reported

A × in this table does not mean "nothing happens". The write returns every item it gave up, one by one. The number of warnings the kitchen-sink workbook above returns when written to each format is Excel 7, ODS 8, Numbers 24.

let result = try workbook.write(to: url, as: .numbers)
for warning in result.warnings {
    print(warning.kind, warning.sheet ?? "-", warning.message)
}
// dropped  Data  conditional format(s) dropped: Numbers rules are not written
// dropped  Data  the print setup (headers, page breaks, print area, orientation) is dropped: …
// degraded Data  formula written as its cached value (Numbers formula archives are not generated)
dropped

The item was not written. The format has no words for it, or it is not implemented yet.

degraded

Written in another shape. The meaning survives, but it does not come back as it was (formula → cached value, and so on).

substituted

Replaced with something close. Gradient fill → one colour; a number format's several sections → one.

On top of that, WriteResult.suggestion proposes "another format would lose less with this content". Try to write a workbook with colour-scale conditional formats and data bars to Numbers, and it recommends xlsx or ods.

Choosing a format

Excel (.xlsx)

When in doubt, this. Every feature reads and writes, and whatever a read file holds that was not interpreted is preserved byte for byte and written back (charts, shapes, VBA, themes).

ODS

When the other party is LibreOffice, or an open format is required for an official submission. The difference from Excel is 5 items, none of them often used.

Numbers

When handing a table for people to read on a Mac or iPad. Values and appearance go through. If the formulas and mechanisms must survive, hand over an xlsx, not a Numbers file.

In one line: mechanisms and all, xlsx or ods; to show people what it looks like, numbers.

How much of Excel survives in Numbers (measured, 2026-08-26)

The question the table claims to answer was answered by machine. One workbook with everything this table measures was prepared, and ① what SwiftSheets writes as .numbers and ② the same workbook as .xlsx, read and saved by Numbers itself, were compared cell by cell.

ResultDetail
Cells whose value or formula disagreed: 0 (of 67 cells) This library's export and Numbers' own import agree on the contents of every cell
What Numbers itself kept and this library did not: nothing any more (once only the pivot table, 14 cells) Since 2026-08-27 this library writes a genuine Numbers pivot table too (one field per axis, one value)
What this library kept and Numbers itself did not: 2 Merged cells (Numbers' own import loses them) and a second table on one sheet
The expansion of array formulas Numbers scatters an internal formula over every cell an array formula covers. This library writes the anchor's formula and each cell's value — the scattered formulas were measured to produce no value on recalculation (Appendix B.26). Since 2026-08-27 reading brings the range back whole
Formulas without a cached value: 2 No value is stored in this library's document, but Numbers computes it when it opens the file (checked on the real application)

Comparing "item counts" in the listings means nothing — Numbers holds one item per cell and this library folds the same thing into one, so the same document shows 13 styles against 2. The sweep compares cell contents; the item counts are listed for reference only.

Measuring this table again yourself

To check that the numbers have not gone stale, run the tests in the repository.

swift test                          # everything: the round trip per format, and the checks with LibreOffice as the judge
swift test --filter FormatSupport   # this table itself: pins which of the 48 survive, the warning counts, and the ○ and × on this page
swift test --filter CrossFormat     # the per-direction checks (the basis of the interoperability guide) and the three-generation test
swift test --filter ODFOnly         # the 6 things ODF has and Excel lacks
swift test --filter ODS             # ODS conditional formats, validation, print setup, pivot tables
swift test --filter Numbers         # Numbers cell styles and formulas (against numbers-parser's expectations)

python3 Tests/NumbersParity/verify_with_numbers_app.py   # has Numbers.app itself open, calculate and re-save
python3 Tests/NumbersParity/compare_excel_import_with_numbers.py   # compares the same workbook as "this library's export" and "Numbers' own import"

The implementation decisions and their reasons are in docs/implementation-spec.html, Appendix B.16 (Excel → ODS / Numbers) and B.17 (ODF → Excel); what a person checks by hand before a release is in MAINTENANCE.md.