Fix date issues
All checks were successful
Build Docker image / build (push) Successful in 3m2s
Build Docker image / deploy (push) Successful in 3s
Build Docker image / verify (push) Successful in 43s

This commit is contained in:
Roman Krček
2025-11-18 13:32:55 +01:00
parent 74910e3346
commit 97460c018c
3 changed files with 83 additions and 41 deletions

View File

@@ -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;
}

View File

@@ -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<string | null>(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,

68
src/lib/utils/date.ts Normal file
View File

@@ -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
}