Articol tehnic

Serii de înălțime pentru rânduri ODS repetate în HotXLS

HotXLS Delphi Component stochează un rând ODS care poartă table:number-rows-repeated și o înălțime de rând ca o singură înregistrare TXLSXRowHeightRun — primul rând, ultimul rând, o înălțime — în loc de o intrare de înălțime per rând repetat, și pliază stilul celulelor goale pe care acele rânduri îl moștenesc într-o singură suprapunere de stil pe interval. Exact acesta este motivul pentru care HotXLS 2.382.2 deschide în 0.02 secunde o foaie de calcul a cărei coadă repetă 1,048,530 de rânduri goale, acolo unde 2.382.1 expira, și motivul pentru care același fișier se salvează înapoi în ODS cu contorul de repetări intact, nu ca un milion de rânduri literale

Fișierul în cauză este banal. LibreOffice Calc scrie o foaie de paisprezece coloane cu 45 de rânduri de date, apoi descrie tot ce se află sub ele printr-un singur element: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Stilul ro1 setează style:row-height="0.452cm", iar fiecare <table:table-column> poartă un table:default-cell-style-name pe care îl moștenește fiecare celulă goală din serie. Tot content.xml are 103 KB. Nimic din fișier nu spune „costisitor”; costul era în întregime al nostru

Cum transformă HotXLS un rând ODS repetat în stare compactă: elementul din content.xml cu table:number-rows-repeated 1048530 și stilul ro1 se mapează pe o singură înregistrare TXLSXRowHeightRun care acoperă rândurile 46 până la 1048575 la 12.81 pt, plus o intrare StyleOverlays per coloană, în timp ce versiunea 2.382.1 extindea același element într-un milion de intrări SetRowHeight și de obiecte de celulă
Contorul de repetări, înălțimea de rând ro1 și stilurile implicite de coloană descriu fiecare rând gol de sub rândul 45, așa că importatorul poate construi o singură înregistrare de serie și suprapuneri per coloană fără să atingă un milion de coordonate

De ce aduce un singur rând repetat un timeout la importul ODS?

Pentru că importatorul obișnuia să îl extindă. În 2.382.1, finalizatorul de rânduri itera SetRowHeight(RowIndex + i, RowHeight) o dată per rând repetat, scriind fiecare înălțime într-o listă de șiruri Name=Value indexată după numărul rândului. Fiecare inserare în acea listă rula o căutare IndexOfName peste tot ce se afla deja în ea, așa că un milion de înălțimi costa un milion de scanări liniare — căutarea pătratică în listă împotriva căreia a fost deschis HXLS-005. În același timp, OdsCommitRow materializa un obiect de celulă pentru fiecare coloană care moștenea un stil, pe fiecare dintre rândurile repetate, pentru că o celulă goală cu stil conta totuși ca celulă

Partea de salvare avea propria versiune a problemei. Fișierul LibreOffice se încheie cu încă un rând ro1 după marea repetare, așa că rândul cu stil cel mai de jos se afla chiar la baza foii, iar OdsBuildTableXml parcurgea fiecare rând până la el, emițând elemente <table:table-row> unul câte unul. Chiar și un registru importat ieftin ar fi fost scris scump. Repararea importului fără a repara exportul ar fi mutat timeout-ul, nu l-ar fi eliminat

Ce este o serie de înălțimi de rând în HotXLS?

O serie este cel mai mic lucru care poate descrie „rândurile 46 până la 1,048,575 au toate 12.81 puncte înălțime” fără să o spună de 1,048,530 de ori. TXLSXRowHeightRun este o înregistrare cu FirstRow, LastRow și Height; TXLSXRowHeightRuns este un tablou dinamic cu astfel de înregistrări, iar fiecare TXLSXWorksheet ține unul în FRowHeightRuns, lângă lista existentă de înălțimi per rând. La importul ODS, finalizatorul de rânduri ramifică acum după contorul de repetări: un contor de 1 apelează în continuare SetRowHeight, orice valoare mai mare apelează o singură dată XlsxAssignRowHeightRun pentru tot intervalul. Intervalul este limitat la XlsxMaxRow, care este 1,048,576, așa că un contor de repetări care depășește foaia este trunchiat, nu respins

