Un foglio di calcolo a quattordici colonne con 45 righe di dati, salvato come ODS, di solito finisce con un elemento in più del previsto: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Quella singola riga descrive tutte le righe dalla 46 in giù fino al fondo della griglia. Espandila alla lettera, come quella riga scritta 1.048.530 volte, e un piccolo content.xml si trasforma in un milione di record di riga, un milione di voci di altezza e, se le colonne portano uno stile di cella predefinito, quasi quindici milioni di celle vuote con stile
Perché una singola riga ripetuta costa cara da importare
Il costo viene dall'espansione, e l'espansione è il modo ovvio di scrivere un importer. Lo stile ro1 imposta style:row-height="0.452cm", cioè 12,81 punti. Un finisher di riga che lo applica con una chiamata SetRowHeight(RowIndex + i, RowHeight) per ogni riga ripetuta scrive 1.048.530 voci in qualunque tabella di altezze per riga tenga l'object model. Se quella tabella è una keyed string list dove ogni scrittura cerca prima una chiave esistente, l'import è quadratico nel conteggio di ripetizione, e un conteggio di un milione è precisamente l'input in cui il quadratico smette di essere teorico
Le celle vuote peggiorano le cose. Ogni <table:table-column> in un file del genere di solito nomina un table:default-cell-style-name, e ogni cella vuota nella riga ripetuta lo eredita. Una vuota con stile è comunque una cella per la maggior parte degli object model. Materializzane una per colonna per riga ripetuta e il conto in memoria schiaccia la tabella delle altezze
Il lato salvataggio ha il problema speculare. Una volta che il modello dice che la riga 1.048.575 ha un'altezza, uno writer che cammina dalla riga 1 all'ultima riga con qualcosa sopra emette un milione di elementi <table:table-row>. Un import economico seguito da un save costoso sposta solo il costo su un altro pulsante
Come HotXLS memorizza l'altezza di una riga ripetuta
HotXLS Delphi Excel Component memorizza l'altezza di una riga ODS ripetuta come run: un record TXLSXRowHeightRun che tiene FirstRow, LastRow e un Height. Quando TXLSXWorkbook.OpenODS termina una riga il cui conteggio di ripetizione è 1, chiama SetRowHeight come farebbe qualunque altro codice. Per qualsiasi conteggio maggiore registra l'intero tratto come una run, così la coda dell'esempio diventa le righe da 46 a 1.048.575 a 12,81 pt. Un conteggio di ripetizione che supera la fine della griglia viene tagliato alla riga 1.048.576 invece di essere rifiutato
Una regola dell'importer sorprende chi fa il diff del risultato contro il file sorgente. Le applicazioni per fogli di calcolo che scrivono ODS tendono a dare a ogni stile di riga un style:row-height, comprese righe che nessuno ha mai ridimensionato, e a marcarle con style:use-optimal-row-height="true". Per una riga che non memorizza alcuna cella, un'altezza automatica è semplicemente l'altezza predefinita, quindi HotXLS non registra alcuna altezza per lei. Le righe che memorizzano qualcosa conservano la loro altezza anche se marcate come ottimali, perché una riga con testo a capo cresciuta per contenere il suo contenuto altrimenti si sgonfierebbe alla riapertura della cartella di lavoro come XLSX. Quel compromesso è quello giusto: perdere un'altezza reale produce una modifica visibile del layout, mentre conservarne una ridondante costa una singola run
La lettura delle altezze passa dalla proprietà RowHeight ordinaria. Controlla prima l'elenco per riga e ricade sulle run solo quando la riga non ha una voce esplicita; HasRowHeight segue lo stesso ordine. Le run non si sovrappongono mai, quindi il fallback si ferma alla prima run che contiene la riga
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Spans: TXLSXAxisSpans;
I: Integer;
begin
Workbook := TXLSXWorkbook.Create;
try
// Un foglio il cui ultimo elemento riga si ripete 1.048.530 volte
Workbook.OpenODS('tail-repeat.ods');
Sheet := Workbook.Sheets[1]; // Sheets è 1-based
// Metadati riga memorizzati: prima le voci a riga singola, poi le run
Spans := Sheet.GetStoredAxisSpans(True);
for I := 0 to High(Spans) do
Writeln(Spans[I].FirstIndex, '..', Spans[I].LastIndex);
// Entrambe le letture arrivano dalla stessa run
Writeln(Sheet.RowHeight[46]:0:2, ' pt');
Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
// Una voce per-riga viene cercata prima della run, quindi vince lei
Sheet.RowHeight[500000] := 36;
// Cancellare una riga dentro la run ci apre un buco di una riga
Sheet.ClearRowHeight(500001);
Writeln(Sheet.HasRowHeight(500001)); // FALSE
Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // sempre l'altezza della run
finally
Workbook.Free;
end;
end;
Le due modifiche dell'esempio si comportano diversamente di proposito. Sheet.RowHeight[500000] := 36 lascia stare la run e aggiunge una voce all'elenco per riga, che oscura la run perché viene consultata prima. ClearRowHeight non può fare così, dato che una riga cancellata deve dichiarare nessuna altezza anche se una run la copre ancora. Quindi elimina ogni voce per riga e poi ricostruisce l'elenco delle run: ogni run fuori dalla riga cancellata resta com'è, la run che la conteneva viene divisa nella parte prima e nella parte dopo, e per la riga 500.001 stessa non viene memorizzato nulla. GetStoredAxisSpans(True) ora elenca 46..500000 e 500002..1048575 come due run separate, accanto alla voce a riga singola per 500.000. ClearRowHeights svuota l'elenco per riga e le run in una chiamata sola
Dove finiscono gli stili delle celle vuote ereditate?
Gli stili vuoti ereditati diventano overlay di stile: rettangoli sul foglio di lavoro che portano uno stile, senza oggetti cella dietro. Mentre legge una riga, l'importer percorre le celle ripetute in tratti di colonne adiacenti che risolvono allo stesso stile. Un tratto il cui stile assomiglia esattamente a Default non memorizza nulla. Un tratto con una formattazione vera, per esempio una colonna centrata o in grassetto predefinita, diventa un valore di riga che copre tutte le sue colonne
Quando quel valore di riga viene commesso e la riga si ripete, HotXLS materializza una singola cella in alto a sinistra del tratto, le applica lo stile ereditato, e registra i sei indici di stile (font, riempimento, bordo, formato numerico, allineamento, protezione) attraverso Sheet.StyleOverlays.Add per l'intero rettangolo: ogni colonna nel tratto, dalla prima riga ripetuta all'ultima. Per le restanti 1.048.529 righe non viene creato nient'altro. Sheet.Cells.Count resta vicino al numero di celle che contengono davvero dati
Una cella dentro il rettangolo becca l'overlay nel momento in cui il tuo codice la tocca. Dopo l'apertura di un foglio la cui coda è formattata da una colonna in grassetto predefinita, Sheet.Cells[700000, 1].FontIndex risolve al font grassetto anche se a quella coordinate una riga prima non esisteva alcuna cella. I file XLSX con una colonna formattata ma vuota passano dallo stesso macchinario, e l'articolo su storage a blocchi di riga e overlay di stile a intervallo copre come più overlay si stratificano e si risolvono. L'importer ODS differisce solo per da dove vengono gli overlay: li deriva da solo dai conteggi di ripetizione
Come SaveAsODS riscrive il conteggio di ripetizione
TXLSXWorkbook.SaveAsODS scrive ogni tratto di righe identiche come un <table:table-row> con table:number-rows-repeated, e chiude un tratto solo dove qualcosa nelle righe cambia. La tabella si estende fino alla riga più bassa che porta qualcosa: un valore, una cella vuota solo-stile, un'altezza a riga singola, la fine di una run di altezza, il bordo in fondo di un overlay, una voce nascosta o di struttura, o un'area di validazione o formato condizionale
Dalla riga 1, lo writer guarda avanti verso il prossimo confine. I candidati sono la prossima cella materializzata, l'inizio o la fine di una run di altezza, qualunque voce a riga singola (altezza, flag nascosto, livello di struttura, stato compresso, interruzione di pagina manuale), i bordi degli overlay, gli intervalli uniti, i collegamenti ipertestuali, i commenti, le immagini, e i bordi di ogni rettangolo di validazione e formato condizionale. Tutto fino a quel confine è un elemento. Dentro ogni elemento la stessa logica gira sulle colonne, così un tratto vuoto diventa un <table:table-cell> con table:number-columns-repeated e il nome di stile a cui risolve l'overlay
Gli stili di riga sono condivisi. L'altezza di riga viene formattata come testo, 12.81pt per l'esempio, e quel testo insieme al flag nascosto e al flag di interruzione di pagina forma la chiave per lo stile di riga automatico. Ogni riga con gli stessi tre valori punta allo stesso stile ro<N>, che tiene una singola proprietà style:row-height. Una riga senza altezza, visibile e senza interruzioni non prende alcuno stile di riga
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);
// La coda viene scritta come pochi elementi riga ripetuti
Workbook.SaveAsODS(Saved);
Writeln('ODS stream: ', Saved.Size, ' bytes');
Saved.Position := 0;
Reopened.OpenODS(Saved);
// Atteso: override, buco e entrambi i pezzi della run sopravvivono al 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); // l'altezza della run
finally
Saved.Free;
Reopened.Free;
Workbook.Free;
end;
end;
Per il foglio qui sopra, la coda si divide in quattro pezzi: le righe da 46 a 499.999 all'altezza della run, la riga 500.000 a 36 pt, la riga 500.001 senza stile di riga, e le righe da 500.002 a 1.048.575 di nuovo all'altezza della run. Qualunque colonna il cui stile predefinito ha lasciato un overlay può aggiungere una divisione dove quell'overlay finisce. Le 45 righe di dati sopra la coda vengono scritte un elemento per riga perché ognuna contiene celle
XLSX è un'altra storia. SpreadsheetML non ha un attributo di ripetizione, e un elemento <row> descrive esattamente una riga, quindi salvare un foglio basato su run come .xlsx enumera le righe che una run copre e scrive ht e customHeight="1" su ciascuna. Il modello resta compatto in memoria; è il formato di destinazione a decidere quanto cresce il file. Se una cartella di lavoro arriva come ODS e deve solo uscire come ODS, non paga mai quel costo per riga
Modifiche che rinumerano le righe
Una seconda rappresentazione delle altezze di riga è corretta solo se ogni operazione che sposta righe sposta anche lei. TXLSXWorksheet.InsertRows e DeleteRows spostano le run insieme agli elenchi per riga: la parte di una run prima del punto di modifica sta dov'è, le righe dentro una finestra eliminata cadono, e il resto si sposta del numero di righe inserite o eliminate. Una run che cavalca un inserimento diventa quindi due run con un varco di righe senza dimensione in mezzo, e una run che cavalca un'eliminazione diventa più corta
procedure ShiftAroundTheRun(Sheet: TXLSXWorksheet);
begin
Sheet.RowHeight[500000] := 36;
Sheet.ClearRowHeight(500001);
// Due nuove righe a 500000: l'override passa a 500002, il buco a 500003
Sheet.InsertRows(500000, 2);
Writeln(Sheet.RowHeight[500002]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500003)); // FALSE
Writeln(Sheet.HasRowHeight(500000)); // FALSE: le righe inserite non hanno altezza
// Rimuovile di nuovo: override e buco tornano ai loro vecchi numeri di riga
Sheet.DeleteRows(500000, 2);
Writeln(Sheet.RowHeight[500000]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500001)); // FALSE
Writeln(Sheet.UsedRange.Row2); // comprende la fine della run spostata
end;
Il round trip non lascia la coda esattamente dov'era. L'inserimento spinge la run di coda da 500.002..1.048.575 a 500.004..1.048.577, e la griglia finisce alla riga 1.048.576, quindi la parte oltre quella riga viene tagliata. L'eliminazione poi risolleva ciò che resta di due, e la run finisce alla riga 1.048.574: la riga 1.048.575 non ha più un'altezza memorizzata. Se le ultime righe della griglia ti interessano, reimposta la loro altezza dopo una coppia inserimento-eliminazione. Le righe inserite in mezzo a una run non ereditano la sua altezza neppure loro: aprono un varco di righe senza dimensione e non prendono l'altezza della run, ecco perché HasRowHeight(500000) ha stampato FALSE qui sopra
Le altre operazioni che spostano righe seguono lo stesso schema. TileRangeAxisMetadata cancella le run su tutto il tratto tiling e poi registra ogni run sorgente una volta per copia al suo offset. TXLSXWorksheet.CopyFrom e TXLSXSheets.AddCopy danno al nuovo foglio la sua copia dell'elenco di run, quindi ClearRowHeights su un clone lascia al loro posto le altezze dell'originale. Sul lato lettura, UsedRange si estende alla prima e all'ultima riga di ogni run, e la camminata delle righe dello writer XLSX si estende attraverso le run così le righe solo-altezza vengono comunque scritte
Righe nascoste e livelli di struttura restano per riga
Altezze di riga e stili vuoti ereditati sono le due cose che collassano in run e overlay. I flag nascosto, i livelli di struttura e lo stato compresso vengono registrati una volta per riga. Una riga ripetuta nascosta, o che sta dentro un <table:table-row-group>, produce una voce per riga ripetuta per ciascuno di quegli attributi, e al salvataggio ogni voce del genere è un confine a sé, quindi il tratto nascosto viene scritto con un <table:table-row> per riga
Per i template che controlli tu, tieni la parte nascosta e raggruppata di un foglio così stretta quanto i dati richiedono. Nascondere venti righe sotto un report è gratis. Nascondere tutto dalla riga 46 al fondo della griglia significa un milione di voci al caricamento e un milione di elementi riga al salvataggio, e l'unica differenza visibile è che nessuno può più scorrere oltre il report
Il lato della cartella di lavoro del formato, incluso quali funzionalità ODS mappano su quali parti dell'object model, sta nella guida di apertura e salvataggio dei file ODS, e altre leve per i file grandi sono raccolte sotto prestazioni con cartelle di lavoro grandi. L'elemento riga stesso, con i suoi attributi di ripetizione e stile, è definito nel capitolo table di ODF 1.3 Part 3
Una ripetizione da un milione di righe è un dettaglio di formato che un reader nativo deve modellare bene, perché non c'è una suite per ufficio sullo sfondo ad assorbirlo. La pagina del componente foglio di calcolo Delphi HotXLS elenca le funzionalità XLS, XLSX e ODS che legge e scrive, insieme alle versioni supportate di Delphi e C++Builder