feat(core): add doubly linked list as shared utility

pull/3359/head
Arman Ozak 6 years ago
parent 439ca98c82
commit 75a13a0aa4

@ -0,0 +1,720 @@
import { LinkedList } from '../utils/linked-list';
describe('Linked List (Doubly)', () => {
let list: LinkedList;
beforeEach(() => (list = new LinkedList()));
describe('#length', () => {
it('should initially be 0', () => {
expect(list.length).toBe(0);
});
});
describe('#head', () => {
it('should initially be undefined', () => {
expect(list.head).toBeUndefined();
});
});
describe('#tail', () => {
it('should initially be undefined', () => {
expect(list.tail).toBeUndefined();
});
});
describe('#add', () => {
describe('#head', () => {
it('should add node to the head of the list', () => {
list.addHead('a');
// "a"
expect(list.head.value).toBe('a');
expect(list.tail.value).toBe('a');
});
it('should create reference to previous and next nodes', () => {
list.add('a').head();
list.add('b').head();
list.add('c').head();
// "c" <-> "b" <-> "a"
expect(list.length).toBe(3);
expect(list.head.value).toBe('c');
expect(list.head.next.value).toBe('b');
expect(list.head.previous).toBeUndefined();
expect(list.tail.value).toBe('a');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.next).toBeUndefined();
});
});
describe('#tail', () => {
it('should add node to the tail of the list', () => {
list.addTail('a');
// "a"
expect(list.head.value).toBe('a');
expect(list.tail.value).toBe('a');
expect(list.tail.next).toBeUndefined();
});
it('should create reference to previous and next nodes', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.previous).toBeUndefined();
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.next).toBeUndefined();
});
});
describe('#after', () => {
it('should place a node after node with given value', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
list.add('x').after('b');
// "a" <-> "b" <-> "x" <-> "c"
expect(list.length).toBe(4);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('x');
expect(list.head.next.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('x');
expect(list.tail.previous.previous.value).toBe('b');
expect(list.tail.previous.previous.previous.value).toBe('a');
});
it('should be able to receive a custom compareFn', () => {
list.add({ x: 1 }).tail();
list.add({ x: 2 }).tail();
list.add({ x: 3 }).tail();
// {"x":1} <-> {"x":2} <-> {"x":3}
list.add({ x: 0 }).after({ x: 1 }, (v1: X, v2: X) => v1.x === v2.x);
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":3}
expect(list.length).toBe(4);
expect(list.head.value.x).toBe(1);
expect(list.head.next.value.x).toBe(0);
expect(list.head.next.next.value.x).toBe(2);
expect(list.head.next.next.next.value.x).toBe(3);
expect(list.tail.value.x).toBe(3);
expect(list.tail.previous.value.x).toBe(2);
expect(list.tail.previous.previous.value.x).toBe(0);
expect(list.tail.previous.previous.previous.value.x).toBe(1);
});
});
describe('#before', () => {
it('should place a node before node with given value', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
list.add('x').before('b');
// "a" <-> "x" <-> "b" <-> "c"
expect(list.length).toBe(4);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('x');
expect(list.head.next.next.value).toBe('b');
expect(list.head.next.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('x');
expect(list.tail.previous.previous.previous.value).toBe('a');
});
it('should be able to receive a custom compareFn', () => {
list.add({ x: 1 }).tail();
list.add({ x: 2 }).tail();
list.add({ x: 3 }).tail();
// {"x":1} <-> {"x":2} <-> {"x":3}
list.add({ x: 0 }).before({ x: 2 }, (v1: X, v2: X) => v1.x === v2.x);
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":3}
expect(list.length).toBe(4);
expect(list.head.value.x).toBe(1);
expect(list.head.next.value.x).toBe(0);
expect(list.head.next.next.value.x).toBe(2);
expect(list.head.next.next.next.value.x).toBe(3);
expect(list.tail.value.x).toBe(3);
expect(list.tail.previous.value.x).toBe(2);
expect(list.tail.previous.previous.value.x).toBe(0);
expect(list.tail.previous.previous.previous.value.x).toBe(1);
});
});
describe('#byIndex', () => {
it('should place a node at given index', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
list.add('x').byIndex(1);
// "a" <-> "x" <-> "b" <-> "c"
list.add('y').byIndex(3);
// "a" <-> "x" <-> "b" <-> "y" <-> "c"
expect(list.length).toBe(5);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('x');
expect(list.head.next.next.value).toBe('b');
expect(list.head.next.next.next.value).toBe('y');
expect(list.head.next.next.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('y');
expect(list.tail.previous.previous.value).toBe('b');
expect(list.tail.previous.previous.previous.value).toBe('x');
expect(list.tail.previous.previous.previous.previous.value).toBe('a');
});
});
});
describe('#find', () => {
it('should return the first node found based on given predicate', () => {
list.add('a').tail();
list.add('x').tail();
list.add('b').tail();
list.add('x').tail();
list.add('c').tail();
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
const node1 = list.find(value => value === 'x');
expect(node1.value).toBe('x');
expect(node1.previous.value).toBe('a');
expect(node1.next.value).toBe('b');
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
const node2 = list.find((_, index) => index === 3);
expect(node2.value).toBe('x');
expect(node2.previous.value).toBe('b');
expect(node2.next.value).toBe('c');
});
it('should return undefined when list is empty', () => {
const node = list.find(value => value === 'x');
expect(node).toBeUndefined();
});
it('should return undefined when predicate finds no match', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
const node = list.find(value => value === 'x');
expect(node).toBeUndefined();
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
});
});
describe('#findIndex', () => {
it('should return the index of the first node found based on given predicate', () => {
list.add('a').tail();
list.add('x').tail();
list.add('b').tail();
list.add('x').tail();
list.add('c').tail();
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
const index1 = list.findIndex(value => value === 'x');
expect(index1).toBe(1);
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
let timesFound = 0;
const index2 = list.findIndex(value => {
if (timesFound > 1) return false;
timesFound += Number(value === 'x');
return timesFound > 1;
});
expect(index2).toBe(3);
});
it('should return -1 when list is empty', () => {
const index = list.findIndex(value => value === 'x');
expect(index).toBe(-1);
});
it('should return -1 when no match is found', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
const index = list.findIndex(value => value === 'x');
expect(index).toBe(-1);
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
});
});
describe('#forEach', () => {
it('should call given function for each node of the list', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
const spy = jest.fn();
list.forEach(spy);
expect(spy.mock.calls).toEqual([
['a', 0, list],
['b', 1, list],
['c', 2, list],
]);
});
it('should not call given function when list is empty', () => {
const spy = jest.fn();
list.forEach(spy);
expect(spy).not.toHaveBeenCalled();
});
});
describe('#drop', () => {
describe('#head', () => {
it('should return undefined when there is no head', () => {
expect(list.drop().head()).toBeUndefined();
});
it('should remove the node from the head of the list', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
list.drop().head();
// "b" <-> "c"
expect(list.length).toBe(2);
expect(list.head.value).toBe('b');
expect(list.head.next.value).toBe('c');
expect(list.head.previous).toBeUndefined();
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.next).toBeUndefined();
});
});
describe('#head', () => {
it('should return undefined when there is no tail', () => {
expect(list.drop().tail()).toBeUndefined();
});
it('should remove the node from the tail of the list', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
list.drop().tail();
// "a" <-> "b"
expect(list.length).toBe(2);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.previous).toBeUndefined();
expect(list.tail.value).toBe('b');
expect(list.tail.previous.value).toBe('a');
expect(list.tail.next).toBeUndefined();
});
});
describe('#byIndex', () => {
it('should remove the node at given index', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
list.add('d').tail();
list.add('e').tail();
// "a" <-> "b" <-> "c" <-> "d" <-> "e"
list.drop().byIndex(1);
// "a" <-> "c" <-> "d" <-> "e"
list.drop().byIndex(2);
// "a" <-> "c" <-> "e"
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('c');
expect(list.head.next.next.value).toBe('e');
expect(list.tail.value).toBe('e');
expect(list.tail.previous.value).toBe('c');
expect(list.tail.previous.previous.value).toBe('a');
});
it('should return undefined when list is empty', () => {
const node = list.drop().byIndex(0);
expect(node).toBeUndefined();
});
it('should return undefined when given index does not exist', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
const node1 = list.drop().byIndex(4);
// "a" <-> "b" <-> "c"
expect(node1).toBeUndefined();
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
// "a" <-> "b" <-> "c"
const node2 = list.drop().byIndex(-1);
// "a" <-> "b" <-> "c"
expect(node2).toBeUndefined();
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
});
});
describe('#byValue', () => {
it('should remove the first node with given value', () => {
list.add('a').tail();
list.add('x').tail();
list.add('b').tail();
list.add('x').tail();
list.add('c').tail();
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
list.drop().byValue('x');
// "a" <-> "b" <-> "x" <-> "c"
expect(list.length).toBe(4);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('x');
expect(list.head.next.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('x');
expect(list.tail.previous.previous.value).toBe('b');
expect(list.tail.previous.previous.previous.value).toBe('a');
// "a" <-> "b" <-> "x" <-> "c"
list.drop().byValue('x');
// "a" <-> "b" <-> "c"
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
});
it('should be able to receive a custom compareFn', () => {
list.add({ x: 1 }).tail();
list.add({ x: 2 }).tail();
list.add({ x: 3 }).tail();
// {"x":1} <-> {"x":2} <-> {"x":3}
list.drop().byValue({ x: 2 }, (v1: X, v2: X) => v1.x === v2.x);
// {"x":1} <-> {"x":3}
expect(list.length).toBe(2);
expect(list.head.value.x).toBe(1);
expect(list.head.next.value.x).toBe(3);
expect(list.tail.value.x).toBe(3);
expect(list.tail.previous.value.x).toBe(1);
});
it('should return undefined when list is empty', () => {
const node = list.drop().byValue('x');
expect(node).toBeUndefined();
});
it('should return undefined when given value is not found', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
const node = list.drop().byValue('x');
// "a" <-> "b" <-> "c"
expect(node).toBeUndefined();
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
});
});
});
describe('#get', () => {
it('should return node at given index', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
const node = list.get(1);
expect(node.value).toBe('b');
expect(node.previous.value).toBe('a');
expect(node.next.value).toBe('c');
});
it('should return undefined when list is empty', () => {
const node = list.get(1);
expect(node).toBeUndefined();
});
it('should return undefined when predicate finds no match', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
const node1 = list.get(4);
expect(node1).toBeUndefined();
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
// "a" <-> "b" <-> "c"
const node2 = list.get(-1);
expect(node2).toBeUndefined();
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
});
});
describe('#indexOf', () => {
it('should return the index of the first node found based on given value', () => {
list.add('a').tail();
list.add('x').tail();
list.add('b').tail();
list.add('x').tail();
list.add('c').tail();
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
const index1 = list.indexOf('x');
expect(index1).toBe(1);
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
let timesFound = 0;
const index2 = list.indexOf('x', (v1: string, v2: string) => {
if (timesFound > 1) return false;
timesFound += Number(v1 === v2);
return timesFound > 1;
});
expect(index2).toBe(3);
});
it('should be able to receive a custom compareFn', () => {
list.add({ x: 1 }).tail();
list.add({ x: 2 }).tail();
list.add({ x: 3 }).tail();
// {"x":1} <-> {"x":2} <-> {"x":3}
const index = list.indexOf({ x: 2 }, (v1: X, v2: X) => v1.x === v2.x);
expect(index).toBe(1);
});
it('should return -1 when list is empty', () => {
const index = list.indexOf('x');
expect(index).toBe(-1);
});
it('should return -1 when no match is found', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
const index = list.indexOf('x');
expect(index).toBe(-1);
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
});
});
describe('#toArray', () => {
it('should return array representation', () => {
list.addTail('a');
list.addTail(2);
list.addTail('c');
list.addTail({ k: 4, v: 'd' });
// "a" <-> 2 <-> "c" <-> {"k":4,"v":"d"}
const arr = list.toArray();
expect(arr).toEqual(['a', 2, 'c', { k: 4, v: 'd' }]);
});
it('should return empty array when list is empty', () => {
const arr = list.toArray();
expect(arr).toEqual([]);
});
});
describe('#toString', () => {
it('should return string representation', () => {
list.addTail('a');
list.addTail(2);
list.addTail('c');
list.addTail({ k: 4, v: 'd' });
// "a" <-> 2 <-> "c" <-> {"k":4,"v":"d"}
const str = list.toString();
expect(str).toBe('"a" <-> 2 <-> "c" <-> {"k":4,"v":"d"}');
});
it('should return empty string when list is empty', () => {
const str = list.toString();
expect(str).toBe('');
});
});
it('should be iterable', () => {
list.addTail('a');
list.addTail('b');
list.addTail('c');
// "a" <-> "b" <-> "c"
const arr = [];
for (let value of list) {
arr.push(value);
}
expect(arr).toEqual(['a', 'b', 'c']);
});
});
interface X {
[k: string]: any;
}

