The spec feature matrix
The features that the public specifications of Excel, OpenDocument and Numbers each have, set out in 196 rows: whether SwiftSheets reads each one, whether it writes it, and which API reaches it. Where the sibling format support table measures "the model's 48 features → 3 formats", this table looks the other way round: "a format's specification → SwiftSheets".
Which way this table reads
The rows are the features each format's public specification has; across them run SwiftSheets' read and write support and the API. The axes are the reverse of the format support table (the model's 48 features × 3 formats), which provides the measured evidence behind this table.
One row is one feature as a user sees it (not one row per XML element). The table does not cover every element of a specification; it lists the units a user can name, following the specification's chapters and part structure.
Public specification
Public specification
No public specification — built from the Help and observation
Six symbols — why this is not a choice between "supported" and "unsupported"
Two things can both be "unsupported" and still mean quite different things to the user: ① something the model merely has no words for, which survives when the file is saved (▣), and ② something that really disappears (×). Read and write are kept apart too — because there really are rows such as Numbers array formulas, which are read but not written, and filters, of which only the hidden rows are read.
| Symbol | Meaning |
|---|---|
| ○ | Round-trips unchanged. Read in the model's own terms, and the same thing is written out |
| △ | Goes through in another shape. A degraded / substituted warning is always returned |
| ▣ | Not modelled. Saving to the same format keeps the original bytes (F3). Converting to another format drops it, with a warning |
| × | Does not go through. Dropped, and a dropped warning is returned |
| — | Not applicable: the format has no such concept |
| ? | Not measured. The slot that keeps ○ or × from being written by guesswork |
A row described as dropped never vanishes in silence: it always comes back as a warning. FormatSupportTests pins the fact that warnings come back, by their count per format (xlsx 7 / ods 8 / numbers 24).
The evidence for each row
A table that says "supported" is soon a lie. So each row ends with a mark saying where its verdict came from.
| Mark | Meaning |
|---|---|
| measured | Measured by FormatSupportTests over the 48 features (written, read back and compared) |
| code check | Checked in the codec code (that the element names, the warning text and the preservation-list entry really exist) |
| document | Stated in README / the implementation spec, Appendix B |
| judged by the application | An external implementation could read it (openpyxl, LibreOffice, numbers-parser, Numbers.app, Excel) |
The entry points every row shares
Read: Workbook(contentsOf:) / Workbook.read(contentsOf:) -> ReadResult / wb.readWarnings
Write: wb.write(to:as:) -> WriteResult / wb.write(as:).data / Workbook.convert(_:to:as:)
Detect: SheetFormat.detect(_:)
Receiving what was dropped: ConversionWarning(kind: .dropped/.degraded/.substituted, subject:, sheet:, location:, message:)
Counts
| Format | Direction | ○ | △ | ▣ | × | — | ? | Rows |
|---|---|---|---|---|---|---|---|---|
| Excel(.xlsx / .xlsm) | Read | 53 | 10 | 11 | 1 | 3 | 0 | 78 |
| Excel(.xlsx / .xlsm) | Write | 53 | 9 | 12 | 1 | 3 | 0 | 78 |
| OpenDocument spreadsheet (.ods) | Read | 39 | 13 | 0 | 2 | 5 | 0 | 59 |
| OpenDocument spreadsheet (.ods) | Write | 36 | 10 | 0 | 11 | 2 | 0 | 59 |
| Apple Numbers(.numbers) | Read | 22 | 15 | 0 | 7 | 15 | 0 | 59 |
| Apple Numbers(.numbers) | Write | 20 | 11 | 0 | 24 | 4 | 0 | 59 |
Excel(.xlsx / .xlsm)
Specification: ECMA-376 / ISO/IEC 29500 — Office Open XML SpreadsheetML
Public specification. The element names are the specification's own. §7 of the SwiftSheets implementation spec holds the mapping.
| Feature | Read | Write | Where in the spec | API | Note (what is reported when it is dropped; evidence) |
|---|---|---|---|---|---|
| Package and parts | |||||
| OPC package (ZIP, content types, relationships) | ○ | ○ | [Content_Types].xml, _rels/.rels | Workbook(contentsOf:) wb.write(to:) | On write, [Content_Types].xml declares only the parts that exist, and every r:id resolves (PreservationTests). measured |
| Preservation of uninterpreted parts (F3) | ▣ | ▣ | xl/charts/, xl/theme/, xl/drawings/, vbaProject.bin and others | wb.preservationSummary PreservationSummary preservationSummary.parts PreservedPartKind | Warning: Converting to another format: "N part(s) … cannot be carried into …" Untouched parts are identical byte for byte. Existing r:id, sheetId and paths never change; new ones are numbered on from the highest. The raw parts, XML and relationships stay at package access; the public entry points are a summary (source format, part count, whether there is VBA) and parts, an inventory by kind of what the model did not take in (SmartArt, slicers, external links, embeddings …; drawings, charts, themes and notes that were read are not counted) (Appendix B.46, B.77). measured |
| ZIP64 (over 4 GB, over 65,535 parts) | ○ | ○ | ZIP64 end of central directory | ZipLimits ReadOptions.limits | Read and written since 0.12.0 (Appendix B.39.1): parts over 4 GB and more than 65,535 parts. ZIP64 records are written only into the entries that need them. ZipTests pins the round trip. This row still said "none" after 0.12.0; Rev 4.22 corrected it. measured |
| Password encryption | △ | ○ | ECMA-376 Part 2 agile / standard encryption([MS-OFFCRYPTO]) | SheetDecrypt.decrypt(_:password:) Workbook(contentsOf:password:) SheetEncrypt.encrypt(_:as:password:) Workbook.write(to:password:) SheetError.wrongPassword UnopenableInput.encryptedOOXML | Warning: The core alone throws (naming the file as encrypted and saying SheetDecrypt opens it). A wrong password is wrongPassword. Excel 2007's standard encryption and RC4 are refused by name Read when SheetDecrypt is linked, written when SheetEncrypt is (0.17.2, Rev 4.29). The core and the 5 products contain no cipher code (scripts/check-no-crypto.sh reads the symbol tables in CI). Agile encryption (AES-256 / SHA-512, the form Excel 2010 and later write) is read and written. The judge is msoffcrypto-tool (Appendix B.39.9). An old .xls (BIFF) is reported separately, as UnopenableInput.legacyCompoundFile. judged by the application |
| Workbook (xl/workbook.xml) | |||||
| Sheet list, order, names, duplication | ○ | ○ | <sheets>/<sheet> | wb.sheets wb.sheetNames addSheet(named:at:) removeSheet(named:) moveSheet(named:to:) duplicateSheet(named:as:) renameSheet(_:to:) | Renaming a sheet updates the formulas that refer to it. measured |
| Sheet visibility (visible, hidden, very hidden) | ○ | ○ | sheet@state | sheet.isHidden sheet.state SheetState | measured |
| Defined names (workbook scope, sheet scope) | ○ | ○ | <definedNames>/<definedName localSheetId> | wb.definedNames sheet.definedNames | Print areas and title rows sit in the same place, as _xlnm.Print_Area / _xlnm.Print_Titles. measured |
| Date epoch (1900 / 1904) | ○ | ○ | workbookPr@date1904 | wb.epoch DateEpoch ExcelDate | The phantom day of 29 February 1900 is handled the same way as in openpyxl. measured |
| Calculation properties and calculation chain | △ | △ | <calcPr calcMode iterate fullPrecision>, xl/calcChain.xml | wb.calculationSettings.calcMode CalculationSettings.CalcMode | calcMode (auto / autoNoTable / manual), iterative calculation and precision as displayed are read and written; the remaining attributes are preserved when written back to the same format. calcChain.xml is always discarded and fullCalcOnLoad is set, because having the opening application recalculate is more reliable (B.76). measured |
| Workbook protection (sheet structure, windows) | ○ | ○ | <workbookProtection> | wb.protection WorkbookProtection setModernPassword(_:) setPassword(_:) | The modern scheme generates a SHA-512 hash. Confirmed over AppleScript that Excel 16.112.2 itself unlocks it (Appendix B.31). judged by the application |
| Workbook view (the active sheet) | ○ | ○ | <bookViews>/<workbookView activeTab> | wb.activeSheet wb.activeIndex | code check |
| References to external workbooks | △ | ▣ | <externalReferences>, xl/externalLinks/externalLinkN.xml, externalLinkPath | wb.externalLinks ExternalLink FormulaExpr ([1]Sheet!A1 is kept as text) | Warning: Converting to another format: a dropped warning (objects) wb.externalLinks answers the [1] index, the target file and the recorded sheet names (B.78). Values in the target are not resolved. Inside formulas the reference is carried as a string, so it survives within the same format. measured |
| Document properties (author, title, dates) | ○ | ○ | docProps/core.xml, docProps/app.xml | wb.metadata DocumentProperties wb.sourceInfo | The name of the application that created the file is kept in sourceInfo.application. code check |
| Custom document properties | ○ | ○ | docProps/custom.xml | wb.customProperties CustomDocumentProperty | 6 types: text, integer, real, boolean, date, and link to a defined name. measured |
| File sharing, custom workbook views, smart tags, web publishing | ▣ | ▣ | <fileSharing>, <customWorkbookViews>, <smartTagPr>, <webPublishing> | — | Warning: Converting to another format: dropped Unknown child elements of <workbook> are written back at their place in schema order (WorkbookWriter.workbookOrder). code check |
| Worksheet (xl/worksheets/sheetN.xml) | |||||
| Cell values (number, text, boolean, error, inline string) | ○ | ○ | <c t="n|s|str|b|e|inlineStr|d"> | sheet["A1"] sheet[1, 2] CellValue | Integers and reals are split the same way as in openpyxl (.integer / .number(Decimal)). Dates are CivilDate, with no time zone. measured |
| Shared strings | ○ | ○ | xl/sharedStrings.xml | — | Strings are shared automatically on write. There is no API the user needs to think about. code check |
| Formatting within a cell (rich text) | ○ | ○ | <is>/<r>/<rPr> | CellValue.richText TextRun | measured |
| Furigana (ruby) | ○ | ○ | <rPh>, <phoneticPr> | sheet[cell: "A1"].phonetic PhoneticText | Warning: Writing to ODS or Numbers: "N phonetic guide(s) (furigana) dropped: … the text is kept" <rPh> (the reading and its UTF-16 range) and <phoneticPr> (font, conversion type, alignment) in shared strings are read into and written from Cell.phonetic. The same text with and without a reading makes two separate entries. openpyxl skips them; this library carries them (B.70). measured |
| Row height, hidden, outline level | ○ | ○ | <row ht customHeight hidden outlineLevel collapsed> | sheet.rowDimension(_:) setHeight(_:ofRow:) RowDimension | measured |
| Column width, hidden, outline level, autofit | ○ | ○ | <cols>/<col width bestFit hidden outlineLevel> | setWidth(_:ofColumn:) columnDimension(_:) autofitColumns(maxWidth:) | autofitColumns follows XlsxWriter's width table. East Asian characters are measured at double width (Appendix B.33). measured |
| Row and column grouping (outline) | ○ | ○ | row@outlineLevel, <outlinePr summaryBelow summaryRight> | groupRows(_:outlineLevel:hidden:) groupColumns(_:_:outlineLevel:) sheet.properties | measured |
| Merged cells | ○ | ○ | <mergeCells>/<mergeCell> | merge(_:) unmerge(_:) sheet.merges mergedRange(containing:) | measured |
| Hyperlinks | ○ | ○ | <hyperlinks>/<hyperlink r:id location tooltip> | sheet[cell: "A1"].hyperlink Hyperlink | measured |
| Frozen panes | ○ | ○ | <sheetViews>/<sheetView>/<pane state="frozen"> | sheet.freezePanes freezePanes(at:) freezePanes | A split pane (split) is treated as frozen. measured |
| Sheet view (zoom, gridlines shown, selection) | ○ | ○ | <sheetView zoomScale showGridLines tabSelected showRowColHeaders showZeros rightToLeft topLeftCell view>, <selection> | sheet.view SheetView SheetView.Kind | Besides zoom, gridlines and selection, the row and column headings, showing zeros, right-to-left, the scroll position and the view type (normal / pageLayout / pageBreakPreview) are read and written (B.76). The remaining attributes, such as showFormulas, colorId and the zoom per view type, are not held (the README's known limits). measured |
| Tab colour | ○ | ○ | <sheetPr>/<tabColor> | sheet.tabColor | measured |
| Sheet protection (a set of permissions naming what is allowed) | ○ | ○ | <sheetProtection> | sheet.protection SheetProtection setModernPassword(_:) | The file records what is forbidden, but the API is named for what is allowed. Unlocking has been confirmed in Excel itself. judged by the application |
| Editable windows in a protected sheet | ○ | ○ | <protectedRanges>/<protectedRange> | sheet.protectedRanges ProtectedRange | ODS and Numbers have none. Converting drops them, with a warning. measured |
| Scenarios (recorded sets of input values) | ○ | ○ | <scenarios>/<scenario>/<inputCells> | sheet.scenarios Scenario ScenarioList | measured |
| Ignored errors, cell watches, custom sheet views, data consolidation | ▣ | ▣ | <ignoredErrors>, <cellWatches>, <customSheetViews>, <dataConsolidate> | — | Warning: Converting to another format: dropped Unknown child elements of <worksheet> are put back in schema order (WorkbookWriter.worksheetOrder). Of these, only consolidation is something an ODF document can hold (see the ODS table). code check |
| Several tables on one sheet | — | — | — | sheet.tables | Warning: 「N other table(s) not written: a worksheet holds a single grid (write .numbers to keep them)」 A worksheet is a single grid. Of several tables coming from Numbers, all but the first are dropped, with a warning. measured |
| Formulas | |||||
| Formulas (held as a syntax tree) | ○ | ○ | <f> | .formula("=SUM(A1:B2)") FormulaExpr value.formula | References follow row insertion and deletion and sheet renames (openpyxl's do not). measured |
| Shared formulas | ○ | ○ | <f t="shared" si ref> | — | Expanded on read. Written out as ordinary formulas. code check |
| Array formulas (with their range) | ○ | ○ | <f t="array" ref> | sheet.table.arrayFormulas[anchor] = CellRange("A2:A4") | measured |
| Cached formula values | ○ | ○ | <c><v> | .formula(_, cached:) ReadOptions(formulaCells: .cachedValues) | formulaCells: .cachedValues also offers a way to read the values alone. code check |
| Formula nesting (up to 64 levels) | △ | △ | Excel's own limit | FormulaExpr.unparsed | Warning: None (lossless as long as it is written back to the same format) A formula nested deeper than 64 levels is held as text and written back unchanged. It does not follow row insertion, and its dialect is not translated. document |
| Stock and currency functions (6 functions, such as STOCK and CURRENCY) | — | △ | — | FormulaExpr.remoteDataFunction | Warning: The cached value is written in place of the formula, with a warning saying so Functions that only Numbers can recalculate; Excel has no function of that name. The value that had been fetched is written, and the warning says so: the same substitution Numbers itself makes when it exports to Excel (Appendix B.27). measured |
| Unknown and newer functions (_xlfn prefix, dynamic arrays) | △ | △ | _xlfn.* | FormulaExpr | Warning: None (carried by name) The meaning of a function is not interpreted. It is carried as a name and a tree of arguments, so it survives within the same format. code check |
| Styles (xl/styles.xml) | |||||
| Fonts, fills, borders, alignment, number formats | ○ | ○ | <fonts> <fills> <borders> <numFmts> <cellXfs> | sheet.style("A1") { $0.font.bold = true } CellStyle Font Fill Border Alignment | Writing appends to the original font / fill / border tables, so entries referenced by index do not move. measured |
| Gradient fills | ○ | ○ | <gradientFill> | Fill.gradient(_:) GradientFill | ODS and Numbers can hold only one colour per cell, so converting keeps the first colour and returns a warning. measured |
| Named cell styles | ○ | ○ | <cellStyles>, <cellStyleXfs> | wb.namedStyles addNamedStyle(_:) CellStyle.namedStyle | code check |
| Differential formats (the formats conditional formatting and tables point to) | ○ | ○ | <dxfs>/<dxf> | wb.differentialStyles DifferentialStyle DifferentialFont | Every field is optional. nil means "leave as is". code check |
| Theme (colours, fonts) | ○ | ○ | xl/theme/theme1.xml, Color.theme | wb.theme Theme wb.rgb(of:) Color.theme(_:tint:) Color.indexed(_:) wb.indexedColors | The theme part's 12 colours and 2 fonts are read into wb.theme. Left untouched, the part stays byte for byte; changed, it is rebuilt at the same path. wb.rgb(of:) resolves theme colours (with tint) and indexed colours to RGB, and the ODS and Numbers writers write that colour (the former "written as default" warning remains only for colours that cannot be resolved) (B.71). measured |
| Table style definitions | ▣ | ▣ | <tableStyles> | — | Copied as is. Keeping the indexes from shifting comes first. code check |
| Conditional formatting | |||||
| 17 kinds of rule (comparison, formula, text, top/bottom, above/below average, duplicate/unique, blank/error, time period) | ○ | ○ | <conditionalFormatting>/<cfRule type operator> | sheet.addConditionalFormatting(.cellIs(.greaterThan, "100", paint: red), over: "B2:B99") ConditionalFormattingRule.Kind | Priorities are renumbered 1…n within the sheet. measured |
| Colour scales, data bars, icon sets | ○ | ○ | <colorScale> <dataBar> <iconSet> <cfvo> | ColorScale DataBar IconSet ConditionalValue | measured |
| Extension conditional formatting (negative data bars, custom icons) | △ | △ | <cfRule><extLst> x14:id, <extLst> x14:conditionalFormattings / x14:dataBar / x14:iconSet | DataBar.negativeColor / axisColor / axisPosition / direction / gradient / borderColor IconSet.customIcons sheet.hasUnmodelledConditionalFormats | Warning: Rules that exist only in 2010 (an x14 cfRule with no 2007 form), when the rules are rebuilt: "N conditional format(s) of Excel 2010's extension dropped" A data bar's negative colour, axis colour and position, direction, solid fill and border, and icon sets with custom icons, are folded from the x14 extension into the rule on read, and rebuilt beside the rule (B.82). Rule types that exist only in x14 are not held; a rebuild drops them, with a warning. measured |
| Data validation | |||||
| Data validation (list, number, date, text length, custom) | ○ | ○ | <dataValidations>/<dataValidation type operator> | sheet.dataValidations = [.list("'Choices'!$A$2:$A$4", over: MultiCellRange("C4:C99")!)] DataValidation | hidesDropDown is named for what the inverted showDropDown attribute actually means. measured |
| Extension data validation (such as a list pointing straight at another sheet) | △ | △ | <extLst> x14:dataValidations | sheet.hasUnmodelledValidations | Warning: A flag is set The file's own block is kept and written back. code check |
| Tables, filters, sorting | |||||
| Named tables | ○ | ○ | xl/tables/tableN.xml, <tableParts> | sheet.structuredTables addStructuredTable(named:over:) StructuredTable TableStyleInfo | Warning: A duplicate name: "named table not written" The part, its content type, its relationship and <tableParts> are all generated. measured |
| AutoFilter (the range) | ○ | ○ | <autoFilter ref> | sheet.autoFilter autoFilter | measured |
| Filter criteria (value list, comparison, colour, icon, dynamic, top 10, date group) | ○ | ○ | <filterColumn> <filters> <customFilters> <colorFilter> <iconFilter> <dynamicFilter> <top10> | sheet.filterColumns FilterColumn RankFilter DynamicFilter ColorFilter IconFilter DateGroup | measured |
| Extension filters | ▣ | ▣ | <extLst> x14:filter | sheet.hasUnmodelledFilters | Warning: A flag is set The original XML is kept as is. code check |
| Recorded sort | ○ | ○ | <sortState>/<sortCondition> | sheet.sortState SortState SortCondition | measured |
| Slicers | ▣ | ▣ | xl/slicers/, xl/slicerCaches/ | — | Warning: Converting to another format: dropped (objects) Not modelled. Saved to the same format, the parts stay as they are. code check |
| Pivot tables | |||||
| Pivot table layout (rows, columns, values, report filter) | ○ | ○ | xl/pivotTables/pivotTableN.xml | wb.addPivotTable(named:to:at:summarizing:on:rows:columns:values:) sheet.pivotTables PivotTable PivotField PivotDataField | Warning: A part that cannot be parsed: "could not be parsed; the pivot table it describes was skipped" The layout is written, the figures are not. The application that opens the file recomputes them from the source range. measured |
| Pivot cache (definition and records) | ○ | ○ | xl/pivotCache/pivotCacheDefinitionN.xml, pivotCacheRecordsN.xml | PivotCache PivotCacheField PivotItem | The records part is carried as XML. A defect that broke it on the second save was fixed on 2026-08-27. code check |
| Drawings and objects | |||||
| Embedded images (placed in a cell, fitted to a range) | ○ | ○ | xl/media/, xl/drawings/drawingN.xml, <twoCellAnchor> <oneCellAnchor> | sheet.addImage(try SheetImage(data:), at: "B2", sizing: .resizeCellToFit) addImage(_:over:) SheetImage ImagePlacement sheet.images SheetImage.Anchor.absolute | Warning: Converting to another format: dropped PNG / JPEG / GIF; the format and pixel size are read from the bytes. Reading: the drawing part's anchors (one-cell, two-cell, absolute) and the media parts go into sheet.images (B.72). Left untouched, the parts stay byte for byte; an addition is spliced in; a change or removal rebuilds the drawing, with a warning. Pictures in other formats (BMP, EMF and so on) are not taken into the model, and a rebuild drops them, with a warning. measured |
| Charts (column, bar, line, pie) | ○ | △ | xl/charts/chartN.xml | sheet.addChart(Chart(.column), over: "D2:K16") chart.addSeries(values:categories:name:) Chart.Kind sheet.charts Chart.Series.nameReference | Warning: A chart with no series: "a … chart with no series was not written" / a kind it cannot draw: "a … chart was not written: the writer draws column, bar, line and pie charts" / to another format: dropped 4 kinds are written, every kind is read (the kind stays in Chart.Kind's raw as the element name, B.69). A chart that was read stays byte for byte if untouched (B.72). openpyxl reads type, title, series and references in agreement, and the rendering was checked in LibreOffice's PDF (Appendix B.34). measured |
| Notes (cell comments) | ○ | ○ | xl/comments1.xml + legacy VML | sheet[cell: "A1"].comment = CellNote(text, author:) sheet.notes | Both the comments part and its companion VML are written. measured |
| Threaded comments | ○ | ○ | xl/threadedComments/threadedCommentN.xml, xl/persons/person.xml, the mirror in comments | cell.thread CommentThread sheet.threads | Warning: To another format: "N threaded comment(s) written as notes …" (substituted) Text, replies, resolved state, dates and people's names are read and written (B.80). The mirror notes for older readers are hidden and rebuilt on write-back. Left untouched, the 4 parts stay byte for byte; changed, they are rebuilt and the person ids are kept. There is no Excel-made specimen; the checks use a specimen built by hand from the published part layout (manual checklist). measured |
| Shapes, text boxes, SmartArt, background images | △ | △ | <xdr:sp>, <xdr:cxnSp>, <xdr:grpSp>, dgm:*, <picture> | sheet.shapes sheet.addShape(_:over:) sheet.addTextBox(_:over:font:) Shape.Geometry | Warning: What a rebuild drops: "N object(s) of the sheet's drawing the model could not read (a group of shapes, SmartArt) dropped" / a geometry with no preset: "written as a rectangle" Shapes and text boxes (xdr:sp / xdr:cxnSp) are read and written: preset geometry, text with one font, alignment, fill, outline, anchor (B.75). Rotation, shadows, gradients and per-paragraph formatting are not held, and stay byte for byte if untouched. SmartArt (the 4 dgm: parts) and groups of shapes are not taken into the model but noted as present: saved untouched to the same format they stay byte for byte, and a rebuild of the drawing drops them by name. Background images remain under preservation. It reads 7 shapes LibreOffice wrote, and LibreOffice reads the shapes it writes (gated). measured |
| OLE objects, ActiveX controls | ▣ | ▣ | <oleObjects>, <controls> | — | Warning: Converting to another format: dropped code check |
| Sparklines | ○ | ○ | <extLst> x14:sparklineGroups, x14:sparklineGroup, x14:sparkline | sheet.sparklines sheet.addSparkline(_:dataRange:at:) SparklineGroup | Warning: To Numbers: "N sparkline group(s) dropped: Numbers has no sparklines" Type (line / column / stacked), 8 colours, marker display, empty-cell handling and several sparklines per group are read and written (B.79). Left untouched, the extLst extension stays byte for byte; changed, only that extension is rebuilt and the other extensions remain. ODS carries them as LibreOffice's calcext:sparkline-groups, and LibreOffice converts them in both directions (gated test). measured |
| Printing | |||||
| Paper, orientation, scale, first page number | ○ | ○ | <pageSetup> | sheet.pageSetup PageSetup | measured |
| Margins, centering, printing gridlines and headings | ○ | ○ | <pageMargins>, <printOptions> | sheet.pageMargins sheet.printOptions PageMargins PrintOptions | measured |
| Header/footer | ○ | ○ | <headerFooter> oddHeader/&L&C&R | sheet.headerFooter HeaderFooter | Held as Excel's format-code string (not broken apart). measured |
| Page breaks | ○ | ○ | <rowBreaks>, <colBreaks> | sheet.rowBreaks sheet.columnBreaks | measured |
| Print area, repeated title rows/columns | ○ | ○ | _xlnm.Print_Area, _xlnm.Print_Titles | setPrintArea(_:) sheet.printArea sheet.printTitleRows sheet.printTitleColumns | measured |
| Macros and external connections | |||||
| VBA macros (.xlsm) | ▣ | ▣ | xl/vbaProject.bin | wb.write(to:as: .xlsm) | Warning: Writing to .xlsx: "VBA project dropped: macros cannot be kept in .xlsx" Only kept as opaque bytes; never executed. measured |
| External data connections, query tables, web queries | ▣ | ▣ | xl/connections.xml, xl/queryTables/ | — | Warning: Converting to another format: dropped code check |
| Rich values (data types such as stocks and geography) | × | × | c@cm, c@vm, xl/richData/ | — | Warning: None (the README's known limits) The cm / vm attributes are outside preservation. This is an honest gap, and the README says so. document |
| Custom XML parts | ▣ | ▣ | customXml/ | — | Warning: Converting to another format: dropped code check |
| Sheets other than worksheets | |||||
| Chart sheets, dialog sheets, macro sheets | ▣ | ▣ | xl/chartsheets/, <chartsheet> | sheet.contentState SheetContentState | Warning: On read: "a chart sheet has no grid the model can read; … written back to .xlsx exactly as it arrived". Writing cells into such a sheet: "N cell(s) … are not saved". Converting to another format: dropped Sheets that are not worksheets. The workbook lists them alongside the worksheets, but the part's root is <chartsheet>, and the model has no words for what it holds. So they are carried uninterpreted: the part goes in and out byte for byte, and its content type and relationship type stay as they were. The specimen was made by Excel 16.112.2 itself (Tests/FixtureGenerator/make_chartsheet_fixture.py), and the written-back result is checked by LibreOffice rendering it to the same number of pages as the original (Appendix B.35). judged by the application |
| Large files | |||||
| Row-by-row reading (without loading everything into memory) | △ | — | Streaming parse of xl/worksheets/sheetN.xml | StreamingReader(contentsOf:) forEachRow(inSheet:) rows(inSheet:) StreamedRow | Warning: None (documented as values and formatting only) 23 MB for a million cells (221 MB for the whole model — docs/performance.json). Merges, notes and preservation are not carried. The umbrella's StreamingReader opens ODS, Numbers and CSV with the same call (Appendix B.40). document |
| Row-by-row writing | — | △ | Same as above | StreamingWriter(to:) XLSXStreamingWriter append(_:) close() warnings | Warning: None A few MB whatever the row count. Values and formatting only. The umbrella's StreamingWriter picks the format from the extension and writes ODS, Numbers and CSV with the same call too (Appendix B.42). document |
| Cell count limit | △ | — | ReadOptions.cellLimit (no limit by default — Appendix B.39.2) | ReadOptions(cellLimit:) | Warning: A degraded warning says "reading stopped here" Reading never stops partway in silence. The reader sets the limit, and Workbook.inspect provides what is needed to decide it. document |
OpenDocument spreadsheet (.ods)
Specification: OASIS OpenDocument Format 1.3 — Part 3 (Schema) / Part 4 (OpenFormula)
Public specification. The element names are taken from the OASIS ODF 1.3 RelaxNG schema. Only the richer parts of conditional formatting have no place in the main specification; for those, LibreOffice's extension namespace calcext: is the de facto standard.
| Feature | Read | Write | Where in the spec | API | Note (what is reported when it is dropped; evidence) |
|---|---|---|---|---|---|
| Package | |||||
| ODF package (mimetype first, uncompressed) | ○ | ○ | mimetype, META-INF/manifest.xml | Workbook(contentsOf:) wb.write(to:as: .ods) | Follows the rule that an uncompressed mimetype comes first. Confirmed by LibreOffice being able to reopen the file. judged by the application |
| The document's 4 parts | ○ | ○ | content.xml, styles.xml, meta.xml, settings.xml | — | settings.xml is read and written too (view settings, such as frozen panes, live there). code check |
| Password encryption | △ | ○ | manifest:encryption-data(ODF 1.3 §4.3) | SheetDecrypt.decrypt(_:password:) Workbook(contentsOf:password:) SheetEncrypt.encrypt(_:as:password:) Workbook.write(to:password:) SheetError.wrongPassword UnopenableInput.encryptedODF | Warning: The core alone throws (naming the file as encrypted and saying SheetDecrypt opens it). A wrong password is wrongPassword. ODF 1.1's Blowfish form is refused by name Read when SheetDecrypt is linked, written when SheetEncrypt is (0.17.2, Rev 4.29). The core and the 5 products contain no cipher code (scripts/check-no-crypto.sh reads the symbol tables in CI). AES-CBC + PBKDF2 (ODF 1.2 and later, the form LibreOffice writes) is read and written. The judge is an independent implementation that follows ODF 1.3 §4.3 using the cryptography library (Appendix B.39.9). judged by the application |
| Embedded object and image parts | △ | △ | Pictures/, Object N/, ObjectReplacements/ | sheet.images sheet.charts | Warning: When embedded objects other than charts remain: "N embedded object(s)/picture(s) of the source ODS are not re-linked" Pictures (Pictures/) and charts (Object N/) are read into the model and rebuilt as new parts on write (B.73). Embeddings that are not charts, such as formula objects, are held but not linked back in, and the warning says so plainly. measured |
| Embedded images (placed in a cell, fitted to a range) | ○ | ○ | Pictures/, draw:frame, draw:image, table:end-cell-address | sheet.addImage(try SheetImage(data:), at: "B2", sizing: .resizeCellToFit) addImage(_:over:) SheetImage sheet.images SheetImage.Anchor.absolute | Warning: A picture in a format the model cannot hold: "a picture in a format the model cannot hold (…) is carried as a part only" PNG / JPEG / GIF are written as a part under Pictures/ (with its media-type in the manifest) and a draw:frame / draw:image inside the anchor cell (Appendix B.43). Sizes are in cm at 96 dpi. A picture fitted to a range gets table:end-cell-address, and LibreOffice stretches it to fill the range. Numbering does not collide with the source ODS's Pictures/. Reading: a draw:frame inside a cell (a range if it has end-cell-address, otherwise 1 cell) and a draw:frame inside table:shapes (absolute position) are read into sheet.images (B.73). measured |
| Tables, rows, columns | |||||
| Tables (sheets), names, visibility | ○ | ○ | table:table, table:name, table:display="false" | wb.sheets sheet.name sheet.isHidden | measured |
| Compression by repetition (identical rows and columns written once) | ○ | ○ | table:number-rows-repeated, table:number-columns-repeated | ReadOptions(cellLimit:) | Warning: Past the limit, a degraded warning says "reading stopped here" With this compression ODF can describe 17 billion cells in 1 KB of XML. Reading stops at 1 million cells by default, and always says so. code check |
| Row height, column width, hidden | ○ | ○ | style:row-height, style:column-width, table:visibility | setHeight(_:ofRow:) setWidth(_:ofColumn:) RowDimension ColumnDimension | measured |
| Row and column grouping (outline) | ○ | ○ | table:table-row-group (nested) | groupRows(_:outlineLevel:hidden:) groupColumns(_:_:outlineLevel:) | ODF expresses the depth of levels by nesting elements. Reading turns the nesting into a level number. measured |
| Header rows, header columns | ○ | ○ | table:table-header-rows, table:table-header-columns | sheet.freezePanes | code check |
| Several tables on one sheet | — | — | Several table:table elements in sequence | sheet.tables | Warning: When the model holds several: "N other table(s) not written" In ODF, 1 sheet = 1 grid. Of several tables coming from Numbers, all but the first are dropped, with a warning. measured |
| Cells and values | |||||
| Cell values (float, percentage, currency, date, time, boolean, string) | ○ | ○ | office:value-type="float|percentage|currency|date|time|boolean|string" | sheet["A1"] CellValue | measured |
| Currency cell type (which currency, held as data) | ○ | ○ | office:value-type="currency", office:currency | CellStyle.numberFormat | Warning: To another format: dropped, "only OpenDocument has it" One of the 6 things only ODF has. 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. measured |
| Merged cells | ○ | ○ | table:number-columns-spanned / -rows-spanned | merge(_:) sheet.merges | measured |
| Formatting within a cell (rich text) | ○ | ○ | text:p / text:span | CellValue.richText TextRun | measured |
| Hyperlinks | ○ | ○ | text:a xlink:href | sheet[cell: "A1"].hyperlink Hyperlink | Warning: Several on a cell whose value is not text: "a cell holds more than one hyperlink; the first was kept" ODF can attach a link to a range of characters. Several links are read and written per richText run (TextRun.hyperlink, B.81), and Cell.hyperlink is the first. measured |
| Notes (annotations) | ○ | ○ | office:annotation | sheet[cell: "A1"].comment CellNote | measured |
| Cell controls (checkboxes and the like) | × | × | office:forms / form:control | Cell.control CellControl | Warning: 「N cell control(s) (checkbox, stepper, slider, rating) dropped: ODF has …」 On read, the contents of office:forms are skipped. Controls coming from Numbers are dropped, keeping only the value. measured |
| Formulas (OpenFormula) | |||||
| Formulas (the OpenFormula dialect) | ○ | ○ | table:formula="of:=[.A1]" | .formula("=SUM(A1:B2)") value.formula?.rendered(as: .ods) FormulaExpr | Translated from the syntax tree into OpenFormula. The intersection operator is understood both as Excel's space and as OpenFormula's !. measured |
| Matrix formulas (array formulas) | ○ | ○ | table:number-matrix-columns-spanned / -rows-spanned | sheet.table.arrayFormulas | The range is written on the anchor cell. measured |
| Formulas OpenFormula lacks, formulas that cannot be translated | △ | △ | — | FormulaExpr.unparsed | Warning: "formula in … dialect could not be translated; cached value written" and 3 others A formula whose meaning would change in translation falls back to its cached value, and the warning says where and why. code check |
| Stock and currency functions (6 functions, such as STOCK and CURRENCY) | — | △ | — | FormulaExpr.remoteDataFunction | Warning: 「… fetches live data and OpenFormula has no such function; the cached value …」 OpenFormula has no function of that name. 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 |
| Named ranges, named expressions | ○ | ○ | table:named-range, table:named-expression | wb.definedNames sheet.definedNames | measured |
| Styles and data styles | |||||
| Cell styles (font, colour, borders, alignment, wrapping) | ○ | ○ | style:style family="table-cell", fo:*, style:* | sheet.style("A1") { … } CellStyle | measured |
| Gradient fills | — | △ | draw:gradient | Fill.gradient(_:) | Warning: 「gradient fill(s) written as their first colour: an ODF cell style has one …」 An ODF cell style can hold only 1 colour. The first colour is used, with a warning. measured |
| Data styles (number formats) | △ | △ | number:number-style, number:date-style, number:currency-style and others | CellStyle.numberFormat NumberFormat | Warning: 「number format … has no ODF data style; General used」/「only its first section is written」 An ODF data style can hold only 1 way of showing a value. Excel's multiple sections, colours and conditions are dropped. A data style that cannot be translated back is also dropped to General, with a warning. measured |
| Conditions attached to a style (conditional formatting in the core specification) | ○ | × | style:map | sheet.conditionalFormatting | A read-only path, kept for files from older generators that have no calcext:. Writing uses calcext: alone. code check |
| Master pages and page layouts | ○ | ○ | style:master-page, style:page-layout | sheet.pageSetup sheet.pageMargins sheet.headerFooter | ODF has a master page per sheet. The print settings live there. measured |
| Conditional formatting | |||||
| Conditional formatting, 18 kinds of rule (comparison, formula, text, top/bottom, above/below average, duplicate/unique, blank/error, time period) | ○ | ○ | calcext:conditional-formats / calcext:condition | sheet.addConditionalFormatting(_:over:) ConditionalFormattingRule.Kind | Only calcext: is written. On read, both calcext: and style:map are read. When LibreOffice converts the file to XLSX, all 18 kinds are reproduced. measured |
| Colour scales, data bars, icon sets | ○ | ○ | calcext:color-scale, calcext:data-bar, calcext:icon-set | ColorScale DataBar IconSet | The ODF 1.3 core specification has no words for them. A data bar's negative colour, axis colour and position, and gradient are carried in calcext attributes (B.82). ODF has no data bar that "hides the value" and no custom icons, so only those are dropped, with a warning. measured |
| Conditional formatting the model could not fully read | △ | × | — | sheet.hasUnmodelledConditionalFormats | Warning: 「a conditional format the model could not read is dropped: ODS is regenerated …」 ODS is regenerated every time, so the XLSX approach of putting the original block back is not available. It is dropped, with a warning. code check |
| Data validation | |||||
| Content validation (drop-down, number, date, text length, custom) | ○ | ○ | table:content-validation, table:condition, table:content-validation-name | sheet.dataValidations DataValidation | ODF lists the rules at the head of the document, and cells point at them by name. measured |
| Data validation the model could not fully read | △ | × | — | sheet.hasUnmodelledValidations | Warning: 「a data validation the model could not read is dropped: ODS is regenerated …」 code check |
| Database ranges, filters, sorting | |||||
| Database ranges (the counterpart of a structured table) | ○ | △ | table:database-range | sheet.structuredTables addStructuredTable(named:over:) | Warning: 「named table … written as an ODF database range: its banded-row style …」 The name and the range go through. The banded-row style is dropped, with a warning. measured |
| Filters (filter criteria) | ○ | △ | table:filter, table:filter-condition | sheet.autoFilter sheet.filterColumns | Warning: Colour, icon, dynamic and date-group filters do not exist in ODF and are dropped, with a warning Value lists and comparison criteria go through. measured |
| Recorded sort | ○ | ○ | table:sort, table:sort-by | sheet.sortState SortState | measured |
| Filters the model could not fully read | △ | × | — | sheet.hasUnmodelledFilters | Warning: 「an auto-filter the model could not read is dropped: ODS is regenerated …」 code check |
| Data pilot (pivot tables) | |||||
| Data pilot tables | ○ | ○ | table:data-pilot-table, table:data-pilot-field | sheet.pivotTables wb.addPivotTable(…) PivotTable | As with Excel's pivot tables, only the layout is written; the application that opens the file computes the figures. measured |
| Protection | |||||
| Table protection | ○ | ○ | table:protected, table:protection-key | sheet.protection SheetProtection | measured |
| Editable windows in a protected sheet | — | × | — | sheet.protectedRanges | Warning: 「N protected range(s) dropped: ODF protects a whole table …」 ODF protects the whole table and has no windows inside it. measured |
| Workbook protection (locking the sheet structure) | ○ | ○ | table:structure-protected on office:spreadsheet (ODF 1.3 §9.1.2) | wb.protection.locksStructure | A flag that forbids adding, deleting, renaming and reordering sheets. The key (table:protection-key) is not compatible with Excel's, so it is not carried; only the flag round-trips. LibreOffice keeps it when reading the file back (Appendix B.40.4). This row used to say "no such concept in ODF", which was wrong. judged by the application |
| Printing | |||||
| Orientation, paper, scale, margins, centering | ○ | ○ | style:page-layout-properties, style:print-orientation, style:scale-to | sheet.pageSetup sheet.pageMargins | measured |
| Header/footer (3 regions: left, centre, right) | ○ | △ | style:header, style:footer, style:region-left/center/right | sheet.headerFooter | Warning: Only font directives are dropped Excel's &L&C&R and ODF's 3 regions are translated both ways. measured |
| Print area, title rows/columns | ○ | ○ | table:print-ranges, table:print | setPrintArea(_:) sheet.printTitleRows | measured |
| Page breaks | ○ | ○ | fo:break-before (on row and column styles) | sheet.rowBreaks sheet.columnBreaks | measured |
| What only ODF has (Appendix B.17) | |||||
| Label ranges (a heading used as is in a formula) | ○ | ○ | table:label-ranges / table:label-range | wb.labelRanges LabelRange | Warning: To another format: "… is dropped: only OpenDocument has it" The "売上" in =SUM(売上) is a column heading, not a defined name. It corresponds to Excel 2003's natural-language formulas, but today's Excel format has nowhere to put it. measured |
| Consolidation definitions | ○ | ○ | table:consolidation | wb.consolidation Consolidation | Warning: To another format: dropped "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. measured |
| Detective arrows (tracing precedents and dependents) | ○ | ○ | table:detective, table:highlighted-range, table:operation | table.detective CellDetective | Warning: To another format: dropped 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. measured |
| Calculation settings (regular expressions, wildcards, case sensitivity, two-digit years, precision as displayed) | ○ | ○ | table:calculation-settings, table:use-regular-expressions, table:null-year | wb.calculationSettings CalculationSettings | Warning: To another format: "a calculation setting is dropped — …; only OpenDocument keeps it in the file" Settings that change the meaning of a search criterion. In Excel they are application settings and never enter the file. measured |
| Date epoch (any date can be the origin) | ○ | ○ | table:null-date | wb.epoch DateEpoch(origin:) | ODF can use any date as the epoch. DateEpoch(origin:) holds it as is and writes it back (B.69). To Excel and Numbers it is re-based onto the 1900 system, with a degraded warning (calendar dates land on the same day). measured |
| What ODF has and the model has no words for | |||||
| Tracked changes (per-cell revisions) | △ | × | table:tracked-changes | wb.unmodelledODFFeatures UnmodelledODFFeatures.trackedChanges | Warning: 「tracked changes (a document's revision history) is dropped: … the model has no word for it」 Their presence is read, and writing says "it was there but was dropped". The contents are not carried. code check |
| DDE links (another running application supplies the values) | △ | × | table:dde-links | UnmodelledODFFeatures.ddeLinks | Warning: Same as above code check |
| Sheets linked in from another document | △ | × | table:table-source | UnmodelledODFFeatures.linkedSheet | Warning: Same as above code check |
| Ranges linked in from another document | △ | × | table:cell-range-source | UnmodelledODFFeatures.linkedRange | Warning: Same as above code check |
| Drawing objects (charts, shapes, text boxes) | △ | △ | draw:frame, draw:object, draw:custom-shape, draw:line, draw:text-box | sheet.charts sheet.addChart(_:over:) Chart.Kind sheet.shapes sheet.addShape(_:over:) Shape.Geometry | Warning: A kind of chart it cannot draw: "a … chart was not written: the writer draws column, bar, line and pie charts" / groups of shapes (draw:g) are not read Charts: every kind is read from the Object N/content.xml (an ODF chart document) that draw:object points to (chart:bar + chart:vertical → column / bar, chart:line, chart:circle → pie, any other class into raw), and 4 kinds are written as chart documents (B.73). Shapes and text boxes: draw:custom-shape (draw:type through a table mapping ooxml-X to LibreOffice's names), draw:line / draw:connector, and the draw:text-box of a draw:frame are read and written (B.75). LibreOffice carries both the charts and the shapes written here into XLSX (gated test). Groups of shapes are skipped. measured |
| Scenarios | × | × | table:scenario | sheet.scenarios | Warning: 「N scenario(s) dropped: an ODF scenario is a whole shadow sheet …」 An ODF scenario is a "shadow sheet", not a set of input values. Writing one would add a sheet, so none is written. measured |
| Tab colour | ○ | ○ | style:table-properties@table:tab-color | sheet.tabColor | ODF 1.3's table:tab-color (the same attribute LibreOffice 26.2 writes; the older tableooo:tab-color is read too). Theme and indexed colours are resolved to RGB before writing (B.74). measured |
| Large files | |||||
| Row-by-row reading (without loading everything into memory) | △ | — | Streaming parse of content.xml | StreamingReader(contentsOf:) ODSStreamingReader forEachRow(inSheet:) rows(inSheet:) | Warning: None (documented as values and formatting only) Walks the body once and delivers only the rows of the requested table (earlier tables are skipped, later ones are not read). Repeated rows and repeated cells are expanded by the same rules as the ordinary reader. Values and formatting only. Appendix B.40.2. measured |
| Row-by-row writing | — | △ | content.xml (office:automatic-styles before the tables) | StreamingWriter(to:) ODSStreamingWriter append(_:) close() warnings | Warning: Formatting and colours ODF lacks: the same warnings as the whole-model writer Each row is turned into XML as it arrives and set aside (in memory up to 8 MiB, beyond that in a temporary file), and on close it is flushed after the styles. Memory stays at a few MB whatever the row count, plus one copy of the body on disk. Values and formatting only. Appendix B.42. measured |
Apple Numbers(.numbers)
Specification: No public specification — the Numbers User Guide (Help) + observation of real files
Apple does not publish the Numbers document format. The left column of this table was built from 2 sources: ① the features the Numbers User Guide gives names to, and ② the archive types that actually exist inside documents (TN./TST./TSCH./TSD. …, 581 kinds extracted mechanically from the schema derived from numbers-parser). Since no row can claim "the specification says so", the evidence is always given as an observation or a measurement.
| Feature | Read | Write | Where in the spec | API | Note (what is reported when it is dropped; evidence) |
|---|---|---|---|---|---|
| Document and package | |||||
| Reading and writing documents (IWA: Snappy compression + Protobuf) | ○ | ○ | Index/*.iwa, Index/Document.iwa | Workbook(contentsOf:) wb.write(to:as: .numbers) Codec.numbers | Handled as a dependency-free, dynamic Protobuf tree (ProtoMessage). Unknown fields round-trip unchanged, byte for byte (NumbersIWATests.fixturesRoundTripByteForByte). judged by the application |
| The basis for writing (the template-and-patch approach) | — | △ | Resources/empty.numbers | Codec.numbers | Warning: A feature that needs a structure the template lacks is dropped, with a warning naming it Built on the empty document bundled with numbers-parser: sheet 1 and its table are rewritten in place, and from sheet 2 on a subgraph is copied and given new ids and UUIDs (spec §11.1). Every write starts from the template, so there is no round-trip preservation (F3). document |
| Document password protection | × | × | .iwph (the marker of an encrypted document) | SheetError.unopenable | Warning: throw「the Numbers document is password-protected (an .iwph package)」 A document protected with a password in Numbers is not opened. code check |
| Keeping up with Numbers versions | △ | — | The version field of TN.DocumentArchive | wb.readWarnings | Warning: 「Numbers document version … is newer than the verified range; read as far as possible」 A document newer than the verified range is still read as far as it can be, with a warning saying so. code check |
| Document properties, custom properties | — | × | — | wb.metadata wb.customProperties | Warning: 「N custom document propert(ies) dropped: Numbers has no free-form document fields」 measured |
| Sheets and canvas | |||||
| Sheets (name, order, several) | ○ | ○ | TN.SheetArchive | wb.sheets addSheet(named:at:) | From sheet 2 on, each sheet is a copied subgraph. The judge is Numbers.app itself being able to open the file (Appendix B.18). measured |
| Several tables on one sheet | ○ | ○ | Several TST.TableInfoArchive in sequence | sheet.tables sheet.addTable(named:at:) Table.anchor | Warning: To another format: "N other table(s) not written: a worksheet holds a single grid" The one place Numbers is stronger. In Excel and ODS, 1 sheet = 1 grid, so all tables but the first are dropped, with a warning. measured |
| Hidden sheets | — | × | — | sheet.isHidden | Warning: 「Numbers has no hidden sheets; the sheet is visible」 measured |
| Tab colour | — | × | — | sheet.tabColor | Warning: 「the tab colour is dropped: Numbers tabs have no colour」 measured |
| Sheet background colour, orientation | × | × | TN.SheetStyleArchive, TN.CommandSetSheetDirectionArchive | — | Warning: None (the model has no counterpart) It exists as an archive type, but the model has nothing to map it to. It is neither read nor written. code check |
| Forms (data-entry tabs made on iPhone / iPad) | × | × | TN.FormBasedSheetArchive | wb.readWarnings | Warning: "the form "<name>" is dropped: a Numbers form is a way of typing into the table <table>, not a sheet of its own …", naming the form and the table it writes into A form is not a table but a screen for typing into an existing table. It holds no values, so instead of being dressed up as an empty sheet it is dropped, with a warning naming it and the table it feeds (so that a writer does not copy that lie). The table the form filled in is read as usual. The specimen can only be made on an iPhone, so the maintainer made it by hand (Fixtures/numbers/form-15.numbers). Appendix B.36. measured |
| Tables | |||||
| Table name | ○ | ○ | TST.TableModelArchive.table_name | Table.name | code check |
| Table position (where it sits on the canvas) | ○ | ○ | B.85 (the geometry of TST.TableInfoArchive) | Table.position Table.anchor sheet.addTable(named:at:) | Points (pt) are read and written as is. Placement order: position → a non-default anchor → below the previous table. measured |
| Header rows, header columns | ○ | ○ | number_of_header_rows / _columns, header_rows_frozen | sheet.freezePanes | Round-tripped as the counterpart of Excel's frozen panes. measured |
| Footer rows | △ | × | number_of_footer_rows | — | Warning: None (only the footer designation is dropped) The rows themselves are read as ordinary rows, losing neither values nor formatting. Only the designation "from here down is the footer" has no word in the model. code check |
| Row height, column width, hidden | ○ | ○ | Row and column info in TST.TableModelArchive | setHeight(_:ofRow:) setWidth(_:ofColumn:) | measured |
| Merged cells | ○ | ○ | TST.MergeRegionMapArchive | merge(_:) sheet.merges | measured |
| Table styles (preset looks such as banded rows) | × | × | TST.TableStyleArchive, TST.TableStylePresetArchive | sheet.structuredTables | Warning: 「N named table(s) dropped: every Numbers table is named, but its own header rows are not this frame」 Every Numbers table has a name, but it is a different frame from an Excel "named table". It is not mapped; it is dropped, with a warning. measured |
| Row and column grouping (outline) | — | × | — | groupRows(_:outlineLevel:hidden:) | Warning: 「row / column grouping is dropped: Numbers groups by category, not by outline level」 Numbers groups by category. It has no idea of level depth. measured |
| Cells and values | |||||
| Cell values (number, text, date, boolean, duration, error) | ○ | ○ | Cell storage in TST.Tile / TST.TileRowInfo | sheet["A1"] CellValue | Numbers stores numbers as decimal128. They are received as Decimal, so no digits are lost. measured |
| Formatting within a cell (rich text) | ○ | ○ | TSWP.StorageArchive, TSWP.CharacterStyleArchive | CellValue.richText TextRun | Numbers holds it as the points where the character style changes. measured |
| Hyperlinks | △ | △ | TSWP.HyperlinkFieldArchive | sheet[cell: "A1"].hyperlink Hyperlink | Warning: Several on a cell whose value is not text: "the cell holds N links; a cell carries one, so the first was kept" A Numbers link is attached to a range of characters, so 1 cell can carry several. Several links are read and written per richText run (TextRun.hyperlink, B.81), and Cell.hyperlink is the first. Whether Numbers itself opens the per-run links is checked on the manual checklist. measured |
| Notes (cell comments) | ○ | ○ | TSD.CommentStorageArchive, TSK.AnnotationAuthorArchive | sheet[cell: "A1"].comment CellNote | Kept in the table's comment list, with 1 shared "author" record per person who wrote one. measured |
| Stock cells (a STOCK formula in current Numbers) | ○ | ○ | — | FormulaExpr.remoteDataFunction | Confirmed with a sample made by hand in Numbers.app. In today's Numbers a stock cell is an ordinary STOCK formula, and it is read as one (Appendix B.29). judged by the application |
| Formulas | |||||
| Formulas (read and written as Numbers expression trees) | ○ | ○ | TSCE.FormulaArchive, TSCE.ASTNodeArrayArchive | .formula("=SUM(A1:B2)") FormulaExpr | The syntax tree is turned into the reverse-Polish node list that Numbers evaluates. A written formula is calculated inside Numbers as a genuine formula (Appendix B.18). judged by the application |
| References to another table | ○ | ○ | 'Sheet::Table'!A1 | FormulaExpr | measured |
| Shapes with no sample to map onto (defined names, functions Numbers lacks, column ranges like A:C, intersection/union) | — | △ | — | value.formula WriteResult.warnings | Warning: "formula written as its cached value: …" names where and why the formula fell back A shape with no real example in the local fixtures is not invented; it falls back to the cached value, with a warning. document |
| Stock and currency functions (6 functions, such as STOCK, STOCKH and CURRENCY) | ○ | ○ | — | FormulaExpr.remoteDataFunction | Formulas only Numbers can recalculate. They are written as formulas and fetched again when the document is opened (Appendix B.27). measured |
| Array formulas (with their expansion) | ○ | × | The formula on the anchor + an internal function on each covered cell | sheet.table.arrayFormulas | Warning: 「N array formula(s) written as the anchor's formula and the covered cells' values …」 Read only. It cannot be written because the internal function produces no value on recalculation: even a Numbers-made sample, disguised as an older version so that Numbers recalculates it, loses its values (measured, Appendix B.26). measured |
| Formatting | |||||
| Cell formatting (font, colour, fill, borders, alignment, wrapping) | ○ | ○ | TST.CellStyleArchive, TST.CellStylePropertiesArchive | sheet.style("A1") { … } CellStyle | measured |
| Font names | ○ | ○ | PostScript names (Helvetica Neue ⇄ HelveticaNeue) | Font.name | A mapping table is kept as a resource (fonts.json), and names are converted both ways. code check |
| Number format (kind + decimal places + thousands separator) | △ | △ | TSK.FormatStructArchive | CellStyle.numberFormat NumberFormat | Warning: 「number format … has no Numbers equivalent」/「… its colours, conditions and negative section are dropped」 Numbers can describe only 1 way of showing a value. Excel's multiple sections, colours and conditions are dropped, with a warning. measured |
| Gradient fills | △ | △ | TSD.FillArchive | Fill.gradient(_:) | Warning: Becomes the first colour, with a warning saying so measured |
| Conditional formatting | |||||
| Conditional formatting, 14 kinds of rule (8 comparisons, 4 text rules, duplicate, unique) | ○ | ○ | predicate_type in TST.ConditionalStyleSetArchive | sheet.addConditionalFormatting(_:over:) ConditionalFormattingRule.Kind | Warning: The remaining rules: "conditional format … is dropped: Numbers has no rule of that kind" predicate_type is an integer Apple has not named. The 14 values were filled in by observation: a document laid out as 1 column, 1 rule was written as .xlsx, Numbers 15.3.1 was made to read it and save it again as .numbers, and the rules that survived were matched back to their original columns by their parameters (Appendix B.18). measured |
| Colour scales, data bars, icon sets | — | × | — | ColorScale DataBar IconSet | Warning: dropped Numbers itself discards the same things when it imports an Excel file. measured |
| Data validation and cell controls | |||||
| Pop-up menus | ○ | △ | TST.PopUpMenuModel | sheet.dataValidations = [.list("\"a,b,c\"", over: …)] DataValidation | Warning: A rule that does not spell out its choices: "only a list whose choices are spelt in the rule becomes a Numbers pop-up menu" In the model's terms, a .list data validation. A list that references a range and numeric conditions are dropped, with a warning (Numbers itself makes the same substitution when it imports Excel, Appendix B.24). measured |
| Cell controls (checkbox, stepper, slider, rating) | ○ | ○ | Cell control info (the dial's minimum, maximum, step) | Cell.control CellControl CellControl.Kind | Warning: When the value does not match the control's type: "a checkbox edits a boolean and a dial edits a number …" Numbers-only. Asked cell by cell over AppleScript, Numbers itself answers with the control's name (Appendix B.25). A cell with a control always has a value (an untouched checkbox is false, a dial its minimum, a rating 0). judged by the application |
| Controls the model has no word for | △ | — | — | — | Warning: 「… a Numbers control the model has no word for …; the value is kept, the control is not」 code check |
| Organise (categories, filters, sorting) | |||||
| Categories (grouping by a column) | △ | × | TST.GroupByArchive, TST.CategoryOwnerRefArchive | wb.readWarnings | Warning: "a category grouping by <column name> is dropped — the model has no word for Numbers categories …" The rows stay flat, and the warning names the columns they were grouped by. A switched-off category is reported too, in a different message. measured |
| Filters | △ | × | TST.FilterSetArchive | RowDimension.hidden | Warning: 「a Numbers filter (N rule(s)) is dropped — the rows it hides are kept as hidden rows …」 Rows the filter hides are kept as hidden rows, and the rules are dropped, with a warning. The same deal Numbers itself makes when it exports to Excel (Appendix B.29). measured |
| Sorting | △ | × | sort_order / rules | — | Warning: "a Numbers sort order (N rule(s), by <column name>) is dropped — the rows already come back sorted" The rows are stored in sorted order. Only the rules are dropped. measured |
| Pivot tables | |||||
| Pivot tables (several levels on both axes, with subtotals) | △ | △ | TST.PivotOwnerArchive, TST.PivotOrderArchive, TST.SummaryModelArchive | wb.addPivotTable(named:to:at:summarizing:on:rows:columns:values:) PivotTable | Warning: The 2nd and later summarised values: "N of its M summarised values dropped — the value lanes … share one placeholder id" Written as a genuine Numbers pivot table (a live summary that Numbers recounts from the source rows). Measured up to 3 levels of nesting. At most 1 summarised value (Appendix B.28). judged by the application |
| Reading pivot tables | △ | — | Same as above | sheet.tables | Warning: None (read as an ordinary table with values) Read back, it becomes an ordinary table. The model does not hold the pivot mechanism. document |
| Report filter fields | — | × | — | PivotTable.pageFields | Warning: 「N report filter(s) dropped — a Numbers pivot has no filter field, and Numbers drops them too …」 code check |
| Objects on the canvas | |||||
| Charts | △ | △ | B.88 (TSCH.ChartDrawableArchive, TN.ChartMediatorArchive) | sheet.charts sheet.addChart(_:over:) Chart.frame | Warning: A kind it cannot draw: "a … chart was not written: the writer draws column, bar, line and pie charts" / series over a range not in a table: "… series of a chart dropped" / a chart not linked to a table stays "a chart" in the read report Kind, title, legend and series ranges are read and written through the mediator's formulas. Styling such as series colours is the template's preset. Numbers 15.3.1 opens and re-saves the file, and the chart survives an export to Excel (gated test). measured |
| Images | ○ | ○ | B.83 (TSD.ImageArchive, TSP.DataInfo) | sheet.images sheet.addImage(_:at:sizing:) sheet.addImage(_:over:) | Warning: A picture that is not PNG / JPEG / GIF (PDF, HEIC) stays "an image" in the read report The anchor is read as a point on the canvas (.absolute). A cell anchor is converted to points using the first table's column widths and row heights, then written. Numbers 15.3.1 opens and re-saves the file (gated test). measured |
| Shapes, text boxes | △ | △ | B.83(TSWP.ShapeInfoArchive) | sheet.shapes sheet.addShape(_:over:) sheet.addTextBox(_:over:font:) | Warning: A geometry outside the 10 (rect / roundRect / ellipse / diamond / triangle / arrows in 4 directions / line): "a shape of geometry … was written as a rectangle" / textAlignment: "not written" (degraded) Carries text, fill, outline and font (font: write only). The 10 shapes are written as unit paths, and reading recognises the same paths (B.87). Any other path becomes numbers-path (outside the presets). measured |
| Movies, audio | × | × | TSD.MovieArchive | — | Warning: On read: "a movie" code check |
| Groups of shapes, connection lines | × | × | TSD.GroupArchive, TSD.ConnectionLineArchive | — | Warning: On read: "a group of objects", "a connection line" code check |
| Protection, printing, other | |||||
| Document password protection (writing) | — | × | — | wb.protection | Warning: 「workbook protection is dropped: Numbers locks a document with a password, which this writer does not set」 measured |
| Sheet protection, protected ranges | — | × | — | sheet.protection sheet.protectedRanges | Warning: "sheet protection is dropped: Numbers protects a whole document, not a sheet" and others measured |
| Printing (paper, orientation, margins, header/footer, title rows, print area, page breaks) | △ | △ | B.84, B.86 (TN.SheetArchive, TN.DocumentArchive) | sheet.pageSetup sheet.headerFooter sheet.printArea sheet.rowBreaks | Warning: Print area, page breaks, title rows not starting at row 1 and a paper size code with no entry in the lookup table are named in "… are dropped: Numbers prints a canvas, not a page grid" / codes other than &P: "header / footer code(s) … dropped" (degraded) Carries orientation, scale, margins, first page number, left/centre/right header/footer (odd pages), paper (1 per document) and title rows/columns from row 1 (repeated header rows). Reading returns what Numbers prints, as is (&P in the centre footer, 72 %). measured |
| Defined names | — | × | — | wb.definedNames sheet.definedNames | Warning: 「N defined name(s) dropped: Numbers has no defined names」 measured |
| Scenarios | — | × | — | sheet.scenarios | Warning: 「N scenario(s) dropped: Numbers has no scenarios」 measured |
| The 1904 date epoch | — | × | — | wb.epoch | Warning: 「the 1904 date origin is dropped: the template has no calculation engine to record it on」 code check |
| VBA macros | — | × | — | — | Warning: 「VBA project dropped: Numbers has no place for it (write .xlsm to keep the macros)」 code check |
| Collaboration history | × | × | TSCK.* (collaboration) | — | Warning: None (the model has no counterpart) The type exists in the registry, but it is neither read nor written. code check |
| Large files | |||||
| Row-by-row reading (without loading everything into memory) | △ | — | Streaming decode of Index/Tables/Tile-*.iwa | StreamingReader(contentsOf:) NumbersStreamingReader forEachRow(inSheet:table:) tableCount(inSheet:) | Warning: None (documented as values and formatting only) Only an index of the parts is built up front; each tile is decoded as the rows advance, then discarded. The 2nd and later tables on 1 sheet are reached with table:. Values and formatting only. Appendix B.40.3. measured |
| Row-by-row writing | — | △ | Index/Tables/Tile-*.iwa written every 256 rows | StreamingWriter(to:) NumbersStreamingWriter append(_:) close() warnings | Warning: Formulas become cached values, rich text becomes plain text, and links, notes and controls are dropped (with counts) Each tile is written into its envelope as soon as it fills, and let go. What remains is the string list, the style list and a row header of about 12 bytes per row. The table's width is that of the widest row; a row wider than the tiles already written is refused. Appendix B.42. measured |
Checking this table yourself
Rows marked "measured" are backed directly by the tests in the repository.
swift test --filter FormatSupport # writes the 48 features to every format and reads them back; pins what survives and the warning counts
swift test --filter CrossFormat # the per-direction (A → B) checks
swift test # everything
python3 scripts/build-spec-feature-matrix.py --check # does this table match its source?
To change the table, edit scripts/spec-feature-matrix.json and run python3 scripts/build-spec-feature-matrix.py. This HTML and the YAML with the same content are generated.