luxon-hijri/src/formatHijriDate.ts

84 lines
3.2 KiB
TypeScript

// formatHijriDate.ts
import { DateTime } from 'luxon';
import { hmLong, hmMedium } from './hMonths';
import { hwLong, hwShort, hwNumeric } from './hWeekdays';
import { toGregorian } from './toGregorian';
import type { HijriDate } from './types';
// Token regex: longest tokens first to prevent partial matches.
const TOKEN_RE =
/iYYYY|iYY|iMMMM|iMMM|iMM|iM|iDD|iD|iEEEE|iEEE|iE|ioooo|iooo|HH|H|hh|h|mm|m|ss|s|a|z{1,3}|ZZ|Z/g;
/**
* Format a Hijri date using a token-based format string.
*
* Hijri-specific tokens use the `i` prefix (iYYYY, iMM, iDD, iEEEE, etc.).
* Time and timezone tokens (HH, mm, ss, a, Z, z) are delegated to Luxon via the
* corresponding Gregorian date.
*
* @param hijriDate - the Hijri date to format
* @param format - a format string containing Hijri and/or Luxon tokens
* @returns the formatted date string
* @throws {RangeError} if the Hijri month is outside the 1-12 range
*/
export function formatHijriDate(hijriDate: HijriDate, format: string): string {
if (hijriDate.hm < 1 || hijriDate.hm > 12) {
throw new RangeError(`Hijri month must be 1-12, got ${hijriDate.hm}`);
}
// Lazy Gregorian DateTime, computed at most once per format call,
// only when a token that needs it is encountered.
let _gregDt: DateTime | undefined;
function getGregDt(): DateTime {
if (!_gregDt) {
const greg = toGregorian(hijriDate.hy, hijriDate.hm, hijriDate.hd);
_gregDt = DateTime.fromJSDate(greg, { zone: 'UTC' });
}
return _gregDt;
}
return format.replace(TOKEN_RE, (match): string => {
switch (match) {
case 'iYYYY':
return String(hijriDate.hy).padStart(4, '0');
case 'iYY':
return String(hijriDate.hy % 100).padStart(2, '0');
case 'iMM':
return String(hijriDate.hm).padStart(2, '0');
case 'iM':
return String(hijriDate.hm);
case 'iMMM':
// Non-null: hm is validated 1-12 above; index hm-1 is always 0-11, within array bounds.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return hmMedium[hijriDate.hm - 1]!;
case 'iMMMM':
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return hmLong[hijriDate.hm - 1]!;
case 'iDD':
return String(hijriDate.hd).padStart(2, '0');
case 'iD':
return String(hijriDate.hd);
case 'iE':
case 'iEEE':
case 'iEEEE': {
// Luxon weekday: 1=Mon … 7=Sun. Modulo 7: Mon=1 … Sat=6, Sun=0.
// hwLong/hwShort/hwNumeric arrays: index 0=Sunday, 1=Monday, … 6=Saturday.
const idx = getGregDt().weekday % 7;
// Non-null: idx is always 0-6 (weekday%7), within all hw* array bounds.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (match === 'iE') return String(hwNumeric[idx]!);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (match === 'iEEE') return hwShort[idx]!;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return hwLong[idx]!;
}
case 'iooo':
case 'ioooo':
return 'AH';
default:
// Delegate time and timezone tokens to Luxon using the Gregorian DateTime.
return getGregDt().toFormat(match);
}
});
}