@ -1,5 +1,6 @@
export * from './common-utils';
export * from './generator-utils';
export * from './initial-utils';
export * from './linked-list';
export * from './route-utils';
export * from './rxjs-utils';

@ -0,0 +1,240 @@
import compare from 'just-compare';
export class ListNode<T = any> {
readonly value: T;
next: ListNode | undefined;
previous: ListNode | undefined;
constructor(value: T) {
this.value = value;
}
}
export class LinkedList<T = any> {
private first: ListNode<T> | undefined;
private last: ListNode<T> | undefined;
private size = 0;
get head(): ListNode<T> | undefined {
return this.first;
}
get tail(): ListNode<T> | undefined {
return this.last;
}
get length(): number {
return this.size;
}
private linkWith(value: T, previousNode: ListNode<T>, nextNode: ListNode<T>): ListNode<T> {
const node = new ListNode(value);
if (!previousNode) return this.addHead(value);
if (!nextNode) return this.addTail(value);
node.previous = previousNode;
previousNode.next = node;
node.next = nextNode;
nextNode.previous = node;
this.size += 1;
return node;
}
add(value: T) {
return {
after: (previousValue: T, compareFn = compare) => {
return this.addAfter(value, previousValue, compareFn);
},
before: (nextValue: T, compareFn = compare) => {
return this.addBefore(value, nextValue, compareFn);
},
byIndex: (position: number): ListNode<T> => {
return this.addByIndex(value, position);
},
head: (): ListNode<T> => {
return this.addHead(value);
},
tail: (): ListNode<T> => {
return this.addTail(value);
},
};
}
addAfter(value: T, previousValue: T, compareFn = compare): ListNode<T> {
const previous = this.find(currentValue => compareFn(currentValue, previousValue));
return previous ? this.linkWith(value, previous, previous.next) : this.addTail(value);
}
addBefore(value: T, nextValue: T, compareFn = compare): ListNode<T> {
const next = this.find(currentValue => compareFn(currentValue, nextValue));
return next ? this.linkWith(value, next.previous, next) : this.addHead(value);
}
addByIndex(value: T, position: number): ListNode<T> {
if (position <= 0) return this.addHead(value);
if (position >= this.size) return this.addTail(value);
const next = this.get(position)!;
return this.linkWith(value, next.previous, next);
}
addHead(value: T): ListNode<T> {
const node = new ListNode(value);
node.next = this.first;
if (this.first) this.first.previous = node;
else this.last = node;
this.first = node;
this.size += 1;
return node;
}
addTail(value: T): ListNode<T> {
const node = new ListNode(value);
if (this.first) {
node.previous = this.last;
this.last!.next = node;
this.last = node;
} else {
this.first = node;
this.last = node;
}
this.size += 1;
return node;
}
drop() {
return {
byIndex: (position: number) => this.dropByIndex(position),
byValue: (value: T, compareFn = compare) => this.dropByValue(value, compareFn),
head: () => this.dropHead(),
tail: () => this.dropTail(),
};
}
dropByIndex(position: number): ListNode<T> | undefined {
if (position === 0) return this.dropHead();
else if (position === this.size - 1) return this.dropTail();
const current = this.get(position);
if (current) {
current.previous!.next = current.next;
current.next!.previous = current.previous;
this.size -= 1;
return current;
}
return undefined;
}
dropByValue(value: T, compareFn = compare): ListNode<T> | undefined {
const position = this.findIndex(currentValue => compareFn(currentValue, value));
if (position < 0) return undefined;
return this.dropByIndex(position);
}
dropHead(): ListNode<T> | undefined {
const head = this.first;
if (head) {
this.first = head.next;
if (this.first) this.first.previous = undefined;
else this.last = undefined;
this.size -= 1;
return head;
}
return undefined;
}
dropTail(): ListNode<T> | undefined {
const tail = this.last;
if (tail) {
this.last = tail.previous;
if (this.last) this.last.next = undefined;
else this.first = undefined;
this.size -= 1;
return tail;
}
return undefined;
}
find(predicate: ListIteratorFunction<T>): ListNode<T> | undefined {
for (let current = this.first, position = 0; current; position += 1, current = current.next) {
if (predicate(current.value, position, this)) return current;
}
return undefined;
}
findIndex(predicate: ListIteratorFunction<T>): number {
for (let current = this.first, position = 0; current; position += 1, current = current.next) {
if (predicate(current.value, position, this)) return position;
}
return -1;
}
forEach<R = boolean>(callback: ListIteratorFunction<T, R>) {
for (let node = this.first, position = 0; node; position += 1, node = node.next) {
callback(node.value, position, this);
}
}
get(position: number): ListNode<T> | undefined {
return this.find((_, index) => position === index);
}
indexOf(value: T, compareFn = compare): number {
return this.findIndex(currentValue => compareFn(currentValue, value));
}
toArray(): T[] {
const array = new Array(this.size);
this.forEach((value, index) => (array[index!] = value));
return array;
}
toString(): string {
return this.toArray()
.map(value => JSON.stringify(value))
.join(' <-> ');
}
*[Symbol.iterator]() {
for (let node = this.first, position = 0; node; position += 1, node = node.next) {
yield node.value;
}
}
}
export type ListIteratorFunction<T = any, R = boolean> = (
value: T,
index?: number,
list?: LinkedList,
) => R;
Loading…
Cancel
Save