معالجة التقويم الياباني والعطلات الرسمية وتاريخ الإغلاق في تطبيقات الأعمال — تصميم مقاوم لتغيّر العصر، وJapaneseCalendar، وحساب أيام العمل في الممارسة العملية

· · C#, ‎.NET, التقويم الياباني (وارِكي), العطلات الرسمية, تاريخ الإغلاق, معالجة التاريخ والوقت, تطبيقات الأعمال, تطوير Windows, الاستشارات التقنية

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

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

1. الخلاصة أولًا

  • تبقى المعالجة الداخلية والتخزين بالتقويم الميلادي؛ ولا يوجد التقويم الياباني إلا في لحظة العرض. يؤدي تخزين سلاسل نصية بالتقويم الياباني إلى مشكلة ثلاثية: استحالة الفرز، واختلاط الصيغ بعد تغيّر العصر، وخطر تسلل تواريخ غير صالحة.
  • فوّض تحويل العصر إلى JapaneseCalendar بدلًا من جدول تناظر مُعدّ يدويًا. تُوفَّر تعريفات العصور من نظام التشغيل، لذا يكفي تحديث المنصة لمواكبة تغيّر العصر.1 ولا يتخلف عن الركب سوى السلاسل النصية الثابتة مثل «هيسي» أو «ريوا» المتبقية داخل التطبيق.
  • لا يمكن تحديد العطلات الرسمية بمعادلة حسابية. إذ يُحدَّد الاعتدالان الربيعي والخريفي في السنة السابقة فقط، وقد تُنقل العطلات الرسمية نفسها بتعديل قانوني عادي أو بقانون تدابير خاصة.2 صمّم النظام حول جدول مرجعي للعطلات الرسمية + عملية تحديث، ويمكنك استخدام ملف CSV للعطلات الرسمية الصادر عن مكتب مجلس الوزراء الياباني كمصدر بيانات.
  • ينبغي تعريف «نهاية الشهر» و«يوم العمل» و«السنة المالية» في المواصفات قبل تعريفها في الكود. وعلى وجه الخصوص، تُقرِّب AddMonths نهاية الشهر تلقائيًا، لذا ينبغي أن يُحسَب تاريخ الإغلاق دائمًا انطلاقًا من تاريخ مرجعي ثابت.3

فيما يلي ملخص لكيفية حفظ كل نوع من أنواع البيانات، أولًا.

البيانات طريقة الحفظ لحظة التحويل/التقييم
التاريخ نفسه (تاريخ الطلب، موعد استحقاق الدفع) DateOnly / DateTime بالتقويم الميلادي (عمود من نوع date في قاعدة البيانات)
صيغة التقويم الياباني (التقارير، الشاشات) لا تُحفَظ تحويل عبر JapaneseCalendar فقط في لحظة العرض/الطباعة
العطلات الرسمية جدول مرجعي للعطلات الرسمية (جدول أو ملف تهيئة) يُرجَع إليه عند حساب أيام العمل
أيام إغلاق الشركة (العطلة الصيفية، ذكرى التأسيس) جدول مرجعي منفصل عن العطلات الرسمية نفس ما سبق (لا يُخلَط مع العطلات الرسمية)
تاريخ الإغلاق / شروط الدفع خاصية في سجل الطرف التجاري («إغلاق في اليوم 20، دفع في نهاية الشهر التالي») يُحسَب من تاريخ مرجعي عند إصدار الفاتورة
السنة المالية لا تُحفَظ (قيمة مشتقة) تُحسَب من التاريخ عند العرض/التجميع

2. التقويم الياباني — اجعله مقصورًا على العرض

2.1 لماذا يجب توحيد المعالجة الداخلية على التقويم الميلادي

التقويم الياباني هو صيغة عرض موجّهة للبشر، وليس تمثيلًا مناسبًا للحساب. لا يوجد ترتيب محدد لـ«هيسي 9» و«ريوا 9» عند المقارنة النصية، وأي حساب لفترة زمنية تعبر حدود العصر ينتهي به الأمر إلى الحاجة إلى التحويل إلى التقويم الميلادي على أي حال. والأسوأ من ذلك أن نظام العصور يتوسع مع كل انتقال، لذا فإن البيانات التي تبقى محفوظة بصيغة التقويم الياباني تتراكم فيها صيغ قديمة وجديدة مختلطة مع كل تغيّر. كما أن تسلل بيانات غير صالحة مثل «شووا 64، 10 يناير» (تاريخ لم يوجد قط، إذ كانت هيسي قد بدأت بالفعل اعتبارًا من 8 يناير) مشكلة كلاسيكية في الأنظمة التي تحفظ إدخال العصر الخام كما هو.

