Technical Article

التواريخ التسلسلية لـ Excel في Delphi: نظام 1900 مقابل 1904 و numFmt

افتح جدول بيانات، وانقر على خلية تعرض 2026-06-19، وسيظل شريط الصيغة يقرأ تاريخاً. اقرأ نفس الخلية من Delphi وتحصل على الرقم 46192. كلا المنظورين صحيحان، لأن Excel لم يخزن تاريخاً في تلك الخلية أبداً. لقد خزن رقماً تسلسلياً، وهو عدد الأيام، وأرفق تنسيق أرقام يخبر الشاشة برسم العدد كتاريخ تقويم. لا يوجد نوع تاريخ في قيمة الخلية. هناك رقم وقاعدة عرض، وقاعدة العرض هي الشيء الوحيد الذي يميز التاريخ عن الكمية العادية.

هذا الفصل هو أصل كل خلل في التاريخ يتعين على مكتبة جداول البيانات تفاديه. الرقم التسلسلي بمفرده لا يوضح أي يوم هو، لأنه لا يذكر ما هو اليوم صفر. يعني نفس الرقم تاريخين متباعدين بأربع سنوات اعتماداً على علم واحد لكتيب العمل. والعدد الذي يجب قراءته كتاريخ سيتم قراءته ككمية مجردة ما لم يفحص شيء ما تنسيقه ويتعرف على نمط التاريخ. هذه هي كيفية بناء نموذج التاريخ في HotXLS، وسبب وجوب ذلك.

خلية التاريخ هي رقم بالإضافة إلى تنسيق

يخزن Excel التاريخ كعدد الأيام منذ الحقبة، مع وقت اليوم في الجزء الكسري. منتصف النهار في الرقم التسلسلي يحمل .5. الجزء الصحيح هو عدد الأيام. لا شيء في القيمة المخزنة يميزها كزمنية. ما يميزها هو تنسيق أرقام الخلية: يطلق ECMA-376 على هذا اسم numFmt، وتظهر الخلية التي يوضح رمز تنسيقها نمط تاريخ أو وقت كتاريخ. قم بإزالة التنسيق وستعرض نفس الخلية رقماً؛ لم تتغير القيمة الأساسية أبداً.

هذا هو السبب في أن قراءة قيمة الخلية تمنحك Variant قد يكون varDate أو قد يكون Double بسيطاً، ولماذا يعد تنسيق الأرقام في نفس الخلية هو الإشارة التي تحدد أيهما يقصده الطرف الثالث. عندما يفتح HotXLS ملف XLSX، تحمل الخلية كلاً من Value و NumberFormatIndex الخاصين بها إلى TXLSXCell، وفهرس التنسيق هو ما تستشيره لمعرفة ما إذا كان الرقم تاريخاً.

var
  Book: TXLSXWorkbook;
  Cell: TXLSXCell;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open('timesheet.xlsx') <> 1 then
      raise Exception.Create('Cannot open workbook');

    Cell := Book.Sheets[0].Cells[1, 1];   // row 1, col 1 (1-based)
    // Value may arrive as varDate or as a plain numeric serial;
    // the format index is the signal that tells them apart.
    Writeln('raw value : ', VarToStr(Cell.Value));
    Writeln('numFmt idx: ', Cell.NumberFormatIndex);
    Writeln('format    : ', Cell.NumberFormat);
  finally
    Book.Free;
  end;
end;

حقبتان، تفصلهما 1462 يوماً

نظام التاريخ الافتراضي، وهو النظام الذي يستخدمه كل كتيب عمل في Windows، يعد من نهاية عام 1899، بحيث يقع الرقم التسلسلي 1 في اليوم الأول من عام 1900. ويعود النظام الآخر إلى أجهزة Macintosh المبكرة ويعد من بداية عام 1904، لذا فإن رقمه التسلسلي 1 يأتي بعد أربع سنوات ويوم. يسجل كتيب العمل النظام الذي يستخدمه في علم واحد. في حزمة OOXML، يكون هذا العلم هو date1904 في جزء كتيب العمل؛ ويظهره HotXLS كخاصية Date1904 لكتيب العمل.

