مقال تقني

قراءة القيم المخزنة لصيغ Excel في Delphi دون إعادة حساب

يقرأ HotXLS، مكتبة Excel الأصلية لـDelphi وC++Builder، القيمة التي خزّنها Excel بالفعل بجانب الصيغة عبر TryGetCachedFormulaValue وIXLSFormulaCacheReader. لا أي من نقطتي الدخول يستدعي الحاسبة، أو يفك رموز الصيغة، أو يحدّث الحالة المتسخة، أو يكتب أي شيء إلى النموذج، لذا يبقى المصنف الذي تقرؤه فقط كما فتحته تماما

السيناريو الذي يدفع إلى هذا مملّ وشائع للغاية. مهمة ليلية تفتح بضع مئات من المصنفات أنتجها غيرك، وتستخرج عمودا واحدا من الإجماليات من كل منها، وتدفع الأرقام إلى مستودع بيانات. الإجماليات موجودة بالفعل في الملفات — حسبها Excel وحفظها. لكن لحظة تسأل المهمة خلية صيغة عن قيمتها، فإن مكتبة ليس لديها سوى إجابة واحدة لهذا السؤال تبني رسما بيانيا للتبعيات وتقيّم الورقة كلها، وتتحول مهمة كان ينبغي أن تكون مقيدة بالإدخال والإخراج إلى معيار قياس للحساب

لماذا تكلّف قراءة خلية صيغة إعادة حساب كاملة؟

لأن جالب القيمة على خلية صيغة هو طلب لإنتاج قيمة، والطريقة الصحيحة عالميا الوحيدة لإنتاج واحدة هي تقييم الصيغة. هذا هو الافتراضي الصحيح لتطبيق يحرّر المصنفات، والافتراضي الخاطئ لخط معالجة يستخرجها. والأسوأ أن التقييم ليس خاليا من الآثار الجانبية: فهو يكتب النتائج مرة أخرى في الخلايا، ويقلّب أعلام الاتساخ، ويمكن أن تُحَل الأمور بشكل مختلف عن التطبيق المنتج حين تكون دالة غير مدعومة أو مرجع خارجي معطوبا. مهمة وصفتها لفريق العمليات بأنها للقراءة فقط تنتج بهدوء مصنفا لم يعد يطابق الموجود على القرص، وإن حفظه شيء لاحقا يتغير الملف على القرص أيضا

قراءة القيم المخزنة مؤقتا هي النصف الآخر من العقد. إنها تجيب عن سؤال أضيق — ماذا خزّن التطبيق المنتج هنا؟ — وترفض الإجابة عن أي شيء آخر. وحين تريد أرقاما طازجة فعلا، ما زال HotXLS يمنحك إعادة حساب تزايدية مدفوعة برسم بياني للتبعيات؛ بيت القصيد هو أن الاستخراج والتقييم يجب أن يكونا استدعاءين مختلفين، لا استدعاء واحدا بمزاجين

ثلاث حقائق متعامدة عن خلية واحدة

الخلاصة أولا: القيمة المخزنة مؤقتا لصيغة تحمل ثلاث حقائق مستقلة، وطيّها في Variant واحدة يضيّع معلومات تحتاجها. يبقيها TXLSFormulaCacheInfo منفصلة بوصفها State وKind وValue. يسجل TXLSFormulaCacheState الأصل عبر خمس حالات — xlfcsNotFormula وxlfcsMissing وxlfcsLoaded وxlfcsCalculated وxlfcsInvalidated — بينما يصنف TXLSFormulaCacheValueKind الحمولة كـxlfcvBlank أو xlfcvNumber أو xlfcvDateTime أو xlfcvString أو xlfcvBoolean أو xlfcvError. هذا الفصل هو ما يسمح بالإبلاغ عن الوجود بصدق: الفراغ المخزّن مؤقتا، والسلسلة الفارغة المخزنة مؤقتا، وFalse المخزنة مؤقتا، والصفر المخزّن مؤقتا، والخطأ المخزَّن مؤقتا، كلها قيم حقيقية، لذا لا يمكن أبدا استنتاج الوجود من VarIsEmpty أو VarIsNull. يُرجع TryGetCachedFormulaValue True فقط مع xlfcsLoaded وxlfcsCalculated، ويملأ مع ذلك حالة قابلة للتشخيص حين يُرجع False

