Το HotXLS αποθηκεύει κάθε criterion του AutoFilter του BIFF8 σε ένα record AUTOFILTER που κουβαλάει δύο δομές DOPER των 10 byte, και ο τύπος του DOPER αποφασίζει πώς συγκρίνει το Excel. Από το v2.384.45, το TXLSWorksheet.ApplyAutoFilter γράφει μια σύγκριση όπως '>=100' ως number DOPER IEEE, ώστε το Excel να ταιριάζει αριθμητικά κελιά αντί να συγκρίνει κείμενο. Το bug report που παρακίνησε την αλλαγή ήταν σύντομο και εκνευριστικό: ένα nightly export εφάρμοζε φίλτρο σε στήλη ποσών, το αρχείο άνοιγε χωρίς παράπονο, το βελάκι του dropdown έδειχνε το criterion, και το φίλτρο ταιρίαζε μηδέν γραμμές. Τίποτα δεν ήταν κατεστραμμένο. Τα bytes ήταν έγκυρο BIFF8, απλώς λάθος λογαριασμού έγκυρο, και αυτή είναι η κατηγορία αποτυχίας που περπατά το άρθρο, μαζί με δύο παλιότερα λάθη σε επίπεδο byte που διορθώθηκαν στο v2.384.18
Τι αποθηκεύει στην πραγματικότητα ένα AutoFilter του BIFF8;
Ένα AutoFilter του BIFF8 είναι ένα σύνολο από τρεις τύπους record, όχι έναν, και μόνο το record ανά field κρατά criteria. Το AUTOFILTERINFO ($009D, [MS-XLS] §2.4.8) καταγράφει πόσες στήλες καλύπτει το filter range. Το FILTERMODE ($009B) είναι ένας marker χωρίς σώμα που το HotXLS βγάζει μόνο όταν τουλάχιστον ένα field έχει ενεργό criterion. Μετά κάθε ενεργό field παίρνει το δικό του record AUTOFILTER ($009E, §2.4.6): ένα μηδενικό field index, μια λέξη grbit της οποίας τα δύο χαμηλά bits είναι το wJoin, δύο DOPERs των ακριβώς 10 byte το καθένα, και μια προαιρετική ουρά που κρατά τους χαρακτήρες οποιουδήποτε string DOPER. Το field index είναι μηδενικό στον δίσκο παρότι το ApplyAutoFilter αριθμεί τα fields από το 1, κάτι που έχει σημασία την πρώτη φορά που κυνηγάτε ένα record μέσα σε hex dump. Το πρώτο byte κάθε DOPER, το vt, λέει τι είδους τελεστέο ακολουθεί:
- Το
$04είναι ένα double IEEE 754 αποθηκευμένο στα υπόλοιπα 8 byte, έτσι αποθηκεύει το Excel μια αριθμητική σύγκριση - Το
$06είναι ένα string του οποίου το μήκος ζει σε ένα μόνο bytecch, με τους ίδιους τους χαρακτήρες σπρωχμένους στην ουρά του record - Το
$08είναι μια τιμή Bes, ένα Boolean ή κωδικός σφάλματος πακεταρισμένα σε δύο bytes - Το
$0Cκαι το$0Eδεν κουβαλούν τελεστέο και σημαίνουν ταίριασμα όλων των κενών και ταίριασμα όλων των μη κενών
Το δεύτερο byte, το grbitSgn, κρατά τη σύγκριση: το 1 έως 6 αντιστοιχούν σε <, =, <=, >, <> και >=. Το HotXLS κρατά και τα δύο bytes ορατά μετά την πράξη μέσω του AutoFilterColumns, του οποίου τα items εκθέτουν Criteria1 και Criteria2 ως objects TXLSAutofilterDOPER με DataType, grbitSgn και Value, ώστε να κάνετε assert πάνω σε αυτό που θα γραφτεί αντί να μαντεύετε
Γιατί ένα φίλτρο '>=100' δεν ταιρίαξε καμία γραμμή στο Excel;
Το φίλτρο δεν ταιρίαξε τίποτα επειδή ο τελεστέος αποθηκεύτηκε ως κείμενο, και το Excel συγκρίνει ένα string DOPER με το κελί ως κείμενο. Πριν το v2.384.45, το CreateFilterDoper στο lxFilter.pas ξεκόλλαζε σωστά το πρόθεμα >= και έθετε το sign σε 6, μετά έχτιζε πάντα ένα DOPER vtString που κρατούσε τους χαρακτήρες 100. Ένα αριθμητικό κελί με 250 δεν ικανοποιεί ποτέ μια σύγκριση κειμένου απέναντι σε "100", οπότε κάθε γραμμή έπεφτε έξω. Καμία εξαίρεση, καμία διαγνωστική, καμία προτροπή επιδιόρθωσης από το Excel. Ο κανόνας από το v2.384.45 είναι σκόπιμα στενός: αν το criterion ξεκινά με τελεστή σύγκρισης και το υπόλοιπο γίνεται parse ως αριθμός με κανόνες invariant culture, το HotXLS γράφει ένα DOPER vtIEEENumber με το ίδιο sign. Μια γυμνή τιμή χωρίς τελεστή μένει σε μορφή string, επειδή έτσι αποθηκεύει το ίδιο το Excel ένα item διαλεγμένο από τη λίστα του dropdown
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 (1-based στην πλευρά του API)
Sh.ApplyAutoFilter('A1:B100', 2, '>=100');
Doper := Sh.AutoFilterColumns.Find(2).Criteria1;
// v2.384.45+: DataType = 4 (IEEE number), grbitSgn = 6 (>=)
// Πριν το fix: DataType = 6 (string), που δεν ταιρίαζε τίποτα
Assert(Doper.DataType = 4);
Wb.SaveAs('orders.xls');
end;
Το parse είναι εκεί που ζουν οι υπόλοιπες κοφτερές άκρες. Ο τελεστέος περνά από TryStrToFloat με τελεία ως διαχωριστικό δεκαδικών, οπότε το '>=1.5' γίνεται αριθμός και το '>=1,5' μένει string DOPER και πάλι ταιριάζει σιωπηλά τίποτα, ό,τι κι αν λέει το locale των Windows. Οι ημερομηνίες είναι η ίδια παγίδα με άλλη στολή: το '>=2026-01-01' δεν είναι αριθμός, οπότε γράφεται ως κείμενο, ενώ το Excel κρατά τα κελιά ημερομηνιών ως serial numbers. Για ισότητα σε αριθμό, τόσο το '=100' όσο και ένα αριθμητικό Variant όπως το 100 δίνουν IEEE DOPER με sign 2, ενώ το γυμνό string '100' δίνει ταίριασμα κειμένου. Χτίστε αριθμητικούς τελεστέους στον κώδικα αντί να τους μορφοποιείτε για ανθρώπους:
var
Fmt: TFormatSettings;
Since: TDateTime;
begin
Fmt := TFormatSettings.Create;
Fmt.DecimalSeparator := '.';
// Κατώφλι με δεκαδικό: μορφοποιείτε πάντα με τελεία
Sh.ApplyAutoFilter('A1:D500', 3, '>' + FloatToStr(1499.5, Fmt));
// Ημερομηνίες: συγκρίνετε με τον serial number που κρατά το Excel στο κελί.
// Ένα TDateTime του Delphi ισούται με τον serial του συστήματος 1900 για ημερομηνίες μετά τον Μάρτιο 1900
Since := EncodeDate(2026, 1, 1);
Sh.AutoFilterColumns.SetFieldCriteria(4, '>=' + IntToStr(Trunc(Since)),
xlAnd, Unassigned);
end;
Πώς ενώνουν οι AND και OR δύο συνθήκες;
Τα bits wJoin του grbit του AUTOFILTER είναι 0 για AND και 1 για OR, και το HotXLS είχε αυτές τις δύο σταθερές ανάποδα μέχρι το v2.384.18. Ένα φίλτρο τύπου between όπως τουλάχιστον 100 και κάτω από 500 αποθηκευόταν ως τουλάχιστον 100 ή κάτω από 500, που στην πράξη ταιριάζει κάθε αριθμό και φαίνεται σαν το φίλτρο απλώς δεν εφαρμόστηκε. Οι δημόσιες σταθερές τελεστών προσθέτουν έναν δεύτερο κίνδυνο porting. Στο HotXLS το xlAnd είναι 0 και το xlOr είναι 1, ενώ το Excel automation τα αριθμεί 1 και 2. Το XlAutoFilterOperator είναι ένα σκέτο Byte, οπότε κώδικας μεταφρασμένος από macro VBA με literal αριθμούς κάνει compile καθαρά, και ένα literal 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;
Booleans, κενά και το ταβάνι των 255 χαρακτήρων
Ένα Boolean criterion αποθηκεύεται ως τιμή Bes ([MS-XLS] §2.5.10), και το Bes βάζει το byte τιμής bBoolErr πρώτο και τη σημαία fError δεύτερο. Το HotXLS τα έγραφε με την αντίστροφη σειρά πριν το v2.384.18, οπότε ένα φίλτρο για TRUE έβαζε 1 στη σημαία σφάλματος και το Excel διάβαζε το criterion ως κωδικό σφάλματος. Writer και reader ήταν αντεστραμμένοι μαζί, γι' αυτό το HotXLS έκανε round trip στα δικά του αρχεία χωρίς παράπονο ενώ το Excel διαφωνούσε, μια υπενθύμιση ότι ένα self-consistent round trip δεν αποδεικνύει τίποτα για συμμόρφωση με το spec. Τα κενά δεν χρειάζονται καθόλου τελεστέο: περνώντας '=' μόνο του προκύπτει ένα DOPER ταίριασμα όλων των κενών ($0C) και '<>' μόνο του ένα DOPER ταίριασμα όλων των μη κενών ($0E)
Τα string criteria χτυπούν σκληρό όριο στη διάταξη του DOPER. Το πεδίο μήκους cch είναι ένα μόνο byte, οπότε ένας string τελεστέος δεν μπορεί να ξεπεράσει τους 255 χαρακτήρες, και το CreateFilterDoper κόβει πιο μακρύ κείμενο μετά την αφαίρεση του τελεστή αντί να αφήσει το byte μήκους να τυλίξει και να αποσυγχρονίσει την ουρά του record. Το κόψιμο είναι σιωπηλό, και ένα φίλτρο σε μακριά στήλη περιγραφών μπορεί να ταιριάζει διαφορετικά από το πλήρες κείμενο που περάσατε. Στο BIFF8 η ουρά αποθηκεύει κάθε string ως flag ενός byte ακολουθούμενο από code units UTF-16, και το δηλωμένο μέγεθος του record πρέπει να μετρά αυτά τα bytes ακριβώς, η ίδια λογιστική πειθαρχία που καλύπτει το πώς παρασύρονται οι δηλώσεις μήκους record BIFF σε έναν XLS writer του Delphi
Γιατί μια δεύτερη κλήση ApplyAutoFilter σβήνει την πρώτη;
Κάθε κλήση ApplyAutoFilter ξαναορίζει ολόκληρο το filter range, οπότε μόνο το criterion της τελευταίας κλήσης επιζεί. Εσωτερικά καλεί το SetAutoFilter, που καθαρίζει κάθε field πριν ξαναχτίσει το range, κάτι σωστό για μία στήλη και απροσδόκητο για δύο. Για να φιλτράρετε πολλές στήλες, καλέστε ApplyAutoFilter μία φορά για να καθορίσετε το range και το πρώτο criterion, μετά προσθέστε τα υπόλοιπα μέσω AutoFilterColumns.SetFieldCriteria, που αφήνει το range και τα άλλα fields ήσυχα. Και οι δύο δρόμοι αγνοούν αριθμό field εκτός range χωρίς να σηκώνουν εξαίρεση, οπότε επαληθεύστε διαβάζοντας πίσω, ιδανικά μετά από επανάνοιγμα του αποθηκευμένου αρχείου:
Sh.ApplyAutoFilter('A1:D500', 1, 'North'); // range + 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);
Έχετε υπόψη ότι το record AUTOFILTER είναι αποθηκευμένος ορισμός: το HotXLS γράφει τα criteria και δεν τα αξιολογεί στο classic φύλλο εργασίας XLS, οπότε ένα pipeline που χρειάζεται τις ταιριαστές γραμμές στον server πρέπει να τις υπολογίσει μόνο του εκεί, ενώ το facade XLSX προσφέρει αξιολόγηση σε επίπεδο γραμμής όπως δείχνει το HotXLS data validation, AutoFilter και πίνακες στο Delphi. Μόλις το Excel όντως κρύψει γραμμές, κάθε σύνολο κάτω από το range εξαρτάται από το πώς μεταχειρίζονται οι SUBTOTAL και AGGREGATE τις κρυμμένες και φιλτραρισμένες γραμμές, που είναι ο επόμενος τόπος όπου ένα αριθμητικό φίλτρο που ταιριάζει σιωπηλά τίποτα εμφανίζεται ως λάθος αριθμός
Το HotXLS διαβάζει και γράφει workbooks BIFF8 XLS και XLSX εγγενώς από Delphi και C++Builder, συμπεριλαμβανομένων criteria AutoFilter με αριθμητικά, Boolean και AND/OR DOPERs που το Excel αξιολογεί όπως πρέπει. Δείτε το HotXLS Delphi spreadsheet component για χαρακτηριστικά, εκδόσεις και λήψη trial