الفجوة بين الحقبتين هي 1462 يوماً بالضبط. وهذا يمثل أربع سنوات تقويمية، ثلاث منها مكونة من 365 يوماً وواحدة من 366 يوماً، بإجمالي 1461 يوماً، بالإضافة إلى يوم آخر لتعويض اليوم والكسر بين اتفاقيتي اليوم صفر. الرقم ثابت ويمكنك حفظه في رأسك. تكمن أهميته في أنه ليس صفراً. الرقم التسلسلي المنسوخ من كتيب عمل 1904 والمفسر تحت قواعد 1900، أو العكس، يجعل كل تاريخ متأخراً أو متقدماً بـ 1462 يوماً، وهو ما يظهر كتواريخ خاطئة بما يزيد قليلاً عن أربع سنوات ويسهل الخطأ فيها على أنها بيانات تالفة.

Because Delphi's own TDateTime is anchored to the 1900 convention, a library that maps Excel serials to TDateTime has to offset by 1462 in both directions whenever the workbook is flagged 1904. Reading a 1904 serial, subtract 1462 before treating it as a TDateTime; writing a TDateTime into a 1904 workbook, subtract 1462 from the serial so Excel renders the day you meant. HotXLS applies this shift internally when it serializes date values for a workbook whose Date1904 is set, so the value you assign as a TDateTime round-trips to the same calendar day on the screen.

غرابة السنة الكبيسة المتعمدة لعام 1900

هناك تجعد شهير في نظام 1900. يعامل Excel عام 1900 كسنة كبيسة ويقبل 29 فبراير 1900 كتاريخ حقيقي، الرقم التسلسلي 60. لم يكن عام 1900 سنة كبيسة، لأن سنوات القرون تكون كبيسة فقط عندما تقبل القسمة على 400، و 1900 ليست كذلك. اليوم الوهمي هو سلوك توافق متعمد موروث من جدول بيانات مبكر تم شحنه مع الخلل، وتم الاحتفاظ به منذ ذلك الحين بحيث تظل الحسابات التسلسلية متطابقة عبر عقود من الملفات.

النتيجة العملية صغيرة ولكنها حقيقية: لأي تاريخ في 1 مارس 1900 أو بعده، يكون الرقم التسلسلي أعلى بواحد مما قد يعطيه عدد أيام صحيح تماماً، لأن يوم 29 فبراير غير الموجود استهلك رقماً. تقوم مكتبة جداول البيانات بإعادة إنتاج الغرابة بدلاً من إصلاحها، لأن مطابقة حسابات Excel تماماً هي المهمة بأكملها. إن تصحيحها قد يضع كل تاريخ حديث متأخراً بيوم واحد عما يعرضه Excel، وهي نتيجة أسوأ من حمل فارق عمره أربعين ألف يوم لا يلمسه أي تاريخ حقيقي في العمل التجاري. لا يحتوي نظام 1904 على يوم وهمي معادل، وهو أحد الأسباب التي جعلت بعض المتاجر تفضلها تاريخياً.

كشف التاريخ من numFmt

عندما يصل رقم من ملف كتبه تطبيق آخر، يكون تنسيقه هو الدليل الوحيد على أنه تاريخ. يعين ECMA-376 كتلة من معرفات التنسيق المدمجة التي يتم تحديد معناها بواسطة المواصفات، وتشغل تنسيقات التاريخ والوقت نطاقات معروفة. المعرفات من 14 إلى 22 هي تنسيقات التاريخ والوقت للغة العامة، التنسيقات المألوفة m/d/yyyy و h:mm وأقاربها. المعرفات من 45 إلى 47 هي تنسيقات الوقت المنقضي. نطاقان إضافيان، من 27 إلى 36 ومن 50 إلى 58، هي تنسيقات التاريخ والوقت الخاصة باللغة والمستخدمة لتقويمات CJK، المحددة في ECMA-376 18.8.30. الخلية التي يقع معرف تنسيق أرقامها في أي من هذه النطاقات هي خلية تاريخ أو وقت.

