Технічна стаття

Критерії DOPER AutoFilter BIFF8 у Delphi з HotXLS

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, тож ви можете асертувати на те, що буде записано, замість вгадувати

Анатомія запису AUTOFILTER у HotXLS: індекс поля з нуля, слово grbit, чиї два молодші біти тримають wJoin, і дві 10-байтові структури DOPER, де байт vt обирає операнд — число IEEE, рядок, Bes Boolean, порожнє чи непорожнє, — поки grbitSgn кодує оператор порівняння, який застосовує Excel
Кожен запис AUTOFILTER несе два 10-байтові DOPER, і байт vt вирішує, чи порівнюватиме Excel критерій як число, текст, Boolean чи тест на порожнечу — прочитайте обидва назад через AutoFilterColumns перед збереженням

Чому фільтр '>=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;
HotXLS записує той самий критерій AutoFilter >=100 або як DOPER vtString, що не збігається з жодною числовою коміркою, або як DOPER vtIEEENumber із grbitSgn 6, який Excel обчислює числово проти суми 250, — тихий збій нульового рядка, який CreateFilterDoper виправив у v2.384.45
Байти обидва рази були валідним BIFF8 — змінився лише байт типу операнда, тому Excel відкривав файл, показував критерій у випадаючому списку і все одно повертав нуль рядків

Розбір — місце, де живуть решта гострих кутів. Операнд проходить через 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;
Розкладка бітів wJoin у HotXLS для критеріїв AutoFilter, де 0 з'єднує два DOPER через AND, а 1 — через OR; числова вісь, що показує, як переставлені до v2.384.18 константи розширювали фільтр «між» у OR, що збігається з усім, і конфлікт нумерації xlAnd/xlOr із Excel automation
Переставлені константи wJoin перетворювали фільтр «між» на такий, що збігається з кожним числом, а перекладений VBA досі компілюється, бо XlAutoFilterOperator — простий Byte: літерал 1, що означав AND у COM automation, тут означає OR

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 за функціями, редакціями та пробним завантаженням