HotXLS Delphi Component ukládá řádek ODS nesoucí table:number-rows-repeated a výšku řádku jako jediný záznam TXLSXRowHeightRun — první řádek, poslední řádek, jedna výška — místo jedné položky výšky na každý opakovaný řádek a složí styl, který ty řádky dědí, do jediného intervalového style overlay. To je celý důvod, proč HotXLS 2.382.2 otevře tabulku, jejíž ocas opakuje 1 048 530 prázdných řádků, za 0,02 sekundy, kde 2.382.1 timeoutovala, a proč se tentýž soubor uloží zpátky do ODS s repeat počtem zachovaným, ne jako milion literálních řádků
Ten soubor je úplně obyčejný. LibreOffice Calc napíše list o čtrnácti sloupcích s 45 řádky dat a pak popíše všechno pod nimi jedním elementem: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Styl ro1 nastavuje style:row-height="0.452cm" a každý <table:table-column> nese table:default-cell-style-name, který dědí každá prázdná buňka v úseku. Celé content.xml má 103 KB. Nic na souboru neříká „drahé"; drahé to bylo celé naší vlně
Proč jeden opakovaný řádek dá ODS importu timeout?
Protože ho importér dřív rozbaloval. Ve 2.382.1 cyklil řádkový dokončovač SetRowHeight(RowIndex + i, RowHeight) jednou na každý opakovaný řádek a zapisoval každou výšku do string listu Name=Value klíčovaného číslem řádku. Každý insert do toho listu běžel IndexOfName lookup přes všechno, co už v něm bylo, takže milion výšek znamenal milion lineárních skenů — kvadratické hledání v listu, proti kterému byl podán HXLS-005. Zároveň OdsCommitRow materializoval buněčný objekt pro každý sloupec, který dědil styl, na každém z opakovaných řádků, protože ostylovaná prázdná buňka se pořád počítala jako buňka
Strana uložení měla vlastní verzi problému. LibreOffice soubor končí ještě jedním řádkem ro1 za velkým repeatem, takže poslední ostylovaný řádek seděl na úplném dně listu a OdsBuildTableXml procházel každý řádek až k němu a emitoval <table:table-row> elementy jeden po druhém. I sešit, který importoval lacině, by zapisoval draze. Opravit import bez opravy exportu by timeout přesunulo, ne odstranilo
Co je úsek výšek řádků v HotXLS?
Úsek je nejmenší věc, která umí popsat „řádky 46 až 1 048 575 jsou všechny vysoké 12,81 bodu", aniž by to řekla 1 048 530 krát. TXLSXRowHeightRun je záznam FirstRow, LastRow a Height; TXLSXRowHeightRuns je dynamické pole z nich a každý TXLSXWorksheet drží jedno v FRowHeightRuns vedle stávajícího per-row listu výšek. Při importu ODS se řádkový dokončovač teď větví podle repeat počtu: počet 1 stále volá SetRowHeight, cokoliv většího volá jednou za celé rozpětí XlsxAssignRowHeightRun. Rozpětí se svírá na XlsxMaxRow, což je 1 048 576, takže repeat počet, který list přestřelí, se usekne, ne odmítne
XlsxAssignRowHeightRun je jediný zapisovatel pole a disjunktnost úseků drží konstrukcí. Pro nový interval zkopíruje každý existující úsek ležící úplně mimo něj, rozdělí jakýkoli úsek, který se s ním překrývá, na kousek před a kousek po a pak přidá nový interval, když je Present true — nebo nepřidá nic, když je Present false, což je způsob, jakým ClearRowHeight probodne jednořádkovou díru. Plyne z toho dvě věci. Pole nikdy neobsahuje překrývající se intervaly, takže lookup může skončit u prvního zásahu. A pole se nikdy nemutuje na místě; při každém volání se staví čerstvá kopie, což na těchhle velikostech nic nestojí a odstraňuje celou třídu aliasing bugů
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Workbook := TXLSXWorkbook.Create;
try
// List, jehož ocasní řádek se opakuje 1 048 530 krát pod jedním stylem řádku
Workbook.OpenODS('conditional-formatting.ods');
Sheet := Workbook.Sheets[1];
// Obě čtení se rozřeší přes tentýž úsek; nic se nerozbalilo
Writeln(Sheet.RowHeight[46]:0:2, ' pt');
Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
// Jednořádkový override zastíní úsek bez rozdělení
Sheet.RowHeight[500000] := 36;
// Vymazání jednoho řádku uvnitř úseku rozřízne úsek na dva kusy
Sheet.ClearRowHeight(500001);
Writeln(Sheet.HasRowHeight(500001)); // False
Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // pořád výška úseku
finally
Workbook.Free;
end;
end;
Pořadí lookupu je ta část, kterou stojí za to zapamatovat. TXLSXWorksheet.GetRowHeight nejdřív zkontroluje per-row list a k úsekům sáhne jen tehdy, když řádek nemá explicitní položku, a HasRowHeight dělá totéž. Takže Sheet.RowHeight[500000] := 36 se úseku vůbec nedotkne — přidá jednu položku do per-row listu a ta vyhraje, protože se hledá první. ClearRowHeight je opak: odstraní jakoukoli per-row položku a pak zavolá XlsxAssignRowHeightRun s Present = False, protože vymazaný řádek se musí číst jako „bez výšky", i když ho úsek pokrývá. ClearRowHeights vyprázdní obě struktury najednou
Kam jdou zděděné styly prázdných buněk?
Do jednoho intervalového style overlay na sloupec, ne do buněčných objektů. OdsCommitRow rozhoduje per column value, zda jde o kompaktní prázdnou buňku: řádek se opakuje víc než jednou, buňka nemá hodnotu, žádný vzorec ani rich text. Pro kompaktní prázdnou buňku vytvoří reálnou buňku jen na prvním řádku úseku, aplikuje na ni zděděný styl a pak zaregistruje stejných šest style indexů — font, fill, border, formát čísla, alignment, protection — jako StyleOverlays.Add pokrývající řádky dva až konec úseku v tom sloupci. Řádky za prvním se ve materializační smyčce přeskočí úplně
Regresní test dává tvaru konkrétní podobu. Po otevření listu, jehož druhý řádek se opakuje 1 048 575 krát pod bold defaultním stylem sloupce, se assertuje, že Sheet.Cells.Count je pod 10, a Sheet.Cells[700000, 1].FontIndex se pořád rozřeší na bold font — overlay dodá styl v momentě, kdy se té souřadnice někdo dotkne. Je to týž mechanismus, který brání formátovanému, ale prázdnému sloupci, aby na straně XLSX stál milion buněk; poznámky o ukládání buněk po blocích řádků a intervalových style overlayích rozebírají, jak se overlaye skládají a rozřešují. Nové je, že je importér ODS vytváří sam, z repeat počtu, místo aby čekal, až aplikace naformátuje rozsah
Jak SaveAsODS zapíše repeat počet zpátky?
Tím, že rozdělí prázdný ocas listu jen tam, kde něco doopravdy mění. OdsBuildTableXml teď sleduje dvě hranice: contentMaxRow, poslední řádek držící hodnotu, vzorec, hyperlink nebo ruční rozdělení stránky, a maxRow, který navíc táhne skrz style-only prázdné buňky, jednořádkové výšky, LastRow každého úseku a spodní hranu každého overlaye. Style-only prázdná buňka se už nepočítá jako obsah — TXLSXCells.IsStyleOnlyBlank je to, co ji vylučuje — takže závěrečný ostylovaný řádek v LibreOffice souboru přestane vléct content hranici na dno listu
Nad contentMaxRow se řádky zapisují jeden po druhém úplně jako dřív. Pod ní zapisovač spočítá nextRow jako nejmenší z: FirstRow dalšího úseku, LastRow + 1 aktuálního úseku, další jednořádkové položky výšky, další hrany overlaye a další materializované buňky. Všechno od aktuálního řádku po nextRow - 1 se pak emituje jako jeden <table:table-row> s table:number-rows-repeated nastaveným na rozdíl, nesoucí jeden <table:table-cell/> na sloupec s názvem stylu rozřešeným z overlaye, když overlay ten sloupec pokrývá. Styl řádku samotného vychází z TOdsAutoStylePool.RowStyleFor(AHidden, ABreakBefore, AHeightSpec), která teď složí text výšky — řekněme 12.81pt — do svého deduplikačního klíče vedle příznaků hidden a page-break, takže každý řádek v úseku sdílí jeden styl ro<N> s jedinou vlastností style:row-height
var
Workbook, Reopened: TXLSXWorkbook;
Saved: TMemoryStream;
begin
Workbook := TXLSXWorkbook.Create;
Reopened := TXLSXWorkbook.Create;
Saved := TMemoryStream.Create;
try
Workbook.OpenODS('conditional-formatting.ods');
Workbook.Sheets[1].RowHeight[500000] := 36;
Workbook.Sheets[1].ClearRowHeight(500001);
// Prázdný ocas se zapíše jako hrstka opakovaných řádků, ne milion
Workbook.SaveAsODS(Saved);
Writeln('ODS size: ', Saved.Size, ' bytes');
Saved.Position := 0;
Reopened.Open(Saved);
// Override, díra i úsek všechny přežijí round trip
Writeln(Reopened.Sheets[1].RowHeight[500000]:0:2); // 36.00
Writeln(Reopened.Sheets[1].HasRowHeight(500001)); // False
Writeln(Reopened.Sheets[1].RowHeight[500002]:0:2); // výška úseku
finally
Saved.Free;
Reopened.Free;
Workbook.Free;
end;
end;
Test, který to přibije, assertuje, že uložený stream je pod 64 KB pro list, jehož výškový úsek pokrývá 1 048 575 řádků s overridem a dírou probodnutou doprostřed. Vedle toho čísla patří dvě upřímné hranice. Za prvé, list s jakýmikoli data validacemi nastaví contentMaxRow na maxRow, takže validace vypnou kompresi ocasu na tom listu a ten se zase zapisuje řádek po řádku. Za druhé, XLSX nemá atribut repeat — SpreadsheetML <row> popisuje jeden řádek — takže export listu stojícího na úsecích do .xlsx enumeruje řádky, které úsek pokrývá, a zapíše atribut ht na každý. Model zůstává v paměti kompaktní; o tom, jak vypadá soubor, rozhoduje formát souboru
Co dluží úsekům každá editace přečíslovávající řádky?
Údržbu. Nová reprezentace metadat řádků je správná jen tehdy, když ji každá operace měnící čísla řádků posouvá spolu s per-row listy, vedle kterých sedí, a commit se dotýká každé z těch operací. InsertRows a DeleteRows jdou přes XlsxShiftRowHeightRuns, které přestaví pole tím, že ponechá část každého úseku ležící před bodem editace, zahodí, co spadne do delete okna, a znovu přidá zbytek posunutý o deltu — takže úsek ležící napříč vložením se stane dvěma úseky s mezerou a úsek napříč vymazáním se zmenší. TileRangeAxisMetadata vymaže úseky přes celé dlaždicované rozpětí a pak znovu zaregistruje každý zdrojový úsek jednou na kopii na jejím offsetu. TXLSXWorksheet.CopyFrom a TXLSXSheets.AddCopy berou Copy() pole místo přiřazení, což je důvod, proč test může vymazat všechny výšky na klonu a stále najít původní list neporušený na řádku 1 048 576
var
Sheet: TXLSXWorksheet;
begin
Sheet := Workbook.Sheets[1];
Sheet.RowHeight[500000] := 36;
Sheet.ClearRowHeight(500001);
// Vlož dva řádky na 500000: override se posune na 500002, díra na 500003
Sheet.InsertRows(500000, 2);
Writeln(Sheet.RowHeight[500002]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500003)); // False
// Smaž je zase: všechno se posune zpátky
Sheet.DeleteRows(500000, 2);
Writeln(Sheet.RowHeight[500000]:0:2); // 36.00
// Dlaždicuj řádky 2..4 dvakrát po listu; výšky úseku následují každou kopii
Sheet.TileRangeAxisMetadata(2, 1, 3, 1, 2, 1);
Writeln(Sheet.RowHeight[7]:0:2); // výška úseku
end;
Hranice na straně čtení mají stejnou povinnost. GetUsedRange zvedne spodní hranu na FirstRow a LastRow každého úseku a BuildRowMajorCellOrder protáhne své maximální řádky včetně metadat skrz každý úsek, takže zapisovač XLSX stále navštíví řádky s výškou a ničím jiným. Kdykoli nad objektovým modelem HotXLS přidáte vlastní strukturu klíčovanou řádky, tohle je checklist: insert, delete, tile, copy, used range a každý serializátor. Vynecháte jeden a selhání je tiché — výšky se rozjedou o počet vložených řádků a nic nevyhodí výjimku
Co zůstává per row a jak vypadají čísla teď
Příznaky hidden, outline úrovně a collapsed stav se pořád rozbalují. Řádkový dokončovač cyklí SetRowHidden a SetRowOutlineLevel jednou na každý opakovaný řádek, takže list, který skryje milionový ocas, nebo ho zanoří do table:table-row-group, platí per-row položku za každý z těch atributů. Změna ve 2.382.2 je zacílená na ty dvě věci, které HXLS-005 doopravdy měřil — výšky a zděděné styly prázdných buněk — a stejná technika úseků by se aplikovala i na ostatní, kdyby to nějaký soubor kdy vyžádal. ODS reader také nereaguje na style:use-optimal-row-height; styl řádku, který říká „optimal" a dává výšku, se importuje s tou výškou
Proti korpusu zvládne conditional-formatting.ods teď cyklus otevři, assert, ulož, otevři znovu, assert znovu za 0,178 sekundy na Win32 a 0,158 sekundy na Win64, přičemž samotné otevření je 0,020 sekundy, uvnitř 60sekundového rozpočtu, který dřív vyčerpala. Rozhraní na úrovni sešitu, kterými formát protéká, popisuje průvodce otevíráním a ukládáním souborů ODS a širší sadu pák pro velké soubory výkon velkých sešitů; samotný ODF řádkový element s jeho atributy repeat a stylu specifikuje ODF 1.3 Part 3 §9.1.4
HotXLS čte a zapisuje XLS, XLSX a ODS z nativního kódu Delphi a C++Builderu bez instalovaného Excelu či LibreOffice, a proto je milionový repeat řádků něco, co musí knihovna umět modelovat dobře, ne předávat externímu procesu — stránka tabulkové komponenty HotXLS pro Delphi uvádí podporované formáty a verze RAD Studia