XlsxAssignRowHeightRun este singurul scriitor al tabloului și menține seriile disjuncte prin construcție. Primește un interval nou, copiază fiecare serie existentă care se află în întregime în afara lui, sparge orice serie care se suprapune cu el în bucata de dinainte și bucata de după, apoi adaugă intervalul nou când Present este true — sau nu adaugă nimic când Present este false, ceea ce este felul în care ClearRowHeight taie o gaură de un singur rând. Două lucruri decurg de aici. Tabloul nu conține niciodată intervale suprapuse, așa că o căutare se poate opri la prima potrivire. Iar tabloul nu este mutat niciodată pe loc; la fiecare apel se construiește o copie nouă, ceea ce nu costă nimic la dimensiunile implicate și elimină o întreagă clasă de bug-uri de aliasare

var
  Workbook: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    // O foaie a cărei coadă de rânduri se repetă de 1,048,530 de ori sub un singur stil de rând
    Workbook.OpenODS('conditional-formatting.ods');
    Sheet := Workbook.Sheets[1];
    // Ambele citiri se rezolvă prin aceeași serie; nimic nu a fost extins
    Writeln(Sheet.RowHeight[46]:0:2, ' pt');
    Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
    // O suprascriere de un singur rând umbrește seria fără să o spargă
    Sheet.RowHeight[500000] := 36;
    // Curățarea unui rând din interiorul seriei taie seria în două bucăți
    Sheet.ClearRowHeight(500001);
    Writeln(Sheet.HasRowHeight(500001)); // False
    Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // încă înălțimea seriei
  finally
    Workbook.Free;
  end;
end;

Ordinea de căutare este partea care merită memorată. TXLSXWorksheet.GetRowHeight verifică mai întâi lista per rând și consultă seriile doar când rândul nu are o intrare explicită, iar HasRowHeight face la fel. Așa că Sheet.RowHeight[500000] := 36 nu atinge deloc seria — adaugă o intrare în lista per rând, iar acea intrare câștigă pentru că este căutată prima. ClearRowHeight face invers: elimină orice intrare per rând și apoi apelează XlsxAssignRowHeightRun cu Present = False, pentru că un rând curățat trebuie să se citească drept „fără înălțime” chiar dacă o serie îl acoperă. ClearRowHeights golește ambele structuri deodată

Chirurgie pe seriile de înălțimi în HotXLS: după OpenODS o serie acoperă rândurile 46 până la 1048575 la 12.81 pt, în timp ce o intrare per rând setează rândul 500000 la 36 pt și câștigă căutarea pentru că GetRowHeight verifică mai întâi lista per rând, iar ClearRowHeight pentru rândul 500001 sparge seria în două bucăți disjuncte în jurul găurii
XlsxAssignRowHeightRun copiază bucățile din afara intervalului curățat și nu adaugă nimic pentru intervalul însuși, așa că seriile rămân disjuncte prin construcție și o căutare se poate opri la prima potrivire, în timp ce suprascrierea de la rândul 500000 rămâne neatinsă

Unde ajung stilurile moștenite ale celulelor goale?

Într-o singură suprapunere de stil pe interval per coloană, nu în obiecte de celulă. OdsCommitRow decide pentru fiecare valoare de coloană dacă este o celulă goală compactă: rândul se repetă de mai multe ori, celula nu are valoare, nici formulă, nici text formatat. Pentru o celulă goală compactă creează o celulă reală doar pe primul rând al seriei, aplică pe ea stilul moștenit, apoi înregistrează aceleași șase indexuri de stil — font, umplere, bordură, format numeric, aliniere, protecție — ca un StyleOverlays.Add care acoperă rândurile de la al doilea până la sfârșitul seriei în acea coloană. Rândurile de după primul sunt sărite complet în bucla de materializare

Testul de regresie face forma concretă. După deschiderea unei foi al cărei al doilea rând se repetă de 1,048,575 de ori sub un stil implicit de coloană îngroșat, se verifică faptul că Sheet.Cells.Count este sub 10, iar Sheet.Cells[700000, 1].FontIndex se rezolvă în continuare la fontul îngroșat — suprapunerea furnizează stilul în momentul în care acea coordonată este atinsă. Acesta este același mecanism care împiedică o coloană formatată dar goală să coste un milion de celule pe partea de XLSX; notele despre stocarea celulelor pe blocuri de rânduri și suprapunerile de stil pe interval acoperă felul în care suprapunerile se stratifică și se rezolvă. Ce este nou aici este că importatorul ODS le creează singur, din contorul de repetări, în loc să aștepte ca o aplicație să formateze un interval

