مقاله فنی

معیارهای DOPER در AutoFilter با BIFF8 در HotXLS و دلفی

HotXLS هر معیار AutoFilter با BIFF8 را به‌صورت یک رکورد AUTOFILTER ذخیره می‌کند که دو ساختار DOPER ده‌بایتی با خود حمل می‌کند، و نوع DOPER است که تصمیم می‌گیرد اکسل چطور مقایسه کند. از v2.384.45 به بعد، TXLSWorksheet.ApplyAutoFilter یک مقایسه مثل '>=100' را به‌صورت یک DOPER عددی IEEE می‌نویسد تا اکسل به‌جای مقایسهٔ متنی، سلول‌های عددی را بگیرد. گزارش باگی که به این تغییر انجامید کوتاه و دیوانه‌کننده بود: یک خروجی شبانه روی ستون مبلغ فیلتر اعمال می‌کرد، فایل بدون هیچ گلایه‌ای باز می‌شد، فلش فهرست کشویی معیار را نشان می‌داد و فیلتر صفر ردیف می‌گرفت. هیچ چیز خراب نبود. بایت‌ها BIFF8 معتبر بودند، فقط از آن نوع معتبرِ غلط، و دقیقاً همین دسته از خرابی‌هاست که این مقاله از کنارشان رد می‌شود، به‌همراه دو اشتباه قدیمی‌تر بایت‌محور که در v2.384.18 اصلاح شده‌اند

یک AutoFilter با BIFF8 واقعاً چه چیزی ذخیره می‌کند؟

یک AutoFilter با BIFF8 مجموعه‌ای از سه نوع رکورد است، نه یکی، و فقط رکورد مخصوص هر فیلد است که معیارها را حمل می‌کند. رکورد AUTOFILTERINFO ($009D، [MS-XLS] §2.4.8) ثبت می‌کند محدودهٔ فیلتر چند ستون را پوشش می‌دهد. FILTERMODE ($009B) یک نشانگر بدون بدنه است که HotXLS فقط وقتی دست‌کم یک فیلد معیار فعال دارد صادر می‌کند. بعد هر فیلد فعال رکورد AUTOFILTER مخصوص خودش را می‌گیرد ($009E، §2.4.6): یک شاخص فیلد مبتنی بر صفر، یک word به نام grbit که دو بیت پایینش wJoin است، دو DOPER که هرکدام دقیقاً 10 بایت‌اند، و یک دنبالهٔ اختیاری که کاراکترهای هر DOPER رشته‌ای را نگه می‌دارد. شاخص فیلد روی دیسک مبتنی بر صفر است، هرچند ApplyAutoFilter فیلدها را از 1 می‌شمارد، و این اولین باری که دنبال یک رکورد در hex dump می‌گردید مهم می‌شود. بایت اول هر DOPER یعنی vt می‌گوید عملوند بعدی از چه نوعی است:

  • $04 یک double با IEEE 754 است که در 8 بایت باقی‌مانده ذخیره می‌شود، و اکسل مقایسهٔ عددی را همین‌طور ذخیره می‌کند
  • $06 یک رشته است که طولش در یک بایت cch جا می‌گیرد و خود کاراکترها به دنبالهٔ رکورد پس زده می‌شوند
  • $08 یک مقدار Bes است، یعنی بولین یا کد خطا فشرده‌شده در دو بایت
  • $0C و $0E عملوندی با خود ندارند و به معنی گرفتن همهٔ خالی‌ها و گرفتن همهٔ غیرخالی‌ها هستند

بایت دوم یعنی grbitSgn نوع مقایسه را حمل می‌کند: اعداد 1 تا 6 به‌ترتیب به <، =، <=، >، <> و >= نگاشت می‌شوند. HotXLS هر دو بایت را بعد از ماجرا هم از طریق AutoFilterColumns قابل مشاهده نگه می‌دارد؛ آیتم‌هایش Criteria1 و Criteria2 را به‌صورت اشیای TXLSAutofilterDOPER با DataType، grbitSgn و Value در دسترس می‌گذارند تا به‌جای حدس زدن، روی چیزی که نوشته خواهد شد assert بگیرید

کالبدشکافی رکورد AUTOFILTER در HotXLS: شاخص فیلد مبتنی بر صفر، word grbit که دو بیت پایینش wJoin را حمل می‌کند، و دو ساختار DOPER ده‌بایتی که بایت vt در آن‌ها عملوند عدد IEEE، رشته، بولین Bes، خالی یا غیرخالی را برمی‌گزیند و grbitSgn عملگر مقایسه‌ای را که اکسل اعمال می‌کند رمزگذاری می‌کند
هر رکورد AUTOFILTER دو DOPER ده‌بایتی حمل می‌کند و بایت vt تصمیم می‌گیرد اکسل معیار را عدد بگیرد، متن، بولین یا آزمون خالی — پیش از ذخیره هر دو را از طریق AutoFilterColumns بخوانید

چرا یک فیلتر '>=100' در اکسل هیچ ردیفی نگرفت؟

