التحويل والمقارنة بين التواريخ

العقد المشترك CalendarDate بين Jalali وHijri وHebrew، وكيفية تحويل التاريخ بين التقويمات، وكيف تعمل المقارنة وحساب الفروق عبر التقويمات والمناطق الزمنية.

في هذه الصفحة
  1. لحظة واحدة وثلاثة تقويمات
  2. التحويل بين التقويمات
    1. مرّر التاريخ إلى make() في الفئة الأخرى
    2. من الميلادي وإليه
    3. المناطق الزمنية تحدد التاريخ
  3. عقد CalendarDate
  4. المقارنة عبر التقويمات
  5. الفروق عبر التقويمات
  6. الحالات الحدية والقيود

لحظة واحدة وثلاثة تقويمات#

تشترك فئات التقويم الثلاث (Jalali وHijri وHebrew) في تصميم واحد: كل منها يغلّف لحظة واحدة من النوع DateTimeImmutable ويعرضها فقط بحسب تقويمه. لذلك لا يمرّ التحويل بين التقويمات عبر نص أبدًا؛ فهو اللحظة نفسها مقروءة بمجموعة أخرى من القواعد. وينتج عن ذلك أمران:

  • تحويل التاريخ ثم إعادته يعطي اللحظة نفسها.
  • مقارنة تواريخ من تقويمات مختلفة أو طرحها أمر معرَّف جيدًا دائمًا، لأنها تُقارن بوصفها لحظات.

تستخدم كل الأمثلة الترويسة التالية:

<?php
require 'vendor/autoload.php';

use RtlyKit\Calendar\Hebrew;
use RtlyKit\Calendar\Hijri;
use RtlyKit\Calendar\Jalali;
use RtlyKit\Contracts\CalendarDate;
use RtlyKit\Exceptions\InvalidDateException;

$utc = new DateTimeZone('UTC');

التحويل بين التقويمات#

مرّر التاريخ إلى make() في الفئة الأخرى#

تقبل كل دالة make() كائنًا من CalendarDate (إضافة إلى DateTimeInterface وطابع زمني من النوع int وسلسلة نصية وnull). مرّر التاريخ الذي لديك إلى الفئة التي تريدها:

$j = Jalali::create(1405, 1, 1, 12, 0, 0, $utc);

echo Hijri::make($j)->format('Y/m/d F', 'en');    // 1447/10/02 Shawwal
echo Hebrew::make($j)->format('Y/m/d F');          // 5786/07/03 Nisan
echo Jalali::make(Hijri::make($j));                 // 1405/01/01 12:00:00

يُنقل وقت اليوم والمنطقة الزمنية معًا. وللتعبير عن النتيجة في منطقة أخرى مرّر DateTimeZone وسيطًا ثانيًا إلى make():

$tehran = new DateTimeZone('Asia/Tehran');

$h = Hijri::make($j, $tehran);
echo $h->getTimezone()->getName();      // Asia/Tehran
echo $h->getHour(), ':', $h->getMinute();   // 15:30   (12:00 UTC)
اختصار الفئة نفسها. تُرجع Hijri::make($hijri) وHebrew::make($hebrew) النسخة نفسها التي مررتها (ويُتجاهل أي وسيط للمنطقة الزمنية أو النمط). وتُرجعها Jalali::make($jalali) كذلك ما لم تمرر منطقة زمنية، فتحصل حينئذ على نسخة في تلك المنطقة. ولتغيير نمط تاريخ Hijri مرّ عبر اللحظة: Hijri::make($h->toGregorian(), null, HijriVariant::Tabular).

من الميلادي وإليه#

تُرجع toGregorian() الكائن الأساسي DateTimeImmutable؛ وتمرير DateTimeInterface إلى make() يسلك الاتجاه المعاكس. وتعمل دوال التحويل الساكنة على أعداد صحيحة مجردة:

$g = new DateTimeImmutable('2026-03-21 12:00', $utc);

echo json_encode([
    Jalali::gregorianToJalali(2026, 3, 21),
    Hijri::gregorianToHijri(2026, 3, 21),
    Hebrew::gregorianToHebrew(2026, 3, 21),
]);
// [[1405,1,1],[1447,10,2],[5786,7,3]]

echo json_encode(Jalali::jalaliToGregorian(1405, 1, 1));   // [2026,3,21]
echo get_class($j->toGregorian());                         // DateTimeImmutable

