Tehnički članak

HotXLS runovi visine za ponovljene ODS retke u Delphiju

HotXLS Delphi Component sprema ODS redak koji nosi table:number-rows-repeated i visinu retka kao jedan TXLSXRowHeightRun zapis — prvi redak, zadnji redak, jedna visina — umjesto jednog unosa visine po ponovljenom retku, a stil prazne ćelije koji ti reci nasljeđuju sklapa u jedan intervalni style overlay. To je cijeli razlog zašto HotXLS 2.382.2 otvara tablicu čiji rep ponavlja 1,048,530 praznih redaka u 0.02 sekunde tamo gdje je 2.382.1 istekao, i zašto se ista datoteka sprema natrag u ODS s netaknutim repeat countom, a ne kao milijun doslovnih redaka

Datoteka o kojoj je riječ je obična. LibreOffice Calc piše list s četrnaest stupaca i 45 redaka podataka, a zatim sve ispod njih opisuje jednim elementom: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Stil ro1 postavlja style:row-height="0.452cm", a svaki <table:table-column> nosi table:default-cell-style-name koji nasljeđuje svaka prazna ćelija u nizu. Cijeli content.xml ima 103 KB. Ništa u datoteci ne govori "skupo"; trošak je bio u cijelosti naš

Kako HotXLS pretvara jedan ponovljeni ODS redak u kompaktno stanje: element content.xml s table:number-rows-repeated 1048530 i stilom ro1 preslikava se u jedan zapis TXLSXRowHeightRun koji pokriva retke 46 do 1048575 na 12.81 pt plus jedan StyleOverlays unos po stupcu, dok je verzija 2.382.1 isti element raširila u milijun SetRowHeight unosa i objekata ćelija
Repeat count, visina retka ro1 i zadani stilovi stupaca opisuju svaki prazni redak ispod retka 45, pa uvoznik može izgraditi jedan zapis runa i overlaye po stupcu bez dodirivanja milijun koordinata

Zašto jedan ponovljeni redak istekne ODS uvoz?

Zato što ga je uvoznik nekad raširivao. U 2.382.1 petlja koja dovršava redak vrtjela je SetRowHeight(RowIndex + i, RowHeight) jednom po ponovljenom retku, upisujući svaku visinu u Name=Value listu stringova ključanu brojem retka. Svaki umetak u tu listu izvršio je IndexOfName pretragu po svemu što je u njoj već bilo, pa je milijun visina koštao milijun linearnih pretraga — kvadratna pretraga liste protiv koje je HXLS-005 i otvoren. Istovremeno je OdsCommitRow materijalizirao objekt ćelije za svaki stupac koji je naslijedio stil, na svakom od ponovljenih redaka, jer se stilizirana prazna ćelija i dalje računala kao ćelija

Strana spremanja imala je vlastitu verziju problema. LibreOffice datoteka završava još jednim ro1 retkom nakon velikog ponavljanja, pa je najviši stilizirani redak sjedio na samom dnu lista, a OdsBuildTableXml prolazio je kroz svaki redak do njega emitirajući elemente <table:table-row> jedan po jedan. Čak bi i radna knjiga koja je bila jeftino uvezena bila skupo zapisana. Popraviti uvoz bez popravka izvoza premjestilo bi istek, ne uklonilo ga

Što je run visine retka u HotXLS-u?

Run je najmanja stvar koja može opisati "reci 46 do 1,048,575 svi su visoki 12.81 točaka" bez da to kaže 1,048,530 puta. TXLSXRowHeightRun je zapis od FirstRow, LastRow i Height; TXLSXRowHeightRuns je dinamičko polje takvih zapisa, a svaki TXLSXWorksheet drži jedno u FRowHeightRuns uz postojeću listu visina po retku. Pri ODS uvozu petlja koja dovršava redak sada se grana po repeat countu: count 1 i dalje poziva SetRowHeight, sve veće poziva XlsxAssignRowHeightRun jednom za cijeli raspon. Raspon se reže na XlsxMaxRow, što je 1,048,576, pa se repeat count koji premaši list odsijeca, a ne odbija

