/** Iran standard time (Asia/Tehran, UTC+3:30; no DST since 2022). */ export const IRAN_TIMEZONE = "Asia/Tehran"; const IRAN_UTC_OFFSET = "+03:30"; const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/; const TIME_HM_RE = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/; const NAIVE_ISO_DATETIME_RE = /^(\d{4}-\d{2}-\d{2})[T\s](\d{1,2}:\d{2}(?::\d{2})?)$/; /** * Gregorian calendar date (`YYYY-MM-DD`) for an instant in Iran. */ export function gregorianDateInIran(d: Date): string { return new Intl.DateTimeFormat("en-CA", { timeZone: IRAN_TIMEZONE, year: "numeric", month: "2-digit", day: "2-digit", }).format(d); } function normalizeTimeToHms(time: string): string | null { const m = String(time).trim().match(TIME_HM_RE); if (!m) return null; const hh = m[1].padStart(2, "0"); const mm = m[2]; const ss = m[3] ?? "00"; return `${hh}:${mm}:${ss}`; } function parseIranLocalIso(isoDate: string, timeHms: string): Date | null { const d = new Date(`${isoDate}T${timeHms}${IRAN_UTC_OFFSET}`); return Number.isNaN(d.getTime()) ? null : d; } /** * Combines a calendar date and optional `HH:MM` / `HH:MM:SS` as Iran local * time and returns the corresponding UTC instant. */ export function parseIranLocalDateTime( date: Date | string, time?: string, ): Date | null { if (date instanceof Date) { if (Number.isNaN(date.getTime())) return null; const cleanTime = (time ?? "").trim(); if (!cleanTime) return date; const timeHms = normalizeTimeToHms(cleanTime); if (!timeHms) return null; return parseIranLocalIso(gregorianDateInIran(date), timeHms); } const raw = String(date).trim(); if (!raw) return null; if (/[zZ]|[+-]\d{2}:?\d{2}$/.test(raw)) { const d = new Date(raw); return Number.isNaN(d.getTime()) ? null : d; } const naive = raw.match(NAIVE_ISO_DATETIME_RE); if (naive) { const timeHms = normalizeTimeToHms(naive[2]); if (!timeHms) return null; return parseIranLocalIso(naive[1], timeHms); } if (DATE_ONLY_RE.test(raw)) { const cleanTime = (time ?? "").trim(); if (!cleanTime) { const d = new Date(`${raw}T00:00:00${IRAN_UTC_OFFSET}`); return Number.isNaN(d.getTime()) ? null : d; } const timeHms = normalizeTimeToHms(cleanTime); if (!timeHms) return null; return parseIranLocalIso(raw, timeHms); } const d = new Date(raw); return Number.isNaN(d.getTime()) ? null : d; }