Een spreadsheet met veertien kolommen en 45 rijen data eindigt, als ODS opgeslagen, meestal met één element meer dan u verwacht: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Die ene regel beschrijft elke rij van 46 tot aan de onderkant van het raster. Hem letterlijk uitklappen, als die rij 1.048.530 keer uitgeschreven, en een kleine content.xml verandert in een miljoen rijrecords, een miljoen hoogte-items en, als de kolommen een standaardcelstijl dragen, bijna vijftien miljoen gestijlde lege cellen
Waarom één herhaalde rij duur is om te importeren
De kosten komen van het uitklappen, en uitklappen is de voor de hand liggende manier om een importer te schrijven. Stijl ro1 zet style:row-height="0.452cm", wat 12.81 punten is. Een rij-afronder die dat toepast met één aanroep SetRowHeight(RowIndex + i, RowHeight) per herhaalde rij schrijft 1.048.530 items weg in welke hoogtetabel per rij het objectmodel ook bijhoudt. Is die tabel een stringlijst met sleutels waarin elke schrijfactie eerst naar een bestaande sleutel zoekt, dan is de import kwadratisch in de herhalingsteller, en een herhalingsteller van een miljoen is precies de invoer waarbij kwadratisch ophoudt theoretisch te zijn
Lege cellen maken het erger. Elke <table:table-column> in zo'n bestand noemt meestal een table:default-cell-style-name, en elke lege cel in de herhaalde rij erft die. Een gestijlde leegte is voor de meeste objectmodellen nog steeds een cel. Per kolom per herhaalde rij één materialiseren en de geheugenrekening overtreft de hoogtetabel verre
De opslagkant heeft het spiegelbeeldprobleem. Zegt het model eenmaal dat rij 1.048.575 een hoogte heeft, dan schrijft een writer die van rij 1 tot de laatste rij met iets erop loopt een miljoen <table:table-row>-elementen. Een goedkope import gevolgd door een dure opslag verplaatst de kosten alleen naar een andere knop
Hoe HotXLS een herhaalde rijhoogte opslaat
HotXLS Delphi Excel Component slaat een herhaalde ODS-rijhoogte op als een run: een record TXLSXRowHeightRun met FirstRow, LastRow en één Height. Rondt TXLSXWorkbook.OpenODS een rij af waarvan de herhalingsteller 1 is, dan roept hij SetRowHeight aan zoals elke andere code dat zou doen. Bij elke grotere teller legt hij de hele spanne als één run vast, dus de staart in het voorbeeld wordt rijen 46 tot 1.048.575 op 12.81 pt. Een herhalingsteller die voorbij het einde van het raster loopt, wordt afgekapt op rij 1.048.576 in plaats van geweigerd
Eén regel in de importer verrast wie het resultaat tegen het bronbestand difft. Spreadsheetapplicaties die ODS schrijven geven vrijwel elke rijstijl een style:row-height, ook rijen die niemand ooit heeft verschaald, en markeren die met style:use-optimal-row-height="true". Voor een rij die helemaal geen cellen opslaat is een automatische hoogte simpelweg de defaulthoogte, dus HotXLS legt voor die rij geen hoogte vast. Rijen die wel iets opslaan houden hun hoogte, ook als die als optimaal is gemarkeerd, want een rij met omlopende tekst die was meegegroeid met zijn inhoud zou anders inkrimpen zodra de workbook opnieuw als XLSX wordt geopend. Die afweging is de juiste: een echte hoogte laten vallen levert een zichtbare lay-outwijziging op, terwijl een redundante houden één run kost
Hoogtes teruglezen gaat via de gewone eigenschap RowHeight. Hij bekijkt eerst de lijst per rij en valt alleen terug op de runs wanneer de rij geen expliciet item heeft; HasRowHeight volgt dezelfde volgorde. Runs overlappen nooit, dus de fallback stopt bij de eerste run die de rij bevat
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Spans: TXLSXAxisSpans;
I: Integer;
begin
Workbook := TXLSXWorkbook.Create;
try
// Een blad waarvan het laatste rijelement 1.048.530 keer herhaalt
Workbook.OpenODS('tail-repeat.ods');
Sheet := Workbook.Sheets[1]; // Sheets is 1-based
// Opgeslagen rijmetadata: eerst items van één rij, daarna runs
Spans := Sheet.GetStoredAxisSpans(True);
for I := 0 to High(Spans) do
Writeln(Spans[I].FirstIndex, '..', Spans[I].LastIndex);
// Beide uitlezingen komen uit dezelfde run
Writeln(Sheet.RowHeight[46]:0:2, ' pt');
Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
// Een item per rij wordt vóór de run opgezocht, dus dat wint
Sheet.RowHeight[500000] := 36;
// Eén rij binnen de run wissen knipt een gat van één rij erin
Sheet.ClearRowHeight(500001);
Writeln(Sheet.HasRowHeight(500001)); // FALSE
Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // nog steeds de runhoogte
finally
Workbook.Free;
end;
end;
De twee bewerkingen in dat voorbeeld gedragen zich met opzet verschillend. Sheet.RowHeight[500000] := 36 laat de run met rust en voegt één item toe aan de lijst per rij, dat de run overschaduwt omdat het eerst wordt geraadpleegd. ClearRowHeight kan niet zo werken, want een gewiste rij moet melden dat hij geen hoogte heeft, ook al omspant een run hem nog. Dus hij verwijdert elk item per rij en bouwt daarna de runlijst opnieuw: elke run buiten de gewiste rij blijft zoals hij is, de run die hem bevatte wordt gesplitst in het stuk ervoor en het stuk erna, en voor rij 500.001 zelf wordt niets opgeslagen. GetStoredAxisSpans(True) zet nu 46..500000 en 500002..1048575 als twee aparte runs op de lijst, naast het single-row-item voor 500.000. ClearRowHeights maakt de lijst per rij en de runs in één aanroep leeg
Waar blijven de geërfde stijlen van lege cellen?
Geërfde lege stijlen worden stijloverlays: rechthoeken op het werkblad die een stijl dragen, zonder celobjecten erachter. Bij het lezen van een rij loopt de importer de herhaalde cellen af in spannen van aangrenzende kolommen die naar dezelfde stijl oplossen. Een span waarvan de stijl er precies uitziet als Default slaat niets op. Een span met echte opmaak, zeg een gecentreerde of vette kolomstandaard, wordt één rijwaarde die al zijn kolommen dekt
Wordt die rijwaarde vastgelegd en herhaalt de rij, dan materialiseert HotXLS één cel in de linkerbovenhoek van de span, past er de geërfde stijl op toe en registreert de zes stijlindexen (font, vulling, rand, getalnotatie, uitlijning, beveiliging) via Sheet.StyleOverlays.Add voor de hele rechthoek: elke kolom in de span, van de eerste herhaalde rij tot de laatste. Voor de overige 1.048.529 rijen wordt niets anders aangemaakt. Sheet.Cells.Count blijft dicht bij het aantal cellen dat echt data bevat
Een cel binnen de rechthoek pakt de overlay op zodra uw code hem aanraakt. Na het openen van een blad waarvan de staart door een vette kolomstandaard is opgemaakt, lost Sheet.Cells[700000, 1].FontIndex op naar het vette font, ook al bestond op die coördinaat een regel eerder geen cel. XLSX-bestanden met een opgemaakte maar lege kolom gaan door dezelfde machinerie, en het artikel over rijblokcelopslag en interval-stijloverlays behandelt hoe meerdere overlays stapelen en oplossen. De ODS-importer verschilt alleen in waar de overlays vandaan komen: hij leidt ze zelf af uit de herhalingstellers
Hoe SaveAsODS de herhalingsteller terugschrijft
TXLSXWorkbook.SaveAsODS schrijft elke strekking identieke rijen als één <table:table-row> met table:number-rows-repeated, en beëindigt een strekking alleen waar er iets aan de rijen verandert. De tabel loopt door tot de laagste rij die iets draagt: een waarde, een lege cel met alleen stijl, een hoogte voor één rij, het einde van een hoogterun, de onderrand van een overlay, een verborgen of overzichtsitem, of een validatie- of voorwaardelijk-opmaakgebied
Vanaf rij 1 kijkt de writer vooruit naar de volgende grens. Kandidaten zijn de volgende gematerialiseerde cel, het begin of einde van een hoogterun, elk item voor één rij (hoogte, verborgen-vlag, overzichtsniveau, samengevouwen toestand, handmatig pagina-einde), overlayranden, samengevoegde bereiken, hyperlinks, commentaren, afbeeldingen, en de randen van elk validatie- en voorwaardelijk-opmaakrechthoek. Alles tot die grens is één element. Binnen elk element draait dezelfde logica over de kolommen, dus een lege strekking wordt één <table:table-cell> met table:number-columns-repeated en de stijlnaam waar de overlay op uitkomt
Rijstijlen worden gedeeld. De rijhoogte wordt als tekst opgemaakt, 12.81pt voor het voorbeeld, en die tekst vormt samen met de verborgen-vlag en de pagina-einde-vlag de sleutel voor de automatische rijstijl. Elke rij met dezelfde drie waarden wijst naar dezelfde stijl ro<N>, die één eigenschap style:row-height bevat. Een rij zonder hoogte, zichtbaar en zonder pagina-einde krijgt helemaal geen rijstijl
var
Workbook, Reopened: TXLSXWorkbook;
Saved: TMemoryStream;
begin
Workbook := TXLSXWorkbook.Create;
Reopened := TXLSXWorkbook.Create;
Saved := TMemoryStream.Create;
try
Workbook.OpenODS('tail-repeat.ods');
Workbook.Sheets[1].RowHeight[500000] := 36;
Workbook.Sheets[1].ClearRowHeight(500001);
// De staart wordt als een paar herhaalde rijelementen geschreven
Workbook.SaveAsODS(Saved);
Writeln('ODS stream: ', Saved.Size, ' bytes');
Saved.Position := 0;
Reopened.OpenODS(Saved);
// Verwacht: override, gat en beide runstukken overleven de round trip
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); // de runhoogte
finally
Saved.Free;
Reopened.Free;
Workbook.Free;
end;
end;
Voor het werkblad hierboven splitst de staart in vier stukken: rijen 46 tot 499.999 op de runhoogte, rij 500.000 op 36 pt, rij 500.001 zonder rijstijl, en rijen 500.002 tot 1.048.575 weer op de runhoogte. Elke kolom waarvan de standaardstijl een overlay achterliet, kan een splitsing toevoegen waar die overlay eindigt. De 45 datarijen boven de staart worden per rij één element geschreven, omdat elk ervan cellen bevat
XLSX is een ander verhaal. SpreadsheetML kent geen herhalingsattribuut, en een element <row> beschrijft precies één rij, dus het opslaan van een blad dat op runs rust als .xlsx somt de rijen op die een run dekt en schrijft ht en customHeight="1" op elk ervan. Het model blijft compact in het geheugen; het doelformaat bepaalt hoe groot het bestand wordt. Komt een workbook als ODS binnen en hoeft hij alleen als ODS te vertrekken, dan betaalt hij die kost per rij nooit
Bewerkingen die rijnummers verschuiven
Een tweede representatie van rijhoogtes is alleen correct als elke bewerking die rijen verplaatst hem ook meeverplaatst. TXLSXWorksheet.InsertRows en DeleteRows schuiven de runs mee met de lijsten per rij: het deel van een run vóór het bewerkingspunt blijft waar het is, rijen binnen een verwijderd venster vallen weg, en de rest schuift met het ingevoegde of verwijderde aantal mee. Een run die een invoeging overspant wordt daardoor twee runs met een gat van rijen zonder hoogte ertussen, en een run die een verwijdering overspant wordt korter
procedure ShiftAroundTheRun(Sheet: TXLSXWorksheet);
begin
Sheet.RowHeight[500000] := 36;
Sheet.ClearRowHeight(500001);
// Twee nieuwe rijen op 500000: de override schuift naar 500002, het gat naar 500003
Sheet.InsertRows(500000, 2);
Writeln(Sheet.RowHeight[500002]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500003)); // FALSE
Writeln(Sheet.HasRowHeight(500000)); // FALSE: ingevoegde rijen hebben geen hoogte
// Verwijder ze weer: de override en het gat keren terug naar hun oude rijnummers
Sheet.DeleteRows(500000, 2);
Writeln(Sheet.RowHeight[500000]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500001)); // FALSE
Writeln(Sheet.UsedRange.Row2); // omvat het einde van de verschoven run
end;
De round trip laat de staart niet precies waar hij was. De invoeging schuift de staartrun van 500.002..1.048.575 door naar 500.004..1.048.577, en het raster eindigt op rij 1.048.576, dus het deel voorbij die rij wordt afgekapt. De verwijdering schuift daarna wat er over is twee omhoog, en de run eindigt op rij 1.048.574: rij 1.048.575 heeft geen opgeslagen hoogte meer. Doen de laatste rijen van het raster er voor u toe, zet dan hun hoogte opnieuw na een paar van invoegen en verwijderen. Rijen die midden in een run worden ingevoegd pakken zijn hoogte ook niet op: ze openen een gat van rijen zonder hoogte en erven de runhoogte niet, wat de reden is dat HasRowHeight(500000) hierboven FALSE printte
De andere rij-verplaatsende bewerkingen volgen hetzelfde patroon. TileRangeAxisMetadata wist de runs over de hele getegelde spanne en registreert daarna elke bronrun één keer per kopie op zijn offset. TXLSXWorksheet.CopyFrom en TXLSXSheets.AddCopy geven het nieuwe werkblad zijn eigen kopie van de runlijst, dus ClearRowHeights op een kloon laat de hoogtes van het origineel staan. Aan de leeskant reikt UsedRange tot de eerste en laatste rij van elke run, en rekt de rijronde van de XLSX-writer zich uit over de runs, zodat rijen met alleen hoogte toch worden geschreven
Verborgen rijen en overzichtsniveaus blijven per rij bewaard
Rijhoogtes en geërfde lege stijlen zijn de twee dingen die samenvouwen tot runs en overlays. Verborgen-vlaggen, overzichtsniveaus en samengevouwen toestand worden één keer per rij vastgelegd. Een herhaalde rij die verborgen is, of die in een <table:table-row-group> zit, levert één item per herhaalde rij op voor elk van die attributen, en bij het opslaan is elk zo'n item een grens op zichzelf, dus de verborgen strekking wordt per rij één <table:table-row> geschreven
Voor sjablonen die u zelf beheert: houd het verborgen en gegroepeerde deel van een werkblad zo strak als de data nodig heeft. Twintig rijen onder een rapport verbergen is gratis. Alles vanaf rij 46 tot de onderkant van het raster verbergen betekent een miljoen items bij het laden en een miljoen rijelementen bij het opslaan, en het enige zichtbare verschil is dat niemand nog voorbij het rapport kan scrollen
De kant van het formaat op werkmaphoog, inclusief welke ODS-functies op welke onderdelen van het objectmodel afbeelden, staat in de walkthrough van ODS-bestanden openen en opslaan, en andere hendels voor grote bestanden zijn verzameld onder prestaties van grote werkmappen. Het rijelement zelf, met zijn herhalings- en stijlattributen, is gedefinieerd in het tabelhoofdstuk van ODF 1.3 Part 3
Een herhaling van een miljoen rijen is een formaatdetail dat een native reader goed moet modelleren, want er is geen officesuite op de achtergrond die het opvangt. De HotXLS Delphi spreadsheet component-pagina somt de XLS-, XLSX- en ODS-functies op die hij leest en schrijft, samen met de ondersteunde Delphi- en C++Builder-versies