Tehnički članak

HotXLS ODS ponovljeni redovi kao run-ovi visine u Delphiju

HotXLS Delphi Component čuva ODS red koji nosi table:number-rows-repeated i visinu reda kao jedan zapis TXLSXRowHeightRun — prvi red, poslednji red, jedna visina — umesto po jednog unosa visine za svaki ponovljeni red, a stil prazne ćelije koji ti redovi nasleđuju svodi u jedan intervalni style overlay. To je ceo razlog zašto HotXLS 2.382.2 otvara tabelu čiji završetak ponavlja 1.048.530 praznih redova za 0,02 sekunde tamo gde je 2.382.1 istekao, i zašto se isti fajl vraća u ODS sa netaknutim brojem ponavljanja a ne kao milion doslovnih redova

Fajl o kojem je reč je običan. LibreOffice Calc piše list sa četrnaest kolona i 45 redova 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 svaka <table:table-column> nosi table:default-cell-style-name koji nasleđuje svaka prazna ćelija u tom nizu. Ceo content.xml ima 103 KB. Ništa u fajlu ne kaže „skupo"; trošak je bio potpuno naš

Kako HotXLS pretvara jedan ponovljeni ODS red u kompaktno stanje: element iz content.xml sa table:number-rows-repeated 1048530 i stilom ro1 preslikava se u jedan zapis TXLSXRowHeightRun koji pokriva redove 46 do 1048575 na 12.81 pt plus jedan StyleOverlays unos po koloni, dok je verzija 2.382.1 isti element razvijala u milion SetRowHeight unosa i cell objekata
Broj ponavljanja, visina reda iz ro1 i podrazumevani stilovi kolona opisuju svaki prazan red ispod reda 45, pa uvoznik može da izgradi jedan run zapis i overlay-e po kolonama bez dodirivanja milion koordinata

Zašto jedan ponovljeni red obara uvoz ODS-a na isteku vremena?

Zato što ga je uvoznik nekad razvijao. U 2.382.1 završetak reda vrtio je SetRowHeight(RowIndex + i, RowHeight) po jednom za svaki ponovljeni red, upisujući svaku visinu u string listu Name=Value sa ključem po broju reda. Svaki umetak u tu listu pokretao je IndexOfName pretragu kroz sve što je već u njoj, pa je milion visina koštao milion linearnih prolaza — kvadratna pretraga liste protiv koje je HXLS-005 i otvoren. Istovremeno je OdsCommitRow materijalizovao cell objekat za svaku kolonu koja je nasledila stil, u svakom od tih ponovljenih redova, jer se i stilizovana prazna ćelija računala kao ćelija

Strana čuvanja imala je svoju verziju istog problema. LibreOffice fajl se završava još jednim ro1 redom posle velikog ponavljanja, pa je najviši stilizovani red stajao na samom dnu lista, a OdsBuildTableXml je prolazio kroz svaki red do njega emitujući <table:table-row> elemente jedan po jedan. Čak i radna sveska koja je jeftino uvezena bila bi skupo upisana. Ispravka uvoza bez ispravke izvoza samo bi pomerila istek vremena, ne bi ga uklonila

Šta je run visine reda u HotXLS-u?

Run je najmanja stvar koja može da opiše „redovi 46 do 1.048.575 su svi visoki 12,81 poena" a da to ne kaže 1.048.530 puta. TXLSXRowHeightRun je zapis sa FirstRow, LastRow i Height; TXLSXRowHeightRuns je dinamički niz takvih zapisa, i svaki TXLSXWorksheet drži jedan u FRowHeightRuns pored postojeće liste visina po redu. Pri uvozu ODS-a završetak reda sada se grana po broju ponavljanja: broj 1 i dalje poziva SetRowHeight, sve veće poziva XlsxAssignRowHeightRun jednom za ceo raspon. Raspon se klampuje na XlsxMaxRow, što je 1.048.576, pa se broj ponavljanja koji premašuje list skraćuje a ne odbija

