Fixed timing issue with UTC

This commit is contained in:
SepehrYahyaee
2026-05-16 10:28:32 +03:30
parent e4d6246103
commit 2c851725a5
3 changed files with 114 additions and 21 deletions

View File

@@ -0,0 +1,26 @@
import { parseIranLocalDateTime } from "./iran-datetime";
describe("parseIranLocalDateTime", () => {
it("parses YYYY-MM-DD + HH:MM as Iran local (not server local)", () => {
const instant = parseIranLocalDateTime("2025-05-16", "18:28");
expect(instant).not.toBeNull();
// 18:28 Iran = 14:58 UTC
expect(instant!.toISOString()).toBe("2025-05-16T14:58:00.000Z");
});
it("parses naive ISO datetime as Iran local", () => {
const instant = parseIranLocalDateTime("2025-05-16T18:28");
expect(instant!.toISOString()).toBe("2025-05-16T14:58:00.000Z");
});
it("respects explicit UTC offset in string", () => {
const instant = parseIranLocalDateTime("2025-05-16T14:58:00.000Z");
expect(instant!.toISOString()).toBe("2025-05-16T14:58:00.000Z");
});
it("combines Date calendar day in Iran with accidentTime", () => {
const dateOnly = new Date("2025-05-16T00:00:00.000Z");
const instant = parseIranLocalDateTime(dateOnly, "18:28");
expect(instant!.toISOString()).toBe("2025-05-16T14:58:00.000Z");
});
});

View File

@@ -0,0 +1,82 @@
/** 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;
}