هناك قيد آخر يُغفَل بسهولة: لا تدعم JapaneseCalendar في ‎.NET‎ سوى التواريخ ابتداءً من 8 سبتمبر من السنة الأولى لعصر ميجي (1868).4 فالتواريخ الأقدم — مثل تاريخ ميلاد قديم مستمد من سجل عائلي — لا يمكن معالجتها بشكل صحيح إن بقيت بصيغة العصر. بتوحيد المعالجة الداخلية على التقويم الميلادي، تنحصر كل هذه المشكلات في طبقة العرض وحدها.

2.2 العرض والإدخال باستخدام JapaneseCalendar

فئتا التقويم الوحيدتان في ‎.NET‎ القادرتان على التعامل مع عصور متعددة هما JapaneseCalendarJapaneseLunisolarCalendar)، ويقابل محدد التنسيق g اسم العصر.1 فيما يلي الحد الأدنى من الكود لعرض تاريخ بالتقويم الياباني.

using System.Globalization;

var jaJpWareki = new CultureInfo("ja-JP");
jaJpWareki.DateTimeFormat.Calendar = new JapaneseCalendar();

var date = new DateOnly(2026, 7, 11);
Console.WriteLine(date.ToString("ggy年M月d日", jaJpWareki));  // 令和8年7月11日
Console.WriteLine(date.ToString("ggyy/MM/dd", jaJpWareki));   // 令和08/07/11

بالنسبة لقبول الإدخال بالتقويم الياباني، من الأكثر أمانًا تصميم واجهة يختار فيها المستخدم العصر من قائمة منسدلة ويُدخِل السنة والشهر واليوم كأرقام، بدلًا من تحليل سلسلة نصية حرة. وإذا كان لا بد من قبول نص حر، فثبِّت التنسيق باستخدام ParseExact.

var input = "令和8年7月11日";
var parsed = DateTime.ParseExact(input, "ggy年M月d日", jaJpWareki);
Console.WriteLine(parsed.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));  // 2026-07-11

تتطلب صيغة السنة الأولى من كل عصر حذرًا. في البيئات التي طُبِّقت فيها تحديثات دعم العصور، تُنسَّق السنة الأولى من كل عصر افتراضيًا كـ«ريوا غان-نن» (元年، وتعني حرفيًا «سنة التأسيس») بدلًا من رقم، وتتوفر مفتاح توافقية — Switch.System.Globalization.FormatJapaneseFirstYearAsANumber — لاستعادة الصيغة الرقمية «ريوا 1».5 تحقّق قبل التنفيذ، كجزء من المواصفات، مما إذا كان تنسيق التقرير يتوقع «ريوا 1» أم «ريوا غان-نن». نقطة أخرى: بشكل افتراضي، يكون التحقق من نطاق سنوات كل عصر متساهلًا، لذا تُقبَل قيمة غير موجودة مثل «هيسي 33» وتُحوَّل إلى التاريخ الفعلي المقابل (ريوا 3). إذا احتجت إلى تحقق صارم من الإدخال يرفض مثل هذه القيم، يمكنك فرض ذلك عبر Switch.System.Globalization.EnforceJapaneseEraYearRanges.5 وسيتوقف قرار إبقاء التحقق متساهلًا أو جعله صارمًا على مدى «تلوّث» البيانات القائمة — وتحديدًا كيفية التعامل مع قيمة مثل «هيسي 32» طُبِعت أو أُدخلت قبل تغيّر العصر.

2.3 كود مقاوم لتغيّر العصر، وما يتخلف عن الركب

ما يمكن استخلاصه من تجربة انتقال عصر ريوا (2019) هو أن المنصة تواكب التغيير، أما التنفيذ المُعدّ يدويًا للتطبيق فلا يواكبه. تعريف أسماء العصور غير مكتوب بشكل ثابت في ‎.NET‎ — إذ تعتمد الآلية على الرجوع إلى معلومات العصر التي يحتفظ بها نظام التشغيل — لذا فبمواصلة تطبيق تحديثات نظام التشغيل و‎.NET‎، يواكب تحويل JapaneseCalendar وتنسيقه العصر الجديد تلقائيًا.1 وما يتخلف عن الركب هو أصول على جانب التطبيق مثل التالية:

  • جداول أو فروع تحويل عصر مُعدّة يدويًا، مثل if (year >= 2019) era = "令和";
  • أسماء عصور مكتوبة مباشرة داخل قوالب التقارير، أو تنسيقات خلايا Excel، أو النماذج المطبوعة مسبقًا
  • كود يعالج بقواعد خاصة اختصار العصر بحرف واحد مثل «R» أو «H» (شائع في تنسيقات التبادل مع الأنظمة الأساسية)
  • البيانات المخزَّنة نفسها، التي تبقى بصيغة التقويم الياباني

