From 7feafd8d05f5db63feb08ca94129ee46c49cb7dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Darida?= Date: Sat, 27 Sep 2025 21:16:24 +0200 Subject: [PATCH 01/18] feat: add base classes and some tests --- src/utils/index.ts | 2 ++ src/utils/list/Binder.ts | 21 +++++++++++++++++++++ src/utils/list/Item.ts | 11 +++++++++++ src/utils/list/List.ts | 17 +++++++++++++++++ src/utils/list/index.ts | 3 +++ tests/utils/list/Binder.test.ts | 10 ++++++++++ tests/utils/list/index.test.ts | 15 +++++++++++++++ 7 files changed, 79 insertions(+) create mode 100644 src/utils/list/Binder.ts create mode 100644 src/utils/list/Item.ts create mode 100644 src/utils/list/List.ts create mode 100644 src/utils/list/index.ts create mode 100644 tests/utils/list/Binder.test.ts create mode 100644 tests/utils/list/index.test.ts diff --git a/src/utils/index.ts b/src/utils/index.ts index 3a2315d..6d78a00 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,5 @@ +export * from './list'; + export * as ColorUtil from './ColorUtil'; export * as MathUtil from './MathUtil'; export * as PointUtil from './PointUtil'; diff --git a/src/utils/list/Binder.ts b/src/utils/list/Binder.ts new file mode 100644 index 0000000..832c2e7 --- /dev/null +++ b/src/utils/list/Binder.ts @@ -0,0 +1,21 @@ +export class Binder { + protected _prev: Binder; + protected _next: Binder; + + public get prev(): Binder { + return this._prev; + } + + public get next(): Binder { + return this._next; + } + + constructor() { + this._prev = this; + this._next = this; + } + + public bind(prev: Binder): void {} + + public unbind(): void {} +} diff --git a/src/utils/list/Item.ts b/src/utils/list/Item.ts new file mode 100644 index 0000000..da9ef41 --- /dev/null +++ b/src/utils/list/Item.ts @@ -0,0 +1,11 @@ +import { Binder } from './Binder'; + +export class Item extends Binder { + protected _data: T; + + constructor(data: T) { + super(); + + this._data = data; + } +} diff --git a/src/utils/list/List.ts b/src/utils/list/List.ts new file mode 100644 index 0000000..a216eb0 --- /dev/null +++ b/src/utils/list/List.ts @@ -0,0 +1,17 @@ +import { Binder } from './Binder'; + +export class List { + private readonly _head: Binder; + private readonly _tail: Binder; + private _size: number; + + public get size(): number { + return this._size; + } + + constructor() { + this._head = new Binder(); + this._tail = new Binder(); + this._size = 0; + } +} diff --git a/src/utils/list/index.ts b/src/utils/list/index.ts new file mode 100644 index 0000000..eb62370 --- /dev/null +++ b/src/utils/list/index.ts @@ -0,0 +1,3 @@ +export * from './Binder'; +export * from './Item'; +export * from './List'; diff --git a/tests/utils/list/Binder.test.ts b/tests/utils/list/Binder.test.ts new file mode 100644 index 0000000..e92c7fd --- /dev/null +++ b/tests/utils/list/Binder.test.ts @@ -0,0 +1,10 @@ +import { Binder } from '../../../src/utils/list/Binder'; + +describe('Test Binder class', () => { + it('should be truthy', () => { + const binder = new Binder(); + expect(binder).toBeTruthy(); + expect(binder.prev).toBe(binder); + expect(binder.next).toBe(binder); + }); +}); diff --git a/tests/utils/list/index.test.ts b/tests/utils/list/index.test.ts new file mode 100644 index 0000000..2b81284 --- /dev/null +++ b/tests/utils/list/index.test.ts @@ -0,0 +1,15 @@ +import { Binder, Item, List } from '../../../src/utils/list'; + +describe('Test index.ts', () => { + it('should export list/Binder class', () => { + expect(Binder).toBeTruthy(); + }); + + it('should export list/Item class', () => { + expect(Item).toBeTruthy(); + }); + + it('should export list/List class', () => { + expect(List).toBeTruthy(); + }); +}); From 16df747c2a734c8e33b5dd6c075c8f4df97d4572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Darida?= Date: Sat, 27 Sep 2025 21:16:29 +0200 Subject: [PATCH 02/18] chore(release): 0.1.27 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c23749..3d375b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [0.1.27](https://github.com/rdarida/gameforge/compare/v0.1.26...v0.1.27) (2025-09-27) + + +### Features + +* add base classes and some tests ([7feafd8](https://github.com/rdarida/gameforge/commit/7feafd8d05f5db63feb08ca94129ee46c49cb7dc)) + ### [0.1.26](https://github.com/rdarida/gameforge/compare/v0.1.25...v0.1.26) (2025-09-27) diff --git a/package-lock.json b/package-lock.json index 25fb0d0..ef352aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gameforge", - "version": "0.1.26", + "version": "0.1.27", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gameforge", - "version": "0.1.26", + "version": "0.1.27", "license": "MIT", "dependencies": { "@pixi/sound": ">=5.2.3 <6.0.0", diff --git a/package.json b/package.json index 6d2fd06..211915f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gameforge", - "version": "0.1.26", + "version": "0.1.27", "description": "Lightweight HTML5 boilerplate for quick 2D game prototyping", "keywords": [ "lightweight", From 72491ff866e4839cc5a759d3833274a857eb5f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Darida?= Date: Sat, 27 Sep 2025 22:00:02 +0200 Subject: [PATCH 03/18] test: add more tests for util/list --- src/utils/list/Item.ts | 4 ++++ src/utils/list/List.ts | 2 +- tests/utils/list/Item.test.ts | 16 ++++++++++++++++ tests/utils/list/List.test.ts | 13 +++++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 tests/utils/list/Item.test.ts create mode 100644 tests/utils/list/List.test.ts diff --git a/src/utils/list/Item.ts b/src/utils/list/Item.ts index da9ef41..b6ef7ae 100644 --- a/src/utils/list/Item.ts +++ b/src/utils/list/Item.ts @@ -3,6 +3,10 @@ import { Binder } from './Binder'; export class Item extends Binder { protected _data: T; + public get data(): T { + return this._data; + } + constructor(data: T) { super(); diff --git a/src/utils/list/List.ts b/src/utils/list/List.ts index a216eb0..8c6a906 100644 --- a/src/utils/list/List.ts +++ b/src/utils/list/List.ts @@ -1,6 +1,6 @@ import { Binder } from './Binder'; -export class List { +export class List { private readonly _head: Binder; private readonly _tail: Binder; private _size: number; diff --git a/tests/utils/list/Item.test.ts b/tests/utils/list/Item.test.ts new file mode 100644 index 0000000..14e5ce8 --- /dev/null +++ b/tests/utils/list/Item.test.ts @@ -0,0 +1,16 @@ +import { Item } from '../../../src/utils/list/Item'; + +describe('Test Item class', () => { + let item: Item; + + beforeEach(() => { + item = new Item('data'); + }); + + it('should be truthy', () => { + expect(item).toBeTruthy(); + expect(item.data).toBe('data'); + expect(item.prev).toBe(item); + expect(item.next).toBe(item); + }); +}); diff --git a/tests/utils/list/List.test.ts b/tests/utils/list/List.test.ts new file mode 100644 index 0000000..2197016 --- /dev/null +++ b/tests/utils/list/List.test.ts @@ -0,0 +1,13 @@ +import { List } from '../../../src/utils/list/List'; + +describe('Test List class', () => { + let list: List; + + beforeEach(() => { + list = new List(); + }); + + it('should be truthy', () => { + expect(list).toBeTruthy(); + }); +}); From 7db6e3d30908933081abadbd94c1105819809d94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Darida?= Date: Sat, 27 Sep 2025 22:20:50 +0200 Subject: [PATCH 04/18] feat: implement Item.parse static method --- src/utils/list/Item.ts | 4 ++++ src/utils/list/List.ts | 10 +++++----- tests/utils/list/Item.test.ts | 6 ++++++ tests/utils/list/List.test.ts | 2 +- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/utils/list/Item.ts b/src/utils/list/Item.ts index b6ef7ae..070567d 100644 --- a/src/utils/list/Item.ts +++ b/src/utils/list/Item.ts @@ -12,4 +12,8 @@ export class Item extends Binder { this._data = data; } + + public static parse(value: K | Item): Item { + return value instanceof Item ? value : new Item(value); + } } diff --git a/src/utils/list/List.ts b/src/utils/list/List.ts index 8c6a906..bc35fb3 100644 --- a/src/utils/list/List.ts +++ b/src/utils/list/List.ts @@ -1,8 +1,8 @@ import { Binder } from './Binder'; -export class List { - private readonly _head: Binder; - private readonly _tail: Binder; +export class List { + private readonly _first: Binder; + private readonly _last: Binder; private _size: number; public get size(): number { @@ -10,8 +10,8 @@ export class List { } constructor() { - this._head = new Binder(); - this._tail = new Binder(); + this._first = new Binder(); + this._last = new Binder(); this._size = 0; } } diff --git a/tests/utils/list/Item.test.ts b/tests/utils/list/Item.test.ts index 14e5ce8..ddacb3a 100644 --- a/tests/utils/list/Item.test.ts +++ b/tests/utils/list/Item.test.ts @@ -13,4 +13,10 @@ describe('Test Item class', () => { expect(item.prev).toBe(item); expect(item.next).toBe(item); }); + + it('should parse a value and return an Item instance', () => { + const item = Item.parse('valueToParse'); + expect(item.data).toBe('valueToParse'); + expect(item).toStrictEqual(Item.parse(item)); + }); }); diff --git a/tests/utils/list/List.test.ts b/tests/utils/list/List.test.ts index 2197016..5f1f658 100644 --- a/tests/utils/list/List.test.ts +++ b/tests/utils/list/List.test.ts @@ -1,7 +1,7 @@ import { List } from '../../../src/utils/list/List'; describe('Test List class', () => { - let list: List; + let list: List; beforeEach(() => { list = new List(); From bf1d77a11736c5a19460ae415c4fb5afd73decd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Darida?= Date: Sun, 28 Sep 2025 01:19:34 +0200 Subject: [PATCH 05/18] feat: implement Binder.bind, and Binder.unbind methods --- src/utils/list/Binder.ts | 43 ++++++++++++++++++++++++++++-- tests/utils/list/Binder.test.ts | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/utils/list/Binder.ts b/src/utils/list/Binder.ts index 832c2e7..2c62b10 100644 --- a/src/utils/list/Binder.ts +++ b/src/utils/list/Binder.ts @@ -2,20 +2,59 @@ export class Binder { protected _prev: Binder; protected _next: Binder; + /** + * Gets the previous Binder in the chain. + */ public get prev(): Binder { return this._prev; } + /** + * Gets the next Binder in the chain. + */ public get next(): Binder { return this._next; } + /** + * Creates a new Binder instance. + * + * Each Binder stats as a self-referencing circular node + * (`prev = this`, `next = this`); + */ constructor() { this._prev = this; this._next = this; } - public bind(prev: Binder): void {} + /** + * Binds the given Binder right after this one in the chain. + * + * @param next The Binder to insert after the current one. + */ + public bind(next: Binder): void { + const a = this; + const b = next; + const c = this._next; - public unbind(): void {} + a._next = b; + b._prev = a; + + if (a === c) return; + + b._next = c; + c._prev = b; + } + + /** + * Unbinds this Binder instance from the chain. + */ + public unbind(): void { + const { _prev: a, _next: b } = this; + + a._next = b; + b._prev = a; + this._prev = this; + this._next = this; + } } diff --git a/tests/utils/list/Binder.test.ts b/tests/utils/list/Binder.test.ts index e92c7fd..9f663a0 100644 --- a/tests/utils/list/Binder.test.ts +++ b/tests/utils/list/Binder.test.ts @@ -7,4 +7,50 @@ describe('Test Binder class', () => { expect(binder.prev).toBe(binder); expect(binder.next).toBe(binder); }); + + it('should bind three Binder instances together', () => { + const a = new Binder(); + const b = new Binder(); + const c = new Binder(); + + a.bind(c); + + expect(a.prev).toBe(a); + expect(a.next).toBe(c); + expect(c.prev).toBe(a); + expect(c.next).toBe(c); + + a.bind(b); + + expect(a.next).toBe(b); + expect(b.prev).toBe(a); + expect(b.next).toBe(c); + expect(c.prev).toBe(b); + }); + + it('should unlink a Binder instance from a chain', () => { + const a = new Binder(); + const b = new Binder(); + const c = new Binder(); + + a.bind(b); + b.bind(c); + + expect(a.prev).toBe(a); + expect(a.next).toBe(b); + expect(b.prev).toBe(a); + expect(b.next).toBe(c); + expect(c.prev).toBe(b); + expect(c.next).toBe(c); + + b.unbind(); + + expect(b.prev).toBe(b); + expect(b.next).toBe(b); + + expect(a.prev).toBe(a); + expect(a.next).toBe(c); + expect(c.prev).toBe(a); + expect(c.next).toBe(c); + }); }); From 7d687c3faf07e82f9fc751f1b87a58d22bc15af3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Darida?= Date: Sun, 28 Sep 2025 01:23:19 +0200 Subject: [PATCH 06/18] docs: add comment to Item class --- src/utils/list/Item.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/utils/list/Item.ts b/src/utils/list/Item.ts index 070567d..f678815 100644 --- a/src/utils/list/Item.ts +++ b/src/utils/list/Item.ts @@ -3,16 +3,33 @@ import { Binder } from './Binder'; export class Item extends Binder { protected _data: T; + /** + * Gets the data stored in this item. + */ public get data(): T { return this._data; } + /** + * Creates a new Item instance with the given data. + * + * @param data The data to store in this item. + */ constructor(data: T) { super(); this._data = data; } + /** + * Converts a value or an Item into an Item instance. + * + * If the given value is already an Item, it is returned unchanged. + * Otherwise, a new Item is created to wrap the value. + * + * @param value Either a raw value of type K or an Item. + * @returns An Item instance. + */ public static parse(value: K | Item): Item { return value instanceof Item ? value : new Item(value); } From eff27958bb142ebb978e22db225db8aeef095aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Darida?= Date: Sun, 28 Sep 2025 02:28:56 +0200 Subject: [PATCH 07/18] refactor: renaming properties in List class --- src/utils/list/List.ts | 18 ++++++++++-------- tests/utils/list/List.test.ts | 1 + 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/utils/list/List.ts b/src/utils/list/List.ts index bc35fb3..077cac6 100644 --- a/src/utils/list/List.ts +++ b/src/utils/list/List.ts @@ -1,17 +1,19 @@ import { Binder } from './Binder'; export class List { - private readonly _first: Binder; - private readonly _last: Binder; - private _size: number; + private readonly _head: Binder; + private readonly _tail: Binder; + private _length: number; - public get size(): number { - return this._size; + public get length(): number { + return this._length; } constructor() { - this._first = new Binder(); - this._last = new Binder(); - this._size = 0; + this._head = new Binder(); + this._tail = new Binder(); + this._head.bind(this._tail); + + this._length = 0; } } diff --git a/tests/utils/list/List.test.ts b/tests/utils/list/List.test.ts index 5f1f658..d37971c 100644 --- a/tests/utils/list/List.test.ts +++ b/tests/utils/list/List.test.ts @@ -9,5 +9,6 @@ describe('Test List class', () => { it('should be truthy', () => { expect(list).toBeTruthy(); + expect(list.length).toBe(0); }); }); From 86ee3455585c7bfd5d032485fc63a3583b8ed3a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Darida?= Date: Sun, 28 Sep 2025 10:54:29 +0200 Subject: [PATCH 08/18] feat: implement List.getFirst and List.addFirst methods --- src/utils/list/List.ts | 18 ++++++++++++++++++ tests/utils/list/List.test.ts | 9 +++++++++ 2 files changed, 27 insertions(+) diff --git a/src/utils/list/List.ts b/src/utils/list/List.ts index 077cac6..dc0ba39 100644 --- a/src/utils/list/List.ts +++ b/src/utils/list/List.ts @@ -1,4 +1,5 @@ import { Binder } from './Binder'; +import { Item } from './Item'; export class List { private readonly _head: Binder; @@ -16,4 +17,21 @@ export class List { this._length = 0; } + + public getFirst(): Item | undefined { + return 0 < this._length ? (this._head.next as Item) : undefined; + } + + public addFirst(element: T): Item { + return this.link(this._head, element); + } + + private link(prev: Binder, element: T): Item { + const item = Item.parse(element); + prev.bind(item); + + this._length++; + + return item; + } } diff --git a/tests/utils/list/List.test.ts b/tests/utils/list/List.test.ts index d37971c..d30d391 100644 --- a/tests/utils/list/List.test.ts +++ b/tests/utils/list/List.test.ts @@ -10,5 +10,14 @@ describe('Test List class', () => { it('should be truthy', () => { expect(list).toBeTruthy(); expect(list.length).toBe(0); + expect(list.getFirst()).toBeUndefined(); + }); + + it('should insert a new element to the beginning of the list', () => { + list.addFirst('item0'); + expect(list.getFirst()?.data).toBe('item0'); + + list.addFirst('item1'); + expect(list.getFirst()?.data).toBe('item1'); }); }); From 537443c11e3ab34c9c652dcd631a46135958a0f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Darida?= Date: Sun, 28 Sep 2025 11:00:21 +0200 Subject: [PATCH 09/18] feat: implement List.getLast and List.addLast methods --- src/utils/list/List.ts | 8 ++++++++ tests/utils/list/List.test.ts | 21 ++++++++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/utils/list/List.ts b/src/utils/list/List.ts index dc0ba39..87b5251 100644 --- a/src/utils/list/List.ts +++ b/src/utils/list/List.ts @@ -22,10 +22,18 @@ export class List { return 0 < this._length ? (this._head.next as Item) : undefined; } + public getLast(): Item | undefined { + return 0 < this._length ? (this._tail.prev as Item) : undefined; + } + public addFirst(element: T): Item { return this.link(this._head, element); } + public addLast(element: T): Item { + return this.link(this._tail.prev, element); + } + private link(prev: Binder, element: T): Item { const item = Item.parse(element); prev.bind(item); diff --git a/tests/utils/list/List.test.ts b/tests/utils/list/List.test.ts index d30d391..24a1840 100644 --- a/tests/utils/list/List.test.ts +++ b/tests/utils/list/List.test.ts @@ -11,13 +11,24 @@ describe('Test List class', () => { expect(list).toBeTruthy(); expect(list.length).toBe(0); expect(list.getFirst()).toBeUndefined(); + expect(list.getLast()).toBeUndefined(); }); - it('should insert a new element to the beginning of the list', () => { - list.addFirst('item0'); - expect(list.getFirst()?.data).toBe('item0'); + it('should insert a new element at the beginning of the list', () => { + list.addFirst('firstItem1'); + expect(list.length).toBe(1); + expect(list.getFirst()?.data).toBe('firstItem1'); - list.addFirst('item1'); - expect(list.getFirst()?.data).toBe('item1'); + list.addFirst('firstItem2'); + expect(list.length).toBe(2); + expect(list.getFirst()?.data).toBe('firstItem2'); + }); + + it('should insert a new element to the end of the list', () => { + list.addLast('lastItem1'); + expect(list.getLast()?.data).toBe('lastItem1'); + + list.addLast('lastItem2'); + expect(list.getLast()?.data).toBe('lastItem2'); }); }); From 67082b8b88e6826e69995236a3f9147b26d77e18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Darida?= Date: Sun, 28 Sep 2025 11:17:58 +0200 Subject: [PATCH 10/18] feat: implement List.removeFirst method --- src/utils/list/List.ts | 14 ++++++++++++++ tests/utils/list/List.test.ts | 13 +++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/utils/list/List.ts b/src/utils/list/List.ts index 87b5251..cd06792 100644 --- a/src/utils/list/List.ts +++ b/src/utils/list/List.ts @@ -34,6 +34,10 @@ export class List { return this.link(this._tail.prev, element); } + public removeFirst(): Item | undefined { + return this.unlink(this.getFirst()); + } + private link(prev: Binder, element: T): Item { const item = Item.parse(element); prev.bind(item); @@ -42,4 +46,14 @@ export class List { return item; } + + private unlink(item: Item | undefined): Item | undefined { + if (item == undefined) return undefined; + + item.unbind(); + + this._length--; + + return item; + } } diff --git a/tests/utils/list/List.test.ts b/tests/utils/list/List.test.ts index 24a1840..4698711 100644 --- a/tests/utils/list/List.test.ts +++ b/tests/utils/list/List.test.ts @@ -14,7 +14,7 @@ describe('Test List class', () => { expect(list.getLast()).toBeUndefined(); }); - it('should insert a new element at the beginning of the list', () => { + it('should add a new element at the beginning of the list', () => { list.addFirst('firstItem1'); expect(list.length).toBe(1); expect(list.getFirst()?.data).toBe('firstItem1'); @@ -24,11 +24,20 @@ describe('Test List class', () => { expect(list.getFirst()?.data).toBe('firstItem2'); }); - it('should insert a new element to the end of the list', () => { + it('should add a new element to the end of the list', () => { list.addLast('lastItem1'); expect(list.getLast()?.data).toBe('lastItem1'); list.addLast('lastItem2'); expect(list.getLast()?.data).toBe('lastItem2'); }); + + it('should remove the first element of the list', () => { + expect(list.removeFirst()).toBeUndefined(); + + list.addFirst('firstItem1'); + const expected = list.addFirst('firstItem2'); + const actual = list.removeFirst(); + expect(actual).toBe(expected); + }); }); From 245f56f1b7e4f94308c886f034f5f5c454d617d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Darida?= Date: Sun, 28 Sep 2025 11:24:17 +0200 Subject: [PATCH 11/18] feat: implement List.removeLast method --- src/utils/list/List.ts | 4 ++++ tests/utils/list/List.test.ts | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/utils/list/List.ts b/src/utils/list/List.ts index cd06792..e20232c 100644 --- a/src/utils/list/List.ts +++ b/src/utils/list/List.ts @@ -38,6 +38,10 @@ export class List { return this.unlink(this.getFirst()); } + public removeLast(): Item | undefined { + return this.unlink(this.getLast()); + } + private link(prev: Binder, element: T): Item { const item = Item.parse(element); prev.bind(item); diff --git a/tests/utils/list/List.test.ts b/tests/utils/list/List.test.ts index 4698711..a9a4785 100644 --- a/tests/utils/list/List.test.ts +++ b/tests/utils/list/List.test.ts @@ -26,9 +26,11 @@ describe('Test List class', () => { it('should add a new element to the end of the list', () => { list.addLast('lastItem1'); + expect(list.length).toBe(1); expect(list.getLast()?.data).toBe('lastItem1'); list.addLast('lastItem2'); + expect(list.length).toBe(2); expect(list.getLast()?.data).toBe('lastItem2'); }); @@ -37,7 +39,16 @@ describe('Test List class', () => { list.addFirst('firstItem1'); const expected = list.addFirst('firstItem2'); - const actual = list.removeFirst(); - expect(actual).toBe(expected); + expect(list.removeFirst()).toBe(expected); + expect(list.length).toBe(1); + }); + + it('should remove the last element of the list', () => { + expect(list.removeLast()).toBeUndefined(); + + list.addLast('firstItem1'); + const expected = list.addLast('firstItem2'); + expect(list.removeLast()).toBe(expected); + expect(list.length).toBe(1); }); }); From a1693fbbf0e62290807912573a87ef2e641bcb9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Darida?= Date: Sun, 28 Sep 2025 11:29:59 +0200 Subject: [PATCH 12/18] feat: implement List.clear method --- src/utils/list/List.ts | 6 ++++++ tests/utils/list/List.test.ts | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/utils/list/List.ts b/src/utils/list/List.ts index e20232c..5ad3dd9 100644 --- a/src/utils/list/List.ts +++ b/src/utils/list/List.ts @@ -42,6 +42,12 @@ export class List { return this.unlink(this.getLast()); } + public clear(): void { + while (0 < this.length) { + this.removeFirst(); + } + } + private link(prev: Binder, element: T): Item { const item = Item.parse(element); prev.bind(item); diff --git a/tests/utils/list/List.test.ts b/tests/utils/list/List.test.ts index a9a4785..2e26428 100644 --- a/tests/utils/list/List.test.ts +++ b/tests/utils/list/List.test.ts @@ -51,4 +51,15 @@ describe('Test List class', () => { expect(list.removeLast()).toBe(expected); expect(list.length).toBe(1); }); + + it('should clear the list', () => { + for (let i = 0; i < 10; ++i) { + list.addFirst(`item${i}`); + } + + expect(list.length).toBe(10); + + list.clear(); + expect(list.length).toBe(0); + }); }); From bc4e287ccdab6002222338085e99fdabd1af9333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Darida?= Date: Sun, 28 Sep 2025 11:44:39 +0200 Subject: [PATCH 13/18] feat: implement List.find and List.contains methods --- src/utils/list/List.ts | 20 ++++++++++++++++++++ tests/utils/list/List.test.ts | 8 ++++++++ 2 files changed, 28 insertions(+) diff --git a/src/utils/list/List.ts b/src/utils/list/List.ts index 5ad3dd9..81ba0fe 100644 --- a/src/utils/list/List.ts +++ b/src/utils/list/List.ts @@ -48,6 +48,26 @@ export class List { } } + public find(element: T): Item | undefined { + let item = this.getFirst(); + + if (item == undefined) return undefined; + + while (item !== this._tail) { + if (item.data === element) { + return item; + } else { + item = item.next as Item; + } + } + + return undefined; + } + + public contains(element: T): boolean { + return this.find(element) != undefined; + } + private link(prev: Binder, element: T): Item { const item = Item.parse(element); prev.bind(item); diff --git a/tests/utils/list/List.test.ts b/tests/utils/list/List.test.ts index 2e26428..785a6de 100644 --- a/tests/utils/list/List.test.ts +++ b/tests/utils/list/List.test.ts @@ -62,4 +62,12 @@ describe('Test List class', () => { list.clear(); expect(list.length).toBe(0); }); + + it('should find the specified element', () => { + expect(list.find('toFind')).toBeUndefined(); + + const item = list.addFirst('toFind'); + expect(list.find('toFind')).toBe(item); + expect(list.contains('notFound')).toBe(false); + }); }); From e448bd02366b32f447da817f28a1b5975f2f1068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Darida?= Date: Sun, 28 Sep 2025 11:46:26 +0200 Subject: [PATCH 14/18] refactor: rename properties head->first, tail->last in List class --- src/utils/list/List.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/utils/list/List.ts b/src/utils/list/List.ts index 81ba0fe..b378a55 100644 --- a/src/utils/list/List.ts +++ b/src/utils/list/List.ts @@ -2,8 +2,8 @@ import { Binder } from './Binder'; import { Item } from './Item'; export class List { - private readonly _head: Binder; - private readonly _tail: Binder; + private readonly _first: Binder; + private readonly _last: Binder; private _length: number; public get length(): number { @@ -11,27 +11,27 @@ export class List { } constructor() { - this._head = new Binder(); - this._tail = new Binder(); - this._head.bind(this._tail); + this._first = new Binder(); + this._last = new Binder(); + this._first.bind(this._last); this._length = 0; } public getFirst(): Item | undefined { - return 0 < this._length ? (this._head.next as Item) : undefined; + return 0 < this._length ? (this._first.next as Item) : undefined; } public getLast(): Item | undefined { - return 0 < this._length ? (this._tail.prev as Item) : undefined; + return 0 < this._length ? (this._last.prev as Item) : undefined; } public addFirst(element: T): Item { - return this.link(this._head, element); + return this.link(this._first, element); } public addLast(element: T): Item { - return this.link(this._tail.prev, element); + return this.link(this._last.prev, element); } public removeFirst(): Item | undefined { @@ -53,7 +53,7 @@ export class List { if (item == undefined) return undefined; - while (item !== this._tail) { + while (item !== this._last) { if (item.data === element) { return item; } else { From fd346882827efd293bcf09a3b8ca0b588eaec324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Darida?= Date: Sun, 28 Sep 2025 12:10:32 +0200 Subject: [PATCH 15/18] docs: add comments to the List class --- src/utils/list/List.ts | 102 ++++++++++++++++++++++++++-------- tests/utils/list/List.test.ts | 40 ++++++------- 2 files changed, 98 insertions(+), 44 deletions(-) diff --git a/src/utils/list/List.ts b/src/utils/list/List.ts index b378a55..8ef40f7 100644 --- a/src/utils/list/List.ts +++ b/src/utils/list/List.ts @@ -1,59 +1,107 @@ import { Binder } from './Binder'; import { Item } from './Item'; +/** + * Represends a generic doubly-linked list. + */ export class List { - private readonly _first: Binder; - private readonly _last: Binder; + private readonly _head: Binder; + private readonly _tail: Binder; private _length: number; + /** + * Gets the first element in the list, + * or `undefined` if the list is empty. + */ + public get first(): Item | undefined { + return 0 < this._length ? (this._head.next as Item) : undefined; + } + + /** + * Gets the last element in the list, + * or `undefined` if the list is empty. + */ + public get last(): Item | undefined { + return 0 < this._length ? (this._tail.prev as Item) : undefined; + } + + /** + * Gets the number of elements in the list. + */ public get length(): number { return this._length; } + /** + * Creates a new empty List instance. + */ constructor() { - this._first = new Binder(); - this._last = new Binder(); - this._first.bind(this._last); + this._head = new Binder(); + this._tail = new Binder(); + this._head.bind(this._tail); this._length = 0; } - public getFirst(): Item | undefined { - return 0 < this._length ? (this._first.next as Item) : undefined; - } - - public getLast(): Item | undefined { - return 0 < this._length ? (this._last.prev as Item) : undefined; - } - - public addFirst(element: T): Item { - return this.link(this._first, element); + /** + * Inserts a new element at the beginning of the list. + * + * @param element The element to insert. + * @returns The created Item wrapping the element. + */ + public unshift(element: T): Item { + return this.link(this._head, element); } - public addLast(element: T): Item { - return this.link(this._last.prev, element); + /** + * Inserts a new element to the end of the list. + * + * @param element The element to insert. + * @returns The created Item wrapping the element. + */ + public push(element: T): Item { + return this.link(this._tail.prev, element); } - public removeFirst(): Item | undefined { - return this.unlink(this.getFirst()); + /** + * Removes and returns the first element of the list. + * + * @returns The removed Item, or `undefined` if the list is empty. + */ + public shift(): Item | undefined { + return this.unlink(this.first); } - public removeLast(): Item | undefined { - return this.unlink(this.getLast()); + /** + * Removes and returns the last element of the list. + * + * @returns The removed Item, or `undefined` if the list is empty. + */ + public pop(): Item | undefined { + return this.unlink(this.last); } + /** + * Removes all elements from the list. + */ public clear(): void { while (0 < this.length) { - this.removeFirst(); + this.shift(); } } + /** + * Finds the first item whose stored value mathes the given element. + * + * @param element The value to search for. + * @returns The matching Item, or `undefined` if not found. + */ public find(element: T): Item | undefined { - let item = this.getFirst(); + let item = this.first; if (item == undefined) return undefined; - while (item !== this._last) { + while (item !== this._tail) { if (item.data === element) { return item; } else { @@ -64,6 +112,12 @@ export class List { return undefined; } + /** + * Determines whether the list contains the specified element. + * + * @param element The value to check for. + * @returns True if the element is found, otherwise false. + */ public contains(element: T): boolean { return this.find(element) != undefined; } diff --git a/tests/utils/list/List.test.ts b/tests/utils/list/List.test.ts index 785a6de..6855e84 100644 --- a/tests/utils/list/List.test.ts +++ b/tests/utils/list/List.test.ts @@ -10,51 +10,51 @@ describe('Test List class', () => { it('should be truthy', () => { expect(list).toBeTruthy(); expect(list.length).toBe(0); - expect(list.getFirst()).toBeUndefined(); - expect(list.getLast()).toBeUndefined(); + expect(list.first).toBeUndefined(); + expect(list.last).toBeUndefined(); }); it('should add a new element at the beginning of the list', () => { - list.addFirst('firstItem1'); + list.unshift('firstItem1'); expect(list.length).toBe(1); - expect(list.getFirst()?.data).toBe('firstItem1'); + expect(list.first?.data).toBe('firstItem1'); - list.addFirst('firstItem2'); + list.unshift('firstItem2'); expect(list.length).toBe(2); - expect(list.getFirst()?.data).toBe('firstItem2'); + expect(list.first?.data).toBe('firstItem2'); }); it('should add a new element to the end of the list', () => { - list.addLast('lastItem1'); + list.push('lastItem1'); expect(list.length).toBe(1); - expect(list.getLast()?.data).toBe('lastItem1'); + expect(list.last?.data).toBe('lastItem1'); - list.addLast('lastItem2'); + list.push('lastItem2'); expect(list.length).toBe(2); - expect(list.getLast()?.data).toBe('lastItem2'); + expect(list.last?.data).toBe('lastItem2'); }); it('should remove the first element of the list', () => { - expect(list.removeFirst()).toBeUndefined(); + expect(list.shift()).toBeUndefined(); - list.addFirst('firstItem1'); - const expected = list.addFirst('firstItem2'); - expect(list.removeFirst()).toBe(expected); + list.unshift('firstItem1'); + const expected = list.unshift('firstItem2'); + expect(list.shift()).toBe(expected); expect(list.length).toBe(1); }); it('should remove the last element of the list', () => { - expect(list.removeLast()).toBeUndefined(); + expect(list.pop()).toBeUndefined(); - list.addLast('firstItem1'); - const expected = list.addLast('firstItem2'); - expect(list.removeLast()).toBe(expected); + list.push('firstItem1'); + const expected = list.push('firstItem2'); + expect(list.pop()).toBe(expected); expect(list.length).toBe(1); }); it('should clear the list', () => { for (let i = 0; i < 10; ++i) { - list.addFirst(`item${i}`); + list.unshift(`item${i}`); } expect(list.length).toBe(10); @@ -66,7 +66,7 @@ describe('Test List class', () => { it('should find the specified element', () => { expect(list.find('toFind')).toBeUndefined(); - const item = list.addFirst('toFind'); + const item = list.unshift('toFind'); expect(list.find('toFind')).toBe(item); expect(list.contains('notFound')).toBe(false); }); From fed2f2aad994de353b3351a422fd1f3438cadddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Darida?= Date: Sun, 28 Sep 2025 13:17:18 +0200 Subject: [PATCH 16/18] feat: implement iterator for List class, and simplify List.find method with it --- src/utils/list/List.ts | 24 ++++++++++++++---------- tests/utils/list/List.test.ts | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/utils/list/List.ts b/src/utils/list/List.ts index 8ef40f7..057b6fd 100644 --- a/src/utils/list/List.ts +++ b/src/utils/list/List.ts @@ -97,16 +97,8 @@ export class List { * @returns The matching Item, or `undefined` if not found. */ public find(element: T): Item | undefined { - let item = this.first; - - if (item == undefined) return undefined; - - while (item !== this._tail) { - if (item.data === element) { - return item; - } else { - item = item.next as Item; - } + for (const item of this) { + if (item.data && item.data === element) return item; } return undefined; @@ -122,6 +114,18 @@ export class List { return this.find(element) != undefined; } + /** + * Returns an iterator over the list items. + */ + public *[Symbol.iterator](): IterableIterator> { + let current = this.first; + + while (current && current !== this._tail) { + yield current; + current = current.next as Item; + } + } + private link(prev: Binder, element: T): Item { const item = Item.parse(element); prev.bind(item); diff --git a/tests/utils/list/List.test.ts b/tests/utils/list/List.test.ts index 6855e84..bf50eeb 100644 --- a/tests/utils/list/List.test.ts +++ b/tests/utils/list/List.test.ts @@ -1,4 +1,5 @@ import { List } from '../../../src/utils/list/List'; +import { Item } from '../../../src/utils/list/Item'; describe('Test List class', () => { let list: List; @@ -12,6 +13,7 @@ describe('Test List class', () => { expect(list.length).toBe(0); expect(list.first).toBeUndefined(); expect(list.last).toBeUndefined(); + expect([...list].length).toBe(0); }); it('should add a new element at the beginning of the list', () => { @@ -22,6 +24,8 @@ describe('Test List class', () => { list.unshift('firstItem2'); expect(list.length).toBe(2); expect(list.first?.data).toBe('firstItem2'); + + expect([...list].length).toBe(2); }); it('should add a new element to the end of the list', () => { @@ -32,6 +36,8 @@ describe('Test List class', () => { list.push('lastItem2'); expect(list.length).toBe(2); expect(list.last?.data).toBe('lastItem2'); + + expect([...list].length).toBe(2); }); it('should remove the first element of the list', () => { @@ -70,4 +76,16 @@ describe('Test List class', () => { expect(list.find('toFind')).toBe(item); expect(list.contains('notFound')).toBe(false); }); + + it('sould iterate over all items in order', () => { + const elements = ['one', 'two', 'three']; + + elements.forEach(element => list.push(element)); + + const items = [...list]; + + items.forEach(({ data }, i) => { + expect(data).toBe(elements[i]); + }); + }); }); From 275f1c02ba41e74da365f505b9fe389e65f6e9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Darida?= Date: Sun, 28 Sep 2025 13:55:21 +0200 Subject: [PATCH 17/18] feat: implement List.forEach method --- src/utils/list/List.ts | 20 ++++++++++++++++++++ tests/utils/list/List.test.ts | 15 +++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/utils/list/List.ts b/src/utils/list/List.ts index 057b6fd..cbc176a 100644 --- a/src/utils/list/List.ts +++ b/src/utils/list/List.ts @@ -114,6 +114,26 @@ export class List { return this.find(element) != undefined; } + /** + * Performs the specified action for each element in a list. + * + * @param callbackfn A function that accepts up to three arguments. forEach calls + * the callbackfn function one time for each element in the list. + * @param thisArg An object to which the this keyword can refer in the + * callbackfn function. If thisArg is omitted, undefined is used as the + * this value. + */ + public forEach( + callbackfn: (value: T, i: number, list: List) => void, + thisArg?: any + ): void { + let i = 0; + + for (const item of this) { + callbackfn.call(thisArg, item.data, i++, this); + } + } + /** * Returns an iterator over the list items. */ diff --git a/tests/utils/list/List.test.ts b/tests/utils/list/List.test.ts index bf50eeb..caf1c1f 100644 --- a/tests/utils/list/List.test.ts +++ b/tests/utils/list/List.test.ts @@ -77,6 +77,21 @@ describe('Test List class', () => { expect(list.contains('notFound')).toBe(false); }); + it('should iterate over all items in order with forEach', () => { + list.push('1'); + list.push('2'); + list.push('4'); + list.push('8'); + + let counter = ''; + + list.forEach((item, i) => { + counter += item + i; + }); + + expect(counter).toBe('10214283'); + }); + it('sould iterate over all items in order', () => { const elements = ['one', 'two', 'three']; From b07ab92384088889215145ff4482fd6d17d47488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Darida?= Date: Sun, 28 Sep 2025 14:08:40 +0200 Subject: [PATCH 18/18] feat: implement List.map method --- src/utils/list/List.ts | 25 +++++++++++++++++++++++++ tests/utils/list/List.test.ts | 17 +++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/utils/list/List.ts b/src/utils/list/List.ts index cbc176a..0e17523 100644 --- a/src/utils/list/List.ts +++ b/src/utils/list/List.ts @@ -134,6 +134,31 @@ export class List { } } + /** + * Calls a defined callback function on each element of a list, and returns + * a list that contains the results. + * + * @param callbackfn A function that accepts up to three arguments. The map + * method calls the callbackfn function one time for each element in the list. + * @param thisArg An object to which the this keyword can refer in the + * callbackfn function. If thisArg is omitted, undefined is used as the this + * value. + */ + public map( + callbackfn: (value: T, index: number, list: List) => U, + thisArg?: any + ): List { + const list = new List(); + + let i = 0; + + for (const item of this) { + list.push(callbackfn.call(thisArg, item.data, i++, this)); + } + + return list; + } + /** * Returns an iterator over the list items. */ diff --git a/tests/utils/list/List.test.ts b/tests/utils/list/List.test.ts index caf1c1f..074283f 100644 --- a/tests/utils/list/List.test.ts +++ b/tests/utils/list/List.test.ts @@ -92,6 +92,23 @@ describe('Test List class', () => { expect(counter).toBe('10214283'); }); + it('should iterate over all items in order with map', () => { + list.push('1'); + list.push('2'); + list.push('4'); + list.push('8'); + + const newList = list.map((item, i) => parseInt(item) + i); + + let counter = ''; + + newList.forEach(item => { + counter += item; + }); + + expect(counter).toBe('13611'); + }); + it('sould iterate over all items in order', () => { const elements = ['one', 'two', 'three'];