فیلتر هیچ چیزی نگرفت چون عملوند به‌صورت متن ذخیره شده بود و اکسل یک DOPER رشته‌ای را مثل متن با سلول مقایسه می‌کند. پیش از v2.384.45، CreateFilterDoper در lxFilter.pas پیشوند >= را درست حذف می‌کرد و علامت را روی 6 می‌گذاشت، بعد همیشه یک DOPER از نوع vtString می‌ساخت که کاراکترهای 100 را در خود داشت. سلول عددیِ حاوی 250 هرگز از یک مقایسهٔ متنی با "100" سر بلند نمی‌کند، پس همهٔ ردیف‌ها حذف می‌شدند. بدون exception، بدون تشخیص، بدون هیچ پیام تعمیر از طرف اکسل. قاعده از v2.384.45 به بعد عمداً باریک است: اگر معیار با یک عملگر مقایسه شروع شود و باقی‌مانده‌اش طبق قواعد invariant-culture به‌عنوان عدد parse شود، HotXLS یک DOPER از نوع vtIEEENumber با همان علامت می‌نویسد. یک مقدار برهنه بدون عملگر به شکل رشته می‌ماند، چون خود اکسل هم آیتمی را که از فهرست کشویی انتخاب شده همین‌طور ذخیره می‌کند

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;

  // Field 2 = ستون دوم از A1:B100 (سمت API یک‌مبنا)
  Sh.ApplyAutoFilter('A1:B100', 2, '>=100');

  Doper := Sh.AutoFilterColumns.Find(2).Criteria1;
  // از v2.384.45 به بعد: DataType = 4 (عدد IEEE)، grbitSgn = 6 (>=)
  // پیش از fix: DataType = 6 (رشته) که هیچ چیزی نمی‌گرفت
  Assert(Doper.DataType = 4);

  Wb.SaveAs('orders.xls');
end;
HotXLS همان معیار AutoFilter با >=100 را یا به‌صورت DOPER با vtString می‌نویسد که هیچ سلول عددی نمی‌گیرد، یا به‌صورت DOPER با vtIEEENumber و grbitSgn برابر 6 که اکسل آن را به‌صورت عددی با مبلغ 250 ارزیابی می‌کند؛ همان خرابی بی‌صدای صفرِ ردیف که CreateFilterDoper در v2.384.45 اصلاح کرد
هر دو بار بایت‌ها BIFF8 معتبر بودند — فقط بایت نوع عملوند عوض شده بود؛ به همین دلیل اکسل فایل را باز می‌کرد، معیار را در فهرست کشویی نشان می‌داد و باز هم صفر ردیف می‌گرفت

لبه‌های تیز باقی‌مانده همان‌جایی‌اند که parse اتفاق می‌افتد. عملوند از TryStrToFloat با نقطه به‌عنوان جداکنندهٔ اعشار می‌گذرد، پس '>=1.5' عدد می‌شود ولی '>=1,5' یک DOPER رشته‌ای می‌ماند و باز هم بی‌سروصدا هیچ چیزی نمی‌گیرد، مهم نیست locale ویندوز چه بگوید. تاریخ‌ها همان دام‌اند با لباس دیگر: '>=2026-01-01' عدد نیست، پس به‌صورت متن نوشته می‌شود، در حالی که اکسل سلول‌های تاریخ را به‌صورت شماره سریال نگه می‌دارد. برای تساوی روی عدد، هم '=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));

  // تاریخ‌ها: با شماره سریالی که اکسل در سلول ذخیره می‌کند مقایسه کن.
  // TDateTime دلفی برای تاریخ‌های بعد از مارس 1900 برابر شماره سریال سیستم 1900 است
  Since := EncodeDate(2026, 1, 1);
  Sh.AutoFilterColumns.SetFieldCriteria(4, '>=' + IntToStr(Trunc(Since)),
    xlAnd, Unassigned);
end;

AND و OR چطور دو شرط را به هم وصل می‌کنند؟

بیت‌های wJoin در grbit رکورد AUTOFILTER برای AND مقدار 0 و برای OR مقدار 1 دارند، و HotXLS تا v2.384.18 همین دو ثابت را جابه‌جا نوشته بود. یک فیلتر از نوع between مثل دست‌کم 100 و کمتر از 500 با معنی دست‌کم 100 یا کمتر از 500 ذخیره می‌شد، که در عمل همهٔ اعداد را می‌گیرد و شبیه این است که اصلاً فیلتری اعمال نشده. ثابت‌های عمومی عملگر خطر دومِ پورت کد را می‌سازند. در HotXLS مقدار xlAnd برابر 0 و مقدار xlOr برابر 1 است، در حالی که Excel automation این‌ها را 1 و 2 شماره‌گذاری می‌کند. نوع XlAutoFilterOperator یک Byte ساده است، پس کدی که از یک ماکروی VBA با اعداد literal ترجمه شده بدون هیچ خطایی کامپایل می‌شود، و همان literal 1 که در COM به معنی AND بود اینجا به معنی 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 چطور یک فیلتر between را به یک OR همه‌گیر گسترش می‌داد، و برخورد شماره‌گذاری xlAnd و xlOr با Excel automation
ثابت‌های جابه‌جای wJoin یک فیلتر between را به فیلتری تبدیل می‌کردند که همهٔ اعداد را می‌گیرد، و VBA ترجمه‌شده همچنان کامپایل می‌شود چون XlAutoFilterOperator یک Byte ساده است — همان literal 1 که زیر COM automation معنی AND می‌داد اینجا معنی OR می‌دهد

