forked from Shared/esg
133 lines
3.0 KiB
TypeScript
133 lines
3.0 KiB
TypeScript
const JALALI_BREAKS = [
|
|
-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210, 1635, 2060, 2097,
|
|
2192, 2262, 2324, 2394, 2456, 3178,
|
|
];
|
|
|
|
export function jalaliDateToGregorianDate(jalaliDate: string): string {
|
|
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(jalaliDate);
|
|
if (!match) {
|
|
throw new Error('birthDate must be a Jalali date in YYYY-MM-DD format');
|
|
}
|
|
|
|
const jy = Number(match[1]);
|
|
const jm = Number(match[2]);
|
|
const jd = Number(match[3]);
|
|
|
|
if (!isValidJalaliDate(jy, jm, jd)) {
|
|
throw new Error('birthDate is not a valid Jalali date');
|
|
}
|
|
|
|
const { gy, gm, gd } = jalaliToGregorian(jy, jm, jd);
|
|
return `${gy}-${pad2(gm)}-${pad2(gd)}`;
|
|
}
|
|
|
|
function isValidJalaliDate(jy: number, jm: number, jd: number): boolean {
|
|
if (jm < 1 || jm > 12 || jd < 1) return false;
|
|
if (jm <= 6) return jd <= 31;
|
|
if (jm <= 11) return jd <= 30;
|
|
return jd <= (isJalaliLeapYear(jy) ? 30 : 29);
|
|
}
|
|
|
|
function isJalaliLeapYear(jy: number): boolean {
|
|
return jalCal(jy).leap === 0;
|
|
}
|
|
|
|
function jalaliToGregorian(
|
|
jy: number,
|
|
jm: number,
|
|
jd: number,
|
|
): { gy: number; gm: number; gd: number } {
|
|
const gy = jalCal(jy).gy;
|
|
const march = jalCal(jy).march;
|
|
const daysInGregorianMonth = [
|
|
31,
|
|
isGregorianLeapYear(gy) ? 29 : 28,
|
|
31,
|
|
30,
|
|
31,
|
|
30,
|
|
31,
|
|
31,
|
|
30,
|
|
31,
|
|
30,
|
|
31,
|
|
];
|
|
|
|
let dayOfYear = jd - 1;
|
|
if (jm <= 6) {
|
|
dayOfYear += (jm - 1) * 31;
|
|
} else {
|
|
dayOfYear += 186 + (jm - 7) * 30;
|
|
}
|
|
|
|
let gd = march + dayOfYear;
|
|
let gm = 3;
|
|
let targetGy = gy;
|
|
|
|
while (gd > daysInGregorianMonth[gm - 1]) {
|
|
gd -= daysInGregorianMonth[gm - 1];
|
|
gm += 1;
|
|
|
|
if (gm > 12) {
|
|
gm = 1;
|
|
targetGy += 1;
|
|
daysInGregorianMonth[1] = isGregorianLeapYear(targetGy) ? 29 : 28;
|
|
}
|
|
}
|
|
|
|
return { gy: targetGy, gm, gd };
|
|
}
|
|
|
|
function jalCal(jy: number): { leap: number; gy: number; march: number } {
|
|
const breaksLength = JALALI_BREAKS.length;
|
|
const gy = jy + 621;
|
|
let leapJ = -14;
|
|
let jp = JALALI_BREAKS[0];
|
|
let jump = 0;
|
|
|
|
if (jy < jp || jy >= JALALI_BREAKS[breaksLength - 1]) {
|
|
throw new Error('Jalali year is out of supported range');
|
|
}
|
|
|
|
for (let i = 1; i < breaksLength; i += 1) {
|
|
const jm = JALALI_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 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 isGregorianLeapYear(year: number): boolean {
|
|
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
}
|
|
|
|
function div(a: number, b: number): number {
|
|
return Math.floor(a / b);
|
|
}
|
|
|
|
function mod(a: number, b: number): number {
|
|
return a - Math.floor(a / b) * b;
|
|
}
|
|
|
|
function pad2(value: number): string {
|
|
return String(value).padStart(2, '0');
|
|
}
|