تقویم جلالی

کلاس تغییرناپذیر Jalali برای تاریخ شمسی. ساخت و خواندن تاریخ، قالب‌بندی با توکن‌های date()، جابه‌جایی، مقایسه و اختلاف، همراه با بازهٔ سال و رفتار خطاها.

در این صفحه
  1. این کلاس چه می‌کند
  2. ساخت تاریخ
    1. از اجزا: create()
    2. از هر ورودی: make()
    3. رشته در make() چطور خوانده می‌شود
    4. با قالب: createFromFormat()
  3. بازهٔ سال و خطاها
  4. خواندن مقدارها
    1. شمارهٔ روز هفته
  5. قالب‌بندی
  6. جابه‌جایی و برش
  7. مقایسه و اختلاف
  8. متدهای استاتیک
  9. نکته‌های کاربردی

این کلاس چه می‌کند#

کلاس RtlyKit\Calendar\Jalali یک تاریخ و زمان در تقویم جلالی (شمسی) است و تغییر نمی‌کند. زیر کار یک DateTimeImmutable نگه می‌دارد. برای همین منطقهٔ زمانی، زمان یونیکس و معادل دقیق میلادی دارد. سال و ماه و روز جلالی از همان لحظه حساب می‌شوند. هر متدی که تاریخ را عوض کند، شیء تازه می‌دهد و شیء اصلی دست‌نخورده می‌ماند. کلاس final است و قرارداد مشترک CalendarDate را دارد. این قرارداد در تبدیل و مقایسهٔ تاریخ‌ها آمده است.

پیش‌نیاز. بسته را نصب کنید (نصب). مثال‌های این صفحه با این چند خط شروع می‌شوند:

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

use RtlyKit\Calendar\Jalali;
use RtlyKit\Exceptions\InvalidDateException;
use function RtlyKit\jdate;   // تابع کمکی فضای‌نام‌دار، همان Jalali::make()
خوب است بدانید. تبدیل بر پایهٔ قاعدهٔ حسابی چرخهٔ ۳۳ ساله انجام می‌شود (سال کبیسه یعنی باقیمانده‌های 1، 5، 9، 13، 17، 22، 26 و 30 بر 33). این قاعده با تقویم رسمی مرکز تقویم دانشگاه تهران برای هر سال از ۱۲۰۶ تا ۱۴۹۷ برابر است (۲۹۳ تاریخ نوروز را مقایسه کرده‌ایم) و بین ۱۱۷۸ تا ۱۵۰۲ با تعریف نجومی هم می‌خواند. بیرون از این بازه قاعدهٔ ثابت حسابی کار می‌کند. جزئیات در دقت و داده.

ساخت تاریخ#

از اجزا: create()#

متد create($year, $month, $day, $hour = 0, $minute = 0, $second = 0, $timezone = null) تاریخ را از اجزای جلالی می‌سازد. طول ماه را هم با همان سال چک می‌کند. پس 1404/12/30 رد می‌شود، چون ۱۴۰۴ کبیسه نیست، ولی 1403/12/30 درست است. اگر منطقهٔ زمانی ندهید، منطقهٔ پیش‌فرض PHP (date_default_timezone_get()) استفاده می‌شود.

$tehran = new DateTimeZone('Asia/Tehran');
$d = Jalali::create(1404, 7, 15, 14, 30, 5, $tehran);

echo $d;                                   // 1404/07/15 14:30:05
echo $d->getTimestamp();                   // 1759834805
echo $d->toGregorian()->format('c');       // 2025-10-07T14:30:05+03:30

از هر ورودی: make()#

متد Jalali::make($time = null, $timezone = null) این ورودی‌ها را قبول می‌کند: DateTimeInterface، هر CalendarDate دیگر (هجری، عبری، جلالی)، زمان یونیکس (عدد صحیح)، رشته یا null (یعنی همین الان). تابع jdate() فقط نام کوتاه همین متد است.

$utc = new DateTimeZone('UTC');

echo Jalali::make(new DateTimeImmutable('2026-03-21 12:00', $utc)); // 1405/01/01 12:00:00
echo Jalali::make(0, $utc);                                        // 1348/10/11 00:00:00
echo jdate('2026-03-21')->format('l j F Y');                       // شنبه 1 فروردین 1405
echo Jalali::now();      // همین لحظه، در منطقهٔ پیش‌فرض
echo Jalali::today();    // امروز، ساعت 00:00:00

