تبدیل تاریخ میلادی و شمسی
کد جاوااسکریپت بدون وابستگی برای تبدیل تاریخ میلادی و شمسی به هم، همراه با نام ماهها، قالببندی فارسی و آزمونهای کوتاه.
کد · ۱۳ آبان ۱۴۰۵
javascript
'use strict';
// تبدیل تاریخ میلادی ⇄ شمسی بدون هیچ وابستگی خارجی
// قاعدهی کبیسه: چرخهی ۳۳ ساله با باقیماندههای ۱، ۵، ۹، ۱۳، ۱۷، ۲۲، ۲۶، ۳۰
// دقت مورد انتظار: بازهی سالهای شمسی ۱۳۰۰ تا ۱۴۹۹
// نام ماههای شمسی
const JALALI_MONTHS = [
'فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور',
'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند'
];
const LEAP_REMAINDERS = [1, 5, 9, 13, 17, 22, 26, 30];
const CYCLE = 33;
const LEAPS_PER_CYCLE = LEAP_REMAINDERS.length; // ۸ کبیسه در هر چرخه
const DAYS_PER_YEAR = 365;
// لنگر: ۱ فروردین ۱۴۰۳ = ۲۰۲۴-۰۳-۲۰ (شمارهی روز ژولیانی ۲۴۶۰۳۹۰)
const ANCHOR_JALALI_YEAR = 1403;
const ANCHOR_JDN = 2460390;
// باقیماندهی مثبت
function mod(a, n) {
return ((a % n) + n) % n;
}
// آیا سال شمسی کبیسه است؟
function isJalaliLeap(jy) {
return LEAP_REMAINDERS.includes(mod(jy, CYCLE));
}
// تعداد سالهای کبیسه در بازهی [0, jy)
function leapsBefore(jy) {
const q = Math.floor(jy / CYCLE);
const r = jy - q * CYCLE;
let count = q * LEAPS_PER_CYCLE;
for (const x of LEAP_REMAINDERS) {
if (x < r) count++;
}
return count;
}
// شمارهی روز (از ۱ فروردین سال ۰) برای اولین روز سال jy
function yearStartIndex(jy) {
return DAYS_PER_YEAR * jy + leapsBefore(jy);
}
// شمارهی روز در سال (از صفر) برای اولین روز ماه jm
function monthStartDoy(jm) {
return jm <= 6 ? (jm - 1) * 31 : 186 + (jm - 7) * 30;
}
// تعداد روزهای ماه شمسی
function daysInJalaliMonth(jy, jm) {
if (jm <= 6) return 31;
if (jm <= 11) return 30;
return isJalaliLeap(jy) ? 30 : 29;
}
// بررسی اعتبار تاریخ شمسی
function assertValidJalali(jy, jm, jd) {
if (!Number.isInteger(jy) || !Number.isInteger(jm) || !Number.isInteger(jd)) {
throw new RangeError('تاریخ باید عدد صحیح باشد');
}
if (jm < 1 || jm > 12) throw new RangeError('ماه نامعتبر است');
if (jd < 1 || jd > daysInJalaliMonth(jy, jm)) throw new RangeError('روز نامعتبر است');
}
// میلادی به شمارهی روز ژولیانی (JDN)
function gregorianToJdn(gy, gm, gd) {
const a = Math.floor((14 - gm) / 12);
const y = gy + 4800 - a;
const m = gm + 12 * a - 3;
return gd
+ Math.floor((153 * m + 2) / 5)
+ 365 * y
+ Math.floor(y / 4)
- Math.floor(y / 100)
+ Math.floor(y / 400)
- 32045;
}
// شمارهی روز ژولیانی به میلادی
function jdnToGregorian(j) {
const a = j + 32044;
const b = Math.floor((4 * a + 3) / 146097);
const c = a - Math.floor((146097 * b) / 4);
const d = Math.floor((4 * c + 3) / 1461);
const e = c - Math.floor((1461 * d) / 4);
const m = Math.floor((5 * e + 2) / 153);
const gd = e - Math.floor((153 * m + 2) / 5) + 1;
const gm = m + 3 - 12 * Math.floor(m / 10);
const gy = 100 * b + d - 4800 + Math.floor(m / 10);
return { gy, gm, gd };
}
// میلادی ← شمسی
// ورودی: سال، ماه، روز میلادی | خروجی: { jy, jm, jd }
function gregorianToJalali(gy, gm, gd) {
if (!Number.isInteger(gy) || gm < 1 || gm > 12 || gd < 1 || gd > 31) {
throw new RangeError('تاریخ میلادی نامعتبر است');
}
const jdn = gregorianToJdn(gy, gm, gd);
// شمارهی روز نسبت به صفر شمسی
const idx = jdn - ANCHOR_JDN + yearStartIndex(ANCHOR_JALALI_YEAR);
// تخمین سال و اصلاح آن
let jy = Math.floor(idx / 365.2422);
while (yearStartIndex(jy) > idx) jy--;
while (yearStartIndex(jy + 1) <= idx) jy++;
const doy = idx - yearStartIndex(jy);
const jm = doy < 186
? Math.floor(doy / 31) + 1
: Math.floor((doy - 186) / 30) + 7;
const jd = doy - monthStartDoy(jm) + 1;
return { jy, jm, jd };
}
// شمسی ← میلادی
// ورودی: سال، ماه، روز شمسی | خروجی: { gy, gm, gd }
function jalaliToGregorian(jy, jm, jd) {
assertValidJalali(jy, jm, jd);
const idx = yearStartIndex(jy) + monthStartDoy(jm) + jd - 1;
const jdn = idx - yearStartIndex(ANCHOR_JALALI_YEAR) + ANCHOR_JDN;
return jdnToGregorian(jdn);
}
// تبدیل رقمهای لاتین به فارسی
function toPersianDigits(value) {
const fa = '۰۱۲۳۴۵۶۷۸۹';
return String(value).replace(/\d/g, (d) => fa[d]);
}
// قالببندی: مثلاً «۱ فروردین ۱۴۰۳»
function formatJalali(jy, jm, jd) {
assertValidJalali(jy, jm, jd);
return `${toPersianDigits(jd)} ${JALALI_MONTHS[jm - 1]} ${toPersianDigits(jy)}`;
}
// ---------- آزمونها ----------
const eqGreg = (a, b) => a.gy === b.gy && a.gm === b.gm && a.gd === b.gd;
const eqJal = (a, b) => a.jy === b.jy && a.jm === b.jm && a.jd === b.jd;
// ۱ فروردین ۱۴۰۳ = 2024-03-20
console.assert(eqGreg(jalaliToGregorian(1403, 1, 1), { gy: 2024, gm: 3, gd: 20 }), 'ا ف ۱۴۰۳ به میلادی');
console.assert(eqJal(gregorianToJalali(2024, 3, 20), { jy: 1403, jm: 1, jd: 1 }), '2024-03-20 به شمسی');
// ۱ فروردین ۱۴۰۰ = 2021-03-21
console.assert(eqGreg(jalaliToGregorian(1400, 1, 1), { gy: 2021, gm: 3, gd: 21 }), 'ا ف ۱۴۰۰ به میلادی');
console.assert(eqJal(gregorianToJalali(2021, 3, 21), { jy: 1400, jm: 1, jd: 1 }), '2021-03-21 به شمسی');
// ۱ فروردین ۱۳۹۹ = 2020-03-20
console.assert(eqGreg(jalaliToGregorian(1399, 1, 1), { gy: 2020, gm: 3, gd: 20 }), 'ا ف ۱۳۹۹ به میلادی');
// ۱ فروردین ۱۴۰۴ = 2025-03-21 (سال ۱۴۰۳ کبیسه است)
console.assert(eqGreg(jalaliToGregorian(1404, 1, 1), { gy: 2025, gm: 3, gd: 21 }), 'ا ف ۱۴۰۴ به میلادی');
// اسفند ۱۴۰۳ سیروزه است و آخرین روزش 2025-03-20 است
console.assert(daysInJalaliMonth(1403, 12) === 30, 'طول اسفند ۱۴۰۳');
console.assert(eqGreg(jalaliToGregorian(1403, 12, 30), { gy: 2025, gm: 3, gd: 20 }), 'آخر اسفند ۱۴۰۳');
// 2024-02-29 = ۱۰ اسفند ۱۴۰۲
console.assert(eqJal(gregorianToJalali(2024, 2, 29), { jy: 1402, jm: 12, jd: 10 }), '2024-02-29 به شمسی');
// کبیسهها
console.assert(isJalaliLeap(1403) === true, '۱۴۰۳ کبیسه است');
console.assert(isJalaliLeap(1399) === true, '۱۳۹۹ کبیسه است');
console.assert(isJalaliLeap(1404) === false, '۱۴۰۴ کبیسه نیست');
console.assert(isJalaliLeap(1402) === false, '۱۴۰۲ کبیسه نیست');
// قالببندی فارسی
console.assert(formatJalali(1403, 1, 1) === '۱ فروردین ۱۴۰۳', 'قالببندی ۱ فروردین ۱۴۰۳');
console.assert(formatJalali(1402, 12, 29) === '۲۹ اسفند ۱۴۰۲', 'قالببندی ۲۹ اسفند ۱۴۰۲');
// آزمون رفتوبرگشت برای همهی روزهای بازهی ۱۳۰۰ تا ۱۴۹۹
let roundTripErrors = 0;
let consecutiveErrors = 0;
let prevG = null;
for (let jy = 1300; jy <= 1499; jy++) {
for (let jm = 1; jm <= 12; jm++) {
const dim = daysInJalaliMonth(jy, jm);
for (let jd = 1; jd <= dim; jd++) {
const g = jalaliToGregorian(jy, jm, jd);
if (!eqJal(gregorianToJalali(g.gy, g.gm, g.gd), { jy, jm, jd })) roundTripErrors++;
// هر روز باید دقیقاً یک روز بعد از روز قبل باشد
if (prevG) {
const diff = gregorianToJdn(g.gy, g.gm, g.gd) - gregorianToJdn(prevG.gy, prevG.gm, prevG.gd);
if (diff !== 1) consecutiveErrors++;
}
prevG = g;
}
}
}
console.assert(roundTripErrors === 0, 'خطای رفتوبرگشت در بازهی ۱۳۰۰–۱۴۹۹: ' + roundTripErrors);
console.assert(consecutiveErrors === 0, 'خطای پیوستگی روزها در بازهی ۱۳۰۰–۱۴۹۹: ' + consecutiveErrors);
// تعداد کبیسهها در هر چرخهی ۳۳ ساله باید ۸ باشد
let leapCount = 0;
for (let y = 1300; y < 1333; y++) if (isJalaliLeap(y)) leapCount++;
console.assert(leapCount === 8, 'تعداد کبیسهها در ۳۳ سال');
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
gregorianToJalali,
jalaliToGregorian,
isJalaliLeap,
formatJalali,
daysInJalaliMonth,
toPersianDigits,
JALALI_MONTHS
};
}