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