-
Notifications
You must be signed in to change notification settings - Fork 0
/
patricia.js
166 lines (146 loc) · 5.14 KB
/
patricia.js
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
156
157
158
159
160
161
162
163
164
165
166
var StringMatcher = require('./stringMatcher');
var LeafIterator = require('./leafIterator');
function Node (options) {
options = options || {};
this.isTerminal = options.isTerminal;
if(typeof this.isTerminal === typeof undefined)
this.isTerminal = true;
this.frequency = options.frequency;
if(typeof this.frequency !== 'number')
this.frequency = 1;
this.label = options.label || '';
this.children = {};
}
Node.prototype.increaseFrequency = function() {
if(this.isTerminal) {
this.frequency++;
} else {
this.isTerminal = true;
this.frequency = 1;
}
};
Node.prototype.split = function(hash, index) {
var childNode = this.routeByHash(hash);
var firstPart = childNode.label.substring(0, index);
childNode.label = childNode.label.substring(index);
this.children[hash] = new Node({ label: firstPart, isTerminal: false, frequency: 0 });
this.children[hash].insertChild(childNode);
};
Node.prototype.insertChild = function(childNode) {
var hash = childNode.label[0];
this.children[hash] = childNode;
};
Node.prototype.routeByHash = function(prefix) {
return this.children[prefix[0]];
};
function PatriciaTree () {
this.root = new Node();
this.count = 0;
}
var progressFrom = function(startNode, itemString, callback) {
var itemMatcher = new StringMatcher(itemString);
var targetNode = startNode.routeByHash(itemMatcher.peek());
var lastNode = startNode;
while (targetNode && itemMatcher.thenMatch(targetNode.label) &&
itemMatcher.lastMatch.type == 'endOfPattern') {
lastNode = targetNode;
targetNode = targetNode.routeByHash(itemMatcher.peek());
}
return callback.call(this, itemMatcher, targetNode, lastNode);
};
PatriciaTree.prototype.insert = function(itemString) {
if(!itemString)
return;
this.count++;
return progressFrom.call(this, this.root, itemString, function(itemMatcher, targetNode, lastNode) {
switch(itemMatcher.lastMatch.type) {
case 'fullMatch':
// when the given string matches to the targetNode's label completely
targetNode.increaseFrequency();
break;
case 'mismatch':
// when the given string doesn't match with the tree
// we can't even reach the targetNode => split the lastNode, then insert there!
var childNode = new Node({
label: itemMatcher.remaining()
});
var hash = itemMatcher.getCharAt(itemMatcher.lastMatch.start);
lastNode.split(hash, itemMatcher.lastMatch.length);
lastNode.children[hash].insertChild(childNode);
break;
case 'endOfPattern':
// when we reached the end of a path in the tree, but we have remaining string to insert
// in this case, targetNode points to nowhere => insert to lastNode!
var node = new Node({
label: itemMatcher.remaining()
});
lastNode.insertChild(node);
break;
case 'endOfStream':
// when we ran out of the given string in the middle of an edge
// split the edge then insert there!
var hash = itemMatcher.getCharAt(itemMatcher.lastMatch.start);
lastNode.split(hash, itemMatcher.lastMatch.length);
lastNode.children[hash].increaseFrequency();
break;
case undefined:
// when nothing matches from the root, insert under root
var childNode = new Node({label: itemString});
this.root.insertChild(childNode);
break;
}
});
};
PatriciaTree.prototype.frequency = function(itemString) {
if(!itemString)
return 0;
return progressFrom.call(this, this.root, itemString, function(itemMatcher, targetNode) {
switch(itemMatcher.lastMatch.type) {
case 'fullMatch': return targetNode.isTerminal ? targetNode.frequency : 0;
case 'mismatch': return 0;
case 'endOfPattern': return 0;
case 'endOfStream': return 0;
case undefined: return 0;
}
});
};
PatriciaTree.prototype.contains = function(itemString) {
return this.frequency(itemString) !== 0;
};
var _getCompletions = function (fromNode, remainingPart) {
var getNextLeaf = new LeafIterator(fromNode, function getChildren (node) {
return node.children;
});
var leaf;
var completions = [];
while(leaf = getNextLeaf()) {
var completionString = remainingPart;
for (var i = 1; i < leaf.path.length; i++) {
completionString += leaf.path[i].label;
};
completions.push(completionString);
}
return completions;
};
PatriciaTree.prototype.getCompletions = function(prefix) {
return progressFrom.call(this, this.root, prefix, function(itemMatcher, targetNode, lastNode) {
switch(itemMatcher.lastMatch.type) {
case 'fullMatch':
return targetNode.isTerminal ? [''] : _getCompletions(targetNode, '');
case 'endOfStream':
var matchedPart = targetNode.label.substring(0, itemMatcher.lastMatch.length);
var remainingPart = targetNode.label.substring(itemMatcher.lastMatch.length);
var fromNode = lastNode.routeByHash(matchedPart);
return _getCompletions(fromNode, remainingPart);
case 'mismatch': return []; // -> in a mismatch case, we can't make an autocomplete
case 'endOfPattern': return []; // -> in this case the tree doesn't contain the prefix
case undefined: return [];
}
});
};
PatriciaTree.prototype.complete = function(prefix) {
return this.getCompletions(prefix).map(function(completion) {
return prefix + completion;
});
};
module.exports = PatriciaTree;