عند تقدير تكلفة إصلاح متعلق بتغيّر العصر، ابدأ بتمشيط الكود المصدري والقوالب بالكامل آليًا بحثًا عن «شووا» و«هيسي» و«ريوا» و«غان-نن» لإجراء جرد شامل. من شبه المؤكد أن تطبيقات الأعمال التي تعمل منذ عصر VB6 أو Access تحتوي في مكان ما على جدول تحويل مُعدّ يدويًا، لذا غالبًا ما تُعالَج هذه المسألة جنبًا إلى جنب مع قرار الاستمرارية الموضّح في «إطالة عمر تطبيق أعمال VB6 / Access أو ترحيله».

2.4 كمين خفي: إعدادات المنطقة في Windows

فخ آخر يُغفَل بسهولة. في إعدادات المنطقة في Windows، يمكن للمستخدم تغيير نوع تقويم نظام التشغيل إلى التقويم الياباني. وبما أن CultureInfo.CurrentCulture تعكس افتراضيًا تغيير إعدادات المستخدم (ما يُعرف بـ«تجاوز المستخدم»)،6 فعلى جهاز يحمل هذا الإعداد، يبدأ DateTime.ToString() بالتنسيق الافتراضي للثقافة فجأة بإرجاع سلاسل نصية بالتقويم الياباني مثل «令和08/07/11». فإذا كان الكود يكتب التواريخ إلى السجلات أو ملفات CSV أو قاعدة البيانات باستخدام ToString() الافتراضي للثقافة، فإن التقويم الياباني يتسلل إلى البيانات المخزَّنة على ذلك الجهاز فقط، مما يُعطِّل عملية التحليل لاحقًا.

يعود الحل إلى نفس الاستنتاج الذي توصلنا إليه في المقال السابق: يجب دائمًا كتابة أي سلسلة نصية لتاريخ تعبر حدًا فاصلًا — التخزين، الاتصال، التسجيل — باستخدام CultureInfo.InvariantCulture وتنسيق صريح. التنسيق الافتراضي للثقافة مخصص للعرض على الشاشة فقط. لمزيد من التفاصيل حول تأثير الثقافة على العرض بشكل عام، راجع أيضًا «تعريب تطبيقات WinForms/WPF».

3. العطلات الرسمية — لماذا لا يجب كتابتها بشكل ثابت في الكود مطلقًا، وكيفية إدارة جدول مرجعي

3.1 العطلات الرسمية «تتغيّر»

لا تكتب أبدًا اكتشاف العطلات الرسمية على هيئة switch (شهر, يوم). تُحدَّد العطلات الرسمية بموجب قانون العطلات الوطنية، وقد تغيّرت مرارًا بفعل التعديلات القانونية والممارسات التشغيلية.2 وفيما يلي الأمثلة الحديثة وحدها:

السنة التغيير
2016 استُحدث يوم الجبل (11 أغسطس)
2019 عقب تغيّر العصر، انتقل عيد ميلاد الإمبراطور من 23 ديسمبر إلى 23 فبراير. لم يكن هناك عيد ميلاد للإمبراطور إطلاقًا في عام 2019
2019 بموجب نظام خاص مرتبط بخلافة العرش، أصبح 1 مايو (يوم التتويج) وتواريخ أخرى عطلات استثنائية
2020 أُعيدت تسمية يوم الرياضة والصحة إلى يوم الرياضة
2020-2021 بموجب قانون تدابير خاصة مرتبط بالأولمبياد، انتقل يوم البحر ويوم الرياضة ويوم الجبل لهذين العامين فقط

إضافة إلى ذلك، لا يحمل يوم الاعتدال الربيعي ولا يوم الاعتدال الخريفي أصلًا أي تاريخ مكتوب في القانون. فهما يُحدَّدان ويُعلَنان في السنة السابقة استنادًا إلى حسابات فلكية، لذا فإن الطريقة الوحيدة الموثوقة لمعرفة اعتدال مستقبلي هي انتظار الإعلان الرسمي.2 إن أي معادلة حسابية تفترض أن «العطلات الرسمية متطابقة كل عام» غير قابلة للتحقيق من حيث المبدأ.

3.2 جدول قرار لمصادر البيانات وطريقة الحفظ

صمّم تحديد العطلات الرسمية على أنه «بيانات + عملية تحديث». وفيما يلي مقارنة بين الخيارات الرئيسية.

الأسلوب المحتوى الحالات المناسبة التنبيهات
جدول مرجعي للعطلات الرسمية (إدارة ذاتية) جدول يحتوي على التاريخ والاسم، يُحدَّث عبر شاشة إدارية الأنظمة التجارية عمومًا التي ترغب في إدارته جنبًا إلى جنب مع أيام إغلاق الشركة دمج التحديث ضمن العملية التشغيلية (تحويل الفحص السنوي إلى تذكير في التقويم)
استيراد ملف CSV من مكتب مجلس الوزراء استيراد ملف CSV للعطلات الرسمية الصادر عن مكتب مجلس الوزراء لتحديث الجدول المرجعي عندما تكون هناك حاجة إلى مصدر أساسي موثوق لتحديثات الجدول (يُوصى به) يحتوي فقط على الفترة المنشورة بالفعل. انتبه لترميز الأحرف
مكتبة عطلات رسمية (NuGet وغيرها) التحديد عبر منطق حسابي + بيانات مضمَّنة استخدامات صغيرة النطاق مثل الأدوات الداخلية التي لا يمكنها الحفاظ على عملية تحديث تعتمد التعديلات القانونية المستقبلية على تحديث المكتبة. يجب التحقق من القواعد المضمَّنة
الكتابة الثابتة في الكود تواريخ مكتوبة مباشرة في الكود المصدري لا يوجد كل تعديل قانوني يفرض إعادة توزيع على جميع العملاء

كمصدر أساسي، يمكنك استخدام ملف CSV لتواريخ وأسماء العطلات الرسمية من عصر شووا 30 (1955) وحتى السنة التالية، الذي ينشره مكتب مجلس الوزراء في صفحته «العطلات الوطنية».2 هناك احتياطان عمليان عند الاستيراد: (1) بما أن المنشور هو فقط الفترة المؤكدة، حدِّد ضمن المواصفات السلوك المتّبع عند الاستعلام عن عطلة رسمية بعد سنتين أو أكثر (إرجاع خطأ، أو التعبئة بقيمة مؤقتة مبنية على القانون مع علامة «مؤقت»)؛ (2) ترميز أحرف هذا الملف ينتمي إلى عائلة Shift_JIS، لذا يجب تحديد الترميز صراحةً عند القراءة، كما هو موضح في «ملف CSV ليس مجرد «نص عادي»».

3.3 العطلة البديلة والعطلة الوطنية وحساب أيام العمل

بالإضافة إلى قائمة التواريخ، يُعرِّف قانون العطلات قاعدتين مشتقتين: العطلة البديلة (عندما تصادف عطلة رسمية يوم أحد، يصبح أقرب يوم تالٍ «ليس عطلة رسمية» يومَ عطلة)، والعطلة الوطنية (يوم عمل محصور بين عطلتين رسميتين يصبح يوم عطلة).2 يتضمن ملف CSV من مكتب مجلس الوزراء هذه القواعد أيضًا كصفوف بيانات، لذا يكفي مع أسلوب استيراد CSV الرجوع إلى الجدول المرجعي فقط. وحتى إذا نفّذت هذا المنطق بنفسك، فإنه مستقر نسبيًا لأنه مستمد من القانون.

اكتب حساب أيام العمل بثلاثة شروط: ليس يوم سبت أو أحد، وغير موجود في الجدول المرجعي للعطلات الرسمية، وغير موجود في جدول أيام إغلاق الشركة.

public sealed class BusinessDayCalendar
{
    private readonly HashSet<DateOnly> _publicHolidays;   // جدول رئيسي للعطلات الرسمية (يشمل العطلات البديلة)
    private readonly HashSet<DateOnly> _companyHolidays;  // جدول رئيسي لعطلات الشركة

    public BusinessDayCalendar(IEnumerable<DateOnly> publicHolidays,
                               IEnumerable<DateOnly> companyHolidays)
    {
        _publicHolidays = [.. publicHolidays];
        _companyHolidays = [.. companyHolidays];
    }

    public bool IsBusinessDay(DateOnly d) =>
        d.DayOfWeek is not (DayOfWeek.Saturday or DayOfWeek.Sunday)
        && !_publicHolidays.Contains(d)
        && !_companyHolidays.Contains(d);

    // إذا صادف الموعد النهائي عطلة، يُرجَّح إلى «يوم العمل السابق». تحديد التأجيل للأمام يعتمد على شروط العقد
    public DateOnly ToPreviousBusinessDay(DateOnly d)
    {
        while (!IsBusinessDay(d)) d = d.AddDays(-1);
        return d;
    }
}

الفصل بين العطلات الرسمية وأيام إغلاق الشركة في جدولين مرجعيين منفصلين أمر مقصود. فـ«تحديد موعد استحقاق تحويل بنكي ينظر إلى العطلات الرسمية فقط (عطلتنا الصيفية لا علاقة لها بذلك)»، بينما «تحديد تاريخ الشحن ينظر إلى كليهما» — أي أن مجموعة أيام العطلة الواجب مراعاتها تختلف باختلاف حالة الاستخدام. فإذا مزجتهما في جدول واحد، يتحول فصلهما لاحقًا إلى عملية تنقيب أثري في بيانات مخزَّنة بالفعل.

4. تاريخ الإغلاق وشروط الدفع والسنة المالية — معالجة «نهاية الشهر» بشكل صحيح في الحسابات

4.1 سلوك التقريب في AddMonths

الفخ الأول في حساب تاريخ الإغلاق هو AddMonths. تقوم DateTime.AddMonths / DateOnly.AddMonths بالتقريب إلى آخر يوم في الشهر الناتج عندما يؤدي الحساب المباشر إلى يوم غير موجود في ذلك الشهر.3

var jan31 = new DateOnly(2026, 1, 31);
var feb = jan31.AddMonths(1);   // 2026-02-28 ── تقريب إلى نهاية الشهر (صحيح)
var mar = feb.AddMonths(1);     // 2026-03-28 ── ما كان يُقصَد به «نهاية الشهر» يتحول بصمت إلى اليوم 28

التقريب نفسه محدَّد بشكل صحيح، لكن تسلسل استدعاء AddMonths آخر على نتيجة مُقرَّبة بالفعل يفقد النية المقصودة بـ«نهاية الشهر». يظهر هذا عمليًا عندما يُبنى جدول فوترة شهري على أساس «تاريخ التنفيذ السابق + شهر واحد»: فبمجرد تجاوز فبراير، يبدأ الحساب بالانزياح بشكل دائم نحو اليوم 28 من كل شهر. هناك إجراءان مضادان:

  • بالنسبة للتواريخ المتكررة، احسب دائمًا انطلاقًا من تعريف نقطة المرجعية («نهاية كل شهر»، «اليوم 20 من كل شهر»)، وليس من النتيجة السابقة
  • اشتقّ «نهاية الشهر» من DateTime.DaysInMonth(y, m) بدلًا من قيمة تاريخ حرفية

4.2 تنفيذ «إغلاق في اليوم 20، دفع في نهاية الشهر التالي»

تاريخ الإغلاق وشروط الدفع خصائص خاصة بكل طرف تجاري. تنفيذ «إغلاق في اليوم 20، دفع في نهاية الشهر التالي» بالكود يكون كالتالي:

public static class PaymentTerms
{
    // تاريخ المعاملة ← تاريخ الإغلاق (مثال: الإغلاق في اليوم 20 من كل شهر)
    public static DateOnly GetClosingDate(DateOnly tradeDate, int closingDay)
    {
        var dayInMonth = Math.Min(closingDay, DateTime.DaysInMonth(tradeDate.Year, tradeDate.Month));
        var closing = new DateOnly(tradeDate.Year, tradeDate.Month, dayInMonth);
        return tradeDate <= closing ? closing : closing.AddMonths(1);
        // ملاحظة: القيمة closingDay=31 (إغلاق نهاية الشهر) تصبح بشكل صحيح نهاية الشهر التالي بفضل تقريب AddMonths
    }

    // تاريخ الإغلاق ← موعد الدفع (مثال: الدفع في نهاية الشهر التالي). تحقق من مواصفات تعديل العطلات (تقديم/تأخير) قبل التطبيق
    public static DateOnly GetPaymentDueDate(DateOnly closingDate, BusinessDayCalendar calendar)
    {
        var nextMonth = closingDate.AddMonths(1);
        var endOfMonth = new DateOnly(nextMonth.Year, nextMonth.Month,
                                      DateTime.DaysInMonth(nextMonth.Year, nextMonth.Month));
        return calendar.ToPreviousBusinessDay(endOfMonth);  // بالنسبة للعقود التي تطبق قاعدة «إذا كانت عطلة، الترجيح إلى يوم العمل السابق»
    }
}

الأهم من الكود هو تثبيت المواصفات مسبقًا. في «الإغلاق بنهاية الشهر»، هل تُحسَب نهاية الشهر بالأيام التقويمية أم بأيام العمل؟ إذا صادف الموعد يوم عطلة، هل يُقدَّم أم يُؤخَّر (يميل الطرف الدافع إلى التقديم، بينما يميل الطرف المستلِم إلى تأخير تاريخ القبض المتوقع — تختلف الممارسة باختلاف وجهة النظر)؟ كيف يُفسَّر في فبراير عقد يقع إغلاقه بين اليومين 29 و31؟ ما لم تُوثَّق هذه النقاط، يصبح كود المنفِّذ كما هو «مواصفة لم يوافق عليها أحد». تنتهي غالبية عمليات تحقيق الأخطاء المتعلقة بتاريخ الإغلاق، عند التحقيق فيها، ليس إلى خطأ في الكود، بل إلى غياب المواصفة.

4.3 السنة المالية والربع السنوي

في اليابان، تبدأ «السنة المالية» لتطبيق الأعمال في أبريل تقريبًا دائمًا. لا تحفظها؛ بل تعامل معها كقيمة مشتقة من التاريخ.