رشته در make() چطور خوانده می‌شود#

رشته‌ها به این ترتیب بررسی می‌شوند:

  1. رقم‌های فارسی و عربی به انگلیسی تبدیل می‌شوند و فاصله‌های دو سر رشته حذف می‌شود. رشتهٔ خالی یا فقط فاصله InvalidDateException می‌دهد.
  2. اگر رشته شکل Y/m/d یا Y-m-d داشته باشد، سالش ۳ یا ۴ رقم باشد (ساعت H:i یا H:i:s بعد از فاصله یا T اختیاری است) و سال کمتر از ۱۷۰۰ باشد، تاریخ جلالی حساب می‌شود.
  3. هر رشتهٔ دیگر، از جمله هر سال ۱۷۰۰ و بالاتر، به DateTimeImmutable داده می‌شود. یعنی میلادی یا متن آزاد است ('2025-10-07'، 'tomorrow'، '10 October 2025').
echo Jalali::make('1403/12/30');          // 1403/12/30 00:00:00
echo Jalali::make('۱۴۰۴/۰۷/۱۵');          // 1404/07/15 00:00:00   (رقم‌های فارسی)
echo Jalali::make('1404-07-15 08:05');    // 1404/07/15 08:05:00
echo Jalali::make('2025-10-07')->format('Y/m/d');  // 1404/07/15   (متن میلادی)
echo Jalali::make('1700/01/01')->format('Y/m/d');  // 1078/10/12   (از 1700 به بعد میلادی است)
مرز ۱۷۰۰. رشته‌ای مثل '0001-01-01' یا '1500-06-01' تاریخ جلالی خوانده می‌شود، نه میلادی. برای تاریخ میلادیِ قبل از ۱۷۰۰ خودتان یک DateTimeImmutable بسازید و همان را به make() بدهید.

ورودی نامعتبر همیشه InvalidDateException می‌دهد:

foreach (['1404/12/30', '1404/13/01', '1404/07/15 25:00', '', 'garbage'] as $s) {
    try {
        Jalali::make($s);
    } catch (InvalidDateException $e) {
        echo "'$s': ", $e->getMessage(), "\n";
    }
}
// '1404/12/30': Invalid Jalali date: 1404/12/30
// '1404/13/01': Invalid Jalali date: 1404/13/1
// '1404/07/15 25:00': Invalid time: 25:0:0
// '': Unable to parse an empty date string.
// 'garbage': Unable to parse date: garbage

با قالب: createFromFormat()#

متد Jalali::createFromFormat($format, $time, $timezone = null) با توکن‌های DateTime::createFromFormat می‌خواند (رقم‌ها می‌توانند فارسی باشند)، ولی سال و ماه و روز را جلالی می‌گیرد. قاعده‌های میلادی، مثل «فوریه حداکثر ۲۹ روز دارد»، اینجا اثر ندارند. خود تقویم جلالی نتیجه را چک می‌کند.

echo Jalali::createFromFormat('Y/m/d H:i', '1404/07/15 08:30', $tehran); // 1404/07/15 08:30:00
echo Jalali::createFromFormat('d-m-Y', '31-06-1404');                    // 1404/06/31 00:00:00

try {
    Jalali::createFromFormat('Y/m/d', 'abc');
} catch (InvalidDateException $e) {
    echo $e->getMessage();   // Unable to parse 'abc' with format 'Y/m/d'
}

بازهٔ سال و خطاها#

سال جلالی از Jalali::MIN_YEAR = -620 تا Jalali::MAX_YEAR = 9377 پشتیبانی می‌شود. این بازه دقیقاً با سال‌های میلادی ۱ تا ۹۹۹۹ برابر است. اولین روز ‎-620/01/01 (برابر 0001-03-21) و آخرین روز 9377/12/30 (برابر 9999-03-20) است. همهٔ راه‌های ورود به کلاس بازه را چک می‌کنند و فقط InvalidDateException می‌دهند. TypeError، ValueError و DateMalformed* بیرون نمی‌آید. این قاعده برای create()، make()، createFromFormat()، زمان یونیکس، add*() و sub*() و تبدیل‌های استاتیک برقرار است.

