const MONTH_NAMES = [ 'فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند', ] as const; function div(a: number, b: number) { return Math.trunc(a / b); } function mod(a: number, b: number) { return a - Math.trunc(a / b) * b; } export function toJalali(gy: number, gm: number, gd: number) { const gdm = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; const gy2 = gm > 2 ? gy + 1 : gy; let days = 355666 + 365 * gy + div(gy2 + 3, 4) - div(gy2 + 99, 100) + div(gy2 + 399, 400) + gd + gdm[gm - 1]; let jy = -1595 + 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 toGregorian(jy: number, jm: number, jd: number) { const jy2 = jy + 1595; let days = -355668 + 365 * jy2 + div(jy2, 33) * 8 + div(mod(jy2, 33) + 3, 4) + jd + (jm < 7 ? (jm - 1) * 31 : (jm - 7) * 30 + 186); let gy = 400 * div(days, 146097); days %= 146097; if (days > 36524) { gy += 100 * div(--days, 36524); days %= 36524; if (days >= 365) days += 1; } gy += 4 * div(days, 1461); days %= 1461; if (days > 365) { gy += div(days - 1, 365); days = (days - 1) % 365; } const gd = days + 1; const salA = [ 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; let dayAcc = gd; for (gm = 1; gm <= 12 && dayAcc > salA[gm]; gm += 1) { dayAcc -= salA[gm]; } return { gy, gm, gd: dayAcc }; } export function dateToJalali(d: Date) { return toJalali(d.getFullYear(), d.getMonth() + 1, d.getDate()); } export function jalaliToDate(jy: number, jm: number, jd: number): Date { const { gy, gm, gd } = toGregorian(jy, jm, jd); return new Date(gy, gm - 1, gd); } export function addJalaliMonths(jy: number, jm: number, delta: number) { const idx = jy * 12 + (jm - 1) + delta; return { jy: Math.floor(idx / 12), jm: (idx % 12) + 1 }; } export function jalaliMonthName(jm: number): string { return MONTH_NAMES[jm - 1] ?? ''; } export function jalaliMonthKey(d: Date): string { const { jy, jm } = dateToJalali(d); return `${jy}-${String(jm).padStart(2, '0')}`; } const FA_DIGITS = '۰۱۲۳۴۵۶۷۸۹'; function toAsciiDigits(value: string) { return value .replace(/[۰-۹]/g, (d) => String(FA_DIGITS.indexOf(d))) .replace(/[٠-٩]/g, (d) => String('٠١٢٣٤٥٦٧٨٩'.indexOf(d))); } function toFaDigits(value: string | number) { return String(value).replace(/\d/g, (d) => FA_DIGITS[Number(d)] ?? d); } function tehranYmd(d: Date) { const [gy, gm, gd] = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Tehran', year: 'numeric', month: '2-digit', day: '2-digit', }) .format(d) .split('-') .map(Number); return { gy, gm, gd }; } /** تاریخ شمسی تهران، مثل ۱۴۰۲/۰۶/۱۲ */ export function formatTehranJalali(d: Date): string { const { gy, gm, gd } = tehranYmd(d); const j = toJalali(gy, gm, gd); return `${toFaDigits(j.jy)}/${toFaDigits(String(j.jm).padStart(2, '0'))}/${toFaDigits(String(j.jd).padStart(2, '0'))}`; } const GREG_MONTHS: Array<[string, number]> = [ ['ژانویه', 1], ['january', 1], ['فوریه', 2], ['february', 2], ['مارس', 3], ['march', 3], ['آوریل', 4], ['april', 4], ['ژوئن', 6], ['june', 6], ['ژوئیه', 7], ['جولای', 7], ['july', 7], ['اوت', 8], ['آگوست', 8], ['آگست', 8], ['august', 8], ['سپتامبر', 9], ['september', 9], ['اکتبر', 10], ['october', 10], ['نوامبر', 11], ['november', 11], ['دسامبر', 12], ['december', 12], ['مه', 5], ['می', 5], ['may', 5], ]; function gregMonthNumber(name: string): number | null { const key = name.trim().toLowerCase(); return GREG_MONTHS.find(([label]) => label === key)?.[1] ?? null; } function inferGregorianYear(month: number, day: number, now = new Date()): number { const { gy, gm, gd } = tehranYmd(now); if (month > gm || (month === gm && day > gd)) return gy - 1; return gy; } function jalaliFromGregorian(year: number, month: number, day: number): string | null { if (month < 1 || month > 12 || day < 1 || day > 31 || year < 1700 || year > 2500) return null; const j = toJalali(year, month, day); return `${toFaDigits(j.jy)}/${toFaDigits(String(j.jm).padStart(2, '0'))}/${toFaDigits(String(j.jd).padStart(2, '0'))}`; } /** «21 سپتامبر» و تاریخ میلادی عددی را در متن کاربر به شمسی تبدیل می‌کند. */ export function rewriteGregorianDates(text: string, now = new Date()): string { const monthAlt = GREG_MONTHS.map(([name]) => name).join('|'); const dayMonth = new RegExp( `(^|[^\\p{L}\\d])([0-9۰-۹]{1,2})\\s*(${monthAlt})(?![\\p{L}\\u200c])(?:\\s+([0-9۰-۹]{4}))?`, 'giu', ); const monthDay = new RegExp( `(^|[^\\p{L}\\d])(${monthAlt})(?![\\p{L}\\u200c])\\s+([0-9۰-۹]{1,2})(?:\\s*,?\\s*([0-9۰-۹]{4}))?`, 'giu', ); const iso = /\b((?:19|20)\d{2})-(\d{2})-(\d{2})\b/g; const fromParts = (dayRaw: string, monthName: string, yearRaw?: string) => { const day = Number(toAsciiDigits(dayRaw)); const month = gregMonthNumber(monthName); if (!month) return null; const year = yearRaw ? Number(toAsciiDigits(yearRaw)) : inferGregorianYear(month, day, now); return jalaliFromGregorian(year, month, day); }; let out = text.replace(dayMonth, (full, lead: string, day: string, month: string, year?: string) => { const next = fromParts(day, month, year); return next ? `${lead}${next}` : full; }); out = out.replace(monthDay, (full, lead: string, month: string, day: string, year?: string) => { const next = fromParts(day, month, year); return next ? `${lead}${next}` : full; }); out = out.replace(iso, (full, year: string, month: string, day: string) => { return jalaliFromGregorian(Number(year), Number(month), Number(day)) ?? full; }); return out; } export function startOfJalaliMonth(jy: number, jm: number): Date { return jalaliToDate(jy, jm, 1); } export function endOfJalaliMonth(jy: number, jm: number): Date { const next = addJalaliMonths(jy, jm, 1); const startNext = jalaliToDate(next.jy, next.jm, 1); return new Date(startNext.getTime() - 1); }