مقال تقني

التقاطع الضمني للأسماء المعرفة في HotXLS لدلفي

الاسم المعرف الذي يشير إلى عمود كامل يقرؤه Excel كخلية واحدة حين يظهر في موضع قياسي: =Vertical+1 في الصف 7 تعني «خلية الصف 7 من Vertical» لا المساحة كلها. يطبق مكوّن HotXLS لدلفي ذلك التقاطع الضمني في v2.382.4 على مستويين، أثناء التقييم وأثناء استخراج التبعيات، لأن قالب قروض بـ 4805 صيغة أظهر أن الحصول على القيمة الصحيحة وحده لا يكفي. حين يوسع موجّه التبعيات الاسم إلى مساحته الكاملة، تُغلق صيغة لاحقة تطعم أي خلية من تلك المساحة دورة غير موجودة، فيرفض TXLSXWorkbook.Recalculate المصنف بأكمله

القالب المقصود مصنف إطفاء قرض جاهز. بتسميم كل قيمة مخبأة إلى 777 وتشغيل Recalculate كامل، أعادت بنيتا المحرك كلتاهما 23، وهي lxErrorRef، كود المرجع الدائري. من بين 4805 صيغ لم تطابق 3842 التوقع المستقل، واحتفظ B18 بـ #VALUE!، وبقي E18 على 777، وكان عدد الدفعات في J7 قد قرأ العناصر النائبة في عمود رصيد غير مكتمل. ثلاثة عيوب منفصلة كانت تختبئ خلف كود إرجاع واحد، وهذه المقالة تستعرض كلًا منها مع الإصلاح الذي عالجه

لماذا يولّد مرجع قياسي لاسم عمود دورة زائفة؟

لأن مخطط التبعيات لا يعرف إلا الحواف، وحافة من صيغة إلى مساحة بـ 480 صفًا هي 480 حافة، إحداها تشير عائدًا عبر خلية تعتمد على تلك الصيغة. خذ =IF(TRUE,Vertical+1,0) في B1 مع Vertical معرفة كـ Inputs!$A$1:$A$2، و=B1+1 في A2. يقيّم Excel B1 كـ A1+1 وA2 كـ B1+1، سلسلة مستقيمة. موجّه يسجل B1 كمعتمدة على A1:A2 يجعل A2 سابقة لـ B1، وA2 تسرد بالفعل B1 كسابقة، فلا يرى طابور Kahn الذي يحرك إعادة الحساب التزايدي في HotXLS أي عقدة تبلغ الدرجة الداخلة صفرًا أبدًا. هذا هو النمط الذي تُبنى منه قوالب القروض: كل صف فترة يشير إلى أعمدة مسماة للرصيد والمعدل وعدد الدفعات، وكل اسم يمتد على الجدول كله، وكل صف يكتب كذلك في تلك الأعمدة. وسّع الأسماء ويصبح المخطط مكونًا ترابطيًا قويًا عملاقًا واحدًا. قيّمها بالتقاطع الضمني ويصبح المخطط مجموعة سلاسل قصيرة، سلسلة لكل صف، وهو ما يصفه ECMA-376 Part 1 §18.17.2 لمعامل مرجعي يُستهلك حيث تُطلب قيمة واحدة

لماذا أغلق اسم عمود دورة زائفة في HotXLS: مع تعريف Vertical كـ Inputs!$A$1:$A$2 يسجل الموجه B1 كمعتمدة على A1:A2 بينما تسرد A2 B1 كسابقة فيبقى طابور Kahn بلا نزيف، بينما يضيّق التقاطع B1 إلى خلية الصف A1 ويحفظ السلسلة لكل صف A2 وB1 وA1 التي يرتبها Recalculate
توسيع الاسم جعل المخطط مكونًا ترابطيًا قويًا عملاقًا واحدًا، وتقييم الصيغ نفسها بالتقاطع الضمني يحوله إلى سلاسل قصيرة، واحدة لكل صف من صفوف الجدول
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Inputs');
    Book.DefinedNames.Add('Vertical', 'Inputs!$A$1:$A$2');
    Book.DefinedNames.Add('Alias', '=Vertical');
    Sheet.Cells[1, 1].Value := 1;
    // موضع قياسي: ينكمش Vertical إلى A1 لأن الصيغة في الصف 1
    Sheet.Cells[1, 2].Formula := '=IF(TRUE,Vertical+1,0)';
    Sheet.Cells[2, 1].Formula := '=B1+1';
    // الاسم الذي تعريفه اسم آخر يخضع للتقاطع كذلك، فهذه A2
    Sheet.Cells[2, 2].Formula := '=Alias';
    // وسيط من فئة مرجع: تُجمع المساحة كلها بلا تقاطع
    Sheet.Cells[3, 2].Formula := '=SUM(Vertical)';
    // الصف 6 خارج A1:A2، فالتقاطع فارغ ويلتقطه IFERROR
    Sheet.Cells[6, 2].Formula := '=IFERROR(Vertical,42)';

    if Book.Recalculate = lxOk then
    begin
      // B1 = 2 وA2 = 3 وB2 = 3 وB3 = 4 وB6 = 42
      // قبل v2.382.4 كان هذا الفرع لا يُبلغ: كانت B1 -> A2 -> B1 دورة
    end;
  finally
    Book.Free;
  end;
