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
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ă
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
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