Initial Commit - Sonnet 4.5

This commit is contained in:
2025-11-22 18:18:23 -05:00
commit 74fd80f91c
21 changed files with 2621 additions and 0 deletions

279
src/core/HashMap.ts Normal file
View File

@@ -0,0 +1,279 @@
import type { IHashMap } from "../interfaces/IHashMap.ts";
import type { IHashFunction } from "../interfaces/IHashFunction.ts";
import { HashNode } from "../models/HashNode.ts";
import { DefaultHashFunction } from "../hash-functions/DefaultHashFunction.ts";
/**
* HashMap implementation using separate chaining for collision resolution.
* Follows SOLID principles:
* - SRP: Focused on hash map operations
* - OCP: Extensible through custom hash functions
* - LSP: Implements IHashMap interface correctly
* - ISP: Uses focused interfaces
* - DIP: Depends on IHashFunction abstraction
*
* @template K - The type of keys
* @template V - The type of values
*/
export class HashMap<K, V> implements IHashMap<K, V>, Iterable<[K, V]> {
private buckets: (HashNode<K, V> | null)[];
private _size: number = 0;
private readonly hashFunction: IHashFunction<K>;
private readonly loadFactorThreshold: number;
private readonly initialCapacity: number;
/**
* Creates a new HashMap instance.
* @param initialCapacity - Initial number of buckets (default: 16)
* @param loadFactorThreshold - Threshold for resizing (default: 0.75)
* @param hashFunction - Custom hash function (optional)
*/
constructor(
initialCapacity: number = 16,
loadFactorThreshold: number = 0.75,
hashFunction?: IHashFunction<K>
) {
if (initialCapacity <= 0) {
throw new Error("Initial capacity must be positive");
}
if (loadFactorThreshold <= 0 || loadFactorThreshold > 1) {
throw new Error("Load factor must be between 0 and 1");
}
this.initialCapacity = initialCapacity;
this.buckets = new Array(initialCapacity).fill(null);
this.loadFactorThreshold = loadFactorThreshold;
this.hashFunction = hashFunction ?? new DefaultHashFunction<K>();
}
/**
* Gets the current number of key-value pairs in the map.
*/
get size(): number {
return this._size;
}
/**
* Gets the current capacity (number of buckets).
*/
get capacity(): number {
return this.buckets.length;
}
/**
* Gets the current load factor.
*/
get loadFactor(): number {
return this._size / this.buckets.length;
}
/**
* Inserts or updates a key-value pair in the map.
* Time Complexity: Average O(1), Worst O(n)
*/
set(key: K, value: V): void {
this.ensureCapacity();
const index = this.hashFunction.hash(key, this.buckets.length);
let node = this.buckets[index] ?? null;
// Check if key already exists
while (node !== null) {
if (this.keysEqual(node.key, key)) {
node.value = value; // Update existing value
return;
}
node = node.next;
}
// Insert new node at the beginning of the chain
const newNode = new HashNode(key, value, this.buckets[index] ?? null);
this.buckets[index] = newNode;
this._size++;
}
/**
* Retrieves the value associated with the given key.
* Time Complexity: Average O(1), Worst O(n)
*/
get(key: K): V | undefined {
const index = this.hashFunction.hash(key, this.buckets.length);
let node = this.buckets[index] ?? null;
while (node !== null) {
if (this.keysEqual(node.key, key)) {
return node.value;
}
node = node.next;
}
return undefined;
}
/**
* Checks if a key exists in the map.
* Time Complexity: Average O(1), Worst O(n)
*/
has(key: K): boolean {
const index = this.hashFunction.hash(key, this.buckets.length);
let node = this.buckets[index] ?? null;
while (node !== null) {
if (this.keysEqual(node.key, key)) {
return true;
}
node = node.next;
}
return false;
}
/**
* Removes a key-value pair from the map.
* Time Complexity: Average O(1), Worst O(n)
*/
delete(key: K): boolean {
const index = this.hashFunction.hash(key, this.buckets.length);
let node = this.buckets[index] ?? null;
let prev: HashNode<K, V> | null = null;
while (node !== null) {
if (this.keysEqual(node.key, key)) {
if (prev === null) {
// Remove head node
this.buckets[index] = node.next;
} else {
// Remove middle or tail node
prev.next = node.next;
}
this._size--;
return true;
}
prev = node;
node = node.next;
}
return false;
}
/**
* Removes all key-value pairs from the map.
* Time Complexity: O(n) where n is the capacity
*/
clear(): void {
this.buckets = new Array(this.initialCapacity).fill(null);
this._size = 0;
}
/**
* Returns an iterator over the keys in the map.
*/
*keys(): IterableIterator<K> {
for (const bucket of this.buckets) {
let node = bucket ?? null;
while (node !== null) {
yield node.key;
node = node.next;
}
}
}
/**
* Returns an iterator over the values in the map.
*/
*values(): IterableIterator<V> {
for (const bucket of this.buckets) {
let node = bucket ?? null;
while (node !== null) {
yield node.value;
node = node.next;
}
}
}
/**
* Returns an iterator over the key-value pairs in the map.
*/
*entries(): IterableIterator<[K, V]> {
for (const bucket of this.buckets) {
let node = bucket ?? null;
while (node !== null) {
yield [node.key, node.value];
node = node.next;
}
}
}
/**
* Makes the HashMap iterable (for...of loops).
*/
[Symbol.iterator](): IterableIterator<[K, V]> {
return this.entries();
}
/**
* Executes a callback for each key-value pair in the map.
*/
forEach(callback: (value: V, key: K, map: IHashMap<K, V>) => void): void {
for (const [key, value] of this.entries()) {
callback(value, key, this);
}
}
/**
* Returns a string representation of the map.
*/
toString(): string {
const entries: string[] = [];
for (const [key, value] of this.entries()) {
entries.push(`${String(key)} => ${String(value)}`);
}
return `HashMap(${this._size}) { ${entries.join(", ")} }`;
}
/**
* Checks if two keys are equal.
* Handles primitive types and object references.
*/
private keysEqual(key1: K, key2: K): boolean {
// Handle NaN case
if (typeof key1 === "number" && typeof key2 === "number") {
if (Number.isNaN(key1) && Number.isNaN(key2)) {
return true;
}
}
// Standard equality
return key1 === key2;
}
/**
* Ensures the map has sufficient capacity.
* Resizes if load factor exceeds threshold.
*/
private ensureCapacity(): void {
if (this.loadFactor >= this.loadFactorThreshold) {
this.resize(this.buckets.length * 2);
}
}
/**
* Resizes the hash table to the new capacity.
* Rehashes all existing entries.
*/
private resize(newCapacity: number): void {
const oldBuckets = this.buckets;
this.buckets = new Array(newCapacity).fill(null);
this._size = 0;
// Rehash all existing entries
for (const bucket of oldBuckets) {
let node = bucket ?? null;
while (node !== null) {
this.set(node.key, node.value);
node = node.next;
}
}
}
}