end;

كيف يقرر HotXLS أن وسيطًا ما قياسي؟

يقرأ HotXLS الجواب من جدول الدوال لا من شكل الوسيط. كل مدخل في TXLSFormula.InitFuncHash يُسجل عبر THashFunc.SetValue مع سلسلة فئة اختيارية لكل وسيط: 'IF' تحمل '100'، و'SUMIF' تحمل '010'، و'VLOOKUP' تحمل '1011'، و'SUM' بلا شيء، فترجع كل وسائطها إلى فئة الدالة على المستوى 0. التابع الجديد TXLSFormula.FunctionArgumentClass(APtg, AArgument) يكشف ذلك البايت عبر THashFuncEntry.ArgClass، والنتيجة 1 تعني فئة القيمة. هذه هي الفئات الثلاث نفسها التي يسندها [MS-XLS] §2.2.2 إلى رموز المعاملات، وكان المرمّز يعتمد عليها أصلًا: عند كتابة مرجع يحسب الـ ptg بالصيغة $24 + $20 * aClass، فينتج PtgRef للفئة 0 وPtgRefV للفئة 1 وPtgRefA للفئة 2. ملف BIFF كتبه Excel يخزن تلك الفئة في كل رمز مرجعي، فالمحرك الذي يطابق جدوله المواصفة يستطيع الإجابة عن «هل هذا الوسيط قياسي» دون النظر إلى البيانات. الوسيط الأوسط في SUMIF هو المعيار، قيمة؛ والأول والثالث مساحتان، مرجعان. أما SUMPRODUCT فمسجل بفئة على مستوى الدالة هي 2، مصفوفة، ولهذا ما يزال =SUMPRODUCT(Vertical,Vertical) يضرب المساحة كلها

ثلاث دوال لا تستشير مدخل جدولها في أي شيء بعد الوسيط الأول. IF (ptg 1) وCHOOSE (ptg 100) وIFERROR (ptg 255) تمرر ما تختاره كما هو، فترث وسائط فروعها فئة الموضع الذي تشغله الدالة ذاتها. هذه القاعدة الواحدة هي ما يجعل =CHOOSE(1,Vertical,0) في G2 تُحل إلى A2 بينما ما تزال =SUMIF(Vertical,">0",Vertical) المجاورة تجمع الصفين معًا، وهي القاعدة التي يمارسها جدول الإطفاء أكثر من غيرها، لأن خلايا فتراته تعتمد على IF لاختبار ما إذا كان القرض ما يزال مفتوحًا

من أين يقرأ HotXLS فئات الوسائط للتقاطع الضمني: تسجل IF بقيمة 100 وSUMIF بقيمة 010 وVLOOKUP بقيمة 1011 وSUM بلا شيء فترجع وسائطها إلى الفئة 0، ويكتب المرمّز رموز المرجع بصيغة ptg $24 زائد $20 مضروبة في الفئة منتجة PtgRef وPtgRefV وPtgRefA، وتورث دوال العبور IF وCHOOSE وIFERROR فئة الموضع الذي تشغله
بما أن جدول الفئات يطابق المواصفة، يستطيع المحرك الإجابة عن كون وسيط ما قياسيًا دون النظر إلى البيانات، وحل CHOOSE إلى A2 بجوار SUMIF تجمع الصفين ينتج عن قاعدة واحدة

حمل الفئة عبر اجتياز التبعيات

مستخرج التبعيات في lxCalc.pas تابع Walk تكراري على شجرة الصياغة المترجمة، وهو موجود مرتين، مرة في TXLSCalculator.ExtractDependencies لمخطط المصنف الواحد ومرة في ExtractWorkspaceDependencies لمخطط ما بين المصنفات. يمنح v2.382.4 كلا الموجّهين معاملين إضافيين. AScalar يبدأ True عند جذر الصيغة، ويُعاد حسابه لكل ابن دالة من FunctionArgumentClass، ويُمرر دون تغيير لوسائط فروع ptg 1 و100 و255. وANameRoot لا يصير True إلا حين ينزل الموجّه إلى التعريف المترجم لاسم، ويبقى عبر عقد SA_GROUP وحدها، أي الأقواس، فلا يُظن اسم معرف كـ =A1:A2+1 مساحة عادية. حين يكون كلا العلمين True عند عقدة SA_RANGE، يضيّق AddResolvedRange المساحة بالمساعد نفسه الذي يستخدمه المقيّم قبل أن يسجل التبعية. والمساعد قصير بما يكفي ليُقتبس كاملًا

