From 75a13a0aa4acab0471e92ca95a1904a9ed4efd9e Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 25 Mar 2020 17:54:11 +0300 Subject: [PATCH] feat(core): add doubly linked list as shared utility --- .../core/src/lib/tests/linked-list.spec.ts | 720 ++++++++++++++++++ .../packages/core/src/lib/utils/index.ts | 1 + .../core/src/lib/utils/linked-list.ts | 240 ++++++ 3 files changed, 961 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts create mode 100644 npm/ng-packs/packages/core/src/lib/utils/linked-list.ts diff --git a/npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts new file mode 100644 index 0000000000..cd0a00096e --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts @@ -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; +} diff --git a/npm/ng-packs/packages/core/src/lib/utils/index.ts b/npm/ng-packs/packages/core/src/lib/utils/index.ts index 0043152ada..a112d9acd8 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/index.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/index.ts @@ -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'; diff --git a/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts b/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts new file mode 100644 index 0000000000..f5a027e882 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts @@ -0,0 +1,240 @@ +import compare from 'just-compare'; + +export class ListNode { + readonly value: T; + next: ListNode | undefined; + previous: ListNode | undefined; + + constructor(value: T) { + this.value = value; + } +} + +export class LinkedList { + private first: ListNode | undefined; + private last: ListNode | undefined; + private size = 0; + + get head(): ListNode | undefined { + return this.first; + } + get tail(): ListNode | undefined { + return this.last; + } + get length(): number { + return this.size; + } + + private linkWith(value: T, previousNode: ListNode, nextNode: ListNode): ListNode { + 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 => { + return this.addByIndex(value, position); + }, + head: (): ListNode => { + return this.addHead(value); + }, + tail: (): ListNode => { + return this.addTail(value); + }, + }; + } + + addAfter(value: T, previousValue: T, compareFn = compare): ListNode { + 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 { + 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 { + 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 { + 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 { + 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 | 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 | undefined { + const position = this.findIndex(currentValue => compareFn(currentValue, value)); + + if (position < 0) return undefined; + + return this.dropByIndex(position); + } + + dropHead(): ListNode | 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 | 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): ListNode | 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): 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(callback: ListIteratorFunction) { + for (let node = this.first, position = 0; node; position += 1, node = node.next) { + callback(node.value, position, this); + } + } + + get(position: number): ListNode | 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 = ( + value: T, + index?: number, + list?: LinkedList, +) => R;