View File

@@ -0,0 +1,88 @@
import { HashMap } from "../index.ts";
/**
* Basic usage examples for the HashMap implementation.
*/
console.log("=== Basic HashMap Usage Examples ===\n");
// Example 1: String keys with number values
console.log("1. String keys with number values:");
const scores = new HashMap<string, number>();
scores.set("Alice", 95);
scores.set("Bob", 87);
scores.set("Charlie", 92);
console.log(`Alice's score: ${scores.get("Alice")}`); // 95
console.log(`Map size: ${scores.size}`); // 3
console.log(`Has Bob? ${scores.has("Bob")}`); // true
console.log("");
// Example 2: Iterating over entries
console.log("2. Iterating with for...of:");
for (const [name, score] of scores) {
console.log(` ${name}: ${score}`);
}
console.log("");
// Example 3: Using forEach
console.log("3. Using forEach:");
scores.forEach((score, name) => {
console.log(` ${name} scored ${score}`);
});
console.log("");
// Example 4: Working with keys and values
console.log("4. Keys and values:");
console.log(" Keys:", Array.from(scores.keys()).join(", "));
console.log(" Values:", Array.from(scores.values()).join(", "));
console.log("");
// Example 5: Deleting entries
console.log("5. Deleting entries:");
console.log(` Deleted Bob? ${scores.delete("Bob")}`); // true
console.log(` Deleted David? ${scores.delete("David")}`); // false
console.log(` Map size after deletion: ${scores.size}`); // 2
console.log("");
// Example 6: Complex object values
console.log("6. Complex object values:");
interface User {
name: string;
email: string;
age: number;
}
const users = new HashMap<number, User>();
users.set(1, { name: "Alice", email: "alice@example.com", age: 30 });
users.set(2, { name: "Bob", email: "bob@example.com", age: 25 });
const user1 = users.get(1);
console.log(` User 1: ${user1?.name} (${user1?.email})`);
console.log("");
// Example 7: Clearing the map
console.log("7. Clearing the map:");
console.log(` Size before clear: ${scores.size}`);
scores.clear();
console.log(` Size after clear: ${scores.size}`);
console.log("");
// Example 8: Load factor and capacity
console.log("8. HashMap internals:");
const map = new HashMap<string, number>(4, 0.75); // Small initial capacity
console.log(` Initial capacity: ${map.capacity}`);
console.log(` Initial load factor: ${map.loadFactor.toFixed(2)}`);
for (let i = 0; i < 10; i++) {
map.set(`key${i}`, i);
}
console.log(` After 10 inserts:`);
console.log(` Size: ${map.size}`);
console.log(` Capacity: ${map.capacity}`);
console.log(` Load factor: ${map.loadFactor.toFixed(2)}`);
console.log("");
console.log("=== Examples Complete ===");

View File

@@ -0,0 +1,111 @@
import { HashMap, NumericHashFunction } from "../index.ts";
import type { IHashFunction } from "../index.ts";
/**
* Examples demonstrating custom hash function usage.
* This showcases the Dependency Inversion Principle and Open/Closed Principle.
*/
console.log("=== Custom Hash Function Examples ===\n");
// Example 1: Using the NumericHashFunction for better numeric key distribution
console.log("1. Using NumericHashFunction:");
const numericMap = new HashMap<number, string>(
16,
0.75,
new NumericHashFunction()
);
numericMap.set(12345, "value1");
numericMap.set(67890, "value2");
numericMap.set(11111, "value3");
console.log(` Get 12345: ${numericMap.get(12345)}`);
console.log(` Map size: ${numericMap.size}`);
console.log("");
// Example 2: Creating a custom hash function for case-insensitive string keys
console.log("2. Case-insensitive string hash function:");
class CaseInsensitiveHashFunction implements IHashFunction<string> {
hash(key: string, capacity: number): number {
const lowerKey = key.toLowerCase();
let hash = 0;
for (let i = 0; i < lowerKey.length; i++) {
const char = lowerKey.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return Math.abs(hash) % capacity;
}
}
const caseInsensitiveMap = new HashMap<string, number>(
16,
0.75,
new CaseInsensitiveHashFunction()
);
caseInsensitiveMap.set("Hello", 1);
caseInsensitiveMap.set("HELLO", 2); // This will overwrite the previous value
console.log(` Get "Hello": ${caseInsensitiveMap.get("Hello")}`); // 2
console.log(` Get "hello": ${caseInsensitiveMap.get("hello")}`); // 2
console.log(` Get "HELLO": ${caseInsensitiveMap.get("HELLO")}`); // 2
console.log(` Map size: ${caseInsensitiveMap.size}`); // 1 (keys are treated as equal)
console.log("");
// Example 3: Custom hash function for complex objects
console.log("3. Custom hash for complex objects:");
interface Point {
x: number;
y: number;
}
class PointHashFunction implements IHashFunction<Point> {
hash(key: Point, capacity: number): number {
// Cantor pairing function for combining two numbers
const hash = ((key.x + key.y) * (key.x + key.y + 1)) / 2 + key.y;
return Math.abs(Math.floor(hash)) % capacity;
}
}
const pointMap = new HashMap<Point, string>(16, 0.75, new PointHashFunction());
const p1: Point = { x: 10, y: 20 };
const p2: Point = { x: 10, y: 20 }; // Same values but different object
const p3: Point = { x: 30, y: 40 };
pointMap.set(p1, "Point 1");
pointMap.set(p3, "Point 3");
console.log(` Get p1: ${pointMap.get(p1)}`); // "Point 1"
console.log(` Get p2 (same values): ${pointMap.get(p2)}`); // undefined (different reference)
console.log(` Get p3: ${pointMap.get(p3)}`); // "Point 3"
console.log("");
// Example 4: Modulo-based hash function
console.log("4. Simple modulo hash function:");
class ModuloHashFunction implements IHashFunction<number> {
hash(key: number, capacity: number): number {
return Math.abs(Math.floor(key)) % capacity;
}
}
const moduloMap = new HashMap<number, string>(8, 0.75, new ModuloHashFunction());
for (let i = 0; i < 20; i++) {
moduloMap.set(i, `value-${i}`);
}
console.log(` Map size: ${moduloMap.size}`);
console.log(` Get 5: ${moduloMap.get(5)}`);
console.log(` Get 15: ${moduloMap.get(15)}`);
console.log("");
console.log("=== Custom Hash Function Examples Complete ===");

View File

@@ -0,0 +1,43 @@
import type { IHashFunction } from "../interfaces/IHashFunction.ts";
/**
* Default hash function implementation.
* Follows Single Responsibility Principle (SRP) - only responsible for hashing.
* Uses a simple but effective string-based hashing algorithm.
*/
export class DefaultHashFunction<K> implements IHashFunction<K> {
hash(key: K, capacity: number): number {
const str = this.convertToString(key);
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
// Ensure positive index
return Math.abs(hash) % capacity;
}
/**
* Converts any key type to a string representation.
* @param key - The key to convert
* @returns A string representation of the key
*/
private convertToString(key: K): string {
if (key === null) return "null";
if (key === undefined) return "undefined";
if (typeof key === "string") return key;
if (typeof key === "number" || typeof key === "boolean") return String(key);
if (typeof key === "object") {
try {
return JSON.stringify(key);
} catch {
return Object.prototype.toString.call(key);
}
}
return String(key);
}
}

View File

@@ -0,0 +1,25 @@
import type { IHashFunction } from "../interfaces/IHashFunction.ts";
/**
* Specialized hash function for numeric keys.
* Follows Single Responsibility Principle (SRP).
* Provides better distribution for numeric keys than the default hash function.
*/
export class NumericHashFunction implements IHashFunction<number> {
/**
* Uses multiplication method for hashing numbers.
* This method provides good distribution for numeric keys.
*/
hash(key: number, capacity: number): number {
// Handle special cases
if (!Number.isFinite(key)) {
return 0;
}
// Multiplication method with golden ratio
const A = 0.6180339887; // (√5 - 1) / 2
const fractionalPart = (Math.abs(key) * A) % 1;
return Math.floor(capacity * fractionalPart);
}
}

33
src/index.ts Normal file
View File

@@ -0,0 +1,33 @@
/**
* @techniker-me/hash-map
* A robust HashMap implementation following OOP SOLID principles.
*
* Features:
* - Generic type support for keys and values
* - Separate chaining for collision resolution
* - Automatic resizing based on load factor
* - Custom hash function support
* - Full iterator support (keys, values, entries)
* - Compatible with for...of loops
*
* SOLID Principles Applied:
* - Single Responsibility: Each class has one clear purpose
* - Open/Closed: Extensible through custom hash functions
* - Liskov Substitution: All implementations follow their interfaces
* - Interface Segregation: Focused, minimal interfaces
* - Dependency Inversion: Depends on abstractions (IHashFunction, IHashMap)
*/
// Core implementation
export { HashMap } from "./core/HashMap.ts";
// Interfaces
export type { IHashMap } from "./interfaces/IHashMap.ts";
export type { IHashFunction } from "./interfaces/IHashFunction.ts";
// Models
export { HashNode } from "./models/HashNode.ts";
// Hash functions
export { DefaultHashFunction } from "./hash-functions/DefaultHashFunction.ts";
export { NumericHashFunction } from "./hash-functions/NumericHashFunction.ts";

View File

@@ -0,0 +1,15 @@
/**
* Interface for hash functions.
* Follows Interface Segregation Principle (ISP) and Dependency Inversion Principle (DIP).
* Allows different hashing strategies to be implemented and injected.
*/
export interface IHashFunction<K> {
/**
* Computes a hash value for the given key.
* @param key - The key to hash
* @param capacity - The current capacity of the hash table
* @returns A hash value in the range [0, capacity)
*/
hash(key: K, capacity: number): number;
}

View File

@@ -0,0 +1,66 @@
/**
* Interface for HashMap operations.
* Follows Interface Segregation Principle (ISP).
* Defines the contract that all HashMap implementations must follow.
*/
export interface IHashMap<K, V> {
/**
* Inserts or updates a key-value pair in the map.
* @param key - The key to insert/update
* @param value - The value to associate with the key
*/
set(key: K, value: V): void;
/**
* Retrieves the value associated with the given key.
* @param key - The key to look up
* @returns The value if found, undefined otherwise
*/
get(key: K): V | undefined;
/**
* Checks if a key exists in the map.
* @param key - The key to check
* @returns true if the key exists, false otherwise
*/
has(key: K): boolean;
/**
* Removes a key-value pair from the map.
* @param key - The key to remove
* @returns true if the key was found and removed, false otherwise
*/
delete(key: K): boolean;
/**
* Removes all key-value pairs from the map.
*/
clear(): void;
/**
* Gets the number of key-value pairs in the map.
*/
get size(): number;
/**
* Returns an iterator over the keys in the map.
*/
keys(): IterableIterator<K>;
/**
* Returns an iterator over the values in the map.
*/
values(): IterableIterator<V>;
/**
* Returns an iterator over the key-value pairs in the map.
*/
entries(): IterableIterator<[K, V]>;
/**
* Executes a callback for each key-value pair in the map.
* @param callback - The function to execute for each entry
*/
forEach(callback: (value: V, key: K, map: IHashMap<K, V>) => void): void;
}

14
src/models/HashNode.ts Normal file
View File

@@ -0,0 +1,14 @@
/**
* Represents a node in the hash table's collision chain.
* Follows Single Responsibility Principle (SRP) - only responsible for storing key-value pairs.
* @template K - The type of the key
* @template V - The type of the value
*/
export class HashNode<K, V> {
constructor(
public key: K,
public value: V,
public next: HashNode<K, V> | null = null
) {}
}