El substream PivotCache de BIFF almacena el dataset cacheado de un PivotTable separado de la vista que lo muestra, y HotXLS lee y escribe ese substream inspeccionando los cuerpos de los records en lugar de confiar en los números de record. Esa distinción es toda la historia: el mismo número de record carga dos layouts de cuerpo incompatibles según qué writer produjo el archivo, así que el lector decide el framing a partir del primer cuerpo de record que ve
Usted se encuentra con esta capa en el momento en que un PivotTable tiene que sobrevivir a un round trip. Una vista pivot sin su caché es una cáscara, y Excel reconstruirá el caché desde el rango fuente cuando abra el archivo, lo cual está bien justo hasta el punto donde el rango fuente ya no existe, los datos se pegaron desde una consulta, o el workbook es un cierre archivado que no debe cambiar cuando alguien lo abra
Dos estructuras, dos lugares en el archivo
Los datos cacheados y la definición del caché viven en partes distintas del workbook, y confundirlos es lo primero que hay que evitar. Los records cacheados 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 vive en cambio en los workbook globals, como PIVOTCACHEDEFINITION = SXStreamID SXVS [SXSRC] [SXADDLCACHE] (§2.1.7.20.3), posicionada después de los records de formato y antes de los records BoundSheet y Country. Así, un solo caché queda descrito en dos lugares separados por cientos de records, y el enlace entre ellos es un identificador de stream que tiene que coincidir en tres ubicaciones a la vez
Cada caché pertenece a un stream bajo _SX_DB_CUR cuyo nombre es la grafía hexadecimal mayúscula de cuatro dígitos de su identificador. SXStreamID.idStm, el campo idstm repetido en el encabezado SXDB, y ese nombre de stream tienen que coincidir todos. Cuando asigne un identificador nuevo, reserve primero cada número ya leído del archivo, o un caché nuevo puede reclamar un número que pertenece a un caché viejo al que el lector aún no llegó
Un identificador más atrapa a la gente. El valor iCache de una vista pivot es la posición base cero del SXStreamID correspondiente en la secuencia global, no un identificador de caché que usted pueda elegir. Al escribir tiene que mapearse desde el objeto caché hacia su posición real de salida, y las vistas existentes tienen que renumerarse junto con él, o actualizar un caché apunta silenciosamente una vista hacia otro distinto
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; // confiar en los records cacheados
Cache.SaveData := True;
Field := Cache.AddField('Region', xlpcftString);
V.ValueType := xlpcftString;
V.StrValue := 'North';
Field.FindOrAddItem(V);
Cache.SetRecordCount(0); // limpiar, y luego dimensionar la grilla
Cache.SetRecordCount(500);
Book.StorePivotCaches;
finally
Book.Free;
end;
end;
El doble SetRecordCount no es superstición. RecordCount es una escritura de propiedad plana que no asigna, y la ruta interna de crecimiento solo inicializa las filas recién agregadas, así que un caché cuyo conteo se fijó por la ruta del encabezado puede terminar con una grilla 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 grilla, y tiene que ocurrir después de que cada campo haya sido agregado, porque el ancho de fila sale del conteo de campos
¿Por qué un número de record no puede decirle el layout del cuerpo?
Porque los números de record y los layouts de cuerpo cambiaron en momentos distintos, así que el mapeo entre ellos no es una función. Un número del set legado solo aparece jamás en archivos de writers viejos, lo que lo vuelve una señal confiable en una dirección. Otro número es genuinamente ambiguo: aparece tanto en archivos correctos como en un rango de versiones intermedias que usaban el número nuevo con el layout de cuerpo viejo
Por eso el framing tiene que decidirse desde el cuerpo, y una vez por substream de caché en lugar de por record. HotXLS fija el dialecto a partir de la longitud del primer record SXDBB de cada substream. En el framing de la especificación, un SXDBB contiene exactamente un record de caché, así que su longitud iguala un ancho de fila. En el framing empaquetado viejo, el primer record 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
Cuando no difieren, el lector toma la lectura de la especificación, bajo el principio de que los archivos escritos por Excel superan en número a los archivos escritos por una build intermedia. Ese punto ciego es angosto por construcción y, cuando ocurre, el archivo en sí se sigue reproduciendo byte a byte. Solo los índices tipados expuestos a quienes llaman resultan afectados
El ancho del índice vive en otro record
SXDBB (§2.4.276) carga un índice por campo de caché cuyo flag de valores distintos esté activo, en orden de campos, y el ancho de cada índice se decide en otra parte: el record de campo SXFDB correspondiente (§2.4.283) declara un flag de short items, y ese flag dice si el índice ocupa dos bytes o uno. Dos records, un contrato implícito, y una sola oración en la especificación conectándolos
Ese acoplamiento es exactamente donde una codificación casera se equivoca. Un writer 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 writer acababa de declarar en SXFDB. Un campo con tres valores distintos quedaba descrito con un byte de ancho en un record y ocupaba dos bits en el otro. El arreglo no fue corregir la aritmética sino extraer la decisión de ancho a una función que ambos emisores llaman, para que los dos records ya no puedan separarse. Es la misma clase de defecto descrita en drift entre declaración de longitud y cuerpo real en records BIFF, donde un tamaño declarado y un cuerpo real se separan
La consecuencia de no leer estos records en absoluto vale la pena deletrearla, porque es fácil subestimarla. Cuando el lector saltaba los índices de record, cada caché cargado desde un archivo reportaba índice cero para cada campo de cada fila, lo que significa que cada fila apuntaba al primer valor de cada campo. Eso no es meramente introspección reducida: la ruta de evaluación pivot y la ruta de llenado caché-a-celda consumen ambas esa grilla. Y un test de round-trip no puede detectarlo, porque un caché que sigue en raw replay se escribe de vuelta desde sus bytes originales
// Los flags de provenance le dicen qué está sosteniendo 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);
// Re-emitir solo es lossless cuando cada record tiene un modelo aquí
if Cache.CanUpgradeFraming then
Writeln('safe to rewrite with the current emitters');
end;
¿Cuándo es lossless reescribir un caché?
Solo cuando tres condiciones se sostienen juntas, y CanUpgradeFraming es la única propiedad que responde la pregunta. El caché debe seguir en raw replay, el substream debe estar en uno de los framings que esta biblioteca escribió incorrectamente en el pasado, y el lector debe haber construido un modelo tipado completo de cada record dentro de él. Un caché que escribió Excel nunca califica, porque su substream trae records para los que HotXLS no tiene modelo, y re-emitir desde el modelo los perdería
La prueba de completitud es más estricta de lo que parece a primera vista. Un record que el lector conservó solo como bytes opacos marca el modelo como incompleto. También un conteo declarado de records de fórmula que el emisor no puede reproducir, porque re-emitir reescribiría una declaración de varios records de fórmula como una declaración de ninguno, y un valor en el archivo que no se puede reproducir equivale a un record que no se puede reproducir
El conservadurismo deliberado atraviesa también al writer. Los índices se hacen clamp al rango legal en lugar de codificarse como un sentinel fuera de banda, porque la especificación define un índice dentro de la secuencia de valores distintos y nada más, y una celda vacía es en sí misma un valor en esa secuencia. Un cuerpo de record de caché que exceda el techo de record BIFF no se escribe en absoluto, lo que requeriría miles de campos de caché y de todos modos es inalcanzable dentro del límite de columnas de BIFF8; el fallback es que Excel refresca desde el rango fuente, que es comportamiento definido y no un archivo corrupto
Las fechas cargan la última dependencia cruzada de records. La conversión serial-a-fecha depende del sistema de fechas del workbook, y el emisor de records no puede ver el workbook, así que la elección de fecha base se pasa como parámetro que toma por default el sistema 1900 y es suministrado por la ruta de guardado a nivel workbook. Bajo el sistema 1900 el número serial es el valor directo; el sistema 1904 difiere por 1462 días. El tratamiento más amplio de los seriales de fecha está en seriales de fecha, el sistema 1904 y formatos numéricos
Si usted trabaja en la capa de vista y no en la de caché, los records que describen el pivot visible están cubiertos en el set de records PivotTable de BIFF8, y el comportamiento del lado de cálculo en campos calculados, items calculados y refresh. Las tres capas vienen en el HotXLS Delphi spreadsheet component, que es lo que hace posible cargar un workbook legado, inspeccionar qué contiene realmente su caché, y decidir si reescribirlo es seguro antes de hacerlo