dateRange.js 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
  2. export function isValidIsoDateYmd(value) {
  3. if (typeof value !== "string") return false;
  4. const s = value.trim();
  5. if (!ISO_DATE_RE.test(s)) return false;
  6. const [y, m, d] = s.split("-").map((x) => Number(x));
  7. if (!Number.isInteger(y) || !Number.isInteger(m) || !Number.isInteger(d)) {
  8. return false;
  9. }
  10. // Keep it predictable (same policy as backend):
  11. // - month: 1..12
  12. // - day: 1..31
  13. // We intentionally do NOT validate month-length precisely (e.g. Feb 30),
  14. // because the backend currently behaves the same way.
  15. if (m < 1 || m > 12) return false;
  16. if (d < 1 || d > 31) return false;
  17. return true;
  18. }
  19. export function normalizeIsoDateYmdOrNull(value) {
  20. if (typeof value !== "string") return null;
  21. const s = value.trim();
  22. if (!s) return null;
  23. return isValidIsoDateYmd(s) ? s : null;
  24. }
  25. /**
  26. * Compare ISO dates (YYYY-MM-DD).
  27. *
  28. * Lexicographic compare is correct for this format.
  29. *
  30. * @param {string} a
  31. * @param {string} b
  32. * @returns {number} -1 | 0 | 1
  33. */
  34. export function compareIsoDatesYmd(a, b) {
  35. const aa = String(a || "");
  36. const bb = String(b || "");
  37. if (aa === bb) return 0;
  38. return aa < bb ? -1 : 1;
  39. }
  40. /**
  41. * Returns true when both dates exist and the range is invalid (from > to).
  42. *
  43. * IMPORTANT:
  44. * - from === to is valid and represents a single day.
  45. */
  46. export function isInvalidIsoDateRange(from, to) {
  47. const f = normalizeIsoDateYmdOrNull(from);
  48. const t = normalizeIsoDateYmdOrNull(to);
  49. if (!f || !t) return false;
  50. return f > t;
  51. }
  52. /**
  53. * Format ISO date (YYYY-MM-DD) as German UI date: DD.MM.YYYY
  54. *
  55. * @param {string|null} ymd
  56. * @returns {string|null}
  57. */
  58. export function formatIsoDateDe(ymd) {
  59. const s = normalizeIsoDateYmdOrNull(ymd);
  60. if (!s) return null;
  61. const [y, m, d] = s.split("-");
  62. return `${d}.${m}.${y}`;
  63. }
  64. /**
  65. * Build a compact German label for the active date filter.
  66. *
  67. * Rules:
  68. * - from + to:
  69. * - same day => "DD.MM.YYYY"
  70. * - range => "DD.MM.YYYY – DD.MM.YYYY"
  71. * - only from => "ab DD.MM.YYYY"
  72. * - only to => "bis DD.MM.YYYY"
  73. * - none => null
  74. *
  75. * @param {{ from?: string|null, to?: string|null }} input
  76. * @returns {string|null}
  77. */
  78. export function formatIsoDateRangeLabelDe({ from = null, to = null } = {}) {
  79. const f = formatIsoDateDe(from);
  80. const t = formatIsoDateDe(to);
  81. if (f && t) {
  82. // Single day
  83. if (normalizeIsoDateYmdOrNull(from) === normalizeIsoDateYmdOrNull(to)) {
  84. return f;
  85. }
  86. return `${f} – ${t}`;
  87. }
  88. if (f) return `ab ${f}`;
  89. if (t) return `bis ${t}`;
  90. return null;
  91. }