تغطي المعرفات المدمجة الحالات الشائعة ولكن ليس الحالات المخصصة. عندما يحدد كتيب العمل رمز التنسيق الخاص به، على سبيل المثال ترتيب غير قياسي أو اسم شهر محلي، يكون المعرف فوق النطاق المدمج ويشير إلى جدول تنسيق الأرقام في كتيب العمل. بالنسبة لتلك، يعني التعرف على التاريخ قراءة سلسلة رمز التنسيق والبحث عن رموز التاريخ. يطوي HotXLS كلا الفحصين في دالة داخلية واحدة، XlsxNumFmtIsDate، والتي ترجع true على الفور لنطاقات التاريخ المدمجة وبخلاف ذلك تحلل رمز التنسيق المخصص من خلال XlsxFormatCodeIsDate. الجانب العام لذلك هو سلسلة NumberFormat للخلية وفهرس NumberFormatIndex الخاص بها، واللذان يمنحانك كلاً من رمز التنسيق المحلول والمعرف لاختباره.

لماذا لا يمكن لمحلل التنسيق مجرد مسح d و m

يبدو تحليل رمز التنسيق لرموز التاريخ أمراً هيناً حتى تتذكر ما يعيش أيضاً في تنسيق الأرقام. إن البحث الساذج عن الأحرف التي تتهجى التواريخ، d و m و y و h و s لليوم والشهر والسنة والساعة والثانية، سيخطئ في بنيتين ليستا رموز تاريخ على الإطلاق.

الأولى هي السلسلة النصية المقتبسة الحرفية. يمكن لتنسيق الأرقام تضمين نص حرفي بين علامتي اقتباس مزدوجتين، لذا فإن التنسيق المالي مثل #,##0 "MM" يلحق الحرفين M و M برقم دون أي معنى زمني على الإطلاق. الماسح الذي يعد الأحرف داخل علامتي الاقتباس كرموز شهر سيصنف بشكل خاطئ تنسيق العملة هذا كتاريخ. الثانية هي قسم الأقواس. تحمل تنسيقات الأرقام توجيهات في أقواس مربعة، وأسماء ألوان مثل [Red]، وشروط مقارنة مثل [>1000]، وعلامات لغة، وعلامات الوقت المنقضي [h] و [mm]. يحتوي بعض محتوى الأقواس على أحرف تواريخ وبعضها لا يحتوي، ويعامل النص بين الأقواس بنفس معاملة جسم التنسيق يؤدي إلى كل من النتائج الإيجابية الخاطئة والحالات المفقودة.

يمر المحلل الصحيح عبر رمز التنسيق حرفاً بحرف، متتبعاً ما إذا كان داخل نص حرفي مقتبس ومدى عمقه داخل الأقواس المتداخلة، وهو يحترم أيضاً هروب الشرطة المائلة الخلفية (backslash escape) التي تقتبس حرفاً واحداً يليه. فقط حرف تاريخ غير مهرب تم العثور عليه خارج أي سلسلة حرفية وخارج أي قسم أقواس يعد رمز تاريخ حقيقي. هذا هو بالضبط كيف يمسح XlsxFormatCodeIsDate: تقلب علامة الاقتباس حالة داخل الحرفية تمنع اكتشاف الرمز حتى علامة الاقتباس المغلقة، وتتخطى الشرطة المائلة الخلفية الحرف التالي، ويمنع عداد عمق الأقواس الاكتشاف داخل نطاقات [...]. المكافأة هي أن #,##0 "MM" يتم قراءته بشكل صحيح كتنسيق أرقام، بينما رمز مخصص مقتضب لا يحتوي إلا على حرف m أو d واحد خارج علامات الاقتباس لا يزال يتم التعرف عليه بشكل صحيح كتاريخ.

قراءة التواريخ من ملفات الطرف الثالث

يتقارب كل ما سبق في سير عمل واحد: تحويل رقم كتبه تطبيق آخر إلى تاريخ يمكنك الوثوق به. يمنحك الرقم التسلسلي عدد الأيام، ويخبرك علم Date1904 لكتيب العمل بالحقبة التي يتم قياس العدد منها، وتنسيق أرقام الخلية أو الرمز المخصص هو الدليل الوحيد على أن الرقم كان مقصوداً به أن يكون تاريخاً في المقام الأول. اسقط أي من الثلاثة وتحصل على إجابة خاطئة معقولة بدلاً من خطأ مرئي.

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Cell: TXLSXCell;
  r: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open('vendor-export.xlsx') <> 1 then
      raise Exception.Create('Cannot open export');

    // The 1904 flag is workbook-wide: read it once, apply it to
    // every serial the workbook hands back.
    if Book.Date1904 then
      Writeln('workbook uses the 1904 date system')
    else
      Writeln('workbook uses the 1900 date system');

    Sheet := Book.Sheets[0];
    for r := 1 to 10 do
    begin
      Cell := Sheet.Cells[r, 1];
      // A date is only a date when its format says so; the same numeric
      // value with a plain format is just a quantity.
      Writeln(Format('row %d  value=%s  numFmt=%d  code="%s"',
        [r, VarToStr(Cell.Value), Cell.NumberFormatIndex, Cell.NumberFormat]));
    end;
  finally
    Book.Free;
  end;
