69 lines
2.6 KiB
TypeScript
69 lines
2.6 KiB
TypeScript
/**
|
|
* Parses a date string from various common formats and returns it in YYYY-MM-DD format.
|
|
* Handles ISO (YYYY-MM-DD), European (DD.MM.YYYY), and US (MM/DD/YYYY) formats,
|
|
* as well as Excel-style serial numbers.
|
|
* @param value The date string to parse.
|
|
* @returns The formatted date string or the original value if parsing fails.
|
|
*/
|
|
export function parseAndFormatDate(value: string | number | undefined): string {
|
|
if (value === undefined || value === null || value === '') return '';
|
|
|
|
const trimmed = value.toString().trim();
|
|
if (!trimmed) return '';
|
|
|
|
let date: Date | null = null;
|
|
|
|
// 1. Try direct parsing (handles ISO 8601 like YYYY-MM-DD)
|
|
const directParse = new Date(trimmed);
|
|
if (!isNaN(directParse.getTime()) && trimmed.match(/^\d{4}/)) {
|
|
date = directParse;
|
|
}
|
|
|
|
// 2. Regex for MM/DD/YYYY or MM.DD.YYYY or MM-DD-YYYY (common in Google Forms)
|
|
if (!date) {
|
|
const mdyMatch = trimmed.match(/^(\d{1,2})[./-](\d{1,2})[./-](\d{2,4})$/);
|
|
if (mdyMatch) {
|
|
const [, m, d, y] = mdyMatch;
|
|
// Basic validation to avoid mixing up DMY and MDY for ambiguous dates like 01/02/2023
|
|
// If the first part is > 12, it's likely a day (DMY), so we'll let the next block handle it.
|
|
if (parseInt(m) <= 12) {
|
|
const year = y.length === 2 ? parseInt(`20${y}`) : parseInt(y);
|
|
date = new Date(year, parseInt(m) - 1, parseInt(d));
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Regex for DD/MM/YYYY or DD.MM.YYYY or DD-MM-YYYY
|
|
if (!date) {
|
|
const dmyMatch = trimmed.match(/^(\d{1,2})[./-](\d{1,2})[./-](\d{2,4})$/);
|
|
if (dmyMatch) {
|
|
const [, d, m, y] = dmyMatch;
|
|
const year = y.length === 2 ? parseInt(`20${y}`) : parseInt(y);
|
|
// Month is 0-indexed in JS
|
|
date = new Date(year, parseInt(m) - 1, parseInt(d));
|
|
}
|
|
}
|
|
|
|
// 4. Handle Excel serial date number (days since 1900-01-01, with Excel's leap year bug)
|
|
if (!date && /^\d{5}$/.test(trimmed)) {
|
|
const serial = parseInt(trimmed, 10);
|
|
// Excel's epoch starts on day 1, which it considers 1900-01-01.
|
|
// JS Date epoch is 1970-01-01.
|
|
// Days between 1900-01-01 and 1970-01-01 is 25569.
|
|
// Excel incorrectly thinks 1900 was a leap year, so we subtract 1 for dates after Feb 1900.
|
|
const excelEpochDiff = serial > 60 ? 25567 : 25568;
|
|
const utcMilliseconds = (serial - excelEpochDiff) * 86400 * 1000;
|
|
date = new Date(utcMilliseconds);
|
|
}
|
|
|
|
// If we have a valid date, format it. Otherwise, return original.
|
|
if (date && !isNaN(date.getTime())) {
|
|
const year = date.getFullYear();
|
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
const day = date.getDate().toString().padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
return trimmed; // Fallback
|
|
}
|