Tehnični članak

Ponavljajoče vrstice ODS kot nizi višin v HotXLS

HotXLS Delphi Component vrstico ODS, ki nosi table:number-rows-repeated in višino vrstice, shrani kot en sam zapis TXLSXRowHeightRun — prva vrstica, zadnja vrstica, ena višina — in ne kot en vnos višine na ponovljeno vrstico, slog praznih celic, ki ga te vrstice podedujejo, pa zloži v en sam prekrivni slog intervala. Prav zato HotXLS 2.382.2 odpre preglednico, katere rep ponavlja 1.048.530 praznih vrstic, v 0,02 sekunde, kjer je 2.382.1 potekel čas, in zato se ista datoteka shrani nazaj v ODS z nedotaknjenim števcem ponovitev in ne kot milijon dobesednih vrstic

Datoteka v vprašanju je običajna. LibreOffice Calc zapiše list s štirinajstimi stolpci in 45 vrsticami podatkov, nato pa vse pod njimi opiše z enim elementom: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Slog ro1 nastavi style:row-height="0.452cm", vsak <table:table-column> pa nosi table:default-cell-style-name, ki ga podeduje vsaka prazna celica v nizu. Celoten content.xml meri 103 KB. Nič na tej datoteki ne pravi "drago"; dragi smo bili povsem mi

Kako HotXLS eno ponovljeno vrstico ODS spremeni v strnjeno stanje: element content.xml s table:number-rows-repeated 1048530 in slogom ro1 se preslika v en sam zapis TXLSXRowHeightRun, ki sega od vrstice 46 do 1048575 pri 12,81 pt, plus po en vnos StyleOverlays na stolpec, medtem ko je različica 2.382.1 isti element razširila v milijon vnosov SetRowHeight in objektov celic
Števec ponovitev, višina vrstice ro1 in privzeti slogi stolpcev opisujejo vsako prazno vrstico pod vrstico 45, zato lahko uvoznik zgradi en zapis niza in prekrivne sloje po stolpcih, ne da bi se dotaknil milijona koordinat

Zakaj ena ponovljena vrstica povzroči prekoračen čas uvoza ODS?

Ker ga je uvoznik včasih razširil. V različici 2.382.1 je zaključek vrstice v zanki klical SetRowHeight(RowIndex + i, RowHeight) enkrat na ponovljeno vrstico in vsako višino zapisal v seznam nizov Name=Value, ključan po številki vrstice. Vsak vnos v ta seznam je pognal iskanje IndexOfName po vsem, kar je bilo v njem, zato je milijon višin stal milijon linearnih pregledov — kvadratno iskanje po seznamu, zaradi katerega je bila vložena prijava HXLS-005. Hkrati je OdsCommitRow za vsak stolpec, ki je podedoval slog, materializiral objekt celice na vsaki od ponovljenih vrstic, ker je stilizirana prazna celica še vedno štela kot celica

Stran shranjevanja je imela svojo različico iste težave. Datoteka LibreOffice se po veliki ponovitvi konča še z eno vrstico ro1, zato je najvišja stilizirana vrstica sedela čisto na dnu lista, OdsBuildTableXml pa je prehodil vsako vrstico do nje in elemente <table:table-row> oddajal enega za drugim. Tudi delovni zvezek, uvožen poceni, bi se zapisal drago. Popraviti uvoz brez izvoza bi prekoračen čas le premaknilo, ne odstranilo

Kaj je niz višin vrstic v HotXLS?

Niz je najmanjša stvar, ki lahko opiše "vrstice 46 do 1.048.575 so vse visoke 12,81 točke", ne da bi to povedala 1.048.530-krat. TXLSXRowHeightRun je zapis s polji FirstRow, LastRow in Height; TXLSXRowHeightRuns je dinamična matrika teh zapisov in vsak TXLSXWorksheet eno hrani v FRowHeightRuns poleg obstoječega seznama višin po vrsticah. Ob uvozu ODS zaključek vrstice zdaj razveji glede na števec ponovitev: števec 1 še vedno pokliče SetRowHeight, vse večje pa enkrat pokliče XlsxAssignRowHeightRun za celoten razpon. Razpon se omeji na XlsxMaxRow, kar je 1.048.576, zato se števec ponovitev, ki prestreli list, odreže in ne zavrne