يجب أن تكون السنة الميلادية من 1 إلى 9999. ونطاق كل تقويم (Jalali من -620 إلى 9377، وHijri من 1 إلى 9665، وHebrew من 3762 إلى 13759) هو الجزء من هذا المدى الذي يستطيع التقويم تمثيله، ولذلك فتحويل تاريخ ميلادي مبكر إلى Hebrew أو Hijri قد يُطلق InvalidDateException مع أن التاريخ الميلادي نفسه صالح:

try { Hebrew::make(new DateTimeImmutable('0001-01-01', $utc)); }
catch (InvalidDateException $e) { echo $e->getMessage(); }
// Date out of the supported Hebrew range (3762..13759): 3761

المناطق الزمنية تحدد التاريخ#

يعتمد التاريخ التقويمي للحظة على المنطقة التي تنظر منها. فاللحظة نفسها لا تزال 1 Farvardin 1405 (21 مارس) بتوقيت UTC لكنها صارت 2 Farvardin في طهران التي تتقدم بـ 3.5 ساعة:

$instant = new DateTimeImmutable('2026-03-21 22:30', $utc);

echo Jalali::make($instant);            // 1405/01/01 22:30:00   (UTC)
echo Jalali::make($instant, $tehran);   // 1405/01/02 02:00:00   (Tehran, +03:30)

مرّر منطقة زمنية صريحة كلما كان اليوم التقويمي مهمًّا؛ ولا تعتمد على الإعداد الافتراضي للخادم.

عقد CalendarDate#

RtlyKit\Contracts\CalendarDate واجهة (interface) ترث Stringable وتنفّذها Jalali وHijri وHebrew. استخدمها في تلميحات الأنواع لكتابة شيفرة تعمل مع كل التقويمات:

function describe(CalendarDate $d): string
{
    return sprintf('%s %d/%02d/%02d', (new ReflectionClass($d))->getShortName(),
        $d->getYear(), $d->getMonth(), $d->getDay());
}

foreach ([Jalali::class, Hijri::class, Hebrew::class] as $class) {
    echo describe($class::make($g)), "\n";
}
// Jalali 1405/01/01
// Hijri 1447/10/02
// Hebrew 5786/07/03
المجموعةالأعضاء
الإنشاءmake(), now(), today(), create()
الحساب التقويمي (ساكن)isValid(), isLeapYear(), daysInYear(), daysInMonth()
دوال القراءةgetYear(), getMonth(), getDay(), getHour(), getMinute(), getSecond(), getDayOfWeek(), getTimestamp(), getTimezone(), toGregorian()
التنسيقformat(), toDateString(), toDateTimeString(), __toString()
التعديلadd/sub لـ Days وHours وMinutes وSeconds وMonths وYears؛ وstartOf/endOf لـ Day وMonth وYear
المقارنةeq, ne, gt, gte, lt, lte, equals, isBefore, isAfter, between, isPast, isFuture, isToday
الفرقdiffInDays(), diffInMonths(), diffInYears()

ضمانات تسري على كل تنفيذ:

  • النسخ final وغير قابلة للتغيير؛ ودوال التعديل تُرجع كائنًا جديدًا.
  • كل دالة تأخذ تاريخًا أو طابعًا زمنيًا أو فرقًا لا تُطلق إلا InvalidDateException للمدخلات الخارجة عن النطاق، ولا تُطلق أبدًا TypeError أو ValueError.
  • تُعلَن format() بوسيط النمط وحده؛ وتضيف كل فئة وسائطها الاختيارية الأخيرة الخاصة بها ($persianDigits في Jalali؛ و$locale و$digits في Hijri؛ و$locale في Hebrew). ومن خلال الواجهة لا يمكنك استدعاء غير format($pattern).
  • ترقيم أيام الأسبوع ليس موحدًا: Jalali يبدأ من السبت، وHijri وHebrew يبدآن من الأحد.
$i = new DateTimeImmutable('2025-10-07', $utc);   // يوم ثلاثاء
echo Jalali::make($i)->getDayOfWeek(), ' ', Hijri::make($i)->getDayOfWeek(), ' ', Hebrew::make($i)->getDayOfWeek();
// 3 2 2   (Jalali: Saturday = 0; the others: Sunday = 0)

المقارنة عبر التقويمات#

تقبل كل دوال المقارنة CalendarDate|DateTimeInterface وتقارن الطوابع الزمنية. ولا حاجة إلى تحويل مسبق:

$nowruz = Jalali::create(1405, 1, 1, 12, 0, 0, $utc);

