// A purely functional Radix Trie // https://en.wikipedia.org/wiki/Radix_tree export const emptyTrie = { children: [], }; // find maximal common prefix, and whether string A is smaller than B const commonPrefix = (strA, strB) => { let i=0 for (; i charB) { return [strA.slice(0, i), false]; } } return [strA.slice(0, i), false]; } // a funny kind of binary search, that assumes 'ls' is a sorted list of strings, and none of the strings have a common prefix const binarySearch = (ls, key) => { return __binarySearch(ls, key, 0, ls.length); } const __binarySearch = (ls, key, min, max) => { if (min === max) { return [max, ""]; // otherwise we go out of bounds } const middle = Math.floor((min+max)/2); const [prefix, smaller] = commonPrefix(key, ls[middle][0]); if (prefix.length > 0) { return [middle, prefix]; } if (smaller) { // key was smaller than middle return __binarySearch(ls, key, min, middle); } return __binarySearch(ls, key, middle+1, max); } // insert (key,value) into trie. export const insert = trie => key => value => { if (key.length === 0) { // set value of current node return { value, children: trie.children, }; } const [insertPos, prefix] = binarySearch(trie.children, key); if (insertPos === trie.children.length) { // insert node at end return { value: trie.value, children: [ ...trie.children, [key, {value, children:[]}], ], }; } if (prefix.length === 0) { // nothing in common... // insert new node into children return { value: trie.value, children: trie.children.toSpliced( insertPos, // insert position 0, // delete nothing [key, {value, children:[]}], ), }; } const [haveKey, haveChildNode] = trie.children[insertPos]; if (prefix.length === haveKey.length) { // recurse return { value: trie.value, children: trie.children.with( insertPos, // position to update [haveKey, insert(haveChildNode)(key.slice(prefix.length))(value)], ) } } // otherwise, split entry: const havePostFix = haveKey.slice(prefix.length); const postFix = key.slice(prefix.length); return { value: trie.value, children: trie.children.with( insertPos, // position to update [prefix, { children: [ [havePostFix, haveChildNode], [postFix, {value, children: []} ], ]}], ), }; }; // given a prefix, return a string X such that prefix+X is a possibly larger prefix for the same entries as the original prefix. export const growPrefix = trie => key => { const [pos, prefix] = binarySearch(trie.children, key); if (prefix.length === 0) { return ""; } if (pos === trie.children.length) { return ""; } const [haveKey, haveChildNode] = trie.children[pos]; if (key.length < haveKey.length) { if (haveKey.startsWith(key)) { return haveKey.slice(key.length); } } if (key.length > haveKey.length) { if (key.startsWith(haveKey)) { return growPrefix(haveChildNode)(key.slice(haveKey.length)); } } return ""; } // get array of (key, value) entries whose keys are prefixed by 'key'. export const suggest = trie => key => maxSuggestions => { return __suggest(trie, "", key, maxSuggestions); } const __suggest = (trie, path, remaining, maxSuggestions) => { if (maxSuggestions === 0) { return []; } if (remaining === "") { const results = []; if (trie.value !== undefined) { results.push([path, trie.value]); maxSuggestions--; } for (const [haveKey, haveChildNode] of trie.children) { const moreSuggestions = __suggest(haveChildNode, path+haveKey, remaining, maxSuggestions); results.push(...moreSuggestions); maxSuggestions -= moreSuggestions.length; if (maxSuggestions === 0) { break; } } return results; } const [pos, prefix] = binarySearch(trie.children, remaining); if (prefix.length === haveKey.length) { return __suggest(haveChildNode, path+haveKey, remaining.slice(haveKey.length), maxSuggestions) } return []; }