public static int GetFiscalYear(DateOnly d) => d.Month >= 4 ? d.Year : d.Year - 1;
// 2026-03-31 ← السنة المالية 2025، 2026-04-01 ← السنة المالية 2026

public static int GetFiscalQuarter(DateOnly d) => ((d.Month + 8) % 12) / 3 + 1;
// أبريل–يونيو ← Q1، يوليو–سبتمبر ← Q2، أكتوبر–ديسمبر ← Q3، يناير–مارس ← Q4

انتبه إلى عدم تخزين السنة المالية بشكل متكرر ودون داعٍ في قاعدة البيانات. فحفظ قيمة يمكن دائمًا اشتقاقها من التاريخ في عمود منفصل يخلق خطر ظهور بيانات لا يتطابق فيها التاريخ مع السنة المالية عند نسيان التحديث أثناء تصحيح تاريخ. وإذا كان لا بد من الاحتفاظ بها لأسباب تتعلق بأداء التجميع، اجعلها عمودًا محسوبًا (مولَّدًا) وامنع الكتابة المباشرة إليه من التطبيق.

4.4 الاختبار — استباق الأخطاء التي لا تظهر إلا «في ذلك اليوم بالذات»

لا تظهر أخطاء العصر والعطلات الرسمية وتاريخ الإغلاق إلا في تواريخ محددة. بالاستناد إلى حقن الوقت الحالي عبر TimeProvider الذي عُرِض في المقال السابق (الفصل السابع من «التاريخ والوقت والمناطق الزمنية في تطبيقات الأعمال»)، إليك خمسة تواريخ ينبغي اختبارها كحد أدنى في هذا المجال:

  • حدود تغيّر العصر: عرض والتحويل العكسي لإدخال بالتقويم الياباني حول 1989-01-07/08 (شووا ← هيسي) و2019-04-30/05-01 (هيسي ← ريوا)
  • السنة الأولى من العصر: التحقق من صيغة «ريوا غان-نن» في التقارير (القسم 2.2)
  • السنوات الكبيسة: حسابات الإغلاق وموعد استحقاق الدفع التي تشمل 29 فبراير (2024، 2028)
  • سلسلة نهايات الشهور: توليد اثني عشر شهرًا من تواريخ الإغلاق بدءًا من 31 يناير، والتحقق من الحفاظ على نهاية الشهر (القسم 4.1)
  • حدود السنة المالية: تحديد السنة المالية والربع السنوي ليومي 31 مارس و1 أبريل

بالإضافة إلى ذلك، يمكن أثناء مراجعة الكود اكتشاف الأنماط التالية آليًا عبر grep.

كود يُعثَر عليه، يستحق الشك ما الذي قد يحدث طريقة الإصلاح
قيم حرفية للعصر مثل "平成" أو "令和" يتخلف عن الركب عند تغيّر العصر التالي الانتقال إلى JapaneseCalendar مع تنسيق g (القسم 2.3)
قيم حرفية لتواريخ العطلات الرسمية (مثل 5, 3) تحديد خاطئ بعد تعديل قانوني أو قانون تدابير خاصة الانتقال إلى الرجوع إلى الجدول المرجعي للعطلات الرسمية (القسم 3.2)
مسار حفظ/إخراج يستخدم ToString() بدون وسائط تسلل التقويم الياباني إلى البيانات المخزَّنة على جهاز مضبوط على عرض العصر InvariantCulture مع تنسيق صريح (القسم 2.4)
تكرار التاريخ السابق.AddMonths(1) استمرار انزياح نهاية الشهر بعد تجاوز فبراير الحساب في كل مرة انطلاقًا من تاريخ مرجعي (القسم 4.1)
new DateTime(y, m, 31) يُطلِق استثناءً في الأشهر ذات 30 يومًا أو أقل اشتقاقه من DateTime.DaysInMonth

5. الخلاصة

يشترك التقويم الياباني والعطلات الرسمية وتاريخ الإغلاق في التعامل مع «مواصفات تتغيّر لاحقًا». لذلك فإن مبدأ التصميم واحد في الحالات الثلاث: لا تكتب في الكود ما قد يتغيّر، بل اعزله في البيانات وفي طبقة تحويل. وحِّد المعالجة الداخلية والتخزين على التقويم الميلادي، مع الاحتفاظ بالتقويم الياباني كتحويل مخصص للعرض فقط؛ واحتفظ بالعطلات الرسمية في جدول مرجعي مصحوب بعملية تحديث؛ ووثِّق «نهاية الشهر» و«يوم العمل» و«تعديل الموعد عند وقوعه في عطلة رسمية» كمواصفة تعاقدية قبل حسابها انطلاقًا من تاريخ مرجعي. افعل كل ذلك، وسيصبح تغيّر العصر القادم، وكذلك التعديل القادم لقانون العطلات الرسمية، بالنسبة لتطبيقك، مجرد حدثين يُعالَجان بـ«تحديث الجدول المرجعي» و«تطبيق تحديث نظام التشغيل».

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

