기술 문서

HotXLS로 보는 Delphi BIFF8 AutoFilter DOPER 조건

HotXLS는 모든 BIFF8 AutoFilter 조건을 10바이트짜리 DOPER 구조 두 개를 실은 AUTOFILTER 레코드로 저장하고, Excel이 비교를 수행하는 방식은 DOPER 타입이 결정합니다. v2.384.45부터 TXLSWorksheet.ApplyAutoFilter는 '>=100' 같은 비교를 IEEE 숫자 DOPER로 기록하므로 Excel이 텍스트를 비교하는 대신 숫자 셀을 매칭합니다. 이 변경을 이끈 버그 리포트는 짧으면서도 짜증을 유발했습니다. 야간 배치 내보내기가 금액 열에 필터를 걸었는데, 파일은 불만 없이 열리고, 드롭다운 화살표에는 조건이 보이는데도 필터가 건진 행은 0개였습니다. 손상된 곳은 없었습니다. 바이트는 유효한 BIFF8이었고, 그저 잘못된 종류의 유효함이었을 뿐입니다. 이 글에서 짚어 볼 바로 그런 실패 클래스와, v2.384.18에서 함께 고쳐진 더 오래된 바이트 수준 실수 두 가지를 다룹니다

BIFF8 AutoFilter에는 실제로 무엇이 저장될까요?

BIFF8 AutoFilter는 레코드 타입 세 개의 조합이지 하나가 아니며, 그중 필드별 레코드만 조건을 담고 있습니다. AUTOFILTERINFO ($009D, [MS-XLS] §2.4.8)는 필터 범위가 몇 개의 열을 커버하는지 기록합니다. FILTERMODE ($009B)는 몸체 없는 마커로, HotXLS는 최소한 하나의 필드에 활성 조건이 있을 때만 내보냅니다. 그다음 활성 필드마다 자체 AUTOFILTER 레코드($009E, §2.4.6)를 하나씩 받습니다. 0 기반 필드 인덱스, 하위 2비트가 wJoin인 grbit 워드, 정확히 10바이트씩인 DOPER 두 개, 그리고 문자열 DOPER의 문자들을 담는 선택적 테일입니다. 필드 인덱스는 디스크에서는 0 기반인데 ApplyAutoFilter는 필드를 1부터 매기므로, 헥스 덤프에서 레코드를 처음 추적할 때 이 차이가 발목을 잡습니다. 각 DOPER의 첫 바이트인 vt는 뒤이는 피연산자의 종류를 말해 줍니다:

  • $04는 나머지 8바이트에 저장되는 IEEE 754 double로, Excel이 숫자 비교를 저장하는 방식입니다
  • $06은 길이가 단일 cch 바이트에 들어가는 문자열이며, 문자 그 자체는 레코드 테일로 밀려 납니다
  • $08은 Bes 값, 즉 두 바이트에 패킹된 Boolean 또는 오류 코드입니다
  • $0C와 $0E는 피연산자를 갖지 않으며 각각 모든 빈 셀 매칭, 모든 비어 있지 않은 셀 매칭을 뜻합니다

두 번째 바이트인 grbitSgn은 비교 연산을 담습니다. 1부터 6까지가 각각 <, =, <=, >, <>, >=에 대응합니다. HotXLS는 AutoFilterColumns를 통해 기록 후에도 두 바이트를 그대로 들여다볼 수 있게 해 줍니다. 이 컬렉션의 항목은 Criteria1과 Criteria2를 DataType, grbitSgn, Value를 갖춘 TXLSAutofilterDOPER 객체로 노출하므로, 감으로 때우는 대신 실제 기록될 내용을 assert로 확인할 수 있습니다

HotXLS AUTOFILTER 레코드 해부도: 0 기반 필드 인덱스와 하위 2비트에 wJoin을 실은 grbit 워드, 그리고 vt 바이트가 IEEE 숫자, 문자열, Bes Boolean, 빈 셀 또는 비어 있지 않은 셀 피연산자를 가리키고 grbitSgn이 Excel이 적용할 비교 연산자를 부호화하는 10바이트짜리 DOPER 구조 두 개
각 AUTOFILTER 레코드는 10바이트짜리 DOPER 두 개를 실으며, vt 바이트가 Excel이 조건을 숫자로 비교할지, 텍스트로 비교할지, Boolean으로 비교할지, 빈 셀 검사로 비교할지 결정합니다. 저장하기 전에 AutoFilterColumns로 둘 다 읽어 확인하세요

