Techninis straipsnis

ODS pasikartojančios eilutės HotXLS: eilučių aukščių ruožai

HotXLS Delphi Component ODS eilutę, nešančią table:number-rows-repeated ir eilutės aukštį, saugo kaip vieną TXLSXRowHeightRun įrašą — pirma eilutė, paskutinė eilutė, vienas aukštis — o ne po vieną aukščio įrašą kiekvienai pasikartojančiai eilutei, ir tuščių ląstelių stilių, kurį tos eilutės paveldi, sulenkia į vieną intervalo stiliaus sluoksnį. Būtent todėl HotXLS 2.382.2 skaičiuoklę, kurios uodegoje kartojasi 1 048 530 tuščių eilučių, atidaro per 0,02 sekundės ten, kur 2.382.1 nukrisdavo dėl laiko, ir todėl tas pats failas įrašomas atgal į ODS su nepažeistu kartojimo skaičiumi, o ne kaip milijonas atskirų eilučių

Minėtas failas yra visiškai įprastas. LibreOffice Calc rašo keturiolikos stulpelių lapą su 45 duomenų eilutėmis, o paskui viską žemiau jų aprašo vienu elementu: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Stilius ro1 nustato style:row-height="0.452cm", o kiekvienas <table:table-column> neša table:default-cell-style-name, kurį paveldi kiekviena tuščia ląstelė tame ruože. Visas content.xml yra 103 KB. Niekas tame faile nesako „brangu"; brangu buvo vien mums

Kaip HotXLS vieną pasikartojančią ODS eilutę paverčia kompaktiška būsena: content.xml elementas su table:number-rows-repeated 1048530 ir stiliumi ro1 atitinka vieną TXLSXRowHeightRun įrašą, apimantį 46–1048575 eilutes po 12,81 taško, plius po vieną StyleOverlays įrašą kiekvienam stulpeliui, o 2.382.1 versija tą patį elementą išplėsdavo į milijoną SetRowHeight įrašų ir ląstelių objektų
Kartojimo skaičius, ro1 eilutės aukštis ir stulpelių numatytieji stiliai aprašo kiekvieną tuščią eilutę žemiau 45-os, tad importuotojas gali sukurti vieną ruožo įrašą ir po vieną sluoksnį kiekvienam stulpeliui, neliesdamas milijono koordinačių

Kodėl viena pasikartojanti eilutė užkiša ODS importą?

Nes importuotojas ją išplėsdavo. 2.382.1 versijoje eilutės užbaigėjas sukosi cikle SetRowHeight(RowIndex + i, RowHeight) po kartą kiekvienai pasikartojančiai eilutei, įrašydamas kiekvieną aukštį į Name=Value eilučių sąrašą, raktuotą eilutės numeriu. Kiekvienas įterpimas į tą sąrašą paleisdavo IndexOfName paiešką per viską, kas jame jau yra, tad milijonas aukščių kainavo milijoną tiesinių nuskaitymų — kvadratinė sąrašo paieška, dėl kurios ir buvo užregistruota HXLS-005. Tuo pat metu OdsCommitRow sukurdavo ląstelės objektą kiekvienam stulpeliui, paveldėjusiam stilių, ir tai darydavo kiekvienoje pasikartojančioje eilutėje, nes ląstelė su stiliumi vis tiek buvo laikoma ląstele

Įrašymo pusė turėjo savo šios problemos variantą. LibreOffice failas pasibaigia dar viena ro1 eilute po didžiojo kartojimo, tad aukščiausia eilutė su stiliumi sėdėjo pačioje lapo apačioje, o OdsBuildTableXml ėjo per visas eilutes iki jos, išvesdamas <table:table-row> elementus po vieną. Net darbaknygė, importuota pigiai, būtų įrašyta brangiai. Sutvarkius importą nesutvarkius eksporto, laiko ribos problema būtų ne pašalinta, o perkelta

Kas yra eilučių aukščių ruožas HotXLS?

Ruožas yra mažiausias dalykas, galintis aprašyti „46–1 048 575 eilutės visos yra 12,81 taško aukščio", nepasakant to 1 048 530 kartų. TXLSXRowHeightRun yra FirstRow, LastRow ir Height įrašas; TXLSXRowHeightRuns yra dinaminis tokių įrašų masyvas, o kiekvienas TXLSXWorksheet vieną tokį laiko FRowHeightRuns, šalia esamo kiekvienos eilutės aukščių sąrašo. Importuojant ODS eilutės užbaigėjas dabar šakojasi pagal kartojimo skaičių: kai skaičius 1, vis tiek kviečiamas SetRowHeight, o kai didesnis — vieną kartą visam ruožui kviečiamas XlsxAssignRowHeightRun. Ruožas apkarpomas iki XlsxMaxRow, kuris yra 1 048 576, tad kartojimo skaičius, viršijantis lapą, nukerpamas, o ne atmetamas

XlsxAssignRowHeightRun yra vienintelis masyvo rašytojas, ir jis ruožus išlaiko nesikertančius jau pačia konstrukcija. Gavęs naują intervalą, jis nukopijuoja kiekvieną esamą ruožą, visiškai esantį už jo ribų, bet kurį su juo besikertantį ruožą perskelia į dalį prieš ir dalį po, o paskui prideda naują intervalą, kai Present yra true — arba neprideda nieko, kai Present yra false, ir būtent taip ClearRowHeight išmuša vienos eilutės skylę. Iš to plaukia du dalykai. Masyve niekada nėra besikertančių intervalų, tad paieška gali sustoti ties pirmu radiniu. Ir masyvas niekada nemutuoja vietoje; kiekvieną kvietimą sukuriama nauja kopija, kuri tokiuose dydžiuose nieko nekainuoja ir panaikina visą klasę susiejimo klaidų

var
  Workbook: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    // Lapas, kurio uodegos eilutė kartojasi 1 048 530 kartų po vienu eilutės stiliumi
    Workbook.OpenODS('conditional-formatting.ods');
    Sheet := Workbook.Sheets[1];
    // Abu skaitymai išsisprendžia per tą patį ruožą; nieko nebuvo išplėsta
    Writeln(Sheet.RowHeight[46]:0:2, ' pt');
    Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
    // Vienos eilutės pakaitas nustelbia ruožą jo neskaldydamas
    Sheet.RowHeight[500000] := 36;
    // Vienos eilutės išvalymas ruožo viduje perpjauna ruožą į dvi dalis
    Sheet.ClearRowHeight(500001);
    Writeln(Sheet.HasRowHeight(500001)); // False
    Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // vis dar ruožo aukštis
  finally
    Workbook.Free;
  end;
end;

Paieškos tvarka yra ta dalis, kurią verta įsiminti. TXLSXWorksheet.GetRowHeight pirmiausia tikrina kiekvienos eilutės sąrašą ir į ruožus kreipiasi tik tada, kai eilutė neturi aiškaus įrašo, o HasRowHeight daro tą patį. Tad Sheet.RowHeight[500000] := 36 ruožo neliečia visiškai — jis prideda vieną įrašą į kiekvienos eilutės sąrašą, ir tas įrašas laimi, nes tikrinamas pirmas. ClearRowHeight yra priešingas: jis pašalina bet kurį kiekvienos eilutės įrašą ir tada kviečia XlsxAssignRowHeightRun su Present = False, nes išvalyta eilutė turi skaitytis kaip „be aukščio", net jei ruožas ją apima. ClearRowHeights ištuština abi struktūras vienu metu

Eilučių aukščių ruožų chirurgija HotXLS: po OpenODS vienas ruožas apima 46–1048575 eilutes po 12,81 taško, o kiekvienos eilutės įrašas 500000 eilutę nustato į 36 taškus ir laimi paiešką, nes GetRowHeight pirmiausia tikrina kiekvienos eilutės sąrašą, ir ClearRowHeight 500001 eilutei perpjauna ruožą į dvi nesikertančias dalis aplink skylę
XlsxAssignRowHeightRun nukopijuoja dalis už išvalyto intervalo ir pačiam intervalui neprideda nieko, tad ruožai lieka nesikertantys jau pačia konstrukcija, o paieška gali sustoti ties pirmu radiniu, kai pakaitas 500 000 eilutėje lieka nepaliestas

Kur nukeliauja paveldimi tuščių ląstelių stiliai?

Į vieną intervalo stiliaus sluoksnį kiekvienam stulpeliui, o ne į ląstelių objektus. OdsCommitRow pagal kiekvieno stulpelio reikšmę nusprendžia, ar tai kompaktiška tuščia vieta: eilutė kartojasi daugiau nei kartą, o ląstelė neturi nei reikšmės, nei formulės, nei raiškiojo teksto. Kompaktiškai tuščiai vietai jis sukuria tikrą ląstelę tik pirmoje ruožo eilutėje, pritaiko jai paveldėtą stilių, o paskui tuos pačius šešis stiliaus indeksus — šriftą, užpildą, rėmelį, skaičiaus formatą, lygiavimą, apsaugą — užregistruoja kaip StyleOverlays.Add, apimantį nuo antros eilutės iki ruožo pabaigos tame stulpelyje. Eilutės po pirmosios materializavimo cikle praleidžiamos visiškai

Regresinis testas šią formą padaro konkrečią. Atidarius lapą, kurio antra eilutė kartojasi 1 048 575 kartus po stora stulpelio numatytąja stiliaus reikšme, tvirtinama, kad Sheet.Cells.Count yra mažesnis už 10, o Sheet.Cells[700000, 1].FontIndex vis tiek išsisprendžia į storą šriftą — sluoksnis stilių paduoda tą akimirką, kai ta koordinatė paliečiama. Tai tas pats mechanizmas, kuris XLSX pusėje neleidžia suformatuotam, bet tuščiam stulpeliui kainuoti milijono ląstelių; pastabos apie eilučių blokų ląstelių saugyklą ir intervalinius stiliaus sluoksnius paaiškina, kaip sluoksniai susideda ir išsisprendžia. Nauja čia tai, kad ODS importuotojas juos sukuria pats, iš kartojimo skaičiaus, o ne laukia, kol programa suformatuos sritį

Kaip SaveAsODS įrašo kartojimo skaičių atgal?

Skaldydamas tuščią lapo uodegą tik ten, kur kas nors iš tikrųjų keičiasi. OdsBuildTableXml dabar seka dvi ribas: contentMaxRow — paskutinę eilutę, laikančią reikšmę, formulę, hipersaitą ar rankinį eilutės lūžį, ir maxRow, kuri papildomai tęsiasi per vien tik stiliaus tuščias ląsteles, vienos eilutės aukščius, kiekvieno ruožo LastRow ir kiekvieno sluoksnio apatinį kraštą. Vien tik stiliaus tuščia ląstelė nebeskaičiuojama kaip turinys — ją išskiria TXLSXCells.IsStyleOnlyBlank — tad paskutinė eilutė su stiliumi LibreOffice faile nustoja tempti turinio ribą į lapo apačią

Virš contentMaxRow eilutės rašomos po vieną lygiai kaip anksčiau. Žemiau jo rašytojas apskaičiuoja nextRow kaip mažiausią iš: kito ruožo FirstRow, dabartinio ruožo LastRow + 1, kito vienos eilutės aukščio įrašo, kito sluoksnio krašto ir kitos materializuotos ląstelės. Tada viskas nuo dabartinės eilutės iki nextRow - 1 išvedama kaip vienas <table:table-row> su table:number-rows-repeated, nustatytu į skirtumą, nešantis po vieną <table:table-cell/> kiekvienam stulpeliui su sluoksnio išspręstu stiliaus pavadinimu, kai sluoksnis tą stulpelį apima. Paties eilutės stiliaus šaltinis yra TOdsAutoStylePool.RowStyleFor(AHidden, ABreakBefore, AHeightSpec), kuris dabar į savo dublikatų atpažinimo raktą įlenkia ir aukščio tekstą — tarkim, 12.81pt — šalia paslėpimo ir puslapio lūžio požymių, tad kiekviena ruožo eilutė dalijasi vienu ro<N> stiliumi su viena style:row-height savybe