مقالات ذات صلة

مجالات الاستشارة ذات الصلة

تتعامل شركة Komura Soft LLC (合同会社小村ソフト) مع تصميم وتنفيذ معالجة التواريخ لتطبيقات الأعمال بما يتوافق مع الأعراف التجارية اليابانية (التقارير بالتقويم الياباني، وحساب أيام العمل، ومعالجة تاريخ الإغلاق)، إلى جانب جرد وإصلاح دعم تغيّر العصر والعطلات الرسمية في التطبيقات القائمة.

روابط مرجعية

  1. Microsoft Learn، Work with eras - .NET. حول كون JapaneseCalendar وJapaneseLunisolarCalendar هما فئتا التقويم الوحيدتان اللتان تتعرفان على عصور متعددة، وتنسيق العصر عبر محدد التنسيق «g»، وكون معلومات العصر تُوفَّر من المنصة بدلًا من الاعتماد على كتابة ثابتة في التطبيق.  2 3

  2. مكتب مجلس الوزراء الياباني، «عن العطلات الوطنية». حول قائمة العطلات الرسمية بموجب قانون العطلات الوطنية، وكون يومي الاعتدال الربيعي والخريفي يُحدَّدان ويُعلَنان في السنة السابقة، وأحكام العطلة البديلة والعطلة الوطنية، وتوفّر ملف CSV لتواريخ وأسماء العطلات الرسمية من عصر شووا 30 وحتى السنة التالية.  2 3 4 5

  3. Microsoft Learn، DateTime.AddMonths(Int32) Method. حول كون النتيجة تُضبَط إلى آخر يوم في الشهر الناتج عندما يتجاوز اليوم الناتج عدد أيام ذلك الشهر (على سبيل المثال، 31 يناير زائد شهر واحد يصبح 28/29 فبراير).  2

  4. Microsoft Learn، JapaneseCalendar Class. حول نطاق التواريخ الذي تدعمه JapaneseCalendar — ابتداءً من 8 سبتمبر من السنة الأولى لعصر ميجي (1868 ميلاديًا) — وكيفية التعامل مع العصور. 

  5. Microsoft Learn، Work with calendars - .NET. حول السلوك الافتراضي المتمثل في تنسيق السنة الأولى من العصر كـ«غان-نن»، ومفتاح Switch.System.Globalization.FormatJapaneseFirstYearAsANumber الذي يستعيد الصيغة الرقمية، وكون التحقق من نطاق سنوات العصر متساهلًا افتراضيًا لكن يمكن جعله صارمًا عبر Switch.System.Globalization.EnforceJapaneseEraYearRanges.  2

  6. Microsoft Learn، CultureInfo.CurrentCulture Property. حول كون CurrentCulture تعكس افتراضيًا تغييرات إعدادات المنطقة لمستخدم Windows (تجاوز المستخدم)، وكون التنسيق الافتراضي يعتمد على هذه الثقافة. 

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

تطوير تطبيقات ويندوز

تنفيذ متطلبات العمل الخاصة باليابان مثل عرض التاريخ بالتقويم الياباني في التقارير، وحساب أيام العمل، ومعالجة تاريخ الإغلاق، يقع في صميم نطاق استشارات تطوير تطبيقات الأعمال على Windows.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