قرار IntersectNamedScalarRange الذي يحمي تبعيات الأسماء في HotXLS: المدى أحادي الخلية أصلًا يمر كما هو، والعمود الواحد يضيق إلى صف الصيغة حين يقع CurRow داخله، والصف الواحد يضيق إلى عمود الصيغة، وأي شيء آخر، مساحة ثنائية الأبعاد أو صف خارج المدى، ينتج #VALUE! أثناء التقييم ولا يسجل أي تبعية
كلا موجّهي التبعيات والمقيّم يستدعون المساعد نفسه، فلا يمكن أن تختلف قيمة تقرؤها الصيغة عن الحافة التي يسجلها المخطط في اسم مُتقاطع أبدًا
function IntersectNamedScalarRange(CurRow, CurCol: Integer;
  var Row1, Row2, Col1, Col2: Integer): Boolean;
begin
  Result := False;
  if (Row1 = Row2) and (Col1 = Col2) then Exit(True);   // خلية أصلًا
  if (Col1 = Col2) and (CurRow >= Row1) and (CurRow <= Row2) then
  begin
    Row1 := CurRow; Row2 := CurRow;                     // عمود واحد: خذ هذا الصف
    Exit(True);
  end;
  if (Row1 = Row2) and (CurCol >= Col1) and (CurCol <= Col2) then
  begin
    Col1 := CurCol; Col2 := CurCol;                     // صف واحد: خذ هذا العمود
    Result := True;
  end;
end;

كل ما يرفضه المساعد، مساحة ثنائية الأبعاد أو مرجع متعدد الأوراق أو صيغة صفها خارج العمود المسمى، ينتج #VALUE! على جانب التقييم ولا تبعية إطلاقًا على جانب المخطط، وهو ما يفعله Excel للتقاطع الفارغ. جانب التقييم يقيم في TXLSCalculator.GetValueItemName: ينزع أغلفة SA_GROUP من التعريف المترجم، وإذا كان الجذر SA_RANGE استدعى GetRangeInfo وتقاطع وجلب الخلية الواحدة عبر FGetValue بدل تقييم التعريف كله. المراجع الخارجية تبقى على المسار القديم، لأنه لا يوجد صف محلي يُقاطع ضده. ومن أين تأتي أصلًا بيانات تخزين الاسم ونطاقه يغطيه مقال الأسماء المعرفة والصيغ ما بين الأوراق؛ والمقصود هنا فقط ما يفعله المحرك بعد أن يُحل الاسم

لماذا قرأت MATCH فوق عمود نصف محسوب القيمة 777؟

لأن وسيط مصفوفة البحث في MATCH مرجع مسح، والمراجع المسحية استُبعدت عمدًا من ترتيب التقييم. مقال البحث المسحي قدّم TXLSDepRange.LookupScan واختتم بقسم عنوانه «ما الذي تتخلى عنه باستبعاد حواف المسح من الترتيب»: قد تعمل صيغة بحث قبل إعادة حساب كل خلية في مداها فتقرأ قيمًا قديمة. في جلسة تفاعلية تتقارب الحال في المرور التالي. وفي إعادة حساب دفعية لقالب مُسمّم لا تتقارب، فقرأت PaymentCount، المعرفة كـ =MATCH(0.01,Balances,-1)+1، العناصر النائبة 777 الجاثمة في عمود الرصيد وأعادت عدد فترات لا يمكن أن يكون صحيحًا

يعامل TXLSDepGraph.TopoOrder الآن حواف المسح بوصفها حواف ترتيب ناعمة. إلى جانب الدرجة الداخلة الصلبة يحفظ مصفوفة ScanInDeg تعدّ سلف المسح المتسخة لكل عقدة وتنقصها كلما أُصدِر هؤلاء السلف، مستخدمًا قوائم ScanPrecedents وScanDependents وScanPrecedentCount التي خزنها التغيير السابق أصلًا. في كل تكرار يمسح طابور Kahn نافذته الجاهزة بحثًا عن أول عقدة صفّرتها ScanInDeg ويبدلها إلى الرأس؛ وإذا كانت كل عقدة جاهزة ما تزال تنتظر سلف مسح، أُبعد الرأس بترتيبه المستقر. حواف المسح لا تدخل الدرجة الداخلة الصلبة قط، فـ VLOOKUP ذاتية المرجع على عمودها الخاص ما تزال مشروعة، لكن البحث الذي يستطيع انتظار سلف قابل للإنجاز ينتظره الآن. والاختبار المرجعي المثبت لهذا، LookupScan_WaitsForDirtyFormulaValues، يسمم ثلاث خلايا رصيد إلى 777 ويتوقع أن يعود PaymentCount بقيمة 3، ثم يقلب المدخل إلى صفر ويتوقع أن ترى =IFERROR(PaymentCount,99) الـ #N/A وتعيد 99