foreach ([[-621, 1, 1], [9378, 1, 1], [1404, 12, 30], [1404, 0, 1], [1404, 7, 31]] as [$y, $m, $d]) {
    try {
        Jalali::create($y, $m, $d);
    } catch (InvalidDateException $e) {
        echo $e->getMessage(), "\n";
    }
}
// Invalid Jalali date: -621/1/1
// Invalid Jalali date: 9378/1/1
// Invalid Jalali date: 1404/12/30
// Invalid Jalali date: 1404/0/1
// Invalid Jalali date: 1404/7/31

ساعت و دقیقه و ثانیه هم چک می‌شوند. Jalali::create(1404, 1, 1, 24, 0, 0) پیام Invalid time: 24:0:0 می‌دهد. زمان یونیکسی که از سال‌های میلادی ۱ تا ۹۹۹۹ بیرون باشد پیام Timestamp out of the supported range: ... می‌دهد.

برای خطاهای تقویم فقط InvalidDateException را بگیرید. برای همهٔ خطاهای کتابخانه کلاس پایهٔ RtlyKit\Exceptions\RtlyKitException (یا اینترفیس RtlyKitThrowable) را بگیرید. بیشتر در مدیریت خطا.

خواندن مقدارها#

متدخروجی
getYear()، getMonth()، getDay()سال، ماه (۱ تا ۱۲) و روز (۱ تا ۳۱) جلالی
getHour()، getMinute()، getSecond()ساعت، دقیقه و ثانیه در منطقهٔ زمانی شیء
getDayOfWeek()۰ = شنبه تا ۶ = جمعه
monthName()نام فارسی ماه، مثلاً مهر
getTimestamp()، getTimezone()زمان یونیکس و DateTimeZone
toGregorian()همان DateTimeImmutable زیرین
toDateString()، toDateTimeString()، __toString()Y/m/d، Y/m/d H:i:s، Y/m/d H:i:s

شمارهٔ روز هفته#

هفتهٔ جلالی از شنبه شروع می‌شود. getDayOfWeek() و توکن w برای شنبه ۰ و برای جمعه ۶ می‌دهند. توکن N از ۱ (شنبه) تا ۷ (جمعه) است. هجری و عبری فرق دارند. آن‌ها از یکشنبه شروع می‌کنند (۰ = یکشنبه)، مثل w خود PHP. شمارهٔ روز هفته را بین تقویم‌ها مقایسه نکنید. خود لحظه‌ها را مقایسه کنید.

$d = Jalali::create(1404, 7, 15);           // سه‌شنبه
foreach (range(0, 6) as $i) {
    $x = $d->addDays($i);
    echo $x->getDayOfWeek(), ':', $x->format('l'), ' ';
}
// 3:سه‌شنبه 4:چهارشنبه 5:پنجشنبه 6:جمعه 0:شنبه 1:یکشنبه 2:دوشنبه

قالب‌بندی#

متد format($pattern = 'Y/m/d H:i:s', $persianDigits = false) توکن‌های date() را در تقویم جلالی حساب می‌کند. اگر آرگومان دوم true باشد، همهٔ رقم‌ها فارسی می‌شوند. بک‌اسلش نویسهٔ بعدی را همان‌طور که هست می‌نویسد. نویسه‌هایی که توکن نیستند عیناً کپی می‌شوند. الگوی بلندتر از Jalali::MAX_FORMAT_LENGTH (۲۵۶ بایت) InvalidDateException با کد input_too_long می‌دهد. بک‌اسلشِ آخر الگو نادیده گرفته می‌شود.

$d = Jalali::create(1404, 7, 15, 14, 30, 5, $tehran);

