Műszaki cikk

HotXLS: ODS ismételt sorok soremagasság-runokkal Delphiben

A HotXLS Delphi Component az olyan ODS-sort, amely table:number-rows-repeated értéket és soremagasságot hordoz, egyetlen TXLSXRowHeightRun rekordként tárolja — első sor, utolsó sor, egy magasság — az ismételt soronkénti egy magasságbejegyzés helyett, az ezek által örökölt üres cellák stílusát pedig egyetlen intervallumos stílusrétegbe vonja össze. Pontosan ez az oka annak, hogy a HotXLS 2.382.2 0,02 másodperc alatt megnyit egy olyan táblázatot, amelynek a vége 1,048,530 üres sort ismétel, ahol a 2.382.1 időtúllépésbe futott, és hogy ugyanez a fájl az ismétlésszám épségében mentődik vissza ODS-be, nem egymillió szó szerinti sorként

A szóban forgó fájl teljesen hétköznapi. A LibreOffice Calc egy tizennégy oszlopos munkalapot ír 45 sor adattal, majd mindent leír alatta egyetlen elemmel: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Az ro1 stílus style:row-height="0.452cm" értéket állít be, és minden <table:table-column> hordoz egy table:default-cell-style-name bejegyzést, amelyet a szakasz minden üres cellája örököl. A teljes content.xml 103 KB. Semmi nem mondja a fájlon, hogy „drága"; a költség teljes egészében a miénk volt

Hogyan alakítja a HotXLS az egyetlen ismételt ODS-sort tömör állapottá: a content.xml table:number-rows-repeated 1048530 értékkel és ro1 stílussal rendelkező eleme egyetlen, a 46. sortól 1048575-ig terjedő, 12.81 pt magasságú TXLSXRowHeightRun rekordra képeződik, oszloponként egy StyleOverlays bejegyzéssel, míg a 2.382.1 verzió ugyanezt az elemet egymillió SetRowHeight bejegyzéssé és cellaobjektummá tágította
Az ismétlésszám, az ro1 sormagasság és az oszlopok alapértelmezett stílusai együtt írják le a 45. sor alatti minden üres sort, így az importáló egyetlen run-rekordot és oszloponkénti rétegeket építhet anélkül, hogy egymillió koordinátához hozzányúlna

Miért fut időtúllépésbe egyetlen ismételt sor az ODS-importnál?

Mert az importáló korábban kitágította. A 2.382.1-ben a sorbefejező ciklus ismételt soronként egyszer hívta a SetRowHeight(RowIndex + i, RowHeight) metódust, és minden magasságot beírt egy sorszám szerint kulcsolt Name=Value sztringlistába. A listába történő minden beszúrás végigfutott egy IndexOfName kereséssel a benne már meglévő összes elemen, így egymillió magasság egymillió lineáris bejárásba került — ez az a négyzetes listakeresés, amely miatt a HXLS-005 megnyílt. Ezzel egy időben az OdsCommitRow minden olyan oszlophoz materializált egy cellaobjektumot, amely stílust örökölt, méghozzá az ismételt sorok mindegyikén, mert egy stílusos üres cella is cellának számított

A mentés oldalának megvolt a maga változata ugyanebből a problémából. A LibreOffice fájl a nagy ismétlés után még egy ro1 sorral zárul, így a legmagasabban lévő stílusos sor a munkalap legalján ült, az OdsBuildTableXml pedig végigsétált minden soron odáig, és egyenként bocsátotta ki a <table:table-row> elemeket. Még egy olcsón importált munkafüzet is drágán íródott volna ki. Az import javítása az export javítása nélkül csak eltolta volna az időtúllépést, nem szüntette volna meg

Mi az a soremagasság-run a HotXLS-ben?

A run a legkisebb dolog, amellyel le lehet írni, hogy „a 46. sortól az 1,048,575. sorig minden sor 12.81 pont magas", anélkül hogy 1,048,530-szor elmondanánk. A TXLSXRowHeightRun a FirstRow, LastRow és Height mezőkből álló rekord; a TXLSXRowHeightRuns ezek dinamikus tömbje, és minden TXLSXWorksheet tart belőle egyet a FRowHeightRuns mezőben, a meglévő soronkénti magasságlista mellett. ODS-importnál a sorbefejező mostantól az ismétlésszám alapján ágazik el: 1-es számnál továbbra is a SetRowHeight fut, minden nagyobb értéknél egyszer hívódik az XlsxAssignRowHeightRun a teljes szakaszra. A szakasz a XlsxMaxRow értékére, azaz 1,048,576-ra van vágva, így a munkalapon túllövő ismétlésszám csonkul, nem pedig visszautasítódik

Az XlsxAssignRowHeightRun a tömb egyetlen írója, és felépítéséből adódóan diszjunktan tartja a runokat. Egy új intervallum fogadásakor átmásol minden olyan létező runot, amely teljes egészében kívül esik rajta, a vele átfedő runokat szétdarabolja az elé és az utána eső részre, majd hozzáfűzi az új intervallumot, ha a Present igaz — vagy semmit nem fűz hozzá, ha a Present hamis, így üt a ClearRowHeight egy egysoros lyukat. Két dolog következik ebből. A tömb soha nem tartalmaz átfedő intervallumokat, így egy keresés megállhat az első találatnál. A tömb pedig soha nem módosul helyben; minden hívásnál friss másolat épül, ami az itteni méreteknél semmibe sem kerül, és az aliasing-hibák egy egész osztályát szünteti meg

var
  Workbook: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    // Olyan munkalap, amelynek a záró sora 1,048,530-szor ismétlődik egyetlen sormagasság-stílus alatt
    Workbook.OpenODS('conditional-formatting.ods');
    Sheet := Workbook.Sheets[1];
    // Mindkét olvasás ugyanazon a runon oldódik fel; semmi nem tágult ki
    Writeln(Sheet.RowHeight[46]:0:2, ' pt');
    Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
    // Az egysoros felülírás árnyékolja a runot, de nem darabolja szét
    Sheet.RowHeight[500000] := 36;
    // Egy sor törlése a runon belül két darabra vágja a runot
    Sheet.ClearRowHeight(500001);
    Writeln(Sheet.HasRowHeight(500001)); // False
    Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // továbbra is a run magassága
  finally
    Workbook.Free;
  end;
end;

A keresési sorrend az a rész, amit érdemes megjegyezni. A TXLSXWorksheet.GetRowHeight először a soronkénti listát nézi meg, és csak akkor fordul a runokhoz, ha a sornak nincs explicit bejegyzése; a HasRowHeight ugyanígy tesz. A Sheet.RowHeight[500000] := 36 tehát hozzá sem nyúl a runhoz — egyetlen bejegyzést tesz a soronkénti listába, és az a bejegyzés nyer, mert azt nézi meg először a keresés. A ClearRowHeight ennek az ellenkezője: eltávolít minden soronkénti bejegyzést, majd Present = False értékkel hívja az XlsxAssignRowHeightRun metódust, mert egy törölt sornak „nincs magassága" jelentéssel kell olvasódnia akkor is, ha egy run lefedi. A ClearRowHeights egyszerre üríti ki mindkét szerkezetet

Soremagasság-run műtét a HotXLS-ben: az OpenODS után egyetlen run fedi a 46. sortól az 1048575. sorig 12.81 pt magassággal, egy soronkénti bejegyzés viszont a 36 pt-ra állítja az 500000. sort, és megnyeri a keresést, mert a GetRowHeight először a soronkénti listát nézi, az 500001. sor ClearRowHeight hívása pedig két diszjunkt darabra vágja a runot a lyuk körül
Az XlsxAssignRowHeightRun a törölt intervallumon kívüli darabokat másolja, magáért az intervallumért pedig semmit nem fűz hozzá, így a runok felépítésüknél fogva diszjunktak maradnak, a keresés megállhat az első találatnál, az 500000. sor felülírása pedig érintetlen

Hová kerülnek az örökölt üres cellák stílusai?

Oszloponként egyetlen intervallumos stílusrétegbe, nem cellaobjektumokba. Az OdsCommitRow oszlopértékenként dönti el, hogy az adott érték tömör üres-e: a sor egynél többször ismétlődik, és a cellának nincs értéke, nincs képlete és nincs rich text tartalma. Tömör üres cella esetén valódi cellát csak a run első sorában hoz létre, arra alkalmazza az örökölt stílust, majd ugyanazt a hat stílusindexet — font, kitöltés, szegély, számformátum, igazítás, védelem — regisztrálja egy StyleOverlays.Add hívással, amely az adott oszlopban a run második sorától a végéig terjed. Az első utáni sorokat a materializálási ciklus teljesen kihagyja

A regressziós teszt kézzelfogóvá teszi az alakzatot. Miután megnyit egy munkalapot, amelynek a második sora 1,048,575-szor ismétlődik egy félkövér oszlopalap-stílus alatt, a teszt azt állítja, hogy a Sheet.Cells.Count 10 alatt van, és a Sheet.Cells[700000, 1].FontIndex továbbra is a félkövér fontra oldódik fel — a réteg szolgáltatja a stílust abban a pillanatban, amikor arra a koordinátára hozzányúlunk. Ugyanez a mechanizmus az, amely az XLSX oldalon megakadályozza, hogy egy formázott, de üres oszlop egymillió cellába kerüljön; a sorblokk-alapú cellatárolásról és intervallumos stílusrétegekről szóló jegyzet tárgyalja, hogyan rétegződnek és oldódnak fel a rétegek. Az itteni újdonság az, hogy az ODS-importáló magától hozza létre őket, az ismétlésszámból, ahelyett hogy megvárná, míg egy alkalmazás formáz egy tartományt