سجل HotXLS TXLSFormulaCacheInfo يبقي ثلاث حقائق متعامدة عن خلية صيغة واحدة منفصلة: حالة الأصل عبر خمس حالات، ونوع الحمولة عبر ست، وقيمة Variant، فلا يُخلط أبدا بين فراغ أو False مخزنين مؤقتا وذاكرة تخزين غائبة
يبقى الأصل ونوع الحمولة وقيمة الحمولة منفصلة، وهي الطريقة الوحيدة للإبلاغ عن فراغ أو صفر أو سلسلة فارغة أو خطأ مخزَّن مؤقتا بوصفه القيمة الحقيقية التي يمثلها
var
  Book: TXLSXWorkbook;
  Info: TXLSFormulaCacheInfo;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-model.xlsx');
    // SheetIndex وRow وCol كلها ذات أساس أحادي هنا
    if Book.TryGetCachedFormulaValue(1, 12, 5, Info) then
      Writeln('cached value: ', VarToStr(Info.Value))
    else
      Writeln('no usable cache, state ordinal ', Ord(Info.State));
  finally
    Book.Free;
  end;
end;

لماذا القيمة المخزنة مؤقتا مفقودة؟

هناك أربعة أسباب بالضبط يُعيد فيها TryGetCachedFormulaValue False، والحالة تخبرك أيها ينطبق. xlfcsNotFormula تعني أن الخلية تحمل قيمة حرفية أو لا شيء إطلاقا، وكون الإحداثيات خارج المدى ينهار إلى الإجابة نفسها. xlfcsMissing تعني أن الخلية صيغة فعلا لكن المنتج لم يخزّن لها حمولة قيمة — وهو ناتج شائع حين يكتب مولّد صيغا ويترك Excel يملأ النتائج عند أول فتح. xlfcsInvalidated تعني أن نص الصيغة استُبدل بعد التحميل، فالقيمة التي كانت هناك تصف تعبيرا لم يعد موجودا. أما xlfcsCalculated فعلى النقيض حالة نجاح: فهي تميّز قيمة أنتجها كودك أنت أو مقيّم HotXLS خلال هذه الجلسة، خلافا لـxlfcsLoaded التي أتت من الملف

الصدق بشأن ذاكرة التخزين المفقودة أهم من التغطية عليها. يرفض HotXLS اختراع قيمة، وهو عند الحفظ صارم بالقدر نفسه — فقط xlfcsLoaded وxlfcsCalculated تُصدران قيمة مخزنة مؤقتا، بينما xlfcsMissing وxlfcsInvalidated تكتبان الصيغة وحدها بدلا من تجميد رقم بالٍ في الملف. يترك لك ذلك ثلاث استجابات عاقلة في خط معالجة: تخطّي الصف وتسجيل الفجوة، أو إعادة حساب ذلك المصنف عمدا وقبول التكلفة، أو التقييم والمطابقة. وإن اختلف الرقم المقيَّم عما كان التطبيق المنتج سيكتبه، فإن متعقّب تقييم الصيغ هو الأداة لمعرفة أين تتباعد الحسابان، بدلا من التخمين من النتيجة

قارئ واحد عبر محركات BIFF الكلاسيكية وOOXML وODF

