Artículo técnico

Criterios DOPER de AutoFilter BIFF8 en Delphi con HotXLS

HotXLS guarda cada criterio de AutoFilter BIFF8 como un registro 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, así que Excel matchea celdas numéricas en lugar de comparar texto. El reporte de bug que motivó el cambio era corto y exasperante: un export nocturno aplicaba un filtro sobre una columna de montos, el archivo abría sin quejarse, la flecha del dropdown mostraba el criterio, y el filtro matcheaba cero filas. Nada estaba corrupto. Los bytes eran BIFF8 válido, solo del tipo equivocado de válido, y esa es la clase de falla que este artículo recorre, junto con dos errores antiguos a nivel de bytes corregidos en la v2.384.18

¿Qué guarda realmente un AutoFilter de BIFF8?

Un AutoFilter de BIFF8 es un conjunto de tres tipos de registro, no uno solo, y solo el registro 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 registro 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 string. El índice de campo es base cero en disco aunque ApplyAutoFilter numera los campos desde 1, algo que importa la primera vez que sale usted a cazar un registro en un hex dump. El primer byte de cada DOPER, vt, dice qué clase de operando viene después:

  • $04 es un double IEEE 754 guardado en los 8 bytes restantes, que es como Excel guarda una comparación numérica
  • $06 es un string cuya longitud vive en un solo byte cch, con los caracteres mismos empujados a la cola del registro
  • $08 es un valor Bes, un Boolean o código de error empaquetado en dos bytes
  • $0C y $0E no llevan operando y significan match con todas las celdas en blanco y con todas las que no lo están

El segundo byte, grbitSgn, guarda la comparación: 1 a 6 se mapean a <, =, <=, >, <> y >=. HotXLS mantiene ambos bytes visibles después del hecho vía AutoFilterColumns, cuyos ítems exponen Criteria1 y Criteria2 como objetos TXLSAutofilterDOPER con DataType, grbitSgn y Value, así que puede hacer assert sobre lo que se va a escribir en lugar de adivinar

Anatomía del registro AUTOFILTER de HotXLS que muestra el índice de campo base cero, la palabra grbit cuyos dos bits bajos guardan wJoin, y dos estructuras DOPER de 10 bytes cuyo byte vt selecciona un operando de número IEEE, string, Boolean Bes, celda en blanco o no en blanco, mientras grbitSgn codifica el operador de comparación que aplica Excel
Cada registro AUTOFILTER lleva dos DOPERs de 10 bytes, y el byte vt decide si Excel compara un criterio como número, texto, Boolean o prueba de celda en blanco — lea ambos de vuelta vía AutoFilterColumns antes de guardar

¿Por qué un filtro '>=100' no matcheó ninguna fila en Excel?

El filtro no matcheó nada porque el operando quedó guardado como texto, y Excel compara un DOPER de string contra la celda como texto. Antes de la v2.384.45, CreateFilterDoper en lxFilter.pas quitaba bien el prefijo >= y fijaba el signo en 6, pero siempre construía un DOPER vtString con los caracteres 100. Una celda numérica con 250 jamás satisface una comparación de texto contra "100", así que todas las filas caían. Sin excepción, sin diagnóstico, sin aviso de reparación de parte de Excel. La regla desde la v2.384.45 es angosta a propósito: si el criterio arranca con un operador de comparación y el resto parsea como número bajo reglas invariantes de cultura, HotXLS escribe un DOPER vtIEEENumber con el mismo signo. Un valor sin operador conserva la forma de string, porque así es como el propio Excel guarda un ítem elegido de la lista del dropdown

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 del lado del 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 (string), que no matcheaba nada
  Assert(Doper.DataType = 4);

  Wb.SaveAs('orders.xls');
end;
HotXLS escribe el mismo criterio de AutoFilter >=100 como un DOPER vtString que no matchea celdas numéricas o como un DOPER vtIEEENumber con grbitSgn 6 que Excel evalúa numéricamente contra el monto 250, que es la falla silenciosa de cero filas que CreateFilterDoper corrigió en la v2.384.45
Los bytes eran BIFF8 válido en ambos casos — solo cambió el byte del tipo de operando, por eso Excel abrió el archivo, mostró el criterio en el dropdown y aun así matcheó cero filas

El parseo es donde viven las aristas filosas restantes. El operando pasa por TryStrToFloat con punto como separador decimal, así que '>=1.5' se vuelve número y '>=1,5' se queda como DOPER de string y otra vez no matchea nada en silencio, diga lo que diga la configuración regional 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 seriales. Para igualdad sobre un número, tanto '=100' como un Variant numérico tal como 100 producen un DOPER IEEE con signo 2, mientras que el string pelado '100' produce un match de texto. Arme los operandos numéricos en código en lugar de formatearlos para humanos:

var
  Fmt: TFormatSettings;
  Since: TDateTime;
begin
  Fmt := TFormatSettings.Create;
  Fmt.DecimalSeparator := '.';

  // Umbral con fracción: formatee siempre con punto
  Sh.ApplyAutoFilter('A1:D500', 3, '>' + FloatToStr(1499.5, Fmt));

  // Fechas: compare contra el número serial 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 son 0 para AND y 1 para OR, y HotXLS tenía esas dos constantes invertidas hasta la v2.384.18. Un filtro estilo between como al menos 100 y menor que 500 se guardaba como al menos 100 o menor que 500, lo que en la práctica matchea todos los números y aparenta que el filtro simplemente no se aplicó. Las constantes públicas de operador suman un segundo peligro de porteo. 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 común, así que código traducido de una macro VBA con números literales compila sin reclamos, y un literal 1 que significaba AND en COM ahora significa OR. Use las constantes nombradas y el problema no puede aparecer:

// Monto entre 100 (inclusive) y 500 (exclusive)
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;
Distribución de bits wJoin de HotXLS para criterios de AutoFilter donde 0 une dos DOPERs con AND y 1 con OR, una recta numérica que muestra cómo las constantes invertidas previas a la v2.384.18 ensancharon un filtro between a un OR que matchea todo, y el choque de numeración de xlAnd y xlOr con la automatización de Excel
Las constantes wJoin invertidas convirtieron un filtro between en uno que matchea todos los números, y el VBA traducido sigue compilando porque XlAutoFilterOperator es un Byte común — un literal 1 que significaba AND bajo automatización COM significa OR aquí

Booleans, celdas en blanco 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 la bandera fError. HotXLS los escribía en el orden contrario antes de la v2.384.18, de modo que un filtro por TRUE ponía 1 en la bandera de error y Excel leía el criterio como un código de error. Escritor y lector estaban intercambiados en espejo, que es por qué HotXLS hacía round-trip de sus propios archivos sin quejarse mientras Excel discrepaba, un recordatorio de que un round-trip autoconsistente no prueba nada sobre conformidad con la especificación. Las celdas en blanco no necesitan operando para nada: pasar '=' solo produce un DOPER de match con todos los vacíos ($0C) y '<>' solo un DOPER de match con los no vacíos ($0E)

Los criterios de string chocan con un límite duro en el layout del DOPER. El campo de longitud cch es de un solo byte, así que un operando de string no puede superar los 255 caracteres, y CreateFilterDoper trunca el texto más largo después de quitar el operador en lugar de dejar que el byte de longitud dé la vuelta y desincronice la cola del registro. El truncado es silencioso, y un filtro sobre una columna de descripciones largas puede matchear distinto del texto completo que usted pasó. En BIFF8 la cola guarda cada string como una bandera de un byte seguida de unidades de código UTF-16, y el tamaño declarado del registro debe contar esos bytes con exactitud, la misma disciplina de contabilidad que cubre cómo se desvían las declaraciones de longitud de registro BIFF en un escritor XLS de Delphi

¿Por qué una segunda llamada a ApplyAutoFilter borra la primera?

Cada llamada a ApplyAutoFilter redefine todo el rango del filtro, así que solo sobrevive el criterio de la última llamada. Internamente llama a SetAutoFilter, que limpia cada campo antes de reconstruir el rango, y eso es correcto para una columna y sorprendente para dos. Para filtrar varias columnas, llame a ApplyAutoFilter una vez para establecer el rango y el primer criterio, y después sume los demás vía AutoFilterColumns.SetFieldCriteria, que deja el rango y los otros campos tranquilos. Ambos caminos ignoran un número de campo fuera del rango sin lanzar, así que verifique 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);

Tenga presente que el registro 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 que matchean del lado del servidor tiene que computarlas por su cuenta allá, mientras que la fachada XLSX ofrece evaluación a nivel de fila como se muestra en validación de datos, AutoFilter y tablas de HotXLS en Delphi. Una vez que Excel sí oculta filas, cualquier total debajo del rango depende de cómo tratan SUBTOTAL y AGGREGATE las filas ocultas y filtradas, que es el siguiente lugar donde un filtro numérico que matchea nada en silencio aparece como un número equivocado

HotXLS lee y escribe libros BIFF8 XLS y XLSX en forma nativa desde Delphi y C++Builder, incluidos criterios de AutoFilter con DOPERs numéricos, Booleanos y AND/OR que Excel evalúa como corresponde. Vea el componente de hojas de cálculo HotXLS para Delphi para funcionalidades, ediciones y una descarga de prueba