Hogyan írja vissza a SaveAsODS az ismétlésszámot?

Úgy, hogy a munkalap üres farkát csak ott bontja fel, ahol valami tényleg változik. Az OdsBuildTableXml mostantól két határt követ: a contentMaxRow az utolsó olyan sor, amely értéket, képletet, hiperhivatkozást vagy kézi sortörést hordoz, a maxRow pedig ezen túl kiterjed a csak stílussal rendelkező üres cellákra, az egysoros magasságokra, minden run LastRow értékére és minden réteg alsó élére. A csak stílussal rendelkező üres cella már nem számít tartalomnak — a TXLSXCells.IsStyleOnlyBlank az, ami kizárja —, így a LibreOffice fájl záró stílusos sora nem rángatja többé a tartalomhatárt a munkalap aljáig

A contentMaxRow felett a sorok pontosan úgy, egyenként íródnak ki, mint korábban. Alatta az író a nextRow értékét a következők közül a legkisebbként számítja: a következő run FirstRow értéke, az aktuális run LastRow + 1 értéke, a következő egysoros magasságbejegyzés, a következő rétegél és a következő materializált cella. Az aktuális sortól a nextRow - 1-ig minden egyetlen <table:table-row> elemmel bocsátódik ki, amelynek a table:number-rows-repeated értéke a különbség, és oszloponként egy <table:table-cell/> elemet hordoz, a réteg által feloldott stílusnévvel, ha az adott oszlopot réteg fedi. Maga a sorstílus a TOdsAutoStylePool.RowStyleFor(AHidden, ABreakBefore, AHeightSpec) hívásból származik, amely mostantól a magasságszöveget — mondjuk a 12.81pt-t — is belevonja a deduplikációs kulcsába a rejtett és oldaltörés jelzők mellé, így a run minden sora egyetlen ro<N> stílust oszt meg egyetlen style:row-height tulajdonsággal

Mit ír a SaveAsODS egy runokkal alátámasztott munkalaphoz: a contentMaxRow a 45. sornál áll meg, ahol az értékek véget érnek, a maxRow viszont kiterjed a magasság-runon és annak felülírásain, a határ feletti sorok egyenként íródnak ki, a farok pedig ismételt table-row elemekként bocsátódik ki, amelyek sorstílusa a RowStyleFor-ból származik, cellastílusai pedig a rétegeken keresztül oldódnak fel
Minden ismételt elem egyetlen egyenletes szakaszt fed le, és a következő runél, magasságbejegyzés, rétegél vagy materializált cella előtt áll meg, így egy érvényesítés nélküli munkalap néhány elemként mentődik, míg az érvényesítések vagy egy XLSX-export soronként fizetnek
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);
    // Az üres farok néhány ismételt sorként íródik ki, nem egymillióként
    Workbook.SaveAsODS(Saved);
    Writeln('ODS size: ', Saved.Size, ' bytes');
    Saved.Position := 0;
    Reopened.Open(Saved);
    // A felülírás, a lyuk és a run is túléli a körbejáratást
    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);   // a run magassága
  finally
    Saved.Free;
    Reopened.Free;
    Workbook.Free;
  end;
end;

Az ezt rögzítő teszt azt állítja, hogy a mentett stream 64 KB alatt van egy olyan munkalapnál, amelynek a magasság-runja 1,048,575 sort fog át, egy felülírással és középre ütött lyukkal. Két őszinte határ tartozik ehhez a számhoz. Először: bármilyen adatérvényesítéssel rendelkező munkalap a contentMaxRow értékét a maxRow-ra állítja, így az érvényesítések kikapcsolják a faroktömörítést azon a lapon, és az újra soronként íródik ki. Másodszor: az XLSX-nek nincs ismétlés-attribútuma — egy SpreadsheetML <row> egyetlen sort ír le —, így egy runokkal alátámasztott munkalap .xlsx-be exportálása felsorolja a run által fedett sorokat, és mindegyikre kiír egy ht attribútumot. A modell tömör marad a memóriában; a fájlformátum dönti el, hogyan néz ki a fájl

Mivel tartozik mostantól minden sor-újraszámozó művelet a runoknak?

