Artículo técnico

Delimitar registros PivotCache de BIFF con HotXLS

El substream PivotCache de BIFF guarda el conjunto de datos en caché de una tabla dinámica por separado de la vista que la muestra, y HotXLS lee y escribe ese substream inspeccionando los cuerpos de registro en lugar de fiarse de los números de registro. Esa distinción es toda la historia: el mismo número de registro transporta dos disposiciones de cuerpo incompatibles según qué escritor produjo el fichero, así que el lector decide el enmarcado a partir del primer cuerpo de registro que ve

Uno se topa con esta capa en cuanto una tabla dinámica tiene que sobrevivir a un viaje de ida y vuelta. Una vista dinámica sin su caché es un caparazón, y Excel reconstruirá la caché desde el rango de origen al abrir el fichero, lo cual va bien justo hasta el punto en que el rango de origen ya no está, los datos se pegaron desde una consulta, o el libro es un cierre archivado que no debe cambiar cuando alguien lo abre

Dos estructuras, dos sitios del fichero

Los datos en caché y la definición de la caché viven en partes distintas del libro, y no confundirlos es lo primero que hay que hacer bien. Los registros en caché forman su propio substream, dado en [MS-XLS] §2.1.7.12 como PIVOTCACHE = SXDB SXDBEx *SXFORMULA *FDB *DBB EOF. Note lo que falta: no hay BOF a la cabeza de esa producción

La definición se sienta en cambio en los globals del libro, como PIVOTCACHEDEFINITION = SXStreamID SXVS [SXSRC] [SXADDLCACHE] (§2.1.7.20.3), posicionada después de los registros de formato y antes de los registros BoundSheet y Country. Así, una sola caché queda descrita en dos sitios separados por cientos de registros, y el enlace entre ambos es un identificador de stream que tiene que coincidir en tres sitios a la vez

Enmarcado PivotCache de HotXLS en BIFF8: el PIVOTCACHEDEFINITION con su SXStreamID se sienta en los globals del libro después del formato y antes de BoundSheet, mientras que los registros en caché viven en un stream bajo el almacenamiento _SX_DB_CUR nombrado con hex mayúsculo de cuatro dígitos que contiene registros SXDB, SXDBEx, SXFORMULA, FDB y DBB sin BOF, y SXStreamID.idStm, el campo idstm de SXDB y el nombre del stream tienen que coincidir
Una sola caché dinámica queda descrita en dos sitios separados por cientos de registros, unidos por un identificador de stream que tiene que coincidir a la vez en los globals, en la cabecera SXDB y en el nombre del substream

Cada caché pertenece a un stream bajo _SX_DB_CUR cuyo nombre es la escritura hexadecimal en mayúsculas de cuatro dígitos de su identificador. SXStreamID.idStm, el campo idstm repetido en la cabecera SXDB, y ese nombre de stream tienen que coincidir los tres. Cuando asigne un identificador nuevo, reserve primero todo número ya leído del fichero, o una caché nueva puede reclamar un número que pertenece a una caché más vieja a la que el lector aún no ha llegado

Hay otro identificador que pilla a la gente. El valor iCache de una vista dinámica es la posición base cero del SXStreamID correspondiente en la secuencia global, no un identificador de caché que usted pueda escoger. Al escribir hay que mapearlo desde el objeto de caché a su posición real de salida, y las vistas existentes tienen que renumerarse junto con él, o actualizar una caché apunta en silencio una vista a otra distinta

var
  Book: TXLSWorkbook;
  Cache: TXLSPivotCache;
  Field: TXLSPivotCacheField;
  V: TXLSPivotCacheValue;
