refactor formatUnixTimestampToDateTime

Use date-fns style instead of using strftime. This requires changing the i18n keys to the way date-fns represents dates (eg: "MMM d, h:mm:ss aaa"  instead of "%b %-d, %H:%M"
This commit is contained in:
Josh Hawkins 2025-04-22 15:54:21 -05:00
parent 5dda6f8f54
commit 6829843e8d

View File

@ -1,5 +1,6 @@
import strftime from "strftime";
import { fromUnixTime, intervalToDuration, formatDuration } from "date-fns"; import { fromUnixTime, intervalToDuration, formatDuration } from "date-fns";
import { Locale } from "date-fns/locale";
import { formatInTimeZone } from "date-fns-tz";
export const longToDate = (long: number): Date => new Date(long * 1000); export const longToDate = (long: number): Date => new Date(long * 1000);
export const epochToLong = (date: number): number => date / 1000; export const epochToLong = (date: number): number => date / 1000;
export const dateToLong = (date: Date): number => epochToLong(date.getTime()); export const dateToLong = (date: Date): number => epochToLong(date.getTime());
@ -108,11 +109,19 @@ const getResolvedTimeZone = () => {
} }
}; };
type DateTimeStyle = {
timezone?: string;
time_format?: "browser" | "12hour" | "24hour";
date_style?: "full" | "long" | "medium" | "short";
time_style?: "full" | "long" | "medium" | "short";
date_format?: string;
locale?: string | Locale;
};
/** /**
* Formats a Unix timestamp into a human-readable date/time string. * Formats a Unix timestamp into a human-readable date/time string.
* *
* The format of the output string is determined by a configuration object passed as an argument, which * The format of the output string is determined by a configuration object passed as an argument, which
* may specify a time zone, 12- or 24-hour time, and various stylistic options for the date and time. * may specify a time zone, 12- or 24-hour time, various stylistic options for the date and time, and a locale.
* If these options are not specified, the function will use system defaults or sensible fallbacks. * If these options are not specified, the function will use system defaults or sensible fallbacks.
* *
* The function is robust to environments where the Intl API is not fully supported, and includes a * The function is robust to environments where the Intl API is not fully supported, and includes a
@ -126,53 +135,71 @@ const getResolvedTimeZone = () => {
*/ */
export const formatUnixTimestampToDateTime = ( export const formatUnixTimestampToDateTime = (
unixTimestamp: number, unixTimestamp: number,
config: { config: DateTimeStyle = {},
timezone?: string;
time_format?: "browser" | "12hour" | "24hour";
date_style?: "full" | "long" | "medium" | "short";
time_style?: "full" | "long" | "medium" | "short";
strftime_fmt?: string;
},
): string => { ): string => {
const { timezone, time_format, date_style, time_style, strftime_fmt } = const { timezone, time_format, date_style, time_style, date_format, locale } =
config; config;
const locale = window.navigator?.language || "en-US";
// Determine the locale to use
let localeCode: string;
let dateFnsLocale: Locale | undefined;
if (typeof locale === "string") {
localeCode = locale;
} else if (locale && "code" in locale) {
localeCode = (locale as Locale).code || "en-US";
dateFnsLocale = locale as Locale;
} else {
localeCode = window.navigator?.language || "en-US";
}
if (isNaN(unixTimestamp)) { if (isNaN(unixTimestamp)) {
return "Invalid time"; return "Invalid time";
} }
try { try {
const date = new Date(unixTimestamp * 1000); const date = new Date(unixTimestamp * 1000);
const resolvedTimeZone = getResolvedTimeZone();
// use strftime_fmt if defined in config if (date_format) {
if (strftime_fmt) { const resolvedTimeZone = timezone || getResolvedTimeZone();
const offset = getUTCOffset(date, timezone || resolvedTimeZone); let formatted = formatInTimeZone(date, resolvedTimeZone, date_format, {
const strftime_locale = strftime.timezone(offset); locale: dateFnsLocale,
return strftime_locale(strftime_fmt, date); });
// Uppercase AM/PM for 12-hour formats
if (date_format.includes("a") || date_format.includes("aaa")) {
formatted = formatted.replace(/am|pm/gi, (match) =>
match.toUpperCase(),
);
}
return formatted;
} }
// DateTime format options // DateTime format options
const options: Intl.DateTimeFormatOptions = { const options: Intl.DateTimeFormatOptions = {
dateStyle: date_style, dateStyle: date_style,
timeStyle: time_style, timeStyle: time_style,
hour12: time_format !== "browser" ? time_format == "12hour" : undefined, hour12: time_format !== "browser" ? time_format === "12hour" : undefined,
}; };
// Only set timeZone option when resolvedTimeZone does not match UTC±HH:MM format, or when timezone is set in config // Only set timeZone option when resolvedTimeZone does not match UTC±HH:MM format, or when timezone is set in config
const resolvedTimeZone = getResolvedTimeZone();
const isUTCOffsetFormat = /^UTC[+-]\d{2}:\d{2}$/.test(resolvedTimeZone); const isUTCOffsetFormat = /^UTC[+-]\d{2}:\d{2}$/.test(resolvedTimeZone);
if (timezone || !isUTCOffsetFormat) { if (timezone || !isUTCOffsetFormat) {
options.timeZone = timezone || resolvedTimeZone; options.timeZone = timezone || resolvedTimeZone;
} }
const formatter = new Intl.DateTimeFormat(locale, options); const formatter = new Intl.DateTimeFormat(localeCode, options);
const formattedDateTime = formatter.format(date); let formattedDateTime = formatter.format(date);
// Regex to check for existence of time. This is needed because dateStyle/timeStyle is not always supported. if (options.hour12) {
formattedDateTime = formattedDateTime.replace(/am|pm/gi, (match) =>
match.toUpperCase(),
);
}
// Regex to check for existence of time
const containsTime = /\d{1,2}:\d{1,2}/.test(formattedDateTime); const containsTime = /\d{1,2}:\d{1,2}/.test(formattedDateTime);
// fallback if the browser does not support dateStyle/timeStyle in Intl.DateTimeFormat // fallback if the browser does not support dateStyle/timeStyle
// This works even tough the timezone is undefined, it will use the runtime's default time zone
if (!containsTime) { if (!containsTime) {
const dateOptions = { const dateOptions = {
...formatMap[date_style ?? ""]?.date, ...formatMap[date_style ?? ""]?.date,
@ -185,10 +212,17 @@ export const formatUnixTimestampToDateTime = (
hour12: options.hour12, hour12: options.hour12,
}; };
return `${date.toLocaleDateString( let fallbackFormatted = `${date.toLocaleDateString(
locale, localeCode,
dateOptions, dateOptions,
)} ${date.toLocaleTimeString(locale, timeOptions)}`; )} ${date.toLocaleTimeString(localeCode, timeOptions)}`;
// Uppercase AM/PM in fallback
if (options.hour12) {
fallbackFormatted = fallbackFormatted.replace(/am|pm/gi, (match) =>
match.toUpperCase(),
);
}
return fallbackFormatted;
} }
return formattedDateTime; return formattedDateTime;