Skip to content

Releases: nanbu/SwiftSheets

v0.28.0

Choose a tag to compare

@nanbu nanbu released this 13 Sep 10:31

What the two applications that depend on the library found when they moved to 0.27.0 (spec Appendix B.92), and two
peaks that had grown at ten million cells (Appendices B.93 and B.94). Two changes compile unchanged and behave
differently: reading row 0 or column 0 now stops, and a read stopped at ReadOptions.cellLimit gives the new warning
kind truncated. Both are under Changed and in Migrating to 1.0.

Added

  • ConversionWarning.Kind.truncated. A read that stops at ReadOptions.cellLimit gives it, about the sheet, in
    XLSX, ODS, Numbers and delimited text, so a caller refusing oversized files checks the kind rather than the wording.
  • SourceInfo.isVerifiedVersion says whether a Numbers document's declared version lies in the range this release
    was verified against; it is nil for the other formats and for a version that does not parse. It is on
    WorkbookSummary.producer too, so a row-by-row reader, which has no warnings, can ask inspect first.
  • CodecSet.streamingReader(contentsOf:format:limits:csv:) and StreamingReader(contentsOf:format:limits:csv:):
    a file walked row by row takes a format, as bytes already could. A compound file is still refused by name, and a
    folder is only ever a Numbers document. The SheetDecrypt form with a password is unchanged.
  • CivilDate(serial:epoch:) is the day CellValue(serial:epoch:) gives, and nil for a time of day, NaN or an
    infinity.

Changed

  • Reading row 0 or column 0 stops, as writing there already did. The integer and CellRef subscripts, cell(_:),
    rowDimension(_:) and columnDimension(_:) (with setWidth(_:ofColumn:) and setHeight(_:ofRow:), which are built
    on them) and CellRef.columnName(_:) stop on a number below 1. A - 1 left over from before 0.23.0 used to read an
    empty cell, and columnName(0) put $$2:$$4 into a formula. Text that does not parse still answers nil, as does
    columnName(validating:), and a malformed file never stops the process.
  • A read stopped at ReadOptions.cellLimit gives truncated instead of degraded, with the subject sheets and
    the same wording in every reader. A switch that lists every case of ConversionWarning.Kind needs the new one.

Fixed

  • The ODS writer's warning for a filter it drops names the sheet's column. It printed the filter's offset as a column,
    so a filter on the first column of C1:E9 read "the filter on column ".

  • The migration guide names the per-format row-by-row readers and writers that became internal in 0.20.0 and where
    their callers go, and says that tableNames(inSheet:) answers [String?].

  • A row-by-row read of a large XLSX file no longer holds an empty furigana slot for every shared string (spec
    Appendix B.93). The shared-string parser kept one of about 80 bytes per string whether or not it had furigana, and
    the row-by-row reader never read them: ten million cells with a hundred thousand strings peaked at 34.7 MB, and now
    peak at 19.6 MB in the same 10.2 s (the old and the new build run alternately five times each).

  • A whole read of a large ODS file no longer sorts every cell to find external links (spec Appendix B.94). The scan
    that numbers the documents formulas refer to built an array of every cell of each table: ten million cells peaked at
    1,339 MB in 27.5 s, and now peak at 1,091 MB in 25.3 s (the old and the new build run alternately five times each).

Performance

  • The performance record is measured again with Swift 6.4 (Xcode 27, macOS 27). At a million cells it follows
    the speed-ups of 0.27.0: reading XLSX takes 1.47 s at a 213 MB peak (3.76 s and 201 MB in the Swift 6.3.3 record),
    and eight sheets side by side 0.65 s at 229 MB (1.45 s and 228 MB). At ten million cells every operation is faster.
    Some whole-model operations there peak higher than the Swift 6.3.3 record says, but the commit that record was taken
    at, built and run alternately with this code on one machine, peaks the same: the difference is the machine's state,
    not the code, apart from the two peaks fixed above.

Full changelog: 0.27.0...0.28.0

v0.27.0

Choose a tag to compare

@nanbu nanbu released this 12 Sep 04:19

The last minor release before the 1.0 release candidate. Numbers gains pictures, shapes, text boxes, charts, the
print setup and a table's exact position on the canvas. The public surface has its final naming pass, and the
renames under Changed break source: each is a compile error at the line to change, mapped in
Migrating to 1.0. ReadOptions.cellLimit now holds for every reader, the codecs read and
write faster with the same bytes, and the documentation is English throughout, with a getting-started guide, a
cookbook whose recipes CI builds and runs, and llms.txt.

