import DateObject from "react-date-object"; import persian from "react-date-object/calendars/persian"; import gregorian from "react-date-object/calendars/gregorian"; export type JalaliDateParts = { year: number; month: number; day: number; }; export const TEHRAN_TIMEZONE = "Asia/Tehran"; const PERSIAN_MONTHS = [ "فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند", ]; const PERSIAN_WEEKDAYS = ["ش", "ی", "د", "س", "چ", "پ", "ج"]; function div(a: number, b: number) { return Math.floor(a / b); } function mod(a: number, b: number) { return a - Math.floor(a / b) * b; } function pad(value: number) { return String(value).padStart(2, "0"); } function createGregorianDateObject(year: number, month: number, day: number) { return new DateObject({ calendar: gregorian, year, month, day, }); } function createPersianDateObject(year: number, month: number, day: number) { return new DateObject({ calendar: persian, year, month, day, }); } function parseGmtOffset(offsetLabel: string) { const normalized = offsetLabel.replace("GMT", ""); const sign = normalized.startsWith("-") ? -1 : 1; const [hoursPart, minutesPart = "0"] = normalized.replace(/[+-]/, "").split(":"); return sign * (Number(hoursPart) * 60 + Number(minutesPart)); } function getTimeZoneOffsetMinutes(date: Date, timeZone: string) { const formatter = new Intl.DateTimeFormat("en-US", { timeZone, timeZoneName: "shortOffset", hour: "2-digit", }); const offsetPart = formatter.formatToParts(date).find((part) => part.type === "timeZoneName")?.value ?? "GMT+0"; return parseGmtOffset(offsetPart); } function getTimeZoneDateParts(date: Date, timeZone: string) { const formatter = new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hourCycle: "h23", }); const parts = formatter.formatToParts(date); const read = (type: string) => Number(parts.find((part) => part.type === type)?.value ?? "0"); return { year: read("year"), month: read("month"), day: read("day"), hour: read("hour"), minute: read("minute"), }; } function zonedDateTimeToUtcDate( year: number, month: number, day: number, hour: number, minute: number, timeZone = TEHRAN_TIMEZONE ) { let utcDate = new Date(Date.UTC(year, month - 1, day, hour, minute, 0, 0)); for (let index = 0; index < 2; index += 1) { const offsetMinutes = getTimeZoneOffsetMinutes(utcDate, timeZone); utcDate = new Date(Date.UTC(year, month - 1, day, hour, minute, 0, 0) - offsetMinutes * 60 * 1000); } return utcDate; } function jalCal(jy: number) { 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; if (jy < breaks[0] || jy >= breaks[bl - 1]) { throw new Error("Invalid Jalali year"); } let jump = 0; let leapJ = -14; let jp = breaks[0]; for (let i = 1; i < bl; i += 1) { const jm = breaks[i]; jump = jm - jp; if (jy < jm) { break; } leapJ += div(jump, 33) * 8 + div(mod(jump, 33), 4); jp = jm; } let n = jy - jp; leapJ += div(n, 33) * 8 + div(mod(n, 33) + 3, 4); if (mod(jump, 33) === 4 && jump - n === 4) { leapJ += 1; } const gy = jy + 621; const leapG = div(gy, 4) - div((div(gy, 100) + 1) * 3, 4) - 150; const march = 20 + leapJ - leapG; if (jump - n < 6) { n = n - jump + div(jump + 4, 33) * 33; } let leap = mod(mod(n + 1, 33) - 1, 4); if (leap === -1) { leap = 4; } return { leap, gy, march }; } function g2d(gy: number, gm: number, gd: number) { let d = div((gy + div(gm - 8, 6) + 100100) * 1461, 4) + div(153 * mod(gm + 9, 12) + 2, 5) + gd - 34840408; d = d - div(div(gy + 100100 + div(gm - 8, 6), 100) * 3, 4) + 752; return d; } function d2g(jdn: number) { let j = 4 * jdn + 139361631; j = j + div(div(4 * jdn + 183187720, 146097) * 3, 4) * 4 - 3908; const i = div(mod(j, 1461), 4) * 5 + 308; const gd = div(mod(i, 153), 5) + 1; const gm = mod(div(i, 153), 12) + 1; const gy = div(j, 1461) - 100100 + div(8 - gm, 6); return { year: gy, month: gm, day: gd }; } function j2d(jy: number, jm: number, jd: number) { const r = jalCal(jy); return g2d(r.gy, 3, r.march) + (jm - 1) * 31 - div(jm, 7) * (jm - 7) + jd - 1; } function d2j(jdn: number): JalaliDateParts { const g = d2g(jdn); let jy = g.year - 621; const r = jalCal(jy); const jdn1f = g2d(g.year, 3, r.march); let k = jdn - jdn1f; if (k >= 0) { if (k <= 185) { return { year: jy, month: 1 + div(k, 31), day: mod(k, 31) + 1, }; } k -= 186; } else { jy -= 1; k += 179; if (r.leap === 1) { k += 1; } } return { year: jy, month: 7 + div(k, 30), day: mod(k, 30) + 1, }; } export function toJalali(gy: number, gm: number, gd: number) { const converted = createGregorianDateObject(gy, gm, gd).convert(persian); return { year: Number(converted.year), month: Number(converted.month.number), day: Number(converted.day), }; } export function toGregorian(jy: number, jm: number, jd: number) { const converted = createPersianDateObject(jy, jm, jd).convert(gregorian); return { year: Number(converted.year), month: Number(converted.month.number), day: Number(converted.day), }; } export function isLeapJalaliYear(year: number) { return createPersianDateObject(year, 1, 1).isLeap; } export function getJalaliMonthDays(year: number, month: number) { if (month <= 6) return 31; if (month <= 11) return 30; return isLeapJalaliYear(year) ? 30 : 29; } export function getPersianMonthName(month: number) { return PERSIAN_MONTHS[month - 1]; } export function getPersianWeekdays() { return PERSIAN_WEEKDAYS; } export function formatDateOnly(year: number, month: number, day: number) { return `${year}-${pad(month)}-${pad(day)}`; } export function parseDateOnly(value: string) { const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/); if (!match) return null; return { year: Number(match[1]), month: Number(match[2]), day: Number(match[3]) }; } export function getGregorianDateInputValue(date: Date) { return formatDateOnly(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate()); } export function formatGregorianDate(date: Date) { return new Intl.DateTimeFormat("en-GB", { timeZone: TEHRAN_TIMEZONE, weekday: "long", year: "numeric", month: "long", day: "numeric", }).format(date); } export function formatGregorianDateTime(date: Date) { return new Intl.DateTimeFormat("en-GB", { timeZone: TEHRAN_TIMEZONE, weekday: "long", year: "numeric", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit", }).format(date); } export function formatPersianDate(date: Date) { return new Intl.DateTimeFormat("fa-IR-u-ca-persian", { timeZone: TEHRAN_TIMEZONE, weekday: "long", year: "numeric", month: "long", day: "numeric", }).format(date); } export function formatPersianDateTime(date: Date) { return new Intl.DateTimeFormat("fa-IR-u-ca-persian", { timeZone: TEHRAN_TIMEZONE, weekday: "long", year: "numeric", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit", hourCycle: "h23", }).format(date); } export function formatPersianTime(date: Date) { return new Intl.DateTimeFormat("fa-IR", { timeZone: TEHRAN_TIMEZONE, hour: "2-digit", minute: "2-digit", hourCycle: "h23", }).format(date); } export function getTehranTimeInputValue(date: Date) { const parts = getTimeZoneDateParts(date, TEHRAN_TIMEZONE); return `${pad(parts.hour)}:${pad(parts.minute)}`; } export function getWeekdayOffset(year: number, month: number, day = 1) { const gregorian = toGregorian(year, month, day); const jsDay = new Date(Date.UTC(gregorian.year, gregorian.month - 1, gregorian.day)).getUTCDay(); return (jsDay + 1) % 7; } export function getTodayJalali() { const nowParts = getTimeZoneDateParts(new Date(), TEHRAN_TIMEZONE); return toJalali(nowParts.year, nowParts.month, nowParts.day); } export function jalaliDateToGregorianString(year: number, month: number, day: number) { const gregorian = toGregorian(year, month, day); return formatDateOnly(gregorian.year, gregorian.month, gregorian.day); } export function gregorianDateAndTimeToUtcIso(date: string, time: string) { const gregorian = parseDateOnly(date); if (!gregorian) return ""; const [hour, minute] = time.split(":").map(Number); return zonedDateTimeToUtcDate( gregorian.year, gregorian.month, gregorian.day, hour || 0, minute || 0 ).toISOString(); } export function jalaliDateTimeToUtcIso(year: number, month: number, day: number, time: string) { const gregorian = toGregorian(year, month, day); const [hour, minute] = time.split(":").map(Number); return zonedDateTimeToUtcDate( gregorian.year, gregorian.month, gregorian.day, hour || 0, minute || 0 ).toISOString(); } export function getDateFromJalaliDateTime(year: number, month: number, day: number, time = "12:00") { const gregorian = toGregorian(year, month, day); const [hour, minute] = time.split(":").map(Number); return zonedDateTimeToUtcDate( gregorian.year, gregorian.month, gregorian.day, hour || 0, minute || 0 ); } export function getGregorianDateForDisplay(year: number, month: number, day: number) { return formatGregorianDate(getDateFromJalaliDateTime(year, month, day, "12:00")); } export function dateValueToJalali(value: string, mode: "date" | "datetime") { if (!value) return null; if (mode === "date") { const parsed = parseDateOnly(value); if (!parsed) return null; return { ...toJalali(parsed.year, parsed.month, parsed.day), time: "", }; } const date = new Date(value); if (Number.isNaN(date.getTime())) return null; const tehranParts = getTimeZoneDateParts(date, TEHRAN_TIMEZONE); return { ...toJalali(tehranParts.year, tehranParts.month, tehranParts.day), time: `${pad(tehranParts.hour)}:${pad(tehranParts.minute)}`, }; }