بولین‌ها، خالی‌ها و سقف 255 کاراکتری

یک معیار بولین به‌صورت مقدار Bes ذخیره می‌شود ([MS-XLS] §2.5.10)، و Bes بایت مقدار یعنی bBoolErr را اول می‌گذارد و فلگ fError را دوم. HotXLS تا v2.384.18 این دو را برعکس می‌نوشت، پس یک فیلتر برای TRUE عدد 1 را در فلگ خطا می‌گذاشت و اکسل معیار را کد خطا می‌خواند. writer و reader با هم عوض شده بودند، و به همین دلیل HotXLS فایل‌های خودش را بدون گلایه round-trip می‌کرد در حالی که اکسل مخالفت می‌کرد؛ یادآوری این که یک round-trip خودسازگار هیچ چیزی دربارهٔ سازگاری با spec ثابت نمی‌کند. خالی‌ها اصلاً عملوند نمی‌خواهند: پاس دادن '=' به‌تنهایی یک DOPER همهٔ خالی‌ها ($0C) تولید می‌کند و '<>' به‌تنهایی یک DOPER همهٔ غیرخالی‌ها ($0E)

معیارهای رشته‌ای در چیدمان DOPER به یک حد سخت می‌خورند. فیلد طول cch یک بایت است، پس یک عملوند رشته‌ای نمی‌تواند از 255 کاراکتر بیشتر شود، و CreateFilterDoper متن طولانی‌تر را بعد از حذف عملگر برش می‌زند تا نگذارد بایت طول دور بپیچد و دنبالهٔ رکورد از هم بپاشد. برش بی‌صدا است، و یک فیلتر روی ستون توضیحات طولانی ممکن است با متنی کامل که پاس داده‌اید متفاوت نتیجه بدهد. در BIFF8 دنبالهٔ رکورد هر رشته را به‌صورت یک فلگ تک‌بایتی و بعدش واحدهای کد UTF-16 ذخیره می‌کند، و اندازهٔ اعلام‌شدهٔ رکورد باید دقیقاً همین بایت‌ها را بشمارد؛ همان نظم دفترداری که در انحراف اعلام طول رکورد BIFF در یک writer دلفی XLS پوشش داده شده

چرا یک فراخوانی دوم ApplyAutoFilter فراخوانی اول را پاک می‌کند؟

هر فراخوانی ApplyAutoFilter کل محدودهٔ فیلتر را از نو تعریف می‌کند، پس فقط معیار آخرین فراخوانی باقی می‌ماند. در درون، SetAutoFilter صدا زده می‌شود که پیش از بازسازی محدوده همهٔ فیلدها را پاک می‌کند؛ برای یک ستون درست است و برای دو ستون غافلگیرکننده. برای فیلتر کردن چند ستون، یک بار ApplyAutoFilter را صدا بزنید تا محدوده و معیار اول جا بیفتد، بعد بقیه را از طریق AutoFilterColumns.SetFieldCriteria اضافه کنید که با محدوده و فیلدهای دیگر کاری ندارد. هر دو مسیر شمارهٔ فیلد بیرون از محدوده را بدون raise نادیده می‌گیرند، پس با خواندن برگشتی راستی‌آزمایی کنید، ترجیحاً بعد از باز کردن دوبارهٔ فایل ذخیره‌شده:

Sh.ApplyAutoFilter('A1:D500', 1, 'North');                     // محدوده + field 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 ارزیابی نمی‌کند، پس pipelineای که به ردیف‌های گرفته‌شده روی سرور نیاز دارد باید خودش همان‌جا محاسبه‌شان کند، در حالی که نمای XLSX ارزیابی در سطح ردیف را ارائه می‌دهد، همان‌طور که در اعتبارسنجی داده، AutoFilter و جدول‌ها در HotXLS نشان داده شده. وقتی اکسل ردیف‌ها را مخفی می‌کند، هر جمعی زیر محدوده به رفتار SUBTOTAL و AGGREGATE با ردیف‌های مخفی و فیلترشده بستگی دارد، که جای بعدی است یک فیلتر عددیِ بی‌سروصدای بی‌نتیجه به شکل یک عدد غلط خودش را نشان می‌دهد

HotXLS کتاب‌های کار XLS و XLSX با BIFF8 را به‌صورت بومی از دلفی و C++Builder می‌خواند و می‌نویسد، از جمله معیارهای AutoFilter با DOPERهای عددی، بولین و AND/OR که اکسل طبق انتظار ارزیابی‌شان می‌کند. برای امکانات، نسخه‌ها و دریافت نسخهٔ آزمایشی کامپوننت صفحه‌گستردهٔ دلفی HotXLS را ببینید