Added

  • Pictures, shapes and text boxes in Numbers (spec Appendix B.83). sheet.images and sheet.shapes are read
    from a Numbers sheet's canvas — the TSD.ImageArchive with its bytes under Data/, the TSWP.ShapeInfoArchive
    with its text, fill and outline — and written back as canvas objects: a picture becomes a data record and an
    image archive, a shape or a text box a rectangle path with a text storage of its own, every reference into the
    stylesheet declared the way Numbers declares it. Numbers places objects at a point, so an anchor reads back as
    .absolute; a cell anchor is written by summing the first table's rows and columns. Only the rectangle and the
    text box are drawn as measured — any other geometry is written as a rectangle and named (degraded), as is a
    text alignment. The reader's report for what stays out (a chart, a movie, a group, a picture that is not PNG /
    JPEG / GIF) is unchanged. ODS furigana was measured and stays reported: LibreOffice Calc discards text:ruby
    in a cell (spec Appendix B.70).

  • The print setup in Numbers (spec Appendix B.84). The sheet archive's own words are read and written:
    orientation, scale, margins, the first page number and the odd header / footer in Numbers' three zones (&L,
    &C, &R). &P becomes Numbers' page number — drawn in the centre of the footer, and said so when it was
    asked for elsewhere; other codes are dropped and named; even / first-page variants are dropped. The paper
    size, fit-to-pages, the print area, title rows and page breaks stay reported, now each by name. A Numbers
    document reads back with the footer Numbers prints (&C&P) and its scale (72 %).

  • Where a table stands on a Numbers canvas (spec Appendix B.85). Table.position (a CanvasPoint in points)
    is read exactly and written back; sheet.addTable(named:at:) places one. A table with a non-default anchor
    goes on the default 98 × 20 pt grid, and one with neither goes below the previous table as before.

  • The paper and the title rows in Numbers (spec Appendix B.86). The paper size goes onto the document (one per
    document; a sheet asking for another is said so) and comes back; title rows / columns from row 1 / column 1
    become the first table's header rows / columns repeated on every printed page. The print area and page breaks
    have no place and stay reported.

  • Ten shape geometries in Numbers (spec Appendix B.87): rectangle, rounded rectangle, ellipse, diamond,
    triangle, the four arrows and a line are drawn as bezier paths and recognised on the way back. Other presets
    are still rectangles, named with the list of what is drawn.

  • Charts in Numbers (spec Appendix B.88). sheet.charts is read from a Numbers canvas — the kind, title and
    legend, and each series' values, categories and name reference followed back through the chart mediator's
    formulas to the table cells — and written: a column, bar, line or pie chart becomes a chart drawable over the
    template's style preset, with its cached grid, its mediator and its registration with the calculation engine.
    Numbers opens the written chart, saves it again and exports it to Excel with the chart intact (judged). Chart
    gains frame (a CanvasRect in points); the XLSX and ODS writers place a chart that
    only has a frame over the cells it covers. A chart whose data is not linked to a table stays reported.

Changed

The last look at the public surface before 1.0 (spec Appendix B.89). Each change is a rename or a narrowing that the
compiler points at; no alias is kept.

  • SheetImage.Anchor.absolute(x:y:width:height:)SheetImage.Anchor.absolute(CanvasRect) — one rectangle type
    for a position on the canvas, the one Chart.frame already uses.
  • addSparkline(_:data:at:)addSparkline(_:dataRange:at:), now with a CellRef twin beside the A1 string, and
    SparklineGroup.Sparkline(dataRange:location:)SparklineGroup.Sparkline(dataRange:at:) — one label per role.
  • addTable(named:anchor:)addTable(named:at:) — the same label as the canvas-point form, told apart by type.
  • IconSet.Icon.setIconSet.Icon.setName, the word IconSet.name uses for the same kind of value.
  • PhoneticText.Run.start and endPhoneticText.Run.range, a Range<Int> of UTF-16 code units made with
    Run(_:over:). The reader clamps a file whose eb comes before its sb instead of trapping on it.
  • DataBar.isGradientDataBar.gradient, the attribute's own word, like percent and reverse.
  • Alignment.wrapTextAlignment.wrapsText and Alignment.shrinkToFitAlignment.shrinksToFit — the two
    imperative Bools 0.24.0's pass missed, spelled the way its fitToPagefitsToPage already is. A test now scans the
    whole public surface for the naming rules, so the next one is caught when it is written.
  • No longer public, because each answered a codec's question rather than a caller's: CommentThread.mirrorPrefix,
    Chart.anchorOrFrameCells, Chart.Kind.drawable (ask isDrawable), Shape.Geometry.presets (ask isPreset)
    and DataBar.usesExtension.
  • A shape made with Shape(_:) and appended by hand covers cell A1 as a span instead of borrowing a picture's
    .original size, so the XLSX and Numbers writers draw it at the same size.
  • The warning for preserved parts a conversion cannot carry names what they hold, from the inventory
    (…; what they hold: pivot 2, slicer 1); the ODS writer's no longer guesses "charts, drawings, VBA…".
  • Cell.init takes thread: like every other extra; CanvasPoint and CanvasRect are Codable;
    PreservationSummary is Hashable; assigning Theme.colors normalises to AARRGGBB, as the initialiser does.

