Artykuł techniczny

Kryteria DOPER AutoFiltera BIFF8 w Delphi z HotXLS

HotXLS zapisuje każde kryterium AutoFiltera BIFF8 jako rekord AUTOFILTER niosący dwie 10-bajtowe struktury DOPER, a typ DOPERa decyduje o tym, jak Excel porównuje. Od v2.384.45 TXLSWorksheet.ApplyAutoFilter zapisuje porównanie typu '>=100' jako DOPER liczbowy IEEE, więc Excel dopasowuje komórki liczbowe zamiast porównywać tekst. Zgłoszenie błędu, które wymusiło tę zmianę, było krótkie i irytujące: nocny eksport nakładał filtr na kolumnę kwot, plik otwierał się bez protestu, strzałka listy rozwijanej pokazywała kryterium, a filtr łapał zero wierszy. Nic nie było uszkodzone. Bajty były poprawnym BIFF8, tylko niewłaściwego rodzaju poprawnym, i właśnie tej klasie awarii przyglądamy się w tym artykule, razem z dwoma starszymi błędami na poziomie bajtów naprawionymi w v2.384.18

Co faktycznie zapisuje AutoFilter BIFF8?

AutoFilter BIFF8 to zestaw trzech typów rekordów, nie jeden, i tylko rekord per pole trzyma kryteria. AUTOFILTERINFO ($009D, [MS-XLS] §2.4.8) zapisuje, ile kolumn obejmuje zakres filtra. FILTERMODE ($009B) to marker bez treści, który HotXLS wysyła tylko wtedy, gdy co najmniej jedno pole ma aktywne kryterium. Potem każde aktywne pole dostaje własny rekord AUTOFILTER ($009E, §2.4.6): indeks pola liczony od zera, słowo grbit, którego dwa najniższe bity to wJoin, dwa DOPERy po dokładnie 10 bajtów każdy i opcjonalny ogon trzymający znaki ewentualnego DOPERa tekstowego. Indeks pola jest na dysku liczony od zera, choć ApplyAutoFilter numeruje pola od 1, co boli przy pierwszym polowaniu na rekord w zrzucie hex. Pierwszy bajt każdego DOPERa, vt, mówi, jaki operand przychodzi dalej:

  • $04 to double IEEE 754 zapisany w pozostałych 8 bajtach, czyli tak Excel trzyma porównanie liczbowe
  • $06 to łańcuch, którego długość mieszka w pojedynczym bajcie cch, a same znaki lądują w ogonie rekordu
  • $08 to wartość Bes, czyli Boolean albo kod błędu spakowany w dwa bajty
  • $0C i $0E nie niosą operandu i znaczą odpowiednio: dopasuj wszystkie puste i dopasuj wszystkie niepuste

Drugi bajt, grbitSgn, trzyma porównanie: wartości od 1 do 6 mapują się na <, =, <=, >, <> i >=. HotXLS pokazuje oba bajty po fakcie przez AutoFilterColumns, którego elementy wystawiają Criteria1 i Criteria2 jako obiekty TXLSAutofilterDOPER z polami DataType, grbitSgn i Value, więc możesz robić asercje na tym, co zostanie zapisane, zamiast zgadywać

Anatomia rekordu AUTOFILTER w HotXLS: indeks pola liczony od zera, słowo grbit, którego dwa najniższe bity trzymają wJoin, oraz dwie 10-bajtowe struktury DOPER, których bajt vt wybiera operand liczbowy IEEE, tekstowy, Bes Boolean, pusty albo niepusty, podczas gdy grbitSgn koduje operator porównania stosowany przez Excel
Każdy rekord AUTOFILTER niesie dwa 10-bajtowe DOPERy, a bajt vt decyduje, czy Excel porówna kryterium jako liczbę, tekst, Boolean albo test pustości — odczytaj oba przez AutoFilterColumns przed zapisem

Dlaczego filtr '>=100' nie pasował do żadnego wiersza w Excelu?

