Files
football-next/lib/persianDate.ts
2026-05-03 17:01:46 +03:30

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)}`,
};
}