مقال تقني

ذهاب وعودة بلا فقد لقيم Double في XLSX في Delphi: 17 رقماً

تكتب HotXLS قيم الخلايا العددية العادية ومخازن الصيغ في XLSX بـ 17 رقماً معنوياً، وعند القراءة تصحح تحليل RTL في ديلفي بفحص عشري إلى binary64 دقيق محدود، فقيمة Double تخزنها في خلية تعود بتاً ببتاً بعد حفظ وإعادة فتح. خمسة عشر رقماً، العدد الذي تعرضه Excel على الشاشة، لا يكفي لتعريف قيمة IEEE 754 binary64، وتظهر الفجوة تماماً حيث تسكن أعمدة المال والمعرفات: الأعداد الصحيحة فوق 1015

بدا الخطأ الذي فرض التغيير غير مؤذ. صيغة ورقة عمل INT(-1E15-0.1) تحسب إلى -1000000000000001، وهو double قابل للتمثيل تماماً. حسبته HotXLS صحيحاً وخزنته في المخزن المؤقت وحفظت الملف، وقرأ المخزن على القرص -1E15. كانت Excel ستعرض القيمة الأصلية بعد إعادة حساب فقط، وأي مستهلك يثق بالقيم المخزنة، وهم الأكثرية، حصل بصمت على عدد ناقص واحداً. لم يرمُ شيء ولم يُحذر شيء وتحقق الملف سليماً

لماذا لا تكفي 15 رقماً معنوياً لقيمة double؟

خمسة عشر رقماً عشرياً مضمونة النجاة من رحلة إلى binary64 والعودة، لكن الاتجاه المعاكس يحتاج 17. لدالة significand في IEEE 754 binary64 عرض 53 بتاً، يقابل ما بين 15 و17 رقماً عشرياً بحسب موضع القيمة. أي سلسلة عشرية بـ 15 رقماً معنوياً على الأكثر تتحول إلى double وتُطبع عائدة دون تغيير. أما double فيحتاج حتى 17 رقماً قبل أن تعرّفه صياغته العشرية تعريفاً فريداً، وفي المدى فوق 253 تتباعد قيم double المتجاورة بمقدار 2، وبين 1015 و 253 بمقدار 0.125 إلى 1

ينسق FloatToStr في ديلفي بـ 15 رقماً معنوياً، وهي الدقة نفسها التي تستخدمها Excel للعرض. ذلك التطابق هو بالضبط سبب اختباء العيب جيداً: افتح الملف في Excel وتبدو الخلية تماماً كما كانت في برنامج ديلفي عندك، لأن Excel أيضاً تعرض 15 رقماً فقط. لا يصير الفرق مرئياً إلا عندما تقارن قيمة double المخزنة، أو تطرح قيمتين متجاورتين، أو تطعم المخزن في شيء يجري حساباً. قيمة -1000000000000001 لها 16 رقماً معنوياً، فتقريبها إلى 15 ينتج -1000000000000000 وتضيع الوحدة اللاحقة إلى الأبد

تكتب HotXLS قيم خلايا XLSX ومخازن الصيغ بـ 17 رقماً معنوياً لأن قيمة double القابلة للتمثيل -1000000000000001 المخزنة من INT(-1E15-0.1) فقدت وحدتها اللاحقة عند تخزينها بـ 15 رقماً بوصفها -1E15
تسع دالة significand في binary64 من 53 بتاً ما بين 15 و17 رقماً عشرياً، فأي قيمة مثل -1000000000000001 تحتاج الأرقام المعنوية الـ 17 كاملة قبل أن تصير صياغتها العشرية فريدة

يعرّف ECMA-376 Part 1 §18.3.1.96 عنصر <v> الذي يحمل قيمة خلية بوصفها نصاً، ولا شيء في المخطط يفرض كم رقماً يجب أن يبث الكاتب لخلية عدد. الدقة مسؤولية الكاتب بالكامل، ولهذا يستطيع منتجان كلاهما أن يكتب XLSX صالحاً مع اختلافهما حول أي عدد في الخلية

كتابة 17 رقماً معنوياً من Delphi

تستخدم HotXLS الآن قاعدة واحدة ثابتة للخلايا العددية:‏ FloatToStrF مع ffGeneral ودقة 17 وإعدادات تنسيق ثابتة. كان فرع التواريخ في كاتب الخلايا يستخدم ذلك الإسقاط عينه أصلاً، فانضممت إليه الأعداد العادية ببساطة. ومن الخارج لا يتغير شيء في الـ API؛ تسند Double أو صيغة، تحفظ، والرمز الذي يهبط في <v> دقيق بما يكفي لإعادة إنتاج الـ double

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Ledger');
    Sheet.Cells[1, 1].Value := 9007199254740991.0;   // 2^53 - 1
    Sheet.Cells[2, 1].Value := -1000000000000001.0;
    Sheet.Cells[3, 1].Formula := 'INT(-1E15-0.1)';
    if Book.Recalculate <> lxOk then
      raise Exception.Create('Recalculation failed');
    // تكتب القيم ومخزن الصيغة بـ 17 رقماً معنوياً
    if Book.SaveAs('ledger.xlsx') <> lxOk then
      raise Exception.Create('Save failed');
  finally
    Book.Free;
  end;
end;

سبعة عشر رقماً سقف لا قاعدة حشو. يُسقط ffGeneral الأصفار اللاحقة، فـ 0.5 ما يزال يُكتب 0.5 والأعداد الصحيحة الصغيرة تبقى قصيرة؛ القيم التي تحتاج الأرقام الإضافية فعلاً وحدها تنالها. الكلفة بايتات قليلة على الخلايا التي كانت تُقتطع سلفاً، وهو أرخص إصلاح صواب ستشحنه يوماً

لماذا تفقد كتابة 17 رقماً قيماً على القراءة رغم ذلك؟

كتابة أرقام كافية لا تنفع إلا إذا حوّلها القارئ إلى أقرب double، و Val في RTL لا تفعل ذلك دوماً. أثناء التحقق قرأ RTL على آلة الاختبار كلاً من 9007199254740990.7 والعدد الصحيح 9007199254740991 إلى قيمة binary64 المجاورة الخاطئة. ينبغي أن تهبط السلسلتان على 253-1؛ إحداهما صياغة من 17 رقماً تستطيع HotXLS إنتاجها، والأخرى كيف تكتب Excel القيمة نفسها. كاتب مُصلَح بمعزل لكان شحن ملفاً لا تستطيع HotXLS نفسها قراءته عائداً صحيحاً

لذا تبقي HotXLS ‏Val بوابةً وتصحح جوابها. بعد أن يقبل القواعد العددية القائمة رمزاً غير تاريخ، يبني محلل دقيق العددَ العشري كسراً من أعداد كبيرة ويقارنه بالمنتصفات الثنائية على جانبي مرشح RTL. إن هبط العشري دون المنتصف الأدنى تنزل المرشحة ulp واحداً؛ وفوق المنتصف الأعلى تنحدر؛ وعند التعادل التام تفوز الـ significand الزوجية، وهي قاعدة IEEE 754 للتقريب إلى الأقرب الزوجي. تُسمح للمرشحة بثمانية تعديلات جار على الأكثر، وهذا وفير لأن Val تخطئ بوحدة لا بميل

التصحيح محدود عن قصد. الرموز الأطول من 48 محرفاً، أو ذات أكثر من 17 رقماً معنوياً، أو ذات مقدار أسّ فوق 324 تتجاوزه وتحفظ نتيجة المحلل القديم، وأي فشل داخلي يعود إلى الشيء نفسه. تستخدم الأعداد الكبيرة 48 طرفاً من 32 بتاً لكل منها، يكفي لأوسع بسط ومقام داخل تلك الحدود. وتفصيل قابلية نقل مهم هنا: الأطراف معرَّفة صراحةً UInt32. تبدو LongWord الخيار البديهي، لكنها 8 بايتات على هدف Linux64 في ديلفي، ونوع طرف يتضاعف عرضه بصمت يكسر كل حمل وإزاحة في الحساب

