تبدیل و مقایسهٔ تاریخ‌ها

قرارداد مشترک CalendarDate در Jalali و Hijri و Hebrew، تبدیل تاریخ بین تقویم‌ها، و مقایسه و اختلاف بین تقویم‌ها و منطقه‌های زمانی مختلف.

در این صفحه
  1. یک لحظه، سه تقویم
  2. تبدیل بین تقویم‌ها
    1. تاریخ را به make() کلاس دیگر بدهید
    2. از میلادی و به میلادی
    3. منطقهٔ زمانی تاریخ را عوض می‌کند
  3. قرارداد CalendarDate
  4. مقایسه بین تقویم‌ها
  5. اختلاف بین تقویم‌ها
  6. موارد مرزی و محدودیت‌ها

یک لحظه، سه تقویم#

سه کلاس تقویم (جلالی، هجری و عبری) یک طراحی دارند. هر کدام فقط یک لحظه (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، زمان یونیکس، رشته یا 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::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

سال میلادی باید بین ۱ و ۹۹۹۹ باشد. بازهٔ هر تقویم (جلالی ‎-۶۲۰ تا ۹۳۷۷، هجری ۱ تا ۹۶۶۵، عبری ۳۷۶۲ تا ۱۳۷۵۹) بخشی از همین بازه است. برای همین ممکن است تاریخ میلادیِ قدیمی را نتوان به عبری یا هجری برد. در این حالت 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

منطقهٔ زمانی تاریخ را عوض می‌کند#

تاریخ تقویمیِ یک لحظه به منطقه‌ای بستگی دارد که از آن نگاه می‌کنید. این لحظه در UTC هنوز اول فروردین ۱۴۰۵ (۲۱ مارس) است، ولی در تهران که ۳٫۵ ساعت جلوتر است، دوم فروردین شده:

$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   (تهران، +03:30)

هر جا روز تقویمی مهم است، منطقه را صریح بدهید و به پیش‌فرض سرور تکیه نکنید.

قرارداد CalendarDate#

RtlyKit\Contracts\CalendarDate یک اینترفیس است که 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 برای جلالی، $locale و $digits برای هجری، $locale برای عبری). از راه اینترفیس فقط format($pattern) را می‌توانید صدا بزنید.
  • شمارهٔ روز هفته یکسان نیست. جلالی از شنبه شروع می‌کند. هجری و عبری از یکشنبه.
$i = new DateTimeImmutable('2025-10-07', $utc);   // سه‌شنبه
echo Jalali::make($i)->getDayOfWeek(), ' ', Hijri::make($i)->getDayOfWeek(), ' ', Hebrew::make($i)->getDayOfWeek();
// 3 2 2   (جلالی: شنبه = 0. دو تقویم دیگر: یکشنبه = 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 با همان تاریخ در تهران برابر نیست، چون ۳٫۵ ساعت فاصله دارند.

برای مرتب‌کردن فهرستی از تقویم‌های مختلف، با 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);   // اول رمضان 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

پس جواب «چند ماه مانده» به تقویمی که می‌پرسید بستگی دارد. همان تقویمی را بردارید که کاربرانتان با آن فکر می‌کنند. جدول واحدها برای هر تقویم در جلالی، هجری و عبری هست.

موارد مرزی و محدودیت‌ها#

  • بازه‌ها. جلالی ‎-۶۲۰ تا ۹۳۷۷، هجری ۱ تا ۹۶۶۵، عبری ۳۷۶۲ تا ۱۳۷۵۹. اگر تقویم مقصد آن لحظه را در بازهٔ خود نداشته باشد، InvalidDateException می‌گیرید.
  • گونهٔ هجری. وقتی تاریخ هجری را به تقویم دیگر ببرید و برگردانید، لحظه همان می‌ماند. گونه همان است که در make() مقصد داده‌اید (به‌طور پیش‌فرض ام‌القری).
  • دقت. جلالی با قاعدهٔ حسابی ۳۳ ساله کار می‌کند و برای ۱۲۰۶ تا ۱۴۹۷ با تقویم رسمی دانشگاه تهران برابر است. جدول ام‌القری برای ۱۳۱۸ تا ۱۵۰۰ هجری قمری با تقویم رسمی KACST می‌خواند (بررسی در 2026-10-08). برای ۱۳۰۰ تا ۱۳۱۷ داده از ICU/CLDR است. بیرون از ۱۳۰۰ تا ۱۵۰۰ قاعدهٔ حسابی Tabular کار می‌کند. جزئیات در دقت و داده.
  • خطاها. برای مشکل تقویمی InvalidDateException را بگیرید و برای هر خطای کتابخانه RtlyKit\Exceptions\RtlyKitException را (مدیریت خطا).