يقيّم مكوّن HotXLS لـ Delphi المعادلة =1<2<3 فتعيد FALSE، وهي الإجابة نفسها التي يعيدها Excel 16، لأن محلل الصيغ فيه يطوي منذ v2.384.3 عوامل المقارنة من اليسار إلى اليمين: تصير 1<2 قيمة TRUE، ثم TRUE<3 تعطي FALSE لأن القيمة المنطقية تتربع فوق كل عدد. والإصدار نفسه يجعل المعامل الفارغ مساويًا لـ 0 ولـ "" معًا، ويسمح لـ SUMIF بمدّ مدى جمع من خلية واحدة إلى شكل مدى المعايير. كل واحدة من هذه تبدو تفصيلًا هامشيًا حتى يخالف مصنف حُسب في Delphi المصنف نفسه مفتوحًا في Excel
يبدأ الخلاف عادة بصيغة كتبها إنسان بالحدس. يكتب أحدهم =0<B2<100 ليتحقق أن الكمية داخل المدى، فيجيب Excel بهدوء FALSE لكل صف، وتنطلق الورقة والعيب مدفونًا فيها. محرك الحساب لا يملك حق إصلاح قصد المستخدم؛ وظيفته أن ينتج القيمة التي كان سيخرج بها Excel، حتى يطابق النتيجة المخبأة التي يكتبها HotXLS في الملف ما يعرضه Excel بعد إعادة حساب. قبل v2.384.3 كان HotXLS يجيب TRUE لفحص المدى ذاك في كل صف، خطأً في الاتجاه المعاكس، وكان تقرير يُولَّد على خادم سيناقض التقرير نفسه مفتوحًا على سطح مكتب
لماذا تعيد =1<2<3 القيمة FALSE في Excel؟
تعيد Excel FALSE لأنها تقرأ سلسلة مقارنات بوصفها (1<2)<3، فخسرت قيمة TRUE الداخلية منافسة الترتيب النوعي أمام العدد 3. أما محلل HotXLS القديم فكان يقرأ النص نفسه بوصفه 1<(2<3): كانت TXLSSyntax.Parse_expr في lxFormula.pas تحلل معاملًا واحدًا، فترى رمز مقارنة، ثم تستدعي نفسها داخل Parse_expr لجانب اليمين، وهو ما يجعل العامل ترابطيًا يمينًا. كان الناتج 1<TRUE، والعدد أدنى من القيمة المنطقية، فكانت النتيجة TRUE. الخطأ متناظر: =3>2>1 تعطي TRUE في Excel وكانت تعطي FALSE في HotXLS، و=1=1=TRUE تعطي TRUE في Excel وكانت تعطي FALSE قبل الإصلاح. ويثبّت اختبار الانحدار CalculateFormula_ComparisonChainsFoldLeftToRight سبع صيغ من هذا النوع مقابل القيم التي يعيدها Excel 16، ويشغّل كل واحدة عبر بنيتي المحرك على السواء، أي TXLSWorkbook الكلاسيكي وTXLSXWorkbook المولود أصلًا من XLSX، باستخدام أسلوب Calculate المشروح في نظرة عامة على محرك صيغ HotXLS
const
Formulas: array [0..6] of string = ('=1<2<3', '=3>2>1', '=(1<2)<3',
'=1<(2<3)', '=1=1=TRUE', '=3>2>1=TRUE', '=1<2<3=FALSE');
// ما تعيده Excel 16: FALSE وTRUE وFALSE وTRUE وTRUE وTRUE وTRUE
var
Classic: IXLSWorkbook;
Xlsx: TXLSXWorkbook;
i: Integer;
begin
Classic := TXLSWorkbook.Create;
Xlsx := TXLSXWorkbook.Create;
try
// تقيّم TXLSXWorkbook.Calculate مقابل الورقة النشطة، وتعيد Null
// عندما لا يملك المصنف أي ورقة على الإطلاق
Xlsx.Sheets.Add('Data');
for i := 0 to High(Formulas) do
Writeln(Formulas[i], ' classic=', VarToStr(Classic.Calculate(Formulas[i])),
' xlsx=', VarToStr(Xlsx.Calculate(Formulas[i])));
finally
Xlsx.Free;
end;
end;
يحوّل الإصلاح Parse_expr إلى حلقة من الشكل نفسه الذي تستخدمه Parse_expr1 سلفًا لمعاملي + و- و&. تحلل المعامل الأول عبر Parse_expr1، وما دام الرمز التالي أحد الرموز = و<> و< و> و<= و>=، تنشئ عقدة مقارنة، وتعلّق النتيجة اليسرى المتراكمة طفلًا أولًا، وتحلل المعامل التالي عبر Parse_expr1 لا عبر Parse_expr، وتجعل العقدة الجديدة هي النتيجة اليسرى للجولة التالية. تفصيلان كان من السهل الخبط فيهما عند تحويل الاستدعاء الذاتي إلى تكرار، وكلاهما في ملاحظات المشرفين: يجب تسليم العقدة المتراكمة بترتيب lChild := Item; Item := nil تحديدًا، وعلى مسار الخطأ أن يخرج بـ Exit بعد تحرير العقدة نصف المبنى بدل أن يقع من الحلقة ويعيد شجرة معلقة بلا مرجع
كيف يرتب HotXLS الأعداد والنصوص والقيم المنطقية في مقارنة؟
يرتب HotXLS الأنواع المختلطة كما تفعل Excel: كل عدد أصغر من كل قيمة نصية، وكل قيمة نصية أصغر من كل قيمة منطقية. يصنف TXLSCalculator.CompareVariants في lxCalc.pas المعاملين عبر GetRetValueType إلى التعداد TXLSRetValueType = (xlVariantValue, xlNumberValue, xlStringValue, xlBooleanValue)، وحين يختلف الصنفان يقارن رقتيهما ببساطة، فترتيب الإعلان في ذلك التعداد هو قاعدة العبور بين الأنواع. وداخل الصنف الواحد المقارنة هي الطبيعية، مع لفتة خاصة بـ Excel للنص: يمر النصان عبر lxUpperCase أولًا، فتكون ="abc"="ABC" قيمتها TRUE. وهذا الترتيب هو سبب استحالة التفكير في نتيجة السلسلة بدونه. إن TRUE<3 ليست تحويلًا لـ TRUE إلى 1، بل قيمة منطقية تقارن بعدد، والمنطقية هي الفائزة. التواريخ أرقام تسلسلية بالنسبة للمحرك (varDate يصنف xlNumberValue)، فالتاريخ يظل دائمًا دون أي نص، بما في ذلك نص يصدف أنه يشبه تاريخًا
بماذا تتساوى الخلية الفارغة في مقارنة؟
الخلية الفارغة بوصفها معامل مقارنة تتساوى مع 0 إذا كان الطرف الآخر عددًا، ومع "" إذا كان الطرف الآخر نصًا، ومنذ v2.384.53 تتساوى مع FALSE إذا كان الطرف الآخر قيمة منطقية، فمع A1 فارغة تكون =A1=0 و=A1="" و=A1=FALSE كلها TRUE. تستبدل TXLSCalculator.CompareVarValues، التي تخدم عوامل المقارنة الستة كلها، الفارغة قبل استدعاء CompareVariants: إذا كان معامل واحد بالضبط Null صار WideString('') عندما يكون رفيقه نصًا، وFalse عندما يكون رفيقه قيمة منطقية، وصفرًا فيما سوى ذلك. ولا تزال فارغتان تتساويان معًا دون استبدال. كان مسار الحساب يحول الفارغة إلى 0 دائمًا، ولهذا كانت =A1+1 تعطي 1، لكن CompareVariants كانت تُبقي Null رتبته أدنى خاصة، دون كل عدد، وعوامل المقارنة كانت تستخدم تلك الرتبة مباشرة
var
Wb: IXLSWorkbook;
Sh: TXLSWorksheet;
begin
Wb := TXLSWorkbook.Create;
Sh := Wb.Sheets.Add;
Sh.Range['B1', 'B1'].Value := 5; // تُترك A1 فارغة عمدًا
Writeln(VarToStr(Wb.Calculate('=A1=0'))); // True
Writeln(VarToStr(Wb.Calculate('=A1=""'))); // True
Writeln(VarToStr(Wb.Calculate('=A1<B1'))); // True: تقارن الفارغة بوصفها 0
Writeln(VarToStr(Wb.Calculate('=A1<0'))); // False؛ وكانت True قبل v2.384.3
end;
السطر الأخير هو الذي ألم بالممارسة فعلًا. تحت الرتبة القديمة كانت الفارغة أصغر من كل عدد، السالبة منهم، فكانت =IF(A1<0,"overdrawn","ok") تسم كل خلية رصيد فارغة بأنها تجاوزت السحب، وكانت =A1=0 تعطي FALSE لخلية يصفها أي مستخدم بأنها صفر. بقي حد واحد بعد v2.384.3: كان الاستبدال يختار بين 0 والنص الفارغ وحدهما، فالفارغة المقارنة بقيمة منطقية أصبحت 0، وهي أدنى من TRUE ومن FALSE معًا، وكانت =A1=FALSE على A1 فارغة تُقيَّم إلى FALSE. ومنذ HotXLS 2.384.53 تُعامل الفارغة المقارنة بقيمة منطقية معاملة FALSE في محركي XLS وXLSX كما تفعل Excel: مع A1 فارغة تعيد =A1=FALSE و=A1<TRUE القيمة TRUE ويعيد =A1=TRUE القيمة FALSE. هذا يعني أيضًا أن المقارنة لا تستطيع تمييز الفارغة عن FALSE، في Excel أو في HotXLS؛ وحين تحتاج الورقة ذلك التمييز فاختبر بـ ISBLANK أو =A1=""
لماذا كانت SUMIF بمدى جمع من خلية واحدة تعيد 0؟
كانت SUMIF تعيد 0 لأن HotXLS كان يقيّد التجوال إلى أصغر المدىين، بينما تحافظ Excel على شكل مدى المعايير ولا تستخدم مدى الجمع إلا لخليلته العليا اليسرى. إذن =SUMIF(A1:A10,">5",B1) تعني B1:B10 في Excel، وهي تسهيل تعتمد عليه قوالب كثيرة مبنية يدويًا. كان العامل المشترك TXLSCalculator.GetValueItemRange2 يقلص عددي صفوفه وأعمدته إلى عددي مدى القيم، فانحصر المثال في اختبار واحد لـ A1 مقابل B1. أزالت v2.384.3 التقييد: يجوب الحلقة الآن مدى المعايير ويقرأ كل قيمة عند الإزاحة نفسها من الزاوية العليا اليسرى لمدى الجمع. ولأن CalcSumIF وCalcAverageIF تستدعيان ذلك العامل معًا، تنال AVERAGEIF إعادة التحجيم نفسها، ومدى الجمع الأكبر من مدى المعايير يُقص إلى شكل المعايير للسبب ذاته. وسيط المعايير في الوسط من صنف القيمة والسيطان الخارجيان من صنف المرجع، وهو التمييز المغطى في مقالة التقاطع الضمني وأصناف الوسائط
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Row: Integer;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Sales');
for Row := 1 to 10 do
begin
Sheet.Cells[Row, 1].Value := Row; // عمود المعايير: من 1 إلى 10
Sheet.Cells[Row, 2].Value := Row * 100; // المبالغ: من 100 إلى 1000
end;
Sheet.Cells[1, 4].Formula := '=SUMIF(A1:A10,">5",B1)'; // مدى جمع من خلية واحدة
Sheet.Cells[2, 4].Formula := '=SUMIF(A1:A10,">5",B1:B10)'; // مدى جمع صريح
if Book.Recalculate = lxOk then
// كل من D1 وD2 يساوي 4000 (600+700+800+900+1000)؛ وكانت D1 تساوي 0 قبل v2.384.3
Writeln(VarToStr(Sheet.Cells[1, 4].Value), ' ', VarToStr(Sheet.Cells[2, 4].Value));
finally
Book.Free;
end;
end;
INDIRECT وYEARFRAC: تصحيحان أهدأ
يحترم INDIRECT الآن وسيطه الثاني، والنص بعد مرجع صالح صار خطأ بدل أن يُتجاهل. مع a1 يساوي FALSE يُحلل النص بصيغة R1C1 مطلقة، فتقرأ =INDIRECT("R2C3",FALSE) الخلية C2؛ كان الكود القديم يتجاهل العلم، فيقرأ "R2" بوصفها العمود R والصف 2، ويعيد بصمت الخلية الخطأ. يُوزَّع العلم بحسب نوع الـ Variant الخاص به (منطقي أو عدد أو نص) لأن تحويل نص Variant مباشرة إلى Double يرفع استثناء. نص R1C1 نسبي مثل R[1]C[1] يعيد #REF!، لأن INDIRECT لا يملك أصل خلية صيغة يحسمه مقابله، ونص A1 بمحارف زائدة مثل "B2 junk" يعيد #REF! كذلك. وتطبق YEARFRAC بأساس 0 الآن قواعد NASD لنهاية فبراير التي نفذتها DAYS360 سلفًا: حين يكون التاريخان آخر يوم في فبراير يصير يوم النهاية 30، ثم نقطة البدء على آخر يوم في فبراير تصير 30. ومن 2024-02-29 إلى 2025-02-28 أصبح العد 360 يومًا، أي كسر يساوي 1 بالضبط، حيث كان Days360US السابق يعد 359
ماذا تضمن هذه الإصلاحات، وما الدرس؟
سلوك سلاسل المقارنات مضمون باختبار يقارن كلا المحركين بقيم مقيسة في Excel 16، وهذا الاختبار موجود لأن أول وصف للإصلاح كان خاطئًا. قالت ملاحظة إصدار v2.384.3 في الأصل إن الطي من اليسار إلى اليمين جعل =1<2<3 تساوي TRUE، وهو بالضبط ما كان ينتجه المحلل الترابطي يمينًا القديم، وعكس ما يعيده Excel والكود الجديد معًا. لم يقيّم أحد المثال؛ كُتب من الحدس القائل «1 أصغر من 2 وهي أصغر من 3». صُححت الملاحظة وأضيف اختبار الصيغ السبع في commit لاحق، والقاعدة التي خرجت منها تصلح لأي من يوثق دلالات الجداول: شغّل المثال في Excel قبل أن تكتب القيمة المتوقعة. ويتبع استبدال المعامل الفارغ وإعادة تحجيم SUMIF سلوك Excel نفسه، بما في ذلك حالة الفارغة مقابل القيمة المنطقية منذ v2.384.53، أما المجاميع الشرطية التي عليها أيضًا تخطي الصفوف المفلترة أو المخفية فتتبع القواعد المنفصلة في مقالة الصفوف المخفية في SUBTOTAL وAGGREGATE
HotXLS مكوّن جداول أصلي لـ Delphi وC++Builder يقرأ ويعيد الحساب ويكتب XLS وXLSX وODS وCSV دون تثبيت Excel، وقواعد المقارنة والفارغ وSUMIF الموصوفة هنا تسكن محرك الحساب الذي تتشاركه بنيتا المصنف معًا. قائمة الدوال الكاملة وخيارات الترخيص على صفحة منتج مكوّن HotXLS لجداول Delphi