XlsxAssignRowHeightRun jedini je pisac tog niza, i drži run-ove disjunktnim po konstrukciji. Za novi interval kopira svaki postojeći run koji leži potpuno izvan njega, deli svaki run koji se s njim preklapa na deo pre i deo posle, a zatim dodaje novi interval kada je Present True — ili ne dodaje ništa kada je Present False, čime ClearRowHeight probija rupu od jednog reda. Iz toga slede dve stvari. Niz nikad ne sadrži intervale koji se preklapaju, pa pretraga može da stane na prvom pogotku. I niz se nikad ne menja na mestu; sveža kopija se gradi pri svakom pozivu, što pri tim veličinama ništa ne košta a uklanja čitavu klasu bugova sa alijasiranjem

var
  Workbook: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    // List čiji se završni red ponavlja 1,048,530 puta pod jednim stilom reda
    Workbook.OpenODS('conditional-formatting.ods');
    Sheet := Workbook.Sheets[1];
    // Oba čitanja se razrešavaju kroz isti run; ništa nije razvijeno
    Writeln(Sheet.RowHeight[46]:0:2, ' pt');
    Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
    // Prevazilaženje za jedan red zasenčuje run bez njegovog deljenja
    Sheet.RowHeight[500000] := 36;
    // Brisanje jednog reda unutar run-a seče run na dva dela
    Sheet.ClearRowHeight(500001);
    Writeln(Sheet.HasRowHeight(500001)); // False
    Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // još uvek visina run-a
  finally
    Workbook.Free;
  end;
end;

Redosled pretrage je deo koji vredi zapamtiti. TXLSXWorksheet.GetRowHeight prvo proverava listu po redu i konsultuje run-ove samo kada red nema eksplicitni unos, a HasRowHeight radi isto. Zato Sheet.RowHeight[500000] := 36 ne dira run uopšte — dodaje jedan unos u listu po redu, i taj unos pobeđuje jer se prvi pretražuje. ClearRowHeight je suprotnost: uklanja svaki unos po redu i zatim poziva XlsxAssignRowHeightRun sa Present = False, jer red koji je obrisan mora da se čita kao „bez visine" čak i ako ga run pokriva. ClearRowHeights prazni obe strukture odjednom

Hirurgija nad run-om visine reda u HotXLS-u: posle OpenODS jedan run pokriva redove 46 do 1048575 na 12.81 pt dok unos po redu postavlja red 500000 na 36 pt i pobeđuje u pretrazi jer GetRowHeight prvo proverava listu po redu, a ClearRowHeight reda 500001 deli run na dva disjunktna dela oko rupe
XlsxAssignRowHeightRun kopira delove izvan obrisanog intervala i ne dodaje ništa za sam interval, pa run-ovi ostaju disjunktni po konstrukciji i pretraga može stati na prvom pogotku dok prevazilaženje u redu 500000 ostaje netaknuto

Gde idu nasleđeni stilovi praznih ćelija?

U jedan intervalni style overlay po koloni, a ne u cell objekte. OdsCommitRow odlučuje po vrednosti kolone da li je prazna ćelija kompaktna: red se ponavlja više od jednom, ćelija nema vrednost, nema formulu i nema rich text. Za kompaktnu praznu ćeliju pravi pravu ćeliju samo u prvom redu run-a, primenjuje nasleđeni stil na nju, a zatim registruje istih šest indeksa stilova — font, fill, border, number format, alignment, protection — kao StyleOverlays.Add koji pokriva redove od drugog do kraja run-a u toj koloni. Redovi posle prvog potpuno se preskaču u petlji materijalizacije

Regresioni test oblik čini opipljivim. Posle otvaranja lista čiji se drugi red ponavlja 1.048.575 puta pod podrazumevanim stilom kolone sa boldom, tvrdi se da je Sheet.Cells.Count ispod 10, a Sheet.Cells[700000, 1].FontIndex i dalje se razrešava na bold font — overlay daje stil u trenutku kada se ta koordinata dotakne. To je isti mehanizam koji sprečava da formatirana ali prazna kolona košta milion ćelija na XLSX strani; beleške o skladištenju ćelija u blokovima redova i intervalnim style overlay-ima pokrivaju kako se overlay-i slažu i razrešavaju. Ovde je novo to što ih ODS uvoznik stvara sam, iz broja ponavljanja, umesto da čeka da aplikacija formatira opseg

