diff --git a/src/05-queue-deque/__test__/deque-js.test.ts b/src/05-queue-deque/__test__/deque-js.test.ts new file mode 100644 index 00000000..60e76b0c --- /dev/null +++ b/src/05-queue-deque/__test__/deque-js.test.ts @@ -0,0 +1,116 @@ +import { describe, expect, test, beforeEach } from '@jest/globals'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const Deque = require('../deque.js') as new () => { + addFront(item: any): void; + addRear(item: any): void; + removeFront(): any; + removeRear(): any; + peekFront(): any; + peekRear(): any; + isEmpty(): boolean; + size: number; + clear(): void; + toString(): string; +}; + +describe('Deque (deque.js)', () => { + let deque: InstanceType; + + beforeEach(() => { + deque = new Deque(); + }); + + test('should add an element to the front of the deque', () => { + deque.addFront(1); + deque.addFront(2); + expect(deque.toString()).toBe('2, 1'); + }); + + test('should add an element to the rear of the deque', () => { + deque.addRear(1); + deque.addRear(2); + expect(deque.toString()).toBe('1, 2'); + }); + + test('should remove an element from the front of the deque', () => { + deque.addFront(1); + deque.addFront(2); + expect(deque.removeFront()).toBe(2); + expect(deque.toString()).toBe('1'); + }); + + test('should return undefined when removing from the front of an empty deque', () => { + expect(deque.removeFront()).toBeUndefined(); + }); + + test('should remove an element from the rear of the deque', () => { + deque.addRear(1); + deque.addRear(2); + expect(deque.removeRear()).toBe(2); + expect(deque.toString()).toBe('1'); + }); + + test('should return undefined when removing from the rear of an empty deque', () => { + expect(deque.removeRear()).toBeUndefined(); + }); + + test('should return the front element of the deque', () => { + deque.addFront(1); + deque.addFront(2); + expect(deque.peekFront()).toBe(2); + }); + + test('should return undefined when peeking the front of an empty deque', () => { + expect(deque.peekFront()).toBeUndefined(); + }); + + test('should return the rear element of the deque', () => { + deque.addRear(1); + deque.addRear(2); + expect(deque.peekRear()).toBe(2); + }); + + test('should return undefined when peeking the rear of an empty deque', () => { + expect(deque.peekRear()).toBeUndefined(); + }); + + test('should return true if the deque is empty', () => { + expect(deque.isEmpty()).toBe(true); + }); + + test('should return false if the deque is not empty', () => { + deque.addFront(1); + expect(deque.isEmpty()).toBe(false); + }); + + test('should return the size of the deque', () => { + deque.addFront(1); + deque.addFront(2); + expect(deque.size).toBe(2); + }); + + test('should clear the deque', () => { + deque.addFront(1); + deque.addFront(2); + deque.clear(); + expect(deque.isEmpty()).toBe(true); + expect(deque.size).toBe(0); + }); + + test('should convert an empty deque to string', () => { + expect(deque.toString()).toBe('Empty Deque'); + }); + + test('should convert the deque to a string with primitive values', () => { + deque.addRear(1); + deque.addRear('two'); + expect(deque.toString()).toBe('1, two'); + }); + + test('should convert the deque to a string with objects', () => { + deque.addFront({ key: 'a', value: 1 }); + deque.addFront({ key: 'b', value: 2 }); + expect(deque.toString()).toBe('{"key":"b","value":2}, {"key":"a","value":1}'); + }); +}); diff --git a/src/06-linked-list/__test__/linked-list_.test.ts b/src/06-linked-list/__test__/linked-list_.test.ts new file mode 100644 index 00000000..23070575 --- /dev/null +++ b/src/06-linked-list/__test__/linked-list_.test.ts @@ -0,0 +1,185 @@ +import { describe, expect, test, beforeEach } from '@jest/globals'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const LinkedList = require('../linked-list_') as new () => { + append(data: any): void; + prepend(data: any): void; + insert(data: any, position: number): boolean; + removeAt(position: number): any; + remove(data: any, compareFunction?: (a: any, b: any) => boolean): any; + indexOf(data: any, compareFunction?: (a: any, b: any) => boolean): number; + isEmpty(): boolean; + clear(): void; + size: number; + forEach(callback: (data: any, index: number) => void): void; + toString(): string; + reverse(): void; +}; + +describe('LinkedList (linked-list_.js)', () => { + let linkedList: InstanceType; + + beforeEach(() => { + linkedList = new LinkedList(); + }); + + test('should create an empty linked list', () => { + expect(linkedList.toString()).toBe(''); + expect(linkedList.isEmpty()).toBe(true); + expect(linkedList.size).toBe(0); + }); + + test('should append nodes to the linked list', () => { + linkedList.append(1); + linkedList.append(2); + linkedList.append(3); + expect(linkedList.toString()).toBe('1, 2, 3'); + expect(linkedList.size).toBe(3); + }); + + test('should prepend nodes to the linked list', () => { + linkedList.prepend(2); + expect(linkedList.toString()).toBe('2'); + linkedList.append(1); + linkedList.prepend(3); + expect(linkedList.toString()).toBe('3, 2, 1'); + }); + + test('should insert a node at position 0', () => { + linkedList.append(1); + expect(linkedList.insert(2, 0)).toBe(true); + expect(linkedList.toString()).toBe('2, 1'); + }); + + test('should insert a node at a given middle position', () => { + linkedList.append(1); + linkedList.append(3); + expect(linkedList.insert(2, 1)).toBe(true); + expect(linkedList.toString()).toBe('1, 2, 3'); + }); + + test('should return false when inserting at an invalid position', () => { + linkedList.append(1); + linkedList.append(3); + expect(linkedList.insert(2, 5)).toBe(false); + expect(linkedList.insert(2, -1)).toBe(false); + expect(linkedList.toString()).toBe('1, 3'); + }); + + test('should remove a node from the head', () => { + linkedList.append(1); + linkedList.append(2); + expect(linkedList.removeAt(0)).toBe(1); + expect(linkedList.toString()).toBe('2'); + }); + + test('should remove a node from the middle', () => { + linkedList.append(1); + linkedList.append(2); + linkedList.append(3); + expect(linkedList.removeAt(1)).toBe(2); + expect(linkedList.toString()).toBe('1, 3'); + }); + + test('should remove a node from the end', () => { + linkedList.append(1); + linkedList.append(2); + linkedList.append(3); + expect(linkedList.removeAt(2)).toBe(3); + expect(linkedList.toString()).toBe('1, 2'); + }); + + test('should throw when removing from an empty list', () => { + expect(() => linkedList.removeAt(0)).toThrow('Cannot remove from an empty list.'); + }); + + test('should throw when removing at an invalid position', () => { + linkedList.append(1); + expect(() => linkedList.removeAt(5)).toThrow('Invalid position'); + expect(() => linkedList.removeAt(-1)).toThrow('Invalid position'); + }); + + test('should remove an element by value', () => { + linkedList.append(1); + linkedList.append(2); + linkedList.append(3); + expect(linkedList.remove(2)).toBe(2); + expect(linkedList.toString()).toBe('1, 3'); + }); + + test('should return null when removing a value that is not found', () => { + linkedList.append(1); + linkedList.append(2); + expect(linkedList.remove(3)).toBeNull(); + }); + + test('should remove using a custom compare function', () => { + linkedList.append({ id: 1 }); + linkedList.append({ id: 2 }); + const removed = linkedList.remove({ id: 2 }, (a: any, b: any) => a.id === b.id); + expect(removed).toEqual({ id: 2 }); + expect(linkedList.size).toBe(1); + }); + + test('should find the index of an element', () => { + linkedList.append(1); + linkedList.append(2); + linkedList.append(3); + expect(linkedList.indexOf(1)).toBe(0); + expect(linkedList.indexOf(2)).toBe(1); + expect(linkedList.indexOf(3)).toBe(2); + }); + + test('should return -1 when the element is not found', () => { + linkedList.append(1); + expect(linkedList.indexOf(2)).toBe(-1); + }); + + test('should report whether the list is empty', () => { + expect(linkedList.isEmpty()).toBe(true); + linkedList.append(1); + expect(linkedList.isEmpty()).toBe(false); + }); + + test('should clear the list', () => { + linkedList.append(1); + linkedList.append(2); + linkedList.clear(); + expect(linkedList.isEmpty()).toBe(true); + expect(linkedList.size).toBe(0); + expect(linkedList.toString()).toBe(''); + }); + + test('should iterate all elements with forEach', () => { + linkedList.append(1); + linkedList.append(2); + linkedList.append(3); + const seen: Array<[any, number]> = []; + linkedList.forEach((data: any, index: number) => seen.push([data, index])); + expect(seen).toEqual([[1, 0], [2, 1], [3, 2]]); + }); + + test('should convert objects to string using JSON.stringify', () => { + linkedList.append({ key: 'key1', value: 1 }); + expect(linkedList.toString()).toBe('{"key":"key1","value":1}'); + }); + + test('should reverse the linked list', () => { + linkedList.append(1); + linkedList.append(2); + linkedList.append(3); + linkedList.reverse(); + expect(linkedList.toString()).toBe('3, 2, 1'); + }); + + test('should reverse a single-element list without error', () => { + linkedList.append(1); + linkedList.reverse(); + expect(linkedList.toString()).toBe('1'); + }); + + test('should reverse an empty list without error', () => { + linkedList.reverse(); + expect(linkedList.toString()).toBe(''); + }); +}); diff --git a/src/08-dictionary-hash/__test__/hash-table-collision.test.ts b/src/08-dictionary-hash/__test__/hash-table-collision.test.ts index 74b452d1..08eb4eab 100644 --- a/src/08-dictionary-hash/__test__/hash-table-collision.test.ts +++ b/src/08-dictionary-hash/__test__/hash-table-collision.test.ts @@ -6,6 +6,7 @@ const HashTableSeparateChaining = require('../hash-table-separate-chaining') as get(key: string): any; remove(key: string): boolean; hash(key: string): number; + toString(): string; }; // eslint-disable-next-line @typescript-eslint/no-require-imports @@ -27,15 +28,46 @@ describe('HashTableSeparateChaining', () => { expect(ht.put('name', 'Alice')).toBe(true); }); - test('remove returns true after put', () => { + test('put returns false when key or value is null', () => { + expect(ht.put(null as any, 'value')).toBe(false); + expect(ht.put('key', null as any)).toBe(false); + }); + + test('get returns the stored value for a key', () => { + ht.put('name', 'Alice'); + expect(ht.get('name')).toBe('Alice'); + }); + + test('get returns undefined for a missing key', () => { + expect(ht.get('missing')).toBeUndefined(); + }); + + test('get returns undefined for a key in an empty bucket', () => { + ht.put('name', 'Alice'); + expect(ht.get('doesNotExist')).toBeUndefined(); + }); + + test('put overwrites an existing key by appending, and get returns the latest value', () => { + ht.put('name', 'Alice'); + ht.put('name', 'Bob'); + expect(ht.get('name')).toBe('Bob'); + }); + + test('remove returns true after put and get returns undefined afterwards', () => { ht.put('name', 'Alice'); expect(ht.remove('name')).toBe(true); + expect(ht.get('name')).toBeUndefined(); }); test('remove returns false when key not found', () => { expect(ht.remove('missing')).toBe(false); }); + test('remove returns false when bucket is empty', () => { + ht.put('name', 'Alice'); + expect(ht.remove('doesNotExist')).toBe(false); + }); + test('hash returns a number in valid range', () => { const h = ht.hash('name'); expect(typeof h).toBe('number'); @@ -43,17 +75,40 @@ describe('HashTableSeparateChaining', () => { expect(h).toBeLessThan(37); }); + test('hash handles non-string keys', () => { + const h = ht.hash({ id: 1 } as any); + expect(typeof h).toBe('number'); + expect(h).toBeGreaterThanOrEqual(0); + }); + + test('hash handles primitive non-string, non-object keys', () => { + const h = ht.hash(42 as any); + expect(typeof h).toBe('number'); + expect(h).toBeGreaterThanOrEqual(0); + }); + test('put multiple keys with same hash slot chains correctly', () => { - // Both put calls should succeed + // 'key1' and 'key2' share the same lose-lose hash slot. expect(ht.put('key1', 'val1')).toBe(true); expect(ht.put('key2', 'val2')).toBe(true); + expect(ht.get('key1')).toBe('val1'); + expect(ht.get('key2')).toBe('val2'); + }); + + test('remove one key from a chain keeps the other reachable', () => { + ht.put('key1', 'val1'); + ht.put('key2', 'val2'); + expect(ht.remove('key1')).toBe(true); + expect(ht.get('key1')).toBeUndefined(); + expect(ht.get('key2')).toBe('val2'); }); - // Note: get() has a known bug — return inside forEach doesn't propagate, - // so get() always returns undefined regardless of what was stored. - test('get always returns undefined due to known bug in forEach return', () => { + test('toString produces output containing stored values', () => { ht.put('name', 'Alice'); - expect(ht.get('name')).toBeUndefined(); + ht.put('city', 'London'); + const result = ht.toString(); + expect(result).toContain('Alice'); + expect(result).toContain('London'); }); }); @@ -64,26 +119,87 @@ describe('HashTableLinearProbing', () => { ht = new HashTableLinearProbing(); }); - // Note: #loseLoseHashCode has a bug — the reduce callback returns the function - // reference instead of calling it, causing all hashes to be NaN. - // put() stores at table[NaN], get() can retrieve from table[NaN] for exact key. - test('put returns true', () => { expect(ht.put('singleKey', 'value')).toBe(true); }); + test('put returns false when key or value is null', () => { + expect(ht.put(null as any, 'value')).toBe(false); + expect(ht.put('key', null as any)).toBe(false); + }); + test('get returns value after put (same key)', () => { ht.put('singleKey', 'myValue'); expect(ht.get('singleKey')).toBe('myValue'); }); + test('get returns undefined for a missing key', () => { + expect(ht.get('notFound')).toBeUndefined(); + }); + + test('put overwrites the value for an existing key', () => { + ht.put('name', 'Alice'); + ht.put('name', 'Bob'); + expect(ht.get('name')).toBe('Bob'); + }); + test('remove returns false when key not found (empty table)', () => { expect(ht.remove('notFound')).toBe(false); }); - test('hash returns NaN due to known bug', () => { - // The reduce callback returns function reference instead of calling it - const h = ht.hash('any'); - expect(isNaN(h as unknown as number)).toBe(true); + test('remove returns true after put and get returns undefined afterwards', () => { + ht.put('name', 'Alice'); + expect(ht.remove('name')).toBe(true); + expect(ht.get('name')).toBeUndefined(); + }); + + test('put/get/remove handle multiple colliding keys via linear probing', () => { + ht.put('john', 1); + ht.put('jane', 2); + ht.put('jack', 3); + + expect(ht.get('john')).toBe(1); + expect(ht.get('jane')).toBe(2); + expect(ht.get('jack')).toBe(3); + + expect(ht.remove('jane')).toBe(true); + expect(ht.get('jane')).toBeUndefined(); + // Removing the middle probed slot should not break access to the others. + expect(ht.get('john')).toBe(1); + expect(ht.get('jack')).toBe(3); + }); + + test('hash returns a number for string and non-string keys', () => { + expect(typeof ht.hash('any')).toBe('number'); + expect(typeof ht.hash(42 as any)).toBe('number'); + }); + + test('hash handles object keys by stringifying them', () => { + expect(typeof ht.hash({ id: 1 } as any)).toBe('number'); + }); + + test('put/get handle a true hash collision via linear probing', () => { + // 'ab' and 'ba' share the same lose-lose hash code. + ht.put('ab', 1); + ht.put('ba', 2); + expect(ht.get('ab')).toBe(1); + expect(ht.get('ba')).toBe(2); + }); + + test('remove repositions a wrapped-around colliding key so it stays reachable', () => { + ht.put('ab', 1); + ht.put('ba', 2); // probes and wraps around to an earlier slot + expect(ht.remove('ab')).toBe(true); + expect(ht.get('ab')).toBeUndefined(); + // 'ba' must still be reachable after the side-effect repositioning. + expect(ht.get('ba')).toBe(2); + }); + + test('remove probes past a non-matching colliding slot to find the key', () => { + ht.put('ab', 1); // occupies the natural hash slot + ht.put('ba', 2); // collides and is probed elsewhere + expect(ht.remove('ba')).toBe(true); + expect(ht.get('ba')).toBeUndefined(); + expect(ht.get('ab')).toBe(1); }); }); diff --git a/src/08-dictionary-hash/hash-table-linear-probing.js b/src/08-dictionary-hash/hash-table-linear-probing.js index 7d562335..9e65d538 100644 --- a/src/08-dictionary-hash/hash-table-linear-probing.js +++ b/src/08-dictionary-hash/hash-table-linear-probing.js @@ -8,7 +8,7 @@ class HashTableLinearProbing { key = this.#elementToString(key); } const calcASCIIValue = (acc, char) => acc + char.charCodeAt(0); - const hash = key.split('').reduce((acc, char) => calcASCIIValue, 0); + const hash = key.split('').reduce(calcASCIIValue, 0); return hash % 37; // mod to reduce the hash code } @@ -17,7 +17,7 @@ class HashTableLinearProbing { key = this.#elementToString(key); } const calcASCIIValue = (acc, char) => (acc * 33) + char.charCodeAt(0); - const hash = key.split('').reduce((acc, char) => calcASCIIValue, 5381); + const hash = key.split('').reduce(calcASCIIValue, 5381); return hash % 1013; } @@ -84,7 +84,7 @@ class HashTableLinearProbing { #verifyRemoveSideEffect(key, removedPosition) { const size = this.#table.length; - let index = removedPosition + 1; + let index = (removedPosition + 1) % size; while (this.#table[index] != null) { const currentKey = this.#table[index].key; const currentHash = this.hash(currentKey); diff --git a/src/08-dictionary-hash/hash-table-separate-chaining.js b/src/08-dictionary-hash/hash-table-separate-chaining.js index 7ba5cbdd..41f45e8e 100644 --- a/src/08-dictionary-hash/hash-table-separate-chaining.js +++ b/src/08-dictionary-hash/hash-table-separate-chaining.js @@ -35,14 +35,15 @@ class HashTableSeparateChaining { get(key) { const index = this.hash(key); const linkedList = this.#table[index]; + let value; if (linkedList != null) { linkedList.forEach((element) => { if (element.key === key) { - return element.value; + value = element.value; } }); } - return undefined; // key not found + return value; // undefined if key not found } remove(key) { @@ -74,7 +75,7 @@ class HashTableSeparateChaining { const keys = Object.keys(this.#table); let objString = `{${keys[0]} => ${this.#table[keys[0]].toString()}}`; for (let i = 1; i < keys.length; i++) { - const value = this.#elementToString(this.#table[keys[i]]).toString(); + const value = this.#table[keys[i]].toString(); objString = `${objString}\n{${keys[i]} => ${value}}`; } return objString; diff --git a/src/10-tree/__test__/comparator.test.ts b/src/10-tree/__test__/comparator.test.ts new file mode 100644 index 00000000..b6068191 --- /dev/null +++ b/src/10-tree/__test__/comparator.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, test } from '@jest/globals'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const Comparator = require('../comparator.js') as new ( + compareFn?: (a: any, b: any) => number +) => { + equal(a: any, b: any): boolean; + lessThan(a: any, b: any): boolean; + greaterThan(a: any, b: any): boolean; +}; + +describe('Comparator (comparator.js)', () => { + test('uses the default compare function when none is provided', () => { + const comparator = new Comparator(); + expect(comparator.equal(1, 1)).toBe(true); + expect(comparator.lessThan(1, 2)).toBe(true); + expect(comparator.greaterThan(2, 1)).toBe(true); + }); + + test('equal returns false for different values', () => { + const comparator = new Comparator(); + expect(comparator.equal(1, 2)).toBe(false); + }); + + test('lessThan returns false when the first value is not smaller', () => { + const comparator = new Comparator(); + expect(comparator.lessThan(2, 1)).toBe(false); + expect(comparator.lessThan(1, 1)).toBe(false); + }); + + test('greaterThan returns false when the first value is not bigger', () => { + const comparator = new Comparator(); + expect(comparator.greaterThan(1, 2)).toBe(false); + expect(comparator.greaterThan(1, 1)).toBe(false); + }); + + test('supports a custom compare function', () => { + const comparator = new Comparator((a: number, b: number) => b - a); // reversed order + expect(comparator.lessThan(2, 1)).toBe(true); + expect(comparator.greaterThan(1, 2)).toBe(true); + expect(comparator.equal(1, 1)).toBe(true); + }); +});