dateRange.js 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  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. // Predictable policy (same as backend):
  11. // - month: 1..12
  12. // - day: 1..31
  13. if (m < 1 || m > 12) return false;
  14. if (d < 1 || d > 31) return false;
  15. return true;
  16. }
  17. export function normalizeIsoDateYmdOrNull(value) {
  18. if (typeof value !== "string") return null;
  19. const s = value.trim();
  20. if (!s) return null;
  21. return isValidIsoDateYmd(s) ? s : null;
  22. }
  23. /**
  24. * Compare ISO dates (YYYY-MM-DD).
  25. * Lexicographic compare is correct for this format.
  26. */
  27. export function compareIsoDatesYmd(a, b) {
  28. const aa = String(a || "");
  29. const bb = String(b || "");
  30. if (aa === bb) return 0;
  31. return aa < bb ? -1 : 1;
  32. }
  33. /**
  34. * Returns true when both dates exist and the range is invalid (from > to).
  35. * IMPORTANT: from === to is valid and represents a single day.
  36. */
  37. export function isInvalidIsoDateRange(from, to) {
  38. const f = normalizeIsoDateYmdOrNull(from);
  39. const t = normalizeIsoDateYmdOrNull(to);
  40. if (!f || !t) return false;
  41. return f > t;
  42. }
  43. /**
  44. * Format ISO date (YYYY-MM-DD) as German UI date: DD.MM.YYYY
  45. */
  46. export function formatIsoDateDe(ymd) {
  47. const s = normalizeIsoDateYmdOrNull(ymd);
  48. if (!s) return null;
  49. const [y, m, d] = s.split("-");
  50. return `${d}.${m}.${y}`;
  51. }
  52. /**
  53. * Build a compact German label for the active date filter.
  54. */
  55. export function formatIsoDateRangeLabelDe({ from = null, to = null } = {}) {
  56. const f = formatIsoDateDe(from);
  57. const t = formatIsoDateDe(to);
  58. if (f && t) {
  59. if (normalizeIsoDateYmdOrNull(from) === normalizeIsoDateYmdOrNull(to)) {
  60. return f;
  61. }
  62. return `${f} – ${t}`;
  63. }
  64. if (f) return `ab ${f}`;
  65. if (t) return `bis ${t}`;
  66. return null;
  67. }
  68. /**
  69. * Convert a Date to ISO YYYY-MM-DD using LOCAL calendar values
  70. * (avoids timezone drift from toISOString()).
  71. */
  72. export function toIsoDateYmdFromDate(date) {
  73. if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null;
  74. const y = date.getFullYear();
  75. const m = String(date.getMonth() + 1).padStart(2, "0");
  76. const d = String(date.getDate()).padStart(2, "0");
  77. return `${y}-${m}-${d}`;
  78. }
  79. /**
  80. * Convert ISO YYYY-MM-DD to a Date (local time).
  81. */
  82. export function toDateFromIsoDateYmd(ymd) {
  83. const s = normalizeIsoDateYmdOrNull(ymd);
  84. if (!s) return null;
  85. const [y, m, d] = s.split("-").map((x) => Number(x));
  86. return new Date(y, m - 1, d);
  87. }