HotXLS Delphi Component gemmer en ODS-række, der bærer table:number-rows-repeated og en rækkehøjde, som én enkelt TXLSXRowHeightRun-post — første række, sidste række, én højde — i stedet for én højdepost pr. gentagen række, og folder den tomcelle-style, rækkerne nedarver, ind i ét enkelt interval style-overlay. Det er hele grunden til, at HotXLS 2.382.2 åbner et regneark, hvis hale gentager 1.048.530 tomme rækker, på 0,02 sekunder, hvor 2.382.1 timede ud, og til at samme fil gemmer tilbage til ODS med repeat-tallet intakt frem for som en million bogstavelige rækker
Filen i spørgsmål er almindelig. LibreOffice Calc skriver et ark med fjorten kolonner og 45 datarækker og beskriver derefter alt under dem med ét element: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Stylen ro1 sætter style:row-height="0.452cm", og hver <table:table-column> bærer en table:default-cell-style-name, som hver tom celle i runet nedarver. Hele content.xml er 103 KB. Intet ved filen siger "dyr"; udgiften var udelukkende vores
Hvorfor får én gentagen række en ODS-import til at time ud?
Fordi importøren plejede at ekspandere den. I 2.382.1 loopede row finisher'en SetRowHeight(RowIndex + i, RowHeight) én gang pr. gentagen række og skrev hver højde ind i en Name=Value-strengliste nøglet med rækkenummer. Hver indsættelse i den liste kørte et IndexOfName-opslag over alt, der allerede var i den, så en million højder kostede en million lineære scans — den kvadratiske listesøgning, HXLS-005 blev indgivet imod. Samtidig materialiserede OdsCommitRow et celleobjekt for hver kolonne, der nedarvede en style, på hver eneste af de gentagne rækker, fordi en stylet tom celle stadig talte som en celle
Save-siden havde sin egen version af problemet. LibreOffice-filen slutter med endnu en ro1-række efter den store gentagelse, så den højest stylede række lå helt i bunden af arket, og OdsBuildTableXml gik hver række op til den og emitterede <table:table-row>-elementer én ad gangen. Selv en arbejdsbog, der var blevet importeret billigt, ville være blevet skrevet dyrt. At rette importen uden at rette eksporten ville have flyttet timeouten, ikke fjernet den
Hvad er et row-height run i HotXLS?
Et run er det mindste, der kan beskrive "række 46 til og med 1.048.575 er alle 12,81 point høje" uden at sige det 1.048.530 gange. TXLSXRowHeightRun er en post af FirstRow, LastRow og Height; TXLSXRowHeightRuns er et dynamisk array af dem, og hver TXLSXWorksheet holder ét i FRowHeightRuns ved siden af den eksisterende per-række-højdeliste. Ved ODS-import forgrener row finisher'en sig nu på repeat-tallet: Et tal på 1 kalder stadig SetRowHeight, alt større kalder XlsxAssignRowHeightRun én gang for hele spændet. Spændet clamps til XlsxMaxRow, som er 1.048.576, så et repeat-tal, der skyder over arket, trunkeres frem for at blive afvist
XlsxAssignRowHeightRun er arrayets eneste writer, og den holder runsene disjunkte ved konstruktion. Givet et nyt interval kopierer den hver eksisterende run, der ligger helt uden for det, splitter enhver run, der overlapper det, op i stykket før og stykket efter og appenderer derefter det nye interval, når Present er True — eller appenderer intet, når Present er False, hvilket er sådan ClearRowHeight slår et ét-række-hul. To ting følger. Arrayet indeholder aldrig overlappende intervaller, så et opslag kan stoppe ved det første hit. Og arrayet muteres aldrig in place; en frisk kopi bygges ved hvert kald, hvilket ikke koster noget ved de størrelser, der er tale om, og fjerner en hel klasse af aliasing-bugs
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Workbook := TXLSXWorkbook.Create;
try
// Et ark, hvis halerække gentages 1.048.530 gange under én row style
Workbook.OpenODS('conditional-formatting.ods');
Sheet := Workbook.Sheets[1];
// Begge læsninger opløses gennem samme run; intet blev ekspanderet
Writeln(Sheet.RowHeight[46]:0:2, ' pt');
Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
// En enkelt-række-override skygger runet uden at splitte det
Sheet.RowHeight[500000] := 36;
// At rydde én række inde i runet skærer runet op i to stykker
Sheet.ClearRowHeight(500001);
Writeln(Sheet.HasRowHeight(500001)); // False
Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // stadig run-højden
finally
Workbook.Free;
end;
end;
Opslagsrækkefølgen er den del, der er værd at huske. TXLSXWorksheet.GetRowHeight tjekker per-række-listen først og konsulterer runsene kun, når rækken ikke har nogen eksplicit post, og HasRowHeight gør det samme. Så rører Sheet.RowHeight[500000] := 36 slet ikke runet — det tilføjer én post til per-række-listen, og den post vinder, fordi den slås op først. ClearRowHeight er det modsatte: Den fjerner enhver per-række-post og kalder derefter XlsxAssignRowHeightRun med Present = False, fordi en ryddet række skal læses som "ingen højde", selv hvis et run dækker den. ClearRowHeights tømmer begge strukturer på én gang
Hvor bliver de nedarvede tomcelle-styles af?
Ind i ét interval style-overlay pr. kolonne, ikke ind i celleobjekter. OdsCommitRow afgør pr. kolonneværdi, om det er en kompakt tom celle: Rækken gentages mere end én gang, cellen har ingen værdi, ingen formel og ingen rich text. For en kompakt tom celle opretter den en rigtig celle kun på runets første række, anvender den nedarvede style på den og registrerer derefter de samme seks style-indekser — font, fill, border, number format, alignment, protection — som en StyleOverlays.Add, der dækker række to til og med runets slutning i den kolonne. Rækker efter den første springes helt over i materialiseringsløkken
Regressionstesten gør formen konkret. Efter at have åbnet et ark, hvis anden række gentages 1.048.575 gange under en fed kolonne-default-style, asseres Sheet.Cells.Count at være under 10, og Sheet.Cells[700000, 1].FontIndex opløses stadig til den fede font — overlayet leverer stylen i det øjeblik, koordinaten røres. Det er samme mekanisme, der forhindrer en formateret men tom kolonne i at koste en million celler på XLSX-siden; noterne om row-block cell storage og interval style overlays dækker, hvordan overlays lægger sig og opløses. Det nye her er, at ODS-importøren skaber dem på egen hånd, ud fra repeat-tallet, i stedet for at vente på, at en applikation formaterer et range
Hvordan skriver SaveAsODS repeat-tallet tilbage?
Ved at splitte arkets tomme hale kun dér, hvor noget faktisk ændrer sig. OdsBuildTableXml sporer nu to grænser: contentMaxRow, den sidste række, der holder en værdi, formel, hyperlink eller manuelt rækkeskift, og maxRow, som desuden strækker sig gennem style-only tomme celler, enkelt-række-højder, hvert runs LastRow og hvert overlays nederste kant. En style-only tom celle tæller ikke længere som indhold — TXLSXCells.IsStyleOnlyBlank er det, der udelukker den — så den afsluttende stylede række i LibreOffice-filen holder op med at slæbe indholdsgrænsen med ned i bunden af arket
Over contentMaxRow skrives rækker én ad gangen præcis som før. Under den beregner writeren nextRow som det mindste af: Næste runs FirstRow, aktuelle runs LastRow + 1, næste enkelt-række-højdepost, næste overlay-kant og næste materialiserede celle. Alt fra den aktuelle række op til nextRow - 1 emiteres derefter som én <table:table-row> med table:number-rows-repeated sat til differencen, med én <table:table-cell/> pr. kolonne med det overlay-opløste stylenavn, når et overlay dækker den kolonne. Rækkens style kommer selv fra TOdsAutoStylePool.RowStyleFor(AHidden, ABreakBefore, AHeightSpec), som nu folder højdeteksten — lad os sige 12.81pt — ind i sin deduplikeringsnøgle sammen med hidden- og page-break-flagene, så hver række i runet deler én ro<N>-style med en enkelt style:row-height-property
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);
// Den tomme hale skrives som en håndfuld gentagne rækker, ikke en million
Workbook.SaveAsODS(Saved);
Writeln('ODS size: ', Saved.Size, ' bytes');
Saved.Position := 0;
Reopened.Open(Saved);
// Override, hul og run overlever alle round trip'en
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); // run-højden
finally
Saved.Free;
Reopened.Free;
Workbook.Free;
end;
end;
Testen, der fastlåser det, asserter at den gemte stream er under 64 KB for et ark, hvis height run spænder over 1.048.575 rækker med en override og et hul slået ind i midten. To ærlige grænser hører ved det tal. For det første sætter et regneark med data validations contentMaxRow til maxRow, så validations deaktiverer halekomprimering på det ark, og det skrives række for række igen. For det andet har XLSX ingen repeat-attribut — en SpreadsheetML <row> beskriver én række — så eksport af et run-båret ark til .xlsx enumererer de rækker, runet dækker, og skriver en ht-attribut på hver. Modellen forbliver kompakt i hukommelsen; filformatet afgør, hvordan filen ser ud
Hvad skylder hver række-omnummererende redigering nu runsene?
Vedligeholdelse. En ny repræsentation af rækkemetadata er kun korrekt, hvis hver operation, der ændrer rækkenumre, flytter den med sammen med de per-række-lister, den bor ved siden af, og commit'en rører hver af disse operationer. InsertRows og DeleteRows går gennem XlsxShiftRowHeightRuns, som genopbygger arrayet ved at beholde den del af hvert run, der ligger før redigeringspunktet, droppe alt, der falder inden for et sletningsvindue, og gen-tilføje resten forskudt med deltaet — så et run, der spænder hen over en indsættelse, bliver til to runs med et mellemrum, og et run, der spænder hen over en sletning, krymper. TileRangeAxisMetadata rydder runsene hen over hele det tiling-spænd og gen-registrerer derefter hver kildes run én gang pr. kopi på dens offset. TXLSXWorksheet.CopyFrom og TXLSXSheets.AddCopy tager en Copy() af arrayet frem for at tildele den, hvilket er grunden til, at testen kan rydde alle højder på en klon og stadig finde originalarket intakt ved række 1.048.576
var
Sheet: TXLSXWorksheet;
begin
Sheet := Workbook.Sheets[1];
Sheet.RowHeight[500000] := 36;
Sheet.ClearRowHeight(500001);
// Indsæt to rækker ved 500000: Overriden flytter til 500002, hullet til 500003
Sheet.InsertRows(500000, 2);
Writeln(Sheet.RowHeight[500002]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500003)); // False
// Slet dem igen: Alt forskubbes tilbage
Sheet.DeleteRows(500000, 2);
Writeln(Sheet.RowHeight[500000]:0:2); // 36.00
// Tile rækker 2..4 to gange ned ad arket; run-højder følger hver kopi
Sheet.TileRangeAxisMetadata(2, 1, 3, 1, 2, 1);
Writeln(Sheet.RowHeight[7]:0:2); // run-højden
end;
Læsesidens grænser har samme forpligtelse. GetUsedRange skruer sin nederste kant op til hvert runs FirstRow og LastRow, og BuildRowMajorCellOrder strækker sin metadata-inklusive maksimale række gennem hvert run, så XLSX-writeren stadig besøger height-only rækker. Tilføjer du nogensinde din egen række-nøglede struktur oven på HotXLS-objektmodellen, er dette tjeklisten: insert, delete, tile, copy, used range og enhver serializer. Misser du én, er fejlen lydløs — højder driver med indsættelsestallet, og intet raiser
Hvad forbliver pr. række, og hvordan ser tallene ud nu
Hidden-flag, outline levels og collapsed state ekspanderer stadig. Row finisher'en looper SetRowHidden og SetRowOutlineLevel én gang pr. gentagen række, så et ark, der skjuler en million-række-hale, eller lægger den i en table:table-row-group, betaler en per-række-post for hver af de attributter. Ændringen i 2.382.2 er afgrænset til de to ting, HXLS-005 faktisk målte — højder og nedarvede tomme styles — og samme run-teknik ville gælde de andre, hvis en fil nogensinde krævede det. ODS-readeren gør heller ikke noget ved style:use-optimal-row-height; en rækkestyle, der siger "optimal" og giver en højde, importeres med den højde
Målt mod korpuset gennemfører conditional-formatting.ods nu open-, assert-, save-, reopen- og re-assert-cyklussen på 0,178 sekunder på Win32 og 0,158 sekunder på Win64, med selve open-steget på 0,020 sekunder, inden for et 60-sekundersbudget, det tidligere udtømte. Interface-ne på arbejdsbog-niveau, som formatet flyder gennem, er beskrevet i gennemgangen af at åbne og gemme ODS-filer, og det bredere sæt af greb til store filer i large workbook performance; selve ODF-række-elementet, med dets repeat- og style-attributter, er specificeret i ODF 1.3 Part 3 §9.1.4
HotXLS læser og skriver XLS, XLSX og ODS fra native Delphi- og C++Builder-kode uden Excel eller LibreOffice installeret, og det er derfor, en million-række-gentagelse er noget, biblioteket selv skal modellere godt frem for at lægge i hænderne på en ekstern proces — siden HotXLS Delphi spreadsheet-komponent lister de understøttede formater og RAD Studio-versioner