Esc

تاریخ شمسی در کد Jalali Dates

ذخیره، نمایش و محاسبه درست تاریخ شمسی در JavaScript، Python، PHP و Go؛ با منطقه زمانی تهران و سال کبیسه

مهارتتوسعه نرم‌افزار

پیش از نصب بدانید

  • منبعساخت بازارچهنوشته و نگهداری‌شده در همین مخزن
  • کد منبعمتن‌باز، در همین مخزنآخرین بررسی: ۱۱ مهر ۱۴۰۵

نصب تاریخ شمسی در کد

Claude Code با افزونه

یک بار بازارچه را اضافه کنید، بعد هر مهارت را جدا نصب کنید.

Claude Code
/plugin marketplace add mcp-farsi/mcp-farsi
/plugin install jalali-dates@mcp-farsi

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

برای یک پروژه خاص، به جای ~/.claude از .claude در ریشه پروژه استفاده کنید. در PowerShell به جای curl بنویسید curl.exe و به جای ~ بنویسید $HOME.

Terminalshell
curl -fsSL --create-dirs -o ~/.claude/skills/jalali-dates/SKILL.md {ORIGIN}/skills/jalali-dates/SKILL.md

Claude.ai و اپ دسکتاپ

فایل‌های مهارت را در پوشه‌ای به نام jalali-dates بگذارید، آن را zip کنید و در تنظیمات Claude، بخش Capabilities، بارگذاری کنید.

دریافت SKILL.md
سازنده
بازارچه MCP
مجوز
MIT
آخرین بررسی
۱۱ مهر ۱۴۰۵

درباره

باگ‌های تاریخ شمسی از رایج‌ترین باگ‌های نرم‌افزارهای ایرانی‌اند: ذخیره تاریخ شمسی به‌صورت رشته، فراموش کردن اسفند کبیسه، یا گزارش ماهانه‌ای که روزهای آخر ماه را جا می‌اندازد. این مهارت به Claude می‌گوید تاریخ را چطور ذخیره کند، با کدام کتابخانه تبدیل کند و چه آزمون‌هایی بنویسد.

کجا به کار می‌آید

  • ساخت گزارش‌های ماهانه و فیلتر بر اساس ماه شمسی در پایگاه داده
  • نمایش تاریخ با Intl.DateTimeFormat و تقویم پارسی
  • انتخاب کتابخانه مناسب در هر زبان و نوشتن آزمون با تاریخ‌های مرزی

متن کامل مهارت

نمایش محتوای SKILL.md

تاریخ شمسی (Jalali dates in software)

Core rules

  1. Store Gregorian, never Jalali strings. Instants go in UTC (timestamptz, ISO 8601 with Z). Date-only values (birthday, due date) go in a Gregorian DATE. A Jalali string such as 1403/01/01 in 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.
  2. Convert at the edges. Parse Jalali input to Gregorian at the API boundary; format Gregorian to Jalali in the UI or report layer.
  3. Convert in the Asia/Tehran zone. The Jalali date of an instant depends on the zone: 2025-03-20T21:00:00Z is 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.
  4. 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 tzdata package.
  • Check: zdump -v Asia/Tehran | tail -n 3 should 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')) gives 7/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: zoneinfo needs pip 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 from formatToParts.
  • 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/dd with Persian, Arabic-Indic or Latin digits. Normalize the digits to ASCII first, then validate with isValidJalaaliDate / 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:00Z gives 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 equal g.
  • jalaali-js, ICU’s Persian calendar (Node 24), jdatetime, persiantools, morilog/jalali and go-persian-calendar all agree on the vectors above.