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:
44
README.md
44
README.md
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
]);
|
||||
|
||||
22
package.json
22
package.json
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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";
|
||||
@@ -42,11 +45,15 @@ async function convertTAPToTeamCity() {
|
||||
// ok 1 - test description
|
||||
// not ok 2 - failed test
|
||||
// # describe block
|
||||
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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, "");
|
||||
}
|
||||
|
||||
@@ -32,7 +33,7 @@ async function runTests() {
|
||||
});
|
||||
|
||||
const suiteStack: string[] = [];
|
||||
|
||||
|
||||
// Read stdout as text
|
||||
const output = await new Response(proc.stdout).text();
|
||||
const lines = output.split("\n");
|
||||
@@ -46,8 +47,11 @@ 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) {
|
||||
const oldSuite = suiteStack.pop();
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IHashFunction } from "../interfaces/IHashFunction.ts";
|
||||
import type { IHashFunction } from "../interfaces/IHashFunction";
|
||||
|
||||
/**
|
||||
* Default hash function implementation.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IHashFunction } from "../interfaces/IHashFunction.ts";
|
||||
import type { IHashFunction } from "../interfaces/IHashFunction";
|
||||
|
||||
/**
|
||||
* Specialized hash function for numeric keys.
|
||||
|
||||
12
src/index.ts
12
src/index.ts
@@ -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";
|
||||
|
||||
@@ -10,7 +10,7 @@ describe("DefaultHashFunction", () => {
|
||||
it("should hash string keys", () => {
|
||||
const hash1 = hashFn.hash("hello", capacity);
|
||||
const hash2 = hashFn.hash("world", capacity);
|
||||
|
||||
|
||||
expect(hash1).toBeGreaterThanOrEqual(0);
|
||||
expect(hash1).toBeLessThan(capacity);
|
||||
expect(hash2).toBeGreaterThanOrEqual(0);
|
||||
@@ -22,7 +22,7 @@ describe("DefaultHashFunction", () => {
|
||||
const hash2 = hashFn.hash(3.14, capacity);
|
||||
const hash3 = hashFn.hash(0, capacity);
|
||||
const hash4 = hashFn.hash(-10, capacity);
|
||||
|
||||
|
||||
expect(hash1).toBeGreaterThanOrEqual(0);
|
||||
expect(hash1).toBeLessThan(capacity);
|
||||
expect(hash2).toBeGreaterThanOrEqual(0);
|
||||
@@ -33,7 +33,7 @@ describe("DefaultHashFunction", () => {
|
||||
it("should hash boolean keys", () => {
|
||||
const hashTrue = hashFn.hash(true, capacity);
|
||||
const hashFalse = hashFn.hash(false, capacity);
|
||||
|
||||
|
||||
expect(hashTrue).toBeGreaterThanOrEqual(0);
|
||||
expect(hashTrue).toBeLessThan(capacity);
|
||||
expect(hashFalse).toBeGreaterThanOrEqual(0);
|
||||
@@ -57,7 +57,7 @@ describe("DefaultHashFunction", () => {
|
||||
it("should hash simple objects", () => {
|
||||
const obj = { name: "Alice", age: 30 };
|
||||
const hash = hashFn.hash(obj, capacity);
|
||||
|
||||
|
||||
expect(hash).toBeGreaterThanOrEqual(0);
|
||||
expect(hash).toBeLessThan(capacity);
|
||||
});
|
||||
@@ -65,7 +65,7 @@ describe("DefaultHashFunction", () => {
|
||||
it("should hash arrays", () => {
|
||||
const arr = [1, 2, 3, 4, 5];
|
||||
const hash = hashFn.hash(arr, capacity);
|
||||
|
||||
|
||||
expect(hash).toBeGreaterThanOrEqual(0);
|
||||
expect(hash).toBeLessThan(capacity);
|
||||
});
|
||||
@@ -76,12 +76,12 @@ describe("DefaultHashFunction", () => {
|
||||
name: "Bob",
|
||||
address: {
|
||||
city: "NYC",
|
||||
zip: "10001"
|
||||
}
|
||||
}
|
||||
zip: "10001",
|
||||
},
|
||||
},
|
||||
};
|
||||
const hash = hashFn.hash(nested, capacity);
|
||||
|
||||
|
||||
expect(hash).toBeGreaterThanOrEqual(0);
|
||||
expect(hash).toBeLessThan(capacity);
|
||||
});
|
||||
@@ -89,7 +89,7 @@ describe("DefaultHashFunction", () => {
|
||||
it("should handle circular references gracefully", () => {
|
||||
const circular: { name: string; self?: unknown } = { name: "test" };
|
||||
circular.self = circular; // Create circular reference
|
||||
|
||||
|
||||
// Should not throw, should fall back to Object.prototype.toString
|
||||
const hash = hashFn.hash(circular, capacity);
|
||||
expect(hash).toBeGreaterThanOrEqual(0);
|
||||
@@ -99,7 +99,7 @@ describe("DefaultHashFunction", () => {
|
||||
it("should hash Date objects", () => {
|
||||
const date = new Date("2024-01-01");
|
||||
const hash = hashFn.hash(date, capacity);
|
||||
|
||||
|
||||
expect(hash).toBeGreaterThanOrEqual(0);
|
||||
expect(hash).toBeLessThan(capacity);
|
||||
});
|
||||
@@ -107,7 +107,7 @@ describe("DefaultHashFunction", () => {
|
||||
it("should hash RegExp objects", () => {
|
||||
const regex = /test/g;
|
||||
const hash = hashFn.hash(regex, capacity);
|
||||
|
||||
|
||||
expect(hash).toBeGreaterThanOrEqual(0);
|
||||
expect(hash).toBeLessThan(capacity);
|
||||
});
|
||||
@@ -115,7 +115,7 @@ describe("DefaultHashFunction", () => {
|
||||
it("should hash Error objects", () => {
|
||||
const error = new Error("Test error");
|
||||
const hash = hashFn.hash(error, capacity);
|
||||
|
||||
|
||||
expect(hash).toBeGreaterThanOrEqual(0);
|
||||
expect(hash).toBeLessThan(capacity);
|
||||
});
|
||||
@@ -143,7 +143,7 @@ describe("DefaultHashFunction", () => {
|
||||
it("should hash symbols", () => {
|
||||
const sym = Symbol("test");
|
||||
const hash = hashFn.hash(sym, capacity);
|
||||
|
||||
|
||||
expect(hash).toBeGreaterThanOrEqual(0);
|
||||
expect(hash).toBeLessThan(capacity);
|
||||
});
|
||||
@@ -151,7 +151,7 @@ describe("DefaultHashFunction", () => {
|
||||
it("should hash bigint", () => {
|
||||
const bigInt = BigInt(12345678901234567890n);
|
||||
const hash = hashFn.hash(bigInt, capacity);
|
||||
|
||||
|
||||
expect(hash).toBeGreaterThanOrEqual(0);
|
||||
expect(hash).toBeLessThan(capacity);
|
||||
});
|
||||
@@ -162,14 +162,14 @@ describe("DefaultHashFunction", () => {
|
||||
const key = "test-key";
|
||||
const hash1 = hashFn.hash(key, capacity);
|
||||
const hash2 = hashFn.hash(key, capacity);
|
||||
|
||||
|
||||
expect(hash1).toBe(hash2);
|
||||
});
|
||||
|
||||
it("should return different hashes for different keys (usually)", () => {
|
||||
const hash1 = hashFn.hash("key1", capacity);
|
||||
const hash2 = hashFn.hash("key2", capacity);
|
||||
|
||||
|
||||
// Note: They COULD collide, but unlikely
|
||||
expect(hash1).toBeGreaterThanOrEqual(0);
|
||||
expect(hash2).toBeGreaterThanOrEqual(0);
|
||||
@@ -180,7 +180,7 @@ describe("DefaultHashFunction", () => {
|
||||
const hash8 = hashFn.hash(key, 8);
|
||||
const hash16 = hashFn.hash(key, 16);
|
||||
const hash32 = hashFn.hash(key, 32);
|
||||
|
||||
|
||||
expect(hash8).toBeLessThan(8);
|
||||
expect(hash16).toBeLessThan(16);
|
||||
expect(hash32).toBeLessThan(32);
|
||||
@@ -196,7 +196,7 @@ describe("NumericHashFunction", () => {
|
||||
it("should hash positive integers", () => {
|
||||
const hash1 = hashFn.hash(42, capacity);
|
||||
const hash2 = hashFn.hash(100, capacity);
|
||||
|
||||
|
||||
expect(hash1).toBeGreaterThanOrEqual(0);
|
||||
expect(hash1).toBeLessThan(capacity);
|
||||
expect(hash2).toBeGreaterThanOrEqual(0);
|
||||
@@ -205,14 +205,14 @@ describe("NumericHashFunction", () => {
|
||||
|
||||
it("should hash negative integers", () => {
|
||||
const hash = hashFn.hash(-42, capacity);
|
||||
|
||||
|
||||
expect(hash).toBeGreaterThanOrEqual(0);
|
||||
expect(hash).toBeLessThan(capacity);
|
||||
});
|
||||
|
||||
it("should hash zero", () => {
|
||||
const hash = hashFn.hash(0, capacity);
|
||||
|
||||
|
||||
expect(hash).toBeGreaterThanOrEqual(0);
|
||||
expect(hash).toBeLessThan(capacity);
|
||||
});
|
||||
@@ -220,7 +220,7 @@ describe("NumericHashFunction", () => {
|
||||
it("should hash floating point numbers", () => {
|
||||
const hash1 = hashFn.hash(3.14159, capacity);
|
||||
const hash2 = hashFn.hash(2.71828, capacity);
|
||||
|
||||
|
||||
expect(hash1).toBeGreaterThanOrEqual(0);
|
||||
expect(hash1).toBeLessThan(capacity);
|
||||
expect(hash2).toBeGreaterThanOrEqual(0);
|
||||
@@ -229,14 +229,14 @@ describe("NumericHashFunction", () => {
|
||||
|
||||
it("should hash very large numbers", () => {
|
||||
const hash = hashFn.hash(Number.MAX_SAFE_INTEGER, capacity);
|
||||
|
||||
|
||||
expect(hash).toBeGreaterThanOrEqual(0);
|
||||
expect(hash).toBeLessThan(capacity);
|
||||
});
|
||||
|
||||
it("should hash very small numbers", () => {
|
||||
const hash = hashFn.hash(Number.MIN_VALUE, capacity);
|
||||
|
||||
|
||||
expect(hash).toBeGreaterThanOrEqual(0);
|
||||
expect(hash).toBeLessThan(capacity);
|
||||
});
|
||||
@@ -245,21 +245,21 @@ describe("NumericHashFunction", () => {
|
||||
describe("special numeric values", () => {
|
||||
it("should handle Infinity", () => {
|
||||
const hash = hashFn.hash(Infinity, capacity);
|
||||
|
||||
|
||||
// Should return 0 for non-finite numbers
|
||||
expect(hash).toBe(0);
|
||||
});
|
||||
|
||||
it("should handle negative Infinity", () => {
|
||||
const hash = hashFn.hash(-Infinity, capacity);
|
||||
|
||||
|
||||
// Should return 0 for non-finite numbers
|
||||
expect(hash).toBe(0);
|
||||
});
|
||||
|
||||
it("should handle NaN", () => {
|
||||
const hash = hashFn.hash(NaN, capacity);
|
||||
|
||||
|
||||
// Should return 0 for non-finite numbers
|
||||
expect(hash).toBe(0);
|
||||
});
|
||||
@@ -270,7 +270,7 @@ describe("NumericHashFunction", () => {
|
||||
const num = 42;
|
||||
const hash1 = hashFn.hash(num, capacity);
|
||||
const hash2 = hashFn.hash(num, capacity);
|
||||
|
||||
|
||||
expect(hash1).toBe(hash2);
|
||||
});
|
||||
|
||||
@@ -279,7 +279,7 @@ describe("NumericHashFunction", () => {
|
||||
const hash8 = hashFn.hash(num, 8);
|
||||
const hash16 = hashFn.hash(num, 16);
|
||||
const hash32 = hashFn.hash(num, 32);
|
||||
|
||||
|
||||
expect(hash8).toBeLessThan(8);
|
||||
expect(hash16).toBeLessThan(16);
|
||||
expect(hash32).toBeLessThan(32);
|
||||
@@ -287,12 +287,12 @@ describe("NumericHashFunction", () => {
|
||||
|
||||
it("should distribute numbers evenly", () => {
|
||||
const hashes = new Set<number>();
|
||||
|
||||
|
||||
// Hash 100 sequential numbers
|
||||
for (let i = 0; i < 100; i++) {
|
||||
hashes.add(hashFn.hash(i, capacity));
|
||||
}
|
||||
|
||||
|
||||
// Should have good distribution (not all in one bucket)
|
||||
expect(hashes.size).toBeGreaterThan(1);
|
||||
});
|
||||
@@ -303,7 +303,7 @@ describe("NumericHashFunction", () => {
|
||||
const hash1 = hashFn.hash(-1, capacity);
|
||||
const hash2 = hashFn.hash(-100, capacity);
|
||||
const hash3 = hashFn.hash(-3.14, capacity);
|
||||
|
||||
|
||||
expect(hash1).toBeGreaterThanOrEqual(0);
|
||||
expect(hash1).toBeLessThan(capacity);
|
||||
expect(hash2).toBeGreaterThanOrEqual(0);
|
||||
@@ -315,10 +315,9 @@ describe("NumericHashFunction", () => {
|
||||
it("should handle same absolute value differently for positive and negative", () => {
|
||||
const hashPos = hashFn.hash(42, capacity);
|
||||
const hashNeg = hashFn.hash(-42, capacity);
|
||||
|
||||
|
||||
// They should hash to the same bucket (due to Math.abs in implementation)
|
||||
expect(hashPos).toBe(hashNeg);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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}]`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user