لا ينبغي لخط معالجة أن يهتم بما إذا كان الملف الذي فتحه للتو BIFF أو OOXML أو ODF. IXLSFormulaCacheReader هي نقطة الدخول الوحيدة للقراءة فقط للثلاثة: كل من TXLSWorkbook.CreateFormulaCacheReader وTXLSXWorkbook.CreateFormulaCacheReader يُرجع مهايئا خفيفا فوق البحث المتفرّق عن الخلايا الذي يستخدمه كل محرك أصلا، بإحداثيات ورقة وصف وعمود متطابقة ذات أساس أحادي. فئات المصنفات تتعمّد ألا تطبّق الواجهة بنفسها — فمرجع واجهة إلى المصنف سيغير دلالات ملكيته ويسمح للمستدعين بالتسلل عبر عقد مدة الحياة. بدلا من ذلك، إتلاف المصنف يمسح المؤشر الخام داخل ذلك العقد، وأي قارئ ما زال كودك يحتفظ به يثير EXLSFormulaCacheReaderInvalidated عند استعلامه التالي بدلا من إزاحة مرجع إلى ذاكرة محرَّرة. إنه فحص مدة حياة سريع الفشل، لا ضمان تزامن

var
  Reader: IXLSFormulaCacheReader;
  Info: TXLSFormulaCacheInfo;
  Row, Missing, Errors: Integer;
  Total: Double;
begin
  Reader := Book.CreateFormulaCacheReader;
  Total := 0;
  Missing := 0;
  Errors := 0;
  for Row := 2 to LastRow do
    if Reader.TryGetCachedFormulaValue(1, Row, 7, Info) then
    begin
      case Info.Kind of
        xlfcvNumber: Total := Total + Double(Info.Value);
        xlfcvError:  Inc(Errors);
      end;
    end
    else if Info.State = xlfcsMissing then
      Inc(Missing);
  // لم تعمل أي حاسبة، ولم يتحرك أي علم اتساخ، وBook بلا تغيير
end;

أين تقطن البايتات المخزنة مؤقتا فعلا

في ملفات .xls الكلاسيكية تكون الذاكرة المؤقتة هي الحقل FormulaValue لسجل Formula، ثمانية بايتات يصفها [MS-XLS] §2.5.133. حين تساوي الكلمة العليا $FFFF لا تكون الحمولة مزدوج IEEE 754 بل متغيرا موسوما، والتخطيط سهل أن يُخطأ فيه بمهارة: نوع المتغير يقع في val[0] وحمولة البولياني أو BErr تقع في val[2]، بينما val[1] غير معرّف. كان HotXLS سابقا يقرأ الحمولة من val[1]، وهو نوع خطأ الفرق بواحد الذي يظهر فقط على ملفات بعينها تخزّن بوليانيًا أو خطأ بدلا من رقم. يتفق القارئ وكاتب الصيغ المشتركة الآن على الإزاحات نفسها، فتنجو TRUE المخزنة مؤقتا من التحميل والحفظ سليمة بدلا من أن تتحلل إلى ضجيج

حقل FormulaValue ذو البايتات الثمانية في سجل Formula الكلاسيكي لـXLS كما يقرؤه HotXLS: مزدوج IEEE 754 ما لم تكن الكلمة العليا FFFF، وعندها يقع نوع المتغير في val صفر وحمولة البولياني أو الخطأ في val اثنين
حين تكون الكلمة العليا FFFF يكون الحقل متغيرا موسوما، وتقع الحمولة في val[2] مع val[1] غير معرّف، وهو البايت نفسه الذي كان القارئ يأخذه بالضبط

