Skip to content

Instantly share code, notes, and snippets.

@alexcarpenter
Last active September 11, 2025 21:12
Show Gist options
  • Save alexcarpenter/40cd14a382683f6b3ed4047e892495ec to your computer and use it in GitHub Desktop.
Save alexcarpenter/40cd14a382683f6b3ed4047e892495ec to your computer and use it in GitHub Desktop.
import {
format as dateFnsFormat,
formatDistanceToNow,
isToday,
isValid,
isYesterday,
parseISO,
} from 'date-fns';
/**
* Standardized date formatting for the entire application.
*/
export type DateFormatOptions =
| {
style: 'display';
relative?: boolean | number;
month?: 'short' | 'long';
year?: boolean | 'auto';
day?: boolean | 'ordinal';
time?: boolean | '12h';
timeSeparator?: string;
}
| {
style: 'iso';
}
| {
style: 'time';
format?: '24h' | '12h' | '24h-seconds' | '12h-seconds';
};
type DateInput = Date | number | string;
const DEFAULT_RELATIVE_THRESHOLD_HOURS = 48;
const TIME_FORMATS = {
'24h': 'HH:mm',
'12h': 'h:mm a',
'24h-seconds': 'HH:mm:ss',
'12h-seconds': 'h:mm:ss a',
} as const;
/**
* Safely parse a date input into a Date object
*/
function parseDate(date: DateInput): Date {
const dateObj = typeof date === 'string' ? parseISO(date) : new Date(date);
if (!isValid(dateObj)) {
throw new Error(`Invalid date provided: ${date}`);
}
return dateObj;
}
/**
* Build format string for display style
*/
function buildDisplayFormat(
options: Extract<DateFormatOptions, { style: 'display' }>,
): string {
const monthFormat = options.month === 'long' ? 'MMMM' : 'MMM';
const showDay = options.day !== false;
const ordinalDay = options.day === 'ordinal';
let formatString = '';
if (showDay) {
const dayFormat = ordinalDay ? 'do' : 'd';
formatString = `${monthFormat} ${dayFormat}`;
} else {
formatString = monthFormat;
}
const shouldShowYear =
options.year === true ||
(options.year !== false && options.year !== 'auto') ||
(options.year === 'auto' &&
new Date().getFullYear() !== new Date().getFullYear());
if (shouldShowYear) {
formatString += ', yyyy';
}
if (options.time) {
const timeFormat = options.time === '12h' ? 'h:mm a' : 'HH:mm';
const separator = options.timeSeparator || 'at';
formatString += ` '${separator}' ${timeFormat}`;
}
return formatString;
}
/**
* Handle relative date formatting
*/
function formatRelativeDate(
dateObj: Date,
options: Extract<DateFormatOptions, { style: 'display' }>,
relative: boolean | number,
): string | null {
const threshold =
typeof relative === 'number' ? relative : DEFAULT_RELATIVE_THRESHOLD_HOURS;
const hoursAgo = (Date.now() - dateObj.getTime()) / (1000 * 60 * 60);
if (hoursAgo >= threshold) {
return null; // Use regular formatting
}
if (isToday(dateObj)) {
if (options.time) {
const timeFormat = options.time === '12h' ? 'h:mm a' : 'HH:mm';
return `today at ${dateFnsFormat(dateObj, timeFormat)}`;
}
return 'today';
}
if (isYesterday(dateObj)) {
if (options.time) {
const timeFormat = options.time === '12h' ? 'h:mm a' : 'HH:mm';
return `yesterday at ${dateFnsFormat(dateObj, timeFormat)}`;
}
return 'yesterday';
}
return formatDistanceToNow(dateObj, { addSuffix: true });
}
/**
* Format a date with flexible modifiers.
*
* @example
* formatDate(date) // "Jan 15, 2024"
* formatDate(date, { style: 'display', month: 'long' }) // "January 15, 2024"
* formatDate(date, { style: 'display', day: 'ordinal' }) // "Jan 15th, 2024"
* formatDate(date, { style: 'iso' }) // "2024-01-15"
* formatDate(date, { style: 'time' }) // "14:30"
*/
export function formatDate(
date: DateInput,
options: DateFormatOptions = { style: 'display' },
): string {
const dateObj = parseDate(date);
switch (options.style) {
case 'iso':
return dateFnsFormat(dateObj, 'yyyy-MM-dd');
case 'time': {
const format = options.format || '24h';
const formatString = TIME_FORMATS[format];
return dateFnsFormat(dateObj, formatString);
}
case 'display': {
if (options.relative) {
const relativeResult = formatRelativeDate(
dateObj,
options,
options.relative,
);
if (relativeResult) {
return relativeResult;
}
}
const formatString = buildDisplayFormat(options);
return dateFnsFormat(dateObj, formatString);
}
default:
options satisfies never;
throw new Error(
`Unexpected date format style: ${(options as any).style}`,
);
}
}
/**
* Format a date as relative time.
* @deprecated Use formatDate with relative option instead
*/
export function formatRelativeTime(
date: DateInput,
options?: { addSuffix?: boolean },
): string {
const dateObj = parseDate(date);
return formatDistanceToNow(dateObj, {
addSuffix: options?.addSuffix ?? true,
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment