HotXLS Delphi Component از v2.384.67 دو مقدار متنی را همانطور مقایسه میکند که Excel 16 انجام میدهد: بدون حساسیت به بزرگی و کوچکی حروف، با ترتیب word sort مربوط به locale کاربر Windows، همان چیزی که CompareStringW با فلگ NORM_IGNORECASE برمیگرداند. خط تیره و آپاستروف در گذر اول نادیده گرفته میشوند و فقط تساوی را میشکنند، پس ="a-b">"ab" برابر TRUE است، در حالی که بقیه علائم نگارشی قبل از ارقام و حروف مرتب میشوند، پس ="a~b"<"ab" هم برابر TRUE است. همین ترتیب حالا عملگرهای مقایسه، معیارهای > / <، مرتبسازی محدوده و VLOOKUP را هدایت میکند
هیچکس باگای با عنوان «ناهمخوانی collation» ثبت نمیکند. گزارشها میگویند COUNTIF(A:A,">M") روی سرور دو سطر بیشتر از Excel میشمارد، یا لیست قیمتی که سرویس گزارشساز مرتب کرده X-100 را جایی میگذارد که Excel نمیگذارد، یا VLOOKUP("ABC",...) برمیگرداند #N/A در حالی که ستون بهوضوح abc دارد. هر سه از یک سؤال میآیند: وقتی هر دو عملوند متناند، کدام کوچکتر است؟ Excel جواب دقیقی دارد، آن جواب چیزی نیست که بیشتر کدهای Delphi میدهند، و HotXLS قبل از v2.384.67 بسته به اینکه کدام مسیر کد سؤال میپرسید سه جواب متفاوت میداد
Excel برای مقایسه دو رشته متنی از چه قاعدهای استفاده میکند؟
Excel متن را با word sort مربوط به locale کاربر مقایسه میکند و بزرگی و کوچکی حروف را نادیده میگیرد. word sort همان collation پیشفرض توابع مقایسه NLS در Windows است: حروف به ترتیب زبانیشان با هم مقایسه میشوند نه به کدپوینتشان، حروف دارای اکسان کنار حرف پایهشان مینشینند، و دو کاراکتر رفتار خاص میگیرند. خط تیره - و آپاستروف ' در گذر اول نادیده گرفته میشوند، پس co-op و coop کنار هم فرود میآیند، و فقط وقتی بقیه رشتهها مساوی شوند حضورشان ترتیب را تصمیم میگیرد. هر علامت نگارشی دیگر مؤثر است و قبل از ارقام مرتب میشود، و ارقام قبل از حروف
جدول نشان میدهد این یعنی چه در عمل، کنار دو مقایسهای که یک توسعهدهنده Delphi به احتمال زیاد سراغش میرود. ستون Excel حکمهایی است که Excel 16 برای IF(A<B,...) برگردانده، و HotXLS از v2.384.67 همانها را بازتولید میکند
| A در برابر B | Excel 16 / HotXLS | CompareStr (ordinal) | CompareText |
|---|---|---|---|
"a-b" vs "ab" | بزرگتر | کوچکتر | کوچکتر |
"a'b" vs "ab" | بزرگتر | کوچکتر | کوچکتر |
"a~b" vs "ab" | کوچکتر | بزرگتر | بزرگتر |
"a_b" vs "ab" | کوچکتر | کوچکتر | بزرگتر |
"ab" vs "AB" | مساوی | بزرگتر | مساوی |
"é" vs "f" | کوچکتر | بزرگتر | بزرگتر |
"Z" vs "f" | بزرگتر | کوچکتر | بزرگتر |
دو نتیجه بهراحتی از چشم میافتد. اول، نقش تساویشکن خط تیره یعنی ="a-b"="ab" برابر FALSE است: دو رشته در ترتیب همسایههای نزدیکاند، اما مساوی نیستند. دوم، تساوی بزرگی حروف را کلاً نادیده میگیرد، پس ab و AB و Ab تا وقتی مقایسه در میان است همه یک کلیدند. مرتب کردن 20 کلمه تست با Range.Sort خود Excel این ترتیب را میدهد: a b، a.b، a_b، a~b، a0، a1b، ab / AB / Ab، ab-، a'b، a-b، -ab، ab1، abc، b، e، é، f، Z؛ داخل گروه ab، جای کاراکتر نادیدهگرفتهشده است که تصمیم میگیرد
ترتیب متنی Excel چطور دقیق مشخص شد؟
ترتیب متنی Excel با اندازهگیری شناسایی شد نه با مستندات، چون مستندات Excel نامی از collation نمیبرد. تست 4000 جفت رشته تصادفی از علائم نگارشی ASCII و ارقام و هر دو حالت بزرگی حروف و فاصلهها و é و ß و ä و کاراکترهای چینی و فرمهای full-width و فاصله غیرشکن تولید کرد، با طولهای 0 تا 4 و نصف جفتها بهشکل near-miss های همدیگر. Excel 16 برای هر جفت IF(A<B,-1,IF(A=B,0,1)) را ارزیابی کرد، و حکمها با API مقایسه Windows با مجموعه فلگهای مختلف تطبیق داده شد
- فقط
NORM_IGNORECASE(word sort پیشفرض، locale کاربر): هیچ ناهمخوانی واقعیای نبود. تنها 7 تفاوت سلولهایی بودند که کل محتوایشان'بود، که Excel آن را بهعنوان کاراکتر پیشوند متن مصرف میکند، پس خطای نمونهبرداری بودند نه تفاوت collation NORM_IGNORECASEباSORT_STRINGSORT: تعداد 41 ناهمخوانی. string sort خط تیره و آپاستروف را علامت معمولی میگیرد، و دقیقاً همین رفتاری است که Excel ندارد- اضافه کردن
NORM_IGNOREWIDTH: به شکل دیگری غلط بود، چون فرم full-width و half-width یک حرف را مساوی میگیرد و Excel آنها را جدا نگه میدارد
یک چک دوم دستچینشده، همه 190 جفت ساختهشده از 20 کلمه پرچالش و نتیجه Range.Sort خود Excel روی همان ستون را مقایسه کرد. هر دو با word sort ساده یعنی NORM_IGNORECASE موافق بودند، و همان 190 حکم بهعلاوه ترتیب مرتبشده حالا بخشی از regression suite مربوط به HotXLSاند، و هم از موتور کلاسیک یعنی TXLSWorkbook میگذرند هم از موتور بومی XLSX یعنی TXLSXWorkbook
چرا CompareText و مقایسه ordinal جواب اشتباه میدهند؟
CompareText و مقایسه ordinal به این دلیل ترتیب Excel را غلط درمیآورند که UTF-16 code unit ها را مقایسه میکنند، و ترتیب کدپوینت علائم نگارشی را در جایهای تصادفی نسبت به حروف میگذارد. خط تیره U+002D و آپاستروف U+0027 هر دو زیر همه حروفاند، پس مقایسه ordinal میگوید "a-b" کوچکتر از "ab" است بهجای اینکه خط تیره را تساویشکن بداند. علامت مد U+007E بالای همه حروف است، پس "a~b" بزرگتر درمیآید، برعکس Excel. CompareText در RTL مربوط به Delphi فقط a..z را به حروف بزرگ میبرد و بعد code unit ها را مقایسه میکند، که یک اعوجاج دوم اضافه میکند: آندرلاین U+005F بین حروف بزرگ و کوچک نشسته، پس آوردن به حروف بزرگ "a_b" را از زیر "ab" میبرد بالایش. هیچکدام از این دو تابع نمیدانند که é باید بین e و f بنشیند
ابزارهای همیشگی Delphi در دو طرف این خط افتادهاند:
CompareStrو عملگر<روی رشته وTComparer<string>.Default(کهCompareStrرا صدا میزند) ordinal و حساس به بزرگی حروفاند، پسTArray.Sort<string>بدون comparer گذاشتنZرا قبل ازfمیگذاردCompareTextوSameTextبعد از تاشدن ASCII-فقط بزرگی حروف، ordinal هستندAnsiCompareTextوWideCompareTextدر RTL مربوط به Delphi روی Windows صدا میزنندCompareString(LOCALE_USER_DEFAULT, NORM_IGNORECASE, ...)، همان فراخوانی که با Excel جور است. یکTStringListمرتبشده با تنظیمات پیشفرضش (UseLocaleبرابر True وCaseSensitiveبرابر False) ازAnsiCompareTextمیگذرد و پس با Excel هم موافق است- روی targetهای POSIX، RTL مربوط به Delphi مسیر
AnsiCompareTextرا به یک collator از ICU میفرستد که الگوریتم دیگری با قواعد نگارشی متفاوت است، وAnsiCompareTextدر Free Pascal روی Windows بعد از تبدیل به code page ANSI صدا میزندCompareStringAرا، که هر کاراکتری را آن صفحه نتواند نمایندگی کند از دست میدهد
پس توابع RTL آگاه-از-locale روی Windows به لطف پیادهسازی درستاند نه به لطف قرارداد، و کدی که ترتیب Excel را لازم دارد بهتر است خودش فراخوانی API را صریح انجام دهد. HotXLS هم در درون همین ترکیب را داشت. عملگرهای مقایسه هر دو رشته را به حروف بزرگ میبردند و کدپوینتها را مقایسه میکردند، شاخههای > / < توابع معیار از مقایسه Variant حساس-به-حروف خود Delphi استفاده میکردند، و VLOOKUP / HLOOKUP هم متن را با همان مقایسه Variant حساس-به-حروف مچ میکردند، و برای همین بود که VLOOKUP("ABC",A1:A20,1,FALSE) نمیتوانست abc را پیدا کند. مرتبسازی محدوده از قبل WideCompareText را داشت. سه مسیر، سه ترتیب
در HotXLS نسخه v2.384.67 چه چیزی عوض شد؟
از v2.384.67 مقایسههای متن-با-متن در مسیرهای محاسبه و مرتبسازی HotXLS از یک تابع میگذرند، XlsCompareText در lxStandard.pas، که صدا میزند CompareStringW(LOCALE_USER_DEFAULT, NORM_IGNORECASE, ...) و CSTR_EQUAL را کم میکند. فراخوانندهها شش عملگر مقایسهاند، مقایسههای المان به المان در فرمولهای آرایهای، شاخههای > و < و >= و <= معیارهای سبک COUNTIF و توابع دیتابیس، VLOOKUP و HLOOKUP (دقیق و تقریبی)، helperهای ترتیب پشت توابع آرایه پویا و XLOOKUP / XMATCH، و مرتبسازی محدوده در هر دو موتور. هدایت مرتبسازی محدوده از همان تابع تضمین میکند ترتیب sort و ترتیب مقایسه دیگر از هم فاصله نگیرند، که اهمیت دارد چون VLOOKUP تقریبی روی متن فقط وقتی معنا دارد که ستون با همان ترتیبی مرتب شده باشد که lookup در آن مقایسه میکند
uses
System.Variants, lxHandleX;
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
Book.Sheets.Add('Data'); // Calculate روی شیت فعال ارزیابی میکند
Writeln(VarToStr(Book.Calculate('="a-b">"ab"'))); // True: خط تیره فقط تساوی را میشکند
Writeln(VarToStr(Book.Calculate('="a-b"="ab"'))); // False: تساوی شکسته شد، مساوی نیستند
Writeln(VarToStr(Book.Calculate('="a~b"<"ab"'))); // True: علائم نگارشی اول
Writeln(VarToStr(Book.Calculate('="ABC"="abc"'))); // True: بزرگی حروف نادیده گرفته میشود
finally
Book.Free;
end;
end;
مقایسههای بین-نوعی قاعده جداگانهای دارند و تغییری نکردند: هر عددی زیر هر مقدار متنی است و هر مقدار متنی زیر هر boolean، همانطور که در مقاله زنجیرههای مقایسه، عملوندهای خالی و SUMIF توضیح داده شده. word sort فقط وقتی اعمال میشود که هر دو عملوند متن باشند. مچ کردن wildcard هم جدا است: معیاری مثل "a*" یا "=ab" یک pattern یا تست تساوی است، که در راهنمای wildcard های Excel در COUNTIF و MATCH و DSUM پوشش داده شده، و collation ای که اینجا بحث شد فقط عملگرهای ترتیب را تصمیم میگیرد
مثال بعدی 20 کلمه تست را در یک ستون load میکند، با TXLSXWorksheet.SortRange مرتبش میکند، و یک شمارش معیار و یک lookup را چک میکند. شمارشها همانهاییاند که Excel 16 برای همان ستون برگردانده
const
Words: array [0..19] of string = ('ab', 'a-b', 'a~b', 'a_b', 'AB', 'a b',
'ab1', 'ab-', '-ab', 'abc', 'a''b', 'Ab', 'b', 'a.b', 'a1b', 'a0',
#$00E9, 'e', 'f', 'Z');
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
i: Integer;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Words');
for i := 0 to High(Words) do
Sheet.Cells[i + 1, 1].Value := WideString(Words[i]);
// Excel 16 روی همان ستون: 11، 11، 14
Writeln(VarToStr(Book.Calculate('=COUNTIF(A1:A20,">ab")')));
Writeln(VarToStr(Book.Calculate('=COUNTIF(A1:A20,"<a-b")')));
Writeln(VarToStr(Book.Calculate('=COUNTIF(A1:A20,">=AB")')));
// قبل از v2.384.67 برابر #N/A بود: lookup حساس به بزرگی حروف مقایسه میکرد
Sheet.Cells[1, 3].Formula := '=VLOOKUP("ABC",A1:A20,1,FALSE)';
Book.Recalculate;
Writeln(VarToStr(Sheet.Cells[1, 3].Value)); // abc
// یک ستون کلید، صعودی: a b، a.b، a_b، a~b، a0، a1b، ab، AB، Ab، ...
Sheet.SortRange(1, 1, 20, 1, [1], [False]);
for i := 1 to 20 do
Writeln(VarToStr(Sheet.Cells[i, 1].Value));
finally
Book.Free;
end;
end;
TXLSXWorksheet.SortRange از یک merge sort پایدار استفاده میکند، پس ab و AB و Ab که مساوی مقایسه میشوند ترتیب نسبی قبل از sort را نگه میدارند. سلولهای خالی در هر دو جهت به انتها میروند، همانطور که در Excel
چطور در کد Delphi خودم به ترتیب sort اکسل برسم؟
برای رسیدن به ترتیب متنی Excel در کد خودتان، CompareStringW را با LOCALE_USER_DEFAULT و NORM_IGNORECASE صدا بزنید و SORT_STRINGSORT یا NORM_IGNOREWIDTH اضافه نکنید. مقدار برگشتی یک نتیجه مقایسه علامتدار نیست: API برمیگرداند CSTR_LESS_THAN (1) یا CSTR_EQUAL (2) یا CSTR_GREATER_THAN (3)، و 0 وقتی فراخوانی شکست بخورد. برای رسیدن به قرارداد همیشگی منفی/صفر/مثبت 2 کم کنید، و اول 0 را تست کنید، چون شکستی که اشتباهی نتیجه گرفته شود میشود -2، یک «کوچکتر» بیصدا
uses
Winapi.Windows, System.SysUtils, System.Generics.Defaults,
System.Generics.Collections;
// ترتیب متنی Excel: word sort با locale کاربر، بدون حساسیت به بزرگی حروف
function ExcelCompareText(const A, B: string): Integer;
var
R: Integer;
begin
R := CompareStringW(LOCALE_USER_DEFAULT, NORM_IGNORECASE,
PWideChar(A), Length(A), PWideChar(B), Length(B));
if R = 0 then
RaiseLastOSError; // صفر یعنی شکست، نه نتیجه مقایسه
Result := R - CSTR_EQUAL; // 1/2/3 میشوند -1/0/1
end;
var
Keys: TArray<string>;
begin
Keys := ['abc', 'a-b', 'AB', 'a~b', '-ab', 'ab'];
TArray.Sort<string>(Keys, TComparer<string>.Construct(
function(const L, R: string): Integer
begin
Result := ExcelCompareText(L, R);
end));
// a~b، ab / AB (مساوی، به هر ترتیبی)، a-b، -ab، abc
end;
TArray.Sort پایدار نیست، پس کلیدهایی که مساوی مقایسه میشوند مثل ab و AB ممکن است به هر ترتیبی دربیایند؛ اگر ترتیب اصلی کلیدهای مساوی مهم است، یک آرایه اندیس را با موقعیت اصلی بهعنوان کلید ثانویه مرتب کنید. حالت برعکسش هم پیش میآید: گاهی یک ستون نباید از ترتیب Excel پیروی کند، مثلاً شماره قطعهها که X-100 و X100 کدهای متمایزند و باید بر اساس کدپوینت مرتب شوند. TXLSXWorksheet.SortRange یک overload دارد که یک TXLSSortCompareEvent میگیرد، متدی با امضا function(const Left, Right: Variant): Integer of object، و بهجای مقایسه داخلی از آن استفاده میکند
uses
System.SysUtils, System.Variants, lxStandard, lxHandleX;
type
TPartNumberOrder = class
function Compare(const Left, Right: Variant): Integer;
end;
function TPartNumberOrder.Compare(const Left, Right: Variant): Integer;
begin
// یک comparer سفارشی سلولهای خالی را هم میگیرد (بهشکل Null): خودتان جایشان را بدهید
if VarIsNull(Left) or VarIsNull(Right) then
Exit(Ord(VarIsNull(Left)) - Ord(VarIsNull(Right)));
Result := CompareStr(VarToStr(Left), VarToStr(Right)); // ordinal، حساس به بزرگی حروف
end;
var
Sheet: TXLSXWorksheet; // یک شیت پرشده، سطرهای 2 تا 501، ستونهای A تا D
Order: TPartNumberOrder;
begin
// ...
Order := TPartNumberOrder.Create;
try
// کلید بر ستون A، صعودی
Sheet.SortRange(2, 1, 501, 4, [1], [False], xlsSortByRows,
xlsSortExcelLike, Order.Compare);
finally
Order.Free;
end;
end;
وقتی comparer سفارشی داده میشود، HotXLS مدیریت خالی خودش را کنار میگذارد و مقدارهای خام کلید را میفرستد، پس comparer باید با Null کنار بیاید. برای کلید نزولی HotXLS هر چیزی که comparer برمیگرداند منفی میکند، که خالیها را هم میبرد بالای لیست مگر اینکه comparer خودش حسابشان کند. در نظر بگیرید که ستونی که اینطور مرتب شده دیگر در ترتیبی نیست که VLOOKUP تقریبی Excel یا XLOOKUP مبتنی بر جستوجوی دودویی انتظارش را دارد؛ دامهای این مدها روی داده مرتبشده به ترتیبی دیگر در راهنمای مدهای جستوجوی دودویی XLOOKUP و XMATCH پوشش داده شده
چرا ممکن است همان workbook روی دستگاه دیگری جور دیگری مرتب شود؟
همان workbook میتواند روی دستگاه دیگری جور دیگری مرتب شود، چون ترتیب متنی Excel به locale کاربر Windows وابسته است و HotXLS عمداً همین وابستگی را دنبال میکند. word sort به زبان وابسته است: collation سوئدی مثلاً ä را بعد از z میگذارد، در حالی که انگلیسی و آلمانی کنار a نگهش میدارند. Excel این را از locale ای که زیرش اجرا میشود به ارث میبرد، پس workbookی که همکار استکهلمی دوباره محاسبه کند میتواند COUNTIF(...,">y") متفاوتی از همان فایل روی دسکتاپی در شیکاگو برگرداند. HotXLS مقدار LOCALE_USER_DEFAULT را پاس میکند تا نتایجش روی همان دستگاه با Excel برابر باشد؛ هر locale ثابتی باعث میشد HotXLS روی هر دستگاهی با تنظیم متفاوت با Excel ناهمخوان شود
سه نتیجه عملی برای تولید سمت-سرور دنبال دارد:
- locale ای که اهمیت دارد مالِ حسابی است که پروسه زیرش اجرا میشود. یک Windows service یا application pool مربوط به IIS ممکن است فرمت منطقهای متفاوتی از دسکتاپ توسعهدهنده داشته باشد، پس نتیجههایی که در IDE دیدهاید خودکار همان چیزی نیست که production محاسبه میکند
- نتیجههای فرمول cacheشدهای که داخل فایل نوشته میشوند locale دستگاه مولد را منعکس میکنند. Excel با locale خودش دوباره محاسبه میکند، پس یک مقدار میتواند وقتی فایل جای دیگری باز و دوباره محاسبه میشود عوض شود؛ این رفتار Excel است نه artifact مربوط به HotXLS
- localeها بیشتر روی حروف دارای اکسان اختلاف دارند، روی ترکیبهای حرفی که بعضی زبانها یک حرف حساب میکنند، و روی scriptهای غیر-لاتین، پس داده تستی محدود به کلمات ساده انگلیسی مشکل را لو نمیدهد
مرز پلتفرم ساده است. HotXLS یک کتابخانه Windows است، ساختهشده برای Win32 و Win64 با Delphi و C++Builder و برای targetهای win32 / win64 با Lazarus و Free Pascal، و همه این buildها همان CompareStringW را صدا میزنند. مسیر collation جداگانهای برای غیر-Windows وجود ندارد. تنها fallback برای فراخوانی ناموفق API است: اگر CompareStringW صفر برگرداند، XlsCompareText رشتههای به-حروف-بزرگشده را بر اساس code unit مقایسه میکند بهجای وسط یک محاسبه مجدد exception دادن، که محاسبه را روشن نگه میدارد اما دیگر ترتیب Excel را تضمین نمیکند
مرجع سریع: مقایسه متن اکسل در HotXLS
- قاعده: word sort با locale کاربر و
NORM_IGNORECASE، بدونSORT_STRINGSORT، بدونNORM_IGNOREWIDTH، در HotXLS از v2.384.67 -و'فقط تساوی را میشکنند:="a-b">"ab"برابر TRUE است و="a-b"="ab"برابر FALSE- بقیه علائم نگارشی قبل از ارقام مرتب میشوند و ارقام قبل از حروف:
="a~b"<"ab"و="a0"<"ab"برابر TRUE هستند - بزرگی حروف هرگز مهم نیست:
="ABC"="abc"برابر TRUE است وVLOOKUP("ABC",...)مقدارabcرا پیدا میکند - مسیرهای پوششدادهشده: عملگرهای مقایسه، مقایسههای آرایه، معیارهای
>/<، VLOOKUP/HLOOKUP، ترتیبدهی آرایه پویا، SortRangeدر هر دو موتور - مشمول این قاعده نیستند: نوعهای مخلوط (عدد < متن < boolean) و معیارهای wildcard، که قواعد خودشان را دارند
- در کد Delphi:
CompareStringW(LOCALE_USER_DEFAULT, NORM_IGNORECASE, ...)، صفر را چک کنید، CSTR_EQUALرا کم کنید؛ ازCompareTextوCompareStrوTComparer<string>.Defaultپرهیز کنید وقتی نتیجه باید با Excel جور باشد - نتیجهها به locale حسابی بستگی دارند که کد را اجرا میکند، هم در Excel و هم در HotXLS
کلمات معمولی زیر هر قاعدهای یکطور مرتب میشوند، پس فقط کدهای خط-تیرهدار و علائم نگارشی و اسمهای دارای اکسان یک collation غلط را لو میدهند. HotXLS حالا روی همه اینها جواب Excel را در هر دو موتور XLS و XLSX میدهد. جزئیات لایسنس و نسخههای پشتیبانیشده Delphi و C++Builder و دانلود آزمایشی در صفحه کامپوننت Excel مربوط به HotXLS در Delphi است