تاریخ شمسی در کد Jalali Dates
ذخیره، نمایش و محاسبه درست تاریخ شمسی در JavaScript، Python، PHP و Go؛ با منطقه زمانی تهران و سال کبیسه
مهارتتوسعه نرمافزار
پیش از نصب بدانید
- منبعساخت بازارچهنوشته و نگهداریشده در همین مخزن
- کد منبعمتنباز، در همین مخزنآخرین بررسی: ۱۱ مهر ۱۴۰۵
نصب تاریخ شمسی در کد
Claude Code با افزونه
یک بار بازارچه را اضافه کنید، بعد هر مهارت را جدا نصب کنید.
/plugin marketplace add mcp-farsi/mcp-farsi
/plugin install jalali-dates@mcp-farsiنصب دستی در پوشه مهارتها
برای یک پروژه خاص، به جای ~/.claude از .claude در ریشه پروژه استفاده کنید. در PowerShell به جای curl بنویسید curl.exe و به جای ~ بنویسید $HOME.
curl -fsSL --create-dirs -o ~/.claude/skills/jalali-dates/SKILL.md {ORIGIN}/skills/jalali-dates/SKILL.mdClaude.ai و اپ دسکتاپ
فایلهای مهارت را در پوشهای به نام jalali-dates بگذارید، آن را zip کنید و در تنظیمات Claude، بخش Capabilities، بارگذاری کنید.
درباره
باگهای تاریخ شمسی از رایجترین باگهای نرمافزارهای ایرانیاند: ذخیره تاریخ شمسی بهصورت رشته، فراموش کردن اسفند کبیسه، یا گزارش ماهانهای که روزهای آخر ماه را جا میاندازد. این مهارت به Claude میگوید تاریخ را چطور ذخیره کند، با کدام کتابخانه تبدیل کند و چه آزمونهایی بنویسد.
کجا به کار میآید
- ساخت گزارشهای ماهانه و فیلتر بر اساس ماه شمسی در پایگاه داده
- نمایش تاریخ با
Intl.DateTimeFormatو تقویم پارسی - انتخاب کتابخانه مناسب در هر زبان و نوشتن آزمون با تاریخهای مرزی
متن کامل مهارت
نمایش محتوای SKILL.md
تاریخ شمسی (Jalali dates in software)
Core rules
- Store Gregorian, never Jalali strings. Instants go in UTC (
timestamptz, ISO 8601 withZ). Date-only values (birthday, due date) go in a GregorianDATE. A Jalali string such as1403/01/01in a VARCHAR is never the source of truth. If a legal document needs the Jalali text exactly as entered, store it in addition to the Gregorian value. - Convert at the edges. Parse Jalali input to Gregorian at the API boundary; format Gregorian to Jalali in the UI or report layer.
- Convert in the Asia/Tehran zone. The Jalali date of an instant depends on the zone:
2025-03-20T21:00:00Zis 00:30 on 1404/01/01 in Tehran but still 1403/12/30 in UTC. Pass the zone explicitly; never rely on the server’s local zone. - Never hand-roll the leap rule. Use Intl or one of the libraries below.
Timezone: Asia/Tehran
- UTC+03:30 all year. Iran stopped daylight saving after the clock change on 2022-09-21; tzdata 2022b and later encode this.
- Systems with older tzdata still jump to +04:30 every spring. Update tzdata in the OS image, Docker base image, JVM, and the Python
tzdatapackage. - Check:
zdump -v Asia/Tehran | tail -n 3should show the last transition on 2022-09-21. In JS:new Intl.DateTimeFormat('en-US', {timeZone: 'Asia/Tehran', timeZoneName: 'longOffset'}).format(new Date('2026-07-01T00:00:00Z'))gives7/1/2026, GMT+03:30. - Use the zone name, not a hard-coded
+03:30. Parliament debated bringing DST back as recently as 2025, so the rule may change again. - Python on Windows:
zoneinfoneedspip install tzdata. MySQL: named zones such as'Asia/Tehran'work only after the time zone tables are loaded (mysql_tzinfo_to_sql).
Calendar facts
| # | Month | Days |
|---|---|---|
| 1 | فروردین Farvardin | 31 |
| 2 | اردیبهشت Ordibehesht | 31 |
| 3 | خرداد Khordad | 31 |
| 4 | تیر Tir | 31 |
| 5 | مرداد Mordad | 31 |
| 6 | شهریور Shahrivar | 31 |
| 7 | مهر Mehr | 30 |
| 8 | آبان Aban | 30 |
| 9 | آذر Azar | 30 |
| 10 | دی Dey | 30 |
| 11 | بهمن Bahman | 30 |
| 12 | اسفند Esfand | 29, or 30 in a leap year |
- 1 Farvardin (Nowruz) currently falls on March 20 or 21. Always convert; never compute it.
- The official calendar is astronomical. Recent leap years are 1395, 1399 and 1403; the next one is 1408. The gap from 1403 to 1408 is five years, so “every 4 years” logic is wrong.
- The 2820-year (Birashk) algorithm is wrong for current dates. It makes 1404 the leap year instead of 1403, so software using it was one day off from 30 Esfand 1403 to the end of 1404. If date code contains the number 2820, replace it with one of the libraries below.
- Weekdays: شنبه، یکشنبه، دوشنبه، سهشنبه، چهارشنبه، پنجشنبه، جمعه. The week starts on Saturday.
Display with Intl (no library)
const d = new Date('2024-03-20T12:00:00Z');
new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
timeZone: 'Asia/Tehran', year: 'numeric', month: '2-digit', day: '2-digit',
}).format(d); // '۱۴۰۳/۰۱/۰۱'
new Intl.DateTimeFormat('fa-IR-u-ca-persian-nu-latn', {
timeZone: 'Asia/Tehran', year: 'numeric', month: '2-digit', day: '2-digit',
}).format(d); // '1403/01/01' (Latin digits)
// Numeric parts for logic: read year/month/day from formatToParts
const parts = Object.fromEntries(
new Intl.DateTimeFormat('en-US-u-ca-persian', {
timeZone: 'Asia/Tehran', year: 'numeric', month: 'numeric', day: 'numeric',
}).formatToParts(d).map((p) => [p.type, p.value]),
); // { year: '1403', month: '1', day: '1', ... }
- Always pass
timeZone. - The word order of long formats (
month: 'long',weekday: 'long',dateStyle: 'full') differs between ICU versions and browsers. When the exact layout matters, build the string yourself fromformatToParts. - Relative time:
new Intl.RelativeTimeFormat('fa', {numeric: 'auto'}).format(-1, 'day')givesدیروز. - Intl only formats. For parsing Jalali input and doing date arithmetic, use a library.
Libraries
| Language | Library | Use for |
|---|---|---|
| JS | jalaali-js |
Tiny converter, no dependencies: toJalaali, toGregorian, isValidJalaaliDate, isLeapJalaaliYear, jalaaliMonthLength |
| JS | date-fns-jalali |
The date-fns API with Jalali semantics (format, addMonths, startOfMonth…) |
| JS | dayjs + jalaliday |
Day.js plugin. v3 is ESM-only: import jalaliday from 'jalaliday/dayjs' |
| React UI | @mui/x-date-pickers with AdapterDateFnsJalali; react-multi-date-picker |
Jalali date pickers |
| Python | jdatetime, persiantools |
Jalali date/datetime types |
| PHP | morilog/jalali |
Jalalian, CalendarUtils, Carbon interop |
| Go | github.com/yaa110/go-persian-calendar (package ptime) |
Persian Time type that works with time.Time |
Avoid moment-jalaali in new code: Moment.js is in maintenance mode.
JavaScript
import { toJalaali, toGregorian, isValidJalaaliDate } from 'jalaali-js';
toJalaali(2025, 3, 20); // { jy: 1403, jm: 12, jd: 30 }
toGregorian(1404, 1, 1); // { gy: 2025, gm: 3, gd: 21 }
isValidJalaaliDate(1404, 12, 30); // false (1404 is not a leap year)
import { format, newDate } from 'date-fns-jalali';
format(new Date(2024, 2, 20), 'yyyy/MM/dd'); // '1403/01/01'
newDate(1403, 0, 1); // Date for 2024-03-20 (month is 0-based!)
import dayjs from 'dayjs';
import jalaliday from 'jalaliday/dayjs';
dayjs.extend(jalaliday);
dayjs('2024-03-20').calendar('jalali').format('YYYY/MM/DD'); // '1403/01/01'
dayjs('1403-01-01', { jalali: true }).format('YYYY-MM-DD'); // '2024-03-20'
jalaali-js takes calendar-date parts (1-based months), not instants: get the Tehran date parts first (formatToParts above), then convert. date-fns-jalali and date-fns operate on the JS Date in the runtime’s local zone, so run servers with TZ=Asia/Tehran or convert explicitly.
MUI X: import { AdapterDateFnsJalali } from '@mui/x-date-pickers/AdapterDateFnsJalali' works with date-fns-jalali v3/v4. For date-fns-jalali v2, import from @mui/x-date-pickers/AdapterDateFnsJalaliV2.
Python
import datetime, jdatetime
from zoneinfo import ZoneInfo
jdatetime.date.fromgregorian(date=datetime.date(2024, 3, 20)) # 1403-01-01
jdatetime.date(1403, 12, 30).togregorian() # 2025-03-20
jdatetime.date(1404, 1, 1).isleap() # False
now_tehran = datetime.datetime.now(ZoneInfo("Asia/Tehran"))
jdatetime.date.fromgregorian(date=now_tehran.date())
from persiantools.jdatetime import JalaliDate
JalaliDate(datetime.date(2024, 3, 20)) # 1403-01-01
JalaliDate(1403, 12, 30).to_gregorian() # 2025-03-20
PHP
// composer require morilog/jalali:3.*
use Morilog\Jalali\Jalalian;
use Morilog\Jalali\CalendarUtils;
$dt = new DateTime('2025-03-20 12:00', new DateTimeZone('Asia/Tehran'));
Jalalian::fromDateTime($dt)->format('Y/m/d'); // 1403/12/30
CalendarUtils::toGregorian(1403, 12, 30); // [2025, 3, 20]
CalendarUtils::checkDate(1404, 12, 30); // false
(new Jalalian(1405, 7, 1))->toCarbon(); // Carbon 2026-09-23
Go
import ptime "github.com/yaa110/go-persian-calendar"
pt := ptime.New(time.Now().In(ptime.Iran()))
pt.Format("yyyy/MM/dd")
g := ptime.Date(1403, ptime.Esfand, 30, 0, 0, 0, 0, ptime.Iran()).Time() // 2025-03-20
Querying by Jalali month or year
Compute the Gregorian boundaries with a library, interpret them as Tehran midnight, and query a half-open range [start, next_start). Never use SQL MONTH() or date_trunc('month', ...): Gregorian months do not line up with Jalali months.
Example, Mehr 1405: toGregorian(1405, 7, 1) is 2026-09-23 and toGregorian(1405, 8, 1) is 2026-10-23.
-- PostgreSQL, timestamptz column: let the database apply the zone rules
SELECT * FROM orders
WHERE created_at >= TIMESTAMP '2026-09-23 00:00' AT TIME ZONE 'Asia/Tehran'
AND created_at < TIMESTAMP '2026-10-23 00:00' AT TIME ZONE 'Asia/Tehran';
-- Equivalent UTC range: [2026-09-22T20:30:00Z, 2026-10-22T20:30:00Z)
-- DATE column (no time part)
SELECT * FROM invoices WHERE due_date >= '2026-09-23' AND due_date < '2026-10-23';
For a Jalali year, use toGregorian(jy, 1, 1) and toGregorian(jy + 1, 1, 1).
For reports grouped by Jalali month, either aggregate per day in SQL and bucket in application code, or generate a calendar dimension table once with a library (gregorian_date, jy, jm, jd, weekday, is_holiday) and join on it.
Weeks, weekends and holidays
- Week starts on Saturday. Where supported,
new Intl.Locale('fa-IR').getWeekInfo()returns{firstDay: 6, weekend: [5]}(6 = Saturday, 5 = Friday). - Friday is the official weekend day. Thursday is a half day or a day off depending on the organization and on government decrees that keep changing; bills to make Thursday (or Saturday) an official day off have been debated for years without a settled outcome. Offices are also often closed by one-off decrees, for example during energy shortages.
- So keep weekend days, business hours and holidays in configuration or data, not in code. Many official holidays follow the lunar Hijri calendar and move every year; load the official yearly holiday list instead of computing it.
Input and UX
<input type="date">always holds a Gregorian ISO value, and browsers do not offer a Jalali picker for it. Use a Jalali picker component, or three selects (year, month name, day).- Accept
yyyy/mm/ddwith Persian, Arabic-Indic or Latin digits. Normalize the digits to ASCII first, then validate withisValidJalaaliDate/CalendarUtils::checkDate(1403/12/30 is valid, 1404/12/30 is not). - Require four-digit years. Two-digit years are ambiguous.
- When users deal with foreign parties (invoices, contracts, bookings), show both calendars: «۱ مهر ۱۴۰۵ (2026-09-23)».
- Month arithmetic clamps at month end (Shahrivar has 31 days, Mehr 30). Test the behavior of the library you chose.
- Sort and compare Gregorian values, not formatted Jalali strings.
Test vectors
| Gregorian | Jalali | Why it matters |
|---|---|---|
| 2016-05-07 | 1395/02/18 | morilog/jalali README example |
| 2021-03-20 | 1399/12/30 | Esfand 30 in a leap year |
| 2023-03-21 | 1402/01/01 | Nowruz on March 21 |
| 2024-03-20 | 1403/01/01 | Nowruz on March 20 (a Wednesday) |
| 2025-03-20 | 1403/12/30 | Software using the 2820-year algorithm shows 1404/01/01 here |
| 2025-03-21 | 1404/01/01 | |
| 2026-03-21 | 1405/01/01 | |
| 2026-09-23 | 1405/07/01 | Start of the 30-day months |
- Leap: 1395, 1399, 1403, 1408. Not leap: 1400 to 1402, 1404 to 1407.
- Timezone vector:
2025-03-20T21:00:00Zgives 1404/01/01 in Asia/Tehran and 1403/12/30 in UTC. - Round-trip test: for every day of a year range,
toGregorian(toJalaali(g))must equalg. - jalaali-js, ICU’s Persian calendar (Node 24), jdatetime, persiantools, morilog/jalali and go-persian-calendar all agree on the vectors above.
