You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
428 lines
8.7 KiB
428 lines
8.7 KiB
/* eslint no-constant-condition: 0 */
|
|
|
|
/* eslint-disable */
|
|
|
|
/**
|
|
* Mnemonist FixedFixedCritBitTreeMap
|
|
* ===================================
|
|
*
|
|
* TODO...
|
|
*
|
|
* [References]:
|
|
* https://cr.yp.to/critbit.html
|
|
* https://www.imperialviolet.org/binary/critbit.pdf
|
|
*/
|
|
var bitwise = require('./utils/bitwise.js'),
|
|
typed = require('./utils/typed-arrays.js');
|
|
|
|
/**
|
|
* Helpers.
|
|
*/
|
|
|
|
/**
|
|
* Helper returning the direction we need to take given a key and an
|
|
* encoded critbit.
|
|
*
|
|
* @param {string} key - Target key.
|
|
* @param {number} critbit - Packed address of byte + mask.
|
|
* @return {number} - 0, left or 1, right.
|
|
*/
|
|
function getDirection(key, critbit) {
|
|
var byteIndex = critbit >> 8;
|
|
|
|
if (byteIndex > key.length - 1)
|
|
return 0;
|
|
|
|
var byte = key.charCodeAt(byteIndex),
|
|
mask = critbit & 0xff;
|
|
|
|
return byte & mask;
|
|
}
|
|
|
|
/**
|
|
* Helper returning the packed address of byte + mask or -1 if strings
|
|
* are identical.
|
|
*
|
|
* @param {string} a - First key.
|
|
* @param {string} b - Second key.
|
|
* @return {number} - Packed address of byte + mask.
|
|
*/
|
|
function findCriticalBit(a, b) {
|
|
var i = 0,
|
|
tmp;
|
|
|
|
// Swapping so a is the shortest
|
|
if (a.length > b.length) {
|
|
tmp = b;
|
|
b = a;
|
|
a = tmp;
|
|
}
|
|
|
|
var l = a.length,
|
|
mask;
|
|
|
|
while (i < l) {
|
|
if (a[i] !== b[i]) {
|
|
mask = bitwise.msb8(
|
|
a.charCodeAt(i) ^ b.charCodeAt(i)
|
|
);
|
|
|
|
return (i << 8) | mask;
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
// Strings are identical
|
|
if (a.length === b.length)
|
|
return -1;
|
|
|
|
// NOTE: x ^ 0 is the same as x
|
|
mask = bitwise.msb8(b.charCodeAt(i));
|
|
|
|
return (i << 8) | mask;
|
|
}
|
|
|
|
/**
|
|
* FixedCritBitTreeMap.
|
|
*
|
|
* @constructor
|
|
*/
|
|
function FixedCritBitTreeMap(capacity) {
|
|
|
|
if (typeof capacity !== 'number' || capacity <= 0)
|
|
throw new Error('mnemonist/fixed-critbit-tree-map: `capacity` should be a positive number.');
|
|
|
|
// Properties
|
|
this.capacity = capacity;
|
|
this.offset = 0;
|
|
this.root = 0;
|
|
this.size = 0;
|
|
|
|
var PointerArray = typed.getSignedPointerArray(capacity + 1);
|
|
|
|
this.keys = new Array(capacity);
|
|
this.values = new Array(capacity);
|
|
this.lefts = new PointerArray(capacity - 1);
|
|
this.rights = new PointerArray(capacity - 1);
|
|
this.critbits = new Uint32Array(capacity);
|
|
}
|
|
|
|
/**
|
|
* Method used to clear the FixedCritBitTreeMap.
|
|
*
|
|
* @return {undefined}
|
|
*/
|
|
FixedCritBitTreeMap.prototype.clear = function() {
|
|
|
|
// Properties
|
|
// TODO...
|
|
this.root = null;
|
|
this.size = 0;
|
|
};
|
|
|
|
/**
|
|
* Method used to set the value of the given key in the trie.
|
|
*
|
|
* @param {string} key - Key to set.
|
|
* @param {any} value - Arbitrary value.
|
|
* @return {FixedCritBitTreeMap}
|
|
*/
|
|
FixedCritBitTreeMap.prototype.set = function(key, value) {
|
|
var pointer;
|
|
|
|
// TODO: yell if capacity is already full!
|
|
|
|
// Tree is empty
|
|
if (this.size === 0) {
|
|
this.keys[0] = key;
|
|
this.values[0] = value;
|
|
|
|
this.size++;
|
|
|
|
this.root = -1;
|
|
|
|
return this;
|
|
}
|
|
|
|
// Walk state
|
|
var pointer = this.root,
|
|
newPointer,
|
|
leftOrRight,
|
|
opposite,
|
|
ancestors = [],
|
|
path = [],
|
|
ancestor,
|
|
parent,
|
|
child,
|
|
critbit,
|
|
internal,
|
|
best,
|
|
dir,
|
|
i,
|
|
l;
|
|
|
|
// Walking the tree
|
|
while (true) {
|
|
|
|
// Traversing an internal node
|
|
if (pointer > 0) {
|
|
pointer -= 1;
|
|
|
|
// Choosing the correct direction
|
|
dir = getDirection(key, this.critbits[pointer]);
|
|
|
|
leftOrRight = dir === 0 ? this.lefts : this.rights;
|
|
newPointer = leftOrRight[pointer];
|
|
|
|
if (newPointer === 0) {
|
|
|
|
// Creating a fitting external node
|
|
pointer = this.size++;
|
|
leftOrRight[newPointer] = -(pointer + 1);
|
|
this.keys[pointer] = key;
|
|
this.values[pointer] = value;
|
|
return this;
|
|
}
|
|
|
|
ancestors.push(pointer);
|
|
path.push(dir);
|
|
pointer = newPointer;
|
|
}
|
|
|
|
// Reaching an external node
|
|
else {
|
|
pointer = -pointer;
|
|
pointer -= 1;
|
|
|
|
// 1. Creating a new external node
|
|
critbit = findCriticalBit(key, this.keys[pointer]);
|
|
|
|
// Key is identical, we just replace the value
|
|
if (critbit === -1) {
|
|
this.values[pointer] = value;
|
|
return this;
|
|
}
|
|
|
|
internal = this.offset++;
|
|
newPointer = this.size++;
|
|
|
|
this.keys[newPointer] = key;
|
|
this.values[newPointer] = value;
|
|
|
|
this.critbits[internal] = critbit;
|
|
|
|
dir = getDirection(key, critbit);
|
|
leftOrRight = dir === 0 ? this.lefts : this.rights;
|
|
opposite = dir === 0 ? this.rights : this.lefts;
|
|
|
|
leftOrRight[internal] = -(newPointer + 1);
|
|
opposite[internal] = -(pointer + 1);
|
|
|
|
// 2. Bubbling up
|
|
best = -1;
|
|
l = ancestors.length;
|
|
|
|
for (i = l - 1; i >= 0; i--) {
|
|
ancestor = ancestors[i];
|
|
|
|
// TODO: this can be made faster
|
|
if ((this.critbits[ancestor] >> 8) > (critbit >> 8)) {
|
|
continue;
|
|
}
|
|
else if ((this.critbits[ancestor] >> 8) === (critbit >> 8)) {
|
|
if ((this.critbits[ancestor] & 0xff) < (critbit & 0xff))
|
|
continue;
|
|
}
|
|
|
|
best = i;
|
|
break;
|
|
}
|
|
|
|
// Do we need to attach to the root?
|
|
if (best < 0) {
|
|
this.root = internal + 1;
|
|
|
|
// Need to rewire parent as child?
|
|
if (l > 0) {
|
|
parent = ancestors[0];
|
|
|
|
opposite[internal] = parent + 1;
|
|
}
|
|
}
|
|
|
|
// Simple case without rotation
|
|
else if (best === l - 1) {
|
|
parent = ancestors[best];
|
|
dir = path[best];
|
|
|
|
leftOrRight = dir === 0 ? this.lefts : this.rights;
|
|
|
|
leftOrRight[parent] = internal + 1;
|
|
}
|
|
|
|
// Full rotation
|
|
else {
|
|
parent = ancestors[best];
|
|
dir = path[best];
|
|
child = ancestors[best + 1];
|
|
|
|
opposite[internal] = child + 1;
|
|
|
|
leftOrRight = dir === 0 ? this.lefts : this.rights;
|
|
|
|
leftOrRight[parent] = internal + 1;
|
|
}
|
|
|
|
return this;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Method used to get the value attached to the given key in the tree or
|
|
* undefined if not found.
|
|
*
|
|
* @param {string} key - Key to get.
|
|
* @return {any}
|
|
*/
|
|
FixedCritBitTreeMap.prototype.get = function(key) {
|
|
|
|
// Walk state
|
|
var pointer = this.root,
|
|
dir;
|
|
|
|
// Walking the tree
|
|
while (true) {
|
|
|
|
// Dead end
|
|
if (pointer === 0)
|
|
return;
|
|
|
|
// Traversing an internal node
|
|
if (pointer > 0) {
|
|
pointer -= 1;
|
|
dir = getDirection(key, this.critbits[pointer]);
|
|
|
|
pointer = dir === 0 ? this.lefts[pointer] : this.rights[pointer];
|
|
}
|
|
|
|
// Reaching an external node
|
|
else {
|
|
pointer = -pointer;
|
|
pointer -= 1;
|
|
|
|
if (this.keys[pointer] !== key)
|
|
return;
|
|
|
|
return this.values[pointer];
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Method used to return whether the given key exists in the tree.
|
|
*
|
|
* @param {string} key - Key to test.
|
|
* @return {boolean}
|
|
*/
|
|
FixedCritBitTreeMap.prototype.has = function(key) {
|
|
|
|
// Walk state
|
|
var pointer = this.root,
|
|
dir;
|
|
|
|
// Walking the tree
|
|
while (true) {
|
|
|
|
// Dead end
|
|
if (pointer === 0)
|
|
return false;
|
|
|
|
// Traversing an internal node
|
|
if (pointer > 0) {
|
|
pointer -= 1;
|
|
dir = getDirection(key, this.critbits[pointer]);
|
|
|
|
pointer = dir === 0 ? this.lefts[pointer] : this.rights[pointer];
|
|
}
|
|
|
|
// Reaching an external node
|
|
else {
|
|
pointer = -pointer;
|
|
pointer -= 1;
|
|
|
|
return this.keys[pointer] === key;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Method used to iterate over the tree in key order.
|
|
*
|
|
* @param {function} callback - Function to call for each item.
|
|
* @param {object} scope - Optional scope.
|
|
* @return {undefined}
|
|
*/
|
|
FixedCritBitTreeMap.prototype.forEach = function(callback, scope) {
|
|
scope = arguments.length > 1 ? scope : this;
|
|
|
|
// Inorder traversal of the tree
|
|
var current = this.root,
|
|
stack = [],
|
|
p;
|
|
|
|
while (true) {
|
|
|
|
if (current !== 0) {
|
|
stack.push(current);
|
|
|
|
current = current > 0 ? this.lefts[current - 1] : 0;
|
|
}
|
|
|
|
else {
|
|
if (stack.length > 0) {
|
|
current = stack.pop();
|
|
|
|
if (current < 0) {
|
|
p = -current;
|
|
p -= 1;
|
|
|
|
callback.call(scope, this.values[p], this.keys[p]);
|
|
}
|
|
|
|
current = current > 0 ? this.rights[current - 1] : 0;
|
|
}
|
|
else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Convenience known methods.
|
|
*/
|
|
FixedCritBitTreeMap.prototype.inspect = function() {
|
|
return this;
|
|
};
|
|
|
|
if (typeof Symbol !== 'undefined')
|
|
FixedCritBitTreeMap.prototype[Symbol.for('nodejs.util.inspect.custom')] = FixedCritBitTreeMap.prototype.inspect;
|
|
|
|
/**
|
|
* Static @.from function taking an arbitrary iterable & converting it into
|
|
* a FixedCritBitTreeMap.
|
|
*
|
|
* @param {Iterable} iterable - Target iterable.
|
|
* @return {FixedCritBitTreeMap}
|
|
*/
|
|
// FixedCritBitTreeMap.from = function(iterable) {
|
|
|
|
// };
|
|
|
|
/**
|
|
* Exporting.
|
|
*/
|
|
module.exports = FixedCritBitTreeMap;
|