const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/; export function isValidIsoDateYmd(value) { if (typeof value !== "string") return false; const s = value.trim(); if (!ISO_DATE_RE.test(s)) return false; const [y, m, d] = s.split("-").map((x) => Number(x)); if (!Number.isInteger(y) || !Number.isInteger(m) || !Number.isInteger(d)) { return false; } // Predictable policy (same as backend): // - month: 1..12 // - day: 1..31 if (m < 1 || m > 12) return false; if (d < 1 || d > 31) return false; return true; } export function normalizeIsoDateYmdOrNull(value) { if (typeof value !== "string") return null; const s = value.trim(); if (!s) return null; return isValidIsoDateYmd(s) ? s : null; } /** * Compare ISO dates (YYYY-MM-DD). * Lexicographic compare is correct for this format. */ export function compareIsoDatesYmd(a, b) { const aa = String(a || ""); const bb = String(b || ""); if (aa === bb) return 0; return aa < bb ? -1 : 1; } /** * Returns true when both dates exist and the range is invalid (from > to). * IMPORTANT: from === to is valid and represents a single day. */ export function isInvalidIsoDateRange(from, to) { const f = normalizeIsoDateYmdOrNull(from); const t = normalizeIsoDateYmdOrNull(to); if (!f || !t) return false; return f > t; } /** * Format ISO date (YYYY-MM-DD) as German UI date: DD.MM.YYYY */ export function formatIsoDateDe(ymd) { const s = normalizeIsoDateYmdOrNull(ymd); if (!s) return null; const [y, m, d] = s.split("-"); return `${d}.${m}.${y}`; } /** * Build a compact German label for the active date filter. */ export function formatIsoDateRangeLabelDe({ from = null, to = null } = {}) { const f = formatIsoDateDe(from); const t = formatIsoDateDe(to); if (f && t) { if (normalizeIsoDateYmdOrNull(from) === normalizeIsoDateYmdOrNull(to)) { return f; } return `${f} – ${t}`; } if (f) return `ab ${f}`; if (t) return `bis ${t}`; return null; } /** * Convert a Date to ISO YYYY-MM-DD using LOCAL calendar values * (avoids timezone drift from toISOString()). */ export function toIsoDateYmdFromDate(date) { if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null; const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, "0"); const d = String(date.getDate()).padStart(2, "0"); return `${y}-${m}-${d}`; } /** * Convert ISO YYYY-MM-DD to a Date (local time). */ export function toDateFromIsoDateYmd(ymd) { const s = normalizeIsoDateYmdOrNull(ymd); if (!s) return null; const [y, m, d] = s.split("-").map((x) => Number(x)); return new Date(y, m - 1, d); }