From 439ca98c8206ca7babff23f748a38a8fab49252a Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 25 Mar 2020 17:53:32 +0300 Subject: [PATCH 1/6] feat(core): make TrackByService publicly available --- npm/ng-packs/packages/core/src/lib/services/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/npm/ng-packs/packages/core/src/lib/services/index.ts b/npm/ng-packs/packages/core/src/lib/services/index.ts index e7f7f9b985..ad23b74fae 100644 --- a/npm/ng-packs/packages/core/src/lib/services/index.ts +++ b/npm/ng-packs/packages/core/src/lib/services/index.ts @@ -3,7 +3,8 @@ export * from './auth.service'; export * from './config-state.service'; export * from './lazy-load.service'; export * from './localization.service'; +export * from './profile-state.service'; export * from './profile.service'; export * from './rest.service'; -export * from './profile-state.service'; export * from './session-state.service'; +export * from './track-by.service'; From 75a13a0aa4acab0471e92ca95a1904a9ed4efd9e Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 25 Mar 2020 17:54:11 +0300 Subject: [PATCH 2/6] 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; From d56f2c7f9453c54069d63b601fd31f73bb97b5ed Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 25 Mar 2020 19:01:09 +0300 Subject: [PATCH 3/6] feat(core): add dropByValueAll to LinkedList --- .../core/src/lib/tests/linked-list.spec.ts | 89 +++++++++++++++++++ .../core/src/lib/utils/linked-list.ts | 13 +++ 2 files changed, 102 insertions(+) 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 index cd0a00096e..77b748cf0d 100644 --- 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 @@ -538,6 +538,95 @@ describe('Linked List (Doubly)', () => { expect(list.tail.previous.previous.value).toBe('a'); }); }); + + describe('#byValueAll', () => { + it('should remove all nodes 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" + + const dropped = list.drop().byValueAll('x'); + + // "a" <-> "b" <-> "c" + + expect(dropped.length).toBe(2); + expect(dropped[0].value).toEqual('x'); + expect(dropped[0].previous.value).toEqual('a'); + expect(dropped[0].next.value).toEqual('b'); + expect(dropped[1].value).toEqual('x'); + expect(dropped[1].previous.value).toEqual('b'); + expect(dropped[1].next.value).toEqual('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: 0 }).tail(); + list.add({ x: 2 }).tail(); + list.add({ x: 0 }).tail(); + list.add({ x: 3 }).tail(); + + // {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} + + const dropped = list.drop().byValueAll({ x: 0 }, (v1: X, v2: X) => v1.x === v2.x); + + // {"x":1} <-> {"x":2} <-> {"x":3} + + expect(dropped.length).toBe(2); + expect(dropped[0].value.x).toEqual(0); + expect(dropped[0].previous.value.x).toEqual(1); + expect(dropped[0].next.value.x).toEqual(2); + expect(dropped[1].value.x).toEqual(0); + expect(dropped[1].previous.value.x).toEqual(2); + expect(dropped[1].next.value.x).toEqual(3); + + expect(list.length).toBe(3); + expect(list.head.value.x).toBe(1); + expect(list.head.next.value.x).toBe(2); + expect(list.head.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(1); + }); + + it('should return empty array when list is empty', () => { + const dropped = list.drop().byValueAll('x'); + expect(dropped).toEqual([]); + }); + + it('should return empty array when given value is not found', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + const dropped = list.drop().byValueAll('x'); + + // "a" <-> "b" <-> "c" + + expect(dropped).toEqual([]); + + 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', () => { 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 index f5a027e882..9c47a6ad9b 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts @@ -117,6 +117,7 @@ export class LinkedList { return { byIndex: (position: number) => this.dropByIndex(position), byValue: (value: T, compareFn = compare) => this.dropByValue(value, compareFn), + byValueAll: (value: T, compareFn = compare) => this.dropByValueAll(value, compareFn), head: () => this.dropHead(), tail: () => this.dropTail(), }; @@ -148,6 +149,18 @@ export class LinkedList { return this.dropByIndex(position); } + dropByValueAll(value: T, compareFn = compare): ListNode[] { + const dropped: ListNode[] = []; + + for (let current = this.first, position = 0; current; position += 1, current = current.next) { + if (compareFn(current.value, value)) { + dropped.push(this.dropByIndex(position - dropped.length)); + } + } + + return dropped; + } + dropHead(): ListNode | undefined { const head = this.first; From 6599c2a6762ea5ae6b27dfd6bc7db88c0ec3b593 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 25 Mar 2020 19:08:55 +0300 Subject: [PATCH 4/6] fix(core): avoid lint errors --- .../packages/core/src/lib/tests/linked-list.spec.ts | 2 +- .../packages/core/src/lib/utils/linked-list.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) 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 index 77b748cf0d..d52050c0ad 100644 --- 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 @@ -796,7 +796,7 @@ describe('Linked List (Doubly)', () => { const arr = []; - for (let value of list) { + for (const value of list) { arr.push(value); } 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 index 9c47a6ad9b..6d2435acdb 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts @@ -1,3 +1,5 @@ +/* tslint:disable:no-non-null-assertion */ + import compare from 'just-compare'; export class ListNode { @@ -25,7 +27,11 @@ export class LinkedList { return this.size; } - private linkWith(value: T, previousNode: ListNode, nextNode: ListNode): ListNode { + private linkWith( + value: T, + previousNode: ListNode | undefined, + nextNode: ListNode | undefined, + ): ListNode { const node = new ListNode(value); if (!previousNode) return this.addHead(value); @@ -154,7 +160,7 @@ export class LinkedList { for (let current = this.first, position = 0; current; position += 1, current = current.next) { if (compareFn(current.value, value)) { - dropped.push(this.dropByIndex(position - dropped.length)); + dropped.push(this.dropByIndex(position - dropped.length)!); } } From 026fb1defba37b72f6353994f356ea8603ddf2f5 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 25 Mar 2020 19:48:28 +0300 Subject: [PATCH 5/6] feat(core): iterate over linked list nodes instead of node values --- .../core/src/lib/tests/linked-list.spec.ts | 39 ++++++++----------- .../core/src/lib/utils/linked-list.ts | 18 ++++----- 2 files changed, 25 insertions(+), 32 deletions(-) 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 index d52050c0ad..518956deba 100644 --- 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 @@ -212,7 +212,7 @@ describe('Linked List (Doubly)', () => { // "a" <-> "x" <-> "b" <-> "x" <-> "c" - const node1 = list.find(value => value === 'x'); + const node1 = list.find(node => node.previous && node.previous.value === 'a'); expect(node1.value).toBe('x'); expect(node1.previous.value).toBe('a'); @@ -220,7 +220,7 @@ describe('Linked List (Doubly)', () => { // "a" <-> "x" <-> "b" <-> "x" <-> "c" - const node2 = list.find((_, index) => index === 3); + const node2 = list.find(node => node.next && node.next.value === 'c'); expect(node2.value).toBe('x'); expect(node2.previous.value).toBe('b'); @@ -228,9 +228,9 @@ describe('Linked List (Doubly)', () => { }); it('should return undefined when list is empty', () => { - const node = list.find(value => value === 'x'); + const found = list.find(node => node.value === 'x'); - expect(node).toBeUndefined(); + expect(found).toBeUndefined(); }); it('should return undefined when predicate finds no match', () => { @@ -240,9 +240,9 @@ describe('Linked List (Doubly)', () => { // "a" <-> "b" <-> "c" - const node = list.find(value => value === 'x'); + const found = list.find(node => node.value === 'x'); - expect(node).toBeUndefined(); + expect(found).toBeUndefined(); expect(list.length).toBe(3); expect(list.head.value).toBe('a'); expect(list.head.next.value).toBe('b'); @@ -263,26 +263,19 @@ describe('Linked List (Doubly)', () => { // "a" <-> "x" <-> "b" <-> "x" <-> "c" - const index1 = list.findIndex(value => value === 'x'); + const index1 = list.findIndex(node => node.previous && node.previous.value === 'a'); 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; - }); + const index2 = list.findIndex(node => node.next && node.next.value === 'c'); expect(index2).toBe(3); }); it('should return -1 when list is empty', () => { - const index = list.findIndex(value => value === 'x'); + const index = list.findIndex(node => node.value === 'x'); expect(index).toBe(-1); }); @@ -294,7 +287,7 @@ describe('Linked List (Doubly)', () => { // "a" <-> "b" <-> "c" - const index = list.findIndex(value => value === 'x'); + const index = list.findIndex(node => node.value === 'x'); expect(index).toBe(-1); expect(list.length).toBe(3); @@ -309,9 +302,9 @@ describe('Linked List (Doubly)', () => { 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(); + const a = list.add('a').tail(); + const b = list.add('b').tail(); + const c = list.add('c').tail(); // "a" <-> "b" <-> "c" @@ -319,9 +312,9 @@ describe('Linked List (Doubly)', () => { list.forEach(spy); expect(spy.mock.calls).toEqual([ - ['a', 0, list], - ['b', 1, list], - ['c', 2, list], + [a, 0, list], + [b, 1, list], + [c, 2, list], ]); }); 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 index 6d2435acdb..42cc5bc941 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts @@ -68,13 +68,13 @@ export class LinkedList { } addAfter(value: T, previousValue: T, compareFn = compare): ListNode { - const previous = this.find(currentValue => compareFn(currentValue, previousValue)); + const previous = this.find(node => compareFn(node.value, 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)); + const next = this.find(node => compareFn(node.value, nextValue)); return next ? this.linkWith(value, next.previous, next) : this.addHead(value); } @@ -148,7 +148,7 @@ export class LinkedList { } dropByValue(value: T, compareFn = compare): ListNode | undefined { - const position = this.findIndex(currentValue => compareFn(currentValue, value)); + const position = this.findIndex(node => compareFn(node.value, value)); if (position < 0) return undefined; @@ -203,7 +203,7 @@ export class LinkedList { 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; + if (predicate(current, position, this)) return current; } return undefined; @@ -211,7 +211,7 @@ export class LinkedList { 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; + if (predicate(current, position, this)) return position; } return -1; @@ -219,7 +219,7 @@ export class LinkedList { forEach(callback: ListIteratorFunction) { for (let node = this.first, position = 0; node; position += 1, node = node.next) { - callback(node.value, position, this); + callback(node, position, this); } } @@ -228,13 +228,13 @@ export class LinkedList { } indexOf(value: T, compareFn = compare): number { - return this.findIndex(currentValue => compareFn(currentValue, value)); + return this.findIndex(node => compareFn(node.value, value)); } toArray(): T[] { const array = new Array(this.size); - this.forEach((value, index) => (array[index!] = value)); + this.forEach((node, index) => (array[index!] = node.value)); return array; } @@ -253,7 +253,7 @@ export class LinkedList { } export type ListIteratorFunction = ( - value: T, + node: ListNode, index?: number, list?: LinkedList, ) => R; From 81a3f6b8563b365ee8644f9b3e017fd357c0840c Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 25 Mar 2020 20:59:52 +0300 Subject: [PATCH 6/6] docs(core): add how to create and use a LinkedList --- docs/en/UI/Angular/Linked-List.md | 844 ++++++++++++++++++++++++++++++ 1 file changed, 844 insertions(+) create mode 100644 docs/en/UI/Angular/Linked-List.md diff --git a/docs/en/UI/Angular/Linked-List.md b/docs/en/UI/Angular/Linked-List.md new file mode 100644 index 0000000000..c3fb307806 --- /dev/null +++ b/docs/en/UI/Angular/Linked-List.md @@ -0,0 +1,844 @@ +# Linked List (Doubly) + + + +The core module provides a useful data structure known as a [doubly linked list](https://en.wikipedia.org/wiki/Doubly_linked_list). Briefly, a doubly linked list is a series of records (a.k.a. nodes) which has information on the previous node, the next node, and its own value (or data). + + + +## Getting Started + +To create a doubly linked list, all you have to do is to import and create a new instance of it: + +```js +import { LinkedList } from '@abp/ng.core'; + +const list = new LinkedList(); +``` + + + +The constructor does not get any parameters. + + + +## Usage + +### How to Add New Nodes + +There are a few methods to create new nodes in a linked list and all of them are separately available as well as revealed from an `add` method. + + + +#### addHead(value: T): ListNode\ + +Adds a node with given value as the first node in list: + +```js +list.addHead('a'); + +// "a" + +list.addHead('b'); + +// "b" <-> "a" + +list.addHead('c'); + +// "c" <-> "b" <-> "a" +``` + + + +#### addTail(value: T): ListNode\ + +Adds a node with given value as the last node in list: + +```js +list.addTail('a'); + +// "a" + +list.addTail('b'); + +// "a" <-> "b" + +list.addTail('c'); + +// "a" <-> "b" <-> "c" +``` + + + +#### addAfter(value: T, previousValue: T, compareFn = compare): ListNode\ + +Adds a node with given value after the first node that has the previous value: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "b" <-> "c" + +list.addAfter('x', 'b'); + +// "a" <-> "b" <-> "x" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addTail({ x: 1 }); +list.addTail({ x: 2 }); +list.addTail({ x: 3 }); + +// {"x":1} <-> {"x":2} <-> {"x":3} + +list.addAfter({ x: 0 }, { x: 2 }, (v1, v2) => v1.x === v2.x); + +// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3} +``` + + + +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### addBefore(value: T, nextValue: T, compareFn = compare): ListNode\ + +Adds a node with given value before the first node that has the next value: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "b" <-> "c" + +list.addBefore('x', 'b'); + +// "a" <-> "x" <-> "b" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addTail({ x: 1 }); +list.addTail({ x: 2 }); +list.addTail({ x: 3 }); + +// {"x":1} <-> {"x":2} <-> {"x":3} + +list.addBefore({ x: 0 }, { x: 2 }, (v1, v2) => v1.x === v2.x); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":3} +``` + + + +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### addByIndex(value: T, position: number): ListNode\ + +Adds a node with given value at the specified position in the list: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "c" + +list.addByIndex('x', 2); + +// "a" <-> "b" <-> "x" <-> "c" +``` + + + +#### add(value: T).head(): ListNode\ + +Adds a node with given value as the first node in list: + +```js +list.add('a').head(); + +// "a" + +list.add('b').head(); + +// "b" <-> "a" + +list.add('c').head(); + +// "c" <-> "b" <-> "a" +``` + + + +> This is an alternative API for `addHead`. + + + +#### add(value: T).tail(): ListNode\ + +Adds a node with given value as the last node in list: + +```js +list.add('a').tail(); + +// "a" + +list.add('b').tail(); + +// "a" <-> "b" + +list.add('c').tail(); + +// "a" <-> "b" <-> "c" +``` + + + +> This is an alternative API for `addTail`. + + + +#### add(value: T).after(previousValue: T, compareFn = compare): ListNode\ + +Adds a node with given value after the first node that has the previous value: + +```js +list.add('a').tail(); +list.add('b').tail(); +list.add('b').tail(); +list.add('c').tail(); + +// "a" <-> "b" <-> "b" <-> "c" + +list.add('x').after('b'); + +// "a" <-> "b" <-> "x" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +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: 2 }, (v1, v2) => v1.x === v2.x); + +// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3} +``` + + + +> This is an alternative API for `addAfter`. +> +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### add(value: T).before(nextValue: T, compareFn = compare): ListNode\ + +Adds a node with given value before the first node that has the next value: + +```js +list.add('a').tail(); +list.add('b').tail(); +list.add('b').tail(); +list.add('c').tail(); + +// "a" <-> "b" <-> "b" <-> "c" + +list.add('x').before('b'); + +// "a" <-> "x" <-> "b" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +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, v2) => v1.x === v2.x); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":3} +``` + + + +> This is an alternative API for `addBefore`. +> +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### add(value: T).byIndex(position: number): ListNode\ + +Adds a node with given value at the specified position in the list: + +```js +list.add('a').tail(); +list.add('b').tail(); +list.add('c').tail(); + +// "a" <-> "b" <-> "c" + +list.add('x').byIndex(2); + +// "a" <-> "b" <-> "x" <-> "c" +``` + + + +> This is an alternative API for `addByIndex`. + + + +### How to Remove Nodes + +There are a few methods to remove nodes from a linked list and all of them are separately available as well as revealed from a `drop` method. + + + +#### dropHead(): ListNode\ | undefined + +Removes the first node from the list: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "c" + +list.dropHead(); + +// "b" <-> "c" +``` + + + +#### dropTail(): ListNode\ | undefined + +Removes the last node from the list: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "c" + +list.dropTail(); + +// "a" <-> "b" +``` + + + +#### dropByIndex(position: number): ListNode\ | undefined + +Removes the node with the specified position from the list: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "c" + +list.dropByIndex(1); + +// "a" <-> "c" +``` + + + +#### dropByValue(value: T, compareFn = compare): ListNode\ | undefined + +Removes the first node with given value from the list: + +```js +list.addTail('a'); +list.addTail('x'); +list.addTail('b'); +list.addTail('x'); +list.addTail('c'); + +// "a" <-> "x" <-> "b" <-> "x" <-> "c" + +list.dropByValue('x'); + +// "a" <-> "b" <-> "x" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addTail({ x: 1 }); +list.addTail({ x: 0 }); +list.addTail({ x: 2 }); +list.addTail({ x: 0 }); +list.addTail({ x: 3 }); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} + +list.dropByValue({ x: 0 }, (v1, v2) => v1.x === v2.x); + +// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3} +``` + + + +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### dropByValueAll(value: T, compareFn = compare): ListNode\\[\] + +Removes all nodes with given value from the list: + +```js +list.addTail('a'); +list.addTail('x'); +list.addTail('b'); +list.addTail('x'); +list.addTail('c'); + +// "a" <-> "x" <-> "b" <-> "x" <-> "c" + +list.dropByValueAll('x'); + +// "a" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addTail({ x: 1 }); +list.addTail({ x: 0 }); +list.addTail({ x: 2 }); +list.addTail({ x: 0 }); +list.addTail({ x: 3 }); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} + +list.dropByValue({ x: 0 }, (v1, v2) => v1.x === v2.x); + +// {"x":1} <-> {"x":2} <-> {"x":3} +``` + + + +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### drop().head(): ListNode\ | undefined + +Removes the first node in list: + +```js +list.add('a').tail(); +list.add('b').tail(); +list.add('c').tail(); + +// "a" <-> "b" <-> "c" + +list.drop().head(); + +// "b" <-> "c" +``` + + + +> This is an alternative API for `dropHead`. + + + +#### drop().tail(): ListNode\ | undefined + +Removes the last node in list: + +```js +list.add('a').tail(); +list.add('b').tail(); +list.add('c').tail(); + +// "a" <-> "b" <-> "c" + +list.drop().tail(); + +// "a" <-> "b" +``` + + + +> This is an alternative API for `dropTail`. + + + +#### drop().byIndex(position: number): ListNode\ | undefined + +Removes the node with the specified position from the list: + +```js +list.add('a').tail(); +list.add('b').tail(); +list.add('c').tail(); + +// "a" <-> "b" <-> "c" + +list.drop().byIndex(1); + +// "a" <-> "c" +``` + + + +> This is an alternative API for `dropByIndex`. + + + +#### drop().byValue(value: T, compareFn = compare): ListNode\ | undefined + +Removes the first node with given value from the list: + +```js +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" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.add({ x: 1 }).tail(); +list.add({ x: 0 }).tail(); +list.add({ x: 2 }).tail(); +list.add({ x: 0 }).tail(); +list.add({ x: 3 }).tail(); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} + +list.drop().byValue({ x: 0 }, (v1, v2) => v1.x === v2.x); + +// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3} +``` + + + +> This is an alternative API for `dropByValue`. +> +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### drop().byValueAll(value: T, compareFn = compare): ListNode\\[\] + +Removes all nodes with given value from the list: + +```js +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().byValueAll('x'); + +// "a" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.add({ x: 1 }).tail(); +list.add({ x: 0 }).tail(); +list.add({ x: 2 }).tail(); +list.add({ x: 0 }).tail(); +list.add({ x: 3 }).tail(); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} + +list.drop().byValueAll({ x: 0 }, (v1, v2) => v1.x === v2.x); + +// {"x":1} <-> {"x":2} <-> {"x":3} +``` + + + +> This is an alternative API for `dropByValueAll`. +> +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +### How to Find Nodes + +There are a few methods to find specific nodes in a linked list. + + + +#### find(predicate: ListIteratorFunction\): ListNode\ | undefined + +Finds the first node from the list that matches the given predicate: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "b" <-> "c" + +const found = list.find(node => node.value === 'b'); + +/* +found.value === "b" +found.previous.value === "a" +found.next.value === "b" +*/ +``` + + + +#### findIndex(predicate: ListIteratorFunction\): number + +Finds the position of the first node from the list that matches the given predicate: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "b" <-> "c" + +const i0 = list.findIndex(node => node.next && node.next.value === 'b'); +const i1 = list.findIndex(node => node.value === 'b'); +const i2 = list.findIndex(node => node.previous && node.previous.value === 'b'); +const i3 = list.findIndex(node => node.value === 'x'); + +/* +i0 === 0 +i1 === 1 +i2 === 2 +i3 === -1 +*/ +``` + + + +#### get(position: number): ListNode\ | undefined + +Finds and returns the node with specific position in the list: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "c" + +const found = list.get(1); + +/* +found.value === "b" +found.previous.value === "a" +found.next.value === "c" +*/ +``` + + + +#### indexOf(value: T, compareFn = compare): number + +Finds the position of the first node from the list that has the given value: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "b" <-> "c" + +const i0 = list.indexOf('a'); +const i1 = list.indexOf('b'); +const i2 = list.indexOf('c'); +const i3 = list.indexOf('x'); + +/* +i0 === 0 +i1 === 1 +i2 === 3 +i3 === -1 +*/ +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addTail({ x: 1 }); +list.addTail({ x: 0 }); +list.addTail({ x: 2 }); +list.addTail({ x: 0 }); +list.addTail({ x: 3 }); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} + +const i0 = indexOf({ x: 1 }, (v1, v2) => v1.x === v2.x); +const i1 = indexOf({ x: 2 }, (v1, v2) => v1.x === v2.x); +const i2 = indexOf({ x: 3 }, (v1, v2) => v1.x === v2.x); +const i3 = indexOf({ x: 0 }, (v1, v2) => v1.x === v2.x); +const i4 = indexOf({ x: 4 }, (v1, v2) => v1.x === v2.x); + +/* +i0 === 0 +i1 === 2 +i2 === 4 +i3 === 1 +i4 === -1 +*/ +``` + + + +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +### How to Check All Nodes + +There are a few ways to iterate over or display a linked list. + + + +#### forEach(callback: ListIteratorFunction\): void + +Runs a callback function on all nodes in a linked list from head to tail: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "c" + +list.forEach((node, index) => console.log(node.value + index)); + +// 'a0' +// 'b1' +// 'c2' +``` + + + +#### \*\[Symbol.iterator\]\(\) + +A linked list is iterable. In other words, you may use methods like `for...of` on it. + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "c" + +for(const node of list) { + console.log(node.value); +} + +// 'a' +// 'b' +// 'c' +``` + + + +#### toArray(): T[] + +Converts a linked list to an array: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "c" + +const arr = list.toArray(); + +/* +arr === ['a', 'b', 'c'] +*/ +``` + + + +#### toString(): string + +Converts a linked list to a string representation of nodes and their relations: + +```js +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(); + +/* +str === '"a" <-> 2 <-> "c" <-> {"k":4,"v":"d"}' +*/ +``` + + +