doublylinked
Version:
Doubly linked list implementation for JavaScript with iterator and array-like interface
718 lines (675 loc) • 18.4 kB
JavaScript
/* doublylinked
------------------------
(c) 2017-present Panates
SQB may be freely distributed under the MIT license.
For details and documentation:
https://panates.github.io/doublylinked/
*/
'use strict';
/**
*
* @class
*/
class DoublyLinked {
/**
* @param {*} element... - The elements to add to the end of the list
* @constructor
*/
constructor(...element) {
this._cursor = undefined;
this._head = undefined;
this._tail = undefined;
this._length = 0;
this._eof = undefined;
if (element.length) {
this.push.apply(this, element);
}
}
/**
*
* @returns {Node}
*/
get cursor() {
return this._cursor;
}
/**
*
* @returns {Node}
*/
get head() {
return this._head;
}
/**
*
* @returns {int}
*/
get length() {
return this._length;
}
/**
*
* @returns {Node}
*/
get tail() {
return this._tail;
}
/**
* Merges cursor list with and given lists/values into new list
*
* @param {String} element... - Lists and/or values to concatenate into a new list
* @return {DoublyLinked} - A new DoublyLinked instance
* @public
*/
concat(...element) {
const result = new DoublyLinked();
const mergeFn = (acc, node) => {
acc.push(node);
return acc;
};
this.reduce(mergeFn, result);
for (const arg of element) {
if (arg instanceof DoublyLinked) {
arg.reduce(mergeFn, result);
} else result.push(arg);
}
return result.reset();
}
/**
* Returns the iterator object contains entries
*
* @return {Iterator}
*/
entries() {
return {
[Symbol.iterator]() {
let _cursor;
let i = 0;
return {
next: () => {
_cursor = _cursor ? _cursor.next : this.head;
return {
value: _cursor && [i++, _cursor.value],
done: !_cursor,
};
},
};
},
};
}
/**
* Returns the iterator object contains keys
*
* @return {Iterator}
*/
keys() {
return {
[Symbol.iterator]() {
let _cursor;
let i = 0;
return {
next: () => {
_cursor = _cursor ? _cursor.next : this.head;
return {
value: _cursor && i++,
done: !_cursor,
};
},
};
},
};
}
/**
* Returns the iterator object contains values
*
* @return {function}
*/
values() {
return {
[Symbol.iterator]() {
let _cursor;
return {
next: () => {
_cursor = _cursor ? _cursor.next : this.head;
return {
value: _cursor && _cursor.value,
done: !_cursor,
};
},
};
},
};
}
/**
* Tests whether all elements in the list pass the test implemented by
* the provided function (from left to right)
*
* @param {Function} callback - Function to test for each element
* @param {*} [thisArg] - Value to use as this when executing callback
* @return {Boolean} - true if the callback function returns a truthy value for every list element; otherwise, false
* @public
*/
every(callback, thisArg) {
if (typeof callback !== 'function') {
throw new TypeError('You must provide a function as first argument');
}
if (!(this._length && callback)) {
return true;
}
thisArg = thisArg !== undefined ? thisArg : this;
let tmp = this._head;
let nxt;
let i = 0;
while (tmp) {
nxt = tmp.next;
if (!callback.call(thisArg, tmp.value, i++, thisArg)) {
return false;
}
tmp = nxt;
}
return true;
}
/**
* Tests whether all elements in the list pass the test implemented by
* the provided function (from right to left)
*
* @param {Function} callback - Function to test for each element
* @param {*} [thisArg] - Value to use as this when executing callback
* @return {Boolean} - true if the callback function returns a truthy value for every list element; otherwise, false
* @public
*/
everyRight(callback, thisArg) {
if (typeof callback !== 'function') {
throw new TypeError('You must provide a function as first argument');
}
if (!(this._length && callback)) {
return true;
}
thisArg = thisArg !== undefined ? thisArg : this;
let tmp = this.tail;
for (let i = 0; i < this._length; i++) {
if (!callback.call(thisArg, tmp.value, this._length - i - 1, thisArg)) {
return false;
}
tmp = tmp.prev;
}
return true;
}
/**
* Creates a new list with all elements that pass the test implemented
* by the provided function
*
* @param {Function} callback - Function to test for each element
* @param {*} [thisArg] - Value to use as this when executing callback
* @return {DoublyLinked} - A new list with the elements that pass the test
* @public
*/
filter(callback, thisArg) {
if (typeof callback !== 'function') {
throw new TypeError('You must provide a function as first argument');
}
thisArg = thisArg !== undefined ? thisArg : this;
let index = 0;
return this.reduce((acc, value) => {
if (callback.call(thisArg, value, index++, thisArg)) {
acc.push(value);
}
return acc;
}, new DoublyLinked());
}
/**
* Returns the value of the first element in the list that satisfies
* the provided testing function. Otherwise undefined is returned
*
* @param {Function} callback - Function to test for each element
* @param {*} [thisArg] - Value to use as this when executing callback
* @return {*} - A value in the list if an element passes the test; otherwise, undefined
* @public
*/
find(callback, thisArg) {
if (typeof callback !== 'function') {
throw new TypeError('You must provide a function as first argument');
}
if (!this._length) {
return;
}
thisArg = thisArg !== undefined ? thisArg : this;
let tmp = this.head;
for (let i = 0; i < this.length; i++) {
if (callback.call(thisArg, tmp.value, i, thisArg)) {
this._cursor = tmp;
this._eof = false;
return tmp.value;
}
tmp = tmp.next;
}
this._cursor = undefined;
}
/**
* Executes a provided function once for each list element (from left to right)
*
* @param {Function} callback - Function to execute for each element
* @param {*} [thisArg] - Value to use as this when executing callback
* @public
*/
forEach(callback, thisArg) {
this.every((element, index, instance) => {
callback.call(this, element, index, instance);
return true;
}, thisArg);
}
/**
* Executes a provided function once for each list element (from right-to-left)
*
* @param {Function} callback - Function to execute for each element
* @param {*} [thisArg] - Value to use as this when executing callback
* @public
*/
forEachRight(callback, thisArg) {
this.everyRight((element, index, instance) => {
callback.call(this, element, index, instance);
return true;
}, thisArg);
}
/**
* Determines whether an list includes a certain element,
* returning true or false as appropriate
*
* @param {*} searchElement - The element to search for
* @param {int} [fromIndex = 0] - The position in this list at which to begin searching for searchElement
* @return {Boolean} - true if the searchElement found in the list; otherwise, false
* @public
*/
includes(searchElement, fromIndex) {
const sameValueZero = (x, y) =>
x === y ||
(typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y));
fromIndex = fromIndex || 0;
if (fromIndex < 0) {
fromIndex = this.length + fromIndex;
}
this.find(
(element, index) =>
index >= fromIndex && sameValueZero(element, searchElement),
);
return !!this.cursor;
}
/**
* Adds one or more elements right after the cursor node of the list and returns
* the new length of the list
*
* @param {*} element... - The elements to add after cursor node
* @returns {int} - The new length of the list
* @public
*/
insert(...element) {
for (const arg of element) {
const node = new Node(this, arg);
if (this._length) {
this._cursor.next = node;
node.prev = this._cursor;
this._cursor = node;
} else {
this._head = node;
this._tail = node;
this._cursor = node;
}
this._length++;
this._eof = false;
}
return this._length;
}
/**
* Joins all elements of the list into a string and returns this string
*
* @param {String} [separator=','] - Specifies a string to separate each pair of adjacent elements of the list
* @return {String} - A string with all list elements joined. If length is 0, the empty string is returned
* @public
*/
join(separator) {
separator = separator || ',';
let out = '';
this.forEach(value => {
out += (out ? separator : '') + value;
});
return out;
}
/**
* Creates a new list with the results of calling a provided function on
* every element in the calling list
*
* @param {Function} callback - Function that produces an element of the new list
* @return {DoublyLinked} - A new list with each element being the result of the callback function
* @public
*/
map(callback) {
if (typeof callback !== 'function') {
throw new TypeError('You must provide a function as first argument');
}
const out = new DoublyLinked();
this.forEach((value, index, instance) =>
out.push(callback(value, index, instance)),
);
return out.reset();
}
/**
* Moves cursor to the next and returns its value
*
* @return {*} - Returns value of next node to the cursor. If cursor reaches to the end it returns undefined
* @public
*/
next() {
if (this._cursor === this._tail) {
this._eof = true;
return undefined;
}
const c = this._cursor ? this._cursor.next : this._head;
this._cursor = c;
return c && c.value;
}
/**
* Moves cursor to the previous and returns its value
*
* @return {*} - Returns value of previous node to the cursor. If cursor reaches to the head it returns undefined
* @public
*/
prev() {
let c;
if (this._eof) {
this._eof = false;
c = this._cursor = this._tail;
return c && c.value;
}
c = this._cursor && this._cursor.prev;
this._cursor = c;
return c && c.value;
}
/**
* Removes the last element from the list and returns that element
*
* @returns {*} - The removed element from the list; undefined if the list is empty.
* @public
*/
pop() {
const ret = this._tail;
if (ret) {
ret.remove();
return ret.value;
}
}
/**
* Adds one or more elements to the end of the list and returns
* the new length of the list
*
* @param {*} element... - The elements to add to the end of the list
* @returns {int} - The new length of the list
* @public
*/
push(...element) {
if (element.length) {
this._eof = false;
}
for (const arg of element) {
const node = new Node(this, arg);
if (this._length) {
this._tail.next = node;
node.prev = this._tail;
this._tail = node;
} else {
this._head = node;
this._tail = node;
}
this._length++;
}
return this._length;
}
/**
* Applies a function against an accumulator and each element in
* the list (from left-to-right) to reduce it to a single value
*
* @param {Function} callback - Function to execute on each element in the list
* @param {*} [initialValue] - Value to use as the first argument to the first call of the callback
* @return {*} - The value that results from the reduction
* @public
*/
reduce(callback, initialValue) {
if (typeof callback !== 'function') {
throw new TypeError('You must provide a function as first argument');
}
let accumulator = initialValue;
this.forEach((value, index) => {
accumulator = callback(accumulator, value, index, this);
});
return accumulator;
}
/**
* Applies a function against an accumulator and each element in
* the list (from right-to-left) to reduce it to a single value
*
* @param {Function} callback - Function to execute on each element in the list
* @param {*} [initialValue] - Value to use as the first argument to the first call of the callback
* @return {*} - The value that results from the reduction
* @public
*/
reduceRight(callback, initialValue) {
if (typeof callback !== 'function') {
throw new TypeError('You must provide a function as first argument');
}
let accumulator = initialValue;
this.forEachRight((value, index) => {
accumulator = callback(accumulator, value, index, this);
});
return accumulator;
}
/**
* Removes an element from the list
*
* @param {*} element - The element to be removed
* @param {int} [fromIndex = 0] - The position in this list at which to begin searching for element
* @return {*} - Returns removed element if found, undefined otherwise
* @public
*/
remove(element, fromIndex) {
if (this.includes(element, fromIndex)) {
const cur = this._cursor;
cur.remove();
return cur.value;
}
}
/**
* Resets cursor to head
*
* @return {DoublyLinked} - Returns the DoublyLinked instance which this method is called
* @public
*/
reset() {
this._cursor = undefined;
this._eof = false;
return this;
}
/**
* Reverses a list in place. The first array element becomes the last, and the last list element becomes the first.
*
* @return {DoublyLinked} - Returns the DoublyLinked instance which this method is called
* @public
*/
reverse() {
let cur = this._head;
let p;
let n;
for (let i = 0; i < this._length; i++) {
p = cur.prev;
n = cur.next;
cur.prev = n;
cur.next = p;
cur = n;
}
p = this._head;
n = this._tail;
this._head = n;
this._tail = p;
this.reset();
return this;
}
/**
* Removes the first element from the list and returns that element
*
* @returns {*} - The removed element from the list; undefined if the list is empty
* @public
*/
shift() {
const ret = this._head;
if (ret) {
ret.remove();
return ret.value;
}
}
/**
* Returns a shallow copy of a portion of an array into a new array object
* selected from start to end (end not included) where start and
* end represent the index of items in that array.
*
* @param {number} [start]
* @param {number} [end]
* @returns {Array}
* @public
*/
slice(start, end) {
start = start || 0;
const acc = [];
this.every((value, index) => {
if (index >= start) {
acc.push(value);
}
return !end || index < end;
});
return acc;
}
/**
* Tests whether all elements in the list pass the test implemented by
* the provided function (from left to right)
*
* @param {Function} callback - Function to test for each element
* @param {*} [thisArg] - Value to use as this when executing callback
* @public
*/
some(callback, thisArg) {
return !this.every(
(element, index, instance) =>
!callback.call(this, element, index, instance),
thisArg,
);
}
/**
* Tests whether at least one element in the list passes the test
* implemented by the provided function (from right to left)
*
* @param {Function} callback - Function to test for each element
* @param {*} [thisArg] - Value to use as this when executing callback
* @public
*/
someRight(callback, thisArg) {
return !this.everyRight(
(element, index, instance) =>
!callback.call(this, element, index, instance),
thisArg,
);
}
/**
* Returns a new array containing elements of the list
*
* @return {Array} - A new Array instance contains elements of the list
* @public
*/
toArray() {
return this.slice();
}
/**
* Returns a string representing the specified list and its elements.
* @return {string} - Returns a string representing the specified list and its elements.
*/
toString() {
return 'DoublyLinked(' + this.join() + ')';
}
/**
* Adds one or more elements to the beginning of the list
* the new length of the list
*
* @param {*} element... - The elements to add to the front of the list
* @returns {int} - The new length of the list
* @public
*/
unshift(...element) {
for (const arg of element) {
const node = new Node(this, arg);
if (this._length) {
this._head.prev = node;
node.next = this._head;
this._head = node;
} else {
this._head = node;
this._tail = node;
}
this._length++;
}
return this._length;
}
/**
* Returns the iterator object contains entries
*
* @return {Object} - Returns the iterator object contains entries
*/
[Symbol.iterator]() {
let _cursor;
return {
next: () => {
_cursor = _cursor ? _cursor.next : this.head;
return {
value: _cursor && _cursor.value,
done: !_cursor,
};
},
};
}
}
/**
*
* @constructor
*/
class Node {
constructor(list, value) {
this.list = list;
this.value = value;
this.prev = undefined;
this.next = undefined;
}
remove() {
if (!this.list) {
return;
}
if (this.prev) {
// noinspection JSUnresolvedVariable
this.prev.next = this.next;
}
if (this.next) {
// noinspection JSUnresolvedVariable
this.next.prev = this.prev;
}
if (this === this.list._cursor) {
this.list._cursor = this.next || this.prev;
}
if (this === this.list._head) {
this.list._head = this.next;
}
if (this === this.list._tail) {
this.list._tail = this.prev;
}
this.list._length--;
this.prev = undefined;
this.next = undefined;
this.list = undefined;
}
}
/**
* Expose `DoublyLinked`.
*/
module.exports = DoublyLinked;