﻿# The spec feature matrix — SwiftSheets against the public specifications of Excel / ODS / Numbers
# Generated. Edit scripts/spec-feature-matrix.json and
# run scripts/build-spec-feature-matrix.py (do not edit this file by hand).
meta:
  title: "The spec feature matrix — SwiftSheets against the public specifications of Excel / ODS / Numbers"
  library_version: "0.28.0"
  as_of: "2026-09-11"
  commit: "0.28.0"
  axis: "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 docs/format-support.html (the model's 48 features × 3 formats), which provides the measured evidence behind this table."
  granularity: "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."
  caveat: "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)."
  environments:
    - "macOS 15"
    - "Swift 6.2 / Xcode 26"
    - "openpyxl 3.1.5 (judge for XLSX)"
    - "LibreOffice 26.2.3 (judge for ODF)"
    - "numbers-parser 4.16.3 / Numbers 15.3.1 (judge for Numbers)"
    - "Microsoft Excel 16.112.2 (judge for protection, Appendix B.31)"
  status_vocabulary:
    full:
      symbol: "○"
      meaning: "Round-trips unchanged. Read in the model's own terms, and the same thing is written out"
    partial:
      symbol: "△"
      meaning: "Goes through in another shape. A degraded / substituted warning is always returned"
    preserved:
      symbol: "▣"
      meaning: "Not modelled. Saving to the same format keeps the original bytes (F3). Converting to another format drops it, with a warning"
    none:
      symbol: "×"
      meaning: "Does not go through. Dropped, and a dropped warning is returned"
    na:
      symbol: "—"
      meaning: "Not applicable: the format has no such concept"
    unverified:
      symbol: "?"
      meaning: "Not measured. The slot that keeps ○ or × from being written by guesswork"
  evidence_types:
    measured: "Measured by FormatSupportTests over the 48 features (written, read back and compared)"
    code: "Checked in the codec code (that the element names, the warning text and the preservation-list entry really exist)"
    readme: "Stated in README / the implementation spec, Appendix B"
    judge: "An external implementation could read it (openpyxl, LibreOffice, numbers-parser, Numbers.app, Excel)"
  common_api:
    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(_:)"
    warnings: "ConversionWarning(kind: .dropped/.degraded/.substituted, subject:, sheet:, location:, message:)"

