Una hoja de cálculo de catorce columnas con 45 filas de datos, guardada como ODS, suele terminar con un elemento más de lo que espera: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Esa única línea describe todas las filas de la 46 hasta el fondo de la cuadrícula. Expándala literalmente, como esa fila escrita 1,048,530 veces, y un content.xml pequeño se convierte en un millón de registros de fila, un millón de entradas de altura y, si las columnas traen un estilo de celda por defecto, casi quince millones de celdas en blanco con estilo
Por qué una única fila repetida es cara de importar
El costo viene de la expansión, y la expansión es la forma obvia de escribir un importador. El estilo ro1 fija style:row-height="0.452cm", que son 12.81 puntos. Un finalizador de filas que lo aplica con una llamada SetRowHeight(RowIndex + i, RowHeight) por fila repetida escribe 1,048,530 entradas en la tabla de alturas por fila que sea que guarde el modelo de objetos. Si esa tabla es una lista de cadenas con clave donde cada escritura primero busca una clave existente, la importación es cuadrática en el conteo de repetición, y un conteo de un millón es precisamente la entrada donde lo cuadrático deja de ser teórico
Las celdas en blanco lo empeoran. Cada <table:table-column> en un archivo así suele nombrar un table:default-cell-style-name, y cada celda vacía de la fila repetida lo hereda. Un blanco con estilo sigue siendo una celda para la mayoría de los modelos de objetos. Materializar una por columna por fila repetida y la factura de memoria eclipsa la tabla de alturas
El lado del guardado tiene el problema espejo. Cuando el modelo dice que la fila 1,048,575 tiene una altura, un writer que recorre desde la fila 1 hasta la última fila con algo encima emite un millón de elementos <table:table-row>. Una importación barata seguida de un guardado caro solo mueve el costo a otro botón
Cómo HotXLS guarda una altura de fila repetida
HotXLS Delphi Excel Component guarda una altura de fila ODS repetida como un tramo (run): un registro TXLSXRowHeightRun que contiene FirstRow, LastRow y una Height. Cuando TXLSXWorkbook.OpenODS termina una fila cuyo conteo de repetición es 1, llama a SetRowHeight como cualquier otro código. Para cualquier conteo mayor registra el tramo completo como un run, así que la cola del ejemplo se vuelve las filas 46 a 1,048,575 a 12.81 pt. Un conteo de repetición que se pase del final de la cuadrícula se corta en la fila 1,048,576 en lugar de rechazarse
Una regla del importador sorprende a quienes comparan el resultado con el archivo fuente con un diff. Las aplicaciones de hojas de cálculo que escriben ODS tienden a dar a cada estilo de fila un style:row-height, incluidas filas que nadie redimensionó jamás, y a marcarlas con style:use-optimal-row-height="true". Para una fila que no guarda ninguna celda, una altura automática es simplemente la altura por defecto, así que HotXLS no registra altura para ella. Las filas que sí guardan algo conservan su altura aunque esté marcada como óptima, porque una fila de texto ajustado que creció para caber su contenido se encogería al reabrir el workbook como XLSX. Ese equilibrio es el correcto: soltar una altura real produce un cambio visible de maquetación, mientras que conservar una redundante cuesta un solo run
La lectura de alturas pasa por la propiedad RowHeight ordinaria. Consulta primero la lista por fila y recurre a los runs solo cuando la fila no tiene entrada explícita; HasRowHeight sigue el mismo orden. Los runs nunca se solapan, así que el recurso se detiene en el primer run que contiene la fila
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Spans: TXLSXAxisSpans;
I: Integer;
begin
Workbook := TXLSXWorkbook.Create;
try
// Una hoja cuyo último elemento de fila se repite 1,048,530 veces
Workbook.OpenODS('tail-repeat.ods');
Sheet := Workbook.Sheets[1]; // Sheets es 1-based
// Metadatos de fila guardados: entradas de una fila primero, luego runs
Spans := Sheet.GetStoredAxisSpans(True);
for I := 0 to High(Spans) do
Writeln(Spans[I].FirstIndex, '..', Spans[I].LastIndex);
// Ambas lecturas vienen del mismo run
Writeln(Sheet.RowHeight[46]:0:2, ' pt');
Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
// Una entrada por fila se consulta antes que el run, así que gana
Sheet.RowHeight[500000] := 36;
// Limpiar una fila dentro del run le abre un agujero de una fila
Sheet.ClearRowHeight(500001);
Writeln(Sheet.HasRowHeight(500001)); // FALSE
Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // sigue la altura del run
finally
Workbook.Free;
end;
end;
Las dos ediciones de ese ejemplo se comportan distinto a propósito. Sheet.RowHeight[500000] := 36 deja el run tranquilo y agrega una entrada a la lista por fila, que le hace sombra al run porque se consulta primero. ClearRowHeight no puede trabajar así, porque una fila limpiada tiene que reportar que no tiene altura aunque un run siga cubriéndola. Así que borra cualquier entrada por fila y luego reconstruye la lista de runs: cada run fuera de la fila limpiada se queda como está, el run que la contenía se parte en la parte anterior y la posterior, y no se guarda nada para la fila 500,001 misma. GetStoredAxisSpans(True) ahora lista 46..500000 y 500002..1048575 como dos runs separados, junto a la entrada de una sola fila para 500,000. ClearRowHeights vacía la lista por fila y los runs en una sola llamada
¿A dónde van los estilos heredados de celdas en blanco?
Los estilos heredados de blancos se convierten en capas de estilo (style overlays): rectángulos sobre la hoja que llevan un estilo, sin objetos de celda detrás. Al leer una fila, el importador recorre las celdas repetidas en tramos de columnas adyacentes que resuelven al mismo estilo. Un tramo cuyo estilo se ve exactamente como Default no guarda nada. Un tramo con formato real, digamos un valor por defecto de columna centrado o en negrita, se vuelve un valor de fila que cubre todas sus columnas
Cuando ese valor de fila se confirma y la fila se repite, HotXLS materializa una sola celda en la esquina superior izquierda del tramo, le aplica el estilo heredado y registra los seis índices de estilo (fuente, relleno, borde, formato numérico, alineación, protección) mediante Sheet.StyleOverlays.Add para todo el rectángulo: cada columna del tramo, desde la primera fila repetida hasta la última. No se crea nada más para las 1,048,529 filas restantes. Sheet.Cells.Count se queda cerca del número de celdas que realmente contienen datos
Una celda dentro del rectángulo toma la capa en el momento en que su código la toca. Tras abrir una hoja cuya cola está formateada por un valor por defecto de columna en negrita, Sheet.Cells[700000, 1].FontIndex resuelve a la fuente en negrita aunque no existiera ninguna celda en esa coordenada una línea antes. Los archivos XLSX con una columna con formato pero vacía pasan por la misma maquinaria, y el artículo sobre almacenamiento de celdas por bloques de fila y capas de estilo por intervalo cubre cómo varias capas se superponen y resuelven. El importador ODS difiere solo en de dónde vienen las capas: las deriva él mismo de los conteos de repetición
Cómo SaveAsODS escribe el conteo de repetición de vuelta
TXLSXWorkbook.SaveAsODS escribe cada tramo de filas idénticas como un <table:table-row> con table:number-rows-repeated, y termina un tramo solo donde algo de las filas cambia. La tabla se extiende hasta la fila más baja que porte algo: un valor, una celda en blanco solo con estilo, una altura de una sola fila, el fin de un run de alturas, el borde inferior de una capa, una entrada oculta o de esquema, o un área de validación o formato condicional
Desde la fila 1, el writer mira hacia adelante buscando la siguiente frontera. Los candidatos son la siguiente celda materializada, el inicio o fin de un run de alturas, cualquier entrada de una sola fila (altura, flag de oculto, nivel de esquema, estado contraído, salto de página manual), bordes de capas, rangos combinados, hipervínculos, comentarios, imágenes, y los bordes de cada rectángulo de validación y formato condicional. Todo hasta esa frontera es un elemento. Dentro de cada elemento la misma lógica corre por columnas, así que un tramo vacío se vuelve un <table:table-cell> con table:number-columns-repeated y el nombre de estilo al que resuelve la capa
Los estilos de fila se comparten. La altura de fila se formatea como texto, 12.81pt para el ejemplo, y ese texto junto con el flag de oculto y el flag de salto de página forma la clave del estilo de fila automático. Cada fila con los mismos tres valores apunta al mismo estilo ro<N>, que guarda una única propiedad style:row-height. Una fila sin altura, visible y sin salto no recibe ningún estilo de fila
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 cola se escribe como unos pocos elementos de fila repetidos
Workbook.SaveAsODS(Saved);
Writeln('ODS stream: ', Saved.Size, ' bytes');
Saved.Position := 0;
Reopened.OpenODS(Saved);
// Esperado: la anulación, el agujero y ambas piezas del run sobreviven al ciclo
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); // la altura del run
finally
Saved.Free;
Reopened.Free;
Workbook.Free;
end;
end;
Para la hoja de arriba, la cola se parte en cuatro piezas: filas 46 a 499,999 a la altura del run, fila 500,000 a 36 pt, fila 500,001 sin estilo de fila, y filas 500,002 a 1,048,575 de nuevo a la altura del run. Cualquier columna cuyo estilo por defecto dejó una capa puede agregar una partición donde termina esa capa. Las 45 filas de datos encima de la cola se escriben un elemento por fila porque cada una contiene celdas
XLSX es otra historia. SpreadsheetML no tiene atributo de repetición, y un elemento <row> describe exactamente una fila, así que guardar una hoja respaldada por runs como .xlsx enumera las filas que un run cubre y escribe ht y customHeight="1" en cada una. El modelo se queda compacto en memoria; el formato de destino decide qué tan grande se hace el archivo. Si un workbook llega como ODS y solo tiene que salir como ODS, nunca paga ese costo por fila
Ediciones que renumeran filas
Una segunda representación de las alturas de fila solo es correcta si cada operación que mueve filas la mueve también. TXLSXWorksheet.InsertRows y DeleteRows desplazan los runs junto con las listas por fila: la parte de un run anterior al punto de edición se queda donde está, las filas dentro de una ventana eliminada se sueltan, y el resto se mueve por el conteo insertado o eliminado. Un run que cruza una inserción se vuelve por lo tanto dos runs con un hueco de filas sin altura entre ellos, y un run que cruza una eliminación se acorta
procedure ShiftAroundTheRun(Sheet: TXLSXWorksheet);
begin
Sheet.RowHeight[500000] := 36;
Sheet.ClearRowHeight(500001);
// Dos filas nuevas en 500000: la anulación pasa a 500002, el agujero a 500003
Sheet.InsertRows(500000, 2);
Writeln(Sheet.RowHeight[500002]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500003)); // FALSE
Writeln(Sheet.HasRowHeight(500000)); // FALSE: las filas insertadas no tienen altura
// Elimínelas otra vez: la anulación y el agujero vuelven a sus números de fila
Sheet.DeleteRows(500000, 2);
Writeln(Sheet.RowHeight[500000]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500001)); // FALSE
Writeln(Sheet.UsedRange.Row2); // incluye el final del run desplazado
end;
El ciclo ida y vuelta no deja la cola exactamente donde estaba. La inserción empuja el run de la cola de 500,002..1,048,575 a 500,004..1,048,577, y la cuadrícula termina en la fila 1,048,576, así que la parte pasada esa fila se corta. La eliminación luego sube lo que queda dos posiciones, y el run termina en la fila 1,048,574: la fila 1,048,575 ya no tiene altura guardada. Si las últimas filas de la cuadrícula le importan, vuelva a fijar su altura después de un par de inserción y eliminación. Las filas insertadas en medio de un run tampoco toman su altura: abren un hueco de filas sin altura y no heredan la altura del run, razón por la cual HasRowHeight(500000) imprimió FALSE arriba
Las demás operaciones que mueven filas siguen el mismo patrón. TileRangeAxisMetadata limpia los runs a lo largo de todo el tramo teselado y luego registra cada run origen una vez por copia en su desplazamiento. TXLSXWorksheet.CopyFrom y TXLSXSheets.AddCopy le dan a la hoja nueva su propia copia de la lista de runs, así que ClearRowHeights sobre un clon deja las alturas del original en su sitio. En el lado de lectura, UsedRange se extiende a la primera y última fila de cada run, y el recorrido de filas del writer XLSX se extiende a través de los runs para que las filas con solo altura se escriban igual
Las filas ocultas y los niveles de esquema se guardan por fila
Las alturas de fila y los estilos heredados de blancos son las dos cosas que colapsan en runs y capas. Los flags de oculto, los niveles de esquema y el estado contraído se registran una vez por fila. Una fila repetida que está oculta, o que vive dentro de un <table:table-row-group>, produce una entrada por fila repetida para cada uno de esos atributos, y al guardar cada entrada es una frontera propia, así que el tramo oculto se escribe un <table:table-row> por fila
Para las plantillas que usted controla, mantenga la parte oculta y agrupada de una hoja tan ajustada como los datos lo necesiten. Ocultar veinte filas debajo de un reporte es gratis. Ocultar todo desde la fila 46 hasta el fondo de la cuadrícula significa un millón de entradas al cargar y un millón de elementos de fila al guardar, y la única diferencia visible es que ya nadie puede desplazarse más allá del reporte
El lado del formato a nivel de workbook, incluido qué características ODS se mapean a qué partes del modelo de objetos, está en el recorrido de apertura y guardado de archivos ODS, y otras palancas para archivos grandes se reúnen bajo rendimiento con workbooks grandes. El elemento de fila mismo, con sus atributos de repetición y estilo, se define en el capítulo de tablas de ODF 1.3 Part 3
Una repetición de un millón de filas es un detalle del formato que un lector nativo tiene que modelar bien, porque no hay suite ofimática de fondo que lo absorba. La página del componente de hojas de cálculo HotXLS para Delphi lista las características XLS, XLSX y ODS que lee y escribe, junto con las versiones de Delphi y C++Builder soportadas