echo $d->format('l j F Y');            // سه‌شنبه 15 مهر 1404
echo $d->format('Y/m/d', true);        // ۱۴۰۴/۰۷/۱۵
echo $d->format('c');                  // 1404-07-15T14:30:05+03:30
echo $d->format('h:i A');              // 02:30 بعد از ظهر
echo $d->format('g:i a');              // 2:30 ب.ظ
echo $d->format('\Y: Y, \d: d');       // Y: 1404, d: 15
echo $d->format('t L z W N D');        // 30 0 200 41 4 س
توکنمعنی
Y yسال، چهار رقمی و دو رقمی
m nماه، با صفر ابتدایی و بدون آن
F Mنام فارسی ماه (M همان F است، چون نام ماه‌های فارسی کوتاه‌شده ندارند)
d jروز، با صفر ابتدایی و بدون آن
l Dنام روز هفته. D شکل تک‌حرفی است (ش ی د س چ پ ج)
w Nشمارهٔ روز هفته از شنبه: ۰ تا ۶ و ۱ تا ۷
zشمارهٔ روز در سال جلالی، از صفر
t Lتعداد روزهای ماه. و ۱ اگر سال کبیسه باشد
H G h gساعت ۲۴ و ۱۲ ساعته، با صفر ابتدایی و بدون آن
i sدقیقه و ثانیه
a Aق.ظ/ب.ظ و قبل از ظهر/بعد از ظهر
Sهمیشه خالی (فارسی پسوند ترتیبی ندارد)
Wشمارهٔ هفتهٔ ISO-8601 برای لحظهٔ میلادی زیرین
cY-m-d\TH:i:sP با تاریخ جلالی
rرشتهٔ RFC 2822 از لحظهٔ میلادی (این استاندارد نام‌های انگلیسی می‌خواهد)
U e T P p O Z I u vاز لحظهٔ زیرین گرفته می‌شوند (زمان یونیکس، منطقه، اختلاف ساعت، میکروثانیه و ...)

جابه‌جایی و برش#

هر متد شیء تازه می‌دهد و منطقهٔ زمانی را نگه می‌دارد.

متدرفتار
addDays() subDays()روز کامل روی لحظهٔ زیرین
addHours() addMinutes() addSeconds() و sub*زمان واقعی سپری‌شده
addMonths() subMonths()ماه تقویمی. اگر ماه مقصد کوتاه‌تر باشد، روز کم می‌شود تا در ماه جا شود
addYears() subYears()هر سال معادل ۱۲ ماه است. ۳۰ اسفندِ سال کبیسه در سال غیرکبیسه ۲۹ اسفند می‌شود
startOfDay() endOfDay()00:00:00 و 23:59:59 همان روز
startOfMonth() endOfMonth()اول ماه 00:00:00، آخر ماه 23:59:59
startOfYear() endOfYear()اول فروردین 00:00:00، آخر اسفند 23:59:59
$x = Jalali::create(1403, 12, 30);              // آخرین روز یک سال کبیسه
echo $x->addDays(1);                            // 1404/01/01 00:00:00
echo $x->subDays(30);                           // 1403/11/30 00:00:00
echo $x->addHours(30);                          // 1404/01/01 06:00:00
echo $x->addYears(1);                           // 1404/12/29 00:00:00  (روز کم شد)
echo $x->subMonths(13);                         // 1402/11/30 00:00:00
echo Jalali::create(1404, 6, 31)->addMonths(1); // 1404/07/30 00:00:00  (روز کم شد)

$m = Jalali::create(1404, 7, 15, 14, 30, 5);
echo $m->startOfMonth();   // 1404/07/01 00:00:00
echo $m->endOfMonth();     // 1404/07/30 23:59:59
echo $m->startOfYear();    // 1404/01/01 00:00:00
echo $m->endOfYear();      // 1404/12/29 23:59:59

اگر نتیجه از بازهٔ پشتیبانی‌شده بیرون برود، یا مقدار جابه‌جایی غیرمنطقی باشد، InvalidDateException می‌گیرید. تاریخ از سر شروع نمی‌شود:

try { Jalali::create(9377, 1, 1)->addYears(1); }
catch (InvalidDateException $e) { echo $e->getMessage(); }
// Jalali year out of the supported range: 9378

try { $x->addDays(PHP_INT_MAX); }
catch (InvalidDateException $e) { echo $e->getMessage(); }
// Cannot shift a date by 9223372036854775807 days: out of the supported range.

مقایسه و اختلاف#

متدهای مقایسه (eq ne gt gte lt lte equals isBefore isAfter between isPast isFuture isToday) لحظه‌ها را مقایسه می‌کنند. هر CalendarDate یا DateTimeInterface را قبول می‌کنند. پس دو مقدار وقتی برابرند که یک لحظه باشند. منطقهٔ زمانی هم اثر دارد و فقط تاریخ چاپ‌شده مهم نیست.