Cum scrie SaveAsODS contorul de repetări înapoi?

Împărțind coada goală a foii doar acolo unde se schimbă efectiv ceva. OdsBuildTableXml urmărește acum două limite: contentMaxRow, ultimul rând care conține o valoare, o formulă, un hyperlink sau o rupere manuală de rând, și maxRow, care se extinde în plus prin celulele goale doar cu stil, prin înălțimile de un singur rând, prin LastRow al fiecărei serii și prin marginea de jos a fiecărei suprapuneri. O celulă goală doar cu stil nu mai contează ca conținut — TXLSXCells.IsStyleOnlyBlank este cea care o exclude — așa că rândul cu stil de la finalul fișierului LibreOffice nu mai târăște limita de conținut până la baza foii

Peste contentMaxRow, rândurile sunt scrise unul câte unul exact ca înainte. Sub el, scriitorul calculează nextRow ca cel mai mic dintre: FirstRow al seriei următoare, LastRow + 1 al seriei curente, următoarea intrare de înălțime de un singur rând, următoarea margine de suprapunere și următoarea celulă materializată. Tot ce merge de la rândul curent până la nextRow - 1 este apoi emis ca un singur <table:table-row> cu table:number-rows-repeated setat la diferența, purtând un <table:table-cell/> per coloană, cu numele de stil rezolvat prin suprapunere atunci când o suprapunere acoperă acea coloană. Stilul de rând în sine vine de la TOdsAutoStylePool.RowStyleFor(AHidden, ABreakBefore, AHeightSpec), care include acum textul înălțimii — 12.81pt, de exemplu — în cheia lui de deduplicare, alături de flag-urile de ascundere și de rupere de pagină, așa că fiecare rând din serie împarte un singur stil ro<N> cu o singură proprietate style:row-height

Ce scrie SaveAsODS pentru o foaie susținută de serii: contentMaxRow se oprește la rândul 45, unde se termină valorile, în timp ce maxRow se extinde prin seria de înălțimi și suprascrierile ei, rândurile de deasupra limitei sunt scrise unul câte unul, iar coada este emisă ca elemente table-row repetate, al căror stil de rând vine din RowStyleFor și ale căror stiluri de celulă se rezolvă prin suprapuneri
Fiecare element repetat acoperă o porțiune uniformă și se oprește la următoarea margine de serie, intrare de înălțime, margine de suprapunere sau celulă materializată, așa că o foaie fără validări se salvează ca o mână de elemente, în timp ce validările sau un export XLSX plătesc per rând
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);
    // Coada goală este scrisă ca o mână de rânduri repetate, nu un milion
    Workbook.SaveAsODS(Saved);
    Writeln('ODS size: ', Saved.Size, ' bytes');
    Saved.Position := 0;
    Reopened.Open(Saved);
    // Suprascrierea, gaura și seria supraviețuiesc toate round-trip-ului
    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);   // înălțimea seriei
  finally
    Saved.Free;
    Reopened.Free;
    Workbook.Free;
  end;
end;

Testul care fixează asta verifică faptul că fluxul salvat este sub 64 KB pentru o foaie a cărei serie de înălțimi acoperă 1,048,575 de rânduri, cu o suprascriere și o gaură tăiată la mijloc. Două limite oneste își au locul lângă acel număr. Prima: o foaie de calcul cu orice validări de date setează contentMaxRow la maxRow, așa că validările dezactivează compactarea cozii pe acea foaie și ea este scrisă din nou rând cu rând. A doua: XLSX nu are atribut de repetare — un <row> SpreadsheetML descrie un singur rând — așa că exportul unei foi susținute de serii în .xlsx enumeră rândurile pe care le acoperă seria și scrie un atribut ht pe fiecare. Modelul rămâne compact în memorie; formatul de fișier decide cum arată fișierul

Ce datorează seriilor fiecare editare care renumerotează rânduri?