Kako SaveAsODS upisuje broj ponavljanja nazad?

Deljenjem praznog završetka lista samo tamo gde se nešto zaista menja. OdsBuildTableXml sada prati dve granice: contentMaxRow, poslednji red koji drži vrednost, formulu, hiperlink ili ručni prelom reda, i maxRow, koji se dodatno proteže kroz prazne ćelije koje nose samo stil, visine pojedinačnih redova, LastRow svakog run-a i donju ivicu svakog overlay-a. Prazna ćelija koja nosi samo stil više se ne računa kao sadržaj — TXLSXCells.IsStyleOnlyBlank je ono što je izuzima — pa završni stilizovani red u LibreOffice fajlu prestaje da vuče granicu sadržaja na dno lista

Iznad contentMaxRow redovi se pišu jedan po jedan tačno kao pre. Ispod njega pisac računa nextRow kao najmanji od: FirstRow sledećeg run-a, LastRow + 1 tekućeg run-a, sledećeg unosa visine pojedinačnog reda, sledeće ivice overlay-a, i sledeće materijalizovane ćelije. Sve od tekućeg reda do nextRow - 1 zatim se emituje kao jedan <table:table-row> sa table:number-rows-repeated postavljenim na razliku, noseći po jedan <table:table-cell/> za svaku kolonu sa imenom stila razrešenim iz overlay-a kada overlay pokriva tu kolonu. Stil reda dolazi iz TOdsAutoStylePool.RowStyleFor(AHidden, ABreakBefore, AHeightSpec), koji sada u svoj ključ za deduplikaciju uz zastavice skrivenosti i preloma strane uvlači i tekst visine — recimo 12.81pt — pa svaki red u run-u deli jedan ro<N> stil sa jednim svojstvom style:row-height

Šta SaveAsODS piše za list koji se oslanja na run: contentMaxRow staje na redu 45 gde vrednosti prestaju dok se maxRow proteže kroz run visine i njegova prevazilaženja, redovi iznad granice pišu se jedan po jedan, a završetak se emituje kao ponovljeni table-row elementi čiji stil reda dolazi iz RowStyleFor a stilovi ćelija se razrešavaju kroz overlay-e
Svaki ponovljeni element pokriva jedan ujednačen deo i staje na sledećoj ivici run-a, unosu visine, ivici overlay-a ili materijalizovanoj ćeliji, pa se list bez validacija čuva kao nekolicina elemenata dok validacije ili XLSX izvoz plaćaju po redu
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 završetak se piše kao nekolicina ponovljenih redova, ne milion
    Workbook.SaveAsODS(Saved);
    Writeln('ODS size: ', Saved.Size, ' bytes');
    Saved.Position := 0;
    Reopened.Open(Saved);
    // Prevazilaženje, 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 run-a
  finally
    Saved.Free;
    Reopened.Free;
    Workbook.Free;
  end;
end;

Test koji ovo prikiva tvrdi da je sačuvani tok manji od 64 KB za list čiji run visine pokriva 1.048.575 redova sa prevazilaženjem i rupom probijenom u sredini. Dve iskrene granice idu uz taj broj. Prvo, radni list sa bilo kakvim data validacijama postavlja contentMaxRow na maxRow, pa validacije isključuju sažimanje završetka na tom listu i on se ponovo piše red po red. Drugo, XLSX nema atribut ponavljanja — SpreadsheetML <row> opisuje jedan red — pa izvoz lista koji se oslanja na run u .xlsx nabraja redove koje run pokriva i piše ht atribut na svakom. Model ostaje kompaktan u memoriji; format fajla odlučuje kako fajl izgleda

Šta svaka izmena koja prenumeriše redove sada duguje run-ovima?

