Update ESLint configuration, package.json, and README; improve formatting and versioning consistency. Adjust TypeScript configuration to disallow importing extensions and refine test scripts for better readability.

This commit is contained in:
2025-11-23 01:15:28 -05:00
parent 3deadc6e75
commit f007573ade
15 changed files with 218 additions and 164 deletions

View File

@@ -64,24 +64,24 @@ new HashMap<K, V>(
### Methods
| Method | Description | Time Complexity |
|--------|-------------|-----------------|
| `set(key: K, value: V): void` | Insert or update a key-value pair | O(1) average |
| `get(key: K): V \| undefined` | Retrieve value by key | O(1) average |
| `has(key: K): boolean` | Check if key exists | O(1) average |
| `delete(key: K): boolean` | Remove entry by key | O(1) average |
| `clear(): void` | Remove all entries | O(n) |
| `keys(): IterableIterator<K>` | Iterator over keys | O(n) |
| `values(): IterableIterator<V>` | Iterator over values | O(n) |
| `entries(): IterableIterator<[K, V]>` | Iterator over entries | O(n) |
| `forEach(callback): void` | Execute callback for each entry | O(n) |
| Method | Description | Time Complexity |
| ------------------------------------- | --------------------------------- | --------------- |
| `set(key: K, value: V): void` | Insert or update a key-value pair | O(1) average |
| `get(key: K): V \| undefined` | Retrieve value by key | O(1) average |
| `has(key: K): boolean` | Check if key exists | O(1) average |
| `delete(key: K): boolean` | Remove entry by key | O(1) average |
| `clear(): void` | Remove all entries | O(n) |
| `keys(): IterableIterator<K>` | Iterator over keys | O(n) |
| `values(): IterableIterator<V>` | Iterator over values | O(n) |
| `entries(): IterableIterator<[K, V]>` | Iterator over entries | O(n) |
| `forEach(callback): void` | Execute callback for each entry | O(n) |
### Properties
| Property | Description |
|----------|-------------|
| `size` | Number of key-value pairs |
| `capacity` | Current number of buckets |
| Property | Description |
| ------------ | ------------------------------------- |
| `size` | Number of key-value pairs |
| `capacity` | Current number of buckets |
| `loadFactor` | Current load factor (size / capacity) |
## Advanced Usage
@@ -108,7 +108,7 @@ class CaseInsensitiveHashFunction implements IHashFunction<string> {
const map = new HashMap<string, number>(
16,
0.75,
new CaseInsensitiveHashFunction()
new CaseInsensitiveHashFunction(),
);
map.set("Hello", 1);
@@ -122,11 +122,7 @@ Use the built-in `NumericHashFunction` for better distribution with numeric keys
```typescript
import { HashMap, NumericHashFunction } from "@techniker-me/hash-map";
const map = new HashMap<number, string>(
16,
0.75,
new NumericHashFunction()
);
const map = new HashMap<number, string>(16, 0.75, new NumericHashFunction());
map.set(12345, "value1");
map.set(67890, "value2");
@@ -155,25 +151,30 @@ console.log(user?.name); // "Alice"
This implementation adheres to all five SOLID principles:
### 1. Single Responsibility Principle (SRP)
- `HashMap` - Manages hash map operations
- `HashNode` - Stores key-value pairs
- `DefaultHashFunction` - Handles hashing logic
- Each class has one clear purpose
### 2. Open/Closed Principle (OCP)
- Extensible through custom hash functions
- Core implementation is closed for modification
### 3. Liskov Substitution Principle (LSP)
- All implementations correctly implement their interfaces
- Subtypes can replace their base types without breaking functionality
### 4. Interface Segregation Principle (ISP)
- `IHashMap` - Focused map operations
- `IHashFunction` - Minimal hashing interface
- Clients depend only on interfaces they use
### 5. Dependency Inversion Principle (DIP)
- Depends on `IHashFunction` abstraction, not concrete implementations
- High-level modules don't depend on low-level modules
@@ -230,6 +231,7 @@ bun test --coverage
```
**Test Coverage: 100%**
- 66 comprehensive tests
- 1,168 assertions
- All edge cases covered

View File

@@ -8,11 +8,31 @@ import { defineConfig } from "eslint/config";
export default defineConfig([
{
ignores: ["dist/**", "node_modules/**", "*.min.js"]
ignores: ["dist/**", "node_modules/**", "*.min.js"],
},
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
plugins: { js },
extends: ["js/recommended"],
languageOptions: { globals: globals.node },
},
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } },
tseslint.configs.recommended,
{ files: ["**/*.json"], plugins: { json }, language: "json/json", extends: ["json/recommended"] },
{ files: ["**/*.md"], plugins: { markdown }, language: "markdown/commonmark", extends: ["markdown/recommended"] },
{ files: ["**/*.css"], plugins: { css }, language: "css/css", extends: ["css/recommended"] },
{
files: ["**/*.json"],
plugins: { json },
language: "json/json",
extends: ["json/recommended"],
},
{
files: ["**/*.md"],
plugins: { markdown },
language: "markdown/commonmark",
extends: ["markdown/recommended"],
},
{
files: ["**/*.css"],
plugins: { css },
language: "css/css",
extends: ["css/recommended"],
},
]);