Filtr nie łapał niczego, bo operand był zapisany jako tekst, a Excel porównuje DOPER tekstowy z komórką jak tekst. Przed v2.384.45 CreateFilterDoper w lxFilter.pas poprawnie zdejmował prefiks >= i ustawiał znak na 6, po czym zawsze budował DOPER vtString ze znakami 100. Komórka liczbowa z 250 nigdy nie spełni porównania tekstowego z "100", więc wypadał każdy wiersz. Żadnego wyjątku, żadnej diagnostyki, żadnej propozycji naprawy od Excela. Reguła od v2.384.45 jest celowo wąska: jeśli kryterium zaczyna się od operatora porównania, a reszta parsuje się jako liczba według reguł invariant culture, HotXLS pisze DOPER vtIEEENumber z tym samym znakiem. Nagie wartości bez operatora zostają w formie tekstowej, bo tak Excel sam zapisuje element wybrany z listy rozwijanej

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;

  // Pole 2 = druga kolumna zakresu A1:B100 (numeracja od 1 po stronie API)
  Sh.ApplyAutoFilter('A1:B100', 2, '>=100');

  Doper := Sh.AutoFilterColumns.Find(2).Criteria1;
  // v2.384.45+: DataType = 4 (liczba IEEE), grbitSgn = 6 (>=)
  // Przed poprawką: DataType = 6 (łańcuch), który nie łapał niczego
  Assert(Doper.DataType = 4);

  Wb.SaveAs('orders.xls');
end;
HotXLS zapisuje to samo kryterium AutoFiltera >=100 jako DOPER vtString, który nie pasuje do żadnej komórki liczbowej, albo jako DOPER vtIEEENumber z grbitSgn 6, który Excel liczy numerycznie wobec kwoty 250 — cicha awaria z zerem wierszy, którą CreateFilterDoper naprawił w v2.384.45
Za obu razem bajty były poprawnym BIFF8 — zmienił się tylko bajt typu operandu, dlatego Excel otwierał plik, pokazywał kryterium na liście rozwijanej, a filtr i tak łapał zero wierszy

W parsowaniu mieszkają pozostałe ostre krawędzie. Operand przechodzi przez TryStrToFloat z kropką jako separatorem dziesiętnym, więc '>=1.5' staje się liczbą, a '>=1,5' zostaje DOPERem tekstowym i znowu po cichu nie łapie niczego, niezależnie od tego, co mówią ustawienia regionalne Windows. Daty to ta sama pułapka w innym przebraniu: '>=2026-01-01' nie jest liczbą, więc ląduje jako tekst, podczas gdy Excel trzyma komórki dat jako liczby seryjne. Przy równości na liczbie zarówno '=100', jak i liczbowy Variant taki jak 100 dają DOPER IEEE ze znakiem 2, a sam łańcuch '100' daje dopasowanie tekstowe. Operandów liczbowych buduj w kodzie, zamiast formatować je dla ludzi:

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

  // Próg z ułamkiem: zawsze formatuj z kropką
  Sh.ApplyAutoFilter('A1:D500', 3, '>' + FloatToStr(1499.5, Fmt));

  // Daty: porównuj z numerem seryjnym, który Excel trzyma w komórce.
  // Delphi TDateTime równa się numerowi seryjnemu systemu 1900 dla dat po marcu 1900
  Since := EncodeDate(2026, 1, 1);
  Sh.AutoFilterColumns.SetFieldCriteria(4, '>=' + IntToStr(Trunc(Since)),
    xlAnd, Unassigned);
end;

Jak AND i OR łączą dwa warunki?

Bity wJoin grbita rekordu AUTOFILTER mają 0 dla AND i 1 dla OR, a HotXLS miał te dwie stałe zamienione miejscami aż do v2.384.18. Filtr typu between, czyli co najmniej 100 i poniżej 500, zapisywał się jako co najmniej 100 albo poniżej 500, co w praktyce łapie każdą liczbę i wygląda, jakby filtr w ogóle nie zadziałał. Publiczne stałe operatorów dodają drugą pułapkę przy przenoszeniu kodu. W HotXLS xlAnd to 0, a xlOr to 1, natomiast automatyzacja Excela numeruje je 1 i 2. XlAutoFilterOperator to zwykły Byte, więc kod przetłumaczony z makra VBA z literalnymi liczbami kompiluje się bez syku, a literalna 1, która w COM znaczyła AND, tu znaczy OR. Używaj stałych nazwanych, a problem nie ma prawa się wydarzyć:

// Kwota między 100 (włącznie) a 500 (wyłącznie)
Sh.ApplyAutoFilter('A1:D500', 3, '>=100', xlAnd, '<500');

with Sh.AutoFilterColumns.Find(3) do
begin
  Assert(Operator = xlAnd);            // wJoin = 0 na dysku
  Assert(Criteria2.grbitSgn = 1);      // 1 = mniejsze niż
