Releases: nanbu/SwiftSheets
Release list
v0.28.0
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 atReadOptions.cellLimitgives 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.isVerifiedVersionsays 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.producertoo, so a row-by-row reader, which has no warnings, can askinspectfirst.CodecSet.streamingReader(contentsOf:format:limits:csv:)andStreamingReader(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 dayCellValue(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
CellRefsubscripts,cell(_:),
rowDimension(_:)andcolumnDimension(_:)(withsetWidth(_:ofColumn:)andsetHeight(_:ofRow:), which are built
on them) andCellRef.columnName(_:)stop on a number below 1. A- 1left over from before 0.23.0 used to read an
empty cell, andcolumnName(0)put$$2:$$4into 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.cellLimitgivestruncatedinstead ofdegraded, with the subjectsheetsand
the same wording in every reader. Aswitchthat lists every case ofConversionWarning.Kindneeds 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 thattableNames(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
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.imagesandsheet.shapesare read
from a Numbers sheet's canvas — theTSD.ImageArchivewith its bytes underData/, theTSWP.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 discardstext: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).&Pbecomes 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(aCanvasPointin points)
is read exactly and written back;sheet.addTable(named:at:)places one. A table with a non-defaultanchor
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.chartsis 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
gainsframe(aCanvasRectin 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 oneChart.framealready uses.addSparkline(_:data:at:)→addSparkline(_:dataRange:at:), now with aCellReftwin 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.set→IconSet.Icon.setName, the wordIconSet.nameuses for the same kind of value.PhoneticText.Run.startandend→PhoneticText.Run.range, aRange<Int>of UTF-16 code units made with
Run(_:over:). The reader clamps a file whoseebcomes before itssbinstead of trapping on it.DataBar.isGradient→DataBar.gradient, the attribute's own word, likepercentandreverse.Alignment.wrapText→Alignment.wrapsTextandAlignment.shrinkToFit→Alignment.shrinksToFit— the two
imperative Bools 0.24.0's pass missed, spelled the way itsfitToPage→fitsToPagealready 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(askisDrawable),Shape.Geometry.presets(askisPreset)
andDataBar.usesExtension. - A shape made with
Shape(_:)and appended by hand covers cell A1 as a span instead of borrowing a picture's
.originalsize, 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.inittakesthread:like every other extra;CanvasPointandCanvasRectareCodable;
PreservationSummaryisHashable; assigningTheme.colorsnormalises toAARRGGBB, as the initialiser does.
Fixed
ReadOptions.cellLimitstops 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 adegradedwarning. 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
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 insheet.shapes;addShape(_:over:)andaddTextBox(_:over:font:)place one. XLSX readsxdr:sp/
xdr:cxnSpand writes them back into the same drawing under the as-read rule (untouched: byte for byte; added:
spliced; changed: rebuilt); ODS reads and writesdraw:custom-shape,draw:lineanddraw: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.
DataBargainsnegativeColor,axisColor,axisPosition,
direction,isGradientandborderColor;IconSetgainscustomIcons. XLSX folds thex14: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.hyperlinkcarries 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 ownhyperlinkis
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.threadholds aCommentThread— 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.sparklinesholdsSparklineGroups (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 thex14:sparklineGroupsextension (untouched: byte for byte), ODS reads and writes
LibreOffice'scalcext:sparkline-groups. Numbers reports them as dropped (spec Appendix B.79). -
An inventory of what the model does not represent.
preservationSummary.partscounts 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.externalLinkslists 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.
SheetViewgainsshowsRowColumnHeaders,
showsZeros,rightToLeft,topLeftCellandkind(normal / page layout / page-break preview);
CalculationSettings.calcModeis<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.tabColoris written astable:tab-coloron 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
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:)andsetStyle(at:_:)
onSheetandTablelose theat:—sheet.cell(ref)besidesheet.cell("B2"), the waymerge(_:),range(_:)
andisMerged(_:)already were.addImage(_:at:)andsubscript(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 wordCellRangealready used);CellRange.shrunk(right:bottom:left:top:)→shrunk(right:down:left:up:)(the
labelsexpandedalready used);CodecSet.streamingWriter(url:format:)andStreamingWriter(url:format:)→
(to:as:)(the labelswrite(to:as:)andwithStreamingWriter(to:as:)use);streamingReader(data:)→
streamingReader(_:)(asread(_:));SheetFormat.detect(from:)anddetect(from:filename:)→ one
detect(_:filename:), the shapeprobe(_:filename:)has, anddetect(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:includeStyles→includesStyles,includeBOM→includesBOM,allowBlank→allowsBlank
(and thelist(choices:over:allowsBlank:)label),preserveUnknownParts→preservesUnknownParts,countCells→
countsCells,showGridLines→showsGridLines,showValue→showsValue, the sixshow…flags of
TableStyleInfo/PivotStyleInfo,showRowGrandTotals/showColumnGrandTotals/showAll/refreshOnLoadon the
pivot types,showInputMessage/showErrorMessage/hideDropDownonDataValidation,lockStructure/
lockWindows/lockRevisiononWorkbookProtection,useWildcards/useRegularExpressions/useFirstPageNumber/
linkToSourceData/fitToPage. File attributes are unchanged. -
One name per thing, continued (B.65).
values(in:)(documented as "same asrows(in:)") andSheets.names
(the same array asWorkbook.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 — assignprintTitleRows/printTitleColumnsor the formula. -
What Swift can say with a type is said with a type (B.66).
hashValue: String?onSheetProtection,
WorkbookProtectionandProtectedRange— the nameHashableowns — issaltedHash.validationError() -> String?
onStructuredTableandPivotTableisvalidate() throws(SheetError.invalidWorkbookcarries the reason). The
ExcelDatenamespace is gone:CellValue(serial:epoch:),cellValue.serial(epoch:),CellValue(iso8601:),
cellValue.iso8601,civilDateTime.serial(epoch:),civilDate.serial(epoch:),Duration(serialDays:)and
duration.serialDaysreplacefromSerial/toSerial/durationFromSerial/fromISO8601/toISO8601. Five
properties whose schema closes the set are enumerations instead of strings:Font.vertAlign→
verticalAlignment: Font.VerticalAlignment?(also onDifferentialFont),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/hasUnmodelledConditionalFormatson the sheet, adroppedread 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,LegacyPasswordHashandModernPasswordHash, withTable.cleanMergedRange(_:),
WriteResult.suggest(from:target:options:),Workbook.noteUnmodelledODFFeatures(_:)andUnopenableInput.probe(in:),
arepackage— 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.baseColWidth→baseColumnWidth,
defaultColWidth→defaultColumnWidth,PivotLocation.firstDataCol→firstDataColumn.
Fixed
SheetView.sqrefis now reallyselectedRanges. 0.23.0's notes, the spec (Appendix B.60) and the commit that
renamed the other abbreviations all saidSheetView.sqref→selectedRanges; the property itself stayedsqref,
and the one test that looked at it expected the old name. A new contract test
(APIContractTests.everyRenameTheChangelogAnnouncesExistsInTheCode) checks everyold→newrename in the
newest CHANGELOG section against the code, so a promised name that does not exist fails before the tag.
v0.25.0
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:framein a
cell, or among the sheet'stable:shapesfor a fixed position) fillsheet.images, and its chart objects
(Object N/) fillsheet.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.addChartnow
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.imagesandsheet.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 asChart.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), andwb.rgb(of:)answers the ARGB aColorstands 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 changedwb.themeregenerates 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.FormatandDateEpochare
RawRepresentablestructs with the same static members (.column,.png,.mac1904…), so a kind, a format or
a date origin can be added without breaking aswitch— which is what reading charts and pictures from files, and
ODF's free date origin, need. Every spelling is unchanged; aswitchover one of them now needs adefault.
Chart.Kind.drawable/isDrawablename 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-datethat 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
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) andmaxColumn(16,384). Positions in Swift collections stay 0-based:sheets[i], pivot field
indices, thetable:argument of a streaming read, and every returned array (values(width:)index 0 is column A).
rowNumberis gone — it isrow.FilterColumn.columnis renamedcolumnOffset(an offset, unchanged in value);
RangeView's relative subscript isview[rowOffset:columnOffset:].rowBreaks/columnBreakskeep 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, andarray[cell.column]still compiles and traps on the last column. Read every- 1and+ 1around
aCellRefand 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 onStreamingReadOptionsandWorkbook. The default is
.formulas. Under.cachedValuesa 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):
ExcelTable→StructuredTable(structuredTables,addStructuredTable,
structuredTable(containing:),StructuredTableColumn) because the ODS codec writes and reads it too;
Cell.comment→Cell.note(the type staysCellNote, the list staysSheet.notes), leaving "comment" to
Excel's threaded comments;Top10Filter→RankFilterandFilterColumn.top10→rank. - 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, plusgeneralandtext. No surviving string changed. The locale-shown builtins (dateXLSX14…) are
reached bybuiltinCode(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;pythonStringfolds intostringValuewith identical output; the globalFormula("=…")becomes
.formula("=…")onCellValue. Sheet.tabColoris aColor?(B.59), not a hexString?: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.sqref→selectedRanges(still a String).
Added
DataValidation.list(choices:over:allowBlank:rejects:)builds a dropdown from the choices themselves, and
listChoicesreads 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 thaninlineListLimitcharacters — and the range-sourcedlist(_: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
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 ofSheetDecrypt/SheetEncryptunsupportedEncryption(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 showserror.description(or
localizedDescription) shows what it showed before, lessunsupportedFeature'sunsupported:prefix. Code
that matched oncase .unsupportedFeaturefor an encrypted file, a missing codec or an unsupported cipher has to
match the new case; code that switches exhaustively overSheetErrorhas three more cases to answer for.
passwordRequiredwas 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.readandCodecSet.inspect
used to find ODF and Numbers encryption inside the codec, so an encrypted.odsgiven to a set without the ODS
codec answered "no codec for .ods" whilestreamingReader, which probes first, answered "encrypted". All three
now answerunopenable(.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 stillstyle(Appendix B.53).sheet.style("A1")returned a
style andsheet.style("A1") { … }changed one — the same name for both. The six changing declarations on
SheetandTablebecomesetStyle(at:_:),setStyle(_:_:)andsetStyle(_ range:_:), which is how every
other editor in this library is named (setRowDimension,setColumnDimension,setWidth,setHeight). The
four reading declarations keep the namestyle, andsheet[cell: "A1"].styleis unchanged. There is no
compatibility alias:sheet.style("A1:D1") { … }becomessheet.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:_:)(withsetWidth(_:ofColumn:)),groupColumns(_:_:outlineLevel:hidden:),
addExcelTable(named:over a1:styleInfo:),addConditionalFormatting(_:over sqref:),freezePanes(at:),
freezePanesA1andautoFilterA1used to do nothing, or — for the freeze — quietly clear it; they now stop with a
message naming the string.addImage(_:at:)andaddImage(_: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 readsMySheet!#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.""andnilkeep meaning
"clear" —freezePanes(at: ""),setPrintArea(nil),freezePanesA1 = niland the rest behave as before.
Validate strings that come from a person withCellRef(_:)/CellRange(_:), which return nil rather than stopping. -
Sheet.addExcelTable(named:over a1:styleInfo:)returnsString, notString?. The Optional only ever meant
"that range did not parse", which is now a programming error.
Added
SheetFormat.productNameis public —.ods→"SheetODS"— so a caller that catchesnoCodec(for:)can
name the product to link in its own words.
v0.21.0
Changed
-
A row-by-row write reaches the destination only when it finishes (Appendix B.51). Every format's
StreamingWriter—StreamingWriter(url:)andcodecs.streamingWriter(url:)alike — now writes into a
temporary file beside the destination and renames it over only whenclose()completes. A row that cannot be
serialized, aclose()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 forclose(), 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 aStreamingWriteResultinstead ofVoid, 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.warningsstill reads the
running list. Assigning the call to aVoidno longer compiles. -
The first failure ends a writer. After a failed
appendoraddSheet— including a refusal the format
itself raises, such as a second sheet in delimited text — the writer saves nothing: further rows andclose()
throwinvalidWorkbook. Code that caught such a refusal and carried on used to get a file; it now gets the
error back fromclose()(or fromwithStreamingWriter). 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-workbookwrite(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 notclose()orcancel()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) andStreamingCleanupError(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
Fixed
- Numbers ignored every date format. A date's format was written as a
CUSTOM_DATEformat struct, which
Numbers only honours when a matching custom-format archive exists in the document — there was none, so a cell
formattedyyyy/m/dshowed as01/09/2026 0:00. The struct is now the built-inDATEkind with the same
date_time_formatpattern, 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
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-bitInt(16 << 30is 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 ofBundle.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.