Refactor date and currency utility functions

- Extract shared detectSystemDateFormat() helper to eliminate
  duplicated system locale detection in date.ts
- Derive SUPPORTED_CURRENCIES from CURRENCY_SYMBOLS map instead
  of maintaining a duplicate hardcoded list
- Consolidate redundant time-parsing regex paths into one
- Use applyTimeFormat() consistently in formatETA()
- Add exhaustive switch default for type safety
This commit is contained in:
AneoPsy
2026-03-03 03:45:12 -10:00
parent 19a1a7584b
commit 15521deb0a
4 changed files with 490 additions and 581 deletions
+4 -28
View File
@@ -31,31 +31,7 @@ export function getCurrencySymbol(currencyCode: string): string {
return CURRENCY_SYMBOLS[currencyCode.toUpperCase()] || currencyCode;
}
export const SUPPORTED_CURRENCIES = [
{ code: 'USD', label: 'USD ($)' },
{ code: 'EUR', label: 'EUR (€)' },
{ code: 'GBP', label: 'GBP (£)' },
{ code: 'CHF', label: 'CHF (Fr.)' },
{ code: 'JPY', label: 'JPY (¥)' },
{ code: 'CNY', label: 'CNY (¥)' },
{ code: 'CAD', label: 'CAD ($)' },
{ code: 'AUD', label: 'AUD ($)' },
{ code: 'INR', label: 'INR (₹)' },
{ code: 'HKD', label: 'HKD (HK$)' },
{ code: 'KRW', label: 'KRW (₩)' },
{ code: 'SEK', label: 'SEK (kr)' },
{ code: 'NOK', label: 'NOK (kr)' },
{ code: 'DKK', label: 'DKK (kr)' },
{ code: 'PLN', label: 'PLN (zł)' },
{ code: 'BRL', label: 'BRL (R$)' },
{ code: 'TWD', label: 'TWD (NT$)' },
{ code: 'SGD', label: 'SGD (S$)' },
{ code: 'NZD', label: 'NZD (NZ$)' },
{ code: 'MXN', label: 'MXN (MX$)' },
{ code: 'CZK', label: 'CZK (Kč)' },
{ code: 'THB', label: 'THB (฿)' },
{ code: 'ZAR', label: 'ZAR (R)' },
{ code: 'TRY', label: 'TRY (₺)' },
{ code: 'RUB', label: 'RUB (₽)' },
{ code: 'HUF', label: 'HUF (Ft)' },
] as const;
export const SUPPORTED_CURRENCIES = Object.entries(CURRENCY_SYMBOLS).map(([code, symbol]) => ({
code,
label: `${code} (${symbol})`,
}));
+54 -121
View File
@@ -13,22 +13,12 @@ export type DateFormat = 'system' | 'us' | 'eu' | 'iso';
* Get the date input placeholder based on format setting.
*/
export function getDatePlaceholder(dateFormat: DateFormat = 'system'): string {
switch (dateFormat) {
case 'us':
return 'MM/DD/YYYY';
case 'eu':
return 'DD/MM/YYYY';
case 'iso':
return 'YYYY-MM-DD';
case 'system':
default: {
// Try to detect system format
const testDate = new Date(2000, 11, 31); // Dec 31, 2000
const formatted = testDate.toLocaleDateString();
if (formatted.startsWith('12')) return 'MM/DD/YYYY';
if (formatted.startsWith('31')) return 'DD/MM/YYYY';
return 'YYYY-MM-DD';
}
const resolved = dateFormat === 'system' ? detectSystemDateFormat() : dateFormat;
switch (resolved) {
case 'us': return 'MM/DD/YYYY';
case 'eu': return 'DD/MM/YYYY';
case 'iso': return 'YYYY-MM-DD';
default: return resolved satisfies never;
}
}
@@ -98,7 +88,6 @@ export function formatTimeInput(date: Date, timeFormat: TimeFormat = 'system'):
* Split a date string by common separators (/, ., -).
*/
function splitDateParts(value: string): string[] | null {
// Try common separators: /, ., -
for (const sep of ['/', '.', '-']) {
const parts = value.split(sep);
if (parts.length === 3) return parts;
@@ -106,6 +95,13 @@ function splitDateParts(value: string): string[] | null {
return null;
}
function detectSystemDateFormat(): 'us' | 'eu' | 'iso' {
const formatted = new Date(2000, 11, 31).toLocaleDateString();
if (formatted.startsWith('12')) return 'us';
if (formatted.startsWith('31')) return 'eu';
return 'iso';
}
/**
* Parse a date string based on format setting.
* Returns null if parsing fails.
@@ -114,77 +110,36 @@ function splitDateParts(value: string): string[] | null {
export function parseDateInput(value: string, dateFormat: DateFormat = 'system'): Date | null {
if (!value) return null;
const parts = splitDateParts(value);
if (!parts) return null;
const resolved = dateFormat === 'system' ? detectSystemDateFormat() : dateFormat;
let day: number, month: number, year: number;
try {
switch (dateFormat) {
case 'us': {
// MM/DD/YYYY (also accepts . and - separators)
const parts = splitDateParts(value);
if (!parts) return null;
month = parseInt(parts[0], 10);
day = parseInt(parts[1], 10);
year = parseInt(parts[2], 10);
break;
}
case 'eu': {
// DD/MM/YYYY (also accepts . and - separators)
const parts = splitDateParts(value);
if (!parts) return null;
day = parseInt(parts[0], 10);
month = parseInt(parts[1], 10);
year = parseInt(parts[2], 10);
break;
}
case 'iso': {
// YYYY-MM-DD (also accepts . and / separators)
const parts = splitDateParts(value);
if (!parts) return null;
year = parseInt(parts[0], 10);
month = parseInt(parts[1], 10);
day = parseInt(parts[2], 10);
break;
}
case 'system':
default: {
// Detect system format and parse accordingly
const testDate = new Date(2000, 11, 31); // Dec 31, 2000
const formatted = testDate.toLocaleDateString();
const parts = splitDateParts(value);
if (parts) {
// Detect format from system locale
if (formatted.startsWith('12')) {
// US format: MM/DD/YYYY
month = parseInt(parts[0], 10);
day = parseInt(parts[1], 10);
year = parseInt(parts[2], 10);
} else if (formatted.startsWith('31')) {
// EU format: DD/MM/YYYY
day = parseInt(parts[0], 10);
month = parseInt(parts[1], 10);
year = parseInt(parts[2], 10);
} else {
// ISO format: YYYY-MM-DD
year = parseInt(parts[0], 10);
month = parseInt(parts[1], 10);
day = parseInt(parts[2], 10);
}
break;
}
return null;
}
}
if (isNaN(day) || isNaN(month) || isNaN(year)) return null;
if (month < 1 || month > 12) return null;
if (day < 1 || day > 31) return null;
if (year < 1900 || year > 2100) return null;
return new Date(year, month - 1, day);
} catch {
return null;
switch (resolved) {
case 'us':
month = parseInt(parts[0], 10);
day = parseInt(parts[1], 10);
year = parseInt(parts[2], 10);
break;
case 'eu':
day = parseInt(parts[0], 10);
month = parseInt(parts[1], 10);
year = parseInt(parts[2], 10);
break;
case 'iso':
year = parseInt(parts[0], 10);
month = parseInt(parts[1], 10);
day = parseInt(parts[2], 10);
break;
}
if (isNaN(day) || isNaN(month) || isNaN(year)) return null;
if (month < 1 || month > 12) return null;
if (day < 1 || day > 31) return null;
if (year < 1900 || year > 2100) return null;
return new Date(year, month - 1, day);
}
/**
@@ -194,41 +149,22 @@ export function parseDateInput(value: string, dateFormat: DateFormat = 'system')
export function parseTimeInput(value: string): { hours: number; minutes: number } | null {
if (!value) return null;
try {
const trimmed = value.trim().toUpperCase();
const trimmed = value.trim();
// Check for 12h format with AM/PM
const ampmMatch = trimmed.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)?$/i);
if (ampmMatch) {
let hours = parseInt(ampmMatch[1], 10);
const minutes = parseInt(ampmMatch[2], 10);
const ampm = ampmMatch[3]?.toUpperCase();
const match = trimmed.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)?$/i);
if (!match) return null;
if (ampm === 'PM' && hours < 12) hours += 12;
if (ampm === 'AM' && hours === 12) hours = 0;
let hours = parseInt(match[1], 10);
const minutes = parseInt(match[2], 10);
const ampm = match[3]?.toUpperCase();
if (hours < 0 || hours > 23) return null;
if (minutes < 0 || minutes > 59) return null;
if (ampm === 'PM' && hours < 12) hours += 12;
if (ampm === 'AM' && hours === 12) hours = 0;
return { hours, minutes };
}
if (hours < 0 || hours > 23) return null;
if (minutes < 0 || minutes > 59) return null;
// Try 24h format HH:MM
const match24 = trimmed.match(/^(\d{1,2}):(\d{2})$/);
if (match24) {
const hours = parseInt(match24[1], 10);
const minutes = parseInt(match24[2], 10);
if (hours < 0 || hours > 23) return null;
if (minutes < 0 || minutes > 59) return null;
return { hours, minutes };
}
return null;
} catch {
return null;
}
return { hours, minutes };
}
/**
@@ -387,21 +323,18 @@ export function formatTimeOnly(
*/
export function formatETA(
remainingMinutes: number,
timeFormat: 'system' | '12h' | '24h' = 'system',
timeFormat: TimeFormat = 'system',
t?: (key: string) => string
): string {
const now = new Date();
const eta = new Date(now.getTime() + remainingMinutes * 60 * 1000);
const today = new Date();
const today = new Date(now);
today.setHours(0, 0, 0, 0);
const etaDay = new Date(eta);
etaDay.setHours(0, 0, 0, 0);
const timeOptions: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit' };
if (timeFormat === '12h') timeOptions.hour12 = true;
else if (timeFormat === '24h') timeOptions.hour12 = false;
const timeOptions = applyTimeFormat({ hour: '2-digit', minute: '2-digit' }, timeFormat);
const timeStr = eta.toLocaleTimeString([], timeOptions);
const dayDiff = Math.floor((etaDay.getTime() - today.getTime()) / 86400000);
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -23,7 +23,7 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-BNe8DBiX.js"></script>
<script type="module" crossorigin src="/assets/index-PuCtc4fr.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D5I4wfky.css">
</head>
<body>