Excel에서 '>=100' 필터가 한 행도 매칭하지 못한 이유는?

필터가 아무것도 건지지 못한 이유는 피연산자가 텍스트로 저장됐고, Excel은 문자열 DOPER를 셀과 텍스트로 비교하기 때문입니다. v2.384.45 이전 lxFilter.pas의 CreateFilterDoper는 >= 접두사를 올바르게 떼어 내고 부호를 6으로 설정한 뒤, 늘 문자 100을 담은 vtString DOPER를 만들었습니다. 250을 담은 숫자 셀은 "100"과의 텍스트 비교를 만족할 수 없으니 모든 행이 탈락합니다. 예외도, 진단 메시지도, Excel의 복구 프롬프트도 없이요. v2.384.45부터의 규칙은 일부러 좁게 유지합니다. 조건이 비교 연산자로 시작하고 나머지 부분이 invariant-culture 규칙으로 숫자로 파싱되면 HotXLS는 같은 부호의 vtIEEENumber DOPER를 기록합니다. 연산자 없이 값만 넣은 경우에는 문자열 형태를 유지하는데, 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가 같은 >=100 AutoFilter 조건을 숫자 셀을 하나도 매칭하지 못하는 vtString DOPER로 쓰는 경우와, grbitSgn 6을 실어 Excel이 금액 250과 숫자로 평가하는 vtIEEENumber DOPER로 쓰는 경우. v2.384.45에서 CreateFilterDoper가 고친 조용한 0행 실패가 바로 이것입니다
두 경우 모두 바이트는 유효한 BIFF8이었습니다. 바뀐 것은 피연산자 타입 바이트 하나뿐이고, 그래서 Excel은 파일을 열고 드롭다운에 조건을 보여 주면서도 매칭 행은 0개였습니다

파싱이 남은 날카로운 모서리들이 모인 자리입니다. 피연산자는 마침표를 소수점 구분자로 쓰는 TryStrToFloat을 통과하므로, '>=1.5'는 숫자가 되고 '>=1,5'는 문자열 DOPER로 남아 Windows 로캘이 뭐라고 하든 다시 조용히 아무것도 매칭하지 않습니다. 날짜는 옷만 갈아입은 같은 함정입니다. '>=2026-01-01'은 숫자가 아니라서 텍스트로 기록되는 반면, Excel은 날짜 셀을 serial number로 보관합니다. 숫자 동등 비교에는 '=100'과 100 같은 숫자 Variant가 모두 부호 2의 IEEE DOPER를 만들고, 값만 담은 문자열 '100'은 텍스트 매칭을 만듭니다. 사람이 읽으라고 포매팅하는 대신, 숫자 피연산자는 코드에서 만드세요:

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

  // 소수부가 붙는 문턱값: 늘 마침표로 포맷합니다
  Sh.ApplyAutoFilter('A1:D500', 3, '>' + FloatToStr(1499.5, Fmt));

  // 날짜: Excel이 셀에 저장하는 serial number와 비교합니다.
  // Delphi TDateTime은 1900년 3월 이후 날짜에서 1900 체계 serial과 같습니다
  Since := EncodeDate(2026, 1, 1);
  Sh.AutoFilterColumns.SetFieldCriteria(4, '>=' + IntToStr(Trunc(Since)),
    xlAnd, Unassigned);
end;

AND와 OR은 두 조건을 어떻게 결합할까요?

