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