export function multiKeySet( m: Map>, a: A, b: B, v: V ): void { const bMap = m.get(a) || new Map(); m.set(a, bMap); bMap.set(b, v); } const tokenRegex = /\b(\w+)\b/gi; /** replace strings in a text according to a relacement map * replaced strings must be 'tokens', surrounded by spaces or punctuation */ export function replaceWords( text: string, replace: Record ): string { return text.replaceAll(tokenRegex, (s) => (s in replace ? replace[s] : s)); } /** return an array partitioned into possibly overlapping groups */ export function grouped(a: T[], size: number, stride = size): T[][] { const groups = []; for (let i = 0; i < a.length; i += stride) { groups.push(a.slice(i, i + size)); } return groups; } /** group an array into subarrays by a key function */ export function groupBy(a: T[], key: (t: T) => K): Map { const groups = new Map(); for (const t of a) { const k = key(t); const group = groups.get(k) || []; group.push(t); groups.set(k, group); } return groups; } /** partition an array into two parts by a discriminator function */ export function partition(a: T[], partFn: (t: T) => boolean): [T[], T[]] { const yesPart: T[] = []; const noPart: T[] = []; for (const t of a) { partFn(t) ? yesPart.push(t) : noPart.push(t); } return [yesPart, noPart]; } /** run an carrying function over every element in an array, * i.e. an inclusive prefix scan */ export function scan(array: T[], fn: (a: T, b: U) => U, zero: U): U[] { const result = [zero]; let current = zero; for (let i = 0; i < array.length; i++) { current = fn(array[i], current); result.push(current); } return result; } /** return a new record by replacing values in 'a' with 'b' as a map. * values in 'a' that are not in 'b' are unchanged. * e.g. {a: "b", x: 9}, {b: 1} yields {a: 1, x: 9} */ export function mapForward( a: Record, b: Record ): Record { const combined = Object.entries(a).map(([key, value]) => { const mappedValue = value in b ? b[value] : value; return [key, mappedValue]; }); return Object.fromEntries(combined); } /** return the last element of an array or undefined */ export function last(a: T[]): T | undefined { return a[a.length - 1]; } /** * Overlap two arrays, returning the tail of b if a is a prefix of b. * Otherwise, return undefined. */ export function overlapTail(a: T[], b: T[]): T[] | undefined { let overlapSize = Math.min(a.length, b.length); while (overlapSize > 0) { const suffix = a.slice(-overlapSize); const prefix = b.slice(0, overlapSize); if (arrayEquals(suffix, prefix)) { break; } else { overlapSize--; } } if (overlapSize) { return b.slice(overlapSize); } } function arrayEquals(a: any[], b: any[]): boolean { return a.length === b.length && a.every((val, index) => val === b[index]); }