Skip to content

Instantly share code, notes, and snippets.

@reloadlife
Last active March 26, 2025 02:18
  • Select an option

Select an option

Revisions

  1. reloadlife revised this gist Mar 26, 2025. 1 changed file with 152 additions and 79 deletions.
    231 changes: 152 additions & 79 deletions format.ts
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,102 @@

    /**
    * 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:
    @@ -131,78 +229,21 @@ export function FormatDate(
    .replace(/\d/g, (digit) => persianDigits[parseInt(digit)] ?? "");
    };

    const gregorianToJalali = (
    gy: number,
    gm: number,
    gd: number,
    ): [number, number, number] => {
    const gdm: readonly number[] = [
    0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334,
    ];

    const jd =
    1721425.5 +
    365 * (gy - 1) +
    Math.floor((gy - 1) / 4) -
    Math.floor((gy - 1) / 100) +
    Math.floor((gy - 1) / 400) +
    Math.floor(
    gdm[gm]! +
    gd +
    (gm > 1 && ((gy % 4 === 0 && gy % 100 !== 0) || gy % 400 === 0)
    ? 1
    : 0),
    );

    const depoch = jd - 1948320.5;
    const cycle = Math.floor(depoch / 1029983);
    const cyear = depoch % 1029983;
    let ycycle: number;

    if (cyear === 1029982) {
    ycycle = 2820;
    } else {
    const aux1 = Math.floor(cyear / 366);
    const aux2 = cyear % 366;
    ycycle =
    Math.floor((2134 * aux1 + 2816 * aux2 + 2815) / 1028522) + aux1 + 1;
    }

    let jy = ycycle + 2820 * cycle + 474;

    if (jy <= 0) {
    jy--;
    }

    const jdn = Math.floor(jd) + 0.5;
    const depoch2 = jdn - 2121445.5;
    const dayInYear =
    depoch2 - 365 * (jy - 474) - Math.floor((jy - 474) / 4) + 1;

    let jm: number;
    let jd2: number;

    if (dayInYear <= 186) {
    jm = Math.ceil(dayInYear / 31);
    jd2 = ((dayInYear - 1) % 31) + 1;
    } else {
    jm = Math.ceil((dayInYear - 6) / 30);
    jd2 = ((dayInYear - 6 - 1) % 30) + 1;
    }
    let result: string = format;

    return [jy, jm - 1, jd2];
    };
    // Get Jalali date components if needed
    const [jYear, jMonth, jDay] = gregorianToJalali(year, month, day);

    let result: string = format;
    if (useJalali || format.includes("j")) {
    const [jYear, jMonth, jDay] = gregorianToJalali(year, month, day);
    // Handle Jalali format tokens
    if (format.includes("j")) {
    // Jalali year formats
    result = result.replace(
    /jYYYY/g,
    useJalali ? toPersianNumeral(jYear) : jYear.toString(),
    usePersianDigits ? toPersianNumeral(jYear) : jYear.toString(),
    );
    result = result.replace(
    /jYY/g,
    useJalali
    usePersianDigits
    ? toPersianNumeral(jYear % 100)
    : (jYear % 100).toString().padStart(2, "0"),
    );
    @@ -212,39 +253,71 @@ export function FormatDate(
    result = result.replace(/jMMM/g, jalaliMonthShortNames[jMonth] ?? "");
    result = result.replace(
    /jMM/g,
    useJalali
    usePersianDigits
    ? toPersianNumeral(jMonth + 1)
    : (jMonth + 1).toString().padStart(2, "0"),
    );
    result = result.replace(
    /jM(?!M)/g,
    useJalali ? toPersianNumeral(jMonth + 1) : (jMonth + 1).toString(),
    usePersianDigits ? toPersianNumeral(jMonth + 1) : (jMonth + 1).toString(),
    );

    // Jalali day formats
    result = result.replace(
    /jDo/g,
    useJalali ? getPersianOrdinalSuffix(jDay) : getOrdinalSuffix(jDay),
    usePersianDigits ? getPersianOrdinalSuffix(jDay) : getOrdinalSuffix(jDay),
    );
    result = result.replace(
    /jDD/g,
    useJalali ? toPersianNumeral(jDay) : jDay.toString().padStart(2, "0"),
    usePersianDigits
    ? toPersianNumeral(jDay)
    : jDay.toString().padStart(2, "0"),
    );
    result = result.replace(
    /jD(?!o)/g,
    useJalali ? toPersianNumeral(jDay) : jDay.toString(),
    usePersianDigits ? toPersianNumeral(jDay) : jDay.toString(),
    );
    }

    result = result.replace(/MMMM/g, monthNames[month] ?? "");
    result = result.replace(/MMM/g, monthShortNames[month] ?? "");
    result = result.replace(/MM/g, (month + 1).toString().padStart(2, "0"));
    result = result.replace(/M(?!M)/g, (month + 1).toString());
    result = result.replace(/Do/g, getOrdinalSuffix(day));
    result = result.replace(/DD/g, day.toString().padStart(2, "0"));
    result = result.replace(/D(?!o)/g, day.toString());
    result = result.replace(/YYYY/g, year.toString());
    result = result.replace(/YY(?!YY)/g, year.toString().slice(-2));
    // 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;
    }
  2. reloadlife revised this gist Mar 25, 2025. No changes.
  3. reloadlife renamed this gist Mar 25, 2025. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  4. reloadlife created this gist Mar 25, 2025.
    250 changes: 250 additions & 0 deletions DateFormatter that supports both Jalali and Gregorian.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,250 @@

    /**
    * 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)] ?? "");
    };

    const gregorianToJalali = (
    gy: number,
    gm: number,
    gd: number,
    ): [number, number, number] => {
    const gdm: readonly number[] = [
    0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334,
    ];

    const jd =
    1721425.5 +
    365 * (gy - 1) +
    Math.floor((gy - 1) / 4) -
    Math.floor((gy - 1) / 100) +
    Math.floor((gy - 1) / 400) +
    Math.floor(
    gdm[gm]! +
    gd +
    (gm > 1 && ((gy % 4 === 0 && gy % 100 !== 0) || gy % 400 === 0)
    ? 1
    : 0),
    );

    const depoch = jd - 1948320.5;
    const cycle = Math.floor(depoch / 1029983);
    const cyear = depoch % 1029983;
    let ycycle: number;

    if (cyear === 1029982) {
    ycycle = 2820;
    } else {
    const aux1 = Math.floor(cyear / 366);
    const aux2 = cyear % 366;
    ycycle =
    Math.floor((2134 * aux1 + 2816 * aux2 + 2815) / 1028522) + aux1 + 1;
    }

    let jy = ycycle + 2820 * cycle + 474;

    if (jy <= 0) {
    jy--;
    }

    const jdn = Math.floor(jd) + 0.5;
    const depoch2 = jdn - 2121445.5;
    const dayInYear =
    depoch2 - 365 * (jy - 474) - Math.floor((jy - 474) / 4) + 1;

    let jm: number;
    let jd2: number;

    if (dayInYear <= 186) {
    jm = Math.ceil(dayInYear / 31);
    jd2 = ((dayInYear - 1) % 31) + 1;
    } else {
    jm = Math.ceil((dayInYear - 6) / 30);
    jd2 = ((dayInYear - 6 - 1) % 30) + 1;
    }

    return [jy, jm - 1, jd2];
    };

    let result: string = format;
    if (useJalali || format.includes("j")) {
    const [jYear, jMonth, jDay] = gregorianToJalali(year, month, day);
    result = result.replace(
    /jYYYY/g,
    useJalali ? toPersianNumeral(jYear) : jYear.toString(),
    );
    result = result.replace(
    /jYY/g,
    useJalali
    ? 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,
    useJalali
    ? toPersianNumeral(jMonth + 1)
    : (jMonth + 1).toString().padStart(2, "0"),
    );
    result = result.replace(
    /jM(?!M)/g,
    useJalali ? toPersianNumeral(jMonth + 1) : (jMonth + 1).toString(),
    );

    // Jalali day formats
    result = result.replace(
    /jDo/g,
    useJalali ? getPersianOrdinalSuffix(jDay) : getOrdinalSuffix(jDay),
    );
    result = result.replace(
    /jDD/g,
    useJalali ? toPersianNumeral(jDay) : jDay.toString().padStart(2, "0"),
    );
    result = result.replace(
    /jD(?!o)/g,
    useJalali ? toPersianNumeral(jDay) : jDay.toString(),
    );
    }

    result = result.replace(/MMMM/g, monthNames[month] ?? "");
    result = result.replace(/MMM/g, monthShortNames[month] ?? "");
    result = result.replace(/MM/g, (month + 1).toString().padStart(2, "0"));
    result = result.replace(/M(?!M)/g, (month + 1).toString());
    result = result.replace(/Do/g, getOrdinalSuffix(day));
    result = result.replace(/DD/g, day.toString().padStart(2, "0"));
    result = result.replace(/D(?!o)/g, day.toString());
    result = result.replace(/YYYY/g, year.toString());
    result = result.replace(/YY(?!YY)/g, year.toString().slice(-2));

    return result;
    }