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

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

View File

@@ -8,11 +8,31 @@ import { defineConfig } from "eslint/config";
export default defineConfig([ 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, tseslint.configs.recommended,
{ files: ["**/*.json"], plugins: { json }, language: "json/json", extends: ["json/recommended"] }, {
{ files: ["**/*.md"], plugins: { markdown }, language: "markdown/commonmark", extends: ["markdown/recommended"] }, files: ["**/*.json"],
{ files: ["**/*.css"], plugins: { css }, language: "css/css", extends: ["css/recommended"] }, 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", "name": "@techniker-me/hash-map",
"version": "1.0.8", "version": "1.0.9",
"description": "A robust HashMap implementation following OOP SOLID principles", "description": "A robust HashMap implementation following OOP SOLID principles",
"type": "module", "type": "module",
"main": "browser/index.ts", "main": "./node/index.js",
"module": "node/index.js", "module": "./browser/index.js",
"types": "types/index.d.ts", "types": "./types/index.d.ts",
"exports": { "exports": {
".": { ".": {
"node": "node/index.js", "types": "./types/index.d.ts",
"browser": "browser/index.js" "node": {
"import": "./node/index.js",
"default": "./node/index.js"
}, },
"./types": "types/index.d.ts" "browser": {
"import": "./browser/index.js",
"default": "./browser/index.js"
},
"default": "./node/index.js"
},
"./types": "./types/index.d.ts"
}, },
"files": [ "files": [
"node", "node",

View File

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

View File

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

View File

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

View File

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

View File

@@ -76,9 +76,9 @@ describe("DefaultHashFunction", () => {
name: "Bob", name: "Bob",
address: { address: {
city: "NYC", city: "NYC",
zip: "10001" zip: "10001",
} },
} },
}; };
const hash = hashFn.hash(nested, capacity); 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>( const numMap = new HashMap<number, string>(
16, 16,
0.75, 0.75,
new NumericHashFunction() new NumericHashFunction(),
); );
numMap.set(123, "value1"); numMap.set(123, "value1");
@@ -273,7 +273,7 @@ describe("HashMap", () => {
const customMap = new HashMap<string, string>( const customMap = new HashMap<string, string>(
8, 8,
0.75, 0.75,
new SimpleHashFunction() new SimpleHashFunction(),
); );
customMap.set("hi", "short"); customMap.set("hi", "short");
@@ -359,4 +359,3 @@ describe("HashMap", () => {
}); });
}); });
}); });

View File

@@ -1,4 +1,3 @@
export class TeamcityReporter { export class TeamcityReporter {
/** /**
* Escape special characters for TeamCity service messages * Escape special characters for TeamCity service messages
@@ -15,22 +14,35 @@ export class TeamcityReporter {
} }
public static reportSuiteStart(suiteName: string): void { public static reportSuiteStart(suiteName: string): void {
console.log(`##teamcity[testSuiteStarted name='${this.escape(suiteName)}']`); console.log(
`##teamcity[testSuiteStarted name='${this.escape(suiteName)}']`,
);
} }
public static reportSuiteEnd(suiteName: string): void { public static reportSuiteEnd(suiteName: string): void {
console.log(`##teamcity[testSuiteFinished name='${this.escape(suiteName)}']`); console.log(
`##teamcity[testSuiteFinished name='${this.escape(suiteName)}']`,
);
} }
public static reportTestStart(testName: string): void { public static reportTestStart(testName: string): void {
console.log(`##teamcity[testStarted name='${this.escape(testName)}']`); 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 { public static reportTestFailed(
testName: string,
failureMessage: string,
details: string,
{
comparisonFailure,
expected,
actual,
}: { comparisonFailure: boolean; expected: string; actual: string },
): void {
const attrs = [ const attrs = [
`name='${this.escape(testName)}'`, `name='${this.escape(testName)}'`,
`message='${this.escape(failureMessage)}'`, `message='${this.escape(failureMessage)}'`,
`details='${this.escape(details)}'` `details='${this.escape(details)}'`,
]; ];
if (comparisonFailure) { if (comparisonFailure) {
@@ -39,22 +51,29 @@ export class TeamcityReporter {
attrs.push(`actual='${this.escape(actual)}'`); attrs.push(`actual='${this.escape(actual)}'`);
} }
console.log(`##teamcity[testFailed ${attrs.join(' ')}]`); console.log(`##teamcity[testFailed ${attrs.join(" ")}]`);
} }
public static reportTestEnd(testName: string, duration?: number): void { public static reportTestEnd(testName: string, duration?: number): void {
const durationAttr = duration !== undefined ? ` duration='${duration}'` : ''; const durationAttr =
console.log(`##teamcity[testFinished name='${this.escape(testName)}'${durationAttr}]`); duration !== undefined ? ` duration='${duration}'` : "";
console.log(
`##teamcity[testFinished name='${this.escape(testName)}'${durationAttr}]`,
);
} }
public static reportTestError(testName: string, error: Error): void { public static reportTestError(testName: string, error: Error): void {
const message = this.escape(error.message || 'Unknown error'); const message = this.escape(error.message || "Unknown error");
const details = this.escape(error.stack || ''); const details = this.escape(error.stack || "");
console.log(`##teamcity[testFailed name='${this.escape(testName)}' message='${message}' details='${details}']`); console.log(
`##teamcity[testFailed name='${this.escape(testName)}' message='${message}' details='${details}']`,
);
} }
public static reportTestIgnored(testName: string, message?: string): void { public static reportTestIgnored(testName: string, message?: string): void {
const msgAttr = message ? ` message='${this.escape(message)}'` : ''; const msgAttr = message ? ` message='${this.escape(message)}'` : "";
console.log(`##teamcity[testIgnored name='${this.escape(testName)}'${msgAttr}]`); console.log(
`##teamcity[testIgnored name='${this.escape(testName)}'${msgAttr}]`,
);
} }
} }

View File

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

View File

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