UNPKG

@tsdotnet/linked-node-list

Version:

An unprotected bi-directional linked list. Useful for implementing other collections.

417 lines 13.1 kB
"use strict"; /* * @author electricessence / https://github.com/electricessence/ * Licensing: MIT */ Object.defineProperty(exports, "__esModule", { value: true }); exports.LinkedValueNodeList = exports.LinkedNodeList = void 0; const tslib_1 = require("tslib"); const InvalidOperationException_1 = tslib_1.__importDefault(require("@tsdotnet/exceptions/dist/InvalidOperationException")); const ArgumentNullException_1 = tslib_1.__importDefault(require("@tsdotnet/exceptions/dist/ArgumentNullException")); const ArgumentException_1 = tslib_1.__importDefault(require("@tsdotnet/exceptions/dist/ArgumentException")); const IterableCollectionBase_1 = tslib_1.__importDefault(require("@tsdotnet/collection-base/dist/IterableCollectionBase")); const collection_base_1 = require("@tsdotnet/collection-base"); /* eslint-disable @typescript-eslint/no-this-alias */ /***************************** * IMPORTANT NOTES ABOUT PERFORMANCE: * http://jsperf.com/simulating-a-queue * * Adding to an array is very fast, but modifying is slow. * LinkedList wins when modifying contents. * http://stackoverflow.com/questions/166884/array-versus-linked-list *****************************/ /** * This class is useful for managing a list of linked nodes, but it does not protect against modifying individual links. * If the consumer modifies a link (sets the previous or next value) it will effectively break the collection. * * It is possible to declare a node type of any kind as long as it contains a previous and next value that can reference another node. * Although not as safe as a protected LinkedList, this class has less overhead and is more flexible. * * The count (or length) of this `LinkedNodeList` is tracked as `.unsafeCount` and calling `.getCount()` will iterate the list. * * @template TNode The node type. */ class LinkedNodeList extends IterableCollectionBase_1.default { constructor() { super(); this._unsafeCount = 0; } /** * Returns the tracked number of nodes in the list. * Since a `LinkedNodeList` is unprotected, it is possible to modify the chain and this count could get out of sync. * To know the actual number of nodes, call `.getCount()` to iterate over each node. * @returns {number} */ get unsafeCount() { return this._unsafeCount; } /** * Returns the first node or undefined if the collection is empty. * @return The first node or undefined. */ get first() { return this._first; } /** * Returns last node or be undefined if the collection is empty. * @return The last node or undefined. */ get last() { return this._last; } /** * Erases the linked node's references to each other and returns the number of nodes. * @returns {number} */ clear() { let n = this._first; let cF = 0, cL = 0; // First, clear in the forward direction. this._first = undefined; while (n) { cF++; const current = n; n = n.next; current.next = undefined; } // Last, clear in the reverse direction. n = this._last; this._last = undefined; while (n) { cL++; const current = n; n = n.previous; current.previous = undefined; } if (cF !== cL) console.warn('LinkedNodeList: Forward versus reverse count does not match when clearing. Forward: ' + cF + ', Reverse: ' + cL); this.incrementVersion(); this._unsafeCount = 0; return cF; } /** * Removes the specified node. * Returns true if successful and false if not found (already removed). * @param node * @returns {boolean} */ removeNode(node) { if (!node) throw new ArgumentNullException_1.default('node'); const _ = this, prev = node.previous, next = node.next; let a = false, b = false; if (prev) prev.next = next; else if (_._first == node) _._first = next; else a = true; if (next) next.previous = prev; else if (_._last == node) _._last = prev; else b = true; if (a !== b) { throw new ArgumentException_1.default('node', `Provided node is has no ${a ? 'previous' : 'next'} reference but is not the ${a ? 'first' : 'last'} node!`); } const removed = !a && !b; if (removed) { _.incrementVersion(); _._unsafeCount--; node.previous = undefined; node.next = undefined; } return removed; } /** * Clears the list. * Provided for use with dispose helpers. */ dispose() { this.clear(); } /** * Clears the list. * Provided for use with object pools. */ recycle() { this.clear(); } /** * Iterates the list to see if a node exists. * @param node * @returns {boolean} */ contains(node) { if (!node) throw new ArgumentNullException_1.default('node'); return this.indexOf(node) != -1; } /** * Iterates the list returns the node at the index requested. * Returns undefined if the index is out of range. * @param index * @returns The node at the index requested or undefined. */ getNodeAt(index) { if (index < 0 || !isFinite(index)) return undefined; let next = this._first; let i = 0; while (next && i++ < index) { next = next.next; } return next; } /** * Iterates the list to find the specific node that matches the predicate condition. * @param {PredicateWithIndex} condition * @returns The found node or undefined. */ find(condition) { let i = 0; for (const e of this) { if (condition(e, i++)) return e; } return undefined; } /** * Iterates the list to find the specified node and returns its index. * @param node * @returns {boolean} */ indexOf(node) { if (node && (node.previous || node.next)) { let index = 0; let c, n = this._first; do { c = n; if (c === node) return index; index++; } while ((n = c && c.next)); } return -1; } /** * Inserts a node before the specified 'before' node. * If no 'before' node is specified, it inserts it as the first node. * @param node * @param before * @returns {LinkedNodeList} */ addNodeBefore(node, before) { assertValidDetached(node); const _ = this; if (!before) { before = _._first; } if (before) { const prev = before.previous; node.previous = prev; node.next = before; before.previous = node; if (prev) prev.next = node; if (before == _._first) _._first = node; } else { _._first = _._last = node; } _.incrementVersion(); _._unsafeCount++; return this; } /** * Removes the first node and returns it if successful. * Returns undefined if the collection is empty. * @return The node that was removed, or undefined if the collection is empty. */ takeFirst() { const node = this._first; if (!node) return undefined; if (node.previous) throw new Error('Collection is corrupted: first node has previous node.'); if (!this.removeNode(node)) throw new Error('Collection is corrupted: unable to remove first node.'); return node; } /** * Removes the last node and returns it if successful. * Returns undefined if the collection is empty. * @return The node that was removed, or undefined if the collection is empty. */ takeLast() { const node = this._last; if (!node) return undefined; if (node.next) throw new Error('Collection is corrupted: last node has next node.'); if (!this.removeNode(node)) throw new Error('Collection is corrupted: unable to remove last node.'); return node; } /** * Removes the first node and returns true if successful. * @returns {boolean} */ removeFirst() { return !!this.takeFirst(); } /** * Removes the last node and returns true if successful. * @returns {boolean} */ removeLast() { return !!this.takeLast(); } /** * Adds a node to the end of the list. * @param node * @returns {LinkedNodeList} */ addNode(node) { this.addNodeAfter(node); return this; } /** * Inserts a node after the specified 'after' node. * If no 'after' node is specified, it appends it as the last node. * @param node * @param after * @returns {LinkedNodeList} */ addNodeAfter(node, after) { assertValidDetached(node); const _ = this; if (!after) { after = _._last; } if (after) { const next = after.next; node.next = next; node.previous = after; after.next = node; if (next) next.previous = node; if (after == _._last) _._last = node; } else { _._first = _._last = node; } _.incrementVersion(); _._unsafeCount++; return _; } /** * Takes and existing node and replaces it. * @param node * @param replacement * @returns {any} */ replace(node, replacement) { if (node == replacement) return this; assertValidDetached(replacement, 'replacement'); const _ = this; replacement.previous = node.previous; replacement.next = node.next; if (node.previous) node.previous.next = replacement; if (node.next) node.next.previous = replacement; if (node == _._first) _._first = replacement; if (node == _._last) _._last = replacement; _.incrementVersion(); return _; } /** * Iterable for iterating this collection in reverse order. * @return {Iterable<ProtectedLinkedNode>} */ get reversed() { const _ = this; return (_._reversed || (_._reversed = Object.freeze(collection_base_1.ExtendedIterable.create({ *[Symbol.iterator]() { let current, prev = _.last; while (prev) { current = prev; prev = current.previous; yield current; } } })))); } *_getIterator() { let current, next = this.first; while (next) { current = next; next = current.next; yield current; } } } exports.LinkedNodeList = LinkedNodeList; /** * This class covers most LinkedNodeList use cases by assuming the node type includes a '.value' property. */ class LinkedValueNodeList extends LinkedNodeList { /** * Adds a node with the given value to the start of the list. * Becomes the first node. * @param value * @return {this} */ prependValue(value) { this.addNodeBefore({ value: value }); return this; } /** * Adds a node with the given value to the end of the list. * Becomes the last node. * @param value * @return {this} */ appendValue(value) { return this.addNode({ value: value }); } /** * Returns an iterable that selects the value of each node. * @returns {Iterable} */ *getValues() { for (const node of this) { yield node.value; } } /** * Copies the values of each node to an array (or array-like object). * @param {TDestination} array The target array. * @param {number} index The starting index of the target array. * @returns {TDestination} The target array. */ copyValuesTo(array, index = 0) { if (!array) throw new ArgumentNullException_1.default('array'); let i = 0; for (const node of this) { array[index + i++] = node.value; } return array; } } exports.LinkedValueNodeList = LinkedValueNodeList; function assertValidDetached(node, propName = 'node') { if (!node) throw new ArgumentNullException_1.default(propName); if (node.next || node.previous) throw new InvalidOperationException_1.default('Cannot add a node to a LinkedNodeList that is already linked.'); } //# sourceMappingURL=LinkedNodeList.js.map