var_dump($nowruz->eq(Hijri::make($nowruz)));                     // bool(true)
var_dump($nowruz->eq($nowruz->toGregorian()));                   // bool(true)
var_dump($nowruz->gt(Hebrew::create(5785, 1, 1, 0, 0, 0, $utc)));  // bool(true)
var_dump($nowruz->between(Hijri::create(1447, 8, 1, 0, 0, 0, $utc),
                          Hebrew::create(5787, 1, 1, 0, 0, 0, $utc))); // bool(true)

تقبل between($a, $b, $equal = true) الحدّين بأي ترتيب وتُدخلهما في النتيجة ما لم تمرر false. والمساواة هي مساواة اللحظات: فالتاريخ Jalali::create(1405, 1, 1) المبني بتوقيت UTC لا يساوي التاريخ المدني نفسه المبني بتوقيت طهران، لأن بينهما 3.5 ساعة.

لترتيب قائمة مختلطة، رتّب بحسب getTimestamp():

$list = [
    Hebrew::create(5786, 1, 1, 0, 0, 0, $utc),
    Jalali::create(1405, 1, 1, 0, 0, 0, $utc),
    Hijri::create(1447, 1, 1, 0, 0, 0, $utc),
];
usort($list, fn (CalendarDate $a, CalendarDate $b) => $a->getTimestamp() <=> $b->getTimestamp());

foreach ($list as $d) { echo $d::class, ' ', $d->toGregorian()->format('Y-m-d'), "\n"; }
// RtlyKit\Calendar\Hijri 2025-06-26
// RtlyKit\Calendar\Hebrew 2025-09-23
// RtlyKit\Calendar\Jalali 2026-03-21

الفروق عبر التقويمات#

تحسب diffInDays() الأيام الكاملة بين اللحظتين ولا تبالي بالتقويمات. أما diffInMonths() وdiffInYears() فتحسبان الأشهر والسنوات الكاملة في تقويم الكائن الذي تستدعيهما عليه؛ ويُعاد أولًا التعبير عن التاريخ الآخر بذلك التقويم. ومع $absolute = false تكون الإشارة إشارة $this - $other.

$target = Hijri::create(1447, 9, 1, 0, 0, 0, $utc);   // 1 رمضان 1447

echo $nowruz->diffInDays($target);          // 31
echo $nowruz->diffInDays($target, false);   // 31   ($nowruz أحدث من $target)
echo $target->diffInDays($nowruz, false);   // -31

echo $nowruz->diffInMonths($target);        // 1    (أشهر جلالية)
echo Hijri::make($nowruz)->diffInMonths($nowruz->addMonths(3));  // 3  (أشهر هجرية)
echo $nowruz->diffInYears(Hijri::create(1450, 1, 1, 0, 0, 0, $utc)); // 2

ولذلك يتوقف جواب «كم شهرًا حتى X» على التقويم الذي تسأل به؛ فاختر التقويم الذي يفكر به مستخدموك. وجداول الوحدات لكل تقويم في Jalali وHijri وHebrew.

الحالات الحدية والقيود#

  • النطاقات. Jalali من -620 إلى 9377، وHijri من 1 إلى 9665، وHebrew من 3762 إلى 13759. والتحويل إلى تقويم لا يحتوي نطاقه على اللحظة يُطلق InvalidDateException.
  • نمط Hijri. قيمة Hijri المحوَّلة إلى تقويم آخر ثم المعادة تحافظ على اللحظة، لكن النمط هو نمط استدعاء make() الهدف (أم القرى افتراضيًا).
  • الدقة. Jalali هو القاعدة الحسابية ذات الـ 33 سنة، لا التقويم الفلكي الرسمي. وبيانات أم القرى في Hijri للسنوات 1300 إلى 1500 هـ مأخوذة من ICU/CLDR؛ وتطابق بدايات أشهرها تقويم KACST الرسمي للسنوات 1318 إلى 1500 هـ (فُحص في 2026-10-08)، أما 1300 إلى 1317 هـ فغير متحقَّق منها؛ وخارج 1300 إلى 1500 هـ تُطبَّق قواعد النمط الجدولي (Tabular) الحسابية. راجع ملاحظات قيد معروف في صفحات التقويمات.
  • الأخطاء. اِلتقط InvalidDateException لمشكلات التقويم، أو RtlyKit\Exceptions\RtlyKitException لأي خطأ في المكتبة (معالجة الأخطاء).