Fixed

  • ReadOptions.cellLimit stops XLSX, Numbers and delimited-text reads too (spec Appendix B.90). It was documented
    as the cell budget for untrusted input in every format, but only the ODS reader counted. One budget covers the
    workbook, sheets parsed side by side included; a sheet that reaches it keeps the cells read so far and says so
    with a degraded warning. With no limit set, nothing is counted.

Performance

  • XLSX reads and writes a quarter to a third faster (spec Appendix B.91). Measured on a million cells, the old
    and the new build run alternately five times each at the same time: write 1.54 → 1.09 s and 254 → 214 MB peak,
    read 2.61 → 1.85 s at 216 MB, row-by-row read 1.85 → 1.35 s at 13 MB, row-by-row write 1.84 → 1.51 s at 11 MB,
    open-edit-save 3.87 → 2.59 s and 255 → 211 MB. A cell with the default style is written without hashing its style
    and read without copying one, a number is trimmed only when its ends are not plain ASCII, a read cell is stored
    once, a dense sheet's rows are written straight from its extent, dated cells share one style per format, and the
    writers' style caches are bounded. The bytes written are unchanged.
  • ODS, CSV and Numbers too (spec Appendix B.91), measured the same way: write ODS 3.39 → 2.25 s, row-by-row
    write ODS 3.52 → 2.50 s, read ODS 3.59 → 3.16 s, read CSV 1.76 → 1.32 s, row-by-row read CSV 1.31 → 0.87 s, write
    CSV 0.88 → 0.65 s, write Numbers 3.90 → 2.20 s, row-by-row write Numbers 3.57 → 3.07 s, each at the peak memory it
    had. The ODS writer works out a cell's style facts once per style, delimited text tests its rules on bytes with one
    locale, and the Numbers writer encodes an integer without long division and its style keys once per style.
  • Fewer copies in the model's own cells (spec Appendix B.91): a cell a reader or an append creates is made without
    comparing the default style with itself, and delimited text reserves its table for the fields it parsed. Building
    the model 0.57 → 0.47 s at 203 MB; read CSV 1.27 → 1.12 s and 238 → 165 MB peak; the XLSX, ODS and Numbers readers
    5–7% faster at the same peak.

Full changelog: 0.26.0...0.27.0

v0.26.0

Choose a tag to compare

@nanbu nanbu released this 11 Sep 06:16

The remaining entrances the 2026-09-10 review proposed are all in: shapes and text boxes, sparklines, threaded
comments, a link on part of a cell's text, Excel 2010's conditional-format extension, the sheet view's remaining
words and the calculation mode, an inventory of preserved parts, and the external-link list — plus the tab colour
in ODS. Every addition is additive; existing calls are unchanged.

Added

  • Shapes and text boxes. Shape — a preset geometry (.rectangle, .rightArrow, .line, … any of OOXML's
    187 presets by name, or .textBox), text with one font and alignment, a solid fill, an outline and an anchor —
    lives in sheet.shapes; addShape(_:over:) and addTextBox(_:over:font:) place one. XLSX reads xdr:sp /
    xdr:cxnSp and writes them back into the same drawing under the as-read rule (untouched: byte for byte; added:
    spliced; changed: rebuilt); ODS reads and writes draw:custom-shape, draw:line and draw:text-box. SmartArt
    and groups of shapes are not modelled: they stay bytes, and are named when a rebuild drops them. Numbers reports
    shapes as dropped (spec Appendix B.75).

  • Excel 2010's conditional-format extension. DataBar gains negativeColor, axisColor, axisPosition,
    direction, isGradient and borderColor; IconSet gains customIcons. XLSX folds the x14: extension into
    the rules on read — a data bar LibreOffice wrote is no longer "a rule the model could not say" — and writes
    the extension back beside the rules with fresh ids. ODS carries what its data bar can say and reports custom
    icons (spec Appendix B.82).

  • A link on part of a cell's text. TextRun.hyperlink carries the links ODS and Numbers hang on runs of
    text, so a cell with several links reads as rich text with each link on its run (the cell's own hyperlink is
    still the first); both formats write them back run by run. Excel holds one link per cell: its writer keeps the
    cell's own or the first run's and reports the rest (spec Appendix B.81).

  • Threaded comments. cell.thread holds a CommentThread — the opening comment, its replies, whether it is
    resolved, and when — beside the note. XLSX reads Excel's threadedComments and persons parts (hiding the
    "[Threaded comment]" mirror note older readers see), keeps them byte for byte until a change, and then
    regenerates the parts with each person's id kept. ODS and Numbers write the conversation as a note and report
    the substitution (spec Appendix B.80).

  • Sparklines. sheet.sparklines holds SparklineGroups (line / column / win-loss, colours, the points shown,
    empty-cell handling, one or more sparklines each drawing a range into a cell); addSparkline(_:data:at:) adds
    one. XLSX reads and writes the x14:sparklineGroups extension (untouched: byte for byte), ODS reads and writes
    LibreOffice's calcext:sparkline-groups. Numbers reports them as dropped (spec Appendix B.79).

  • An inventory of what the model does not represent. preservationSummary.parts counts the preserved
    material by kind (PreservedPartKind: .smartArt, .vbaProject, .slicer, .externalLink, .embeddedObject, …) — the answer to
    "what will a conversion drop?" before any write. Parts the model read (a sheet's drawing and its charts, the
    theme, the notes) are not listed (spec Appendix B.77).

  • The other workbooks the formulas refer to. wb.externalLinks lists them (ExternalLink: the number the
    formulas use, the file, the sheet names) — from OOXML's link parts, or from the documents ODS formulas name.
    Values are never resolved (spec Appendix B.78).

  • The sheet view's remaining words, and the calculation mode. SheetView gains showsRowColumnHeaders,
    showsZeros, rightToLeft, topLeftCell and kind (normal / page layout / page-break preview);
    CalculationSettings.calcMode is <calcPr calcMode> (auto / autoNoTable / manual). XLSX reads and writes all
    of them; ODS carries them through LibreOffice's settings (ShowGrid, ZoomValue, AutoCalculate, …) and the
    table style's writing mode, and reports the view kind as not carried (spec Appendix B.76).

  • ODS carries the tab colour. sheet.tabColor is written as table:tab-color on the table style — the
    attribute ODF 1.3 defines and LibreOffice writes — and read back; a theme colour is resolved to RGB first. The
    "ODF 1.3 has no tab colour" warning is gone, and the all-features workbook now returns 8 warnings for ODS, not 9
    (spec Appendix B.74).