تصحح HotXLS نتيجة Val في RTL لـ Delphi لأعداد XLSX ذات 17 رقماً بمقارنة العشري بوصفه كسر أعداد كبيرة مع المنتصفات الثنائية والتنقل ulp واحداً نحو تقريب IEEE 754 إلى الأقرب الزوجي
ينبغي أن تهبط 9007199254740990.7 والعدد الصحيح الذي تكتبه Excel كلاهما على 2^53 - 1، فيعدل القارئ الدقيق ثمانية جيران على الأكثر ويحفظ التحليل القديم خارج حدّي الـ 48 محرفاً والأس 324

التحقق من الرحلة ذهاباً وعوداً بتاً ببت

مقارنة قيم double بـ = مقبولة في هذا الاختبار، لأن الهدف المساواة التامة، لكن مقارنة نمط البتات تجعل تقرير الفشل أنفع: انحراف ulp واحد يظهر فرقاً بمقدار 1 في القيمة الخام ذات 64 بتاً بدل عددين يُطبعان متطابقين

function SameBits(A, B: Double): Boolean;
var
  BitsA, BitsB: UInt64;
begin
  Move(A, BitsA, SizeOf(BitsA));
  Move(B, BitsB, SizeOf(BitsB));
  Result := BitsA = BitsB;
end;

procedure CheckReopen(const FileName: string);
const
  Expected: array[1..3] of Double =
    (9007199254740991.0, -1000000000000001.0, -1000000000000001.0);
var
  Book: TXLSXWorkbook;
  Row: Integer;
  Stored: Double;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open(FileName) <> lxOk then
      raise Exception.Create('Open failed');
    for Row := 1 to 3 do
    begin
      Stored := Book.Sheets[1].Cells[Row, 1].Value;
      if not SameBits(Stored, Expected[Row]) then
        Writeln('row ', Row, ' drifted: ', FloatToStrF(Stored, ffGeneral, 17, 0));
    end;
  finally
    Book.Free;
  end;
end;

الصف 3 هو المثير للاهتمام. يقرأ النتيجة المخزنة لـ INT(-1E15-0.1) دون إعادة حساب، فيثبت أن المخزن نفسه بلا فقد لا أن محرك الصيغ قادر على إعادة حساب القيمة. وإن كنت تعتمد على النتائج المخزنة في أنبوب معالجة فتغطي دليل قراءة قيم الصيغ المخزنة دون إعادة حساب المنفصل حالات المخزن ولماذا لا يُخترع مخزن مفقود قط

أي أجزاء XLSX أخرى تحمل مخازن عددية؟

إصلاح كاتب الخلايا أغلق مساراً واحداً من عدة، وكان لا بد لكل نقطة كتابة عددية متبقية أن تُفحص على حدة. غطى الإصدار الأول قيم أوراق العمل ومخازن الصيغ، بما في ذلك مخازن قيم مخرجات Pivot. ما زالت مخازن مصادر Pivot ومخازن Chart تمر بقمع الـ 15 رقماً وأُغلقت في إصدارين لاحقين لا بالافتراض

  • مخزن مصدر Pivot: قيم sharedItems وكاتبا cacheRecords المضمّن والمنساب يستخدمان الآن الثابت نفسه ذا 17 رقماً، ويطبق مسار قراءة الـ pivot التصحيح الدقيق نفسه للمنتصفات. والسمات الوصفية مثل refreshedDate تحفظ صياغتها الأصلية
  • مخازن Chart: نقطة الكتابة الوحيدة التي تبني قيم النقاط المخزنة انتقلت إلى 17 رقماً، ونقاط القراءة الست، التي تغطي قيم خطوط الاتجاه وفئاتها والتحقق من أشرطة الخطأ ومساري عرض، تجري كلها عبر مساعد تحليل دقيق واحد
  • كل ما عدا ذلك عددياً، كأعراض الأعمدة وحدود المحاور، ما يزال يمر بالمنسق الدفاعي الذي يفرغ القيم غير المنتهية وتحت العادية إلى 0، لأن جزءاً مسلسلاً يجب ألا يحوي float لا يمكن تحليله قط

