mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 07:51:13 -05:00
201 lines
6.1 KiB
TypeScript
201 lines
6.1 KiB
TypeScript
/**
|
|
* Date Utility Functions for Timezone-Agnostic Date Handling
|
|
*
|
|
* This module provides utilities for handling calendar dates (not moments in time)
|
|
* without timezone shifts. All dates are stored as YYYY-MM-DD strings in the database
|
|
* using the DATE type (not TIMESTAMP).
|
|
*
|
|
* Key Principle: Calendar dates like "January 1, 2024" should remain "2024-01-01"
|
|
* regardless of user timezone. We never use UTC conversion for DATE fields.
|
|
*
|
|
* @see docs/DATE_HANDLING.md for full documentation
|
|
*/
|
|
|
|
/**
|
|
* Converts a Date object to YYYY-MM-DD string in LOCAL timezone
|
|
*
|
|
* This prevents timezone shifts where selecting "Jan 1, 2024" could
|
|
* save as "2023-12-31" or "2024-01-02" due to UTC conversion.
|
|
*
|
|
* @param date - Date object to convert
|
|
* @returns YYYY-MM-DD formatted string in local timezone
|
|
*
|
|
* @example
|
|
* // User in UTC-8 selects Jan 1, 2024 11:00 PM
|
|
* const date = new Date('2024-01-01T23:00:00-08:00');
|
|
* toDateOnly(date); // Returns "2024-01-01" ✅ (NOT "2024-01-02")
|
|
*/
|
|
export function toDateOnly(date: Date): string {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
/**
|
|
* Parses a YYYY-MM-DD string to a Date object at midnight local time
|
|
*
|
|
* @param dateString - YYYY-MM-DD formatted string
|
|
* @returns Date object set to midnight local time
|
|
*
|
|
* @example
|
|
* parseDateOnly('2024-01-01'); // Returns Date object for Jan 1, 2024 00:00:00 local
|
|
*/
|
|
export function parseDateOnly(dateString: string): Date {
|
|
const [year, month, day] = dateString.split('-').map(Number);
|
|
return new Date(year, month - 1, day);
|
|
}
|
|
|
|
/**
|
|
* Gets current date as YYYY-MM-DD string in local timezone
|
|
*
|
|
* @returns Current date in YYYY-MM-DD format
|
|
*
|
|
* @example
|
|
* getCurrentDateLocal(); // "2024-01-15"
|
|
*/
|
|
export function getCurrentDateLocal(): string {
|
|
return toDateOnly(new Date());
|
|
}
|
|
|
|
/**
|
|
* Formats a date string for display based on precision
|
|
*
|
|
* @param dateString - YYYY-MM-DD formatted string
|
|
* @param precision - Display precision: 'day', 'month', or 'year'
|
|
* @returns Formatted display string
|
|
*
|
|
* @example
|
|
* formatDateDisplay('2024-01-01', 'year'); // "2024"
|
|
* formatDateDisplay('2024-01-01', 'month'); // "January 2024"
|
|
* formatDateDisplay('2024-01-01', 'day'); // "January 1, 2024"
|
|
*/
|
|
export function formatDateDisplay(
|
|
dateString: string | null | undefined,
|
|
precision: 'day' | 'month' | 'year' = 'day'
|
|
): string {
|
|
if (!dateString) return '';
|
|
|
|
const date = parseDateOnly(dateString);
|
|
|
|
switch (precision) {
|
|
case 'year':
|
|
return date.getFullYear().toString();
|
|
case 'month':
|
|
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
|
|
case 'day':
|
|
default:
|
|
return date.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates YYYY-MM-DD date format
|
|
*
|
|
* @param dateString - String to validate
|
|
* @returns true if valid YYYY-MM-DD format
|
|
*
|
|
* @example
|
|
* isValidDateString('2024-01-01'); // true
|
|
* isValidDateString('01/01/2024'); // false
|
|
* isValidDateString('2024-1-1'); // false
|
|
*/
|
|
export function isValidDateString(dateString: string): boolean {
|
|
return /^\d{4}-\d{2}-\d{2}$/.test(dateString);
|
|
}
|
|
|
|
/**
|
|
* Checks if a date is within a valid range
|
|
*
|
|
* @param dateString - YYYY-MM-DD formatted string
|
|
* @param minYear - Minimum allowed year (default: 1800)
|
|
* @param maxYear - Maximum allowed year (default: current year + 10)
|
|
* @returns true if date is within range
|
|
*/
|
|
export function isDateInRange(
|
|
dateString: string,
|
|
minYear: number = 1800,
|
|
maxYear: number = new Date().getFullYear() + 10
|
|
): boolean {
|
|
if (!isValidDateString(dateString)) return false;
|
|
|
|
const year = parseInt(dateString.split('-')[0]);
|
|
return year >= minYear && year <= maxYear;
|
|
}
|
|
|
|
/**
|
|
* Compares two date strings
|
|
*
|
|
* @param date1 - First date in YYYY-MM-DD format
|
|
* @param date2 - Second date in YYYY-MM-DD format
|
|
* @returns -1 if date1 < date2, 0 if equal, 1 if date1 > date2
|
|
*/
|
|
export function compareDateStrings(date1: string, date2: string): number {
|
|
if (date1 === date2) return 0;
|
|
return date1 < date2 ? -1 : 1;
|
|
}
|
|
|
|
/**
|
|
* Safely parses a date value (string or Date) for display formatting
|
|
* Ensures YYYY-MM-DD strings are interpreted as local dates, not UTC
|
|
*
|
|
* This prevents timezone bugs where "1972-10-01" would display as
|
|
* "September 30, 1972" for users in negative UTC offset timezones.
|
|
*
|
|
* @param date - Date string (YYYY-MM-DD) or Date object
|
|
* @returns Date object in local timezone
|
|
*
|
|
* @example
|
|
* // User in UTC-8 viewing "1972-10-01"
|
|
* parseDateForDisplay("1972-10-01"); // Returns Oct 1, 1972 00:00 PST ✅
|
|
* // NOT Sep 30, 1972 16:00 PST (what new Date() would create)
|
|
*/
|
|
export function parseDateForDisplay(date: string | Date): Date {
|
|
if (date instanceof Date) {
|
|
return date;
|
|
}
|
|
// If it's a YYYY-MM-DD string, use parseDateOnly for local interpretation
|
|
if (typeof date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
return parseDateOnly(date);
|
|
}
|
|
// Fallback for other date strings (timestamps, ISO strings, etc.)
|
|
return new Date(date);
|
|
}
|
|
|
|
/**
|
|
* Creates a date string for a specific precision
|
|
* Sets the date to the first day of the period for month/year precision
|
|
*
|
|
* @param date - Date object
|
|
* @param precision - 'day', 'month', or 'year'
|
|
* @returns YYYY-MM-DD formatted string
|
|
*
|
|
* @example
|
|
* const date = new Date('2024-06-15');
|
|
* toDateWithPrecision(date, 'year'); // "2024-01-01"
|
|
* toDateWithPrecision(date, 'month'); // "2024-06-01"
|
|
* toDateWithPrecision(date, 'day'); // "2024-06-15"
|
|
*/
|
|
export function toDateWithPrecision(
|
|
date: Date,
|
|
precision: 'day' | 'month' | 'year'
|
|
): string {
|
|
const year = date.getFullYear();
|
|
const month = date.getMonth() + 1;
|
|
const day = date.getDate();
|
|
|
|
switch (precision) {
|
|
case 'year':
|
|
return `${year}-01-01`;
|
|
case 'month':
|
|
return `${year}-${String(month).padStart(2, '0')}-01`;
|
|
case 'day':
|
|
default:
|
|
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
}
|
|
}
|