شحن HotXLS، مكوّن جداول Excel الأصلي لـ Delphi و C++Builder، إصلاحَين مترابطين لـ AGGREGATE في سبتمبر 2026. صحح الإصدار 2.382.0 وسيط الخيارات حتى يتجاهل الكود 1/3/5/7 الصفوف المخفية، ويتجاهل 2/3/6/7 قيم الأخطاء، ويتجاهل 0 حتى 3 خلايا SUBTOTAL و AGGREGATE المتداخلة، تماماً كما توثق Microsoft. ثم أوقف الإصدار 2.382.3 أعلامَ التحديد تلك عن التسرب إلى تقييم الخلايا التي تشير إليها الدالة نفسها. العيب الأول محرج بالطريقة التي تكون بها دائماً عيوب نسخ الجداول: مواضع البتات كانت مقلوبة، فكل صيغة استخدمت كود خيارات غير صفري حصلت على سياسة لم يطلبها صاحبها. والثاني أكثر إثارة، لأنه شكل ستقابله في أي مقيِّم يستخدم حقلاً عابراً لتمرير سياق إلى تجول عودي. تجميع خارجي يسلّح علماً، ويتجول في مدى، ويسحب خلية صيغتها لم تحسب بعد. تلك الصيغة تجري على الحاسبة نفسها وترى العلم المسلح نفسه فتجمّع بهدوء الصفوف الخطأ، مُنتجةً عدداً منحرفاً بمقدار لا يفسره أحد من نص الصيغة وحده
ماذا تختار خيارات AGGREGATE من 0 إلى 7 فعلاً؟
وسيط خيارات AGGREGATE مصفوفة من ثلاث بتات، والبتات الثلاث مستقلة. البت 0 (القيمة 1) يعني تجاهل الصفوف المخفية، والبت 1 (القيمة 2) يعني تجاهل قيم الأخطاء، والبت 2 (القيمة 4) يعني التوقف عن تجاهل خلايا SUBTOTAL و AGGREGATE المتداخلة، لأن تخطيها هو الافتراضي للأكواد الدنيا. أمران في هذا سهلا الالتباس. بت الصفوف المخفية هو البت الأدنى لا الأوسط، فـ AGGREGATE(9,1,...) هي صيغة الإجمالي المرشح و AGGREGATE(9,2,...) هي المتسامحة مع الأخطاء. وسياسة التجميع المتداخل معكوسة بالنسبة إلى الاثنتين الأخريين: الأكواد 4 حتى 7 وحدها تعامل خلية صيغتها الخاصة SUBTOTAL أو AGGREGATE كقيمة عادية. ويعرّف ECMA-376 الجزء 1 §18.17.7 تابع SUBTOTAL بالتقسيم ذاته شمولاً أو استثناءً للصفوف المخفية عبر الأكواد 1-11 و101-111، وتعمّم AGGREGATE، المخزنة في ملفات OOXML تحت البادئة _xlfn.، ذلك التقسيم في وسيط الخيارات، فالجدول الذي تنشره Microsoft لدالة AGGREGATE عقد يجب على المحرك الوفاء به لا ميسّرة
| الخيار | الصفوف المخفية | قيم الخطأ | SUBTOTAL / AGGREGATE متداخلة |
|---|---|---|---|
| 0 | مشمولة | تنتشر | متجاهلة |
| 1 | متجاهلة | تنتشر | متجاهلة |
| 2 | مشمولة | متجاهلة | متجاهلة |
| 3 | متجاهلة | متجاهلة | متجاهلة |
| 4 | مشمولة | تنتشر | مشمولة |
| 5 | متجاهلة | تنتشر | مشمولة |
| 6 | مشمولة | متجاهلة | مشمولة |
| 7 | متجاهلة | متجاهلة | مشمولة |
لماذا كانت خيارات AGGREGATE في HotXLS معكوسة؟
لأن TXLSCalculator.CalcAggregateFunc الأصلية كُتبت من إعادة صياغة للجدول لا من الجدول. حسبت ignoreErrors := (optCode >= 4) and (optCode <= 7) وسلّحت بوابة الصفوف المخفية للأكواد 2 و 3 و 6 و 7، بينما لم تُنفَّذ سياسة التجميع المتداخل إطلاقاً. وسردت المقالة الأسبق عن الصفوف المخفية في SUBTOTAL و AGGREGATE تلك الفجوة كحدود مفتوحة ووصفت الربط القديم كما شحن حينها؛ وكان الوصف صادقاً عن الكود وخطأً عن Excel، ولم ينتبه أحد لمدة طويلة لأن السياستين اللتين يجمعهما معظم الناس، مخفي زائد أخطاء، تهبطان على الكودين 3 و 7 تحت الجدولين معاً. أكواد البت الواحد وحدها كشفت القلب: أعادت AGGREGATE(9,1,A1:A4) المجموع غير المرشح، وتخطت AGGREGATE(9,2,...) الصفوف المخفية بينما ما زالت تنشر #DIV/0!. برز العيب من مراجعة ساكنة لـ lxCalc.pas، سُجل بـ HXLS-008 في سجل المشكلات المعروفة للمشروع، لا من ملف عميل، وهذا يقول شيئاً عن ندرة ظهور أكواد البت الواحد في مصنفات الإنتاج. أعاد الإصدار 2.382.0 كتابة فك الترميز كثلاث اختبارات عضوية في مجموعات وأضاف بوابة ثانية لسياسة التداخل، موصولة عبر رد اتصال جديد TXLSIsSubtotalCell يوفره دفتر العمل إلى جانب TXLSIsRowHidden
// TXLSCalculator.CalcAggregateFunc، صيغة v2.382.3
if (optCode < 0) or (optCode > 7) then
begin
Result := lxErrorValue; // يرفض Excel الأكواد خارج 0..7
Exit;
end;
ignoreErrors := optCode in [2, 3, 6, 7];
prevIgnoreHidden := FIgnoreHiddenRows;
prevIgnoreSubtotal := FIgnoreSubtotalCells;
FIgnoreHiddenRows := (optCode in [1, 3, 5, 7]) and Assigned(FIsRowHidden);
FIgnoreSubtotalCells := (optCode in [0, 1, 2, 3]) and Assigned(FIsSubtotalCell);
try
// ... اربط function_num بـ iftab الداخلي وجول في ref1..refN ...
finally
FIgnoreHiddenRows := prevIgnoreHidden;
FIgnoreSubtotalCells := prevIgnoreSubtotal;
end;
لاحظ أن العَلَمَين يُسندان بلا شرط لا يُضبطان فقط عندما يطلب الخيار ذلك. ما زالت نسخة v2.382.0 تستخدم if ... then FIgnoreHiddenRows := True، مما يعني أن AGGREGATE بالكود 4 متداخلة داخل SUBTOTAL(109, ...) ورثت بوابة الصفوف المخفية الخارجية بدل أن تمسحها. إسناد القيمة المفكوكة عند الدخول واستعادة القيمة السابقة في كتلة finally يجعل كل استدعاء AGGREGATE يملك سياسته طوال مدة تجوله ولا أكثر. كما جعل الإصدار 2.382.0 صيغة المصفوفة صادقة: عندما يقيّم وسيط إلى مصفوفة Variant أحادية أو ثنائية البعد، تجول CalcAggregateFunc الآن كل عنصر وتطبق سياسة الخطأ لكل عنصر، حيث كان الكود القديم يفحص NaN مضاعفاً فقط ويسلّم المصفوفة كلها لـ ExcelSum بخلاف ذلك
لماذا يتسرب AGGREGATE خارجي إلى الصيغ التي يشير إليها؟
لأن FIgnoreHiddenRows و FIgnoreSubtotalCells حقول على الحاسبة، والحاسبة يتشاركها كل صيغة تقيَّم أثناء إعادة حساب واحدة. صُممت البوابات كحقول مسودة تحديداً حتى تستشيرها ست حلقات تجول خلايا دون تمرير وسيط عبر كل توقيع، وذلك التصميم سليم ما دام كل ما يجري بينما البوابة مسلحة يخص التجميع الذي سلّحها. تنكسر الافتراضة عند نقطة واحدة بعينها: FGetValue. عندما يطلب مجول من دفتر العمل قيمة خلية وتحمل تلك الخلية صيغة بلا نتيجة مخزنة، يجمع دفتر العمل الصيغة ويقيّمها في الحال، على TXLSCalculator نفسها، والبوابتان الخارجيتان ما زالتا مضبوطتين. تُظهر عينة الانحدار في HotXLS.WorkbookApiTests.pas الفشل بأربع خلايا. تحمل A1 القيمة 10، و A2 القيمة 20 على صف مخفي، و A3 تحمل =1/0، و A4 تحمل =SUBTOTAL(9,A1:A2) وقيمتها الصحيحة 30. قيّم الآن =AGGREGATE(9,7,A1:A4): تجاهل الصفوف المخفية، وتجاهل الأخطاء، وعُدّ المجموع الفرعي المتداخل قيمة. يعيد Excel القيمة 10 + 30 = 40. ومع A4 غير مخزنة، سلّح المحرك ما قبل 2.382.3 بوابة الصفوف المخفية، وتجول إلى A4، وأشعل تقييمها، ورثت CalcSubtotalFunc للكود 9 البوابة المسلحة، لأنها لا تضبط العلم إلا للأكواد 101 حتى 111 ولا تمسحه قط. قيّمت A4 إلى 10 بدل 30، وعاد الإجمالي الخارجي 20. ولا شيء في أياً من الصيغتين يذكر الصفوف المخفية على المسار الذي أنتج العدد الخطأ
وسربت بوابة التجميع المتداخل بالطريقة نفسها في الاتجاه الآخر. مع الأكواد 0 حتى 3 يكون FIgnoreSubtotalCells مسلحاً، ويحترمه المجول العام للمدى في GetValueItemRange، فيسقط سلفٌ صيغته =SUM(B1:B3) بهدوء B2 إذا صادف أن B2 تحوي SUBTOTAL. والأسوأ أن CalcSubtotalFunc تعيد ضبط FIgnoreSubtotalCells إلى False عند الخروج بدل استعادة القيمة السابقة، فسلف SUBTOTAL غير مخزن بلغ في منتصف التجول حَلّ سلاح البوابة الخارجية عن كل خلية بعده. يسجل سجل المشكلات المعروفة للمشروع ذلك تحت HXLS-008 بوصفه تسرب حالة تحديد متداخلة، وذلك هو الاسم الصحيح لصنف العيب: عَلَم عابر عام صحيح للإطار الذي ضبطه وخطأ لكل إطار يرثه
كيف يعزل AggregateGetCellValue و AggregateGetItemValue التجول؟
يضع الإصلاح في v2.382.3 حداً حول كل نقطة تقرأ فيها AGGREGATE قيمة لم تحسبها هي. تلف TXLSCalculator.AggregateGetCellValue استدعاء FGetValue الخام: تحفظ كلا العَلَمَين، وتمسحهما، وتنفذ الجلب، وتعيدهما في كتلة finally. ما زال التجميع الخارجي يطبق سياسته الخاصة على الخلية التي جلبها للتو، لأن اختبارَي الصفوف المخفية والخلية المتداخلة يجريان في المجول حول الجلب، لكن صيغة السلف ذاتها تجري بلا أي سياسة، وهو ما يفعله Excel
function TXLSCalculator.AggregateGetCellValue(SheetIndex, Row, Col: Integer;
var Value: Variant; var OutOfRange: Boolean): Integer;
var
Hidden, Nested: Boolean;
begin
Hidden := FIgnoreHiddenRows;
Nested := FIgnoreSubtotalCells;
FIgnoreHiddenRows := False; // صيغة السلف تملك سياستها الخاصة
FIgnoreSubtotalCells := False;
try
Result := FGetValue(SheetIndex, Row, Col, Value, OutOfRange);
finally
FIgnoreHiddenRows := Hidden;
FIgnoreSubtotalCells := Nested;
end;
end;
تفعل AggregateGetItemValue الشيء نفسه للوسائط غير المداهات، وعليها أن تفعل أكثر من مسح العلامات، لأن وسيطاً مثل A1:A4/(B1:B4-20) مصفوفة محسوبة يجب أن ينجو شكل عناصرها. تجسد الغلاف مدى عادياً إلى مصفوفة Variant ثنائية البعد عبر AggregateGetCellValue، وتربط خلية أعادت رمز خطأ بـ VarAsError حتى ما زال يمكن تطبيق سياسة الخطأ لكل عنصر، وتتكرر عبر عقد المعاملات الثنائية والأحادية (SA_ADD و SA_DIV و SA_UNARMINUS وسائرها) بـ ApplyArrayBinaryOp و ApplyArrayUnaryOp؛ وغير ذلك يهبط إلى GetValueItem العادية. حارسان أمام التجسيد: مدى أكبر من EffectiveFormulaArrayMemoryLimit يعيد lxErrorResourceLimit، ومدى متعدد الأوراق أو معكوس يعيد #VALUE!. ورمز حد الموارد لا يعامل عمداً كخطأ خلية قابل للتجاهل حتى تحت الخيارات 2/3/6/7، لأن محركاً ابتلع إشارته عن نفاد الذاكرة لأن المستخدم طلب تخطي #N/A كان يكذب. وحُوّلت جميع تجاولات AGGREGATE الثلاثة، AggregateCollectRange لعائلة SUM، و AggregateReduceVariance لـ STDEV و VAR و PRODUCT، و AggregateReduceWithK لـ MEDIAN وصيغ المئينات، من FGetValue و GetValueItem إلى الغلافين، وكسب كل واحد منها اختبار الخلية المتداخلة عبر FIsSubtotalCell
أي خطأ تعيد AGGREGATE حين لا تتجاهل الأخطاء؟
الأصلي، منذ v2.382.3. كشف الإصدار 2.382.0 خلايا الأخطاء صحيحاً لكنه انهار كل واحدة منها إلى lxErrorValue، فأعادت AGGREGATE(9,4,A1:A3) فوق خلية #DIV/0! القيمة #VALUE!، حيث ينشر Excel أول خطأ يقابله دون تغيير. تربط المساعدة البديلة AggregateErrorCode قيمة Variant برمز lxError* المطابق، سواء كانت القيمة varError حقيقية أو إحدى سلاسل الأخطاء السبع، وصارت AggregateValueIsError مجرد فحص لناتج غير صفري. يسجل كل مجول أول رمز خطأ يراه ويعيد ذلك الرمز، وهو ما يعني أيضاً أن خلية لم تُحسب صيغتها قط، ويصل خطؤها إذن كرمز إرجاع من FGetValue لا كقيمة Variant مخزنة، تنتشر بالطريقة نفسها التي تنتشر بها المخزنة. وتحصل دالتا عدّ على معاملة خاصة داخل AggregateCollectRange، والمعاملة تتبع SUBTOTAL لا SUM. للدالة الداخلية 0، أي COUNT، لا تُعد خلية خطأ قط ولا تنتشر أبداً أياً كان كود الخيارات، لأن COUNT تعدّ الأعداد وحدها. وللدالة الداخلية 169، أي COUNTA، خلية الخطأ قيمة غير فارغة وتعدّ 1 إلا إذا تجاهل كود الخيارات الأخطاء، وفي تلك الحالة تتخطى. تلك اللامتماثلية هي كيف يعامل Excel تابعي COUNT و COUNTA خارج AGGREGATE أيضاً، وهي من نوع التفاصيل التي تخطئ فيها بصمت قاعدة عامة مثل "إن كان خطأً فانشره"
ماذا تتحقق منه مصفوفة انحدار الخيارات الثمانية؟
تُجسَّد العينة الموصوفة أعلاه كمصفوفة كاملة في AggregateFunc_OptionMatrixCoversHiddenErrorsAndNestedAggregates: لكل كود خيارات من 0 إلى 7 تقيّم صيغة SUM وصيغة MEDIAN معاً فوق A1:A4 وتفحص النتيجة مقابل توقع مشتق يدوياً. يجب أن تنشر الأكواد 0 و 1 و 4 و 5 خطأ #DIV/0! من A3، لأن ولا واحدة منها تتجاهل الأخطاء. يعطي الكود 2 لمجموع SUM قيمة 30 ولمتوسط MEDIAN قيمة 15، من 10 و 20 مع تخطي A4 المتداخلة. ويعطي الكود 3 قيمتي 10 و 10. ويعطي الكود 6 قيمتي 60 و 20، لأن الـ 30 في A4 تعدّ الآن. ويعطي الكود 7 قيمتي 40 و 20، وهي الحالة التي كانت تعيد 20 قبل إصلاح التسرب. وجولة القبول الأوسع المسجلة في سجل المشكلات المعروفة تغطي أعداد الدوال التسعة عشر كلها مقابل الأكواد الثمانية كلها، وكل سلف مخزناً وغير مخزن، لعدة 304 سيناريو على Win32 و Win64
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Data');
Sheet.Cells[1, 1].Value := 10;
Sheet.Cells[2, 1].Value := 20;
Sheet.Cells[3, 1].Formula := '=1/0';
Sheet.Cells[4, 1].Formula := '=SUBTOTAL(9,A1:A2)'; // المجموع الفرعي للمجموعة = 30
Sheet.RowHidden[2] := True;
Sheet.Cells[6, 1].Formula := '=AGGREGATE(9,1,A1:A4)'; // #DIV/0! تجاهلت المخفية وينتشر الخطأ
Sheet.Cells[7, 1].Formula := '=AGGREGATE(9,3,A1:A4)'; // 10 مخفية + خطأ + متداخل متجاهلة
Sheet.Cells[8, 1].Formula := '=AGGREGATE(9,6,A1:A4)'; // 60 تُتخطى الأخطاء وحدها
Sheet.Cells[9, 1].Formula := '=AGGREGATE(9,7,A1:A4)'; // 40 كانت 20 قبل v2.382.3
Book.Recalculate;
Book.SaveAs('aggregate-options.xlsx');
finally
Book.Free;
end;
end;
أين ما زال الحد؟
ثلاثة حدود تعرفها قبل أن تبني على هذا. أولاً، محمول التجميع المتداخل نصي. يجيب TXLSXWorkbook.GetCalcIsSubtotalCell وتوَأمُه في المحرك الكلاسيكي بالقيمة True عندما تبدأ صيغة خلية بـ SUBTOTAL( أو AGGREGATE( أو _xlfn.AGGREGATE(، بعلامة يساوي المبتدئة أو بدونها، فصيغة مثل =IF(C1,SUBTOTAL(9,B1:B9),0) أو =SUBTOTAL(9,B1:B9)*2 لا تُعرف كمتداخلة وستُعدّ مرتين بواسطة الأكواد 0 حتى 3 حيث كان Excel سيتخطاها؛ فالمولد الذي يصدر مجموعات فرعية محسوبة عليه أن يُبقي استدعاء التجميع في رأس الصيغة. ثانياً، العزل يسكن في مجاولات AGGREGATE الثلاثة. ما زالت CalcSubtotalFunc تجول عبر GetValueItemRange و CollectRangeValues و SubtotalReduceVariance، التي تستدعي FGetValue مباشرة، فيمكن لـ SUBTOTAL(109, ...) مداها يحوي صيغة سلف غير مخزنة أن يمرر بوابة صفوفه المخفية إلى ذلك السلف. تقيّم Recalculate الكاملة السلفَ قبل التابعين، فيؤخذ المسار المخزن ولا يورث البوابة قط؛ والتعرض محدود بالتقييم المخصوص عبر Calculate وبالمصنفات المحمّلة دون قيم مخزنة، وإن كنت تعتمد على إعادة الحساب التزايدية فوق رسم التبعية لإبقاء النماذج الكبيرة مستجيبة، فضمانة الترتيب نفسها هي ما يبقي هذا التسرب ساكناً. ثالثاً، كلا البوابتين مشروطتان بـ Assigned(FIsRowHidden) و Assigned(FIsSubtotalCell). تربط واجهتا دفتر العمل كلا رد الاتصال في بانيَيهما، لكن كوداً يبني TXLSCalculator يدوياً بالوسيطين الأصليين وحدهما يحصل على سلوك الإشمال-الكل التراثي لكل كود خيارات، بصمت. وحين يبدو إجمال خاطئاً ونص الصيغة صحيحاً، فـ تتبع التقييم خطوة بخطوة أسرع طريق لترى هل قُيّم سلف تحت بوابة موروثة أم أن رد اتصال لم يُربط ببساطة
محرك الحساب الموصوف هنا، وفاكّ ترميز الخيارات، وغلافا الجلب المعزولان، ومصفوفة الانحدار التي تثبّتها جميعاً، تشحن كلها بالمصدر مع مكوّن HotXLS Delphi spreadsheet، الذي يقرأ ويكتب ويعيد حساب مصنفات XLS و XLSX و ODS في Delphi و C++Builder دون تثبيت Excel