resultsSorting.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. export const SEARCH_RESULTS_SORT = Object.freeze({
  2. RELEVANCE: "relevance",
  3. DATE_DESC: "date_desc",
  4. BRANCH_ASC: "branch_asc",
  5. });
  6. function pad2(value) {
  7. return String(value || "").padStart(2, "0");
  8. }
  9. /**
  10. * Extract a comparable branch number from ids like "NL01", "NL200".
  11. *
  12. * Why this exists:
  13. * - A lexicographic compare would sort "NL10" before "NL2" (wrong).
  14. * - We want deterministic ordering that matches how humans read branch ids.
  15. *
  16. * @param {any} branchId
  17. * @returns {number|null}
  18. */
  19. function toBranchNumber(branchId) {
  20. const raw = String(branchId || "").trim();
  21. const match = /^NL(\d+)$/i.exec(raw);
  22. if (!match) return null;
  23. const n = Number(match[1]);
  24. return Number.isInteger(n) ? n : null;
  25. }
  26. /**
  27. * Compare two branch ids.
  28. *
  29. * Policy:
  30. * - If both look like NL<number>, compare numerically (NL2 < NL10).
  31. * - If only one looks like NL<number>, prefer the valid one first.
  32. * - Otherwise fall back to a stable lexicographic compare.
  33. *
  34. * @param {any} a
  35. * @param {any} b
  36. * @returns {number}
  37. */
  38. function compareBranchIds(a, b) {
  39. const aa = String(a || "");
  40. const bb = String(b || "");
  41. const na = toBranchNumber(aa);
  42. const nb = toBranchNumber(bb);
  43. if (na !== null && nb !== null) return na - nb;
  44. if (na !== null && nb === null) return -1;
  45. if (na === null && nb !== null) return 1;
  46. return aa.localeCompare(bb, "en");
  47. }
  48. /**
  49. * Build an ISO-like date key (YYYY-MM-DD) from a search item.
  50. *
  51. * Note:
  52. * - The backend guarantees year/month/day for search items.
  53. * - We still keep this defensive to avoid runtime crashes on malformed items.
  54. *
  55. * @param {any} item
  56. * @returns {string}
  57. */
  58. export function toSearchItemIsoDateKey(item) {
  59. const y = String(item?.year || "");
  60. const m = pad2(item?.month);
  61. const d = pad2(item?.day);
  62. return `${y}-${m}-${d}`;
  63. }
  64. /**
  65. * Format the search item date as German UI string: DD.MM.YYYY
  66. *
  67. * Important:
  68. * - This is user-facing output (German).
  69. * - We still keep it pure/testable and independent from UI frameworks.
  70. *
  71. * @param {any} item
  72. * @returns {string}
  73. */
  74. export function formatSearchItemDateDe(item) {
  75. const y = String(item?.year || "");
  76. const m = pad2(item?.month);
  77. const d = pad2(item?.day);
  78. return `${d}.${m}.${y}`;
  79. }
  80. /**
  81. * Sort search items according to the selected sort mode.
  82. *
  83. * UX/API policy:
  84. * - RELEVANCE:
  85. * Keep backend order (assumed relevance-ranked). We still return a shallow copy
  86. * to avoid accidental mutations by callers.
  87. *
  88. * - DATE_DESC:
  89. * Newest date first.
  90. * Tie-breakers: branch (stable, numeric for NLxx), then filename asc.
  91. *
  92. * - BRANCH_ASC:
  93. * Branch ascending (NL01..NLxx), then newest date first, then filename asc.
  94. * This keeps multi/all searches easy to scan without introducing UI grouping headers.
  95. *
  96. * @param {any[]} items
  97. * @param {"relevance"|"date_desc"|"branch_asc"|string} sortMode
  98. * @returns {any[]}
  99. */
  100. export function sortSearchItems(items, sortMode) {
  101. const arr = Array.isArray(items) ? [...items] : [];
  102. if (sortMode === SEARCH_RESULTS_SORT.RELEVANCE) return arr;
  103. if (sortMode === SEARCH_RESULTS_SORT.DATE_DESC) {
  104. return arr.sort((a, b) => {
  105. const da = toSearchItemIsoDateKey(a);
  106. const db = toSearchItemIsoDateKey(b);
  107. // Newest first
  108. if (da !== db) return da < db ? 1 : -1;
  109. // Stable tie-breakers
  110. const ba = a?.branch;
  111. const bb = b?.branch;
  112. const branchCmp = compareBranchIds(ba, bb);
  113. if (branchCmp !== 0) return branchCmp;
  114. const fa = String(a?.filename || "");
  115. const fb = String(b?.filename || "");
  116. return fa.localeCompare(fb, "de");
  117. });
  118. }
  119. if (sortMode === SEARCH_RESULTS_SORT.BRANCH_ASC) {
  120. return arr.sort((a, b) => {
  121. const ba = a?.branch;
  122. const bb = b?.branch;
  123. const branchCmp = compareBranchIds(ba, bb);
  124. if (branchCmp !== 0) return branchCmp;
  125. // Within the same branch: newest date first
  126. const da = toSearchItemIsoDateKey(a);
  127. const db = toSearchItemIsoDateKey(b);
  128. if (da !== db) return da < db ? 1 : -1;
  129. const fa = String(a?.filename || "");
  130. const fb = String(b?.filename || "");
  131. return fa.localeCompare(fb, "de");
  132. });
  133. }
  134. // Unknown sort mode => fail-safe to backend order.
  135. return arr;
  136. }