$a = Jalali::create(1405, 1, 1, 0, 0, 0, $tehran);
var_dump($a->eq(new DateTimeImmutable('2026-03-21 00:00', $tehran))); // bool(true)
var_dump($a->between(Jalali::create(1404, 1, 1), Jalali::create(1406, 1, 1))); // bool(true)
متدمعنی
diffInDays($other, $absolute = true)تعداد روز کامل، به سمت صفر گرد می‌شود. با false علامت همان علامت $this - $other است
diffInMonths($other, $absolute = true)تعداد ماه کامل جلالی. روز و ساعت هم حساب می‌شود (۱۵ فروردین تا ۱۴ تیر می‌شود ۲ ماه)
diffInYears($other, $absolute = true)تعداد سال کامل جلالی، بر اساس سالگرد
$jan = Jalali::create(1404, 1, 1);
$feb = Jalali::create(1404, 2, 1);
echo $jan->diffInDays($feb);                 // 31
echo $jan->diffInDays($feb, false);          // -31
echo Jalali::create(1404, 1, 15)->diffInMonths(Jalali::create(1404, 4, 14)); // 2
echo Jalali::create(1380, 5, 5)->diffInYears(Jalali::create(1404, 5, 4));    // 23
echo Jalali::create(1380, 5, 5)->diffInYears(Jalali::create(1404, 5, 5));    // 24

متدهای استاتیک#

متدخروجی
Jalali::isValid($y, $m, $d)bool. برای سال یا ماه یا روز بیرون از بازه false می‌دهد و هیچ‌وقت خطا پرتاب نمی‌کند
Jalali::isLeapYear($y)اگر باقیمانده‌ی $y بر ۳۳ یکی از 1، 5، 9، 13، 17، 22، 26، 30 باشد، کبیسه است
Jalali::daysInYear($y)۳۶۵ یا ۳۶۶
Jalali::daysInMonth($y, $m)۳۱ برای ماه‌های ۱ تا ۶، ۳۰ برای ۷ تا ۱۱، و ۲۹ یا ۳۰ برای اسفند. برای شمارهٔ ماه نامعتبر 0
Jalali::gregorianToJalali($gy, $gm, $gd)[سال, ماه, روز]. سال میلادی باید ۱ تا ۹۹۹۹ باشد
Jalali::jalaliToGregorian($jy, $jm, $jd)[سال, ماه, روز]. روز ۱ تا ۳۱ برای هر ماه قبول می‌شود و اضافه‌اش به ماه بعد می‌رود
echo implode(',', array_filter(range(1399, 1410), Jalali::isLeapYear(...))); // 1399,1403,1408
echo Jalali::daysInMonth(1403, 12), ' ', Jalali::daysInMonth(1404, 12);        // 30 29
echo Jalali::daysInYear(1403), ' ', Jalali::daysInYear(1404);                  // 366 365
echo json_encode(Jalali::gregorianToJalali(2026, 3, 21));                      // [1405,1,1]
echo json_encode(Jalali::jalaliToGregorian(1405, 1, 1));                       // [2026,3,21]

try { Jalali::gregorianToJalali(10000, 1, 1); }
catch (InvalidDateException $e) { echo $e->getMessage(); }
// Gregorian date out of the supported range (years 1-9999): 10000-1-1

نکته‌های کاربردی#

  • منطقهٔ زمانی. تاریخی که بدون منطقه ساخته شود، منطقهٔ پیش‌فرض PHP را می‌گیرد. اگر نتیجه نباید به تنظیمات سرور وابسته باشد، به create() یا make() یک DateTimeZone بدهید. ساعت و دقیقه و خود تاریخ در منطقهٔ همان شیء خوانده می‌شوند.
  • مرز روز. روز در نیمه‌شبِ منطقهٔ شیء عوض می‌شود.
  • مثل یک مقدار. شیءها Stringable هستند. می‌توانید راحت آن‌ها را به اشتراک بگذارید و در آرایه نگه دارید. برای مرتب‌کردن از getTimestamp() استفاده کنید.
  • ببینید. تبدیل به تقویم‌های دیگر و مقایسهٔ بین تقویم‌ها: تبدیل و مقایسهٔ تاریخ‌ها. Carbon: ماکروهای Carbon. تعطیلات رسمی ایران: تعطیلات.