begin
  Book := TXLSWorkbook.Create(nil);
  try
    Book.LoadFromFile('sales.xls');
    Cache := Book.PivotCaches.Add;
    Cache.SourceRangeSheet := 'Data';
    Cache.SourceFirstRow := 1;  Cache.SourceFirstCol := 1;
    Cache.SourceLastRow := 500; Cache.SourceLastCol := 6;
    Cache.SourceDataType := 1;        // SXVS SHEET, MS-XLS 2.4.317
    Cache.RefreshOnLoad := False;     // confíe en los registros en caché
    Cache.SaveData := True;

    Field := Cache.AddField('Region', xlpcftString);
    V.ValueType := xlpcftString;
    V.StrValue := 'North';
    Field.FindOrAddItem(V);

    Cache.SetRecordCount(0);          // limpie, y luego dimensione la cuadrícula de registros
    Cache.SetRecordCount(500);
    Book.StorePivotCaches;
  finally
    Book.Free;
  end;
end;

El doble SetRecordCount no es superstición. RecordCount es una escritura de propiedad corriente que no reserva, y la ruta interna de crecimiento solo inicializa las filas recién añadidas, así que una caché cuyo conteo se fijó por la ruta de la cabecera puede acabar con una cuadrícula de índices de longitud cero. Las escrituras a RecordIndices se descartan entonces sin error. Fijar el conteo a cero y de vuelta re-establece la cuadrícula, y tiene que ocurrir cuando ya se han añadido todos los campos, porque el ancho de fila sale del número de campos

¿Por qué un número de registro no puede decirle la disposición del cuerpo?

Porque los números de registro y las disposiciones de cuerpo cambiaron en momentos distintos, así que el mapeo entre ambos no es una función. Un número del conjunto legado solo aparece en ficheros de escritores más viejos, lo que lo convierte en una señal fiable en una dirección. Otro número es genuinamente ambiguo: aparece tanto en ficheros correctos como en un rango de versiones intermedias que usaban el número nuevo con la disposición de cuerpo vieja

El enmarcado tiene por tanto que decidirse a partir del cuerpo, y una vez por substream de caché en lugar de por registro. HotXLS fija el dialecto a partir de la longitud del primer registro SXDBB de cada substream. En el enmarcado de la especificación, un SXDBB contiene exactamente un registro de caché, así que su longitud es igual a un ancho de fila. En el enmarcado empaquetado más viejo, el primer registro contiene tantas filas como quepan, así que para cualquier caché con más de una fila es de al menos dos anchos de fila. La comparación es decisiva siempre que las dos predicciones difieran

Fijación de enmarcado SXDBB de HotXLS: un número de registro transporta dos disposiciones de cuerpo incompatibles, así que el lector compara la longitud del primer registro SXDBB contra el ancho de fila, un ancho de fila fija el dialecto de la especificación mientras que dos o más anchos fijan el enmarcado empaquetado legado, los empates toman la lectura de la especificación, y el dialecto se fija una vez por substream de caché, no por registro
Los números de registro no pueden decidir la disposición del cuerpo porque ambos cambiaron en momentos distintos, así que HotXLS fija el dialecto una vez por substream a partir de la primera longitud SXDBB y se queda con la lectura de la especificación en caso de empate

Cuando no difieren, el lector toma la lectura de la especificación, con el principio de que los ficheros escritos por Excel superan en número a los escritos por una build intermedia. Ese punto ciego es estrecho por construcción y, cuando ocurre, el fichero en sí sigue reproduciéndose byte a byte. Solo se ven afectados los índices tipados expuestos a quien llama

El ancho del índice vive en otro registro

SXDBB (§2.4.276) transporta un índice por cada campo de caché cuyo flag de valores distintos está activado, en orden de campos, y el ancho de cada índice se decide en otra parte: el registro de campo SXFDB correspondiente (§2.4.283) declara un flag de items cortos, y ese flag dice si el índice ocupa dos bytes o uno. Dos registros, un contrato implícito, y una sola frase en la especificación que los conecta

Ese acoplamiento es justo donde una codificación casera se equivoca. Un escritor anterior de HotXLS empaquetaba cada campo en el mínimo número de bits, rellenando hasta un límite de byte entre filas, lo cual es defendible en aislamiento y contradice directamente el ancho que el mismo escritor acababa de declarar en SXFDB. Un campo con tres valores distintos quedaba descrito como de un byte de ancho en un registro y ocupaba dos bits en el otro. El arreglo no fue corregir la aritmética sino extraer la decisión del ancho a una función que ambos emisores llaman, de modo que los dos registros ya no pueden divergir. Es la misma clase de defecto descrita en la deriva de declaración de longitud de registros BIFF, donde un tamaño declarado y un cuerpo real se separan