v0.24.0

Choose a tag to compare

@nanbu nanbu released this 11 Sep 08:53

The consistency pass of the 1.0 API review: six groups of naming rules made uniform (spec Appendices B.62–B.67),
plus the selectedRanges rename 0.23.0 announced but did not ship. Names and labels only, no compatibility aliases;
the five places where behaviour changed are named in their entries.

Changed

  • The A1-string twins have twin labels (B.62). cell(at:), removeCell(at:), style(at:) and setStyle(at:_:)
    on Sheet and Table lose the at:sheet.cell(ref) beside sheet.cell("B2"), the way merge(_:), range(_:)
    and isMerged(_:) already were. addImage(_:at:) and subscript(cell:), whose two sides already shared a label,
    are unchanged.

  • Same operation, same word; same role, same label (B.63). CellRef.offset(rows:columns:)shifted(rows:columns:)
    (the word CellRange already used); CellRange.shrunk(right:bottom:left:top:)shrunk(right:down:left:up:) (the
    labels expanded already used); CodecSet.streamingWriter(url:format:) and StreamingWriter(url:format:)
    (to:as:) (the labels write(to:as:) and withStreamingWriter(to:as:) use); streamingReader(data:)
    streamingReader(_:) (as read(_:)); SheetFormat.detect(from:) and detect(from:filename:) → one
    detect(_:filename:), the shape probe(_:filename:) has, and detect(in:) is no longer public.

  • Bool properties read as assertions (B.64). Twenty-eight imperative spellings become the predicate form the other
    forty-five already had: includeStylesincludesStyles, includeBOMincludesBOM, allowBlankallowsBlank
    (and the list(choices:over:allowsBlank:) label), preserveUnknownPartspreservesUnknownParts, countCells
    countsCells, showGridLinesshowsGridLines, showValueshowsValue, the six show… flags of
    TableStyleInfo / PivotStyleInfo, showRowGrandTotals / showColumnGrandTotals / showAll / refreshOnLoad on the
    pivot types, showInputMessage / showErrorMessage / hideDropDown on DataValidation, lockStructure /
    lockWindows / lockRevision on WorkbookProtection, useWildcards / useRegularExpressions / useFirstPageNumber /
    linkToSourceData / fitToPage. File attributes are unchanged.

  • One name per thing, continued (B.65). values(in:) (documented as "same as rows(in:)") and Sheets.names
    (the same array as Workbook.sheetNames) are gone. freezePanes(at:) is gone: freezePanesA1 = "B2" is the string way
    in, and assigning "A1" clears like nil. The print settings' string twins are settable properties instead of getter
    plus setter method: printTitles + setPrintTitles(_:)printTitlesFormula: String?, printAreaFormula +
    setPrintArea(_:)printAreaFormula: String? (nil when unset, where it was ""); setPrintTitleRows(_:) and
    setPrintTitleColumns(_:) are gone — assign printTitleRows / printTitleColumns or the formula.

  • What Swift can say with a type is said with a type (B.66). hashValue: String? on SheetProtection,
    WorkbookProtection and ProtectedRange — the name Hashable owns — is saltedHash. validationError() -> String?
    on StructuredTable and PivotTable is validate() throws (SheetError.invalidWorkbook carries the reason). The
    ExcelDate namespace is gone: CellValue(serial:epoch:), cellValue.serial(epoch:), CellValue(iso8601:),
    cellValue.iso8601, civilDateTime.serial(epoch:), civilDate.serial(epoch:), Duration(serialDays:) and
    duration.serialDays replace fromSerial / toSerial / durationFromSerial / fromISO8601 / toISO8601. Five
    properties whose schema closes the set are enumerations instead of strings: Font.vertAlign
    verticalAlignment: Font.VerticalAlignment? (also on DifferentialFont), Font.scheme: Font.Scheme?,
    ConditionalFormattingRule.timePeriod: TimePeriod?, StructuredTableColumn.totalsRowFunction: TotalsRowFunction?,
    DynamicFilter.kind: Kind. Files are written with the same values; a value outside the schema is dropped on read
    and reported — hasUnmodelledFilters / hasUnmodelledConditionalFormats on the sheet, a dropped read warning
    naming the column for a totals-row function — never guessed at.

  • Plumbing leaves the public surface (B.67). CRC32, ZipInspection, TextEncodingSniffer, OOXMLEscape, Units,
    CellPixels, TextWidth, LegacyPasswordHash and ModernPasswordHash, with Table.cleanMergedRange(_:),
    WriteResult.suggest(from:target:options:), Workbook.noteUnmodelledODFFeatures(_:) and UnopenableInput.probe(in:),
    are package — the codecs' tools, not a caller's. setModernPassword(_:spinCount:) keeps its default of 100,000 as a
    literal. The last three abbreviations become words: SheetFormatProperties.baseColWidthbaseColumnWidth,
    defaultColWidthdefaultColumnWidth, PivotLocation.firstDataColfirstDataColumn.

