Files
hash-map/tests/HashMap.test.ts

363 lines
9.4 KiB
TypeScript

import { describe, it, expect, beforeEach } from "bun:test";
import { HashMap, NumericHashFunction } from "../src/index.ts";
import type { IHashFunction } from "../src/index.ts";
describe("HashMap", () => {
let map: HashMap<string, number>;
beforeEach(() => {
map = new HashMap<string, number>();
});
describe("constructor", () => {
it("should create an empty map with default capacity", () => {
expect(map.size).toBe(0);
expect(map.capacity).toBe(16);
});
it("should create a map with custom initial capacity", () => {
const customMap = new HashMap<string, number>(32);
expect(customMap.capacity).toBe(32);
});
it("should throw error for invalid capacity", () => {
expect(() => new HashMap<string, number>(0)).toThrow();
expect(() => new HashMap<string, number>(-1)).toThrow();
});
it("should throw error for invalid load factor", () => {
expect(() => new HashMap<string, number>(16, 0)).toThrow();
expect(() => new HashMap<string, number>(16, 1.5)).toThrow();
});
});
describe("set and get", () => {
it("should set and get a value", () => {
map.set("key1", 100);
expect(map.get("key1")).toBe(100);
});
it("should update existing value", () => {
map.set("key1", 100);
map.set("key1", 200);
expect(map.get("key1")).toBe(200);
expect(map.size).toBe(1);
});
it("should return undefined for non-existent key", () => {
expect(map.get("nonexistent")).toBeUndefined();
});
it("should handle multiple key-value pairs", () => {
map.set("a", 1);
map.set("b", 2);
map.set("c", 3);
expect(map.get("a")).toBe(1);
expect(map.get("b")).toBe(2);
expect(map.get("c")).toBe(3);
expect(map.size).toBe(3);
});
});
describe("has", () => {
it("should return true for existing key", () => {
map.set("key1", 100);
expect(map.has("key1")).toBe(true);
});
it("should return false for non-existent key", () => {
expect(map.has("nonexistent")).toBe(false);
});
});
describe("delete", () => {
it("should delete existing key", () => {
map.set("key1", 100);
expect(map.delete("key1")).toBe(true);
expect(map.has("key1")).toBe(false);
expect(map.size).toBe(0);
});
it("should return false for non-existent key", () => {
expect(map.delete("nonexistent")).toBe(false);
});
it("should handle deletion from chain", () => {
// Add multiple items that might collide
map.set("a", 1);
map.set("b", 2);
map.set("c", 3);
map.delete("b");
expect(map.has("b")).toBe(false);
expect(map.has("a")).toBe(true);
expect(map.has("c")).toBe(true);
expect(map.size).toBe(2);
});
});
describe("clear", () => {
it("should remove all entries", () => {
map.set("a", 1);
map.set("b", 2);
map.set("c", 3);
map.clear();
expect(map.size).toBe(0);
expect(map.has("a")).toBe(false);
expect(map.has("b")).toBe(false);
expect(map.has("c")).toBe(false);
});
});
describe("size", () => {
it("should track size correctly", () => {
expect(map.size).toBe(0);
map.set("a", 1);
expect(map.size).toBe(1);
map.set("b", 2);
expect(map.size).toBe(2);
map.delete("a");
expect(map.size).toBe(1);
map.clear();
expect(map.size).toBe(0);
});
});
describe("keys", () => {
it("should iterate over all keys", () => {
map.set("a", 1);
map.set("b", 2);
map.set("c", 3);
const keys = Array.from(map.keys());
expect(keys).toHaveLength(3);
expect(keys).toContain("a");
expect(keys).toContain("b");
expect(keys).toContain("c");
});
it("should return empty iterator for empty map", () => {
const keys = Array.from(map.keys());
expect(keys).toHaveLength(0);
});
});
describe("values", () => {
it("should iterate over all values", () => {
map.set("a", 1);
map.set("b", 2);
map.set("c", 3);
const values = Array.from(map.values());
expect(values).toHaveLength(3);
expect(values).toContain(1);
expect(values).toContain(2);
expect(values).toContain(3);
});
});
describe("entries", () => {
it("should iterate over all entries", () => {
map.set("a", 1);
map.set("b", 2);
const entries = Array.from(map.entries());
expect(entries).toHaveLength(2);
expect(entries).toContainEqual(["a", 1]);
expect(entries).toContainEqual(["b", 2]);
});
});
describe("forEach", () => {
it("should iterate with forEach", () => {
map.set("a", 1);
map.set("b", 2);
map.set("c", 3);
const result: [string, number][] = [];
map.forEach((value, key) => {
result.push([key, value]);
});
expect(result).toHaveLength(3);
expect(result).toContainEqual(["a", 1]);
expect(result).toContainEqual(["b", 2]);
expect(result).toContainEqual(["c", 3]);
});
});
describe("iterable", () => {
it("should work with for...of", () => {
map.set("a", 1);
map.set("b", 2);
const entries: [string, number][] = [];
for (const entry of map) {
entries.push(entry);
}
expect(entries).toHaveLength(2);
expect(entries).toContainEqual(["a", 1]);
expect(entries).toContainEqual(["b", 2]);
});
});
describe("resizing", () => {
it("should resize when load factor exceeds threshold", () => {
const smallMap = new HashMap<string, number>(4, 0.75);
expect(smallMap.capacity).toBe(4);
// Add enough items to trigger resize
for (let i = 0; i < 10; i++) {
smallMap.set(`key${i}`, i);
}
expect(smallMap.size).toBe(10);
expect(smallMap.capacity).toBeGreaterThan(4);
// Verify all items are still accessible
for (let i = 0; i < 10; i++) {
expect(smallMap.get(`key${i}`)).toBe(i);
}
});
it("should maintain all entries after resize", () => {
const smallMap = new HashMap<string, number>(2, 0.5);
const entries = [
["a", 1],
["b", 2],
["c", 3],
["d", 4],
["e", 5],
] as const;
for (const [key, value] of entries) {
smallMap.set(key, value);
}
for (const [key, value] of entries) {
expect(smallMap.get(key)).toBe(value);
}
});
});
describe("custom hash function", () => {
it("should work with NumericHashFunction", () => {
const numMap = new HashMap<number, string>(
16,
0.75,
new NumericHashFunction()
);
numMap.set(123, "value1");
numMap.set(456, "value2");
expect(numMap.get(123)).toBe("value1");
expect(numMap.get(456)).toBe("value2");
});
it("should work with custom hash function", () => {
class SimpleHashFunction implements IHashFunction<string> {
hash(key: string, capacity: number): number {
return key.length % capacity;
}
}
const customMap = new HashMap<string, string>(
8,
0.75,
new SimpleHashFunction()
);
customMap.set("hi", "short");
customMap.set("hello", "medium");
expect(customMap.get("hi")).toBe("short");
expect(customMap.get("hello")).toBe("medium");
});
});
describe("edge cases", () => {
it("should handle null values", () => {
const nullMap = new HashMap<string, null>();
nullMap.set("key", null);
expect(nullMap.get("key")).toBeNull();
expect(nullMap.has("key")).toBe(true);
});
it("should handle undefined values", () => {
const undefinedMap = new HashMap<string, undefined>();
undefinedMap.set("key", undefined);
expect(undefinedMap.has("key")).toBe(true);
});
it("should handle empty string keys", () => {
map.set("", 100);
expect(map.get("")).toBe(100);
});
it("should handle numeric keys", () => {
const numMap = new HashMap<number, string>();
numMap.set(0, "zero");
numMap.set(1, "one");
numMap.set(-1, "negative one");
expect(numMap.get(0)).toBe("zero");
expect(numMap.get(1)).toBe("one");
expect(numMap.get(-1)).toBe("negative one");
});
it("should handle large number of entries", () => {
const largeMap = new HashMap<number, number>();
const count = 1000;
for (let i = 0; i < count; i++) {
largeMap.set(i, i * 2);
}
expect(largeMap.size).toBe(count);
for (let i = 0; i < count; i++) {
expect(largeMap.get(i)).toBe(i * 2);
}
});
});
describe("collision handling", () => {
it("should handle hash collisions correctly", () => {
// Create a map with small capacity to increase collision probability
const collisionMap = new HashMap<string, number>(2, 0.99);
collisionMap.set("a", 1);
collisionMap.set("b", 2);
collisionMap.set("c", 3);
collisionMap.set("d", 4);
expect(collisionMap.size).toBe(4);
expect(collisionMap.get("a")).toBe(1);
expect(collisionMap.get("b")).toBe(2);
expect(collisionMap.get("c")).toBe(3);
expect(collisionMap.get("d")).toBe(4);
});
});
describe("toString", () => {
it("should provide readable string representation", () => {
map.set("a", 1);
map.set("b", 2);
const str = map.toString();
expect(str).toContain("HashMap");
expect(str).toContain("2"); // size
});
});
});