Karbantartással. A sormetaadatok új reprezentációja csak akkor helyes, ha minden olyan művelet, amely megváltoztatja a sorszámokat, együtt mozgatja a mellette ülő soronkénti listákkal, és a commit mindegyik ilyen művelethez hozzányúl. Az InsertRows és a DeleteRows az XlsxShiftRowHeightRuns metóduson megy át, amely úgy építi újra a tömböt, hogy megtartja minden runnak a szerkesztési pont elé eső részét, eldobja azt, ami egy törlési ablakba esik, és a maradékot a deltával eltolva visszateszi — így a beszúráson átnyúló run két runná válik egy hézaggal, a törlésen átnyúló run pedig összezsugorodik. A TileRangeAxisMetadata törli a runokat a teljes csempézett szakaszon, majd másolatonként egyszer újra regisztrálja az egyes forrás-runokat a saját eltolásuknál. A TXLSXWorksheet.CopyFrom és a TXLSXSheets.AddCopy a tömb Copy() másolatát veszi át a hozzárendelés helyett, ezért tudja a teszt az összes magasságot törölni egy klónon, miközben az eredeti munkalap érintetlenül megvan az 1,048,576. sorban

var
  Sheet: TXLSXWorksheet;
begin
  Sheet := Workbook.Sheets[1];
  Sheet.RowHeight[500000] := 36;
  Sheet.ClearRowHeight(500001);
  // Két sor beszúrása 500000-nél: a felülírás 500002-re, a lyuk 500003-ra kerül
  Sheet.InsertRows(500000, 2);
  Writeln(Sheet.RowHeight[500002]:0:2);   // 36.00
  Writeln(Sheet.HasRowHeight(500003));    // False
  // Töröljük őket újra: minden visszacsúszik
  Sheet.DeleteRows(500000, 2);
  Writeln(Sheet.RowHeight[500000]:0:2);   // 36.00
  // A 2..4. sorok kétszeres csempézése lefelé; a run-magasságok követik az egyes másolatokat
  Sheet.TileRangeAxisMetadata(2, 1, 3, 1, 2, 1);
  Writeln(Sheet.RowHeight[7]:0:2);        // a run magassága
end;

Az olvasási oldali határokra ugyanez a kötelezettség vonatkozik. A GetUsedRange az alsó élét minden run FirstRow és LastRow értékéig tolja, a BuildRowMajorCellOrder pedig a metaadatokat is beleszámító maximális sorát minden runon keresztül kiterjeszti, hogy az XLSX-író a csak magassággal rendelkező sorokat is meglátogassa. Ha valaha saját, sorra kulcsolt szerkezetet épít a HotXLS objektummodelljére, ez az ellenőrzőlista: beszúrás, törlés, csempézés, másolás, használt tartomány és minden szerializáló. Ha egyet kihagy, a hiba csendes — a magasságok elcsúsznak a beszúrások számával, és semmi nem dob kivételt

Mi marad soronkénti, és hogyan festenek most a számok

A rejtett jelzők, a tagolási szintek és az összecsukott állapot továbbra is kitágulnak. A sorbefejező ismételt soronként egyszer hívja a SetRowHidden és a SetRowOutlineLevel metódust, így az a munkalap, amely elrejt egy egymillió soros farkot, vagy beágyazza egy table:table-row-group elembe, ezekért az attribútumokért soronkénti bejegyzést fizet. A 2.382.2 változtatása arra a két dologra terjed ki, amelyet a HXLS-005 ténylegesen mért — a magasságokra és az örökölt üres stílusokra —, és ugyanez a run-technika a többi tulajdonságra is alkalmazható volna, ha egy fájl valaha megkövetelné. Az ODS-olvasó a style:use-optimal-row-height bejegyzésre sem reagál; az a sorstílus, amely azt mondja, hogy „optimális", és mégis ad egy magasságot, azzal a magassággal importálódik

A korpuszon a conditional-formatting.ods mostantól 0,178 másodperc alatt teljesíti a megnyitás, ellenőrzés, mentés, újranyitás és újraellenőrzés ciklusát Win32 alatt, és 0,158 másodperc alatt Win64 alatt, maga a megnyitási szakasz 0,020 másodperc, mindez egy olyan 60 másodperces kereten belül, amelyet korábban kimerített. A munkafüzet szintű felületeket, amelyeken a formátum átfolyik, az ODS-fájlok megnyitásáról és mentéséről szóló végigvezetés írja le, a nagy fájlok szélesebb eszközkészletét pedig a nagy munkafüzetek teljesítményéről szóló cikk; magát az ODF sorelemet, ismétlés- és stílusattribútumaival együtt, az ODF 1.3 Part 3 §9.1.4 specifikálja

A HotXLS natív Delphi és C++Builder kódból olvassa és írja az XLS, XLSX és ODS formátumot Excel vagy LibreOffice telepítése nélkül, ezért az egymillió soros ismétlés olyasmi, amit a könyvtárnak rendesen modelleznie kell, nem pedig külső folyamatra bíznia — a HotXLS Delphi táblázatkezelő komponens oldala felsorolja a támogatott formátumokat és RAD Studio verziókat