Skip to content

Instantly share code, notes, and snippets.

@reloadlife
Last active March 26, 2025 02:18
Show Gist options
  • Save reloadlife/235a43deef6cea713c87e27358b2fd5b to your computer and use it in GitHub Desktop.
Save reloadlife/235a43deef6cea713c87e27358b2fd5b to your computer and use it in GitHub Desktop.
DateFormatter that supports both Jalali and Gregorian
/**
* Converts a Gregorian date to a Jalali (Persian) date
* @param gy Gregorian year
* @param gm Gregorian month (0-indexed, like JavaScript Date)
* @param gd Gregorian day of month
* @returns [jalaliYear, jalaliMonth, jalaliDay] (month is 0-indexed)
*/
const gregorianToJalali = (
gy: number,
gm: number,
gd: number,
): [number, number, number] => {
// Convert from 0-based month to 1-based month for calculation
const g_month = gm + 1;
// Days accumulated in each month in Gregorian calendar
const g_days_in_month = [
0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334,
];
// Calculate Jalali year
let jy = gy - 621;
// Adjust calculation based on where date falls in Gregorian calendar
const leap = (gy % 4 === 0 && gy % 100 !== 0) || gy % 400 === 0;
const march_day =
g_month > 2 || (g_month === 2 && gd >= (leap ? 20 : 19)) ? 1 : 0;
jy -= g_month < 3 ? 1 : 0;
// Calculate day of year in Gregorian calendar
let g_day_of_year = g_days_in_month[g_month - 1]! + gd;
if (leap && g_month > 2) {
g_day_of_year++;
}
// Calculate day of year in Jalali calendar
let j_day_of_year = g_day_of_year - (march_day === 1 ? 79 : leap ? 11 : 10);
if (j_day_of_year < 1) {
// Date is in previous Jalali year
const j_leap = isJalaliLeapYear(jy - 1);
j_day_of_year += j_leap ? 366 : 365;
}
// Determine Jalali month and day
let jm = 0;
let jd = 0;
if (j_day_of_year <= 186) {
// First 6 months are 31 days each
jm = Math.ceil(j_day_of_year / 31);
jd = j_day_of_year - (jm - 1) * 31;
} else {
// Remaining months are 30 days each (except the last one which can be 29 or 30)
jm = 6 + Math.ceil((j_day_of_year - 186) / 30);
jd = j_day_of_year - 186 - (jm - 7) * 30;
}
// Return Jalali date with 0-based month to match JavaScript conventions
return [jy, jm - 1, jd];
};
/**
* Determines if a Jalali year is a leap year
* @param jy Jalali year
* @returns true if leap year, false otherwise
*/
const isJalaliLeapYear = (jy: number): boolean => {
// Algorithm to determine Jalali leap years
const breaks = [
-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210, 1635, 2060, 2097,
2192, 2262, 2324, 2394, 2456,
];
const bl = breaks.length;
let jp = breaks[0]!;
if (jy < jp || jy >= breaks[bl - 1]!) {
throw new Error("Invalid Jalali year");
}
let jump = 0;
for (let i = 1; i < bl; i++) {
const curr = breaks[i]!;
jump = curr - jp;
if (jy < curr) {
break;
}
jp = curr;
}
jy -= jp;
// Check leap year based on 33-year cycle with 8 leap years
return jump - (jy % 33) > 4 || jump - (jy % 33) === 4
? (jy % 33) % 4 === 0
: (jy % 33) % 4 === 3;
};
/**
* Formats a date according to the specified format string in either Gregorian or Jalali calendar.
* Supports:
* - MMMM: Full month name (e.g., "March" or "فروردین")
* - MMM: Short month name (e.g., "Mar" or "فرو")
* - MM: Zero-padded month (e.g., "03")
* - M: Month number (e.g., "3")
* - Do: Day with ordinal suffix (e.g., "26th" or "۲۶ام")
* - DD: Zero-padded day (e.g., "26")
* - D: Day number (e.g., "26")
* - YYYY: Full year (e.g., "2025" or "۱۴۰۴")
* - YY: Two-digit year (e.g., "25" or "۰۴")
* - jMMMM, jMMM, jMM, jM: Jalali month formats
* - jDo, jDD, jD: Jalali day formats
* - jYYYY, jYY: Jalali year formats
*
* @param date - The date to format
* @param format - The format string
* @param useJalali - Whether to use Jalali calendar (default: false)
* @param usePersianDigits - Whether to use Persian digits (default: false)
* @returns The formatted date string
*/
export function FormatDate(
date: Date,
format: string,
useJalali = false,
usePersianDigits = false,
): string {
const day: number = date.getDate();
const month: number = date.getMonth();
const year: number = date.getFullYear();
const monthNames: string[] = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const monthShortNames: string[] = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
const jalaliMonthNames: string[] = [
"فروردین",
"اردیبهشت",
"خرداد",
"تیر",
"مرداد",
"شهریور",
"مهر",
"آبان",
"آذر",
"دی",
"بهمن",
"اسفند",
];
const jalaliMonthShortNames: string[] = [
"فرو",
"ارد",
"خرد",
"تیر",
"مرد",
"شهر",
"مهر",
"آبا",
"آذر",
"دی",
"بهم",
"اسف",
];
const getOrdinalSuffix = (num: number): string => {
const j: number = num % 10;
const k: number = num % 100;
if (j === 1 && k !== 11) {
return num + "st";
}
if (j === 2 && k !== 12) {
return num + "nd";
}
if (j === 3 && k !== 13) {
return num + "rd";
}
return num + "th";
};
const getPersianOrdinalSuffix = (num: number): string => {
const persianNum = toPersianNumeral(num);
return persianNum + "ام";
};
const toPersianNumeral = (num: number): string => {
if (!usePersianDigits) return num.toString();
const persianDigits = [
"۰",
"۱",
"۲",
"۳",
"۴",
"۵",
"۶",
"۷",
"۸",
"۹",
] as const;
return num
.toString()
.replace(/\d/g, (digit) => persianDigits[parseInt(digit)] ?? "");
};
let result: string = format;
// Get Jalali date components if needed
const [jYear, jMonth, jDay] = gregorianToJalali(year, month, day);
// Handle Jalali format tokens
if (format.includes("j")) {
// Jalali year formats
result = result.replace(
/jYYYY/g,
usePersianDigits ? toPersianNumeral(jYear) : jYear.toString(),
);
result = result.replace(
/jYY/g,
usePersianDigits
? toPersianNumeral(jYear % 100)
: (jYear % 100).toString().padStart(2, "0"),
);
// Jalali month formats
result = result.replace(/jMMMM/g, jalaliMonthNames[jMonth] ?? "");
result = result.replace(/jMMM/g, jalaliMonthShortNames[jMonth] ?? "");
result = result.replace(
/jMM/g,
usePersianDigits
? toPersianNumeral(jMonth + 1)
: (jMonth + 1).toString().padStart(2, "0"),
);
result = result.replace(
/jM(?!M)/g,
usePersianDigits ? toPersianNumeral(jMonth + 1) : (jMonth + 1).toString(),
);
// Jalali day formats
result = result.replace(
/jDo/g,
usePersianDigits ? getPersianOrdinalSuffix(jDay) : getOrdinalSuffix(jDay),
);
result = result.replace(
/jDD/g,
usePersianDigits
? toPersianNumeral(jDay)
: jDay.toString().padStart(2, "0"),
);
result = result.replace(
/jD(?!o)/g,
usePersianDigits ? toPersianNumeral(jDay) : jDay.toString(),
);
}
// Handle Gregorian format tokens (only if not in full Jalali mode or when explicit Gregorian tokens exist)
if (!useJalali || !format.startsWith("j")) {
result = result.replace(/MMMM/g, monthNames[month] ?? "");
result = result.replace(/MMM/g, monthShortNames[month] ?? "");
result = result.replace(
/MM/g,
usePersianDigits
? toPersianNumeral(month + 1)
: (month + 1).toString().padStart(2, "0"),
);
result = result.replace(
/\bM(?![M\w])/g,
usePersianDigits ? toPersianNumeral(month + 1) : (month + 1).toString(),
);
result = result.replace(
/Do/g,
usePersianDigits ? getPersianOrdinalSuffix(day) : getOrdinalSuffix(day),
);
result = result.replace(
/DD/g,
usePersianDigits
? toPersianNumeral(day)
: day.toString().padStart(2, "0"),
);
result = result.replace(
/D(?!o)/g,
usePersianDigits ? toPersianNumeral(day) : day.toString(),
);
result = result.replace(
/YYYY/g,
usePersianDigits ? toPersianNumeral(year) : year.toString(),
);
result = result.replace(
/YY(?!YY)/g,
usePersianDigits
? toPersianNumeral(parseInt(year.toString().slice(-2)))
: year.toString().slice(-2),
);
}
return result;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment