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
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
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
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