AUTOFILTER grbit의 wJoin 비트는 AND가 0, OR이 1인데, HotXLS는 v2.384.18까지 이 두 상수를 뒤집어 놓고 있었습니다. 100 이상 500 미만 같은 between 스타일 필터가 100 이상 또는 500 미만으로 저장되곤 했고, 이는 실제로는 모든 숫자를 매칭하니 필터가 아예 적용되지 않은 것처럼 보입니다. 공개된 연산자 상수에는 두 번째 포팅 함정이 도사리고 있습니다. HotXLS에서 xlAnd는 0, xlOr은 1이지만 Excel automation은 이를 각각 1과 2로 매깁니다. XlAutoFilterOperator는 그냥 Byte라서 VBA 매크로에서 literal 숫자와 함께 옮겨 온 코드는 멀쩡히 컴파일되고, COM에서 AND를 뜻하던 literal 1이 이제는 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;
AutoFilter 조건의 HotXLS wJoin 비트 레이아웃(0은 두 DOPER를 AND로, 1은 OR로 결합), v2.384.18 이전 뒤바뀐 상수가 between 필터를 모든 것을 매칭하는 OR로 넓혔음을 보여 주는 수직선, 그리고 Excel automation과의 xlAnd xlOr 번호 충돌
뒤집힌 wJoin 상수는 between 필터를 모든 숫자를 매칭하는 필터로 만들고, XlAutoFilterOperator가 그냥 Byte라 번역된 VBA도 멀쩡히 컴파일됩니다. COM automation에서 AND를 뜻하던 literal 1이 여기서는 OR을 뜻합니다

Boolean, 빈 셀, 그리고 255자 상한

Boolean 조건은 Bes 값([MS-XLS] §2.5.10)으로 저장되며, Bes는 값 바이트 bBoolErr를 먼저 놓고 fError 플래그를 그다음에 놓습니다. HotXLS는 v2.384.18까지 이 둘을 반대 순서로 썼기 때문에 TRUE에 대한 필터가 오류 플래그에 1을 넣었고 Excel은 그 조건을 오류 코드로 읽었습니다. 작성기와 리더가 함께 뒤집혀 있었기에 HotXLS는 자기 파일을 불평 없이 왕복 전송하면서 Excel과는 어긋났고, 이는 자기 일관적인 왕복 전송이 스펙 준수를 아무것도 증명하지 않는다는 점을 일깨워 줍니다. 빈 셀은 피연산자가 전혀 필요 없습니다. '='만 넘기면 모든 빈 셀 매칭 DOPER($0C)가 되고, '<>'만 넘기면 모든 비어 있지 않은 셀 매칭 DOPER($0E)가 됩니다

문자열 조건은 DOPER 레이아웃에서 딱 한계에 부딪힙니다. cch 길이 필드가 단일 바이트라 문자열 피연산자는 255자를 넘을 수 없고, CreateFilterDoper는 길이 바이트가 wrap되어 레코드 테일이 어긋나게 두는 대신 연산자를 떼어 낸 뒤 더 긴 텍스트를 잘라 냅니다. 잘림은 조용히 일어나고, 긴 설명 열에 건 필터는 넘겨 준 전체 텍스트와 다르게 매칭할 수 있습니다. BIFF8에서 테일은 각 문자열을 1바이트 플래그 뒤에 UTF-16 코드 유닛으로 저장하며, 선언된 레코드 크기는 이 바이트들을 정확히 세어야 합니다. Delphi XLS 작성기에서 BIFF 레코드 길이 선언이 어긋나는 방식에서 다룬 것과 같은 장부 관리 규율입니다

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 퍼사드는 Delphi에서 HotXLS 데이터 유효성 검사, AutoFilter와 테이블에서 보여 주듯 행 수준 평가를 제공합니다. Excel이 실제로 행을 숨긴 뒤에는 범위 아래의 합계가 SUBTOTAL과 AGGREGATE가 숨겨진 행과 필터된 행을 다루는 방식에 좌우됩니다. 조용히 아무것도 매칭하지 않는 숫자 필터가 다음으로 드러나는 곳이 바로 이 잘못된 합계입니다

HotXLS는 Delphi와 C++Builder에서 BIFF8 XLS와 XLSX 워크북을 네이티브로 읽고 쓰며, Excel이 의도대로 평가하는 숫자·Boolean·AND/OR DOPER를 담은 AutoFilter 조건도 예외가 아닙니다. 기능, 에디션과 평가판 다운로드는 HotXLS Delphi 스프레드시트 컴포넌트를 참조하세요