مقال تقني

الحساب التكراري للمراجع الدائرية في Delphi

لحساب المراجع الدائرية (circular references) المقصودة في Delphi، يعرض HotXLS الحساب التكراري (iterative calculation) على محرك XLSX الخاص به: فقم بتعيين TXLSXWorkbook.Iterate إلى True ويمر TXLSXWorkbook.Recalculate عبر كل حلقة مرجعية مكتشفة نحو نقطة ثابتة — حتى عدد مرات مرور IterateCount، أو حتى تتغير كل خلية بأقل من IterateDelta — بدلاً من إرجاع الخطأ #REF! والاستسلام

ويهمنا هذا التمييز أكثر مما يوحي به المتغير البولياني البسيط؛ فنفس المحرك الذي يكتشف الحلقة المرجعية ويرفض الدوران فيها سيقوم، مع تبديل خاصية واحدة، بتقييم تلك الحلقة عمداً حتى تستقر؛ وإن إبقاء السلوكين واضحين — متى تكون الحلقة عيباً يجب الإبلاغ عنه ومتى تكون نموذجاً يجب حله — هو الموضوع الكامل لهذا المقال

لماذا تؤدي المراجع الدائرية إلى حدوث خطأ افتراضيًا؟

بشكل افتراضي، يعامل HotXLS أي حلقة مرجعية كخطأ في التأليف ويبلغ عنها بدلاً من حسابها؛ حيث يبني TXLSXWorkbook.Recalculate رسمًا بيانيًا لتبعيات الصيغ، ويقيم كل خلية صيغة بالترتيب الطوبولوجي، ويرجع lxErrorRef لحظة العثور على حلقة — وهي العقد التي لا يمكن تحريرها أبدًا أثناء الفرز الطوبولوجي؛ وتحتفظ أعضاء الحلقة تلك بقيمها القديمة المخزنة مؤقتًا، بينما لا تزال كل صيغة خارج الحلقة تقيم بشكل طبيعي؛ وميكانيكا ذلك الرسم البياني، ولماذا يتم تخطي أعضاء الحلقة بدلاً من الدوران فيها، مغطاة في المقال المصاحب حول إعادة حساب الصيغ التدريجي ورسم بياني للتبعيات

الخيار الافتراضي هو الأكثر أماناً لأن معظم الحلقات هي أخطاء برمجية: كصف ملخص تم سحبه بالخطأ إلى نطاق SUM الخاص به، أو عملية نسخ ولصق أزاحت المرجع إلى نفسه؛ ورمز خطأ واضح عند وقت إعادة الحساب هو بالضبط ما تريده لتلك الحالات؛ ولكن هناك فئة محددة وهامة من النماذج دائرية عن قصد: كجداول الفائدة المركبة، وتخصيص التكاليف أو النفقات العامة الدائرية بين الأقسام، وحسابات الرسوم المستندة إلى الرصيد، حيث تصف جميعها قيمة تغذي مدخلاتها الخاصة بشكل شرعي — ولا يقوم Excel بحسابها إلا بمجرد قيام المستخدم بتحديد ملف ← خيارات ← الصيغ ← تمكين الحساب التكراري

كيف تقوم بتمكين الحساب التكراري في HotXLS؟

يطابق HotXLS خانة الاختيار تلك لـ Excel بثلاثة خصائص في TXLSXWorkbook: حيث يشغل Iterate الوضع، وعندما يكون True يتم تسليم الحلقة المكتشفة إلى محلل تكراري بدلاً من إنتاج lxErrorRef؛ ولننظر إلى زوج خلايا الفائدة المركبة الكلاسيكي، حيث يعتمد رصيد الإغلاق على الفائدة وتعتمد الفائدة على الرصيد

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Model');
    Sheet.Cells[1, 2].Value   := 1000;     // B1: رأس المال الافتتاحي
    Sheet.Cells[2, 2].Value   := 0.05;     // B2: معدل الفترة
    Sheet.Cells[3, 2].Formula := 'B1+B4';  // B3: الرصيد = رأس المال + الفائدة
    Sheet.Cells[4, 2].Formula := 'B3*B2';  // B4: الفائدة = الرصيد * المعدل

    Book.Iterate := True;                   // تفعيل الحساب التكراري
    if Book.Recalculate = lxOk then
      // يتقارب B3 إلى 1052.63...، و B4 إلى 52.63...
      Report(Sheet.Cells[3, 2].Value);
  finally
    Book.Free;
  end;
end;

