HotXLS зберігає кожен критерій BIFF8 AutoFilter у записі AUTOFILTER, що несе дві 10-байтові структури DOPER, і саме тип DOPER вирішує, як порівнюватиме Excel. Починаючи з v2.384.45, TXLSWorksheet.ApplyAutoFilter записує порівняння на кшталт '>=100' як числовий DOPER IEEE, тож Excel зіставляє числові комірки, а не порівнює текст. Звіт про баг, що спонукав зміну, був коротким і бісивим: нічний експорт навішував фільтр на колонку сум, файл відкривався без жодної скарги, стрілка випадаючого списку показувала критерій — а фільтр повертав нуль рядків. Нічого не було пошкоджено. Байти були валідним BIFF8, просто валідним не того сорту, і саме цей клас збоїв розбирає ця стаття разом із двома старішими байтовими помилками, виправленими у v2.384.18
Що насправді зберігає BIFF8 AutoFilter?
BIFF8 AutoFilter — це набір із трьох типів записів, а не один запис, і лише покомпонентний запис поля тримає критерії. AUTOFILTERINFO ($009D, [MS-XLS] §2.4.8) фіксує, скільки колонок покриває діапазон фільтра. FILTERMODE ($009B) — маркер без тіла, який HotXLS емітує, лише коли щонайменше одне поле має активний критерій. Далі кожне активне поле отримує власний запис AUTOFILTER ($009E, §2.4.6): індекс поля з нуля, слово grbit, чиї два молодші біти — це wJoin, два DOPER ровно по 10 байтів кожен і опціональний хвіст, що тримає символи будь-якого рядкового DOPER. Індекс поля на диску рахується від нуля, хоча ApplyAutoFilter нумерує поля від 1, і це відчутно вперше, коли ви підете шукати запис у hex-дампі. Перший байт кожного DOPER, vt, каже, який операнд іде далі:
$04— це IEEE 754 double у решті 8 байтів, саме так Excel зберігає числове порівняння$06— це рядок, чия довжина живе в одному байтіcch, а самі символи заштовхані в хвіст запису$08— це значення Bes, Boolean або код помилки, запаковані у два байти$0Cі$0Eне несуть операнда й означають збіг з усіма порожніми та збіг з усіма непорожніми
Другий байт, grbitSgn, тримає порівняння: 1–6 відображаються на <, =, <=, >, <> і >=. HotXLS тримає обидва байти видимими постфактум через AutoFilterColumns, чиї елементи виставляють Criteria1 і Criteria2 як об'єкти TXLSAutofilterDOPER з DataType, grbitSgn і Value, тож ви можете асертувати на те, що буде записано, замість вгадувати
Чому фільтр '>=100' не зачепив жодного рядка в Excel?
Фільтр не зібрав нічого, бо операнд було збережено як текст, а Excel порівнює рядковий DOPER із коміркою як текст. До v2.384.45 CreateFilterDoper у lxFilter.pas коректно здирав префікс >= і ставив знак у 6, але завжди будував DOPER vtString із символами 100. Числова комірка зі значенням 250 ніколи не задовольняє текстове порівняння з "100", тож усі рядки відсіювалися. Жодного винятку, жодної діагностики, жодної пропозиції ремонту від Excel. Правило від v2.384.45 навмисно вузьке: якщо критерій починається з оператора порівняння, а решта парситься як число за правилами інваріантної культури, HotXLS пише DOPER vtIEEENumber із тим самим знаком. Голе значення без оператора лишається рядковим, бо саме так Excel сам зберігає елемент, обраний із випадаючого списку
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;
// Поле 2 = друга колонка діапазону A1:B100 (на боці API нумерація з 1)
Sh.ApplyAutoFilter('A1:B100', 2, '>=100');
Doper := Sh.AutoFilterColumns.Find(2).Criteria1;
// v2.384.45+: DataType = 4 (число IEEE), grbitSgn = 6 (>=)
// До виправлення: DataType = 6 (рядок), що не збігався ні з чим
Assert(Doper.DataType = 4);
Wb.SaveAs('orders.xls');
end;
Розбір — місце, де живуть решта гострих кутів. Операнд проходить через TryStrToFloat із крапкою як десятковим розділювачем, тож '>=1.5' стає числом, а '>=1,5' лишається рядковим DOPER і знову мовчки не збігається ні з чим, що б там не казала локаль Windows. Дати — та сама пастка в іншому костюмі: '>=2026-01-01' не є числом, тож записується як текст, тоді як Excel тримає комірки дат як серійні номери. Для рівності на числі і '=100', і числовий Variant на кшталт 100 дають DOPER IEEE зі знаком 2, а голий рядок '100' дає текстовий збіг. Будуйте числові операнди в коді, а не форматуйте їх для людей:
var
Fmt: TFormatSettings;
Since: TDateTime;
begin
Fmt := TFormatSettings.Create;
Fmt.DecimalSeparator := '.';
// Поріг із дробом: завжди форматуйте з крапкою
Sh.ApplyAutoFilter('A1:D500', 3, '>' + FloatToStr(1499.5, Fmt));
// Дати: порівнюйте із серійним номером, який Excel тримає в комірці.
// Delphi TDateTime дорівнює серійному номеру системи 1900 для дат після березня 1900
Since := EncodeDate(2026, 1, 1);
Sh.AutoFilterColumns.SetFieldCriteria(4, '>=' + IntToStr(Trunc(Since)),
xlAnd, Unassigned);
end;
Як AND і OR з'єднують дві умови?
Біти wJoin у grbit запису AUTOFILTER — це 0 для AND і 1 для OR, і HotXLS тримав ці дві константи навпаки до v2.384.18. Фільтр типу «між», скажімо щонайменше 100 і менше 500, зберігався як щонайменше 100 або менше 500, що на практиці збігається з кожним числом і виглядає так, ніби фільтр просто не застосувався. Публічні константи операторів додають другу небезпеку портування. У HotXLS xlAnd — це 0, а xlOr — це 1, тоді як Excel automation нумерує їх 1 і 2. XlAutoFilterOperator — простий Byte, тож код, перекладений з макросу VBA із літеральними числами, компілюється чисто, а літерал 1, що означав AND у COM, тепер означає OR. Користуйтеся іменованими константами — і проблема не виникне:
// Сума від 100 (включно) до 500 (не включаючи)
Sh.ApplyAutoFilter('A1:D500', 3, '>=100', xlAnd, '<500');
with Sh.AutoFilterColumns.Find(3) do
begin
Assert(Operator = xlAnd); // wJoin = 0 на диску
Assert(Criteria2.grbitSgn = 1); // 1 = менше
end;
Boolean, порожні комірки і стеля в 255 символів
Boolean-критерій зберігається як значення Bes ([MS-XLS] §2.5.10), і Bes ставить байт значення bBoolErr першим, а прапорець fError — другим. HotXLS писав їх у зворотному порядку до v2.384.18, тож фільтр за TRUE клав 1 у прапорець помилки, і Excel читав критерій як код помилки. Writer і reader були переставлені разом, тому HotXLS ганяв round trip по власних файлах без скарг, поки Excel був не згоден, — нагадування, що самозгодний round trip не доводить нічого про відповідність специфікації. Порожні комірки взагалі не потребують операнда: передача '=' сама по собі дає DOPER «збіг з усіма порожніми» ($0C), а '<>' сама по собі — DOPER «збіг з усіма непорожніми» ($0E)
Рядкові критерії впираються в жорстку межу розкладки DOPER. Поле довжини cch — один байт, тож рядковий операнд не може перевищити 255 символів, і CreateFilterDoper обрізає довший текст після здирання оператора, замість дозволити байту довжини переповнитися і розсинхронізувати хвіст запису. Обрізання мовчазне, і фільтр за довгою колонкою описів може збігатися інакше, ніж повний текст, який ви передали. У BIFF8 хвіст зберігає кожен рядок як однобайтовий прапорець, за яким ідуть кодові одиниці UTF-16, і заявлений розмір запису мусить рахувати ці байти точно — та сама бухгалтерська дисципліна, яку розбирає стаття про те, як дрейфують оголошення довжини записів BIFF у Delphi-письменнику XLS
Чому повторний виклик ApplyAutoFilter стирає перший?
Кожен виклик ApplyAutoFilter перевизначає весь діапазон фільтра, тож виживає лише критерій останнього виклику. Всередині він викликає SetAutoFilter, який чистить кожне поле перед перебудовою діапазону, і це коректно для однієї колонки й несподівано для двох. Щоб фільтрувати кілька колонок, викличте ApplyAutoFilter один раз, щоб закріпити діапазон і перший критерій, а решту додавайте через AutoFilterColumns.SetFieldCriteria, який лишає діапазон і інші поля в спокої. Обидва шляхи ігнорують номер поля поза діапазоном без підйому винятку, тож перевіряйте читанням назад, бажано після повторного відкриття збереженого файлу:
Sh.ApplyAutoFilter('A1:D500', 1, 'North'); // діапазон + поле 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);
Майте на увазі, що запис AUTOFILTER — це збережене визначення: HotXLS пише критерії, але не обчислює їх на класичному аркуші XLS, тож конвеєру, якому потрібні рядки, що збіглися, на сервері, доведеться рахувати їх там самому, тоді як фасад XLSX пропонує обчислення на рівні рядків, як показано в статті про валідацію даних, AutoFilter і таблиці HotXLS у Delphi. Щойно Excel таки приховує рядки, будь-які підсумки під діапазоном залежать від того, як SUBTOTAL і AGGREGATE поводяться з прихованими та відфільтрованими рядками, і це наступне місце, де числовий фільтр, що мовчки не збігається ні з чим, випливає як хибне число
HotXLS читає і пише книги BIFF8 XLS та XLSX нативно з Delphi і C++Builder, включно з критеріями AutoFilter із числовими, Boolean та AND/OR DOPER, які Excel обчислює як задумано. Дивіться компонент електронних таблиць HotXLS для Delphi за функціями, редакціями та пробним завантаженням