1
1
import * as _ from 'lodash' ;
2
- import { TrieData , isLeafValue } from "../runtime/trie" ;
2
+ import { TrieData , isLeafValue , TrieValue } from "../runtime/trie" ;
3
3
4
- export function buildTrie ( map : { [ key : string ] : string | string [ ] } ) : TrieData {
4
+ type TrieInput = Map <
5
+ Array < string | RegExp > ,
6
+ string | string [ ]
7
+ > ;
8
+
9
+ export function buildTrie ( map : TrieInput ) : TrieData {
5
10
return optimizeTrie ( buildNaiveTrie ( map ) ) ;
6
11
}
7
12
8
13
// Build a simple naive trie. One level per char, string nodes at the leaves,
9
14
// and '' keys for leaf nodes half way down paths.
10
- export function buildNaiveTrie ( map : { [ key : string ] : string | string [ ] } ) : TrieData {
11
- const root = < TrieData > { } ;
15
+ export function buildNaiveTrie ( map : TrieInput ) : TrieData {
16
+ const root = < TrieData > new Map ( ) ;
12
17
13
18
// For each key, make a new level for each char in the key (or use an existing
14
19
// level), and place the leaf when we get to the end of the key.
15
20
16
- _ . forEach ( map , ( value , key ) => {
21
+ for ( let [ keys , value ] of map ) {
17
22
let trie : TrieData = root ;
18
- _ . forEach ( key , ( char , i ) => {
19
- let nextStep = trie [ char ] ;
20
23
21
- if ( i === key . length - 1 ) {
24
+ const keyChunks = _ . flatMap < string | RegExp , string | RegExp > ( keys , ( key ) => {
25
+ if ( _ . isRegExp ( key ) ) return key ;
26
+ else return key . split ( '' ) ;
27
+ } ) ;
28
+ _ . forEach ( keyChunks , ( chunk , i ) => {
29
+ let nextStep = chunk instanceof RegExp ?
30
+ trie . get ( _ . find ( [ ...trie . keys ( ) ] , k => _ . isEqual ( chunk , k ) ) ! ) :
31
+ trie . get ( chunk ) ;
32
+
33
+ let isLastChunk = i === keyChunks . length - 1 ;
34
+
35
+ if ( isLastChunk ) {
22
36
// We're done - write our value into trie[char]
23
37
24
38
if ( isLeafValue ( nextStep ) ) {
25
39
throw new Error ( 'Duplicate key' ) ; // Should really never happen
26
40
} else if ( typeof nextStep === 'object' ) {
27
41
// We're half way down another key - add an empty branch
28
- nextStep [ '' ] = value ;
42
+ nextStep . set ( '' , value ) ;
29
43
} else {
30
44
// We're a fresh leaf at the end of a branch
31
- trie [ char ] = value ;
45
+ trie . set ( chunk , value ) ;
32
46
}
33
47
} else {
34
48
// We have more to go - iterate into trie[char]
35
49
36
50
if ( isLeafValue ( nextStep ) ) {
37
51
// We're at what is currently a leaf value
38
52
// Transform it into a node with '' for the value.
39
- nextStep = { '' : nextStep } ;
40
- trie [ char ] = nextStep ;
53
+ nextStep = new Map ( [ [ '' , nextStep ] ] ) ;
54
+ trie . set ( chunk , nextStep ) ;
41
55
} else if ( typeof nextStep === 'undefined' ) {
42
56
// We're adding a new branch to the trie
43
- nextStep = { } ;
44
- trie [ char ] = nextStep ;
57
+ nextStep = new Map ( ) ;
58
+ trie . set ( chunk , nextStep ) ;
45
59
}
46
60
47
61
trie = nextStep ;
48
62
}
49
63
} ) ;
50
- } ) ;
64
+ }
51
65
52
66
return root ;
53
67
}
@@ -59,45 +73,55 @@ export function buildNaiveTrie(map: { [key: string]: string | string[] }): TrieD
59
73
export function optimizeTrie ( trie : TrieData ) : TrieData {
60
74
if ( _ . isString ( trie ) ) return trie ;
61
75
62
- const keys = Object . keys ( trie ) ;
76
+ const keys = [ ... trie . keys ( ) ] . filter ( k => k !== '' ) ;
63
77
64
78
if ( keys . length === 0 ) return trie ;
65
79
66
80
if ( keys . length === 1 ) {
81
+ // If this level has one string key, combine it with the level below
67
82
const [ key ] = keys ;
68
- const child = trie [ key ] ! ;
83
+ const child = trie . get ( key ) ! ;
69
84
85
+ // If the child is a final value, we can't combine this key with it, and we're done
86
+ // TODO: Could optimize further here, and pull the child up in this case?
87
+ // (Only if trie.size === 1 too). Seems unnecessary for now, a little risky.
70
88
if ( isLeafValue ( child ) ) return trie ;
71
89
72
- // Don't combine if our child has a leaf node attached - this would break
73
- // search (en route leaf nodes need to always be under '' keys)
74
- if ( ! child [ '' ] ) {
75
- // Return the only child, with every key prefixed with this key
76
- const newChild = optimizeTrie (
77
- _ . mapKeys ( child , ( _value , childKey ) => key + childKey )
90
+ if (
91
+ // Don't combine if our child has a leaf node attached - this would break
92
+ // search (en route leaf nodes need to always be under '' keys)
93
+ ! child . get ( '' ) &&
94
+ // If this key or any child key is a regex, we don't try to combine the
95
+ // keys together. It's possible to do so, but a little messy,
96
+ // not strictly necessary, and hurts runtime perf (testing up to N regexes
97
+ // is worse than testing 1 regex + 1 string hash lookup).
98
+ ! _ . isRegExp ( keys [ 0 ] ) &&
99
+ ! _ . some ( [ ...child . keys ( ) ] , k => _ . isRegExp ( k ) )
100
+ ) {
101
+ // Replace this node with the only child, with every key prefixed with this key
102
+ const collapsedChild = mapMap ( child , ( childKey , value ) =>
103
+ // We know keys are strings because we checked above
104
+ [ key + ( childKey as string ) , value ]
78
105
) ;
79
-
80
- return newChild ;
81
- }
82
- }
83
-
84
- if ( keys . length === 2 && _ . includes ( keys , '' ) ) {
85
- const [ key ] = keys . filter ( k => k !== '' ) ;
86
- const child = trie [ key ] ! ;
87
-
88
- const childKeys = Object . keys ( child ) ;
89
- if ( ! isLeafValue ( child ) && childKeys . length === 1 && childKeys [ 0 ] !== '' ) {
90
- // If child has only one key and it's not '', pull it up.
91
- return optimizeTrie ( {
92
- '' : trie [ '' ] ,
93
- [ key + childKeys [ 0 ] ] : child [ childKeys [ 0 ] ]
94
- } ) ;
106
+ // We might still have an en-route leaf node at this level - don't lose it.
107
+ if ( trie . get ( '' ) ) collapsedChild . set ( '' , trie . get ( '' ) ) ;
108
+ // Then we reoptimize this same level again (we might be able to to collapse further)
109
+ return optimizeTrie ( collapsedChild ) ;
95
110
}
96
111
}
97
112
98
113
// Recursive DFS through the child values to optimize them in turn
99
- return _ . mapValues ( trie , ( child ) => {
100
- if ( isLeafValue ( child ) ) return child ;
101
- else return optimizeTrie ( child ! ) ;
114
+ return mapMap ( trie , ( key , child ) : [ string | RegExp , TrieValue ] => {
115
+ if ( isLeafValue ( child ) ) return [ key , child ] ;
116
+ else return [ key , optimizeTrie ( child ! ) ] ;
102
117
} ) ;
118
+ }
119
+
120
+ function mapMap < K , V , K2 , V2 > (
121
+ map : Map < K , V > ,
122
+ mapping : ( a : K , b : V ) => [ K2 , V2 ]
123
+ ) : Map < K2 , V2 > {
124
+ return new Map (
125
+ Array . from ( map , ( [ k , v ] ) => mapping ( k , v ) )
126
+ ) ;
103
127
}
0 commit comments