XlsxAssignRowHeightRun jedini je pisac tog polja i održava runove disjunktnima po konstrukciji. Za novi interval kopira svaki postojeći run koji u cijelosti leži izvan njega, dijeli svaki run koji se s njim preklapa na dio prije i dio poslije, zatim dodaje novi interval kada je Present true — ili ne dodaje ništa kada je Present false, čime ClearRowHeight probija rupu od jednog retka. Iz toga slijede dvije stvari. Polje nikad ne sadrži preklapajuće intervale, pa se pretraga može zaustaviti na prvom pogotku. I polje se nikad ne mijenja na mjestu; svaki poziv gradi svježu kopiju, što pri tim veličinama ne košta ništa i uklanja cijelu klasu bugova s aliasima

var
  Workbook: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    // List čiji se zadnji redak ponavlja 1,048,530 puta pod jednim stilom retka
    Workbook.OpenODS('conditional-formatting.ods');
    Sheet := Workbook.Sheets[1];
    // Oba čitanja razrješavaju se kroz isti run; ništa nije bilo rašireno
    Writeln(Sheet.RowHeight[46]:0:2, ' pt');
    Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
    // Prepisivanje jednog retka zasjenjuje run bez njegova dijeljenja
    Sheet.RowHeight[500000] := 36;
    // Brisanje jednog retka unutar runa reže run na dva dijela
    Sheet.ClearRowHeight(500001);
    Writeln(Sheet.HasRowHeight(500001)); // False
    Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // još visina runa
  finally
    Workbook.Free;
  end;
end;

Redoslijed pretrage je dio koji vrijedi zapamtiti. TXLSXWorksheet.GetRowHeight prvo provjerava listu po retku i konzultira runove samo kada redak nema izričit unos, a HasRowHeight radi isto. Pa Sheet.RowHeight[500000] := 36 uopće ne dira run — dodaje jedan unos u listu po retku, i taj unos pobjeđuje jer se pretražuje prvi. ClearRowHeight je suprotno: uklanja svaki unos po retku i zatim poziva XlsxAssignRowHeightRun s Present = False, jer se obrisani redak mora čitati kao "bez visine" čak i ako ga run prekriva. ClearRowHeights prazni obje strukture odjednom

Zahvat na runu visine retka u HotXLS-u: nakon OpenODS jedan run pokriva retke 46 do 1048575 na 12.81 pt, dok unos po retku postavlja redak 500000 na 36 pt i pobjeđuje u pretrazi jer GetRowHeight prvo provjerava listu po retku, a ClearRowHeight retka 500001 dijeli run na dva disjunktna dijela oko rupe
XlsxAssignRowHeightRun kopira dijelove izvan obrisanog intervala i ne dodaje ništa za sam interval, pa runovi ostaju disjunktni po konstrukciji i pretraga se može zaustaviti na prvom pogotku, dok prepisivanje na retku 500000 ostaje netaknuto

Kamo idu naslijeđeni stilovi praznih ćelija?

U jedan intervalni style overlay po stupcu, a ne u objekte ćelija. OdsCommitRow po vrijednosti stupca odlučuje je li riječ o kompaktnoj praznini: redak se ponavlja više od jednom, ćelija nema vrijednost, formulu niti rich text. Za kompaktnu prazninu stvara pravu ćeliju samo na prvom retku runa, primjenjuje na nju naslijeđeni stil, a zatim registrira istih šest indeksa stilova — font, fill, obrub, format broja, poravnanje, zaštitu — kao StyleOverlays.Add koji pokriva retke od drugog do kraja runa u tom stupcu. Reci nakon prvog potpuno se preskaču u petlji materijalizacije

Regresijski test tu formu čini konkretnom. Nakon otvaranja lista čiji se drugi redak ponavlja 1,048,575 puta pod podebljanim zadanim stilom stupca, tvrdi se da je Sheet.Cells.Count manji od 10, a Sheet.Cells[700000, 1].FontIndex i dalje se razrješava na podebljani font — overlay daje stil u trenutku kada se ta koordinata dotakne. To je isti mehanizam koji sprema da formatirani ali prazan stupac ne košta milijun ćelija na XLSX strani; bilješke o row-block pohrani ćelija i intervalnim style overlayima pokrivaju kako se overlayi slažu i razrješavaju. Novo je ovdje to što ih ODS uvoznik stvara sam, iz repeat counta, a ne čeka da aplikacija formatira raspon