La consecuencia de no leer estos registros en absoluto merece detalle, porque es fácil subestimarla. Cuando el lector se saltaba los índices de registro, cada caché cargada desde un fichero reportaba índice cero para cada campo de cada fila, lo que significa que cada fila apuntaba al primer valor de cada campo. No es meramente una introspección reducida: la ruta de evaluación de la tabla dinámica y la ruta de relleno de caché a celda consumen ambas esa cuadrícula. Y un test de ida y vuelta no puede detectarlo, porque una caché aún en replay bruto se escribe de vuelta desde sus bytes originales

// Los flags de procedencia le dicen qué tiene entre manos y qué puede reescribirse
if Cache.FromRawBlobs then
begin
  Writeln('stream id        : ', IntToHex(Cache.StreamId, 4));
  Writeln('legacy framing   : ', Cache.RawFramingIsLegacy);
  Writeln('own storage      : ', Cache.RawHasStorageStream);
  Writeln('model complete   : ', Cache.RawModelIsComplete);
  // Reemitir solo es sin pérdidas cuando cada registro tiene aquí un modelo
  if Cache.CanUpgradeFraming then
    Writeln('safe to rewrite with the current emitters');
end;

¿Cuándo es reescribir una caché una operación sin pérdidas?

Solo cuando tres condiciones se dan a la vez, y CanUpgradeFraming es la única propiedad que responde a la pregunta. La caché tiene que seguir en replay bruto, el substream tiene que estar en uno de los enmarcados que esta biblioteca escribió incorrectamente antes, y el lector tiene que haber construido un modelo tipado completo de cada registro dentro. Una caché escrita por Excel nunca cumple, porque su substream transporta registros para los que HotXLS no tiene modelo, y reemitir desde el modelo los dejaría caer

El test de completitud es más estricto de lo que parece a primera vista. Un registro que el lector conservó solo como bytes opacos marca el modelo como incompleto. También lo marca un conteo declarado de registros de fórmula que el emisor no puede reproducir, porque reemitir reescribiría una declaración de varios registros de fórmula como una declaración de ninguno, y un valor del fichero que no puede reproducirse equivale a un registro que no puede reproducirse

El conservadurismo deliberado recorre también al escritor. Los índices se acotan al rango legal en lugar de codificarse como un sentinel fuera de banda, porque la especificación define un índice en la secuencia de valores distintos y nada más, y una celda vacía es en sí misma un valor de esa secuencia. Un cuerpo de registro de caché que exceda el techo de registro BIFF no se escribe en absoluto, lo que requeriría miles de campos de caché y es inalcanzable dentro del límite de columnas de BIFF8 de todos modos; el fallback es que Excel refresca desde el rango de origen, que es comportamiento definido y no un fichero corrupto

Las fechas llevan la última dependencia entre registros. La conversión de serial a fecha depende del sistema de fechas del libro, y el emisor de registros no puede ver el libro, así que la elección de fecha base se pasa como parámetro que por defecto toma el sistema 1900 y que suministra la ruta de guardado a nivel de libro. Bajo el sistema 1900 el número serial es el valor directamente; el sistema 1904 difiere en 1462 días. El tratamiento más amplio de los seriales de fecha está en seriales de fecha, el sistema 1904 y los formatos numéricos

Si trabaja en la capa de vista y no en la de caché, los registros que describen la tabla dinámica visible quedan cubiertos en el conjunto de registros PivotTable de BIFF8, y el comportamiento por el lado del cálculo en campos calculados, items calculados y refresh. Las tres capas se envían en el componente de hoja de cálculo Delphi HotXLS, que es lo que hace posible cargar un libro legado, inspeccionar qué contiene de verdad su caché y decidir si reescribirla es seguro antes de hacerlo