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; } // Keep it predictable (same policy as backend): // - month: 1..12 // - day: 1..31 // We intentionally do NOT validate month-length precisely (e.g. Feb 30), // because the backend currently behaves the same way. 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. * * @param {string} a * @param {string} b * @returns {number} -1 | 0 | 1 */ 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 * * @param {string|null} ymd * @returns {string|null} */ 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. * * Rules: * - from + to: * - same day => "DD.MM.YYYY" * - range => "DD.MM.YYYY – DD.MM.YYYY" * - only from => "ab DD.MM.YYYY" * - only to => "bis DD.MM.YYYY" * - none => null * * @param {{ from?: string|null, to?: string|null }} input * @returns {string|null} */ export function formatIsoDateRangeLabelDe({ from = null, to = null } = {}) { const f = formatIsoDateDe(from); const t = formatIsoDateDe(to); if (f && t) { // Single day if (normalizeIsoDateYmdOrNull(from) === normalizeIsoDateYmdOrNull(to)) { return f; } return `${f} – ${t}`; } if (f) return `ab ${f}`; if (t) return `bis ${t}`; return null; }