All files node.ts

98.57% Statements 69/70
95.24% Branches 20/21
100% Functions 8/8
100% Lines 45/45

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155  4x                                         4x       469x 469x 469x     4x 14x                                                                                 4x 37x   37x     37x 3x     37x     4x 44x 44x   33x 33x   29x       29x               4x 38x   37x 37x   37x 76x 76x 70x     31x     4x 67x     67x 5x       67x 2x     67x     4x 74x     74x   74x     74x 53x   53x     74x   4x  
import { Word, NextNodes, NodeStack, SuggestedPattern, Value } from './types'
import { Suggestion } from './suggestion'
 
/**
 * **NOTE:** a valid [[Match]] with a `remainder` can **only** occur if the partial match 
 * occurs at a word boundary (i.e., the [[Node | node]] contained in the [[Match| match]]
 * has `end === true`). This is due to the fact that the API for defining a [[Pattern | pattern]]
 * does not support concatenating a lookup result and char sequences into a single word. E.g.,
 * a pattern such as `<some_lookup_context>s` to make a lookup result plural is not supported.
 */
export interface Match {
    nodes: NodeStack
    remainder: Word[]
}
 
/**
 * <p align="center"> 
 *   <img height="80px" src="https://web.archive.org/web/20090806155747/http://www.geocities.com/brerfoxkaleidscope/man_hampster_wheel_lg_wht.gif">
 *   <img height="80px" src="https://web.archive.org/web/20090806155747/http://www.geocities.com/brerfoxkaleidscope/man_hampster_wheel_lg_wht.gif">
 *   <img height="80px" src="https://web.archive.org/web/20090806155747/http://www.geocities.com/brerfoxkaleidscope/man_hampster_wheel_lg_wht.gif">
 * </p>
 */
export class Node {
    end: boolean
    next: NextNodes
 
    constructor(readonly value: Value) {
        this.end = false
        this.next = { char: {}, word: {}, lookup: {} }
    }
 
    public isLeaf(): boolean {
        return Object.keys(this.next.char).length === 0
            && Object.keys(this.next.word).length === 0
            && Object.keys(this.next.lookup).length === 0
    }
 
    /**
     * Given an input sequence of words, a starting [[Node | node]], and
     * a [[Dictionary | dictionary]], finds all valid matching paths that
     * the input satisfies.
     * 
     * #### Simple Example
     * If we have a starting node which yields the following
     * sub-trie,
     * ```
     * t - r - i - e
     *       \
     *         e - e
     * ```
     * and input *"tr"*, the returned node will be *"r"*.
     *
     * #### Advanced Example
     * With a more complex starting trie,
     * ```
     * null - a - b - c -   - d (1)
     *      \
     *        <X> -   - d (2)
     *
     * <X>: null - a - b - c
     *           \ 
     *             <Y>
     *
     * <Y>: null - a - b - c -   - d (3)
     * ```
     * given **"abc d"**, it wll return the **"d"** [[Node | nodes]] labeled
     * _(1)_, _(2)_, _(3)_
     * 
     * Since patterns can span multiple levels of nested contexts, we need to return
     * not only the matched words (partially matched on completed words), but also the
     * remainder of the match. This way, we can check for matches in parent contexts in
     * case a pattern satisfies a match over an arbitrary number of contextual levels.
     */
    public matchPattern(tokens: Word[]): Match[] {
        Iif (tokens.length === 0) return []
 
        let matches: Match[] = []
 
        // find matching paths from lookups
        for (const [alias, lookup] of Object.entries(this.next.lookup)) {
            matches = matches.concat(lookup.matchPattern(tokens))
        }
 
        return matches.concat(this.matchWord(tokens))
    }
 
    public matchWord(tokens: Word[]): Match[] {
        const word = this.next.word[tokens[0][0]]
        if (!word) return []
 
        const node = word.matchChars(tokens[0])
        if (!node) return []
 
        const match = { nodes: [node], remainder: tokens.slice(1) }
 
        // (1) if there are no remainders keep searching. include match in results if it is a terminal.
        // (2) if there are no remainders, return this single match (regardless of if it isa terminal).
        return match.remainder.length > 0
            ? (node.end ? [match] : []).concat(node.matchPattern(match.remainder))  // (1)
            : [match]                                                               // (2)
    }
 
    /**
     * Given an word, returns the final node which matches the complete word. null otherwise.
     */
    public matchChars(word: Word): Node | null {
        if (this.value !== word[0]) return null
 
        let node: Node = this
        word = word.substr(1)
 
        while (word) {
            node = node.next.char[word[0]]
            if (!node) return null
            word = word.substr(1)
        }
 
        return node
    }
 
    public completePattern(tokens: SuggestedPattern): Suggestion[] {
        let suggestions: Suggestion[] = []
 
        // complete pattern in all next lookups
        for (const [alias, lookup] of Object.entries(this.next.lookup)) {
            suggestions = suggestions.concat(lookup.completePattern([...tokens, { [lookup.value as string]: lookup.contexts }]))
        }
 
        // complete pattern in all next words
        for (const [char, word] of Object.entries(this.next.word)) {
            suggestions = suggestions.concat(word.completeWord([...tokens, char]))
        }
 
        return suggestions.concat(this.completeWord(tokens))
    }
 
    public completeWord(tokens: SuggestedPattern): Suggestion[] {
        let suggestions: Suggestion[] = []
 
        // if this is an ending node, add it to suggestions
        if (this.end) suggestions.push(new Suggestion([...tokens]))
 
        const lastWord: Word = (tokens.pop() as Word) || ''
 
        // augment the last token with the next characters
        for (const char of Object.values(this.next.char)) {
            const augmentedTokens = [...tokens, `${lastWord}${char.value}`]
 
            suggestions = suggestions.concat(char.completePattern(augmentedTokens))
        }
 
        return suggestions
    }
}