Imported customers keep their birthday, and assistant text plus remaining calendars use the Jalali date. Co-authored-by: Cursor <cursoragent@cursor.com>
230 lines
6.5 KiB
TypeScript
230 lines
6.5 KiB
TypeScript
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);
|
||
}
|