فحص التوافق الأهم كان مع Excel نفسها. تكتب Excel ‏253-1 بوصفها العدد الصحيح 9007199254740991؛ وقد تكتب HotXLS الصياغة المكافئة ذات 17 رقماً 9007199254740990.7. يحل الرمزان إلى double نفسه على الجانبين، فملف مؤلف في Excel أعيد فتحه وحُفظ بواسطة HotXLS ثم فُتح مجدداً في Excel يحفظ القيم نفسها في مخزن Pivot. الإقرار المتبادل لا الصياغة المتطابقة هو الخاصية التي تُختبر

تكتب Excel ‏2^53 - 1 بوصفها العدد الصحيح 9007199254740991 بينما قد تكتب HotXLS الصياغة ذات 17 رقماً 9007199254740990.7، ويحل رمزا XLSX كلاهما إلى double نفسه في Delphi و Excel
المصنف الذي يسافر من Excel عبر إعادة حفظ في HotXLS وعودةً يحفظ قيم مخزن Pivot بتاً ببت، لأن القارئين كليهما يقبل أي صياغة للقيمة

حدود وحالات حدّية تستحق المعرفة

بلا فقد هنا تعني حفظ قيمة binary64، لا أن كل قيمة قابلة للتمثيل. النتائج غير المنتهية ما تزال تُحفظ أخطاء #NUM! وتحت العادية ما تزال تُحفظ صفراً، تماماً كما قبل. والعرض شأن منفصل أيضاً: ستواصل Excel عرض 15 رقماً معنوياً على الشاشة، فالمستخدم الذي يكتب 9007199254740993 في Excel ما يزال ينال قيمة مخزنة مختلفة، ولا كاتب يستطيع إصلاح ذلك. وإن كان مصنفك يستخدم Set precision as displayed فالقيم تُقرَّب عمداً إلى صيغتها العددية، وهو عكس ما تدور حوله هذه المقالة؛ وقواعد ذاك النمط مغطاة في كيفية تطبيق HotXLS لتقريب دقة Excel كما معروضة

القارئ الدقيق لا يستبدل Val أيضاً؛ يصحح فقط القيم التي قبلتها Val سلفاً. الرمز الذي يرفضه RTL ما يزال مرفوضاً، والرمز خارج حدود الـ 48 محرفاً و17 رقماً والأس 324 يحفظ ما أنتجه المحلل القديم. ولملفات كتبتها HotXLS أو Excel لا يهم ذلك قط، لأن كلتيهما لا تنتج رموزاً كهذه لقيم الخلايا، لكن جزء XML مصنوع يدوياً بـ 40 رقماً معنوياً يُقرأ بدقة RTL لا بالتصحيح الدقيق

FAQ

هل تغير HotXLS العدد الذي تعرضه Excel لخلية؟

لا. تغير HotXLS فقط كم رقماً يدخل رمز <v> المخزن. ما تزال Excel تعرض 15 رقماً معنوياً، فتبدو الخلايا كما هي؛ والفرق أن قيمة double التحتية تطابق الآن ما أسند إليه كود ديلفي عندك

لماذا لا تُكتب 16 رقماً بدل 17 فحسب؟

ستة عشر رقماً لا تكفي لكل قيمة binary64. بعض قيم double تحتاج 17 رقماً معنوياً قبل أن تصير صياغتها العشرية فريدة، فكاتب من 16 رقماً سيظل يربط قيمتي double مختلفتين بالنص نفسه في حالات نادرة، وهي بالضبط صنف الانحراف الصامت الذي يزيله التغيير

هل تحتاج الملفات المكتوبة قبل الإصلاح إلى إعادة توليد؟

فقط إن كانت تحوي قيماً احتاجت أكثر من 15 رقماً. تلك الرموز فقدت أرقامها الأخيرة على القرص أصلاً وقراءتها أدق لا يمكنها استعادة ما لم يُكتب قط. إعادة الحفظ من البيانات الأصلية، أو إعادة حساب الصيغ والحفظ مجدداً، تنتج ملفاً دقيقاً

المخرجات العددية ذات 17 رقماً وتصحيح القراءة الدقيق لـ binary64 ومعالجة مخازن Pivot و Chart المطابقة كلها من HotXLS Delphi Spreadsheet Component لـ Delphi و C++Builder و Lazarus، الذي يقرأ ويكتب XLSX دون تثبيت Excel أو أتمتة OLE