HotXLS escribe definiciones de tabla dinámica XLSX cuyos elementos pivotField y cacheField validan contra el esquema ECMA-376 Part 1 §18.10: los atributos de axis usan los tokens ST_Axis axisRow, axisCol y axisPage, los campos del área de valores llevan dataField="1", las listas de ítems nunca están vacías, y los cache fields guardan un numFmtId numérico. Desde la v2.384.33 el lector también honra los valores por defecto del esquema que antes leía mal
Los bugs detrás de esta limpieza comparten un rasgo poco halagador: ninguno falló jamás un test. HotXLS escribía un pivot, HotXLS lo releía, cada campo aterrizaba en el axis correcto, y el suite de round-trip se mantuvo verde por años. El problema era que escritor y lector habían acordado calladamente un dialecto privado. Un pivot construido desde Delphi se veía bien para el componente que lo hizo, mientras que un chequeo contra CT_PivotField y CT_CacheField destapaba tokens de enumeración inválidos, un elemento vacío que el esquema prohíbe y flags que Excel espera pero nunca recibió. Si usted genera pivots en un servidor y se los manda a gente que los abre en Excel o se los pasa a sus propios parsers, el único contrato que cuenta es el esquema, no lo que su propio lector decida perdonar
¿Por qué los round trips de HotXLS nunca atraparon los tokens de axis equivocados?
Los round trips de HotXLS nunca atraparon los tokens de axis equivocados porque el lector aceptaba ambas escrituras. El viejo XlsxPivotAxisAttr emitía axis="rowAxis", colAxis y pageAxis, que se leen natural en inglés pero no existen en el esquema; ST_Axis define exactamente cuatro valores, axisRow, axisCol, axisPage y axisValues. Mientras tanto PivotAxisFromToken en lxPivotXml.pas matcheaba tanto el token del esquema como el inventado, así que cada self-test pasaba. El escritor ahora emite solo los tokens del esquema, y el lector sigue aceptando las escrituras viejas para que los archivos guardados por versiones anteriores de HotXLS sigan cargando con su layout intacto
<!-- antes de v2.384.33: valor ST_Axis inválido, CT_Items vacío -->
<pivotField axis="rowAxis" defaultSubtotal="1"><items count="0"></items></pivotField>
<!-- desde la v2.384.33 -->
<pivotField axis="axisRow" defaultSubtotal="1">
<items count="4"><item x="0"/><item x="1"/><item x="2" h="1"/><item t="default"/></items>
</pivotField>
¿Qué exige CT_PivotField que el viejo escritor se saltaba?
CT_PivotField exige tres cosas que el viejo BuildPivotTableXml omitía o hacía mal. Primero, un campo agregado en el área de valores debe decirlo en su propia definición con dataField="1"; el escritor ahora fija ese flag en cada campo referido por una entrada en DataFields, y no solo en la lista <dataFields>. Segundo, CT_Items necesita al menos un item, así que un campo sin ítems ya no recibe un <items count="0"> vacío y el elemento entero simplemente se omite. Tercero, cada item conserva su estado: h="1" para un item oculto (TXLSPivotItem.IsHidden) y sd="0" para detalles contraídos (IsDetailHidden), ambos de los cuales el viejo escritor soltaba en cada guardado
La parte sutil son los ítems finales de subtotal. Cuando un campo tiene ítems, Excel lista un item extra por función de subtotal después de los ítems de datos, tipeados con ST_ItemType: <item t="default"/> para el subtotal automático, y luego sum, countA, avg, max, min, product, count, stdDev, stdDevP, var y varP para los explícitos. HotXLS deriva esas entradas de TXLSPivotField.Subtotals al guardar y las cuenta dentro de items count. Los campos creados por AddPivotTable arrancan con un set Subtotals vacío, que escribe defaultSubtotal="0" y ningún ítem final, así que pida los subtotales explícitamente cuando el reporte los necesita. Note la trampa de nombres: xlpsCount mapea a countA (todas las entradas) y xlpsCountNums mapea a count (solo números)
uses
lxHandleX, lxPivot;
var
Book : TXLSXWorkbook;
Sheet : TXLSXWorksheet;
Pivot : TXLSPivotTable;
Region: TXLSPivotField;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('orders.xlsx');
Sheet := Book.Sheets[1]; // base 1, como el motor XLS
Pivot := Sheet.AddPivotTable('Data!$A$1:$D$800', 3, 6, 'RegionTotals');
if Pivot = nil then
raise Exception.Create('Bad source range or anchor');
Region := Pivot.AddRowField('Region'); // nil si no existe el campo
if Region <> nil then
Region.Subtotals := [xlpsDefault, xlpsAverage]; // -> t="default", t="avg"
Pivot.AddColumnField('Quarter');
Pivot.AddDataFieldByName('Revenue', xlpaSum); // marca Revenue con dataField="1"
Book.SaveAs('orders-pivot.xlsx');
finally
Book.Free;
end;
end;
¿Cómo lee HotXLS ahora los ítems de subtotal y los defaults del esquema?
El lector de HotXLS ahora se salta cualquier item cuyo atributo t esté presente y no sea data, porque las entradas de subtotal, total general y vacío no llevan índice de caché. Antes de la v2.384.34 esas entradas se cargaban como ítems ordinarios con CacheItemIndex en -1, así que un pivot hecho por Excel volvía con miembros fantasma que no apuntaban a ningún lado, y cualquier código que recorriera Items tenía que filtrarlos a mano. Como el escritor reconstruye las entradas finales desde Subtotals, el trabajo del lector es traducirlas a ese set, no mantenerlas como datos
El segundo fix del lector va sobre atributos ausentes. En el esquema, defaultSubtotal en CT_PivotField y containsString en CT_SharedItems tienen ambos por defecto true, y Excel los omite cuando llevan ese default. HotXLS leía un atributo faltante como false, lo que significaba que cada pivot guardado por Excel perdía calladamente su subtotal por defecto al cargar, y un cache field de texto puro se clasificaba como mixto en lugar de string. Es la imagen especular del bug del axis: un escritor que siempre deletrea cada atributo nunca ejercita el camino del default, así que solo los archivos de otro productor lo exponen
¿Por qué numFmtId="General" era inválido en los cache fields?
El valor numFmtId="General" era inválido porque ST_NumFmtId es un entero sin signo, no un nombre de formato. El viejo escritor de caché tenía esa string hard-codeada en cada cacheField, tomando prestado el nombre que los usuarios ven en el diálogo Format Cells. HotXLS ahora escribe el NumberFormat del cache field como número, que es 0 (el formato General incorporado) salvo que algo lo haya fijado. Un parser estricto que tipa atributos desde el esquema rechaza de plano el valor viejo, y esa es exactamente la clase de falla que se convierte en diálogo de reparación; el artículo sobre las reglas OPC y de markup detrás del diálogo de reparación de Excel cubre cómo se disparan esos diálogos
¿Por qué se cortaban las tablas dinámicas por debajo de la fila 65535?
Las tablas dinámicas XLSX colocadas en la fila 65536 o debajo se cortaban porque el modelo compartido de pivot guardaba FirstRow, LastRow, FirstHeaderRow, FirstDataRow y sus contrapartes de columna como Word, y el código de corrimiento de filas las recortaba con Min(.., High(Word)). Es un sobrante del registro SxView de BIFF8, donde 16 bits alcanzan, pero una hoja XLSX llega a 1.048.576 filas. Desde la v2.384.37 esas propiedades de TXLSPivotTable son Integer, los recortes desaparecieron, y solo el escritor BIFF8 angosta los valores. TXLSXWorksheet.AddPivotTable y AddPivotTableCopy ahora devuelven nil para un ancla fuera de 1..1048576 por 1..16384, o para una copia cuya extensión se saldría de la grilla
var
Pivot: TXLSPivotTable;
Check: TXLSXWorkbook;
begin
// La fila 70001 daba la vuelta al rango de 16 bits; ahora sobrevive guardado y carga
Pivot := Sheet.AddPivotTable('Data!$A$1:$D$800', 70001, 1, 'LateTotals');
if Pivot = nil then
Exit; // ancla fuera de la hoja o rango fuente irresoluble
Pivot.AddRowField('Region');
Pivot.AddDataFieldByName('Revenue', xlpaSum);
Book.SaveAs('late.xlsx');
Check := TXLSXWorkbook.Create;
try
Check.Open('late.xlsx');
Pivot := Check.Sheets[1].PivotTables.FindByName('LateTotals');
Assert((Pivot <> nil) and (Pivot.FirstRow = 70001));
finally
Check.Free;
end;
end;
El motor XLS clásico recibió el fix correspondiente en la v2.384.38. Su modelo guardaba los valores crudos base 0 de SxView y DConRef y dejaba pasar las anclas de AddPivotTable tal cual, mientras la documentación, los demos y el motor XLSX usaban todos celdas base 1 como Cells[Row, Col]. Ambos motores ahora mantienen posiciones base 1 en el modelo, el lector BIFF8 suma 1 y el escritor resta 1 en el límite del registro, así que el código que anclaba en (0, 0) debe moverse a (1, 1), porque el AddPivotTable clásico ahora devuelve nil para un ancla fuera de 1..65536 por 1..256; la llamada nueva escribe los mismos bytes que la vieja. El layout del registro no cambió y está descrito en los registros SX de BIFF8 detrás de las tablas dinámicas del .xls clásico
Valide contra el esquema, no contra su propio lector
La lección generaliza más allá de los pivots: un lector tolerante esconde violaciones del escritor, así que un round trip por su propio código prueba consistencia, no corrección. Cada bug aquí sobrevivió porque el lado tolerante y el lado defectuoso vivían en la misma librería. Los chequeos que de verdad atrapan esta clase de defecto son una validación de esquema de las partes generadas, archivos producidos por Excel pasados por su lector con atributos omitidos en sus defaults, y fixtures que fijan el token exacto en lugar del resultado parseado. Los pivots que usted construye vía el API, incluidos los calculated fields, calculated items y layouts de porcentaje del total que muestra construir y refrescar tablas dinámicas XLSX con calculated fields, reciben el XML corregido sin cambio de código, mientras que los pivots cargados de archivos de Excel siguen repitiendo sus partes originales hasta que usted los modifique
Todos estos fixes vienen en el actual componente de hojas de cálculo HotXLS para Delphi, que lee y escribe XLS, XLSX y tablas dinámicas desde Delphi y C++Builder sin Excel ni automatización COM en la máquina