يشير B3 إلى B4 ويشير B4 إلى B3، ولذلك يبلغ رسم بياني التبعيات عن حلقة ذات عقدتين؛ ومع ترك Iterate على قيمته الافتراضية False، سيعود هذا الزوج كـ lxErrorRef ولن تستقر أي من الخليتين؛ ومع تعيينه إلى True، يقوم Recalculate ببذر الحلقة من القيم المخزنة مؤقتًا الحالية ويعيد تقييم أعضائها دورة تلو الأخرى، مغذيًا مخرجات كل دورة كمدخلات للدورة التالية، حتى تتوقف الأرقام عن الحركة؛ وهنا الصيغة المغلقة هي رأس المال / (1 - rate)، ولذلك يستقر الرصيد عند 1052.63 وتستقر الفائدة عند 52.63 — وهي نفس الأرقام التي ينتجها Excel مع تمكين التكرار

ما الذي يجعل التكرار يتوقف؟

يحد المحلل شرطا توقف مستقلان، وفهم كليهما هو ما يحمي النموذج من حدوث خطأ أو الدوران اللانهائي؛ حيث إن IterateCount هو حد أقصى صلب لعدد مرات إعادة تقييم أعضاء الحلقة، وقيمته الافتراضية ١٠٠، متطابقة مع Excel؛ و IterateDelta هو حد التقارب: فبعد كل دورة يقيس المحلل أكبر تغيير رقمي عبر جميع خلايا الحلقة، ومجرد هبوط ذلك التغيير الأقصى تحت IterateDelta — الافتراضي 0.001 — تنقطع حلقة التكرار مبكراً؛ وأي من الشرطين يتحقق أولاً ينهي التكرار

Book.Iterate := True;
Book.IterateCount := 1000;    // حد صلب: على الأكثر ١٠٠٠ دورة فوق الحلقة
Book.IterateDelta := 0.0001;  // تقارب: التوقف بمجرد تحرك كل خلية بأقل من 0.0001

case Book.Recalculate of
  lxOk:
    // تقاربت الحلقة، أو بلغت حد الـ ١٠٠٠ دورة واحتفظت بـ
    // قيم التكرار الأخير — كلا المسارين يرجع lxOk عندما يكون Iterate هو True
    SaveWorkbook(Book);
  lxErrorRef:
    // يمكن الوصول إليه فقط مع Iterate = False: تم الإبلاغ عن الحلقة، ولم يتم حلها
    LogWarning('Circular reference with iteration disabled');
end;

هناك نتيجة واحدة تستحق الذكر بوضوح، لأنها الحد الحقيقي للميزة؛ فعند الوصول إلى الحد الأقصى دون هبوط التغيير تحت IterateDelta، لا تثير الدالة SolveCycleIteratively خطأ — بل ترجع lxOk وتترك الخلايا محتفظة بقيم التكرار الأخير، تماماً كما يكتب Excel آخر الأرقام المحسوبة عندما يتم الوصول إلى حد التكرار الخاص به دون تقارب؛ ولذلك فإن رمز الإرجاع الناجح من Recalculate تحت الوضع التكراري يعني "المحلل تم تشغيله"، وليس "المحلل تقارب"؛ والنموذج الذي تتباعد فيه حلقة التغذية المرتدة أو تتذبذب سيستهلك بهدوء جميع دورات IterateCount ويعيد أرقاماً ليست نقطة ثابتة على الإطلاق، ولا يشير أي استثناء إلى هذا الفرق

كيف يتم حفظ الإعداد في ملفات XLSX و XLS؟

تستمر إعدادات الحساب التكراري في كلا تنسيقي جداول البيانات، ولذلك فإن دفتر العمل المفتوح في Excel يتصرف بالطريقة التي قام كود Delphi الخاص بك بتهيئتها؛ وعلى جانب XLSX، يصدر الكاتب عنصر OOXML <calcPr> فقط عندما يكون Iterate مضبوطًا على True، ويتجاهل أي سمة لا تزال على قيمتها الافتراضية لإبقاء المخرجات في حدها الأدنى: فدفتر العمل الذي يستخدم الافتراضيات يكتب فقط <calcPr iterate="1"/>، بينما يظهر iterateCount فقط عندما يختلف عن ١٠٠ ويظهر iterateDelta فقط عندما يختلف عن 0.001؛ وعند الفتح، يقرأ TXLSXWorkbook نفس السمات الثلاث، وبذلك تكون الدورة الكاملة متماثلة