هل من الخطأ حفظ التواريخ في قاعدة البيانات بصيغة التقويم الياباني كما هي؟
لا ننصح بذلك، لأربعة أسباب. أولًا، تفقد سلاسل نصية مثل «هيسي 9» و«ريوا 9» ترتيبها المنطقي عند المقارنة النصية بمجرد تجاوز حدود العصر، مما يجعل الفرز والبحث ضمن نطاق زمني مستحيلًا. ثانيًا، في اللحظة التي يحدث فيها تغيّر العصر التالي، تختلط الصيغ القديمة والجديدة في البيانات المخزّنة بالفعل. ثالثًا، من السهل أن تتسلل بيانات فاسدة تحتوي على تواريخ لم توجد قط، مثل «شووا 64، 10 يناير» (في ذلك اليوم كانت هيسي 1 قد بدأت بالفعل، إذ تغيّر العصر في 8 يناير)، فتتحول إلى قنبلة موقوتة عند تحليل البيانات. رابعًا، لا تدعم JapaneseCalendar في ‎.NET‎ سوى التواريخ ابتداءً من 8 سبتمبر من السنة الأولى لعصر ميجي (1868)؛ فالتواريخ الأقدم — مثل تاريخ ميلاد قديم مستمد من سجل عائلي — لا يمكن معالجتها بشكل صحيح إن بقيت بصيغة العصر. القاعدة هي توحيد المعالجة الداخلية والتخزين على التقويم الميلادي، والتحويل إلى التقويم الياباني فقط في لحظة العرض أو الطباعة.
كيف ينبغي لتطبيق أن يستعد لتغيّر العصر القادم؟
تعريف أسماء العصور غير مكتوب بشكل ثابت داخل ‎.NET‎ نفسها؛ بل تعتمد الآلية على الرجوع إلى معلومات العصر التي يوفّرها نظام التشغيل، لذا يكفي الاستمرار في تطبيق تحديثات نظام التشغيل و‎.NET‎ حتى تتبع القدرة على «العرض بالعصر الجديد» تلقائيًا. وحتى في انتقال عصر ريوا، تمثّل جوهر الاستجابة في تطبيق تحديثات المنصة فحسب. المشكلة الحقيقية تكمن في كود التطبيق وبياناته: الفروع الشرطية التي تكتب «هيسي» أو «ريوا» كسلاسل نصية ثابتة، ودوال التحويل المُعدّة يدويًا التي تمتلك جدول تناظر خاصًا بها بين العصر والسنة الميلادية، وأسماء العصور المكتوبة مباشرة داخل قوالب التقارير، وحقول «ريوا __» على النماذج المطبوعة مسبقًا — لا شيء من ذلك يتبع تلقائيًا. هناك أربعة إجراءات عملية معقولة: (1) توحيد المعالجة الداخلية والتخزين على التقويم الميلادي بحيث يقتصر التقويم الياباني على العرض فقط؛ (2) التخلي عن منطق تحويل العصر المُعدّ يدويًا والاعتماد على JapaneseCalendar بدلًا منه؛ (3) تمشيط الكود المصدري بالكامل آليًا بحثًا عن «شووا» و«هيسي» و«ريوا» لإجراء جرد شامل؛ (4) إعداد اختبارات تفترض تغيّر عصر مستقبلي افتراضي.
ألا يمكن تحديد العطلات الرسمية بمعادلة حسابية؟ ولماذا يجب حفظها كبيانات؟
من المستحيل من حيث المبدأ بناء معادلة كاملة، لثلاثة أسباب. أولًا، يُحدَّد يوم الاعتدال الربيعي ويوم الاعتدال الخريفي في السنة السابقة عبر قرار من مجلس الوزراء استنادًا إلى حسابات فلكية، ولا يردان في القانون كتاريخين ثابتين. ثانيًا، تُضاف العطلات الرسمية أو تُنقل أو يُعاد تسميتها عبر تعديلات قانونية: فقد استُحدث يوم الجبل في 2016، وانتقل عيد ميلاد الإمبراطور من 23 ديسمبر إلى 23 فبراير عام 2019 عقب تغيّر العصر (**لم يكن هناك عيد ميلاد للإمبراطور إطلاقًا في عام 2019**)، وأُعيدت تسمية يوم الرياضة والصحة إلى يوم الرياضة. ثالثًا، في عامي 2020 و2021، نقل قانون تدابير خاصة مرتبط بالأولمبياد يوم البحر ويوم الرياضة ويوم الجبل **لهذين العامين فقط**، مما يُظهر أن حتى قاعدة مثل «دائمًا يوم الاثنين الثالث» قد تُلغى مؤقتًا. لذلك، فإن التصميم الصحيح هو «جدول مرجعي للعطلات الرسمية + عملية تحديث»، مع اقتصار منطق الحساب على قواعد مساعدة مثل العطلة البديلة.
انزاح موعد استحقاق الدفع عن نهاية الشهر بعد استخدام AddMonths(1). لماذا؟
لأن DateTime.AddMonths مصمَّمة وفق المواصفات لتقريب النتيجة إلى آخر يوم في الشهر الناتج عندما يؤدي الحساب المباشر إلى يوم غير موجود في ذلك الشهر. تطبيق AddMonths(1) على 31 يناير يُعطي 28 فبراير (أو 29 في السنة الكبيسة) — وهذا السلوك في حد ذاته صحيح. لكن إذا نُقلت هذه النتيجة المقرَّبة إلى استدعاء AddMonths آخر، فإن تطبيق AddMonths(1) على 28 فبراير يُعطي 28 مارس، ومنذ تلك اللحظة يستمر الانزياح إلى الأبد: ما كان يُقصد به «نهاية الشهر» يتحول بصمت وبشكل دائم إلى «اليوم 28». يتألف الحل من جزأين: بالنسبة للحسابات المتكررة، احسب دائمًا انطلاقًا من نقطة مرجعية ثابتة (تاريخ العقد، أو تعريف «نهاية كل شهر») بدلًا من الإضافة إلى النتيجة السابقة، واشتقّ «نهاية الشهر» من DateTime.DaysInMonth بدلًا من قيمة تاريخ حرفية.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

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

روابط عامة

العودة إلى المدونة