From 4f8670c6579aafd57e1a4ec2bd85fef284e4681e Mon Sep 17 00:00:00 2001 From: TheSparta Date: Thu, 21 Dec 2023 22:27:19 +0000 Subject: [PATCH] Reworked LinkedList and Queue - Reworked LinkedList and Queue - Written tests for LinkedList and Queue --- project/src/context/ApplicationContext.ts | 24 +- project/src/utils/ImporterUtil.ts | 4 +- .../src/utils/collections/lists/LinkedList.ts | 464 +++++++++++------- project/src/utils/collections/lists/Nodes.ts | 5 + project/src/utils/collections/queue/Queue.ts | 47 +- .../collections/lists/LinkedList.test.ts | 263 ++++++++++ .../utils/collections/queue/Queue.test.ts | 63 +++ 7 files changed, 646 insertions(+), 224 deletions(-) create mode 100644 project/src/utils/collections/lists/Nodes.ts create mode 100644 project/tests/utils/collections/lists/LinkedList.test.ts create mode 100644 project/tests/utils/collections/queue/Queue.test.ts diff --git a/project/src/context/ApplicationContext.ts b/project/src/context/ApplicationContext.ts index 5389df23..7fc19efd 100644 --- a/project/src/context/ApplicationContext.ts +++ b/project/src/context/ApplicationContext.ts @@ -12,14 +12,13 @@ export class ApplicationContext /** * Called like: - * + * ``` * const registerPlayerInfo = this.applicationContext.getLatestValue(ContextVariableType.REGISTER_PLAYER_REQUEST).getValue(); * * const activePlayerSessionId = this.applicationContext.getLatestValue(ContextVariableType.SESSION_ID).getValue(); * * const matchInfo = this.applicationContext.getLatestValue(ContextVariableType.RAID_CONFIGURATION).getValue(); - * @param type - * @returns + * ``` */ public getLatestValue(type: ContextVariableType): ContextVariable { @@ -27,18 +26,21 @@ export class ApplicationContext { return this.variables.get(type)?.getTail()?.getValue(); } - - return undefined; } public getValues(type: ContextVariableType): ContextVariable[] { if (this.variables.has(type)) { - return this.variables.get(type).toList(); - } + const res: ContextVariable[] = []; - return undefined; + for (const value of this.variables.get(type).values()) + { + res.push(value); + } + + return res; + } } public addValue(type: ContextVariableType, value: any): void @@ -53,12 +55,12 @@ export class ApplicationContext list = new LinkedList(); } - if (list.getSize() >= ApplicationContext.holderMaxSize) + if (list.length >= ApplicationContext.holderMaxSize) { - list.removeFirst(); + list.shift(); } - list.add(new ContextVariable(value, type)); + list.append(new ContextVariable(value, type)); this.variables.set(type, list); } diff --git a/project/src/utils/ImporterUtil.ts b/project/src/utils/ImporterUtil.ts index 51173cec..c90edfea 100644 --- a/project/src/utils/ImporterUtil.ts +++ b/project/src/utils/ImporterUtil.ts @@ -127,14 +127,14 @@ export class ImporterUtil directoriesToRead.enqueueAll(directories.map((d) => `${filepath}${d}`)); filesToProcess.enqueueAll(files.map((f) => new VisitNode(filepath, f))); - while (!directoriesToRead.isEmpty()) + while (directoriesToRead.length !== 0) { const directory = directoriesToRead.dequeue(); filesToProcess.enqueueAll(this.vfs.getFiles(directory).map((f) => new VisitNode(`${directory}/`, f))); directoriesToRead.enqueueAll(this.vfs.getDirs(directory).map((d) => `${directory}/${d}`)); } - while (!filesToProcess.isEmpty()) + while (filesToProcess.length !== 0) { const fileNode = filesToProcess.dequeue(); if (this.vfs.getFileExtension(fileNode.fileName) === "json") diff --git a/project/src/utils/collections/lists/LinkedList.ts b/project/src/utils/collections/lists/LinkedList.ts index 3a60d5ca..7c85a78e 100644 --- a/project/src/utils/collections/lists/LinkedList.ts +++ b/project/src/utils/collections/lists/LinkedList.ts @@ -1,231 +1,317 @@ +import { LinkedListNode } from "./Nodes"; + export class LinkedList { - private head: LinkedListNode; - private tail: LinkedListNode; + private head?: LinkedListNode; + private tail?: LinkedListNode; + private _length: number; - public add(t: T): void + public get length(): number { + return this._length; + } + + private set length(value: number) + { + this._length = value; + } + + constructor() + { + this.length = 0; + this.head = this.tail = undefined; + } + + /** + * Adds an element to the start of the list. + */ + public prepend(value: T): void + { + const node = new LinkedListNode(value); + this.length++; + if (!this.head) { - const node = new LinkedListNode(t); - this.head = node; - this.tail = node; + this.head = this.tail = node; + return; } - else + + node.next = this.head; + this.head.prev = node; + this.head = node; + } + + /** + * Adds an element at the given index to the list. + */ + public insertAt(value: T, idx: number): void + { + if (idx < 0 || idx > this.length) { - let ref = this.head; - let next = this.head.getNextNode(); - while (next) - { - ref = next; - next = ref.getNextNode(); - } - const node = new LinkedListNode(t, ref); - ref.setNextNode(node); - this.tail = node; + return; } - } - public addRange(list: T[]): void - { - for (const item of list) + if (idx === 0) { - this.add(item); + this.prepend(value); + return; } - } - public getHead(): LinkedListNode - { - return this.head; - } - - public getTail(): LinkedListNode - { - return this.tail; - } - - public isEmpty(): boolean - { - return this.head === undefined || this.head === null; - } - - public getSize(): number - { - let size = 0; - let next = this.head; - while (next) + if (idx === this.length) { - size++; - next = next.getNextNode(); + this.append(value); + return; + } + + let ref = this.head; + for (let i = 0; i <= idx; ++i) + { + ref = ref.next; + } + + const node = new LinkedListNode(value); + this.length++; + + node.next = ref; + node.prev = ref.prev; + ref.prev = node; + + if (node.prev) + { + node.prev.next = node; } - return size; } - public removeFirst(): LinkedListNode + /** + * Adds an element to the end of the list. + */ + public append(value: T): void { - if (!this.head) - { - return undefined; - } + const node = new LinkedListNode(value); + this.length++; - const node = this.head; - if (this.head.getNextNode()) - { - this.head = this.head.getNextNode(); - this.head.setPreviousNode(undefined); - } - else - { - this.head = undefined; - } - return node; - } - - public removeLast(): LinkedListNode - { if (!this.tail) { - return undefined; + this.head = this.tail = node; + return; } - const node = this.tail; - if (this.tail.getPreviousNode()) + node.prev = this.tail; + this.tail.next = node; + this.tail = this.tail.next; + } + + /** + * Returns the first element's value. + */ + public getHead(): T + { + return this.head?.value; + } + + /** + * Finds the element from the list at the given index and returns it's value. + */ + public get(idx: number): T + { + if (idx < 0 || idx >= this.length) { - this.tail = this.tail.getPreviousNode(); - this.tail.setNextNode(undefined); + return; } - else + + if (idx === 0) + { + return this.getHead(); + } + + if (idx === this.length - 1) + { + return this.getTail(); + } + + for (const [index, value] of this.entries()) + { + if (idx === index) + { + return value; + } + } + } + + /** + * Returns the last element's value. + */ + public getTail(): T + { + return this.tail?.value; + } + + /** + * Finds and removes the first element from a list that has a value equal to the given value, returns it's value if it successfully removed it. + */ + public remove(value: T): T + { + let ref = this.head; + for (let i = 0; ref && i < this.length; ++i) + { + if (ref.value === value) + { + break; + } + ref = ref.next; + } + + if (!ref) + { + return; + } + + this.length--; + + if (this.length === 0) + { + const out = this.head.value; + this.head = this.tail = undefined; + return out; + } + + if (ref.prev) + { + ref.prev.next = ref.next; + } + if (ref.next) + { + ref.next.prev = ref.prev; + } + + if (ref === this.head) + { + this.head = ref.next; + } + + if (ref === this.tail) + { + this.tail = ref.prev; + } + + ref.prev = ref.next = undefined; + + return ref.value; + } + + /** + * Removes the first element from the list and returns it's value. If the list is empty, undefined is returned and the list is not modified. + */ + public shift(): T + { + if (!this.head) + { + return; + } + + this.length--; + + const ref = this.head; + this.head = this.head.next; + + ref.next = undefined; + + if (this.length === 0) { this.tail = undefined; } - return node; + + return ref.value; } - public indexOf(func: (t: T) => boolean): number + /** + * Removes the element from the list at the given index and returns it's value. + */ + public removeAt(idx: number): T { - const node = this.head; - let index = 0; - while (node) + if (idx < 0 || idx >= this.length) { - if (func(node.getValue())) - { - return index; - } - index++; + return; } - return undefined; + + if (idx === 0) + { + return this.shift(); + } + + if (idx === this.length - 1) + { + return this.pop(); + } + + let ref = this.head; + this.length--; + + for (let i = 0; i < idx; ++i) + { + ref = ref.next; + } + + if (ref.prev) + { + ref.prev.next = ref.next; + } + if (ref.next) + { + ref.next.prev = ref.prev; + } + + return ref.value; } - public contains(func: (t: T) => boolean): boolean + /** + * Removes the last element from the list and returns it's value. If the list is empty, undefined is returned and the list is not modified. + */ + public pop(): T + { + if (!this.tail) + { + return; + } + + this.length--; + + const ref = this.tail; + this.tail = this.tail.prev; + + ref.prev = undefined; + + if (this.length === 0) + { + this.head = undefined; + } + + return ref.value; + } + + /** + * Returns an iterable of index, value pairs for every entry in the list. + */ + public *entries(): IterableIterator<[number, T]> + { + let node = this.head; + for (let i = 0; i < this.length; ++i) + { + yield [i, node.value]; + node = node.next; + } + } + + /** + * Returns an iterable of values in the list. + */ + public *values(): IterableIterator { let node = this.head; while (node) { - if (func(node.getValue())) - { - return true; - } - node = node.getNextNode(); + yield node.value; + node = node.next; } - return false; - } - - public forEachNode(func: (t: LinkedListNode) => void): void - { - let node = this.head; - while (node) - { - func(node); - node = node.getNextNode(); - } - } - - public forEachValue(func: (t: T) => void): void - { - let node = this.head; - while (node) - { - func(node.getValue()); - node = node.getNextNode(); - } - } - - public findFirstNode(func: (t: LinkedListNode) => boolean): LinkedListNode - { - let node = this.head; - while (node) - { - if (func(node)) - { - return node; - } - node = node.getNextNode(); - } - return undefined; - } - - public findFirstValue(func: (t: T) => boolean): T - { - let node = this.head; - while (node) - { - if (func(node.getValue())) - { - return node.getValue(); - } - node = node.getNextNode(); - } - return undefined; - } - - public toList(): T[] - { - const elements: T[] = []; - let node = this.head; - while (node) - { - elements.push(node.getValue()); - node = node.getNextNode(); - } - return elements; - } -} - -export class LinkedListNode -{ - private previous: LinkedListNode; - private value: T; - private next: LinkedListNode; - - constructor(value: T, previous: LinkedListNode = undefined, next: LinkedListNode = undefined) - { - this.value = value; - this.previous = previous; - this.next = next; - } - - public getValue(): T - { - return this.value; - } - - public getNextNode(): LinkedListNode - { - return this.next; - } - - public setNextNode(node: LinkedListNode): void - { - this.next = node; - } - - public getPreviousNode(): LinkedListNode - { - return this.previous; - } - - public setPreviousNode(node: LinkedListNode): void - { - this.previous = node; } } diff --git a/project/src/utils/collections/lists/Nodes.ts b/project/src/utils/collections/lists/Nodes.ts new file mode 100644 index 00000000..bb045ee1 --- /dev/null +++ b/project/src/utils/collections/lists/Nodes.ts @@ -0,0 +1,5 @@ +export class LinkedListNode +{ + constructor(public value: T, public prev?: LinkedListNode, public next?: LinkedListNode) + {} +} diff --git a/project/src/utils/collections/queue/Queue.ts b/project/src/utils/collections/queue/Queue.ts index 5bce00e4..27e2d6dc 100644 --- a/project/src/utils/collections/queue/Queue.ts +++ b/project/src/utils/collections/queue/Queue.ts @@ -1,21 +1,30 @@ +import { LinkedList } from "../lists/LinkedList"; + export class Queue { - private elements: Record; - private head: number; - private tail: number; + private list: LinkedList; + + public get length(): number + { + return this.list.length; + } + constructor() { - this.elements = {}; - this.head = 0; - this.tail = 0; + this.list = new LinkedList(); } + /** + * Adds an element to the end of the queue. + */ public enqueue(element: T): void { - this.elements[this.tail] = element; - this.tail++; + this.list.append(element); } + /** + * Iterates over the elements received and adds each one to the end of the queue. + */ public enqueueAll(elements: T[]): void { for (const element of elements) @@ -24,25 +33,19 @@ export class Queue } } + /** + * Removes the first element from the queue and returns it's value. If the queue is empty, undefined is returned and the queue is not modified. + */ public dequeue(): T { - const item = this.elements[this.head]; - delete this.elements[this.head]; - this.head++; - return item; + return this.list.shift(); } + /** + * Returns the first element's value. + */ public peek(): T { - return this.elements[this.head]; - } - public getLength(): number - { - return this.tail - this.head; - } - - public isEmpty(): boolean - { - return this.getLength() === 0; + return this.list.getHead(); } } diff --git a/project/tests/utils/collections/lists/LinkedList.test.ts b/project/tests/utils/collections/lists/LinkedList.test.ts new file mode 100644 index 00000000..a4c06278 --- /dev/null +++ b/project/tests/utils/collections/lists/LinkedList.test.ts @@ -0,0 +1,263 @@ +import "reflect-metadata"; +import { describe, expect, it } from "vitest"; + +import { LinkedList } from "@spt-aki/utils/collections/lists/LinkedList"; + +describe("LinkedList", () => +{ + describe("prepend", () => + { + const list = new LinkedList(); + list.prepend(420); + list.prepend(69); + list.prepend(8008135); + list.prepend(1337); + + it("adds elements to the begining of the list", () => + { + expect(list.getHead()).toEqual(1337); + expect(list.length).toEqual(4); + }); + }); + + describe("append", () => + { + const list = new LinkedList(); + list.append(420); + list.append(69); + list.append(8008135); + list.append(1337); + + it("adds elements to the end of the list", () => + { + expect(list.getHead()).toEqual(420); + expect(list.length).toEqual(4); + }); + }); + + describe("insertAt", () => + { + describe("empty list", () => + { + const list = new LinkedList(); + + it("should allow insertions at index 0 only", () => + { + list.insertAt(420, 1); + expect(list.length).toEqual(0); + + list.insertAt(420, 0); + expect(list.length).toEqual(1); + }); + }); + + describe("filled list", () => + { + const list = new LinkedList(); + list.append(420); + list.append(69); + list.append(8008135); + list.append(1337); + + it("shouldn't insert if index is < 0 and > length", () => + { + list.insertAt(10100111001, -1); + expect(list.length).toEqual(4); + + list.insertAt(123, 5); // index 4 would work even though it's out of bounds because it's the next index, it's the same as doing an append + expect(list.length).toEqual(4); + }); + + it("should insert if index is between 0 and length", () => + { + list.insertAt(10100111001, 0); + expect(list.length).toEqual(5); + + list.insertAt(69420, 3); + expect(list.length).toEqual(6); + + list.insertAt(123, 6); + expect(list.length).toEqual(7); + }); + }); + }); + + describe("getHead/getTail", () => + { + it("should return undefined if the list is empty", () => + { + const list = new LinkedList(); + expect(list.getHead()).toEqual(undefined); + expect(list.getTail()).toEqual(undefined); + }); + + it("should return the head and the tail values if the list has 1 or more elements", () => + { + const list = new LinkedList(); + list.append(420); + list.append(69); + list.append(8008135); + + expect(list.getHead()).toEqual(420); + expect(list.getTail()).toEqual(8008135); + }); + }); + + describe("get", () => + { + describe("empty list", () => + { + const list = new LinkedList(); + + it("should return undefined", () => + { + expect(list.get(0)).toEqual(undefined); + expect(list.get(1)).toEqual(undefined); + }); + }); + + describe("filled list", () => + { + const list = new LinkedList(); + list.append(420); + list.append(69); + list.append(8008135); + list.append(1337); + + it("should return undefined if index is < 0 or >= length", () => + { + expect(list.get(-1)).toEqual(undefined); + expect(list.get(list.length)).toEqual(undefined); + }); + + it("should return the value if the index is between 0 and length - 1", () => + { + expect(list.get(0)).toEqual(420); + expect(list.get(1)).toEqual(69); + expect(list.get(list.length - 1)).toEqual(1337); + }); + }); + }); + + describe("remove", () => + { + const list = new LinkedList(); + list.append(420); + list.append(69); + list.append(8008135); + list.append(1337); + + it("should return undefined if it doesn't find any element with the same value", () => + { + expect(list.remove(10100111001)).toEqual(undefined); + expect(list.length).toEqual(4); + }); + + it("should remove an element and return it's value if one is found with the same value", () => + { + expect(list.remove(420)).toEqual(420); + expect(list.length).toEqual(3); + + expect(list.remove(8008135)).toEqual(8008135); + expect(list.length).toEqual(2); + + expect(list.remove(1337)).toEqual(1337); + expect(list.length).toEqual(1); + + expect(list.remove(69)).toEqual(69); + expect(list.length).toEqual(0); + }); + }); + + describe("shift", () => + { + describe("empty list", () => + { + const list = new LinkedList(); + + it("shouldn't change the list and should return undefined if list is empty", () => + { + expect(list.shift()).toEqual(undefined); + expect(list.length).toEqual(0); + }); + }); + + describe("filled list", () => + { + const list = new LinkedList(); + list.append(420); + list.append(1337); + + it("should remove the first element and return it's value", () => + { + expect(list.shift()).toEqual(420); + expect(list.length).toEqual(1); + + expect(list.shift()).toEqual(1337); + expect(list.length).toEqual(0); + }); + }); + }); + + describe("pop", () => + { + describe("empty list", () => + { + const list = new LinkedList(); + + it("shouldn't change the list and should return undefined if list is empty", () => + { + expect(list.pop()).toEqual(undefined); + expect(list.length).toEqual(0); + }); + }); + + describe("filled list", () => + { + const list = new LinkedList(); + list.append(420); + list.append(1337); + + it("should remove the first element and return it's value", () => + { + expect(list.pop()).toEqual(1337); + expect(list.length).toEqual(1); + + expect(list.pop()).toEqual(420); + expect(list.length).toEqual(0); + }); + }); + }); + + describe("removeAt", () => + { + const list = new LinkedList(); + list.append(420); + list.append(69); + list.append(8008135); + list.append(1337); + list.append(10100111001); + + it("should return undefined if index is < 0 or >= length", () => + { + expect(list.removeAt(-1)).toEqual(undefined); + expect(list.removeAt(list.length)).toEqual(undefined); + }); + + it("should remove an element and return it's value if index is between 0 and length - 1", () => + { + expect(list.removeAt(0)).toEqual(420); + expect(list.length).toEqual(4); + + expect(list.removeAt(2)).toEqual(1337); + expect(list.length).toEqual(3); + + expect(list.removeAt(list.length - 1)).toEqual(10100111001); + expect(list.length).toEqual(2); + + expect(list.removeAt(1)).toEqual(8008135); + expect(list.removeAt(0)).toEqual(69); + expect(list.length).toEqual(0); + }); + }); +}); diff --git a/project/tests/utils/collections/queue/Queue.test.ts b/project/tests/utils/collections/queue/Queue.test.ts new file mode 100644 index 00000000..77018a7c --- /dev/null +++ b/project/tests/utils/collections/queue/Queue.test.ts @@ -0,0 +1,63 @@ +import "reflect-metadata"; +import { describe, expect, it } from "vitest"; + +import { Queue } from "@spt-aki/utils/collections/queue/Queue"; + +describe("LinkedList", () => +{ + describe("enqueue", () => + { + const queue = new Queue(); + queue.enqueue(420); + queue.enqueue(69); + queue.enqueue(8008135); + queue.enqueue(1337); + + it("adds elements to the end of the queue", () => + { + expect(queue.peek()).toEqual(420); + expect(queue.length).toEqual(4); + }); + }); + + describe("enqueueAll", () => + { + const queue = new Queue(); + queue.enqueueAll([420, 69, 8008135, 1337]); + + it("iterates the array and adds each element to the end of the queue", () => + { + expect(queue.peek()).toEqual(420); + expect(queue.length).toEqual(4); + }); + }); + + describe("dequeue", () => + { + const queue = new Queue(); + queue.enqueueAll([420, 69, 8008135, 1337]); + + it("removes the first element and return it's value", () => + { + expect(queue.dequeue()).toEqual(420); + expect(queue.peek()).toEqual(69); + expect(queue.length).toEqual(3); + + expect(queue.dequeue()).toEqual(69); + expect(queue.peek()).toEqual(8008135); + expect(queue.length).toEqual(2); + + expect(queue.dequeue()).toEqual(8008135); + expect(queue.peek()).toEqual(1337); + expect(queue.length).toEqual(1); + + expect(queue.dequeue()).toEqual(1337); + expect(queue.peek()).toEqual(undefined); + expect(queue.length).toEqual(0); + + expect(queue.dequeue()).toEqual(undefined); + expect(queue.peek()).toEqual(undefined); + expect(queue.length).toEqual(0); + }); + }); +});