ويحمل محرك BIFF8 (.xls) القديم، TXLSWorkbook، الحالة المكافئة من خلال ثلاثة سجلات منفصلة تحت خاصية ثلاثية مختلفة؛ حيث تطابق EnableIteration السجل CalcIter ($0011، [MS-XLS] الفقرة 2.4.33)، و MaxIterations السجل CalcCount ($000C، [MS-XLS] الفقرة 2.4.31)، و MaxIterationChange السجل CalcDelta ($0010، [MS-XLS] الفقرة 2.4.32)؛ وتفرض أدوات التعيين نطاقات المواصفات — حيث يجب أن يقع CalcCount في النطاق 1..32767، ولذلك يتم تقييد MaxIterations، وتتراجع قيمة MaxIterationChange السالبة تلقائيًا إلى القيمة الافتراضية 0.001؛ واضبطها على دفتر عمل .xls محمل وسيتم كتابة سجلات الحساب الثلاثة بأمانة عند الحفظ

var
  Book: TXLSWorkbook;   // محرك BIFF8 (.xls)
begin
  Book := TXLSWorkbook.Create;
  try
    Book.Open('model.xls');
    Book.EnableIteration    := True;    // السجل CalcIter $0011
    Book.MaxIterations      := 500;     // السجل CalcCount $000C (مقيد في النطاق 1..32767)
    Book.MaxIterationChange := 0.0001;  // السجل CalcDelta $0010
    Book.SaveAs('model.xls');           // تدور سجلات الحساب الثلاثة دورة كاملة
  finally
    Book.Free;
  end;
end;

لاحظ الانقسام المتعمد في التسمية: يتحدث محرك XLSX بـ Iterate / IterateCount / IterateDelta (مصطلحات OOXML)، بينما يتحدث محرك BIFF8 بـ EnableIteration / MaxIterations / MaxIterationChange (بما يتماشى مع أسماء سجلات [MS-XLS])؛ وتصف كلتا الثلاثيتين نفس المفاتيح الثلاثة — مفتاح تشغيل/إيقاف، وحد أقصى للتكرار، ودلتا تقارب — مع نفس الافتراضيات: إيقاف، و١٠٠، و0.001

متى يكون المرجع الدائري خطأً برمجياً بدلاً من نموذج؟

تمكين التكرار ليس طريقة لإخفاء تحذيرات المراجع الدائرية، ومعاملته بهذه الطريقة هي المصيدة؛ حيث يؤدي تشغيل Iterate عالمياً إلى تحويل كل حلقة غير مقصودة — وهي الحلقات التي وجد رمز الخطأ الافتراضي لالتقاطها — إلى رقم متقارب بصمت أو غير متقارب بصمت؛ والانضباط المطلوب هو العكس: حافظ على Iterate مضبوطاً على False كوضع عادي بحيث لا تزال أخطاء التأليف الحقيقية تظهر كـ lxErrorRef، وقم بتمكين التكرار فقط في دفاتر العمل التي تكون دائريتها مقصودة ومفهومة

وعندما تظهر حلقة بالفعل وتكون غير متأكد من نوعها، فإن متتبع تقييم الصيغ هو الأداة التي تميز بينهما: فتتبع الصيغة المشتبه بها وتصبح سلسلة المراجع التي تلتف حول نفسها مرئية خطوة بخطوة، بحيث يمكنك تحديد ما إذا كانت ترمّز حلقة تغذية مرتدة حقيقية أو مرجعاً ذاتياً شارداً؛ ويساعد أيضاً تذكر أن خلية الحلقة يمكنها استدعاء أي دالة مدمجة أثناء الدوران — فنفس الحاسبة التي تحل صيغة هندسية أو أعداد مركبة تقيم أعضاء الحلقة في كل دورة — ولذلك فإن النموذج المتباعد غالباً ما يكون مشكلة صيغة داخل الحلقة، وليس مشكلة إعدادات تكرار

قائمة المراجعة العملية قصيرة: تأكد من أن الحلقة لها نقطة ثابتة حقيقية قبل الاعتماد على التكرار؛ وحافظ على IterateDelta ضيقاً بما يكفي ليعني "التقارب" ما يحتاجه نموذجك؛ وبعد عملية Recalculate تتوقع تقاربها، قم بإجراء فحص منطقي لمخرج معروف بدلاً من الثقة في lxOk بمفرده، لأن ذلك الرمز لا يمكنه التمييز بين التقارب وبلوغ حد التكرار

يعد حساب المراجع الدائرية التكراري جزءاً من محرك XLSX في مكون HotXLS لـ Delphi Excel، إلى جانب إعادة حساب التبعيات التدريجي الذي يعتمد عليه وثبات OOXML و BIFF8 الذي يحمل الإعداد في كل ملف تكتبه؛ وبالنسبة للنماذج المالية والهندسية الدائرية عن قصد، فهو الفرق بين رمز خطأ وإجابة