أمانة النوع في صيغ الحزم مشكلة منفصلة بفخها الخاص. في OOXML تتدلى القيمة المخزنة مؤقتا من العنصر c بصيغة <v>، مع السمة t التي تسمّي النوع حسب ECMA-376 Part 1 §18.3.1.4. يقرأ HotXLS t="e" مباشرة في متغير varError ويعيد ربطه بنص الخطأ القياسي عند الحفظ، فلا تتنكر الأخطاء أبدا كأعداد صحيحة عادية — لكن RTL الخاصة بـDelphi لن تساعدك هنا، لأن VarAsType(Integer, varError) تثير استثناء تحويل. البناء العامل يضبط TVarData.VType وTVarData.VError مباشرة. التواريخ تتبع الانضباط نفسه في الاتجاه المعاكس: t="d" ونوع قيمة التاريخ في ODF تصريحان نوعيان صريحان ويصبحان varDate، بينما ذاكرة BIFF الرقمية لا تحمل أي علم تاريخ ولذلك تبقى Double. لا يخمّن HotXLS تاريخا أبدا من تنسيق رقم الخلية، لأن تنسيق الرقم عرض والذاكرة المؤقتة بيانات. يضيف ODF حالة أخرى جديرة بالمعرفة — office:value-type="void" تعبّر عن ذاكرة مؤقتة موجودة لكنها لا تحمل قيمة، وبما أن ODF بلا نوع قيمة خطأ، فإن النص الذي يشبه الخطأ يُحفَظ كنص بدلا من ترقيته إلى خطأ

function DescribeCache(const Info: TXLSFormulaCacheInfo): string;
begin
  case Info.State of
    xlfcsNotFormula:  Result := 'not a formula cell';
    xlfcsMissing:     Result := 'formula stored with no cached value';
    xlfcsInvalidated: Result := 'formula replaced since load';
  else
    case Info.Kind of
      xlfcvError:    Result := 'error code ' + IntToStr(TVarData(Info.Value).VError);
      xlfcvDateTime: Result := DateTimeToStr(VarToDateTime(Info.Value));
      xlfcvBoolean:  Result := BoolToStr(Info.Value, True);
      xlfcvNumber:   Result := FloatToStr(Double(Info.Value));
      xlfcvString:   Result := VarToStr(Info.Value);
    else
      Result := 'present but blank';
    end;
  end;
end;

هل تشارك الصيغ المشتركة قيمها المخزنة مؤقتا؟

لا، وافتراض العكس هو كيف ينتهي مسح بالإبلاغ عن الرقم نفسه لعمود كامل. الصيغة المشتركة في OOXML تشارك تعبير الصيغة وتحسين التخزين فقط؛ كل خلية عضو ما زالت تملك <v> خاصة بها. لذلك لا ينشر HotXLS أبدا ذاكرة العضو الجذر إلى تابع وصل بلا قيمة، والتابع الذي حُمِّل كـxlfcsMissing ما زال يُبلغ عن xlfcsMissing بعد الحفظ وإعادة الفتح. وإن كنت تدرس كيف تُخزَّن المجموعة وتُوسَّع أصلا، فإن آليات سمة si للصيغة المشتركة وتوسيعها مشروحة في موضع منفصل؛ أما لقراءة الذاكرة المؤقتة، فالقاعدة تختصر في سطر واحد — اسأل كل خلية، ولا تثق بشيء لم تسأل عنه

عرض من HotXLS لمجموعة صيغ مشتركة في OOXML تشارك فيها سمة si التعبير وتخطيط التخزين فقط، بينما تملك كل خلية عضو قيمتها المخزنة مؤقتا، فيبقي تابع حُمِّل بلا قيمة يُبلغ عن xlfcsMissing
المجموعة تشارك التعبير لا الأرقام، فذاكرة الجذر لا تُنشر أبدا والعضو الذي وصل بلا قيمة يبقي يُبلغ عن تلك الفجوة

قراءة القيم المخزنة مؤقتا، والقارئ الموحّد عبر المحركات، ومحرك إعادة الحساب الذي يمكنك أن تختار ألا تستدعيه، كلها تشحن في مكوّن HotXLS القياسي لجداول بيانات Delphi لـDelphi وC++Builder، دون أي اعتماد على Excel أو على أي خادم أتمتة OLE؛ صفحة المنتج تحمل مرجع API الكامل لنقاط دخول المصنفات والقارئ الموضحة هنا