Technický článek

Opakované řádky ODS jako úseky výšek řádků v HotXLS

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ě

Jak HotXLS promění jeden opakovaný řádek ODS v kompaktní stav: element content.xml s table:number-rows-repeated 1048530 a stylem ro1 se mapuje na jediný záznam TXLSXRowHeightRun přes řádky 46 až 1048575 o 12,81 pt plus jednu položku StyleOverlays na sloupec, zatímco verze 2.382.1 rozbalila tentýž element do milionu položek SetRowHeight a buněčných objektů
Repeat počet, výška řádku ro1 a defaultní styly sloupců popisují každý prázdný řádek pod řádkem 45, takže importér postaví jeden záznam úseku a overlaye po sloupcích bez osažení milionu souřadnic

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

Chirurgie úseku výšek řádků v HotXLS: po OpenODS pokrývá jeden úsek řádky 46 až 1048575 o 12,81 pt, zatímco per-row položka nastaví řádek 500000 na 36 pt a vyhraje lookup, protože GetRowHeight kontroluje per-row list nejdřív, a ClearRowHeight na řádku 500001 rozdělí úsek na dva disjunktní kusy kolem díry
XlsxAssignRowHeightRun zkopíruje kusy mimo vymazaný interval a pro interval samotný nepřidá nic, takže úseky zůstávají disjunktní konstrukcí a lookup může skončit u prvního zásahu, zatímco override na řádku 500000 zůstává nedotčený

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

Co SaveAsODS zapíše pro list stojící na úsecích: contentMaxRow se zastaví na řádku 45, kde končí hodnoty, zatímco maxRow táhne skrz výškový úsek a jeho overridee, řádky nad hranicí se zapisují jeden po druhém a ocas se emituje jako opakované elementy table-row, jejichž styl řádku vychází z RowStyleFor a styly buněk se rozřešují přes overlaye
Každý opakovaný element pokrývá jediný rovnoměrný úsek a zastaví se na další hraně úseku, položce výšky, hraně overlaye nebo materializované buňce, takže list bez validací se uloží jako hrstka elementů, zatímco validace nebo export do XLSX platí za každý řádek
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