end;

يحتوي جانب BIFF القديم على مصيدة إضافية تستحق التسمية. في تدفق .xls أقدم، يمكن تعبئة مجموعة من خلايا الأرقام المتجاورة في سجل واحد متعدد الخلايا، وهو MULRK، والذي يخزن عدة قيم مع مراجع تنسيقها في بنية واحدة. خلايا التاريخ المخزنة بهذه الطريقة ليست أقل تواريخ لكونها معبأة، لذا يجب تطبيق نفس اختبار معرف التنسيق لكل خلية، ولا تزال إزاحة 1904 تحكم كل رقم تسلسلي تنتجه. القارئ الذي يفحص فقط سجلات الأرقام المستقلة، ويتخطى السجلات المعبأة، سيحول بصمت عموداً من التواريخ إلى عمود من الأعداد الصحيحة.

رسم الخرائط التسلسلية لـ TDateTime في الممارسة العملية

بمجرد أن يؤكد فحص التنسيق التاريخ ويعرف علم Date1904، يكون التحويل ميكانيكياً. القيمة التي يرجعها HotXLS بالفعل كـ varDate هي TDateTime يمكنك استخدامه مباشرة. القيمة التي تصل كـ Double مجرد، وهو ما يحدث عندما يكتب المصدر رقماً تسلسلياً بدون تنسيق تاريخ معترف به، يتم تحويلها عن طريق قراءتها كعدد أيام على محور 1900، وبالنسبة لكتيب عمل 1904، طرح إزاحة 1462 يوماً أولاً بحيث تتطابق الحقب. وبالذهاب في الاتجاه الآخر، فإن تعيين TDateTime لخلية يخزن الرقم التسلسلي المستند إلى 1900، ويطبق HotXLS نفس إزاحة 1462 يوماً عند الحفظ عندما يتم وضع علم Date1904 لكتيب العمل، بحيث يعرض الملف المحفوظ اليوم الذي قصدته بدلاً من اليوم المنحرف بأربع سنوات.

ضع العلم عمداً عند إنشاء كتيب عمل. يترك الافتراضي Date1904 خاطئاً، وهو ما يطابق Excel لنظام Windows وهو تقريباً كل ما تريده دائماً؛ وضعه صحيحاً فقط عندما تقوم بإعادة إنتاج كتيب عمل Mac أو يتوقع نظام لاحق محور 1904 على وجه التحديد. القاعدة الوحيدة التي تمنع فئة أخطاء السنوات الأربع بأكملها هي الاتساق: اختر الحقبة مرة واحدة لكل كتيب عمل، واكتب كل تاريخ تحتها، واقرأ كل رقم تسلسلي تحت العلم الذي يحمله الملف بالفعل.

التواريخ هي عمود واحد في قصة أوسع حول ما تحتويه الخلية بالفعل. تغطية طبقة البيانات التعريفية المجاورة، العنوان والمؤلف والطوابع الزمنية التي تصاحب الشبكة، في مقالنا حول البيانات التعريفية لكتيب العمل وخصائص المستند، حيث يتم تخزين قيم Created و Modified نفسها كـ TDateTime بنفس اتفاقية غير المعين يساوي صفراً. عندما يكون التاريخ نتيجة عملية حسابية وليس قيمة مخزنة، فإن قواعد التقييم في مقالنا حول محرك الصيغ والوظائف المخصصة تحدد الرقم التسلسلي الذي يرسمه التنسيق بعد ذلك. كلاهما يعمل على نفس نموذج التاريخ الذي يتم شحنه في مكون HotXLS لـ Delphi و C++Builder، والذي يقرأ ويكتب تواريخ XLS و XLSX دون أتمتة Excel.