View File

@@ -1,17 +1,25 @@
{
"name": "@techniker-me/hash-map",
"version": "1.0.8",
"version": "1.0.9",
"description": "A robust HashMap implementation following OOP SOLID principles",
"type": "module",
"main": "browser/index.ts",
"module": "node/index.js",
"types": "types/index.d.ts",
"main": "./node/index.js",
"module": "./browser/index.js",
"types": "./types/index.d.ts",
"exports": {
".": {
"node": "node/index.js",
"browser": "browser/index.js"
"types": "./types/index.d.ts",
"node": {
"import": "./node/index.js",
"default": "./node/index.js"
},
"browser": {
"import": "./browser/index.js",
"default": "./browser/index.js"
},
"default": "./node/index.js"
},
"./types": "types/index.d.ts"
"./types": "./types/index.d.ts"
},
"files": [
"node",

View File

@@ -13,4 +13,4 @@ if [ ! -d "${distDirectory}" ]; then
fi
echo "Preparing [package.json] to [${distDirectory}]"
jq '{name, version, author, type, types, exports, files, publishConfig}' "${packageJsonPath}" > "${distDirectory}/package.json"
jq '{name, version, author, type, main, module, types, exports, files, publishConfig}' "${packageJsonPath}" > "${distDirectory}/package.json"

View File

@@ -16,10 +16,13 @@ async function convertTAPToTeamCity() {
return;
}
const proc = Bun.spawn(["bun", "test", "--reporter=tap", ...process.argv.slice(2)], {
stdout: "pipe",
stderr: "pipe",
});
const proc = Bun.spawn(
["bun", "test", "--reporter=tap", ...process.argv.slice(2)],
{
stdout: "pipe",
stderr: "pipe",
},
);
const decoder = new TextDecoder();
let currentSuite = "Tests";
@@ -46,7 +49,11 @@ async function convertTAPToTeamCity() {
if (line.startsWith("# ")) {
// Suite/describe block
const suiteName = line.substring(2).trim();
if (suiteName && !suiteName.startsWith("tests") && !suiteName.startsWith("pass")) {
if (
suiteName &&
!suiteName.startsWith("tests") &&
!suiteName.startsWith("pass")
) {
if (testCount > 0) {
TeamcityReporter.reportSuiteEnd(currentSuite);
}
@@ -67,12 +74,11 @@ async function convertTAPToTeamCity() {
if (not) {
// Test failed
TeamcityReporter.reportTestFailed(
fullName,
"Test failed",
line,
{ comparisonFailure: false, expected: "", actual: "" }
);
TeamcityReporter.reportTestFailed(fullName, "Test failed", line, {
comparisonFailure: false,
expected: "",
actual: "",
});
}
TeamcityReporter.reportTestEnd(fullName);
@@ -96,4 +102,3 @@ convertTAPToTeamCity().catch((error) => {
console.error("Error:", error);
process.exit(1);
});

View File

@@ -6,6 +6,7 @@ const isTeamCity = process.env.TEAMCITY_VERSION !== undefined;
// Strip ANSI color codes
function stripAnsi(str: string): string {
// eslint-disable-next-line no-control-regex
return str.replace(/\x1b\[[0-9;]*m/g, "");
}
@@ -46,7 +47,10 @@ async function runTests() {
// Check for test suite/describe blocks (files ending with .test.ts)
if (line.match(/tests\/.+\.test\.ts:$/)) {
const fileName = line.replace(":", "").replace("tests/", "").replace(".test.ts", "");
const fileName = line
.replace(":", "")
.replace("tests/", "")
.replace(".test.ts", "");
// End previous suite if exists
while (suiteStack.length > 0) {
@@ -73,12 +77,11 @@ async function runTests() {
if (failMatch) {
const testName = failMatch[1].trim();
TeamcityReporter.reportTestStart(testName);
TeamcityReporter.reportTestFailed(
testName,
"Test failed",
line,
{ comparisonFailure: false, expected: "", actual: "" }
);
TeamcityReporter.reportTestFailed(testName, "Test failed", line, {
comparisonFailure: false,
expected: "",
actual: "",
});
TeamcityReporter.reportTestEnd(testName);
}
}

View File

@@ -1,7 +1,7 @@
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";
import type { IHashMap } from "../interfaces/IHashMap";
import type { IHashFunction } from "../interfaces/IHashFunction";
import { HashNode } from "../models/HashNode";
import { DefaultHashFunction } from "../hash-functions/DefaultHashFunction";
/**
* HashMap implementation using separate chaining for collision resolution.

View File

@@ -1,4 +1,4 @@
import type { IHashFunction } from "../interfaces/IHashFunction.ts";
import type { IHashFunction } from "../interfaces/IHashFunction";
/**
* Default hash function implementation.

View File

@@ -1,4 +1,4 @@
import type { IHashFunction } from "../interfaces/IHashFunction.ts";
import type { IHashFunction } from "../interfaces/IHashFunction";
/**
* Specialized hash function for numeric keys.

View File

@@ -19,15 +19,15 @@
*/
// Core implementation
export { HashMap } from "./core/HashMap.ts";
export { HashMap } from "./core/HashMap";
// Interfaces
export type { IHashMap } from "./interfaces/IHashMap.ts";
export type { IHashFunction } from "./interfaces/IHashFunction.ts";
export type { IHashMap } from "./interfaces/IHashMap";
export type { IHashFunction } from "./interfaces/IHashFunction";
// Models
export { HashNode } from "./models/HashNode.ts";
export { HashNode } from "./models/HashNode";
// Hash functions
export { DefaultHashFunction } from "./hash-functions/DefaultHashFunction.ts";
export { NumericHashFunction } from "./hash-functions/NumericHashFunction.ts";
export { DefaultHashFunction } from "./hash-functions/DefaultHashFunction";
export { NumericHashFunction } from "./hash-functions/NumericHashFunction";

View File

@@ -76,9 +76,9 @@ describe("DefaultHashFunction", () => {
name: "Bob",
address: {
city: "NYC",
zip: "10001"
}
}
zip: "10001",
},
},
};
const hash = hashFn.hash(nested, capacity);
@@ -321,4 +321,3 @@ describe("NumericHashFunction", () => {
});
});
});

View File

@@ -253,7 +253,7 @@ describe("HashMap", () => {
const numMap = new HashMap<number, string>(
16,
0.75,
new NumericHashFunction()
new NumericHashFunction(),
);
numMap.set(123, "value1");
@@ -273,7 +273,7 @@ describe("HashMap", () => {
const customMap = new HashMap<string, string>(
8,
0.75,
new SimpleHashFunction()
new SimpleHashFunction(),
);
customMap.set("hi", "short");
@@ -359,4 +359,3 @@ describe("HashMap", () => {
});
});
});

View File

@@ -1,60 +1,79 @@
export class TeamcityReporter {
/**
* Escape special characters for TeamCity service messages
* https://www.jetbrains.com/help/teamcity/service-messages.html#Escaped+values
*/
private static escape(str: string): string {
return str
.replace(/\|/g, "||")
.replace(/'/g, "|'")
.replace(/\n/g, "|n")
.replace(/\r/g, "|r")
.replace(/\[/g, "|[")
.replace(/]/g, "|]");
/**
* Escape special characters for TeamCity service messages
* https://www.jetbrains.com/help/teamcity/service-messages.html#Escaped+values
*/
private static escape(str: string): string {
return str
.replace(/\|/g, "||")
.replace(/'/g, "|'")
.replace(/\n/g, "|n")
.replace(/\r/g, "|r")
.replace(/\[/g, "|[")
.replace(/]/g, "|]");
}
public static reportSuiteStart(suiteName: string): void {
console.log(
`##teamcity[testSuiteStarted name='${this.escape(suiteName)}']`,
);
}
public static reportSuiteEnd(suiteName: string): void {
console.log(
`##teamcity[testSuiteFinished name='${this.escape(suiteName)}']`,
);
}
public static reportTestStart(testName: string): void {
console.log(`##teamcity[testStarted name='${this.escape(testName)}']`);
}
public static reportTestFailed(
testName: string,
failureMessage: string,
details: string,
{
comparisonFailure,
expected,
actual,
}: { comparisonFailure: boolean; expected: string; actual: string },
): void {
const attrs = [
`name='${this.escape(testName)}'`,
`message='${this.escape(failureMessage)}'`,
`details='${this.escape(details)}'`,
];
if (comparisonFailure) {
attrs.push(`type='comparisonFailure'`);
attrs.push(`expected='${this.escape(expected)}'`);
attrs.push(`actual='${this.escape(actual)}'`);
}
public static reportSuiteStart(suiteName: string): void {
console.log(`##teamcity[testSuiteStarted name='${this.escape(suiteName)}']`);
}
console.log(`##teamcity[testFailed ${attrs.join(" ")}]`);
}
public static reportSuiteEnd(suiteName: string): void {
console.log(`##teamcity[testSuiteFinished name='${this.escape(suiteName)}']`);
}
public static reportTestEnd(testName: string, duration?: number): void {
const durationAttr =
duration !== undefined ? ` duration='${duration}'` : "";
console.log(
`##teamcity[testFinished name='${this.escape(testName)}'${durationAttr}]`,
);
}
public static reportTestStart(testName: string): void {
console.log(`##teamcity[testStarted name='${this.escape(testName)}']`);
}
public static reportTestError(testName: string, error: Error): void {
const message = this.escape(error.message || "Unknown error");
const details = this.escape(error.stack || "");
console.log(
`##teamcity[testFailed name='${this.escape(testName)}' message='${message}' details='${details}']`,
);
}
public static reportTestFailed(testName: string, failureMessage: string, details: string, {comparisonFailure, expected, actual}: {comparisonFailure: boolean, expected: string, actual: string}): void {
const attrs = [
`name='${this.escape(testName)}'`,
`message='${this.escape(failureMessage)}'`,
`details='${this.escape(details)}'`
];
if (comparisonFailure) {
attrs.push(`type='comparisonFailure'`);
attrs.push(`expected='${this.escape(expected)}'`);
attrs.push(`actual='${this.escape(actual)}'`);
}
console.log(`##teamcity[testFailed ${attrs.join(' ')}]`);
}
public static reportTestEnd(testName: string, duration?: number): void {
const durationAttr = duration !== undefined ? ` duration='${duration}'` : '';
console.log(`##teamcity[testFinished name='${this.escape(testName)}'${durationAttr}]`);
}
public static reportTestError(testName: string, error: Error): void {
const message = this.escape(error.message || 'Unknown error');
const details = this.escape(error.stack || '');
console.log(`##teamcity[testFailed name='${this.escape(testName)}' message='${message}' details='${details}']`);
}
public static reportTestIgnored(testName: string, message?: string): void {
const msgAttr = message ? ` message='${this.escape(message)}'` : '';
console.log(`##teamcity[testIgnored name='${this.escape(testName)}'${msgAttr}]`);
}
public static reportTestIgnored(testName: string, message?: string): void {
const msgAttr = message ? ` message='${this.escape(message)}'` : "";
console.log(
`##teamcity[testIgnored name='${this.escape(testName)}'${msgAttr}]`,
);
}
}

View File

@@ -39,7 +39,7 @@ export function setupTeamCityReporting() {
name: string,
message: string,
details: string,
comparison?: { expected: string; actual: string }
comparison?: { expected: string; actual: string },
) => {
TeamcityReporter.reportTestFailed(name, message, details, {
comparisonFailure: !!comparison,
@@ -52,4 +52,3 @@ export function setupTeamCityReporting() {
},
};
}

View File

@@ -6,8 +6,8 @@
"moduleResolution": "bundler",
"esModuleInterop": true,
"allowImportingTsExtensions": true,
"allowArbitraryExtensions": true,
"allowImportingTsExtensions": false,
"allowArbitraryExtensions": false,
"isolatedModules": true,
"declaration": true,
"emitDeclarationOnly": true,
@@ -22,5 +22,5 @@
"noPropertyAccessFromIndexSignature": false
},
"include": ["src"],
"exclude": ["dist", "test"]
"exclude": ["dist", "test", "src/examples"]
}