From 97460c018c86c50bd8252ba40b8ff8f701925a6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20Kr=C4=8Dek?= Date: Tue, 18 Nov 2025 13:32:55 +0100 Subject: [PATCH] Fix date issues --- src/lib/components/wizard/StepGenerate.svelte | 50 +++----------- .../components/wizard/StepRowFilter.svelte | 6 +- src/lib/utils/date.ts | 68 +++++++++++++++++++ 3 files changed, 83 insertions(+), 41 deletions(-) create mode 100644 src/lib/utils/date.ts diff --git a/src/lib/components/wizard/StepGenerate.svelte b/src/lib/components/wizard/StepGenerate.svelte index c09765f..7309a6f 100644 --- a/src/lib/components/wizard/StepGenerate.svelte +++ b/src/lib/components/wizard/StepGenerate.svelte @@ -201,52 +201,22 @@ }); } - // Format a date-like string into "DD MM YY" with basic heuristics for common inputs + // Format a YYYY-MM-DD string into "DD MM YY" function formatDateDDMMYY(value: string): string { if (!value) return ''; const trimmed = value.trim(); - // ISO: YYYY-MM-DD or YYYY/MM/DD - let m = trimmed.match(/^(\d{4})[\/\-](\d{2})[\/\-](\d{2})$/); - if (m) { - const [, y, mo, d] = m; - return `${d} ${mo} ${y.slice(-2)}`; - } - - // DMY with separators: DD.MM.YYYY or DD/MM/YYYY or DD-MM-YYYY - m = trimmed.match(/^(\d{2})[.\/-](\d{2})[.\/-](\d{4})$/); - if (m) { - const [, d, mo, y] = m; - return `${d} ${mo} ${y.slice(-2)}`; - } - - // DMY two-digit year: DD.MM.YY or DD/MM/YY or DD-MM-YY - m = trimmed.match(/^(\d{2})[.\/-](\d{2})[.\/-](\d{2})$/); - if (m) { - const [, d, mo, y2] = m; - return `${d} ${mo} ${y2}`; - } - - // Try native Date parsing as a last resort - const parsed = new Date(trimmed); - if (!isNaN(parsed.getTime())) { - const dd = String(parsed.getDate()).padStart(2, '0'); - const mm = String(parsed.getMonth() + 1).padStart(2, '0'); - const yy = String(parsed.getFullYear()).slice(-2); - return `${dd} ${mm} ${yy}`; - } - - // Handle plain numeric serials (e.g., Google/Excel serial days since 1899-12-30) - if (/^\d{5,}$/.test(trimmed)) { - const days = parseInt(trimmed, 10); - const base = new Date(Date.UTC(1899, 11, 30)); - base.setUTCDate(base.getUTCDate() + days); - const dd = String(base.getUTCDate()).padStart(2, '0'); - const mm = String(base.getUTCMonth() + 1).padStart(2, '0'); - const yy = String(base.getUTCFullYear()).slice(-2); - return `${dd} ${mm} ${yy}`; + // Expects YYYY-MM-DD and splits it + const parts = trimmed.split('-'); + if (parts.length === 3) { + const [y, mo, d] = parts; + if (y.length === 4 && mo.length === 2 && d.length === 2) { + return `${d} ${mo} ${y.slice(-2)}`; + } } + // Fallback for any other format that might slip through + console.warn(`Unexpected date format received in formatDateDDMMYY: "${trimmed}"`); return trimmed; } diff --git a/src/lib/components/wizard/StepRowFilter.svelte b/src/lib/components/wizard/StepRowFilter.svelte index 8a6d9ce..56703ef 100644 --- a/src/lib/components/wizard/StepRowFilter.svelte +++ b/src/lib/components/wizard/StepRowFilter.svelte @@ -10,6 +10,7 @@ import { v4 as uuid } from 'uuid'; import Navigator from './subcomponents/Navigator.svelte'; import { onMount } from 'svelte'; + import { parseAndFormatDate } from '$lib/utils/date'; let isLoading = $state(true); let error = $state(null); @@ -53,6 +54,9 @@ const name = mapping.name !== -1 ? row[mapping.name] || '' : ''; const pictureUrl = mapping.pictureUrl !== -1 ? row[mapping.pictureUrl] || '' : ''; + const birthdayRaw = mapping.birthday !== -1 ? row[mapping.birthday] : ''; + const birthday = parseAndFormatDate(birthdayRaw); + if (!name && !pictureUrl) { return null; // Skip entirely empty rows } @@ -68,7 +72,7 @@ id: uuid(), name, nationality: mapping.nationality !== -1 ? row[mapping.nationality] || '' : '', - birthday: mapping.birthday !== -1 ? row[mapping.birthday] || '' : '', + birthday, pictureUrl, alreadyPrinted, _rowIndex: index + 1, diff --git a/src/lib/utils/date.ts b/src/lib/utils/date.ts new file mode 100644 index 0000000..34700ef --- /dev/null +++ b/src/lib/utils/date.ts @@ -0,0 +1,68 @@ +/** + * 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 +}