362 lines
9.4 KiB
TypeScript
362 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
|
|
});
|
|
});
|
|
});
|