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.
448 lines
10 KiB
448 lines
10 KiB
3 years ago
|
/**
|
||
|
* Mnemonist MultiArray
|
||
|
* =====================
|
||
|
*
|
||
|
* Memory-efficient representation of an array of arrays. In JavaScript and
|
||
|
* most high-level languages, creating objects has a cost. This implementation
|
||
|
* is therefore able to represent nested containers without needing to create
|
||
|
* objects. This works by storing singly linked lists in a single flat array.
|
||
|
* However, this means that this structure comes with some read/write
|
||
|
* overhead but consume very few memory.
|
||
|
*
|
||
|
* This structure should be particularly suited to indices that will need to
|
||
|
* merge arrays anyway when queried and that are quite heavily hit (such as
|
||
|
* an inverted index or a quad tree).
|
||
|
*
|
||
|
* Note: the implementation does not require to keep track of head pointers
|
||
|
* but this comes with some advantages such as not needing to offset pointers
|
||
|
* by 1 and being able to perform in-order iteration. This remains quite lean
|
||
|
* in memory and does not hinder performance whatsoever.
|
||
|
*/
|
||
|
var typed = require('./utils/typed-arrays.js'),
|
||
|
Vector = require('./vector.js'),
|
||
|
Iterator = require('obliterator/iterator');
|
||
|
|
||
|
var PointerVector = Vector.PointerVector;
|
||
|
|
||
|
/**
|
||
|
* MultiArray.
|
||
|
*
|
||
|
* @constructor
|
||
|
*/
|
||
|
function MultiArray(Container, capacity) {
|
||
|
this.capacity = capacity || null;
|
||
|
this.Container = Container || Array;
|
||
|
this.hasFixedCapacity = this.capacity !== null;
|
||
|
|
||
|
if (typeof this.Container !== 'function')
|
||
|
throw new Error('mnemonist/multi-array.constructor: container should be a function.');
|
||
|
|
||
|
this.clear();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Method used to clear the structure.
|
||
|
*
|
||
|
* @return {undefined}
|
||
|
*/
|
||
|
MultiArray.prototype.clear = function() {
|
||
|
|
||
|
// Properties
|
||
|
this.size = 0;
|
||
|
this.dimension = 0;
|
||
|
|
||
|
// NOTE: #.heads, #.tails & #.lengths have a length equal to the dimension of
|
||
|
// the array, while #.pointers has a length equal to its size.
|
||
|
|
||
|
// Storage
|
||
|
if (this.hasFixedCapacity) {
|
||
|
var capacity = this.capacity;
|
||
|
|
||
|
var PointerArray = typed.getPointerArray(capacity);
|
||
|
|
||
|
var policy = function(currentCapacity) {
|
||
|
var newCapacity = Math.max(1, Math.ceil(currentCapacity * 1.5));
|
||
|
|
||
|
// Clamping max allocation
|
||
|
return Math.min(newCapacity, capacity);
|
||
|
};
|
||
|
|
||
|
var initialCapacity = Math.max(8, capacity);
|
||
|
|
||
|
this.tails = new Vector(PointerArray, {policy: policy, initialCapacity: initialCapacity});
|
||
|
this.lengths = new Vector(PointerArray, {policy: policy, initialCapacity: initialCapacity});
|
||
|
this.pointers = new PointerArray(capacity);
|
||
|
|
||
|
this.items = new this.Container(capacity);
|
||
|
}
|
||
|
else {
|
||
|
|
||
|
this.tails = new PointerVector();
|
||
|
this.lengths = new PointerVector();
|
||
|
this.pointers = new PointerVector();
|
||
|
|
||
|
this.items = new this.Container();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Method used to add an item to the container at the given index.
|
||
|
*
|
||
|
* @param {number} index - Index of the container.
|
||
|
* @param {any} item - Item to add.
|
||
|
* @return {MultiArray}
|
||
|
*/
|
||
|
MultiArray.prototype.set = function(index, item) {
|
||
|
var pointer = this.size;
|
||
|
|
||
|
// TODO: this can be factorized!
|
||
|
|
||
|
if (this.hasFixedCapacity) {
|
||
|
|
||
|
if (index >= this.capacity || this.size === this.capacity)
|
||
|
throw new Error('mnemonist/multi-array: attempting to allocate further than capacity.');
|
||
|
|
||
|
// This linked list does not exist yet. Let's create it
|
||
|
if (index >= this.dimension) {
|
||
|
|
||
|
// We may be required to grow the vectors
|
||
|
this.dimension = index + 1;
|
||
|
this.tails.grow(this.dimension);
|
||
|
this.lengths.grow(this.dimension);
|
||
|
|
||
|
this.tails.resize(this.dimension);
|
||
|
this.lengths.resize(this.dimension);
|
||
|
|
||
|
this.lengths.array[index] = 1;
|
||
|
}
|
||
|
|
||
|
// Appending to the list
|
||
|
else {
|
||
|
this.pointers[pointer] = this.tails.array[index];
|
||
|
this.lengths.array[index]++;
|
||
|
}
|
||
|
|
||
|
this.tails.array[index] = pointer;
|
||
|
this.items[pointer] = item;
|
||
|
}
|
||
|
else {
|
||
|
|
||
|
// This linked list does not exist yet. Let's create it
|
||
|
if (index >= this.dimension) {
|
||
|
|
||
|
// We may be required to grow the vectors
|
||
|
this.dimension = index + 1;
|
||
|
this.tails.grow(this.dimension);
|
||
|
this.lengths.grow(this.dimension);
|
||
|
|
||
|
this.tails.resize(this.dimension);
|
||
|
this.lengths.resize(this.dimension);
|
||
|
|
||
|
this.pointers.push(0);
|
||
|
this.lengths.array[index] = 1;
|
||
|
}
|
||
|
|
||
|
// Appending to the list
|
||
|
else {
|
||
|
this.pointers.push(this.tails.array[index]);
|
||
|
this.lengths.array[index]++;
|
||
|
}
|
||
|
|
||
|
this.tails.array[index] = pointer;
|
||
|
this.items.push(item);
|
||
|
}
|
||
|
|
||
|
this.size++;
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Method used to push a new container holding the given value.
|
||
|
* Note: it might be useful to make this function able to take an iterable
|
||
|
* or variadic someday. For the time being it's just a convenience for
|
||
|
* implementing compact multi maps and such.
|
||
|
*
|
||
|
* @param {any} item - Item to add.
|
||
|
* @return {MultiArray}
|
||
|
*/
|
||
|
MultiArray.prototype.push = function(item) {
|
||
|
var pointer = this.size,
|
||
|
index = this.dimension;
|
||
|
|
||
|
if (this.hasFixedCapacity) {
|
||
|
|
||
|
if (index >= this.capacity || this.size === this.capacity)
|
||
|
throw new Error('mnemonist/multi-array: attempting to allocate further than capacity.');
|
||
|
|
||
|
this.items[pointer] = item;
|
||
|
}
|
||
|
else {
|
||
|
this.items.push(item);
|
||
|
this.pointers.push(0);
|
||
|
}
|
||
|
|
||
|
this.lengths.push(1);
|
||
|
this.tails.push(pointer);
|
||
|
|
||
|
this.dimension++;
|
||
|
this.size++;
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Method used to get the desired container.
|
||
|
*
|
||
|
* @param {number} index - Index of the container.
|
||
|
* @return {array}
|
||
|
*/
|
||
|
MultiArray.prototype.get = function(index) {
|
||
|
if (index >= this.dimension)
|
||
|
return;
|
||
|
|
||
|
var pointers = this.hasFixedCapacity ? this.pointers : this.pointers.array;
|
||
|
|
||
|
var pointer = this.tails.array[index],
|
||
|
length = this.lengths.array[index],
|
||
|
i = length;
|
||
|
|
||
|
var array = new this.Container(length);
|
||
|
|
||
|
while (i !== 0) {
|
||
|
array[--i] = this.items[pointer];
|
||
|
pointer = pointers[pointer];
|
||
|
}
|
||
|
|
||
|
return array;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Method used to check if a container exists at the given index.
|
||
|
*
|
||
|
* @param {number} index - Index of the container.
|
||
|
* @return {boolean}
|
||
|
*/
|
||
|
MultiArray.prototype.has = function(index) {
|
||
|
return index < this.dimension;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Method used to get the size of the container stored at given index.
|
||
|
*
|
||
|
* @param {number} index - Index of the container.
|
||
|
* @return {number}
|
||
|
*/
|
||
|
MultiArray.prototype.multiplicity = function(index) {
|
||
|
if (index >= this.dimension)
|
||
|
return 0;
|
||
|
|
||
|
return this.lengths.array[index];
|
||
|
};
|
||
|
MultiArray.prototype.count = MultiArray.prototype.multiplicity;
|
||
|
|
||
|
/**
|
||
|
* Method used to iterate over the structure's containers.
|
||
|
*
|
||
|
* @return {Iterator}
|
||
|
*/
|
||
|
MultiArray.prototype.containers = function() {
|
||
|
var self = this,
|
||
|
l = this.dimension,
|
||
|
i = 0;
|
||
|
|
||
|
return new Iterator(function() {
|
||
|
if (i >= l)
|
||
|
return {done: true};
|
||
|
|
||
|
return {value: self.get(i++)};
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Method used to iterate over the structure's associations.
|
||
|
*
|
||
|
* @return {Iterator}
|
||
|
*/
|
||
|
MultiArray.prototype.associations = function() {
|
||
|
var self = this,
|
||
|
l = this.dimension,
|
||
|
i = 0;
|
||
|
|
||
|
return new Iterator(function() {
|
||
|
if (i >= l)
|
||
|
return {done: true};
|
||
|
|
||
|
var data = {value: [i, self.get(i)]};
|
||
|
|
||
|
i++;
|
||
|
|
||
|
return data;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Method used to iterate over the structure's values in the global insertion
|
||
|
* order.
|
||
|
*
|
||
|
* @param {number} [index] - Optionally, iterate over the values of a single
|
||
|
* container at index.
|
||
|
* @return {Iterator}
|
||
|
*/
|
||
|
MultiArray.prototype.values = function(index) {
|
||
|
var items = this.items,
|
||
|
length,
|
||
|
i = 0;
|
||
|
|
||
|
if (typeof index === 'number') {
|
||
|
if (index >= this.dimension)
|
||
|
return Iterator.empty();
|
||
|
|
||
|
length = this.lengths.array[index];
|
||
|
items = this.items;
|
||
|
|
||
|
var pointers = this.hasFixedCapacity ? this.pointers : this.pointers.array;
|
||
|
|
||
|
if (length === 0)
|
||
|
return Iterator.empty();
|
||
|
|
||
|
var pointer = this.tails.array[index],
|
||
|
v;
|
||
|
|
||
|
return new Iterator(function() {
|
||
|
if (i === length)
|
||
|
return {done: true};
|
||
|
|
||
|
i++;
|
||
|
v = items[pointer];
|
||
|
pointer = pointers[pointer];
|
||
|
|
||
|
return {done: false, value: v};
|
||
|
});
|
||
|
}
|
||
|
|
||
|
length = this.size;
|
||
|
|
||
|
return new Iterator(function() {
|
||
|
if (i >= length)
|
||
|
return {done: true};
|
||
|
|
||
|
return {done: false, value: items[i++]};
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Method used to iterate over the structure's entries.
|
||
|
*
|
||
|
* @return {Iterator}
|
||
|
*/
|
||
|
MultiArray.prototype.entries = function() {
|
||
|
if (this.size === 0)
|
||
|
return Iterator.empty();
|
||
|
|
||
|
var inContainer = false,
|
||
|
pointer,
|
||
|
length,
|
||
|
i = 0,
|
||
|
j = 0,
|
||
|
l = this.dimension,
|
||
|
v;
|
||
|
|
||
|
var pointers = this.hasFixedCapacity ? this.pointers : this.pointers.array,
|
||
|
items = this.items,
|
||
|
tails = this.tails.array,
|
||
|
lengths = this.lengths.array;
|
||
|
|
||
|
var iterator = new Iterator(function next() {
|
||
|
if (!inContainer) {
|
||
|
|
||
|
if (i >= l)
|
||
|
return {done: true};
|
||
|
|
||
|
length = lengths[i];
|
||
|
pointer = tails[i];
|
||
|
i++;
|
||
|
|
||
|
if (length === 0)
|
||
|
return next();
|
||
|
|
||
|
j = 0;
|
||
|
inContainer = true;
|
||
|
}
|
||
|
|
||
|
if (j === length) {
|
||
|
inContainer = false;
|
||
|
return next();
|
||
|
}
|
||
|
|
||
|
v = items[pointer];
|
||
|
|
||
|
// TODO: guard for out-of-bounds
|
||
|
pointer = pointers[pointer];
|
||
|
|
||
|
j++;
|
||
|
|
||
|
return {
|
||
|
done: false,
|
||
|
value: [i - 1, v]
|
||
|
};
|
||
|
});
|
||
|
|
||
|
return iterator;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Method used to iterate over the structure's keys.
|
||
|
*
|
||
|
* @return {Iterator}
|
||
|
*/
|
||
|
MultiArray.prototype.keys = function() {
|
||
|
var i = 0,
|
||
|
l = this.dimension;
|
||
|
|
||
|
return new Iterator(function() {
|
||
|
if (i >= l)
|
||
|
return {done: true};
|
||
|
|
||
|
return {done: false, value: i++};
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Convenience known methods.
|
||
|
*/
|
||
|
MultiArray.prototype.inspect = function() {
|
||
|
var proxy = new Array(this.dimension),
|
||
|
i,
|
||
|
l;
|
||
|
|
||
|
for (i = 0, l = this.dimension; i < l; i++)
|
||
|
proxy[i] = Array.from(this.get(i));
|
||
|
|
||
|
if (this.hasFixedCapacity) {
|
||
|
proxy.type = this.Container.name;
|
||
|
proxy.capacity = this.capacity;
|
||
|
}
|
||
|
|
||
|
proxy.size = this.size;
|
||
|
proxy.dimension = this.dimension;
|
||
|
|
||
|
// Trick so that node displays the name of the constructor
|
||
|
Object.defineProperty(proxy, 'constructor', {
|
||
|
value: MultiArray,
|
||
|
enumerable: false
|
||
|
});
|
||
|
|
||
|
return proxy;
|
||
|
};
|
||
|
|
||
|
if (typeof Symbol !== 'undefined')
|
||
|
MultiArray.prototype[Symbol.for('nodejs.util.inspect.custom')] = MultiArray.prototype.inspect;
|
||
|
|
||
|
// TODO: .from
|
||
|
|
||
|
/**
|
||
|
* Exporting.
|
||
|
*/
|
||
|
module.exports = MultiArray;
|