Artículo técnico

Filas ODS repetidas como tramos de altura en HotXLS Delphi

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

Cómo HotXLS convierte una fila ODS repetida en estado compacto: el elemento content.xml con table:number-rows-repeated 1048530 y estilo ro1 se mapea a un único registro TXLSXRowHeightRun que cubre las filas 46 a 1048575 a 12.81 pt más una entrada StyleOverlays por tramo de columnas del mismo estilo, en lugar de expandir el mismo elemento en un millón de entradas SetRowHeight y objetos de celda
El conteo de repetición, la altura ro1 y los estilos por defecto de columna describen cada fila en blanco debajo de la fila 45, así que el importador puede construir un registro de tramo y unas pocas capas por tramo de columnas sin tocar un millón de coordenadas

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

Cirugía de tramos de altura de fila en HotXLS: tras OpenODS un run cubre las filas 46 a 1048575 a 12.81 pt mientras una entrada por fila pone la fila 500000 a 36 pt y gana la búsqueda porque RowHeight consulta primero la lista por fila, y ClearRowHeight de la fila 500001 parte el run en dos piezas disjuntas alrededor del agujero
ClearRowHeight conserva las piezas del run fuera de la fila limpiada y no guarda nada para la fila misma, así que los runs nunca se solapan, una búsqueda puede detenerse en el primer acierto, y la anulación en la fila 500000 queda intacta

¿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

Lo que SaveAsODS escribe para una hoja respaldada por runs: las filas 1 a 45 traen valores y se escriben un elemento por fila, la cola hasta la fila 1048575 se emite como elementos table-row repetidos que se parten en bordes de runs, anulaciones por fila y bordes de capas, las filas con la misma altura comparten un estilo de fila automático, y los estilos de celda resuelven a través de las capas
Cada elemento repetido cubre un tramo uniforme y se detiene en el siguiente borde de run, entrada de altura, borde de capa, área de regla o celda materializada, así que una cola vacía se guarda como un puñado de elementos mientras que una exportación XLSX escribe un elemento de fila por fila cubierta
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