Kako SaveAsODS zapisuje repeat count natrag?

Tako da prazan rep lista dijeli samo tamo gdje se nešto doista mijenja. OdsBuildTableXml sada prati dvije granice: contentMaxRow, zadnji redak koji drži vrijednost, formulu, hyperlink ili ručni prijelom retka, i maxRow, koji se dodatno proteže kroz prazne ćelije koje nose samo stil, visine pojedinačnih redaka, svaki LastRow runa i svaki donji rub overlaya. Prazna ćelija koja nosi samo stil više se ne računa kao sadržaj — TXLSXCells.IsStyleOnlyBlank je ono što je isključuje — pa završni stilizirani redak u LibreOffice datoteci prestaje vući granicu sadržaja na dno lista

Iznad contentMaxRow reci se pišu jedan po jedan točno kao prije. Ispod njega pisac računa nextRow kao najmanji od: FirstRow sljedećeg runa, LastRow + 1 trenutnog runa, sljedeći unos visine pojedinačnog retka, sljedeći rub overlaya i sljedeća materijalizirana ćelija. Sve od trenutnog retka do nextRow - 1 zatim se emitira kao jedan <table:table-row> s table:number-rows-repeated postavljenim na tu razliku, noseći po jedan <table:table-cell/> za svaki stupac, s imenom stila razriješenim kroz overlay kada overlay pokriva taj stupac. Stil retka sam dolazi iz TOdsAutoStylePool.RowStyleFor(AHidden, ABreakBefore, AHeightSpec), koji sada tekst visine — recimo 12.81pt — sklapa u svoj ključ za deduplikaciju uz zastavice skrivenosti i prijeloma stranice, pa svaki redak u runu dijeli jedan ro<N> stil s jednim svojstvom style:row-height

Što SaveAsODS piše za list s runovima: contentMaxRow zaustavlja se na retku 45 gdje vrijednosti završavaju, dok se maxRow proteže kroz run visine i njegova prepisivanja, reci iznad granice pišu se jedan po jedan, a rep se emitira kao ponovljeni table-row elementi čiji stil retka dolazi iz RowStyleFor, a stilovi ćelija razrješavaju kroz overlaye
Svaki ponovljeni element pokriva jedan ujednačen raspon i zaustavlja se na sljedećem rubu runa, unosu visine, rubu overlaya ili materijaliziranoj ćeliji, pa se list bez validacija sprema kao nekolicina elemenata, dok validacije ili XLSX izvoz plaćaju po retku
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);
    // Prazan rep piše se kao nekolicina ponovljenih redaka, ne milijun
    Workbook.SaveAsODS(Saved);
    Writeln('ODS size: ', Saved.Size, ' bytes');
    Saved.Position := 0;
    Reopened.Open(Saved);
    // Prepisivanje, rupa i run svi preživljavaju 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);   // visina runa
  finally
    Saved.Free;
    Reopened.Free;
    Workbook.Free;
  end;
end;

Test koji ovo prikiva tvrdi da je spremljeni stream manji od 64 KB za list čiji run visine pokriva 1,048,575 redaka s prepisivanjem i probijenom rupom u sredini. Dvije iskrene granice pripadaju uz taj broj. Prvo, radni list s bilo kojim data validations postavlja contentMaxRow na maxRow, pa validacije isključuju sažimanje repa na tom listu i on se ponovno piše redak po redak. Drugo, XLSX nema atribut ponavljanja — SpreadsheetML <row> opisuje jedan redak — pa izvoz lista s runovima u .xlsx nabraja retke koje run pokriva i na svaki piše atribut ht. Model ostaje kompaktan u memoriji; format datoteke odlučuje kako datoteka izgleda

Što svaka izmjena koja prenumerira retke sada duguje runovima?