formats:
  - id: "xlsx"
    name: "Excel（.xlsx / .xlsm）"
    spec:
      name: "ECMA-376 / ISO/IEC 29500 — Office Open XML SpreadsheetML"
      public: true
      note: "Public specification. The element names are the specification's own. §7 of the SwiftSheets implementation spec holds the mapping."
    totals:
      read: {full: 53, partial: 10, preserved: 11, none: 1, na: 3, unverified: 0}
      write: {full: 53, partial: 9, preserved: 12, none: 1, na: 3, unverified: 0}
      rows: 78
    areas:
      - id: "package"
        name: "Package and parts"
        features:
          - id: "xlsx.package.opc"
            name: "OPC package (ZIP, content types, relationships)"
            spec_anchor: "[Content_Types].xml, _rels/.rels"
            read: "full"
            write: "full"
            api:
              - "Workbook(contentsOf:)"
              - "wb.write(to:)"
            warning: null
            evidence: "measured"
            note: "On write, [Content_Types].xml declares only the parts that exist, and every r:id resolves (PreservationTests)."
          - id: "xlsx.package.preservation"
            name: "Preservation of uninterpreted parts (F3)"
            spec_anchor: "xl/charts/, xl/theme/, xl/drawings/, vbaProject.bin and others"
            read: "preserved"
            write: "preserved"
            api:
              - "wb.preservationSummary"
              - "PreservationSummary"
              - "preservationSummary.parts"
              - "PreservedPartKind"
            warning: "Converting to another format: \"N part(s) … cannot be carried into …\""
            evidence: "measured"
            note: "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)."
          - id: "xlsx.package.zip64"
            name: "ZIP64 (over 4 GB, over 65,535 parts)"
            spec_anchor: "ZIP64 end of central directory"
            read: "full"
            write: "full"
            api:
              - "ZipLimits"
              - "ReadOptions.limits"
            warning: null
            evidence: "measured"
            note: "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."
          - id: "xlsx.package.encryption"
            name: "Password encryption"
            spec_anchor: "ECMA-376 Part 2 agile / standard encryption（[MS-OFFCRYPTO]）"
            read: "partial"
            write: "full"
            api:
              - "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"
            evidence: "judge"
            note: "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."
      - id: "workbook"
        name: "Workbook (xl/workbook.xml)"
        features:
          - id: "xlsx.wb.sheets"
            name: "Sheet list, order, names, duplication"
            spec_anchor: "<sheets>/<sheet>"
            read: "full"
            write: "full"
            api:
              - "wb.sheets"
              - "wb.sheetNames"
              - "addSheet(named:at:)"
              - "removeSheet(named:)"
              - "moveSheet(named:to:)"
              - "duplicateSheet(named:as:)"
              - "renameSheet(_:to:)"
            warning: null
            evidence: "measured"
            note: "Renaming a sheet updates the formulas that refer to it."
          - id: "xlsx.wb.state"
            name: "Sheet visibility (visible, hidden, very hidden)"
            spec_anchor: "sheet@state"
            read: "full"
            write: "full"
            api:
              - "sheet.isHidden"
              - "sheet.state"
              - "SheetState"
            warning: null
            evidence: "measured"
            note: null
          - id: "xlsx.wb.definednames"
            name: "Defined names (workbook scope, sheet scope)"
            spec_anchor: "<definedNames>/<definedName localSheetId>"
            read: "full"
            write: "full"
            api:
              - "wb.definedNames"
              - "sheet.definedNames"
            warning: null
            evidence: "measured"
            note: "Print areas and title rows sit in the same place, as _xlnm.Print_Area / _xlnm.Print_Titles."
          - id: "xlsx.wb.epoch"
            name: "Date epoch (1900 / 1904)"
            spec_anchor: "workbookPr@date1904"
            read: "full"
            write: "full"
            api:
              - "wb.epoch"
              - "DateEpoch"
              - "ExcelDate"
            warning: null
            evidence: "measured"
            note: "The phantom day of 29 February 1900 is handled the same way as in openpyxl."
          - id: "xlsx.wb.calcpr"
            name: "Calculation properties and calculation chain"
            spec_anchor: "<calcPr calcMode iterate fullPrecision>, xl/calcChain.xml"
            read: "partial"
            write: "partial"
            api:
              - "wb.calculationSettings.calcMode"
              - "CalculationSettings.CalcMode"
            warning: null
            evidence: "measured"
            note: "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)."
          - id: "xlsx.wb.protection"
            name: "Workbook protection (sheet structure, windows)"
            spec_anchor: "<workbookProtection>"
            read: "full"
            write: "full"
            api:
              - "wb.protection"
              - "WorkbookProtection"
              - "setModernPassword(_:)"
              - "setPassword(_:)"
            warning: null
            evidence: "judge"
            note: "The modern scheme generates a SHA-512 hash. Confirmed over AppleScript that Excel 16.112.2 itself unlocks it (Appendix B.31)."
          - id: "xlsx.wb.views"
            name: "Workbook view (the active sheet)"
            spec_anchor: "<bookViews>/<workbookView activeTab>"
            read: "full"
            write: "full"
            api:
              - "wb.activeSheet"
              - "wb.activeIndex"
            warning: null
            evidence: "code"
            note: null
          - id: "xlsx.wb.external"
            name: "References to external workbooks"
            spec_anchor: "<externalReferences>, xl/externalLinks/externalLinkN.xml, externalLinkPath"
            read: "partial"
            write: "preserved"
            api:
              - "wb.externalLinks"
              - "ExternalLink"
              - "FormulaExpr ([1]Sheet!A1 is kept as text)"
            warning: "Converting to another format: a dropped warning (objects)"
            evidence: "measured"
            note: "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."
          - id: "xlsx.wb.props"
            name: "Document properties (author, title, dates)"
            spec_anchor: "docProps/core.xml, docProps/app.xml"
            read: "full"
            write: "full"
            api:
              - "wb.metadata"
              - "DocumentProperties"
              - "wb.sourceInfo"
            warning: null
            evidence: "code"
            note: "The name of the application that created the file is kept in sourceInfo.application."
          - id: "xlsx.wb.customprops"
            name: "Custom document properties"
            spec_anchor: "docProps/custom.xml"
            read: "full"
            write: "full"
            api:
              - "wb.customProperties"
              - "CustomDocumentProperty"
            warning: null
            evidence: "measured"
            note: "6 types: text, integer, real, boolean, date, and link to a defined name."
          - id: "xlsx.wb.misc"
            name: "File sharing, custom workbook views, smart tags, web publishing"
            spec_anchor: "<fileSharing>, <customWorkbookViews>, <smartTagPr>, <webPublishing>"
            read: "preserved"
            write: "preserved"
            api: []
            warning: "Converting to another format: dropped"
            evidence: "code"
            note: "Unknown child elements of <workbook> are written back at their place in schema order (WorkbookWriter.workbookOrder)."
      - id: "worksheet"
        name: "Worksheet (xl/worksheets/sheetN.xml)"
        features:
          - id: "xlsx.ws.cellvalues"
            name: "Cell values (number, text, boolean, error, inline string)"
            spec_anchor: "<c t=\"n|s|str|b|e|inlineStr|d\">"
            read: "full"
            write: "full"
            api:
              - "sheet[\"A1\"]"
              - "sheet[1, 2]"
              - "CellValue"
            warning: null
            evidence: "measured"
            note: "Integers and reals are split the same way as in openpyxl (.integer / .number(Decimal)). Dates are CivilDate, with no time zone."
          - id: "xlsx.ws.sst"
            name: "Shared strings"
            spec_anchor: "xl/sharedStrings.xml"
            read: "full"
            write: "full"
            api: []
            warning: null
            evidence: "code"
            note: "Strings are shared automatically on write. There is no API the user needs to think about."
          - id: "xlsx.ws.richtext"
            name: "Formatting within a cell (rich text)"
            spec_anchor: "<is>/<r>/<rPr>"
            read: "full"
            write: "full"
            api:
              - "CellValue.richText"
              - "TextRun"
            warning: null
            evidence: "measured"
            note: null
          - id: "xlsx.ws.furigana"
            name: "Furigana (ruby)"
            spec_anchor: "<rPh>, <phoneticPr>"
            read: "full"
            write: "full"
            api:
              - "sheet[cell: \"A1\"].phonetic"
              - "PhoneticText"
            warning: "Writing to ODS or Numbers: \"N phonetic guide(s) (furigana) dropped: … the text is kept\""
            evidence: "measured"
            note: "<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)."
          - id: "xlsx.ws.rows"
            name: "Row height, hidden, outline level"
            spec_anchor: "<row ht customHeight hidden outlineLevel collapsed>"
            read: "full"
            write: "full"
            api:
              - "sheet.rowDimension(_:)"
              - "setHeight(_:ofRow:)"
              - "RowDimension"
            warning: null
            evidence: "measured"
            note: null
          - id: "xlsx.ws.cols"
            name: "Column width, hidden, outline level, autofit"
            spec_anchor: "<cols>/<col width bestFit hidden outlineLevel>"
            read: "full"
            write: "full"
            api:
              - "setWidth(_:ofColumn:)"
              - "columnDimension(_:)"
              - "autofitColumns(maxWidth:)"
            warning: null
            evidence: "measured"
            note: "autofitColumns follows XlsxWriter's width table. East Asian characters are measured at double width (Appendix B.33)."
          - id: "xlsx.ws.outline"
            name: "Row and column grouping (outline)"
            spec_anchor: "row@outlineLevel, <outlinePr summaryBelow summaryRight>"
            read: "full"
            write: "full"
            api:
              - "groupRows(_:outlineLevel:hidden:)"
              - "groupColumns(_:_:outlineLevel:)"
              - "sheet.properties"
            warning: null
            evidence: "measured"
            note: null
          - id: "xlsx.ws.merge"
            name: "Merged cells"
            spec_anchor: "<mergeCells>/<mergeCell>"
            read: "full"
            write: "full"
            api:
              - "merge(_:)"
              - "unmerge(_:)"
              - "sheet.merges"
              - "mergedRange(containing:)"
            warning: null
            evidence: "measured"
            note: null
          - id: "xlsx.ws.hyperlink"
            name: "Hyperlinks"
            spec_anchor: "<hyperlinks>/<hyperlink r:id location tooltip>"
            read: "full"
            write: "full"
            api:
              - "sheet[cell: \"A1\"].hyperlink"
              - "Hyperlink"
            warning: null
            evidence: "measured"
            note: null
          - id: "xlsx.ws.freeze"
            name: "Frozen panes"
            spec_anchor: "<sheetViews>/<sheetView>/<pane state=\"frozen\">"
            read: "full"
            write: "full"
            api:
              - "sheet.freezePanes"
              - "freezePanes(at:)"
              - "freezePanes"
            warning: null
            evidence: "measured"
            note: "A split pane (split) is treated as frozen."
          - id: "xlsx.ws.sheetview"
            name: "Sheet view (zoom, gridlines shown, selection)"
            spec_anchor: "<sheetView zoomScale showGridLines tabSelected showRowColHeaders showZeros rightToLeft topLeftCell view>, <selection>"
            read: "full"
            write: "full"
            api:
              - "sheet.view"
              - "SheetView"
              - "SheetView.Kind"
            warning: null
            evidence: "measured"
            note: "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)."
          - id: "xlsx.ws.tabcolor"
            name: "Tab colour"
            spec_anchor: "<sheetPr>/<tabColor>"
            read: "full"
            write: "full"
            api:
              - "sheet.tabColor"
            warning: null
            evidence: "measured"
            note: null
          - id: "xlsx.ws.protection"
            name: "Sheet protection (a set of permissions naming what is allowed)"
            spec_anchor: "<sheetProtection>"
            read: "full"
            write: "full"
            api:
              - "sheet.protection"
              - "SheetProtection"
              - "setModernPassword(_:)"
            warning: null
            evidence: "judge"
            note: "The file records what is forbidden, but the API is named for what is allowed. Unlocking has been confirmed in Excel itself."
          - id: "xlsx.ws.protectedranges"
            name: "Editable windows in a protected sheet"
            spec_anchor: "<protectedRanges>/<protectedRange>"
            read: "full"
            write: "full"
            api:
              - "sheet.protectedRanges"
              - "ProtectedRange"
            warning: null
            evidence: "measured"
            note: "ODS and Numbers have none. Converting drops them, with a warning."
          - id: "xlsx.ws.scenarios"
            name: "Scenarios (recorded sets of input values)"
            spec_anchor: "<scenarios>/<scenario>/<inputCells>"
            read: "full"
            write: "full"
            api:
              - "sheet.scenarios"
              - "Scenario"
              - "ScenarioList"
            warning: null
            evidence: "measured"
            note: null
          - id: "xlsx.ws.watches"
            name: "Ignored errors, cell watches, custom sheet views, data consolidation"
            spec_anchor: "<ignoredErrors>, <cellWatches>, <customSheetViews>, <dataConsolidate>"
            read: "preserved"
            write: "preserved"
            api: []
            warning: "Converting to another format: dropped"
            evidence: "code"
            note: "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)."
          - id: "xlsx.ws.multitable"
            name: "Several tables on one sheet"
            spec_anchor: "—"
            read: "na"
            write: "na"
            api:
              - "sheet.tables"
            warning: "「N other table(s) not written: a worksheet holds a single grid (write .numbers to keep them)」"
            evidence: "measured"
            note: "A worksheet is a single grid. Of several tables coming from Numbers, all but the first are dropped, with a warning."
      - id: "formula"
        name: "Formulas"
        features:
          - id: "xlsx.formula.normal"
            name: "Formulas (held as a syntax tree)"
            spec_anchor: "<f>"
            read: "full"
            write: "full"
            api:
              - ".formula(\"=SUM(A1:B2)\")"
              - "FormulaExpr"
              - "value.formula"
            warning: null
            evidence: "measured"
            note: "References follow row insertion and deletion and sheet renames (openpyxl's do not)."
          - id: "xlsx.formula.shared"
            name: "Shared formulas"
            spec_anchor: "<f t=\"shared\" si ref>"
            read: "full"
            write: "full"
            api: []
            warning: null
            evidence: "code"
            note: "Expanded on read. Written out as ordinary formulas."
          - id: "xlsx.formula.array"
            name: "Array formulas (with their range)"
            spec_anchor: "<f t=\"array\" ref>"
            read: "full"
            write: "full"
            api:
              - "sheet.table.arrayFormulas[anchor] = CellRange(\"A2:A4\")"
            warning: null
            evidence: "measured"
            note: null
          - id: "xlsx.formula.cached"
            name: "Cached formula values"
            spec_anchor: "<c><v>"
            read: "full"
            write: "full"
            api:
              - ".formula(_, cached:)"
              - "ReadOptions(formulaCells: .cachedValues)"
            warning: null
            evidence: "code"
            note: "formulaCells: .cachedValues also offers a way to read the values alone."
          - id: "xlsx.formula.nesting"
            name: "Formula nesting (up to 64 levels)"
            spec_anchor: "Excel's own limit"
            read: "partial"
            write: "partial"
            api:
              - "FormulaExpr.unparsed"
            warning: "None (lossless as long as it is written back to the same format)"
            evidence: "readme"
            note: "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."
          - id: "xlsx.formula.remote"
            name: "Stock and currency functions (6 functions, such as STOCK and CURRENCY)"
            spec_anchor: "—"
            read: "na"
            write: "partial"
            api:
              - "FormulaExpr.remoteDataFunction"
            warning: "The cached value is written in place of the formula, with a warning saying so"
            evidence: "measured"
            note: "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)."
          - id: "xlsx.formula.unknownfn"
            name: "Unknown and newer functions (_xlfn prefix, dynamic arrays)"
            spec_anchor: "_xlfn.*"
            read: "partial"
            write: "partial"
            api:
              - "FormulaExpr"
            warning: "None (carried by name)"
            evidence: "code"
            note: "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."
      - id: "styles"
        name: "Styles (xl/styles.xml)"
        features:
          - id: "xlsx.style.cell"
            name: "Fonts, fills, borders, alignment, number formats"
            spec_anchor: "<fonts> <fills> <borders> <numFmts> <cellXfs>"
            read: "full"
            write: "full"
            api:
              - "sheet.style(\"A1\") { $0.font.bold = true }"
              - "CellStyle"
              - "Font"
              - "Fill"
              - "Border"
              - "Alignment"
            warning: null
            evidence: "measured"
            note: "Writing appends to the original font / fill / border tables, so entries referenced by index do not move."
          - id: "xlsx.style.gradient"
            name: "Gradient fills"
            spec_anchor: "<gradientFill>"
            read: "full"
            write: "full"
            api:
              - "Fill.gradient(_:)"
              - "GradientFill"
            warning: null
            evidence: "measured"
            note: "ODS and Numbers can hold only one colour per cell, so converting keeps the first colour and returns a warning."
          - id: "xlsx.style.named"
            name: "Named cell styles"
            spec_anchor: "<cellStyles>, <cellStyleXfs>"
            read: "full"
            write: "full"
            api:
              - "wb.namedStyles"
              - "addNamedStyle(_:)"
              - "CellStyle.namedStyle"
            warning: null
            evidence: "code"
            note: null
          - id: "xlsx.style.dxf"
            name: "Differential formats (the formats conditional formatting and tables point to)"
            spec_anchor: "<dxfs>/<dxf>"
            read: "full"
            write: "full"
            api:
              - "wb.differentialStyles"
              - "DifferentialStyle"
              - "DifferentialFont"
            warning: null
            evidence: "code"
            note: "Every field is optional. nil means \"leave as is\"."
          - id: "xlsx.style.theme"
            name: "Theme (colours, fonts)"
            spec_anchor: "xl/theme/theme1.xml, Color.theme"
            read: "full"
            write: "full"
            api:
              - "wb.theme"
              - "Theme"
              - "wb.rgb(of:)"
              - "Color.theme(_:tint:)"
              - "Color.indexed(_:)"
              - "wb.indexedColors"
            warning: null
            evidence: "measured"
            note: "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)."
          - id: "xlsx.style.tablestyles"
            name: "Table style definitions"
            spec_anchor: "<tableStyles>"
            read: "preserved"
            write: "preserved"
            api: []
            warning: null
            evidence: "code"
            note: "Copied as is. Keeping the indexes from shifting comes first."
      - id: "cf"
        name: "Conditional formatting"
        features:
          - id: "xlsx.cf.rules"
            name: "17 kinds of rule (comparison, formula, text, top/bottom, above/below average, duplicate/unique, blank/error, time period)"
            spec_anchor: "<conditionalFormatting>/<cfRule type operator>"
            read: "full"
            write: "full"
            api:
              - "sheet.addConditionalFormatting(.cellIs(.greaterThan, \"100\", paint: red), over: \"B2:B99\")"
              - "ConditionalFormattingRule.Kind"
            warning: null
            evidence: "measured"
            note: "Priorities are renumbered 1…n within the sheet."
          - id: "xlsx.cf.scales"
            name: "Colour scales, data bars, icon sets"
            spec_anchor: "<colorScale> <dataBar> <iconSet> <cfvo>"
            read: "full"
            write: "full"
            api:
              - "ColorScale"
              - "DataBar"
              - "IconSet"
              - "ConditionalValue"
            warning: null
            evidence: "measured"
            note: null
          - id: "xlsx.cf.ext"
            name: "Extension conditional formatting (negative data bars, custom icons)"
            spec_anchor: "<cfRule><extLst> x14:id, <extLst> x14:conditionalFormattings / x14:dataBar / x14:iconSet"
            read: "partial"
            write: "partial"
            api:
              - "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\""
            evidence: "measured"
            note: "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."
      - id: "validation"
        name: "Data validation"
        features:
          - id: "xlsx.dv.rules"
            name: "Data validation (list, number, date, text length, custom)"
            spec_anchor: "<dataValidations>/<dataValidation type operator>"
            read: "full"
            write: "full"
            api:
              - "sheet.dataValidations = [.list(\"'Choices'!$A$2:$A$4\", over: MultiCellRange(\"C4:C99\")!)]"
              - "DataValidation"
            warning: null
            evidence: "measured"
            note: "hidesDropDown is named for what the inverted showDropDown attribute actually means."
          - id: "xlsx.dv.ext"
            name: "Extension data validation (such as a list pointing straight at another sheet)"
            spec_anchor: "<extLst> x14:dataValidations"
            read: "partial"
            write: "partial"
            api:
              - "sheet.hasUnmodelledValidations"
            warning: "A flag is set"
            evidence: "code"
            note: "The file's own block is kept and written back."
      - id: "tables"
        name: "Tables, filters, sorting"
        features:
          - id: "xlsx.table.part"
            name: "Named tables"
            spec_anchor: "xl/tables/tableN.xml, <tableParts>"
            read: "full"
            write: "full"
            api:
              - "sheet.structuredTables"
              - "addStructuredTable(named:over:)"
              - "StructuredTable"
              - "TableStyleInfo"
            warning: "A duplicate name: \"named table not written\""
            evidence: "measured"
            note: "The part, its content type, its relationship and <tableParts> are all generated."
          - id: "xlsx.filter.auto"
            name: "AutoFilter (the range)"
            spec_anchor: "<autoFilter ref>"
            read: "full"
            write: "full"
            api:
              - "sheet.autoFilter"
              - "autoFilter"
            warning: null
            evidence: "measured"
            note: null
          - id: "xlsx.filter.columns"
            name: "Filter criteria (value list, comparison, colour, icon, dynamic, top 10, date group)"
            spec_anchor: "<filterColumn> <filters> <customFilters> <colorFilter> <iconFilter> <dynamicFilter> <top10>"
            read: "full"
            write: "full"
            api:
              - "sheet.filterColumns"
              - "FilterColumn"
              - "RankFilter"
              - "DynamicFilter"
              - "ColorFilter"
              - "IconFilter"
              - "DateGroup"
            warning: null
            evidence: "measured"
            note: null
          - id: "xlsx.filter.ext"
            name: "Extension filters"
            spec_anchor: "<extLst> x14:filter"
            read: "preserved"
            write: "preserved"
            api:
              - "sheet.hasUnmodelledFilters"
            warning: "A flag is set"
            evidence: "code"
            note: "The original XML is kept as is."
          - id: "xlsx.sort"
            name: "Recorded sort"
            spec_anchor: "<sortState>/<sortCondition>"
            read: "full"
            write: "full"
            api:
              - "sheet.sortState"
              - "SortState"
              - "SortCondition"
            warning: null
            evidence: "measured"
            note: null
          - id: "xlsx.slicer"
            name: "Slicers"
            spec_anchor: "xl/slicers/, xl/slicerCaches/"
            read: "preserved"
            write: "preserved"
            api: []
            warning: "Converting to another format: dropped (objects)"
            evidence: "code"
            note: "Not modelled. Saved to the same format, the parts stay as they are."
      - id: "pivot"
        name: "Pivot tables"
        features:
          - id: "xlsx.pivot.table"
            name: "Pivot table layout (rows, columns, values, report filter)"
            spec_anchor: "xl/pivotTables/pivotTableN.xml"
            read: "full"
            write: "full"
            api:
              - "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\""
            evidence: "measured"
            note: "The layout is written, the figures are not. The application that opens the file recomputes them from the source range."
          - id: "xlsx.pivot.cache"
            name: "Pivot cache (definition and records)"
            spec_anchor: "xl/pivotCache/pivotCacheDefinitionN.xml, pivotCacheRecordsN.xml"
            read: "full"
            write: "full"
            api:
              - "PivotCache"
              - "PivotCacheField"
              - "PivotItem"
            warning: null
            evidence: "code"
            note: "The records part is carried as XML. A defect that broke it on the second save was fixed on 2026-08-27."
      - id: "drawing"
        name: "Drawings and objects"
        features:
          - id: "xlsx.draw.image"
            name: "Embedded images (placed in a cell, fitted to a range)"
            spec_anchor: "xl/media/, xl/drawings/drawingN.xml, <twoCellAnchor> <oneCellAnchor>"
            read: "full"
            write: "full"
            api:
              - "sheet.addImage(try SheetImage(data:), at: \"B2\", sizing: .resizeCellToFit)"
              - "addImage(_:over:)"
              - "SheetImage"
              - "ImagePlacement"
              - "sheet.images"
              - "SheetImage.Anchor.absolute"
            warning: "Converting to another format: dropped"
            evidence: "measured"
            note: "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."
          - id: "xlsx.draw.chart"
            name: "Charts (column, bar, line, pie)"
            spec_anchor: "xl/charts/chartN.xml"
            read: "full"
            write: "partial"
            api:
              - "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"
            evidence: "measured"
            note: "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)."
          - id: "xlsx.draw.comment"
            name: "Notes (cell comments)"
            spec_anchor: "xl/comments1.xml + legacy VML"
            read: "full"
            write: "full"
            api:
              - "sheet[cell: \"A1\"].comment = CellNote(text, author:)"
              - "sheet.notes"
            warning: null
            evidence: "measured"
            note: "Both the comments part and its companion VML are written."
          - id: "xlsx.draw.threaded"
            name: "Threaded comments"
            spec_anchor: "xl/threadedComments/threadedCommentN.xml, xl/persons/person.xml, the mirror in comments"
            read: "full"
            write: "full"
            api:
              - "cell.thread"
              - "CommentThread"
              - "sheet.threads"
            warning: "To another format: \"N threaded comment(s) written as notes …\" (substituted)"
            evidence: "measured"
            note: "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)."
          - id: "xlsx.draw.shapes"
            name: "Shapes, text boxes, SmartArt, background images"
            spec_anchor: "<xdr:sp>, <xdr:cxnSp>, <xdr:grpSp>, dgm:*, <picture>"
            read: "partial"
            write: "partial"
            api:
              - "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\""
            evidence: "measured"
            note: "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)."
          - id: "xlsx.draw.ole"
            name: "OLE objects, ActiveX controls"
            spec_anchor: "<oleObjects>, <controls>"
            read: "preserved"
            write: "preserved"
            api: []
            warning: "Converting to another format: dropped"
            evidence: "code"
            note: null
          - id: "xlsx.draw.sparkline"
            name: "Sparklines"
            spec_anchor: "<extLst> x14:sparklineGroups, x14:sparklineGroup, x14:sparkline"
            read: "full"
            write: "full"
            api:
              - "sheet.sparklines"
              - "sheet.addSparkline(_:dataRange:at:)"
              - "SparklineGroup"
            warning: "To Numbers: \"N sparkline group(s) dropped: Numbers has no sparklines\""
            evidence: "measured"
            note: "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)."
      - id: "print"
        name: "Printing"
        features:
          - id: "xlsx.print.setup"
            name: "Paper, orientation, scale, first page number"
            spec_anchor: "<pageSetup>"
            read: "full"
            write: "full"
            api:
              - "sheet.pageSetup"
              - "PageSetup"
            warning: null
            evidence: "measured"
            note: null
          - id: "xlsx.print.margins"
            name: "Margins, centering, printing gridlines and headings"
            spec_anchor: "<pageMargins>, <printOptions>"
            read: "full"
            write: "full"
            api:
              - "sheet.pageMargins"
              - "sheet.printOptions"
              - "PageMargins"
              - "PrintOptions"
            warning: null
            evidence: "measured"
            note: null
          - id: "xlsx.print.headerfooter"
            name: "Header/footer"
            spec_anchor: "<headerFooter> oddHeader/&L&C&R"
            read: "full"
            write: "full"
            api:
              - "sheet.headerFooter"
              - "HeaderFooter"
            warning: null
            evidence: "measured"
            note: "Held as Excel's format-code string (not broken apart)."
          - id: "xlsx.print.breaks"
            name: "Page breaks"
            spec_anchor: "<rowBreaks>, <colBreaks>"
            read: "full"
            write: "full"
            api:
              - "sheet.rowBreaks"
              - "sheet.columnBreaks"
            warning: null
            evidence: "measured"
            note: null
          - id: "xlsx.print.area"
            name: "Print area, repeated title rows/columns"
            spec_anchor: "_xlnm.Print_Area, _xlnm.Print_Titles"
            read: "full"
            write: "full"
            api:
              - "setPrintArea(_:)"
              - "sheet.printArea"
              - "sheet.printTitleRows"
              - "sheet.printTitleColumns"
            warning: null
            evidence: "measured"
            note: null
      - id: "macro"
        name: "Macros and external connections"
        features:
          - id: "xlsx.macro.vba"
            name: "VBA macros (.xlsm)"
            spec_anchor: "xl/vbaProject.bin"
            read: "preserved"
            write: "preserved"
            api:
              - "wb.write(to:as: .xlsm)"
            warning: "Writing to .xlsx: \"VBA project dropped: macros cannot be kept in .xlsx\""
            evidence: "measured"
            note: "Only kept as opaque bytes; never executed."
          - id: "xlsx.macro.connections"
            name: "External data connections, query tables, web queries"
            spec_anchor: "xl/connections.xml, xl/queryTables/"
            read: "preserved"
            write: "preserved"
            api: []
            warning: "Converting to another format: dropped"
            evidence: "code"
            note: null
          - id: "xlsx.macro.richvalue"
            name: "Rich values (data types such as stocks and geography)"
            spec_anchor: "c@cm, c@vm, xl/richData/"
            read: "none"
            write: "none"
            api: []
            warning: "None (the README's known limits)"
            evidence: "readme"
            note: "The cm / vm attributes are outside preservation. This is an honest gap, and the README says so."
          - id: "xlsx.macro.customxml"
            name: "Custom XML parts"
            spec_anchor: "customXml/"
            read: "preserved"
            write: "preserved"
            api: []
            warning: "Converting to another format: dropped"
            evidence: "code"
            note: null
      - id: "sheettypes"
        name: "Sheets other than worksheets"
        features:
          - id: "xlsx.sheettype.chartsheet"
            name: "Chart sheets, dialog sheets, macro sheets"
            spec_anchor: "xl/chartsheets/, <chartsheet>"
            read: "preserved"
            write: "preserved"
            api:
              - "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"
            evidence: "judge"
            note: "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)."
      - id: "streaming"
        name: "Large files"
        features:
          - id: "xlsx.stream.read"
            name: "Row-by-row reading (without loading everything into memory)"
            spec_anchor: "Streaming parse of xl/worksheets/sheetN.xml"
            read: "partial"
            write: "na"
            api:
              - "StreamingReader(contentsOf:)"
              - "forEachRow(inSheet:)"
              - "rows(inSheet:)"
              - "StreamedRow"
            warning: "None (documented as values and formatting only)"
            evidence: "readme"
            note: "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)."
          - id: "xlsx.stream.write"
            name: "Row-by-row writing"
            spec_anchor: "Same as above"
            read: "na"
            write: "partial"
            api:
              - "StreamingWriter(to:)"
              - "XLSXStreamingWriter"
              - "append(_:)"
              - "close()"
              - "warnings"
            warning: "None"
            evidence: "readme"
            note: "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)."
          - id: "xlsx.limit.cells"
            name: "Cell count limit"
            spec_anchor: "ReadOptions.cellLimit (no limit by default — Appendix B.39.2)"
            read: "partial"
            write: "na"
            api:
              - "ReadOptions(cellLimit:)"
            warning: "A degraded warning says \"reading stopped here\""
            evidence: "readme"
            note: "Reading never stops partway in silence. The reader sets the limit, and Workbook.inspect provides what is needed to decide it."
  - id: "ods"
    name: "OpenDocument spreadsheet (.ods)"
    spec:
      name: "OASIS OpenDocument Format 1.3 — Part 3 (Schema) / Part 4 (OpenFormula)"
      public: true
      note: "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."
    totals:
      read: {full: 39, partial: 13, preserved: 0, none: 2, na: 5, unverified: 0}
      write: {full: 36, partial: 10, preserved: 0, none: 11, na: 2, unverified: 0}
      rows: 59
    areas:
      - id: "package"
        name: "Package"
        features:
          - id: "ods.package.zip"
            name: "ODF package (mimetype first, uncompressed)"
            spec_anchor: "mimetype, META-INF/manifest.xml"
            read: "full"
            write: "full"
            api:
              - "Workbook(contentsOf:)"
              - "wb.write(to:as: .ods)"
            warning: null
            evidence: "judge"
            note: "Follows the rule that an uncompressed mimetype comes first. Confirmed by LibreOffice being able to reopen the file."
          - id: "ods.package.parts"
            name: "The document's 4 parts"
            spec_anchor: "content.xml, styles.xml, meta.xml, settings.xml"
            read: "full"
            write: "full"
            api: []
            warning: null
            evidence: "code"
            note: "settings.xml is read and written too (view settings, such as frozen panes, live there)."
          - id: "ods.package.encryption"
            name: "Password encryption"
            spec_anchor: "manifest:encryption-data（ODF 1.3 §4.3）"
            read: "partial"
            write: "full"
            api:
              - "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"
            evidence: "judge"
            note: "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)."
          - id: "ods.package.opaque"
            name: "Embedded object and image parts"
            spec_anchor: "Pictures/, Object N/, ObjectReplacements/"
            read: "partial"
            write: "partial"
            api:
              - "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\""
            evidence: "measured"
            note: "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."
          - id: "ods.draw.image"
            name: "Embedded images (placed in a cell, fitted to a range)"
            spec_anchor: "Pictures/, draw:frame, draw:image, table:end-cell-address"
            read: "full"
            write: "full"
            api:
              - "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\""
            evidence: "measured"
            note: "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)."
      - id: "table"
        name: "Tables, rows, columns"
        features:
          - id: "ods.table.sheet"
            name: "Tables (sheets), names, visibility"
            spec_anchor: "table:table, table:name, table:display=\"false\""
            read: "full"
            write: "full"
            api:
              - "wb.sheets"
              - "sheet.name"
              - "sheet.isHidden"
            warning: null
            evidence: "measured"
            note: null
          - id: "ods.table.repeat"
            name: "Compression by repetition (identical rows and columns written once)"
            spec_anchor: "table:number-rows-repeated, table:number-columns-repeated"
            read: "full"
            write: "full"
            api:
              - "ReadOptions(cellLimit:)"
            warning: "Past the limit, a degraded warning says \"reading stopped here\""
            evidence: "code"
            note: "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."
          - id: "ods.table.rowcol"
            name: "Row height, column width, hidden"
            spec_anchor: "style:row-height, style:column-width, table:visibility"
            read: "full"
            write: "full"
            api:
              - "setHeight(_:ofRow:)"
              - "setWidth(_:ofColumn:)"
              - "RowDimension"
              - "ColumnDimension"
            warning: null
            evidence: "measured"
            note: null
          - id: "ods.table.group"
            name: "Row and column grouping (outline)"
            spec_anchor: "table:table-row-group (nested)"
            read: "full"
            write: "full"
            api:
              - "groupRows(_:outlineLevel:hidden:)"
              - "groupColumns(_:_:outlineLevel:)"
            warning: null
            evidence: "measured"
            note: "ODF expresses the depth of levels by nesting elements. Reading turns the nesting into a level number."
          - id: "ods.table.header"
            name: "Header rows, header columns"
            spec_anchor: "table:table-header-rows, table:table-header-columns"
            read: "full"
            write: "full"
            api:
              - "sheet.freezePanes"
            warning: null
            evidence: "code"
            note: null
          - id: "ods.table.multi"
            name: "Several tables on one sheet"
            spec_anchor: "Several table:table elements in sequence"
            read: "na"
            write: "na"
            api:
              - "sheet.tables"
            warning: "When the model holds several: \"N other table(s) not written\""
            evidence: "measured"
            note: "In ODF, 1 sheet = 1 grid. Of several tables coming from Numbers, all but the first are dropped, with a warning."
      - id: "cell"
        name: "Cells and values"
        features:
          - id: "ods.cell.value"
            name: "Cell values (float, percentage, currency, date, time, boolean, string)"
            spec_anchor: "office:value-type=\"float|percentage|currency|date|time|boolean|string\""
            read: "full"
            write: "full"
            api:
              - "sheet[\"A1\"]"
              - "CellValue"
            warning: null
            evidence: "measured"
            note: null
          - id: "ods.cell.currency"
            name: "Currency cell type (which currency, held as data)"
            spec_anchor: "office:value-type=\"currency\", office:currency"
            read: "full"
            write: "full"
            api:
              - "CellStyle.numberFormat"
            warning: "To another format: dropped, \"only OpenDocument has it\""
            evidence: "measured"
            note: "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."
          - id: "ods.cell.span"
            name: "Merged cells"
            spec_anchor: "table:number-columns-spanned / -rows-spanned"
            read: "full"
            write: "full"
            api:
              - "merge(_:)"
              - "sheet.merges"
            warning: null
            evidence: "measured"
            note: null
          - id: "ods.cell.text"
            name: "Formatting within a cell (rich text)"
            spec_anchor: "text:p / text:span"
            read: "full"
            write: "full"
            api:
              - "CellValue.richText"
              - "TextRun"
            warning: null
            evidence: "measured"
            note: null
          - id: "ods.cell.link"
            name: "Hyperlinks"
            spec_anchor: "text:a xlink:href"
            read: "full"
            write: "full"
            api:
              - "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\""
            evidence: "measured"
            note: "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."
          - id: "ods.cell.note"
            name: "Notes (annotations)"
            spec_anchor: "office:annotation"
            read: "full"
            write: "full"
            api:
              - "sheet[cell: \"A1\"].comment"
              - "CellNote"
            warning: null
            evidence: "measured"
            note: null
          - id: "ods.cell.control"
            name: "Cell controls (checkboxes and the like)"
            spec_anchor: "office:forms / form:control"
            read: "none"
            write: "none"
            api:
              - "Cell.control"
              - "CellControl"
            warning: "「N cell control(s) (checkbox, stepper, slider, rating) dropped: ODF has …」"
            evidence: "measured"
            note: "On read, the contents of office:forms are skipped. Controls coming from Numbers are dropped, keeping only the value."
      - id: "formula"
        name: "Formulas (OpenFormula)"
        features:
          - id: "ods.formula.of"
            name: "Formulas (the OpenFormula dialect)"
            spec_anchor: "table:formula=\"of:=[.A1]\""
            read: "full"
            write: "full"
            api:
              - ".formula(\"=SUM(A1:B2)\")"
              - "value.formula?.rendered(as: .ods)"
              - "FormulaExpr"
            warning: null
            evidence: "measured"
            note: "Translated from the syntax tree into OpenFormula. The intersection operator is understood both as Excel's space and as OpenFormula's !."
          - id: "ods.formula.matrix"
            name: "Matrix formulas (array formulas)"
            spec_anchor: "table:number-matrix-columns-spanned / -rows-spanned"
            read: "full"
            write: "full"
            api:
              - "sheet.table.arrayFormulas"
            warning: null
            evidence: "measured"
            note: "The range is written on the anchor cell."
          - id: "ods.formula.untranslatable"
            name: "Formulas OpenFormula lacks, formulas that cannot be translated"
            spec_anchor: "—"
            read: "partial"
            write: "partial"
            api:
              - "FormulaExpr.unparsed"
            warning: "\"formula in … dialect could not be translated; cached value written\" and 3 others"
            evidence: "code"
            note: "A formula whose meaning would change in translation falls back to its cached value, and the warning says where and why."
          - id: "ods.formula.remote"
            name: "Stock and currency functions (6 functions, such as STOCK and CURRENCY)"
            spec_anchor: "—"
            read: "na"
            write: "partial"
            api:
              - "FormulaExpr.remoteDataFunction"
            warning: "「… fetches live data and OpenFormula has no such function; the cached value …」"
            evidence: "measured"
            note: "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)."
          - id: "ods.formula.named"
            name: "Named ranges, named expressions"
            spec_anchor: "table:named-range, table:named-expression"
            read: "full"
            write: "full"
            api:
              - "wb.definedNames"
              - "sheet.definedNames"
            warning: null
            evidence: "measured"
            note: null
      - id: "style"
        name: "Styles and data styles"
        features:
          - id: "ods.style.cell"
            name: "Cell styles (font, colour, borders, alignment, wrapping)"
            spec_anchor: "style:style family=\"table-cell\", fo:*, style:*"
            read: "full"
            write: "full"
            api:
              - "sheet.style(\"A1\") { … }"
              - "CellStyle"
            warning: null
            evidence: "measured"
            note: null
          - id: "ods.style.gradient"
            name: "Gradient fills"
            spec_anchor: "draw:gradient"
            read: "na"
            write: "partial"
            api:
              - "Fill.gradient(_:)"
            warning: "「gradient fill(s) written as their first colour: an ODF cell style has one …」"
            evidence: "measured"
            note: "An ODF cell style can hold only 1 colour. The first colour is used, with a warning."
          - id: "ods.style.datastyle"
            name: "Data styles (number formats)"
            spec_anchor: "number:number-style, number:date-style, number:currency-style and others"
            read: "partial"
            write: "partial"
            api:
              - "CellStyle.numberFormat"
              - "NumberFormat"
            warning: "「number format … has no ODF data style; General used」／「only its first section is written」"
            evidence: "measured"
            note: "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."
          - id: "ods.style.map"
            name: "Conditions attached to a style (conditional formatting in the core specification)"
            spec_anchor: "style:map"
            read: "full"
            write: "none"
            api:
              - "sheet.conditionalFormatting"
            warning: null
            evidence: "code"
            note: "A read-only path, kept for files from older generators that have no calcext:. Writing uses calcext: alone."
          - id: "ods.style.masterpage"
            name: "Master pages and page layouts"
            spec_anchor: "style:master-page, style:page-layout"
            read: "full"
            write: "full"
            api:
              - "sheet.pageSetup"
              - "sheet.pageMargins"
              - "sheet.headerFooter"
            warning: null
            evidence: "measured"
            note: "ODF has a master page per sheet. The print settings live there."
      - id: "cf"
        name: "Conditional formatting"
        features:
          - id: "ods.cf.calcext"
            name: "Conditional formatting, 18 kinds of rule (comparison, formula, text, top/bottom, above/below average, duplicate/unique, blank/error, time period)"
            spec_anchor: "calcext:conditional-formats / calcext:condition"
            read: "full"
            write: "full"
            api:
              - "sheet.addConditionalFormatting(_:over:)"
              - "ConditionalFormattingRule.Kind"
            warning: null
            evidence: "measured"
            note: "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."
          - id: "ods.cf.scales"
            name: "Colour scales, data bars, icon sets"
            spec_anchor: "calcext:color-scale, calcext:data-bar, calcext:icon-set"
            read: "full"
            write: "full"
            api:
              - "ColorScale"
              - "DataBar"
              - "IconSet"
            warning: null
            evidence: "measured"
            note: "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."
          - id: "ods.cf.unmodelled"
            name: "Conditional formatting the model could not fully read"
            spec_anchor: "—"
            read: "partial"
            write: "none"
            api:
              - "sheet.hasUnmodelledConditionalFormats"
            warning: "「a conditional format the model could not read is dropped: ODS is regenerated …」"
            evidence: "code"
            note: "ODS is regenerated every time, so the XLSX approach of putting the original block back is not available. It is dropped, with a warning."
      - id: "validation"
        name: "Data validation"
        features:
          - id: "ods.dv.rules"
            name: "Content validation (drop-down, number, date, text length, custom)"
            spec_anchor: "table:content-validation, table:condition, table:content-validation-name"
            read: "full"
            write: "full"
            api:
              - "sheet.dataValidations"
              - "DataValidation"
            warning: null
            evidence: "measured"
            note: "ODF lists the rules at the head of the document, and cells point at them by name."
          - id: "ods.dv.unmodelled"
            name: "Data validation the model could not fully read"
            spec_anchor: "—"
            read: "partial"
            write: "none"
            api:
              - "sheet.hasUnmodelledValidations"
            warning: "「a data validation the model could not read is dropped: ODS is regenerated …」"
            evidence: "code"
            note: null
      - id: "db"
        name: "Database ranges, filters, sorting"
        features:
          - id: "ods.db.range"
            name: "Database ranges (the counterpart of a structured table)"
            spec_anchor: "table:database-range"
            read: "full"
            write: "partial"
            api:
              - "sheet.structuredTables"
              - "addStructuredTable(named:over:)"
            warning: "「named table … written as an ODF database range: its banded-row style …」"
            evidence: "measured"
            note: "The name and the range go through. The banded-row style is dropped, with a warning."
          - id: "ods.db.filter"
            name: "Filters (filter criteria)"
            spec_anchor: "table:filter, table:filter-condition"
            read: "full"
            write: "partial"
            api:
              - "sheet.autoFilter"
              - "sheet.filterColumns"
            warning: "Colour, icon, dynamic and date-group filters do not exist in ODF and are dropped, with a warning"
            evidence: "measured"
            note: "Value lists and comparison criteria go through."
          - id: "ods.db.sort"
            name: "Recorded sort"
            spec_anchor: "table:sort, table:sort-by"
            read: "full"
            write: "full"
            api:
              - "sheet.sortState"
              - "SortState"
            warning: null
            evidence: "measured"
            note: null
          - id: "ods.db.unmodelled"
            name: "Filters the model could not fully read"
            spec_anchor: "—"
            read: "partial"
            write: "none"
            api:
              - "sheet.hasUnmodelledFilters"
            warning: "「an auto-filter the model could not read is dropped: ODS is regenerated …」"
            evidence: "code"
            note: null
      - id: "pilot"
        name: "Data pilot (pivot tables)"
        features:
          - id: "ods.pilot.table"
            name: "Data pilot tables"
            spec_anchor: "table:data-pilot-table, table:data-pilot-field"
            read: "full"
            write: "full"
            api:
              - "sheet.pivotTables"
              - "wb.addPivotTable(…)"
              - "PivotTable"
            warning: null
            evidence: "measured"
            note: "As with Excel's pivot tables, only the layout is written; the application that opens the file computes the figures."
      - id: "protect"
        name: "Protection"
        features:
          - id: "ods.protect.table"
            name: "Table protection"
            spec_anchor: "table:protected, table:protection-key"
            read: "full"
            write: "full"
            api:
              - "sheet.protection"
              - "SheetProtection"
            warning: null
            evidence: "measured"
            note: null
          - id: "ods.protect.ranges"
            name: "Editable windows in a protected sheet"
            spec_anchor: "—"
            read: "na"
            write: "none"
            api:
              - "sheet.protectedRanges"
            warning: "「N protected range(s) dropped: ODF protects a whole table …」"
            evidence: "measured"
            note: "ODF protects the whole table and has no windows inside it."
          - id: "ods.protect.workbook"
            name: "Workbook protection (locking the sheet structure)"
            spec_anchor: "table:structure-protected on office:spreadsheet (ODF 1.3 §9.1.2)"
            read: "full"
            write: "full"
            api:
              - "wb.protection.locksStructure"
            warning: null
            evidence: "judge"
            note: "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."
      - id: "print"
        name: "Printing"
        features:
          - id: "ods.print.page"
            name: "Orientation, paper, scale, margins, centering"
            spec_anchor: "style:page-layout-properties, style:print-orientation, style:scale-to"
            read: "full"
            write: "full"
            api:
              - "sheet.pageSetup"
              - "sheet.pageMargins"
            warning: null
            evidence: "measured"
            note: null
          - id: "ods.print.headerfooter"
            name: "Header/footer (3 regions: left, centre, right)"
            spec_anchor: "style:header, style:footer, style:region-left/center/right"
            read: "full"
            write: "partial"
            api:
              - "sheet.headerFooter"
            warning: "Only font directives are dropped"
            evidence: "measured"
            note: "Excel's &L&C&R and ODF's 3 regions are translated both ways."
          - id: "ods.print.range"
            name: "Print area, title rows/columns"
            spec_anchor: "table:print-ranges, table:print"
            read: "full"
            write: "full"
            api:
              - "setPrintArea(_:)"
              - "sheet.printTitleRows"
            warning: null
            evidence: "measured"
            note: null
          - id: "ods.print.break"
            name: "Page breaks"
            spec_anchor: "fo:break-before (on row and column styles)"
            read: "full"
            write: "full"
            api:
              - "sheet.rowBreaks"
              - "sheet.columnBreaks"
            warning: null
            evidence: "measured"
            note: null
      - id: "odfonly"
        name: "What only ODF has (Appendix B.17)"
        features:
          - id: "ods.only.label"
            name: "Label ranges (a heading used as is in a formula)"
            spec_anchor: "table:label-ranges / table:label-range"
            read: "full"
            write: "full"
            api:
              - "wb.labelRanges"
              - "LabelRange"
            warning: "To another format: \"… is dropped: only OpenDocument has it\""
            evidence: "measured"
            note: "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."
          - id: "ods.only.consolidation"
            name: "Consolidation definitions"
            spec_anchor: "table:consolidation"
            read: "full"
            write: "full"
            api:
              - "wb.consolidation"
              - "Consolidation"
            warning: "To another format: dropped"
            evidence: "measured"
            note: "\"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."
          - id: "ods.only.detective"
            name: "Detective arrows (tracing precedents and dependents)"
            spec_anchor: "table:detective, table:highlighted-range, table:operation"
            read: "full"
            write: "full"
            api:
              - "table.detective"
              - "CellDetective"
            warning: "To another format: dropped"
            evidence: "measured"
            note: "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."
          - id: "ods.only.calcsettings"
            name: "Calculation settings (regular expressions, wildcards, case sensitivity, two-digit years, precision as displayed)"
            spec_anchor: "table:calculation-settings, table:use-regular-expressions, table:null-year"
            read: "full"
            write: "full"
            api:
              - "wb.calculationSettings"
              - "CalculationSettings"
            warning: "To another format: \"a calculation setting is dropped — …; only OpenDocument keeps it in the file\""
            evidence: "measured"
            note: "Settings that change the meaning of a search criterion. In Excel they are application settings and never enter the file."
          - id: "ods.only.nulldate"
            name: "Date epoch (any date can be the origin)"
            spec_anchor: "table:null-date"
            read: "full"
            write: "full"
            api:
              - "wb.epoch"
              - "DateEpoch(origin:)"
            warning: null
            evidence: "measured"
            note: "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)."
      - id: "unmodelled"
        name: "What ODF has and the model has no words for"
        features:
          - id: "ods.un.tracked"
            name: "Tracked changes (per-cell revisions)"
            spec_anchor: "table:tracked-changes"
            read: "partial"
            write: "none"
            api:
              - "wb.unmodelledODFFeatures"
              - "UnmodelledODFFeatures.trackedChanges"
            warning: "「tracked changes (a document's revision history) is dropped: … the model has no word for it」"
            evidence: "code"
            note: "Their presence is read, and writing says \"it was there but was dropped\". The contents are not carried."
          - id: "ods.un.dde"
            name: "DDE links (another running application supplies the values)"
            spec_anchor: "table:dde-links"
            read: "partial"
            write: "none"
            api:
              - "UnmodelledODFFeatures.ddeLinks"
            warning: "Same as above"
            evidence: "code"
            note: null
          - id: "ods.un.linkedsheet"
            name: "Sheets linked in from another document"
            spec_anchor: "table:table-source"
            read: "partial"
            write: "none"
            api:
              - "UnmodelledODFFeatures.linkedSheet"
            warning: "Same as above"
            evidence: "code"
            note: null
          - id: "ods.un.linkedrange"
            name: "Ranges linked in from another document"
            spec_anchor: "table:cell-range-source"
            read: "partial"
            write: "none"
            api:
              - "UnmodelledODFFeatures.linkedRange"
            warning: "Same as above"
            evidence: "code"
            note: null
          - id: "ods.un.draw"
            name: "Drawing objects (charts, shapes, text boxes)"
            spec_anchor: "draw:frame, draw:object, draw:custom-shape, draw:line, draw:text-box"
            read: "partial"
            write: "partial"
            api:
              - "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"
            evidence: "measured"
            note: "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."
          - id: "ods.un.scenario"
            name: "Scenarios"
            spec_anchor: "table:scenario"
            read: "none"
            write: "none"
            api:
              - "sheet.scenarios"
            warning: "「N scenario(s) dropped: an ODF scenario is a whole shadow sheet …」"
            evidence: "measured"
            note: "An ODF scenario is a \"shadow sheet\", not a set of input values. Writing one would add a sheet, so none is written."
          - id: "ods.un.tabcolor"
            name: "Tab colour"
            spec_anchor: "style:table-properties@table:tab-color"
            read: "full"
            write: "full"
            api:
              - "sheet.tabColor"
            warning: null
            evidence: "measured"
            note: "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)."
      - id: "streaming"
        name: "Large files"
        features:
          - id: "ods.stream.read"
            name: "Row-by-row reading (without loading everything into memory)"
            spec_anchor: "Streaming parse of content.xml"
            read: "partial"
            write: "na"
            api:
              - "StreamingReader(contentsOf:)"
              - "ODSStreamingReader"
              - "forEachRow(inSheet:)"
              - "rows(inSheet:)"
            warning: "None (documented as values and formatting only)"
            evidence: "measured"
            note: "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."
          - id: "ods.stream.write"
            name: "Row-by-row writing"
            spec_anchor: "content.xml (office:automatic-styles before the tables)"
            read: "na"
            write: "partial"
            api:
              - "StreamingWriter(to:)"
              - "ODSStreamingWriter"
              - "append(_:)"
              - "close()"
              - "warnings"
            warning: "Formatting and colours ODF lacks: the same warnings as the whole-model writer"
            evidence: "measured"
            note: "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."
  - id: "numbers"
    name: "Apple Numbers（.numbers）"
    spec:
      name: "No public specification — the Numbers User Guide (Help) + observation of real files"
      public: false
      note: "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."
    totals:
      read: {full: 22, partial: 15, preserved: 0, none: 7, na: 15, unverified: 0}
      write: {full: 20, partial: 11, preserved: 0, none: 24, na: 4, unverified: 0}
      rows: 59
    areas:
      - id: "package"
        name: "Document and package"
        features:
          - id: "num.package.iwa"
            name: "Reading and writing documents (IWA: Snappy compression + Protobuf)"
            spec_anchor: "Index/*.iwa, Index/Document.iwa"
            read: "full"
            write: "full"
            api:
              - "Workbook(contentsOf:)"
              - "wb.write(to:as: .numbers)"
              - "Codec.numbers"
            warning: null
            evidence: "judge"
            note: "Handled as a dependency-free, dynamic Protobuf tree (ProtoMessage). Unknown fields round-trip unchanged, byte for byte (NumbersIWATests.fixturesRoundTripByteForByte)."
          - id: "num.package.template"
            name: "The basis for writing (the template-and-patch approach)"
            spec_anchor: "Resources/empty.numbers"
            read: "na"
            write: "partial"
            api:
              - "Codec.numbers"
            warning: "A feature that needs a structure the template lacks is dropped, with a warning naming it"
            evidence: "readme"
            note: "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)."
          - id: "num.package.encryption"
            name: "Document password protection"
            spec_anchor: ".iwph (the marker of an encrypted document)"
            read: "none"
            write: "none"
            api:
              - "SheetError.unopenable"
            warning: "throw「the Numbers document is password-protected (an .iwph package)」"
            evidence: "code"
            note: "A document protected with a password in Numbers is not opened."
          - id: "num.package.version"
            name: "Keeping up with Numbers versions"
            spec_anchor: "The version field of TN.DocumentArchive"
            read: "partial"
            write: "na"
            api:
              - "wb.readWarnings"
            warning: "「Numbers document version … is newer than the verified range; read as far as possible」"
            evidence: "code"
            note: "A document newer than the verified range is still read as far as it can be, with a warning saying so."
          - id: "num.package.metadata"
            name: "Document properties, custom properties"
            spec_anchor: "—"
            read: "na"
            write: "none"
            api:
              - "wb.metadata"
              - "wb.customProperties"
            warning: "「N custom document propert(ies) dropped: Numbers has no free-form document fields」"
            evidence: "measured"
            note: null
      - id: "sheet"
        name: "Sheets and canvas"
        features:
          - id: "num.sheet.sheets"
            name: "Sheets (name, order, several)"
            spec_anchor: "TN.SheetArchive"
            read: "full"
            write: "full"
            api:
              - "wb.sheets"
              - "addSheet(named:at:)"
            warning: null
            evidence: "measured"
            note: "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)."
          - id: "num.sheet.multitable"
            name: "Several tables on one sheet"
            spec_anchor: "Several TST.TableInfoArchive in sequence"
            read: "full"
            write: "full"
            api:
              - "sheet.tables"
              - "sheet.addTable(named:at:)"
              - "Table.anchor"
            warning: "To another format: \"N other table(s) not written: a worksheet holds a single grid\""
            evidence: "measured"
            note: "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."
          - id: "num.sheet.hidden"
            name: "Hidden sheets"
            spec_anchor: "—"
            read: "na"
            write: "none"
            api:
              - "sheet.isHidden"
            warning: "「Numbers has no hidden sheets; the sheet is visible」"
            evidence: "measured"
            note: null
          - id: "num.sheet.tabcolor"
            name: "Tab colour"
            spec_anchor: "—"
            read: "na"
            write: "none"
            api:
              - "sheet.tabColor"
            warning: "「the tab colour is dropped: Numbers tabs have no colour」"
            evidence: "measured"
            note: null
          - id: "num.sheet.background"
            name: "Sheet background colour, orientation"
            spec_anchor: "TN.SheetStyleArchive, TN.CommandSetSheetDirectionArchive"
            read: "none"
            write: "none"
            api: []
            warning: "None (the model has no counterpart)"
            evidence: "code"
            note: "It exists as an archive type, but the model has nothing to map it to. It is neither read nor written."
          - id: "num.sheet.form"
            name: "Forms (data-entry tabs made on iPhone / iPad)"
            spec_anchor: "TN.FormBasedSheetArchive"
            read: "none"
            write: "none"
            api:
              - "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"
            evidence: "measured"
            note: "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."
      - id: "table"
        name: "Tables"
        features:
          - id: "num.table.name"
            name: "Table name"
            spec_anchor: "TST.TableModelArchive.table_name"
            read: "full"
            write: "full"
            api:
              - "Table.name"
            warning: null
            evidence: "code"
            note: null
          - id: "num.table.position"
            name: "Table position (where it sits on the canvas)"
            spec_anchor: "B.85 (the geometry of TST.TableInfoArchive)"
            read: "full"
            write: "full"
            api:
              - "Table.position"
              - "Table.anchor"
              - "sheet.addTable(named:at:)"
            warning: null
            evidence: "measured"
            note: "Points (pt) are read and written as is. Placement order: position → a non-default anchor → below the previous table."
          - id: "num.table.header"
            name: "Header rows, header columns"
            spec_anchor: "number_of_header_rows / _columns, header_rows_frozen"
            read: "full"
            write: "full"
            api:
              - "sheet.freezePanes"
            warning: null
            evidence: "measured"
            note: "Round-tripped as the counterpart of Excel's frozen panes."
          - id: "num.table.footer"
            name: "Footer rows"
            spec_anchor: "number_of_footer_rows"
            read: "partial"
            write: "none"
            api: []
            warning: "None (only the footer designation is dropped)"
            evidence: "code"
            note: "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."
          - id: "num.table.size"
            name: "Row height, column width, hidden"
            spec_anchor: "Row and column info in TST.TableModelArchive"
            read: "full"
            write: "full"
            api:
              - "setHeight(_:ofRow:)"
              - "setWidth(_:ofColumn:)"
            warning: null
            evidence: "measured"
            note: null
          - id: "num.table.merge"
            name: "Merged cells"
            spec_anchor: "TST.MergeRegionMapArchive"
            read: "full"
            write: "full"
            api:
              - "merge(_:)"
              - "sheet.merges"
            warning: null
            evidence: "measured"
            note: null
          - id: "num.table.style"
            name: "Table styles (preset looks such as banded rows)"
            spec_anchor: "TST.TableStyleArchive, TST.TableStylePresetArchive"
            read: "none"
            write: "none"
            api:
              - "sheet.structuredTables"
            warning: "「N named table(s) dropped: every Numbers table is named, but its own header rows are not this frame」"
            evidence: "measured"
            note: "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."
          - id: "num.table.outline"
            name: "Row and column grouping (outline)"
            spec_anchor: "—"
            read: "na"
            write: "none"
            api:
              - "groupRows(_:outlineLevel:hidden:)"
            warning: "「row / column grouping is dropped: Numbers groups by category, not by outline level」"
            evidence: "measured"
            note: "Numbers groups by category. It has no idea of level depth."
      - id: "cell"
        name: "Cells and values"
        features:
          - id: "num.cell.value"
            name: "Cell values (number, text, date, boolean, duration, error)"
            spec_anchor: "Cell storage in TST.Tile / TST.TileRowInfo"
            read: "full"
            write: "full"
            api:
              - "sheet[\"A1\"]"
              - "CellValue"
            warning: null
            evidence: "measured"
            note: "Numbers stores numbers as decimal128. They are received as Decimal, so no digits are lost."
          - id: "num.cell.richtext"
            name: "Formatting within a cell (rich text)"
            spec_anchor: "TSWP.StorageArchive, TSWP.CharacterStyleArchive"
            read: "full"
            write: "full"
            api:
              - "CellValue.richText"
              - "TextRun"
            warning: null
            evidence: "measured"
            note: "Numbers holds it as the points where the character style changes."
          - id: "num.cell.link"
            name: "Hyperlinks"
            spec_anchor: "TSWP.HyperlinkFieldArchive"
            read: "partial"
            write: "partial"
            api:
              - "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\""
            evidence: "measured"
            note: "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."
          - id: "num.cell.note"
            name: "Notes (cell comments)"
            spec_anchor: "TSD.CommentStorageArchive, TSK.AnnotationAuthorArchive"
            read: "full"
            write: "full"
            api:
              - "sheet[cell: \"A1\"].comment"
              - "CellNote"
            warning: null
            evidence: "measured"
            note: "Kept in the table's comment list, with 1 shared \"author\" record per person who wrote one."
          - id: "num.cell.stock"
            name: "Stock cells (a STOCK formula in current Numbers)"
            spec_anchor: "—"
            read: "full"
            write: "full"
            api:
              - "FormulaExpr.remoteDataFunction"
            warning: null
            evidence: "judge"
            note: "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)."
      - id: "formula"
        name: "Formulas"
        features:
          - id: "num.formula.tree"
            name: "Formulas (read and written as Numbers expression trees)"
            spec_anchor: "TSCE.FormulaArchive, TSCE.ASTNodeArrayArchive"
            read: "full"
            write: "full"
            api:
              - ".formula(\"=SUM(A1:B2)\")"
              - "FormulaExpr"
            warning: null
            evidence: "judge"
            note: "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)."
          - id: "num.formula.cross"
            name: "References to another table"
            spec_anchor: "'Sheet::Table'!A1"
            read: "full"
            write: "full"
            api:
              - "FormulaExpr"
            warning: null
            evidence: "measured"
            note: null
          - id: "num.formula.unsupported"
            name: "Shapes with no sample to map onto (defined names, functions Numbers lacks, column ranges like A:C, intersection/union)"
            spec_anchor: "—"
            read: "na"
            write: "partial"
            api:
              - "value.formula"
              - "WriteResult.warnings"
            warning: "\"formula written as its cached value: …\" names where and why the formula fell back"
            evidence: "readme"
            note: "A shape with no real example in the local fixtures is not invented; it falls back to the cached value, with a warning."
          - id: "num.formula.remote"
            name: "Stock and currency functions (6 functions, such as STOCK, STOCKH and CURRENCY)"
            spec_anchor: "—"
            read: "full"
            write: "full"
            api:
              - "FormulaExpr.remoteDataFunction"
            warning: null
            evidence: "measured"
            note: "Formulas only Numbers can recalculate. They are written as formulas and fetched again when the document is opened (Appendix B.27)."
          - id: "num.formula.array"
            name: "Array formulas (with their expansion)"
            spec_anchor: "The formula on the anchor + an internal function on each covered cell"
            read: "full"
            write: "none"
            api:
              - "sheet.table.arrayFormulas"
            warning: "「N array formula(s) written as the anchor's formula and the covered cells' values …」"
            evidence: "measured"
            note: "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)."
      - id: "style"
        name: "Formatting"
        features:
          - id: "num.style.cell"
            name: "Cell formatting (font, colour, fill, borders, alignment, wrapping)"
            spec_anchor: "TST.CellStyleArchive, TST.CellStylePropertiesArchive"
            read: "full"
            write: "full"
            api:
              - "sheet.style(\"A1\") { … }"
              - "CellStyle"
            warning: null
            evidence: "measured"
            note: null
          - id: "num.style.font"
            name: "Font names"
            spec_anchor: "PostScript names (Helvetica Neue ⇄ HelveticaNeue)"
            read: "full"
            write: "full"
            api:
              - "Font.name"
            warning: null
            evidence: "code"
            note: "A mapping table is kept as a resource (fonts.json), and names are converted both ways."
          - id: "num.style.numfmt"
            name: "Number format (kind + decimal places + thousands separator)"
            spec_anchor: "TSK.FormatStructArchive"
            read: "partial"
            write: "partial"
            api:
              - "CellStyle.numberFormat"
              - "NumberFormat"
            warning: "「number format … has no Numbers equivalent」／「… its colours, conditions and negative section are dropped」"
            evidence: "measured"
            note: "Numbers can describe only 1 way of showing a value. Excel's multiple sections, colours and conditions are dropped, with a warning."
          - id: "num.style.gradient"
            name: "Gradient fills"
            spec_anchor: "TSD.FillArchive"
            read: "partial"
            write: "partial"
            api:
              - "Fill.gradient(_:)"
            warning: "Becomes the first colour, with a warning saying so"
            evidence: "measured"
            note: null
      - id: "cf"
        name: "Conditional formatting"
        features:
          - id: "num.cf.rules"
            name: "Conditional formatting, 14 kinds of rule (8 comparisons, 4 text rules, duplicate, unique)"
            spec_anchor: "predicate_type in TST.ConditionalStyleSetArchive"
            read: "full"
            write: "full"
            api:
              - "sheet.addConditionalFormatting(_:over:)"
              - "ConditionalFormattingRule.Kind"
            warning: "The remaining rules: \"conditional format … is dropped: Numbers has no rule of that kind\""
            evidence: "measured"
            note: "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)."
          - id: "num.cf.scales"
            name: "Colour scales, data bars, icon sets"
            spec_anchor: "—"
            read: "na"
            write: "none"
            api:
              - "ColorScale"
              - "DataBar"
              - "IconSet"
            warning: "dropped"
            evidence: "measured"
            note: "Numbers itself discards the same things when it imports an Excel file."
      - id: "control"
        name: "Data validation and cell controls"
        features:
          - id: "num.ctl.popup"
            name: "Pop-up menus"
            spec_anchor: "TST.PopUpMenuModel"
            read: "full"
            write: "partial"
            api:
              - "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\""
            evidence: "measured"
            note: "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)."
          - id: "num.ctl.controls"
            name: "Cell controls (checkbox, stepper, slider, rating)"
            spec_anchor: "Cell control info (the dial's minimum, maximum, step)"
            read: "full"
            write: "full"
            api:
              - "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 …\""
            evidence: "judge"
            note: "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)."
          - id: "num.ctl.other"
            name: "Controls the model has no word for"
            spec_anchor: "—"
            read: "partial"
            write: "na"
            api: []
            warning: "「… a Numbers control the model has no word for …; the value is kept, the control is not」"
            evidence: "code"
            note: null
      - id: "organise"
        name: "Organise (categories, filters, sorting)"
        features:
          - id: "num.org.category"
            name: "Categories (grouping by a column)"
            spec_anchor: "TST.GroupByArchive, TST.CategoryOwnerRefArchive"
            read: "partial"
            write: "none"
            api:
              - "wb.readWarnings"
            warning: "\"a category grouping by <column name> is dropped — the model has no word for Numbers categories …\""
            evidence: "measured"
            note: "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."
          - id: "num.org.filter"
            name: "Filters"
            spec_anchor: "TST.FilterSetArchive"
            read: "partial"
            write: "none"
            api:
              - "RowDimension.hidden"
            warning: "「a Numbers filter (N rule(s)) is dropped — the rows it hides are kept as hidden rows …」"
            evidence: "measured"
            note: "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)."
          - id: "num.org.sort"
            name: "Sorting"
            spec_anchor: "sort_order / rules"
            read: "partial"
            write: "none"
            api: []
            warning: "\"a Numbers sort order (N rule(s), by <column name>) is dropped — the rows already come back sorted\""
            evidence: "measured"
            note: "The rows are stored in sorted order. Only the rules are dropped."
      - id: "pivot"
        name: "Pivot tables"
        features:
          - id: "num.pivot.write"
            name: "Pivot tables (several levels on both axes, with subtotals)"
            spec_anchor: "TST.PivotOwnerArchive, TST.PivotOrderArchive, TST.SummaryModelArchive"
            read: "partial"
            write: "partial"
            api:
              - "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\""
            evidence: "judge"
            note: "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)."
          - id: "num.pivot.read"
            name: "Reading pivot tables"
            spec_anchor: "Same as above"
            read: "partial"
            write: "na"
            api:
              - "sheet.tables"
            warning: "None (read as an ordinary table with values)"
            evidence: "readme"
            note: "Read back, it becomes an ordinary table. The model does not hold the pivot mechanism."
          - id: "num.pivot.filterfield"
            name: "Report filter fields"
            spec_anchor: "—"
            read: "na"
            write: "none"
            api:
              - "PivotTable.pageFields"
            warning: "「N report filter(s) dropped — a Numbers pivot has no filter field, and Numbers drops them too …」"
            evidence: "code"
            note: null
      - id: "canvas"
        name: "Objects on the canvas"
        features:
          - id: "num.canvas.chart"
            name: "Charts"
            spec_anchor: "B.88 (TSCH.ChartDrawableArchive, TN.ChartMediatorArchive)"
            read: "partial"
            write: "partial"
            api:
              - "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"
            evidence: "measured"
            note: "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)."
          - id: "num.canvas.image"
            name: "Images"
            spec_anchor: "B.83 (TSD.ImageArchive, TSP.DataInfo)"
            read: "full"
            write: "full"
            api:
              - "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"
            evidence: "measured"
            note: "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)."
          - id: "num.canvas.shape"
            name: "Shapes, text boxes"
            spec_anchor: "B.83（TSWP.ShapeInfoArchive）"
            read: "partial"
            write: "partial"
            api:
              - "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)"
            evidence: "measured"
            note: "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)."
          - id: "num.canvas.movie"
            name: "Movies, audio"
            spec_anchor: "TSD.MovieArchive"
            read: "none"
            write: "none"
            api: []
            warning: "On read: \"a movie\""
            evidence: "code"
            note: null
          - id: "num.canvas.group"
            name: "Groups of shapes, connection lines"
            spec_anchor: "TSD.GroupArchive, TSD.ConnectionLineArchive"
            read: "none"
            write: "none"
            api: []
            warning: "On read: \"a group of objects\", \"a connection line\""
            evidence: "code"
            note: null
      - id: "misc"
        name: "Protection, printing, other"
        features:
          - id: "num.misc.protect"
            name: "Document password protection (writing)"
            spec_anchor: "—"
            read: "na"
            write: "none"
            api:
              - "wb.protection"
            warning: "「workbook protection is dropped: Numbers locks a document with a password, which this writer does not set」"
            evidence: "measured"
            note: null
          - id: "num.misc.sheetprotect"
            name: "Sheet protection, protected ranges"
            spec_anchor: "—"
            read: "na"
            write: "none"
            api:
              - "sheet.protection"
              - "sheet.protectedRanges"
            warning: "\"sheet protection is dropped: Numbers protects a whole document, not a sheet\" and others"
            evidence: "measured"
            note: null
          - id: "num.misc.print"
            name: "Printing (paper, orientation, margins, header/footer, title rows, print area, page breaks)"
            spec_anchor: "B.84, B.86 (TN.SheetArchive, TN.DocumentArchive)"
            read: "partial"
            write: "partial"
            api:
              - "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)"
            evidence: "measured"
            note: "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 %)."
          - id: "num.misc.definednames"
            name: "Defined names"
            spec_anchor: "—"
            read: "na"
            write: "none"
            api:
              - "wb.definedNames"
              - "sheet.definedNames"
            warning: "「N defined name(s) dropped: Numbers has no defined names」"
            evidence: "measured"
            note: null
          - id: "num.misc.scenario"
            name: "Scenarios"
            spec_anchor: "—"
            read: "na"
            write: "none"
            api:
              - "sheet.scenarios"
            warning: "「N scenario(s) dropped: Numbers has no scenarios」"
            evidence: "measured"
            note: null
          - id: "num.misc.epoch"
            name: "The 1904 date epoch"
            spec_anchor: "—"
            read: "na"
            write: "none"
            api:
              - "wb.epoch"
            warning: "「the 1904 date origin is dropped: the template has no calculation engine to record it on」"
            evidence: "code"
            note: null
          - id: "num.misc.vba"
            name: "VBA macros"
            spec_anchor: "—"
            read: "na"
            write: "none"
            api: []
            warning: "「VBA project dropped: Numbers has no place for it (write .xlsm to keep the macros)」"
            evidence: "code"
            note: null
          - id: "num.misc.collab"
            name: "Collaboration history"
            spec_anchor: "TSCK.* （collaboration）"
            read: "none"
            write: "none"
            api: []
            warning: "None (the model has no counterpart)"
            evidence: "code"
            note: "The type exists in the registry, but it is neither read nor written."
      - id: "streaming"
        name: "Large files"
        features:
          - id: "num.stream.read"
            name: "Row-by-row reading (without loading everything into memory)"
            spec_anchor: "Streaming decode of Index/Tables/Tile-*.iwa"
            read: "partial"
            write: "na"
            api:
              - "StreamingReader(contentsOf:)"
              - "NumbersStreamingReader"
              - "forEachRow(inSheet:table:)"
              - "tableCount(inSheet:)"
            warning: "None (documented as values and formatting only)"
            evidence: "measured"
            note: "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."
          - id: "num.stream.write"
            name: "Row-by-row writing"
            spec_anchor: "Index/Tables/Tile-*.iwa written every 256 rows"
            read: "na"
            write: "partial"
            api:
              - "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)"
            evidence: "measured"
            note: "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."
