From 4d93244f06b3f54c3c700b7439ce1ddadf3b4dc9 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Thu, 26 Mar 2020 19:06:27 +0300 Subject: [PATCH] feat(core): add methods to LinkedList for adding and dropping multiple nodes --- .../core/src/lib/tests/linked-list.spec.ts | 870 ++++++++++++++++-- .../core/src/lib/utils/linked-list.ts | 257 ++++-- 2 files changed, 997 insertions(+), 130 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 518956deba..3ad9eeda63 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 @@ -1,4 +1,4 @@ -import { LinkedList } from '../utils/linked-list'; +import { LinkedList, ListNode } from '../utils/linked-list'; describe('Linked List (Doubly)', () => { let list: LinkedList; @@ -44,9 +44,11 @@ describe('Linked List (Doubly)', () => { expect(list.length).toBe(3); expect(list.head.value).toBe('c'); expect(list.head.next.value).toBe('b'); + expect(list.head.next.next.value).toBe('a'); expect(list.head.previous).toBeUndefined(); expect(list.tail.value).toBe('a'); expect(list.tail.previous.value).toBe('b'); + expect(list.tail.previous.previous.value).toBe('c'); expect(list.tail.next).toBeUndefined(); }); }); @@ -72,34 +74,49 @@ describe('Linked List (Doubly)', () => { 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.head.previous).toBeUndefined(); expect(list.tail.value).toBe('c'); expect(list.tail.previous.value).toBe('b'); + expect(list.tail.previous.previous.value).toBe('a'); expect(list.tail.next).toBeUndefined(); }); }); describe('#after', () => { - it('should place a node after node with given value', () => { + it('should add a node after node with given value', () => { list.add('a').tail(); - list.add('b').tail(); - list.add('c').tail(); - // "a" <-> "b" <-> "c" + // "a" - list.add('x').after('b'); + list.add('b').after('a'); + list.add('c').after('b'); - // "a" <-> "b" <-> "x" <-> "c" + // "a" <-> "b" <-> "c" - expect(list.length).toBe(4); + 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('x'); - expect(list.head.next.next.next.value).toBe('c'); + expect(list.head.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'); + expect(list.tail.previous.value).toBe('b'); + expect(list.tail.previous.previous.value).toBe('a'); + }); + + it('should add a node to tail if given value is not found', () => { + list.add('a').tail(); + + // "a" + + list.add('b').after('x'); + + // "a" <-> "b" + + expect(list.length).toBe(2); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('b'); + expect(list.tail.value).toBe('b'); + expect(list.tail.previous.value).toBe('a'); }); it('should be able to receive a custom compareFn', () => { @@ -126,79 +143,516 @@ describe('Linked List (Doubly)', () => { }); describe('#before', () => { - it('should place a node before node with given value', () => { + it('should add a node before node with given value', () => { + list.add('c').tail(); + + // "c" + + list.add('b').before('c'); + list.add('a').before('b'); + + // "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 add a node to head if given value is not found', () => { + list.add('b').tail(); + + // "a" + + list.add('a').before('x'); + + // "a" <-> "b" + + expect(list.length).toBe(2); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('b'); + expect(list.tail.value).toBe('b'); + expect(list.tail.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 add a node at given index', () => { list.add('a').tail(); list.add('b').tail(); list.add('c').tail(); // "a" <-> "b" <-> "c" - list.add('x').before('b'); + list.add('x').byIndex(1); // "a" <-> "x" <-> "b" <-> "c" - expect(list.length).toBe(4); + 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'); + }); + + it('should add a node to head if given index is zero', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + list.add('x').byIndex(0); + + // "x" <-> "a" <-> "b" <-> "c" + + expect(list.length).toBe(4); + expect(list.head.value).toBe('x'); + expect(list.head.next.value).toBe('a'); + 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'); + expect(list.tail.previous.previous.value).toBe('a'); + expect(list.tail.previous.previous.previous.value).toBe('x'); + }); + + it('should add a node to tail if given index more than size', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + list.add('x').byIndex(4); + + // "a" <-> "b" <-> "c" <-> "x" + + 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('c'); + expect(list.head.next.next.next.value).toBe('x'); + expect(list.tail.value).toBe('x'); + expect(list.tail.previous.value).toBe('c'); + expect(list.tail.previous.previous.value).toBe('b'); + expect(list.tail.previous.previous.previous.value).toBe('a'); + }); + + it('should be able to add a node at given index counting from right to left', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + list.add('x').byIndex(-1); + + // "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 add a node to head if given index is less than minus size', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + list.add('x').byIndex(-4); + + // "x" <-> "a" <-> "b" <-> "c" + + expect(list.length).toBe(4); + expect(list.head.value).toBe('x'); + expect(list.head.next.value).toBe('a'); + 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('a'); + expect(list.tail.previous.previous.previous.value).toBe('x'); + }); + }); + }); + + describe('#addMany', () => { + describe('#head', () => { + it('should add multiple nodes to the head of the list', () => { + list.add('x').head(); + + // "x" + + list.addMany(['a', 'b', 'c']).head(); + + // "a" <-> "b" <-> "c" <-> "x" + + 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('c'); + expect(list.head.next.next.next.value).toBe('x'); + expect(list.head.previous).toBeUndefined(); + expect(list.tail.value).toBe('x'); + expect(list.tail.previous.value).toBe('c'); + expect(list.tail.previous.previous.value).toBe('b'); + expect(list.tail.previous.previous.previous.value).toBe('a'); + expect(list.tail.next).toBeUndefined(); + }); + }); + + describe('#tail', () => { + it('should add multiple nodes to the tail of the list', () => { + list.add('x').tail(); + + // "x" + + list.addMany(['a', 'b', 'c']).tail(); + + // "x" <-> "a" <-> "b" <-> "c" + + expect(list.length).toBe(4); + expect(list.head.value).toBe('x'); + expect(list.head.next.value).toBe('a'); + expect(list.head.next.next.value).toBe('b'); + expect(list.head.next.next.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.previous.previous.value).toBe('a'); + expect(list.tail.previous.previous.previous.value).toBe('x'); + expect(list.tail.next).toBeUndefined(); + }); + }); + + describe('#after', () => { + it('should add multiple nodes after node with given value', () => { + list.add('a').tail(); + + // "a" + + list.addMany(['b', 'c']).after('a'); + + // "a" <-> "b" <-> "c" + + list.addMany(['x', 'y', 'z']).after('b'); + + // "a" <-> "b" <-> "x" <-> "y" <-> "z" <-> "c" + + expect(list.length).toBe(6); + 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('y'); + expect(list.head.next.next.next.next.value).toBe('z'); + expect(list.head.next.next.next.next.next.value).toBe('c'); + expect(list.tail.value).toBe('c'); + expect(list.tail.previous.value).toBe('z'); + expect(list.tail.previous.previous.value).toBe('y'); + expect(list.tail.previous.previous.previous.value).toBe('x'); + expect(list.tail.previous.previous.previous.previous.value).toBe('b'); + expect(list.tail.previous.previous.previous.previous.previous.value).toBe('a'); + }); + + it('should add multiple nodes to tail if given value is not found', () => { + list.add('a').tail(); + + // "a" + + list.addMany(['b', 'c']).after('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.addMany([{ x: 1 }, { x: 2 }, { x: 3 }]).tail(); + + // {"x":1} <-> {"x":2} <-> {"x":3} + + list.addMany([{ x: 4 }, { x: 5 }]).after({ x: 1 }, (v1: X, v2: X) => v1.x === v2.x); + + // {"x":1} <-> {"x":4} <-> {"x":5} <-> {"x":2} <-> {"x":3} + + expect(list.length).toBe(5); + expect(list.head.value.x).toBe(1); + expect(list.head.next.value.x).toBe(4); + expect(list.head.next.next.value.x).toBe(5); + expect(list.head.next.next.next.value.x).toBe(2); + expect(list.head.next.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(5); + expect(list.tail.previous.previous.previous.value.x).toBe(4); + expect(list.tail.previous.previous.previous.previous.value.x).toBe(1); + }); + + it('should not change the list when empty array given as value', () => { + list.add('a').tail(); + + // "a" + + list.addMany([]).after('a'); + + // "a" + + expect(list.length).toBe(1); + expect(list.head.value).toBe('a'); + expect(list.tail.value).toBe('a'); + }); + }); + + describe('#before', () => { + it('should add multiple nodes before node with given value', () => { + list.add('c').tail(); + + // "c" + + list.addMany(['a', 'b']).before('c'); + + // "a" <-> "b" <-> "c" + + list.addMany(['x', 'y', 'z']).before('b'); + + // "a" <-> "x" <-> "y" <-> "z" <-> "b" <-> "c" + + expect(list.length).toBe(6); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('x'); + expect(list.head.next.next.value).toBe('y'); + expect(list.head.next.next.next.value).toBe('z'); + expect(list.head.next.next.next.next.value).toBe('b'); + expect(list.head.next.next.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('z'); + expect(list.tail.previous.previous.previous.value).toBe('y'); + expect(list.tail.previous.previous.previous.previous.value).toBe('x'); + expect(list.tail.previous.previous.previous.previous.previous.value).toBe('a'); + }); + + it('should add multiple nodes to head if given value is not found', () => { + list.add('c').tail(); + + // "c" + + list.addMany(['a', 'b']).before('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.addMany([{ x: 1 }, { x: 2 }, { x: 3 }]).tail(); + + // {"x":1} <-> {"x":2} <-> {"x":3} + + list.addMany([{ x: 4 }, { x: 5 }]).before({ x: 3 }, (v1: X, v2: X) => v1.x === v2.x); + + // {"x":1} <-> {"x":2} <-> {"x":4} <-> {"x":5} <-> {"x":3} + + expect(list.length).toBe(5); + expect(list.head.value.x).toBe(1); + expect(list.head.next.value.x).toBe(2); + expect(list.head.next.next.value.x).toBe(4); + expect(list.head.next.next.next.value.x).toBe(5); + expect(list.head.next.next.next.next.value.x).toBe(3); + expect(list.tail.value.x).toBe(3); + expect(list.tail.previous.value.x).toBe(5); + expect(list.tail.previous.previous.value.x).toBe(4); + expect(list.tail.previous.previous.previous.value.x).toBe(2); + expect(list.tail.previous.previous.previous.previous.value.x).toBe(1); + }); + + it('should not change the list when empty array given as value', () => { + list.add('a').tail(); + + // "a" + + list.addMany([]).before('a'); + + // "a" + + expect(list.length).toBe(1); + expect(list.head.value).toBe('a'); + expect(list.tail.value).toBe('a'); + }); + }); + + describe('#byIndex', () => { + it('should add multiple nodes starting from given index', () => { + list.addMany(['a', 'b', 'c']).tail(); + + // "a" <-> "b" <-> "c" + + list.addMany(['x', 'y']).byIndex(1); + + // "a" <-> "x" <-> "y" <-> "b" <-> "c" + + list.addMany(['z']).byIndex(4); + + // "a" <-> "x" <-> "y" <-> "b" <-> "z" <-> "c" + + expect(list.length).toBe(6); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('x'); + expect(list.head.next.next.value).toBe('y'); + expect(list.head.next.next.next.value).toBe('b'); + expect(list.head.next.next.next.next.value).toBe('z'); + expect(list.head.next.next.next.next.next.value).toBe('c'); + expect(list.tail.value).toBe('c'); + expect(list.tail.previous.value).toBe('z'); + expect(list.tail.previous.previous.value).toBe('b'); + expect(list.tail.previous.previous.previous.value).toBe('y'); + expect(list.tail.previous.previous.previous.previous.value).toBe('x'); + expect(list.tail.previous.previous.previous.previous.previous.value).toBe('a'); + }); + + it('should add multiple nodes to head if given index is zero', () => { + list.addMany(['a', 'b', 'c']).tail(); + + // "a" <-> "b" <-> "c" + + list.addMany(['x', 'y']).byIndex(0); + + // "x" <-> "y" <-> "a" <-> "b" <-> "c" + + expect(list.length).toBe(5); + expect(list.head.value).toBe('x'); + expect(list.head.next.value).toBe('y'); + expect(list.head.next.next.value).toBe('a'); + expect(list.head.next.next.next.value).toBe('b'); + expect(list.head.next.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('a'); + expect(list.tail.previous.previous.previous.value).toBe('y'); + expect(list.tail.previous.previous.previous.previous.value).toBe('x'); }); - it('should be able to receive a custom compareFn', () => { - list.add({ x: 1 }).tail(); - list.add({ x: 2 }).tail(); - list.add({ x: 3 }).tail(); + it('should add multiple nodes to tail if given index more than size', () => { + list.addMany(['a', 'b', 'c']).tail(); - // {"x":1} <-> {"x":2} <-> {"x":3} + // "a" <-> "b" <-> "c" - list.add({ x: 0 }).before({ x: 2 }, (v1: X, v2: X) => v1.x === v2.x); + list.addMany(['x', 'y']).byIndex(4); - // {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":3} + // "a" <-> "b" <-> "c" <-> "x" <-> "y" - 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); + expect(list.length).toBe(5); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('b'); + expect(list.head.next.next.value).toBe('c'); + expect(list.head.next.next.next.value).toBe('x'); + expect(list.head.next.next.next.next.value).toBe('y'); + expect(list.tail.value).toBe('y'); + expect(list.tail.previous.value).toBe('x'); + expect(list.tail.previous.previous.value).toBe('c'); + expect(list.tail.previous.previous.previous.value).toBe('b'); + expect(list.tail.previous.previous.previous.previous.value).toBe('a'); }); - }); - describe('#byIndex', () => { - it('should place a node at given index', () => { - list.add('a').tail(); - list.add('b').tail(); - list.add('c').tail(); + it('should be able to add multiple nodes at given index counting from right to left', () => { + list.addMany(['a', 'b', 'c']).tail(); // "a" <-> "b" <-> "c" - list.add('x').byIndex(1); - - // "a" <-> "x" <-> "b" <-> "c" - - list.add('y').byIndex(3); + list.addMany(['x', 'y']).byIndex(-1); - // "a" <-> "x" <-> "b" <-> "y" <-> "c" + // "a" <-> "b" <-> "x" <-> "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.value).toBe('b'); + expect(list.head.next.next.value).toBe('x'); 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.value).toBe('x'); + expect(list.tail.previous.previous.previous.value).toBe('b'); expect(list.tail.previous.previous.previous.previous.value).toBe('a'); }); + + it('should add a node to head if given index is less than minus size', () => { + list.addMany(['a', 'b', 'c']).tail(); + + // "a" <-> "b" <-> "c" + + list.addMany(['x', 'y']).byIndex(-4); + + // "x" <-> "y" <-> "a" <-> "b" <-> "c" + + expect(list.length).toBe(5); + expect(list.head.value).toBe('x'); + expect(list.head.next.value).toBe('y'); + expect(list.head.next.next.value).toBe('a'); + expect(list.head.next.next.next.value).toBe('b'); + expect(list.head.next.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('a'); + expect(list.tail.previous.previous.previous.value).toBe('y'); + expect(list.tail.previous.previous.previous.previous.value).toBe('x'); + }); }); }); @@ -212,19 +666,19 @@ describe('Linked List (Doubly)', () => { // "a" <-> "x" <-> "b" <-> "x" <-> "c" - const node1 = list.find(node => node.previous && node.previous.value === 'a'); + const found1 = list.find(node => node.previous && node.previous.value === 'a'); - expect(node1.value).toBe('x'); - expect(node1.previous.value).toBe('a'); - expect(node1.next.value).toBe('b'); + expect(found1.value).toBe('x'); + expect(found1.previous.value).toBe('a'); + expect(found1.next.value).toBe('b'); // "a" <-> "x" <-> "b" <-> "x" <-> "c" - const node2 = list.find(node => node.next && node.next.value === 'c'); + const found2 = list.find(node => node.next && node.next.value === 'c'); - expect(node2.value).toBe('x'); - expect(node2.previous.value).toBe('b'); - expect(node2.next.value).toBe('c'); + expect(found2.value).toBe('x'); + expect(found2.previous.value).toBe('b'); + expect(found2.next.value).toBe('c'); }); it('should return undefined when list is empty', () => { @@ -329,7 +783,8 @@ describe('Linked List (Doubly)', () => { describe('#drop', () => { describe('#head', () => { it('should return undefined when there is no head', () => { - expect(list.drop().head()).toBeUndefined(); + const dropped = list.drop().head(); + expect(dropped).toBeUndefined(); }); it('should remove the node from the head of the list', () => { @@ -350,12 +805,22 @@ describe('Linked List (Doubly)', () => { expect(list.tail.value).toBe('c'); expect(list.tail.previous.value).toBe('b'); expect(list.tail.next).toBeUndefined(); + + // "b" <-> "c" + + list.drop().head(); + list.drop().head(); + + expect(list.length).toBe(0); + expect(list.head).toBeUndefined(); + expect(list.tail).toBeUndefined(); }); }); - describe('#head', () => { + describe('#tail', () => { it('should return undefined when there is no tail', () => { - expect(list.drop().tail()).toBeUndefined(); + const dropped = list.drop().tail(); + expect(dropped).toBeUndefined(); }); it('should remove the node from the tail of the list', () => { @@ -376,6 +841,15 @@ describe('Linked List (Doubly)', () => { expect(list.tail.value).toBe('b'); expect(list.tail.previous.value).toBe('a'); expect(list.tail.next).toBeUndefined(); + + // "a" <-> "b" + + list.drop().tail(); + list.drop().tail(); + + expect(list.length).toBe(0); + expect(list.head).toBeUndefined(); + expect(list.tail).toBeUndefined(); }); }); @@ -393,7 +867,7 @@ describe('Linked List (Doubly)', () => { // "a" <-> "c" <-> "d" <-> "e" - list.drop().byIndex(2); + list.drop().byIndex(-2); // "a" <-> "c" <-> "e" @@ -404,11 +878,21 @@ describe('Linked List (Doubly)', () => { expect(list.tail.value).toBe('e'); expect(list.tail.previous.value).toBe('c'); expect(list.tail.previous.previous.value).toBe('a'); + + // "a" <-> "c" <-> "e" + + list.drop().byIndex(2); + list.drop().byIndex(1); + list.drop().byIndex(0); + + expect(list.length).toBe(0); + expect(list.head).toBeUndefined(); + expect(list.tail).toBeUndefined(); }); it('should return undefined when list is empty', () => { - const node = list.drop().byIndex(0); - expect(node).toBeUndefined(); + const dropped = list.drop().byIndex(0); + expect(dropped).toBeUndefined(); }); it('should return undefined when given index does not exist', () => { @@ -418,11 +902,11 @@ describe('Linked List (Doubly)', () => { // "a" <-> "b" <-> "c" - const node1 = list.drop().byIndex(4); + const dropped1 = list.drop().byIndex(4); // "a" <-> "b" <-> "c" - expect(node1).toBeUndefined(); + expect(dropped1).toBeUndefined(); expect(list.length).toBe(3); expect(list.head.value).toBe('a'); expect(list.head.next.value).toBe('b'); @@ -433,11 +917,11 @@ describe('Linked List (Doubly)', () => { // "a" <-> "b" <-> "c" - const node2 = list.drop().byIndex(-1); + const dropped2 = list.drop().byIndex(-4); // "a" <-> "b" <-> "c" - expect(node2).toBeUndefined(); + expect(dropped2).toBeUndefined(); expect(list.length).toBe(3); expect(list.head.value).toBe('a'); expect(list.head.next.value).toBe('b'); @@ -506,8 +990,8 @@ describe('Linked List (Doubly)', () => { }); it('should return undefined when list is empty', () => { - const node = list.drop().byValue('x'); - expect(node).toBeUndefined(); + const dropped = list.drop().byValue('x'); + expect(dropped).toBeUndefined(); }); it('should return undefined when given value is not found', () => { @@ -622,6 +1106,214 @@ describe('Linked List (Doubly)', () => { }); }); + describe('#dropMany', () => { + describe('#head', () => { + it('should return empty array when there is no head', () => { + const dropped = list.dropMany(1).head(); + expect(dropped).toEqual([]); + }); + + it('should remove multiple nodes from the head of the list', () => { + list.addMany(['a', 'b', 'c', 'd', 'e']).tail(); + + // "a" <-> "b" <-> "c" <-> "d" <-> "e" + + list.dropMany(3).head(); + + // "d" <-> "e" + + expect(list.length).toBe(2); + expect(list.head.value).toBe('d'); + expect(list.head.next.value).toBe('e'); + expect(list.head.previous).toBeUndefined(); + expect(list.tail.value).toBe('e'); + expect(list.tail.previous.value).toBe('d'); + expect(list.tail.next).toBeUndefined(); + }); + + it('should not change the list when count is less than or equal to zero', () => { + list.add('a').tail(); + + // "a" + + list.dropMany(0).head(); + + // "a" + + list.dropMany(-1).head(); + + // "a" + + expect(list.length).toBe(1); + expect(list.head.value).toBe('a'); + expect(list.tail.value).toBe('a'); + }); + }); + + describe('#tail', () => { + it('should return empty array when there is no tail', () => { + const dropped = list.dropMany(1).tail(); + expect(dropped).toEqual([]); + }); + + it('should remove multiple nodes from the tail of the list', () => { + list.addMany(['a', 'b', 'c', 'd', 'e']).tail(); + + // "a" <-> "b" <-> "c" <-> "d" <-> "e" + + list.dropMany(3).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(); + }); + + it('should not change the list when count is less than or equal to zero', () => { + list.add('a').tail(); + + // "a" + + list.dropMany(0).tail(); + + // "a" + + list.dropMany(-1).tail(); + + // "a" + + expect(list.length).toBe(1); + expect(list.head.value).toBe('a'); + expect(list.tail.value).toBe('a'); + }); + }); + + describe('#byIndex', () => { + it('should remove multiple nodes starting from given index', () => { + list.addMany(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']).tail(); + + // "a" <-> "b" <-> "c" <-> "d" <-> "e" <-> "f" <-> "g" <-> "h" + + list.dropMany(2).byIndex(1); + + // "a" <-> "d" <-> "e" <-> "f" <-> "g" <-> "h" + + list.dropMany(2).byIndex(-3); + + // "a" <-> "d" <-> "e" <-> "h" + + expect(list.length).toBe(4); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('d'); + expect(list.head.next.next.value).toBe('e'); + expect(list.head.next.next.next.value).toBe('h'); + expect(list.tail.value).toBe('h'); + expect(list.tail.previous.value).toBe('e'); + expect(list.tail.previous.previous.value).toBe('d'); + expect(list.tail.previous.previous.previous.value).toBe('a'); + + list.dropMany(4).byIndex(0); + + expect(list.length).toBe(0); + expect(list.head).toBeUndefined(); + expect(list.tail).toBeUndefined(); + }); + + it('should return empty array when list is empty', () => { + const dropped = list.dropMany(1).byIndex(0); + expect(dropped).toEqual([]); + }); + + it('should return empty array when given index does not exist', () => { + list.addMany(['a', 'b', 'c']).tail(); + + // "a" <-> "b" <-> "c" + + const dropped = list.dropMany(3).byIndex(4); + + // "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'); + }); + + it('should remove from start when given index is less than minus size', () => { + list.addMany(['a', 'b', 'c', 'd', 'e']).tail(); + + // "a" <-> "b" <-> "c" <-> "d" <-> "e" + + list.dropMany(2).byIndex(-9); + + // "c" <-> "d" <-> "e" + + expect(list.length).toBe(3); + expect(list.head.value).toBe('c'); + expect(list.head.next.value).toBe('d'); + expect(list.head.next.next.value).toBe('e'); + expect(list.tail.value).toBe('e'); + expect(list.tail.previous.value).toBe('d'); + expect(list.tail.previous.previous.value).toBe('c'); + }); + + it('should remove from end when given index + count is larger than or equal to size', () => { + list.addMany(['a', 'b', 'c', 'd', 'e']).tail(); + + // "a" <-> "b" <-> "c" <-> "d" <-> "e" + + list.dropMany(2).byIndex(3); + + // "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'); + + // "a" <-> "b" <-> "c" + + list.dropMany(9).byIndex(1); + + // "a" + + expect(list.length).toBe(1); + expect(list.head.value).toBe('a'); + expect(list.tail.value).toBe('a'); + }); + + it('should not change the list when count is less than or equal to zero', () => { + list.add('a').tail(); + + // "a" + + list.dropMany(0).byIndex(0); + + // "a" + + list.dropMany(-1).byIndex(0); + + // "a" + + expect(list.length).toBe(1); + expect(list.head.value).toBe('a'); + expect(list.tail.value).toBe('a'); + }); + }); + }); + describe('#get', () => { it('should return node at given index', () => { list.add('a').tail(); @@ -761,6 +1453,27 @@ describe('Linked List (Doubly)', () => { }); }); + describe('#toNodeArray', () => { + it('should return array of nodes', () => { + 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.toNodeArray(); + + expect(arr.every(node => node instanceof ListNode)).toBe(true); + expect(arr.map(node => node.value)).toEqual(['a', 2, 'c', { k: 4, v: 'd' }]); + }); + + it('should return empty array when list is empty', () => { + const arr = list.toNodeArray(); + expect(arr).toEqual([]); + }); + }); + describe('#toString', () => { it('should return string representation', () => { list.addTail('a'); @@ -778,6 +1491,23 @@ describe('Linked List (Doubly)', () => { const str = list.toString(); expect(str).toBe(''); }); + + it('should be coercible', () => { + list.addMany(['a', 'b', 'c']).tail(); + + // "a" <-> "b" <-> "c" + + expect('' + list).toBe('"a" <-> "b" <-> "c"'); + }); + + it('should be able to receive a custom mapperFn', () => { + list.addMany([{ x: 1 }, { x: 2 }, { x: 3 }]).tail(); + + // {"x":1} <-> {"x":2} <-> {"x":3} + + const str = list.toString(value => value.x); + expect(str).toBe('1 <-> 2 <-> 3'); + }); }); it('should be iterable', () => { 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 42cc5bc941..9bd3e21941 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 @@ -27,65 +27,107 @@ export class LinkedList { return this.size; } - private linkWith( + private attach( value: T, previousNode: ListNode | undefined, nextNode: ListNode | undefined, ): ListNode { - const node = new ListNode(value); - if (!previousNode) return this.addHead(value); + if (!nextNode) return this.addTail(value); + const node = new ListNode(value); node.previous = previousNode; previousNode.next = node; node.next = nextNode; nextNode.previous = node; - this.size += 1; + this.size++; + + return node; + } + + private attachMany( + values: T[], + previousNode: ListNode | undefined, + nextNode: ListNode | undefined, + ): ListNode[] { + if (!values.length) return []; + + if (!previousNode) return this.addManyHead(values); + + if (!nextNode) return this.addManyTail(values); + + const list = new LinkedList(); + list.addManyTail(values); + list.first!.previous = previousNode; + previousNode.next = list.first; + list.last!.next = nextNode; + nextNode.previous = list.last; + + this.size += values.length; + + return list.toNodeArray(); + } + + private detach(node: ListNode) { + if (!node.previous) return this.dropHead(); + + if (!node.next) return this.dropTail(); + + node.previous.next = node.next; + node.next.previous = node.previous; + + this.size--; 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); - }, + after: (previousValue: T, compareFn: ListComparisonFn = compare) => + this.addAfter(value, previousValue, compareFn), + before: (nextValue: T, compareFn: ListComparisonFn = compare) => + this.addBefore(value, nextValue, compareFn), + byIndex: (position: number) => this.addByIndex(value, position), + head: () => this.addHead(value), + tail: () => this.addTail(value), + }; + } + + addMany(values: T[]) { + return { + after: (previousValue: T, compareFn: ListComparisonFn = compare) => + this.addManyAfter(values, previousValue, compareFn), + before: (nextValue: T, compareFn: ListComparisonFn = compare) => + this.addManyBefore(values, nextValue, compareFn), + byIndex: (position: number) => this.addManyByIndex(values, position), + head: () => this.addManyHead(values), + tail: () => this.addManyTail(values), }; } - addAfter(value: T, previousValue: T, compareFn = compare): ListNode { + addAfter(value: T, previousValue: T, compareFn: ListComparisonFn = compare): ListNode { const previous = this.find(node => compareFn(node.value, previousValue)); - return previous ? this.linkWith(value, previous, previous.next) : this.addTail(value); + return previous ? this.attach(value, previous, previous.next) : this.addTail(value); } - addBefore(value: T, nextValue: T, compareFn = compare): ListNode { + addBefore(value: T, nextValue: T, compareFn: ListComparisonFn = compare): ListNode { const next = this.find(node => compareFn(node.value, nextValue)); - return next ? this.linkWith(value, next.previous, next) : this.addHead(value); + return next ? this.attach(value, next.previous, next) : this.addHead(value); } addByIndex(value: T, position: number): ListNode { + if (position < 0) position += this.size; + else if (position >= this.size) return this.addTail(value); + 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); + return this.attach(value, next.previous, next); } addHead(value: T): ListNode { @@ -97,7 +139,7 @@ export class LinkedList { else this.last = node; this.first = node; - this.size += 1; + this.size++; return node; } @@ -114,51 +156,92 @@ export class LinkedList { this.last = node; } - this.size += 1; + this.size++; return node; } + addManyAfter( + values: T[], + previousValue: T, + compareFn: ListComparisonFn = compare, + ): ListNode[] { + const previous = this.find(node => compareFn(node.value, previousValue)); + + return previous ? this.attachMany(values, previous, previous.next) : this.addManyTail(values); + } + + addManyBefore( + values: T[], + nextValue: T, + compareFn: ListComparisonFn = compare, + ): ListNode[] { + const next = this.find(node => compareFn(node.value, nextValue)); + + return next ? this.attachMany(values, next.previous, next) : this.addManyHead(values); + } + + addManyByIndex(values: T[], position: number): ListNode[] { + if (position < 0) position += this.size; + + if (position <= 0) return this.addManyHead(values); + + if (position >= this.size) return this.addManyTail(values); + + const next = this.get(position)!; + + return this.attachMany(values, next.previous, next); + } + + addManyHead(values: T[]): ListNode[] { + return values.reduceRight[]>((nodes, value) => { + nodes.unshift(this.addHead(value)); + return nodes; + }, []); + } + + addManyTail(values: T[]): ListNode[] { + return values.map(value => this.addTail(value)); + } + drop() { 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), + byValue: (value: T, compareFn: ListComparisonFn = compare) => + this.dropByValue(value, compareFn), + byValueAll: (value: T, compareFn: ListComparisonFn = compare) => + this.dropByValueAll(value, compareFn), head: () => this.dropHead(), tail: () => this.dropTail(), }; } + dropMany(count: number) { + return { + byIndex: (position: number) => this.dropManyByIndex(count, position), + head: () => this.dropManyHead(count), + tail: () => this.dropManyTail(count), + }; + } + dropByIndex(position: number): ListNode | undefined { - if (position === 0) return this.dropHead(); - else if (position === this.size - 1) return this.dropTail(); + if (position < 0) position += this.size; const current = this.get(position); - if (current) { - current.previous!.next = current.next; - current.next!.previous = current.previous; - - this.size -= 1; - - return current; - } - - return undefined; + return current ? this.detach(current) : undefined; } - dropByValue(value: T, compareFn = compare): ListNode | undefined { + dropByValue(value: T, compareFn: ListComparisonFn = compare): ListNode | undefined { const position = this.findIndex(node => compareFn(node.value, value)); - if (position < 0) return undefined; - - return this.dropByIndex(position); + return position < 0 ? undefined : this.dropByIndex(position); } - dropByValueAll(value: T, compareFn = compare): ListNode[] { + dropByValueAll(value: T, compareFn: ListComparisonFn = compare): ListNode[] { const dropped: ListNode[] = []; - for (let current = this.first, position = 0; current; position += 1, current = current.next) { + for (let current = this.first, position = 0; current; position++, current = current.next) { if (compareFn(current.value, value)) { dropped.push(this.dropByIndex(position - dropped.length)!); } @@ -176,7 +259,7 @@ export class LinkedList { if (this.first) this.first.previous = undefined; else this.last = undefined; - this.size -= 1; + this.size--; return head; } @@ -193,7 +276,7 @@ export class LinkedList { if (this.last) this.last.next = undefined; else this.first = undefined; - this.size -= 1; + this.size--; return tail; } @@ -201,24 +284,66 @@ export class LinkedList { return undefined; } - find(predicate: ListIteratorFunction): ListNode | undefined { - for (let current = this.first, position = 0; current; position += 1, current = current.next) { + dropManyByIndex(count: number, position: number): ListNode[] { + if (count <= 0) return []; + + if (position < 0) position = Math.max(position + this.size, 0); + else if (position >= this.size) return []; + + count = Math.min(count, this.size - position); + + const dropped: ListNode[] = []; + + while (count--) { + const current = this.get(position); + dropped.push(this.detach(current!)!); + } + + return dropped; + } + + dropManyHead(count: Exclude): ListNode[] { + if (count <= 0) return []; + + count = Math.min(count, this.size); + + const dropped: ListNode[] = []; + + while (count--) dropped.unshift(this.dropHead()!); + + return dropped; + } + + dropManyTail(count: Exclude): ListNode[] { + if (count <= 0) return []; + + count = Math.min(count, this.size); + + const dropped: ListNode[] = []; + + while (count--) dropped.push(this.dropTail()!); + + return dropped; + } + + find(predicate: ListIteratorFn): ListNode | undefined { + for (let current = this.first, position = 0; current; position++, current = current.next) { if (predicate(current, position, this)) return current; } return undefined; } - findIndex(predicate: ListIteratorFunction): number { - for (let current = this.first, position = 0; current; position += 1, current = current.next) { + findIndex(predicate: ListIteratorFn): number { + for (let current = this.first, position = 0; current; position++, current = current.next) { if (predicate(current, position, this)) return position; } return -1; } - forEach(callback: ListIteratorFunction) { - for (let node = this.first, position = 0; node; position += 1, node = node.next) { + forEach(callback: ListIteratorFn) { + for (let node = this.first, position = 0; node; position++, node = node.next) { callback(node, position, this); } } @@ -227,7 +352,7 @@ export class LinkedList { return this.find((_, index) => position === index); } - indexOf(value: T, compareFn = compare): number { + indexOf(value: T, compareFn: ListComparisonFn = compare): number { return this.findIndex(node => compareFn(node.value, value)); } @@ -239,20 +364,32 @@ export class LinkedList { return array; } - toString(): string { + toNodeArray(): ListNode[] { + const array = new Array(this.size); + + this.forEach((node, index) => (array[index!] = node)); + + return array; + } + + toString(mapperFn: ListMapperFn = JSON.stringify): string { return this.toArray() - .map(value => JSON.stringify(value)) + .map(value => mapperFn(value)) .join(' <-> '); } *[Symbol.iterator]() { - for (let node = this.first, position = 0; node; position += 1, node = node.next) { + for (let node = this.first, position = 0; node; position++, node = node.next) { yield node.value; } } } -export type ListIteratorFunction = ( +export type ListMapperFn = (value: T) => any; + +export type ListComparisonFn = (value1: T, value2: T) => boolean; + +export type ListIteratorFn = ( node: ListNode, index?: number, list?: LinkedList,