من أين جاء القطع إلى أربع منازل عشرية؟

من حساب Variant في دلفي، وفي المواضع المتداخلة فقط. كانت المعاملات الثنائية في TXLSCalculator.GetValueItem تنسخ أصلًا + أو - من المستوى الأعلى إلى متغيرين محليين من Double، فكانت =B1-A1 سليمة. أما داخل =IF(TRUE,B1-A1,0) فاشتغل الطرح نفسه على شكل Value := Value - SubValue بين متغيري Variant، وحين كان أحد المعاملين قيمة خلية من Int64 والآخر Double، كانت النتيجة التي لاحظناها Currency، نوع ذو فاصلة ثابتة بأربع منازل عشرية، فعاد 1066.1854641400994 ناقص 120 مقطوعًا إلى أربع منازل. وفي جدول كل دفعة تُركّب فيه من الصف السابق، يمشي هذا الخطأ عبر مئات الفترات قبل أن يصل إلى الإجماليات

// TXLSCalculator.GetValueItem، فرع الحساب الثنائي (lxCalc.pas)
if VarIsNull(Value) then Value := 0;
if VarIsNull(SubValue) then SubValue := 0;
// حساب Variant المختلط Int64/Double قد يرقى إلى Currency.
// حساب جداول البيانات يجب أن يحافظ على دقة الفاصلة العائمة.
if VarIsNumeric(Value) then Value := Double(Value);
if VarIsNumeric(SubValue) then SubValue := Double(SubValue);

يعمل الحرس قبل SA_ADD وSA_SUB وSA_MUL وSA_DIV على السواء، والاختبار المرجعي Arithmetic_MixedInt64AndDoubleKeepsPrecision يخزن Int64(120) في A1 و1066.1854641400994 في B1، ثم يفحص الفرق والمجموع المتداخلين بدقة 1E-10 والضرب والقسمة بدقة 1E-8 و1E-12. لا يدعي HotXLS معرفة كل قواعد الترقية التي يطبقها RTL على أنواع Variant المختلطة عبر إصدارات المترجم؛ بل يدعي أن حساب جداول البيانات هو IEEE double، وهو الآن يجعل المعاملين كليهما double قبل أن يراهما العامل، فيسقط السؤال من أصله

ما يضمنه الإصلاح وما لا يضمنه

بعد v2.382.4 تعيد بنيتا المحرك كلتاهما lxOk للقالب المسمّم، وتطابق كل القيم المخبأة البالغ عددها 4805 التوقع المستقل صفًا صفًا ضمن 1E-7، وتصمد التأكيدات القائلة إن المخابئ سُممت فعلًا وإن بصمة المصدر لم تتغير وإن كل صيغة ما تزال موجودة. لم يُفعَّل تكرار ولم يُكتم كود خطأ للوصول إلى ذلك. الدورة الحقيقية عبر اسم، =B1 في A1 مع بقاء B1 تقرأ Vertical، ما تزال تعيد خطأ، وينتهي الاختبار NamedScalarRanges_IntersectWithoutFalseCycles بتأكيد ذلك تحديدًا

الحدود تستحق التصريح بها بوضوح. التقاطع الضمني ينطبق فقط على اسم تعريفه المترجم، بعد نزع الأقواس، مساحة عمود واحد أو صف واحد في ورقة واحدة؛ الاسم ثنائي الأبعاد في موضع قياسي يعطي #VALUE! كما في Excel، والدالة التي لا يعرفها الجدول تأخذ الفئة 0 من FunctionArgumentClass، فتبقى وسائط أسمائها تُوسع بالكامل. الترتيب الناعم تفضيل لا ضمان: دورة مسحية صرفة ما تزال تُقيَّم بالترتيب المستقر وتقرأ ما هو مخبأ، وهو السلوك الذي قبله مقال البحث المسحي عمدًا. ونتيجة القالب كله تتحقق مقابل سكربت توقع مستقل لا مقابل محرك جداول آخر، لأن مجموعة الأوفيس المرجعية لم تُكمل إعادة حساب القالب الأصلي ضمن ميزانية 60 ثانية. HotXLS مكوّن جداول أصلي لدلفي وC++Builder يقرأ XLS وXLSX وODS وCSV ويعيد حسابها ويكتبها دون تنصيب Excel؛ وتقاطع الأسماء وجدول فئات الوسائط والترتيب الناعم للمسح تنطبق على كل صيغة لأن محرك الحساب مشترك، وتغطية الدوال الحالية مسردة على صفحة منتج مكوّن جداول HotXLS لدلفي