Fixed

  • SheetView.sqref is now really selectedRanges. 0.23.0's notes, the spec (Appendix B.60) and the commit that
    renamed the other abbreviations all said SheetView.sqrefselectedRanges; the property itself stayed sqref,
    and the one test that looked at it expected the old name. A new contract test
    (APIContractTests.everyRenameTheChangelogAnnouncesExistsInTheCode) checks every oldnew rename in the
    newest CHANGELOG section against the code, so a promised name that does not exist fails before the tag.

v0.25.0

Choose a tag to compare

@nanbu nanbu released this 10 Sep 22:12

What the model could not read from a file until now, it reads: an XLSX file's pictures, charts, theme and furigana,
an ODS file's pictures and charts — and charts can be written into ODS. Every one of these is additive
(spec Appendices B.69–B.73); the one change to existing code is that three lists (Chart.Kind,
SheetImage.Format, DateEpoch) are structs now, so a switch over them needs a default. Untouched pictures,
charts and theme parts are still written back byte for byte.

Added

  • ODS pictures and charts are read, and charts are written (B.73). A source ODS's pictures (draw:frame in a
    cell, or among the sheet's table:shapes for a fixed position) fill sheet.images, and its chart objects
    (Object N/) fill sheet.charts — every class read, the four the writers draw by name; both are written back as
    fresh parts, so the "not re-linked" warning is left for embedded objects that are not charts. addChart now
    writes a chart document into ODS instead of dropping the chart; LibreOffice carries it into XLSX.

  • Pictures and charts are read (B.72). An opened XLSX file's drawing fills sheet.images and sheet.charts:
    one-cell, two-cell and absolute anchors (SheetImage.Anchor.absolute, in points), PNG / JPEG / GIF media,
    every chart kind (the four the writers draw by name, the rest by their element as Chart.Kind(rawValue:)),
    titles, legends and series with their references — a series name held as a reference is
    Chart.Series.nameReference. Untouched, the drawing and its parts are written back byte for byte; an added
    picture or chart is spliced in beside them; a change or a removal rebuilds the drawing from the model, and
    what the drawing held that the model cannot say (shapes, pictures in other formats) is reported as dropped.

  • The theme is read, and colours resolve through it (B.71). Workbook.theme: Theme? holds the twelve scheme
    colours and the two scheme fonts of an XLSX file's theme part (nil for a new workbook, which resolves as
    Theme.office), and wb.rgb(of:) answers the ARGB a Color stands for — a theme colour with Excel's tint rule
    applied, an indexed colour through the file's palette or the legacy one. An untouched theme part is written back
    byte for byte; a changed wb.theme regenerates it. The ODS and Numbers writers now write the resolved RGB where
    they used to write black and report "theme/indexed colours written as default".

  • Furigana travels (B.70). Cell.phonetic: PhoneticText? holds the phonetic guide Excel shows above a cell's
    text — the readings run by run with their UTF-16 spans, the conversion kind, the alignment and the font. The XLSX
    reader takes <rPh> / <phoneticPr> from shared and inline strings (openpyxl skips them; this is the one
    deliberate step past its behaviour), the writer puts them back in the shared-string table, where the same text
    with and without readings is two entries as in Excel's own files. The ODS and Numbers writers report the guides
    as dropped and keep the text.