XlsxAssignRowHeightRun je edini pisec te matrike in nize po zgradbi ohranja disjunktne. Za dani interval kopira vsak obstoječi niz, ki leži povsem zunaj njega, vsak niz, ki se z njim prekriva, razcepi na kos pred njim in kos za njim, nato pa novi interval pripne, kadar je Present resničen — ali ne pripne ničesar, kadar je Present neresničen, in tako ClearRowHeight izseka enovrstično luknjo. Iz tega sledi dvoje. Matrika nikoli ne vsebuje prekrivajočih se intervalov, zato se iskanje lahko ustavi ob prvem zadetku. In matrika se nikoli ne spreminja na mestu; ob vsakem klicu se zgradi sveža kopija, kar pri takšnih velikostih ne stane nič in odstrani cel razred hroščev z vzdevki

var
  Workbook: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    // List, katerega repna vrstica se ponovi 1.048.530-krat pod enim slogom vrstice
    Workbook.OpenODS('conditional-formatting.ods');
    Sheet := Workbook.Sheets[1];
    // Oba branja se razrešita skozi isti niz; nič ni bilo razširjeno
    Writeln(Sheet.RowHeight[46]:0:2, ' pt');
    Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
    // Povozitev ene same vrstice zasenči niz, ne da bi ga razcepila
    Sheet.RowHeight[500000] := 36;
    // Brisanje ene vrstice znotraj niza razreže niz na dva kosa
    Sheet.ClearRowHeight(500001);
    Writeln(Sheet.HasRowHeight(500001)); // False
    Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // še vedno višina niza
  finally
    Workbook.Free;
  end;
end;

Vrstni red iskanja si velja zapomniti. TXLSXWorksheet.GetRowHeight najprej pogleda seznam po vrsticah in nize upošteva šele, ko vrstica nima izrecnega vnosa, HasRowHeight pa naredi isto. Zato se Sheet.RowHeight[500000] := 36 niza sploh ne dotakne — doda en vnos v seznam po vrsticah in ta vnos zmaga, ker se išče prvi. ClearRowHeight je nasprotje: odstrani vsak vnos po vrsticah in nato pokliče XlsxAssignRowHeightRun z Present = False, ker se mora izbrisana vrstica brati kot "brez višine", tudi če jo niz pokriva. ClearRowHeights izprazni obe strukturi hkrati

Operacija na nizu višin vrstic v HotXLS: po OpenODS en niz pokriva vrstice 46 do 1048575 pri 12,81 pt, medtem ko vnos po vrsticah nastavi vrstico 500000 na 36 pt in zmaga pri iskanju, ker GetRowHeight najprej pogleda seznam po vrsticah, ClearRowHeight vrstice 500001 pa niz razcepi na dva disjunktna kosa okoli luknje
XlsxAssignRowHeightRun kopira kose zunaj izbrisanega intervala in za interval sam ne pripne ničesar, zato nizi po zgradbi ostanejo disjunktni, iskanje pa se lahko ustavi ob prvem zadetku, medtem ko povozitev v vrstici 500000 ostane nedotaknjena

Kam gredo podedovani slogi praznih celic?

V en sam prekrivni slog intervala na stolpec, ne v objekte celic. OdsCommitRow za vsako vrednost stolpca odloči, ali je strnjena prazna celica: vrstica se ponovi več kot enkrat, celica pa nima vrednosti, formule ne bogatega besedila. Za strnjeno prazno celico ustvari pravo celico le v prvi vrstici niza, ji dodeli podedovani slog in nato registrira istih šest indeksov sloga — pisavo, polnilo, obrobo, obliko števila, poravnavo, zaščito — kot StyleOverlays.Add, ki pokriva vrstice od druge do konca niza v tem stolpcu. Vrstice za prvo se v zanki materializacije v celoti preskočijo

Regresijski test obliko naredi konkretno. Po odprtju lista, katerega druga vrstica se ponovi 1.048.575-krat pod privzetim slogom krepkega stolpca, se zatrdi, da je Sheet.Cells.Count pod 10, Sheet.Cells[700000, 1].FontIndex pa se še vedno razreši v krepko pisavo — prekrivni sloj priskrbi slog v trenutku, ko se te koordinate dotaknete. To je isti mehanizem, ki na strani XLSX prepreči, da bi oblikovan, a prazen stolpec stal milijon celic; zapiski o shranjevanju celic po blokih vrstic in prekrivnih slogih intervalov pokrivajo, kako se prekrivni sloji nalagajo in razrešujejo. Novo je tu to, da jih uvoznik ODS ustvari sam, iz števca ponovitev, namesto da bi čakal, da aplikacija oblikuje obseg

Kako SaveAsODS zapiše števec ponovitev nazaj?

Tako da prazen rep lista razdeli le tam, kjer se res kaj spremeni. OdsBuildTableXml zdaj sledi dvema mejama: contentMaxRow, zadnji vrstici, ki nosi vrednost, formulo, hiperpovezavo ali ročni prelom vrstice, in maxRow, ki se dodatno razteza čez prazne celice samo s slogom, višine posameznih vrstic, LastRow vsakega niza in spodnji rob vsakega prekrivnega sloja. Prazna celica samo s slogom ne šteje več kot vsebina — izloča jo TXLSXCells.IsStyleOnlyBlank — zato končna stilizirana vrstica v datoteki LibreOffice ne vleče več meje vsebine na dno lista

Nad contentMaxRow se vrstice pišejo ena za drugo natanko kot prej. Pod njo pisec izračuna nextRow kot najmanjšo od: FirstRow naslednjega niza, LastRow + 1 trenutnega niza, naslednjega vnosa višine posamezne vrstice, naslednjega roba prekrivnega sloja in naslednje materializirane celice. Vse od trenutne vrstice do nextRow - 1 se nato odda kot en <table:table-row> z table:number-rows-repeated, nastavljenim na razliko, ki nosi po en <table:table-cell/> na stolpec, in sicer z imenom sloga, razrešenim prek prekrivnega sloja, kadar ta stolpec pokriva kak prekrivni sloj. Slog vrstice sam pride iz TOdsAutoStylePool.RowStyleFor(AHidden, ABreakBefore, AHeightSpec), ki zdaj besedilo višine — recimo 12.81pt — zloži v svoj ključ za odstranjevanje dvojnikov poleg zastavic skritosti in preloma strani, zato si vse vrstice v nizu delijo en slog ro<N> z eno samo lastnostjo style:row-height

Kaj SaveAsODS zapiše za list, podprt z nizi: contentMaxRow se ustavi pri vrstici 45, kjer se vrednosti končajo, maxRow pa se razteza čez niz višin in njegove povozitve; vrstice nad mejo se pišejo ena za drugo, rep pa se odda kot ponovljeni elementi table-row, katerih slog vrstice pride iz RowStyleFor, slogi celic pa se razrešijo prek prekrivnih slojev
Vsak ponovljeni element obsega en enoten odsek in se ustavi ob naslednjem robu niza, vnosu višine, robu prekrivnega sloja ali materializirani celici, zato se list brez preverjanj shrani kot pest elementov, preverjanja ali izvoz v XLSX pa plačajo po vrstico
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);
    // Prazen rep se zapiše kot pest ponovljenih vrstic in ne milijon
    Workbook.SaveAsODS(Saved);
    Writeln('ODS size: ', Saved.Size, ' bytes');
    Saved.Position := 0;
    Reopened.Open(Saved);
    // Povozitev, luknja in niz vsi preživijo obhod
    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);   // višina niza
  finally
    Saved.Free;
    Reopened.Free;
    Workbook.Free;
  end;
end;

Test, ki to pripne, zatrdi, da je shranjeni tok pod 64 KB za list, katerega niz višin sega čez 1.048.575 vrstic s povozitvijo in luknjo, izsekana na sredini. Ob tej številki spadata dve pošteni meji. Prvič, delovni list s kakršnim koli preverjanjem podatkov nastavi contentMaxRow na maxRow, zato preverjanja na tem listu izklopijo stiskanje repa in list se znova piše vrstico za vrstico. Drugič, XLSX ne pozna atributa ponovitve — <row> v SpreadsheetML opisuje eno vrstico — zato izvoz lista, podprtega z nizi, v .xlsx našteje vrstice, ki jih niz pokriva, in na vsako zapiše atribut ht. Model v pomnilniku ostane strnjen; kakšna bo datoteka, odloči format datoteke

Kaj vsako urejanje s preštevilčenjem vrstic zdaj dolguje nizom?

Vzdrževanje. Nova predstavitev metapodatkov vrstic je pravilna le, če jo vsaka operacija, ki spremeni številke vrstic, premakne skupaj s seznami po vrsticah, ob katerih stoji, in ta sprememba se dotakne vsake od teh operacij. InsertRows in DeleteRows gresta skozi XlsxShiftRowHeightRuns, ki matriko znova zgradi tako, da obdrži del vsakega niza pred mestom urejanja, spusti, kar pade znotraj okna brisanja, in preostanek znova doda, premaknjen za delto — zato niz, ki sega čez vstavljanje, postane dva niza z vrzeljo, niz, ki sega čez brisanje, pa se skrči. TileRangeAxisMetadata izbriše nize čez celoten razporejeni razpon in nato vsak izvorni niz znova registrira enkrat na kopijo na njenem odmiku. TXLSXWorksheet.CopyFrom in TXLSXSheets.AddCopy vzameta Copy() matrike in je ne dodelita, zato lahko test na klonu izbriše vse višine in izvirni list v vrstici 1.048.576 še vedno najde nedotaknjen

var
  Sheet: TXLSXWorksheet;
begin
  Sheet := Workbook.Sheets[1];
  Sheet.RowHeight[500000] := 36;
  Sheet.ClearRowHeight(500001);
  // Vstavi dve vrstici pri 500000: povozitev se premakne na 500002, luknja na 500003
  Sheet.InsertRows(500000, 2);
  Writeln(Sheet.RowHeight[500002]:0:2);   // 36.00
  Writeln(Sheet.HasRowHeight(500003));    // False
  // Znova ju izbriši: vse se premakne nazaj
  Sheet.DeleteRows(500000, 2);
  Writeln(Sheet.RowHeight[500000]:0:2);   // 36.00
  // Razporedi vrstice 2..4 dvakrat po listu; višine nizov sledijo vsaki kopiji
  Sheet.TileRangeAxisMetadata(2, 1, 3, 1, 2, 1);
  Writeln(Sheet.RowHeight[7]:0:2);        // višina niza
end;

Meje na strani branja imajo isto dolžnost. GetUsedRange svoj spodnji rob potisne na FirstRow in LastRow vsakega niza, BuildRowMajorCellOrder pa svojo največjo vrstico, ki vključuje metapodatke, razširi čez vsak niz, da pisec XLSX še vedno obiše vrstice samo z višino. Če kdaj na objektni model HotXLS dodate svojo strukturo, ključano po vrsticah, je to kontrolni seznam: vstavljanje, brisanje, razporejanje, kopiranje, uporabljeni obseg in vsak serializator. Zgrešite enega in odpoved je tiha — višine se premaknejo za število vstavljenih vrstic, izjeme pa ni nobene

Kaj ostane po vrsticah in kako so videti številke zdaj

Zastavice skritosti, ravni orisa in stanje strnjenosti se še vedno razširijo. Zaključek vrstice v zanki kliče SetRowHidden in SetRowOutlineLevel enkrat na ponovljeno vrstico, zato list, ki skrije milijon vrstic dolg rep ali ga gnezdi v table:table-row-group, plača po en vnos na vrstico za vsakega od teh atributov. Sprememba v različici 2.382.2 je omejena na dvoje, kar je HXLS-005 dejansko meril — višine in podedovane sloge praznih celic — in ista tehnika nizov bi se uporabila tudi za druge, če bi kdaj kakšna datoteka to zahtevala. Bralnik ODS tudi ne upošteva style:use-optimal-row-height; slog vrstice, ki pravi "optimalno" in navaja višino, se uvozi s to višino

Proti korpusu conditional-formatting.ods zdaj opravi cikel odpiranja, trditve, shranjevanja, ponovnega odpiranja in ponovne trditve v 0,178 sekunde na Win32 in 0,158 sekunde na Win64, pri čemer sama stopnja odpiranja traja 0,020 sekunde, znotraj 60-sekundnega proračuna, ki ga je prej izčrpal. Vmesniki na ravni delovnega zvezka, skozi katere teče format, so opisani v pregledu odpiranja in shranjevanja datotek ODS, širši nabor vzvodov za velike datoteke pa v zmogljivosti velikih delovnih zvezkov; sam element vrstice ODF, s svojima atributoma ponovitve in sloga, je določen v ODF 1.3 3. del §9.1.4

HotXLS bere in piše XLS, XLSX in ODS iz izvorne kode Delphi in C++Builder brez nameščenega Excela ali LibreOfficea, zato je milijon vrstic dolga ponovitev nekaj, kar mora knjižnica dobro modelirati, in ne nekaj, kar bi predala zunanjemu procesu — stran izdelka komponente HotXLS Delphi za preglednice navaja podprte formate in različice RAD Studio