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 { Locale } from "date-fns/locale";
import { formatInTimeZone } from "date-fns-tz";
export const longToDate = (long: number): Date => new Date(long * 1000);
export const epochToLong = (date: number): number => date / 1000;
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.
*
* 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.
*
* 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 = (
unixTimestamp: number,
config: {
timezone?: string;
time_format?: "browser" | "12hour" | "24hour";
date_style?: "full" | "long" | "medium" | "short";
time_style?: "full" | "long" | "medium" | "short";
strftime_fmt?: string;
},
config: DateTimeStyle = {},
): string => {
const { timezone, time_format, date_style, time_style, strftime_fmt } =
const { timezone, time_format, date_style, time_style, date_format, locale } =
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)) {
return "Invalid time";
}
try {
const date = new Date(unixTimestamp * 1000);
const resolvedTimeZone = getResolvedTimeZone();
// use strftime_fmt if defined in config
if (strftime_fmt) {
const offset = getUTCOffset(date, timezone || resolvedTimeZone);
const strftime_locale = strftime.timezone(offset);
return strftime_locale(strftime_fmt, date);
if (date_format) {
const resolvedTimeZone = timezone || getResolvedTimeZone();
let formatted = formatInTimeZone(date, resolvedTimeZone, date_format, {
locale: dateFnsLocale,
});
// 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
const options: Intl.DateTimeFormatOptions = {
dateStyle: date_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
const resolvedTimeZone = getResolvedTimeZone();
const isUTCOffsetFormat = /^UTC[+-]\d{2}:\d{2}$/.test(resolvedTimeZone);
if (timezone || !isUTCOffsetFormat) {
options.timeZone = timezone || resolvedTimeZone;
}
const formatter = new Intl.DateTimeFormat(locale, options);
const formattedDateTime = formatter.format(date);
const formatter = new Intl.DateTimeFormat(localeCode, options);
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);
// fallback if the browser does not support dateStyle/timeStyle in Intl.DateTimeFormat
// This works even tough the timezone is undefined, it will use the runtime's default time zone
// fallback if the browser does not support dateStyle/timeStyle
if (!containsTime) {
const dateOptions = {
...formatMap[date_style ?? ""]?.date,
@ -185,10 +212,17 @@ export const formatUnixTimestampToDateTime = (
hour12: options.hour12,
};
return `${date.toLocaleDateString(
locale,
let fallbackFormatted = `${date.toLocaleDateString(
localeCode,
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;