end;
Układ bitów wJoin w HotXLS dla kryteriów AutoFiltera, gdzie 0 łączy dwa DOPERy przez AND, a 1 przez OR, oś liczbowa pokazująca, jak zamienione stałe sprzed v2.384.18 rozszerzały filtr between w OR łapiące wszystko, oraz zderzenie numeracji xlAnd i xlOr z automatyzacją Excela
Zamienione stałe wJoin zmieniały filtr between w taki, który łapie każdą liczbę, a przetłumaczone VBA nadal się kompiluje, bo XlAutoFilterOperator to zwykły Byte — literalna 1, która w automatyzacji COM znaczyła AND, tu znaczy OR

Wartości Boolean, puste komórki i limit 255 znaków

Kryterium Boolean jest zapisywane jako wartość Bes ([MS-XLS] §2.5.10), a Bes kładzie bajt wartości bBoolErr pierwszy, a flagę fError drugą. HotXLS zapisywał je w odwrotnej kolejności przed v2.384.18, więc filtr po TRUE wkładał 1 do flagi błędu, a Excel czytał kryterium jako kod błędu. Writer i reader były zamienione razem, dlatego HotXLS robił round-trip własnych plików bez narzekań, podczas gdy Excel się nie zgadzał — przypomnienie, że samospójny round-trip nie dowodzi zgodności ze specyfikacją. Puste komórki w ogóle nie potrzebują operandu: samo '=' daje DOPER dopasowujący wszystkie puste ($0C), a samo '<>' DOPER dopasowujący wszystkie niepuste ($0E)

Kryteria tekstowe trafiają na twardy limit w układzie DOPERa. Pole długości cch to pojedynczy bajt, więc operand tekstowy nie może przekroczyć 255 znaków, a CreateFilterDoper obcina dłuższy tekst po zdjęciu operatora, zamiast pozwolić bajtowi długości się przepełnić i rozsynchronizować ogon rekordu. Obcinanie jest ciche, więc filtr na długiej kolumnie opisów może dopasowywać inaczej niż pełny tekst, który podałeś. W BIFF8 ogon trzyma każdy łańcuch jako jedno-bajtową flagę, po której następują jednostki kodu UTF-16, a zadeklarowany rozmiar rekordu musi liczyć te bajty co do jednego — ta sama dyscyplina księgowania, którą opisuje dryf deklaracji długości rekordów BIFF w pisarzu XLS w Delphi

Dlaczego drugie wywołanie ApplyAutoFiltera kasuje pierwsze?

Każde wywołanie ApplyAutoFilter definiuje od nowa cały zakres filtra, więc przeżywa tylko kryterium z ostatniego wywołania. Wewnątrz woła SetAutoFilter, które czyści każde pole przed przebudową zakresu, co dla jednej kolumny jest poprawne, a dla dwóch zaskakuje. Żeby filtrować kilka kolumn, wołaj ApplyAutoFilter raz, żeby ustanowić zakres i pierwsze kryterium, a pozostałe dodawaj przez AutoFilterColumns.SetFieldCriteria, które zostawia zakres i inne pola w spokoju. Obie ścieżki ignorują numer pola poza zakresem bez podnoszenia wyjątku, więc weryfikuj przez odczyt zwrotny, najlepiej po ponownym otwarciu zapisanego pliku:

Sh.ApplyAutoFilter('A1:D500', 1, 'North');                     // zakres + pole 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);

Pamiętaj, że rekord AUTOFILTER to zapisana definicja: HotXLS zapisuje kryteria i nie wylicza ich na klasycznym arkuszu XLS, więc pipeline, który potrzebuje pasujących wierszy na serwerze, musi policzyć je tam samodzielnie, natomiast fasada XLSX oferuje ewaluację na poziomie wierszy, co pokazuje walidacja danych, AutoFilter i tabele HotXLS w Delphi. Gdy Excel już ukryje wiersze, sumy pod zakresem zależą od tego, jak SUBTOTAL i AGGREGATE traktują wiersze ukryte i przefiltrowane, co jest kolejnym miejscem, gdzie liczbowy filtr cicho nie łapiący niczego pokazuje się jako zła liczba

HotXLS czyta i zapisuje skoroszyty BIFF8 XLS oraz XLSX natywnie z Delphi i C++Buildera, w tym kryteria AutoFiltera z DOPERami liczbowymi, logicznymi i AND/OR, które Excel wylicza zgodnie z intencją. Funkcje, edycje i wersję próbną znajdziesz na stronie arkuszowego komponentu HotXLS dla Delphi