Last active
March 26, 2025 02:18
-
-
Save reloadlife/235a43deef6cea713c87e27358b2fd5b to your computer and use it in GitHub Desktop.
DateFormatter that supports both Jalali and Gregorian
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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