// ============================================================ // ابزار تقویم شمسی — پورت jalali.js به TypeScript // تبدیل‌ها با الگوریتم استاندارد Kazimierz M. Borkowski // ============================================================ const FA_DIGITS = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"]; export function div(a: number, b: number): number { return Math.floor(a / b); } export function jalaliToGregorian(jy: number, jm: number, jd: number): [number, number, number] { jy = +jy; jm = +jm; jd = +jd; let gy = jy <= 979 ? 621 : 1600; jy -= jy <= 979 ? 0 : 979; let days = 365 * jy + div(jy, 33) * 8 + div((jy % 33) + 3, 4) + 78 + jd + (jm < 7 ? (jm - 1) * 31 : (jm - 7) * 30 + 186); gy += 400 * div(days, 146097); days %= 146097; if (days > 36524) { gy += 100 * div(--days, 36524); if (days === 36524) days = 0; days++; } gy += 4 * div(days, 1461); days %= 1461; if (days > 365) { gy += div(days - 1, 365); days = (days - 1) % 365; } let gd = days + 1; const sal_a = [0, 31, (gy % 4 === 0 && gy % 100 !== 0) || gy % 400 === 0 ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; let gm = 0; for (; gm < 13 && gd > sal_a[gm]; gm++) gd -= sal_a[gm]; return [gy, gm, gd]; } export function gregorianToJalali(gy: number, gm: number, gd: number): [number, number, number] { gy = +gy; gm = +gm; gd = +gd; const g_d_m = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; let jy = gy <= 1600 ? 0 : 979; gy -= gy <= 1600 ? 621 : 1600; let days = 365 * gy + div(gy + 3, 4) - div(gy + 99, 100) + div(gy + 399, 400) - 80 + gd + g_d_m[gm - 1]; jy += 33 * div(days, 12053); days %= 12053; jy += 4 * div(days, 1461); days %= 1461; if (days > 365) { jy += div(days - 1, 365); days = (days - 1) % 365; } const jm = days < 186 ? 1 + div(days, 31) : 7 + div(days - 186, 30); const jd = 1 + (days < 186 ? days % 31 : (days - 186) % 30); return [jy, jm, jd]; } export function isLeapJalali(jy: number): boolean { const breaks = [-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210, 1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456, 3178]; const jp = breaks[0]; const jm = breaks[1]; let jump = 0; let jp2 = jp; for (const jl of breaks) { jump = jl - jp2; if (jy <= jl) break; jp2 = jl; } let n = jy - jm; if (n < jump) n = n + jump + 1; return (n % 33) % 4 === 1; } // ---------- formatting (Persian digits) ---------- export function toFa(s: string | number): string { return String(s).replace(/[0-9]/g, (d) => FA_DIGITS[Number(d)]); } // ماه‌های شمسی (نسخه‌ی درست‌شده — «مهر» به‌جای «آذر» تکراری) export const MONTHS = [ "فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند", ]; export const WEEKDAYS = ["ش", "ی", "د", "س", "چ", "پ", "ج"]; export function toAsciiDigits(s: string | number | null | undefined): string { if (s == null) return ""; return String(s) .replace(/[۰-۹]/g, (d) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(d))) .replace(/[٠-٩]/g, (d) => String("٠١٢٣٤٥٦٧٨٩".indexOf(d))); } // format an ISO (gregorian) string to "۱۴۰۳/۰۵/۱۲" export function isoToJalaliStr(iso: string | null | undefined): string { if (!iso) return ""; const d = new Date(iso); if (Number.isNaN(d.getTime())) return ""; const [jy, jm, jd] = gregorianToJalali(d.getFullYear(), d.getMonth() + 1, d.getDate()); return `${toFa(jy)}/${toFa(String(jm).padStart(2, "0"))}/${toFa(String(jd).padStart(2, "0"))}`; } // format with month name: "۱۸ مرداد ۱۴۰۴" export function isoToJalaliLong(iso: string | null | undefined): string { if (!iso) return ""; const d = new Date(iso); if (Number.isNaN(d.getTime())) return ""; const [jy, jm, jd] = gregorianToJalali(d.getFullYear(), d.getMonth() + 1, d.getDate()); return `${toFa(jd)} ${MONTHS[jm - 1]} ${toFa(jy)}`; } // parse a jalali "yyyy/m/d" or "yyyy/m/d h:m" string to a JS Date (gregorian) ISO string export function jalaliStrToIso(str: string | null | undefined): string | null { if (!str) return null; const s = toAsciiDigits(str).trim(); const m = s.match(/^(1[34]\d{2})[/\-.](\d{1,2})[/\-.](\d{1,2})(?:[ T](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?/); if (!m) { const nums = s .split(/[/\-.]/) .map((x) => parseInt(x, 10)); if (nums.length < 3 || nums.some((n) => Number.isNaN(n))) return null; const [jy, jm, jd] = nums; const [gy, gm, gd] = jalaliToGregorian(jy, jm, jd); const dt = new Date(gy, gm - 1, gd); return Number.isNaN(dt.getTime()) ? null : dt.toISOString(); } const jy = parseInt(m[1], 10); const jm = parseInt(m[2], 10); const jd = parseInt(m[3], 10); const h = m[4] != null ? parseInt(m[4], 10) : 0; const min = m[5] != null ? parseInt(m[5], 10) : 0; const sec = m[6] != null ? parseInt(m[6], 10) : 0; const [gy, gm, gd] = jalaliToGregorian(jy, jm, jd); const dt = new Date(gy, gm - 1, gd, h, min, sec); return Number.isNaN(dt.getTime()) ? null : dt.toISOString(); } export function daysInMonth(jy: number, jm: number): number { if (jm <= 6) return 31; if (jm <= 11) return 30; return isLeapJalali(jy) ? 30 : 29; }