Changed

  • Three lists that will grow are structs, not enums (B.69). Chart.Kind, SheetImage.Format and DateEpoch are
    RawRepresentable structs with the same static members (.column, .png, .mac1904 …), so a kind, a format or
    a date origin can be added without breaking a switch — which is what reading charts and pictures from files, and
    ODF's free date origin, need. Every spelling is unchanged; a switch over one of them now needs a default.
    Chart.Kind.drawable / isDrawable name the four kinds the writers draw; a chart of another kind is reported as
    dropped instead of written as an empty part. DateEpoch(origin:) carries any origin: the ODS reader keeps a
    table:null-date that is neither 1899-12-30 nor 1904-01-01 instead of reading it as 1900 with a warning, the ODS
    writer writes it back, and the XLSX and Numbers writers re-base such an origin onto 1900 and say so (the model's
    dates are civil dates, so they land on the same day).

v0.23.0

Choose a tag to compare

@nanbu nanbu released this 10 Sep 11:22

The last seven items of the 1.0 API review, each its own commit and spec appendix (B.54–B.60), and the change of
cell numbering that came out of reviewing them (B.61). No compatibility
aliases: a name that stays at 1.0 stays for good, so the old ones go now. Every change is a rename or a typed
replacement; no file output changes.

Changed

  • Cell coordinates are the numbers the sheet shows — 1-based (B.61). CellRef(row: 1, column: 1) is A1;
    sheet[1, 2] is B1; insertRows(at: 2) inserts above row 2, as Excel's own command does. This reaches every
    integer that names a cell, row or column: CellRef, CellRange, RangeBounds, the subscripts, the
    dimension dictionaries and their accessors, insert/delete, groups, print titles, autofitColumn, append([Int:…]),
    Table.anchor, nextAppendRow, StreamedRow.index, columnName(_:) (1 → "A"), columnIndex(_:) ("A" → 1),
    maxRow (1,048,576) and maxColumn (16,384). Positions in Swift collections stay 0-based: sheets[i], pivot field
    indices, the table: argument of a streaming read, and every returned array (values(width:) index 0 is column A).
    rowNumber is gone — it is row. FilterColumn.column is renamed columnOffset (an offset, unchanged in value);
    RangeView's relative subscript is view[rowOffset:columnOffset:]. rowBreaks / columnBreaks keep the file's
    own <brk id> values.
    The compiler will not find this migration for you: ws[r - 1, c - 1] still compiles and reads the row
    above, and array[cell.column] still compiles and traps on the last column. Read every - 1 and + 1 around
    a CellRef and remove it; a write to row 0 or column 0 now stops with a message.
  • Fixed: an ODS file declaring more <table:table-column> elements than a sheet has columns, with a width or
    hidden flag on the ones past the end, trapped while reading. They are ignored now, as the spec promises.
  • How a formula cell is read is an enum, not a Bool (B.54). ReadOptions(dataOnly: true)
    ReadOptions(formulaCells: .cachedValues); the same on StreamingReadOptions and Workbook. The default is
    .formulas. Under .cachedValues a formula the file never computed reads as an empty cell — the readers already
    did this; it is now written down and tested.
  • Three model names (B.56): ExcelTableStructuredTable (structuredTables, addStructuredTable,
    structuredTable(containing:), StructuredTableColumn) because the ODS codec writes and reads it too;
    Cell.commentCell.note (the type stays CellNote, the list stays Sheet.notes), leaving "comment" to
    Excel's threaded comments; Top10FilterRankFilter and FilterColumn.top10rank.
  • Number-format constants say what they are (B.57). Thirty-four openpyxl-style names become eighteen:
    number, numberTwoDecimals, numberThousands, numberThousandsTwoDecimals, percent, percentTwoDecimals,
    scientific, isoDate, isoDateTime, time24, time24Seconds, time12, time12Seconds, minutesSeconds,
    elapsed, plus general and text. No surviving string changed. The locale-shown builtins (dateXLSX14 …) are
    reached by builtinCode(14); the currency and locale-flavoured ones are written as codes.
  • One name per value (B.58): CellValue.dataType / Cell.dataType (openpyxl's letter) are removed — switch on
    the case; pythonString folds into stringValue with identical output; the global Formula("=…") becomes
    .formula("=…") on CellValue.
  • Sheet.tabColor is a Color? (B.59), not a hex String?: Color(hex: "1072BA"), and a theme or indexed tab
    colour from a file no longer reads as nil.
  • Abbreviations are words (B.60): CellRef(row:column:), .column, .maxColumn, offset(rows:columns:),
    CellRange(minRow:minColumn:maxRow:maxColumn:), .minColumn / .maxColumn, size.columns, .columns,
    shifted / shift / moveRange(…columns:), RangeBounds(minColumn:…), RangeView[row:column:];
    SheetView.sqrefselectedRanges (still a String).

Added

  • DataValidation.list(choices:over:allowBlank:rejects:) builds a dropdown from the choices themselves, and
    listChoices reads them back (B.55). It returns nil for what an inline list cannot hold — a comma or a double
    quote in a choice, no choices, or more than inlineListLimit characters — and the range-sourced list(_:over:)
    is the way round that. The ODS and Numbers writers now take the inline spelling from the model instead of parsing
    it themselves.

v0.22.0

Choose a tag to compare

@nanbu nanbu released this 10 Sep 00:20

Changed

  • A refusal says which kind of refusal it is (Appendix B.52). Four failures used to arrive as
    SheetError.unsupportedFeature(String), told apart only by reading the message. Each now has its own case:

    it was it is now
    unsupportedFeature("the OOXML package is encrypted …") unopenable(.encryptedOOXML)
    unsupportedFeature("the ODF package is encrypted …") unopenable(.encryptedODF)
    unsupportedFeature("the Numbers document is password-protected …") unopenable(.encryptedNumbers)
    unsupportedFeature("an OLE compound file: the legacy .xls …") unopenable(.legacyCompoundFile)
    unsupportedFeature("no codec for .ods is in this CodecSet …") noCodec(for: .ods)
    unsupportedFeature("the ODF entry is encrypted with Blowfish CFB …") and the other protection refusals of SheetDecrypt / SheetEncrypt unsupportedEncryption(detail:)

    unsupportedFeature(String) keeps everything that is a limit of a format or of the environment — a second sheet
    in delimited text, a Numbers table past its row and column caps, text an encoding cannot carry, a row-by-row
    write on WASI. The messages themselves are unchanged, so code that shows error.description (or
    localizedDescription) shows what it showed before, less unsupportedFeature's unsupported: prefix. Code
    that matched on case .unsupportedFeature for an encrypted file, a missing codec or an unsupported cipher has to
    match the new case; code that switches exhaustively over SheetError has three more cases to answer for.
    passwordRequired was considered and rejected: it would name a remedy that works for two of the four
    (a password cannot open a protected Numbers document or a legacy .xls).

  • An encrypted package says so before the set is asked for a codec. CodecSet.read and CodecSet.inspect
    used to find ODF and Numbers encryption inside the codec, so an encrypted .ods given to a set without the ODS
    codec answered "no codec for .ods" while streamingReader, which probes first, answered "encrypted". All three
    now answer unopenable(.encryptedODF) whatever the set holds. Naming a format explicitly (read(_:format:))
    is unchanged: the codec named finds the encryption in its own package.

  • Changing a style is setStyle, taking one is still style (Appendix B.53). sheet.style("A1") returned a
    style and sheet.style("A1") { … } changed one — the same name for both. The six changing declarations on
    Sheet and Table become setStyle(at:_:), setStyle(_:_:) and setStyle(_ range:_:), which is how every
    other editor in this library is named (setRowDimension, setColumnDimension, setWidth, setHeight). The
    four reading declarations keep the name style, and sheet[cell: "A1"].style is unchanged. There is no
    compatibility alias: sheet.style("A1:D1") { … } becomes sheet.setStyle("A1:D1") { … }.

  • An A1 string that will not parse now stops where it changes something, and returns a default where it only
    reads
    (Appendix B.53). The model used to answer five different ways, so the same typo crashed at one entry
    point and silently wrote nothing at another. Ten entry points change: removeCell(_:), moveRange(_:rows:cols:),
    unmerge(_:), setColumnDimension(_ name:_:) (with setWidth(_:ofColumn:)), groupColumns(_:_:outlineLevel:hidden:),
    addExcelTable(named:over a1:styleInfo:), addConditionalFormatting(_:over sqref:), freezePanes(at:),
    freezePanesA1 and autoFilterA1 used to do nothing, or — for the freeze — quietly clear it; they now stop with a
    message naming the string. addImage(_:at:) and addImage(_:over:) already stopped, on a bare !; they now say why.
    The four that take a defined-name formula as a file saves it — setPrintArea(_:), setPrintTitles(_:),
    setPrintTitleRows(_:), setPrintTitleColumns(_:) — stay lenient and drop what will not parse: a workbook whose
    print area reads MySheet!#REF! has to open.
    Reading (sheet["A1"], style(_:), rows(in:), column(_:), isMerged(_:), columnDimension(_:) …) is
    unchanged and still answers with nil, an empty array, false or the default. "" and nil keep meaning
    "clear"
    freezePanes(at: ""), setPrintArea(nil), freezePanesA1 = nil and the rest behave as before.
    Validate strings that come from a person with CellRef(_:) / CellRange(_:), which return nil rather than stopping.

  • Sheet.addExcelTable(named:over a1:styleInfo:) returns String, not String?. The Optional only ever meant
    "that range did not parse", which is now a programming error.

Added

  • SheetFormat.productName is public — .ods"SheetODS" — so a caller that catches noCodec(for:) can
    name the product to link in its own words.

v0.21.0

Choose a tag to compare

@nanbu nanbu released this 09 Sep 21:48

Changed

  • A row-by-row write reaches the destination only when it finishes (Appendix B.51). Every format's
    StreamingWriterStreamingWriter(url:) and codecs.streamingWriter(url:) alike — now writes into a
    temporary file beside the destination and renames it over only when close() completes. A row that cannot be
    serialized, a close() that fails half way, a writer let go of without closing: the file that was already there
    is exactly as it was, and a destination that did not exist does not appear. Arguments are unchanged. Code that
    read the destination while rows were still arriving now has to wait for close(), and the destination's
    directory must exist (none is created) and hold a plain file — a directory or a symbolic link is refused.

  • StreamingWriter.close() returns a StreamingWriteResult instead of Void, and its result is not
    discardable: keep it (let result = try writer.close()) or discard it explicitly (_ = try writer.close()).
    The result carries the format written and the warnings as of the save; writer.warnings still reads the
    running list. Assigning the call to a Void no longer compiles.

  • The first failure ends a writer. After a failed append or addSheet — including a refusal the format
    itself raises, such as a second sheet in delimited text — the writer saves nothing: further rows and close()
    throw invalidWorkbook. Code that caught such a refusal and carried on used to get a file; it now gets the
    error back from close() (or from withStreamingWriter). Misuse after the end throws rather than trapping.

  • Row-by-row writing to a file is refused on WASI with unsupportedFeature, until the rename this save is
    built on is verified under a WASI runtime. Whole-workbook write(to:as:) on WASI is unchanged.

Added

  • CodecSet.withStreamingWriter(to:as:sheetName:epoch:csv:_:) (Appendix B.51) — rows in a closure, saved when
    the closure returns and cancelled when it throws:
    try CodecSet.all.withStreamingWriter(to: url, as: .xlsx) { writer in … }. The closure's own error is what comes
    out, and nothing is saved; the writer belongs to the closure, which may not close() or cancel() it.

  • StreamingWriter.cancel() — throws the rows away and releases what the format was holding (file handles, an
    ODS writer's spilled rows) now rather than at the next collection. Harmless after a save: a finished file is not
    deleted by cancelling the writer that wrote it.

  • StreamingWriteResult (format, warnings) and StreamingCleanupError (primaryError,
    cleanupErrors) — the second only when cleaning up after a failure fails too, so neither half is lost. Neither
    can be constructed by a caller.

v0.19.1

Choose a tag to compare

@nanbu nanbu released this 06 Sep 06:42

Fixed

  • Numbers ignored every date format. A date's format was written as a CUSTOM_DATE format struct, which
    Numbers only honours when a matching custom-format archive exists in the document — there was none, so a cell
    formatted yyyy/m/d showed as 01/09/2026 0:00. The struct is now the built-in DATE kind with the same
    date_time_format pattern, which Numbers draws as written (2026/9/1). Reading is unchanged: both kinds were
    already mapped back to the Excel code. Measured in Numbers 2026-09-06 through Stream's export.

v0.19.0

Choose a tag to compare

@nanbu nanbu released this 06 Sep 04:28

Added

  • WebAssembly (wasm32-wasi). The library builds and runs under WASI with the swift.org toolchain and its
    Wasm SDK (swift build --swift-sdk swift-6.3.3-RELEASE_wasm; Xcode's Swift has no WebAssembly backend). WASI has
    no zlib, one thread, no temporary files and no resource bundles, so five places branch on #if os(WASI)
    (spec Appendix B.45): a pure Swift DEFLATE route — a complete RFC 1951 inflater for reading, stored blocks for
    writing (valid DEFLATE that folds nothing; files come out larger) — and a table CRC-32; ZIP64 markers and the
    default expansion limit made safe for a 32-bit Int (16 << 30 is 0 there, which refused every file);
    the XLSX sheet read runs one sheet after another; CodecSet.write(to:) writes without the atomic replace; the
    Numbers codec's schema and template are read from /SwiftSheets_SheetNumbers.resources/, which the host
    mounts, instead of Bundle.module. The Apple, zlib and Linux routes are untouched, and the native suite is
    unchanged. Verified with node's WASI against Stream's golden fixtures (xlsx import byte-for-byte; xlsx / ods /
    numbers written and read back); LibreOffice opens all three. Not yet verified in Numbers or Excel themselves.