Skip to the guide
All guides

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

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

app RTLY-Kit 0.3.0checked reading 8 minutes

On this page

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

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

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

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

PHP
<?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). مرّر التاريخ الذي لديك إلى الفئة التي تريدها:

PHP
$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():

PHP
$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::make() أيضًا نمطًا، وقيمته الافتراضية null: null تُبقي نمط النسخة، والنمط الذي تمرره (ومنه UmmAlQura) يحوّل التاريخ إليه إذا اختلف.

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

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

PHP
$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 مع أن التاريخ الميلادي نفسه صالح:

PHP
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 ساعة:

PHP
$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 وJsonSerializable، فيعطي json_encode($date) النص نفسه الذي يعطيه (string) $date، وتنفّذها Jalali وHijri وHebrew. استخدمها في تلميحات الأنواع لكتابة شيفرة تعمل مع كل التقويمات:

PHP
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 يبدآن من الأحد.
PHP
$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 وتقارن اللحظة كاملة، بما فيها الميكروثواني. ولا حاجة إلى تحويل مسبق:

PHP
$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():

PHP
$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.

PHP
$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.

لا تكون add*() وdiff*() عكس بعضهما تمامًا في نهاية الشهر القصير. فـ addMonths() وaddYears() تقصّان اليوم إلى طول الشهر الهدف، أما diffInMonths() وdiffInYears() فتعدّان الوحدات الكاملة من الأيام الفعلية، فيكون التاريخ المقصوص ناقصًا يومًا عن وحدة كاملة. وكذلك يفعل Carbon. وإذا احتجت إلى التاريخ الأصلي فاحتفظ به.

PHP
$start = Jalali::create(1403, 6, 31, 0, 0, 0, $utc);
echo $start->addMonths(1);                         // 1403/07/30 00:00:00  (the day is cut to 30)
echo $start->diffInMonths($start->addMonths(1));   // 0

$leapDay = Jalali::create(1403, 12, 30, 0, 0, 0, $utc);
echo $leapDay->addYears(1);                        // 1404/12/29 00:00:00
echo $leapDay->diffInYears($leapDay->addYears(1)); // 0

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

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