UNPKG

@1771technologies/lytenyte-pro

Version:

Blazingly fast headless React data grid with 100s of features.

141 lines (140 loc) 5.71 kB
/** * Adds ln__total rows at every dimension level (except the measure/leaf), * plus ln__grand_total rows at the top level, for every distinct leaf. * * Rules: * - Paths are split by "-->" (whitespace around segments is trimmed). * - The last segment is treated as the "leaf" (e.g., measure name). * - For every prefix group, we add a total row by replacing the next segment * with ln__total and then appending the leaf. * Example: A-->B-->C-->Leaf * - A-->B-->ln__total-->Leaf (totals across C within A,B) * - A-->ln__total-->Leaf (totals across B,C within A) * - For each distinct leaf, we add ln__grand_total-->Leaf. * - Output is ordered alphabetically (case-insensitive) within each group, * with ln__total rows placed after non-total rows at the same grouping level, * and ln__grand_total rows at the end. */ export function pivotPathsWithTotals(paths, measures) { const SEP = "-->"; const TOTAL = "ln__total"; const GRAND = "ln__grand_total"; // ---- Parse + normalize ---- const parsed = paths .map((p) => p .split(SEP) .map((s) => s.trim()) .filter((s) => s.length > 0)) .filter((parts) => parts.length >= 2); // need at least [dim, leaf] if (parsed.length === 0) return []; // Leaf is always last segment const leaves = new Set(); for (const parts of parsed) leaves.add(parts[parts.length - 1]); // Use a Set for de-duping output const out = new Set(); // Always include original paths (normalized) for (const parts of parsed) out.add(parts.join(SEP)); // ---- Build totals at every dimension level (except the leaf) ---- // // For a path: d0 -> d1 -> ... -> d(n-2) -> leaf // We add totals at levels i = n-2 down to 1, producing: // d0 -> ... -> d(i-1) -> TOTAL -> leaf // // (No group-total at level 0, since that's the grand total.) const prefixesByLenAndLeaf = new Map(); // key: `${i}|${leaf}` where i is the prefix length (number of dims to keep, >=1) // value: set of serialized prefixes (dims only, no leaf) for (const parts of parsed) { const leaf = parts[parts.length - 1]; const dims = parts.slice(0, -1); // all but leaf // Consider all prefix lengths from 1..dims.length (we'll add totals below the prefix) // For totals we need at least 1 kept dim, and at least one dim below it to total across. // Example: dims length = 1 => no per-group totals possible. for (let keep = 1; keep < dims.length; keep++) { const prefix = dims.slice(0, keep).join(SEP); const k = `${keep}|${leaf}`; if (!prefixesByLenAndLeaf.has(k)) prefixesByLenAndLeaf.set(k, new Set()); prefixesByLenAndLeaf.get(k).add(prefix); } } // Create total rows for (const [key, prefixes] of prefixesByLenAndLeaf.entries()) { const [, leaf] = key.split("|"); for (const prefix of prefixes) { out.add(`${prefix}${SEP}${TOTAL}${SEP}${leaf}`); } } // Also add "top level totals" like: d0 -> TOTAL -> leaf // This is already covered by keep=1 when dims.length >= 2. // ---- Grand totals per leaf ---- for (const leaf of leaves) { out.add(`${GRAND}${SEP}${leaf}`); } // ---- Sorting rules ---- // We want: // - Alphabetical ordering (case-insensitive) at each level // - ln__total should come after normal values at the same level // - ln__grand_total should come last overall // // We'll implement a comparator over the whole path that enforces: // 1) GRAND always last // 2) Otherwise compare segment-by-segment: // - At any segment, non-TOTAL comes before TOTAL // - Then alphabetical (case-insensitive), with a stable tie-breaker on raw // 3) If one path is prefix of another, shorter first const sortKey = (seg) => { if (seg === TOTAL) return [1, seg.toLowerCase(), seg]; return [0, seg.toLowerCase(), seg]; }; const tieIndex = Object.fromEntries(measures.map((x, i) => [x.toLowerCase(), i])); const cmp = (a, b) => { const aParts = a.split(SEP).map((s) => s.trim()); const bParts = b.split(SEP).map((s) => s.trim()); const aIsGrand = aParts[0] === GRAND; const bIsGrand = bParts[0] === GRAND; if (aIsGrand && !bIsGrand) return 1; if (!aIsGrand && bIsGrand) return -1; const len = Math.max(aParts.length, bParts.length); for (let i = 0; i < len; i++) { const as = aParts[i]; const bs = bParts[i]; if (as === undefined) return -1; if (bs === undefined) return 1; const ak = sortKey(as); const bk = sortKey(bs); // total positioning if (ak[0] !== bk[0]) return ak[0] - bk[0]; // Tie break at leaf if (i === aParts.length - 1 && i === bParts.length - 1) { const left = tieIndex[ak[1]]; const right = tieIndex[bk[1]]; if (left < right) return -1; if (left > right) return 1; } // case-insensitive alphabetical if (ak[1] < bk[1]) return -1; if (ak[1] > bk[1]) return 1; // raw tie-breaker if (ak[2] < bk[2]) return -1; if (ak[2] > bk[2]) return 1; } return 0; }; return Array.from(out).sort(cmp); }