io
This commit is contained in:
405
lib/persianDate.ts
Normal file
405
lib/persianDate.ts
Normal file
@@ -0,0 +1,405 @@
|
||||
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)}`,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user