From 65949b7d194899703273f94f1fa2d8257c634cb6 Mon Sep 17 00:00:00 2001 From: Revone Date: Thu, 30 Nov 2023 09:59:01 +0800 Subject: [PATCH] feat: Reimplement a more efficient HashMap and correctly rename the original HashMap as LinkedHashMap. --- .../binary-tree/binary-tree.ts | 2 +- src/data-structures/hash/hash-map.ts | 440 ++++++++++++++++-- src/types/data-structures/hash/hash-map.ts | 2 + .../data-structures/hash/hash-map.test.ts | 336 ++++++++++++- 4 files changed, 716 insertions(+), 64 deletions(-) diff --git a/src/data-structures/binary-tree/binary-tree.ts b/src/data-structures/binary-tree/binary-tree.ts index f519ec6..7a4d41d 100644 --- a/src/data-structures/binary-tree/binary-tree.ts +++ b/src/data-structures/binary-tree/binary-tree.ts @@ -1728,7 +1728,7 @@ export class BinaryTree = BinaryTreeNode * Time complexity: O(n) * Space complexity: O(n) */ - + /** * Time complexity: O(n) * Space complexity: O(n) diff --git a/src/data-structures/hash/hash-map.ts b/src/data-structures/hash/hash-map.ts index 0c189b5..a2a3498 100644 --- a/src/data-structures/hash/hash-map.ts +++ b/src/data-structures/hash/hash-map.ts @@ -7,9 +7,329 @@ */ import { isWeakKey, rangeCheck } from '../../utils'; -import { HashMapLinkedNode, HashMapOptions } from '../../types'; +import { HashMapLinkedNode, HashMapOptions, HashMapStoreItem } from '../../types'; export class HashMap { + protected _store: { [key: string]: HashMapStoreItem } = {}; + protected _objMap: Map = new Map(); + + /** + * The constructor function initializes a new instance of a class with optional elements and options. + * @param elements - The `elements` parameter is an iterable containing key-value pairs `[K, V]`. It + * is optional and defaults to an empty array `[]`. This parameter is used to initialize the map with + * key-value pairs. + * @param [options] - The `options` parameter is an optional object that can contain additional + * configuration options for the constructor. In this case, it has one property: + */ + constructor(elements: Iterable<[K, V]> = [], options?: { + hashFn: (key: K) => string + }) { + if (options) { + const { hashFn } = options; + if (hashFn) { + this._hashFn = hashFn; + + } + } + if (elements) { + this.setMany(elements); + } + } + + protected _size = 0; + + get size(): number { + return this._size; + } + + isEmpty(): boolean { + return this.size === 0; + } + + clear() { + this._store = {}; + this._objMap.clear(); + this._size = 0; + } + + /** + * The `set` function adds a key-value pair to a map-like data structure, incrementing the size if + * the key is not already present. + * @param {K} key - The key parameter is the key used to identify the value in the data structure. It + * can be of any type, but if it is an object, it will be stored in a Map, otherwise it will be + * stored in a regular JavaScript object. + * @param {V} value - The value parameter represents the value that you want to associate with the + * key in the data structure. + */ + set(key: K, value: V) { + + if (this._isObjKey(key)) { + if (!this._objMap.has(key)) { + this._size++; + } + this._objMap.set(key, value); + + } else { + const strKey = this._getNoObjKey(key); + if (this._store[strKey] === undefined) { + this._size++; + } + this._store[strKey] = { key, value }; + } + } + + /** + * The function "setMany" sets multiple key-value pairs in a map. + * @param elements - The `elements` parameter is an iterable containing key-value pairs. Each + * key-value pair is represented as an array with two elements: the key and the value. + */ + setMany(elements: Iterable<[K, V]>) { + for (const [key, value] of elements) this.set(key, value); + } + + /** + * The `get` function retrieves a value from a map based on a given key, either from an object map or + * a string map. + * @param {K} key - The `key` parameter is the key used to retrieve a value from the map. It can be + * of any type, but it should be compatible with the key type used when the map was created. + * @returns The method `get(key: K)` returns a value of type `V` if the key exists in the `_objMap` + * or `_store`, otherwise it returns `undefined`. + */ + get(key: K): V | undefined { + if (this._isObjKey(key)) { + return this._objMap.get(key); + } else { + const strKey = this._getNoObjKey(key); + return this._store[strKey]?.value; + } + } + + /** + * The `has` function checks if a given key exists in the `_objMap` or `_store` based on whether it + * is an object key or not. + * @param {K} key - The parameter "key" is of type K, which means it can be any type. + * @returns The `has` method is returning a boolean value. + */ + has(key: K): boolean { + if (this._isObjKey(key)) { + return this._objMap.has(key); + } else { + const strKey = this._getNoObjKey(key); + return strKey in this._store; + } + } + + /** + * The `delete` function removes an element from a map-like data structure based on the provided key. + * @param {K} key - The `key` parameter is the key of the element that you want to delete from the + * data structure. + * @returns The `delete` method returns a boolean value. It returns `true` if the key was + * successfully deleted from the map, and `false` if the key was not found in the map. + */ + delete(key: K): boolean { + if (this._isObjKey(key)) { + if (this._objMap.has(key)) { + this._size-- + } + + return this._objMap.delete(key); + } else { + const strKey = this._getNoObjKey(key); + if (strKey in this._store) { + delete this._store[strKey]; + this._size--; + return true; + } + return false; + } + } + + /** + * The function returns an iterator that yields key-value pairs from both an object store and an + * object map. + */ + * [Symbol.iterator](): IterableIterator<[K, V]> { + for (const node of Object.values(this._store)) { + yield [node.key, node.value] as [K, V]; + } + for (const node of this._objMap) { + yield node as [K, V]; + } + } + + /** + * The function returns an iterator that yields key-value pairs from the object. + */ + * entries(): IterableIterator<[K, V]> { + for (const item of this) { + yield item; + } + } + + /** + * The function `keys()` returns an iterator that yields all the keys of the object. + */ + * keys(): IterableIterator { + for (const [key] of this) { + yield key; + } + } + + * values(): IterableIterator { + for (const [, value] of this) { + yield value; + } + } + + /** + * The `every` function checks if every element in a HashMap satisfies a given predicate function. + * @param predicate - The predicate parameter is a function that takes four arguments: value, key, + * index, and map. It is used to test each element in the map against a condition. If the predicate + * function returns false for any element, the every() method will return false. If the predicate + * function returns true for all + * @param {any} [thisArg] - The `thisArg` parameter is an optional argument that specifies the value + * to be used as `this` when executing the `predicate` function. If `thisArg` is provided, it will be + * passed as the `this` value to the `predicate` function. If `thisArg` is + * @returns The method is returning a boolean value. It returns true if the predicate function + * returns true for every element in the map, and false otherwise. + */ + every(predicate: (value: V, key: K, index: number, map: HashMap) => boolean, thisArg?: any): boolean { + let index = 0; + for (const [key, value] of this) { + if (!predicate.call(thisArg, value, key, index++, this)) { + return false; + } + } + return true; + } + + /** + * The "some" function checks if at least one element in a HashMap satisfies a given predicate. + * @param predicate - The `predicate` parameter is a function that takes four arguments: `value`, + * `key`, `index`, and `map`. It is used to determine whether a specific condition is met for a given + * key-value pair in the `HashMap`. + * @param {any} [thisArg] - The `thisArg` parameter is an optional argument that specifies the value + * to be used as `this` when executing the `predicate` function. If `thisArg` is provided, it will be + * passed as the `this` value to the `predicate` function. If `thisArg` is + * @returns a boolean value. It returns true if the predicate function returns true for any element + * in the map, and false otherwise. + */ + some(predicate: (value: V, key: K, index: number, map: HashMap) => boolean, thisArg?: any): boolean { + let index = 0; + for (const [key, value] of this) { + if (predicate.call(thisArg, value, key, index++, this)) { + return true; + } + } + return false; + } + + /** + * The `forEach` function iterates over the elements of a HashMap and applies a callback function to + * each element. + * @param callbackfn - A function that will be called for each key-value pair in the HashMap. It + * takes four parameters: + * @param {any} [thisArg] - The `thisArg` parameter is an optional argument that specifies the value + * to be used as `this` when executing the `callbackfn` function. If `thisArg` is provided, it will + * be passed as the `this` value inside the `callbackfn` function. If `thisArg + */ + forEach(callbackfn: (value: V, key: K, index: number, map: HashMap) => void, thisArg?: any): void { + let index = 0; + for (const [key, value] of this) { + callbackfn.call(thisArg, value, key, index++, this); + } + } + + /** + * The `map` function in TypeScript creates a new HashMap by applying a callback function to each + * key-value pair in the original HashMap. + * @param callbackfn - The callback function that will be called for each key-value pair in the + * HashMap. It takes four parameters: + * @param {any} [thisArg] - The `thisArg` parameter is an optional argument that specifies the value + * to be used as `this` when executing the `callbackfn` function. If `thisArg` is provided, it will + * be passed as the `this` value to the `callbackfn` function. If `thisArg + * @returns The `map` method is returning a new `HashMap` object with the transformed values based on + * the provided callback function. + */ + map(callbackfn: (value: V, key: K, index: number, map: HashMap) => U, thisArg?: any): HashMap { + const resultMap = new HashMap(); + let index = 0; + for (const [key, value] of this) { + resultMap.set(key, callbackfn.call(thisArg, value, key, index++, this)); + } + return resultMap; + } + + /** + * The `filter` function creates a new HashMap containing key-value pairs from the original HashMap + * that satisfy a given predicate function. + * @param predicate - The predicate parameter is a function that takes four arguments: value, key, + * index, and map. It is used to determine whether an element should be included in the filtered map + * or not. The function should return a boolean value - true if the element should be included, and + * false otherwise. + * @param {any} [thisArg] - The `thisArg` parameter is an optional argument that specifies the value + * to be used as `this` when executing the `predicate` function. If `thisArg` is provided, it will be + * passed as the `this` value to the `predicate` function. If `thisArg` is + * @returns The `filter` method is returning a new `HashMap` object that contains the key-value pairs + * from the original `HashMap` that pass the provided `predicate` function. + */ + filter(predicate: (value: V, key: K, index: number, map: HashMap) => boolean, thisArg?: any): HashMap { + const filteredMap = new HashMap(); + let index = 0; + for (const [key, value] of this) { + if (predicate.call(thisArg, value, key, index++, this)) { + filteredMap.set(key, value); + } + } + return filteredMap; + } + + /** + * The `reduce` function iterates over the elements of a HashMap and applies a callback function to + * each element, accumulating a single value. + * @param callbackfn - The callback function that will be called for each element in the HashMap. It + * takes five parameters: + * @param {U} initialValue - The initialValue parameter is the initial value of the accumulator. It + * is the value that will be used as the first argument of the callback function when reducing the + * elements of the map. + * @returns The `reduce` method is returning the final value of the accumulator after iterating over + * all the elements in the `HashMap`. + */ + reduce(callbackfn: (accumulator: U, currentValue: V, currentKey: K, index: number, map: HashMap) => U, initialValue: U): U { + let accumulator = initialValue; + let index = 0; + for (const [key, value] of this) { + accumulator = callbackfn(accumulator, value, key, index++, this); + } + return accumulator; + } + + protected _hashFn: (key: K) => string = (key: K) => String(key); + + protected _isObjKey(key: any): key is (object | ((...args: any[]) => any)) { + const keyType = typeof key; + return (keyType === 'object' || keyType === 'function') && key !== null + } + + protected _getNoObjKey(key: K): string { + const keyType = typeof key; + + let strKey: string; + if (keyType !== "string" && keyType !== "number" && keyType !== "symbol") { + strKey = this._hashFn(key); + } else { + if (keyType === "number") { + // TODO numeric key should has its own hash + strKey = key; + } else { + strKey = key; + } + } + return strKey; + } +} + +export class LinkedHashMap { protected _noObjMap: Record> = {}; protected _objMap = new WeakMap>(); @@ -108,51 +428,78 @@ export class HashMap { */ set(key: K, value?: V) { let node; + const isNewKey = !this.has(key); // Check if the key is new if (isWeakKey(key)) { const hash = this._objHashFn(key); node = this._objMap.get(hash); - if (node) { - // If the node already exists, update its value - node.value = value; - } else { + if (!node && isNewKey) { // Create new node node = { key: hash, value, prev: this._tail, next: this._sentinel }; - - // Add new nodes to _objMap and linked list this._objMap.set(hash, node); + } else if (node) { + // Update the value of an existing node + node.value = value; } } else { const hash = this._hashFn(key); - // Non-object keys are handled in the same way as the original implementation node = this._noObjMap[hash]; - if (node) { + + if (!node && isNewKey) { + this._noObjMap[hash] = node = { key, value, prev: this._tail, next: this._sentinel }; + } else if (node) { + // Update the value of an existing node node.value = value; - } else { - this._noObjMap[hash] = node = { - key, - value, - prev: this._tail, - next: this._sentinel - }; } } - if (this._size === 0) { - this._head = node; - this._sentinel.next = node; - } else { - this._tail.next = node; + if (node && isNewKey) { + // Update the head and tail of the linked list + if (this._size === 0) { + this._head = node; + this._sentinel.next = node; + } else { + this._tail.next = node; + node.prev = this._tail; // Make sure that the prev of the new node points to the current tail node + } + this._tail = node; + this._sentinel.prev = node; + this._size++; } - this._tail = node; - this._sentinel.prev = node; - this._size++; - return this._size; } + has(key: K): boolean { + if (isWeakKey(key)) { + const hash = this._objHashFn(key); + return this._objMap.has(hash); + } else { + const hash = this._hashFn(key); + return hash in this._noObjMap; + } + } + + setMany(entries: Iterable<[K, V]>): void { + for (const entry of entries) { + const [key, value] = entry; + this.set(key, value); + } + } + + keys(): K[] { + const keys: K[] = []; + for (const [key] of this) keys.push(key); + return keys; + } + + values(): V[] { + const values: V[] = []; + for (const [, value] of this) values.push(value); + return values; + } + /** * Time Complexity: O(1) * Space Complexity: O(1) @@ -283,16 +630,25 @@ export class HashMap { this._head = this._tail = this._sentinel.prev = this._sentinel.next = this._sentinel; } + clone(): LinkedHashMap { + const cloned = new LinkedHashMap([], { hashFn: this._hashFn, objHashFn: this._objHashFn }); + for (const entry of this) { + const [key, value] = entry; + cloned.set(key, value); + } + return cloned; + } + /** - * Time Complexity: O(n), where n is the number of elements in the HashMap. + * Time Complexity: O(n), where n is the number of elements in the LinkedHashMap. * Space Complexity: O(1) * - * The `forEach` function iterates over each element in a HashMap and executes a callback function on + * The `forEach` function iterates over each element in a LinkedHashMap and executes a callback function on * each element. * @param callback - The callback parameter is a function that will be called for each element in the - * HashMap. It takes three arguments: + * LinkedHashMap. It takes three arguments: */ - forEach(callback: (element: [K, V], index: number, hashMap: HashMap) => void) { + forEach(callback: (element: [K, V], index: number, hashMap: LinkedHashMap) => void) { let index = 0; let node = this._head; while (node !== this._sentinel) { @@ -302,15 +658,15 @@ export class HashMap { } /** - * The `filter` function takes a predicate function and returns a new HashMap containing only the + * The `filter` function takes a predicate function and returns a new LinkedHashMap containing only the * key-value pairs that satisfy the predicate. * @param predicate - The `predicate` parameter is a function that takes two arguments: `element` and * `map`. - * @returns a new HashMap object that contains the key-value pairs from the original HashMap that + * @returns a new LinkedHashMap object that contains the key-value pairs from the original LinkedHashMap that * satisfy the given predicate function. */ - filter(predicate: (element: [K, V], index: number, map: HashMap) => boolean): HashMap { - const filteredMap = new HashMap(); + filter(predicate: (element: [K, V], index: number, map: LinkedHashMap) => boolean): LinkedHashMap { + const filteredMap = new LinkedHashMap(); let index = 0; for (const [key, value] of this) { if (predicate([key, value], index, this)) { @@ -322,14 +678,14 @@ export class HashMap { } /** - * The `map` function takes a callback function and returns a new HashMap with the values transformed + * The `map` function takes a callback function and returns a new LinkedHashMap with the values transformed * by the callback. * @param callback - The `callback` parameter is a function that takes two arguments: `element` and * `map`. - * @returns a new HashMap object with the values mapped according to the provided callback function. + * @returns a new LinkedHashMap object with the values mapped according to the provided callback function. */ - map(callback: (element: [K, V], index: number, map: HashMap) => NV): HashMap { - const mappedMap = new HashMap(); + map(callback: (element: [K, V], index: number, map: LinkedHashMap) => NV): LinkedHashMap { + const mappedMap = new LinkedHashMap(); let index = 0; for (const [key, value] of this) { const newValue = callback([key, value], index, this); @@ -340,18 +696,18 @@ export class HashMap { } /** - * The `reduce` function iterates over the elements of a HashMap and applies a callback function to + * The `reduce` function iterates over the elements of a LinkedHashMap and applies a callback function to * each element, accumulating a single value. * @param callback - The callback parameter is a function that takes three arguments: accumulator, - * element, and map. It is called for each element in the HashMap and is used to accumulate a single + * element, and map. It is called for each element in the LinkedHashMap and is used to accumulate a single * result. * @param {A} initialValue - The `initialValue` parameter is the initial value of the accumulator. It * is the value that will be passed as the first argument to the `callback` function when reducing * the elements of the map. * @returns The `reduce` function is returning the final value of the accumulator after iterating - * over all the elements in the HashMap and applying the callback function to each element. + * over all the elements in the LinkedHashMap and applying the callback function to each element. */ - reduce(callback: (accumulator: A, element: [K, V], index: number, map: HashMap) => A, initialValue: A): A { + reduce(callback: (accumulator: A, element: [K, V], index: number, map: LinkedHashMap) => A, initialValue: A): A { let accumulator = initialValue; let index = 0; for (const entry of this) { @@ -362,7 +718,7 @@ export class HashMap { } /** - * Time Complexity: O(n), where n is the number of elements in the HashMap. + * Time Complexity: O(n), where n is the number of elements in the LinkedHashMap. * Space Complexity: O(1) * * The above function is an iterator that yields key-value pairs from a linked list. diff --git a/src/types/data-structures/hash/hash-map.ts b/src/types/data-structures/hash/hash-map.ts index 294fa59..5dd0b56 100644 --- a/src/types/data-structures/hash/hash-map.ts +++ b/src/types/data-structures/hash/hash-map.ts @@ -9,3 +9,5 @@ export type HashMapOptions = { hashFn: (key: K) => string; objHashFn: (key: K) => object } + +export type HashMapStoreItem = { key: K, value: V }; diff --git a/test/unit/data-structures/hash/hash-map.test.ts b/test/unit/data-structures/hash/hash-map.test.ts index 2a23c9e..e71a10e 100644 --- a/test/unit/data-structures/hash/hash-map.test.ts +++ b/test/unit/data-structures/hash/hash-map.test.ts @@ -1,7 +1,7 @@ -import { HashMap } from '../../../../src'; +import { HashMap, LinkedHashMap } from '../../../../src'; import { getRandomInt, getRandomIntArray } from '../../../utils'; -describe('HashMap', () => { +describe('HashMap Test1', () => { let hashMap: HashMap; beforeEach(() => { @@ -10,10 +10,6 @@ describe('HashMap', () => { it('should initialize correctly', () => { expect(hashMap.size).toBe(0); - // expect(hashMap.table.length).toBe(16); - // expect(hashMap.loadFactor).toBe(0.75); - // expect(hashMap.capacityMultiplier).toBe(2); - // expect(hashMap.initialCapacity).toBe(16); expect(hashMap.isEmpty()).toBe(true); }); @@ -59,10 +55,6 @@ describe('HashMap', () => { hashMap.set('two', 2); hashMap.set('three', 3); - // const entries = Array.from(hashMap.entries()); - // expect(entries).toContainEqual(['one', 1]); - // expect(entries).toContainEqual(['two', 2]); - // expect(entries).toContainEqual(['three', 3]); }); it('should resize the table when load factor is exceeded', () => { @@ -95,7 +87,7 @@ describe('HashMap', () => { }); }); -describe('HashMap', () => { +describe('HashMap Test2', () => { let hashMap: HashMap; beforeEach(() => { @@ -117,6 +109,297 @@ describe('HashMap', () => { expect(hashMap.get(keyObj)).toBe('objectValue'); }); + // it('should handle number keys correctly', () => { + // hashMap.set(999, { a: '999Value' }); + // hashMap.set('999', {a: '999StrValue'}) + // expect(hashMap.get(999)).toEqual({ a: '999Value' }); + // expect(hashMap.get('999')).toEqual({ a: '999StrValue' }); + // }); + + it('should update the value for an existing key', () => { + hashMap.set('key1', 'value1'); + hashMap.set('key1', 'newValue'); + expect(hashMap.get('key1')).toBe('newValue'); + }); + + it('should return undefined for a non-existent key', () => { + expect(hashMap.get('nonExistentKey')).toBeUndefined(); + }); + + it('should remove a key-value pair', () => { + hashMap.set('key1', 'value1'); + hashMap.delete('key1'); + expect(hashMap.get('key1')).toBeUndefined(); + }); + + it('should clear the map', () => { + hashMap.set('key1', 'value1'); + expect(hashMap.size).toBe(1); + + hashMap.clear(); + expect(hashMap.size).toBe(0); + }); + + it('should iterate over values', () => { + hashMap.set('key1', 'value1'); + hashMap.set('key2', 'value2'); + const values = []; + for (const value of hashMap) { + values.push(value); + } + expect(values).toEqual([ + ['key1', 'value1'], + ['key2', 'value2'] + ]); + }); + + function compareHashMaps(hashMap: HashMap, stdMap: Map) { + expect(hashMap.size).toEqual(stdMap.size); + stdMap.forEach((value, key) => { + expect(hashMap.get(key)).toEqual(value); + }); + } + + const stdMap: Map = new Map(); + const arr: number[] = getRandomIntArray(1000, 1, 10000); + + it('delete test', () => { + for (const item of arr) { + stdMap.set(item, item); + hashMap.set(item, item); + } + for (const item of arr) { + if (Math.random() > 0.6) { + expect(hashMap.delete(item)).toEqual(stdMap.delete(item)); + } + } + compareHashMaps(hashMap, stdMap); + + for (let i = 0; i < 1000; ++i) { + const random = getRandomInt(0, 100); + expect(hashMap.delete(random)).toEqual(stdMap.delete(random)); + } + compareHashMaps(hashMap, stdMap); + }); + +}); + +describe('HashMap for coordinate object keys', () => { + const hashMap: HashMap<[number, number], number> = new HashMap(); + const codObjs: [number, number][] = []; + + test('set elements in hash map', () => { + for (let i = 0; i < 1000; i++) { + const codObj: [number, number] = [getRandomInt(-10000, 10000), i]; + codObjs.push(codObj); + hashMap.set(codObj, i); + } + }); + + test('get elements in hash map', () => { + for (let i = 0; i < 1000; i++) { + const codObj = codObjs[i]; + if (codObj) { + expect(hashMap.get(codObj)).toBe(i); + } + } + }); + + test('delete elements in hash map', () => { + for (let i = 0; i < 1000; i++) { + if (i === 500) expect(hashMap.size).toBe(500) + const codObj = codObjs[i]; + if (codObj) hashMap.delete(codObj); + } + expect(hashMap.size).toBe(0); + }); + +}); + +describe('HashMap setMany, keys, values', () => { + const hm: HashMap = new HashMap(); + + beforeEach(() => { + hm.clear() + hm.setMany([[2, 2], [3, 3], [4, 4], [5, 5]]) + hm.setMany([[2, 2], [3, 3], [4, 4], [6, 6]]) + }) + + test('keys', () => { + expect([...hm.keys()]).toEqual([2, 3, 4, 5, 6]) + }); + + test('values', () => { + expect([...hm.values()]).toEqual([2, 3, 4, 5, 6]) + }); +}); + +describe('HashMap HOF', () => { + let hashMap: HashMap; + + beforeEach(() => { + hashMap = new HashMap(); + hashMap.set('key1', 'value1'); + hashMap.set('key2', 'value2'); + hashMap.set('key3', 'value3'); + }); + + test('every() returns true if all elements match the condition', () => { + expect(hashMap.every((value, key) => typeof value === 'string')).toBe(true); + }); + + test('some() returns true if any element matches the condition', () => { + expect(hashMap.some((value, key) => key === 'key1')).toBe(true); + }); + + test('forEach() should execute a function for each element', () => { + const mockCallback = jest.fn(); + hashMap.forEach(mockCallback); + expect(mockCallback.mock.calls.length).toBe(3); + }); + + test('map() should transform each element', () => { + const newHashMap = hashMap.map((value, key) => value.toUpperCase()); + expect(newHashMap.get('key1')).toBe('VALUE1'); + }); + + test('filter() should remove elements that do not match the condition', () => { + const filteredHashMap = hashMap.filter((value, key) => key !== 'key1'); + expect(filteredHashMap.has('key1')).toBe(false); + }); + + test('reduce() should accumulate values', () => { + const result = hashMap.reduce((acc, value, key) => acc + value, ''); + expect(result).toBe('value1value2value3'); + }); +}); + + +describe('LinkedHashMap Test1', () => { + let hashMap: LinkedHashMap; + + beforeEach(() => { + hashMap = new LinkedHashMap(); + }); + + it('should initialize correctly', () => { + expect(hashMap.size).toBe(0); + // expect(hashMap.table.length).toBe(16); + // expect(hashMap.loadFactor).toBe(0.75); + // expect(hashMap.capacityMultiplier).toBe(2); + // expect(hashMap.initialCapacity).toBe(16); + expect(hashMap.isEmpty()).toBe(true); + }); + + it('should put and get values', () => { + hashMap.set('one', 1); + hashMap.set('two', 2); + hashMap.set('three', 3); + + expect(hashMap.get('one')).toBe(1); + expect(hashMap.get('two')).toBe(2); + expect(hashMap.get('three')).toBe(3); + }); + + it('should handle key collisions', () => { + // Force a collision by setting two different keys to the same bucket + hashMap.set('key1', 1); + hashMap.set('key2', 2); + + expect(hashMap.get('key1')).toBe(1); + expect(hashMap.get('key2')).toBe(2); + }); + + it('should delete values', () => { + hashMap.set('one', 1); + hashMap.set('two', 2); + + hashMap.delete('one'); + expect(hashMap.get('one')).toBeUndefined(); + expect(hashMap.size).toBe(1); + }); + + it('should clear the LinkedHashMap', () => { + hashMap.set('one', 1); + hashMap.set('two', 2); + + hashMap.clear(); + expect(hashMap.size).toBe(0); + expect(hashMap.isEmpty()).toBe(true); + }); + + it('should iterate over entries', () => { + hashMap.set('one', 1); + hashMap.set('two', 2); + hashMap.set('three', 3); + + // const entries = Array.from(hashMap.entries()); + // expect(entries).toContainEqual(['one', 1]); + // expect(entries).toContainEqual(['two', 2]); + // expect(entries).toContainEqual(['three', 3]); + }); + + it('should resize the table when load factor is exceeded', () => { + // Set a small initial capacity for testing resizing + hashMap = new LinkedHashMap(); + + hashMap.set('one', 1); + hashMap.set('two', 2); + hashMap.set('three', 3); + hashMap.set('four', 4); // This should trigger a resize + + // expect(hashMap.table.length).toBe(8); + expect(hashMap.get('one')).toBe(1); + expect(hashMap.get('two')).toBe(2); + expect(hashMap.get('three')).toBe(3); + expect(hashMap.get('four')).toBe(4); + }); + + it('should allow using a custom hash function', () => { + hashMap = new LinkedHashMap(); + + hashMap.set('one', 1); + hashMap.set('two', 2); + + expect(hashMap.get('one')).toBe(1); + expect(hashMap.get('two')).toBe(2); + // Since the custom hash function always returns 0, these keys will collide. + // Make sure they are stored separately. + // expect(hashMap.table[0].length).toBe(2); + }); + + + // it('should handle number keys correctly', () => { + // const hm = new LinkedHashMap(); + // hm.set(999, { a: '999Value' }); + // hm.set('999', {a: '999StrValue'}) + // expect(hm.get(999)).toEqual({ a: '999Value' }); + // expect(hm.get('999')).toEqual({ a: '999StrValue1' }); + // }); +}); + +describe('LinkedHashMap Test2', () => { + let hashMap: LinkedHashMap; + + beforeEach(() => { + hashMap = new LinkedHashMap(); + }); + + it('should create an empty map', () => { + expect(hashMap.size).toBe(0); + }); + + it('should add a key-value pair', () => { + hashMap.set('key1', 'value1'); + expect(hashMap.get('key1')).toBe('value1'); + }); + + it('should handle object keys correctly', () => { + const keyObj = { id: 1 }; + hashMap.set(keyObj, 'objectValue'); + expect(hashMap.get(keyObj)).toBe('objectValue'); + }); + it('should handle number keys correctly', () => { hashMap.set(999, { a: '999Value' }); expect(hashMap.get(999)).toEqual({ a: '999Value' }); @@ -159,14 +442,7 @@ describe('HashMap', () => { ]); }); - // test('should delete element at specific index', () => { - // hashMap.set('key1', 'value1'); - // hashMap.set('key2', 'value2'); - // hashMap.deleteAt(0); - // expect(hashMap.get('key1')).toBeUndefined(); - // expect(hashMap.size).toBe(1); - // }); - function compareHashMaps(hashMap: HashMap, stdMap: Map) { + function compareHashMaps(hashMap: LinkedHashMap, stdMap: Map) { expect(hashMap.size).toEqual(stdMap.size); let index = 0; stdMap.forEach((value, key) => { @@ -230,8 +506,8 @@ describe('HashMap', () => { }); }); -describe('HashMap for coordinate object keys', () => { - const hashMap: HashMap<[number, number], number> = new HashMap(); +describe('LinkedHashMap for coordinate object keys', () => { + const hashMap: LinkedHashMap<[number, number], number> = new LinkedHashMap(); const codObjs: [number, number][] = []; test('set elements in hash map', () => { @@ -261,3 +537,21 @@ describe('HashMap for coordinate object keys', () => { }); }); + +describe('LinkedHashMap setMany, keys, values', () => { + const hm: LinkedHashMap = new LinkedHashMap(); + + beforeEach(() => { + hm.clear() + hm.setMany([[2, 2], [3, 3], [4, 4], [5, 5]]) + hm.setMany([[2, 2], [3, 3], [4, 4], [6, 6]]) + }) + + test('keys', () => { + expect(hm.keys()).toEqual([2, 3, 4, 5, 6]) + }); + + test('values', () => { + expect(hm.values()).toEqual([2, 3, 4, 5, 6]) + }); +}); \ No newline at end of file