Files
ERP/frontend/src/lib/jalali.ts
T

179 lines
6.2 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.
// ============================================================
// ابزار تقویم شمسی — پورت jalali.js به TypeScript
// تبدیل‌ها با الگوریتم استاندارد Kazimierz M. Borkowski
// ============================================================
const FA_DIGITS = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"];
export function div(a: number, b: number): number {
return Math.floor(a / b);
}
export function jalaliToGregorian(jy: number, jm: number, jd: number): [number, number, number] {
jy = +jy;
jm = +jm;
jd = +jd;
let gy = jy <= 979 ? 621 : 1600;
jy -= jy <= 979 ? 0 : 979;
let days =
365 * jy +
div(jy, 33) * 8 +
div((jy % 33) + 3, 4) +
78 +
jd +
(jm < 7 ? (jm - 1) * 31 : (jm - 7) * 30 + 186);
gy += 400 * div(days, 146097);
days %= 146097;
if (days > 36524) {
gy += 100 * div(--days, 36524);
if (days === 36524) days = 0;
days++;
}
gy += 4 * div(days, 1461);
days %= 1461;
if (days > 365) {
gy += div(days - 1, 365);
days = (days - 1) % 365;
}
let gd = days + 1;
const sal_a = [0, 31, (gy % 4 === 0 && gy % 100 !== 0) || gy % 400 === 0 ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let gm = 0;
for (; gm < 13 && gd > sal_a[gm]; gm++) gd -= sal_a[gm];
return [gy, gm, gd];
}
export function gregorianToJalali(gy: number, gm: number, gd: number): [number, number, number] {
gy = +gy;
gm = +gm;
gd = +gd;
const g_d_m = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
let jy = gy <= 1600 ? 0 : 979;
gy -= gy <= 1600 ? 621 : 1600;
let days =
365 * gy +
div(gy + 3, 4) -
div(gy + 99, 100) +
div(gy + 399, 400) -
80 +
gd +
g_d_m[gm - 1];
jy += 33 * div(days, 12053);
days %= 12053;
jy += 4 * div(days, 1461);
days %= 1461;
if (days > 365) {
jy += div(days - 1, 365);
days = (days - 1) % 365;
}
const jm = days < 186 ? 1 + div(days, 31) : 7 + div(days - 186, 30);
const jd = 1 + (days < 186 ? days % 31 : (days - 186) % 30);
return [jy, jm, jd];
}
export function isLeapJalali(jy: number): boolean {
const breaks = [-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210, 1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456, 3178];
const jp = breaks[0];
const jm = breaks[1];
let jump = 0;
let jp2 = jp;
for (const jl of breaks) {
jump = jl - jp2;
if (jy <= jl) break;
jp2 = jl;
}
let n = jy - jm;
if (n < jump) n = n + jump + 1;
return (n % 33) % 4 === 1;
}
// ---------- formatting (Persian digits) ----------
export function toFa(s: string | number): string {
return String(s).replace(/[0-9]/g, (d) => FA_DIGITS[Number(d)]);
}
// ماه‌های شمسی (نسخه‌ی درست‌شده — «مهر» به‌جای «آذر» تکراری)
export const MONTHS = [
"فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور",
"مهر", "آبان", "آذر", "دی", "بهمن", "اسفند",
];
export const WEEKDAYS = ["ش", "ی", "د", "س", "چ", "پ", "ج"];
export function toAsciiDigits(s: string | number | null | undefined): string {
if (s == null) return "";
return String(s)
.replace(/[۰-۹]/g, (d) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(d)))
.replace(/[٠-٩]/g, (d) => String("٠١٢٣٤٥٦٧٨٩".indexOf(d)));
}
// format an ISO (gregorian) string to "۱۴۰۳/۰۵/۱۲"
export function isoToJalaliStr(iso: string | null | undefined): string {
if (!iso) return "";
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "";
const [jy, jm, jd] = gregorianToJalali(d.getFullYear(), d.getMonth() + 1, d.getDate());
return `${toFa(jy)}/${toFa(String(jm).padStart(2, "0"))}/${toFa(String(jd).padStart(2, "0"))}`;
}
// format an ISO string to "۱۴۰۳/۰۵/۱۲ ۱۴:۳۰"
export function isoToJalaliDateTime(iso: string | null | undefined): string {
if (!iso) return "";
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "";
const [jy, jm, jd] = gregorianToJalali(d.getFullYear(), d.getMonth() + 1, d.getDate());
const hh = String(d.getHours()).padStart(2, "0");
const mm = String(d.getMinutes()).padStart(2, "0");
return `${toFa(jy)}/${toFa(String(jm).padStart(2, "0"))}/${toFa(String(jd).padStart(2, "0"))} ${toFa(hh)}:${toFa(mm)}`;
}
// format time only: "۱۴:۳۰"
export function isoToTimeStr(iso: string | null | undefined): string {
if (!iso) return "";
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "";
const hh = String(d.getHours()).padStart(2, "0");
const mm = String(d.getMinutes()).padStart(2, "0");
return `${toFa(hh)}:${toFa(mm)}`;
}
// format with month name: "۱۸ مرداد ۱۴۰۴"
export function isoToJalaliLong(iso: string | null | undefined): string {
if (!iso) return "";
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "";
const [jy, jm, jd] = gregorianToJalali(d.getFullYear(), d.getMonth() + 1, d.getDate());
return `${toFa(jd)} ${MONTHS[jm - 1]} ${toFa(jy)}`;
}
// parse a jalali "yyyy/m/d" or "yyyy/m/d h:m" string to a JS Date (gregorian) ISO string
export function jalaliStrToIso(str: string | null | undefined): string | null {
if (!str) return null;
const s = toAsciiDigits(str).trim();
const m = s.match(/^(1[34]\d{2})[/\-.](\d{1,2})[/\-.](\d{1,2})(?:[ T](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?/);
if (!m) {
const nums = s
.split(/[/\-.]/)
.map((x) => parseInt(x, 10));
if (nums.length < 3 || nums.some((n) => Number.isNaN(n))) return null;
const [jy, jm, jd] = nums;
const [gy, gm, gd] = jalaliToGregorian(jy, jm, jd);
const dt = new Date(gy, gm - 1, gd);
return Number.isNaN(dt.getTime()) ? null : dt.toISOString();
}
const jy = parseInt(m[1], 10);
const jm = parseInt(m[2], 10);
const jd = parseInt(m[3], 10);
const h = m[4] != null ? parseInt(m[4], 10) : 0;
const min = m[5] != null ? parseInt(m[5], 10) : 0;
const sec = m[6] != null ? parseInt(m[6], 10) : 0;
const [gy, gm, gd] = jalaliToGregorian(jy, jm, jd);
const dt = new Date(gy, gm - 1, gd, h, min, sec);
return Number.isNaN(dt.getTime()) ? null : dt.toISOString();
}
export function daysInMonth(jy: number, jm: number): number {
if (jm <= 6) return 31;
if (jm <= 11) return 30;
return isLeapJalali(jy) ? 30 : 29;
}