HotXLS Delphi Component tallentaa ODS-rivin, joka kantaa table:number-rows-repeated-määritettä ja rivikorkeutta, yhtenä TXLSXRowHeightRun-tietueena — ensimmäinen rivi, viimeinen rivi, yksi korkeus — yhden korkeusmerkinnän sijaan per toistuva rivi, ja taittaa noiden rivien perimän tyhjän solun tyylin yhdeksi intervallityylipäällysteeksi. Juuri se on koko syy siihen, että HotXLS 2.382.2 avaa laskentataulukon, jonka häntä toistaa 1 048 530 tyhjää riviä, 0.02 sekunnissa siinä missä 2.382.1 aikakatkesi, ja siihen että sama tiedosto tallentuu takaisin ODS:ään toistoluku ehjänä eikä miljoonana kirjaimellisena rivinä
Kyseinen tiedosto on aivan tavallinen. LibreOffice Calc kirjoittaa neljäntoista sarakkeen taulukon, jossa on 45 riviä dataa, ja kuvaa sitten kaiken niiden alapuolella yhdellä elementillä: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Tyyli ro1 asettaa style:row-height="0.452cm", ja jokainen <table:table-column> kantaa table:default-cell-style-name-määritettä, jonka jokainen tyhjä solu jaksossa perii. Koko content.xml on 103 KB. Mikään tiedostossa ei sano kallis; kalleus oli täysin meidän
Miksi yksi toistuva rivi saa ODS-tuonnin aikakatkaisuun?
Koska tuontiohjelma tapasi laajentaa sen. Versiossa 2.382.1 rivinviimeistelijä toisti SetRowHeight(RowIndex + i, RowHeight)-kutsun kerran per toistuva rivi ja kirjoitti kunkin korkeuden Name=Value-merkkijonolistaan, jonka avain oli rivinumero. Jokainen lisäys tuohon listaan ajoi IndexOfName-haun kaiken jo listassa olevan yli, joten miljoona korkeutta maksoi miljoona lineaarista skannausta — se neliöllinen listahaku, jota vastaan HXLS-005 kirjattiin. Samaan aikaan OdsCommitRow materialisoi soluobjektin jokaiselle sarakkeelle, joka peri tyylin, jokaisella toistuvalla rivillä, koska tyylitelty tyhjä solu laskettiin yhä soluksi
Tallennuspuolella oli oma versionsa ongelmasta. LibreOfficen tiedosto päättyy vielä yhteen ro1-riviin ison toiston jälkeen, joten ylin tyylitelty rivi istui aivan taulukon pohjassa, ja OdsBuildTableXml kävi läpi jokaisen rivin siihen asti tuottaen <table:table-row>-elementtejä yksi kerrallaan. Halvallakin tuotu työkirja olisi kirjoitettu kalliisti. Tuonnin korjaaminen ilman viennin korjaamista olisi siirtänyt aikakatkaisun, ei poistanut sitä
Mikä on rivikorkeusjakso HotXLS:ssä?
Jakso on pienin asia, joka pystyy kuvaamaan, että rivit 46–1 048 575 ovat kaikki 12.81 pisteen korkuisia, sanomatta sitä 1 048 530 kertaa. TXLSXRowHeightRun on tietue, jossa on FirstRow, LastRow ja Height; TXLSXRowHeightRuns on niiden dynaaminen taulukko, ja jokainen TXLSXWorksheet pitää sellaista kentässä FRowHeightRuns olemassa olevan rivikohtaisen korkeuslistan rinnalla. ODS-tuonnissa rivinviimeistelijä haarautuu nyt toistoluvun mukaan: luku 1 kutsuu yhä SetRowHeight-funktiota, mikä tahansa suurempi kutsuu XlsxAssignRowHeightRun-funktiota kerran koko välin osalta. Väli rajataan XlsxMaxRow-arvoon, joka on 1 048 576, joten taulukon yli menevä toistoluku katkaistaan eikä hylätä
XlsxAssignRowHeightRun on taulukon ainoa kirjoittaja, ja se pitää jaksot rakenteellisesti erillisinä. Saatuaan uuden välin se kopioi jokaisen olemassa olevan jakson, joka on kokonaan sen ulkopuolella, jakaa minkä tahansa sen kanssa päällekkäisen jakson väliä edeltävään ja sitä seuraavaan osaan ja lisää sitten uuden välin, kun Present on tosi — tai ei lisää mitään, kun Present on epätosi, millä tavalla ClearRowHeight lyö yhden rivin kokoisen reiän. Tästä seuraa kaksi asiaa. Taulukko ei koskaan sisällä päällekkäisiä välejä, joten haku voi pysähtyä ensimmäiseen osumaan. Eikä taulukkoa koskaan muuteta paikallaan; joka kutsulla rakennetaan tuore kopio, mikä ei maksa mitään näillä ko'oilla ja poistaa kokonaisen aliaksointibugien luokan
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Workbook := TXLSXWorkbook.Create;
try
// Taulukkosivu, jonka häntärivi toistuu 1 048 530 kertaa yhden rivityylin alla
Workbook.OpenODS('conditional-formatting.ods');
Sheet := Workbook.Sheets[1];
// Molemmat haut ratkeavat saman jakson kautta; mitään ei laajennettu
Writeln(Sheet.RowHeight[46]:0:2, ' pt');
Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
// Yhden rivin ohitus varjostaa jaksoa jakamatta sitä
Sheet.RowHeight[500000] := 36;
// Yhden rivin tyhjentäminen jakson sisällä leikkaa jakson kahteen osaan
Sheet.ClearRowHeight(500001);
Writeln(Sheet.HasRowHeight(500001)); // False
Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // yhä jakson korkeus
finally
Workbook.Free;
end;
end;
Hakujärjestys on se osa, joka kannattaa painaa mieleen. TXLSXWorksheet.GetRowHeight tarkistaa rivikohtaisen listan ensin ja kysyy jaksoilta vain kun rivillä ei ole nimenomaista merkintää, ja HasRowHeight tekee samoin. Niinpä Sheet.RowHeight[500000] := 36 ei kosketa jaksoon lainkaan — se lisää yhden merkinnän rivikohtaiseen listaan, ja tuo merkintä voittaa, koska se haetaan ensin. ClearRowHeight on päinvastainen: se poistaa mahdollisen rivikohtaisen merkinnän ja kutsuu sitten XlsxAssignRowHeightRun-funktiota arvolla Present = False, koska tyhjennetyn rivin on luettava korkeudettomana vaikka jakso kattaisi sen. ClearRowHeights tyhjentää molemmat rakenteet kerralla
Minne perityt tyhjien solujen tyylit menevät?
Yhteen intervallityylipäällysteeseen per sarake, ei soluobjekteihin. OdsCommitRow päättää sarakekohtaisesti, onko arvo kompakti tyhjä: rivi toistuu useammin kuin kerran, solussa ei ole arvoa, ei kaavaa eikä rikasta tekstiä. Kompaktille tyhjälle se luo oikean solun vain jakson ensimmäisellä rivillä, soveltaa siihen perittyä tyyliä ja rekisteröi sitten samat kuusi tyyli-indeksiä — fontti, täyttö, reunus, lukumuoto, tasaus, suojaus — StyleOverlays.Add-merkintänä, joka kattaa kyseisessä sarakkeessa rivit kahdesta jakson loppuun. Ensimmäisen jälkeiset rivit ohitetaan materialisointisilmukassa kokonaan
Regressiotesti tekee muodosta konkreettisen. Avattuaan taulukon, jonka toinen rivi toistuu 1 048 575 kertaa lihavoidun sarakeoletustyylin alla, testi väittää, että Sheet.Cells.Count on alle 10, ja että Sheet.Cells[700000, 1].FontIndex ratkeaa yhä lihavoituun fonttiin — päällyste toimittaa tyylin heti kun tuohon koordinaattiin kosketaan. Tämä on sama mekanismi, joka estää muotoillun mutta tyhjän sarakkeen maksamasta miljoonaa solua XLSX-puolella; muistiinpanot rivilohkojen solutallennuksesta ja intervallityylipäällysteistä kertovat, miten päällysteet kerrostuvat ja ratkeavat. Uutta tässä on se, että ODS-tuontiohjelma luo ne itse toistoluvusta sen sijaan, että odottaisi sovelluksen muotoilevan alueen
Miten SaveAsODS kirjoittaa toistoluvun takaisin?
Jakamalla taulukon tyhjä häntä vain sieltä, missä jokin todella muuttuu. OdsBuildTableXml seuraa nyt kahta rajaa: contentMaxRow, eli viimeinen rivi, jolla on arvo, kaava, hyperlinkki tai manuaalinen rivinvaihto, ja maxRow, joka ulottuu lisäksi pelkkien tyylillisten tyhjien solujen, yhden rivin korkeuksien, jokaisen jakson LastRow-arvon ja jokaisen päällysteen alareunan läpi. Pelkän tyylin tyhjä solu ei enää lasketa sisällöksi — TXLSXCells.IsStyleOnlyBlank on se, mikä rajaa sen pois — joten LibreOfficen tiedoston perässä oleva tyylitelty rivi lakkaa raahaamasta sisältörajaa taulukon pohjaan
contentMaxRow-rajan yläpuolella rivit kirjoitetaan yksi kerrallaan täsmälleen kuten ennenkin. Sen alapuolella kirjoittaja laskee nextRow-arvon pienimpänä seuraavista: seuraavan jakson FirstRow, nykyisen jakson LastRow + 1, seuraava yhden rivin korkeusmerkintä, seuraava päällysteen reuna ja seuraava materialisoitu solu. Kaikki nykyisestä rivistä nextRow - 1:een tuotetaan sitten yhtenä <table:table-row>-elementtinä, jonka table:number-rows-repeated on erotus, kantaen yhtä <table:table-cell/>-merkintää per sarake päällysteen ratkaisemalla tyylinimellä, kun päällyste kattaa kyseisen sarakkeen. Rivin tyyli itsessään tulee TOdsAutoStylePool.RowStyleFor(AHidden, ABreakBefore, AHeightSpec) -kutsusta, joka nyt taittaa korkeustekstin — vaikkapa 12.81pt — deduplikointiavaimensa osaksi piilotetun lipun ja sivunvaihtolippujen rinnalle, joten jokainen jakson rivi jakaa yhden ro<N>-tyylin, jossa on yksi style:row-height-ominaisuus
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);
// Tyhjä häntä kirjoitetaan kourallisena toistuvia rivejä, ei miljoonana
Workbook.SaveAsODS(Saved);
Writeln('ODS size: ', Saved.Size, ' bytes');
Saved.Position := 0;
Reopened.Open(Saved);
// Ohitus, aukko ja jakso selviävät kaikki edestakaisesta kierroksesta
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); // jakson korkeus
finally
Saved.Free;
Reopened.Free;
Workbook.Free;
end;
end;
Testi, joka kiinnittää tämän, väittää tallennetun virran olevan alle 64 KB taulukosta, jonka korkeusjakso kattaa 1 048 575 riviä ja jonka keskelle on lyöty ohitus ja aukko. Kaksi rehellistä rajaa kuuluu tuon luvun viereen. Ensinnäkin taulukkosivu, jossa on yhtään datan validointia, asettaa contentMaxRow-arvoksi maxRow, joten validoinnit poistavat häntäpakkaamisen käytöstä kyseisellä sivulla ja se kirjoitetaan taas rivi riviltä. Toiseksi XLSX:ssä ei ole toistomääritettä — SpreadsheetML:n <row> kuvaa yhden rivin — joten jakson varaan tallennetun taulukon vienti muotoon .xlsx luettelee jakson kattamat rivit ja kirjoittaa ht-attribuutin jokaiselle. Malli pysyy muistissa kompaktina; tiedostomuoto ratkaisee, miltä tiedosto näyttää
Mitä jokainen rivit uudelleennumeroiva muokkaus on nyt velkaa jaksoille?
Ylläpitoa. Uusi rivimetatiedon esitys on oikea vain jos jokainen rivinumeroita muuttava operaatio siirtää sitä mukana sen rinnalla olevien rivikohtaisten listojen tavoin, ja commit koskettaa jokaista noista operaatioista. InsertRows ja DeleteRows kulkevat XlsxShiftRowHeightRuns-funktion kautta, joka rakentaa taulukon uudelleen pitämällä kunkin jakson osan, joka on muokkauskohdan edellä, pudottamalla poistoikkunan sisään jäävän ja lisäämällä jäljelle jäävän deltalla siirrettynä — joten lisäyksen yli ulottuvasta jaksosta tulee kaksi jaksoa, joiden välissä on aukko, ja poiston yli ulottuva jakso kutistuu. TileRangeAxisMetadata tyhjentää jaksot koko laatoitetun välin yli ja rekisteröi sitten jokaisen lähdejakson kerran per kopio siirtymänsä kohdalla. TXLSXWorksheet.CopyFrom ja TXLSXSheets.AddCopy ottavat taulukosta Copy()-kopion sen sijaan että sijoittaisivat sen, minkä takia testi voi tyhjentää kaikki korkeudet kloonista ja löytää alkuperäisen taulukon yhä ehjänä riviltä 1 048 576
var
Sheet: TXLSXWorksheet;
begin
Sheet := Workbook.Sheets[1];
Sheet.RowHeight[500000] := 36;
Sheet.ClearRowHeight(500001);
// Lisää kaksi riviä kohtaan 500000: ohitus siirtyy kohtaan 500002, aukko kohtaan 500003
Sheet.InsertRows(500000, 2);
Writeln(Sheet.RowHeight[500002]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500003)); // False
// Poista ne taas: kaikki siirtyy takaisin
Sheet.DeleteRows(500000, 2);
Writeln(Sheet.RowHeight[500000]:0:2); // 36.00
// Laatoita rivit 2..4 kahdesti alaspäin taulukossa; jaksokorkeudet seuraavat jokaista kopiota
Sheet.TileRangeAxisMetadata(2, 1, 3, 1, 2, 1);
Writeln(Sheet.RowHeight[7]:0:2); // jakson korkeus
end;
Lukupuolen rajoilla on sama velvoite. GetUsedRange nostaa alareunansa jokaisen jakson FirstRow- ja LastRow-arvoon, ja BuildRowMajorCellOrder jatkaa metatiedot sisältävää maksimiriviään jokaisen jakson läpi, jotta XLSX-kirjoittaja käy yhä läpi pelkkien korkeuksien rivit. Jos joskus lisäät oman riviavaimellisen rakenteesi HotXLS:n objektimallin päälle, tässä on tarkistuslista: lisäys, poisto, laatoitus, kopiointi, käytetty alue ja jokainen serialisoija. Yhdenkin unohtaminen johtaa hiljaiseen vikaan — korkeudet ajautuvat lisäysmäärän verran, eikä mikään nosta poikkeusta
Mikä pysyy rivikohtaisena ja miltä luvut näyttävät nyt
Piilotetut liput, jäsennystasot ja kootut tilat laajenevat yhä. Rivinviimeistelijä toistaa SetRowHidden- ja SetRowOutlineLevel-kutsut kerran per toistuva rivi, joten taulukko, joka piilottaa miljoonan rivin hännän tai sisäkkäistää sen table:table-row-group-rakenteeseen, maksaa rivikohtaisen merkinnän kummastakin attribuutista. 2.382.2:n muutos rajautuu niihin kahteen asiaan, joita HXLS-005 todella mittasi — korkeuksiin ja perittyihin tyhjiin tyyleihin — ja sama jaksotekniikka pätisi muihinkin, jos jokin tiedosto sitä joskus vaatii. ODS-lukija ei myöskään toimi style:use-optimal-row-height-määritteen perusteella; rivityyli, joka sanoo optimaalinen ja antaa korkeuden, tuodaan tuolla korkeudella
Korpusta vasten conditional-formatting.ods suorittaa nyt avaus-, varmistus-, tallennus-, uudelleenavaus- ja uudelleenvarmistuskierroksen 0.178 sekunnissa Win32-alustalla ja 0.158 sekunnissa Win64-alustalla, avausvaiheen ollessa itsessään 0.020 sekuntia, 60 sekunnin budjetissa jonka se aiemmin kulutti loppuun. Työkirjatason rajapinnat, joiden läpi formaatit virtaavat, kuvataan läpikäynnissä ODS-tiedostojen avaamisesta ja tallentamisesta, ja laajempi valikoima keinoja isoja tiedostoja varten artikkelissa suurten työkirjojen suorituskyvystä; itse ODF-rivielementti toisto- ja tyyliattribuutteineen on määritelty ODF 1.3 Part 3 §9.1.4:ssä
HotXLS lukee ja kirjoittaa XLS-, XLSX- ja ODS-muotoja natiivista Delphi- ja C++Builder-koodista ilman asennettua Exceliä tai LibreOfficea, minkä takia miljoonan rivin toisto on asia, joka kirjaston on mallinnettava hyvin sen sijaan että se annettaisiin ulkoiselle prosessille — HotXLS Delphi -taulukkokomponentin sivulla luetellaan tuetut formaatit ja RAD Studio -versiot