Održavanje. Nova reprezentacija metapodataka o retku ispravna je samo ako je svaka operacija koja mijenja brojeve redaka pomakne zajedno s listama po retku uz koje sjedi, i commit dotiče svaku od tih operacija. InsertRows i DeleteRows idu kroz XlsxShiftRowHeightRuns, koji ponovno gradi polje zadržavajući dio svakog runa koji leži prije točke izmjene, odbacujući sve što padne unutar prozora brisanja i ponovno dodajući ostatak pomaknut za deltu — pa run koji premošćuje umetanje postaje dva runa s prazninom, a run koji premošćuje brisanje se skraćuje. TileRangeAxisMetadata briše runove preko cijelog popločanog raspona, zatim ponovno registrira svaki izvorni run jednom po kopiji na njegovom pomaku. TXLSXWorksheet.CopyFrom i TXLSXSheets.AddCopy uzimaju Copy() polja umjesto da ga dodjeljuju, zato test može obrisati sve visine na klonu i naći originalni list netaknut na retku 1,048,576

var
  Sheet: TXLSXWorksheet;
begin
  Sheet := Workbook.Sheets[1];
  Sheet.RowHeight[500000] := 36;
  Sheet.ClearRowHeight(500001);
  // Umetni dva retka na 500000: prepisivanje se seli na 500002, rupa na 500003
  Sheet.InsertRows(500000, 2);
  Writeln(Sheet.RowHeight[500002]:0:2);   // 36.00
  Writeln(Sheet.HasRowHeight(500003));    // False
  // Obriši ih ponovno: sve se vraća natrag
  Sheet.DeleteRows(500000, 2);
  Writeln(Sheet.RowHeight[500000]:0:2);   // 36.00
  // Popločaj retke 2..4 dvaput niz list; visine runa prate svaku kopiju
  Sheet.TileRangeAxisMetadata(2, 1, 3, 1, 2, 1);
  Writeln(Sheet.RowHeight[7]:0:2);        // visina runa
end;

Granice na strani čitanja imaju istu obvezu. GetUsedRange podiže svoj donji rub na FirstRow i LastRow svakog runa, a BuildRowMajorCellOrder proteže svoj maksimum redaka uključujući metapodatke kroz svaki run da XLSX pisac i dalje posjeti retke koji nose samo visinu. Ako ikada dodate vlastitu strukturu ključanu retkom povrh HotXLS objektnog modela, ovo je kontrolna lista: umetanje, brisanje, popločavanje, kopiranje, iskorišteni raspon i svaki serializer. Promašite jedno i pad je tih — visine odlutaju za broj umetnutih redaka, a ništa ne diže iznimku

Što ostaje po retku, i kako brojevi sada izgledaju

Zastavice skrivenosti, razine outlinea i stanje skupljenosti i dalje se raširuju. Petlja koja dovršava redak vrti SetRowHidden i SetRowOutlineLevel jednom po ponovljenom retku, pa list koji skriva rep od milijun redaka, ili ga ugnježđuje unutar table:table-row-group, plaća unos po retku za svaki od tih atributa. Promjena u 2.382.2 ograničena je na dvije stvari koje je HXLS-005 doista mjerio — visine i naslijeđene stilove praznih ćelija — a ista tehnika runova primijenila bi se na ostale ako bi je neka datoteka ikad zatražila. ODS reader također ne reagira na style:use-optimal-row-height; stil retka koji kaže "optimal" i daje visinu uvozi se s tom visinom

Protiv corpusa, conditional-formatting.ods sada završava ciklus otvaranja, tvrdnje, spremanja, ponovnog otvaranja i ponovne tvrdnje u 0.178 sekundi na Win32 i 0.158 sekundi na Win64, pri čemu sama faza otvaranja traje 0.020 sekundi, unutar budžeta od 60 sekundi koji je prije iscrpljivao. Sučelja na razini radne knjige kroz koja ti formati prolaze opisana su u pregledu otvaranja i spremanja ODS datoteka, a širi skup poluga za velike datoteke u performansama velikih radnih knjiga; sam ODF element retka, s atributima ponavljanja i stila, specificiran je u ODF 1.3 Part 3 §9.1.4

HotXLS čita i piše XLS, XLSX i ODS iz izvornog Delphi i C++Builder koda bez instaliranog Excela ili LibreOfficea, zato je ponavljanje od milijun redaka nešto što biblioteka mora dobro modelirati, a ne predati vanjskom procesu — stranica HotXLS Delphi spreadsheet komponente navodi podržane formate i verzije RAD Studija