Tekninen artikkeli

ODS-toistuvat rivit rivikorkeusjaksoina HotXLS:ssä

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

Miten HotXLS muuttaa yhden toistuvan ODS-rivin kompaktiksi tilaksi: content.xml-elementti, jossa on table:number-rows-repeated 1048530 ja tyyli ro1, kuvautuu yhdeksi TXLSXRowHeightRun-tietueeksi, joka kattaa rivit 46–1048575 korkeudella 12.81 pt, sekä yhdeksi StyleOverlays-merkinnäksi per sarake, kun taas versio 2.382.1 laajensi saman elementin miljoonaksi SetRowHeight-merkinnäksi ja soluobjektiksi
Toistoluku, ro1-rivikorkeus ja sarakkeiden oletustyylit kuvaavat jokaisen tyhjän rivin rivin 45 alapuolella, joten tuontiohjelma voi rakentaa yhden jaksotietueen ja sarakekohtaiset päällysteet koskematta miljoonaan koordinaattiin

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

Rivikorkeusjaksojen leikkaus HotXLS:ssä: OpenODS-kutsun jälkeen yksi jakso kattaa rivit 46–1048575 korkeudella 12.81 pt, kun taas rivikohtainen merkintä asettaa rivin 500000 korkeudeksi 36 pt ja voittaa haun koska GetRowHeight tarkistaa rivikohtaisen listan ensin, ja rivin 500001 ClearRowHeight jakaa jakson kahdeksi erilliseksi osaksi reiän ympäriltä
XlsxAssignRowHeightRun kopioi tyhjennetyn välin ulkopuoliset osat eikä lisää väliä itseään varten mitään, joten jaksot pysyvät rakenteellisesti erillisinä ja haku voi pysähtyä ensimmäiseen osumaan samalla kun rivin 500000 ohitus jää koskemattomaksi

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

Mitä SaveAsODS kirjoittaa jakson varaan tallennetusta taulukosta: contentMaxRow pysähtyy riville 45, jossa arvot loppuvat, kun taas maxRow ulottuu korkeusjakson ja sen ohitusten läpi, rajan yläpuoliset rivit kirjoitetaan yksi kerrallaan ja häntä tuotetaan toistuvina table-row-elementteinä, joiden rivityyli tulee RowStyleFor-funktiosta ja joiden solutyylit ratkeavat päällysteiden kautta
Jokainen toistuva elementti kattaa yhden yhtenäisen jakson ja pysähtyy seuraavaan jakson reunaan, korkeusmerkintään, päällysteen reunaan tai materialisoituun soluun, joten ilman validointia oleva taulukkosivu tallentuu kourallisena elementtejä, kun taas validoinnit tai XLSX-vienti maksavat rivi riviltä
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