Files
CRM_Montzar/apps/api/src/common/jalali.ts
T
MahdiandCursor cf326d1ed9 fix: keep birthdays and show Shamsi dates
Imported customers keep their birthday, and assistant text plus remaining calendars use the Jalali date.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 18:04:53 +03:30

230 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}