HotXLS guarda cada criterio de AutoFilter BIFF8 como un record AUTOFILTER que lleva dos estructuras DOPER de 10 bytes, y el tipo de DOPER decide cómo compara Excel. Desde la v2.384.45, TXLSWorksheet.ApplyAutoFilter escribe una comparación como '>=100' como un DOPER de número IEEE, de modo que Excel coincide con celdas numéricas en lugar de comparar texto. El informe del bug que motivó el cambio era breve y desesperante: una exportación nocturna aplicaba un filtro sobre una columna de importes, el archivo abría sin queja alguna, la flecha del desplegable mostraba el criterio, y el filtro coincidía con cero filas. Nada estaba corrupto. Los bytes eran BIFF8 válidos, simplemente válidos del tipo equivocado, y esa es la clase de fallo que recorre este artículo, junto con dos errores antiguos a nivel de bytes corregidos en v2.384.18
¿Qué guarda realmente un AutoFilter BIFF8?
Un AutoFilter BIFF8 es un conjunto de tres tipos de record, no uno solo, y solo el record por campo guarda criterios. AUTOFILTERINFO ($009D, [MS-XLS] §2.4.8) anota cuántas columnas cubre el rango del filtro. FILTERMODE ($009B) es un marcador sin cuerpo que HotXLS emite solo cuando al menos un campo tiene un criterio activo. Después, cada campo activo recibe su propio record AUTOFILTER ($009E, §2.4.6): un índice de campo base cero, una palabra grbit cuyos dos bits bajos son wJoin, dos DOPERs de exactamente 10 bytes cada uno, y una cola opcional que guarda los caracteres de cualquier DOPER de cadena. El índice de campo es base cero en disco aunque ApplyAutoFilter numera los campos desde 1, algo que importa la primera vez que vas a cazar un record en un volcado hexadecimal. El primer byte de cada DOPER, vt, dice qué clase de operando viene detrás:
$04es un double IEEE 754 almacenado en los 8 bytes restantes, que es como Excel guarda una comparación numérica$06es una cadena cuya longitud vive en un único bytecch, con los caracteres propiamente dichos empujados a la cola del record$08es un valor Bes, un Boolean o código de error empaquetado en dos bytes$0Cy$0Eno llevan operando y significan coincidir con todos los vacíos y coincidir con todos los no vacíos
El segundo byte, grbitSgn, guarda la comparación: 1 a 6 se corresponden con <, =, <=, >, <> y >=. HotXLS mantiene ambos bytes visibles a posteriori mediante AutoFilterColumns, cuyos elementos exponen Criteria1 y Criteria2 como objetos TXLSAutofilterDOPER con DataType, grbitSgn y Value, así que puedes hacer assert sobre lo que se va a escribir en vez de andar adivinando
¿Por qué un filtro '>=100' no coincidía con ninguna fila en Excel?
El filtro no coincidía con nada porque el operando se guardaba como texto, y Excel compara un DOPER de cadena contra la celda como texto. Antes de v2.384.45, CreateFilterDoper en lxFilter.pas recortaba correctamente el prefijo >= y fijaba el signo a 6, y luego construía siempre un DOPER vtString con los caracteres 100. Una celda numérica con 250 nunca satisface una comparación de texto contra "100", así que caían todas las filas. Sin excepción, sin diagnóstico, sin aviso de reparación por parte de Excel. La regla desde v2.384.45 es estrecha a propósito: si el criterio empieza con un operador de comparación y el resto parsea como número bajo reglas de cultura invariante, HotXLS escribe un DOPER vtIEEENumber con el mismo signo. Un valor desnudo sin operador conserva la forma de cadena, porque así es como el propio Excel guarda un elemento escogido de la lista desplegable
var
Wb: IXLSWorkbook;
Sh: TXLSWorksheet;
Doper: TXLSAutofilterDOPER;
begin
Wb := TXLSWorkbook.Create;
Sh := Wb.Sheets.Add;
Sh.Cells[1, 1].Value := 'Region';
Sh.Cells[1, 2].Value := 'Amount';
Sh.Cells[2, 1].Value := 'North';
Sh.Cells[2, 2].Value := 250;
// Campo 2 = segunda columna de A1:B100 (base 1 en el lado de la API)
Sh.ApplyAutoFilter('A1:B100', 2, '>=100');
Doper := Sh.AutoFilterColumns.Find(2).Criteria1;
// v2.384.45+: DataType = 4 (número IEEE), grbitSgn = 6 (>=)
// Antes del fix: DataType = 6 (cadena), que no coincidía con nada
Assert(Doper.DataType = 4);
Wb.SaveAs('orders.xls');
end;
El parseo es donde viven los bordes afilados restantes. El operando pasa por TryStrToFloat con punto como separador decimal, de modo que '>=1.5' se convierte en número y '>=1,5' se queda como DOPER de cadena y de nuevo no coincide con nada en silencio, digan lo que digan las opciones regionales de Windows. Las fechas son la misma trampa con otro disfraz: '>=2026-01-01' no es un número, así que se escribe como texto, mientras Excel guarda las celdas de fecha como números de serie. Para igualdad sobre un número, tanto '=100' como un Variant numérico como 100 producen un DOPER IEEE con signo 2, mientras que la cadena desnuda '100' produce una coincidencia de texto. Construye los operandos numéricos en código en vez de formatearlos para humanos:
var
Fmt: TFormatSettings;
Since: TDateTime;
begin
Fmt := TFormatSettings.Create;
Fmt.DecimalSeparator := '.';
// Umbral con decimales: formatea siempre con punto
Sh.ApplyAutoFilter('A1:D500', 3, '>' + FloatToStr(1499.5, Fmt));
// Fechas: compara contra el número de serie que Excel guarda en la celda.
// Un TDateTime de Delphi equivale al serial del sistema 1900 para fechas posteriores a marzo de 1900
Since := EncodeDate(2026, 1, 1);
Sh.AutoFilterColumns.SetFieldCriteria(4, '>=' + IntToStr(Trunc(Since)),
xlAnd, Unassigned);
end;
¿Cómo unen AND y OR dos condiciones?
Los bits wJoin del grbit de AUTOFILTER valen 0 para AND y 1 para OR, y HotXLS tenía esas dos constantes invertidas hasta v2.384.18. Un filtro tipo between como al menos 100 y por debajo de 500 se guardaba como al menos 100 o por debajo de 500, lo que en la práctica coincide con todos los números y aparenta que el filtro sencillamente no se aplicó. Las constantes públicas de operador añaden un segundo peligro de portado. En HotXLS, xlAnd es 0 y xlOr es 1, mientras que la automatización de Excel los numera 1 y 2. XlAutoFilterOperator es un Byte corriente, así que código traducido de una macro VBA con números literales compila sin rechistar, y un 1 literal que significaba AND en COM ahora significa OR. Usa las constantes con nombre y el problema no puede surgir:
// Importe entre 100 (incluido) y 500 (excluido)
Sh.ApplyAutoFilter('A1:D500', 3, '>=100', xlAnd, '<500');
with Sh.AutoFilterColumns.Find(3) do
begin
Assert(Operator = xlAnd); // wJoin = 0 en disco
Assert(Criteria2.grbitSgn = 1); // 1 = menor que
end;
Booleanos, vacíos y el techo de 255 caracteres
Un criterio Boolean se guarda como un valor Bes ([MS-XLS] §2.5.10), y Bes pone primero el byte de valor bBoolErr y después el flag fError. HotXLS los escribía en el orden contrario antes de v2.384.18, de modo que un filtro para TRUE metía un 1 en el flag de error y Excel leía el criterio como un código de error. Escritor y lector estaban intercambiados a la vez, que es por lo que HotXLS hacía round-trip de sus propios archivos sin queja mientras Excel discrepaba, un recordatorio de que un round-trip autoconsistente no prueba nada sobre la conformidad con la especificación. Los vacíos no necesitan operando alguno: pasar '=' por sí solo produce un DOPER de coincidir con todos los vacíos ($0C) y '<>' por sí solo un DOPER de coincidir con todos los no vacíos ($0E)
Los criterios de cadena chocan con un límite duro en la disposición del DOPER. El campo de longitud cch es un único byte, así que un operando de cadena no puede pasar de 255 caracteres, y CreateFilterDoper trunca el texto más largo tras quitar el operador en vez de dejar que el byte de longitud dé la vuelta y desincronice la cola del record. El truncado es silencioso, y un filtro sobre una columna de descripciones largas puede coincidir de forma distinta al texto completo que pasaste. En BIFF8 la cola guarda cada cadena como un flag de un byte seguido de unidades de código UTF-16, y el tamaño de record declarado debe contar esos bytes exactamente, la misma disciplina de contabilidad que cubre cómo se desvían las declaraciones de longitud de record BIFF en un escritor XLS en Delphi
¿Por qué una segunda llamada a ApplyAutoFilter borra la primera?
Cada llamada a ApplyAutoFilter redefine el rango de filtro entero, así que solo sobrevive el criterio de la última llamada. Internamente llama a SetAutoFilter, que limpia todos los campos antes de reconstruir el rango, algo correcto para una columna y sorprendente para dos. Para filtrar varias columnas, llama a ApplyAutoFilter una vez para establecer el rango y el primer criterio, y añade los demás vía AutoFilterColumns.SetFieldCriteria, que deja en paz el rango y los otros campos. Ambos caminos ignoran sin lanzar un número de campo fuera del rango, así que verifica leyendo de vuelta, idealmente tras reabrir el archivo guardado:
Sh.ApplyAutoFilter('A1:D500', 1, 'North'); // rango + campo 1
Sh.AutoFilterColumns.SetFieldCriteria(3, '>=100', xlAnd, Unassigned);
Sh.AutoFilterColumns.SetFieldCriteria(4, True, xlAnd, Unassigned);
Wb.SaveAs('orders.xls');
Wb := TXLSWorkbook.Create;
Wb.Open('orders.xls');
Assert(Wb.Sheets[1].AutoFilterColumns.Find(1).Active);
Assert(Wb.Sheets[1].AutoFilterColumns.Find(3).Criteria1.DataType = 4);
Ten presente que el record AUTOFILTER es una definición almacenada: HotXLS escribe los criterios y no los evalúa en la hoja XLS clásica, así que un pipeline que necesita las filas coincidentes en el servidor tiene que calcularlas allí por su cuenta, mientras que la fachada XLSX ofrece evaluación a nivel de fila como se muestra en validación de datos, AutoFilter y tablas en Delphi con HotXLS. Cuando Excel ya oculta filas, cualquier total bajo el rango depende de cómo tratan SUBTOTAL y AGGREGATE las filas ocultas y filtradas, que es el siguiente sitio donde un filtro numérico que en silencio no coincide con nada aparece como una cifra equivocada
HotXLS lee y escribe workbooks BIFF8 XLS y XLSX de forma nativa desde Delphi y C++Builder, incluidos criterios de AutoFilter con DOPERs numéricos, Boolean y AND/OR que Excel evalúa como corresponde. Consulta el componente de hojas de cálculo HotXLS para Delphi para funciones, ediciones y una descarga de prueba