Întreținere. O reprezentare nouă a metadatelor de rând este corectă doar dacă fiecare operație care schimbă numerele de rând o mută împreună cu listele per rând de lângă ea, iar commit-ul atinge fiecare dintre acele operații. InsertRows și DeleteRows trec prin XlsxShiftRowHeightRuns, care reconstruiește tabloul păstrând partea fiecărei serii care se află înaintea punctului de editare, aruncând ce cade în interiorul unei ferestre de ștergere și re-adăugând restul deplasat cu delta — așa că o serie care se întinde peste o inserare devine două serii cu un gol între ele, iar una care se întinde peste o ștergere se micșorează. TileRangeAxisMetadata curăță seriile pe toată porțiunea repetată, apoi re-înregistrează fiecare serie sursă o dată per copie, la offset-ul ei. TXLSXWorksheet.CopyFrom și TXLSXSheets.AddCopy iau un Copy() al tabloului în loc să îl atribuie, motiv pentru care testul poate curăța toate înălțimile pe o clonă și găsește totuși foaia originală intactă la rândul 1,048,576

var
  Sheet: TXLSXWorksheet;
begin
  Sheet := Workbook.Sheets[1];
  Sheet.RowHeight[500000] := 36;
  Sheet.ClearRowHeight(500001);
  // Inserează două rânduri la 500000: suprascrierea se mută la 500002, gaura la 500003
  Sheet.InsertRows(500000, 2);
  Writeln(Sheet.RowHeight[500002]:0:2);   // 36.00
  Writeln(Sheet.HasRowHeight(500003));    // False
  // Șterge-le din nou: totul se deplasează înapoi
  Sheet.DeleteRows(500000, 2);
  Writeln(Sheet.RowHeight[500000]:0:2);   // 36.00
  // Repetă rândurile 2..4 de două ori în josul foii; înălțimile seriei urmează fiecare copie
  Sheet.TileRangeAxisMetadata(2, 1, 3, 1, 2, 1);
  Writeln(Sheet.RowHeight[7]:0:2);        // înălțimea seriei
end;

Limitele din partea de citire au aceeași obligație. GetUsedRange își ridică marginea de jos până la FirstRow și LastRow ale fiecărei serii, iar BuildRowMajorCellOrder își extinde rândul maxim care include metadatele prin fiecare serie, ca scriitorul XLSX să viziteze în continuare și rândurile care au doar înălțime. Dacă veți adăuga vreodată o structură proprie indexată pe rânduri peste modelul de obiecte HotXLS, aceasta este lista de verificare: inserare, ștergere, repetare, copiere, interval folosit și fiecare serializator. Ratați unul și eșecul este tăcut — înălțimile se deplasează cu numărul de inserări și nimic nu ridică excepție

Ce rămâne per rând și cum arată cifrele acum

Flag-urile de ascundere, nivelurile de contur și starea de pliere se extind în continuare. Finalizatorul de rânduri itera SetRowHidden și SetRowOutlineLevel o dată per rând repetat, așa că o foaie care ascunde o coadă de un milion de rânduri, sau o imbrică într-un table:table-row-group, plătește o intrare per rând pentru fiecare dintre acele atribute. Modificarea din 2.382.2 este limitată la cele două lucruri pe care HXLS-005 le-a măsurat efectiv — înălțimi și stiluri moștenite de celule goale — iar aceeași tehnică de serii s-ar aplica și celorlalte dacă vreun fișier ar cere-o. Reader-ul ODS nu acționează nici pe style:use-optimal-row-height; un stil de rând care spune „optimal” și dă o înălțime este importat cu acea înălțime

Pe corpus, conditional-formatting.ods încheie acum ciclul de deschidere, verificare, salvare, redeschidere și re-verificare în 0.178 secunde pe Win32 și 0.158 secunde pe Win64, cu etapa de deschidere în sine la 0.020 secunde, într-un buget de 60 de secunde pe care îl epuiza anterior. Interfețele la nivel de registru prin care trece formatul sunt descrise în ghidul despre deschiderea și salvarea fișierelor ODS, iar setul mai larg de pârghii pentru fișiere mari în performanța registrelor mari; elementul de rând ODF în sine, cu atributele lui de repetare și de stil, este specificat în ODF 1.3 Part 3 §9.1.4

HotXLS citește și scrie XLS, XLSX și ODS din cod nativ Delphi și C++Builder fără Excel sau LibreOffice instalat, motiv pentru care o repetare de un milion de rânduri este ceva ce librăria trebuie să modeleze bine, nu să delege unui proces extern — pagina componentei HotXLS de foaie de calcul pentru Delphi listează formatele acceptate și versiunile RAD Studio