406 lines
10 KiB
TypeScript
406 lines
10 KiB
TypeScript
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)}`,
|
|
};
|
|
}
|