Files
SkyMoney/web/src/utils/timezone.ts

157 lines
5.2 KiB
TypeScript

/**
* Timezone utility functions for consistent date handling across the application.
*
* All dates should be:
* 1. Stored in the backend as UTC ISO strings
* 2. Displayed to users in their saved timezone
* 3. Input from users interpreted in their saved timezone
*
* The user's timezone is stored in the database and should be fetched from the dashboard.
*/
/**
* Get today's date in the user's timezone as YYYY-MM-DD format.
* This should be used for date inputs to ensure consistency with user's timezone.
*
* @param userTimezone - IANA timezone string (e.g., "America/New_York")
* @returns Date string in YYYY-MM-DD format
*/
export function getTodayInTimezone(userTimezone: string): string {
const now = new Date();
// Use Intl.DateTimeFormat to get the date in the user's timezone
const formatter = new Intl.DateTimeFormat('en-CA', { // en-CA gives YYYY-MM-DD format
timeZone: userTimezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
return formatter.format(now);
}
/**
* Convert a date input string (YYYY-MM-DD) to an ISO string that represents
* midnight in the user's timezone.
*
* This should be used when sending date-only data to the backend.
*
* @param dateString - Date in YYYY-MM-DD format
* @param userTimezone - IANA timezone string
* @returns ISO string representing midnight in the user's timezone
*/
export function dateStringToUTCMidnight(dateString: string, userTimezone: string): string {
// Parse the date string as-is (YYYY-MM-DD)
const [year, month, day] = dateString.split('-').map(Number);
// Create a date object representing midnight in the user's timezone
// We format a string that includes timezone info
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}T00:00:00`;
// Get the date/time string in the user's timezone to calculate offset
const tzDate = new Date(new Date(dateStr).toLocaleString('en-US', { timeZone: userTimezone }));
const utcDate = new Date(new Date(dateStr).toLocaleString('en-US', { timeZone: 'UTC' }));
const offset = tzDate.getTime() - utcDate.getTime();
// Create final date adjusted for timezone
const adjustedDate = new Date(Date.UTC(year, month - 1, day, 0, 0, 0) - offset);
return adjustedDate.toISOString();
}
/**
* Format an ISO date string for display in the user's timezone.
*
* @param isoString - ISO date string from backend
* @param userTimezone - IANA timezone string
* @param options - Intl.DateTimeFormatOptions for formatting
* @returns Formatted date string
*/
export function formatDateInTimezone(
isoString: string,
userTimezone: string,
options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'short', day: 'numeric' }
): string {
const date = new Date(isoString);
return new Intl.DateTimeFormat('en-US', {
...options,
timeZone: userTimezone,
}).format(date);
}
/**
* Convert an ISO string to YYYY-MM-DD format in the user's timezone.
* This is useful for populating date inputs.
*
* @param isoString - ISO date string from backend
* @param userTimezone - IANA timezone string
* @returns Date string in YYYY-MM-DD format
*/
export function isoToDateString(isoString: string, userTimezone: string): string {
const date = new Date(isoString);
const formatter = new Intl.DateTimeFormat('en-CA', {
timeZone: userTimezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
return formatter.format(date);
}
/**
* Get the current date and time as an ISO string in UTC.
* This should be used for timestamps (not date-only fields).
*
* @returns ISO string in UTC
*/
export function getCurrentTimestamp(): string {
return new Date().toISOString();
}
/**
* Compare two date strings (YYYY-MM-DD) to determine if date1 is before date2.
* This is timezone-safe because it compares date strings directly.
*
* @param date1 - First date string
* @param date2 - Second date string
* @returns true if date1 is before date2
*/
export function isDateBefore(date1: string, date2: string): boolean {
return date1 < date2;
}
/**
* Compare two date strings (YYYY-MM-DD) to determine if date1 is after date2.
* This is timezone-safe because it compares date strings directly.
*
* @param date1 - First date string
* @param date2 - Second date string
* @returns true if date1 is after date2
*/
export function isDateAfter(date1: string, date2: string): boolean {
return date1 > date2;
}
/**
* Add days to a date string, accounting for the user's timezone.
*
* @param dateString - Date in YYYY-MM-DD format
* @param days - Number of days to add
* @param userTimezone - IANA timezone string
* @returns New date string in YYYY-MM-DD format
*/
export function addDaysToDate(dateString: string, days: number, userTimezone: string): string {
const baseISO = dateStringToUTCMidnight(dateString, userTimezone);
const base = new Date(baseISO);
base.setUTCDate(base.getUTCDate() + days);
return isoToDateString(base.toISOString(), userTimezone);
}
/**
* Get the user's browser timezone as a fallback.
* This should only be used when the backend timezone is not available.
*
* @returns IANA timezone string
*/
export function getBrowserTimezone(): string {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
}