Ką SaveAsODS įrašo ruožu paremtam lapui: contentMaxRow sustoja ties 45 eilute, kur baigiasi reikšmės, o maxRow tęsiasi per aukščių ruožą ir jo pakaitalus, eilutės virš ribos rašomos po vieną, o uodega išvedama kaip pasikartojantys table-row elementai, kurių eilutės stilius ateina iš RowStyleFor, o ląstelių stiliai išsisprendžia per sluoksnius
Kiekvienas pasikartojantis elementas apima vieną vienodą ruožą ir sustoja ties kitu ruožo kraštu, aukščio įrašu, sluoksnio kraštu ar materializuota ląstele, tad lapas be tikrinimų įrašomas kaip saujelė elementų, o tikrinimai ar XLSX eksportas moka už kiekvieną eilutę
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);
    // Tuščia uodega įrašoma kaip saujelė pasikartojančių eilučių, o ne milijonas
    Workbook.SaveAsODS(Saved);
    Writeln('ODS size: ', Saved.Size, ' bytes');
    Saved.Position := 0;
    Reopened.Open(Saved);
    // Pakaitas, skylė ir ruožas visi išlieka po kelio pirmyn ir atgal
    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);   // ruožo aukštis
  finally
    Saved.Free;
    Reopened.Free;
    Workbook.Free;
  end;
end;

Testas, kuris tai įtvirtina, tvirtina, kad įrašytas srautas yra mažesnis už 64 KB lapui, kurio aukščių ruožas apima 1 048 575 eilutes su pakaitu ir per vidurį išmušta skyle. Prie to skaičiaus dera dvi sąžiningos ribos. Pirma, darbalapis su bet kokiais duomenų tikrinimais contentMaxRow nustato į maxRow, tad tikrinimai tame lape išjungia uodegos suspaudimą ir jis vėl rašomas eilutė po eilutės. Antra, XLSX neturi kartojimo atributo — SpreadsheetML <row> aprašo vieną eilutę — tad eksportuojant ruožu paremtą lapą į .xlsx, išvardijamos eilutės, kurias ruožas apima, ir kiekvienai įrašomas ht atributas. Modelis atmintyje lieka kompaktiškas; failo formatas nusprendžia, kaip failas atrodo

Ką dabar kiekvienas eilučių pernumeravimo veiksmas skolingas ruožams?

Priežiūrą. Nauja eilučių metaduomenų reprezentacija yra teisinga tik tada, jei kiekviena operacija, keičianti eilučių numerius, ją perkelia kartu su šalia esančiais kiekvienos eilutės sąrašais, ir šis pakeitimas paliečia kiekvieną tokią operaciją. InsertRows ir DeleteRows eina per XlsxShiftRowHeightRuns, kuris masyvą atstato iš naujo: palieka kiekvieno ruožo dalį, esančią prieš redagavimo tašką, atmeta viską, kas patenka į trynimo langą, ir vėl prideda likutį, pastumtą delta — tad ruožas, kertantis įterpimą, virsta dviem ruožais su tarpu, o ruožas, kertantis trynimą, susitraukia. TileRangeAxisMetadata išvalo ruožus visame plytelėmis dengtame plote, o paskui iš naujo užregistruoja kiekvieną šaltinio ruožą po kartą kiekvienai kopijai jos poslinkiu. TXLSXWorksheet.CopyFrom ir TXLSXSheets.AddCopy ima masyvo Copy(), o ne jį priskiria, ir todėl testas gali klone išvalyti visus aukščius ir vis tiek rasti originalų lapą nepaliestą 1 048 576 eilutėje

var
  Sheet: TXLSXWorksheet;
