226 lines
6.5 KiB
TypeScript
Raw Normal View History

2025-04-08 13:27:30 -04:00
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import duration from 'dayjs/plugin/duration';
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(duration);
2025-03-27 20:19:43 +00:00
export const unitHierarchy = [
'years',
'months',
'days',
'hours',
'minutes',
'seconds',
'milliseconds'
] as const;
export type TimeUnit = (typeof unitHierarchy)[number];
export type TimeDifference = Record<TimeUnit, number>;
2025-03-27 10:14:05 -04:00
2025-04-08 13:27:30 -04:00
// Mapping common abbreviations to IANA time zone names
export const tzMap: { [abbr: string]: string } = {
EST: 'America/New_York',
EDT: 'America/New_York',
CST: 'America/Chicago',
CDT: 'America/Chicago',
MST: 'America/Denver',
MDT: 'America/Denver',
PST: 'America/Los_Angeles',
PDT: 'America/Los_Angeles',
GMT: 'Etc/GMT',
UTC: 'Etc/UTC'
// add more mappings as needed
};
// Parse a date string with a time zone abbreviation,
// e.g. "02/02/2024 14:55 EST"
export const parseWithTZ = (dateTimeStr: string): dayjs.Dayjs => {
const parts = dateTimeStr.trim().split(' ');
const tzAbbr = parts.pop()!; // extract the timezone part (e.g., EST)
const dateTimePart = parts.join(' ');
const tzName = tzMap[tzAbbr];
if (!tzName) {
throw new Error(`Timezone abbreviation ${tzAbbr} not supported`);
}
// Parse using the format "MM/DD/YYYY HH:mm" in the given time zone
return dayjs.tz(dateTimePart, 'MM/DD/YYYY HH:mm', tzName);
};
2025-03-27 10:14:05 -04:00
export const calculateTimeBetweenDates = (
startDate: Date,
2025-03-27 20:19:43 +00:00
endDate: Date
): TimeDifference => {
2025-04-08 13:27:30 -04:00
let start = dayjs(startDate);
let end = dayjs(endDate);
// Swap dates if start is after end
if (end.isBefore(start)) {
[start, end] = [end, start];
2025-03-27 10:14:05 -04:00
}
2025-04-08 13:27:30 -04:00
// Calculate each unit incrementally so that the remainder is applied for subsequent units.
const years = end.diff(start, 'year');
const startPlusYears = start.add(years, 'year');
const months = end.diff(startPlusYears, 'month');
const startPlusMonths = startPlusYears.add(months, 'month');
const days = end.diff(startPlusMonths, 'day');
const startPlusDays = startPlusMonths.add(days, 'day');
2025-03-27 10:14:05 -04:00
2025-04-08 13:27:30 -04:00
const hours = end.diff(startPlusDays, 'hour');
const startPlusHours = startPlusDays.add(hours, 'hour');
2025-03-27 10:14:05 -04:00
2025-04-08 13:27:30 -04:00
const minutes = end.diff(startPlusHours, 'minute');
const startPlusMinutes = startPlusHours.add(minutes, 'minute');
const seconds = end.diff(startPlusMinutes, 'second');
const startPlusSeconds = startPlusMinutes.add(seconds, 'second');
const milliseconds = end.diff(startPlusSeconds, 'millisecond');
2025-03-27 10:14:05 -04:00
return {
2025-04-08 13:27:30 -04:00
years,
2025-03-27 10:14:05 -04:00
months,
2025-04-08 13:27:30 -04:00
days,
hours,
minutes,
seconds,
milliseconds
2025-03-27 10:14:05 -04:00
};
};
2025-04-08 13:27:30 -04:00
// Calculate duration between two date strings with timezone abbreviations
export const getDuration = (
startStr: string,
endStr: string
): TimeDifference => {
const start = parseWithTZ(startStr);
const end = parseWithTZ(endStr);
if (end.isBefore(start)) {
throw new Error('End date must be after start date');
}
return calculateTimeBetweenDates(start.toDate(), end.toDate());
};
2025-03-27 10:14:05 -04:00
export const formatTimeDifference = (
difference: TimeDifference,
2025-04-08 13:27:30 -04:00
includeUnits: TimeUnit[] = unitHierarchy.slice(0, -2)
2025-03-27 10:14:05 -04:00
): string => {
2025-04-08 13:27:30 -04:00
// First normalize the values (convert 24 hours to 1 day, etc.)
const normalized = { ...difference };
// Convert milliseconds to seconds
if (normalized.milliseconds >= 1000) {
const additionalSeconds = Math.floor(normalized.milliseconds / 1000);
normalized.seconds += additionalSeconds;
normalized.milliseconds %= 1000;
}
// Convert seconds to minutes
if (normalized.seconds >= 60) {
const additionalMinutes = Math.floor(normalized.seconds / 60);
normalized.minutes += additionalMinutes;
normalized.seconds %= 60;
}
// Convert minutes to hours
if (normalized.minutes >= 60) {
const additionalHours = Math.floor(normalized.minutes / 60);
normalized.hours += additionalHours;
normalized.minutes %= 60;
}
// Convert hours to days if 24 or more
if (normalized.hours >= 24) {
const additionalDays = Math.floor(normalized.hours / 24);
normalized.days += additionalDays;
normalized.hours %= 24;
}
const timeUnits: { key: TimeUnit; value: number; label: string }[] = [
{ key: 'years', value: normalized.years, label: 'year' },
{ key: 'months', value: normalized.months, label: 'month' },
{ key: 'days', value: normalized.days, label: 'day' },
{ key: 'hours', value: normalized.hours, label: 'hour' },
{ key: 'minutes', value: normalized.minutes, label: 'minute' },
{ key: 'seconds', value: normalized.seconds, label: 'second' },
{
key: 'milliseconds',
value: normalized.milliseconds,
label: 'millisecond'
}
2025-03-27 20:19:43 +00:00
];
const parts = timeUnits
.filter(({ key }) => includeUnits.includes(key))
2025-04-08 13:27:30 -04:00
.map(({ value, label }) => {
if (value === 0) return '';
return `${value} ${label}${value === 1 ? '' : 's'}`;
2025-03-27 20:19:43 +00:00
})
.filter(Boolean);
2025-03-27 10:14:05 -04:00
if (parts.length === 0) {
2025-04-08 13:27:30 -04:00
return '0 minutes';
2025-03-27 10:14:05 -04:00
}
return parts.join(', ');
};
export const getTimeWithTimezone = (
dateString: string,
timeString: string,
timezone: string
): Date => {
2025-03-27 20:19:43 +00:00
// If timezone is "local", return the local date
if (timezone === 'local') {
2025-04-08 13:27:30 -04:00
const dateTimeString = `${dateString}T${timeString}`;
return dayjs(dateTimeString).toDate();
2025-03-27 20:19:43 +00:00
}
2025-04-08 13:27:30 -04:00
// Check if the timezone is a known abbreviation
if (tzMap[timezone]) {
const dateTimeString = `${dateString} ${timeString}`;
return dayjs
.tz(dateTimeString, 'YYYY-MM-DD HH:mm', tzMap[timezone])
.toDate();
}
// Handle GMT+/- format
2025-03-27 20:19:43 +00:00
const match = timezone.match(/^GMT(?:([+-]\d{1,2})(?::(\d{2}))?)?$/);
if (!match) {
throw new Error('Invalid timezone format');
}
2025-04-08 13:27:30 -04:00
const dateTimeString = `${dateString}T${timeString}Z`;
const utcDate = dayjs.utc(dateTimeString);
if (!utcDate.isValid()) {
throw new Error('Invalid date or time format');
}
2025-03-27 20:19:43 +00:00
const offsetHours = match[1] ? parseInt(match[1], 10) : 0;
const offsetMinutes = match[2] ? parseInt(match[2], 10) : 0;
const totalOffsetMinutes =
offsetHours * 60 + (offsetHours < 0 ? -offsetMinutes : offsetMinutes);
2025-04-08 13:27:30 -04:00
return utcDate.subtract(totalOffsetMinutes, 'minute').toDate();
2025-03-27 20:19:43 +00:00
};
export const formatTimeWithLargestUnit = (
difference: TimeDifference,
largestUnit: TimeUnit
): string => {
const largestUnitIndex = unitHierarchy.indexOf(largestUnit);
2025-04-08 13:27:30 -04:00
const unitsToInclude = unitHierarchy.slice(
largestUnitIndex,
unitHierarchy.length // Include milliseconds if it's the largest unit requested
);
return formatTimeDifference(difference, unitsToInclude);
2025-03-27 10:14:05 -04:00
};