diff --git a/CHANGELOG.md b/CHANGELOG.md index fa7de7f..c407217 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -246,6 +246,21 @@ because it turns other people's test suites red. ### Changed +- **A spreadsheet can now be built wider than a spreadsheet can open.** The + `columns` setting of `xlsx` used to stop at 64. It now reaches 32768, which is + the ceiling `csv` already had. + + The number matters because of where a reader stops. Excel and LibreOffice + Calc both hold 16384 columns. Measured with Calc: a sheet of 16384 columns + opens whole, and a sheet of 16385 opens as 16384 - the last column is dropped + and nothing is said about it. At a ceiling of 64 there was no way to build a + file that asks a spreadsheet about its own limit, which is the kind of file + this tool exists to produce. + + `rows` times `columns` still cannot pass 2 million cells, so a sheet 16385 + columns wide holds up to 122 rows. Nothing about a sheet of 64 columns or + fewer changes, and the default is still one column. + - **Byte counts are grouped in threes.** A total used to print as `2516582400 B`. It now prints as `2 516 582 400 B`, in every message that names a number of bytes - `tfg formats`, the summary a run prints, what a diff --git a/internal/format/xlsx/xlsx.go b/internal/format/xlsx/xlsx.go index 0b40d77..3b0c40f 100644 --- a/internal/format/xlsx/xlsx.go +++ b/internal/format/xlsx/xlsx.go @@ -39,9 +39,23 @@ const ( maxRows = 200_000 minColumns = 1 - // Sixteen thousand three hundred and eighty four is the format's own limit. - // This one is the width a person would actually look at. - maxColumns = 64 + // maxColumns is deliberately ABOVE the width of a spreadsheet, and that is + // the whole reason for the number. It is the same ceiling CSV carries, for + // the same question asked of the same reader. + // + // It said 64 until 2026-09-08, with the reason "the width a person would + // actually look at". That reason describes a document somebody reads, and + // this tool writes fixtures somebody tests with - a ceiling belongs to the + // reader under test, not to our own comfort. At 64 there was no way to + // build a sheet that asks Excel about its own limit at all. + // + // Measured 2026-09-08 with LibreOffice Calc 26.2.5.2 headless, on + // workbooks built outside this tool because this tool could not build + // them: 16384 columns come back whole, and 16385 come back as 16384 with + // the last column dropped, exit 0 and not one word on either stream. That + // silent loss is the thing a tester needs a fixture for, and standing on + // both sides of the line is what a boundary set is. + maxColumns = 32768 // The sheet is held in memory while the package is built, so the pair is // bounded as well as each side. @@ -75,7 +89,7 @@ func init() { Name: "columns", Kind: format.PropertyInt, Min: minColumns, Max: maxColumns, Unit: "columns", Default: "1", - Detail: "How many columns each row has.", + Detail: "How many columns each row has. Above 16384 a spreadsheet may show only the first 16384 and drop the rest without a word.", }, }, JointLimits: []format.JointLimit{{ diff --git a/internal/guard/columnceiling_test.go b/internal/guard/columnceiling_test.go new file mode 100644 index 0000000..182ed85 --- /dev/null +++ b/internal/guard/columnceiling_test.go @@ -0,0 +1,79 @@ +package guard + +import ( + "testing" + + "github.com/donislawdev/TestingFilesGenerator/internal/format" + _ "github.com/donislawdev/TestingFilesGenerator/internal/format/all" +) + +// spreadsheetWidth is where a spreadsheet stops, measured rather than +// remembered: 2026-09-03 for CSV and again 2026-09-08 for XLSX, both with +// LibreOffice Calc 26.2.5.2 headless. A table of this many columns comes back +// whole. One column more comes back with this many, the last column dropped, +// exit 0 and not one word on either stream. +const spreadsheetWidth = 16384 + +// narrowerOnPurpose names a format whose columns cannot reach past a +// spreadsheet because the format itself stops first, with the reason. +// +// It is empty, and that is the point rather than an oversight. A format ends up +// here only when its OWN structure caps it - the way an icon stores each side +// in a single byte - and never because a smaller number felt tidier. Writing +// the reason down is the price of the exception, which is what stops this +// becoming the sort of list somebody adds to instead of thinking. +var narrowerOnPurpose = map[string]string{} + +// A format with columns has to offer more of them than a spreadsheet accepts. +// +// The ceiling of a setting belongs to the reader under test, not to what this +// tool finds comfortable to write. Standing on both sides of a limit is what a +// boundary set is for, so a ceiling that stops at the limit offers the last +// table that survives and never the first that does not. +// +// This is not hypothetical tidiness, it is a defect this project shipped. +// XLSX declared 64 from the day it was written until 2026-09-08, with the +// reason "the width a person would actually look at" - which describes a +// document somebody reads rather than a fixture somebody tests with. CSV asked +// the same question of the same reader and answered 32768. Nothing compared +// them, and at 64 there was no way to build a sheet that asks Excel about its +// own limit at all. +// +// What it asks about is the OFFER, because that is what a ceiling is. That the +// file itself works was measured on 2026-09-08 rather than asserted here: a +// workbook of 16385 columns built by this tool converts through Calc at exit 0 +// and comes back with 16384, which is the same silent loss a workbook from +// another writer produces. Building one costs twelve megabytes, which is not a +// price worth paying on every run of this suite. +// +// Asked of every registered format rather than of the two that have the +// setting today, so a third tabular format is covered on the day it arrives. +func TestAFormatWithColumnsReachesPastWhatASpreadsheetAccepts(t *testing.T) { + examined := 0 + + for _, d := range format.All() { + for _, p := range d.Properties { + if p.Name != "columns" || p.Kind != format.PropertyInt { + continue + } + if why, narrow := narrowerOnPurpose[d.ID]; narrow { + if p.Max > spreadsheetWidth { + t.Errorf("%s is excused as narrower on purpose (%s) and reaches %d anyway - take the excuse off", + d.ID, why, p.Max) + } + continue + } + examined++ + if p.Max <= spreadsheetWidth { + t.Errorf("%s offers at most %d columns and a spreadsheet accepts %d, "+ + "so no fixture built from it can ask the reader about its own limit - "+ + "raise the ceiling, or name the structural reason in narrowerOnPurpose", + d.ID, p.Max, spreadsheetWidth) + } + } + } + + if examined == 0 { + t.Fatal("no format offers a column count, so this proved nothing") + } +} diff --git a/web/public/formats/index.html b/web/public/formats/index.html index 395cb95..22cc6cc 100644 --- a/web/public/formats/index.html +++ b/web/public/formats/index.html @@ -623,7 +623,7 @@

Settings each format accepts

columns - 1 - 64 columns + 1 - 32768 columns xml diff --git a/web/public/pl/formaty/index.html b/web/public/pl/formaty/index.html index 7bd8d9d..5c63a31 100644 --- a/web/public/pl/formaty/index.html +++ b/web/public/pl/formaty/index.html @@ -623,7 +623,7 @@

Ustawienia, które przyjmuje każdy format

columns - 1 - 64 kolumn + 1 - 32768 kolumn xml