1
0
forked from Yara724/api
Files
yara724-api/src/helpers/date-jalali.ts
2026-06-01 14:11:23 +03:30

197 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Converts an ISO date string or Date to Jalali (Persian) date and time.
* @param input - ISO date string (e.g. 2026-02-08T13:51:20.747+00:00) or Date
* @returns Tuple [dateStr, timeStr] e.g. ['1404/11/24', '13:37']
*/
export function toJalaliDateAndTime(input: string | Date): [string, string] {
const d = new Date(input);
const dateStr = toJalaliDate(d);
const timeStr = toJalaliTime(d);
return [dateStr, timeStr];
}
/**
* Converts a Jalali (Persian) date in any of the common shapes used across
* this codebase to a Gregorian `YYYY-MM-DD` string.
*
* Accepted input shapes:
* - number `13770624` (packed `YYYYMMDD`)
* - string `"13770624"` / `"1377-06-24"` / `"1377/06/24"` / `"1377/6/24"`
* - already-Gregorian `YYYY-MM-DD` strings are returned untouched as long as
* they parse (we treat them as Gregorian only when the year is clearly
* Gregorian, i.e. >= 1900). Otherwise the year is treated as Jalali.
*
* Returns `null` when the input cannot be parsed into a valid Jalali date.
*/
export function jalaliToGregorianDate(
input: string | number | null | undefined,
): string | null {
if (input === null || input === undefined) return null;
const raw = typeof input === "number" ? String(input) : String(input).trim();
if (!raw) return null;
let year = 0;
let month = 0;
let day = 0;
const separated = raw.match(/^(\d{4})[\-/](\d{1,2})[\-/](\d{1,2})$/);
if (separated) {
year = parseInt(separated[1], 10);
month = parseInt(separated[2], 10);
day = parseInt(separated[3], 10);
} else {
const digits = raw.replace(/\D/g, "");
if (digits.length === 8) {
year = parseInt(digits.slice(0, 4), 10);
month = parseInt(digits.slice(4, 6), 10);
day = parseInt(digits.slice(6, 8), 10);
} else {
return null;
}
}
if (!year || !month || !day) return null;
// If the year already looks Gregorian (>= 1900), just normalise formatting.
if (year >= 1900) {
const mm = String(month).padStart(2, "0");
const dd = String(day).padStart(2, "0");
const d = new Date(`${year}-${mm}-${dd}`);
if (isNaN(d.getTime())) return null;
return `${year}-${mm}-${dd}`;
}
return jalaliPartsToGregorian(year, month, day);
}
/**
* Converts a Date to Jalali date string (e.g. 1404/11/24).
*/
export function toJalaliDate(d: Date): string {
const persianDate = d.toLocaleDateString("fa-IR", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
return persianDigitsToEnglish(persianDate, "/");
}
/**
* Converts a Date to 24h time string (e.g. 13:37).
*/
export function toJalaliTime(d: Date): string {
const hours = d.getHours().toString().padStart(2, "0");
const minutes = d.getMinutes().toString().padStart(2, "0");
return `${hours}:${minutes}`;
}
/**
* Converts Jalali year/month/day parts to a Gregorian `YYYY-MM-DD` string
* using the algorithmic conversion (no external deps).
* Returns `null` if the resulting Gregorian date is invalid.
*/
function jalaliPartsToGregorian(
jYear: number,
jMonth: number,
jDay: number,
): string | null {
const jy = jYear - 979;
const jm = jMonth - 1;
const jd = jDay - 1;
// Jalali day number — correct leap cycle is every 33 years with 8 leaps
let jDayNo =
365 * jy + Math.floor(jy / 33) * 8 + Math.floor(((jy % 33) + 3) / 4);
const jalaliMonthDays = [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29];
for (let i = 0; i < jm; i++) {
jDayNo += jalaliMonthDays[i];
}
jDayNo += jd;
// Offset to Gregorian day number anchored at 1600
let gDayNo = jDayNo + 79;
let gy = 1600 + 400 * Math.floor(gDayNo / 146097);
gDayNo %= 146097;
let leap = true;
if (gDayNo >= 36525) {
gDayNo--;
gy += 100 * Math.floor(gDayNo / 36524);
gDayNo %= 36524;
if (gDayNo >= 365) gDayNo++;
else leap = false;
}
gy += 4 * Math.floor(gDayNo / 1461);
gDayNo %= 1461;
if (gDayNo >= 366) {
leap = false;
gDayNo--;
gy += Math.floor(gDayNo / 365);
gDayNo %= 365;
}
const gregorianMonthDays = [
31,
leap ? 29 : 28,
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
];
let gm = 0;
for (let i = 0; i < 12; i++) {
if (gDayNo < gregorianMonthDays[i]) {
gm = i + 1;
break;
}
gDayNo -= gregorianMonthDays[i];
}
const gd = gDayNo + 1;
const mm = String(gm).padStart(2, "0");
const dd = String(gd).padStart(2, "0");
const result = `${gy}-${mm}-${dd}`;
const check = new Date(result);
if (isNaN(check.getTime())) return null;
return result;
}
const PERSIAN_TO_ENGLISH: Record<string, string> = {
"۰": "0",
"۱": "1",
"۲": "2",
"۳": "3",
"۴": "4",
"۵": "5",
"۶": "6",
"۷": "7",
"۸": "8",
"۹": "9",
};
function persianDigitsToEnglish(str: string, separator: string): string {
return str
.split(separator)
.map((part) =>
part
.split("")
.map((char) => PERSIAN_TO_ENGLISH[char] ?? char)
.join(""),
)
.join("/");
}