begin
  Sheet := Workbook.Sheets[1];
  Sheet.RowHeight[500000] := 36;
  Sheet.ClearRowHeight(500001);
  // Įterpti dvi eilutes ties 500000: pakaitas keliasi į 500002, skylė į 500003
  Sheet.InsertRows(500000, 2);
  Writeln(Sheet.RowHeight[500002]:0:2);   // 36.00
  Writeln(Sheet.HasRowHeight(500003));    // False
  // Vėl jas ištrinti: viskas grįžta atgal
  Sheet.DeleteRows(500000, 2);
  Writeln(Sheet.RowHeight[500000]:0:2);   // 36.00
  // Plytelėmis 2..4 eilutes du kartus lapo žemyn; ruožų aukščiai seka kiekvieną kopiją
  Sheet.TileRangeAxisMetadata(2, 1, 3, 1, 2, 1);
  Writeln(Sheet.RowHeight[7]:0:2);        // ruožo aukštis
end;

Skaitymo pusės riboms galioja ta pati prievolė. GetUsedRange apatinį kraštą pakelia iki kiekvieno ruožo FirstRow ir LastRow, o BuildRowMajorCellOrder savo didžiausią eilutę, įskaitant metaduomenis, tęsia per kiekvieną ruožą, kad XLSX rašytojas vis tiek aplankytų vien tik aukščio eilutes. Jei kada nors pridėsite savo eilute raktuotą struktūrą ant HotXLS objektinio modelio, štai kontrolinis sąrašas: įterpimas, trynimas, plytelės, kopijavimas, naudojama sritis ir kiekvienas serializatorius. Praleiskite vieną — ir gedimas yra tylus: aukščiai nuslenka įterpimų skaičiumi, ir niekas neišmeta išimties

Kas lieka kiekvienai eilutei atskirai ir kaip dabar atrodo skaičiai

Paslėpimo požymiai, kontūro lygmenys ir sulenkta būsena vis dar išplečiami. Eilutės užbaigėjas sukasi cikle SetRowHidden ir SetRowOutlineLevel po kartą kiekvienai pasikartojančiai eilutei, tad lapas, paslepiantis milijono eilučių uodegą ar įdėjęs ją į table:table-row-group, moka po įrašą už kiekvieną eilutę už kiekvieną iš tų atributų. 2.382.2 pakeitimas aprėpia du dalykus, kuriuos HXLS-005 tikrai matavo — aukščius ir paveldimus tuščių ląstelių stilius — ir ta pati ruožų technika būtų taikoma kitiems, jei koks nors failas to pareikalautų. ODS skaitytuvas taip pat nereaguoja į style:use-optimal-row-height; eilutės stilius, sakantis „optimalus" ir duodantis aukštį, importuojamas su tuo aukščiu

Su korpusu conditional-formatting.ods dabar atidarymo, tvirtinimo, įrašymo, atidarymo iš naujo ir pakartotinio tvirtinimo ciklą įveikia per 0,178 sekundės Win32 ir 0,158 sekundės Win64, o pats atidarymo etapas trunka 0,020 sekundės — 60 sekundžių biudžete, kurį anksčiau išeikvodavo. Darbaknygės lygmens sąsajos, per kurias teka formatas, aprašytos ODS failų atidarymo ir įrašymo apžvalgoje, o platesnis svirtelių rinkinys dideliems failams — didelių darbaknygių našumo straipsnyje; pats ODF eilutės elementas su kartojimo ir stiliaus atributais apibrėžtas ODF 1.3 3 dalies §9.1.4

HotXLS skaito ir rašo XLS, XLSX bei ODS savo Delphi ir C++Builder kodu be įdiegto Excel ar LibreOffice, ir būtent todėl milijono eilučių kartojimas yra tai, ką biblioteka turi gerai sumodeliuoti, o ne perduoti išoriniam procesui — HotXLS Delphi spreadsheet komponento puslapyje išvardyti palaikomi formatai ir RAD Studio versijos