Održavanje. Nova reprezentacija metapodataka redova ispravna je samo ako je svaka operacija koja menja brojeve redova pomera zajedno sa listama po redu pored kojih stoji, i ovaj commit dira svaku od tih operacija. InsertRows i DeleteRows idu kroz XlsxShiftRowHeightRuns, koji ponovo gradi niz tako što zadržava deo svakog run-a pre tačke izmene, odbacuje sve što padne unutar prozora brisanja, i ponovo dodaje ostatak pomeren za deltu — pa run koji premošćuje umetanje postaje dva run-a sa prazninom, a run koji premošćuje brisanje se skraćuje. TileRangeAxisMetadata prazni run-ove preko celog popločanog raspona, a zatim ponovo registruje svaki izvorni run jednom po kopiji na njegovom odmaku. TXLSXWorksheet.CopyFrom i TXLSXSheets.AddCopy uzimaju Copy() niza umesto da ga dodeljuju, i zato test može da obriše sve visine na klonu a da originalni list i dalje nađe netaknut u redu 1.048.576

var
  Sheet: TXLSXWorksheet;
begin
  Sheet := Workbook.Sheets[1];
  Sheet.RowHeight[500000] := 36;
  Sheet.ClearRowHeight(500001);
  // Umetni dva reda na 500000: prevazilaženje se pomera 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 ponovo: sve se vraća nazad
  Sheet.DeleteRows(500000, 2);
  Writeln(Sheet.RowHeight[500000]:0:2);   // 36.00
  // Popločaj redove 2..4 dvaput niz list; visine run-a prate svaku kopiju
  Sheet.TileRangeAxisMetadata(2, 1, 3, 1, 2, 1);
  Writeln(Sheet.RowHeight[7]:0:2);        // visina run-a
end;

Granice na strani čitanja imaju istu obavezu. GetUsedRange podiže svoju donju ivicu na FirstRow i LastRow svakog run-a, a BuildRowMajorCellOrder proteže svoj maksimalni red koji uključuje metapodatke kroz svaki run, tako da XLSX pisac i dalje obilazi redove koji nose samo visinu. Ako ikada dodate sopstvenu strukturu sa ključem po redu povrh HotXLS objektnog modela, ovo je checklista: umetanje, brisanje, popločavanje, kopiranje, used range, i svaki serializer. Promašite jedno i otkaz je tih — visine odlutaju za broj umetnutih redova, i ništa ne baca izuzetak

Šta ostaje po redu, i kako brojevi sada izgledaju

Zastavice skrivenosti, nivoi outline-a i stanje sažimanja i dalje se razvijaju. Završetak reda vrti SetRowHidden i SetRowOutlineLevel po jednom za svaki ponovljeni red, pa list koji skriva završetak od milion redova, ili ga ugnježđuje u table:table-row-group, plaća unos po redu za svaki od tih atributa. Izmena u 2.382.2 ograničena je na dve stvari koje je HXLS-005 zaista merio — visine i nasleđene stilove praznih ćelija — a ista tehnika sa run-ovima primenila bi se i na ostale ako bi neki fajl to ikad zatražio. ODS reader takođe ne postupa po style:use-optimal-row-height; stil reda koji kaže „optimal" i daje visinu uvozi se sa tom visinom

Nad corpus-om, conditional-formatting.ods sada završava ciklus otvaranja, tvrdnje, čuvanja, ponovnog otvaranja i ponovne tvrdnje za 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 prethodno iscrpljivao. Interfejsi na nivou radne sveske kroz koje format prolazi opisani su u tekstu o otvaranju i čuvanju ODS fajlova, a širi set poluga za velike fajlove u tekstu o performansama velikih radnih sveska; sam ODF element reda, sa svojim 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 nativnog Delphi i C++Builder koda bez instaliranog Excel-a ili LibreOffice-a, i zato je ponavljanje od milion redova nešto što biblioteka mora dobro da modeluje umesto da ga preda spoljnom procesu — stranica HotXLS Delphi spreadsheet komponente navodi podržane formate i verzije RAD Studio-a