/** تبدیل و ابزار تقویم شمسی (الگوریتم استاندارد جلالی) */ export type JalaliDate = { jy: number; jm: number; jd: number }; const MONTH_NAMES = [ 'فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند', ] as const; const WEEKDAY_NAMES = [ 'یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه', 'شنبه', ] as const; function div(a: number, b: number) { return Math.trunc(a / b); } export function isLeapJalaliYear(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 bl = breaks.length; let jp = breaks[0]; let jump = 0; for (let i = 1; i < bl; i += 1) { const jm = breaks[i]; jump = jm - jp; if (jy < jm) break; jp = jm; } let n = jy - jp; if (jump - n < 6) n = n - jump + div(jump + 4, 33) * 33; let leap = mod(n + 1, 33) - 1; if (leap === -1) leap = 32; return leap === 0 || leap % 4 === 0; } function mod(a: number, b: number) { return a - Math.trunc(a / b) * b; } export function jalaliMonthLength(jy: number, jm: number): number { if (jm <= 6) return 31; if (jm <= 11) return 30; return isLeapJalaliYear(jy) ? 30 : 29; } export function toJalali(gy: number, gm: number, gd: number): JalaliDate { const gdm = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; let 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): { gy: number; gm: number; gd: 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): JalaliDate { 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): { jy: number; jm: 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 weekdayName(d: Date): string { return WEEKDAY_NAMES[d.getDay()] ?? ''; } /** مثل: پنجشنبه ۵ شهریور ۱۴۰۵ */ export function formatJalaliLong(d: Date): string { const { jy, jm, jd } = dateToJalali(d); return `${weekdayName(d)} ${toFaDigits(jd)} ${jalaliMonthName(jm)} ${toFaDigits(jy)}`; } /** مثل: دوشنبه ۲۶ مرداد */ export function formatJalaliShort(d: Date): string { const { jm, jd } = dateToJalali(d); return `${weekdayName(d)} ${toFaDigits(jd)} ${jalaliMonthName(jm)}`; } /** مثل: ۸ شهریور ۱۴۰۵ */ export function formatJalaliDay(d: Date): string { const { jy, jm, jd } = dateToJalali(d); return `${toFaDigits(jd)} ${jalaliMonthName(jm)} ${toFaDigits(jy)}`; } export function toFaDigits(value: number | string): string { return String(value).replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[Number(d)]!); } export function startOfLocalDay(d = new Date()): Date { const x = new Date(d); x.setHours(0, 0, 0, 0); return x; } export function formatClockFa(d: Date): string { const hh = String(d.getHours()).padStart(2, '0'); const mm = String(d.getMinutes()).padStart(2, '0'); return `${toFaDigits(hh)}:${toFaDigits(mm)}`; } /** مثل: ۲۴ مرداد ۱۱:۲۰ */ export function formatJalaliDayClock(d: Date): string { const { jm, jd } = dateToJalali(d); return `${toFaDigits(jd)} ${jalaliMonthName(jm)} ${formatClockFa(d)}`; } /** مثل: ۸ شهریور ۱۴۰۵، ۱۶:۳۰ */ export function formatJalaliDateTime(d: Date): string { return `${formatJalaliDay(d)}، ${formatClockFa(d)}`; } export function calendarDaysBetween(from: Date, to: Date): number { return Math.round( (startOfLocalDay(to).getTime() - startOfLocalDay(from).getTime()) / 86_400_000, ); } /** آخرین ارتباط: امروز ۱۶:۳۰ / ۶ روز پیش */ export function formatRelativeContact(iso: string | null | undefined): string { if (!iso) return '—'; const d = new Date(iso); if (Number.isNaN(d.getTime())) return '—'; const days = calendarDaysBetween(d, new Date()); if (days <= 0) return `امروز ${formatClockFa(d)}`; if (days === 1) return 'دیروز'; return `${toFaDigits(days)} روز پیش`; } /** فعالیت بعدی: امروز ۱۶:۳۰ / فردا ۱۰:۰۰ / ۲۴ مرداد ۱۱:۲۰ */ export function formatNextSlot(iso: string | null | undefined): string { if (!iso) return '—'; const d = new Date(iso); if (Number.isNaN(d.getTime())) return '—'; const days = calendarDaysBetween(new Date(), d); if (days === 0) return `امروز ${formatClockFa(d)}`; if (days === 1) return `فردا ${formatClockFa(d)}`; if (days === -1) return `دیروز ${formatClockFa(d)}`; return formatJalaliDayClock(d); } export function gregorianDayKey(d: Date): string { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${day}`; } export function parseLocalInput(value: string | null | undefined): Date | null { if (!value) return null; const match = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2}))?/.exec(value); if (match) { return new Date( Number(match[1]), Number(match[2]) - 1, Number(match[3]), match[4] ? Number(match[4]) : 0, match[5] ? Number(match[5]) : 0, ); } const parsed = new Date(value); return Number.isNaN(parsed.getTime()) ? null : parsed; } export function formatLocalDateInput(d: Date): string { return gregorianDayKey(d); } export function formatLocalDateTimeInput(d: Date): string { const pad = (n: number) => String(n).padStart(2, '0'); return `${gregorianDayKey(d)}T${pad(d.getHours())}:${pad(d.getMinutes())}`; } /** مثل: ۱۴۰۵/۰۶/۱۲ */ export function formatJalaliNumeric(d: Date): string { const { jy, jm, jd } = dateToJalali(d); return `${toFaDigits(jy)}/${toFaDigits(String(jm).padStart(2, '0'))}/${toFaDigits(String(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 toAsciiDigits(value: string) { return value .replace(/[۰-۹]/g, (d) => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(d))) .replace(/[٠-٩]/g, (d) => String('٠١٢٣٤٥٦٧٨٩'.indexOf(d))); } 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 = now.getFullYear(); const gm = now.getMonth() + 1; const gd = now.getDate(); if (month > gm || (month === gm && day > gd)) return gy - 1; return gy; } /** «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 stamp = (year: number, month: number, day: number) => { 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'))}`; }; 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 stamp(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 stamp(Number(year), Number(month), Number(day)) ?? full; }); return out; }