const { Collection, CollectionEvent } = require('./Collection')
, { EqualityComparer } = require('./EqualityComparer')
, { Observable, fromEvent } = require('rxjs')
, symbolLinkedListAdd = Symbol('linkedListAdd')
, symbolLinkedListRemove = Symbol('linkedListRemove');
/**
* @template T
* @extends {CollectionEvent<T>}
*/
class LinkedListEvent extends CollectionEvent {
/**
* @param {LinkedListNode<T>} node
* @param {Boolean} add True, if the node was added. If false, the node was removed.
* @param {Boolean} first True, iff the node is the first node now
* @param {Boolean} last True, iff the node is the last node now
*/
constructor(node, add, first, last) {
super(node);
/** @type {LinkedListNode<T>} */
this.item = node;
/** @protected */
this._add = add;
/** @protected */
this._first = first;
/** @protected */
this._last = last;
};
/**
* Returns whether this represents an add-event.
*
* @type {Boolean}
*/
get added() {
return this._add;
};
/**
* Returns whether this represents a remove-event.
*
* @type {Boolean}
*/
get removed() {
return !this._add;
};
/**
* Returns whether the node is the first now.
*
* @type {Boolean}
*/
get firstNode() {
return this._first;
};
/**
* Returns whether the node is the last now.
*
* @type {Boolean}
*/
get lastNode() {
return this._last;
};
};
/**
* @template T
* @author Sebastian Hönel <development@hoenel.net>
*/
class LinkedListNode {
/**
* @param {T} item
* @param {LinkedList<T>} list
* @param {LinkedListNode<T>} prev
* @param {LinkedListNode<T>} next
*/
constructor(item, list, prev = null, next = null) {
this.item = item;
this.list = list;
this.prev = prev;
this.next = next;
};
};
/**
* @template T
* @extends {Collection<T>}
* @author Sebastian Hönel <development@hoenel.net>
*/
class LinkedList extends Collection {
/**
* Creates a new, empty doubly-linked LinkedList.
*
* @param {EqualityComparer<T>} [eqComparer] Optional. Defaults To EqualityComparer<T>.default.
*/
constructor(eqComparer = EqualityComparer.default) {
super(eqComparer);
/**
* @type {LinkedListNode<T>}
* @protected
*/
this._firstNode = null;
/**
* @type {LinkedListNode<T>}
* @protected
*/
this._lastNode = null;
/**
* @type {Set<LinkedListNode<T>>}
* @protected
*/
this._lookupSet = new Set();
/** @protected */
this._size = 0;
/** @type {Observable<LinkedListEvent<T>>} */
this.observableNodeAdd = Object.freeze(fromEvent(this, symbolLinkedListAdd));
/** @type {Observable<LinkedListEvent<T>>} */
this.observableNodeRemove = Object.freeze(fromEvent(this, symbolLinkedListRemove));
};
/**
* @protected
* @throws {Error} If this LinkedList is empty.
* @returns {Boolean} true
*/
_requireNotEmpty() {
if (this._firstNode === null) {
throw new Error('The LinkedList is empty.');
}
return true;
};
/**
* @protected
* @throws {Error} If the given node is not a LinkedListNode.
* @param {LinkedListNode<T>} node
*/
_requireIsNode(node) {
if (!(node instanceof LinkedListNode)) {
throw new Error('The node is not a LinkedListNode.');
}
return true;
};
/**
* @protected
* @param {LinkedListNode<T>} node
* @param {T} item
* @param {Boolean} after
* @returns {LinkedListNode<T>} The newly added node.
*/
_add(node, item, after = true) {
if (!this.hasNode(node)) {
throw new Error('The node given is not a member of this LinkedList.');
}
const newNode = new LinkedListNode(
item, this, after ? node : node.prev, after ? node.next : node);
if (newNode.prev instanceof LinkedListNode) {
newNode.prev.next = newNode;
if (newNode.prev.prev === null) {
this._firstNode = newNode.prev;
}
} else {
this._firstNode = newNode;
}
if (newNode.next instanceof LinkedListNode) {
newNode.next.prev = newNode;
if (newNode.next.next === null) {
this._lastNode = newNode.next;
}
} else {
this._lastNode = newNode;
}
this._lookupSet.add(newNode);
this._size++;
this.emit(symbolLinkedListAdd, new LinkedListEvent(
newNode, true, this.first === newNode, this.last === newNode));
return newNode;
};
/**
* @override
* @inheritDoc
* @type {Number}
*/
get size() {
return this._size;
};
/**
* @override
* @inheritDoc
* @type {Boolean}
*/
get isEmpty() {
return this._size === 0;
};
/**
* @type {LinkedListNode<T>}
*/
get first() {
this._requireNotEmpty();
return this._firstNode;
};
/**
* @type {LinkedListNode<T>}
*/
get last() {
this._requireNotEmpty();
return this._lastNode;
};
/**
* @returns {IterableIterator<LinkedListNode<T>>}
*/
*nodes() {
let node = this._firstNode;
while (node instanceof LinkedListNode) {
yield node;
node = node.next;
}
};
/**
* @returns {IterableIterator<LinkedListNode<T>>}
*/
*nodesReversed() {
let node = this._lastNode;
while (node instanceof LinkedListNode) {
yield node;
node = node.prev;
}
};
/**
* @returns {IterableIterator<T>}
*/
*entries() {
for (const node of this.nodes()) {
yield node.item;
}
};
/**
* @returns {IterableIterator<T>}
*/
*entriesReversed() {
for (const node of this.nodesReversed()) {
yield node.item;
}
};
/**
* @override
* @inheritDoc
* @param {T} item
* @param {EqualityComparer<T>} [eqComparer] an optional EqualityComparer to use. If not provided, will use the Collection's EqualityComparer.
* @returns {Boolean}
*/
has(item, eqComparer = null) {
eqComparer = eqComparer instanceof EqualityComparer ? eqComparer : this._eqComparer;
for (const value of this.entries()) {
if (eqComparer.equals(item, value)) {
return true;
}
}
return false;
};
/**
* Returns a value indicating whether or not this LinkedList has
* the node provided.
*
* @param {LinkedListNode<T>} node
* @returns {Boolean}
*/
hasNode(node) {
this._requireIsNode(node);
return this._lookupSet.has(node);
};
/**
* @override
* @inheritDoc
* @returns {this}
*/
clear() {
super.clear();
this._firstNode = null;
this._lastNode = null;
this._size = 0;
this._lookupSet.clear();
return this;
};
/**
* @param {T} item
* @returns {LinkedListNode<T>} The node that has been created.
*/
addFirst(item) {
if (this.isEmpty) {
this._size++;
this._firstNode = this._lastNode = new LinkedListNode(item, this);
this._lookupSet.add(this._firstNode);
this.emit(symbolLinkedListAdd, new LinkedListEvent(
this._firstNode, true, true, true));
return this._firstNode;
} else {
return this._add(this.first, item, false);
}
};
/**
* @param {T} item
* @returns {LinkedListNode<T>} The node that has been created.
*/
addLast(item) {
if (this.isEmpty) {
this._size++;
this._lastNode = this._firstNode = new LinkedListNode(item, this);
this._lookupSet.add(this._lastNode);
this.emit(symbolLinkedListAdd, new LinkedListEvent(
this._lastNode, true, true, true));
return this._lastNode;
} else {
return this._add(this.last, item, true);
}
};
/**
* @param {LinkedListNode<T>} node
* @param {T} item
* @returns {LinkedListNode<T>} The newly created and inserted node.
*/
addAfter(node, item) {
return this._add(node, item, true);
};
/**
* @param {LinkedListNode<T>} node
* @param {T} item
*/
addBefore(node, item) {
return this._add(node, item, false);
};
/**
* @param {LinkedListNode<T>} node
* @returns {LinkedListNode} The removed node
*/
remove(node) {
if (!this.hasNode(node)) {
throw new Error('This LinkedList does not have the given node.');
}
/**
* @param {LinkedListNode<T>} theNode
*/
const checkHeadTail = theNode => {
if (theNode.prev === null) {
this._firstNode = theNode;
}
if (theNode.next === null) {
this._lastNode = theNode;
};
}
let wasFirst = true, wasLast = true;
if (node.prev instanceof LinkedListNode) {
node.prev.next = node.next instanceof LinkedListNode ? node.next : null;
checkHeadTail(node.prev);
wasFirst = false;
}
if (node.next instanceof LinkedListNode) {
node.next.prev = node.prev instanceof LinkedListNode ? node.prev : null;
checkHeadTail(node.next);
wasLast = false;
}
node.next = node.prev = null;
this._size--;
this._lookupSet.delete(node);
this.emit(symbolLinkedListRemove, new LinkedListEvent(
node, false, wasFirst, wasLast));
return node;
};
/**
* @returns {LinkedListNode<T>} The first node after it has been removed
*/
removeFirst() {
return this.remove(this.first);
};
/**
* @returns {LinkedListNode<T>} The last node after it has been removed.
*/
removeLast() {
return this.remove(this.last);
};
};
module.exports = Object.freeze({
LinkedList,
LinkedListNode,
LinkedListEvent,
symbolLinkedListAdd,
symbolLinkedListRemove
});