export const SEARCH_RESULTS_SORT = Object.freeze({ RELEVANCE: "relevance", DATE_DESC: "date_desc", BRANCH_ASC: "branch_asc", }); function pad2(value) { return String(value || "").padStart(2, "0"); } /** * Extract a comparable branch number from ids like "NL01", "NL200". * * Why this exists: * - A lexicographic compare would sort "NL10" before "NL2" (wrong). * - We want deterministic ordering that matches how humans read branch ids. * * @param {any} branchId * @returns {number|null} */ function toBranchNumber(branchId) { const raw = String(branchId || "").trim(); const match = /^NL(\d+)$/i.exec(raw); if (!match) return null; const n = Number(match[1]); return Number.isInteger(n) ? n : null; } /** * Compare two branch ids. * * Policy: * - If both look like NL, compare numerically (NL2 < NL10). * - If only one looks like NL, prefer the valid one first. * - Otherwise fall back to a stable lexicographic compare. * * @param {any} a * @param {any} b * @returns {number} */ function compareBranchIds(a, b) { const aa = String(a || ""); const bb = String(b || ""); const na = toBranchNumber(aa); const nb = toBranchNumber(bb); if (na !== null && nb !== null) return na - nb; if (na !== null && nb === null) return -1; if (na === null && nb !== null) return 1; return aa.localeCompare(bb, "en"); } /** * Build an ISO-like date key (YYYY-MM-DD) from a search item. * * Note: * - The backend guarantees year/month/day for search items. * - We still keep this defensive to avoid runtime crashes on malformed items. * * @param {any} item * @returns {string} */ export function toSearchItemIsoDateKey(item) { const y = String(item?.year || ""); const m = pad2(item?.month); const d = pad2(item?.day); return `${y}-${m}-${d}`; } /** * Format the search item date as German UI string: DD.MM.YYYY * * Important: * - This is user-facing output (German). * - We still keep it pure/testable and independent from UI frameworks. * * @param {any} item * @returns {string} */ export function formatSearchItemDateDe(item) { const y = String(item?.year || ""); const m = pad2(item?.month); const d = pad2(item?.day); return `${d}.${m}.${y}`; } /** * Sort search items according to the selected sort mode. * * UX/API policy: * - RELEVANCE: * Keep backend order (assumed relevance-ranked). We still return a shallow copy * to avoid accidental mutations by callers. * * - DATE_DESC: * Newest date first. * Tie-breakers: branch (stable, numeric for NLxx), then filename asc. * * - BRANCH_ASC: * Branch ascending (NL01..NLxx), then newest date first, then filename asc. * This keeps multi/all searches easy to scan without introducing UI grouping headers. * * @param {any[]} items * @param {"relevance"|"date_desc"|"branch_asc"|string} sortMode * @returns {any[]} */ export function sortSearchItems(items, sortMode) { const arr = Array.isArray(items) ? [...items] : []; if (sortMode === SEARCH_RESULTS_SORT.RELEVANCE) return arr; if (sortMode === SEARCH_RESULTS_SORT.DATE_DESC) { return arr.sort((a, b) => { const da = toSearchItemIsoDateKey(a); const db = toSearchItemIsoDateKey(b); // Newest first if (da !== db) return da < db ? 1 : -1; // Stable tie-breakers const ba = a?.branch; const bb = b?.branch; const branchCmp = compareBranchIds(ba, bb); if (branchCmp !== 0) return branchCmp; const fa = String(a?.filename || ""); const fb = String(b?.filename || ""); return fa.localeCompare(fb, "de"); }); } if (sortMode === SEARCH_RESULTS_SORT.BRANCH_ASC) { return arr.sort((a, b) => { const ba = a?.branch; const bb = b?.branch; const branchCmp = compareBranchIds(ba, bb); if (branchCmp !== 0) return branchCmp; // Within the same branch: newest date first const da = toSearchItemIsoDateKey(a); const db = toSearchItemIsoDateKey(b); if (da !== db) return da < db ? 1 : -1; const fa = String(a?.filename || ""); const fb = String(b?.filename || ""); return fa.localeCompare(fb, "de"); }); } // Unknown sort mode => fail-safe to backend order. return arr; }