mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 02:44:45 +00:00
feat(monorepo): migrate vue packages and apply oxlint refactors
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { defineConfig } from 'oxlint';
|
||||
import { compose, base, typescript, imports } from '@robonen/oxlint';
|
||||
import { compose, base, typescript, imports, stylistic } from '@robonen/oxlint';
|
||||
|
||||
export default defineConfig(compose(base, typescript, imports));
|
||||
export default defineConfig(compose(base, typescript, imports, stylistic));
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"url": "git+https://github.com/robonen/tools.git",
|
||||
"directory": "packages/stdlib"
|
||||
},
|
||||
"packageManager": "pnpm@10.29.3",
|
||||
"packageManager": "pnpm@10.30.3",
|
||||
"engines": {
|
||||
"node": ">=24.13.1"
|
||||
},
|
||||
@@ -34,7 +34,8 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "oxlint -c oxlint.config.ts",
|
||||
"lint:check": "oxlint -c oxlint.config.ts",
|
||||
"lint:fix": "oxlint -c oxlint.config.ts --fix",
|
||||
"test": "vitest run",
|
||||
"dev": "vitest dev",
|
||||
"build": "tsdown"
|
||||
@@ -43,6 +44,7 @@
|
||||
"@robonen/oxlint": "workspace:*",
|
||||
"@robonen/tsconfig": "workspace:*",
|
||||
"@robonen/tsdown": "workspace:*",
|
||||
"@stylistic/eslint-plugin": "catalog:",
|
||||
"oxlint": "catalog:",
|
||||
"tsdown": "catalog:"
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('cluster', () => {
|
||||
|
||||
it('return an empty array if the input array is empty', () => {
|
||||
const result = cluster([], 3);
|
||||
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
* @name cluster
|
||||
* @category Arrays
|
||||
* @description Cluster an array into subarrays of a specific size
|
||||
*
|
||||
*
|
||||
* @param {Value[]} arr The array to cluster
|
||||
* @param {number} size The size of each cluster
|
||||
* @returns {Value[][]} The clustered array
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* cluster([1, 2, 3, 4, 5, 6, 7, 8], 3) // => [[1, 2, 3], [4, 5, 6], [7, 8]]
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* cluster([1, 2, 3, 4], -1) // => []
|
||||
*
|
||||
*
|
||||
* @since 0.0.3
|
||||
*/
|
||||
export function cluster<Value>(arr: Value[], size: number): Value[][] {
|
||||
|
||||
@@ -20,4 +20,4 @@ describe('first', () => {
|
||||
expect(first([1, 2, 3], 42)).toBe(1);
|
||||
expect(first(['a', 'b', 'c'], 'default')).toBe('a');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
* @name first
|
||||
* @category Arrays
|
||||
* @description Returns the first element of an array
|
||||
*
|
||||
*
|
||||
* @param {Value[]} arr The array to get the first element from
|
||||
* @param {Value} [defaultValue] The default value to return if the array is empty
|
||||
* @returns {Value | undefined} The first element of the array, or the default value if the array is empty
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* first([1, 2, 3]); // => 1
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* first([]); // => undefined
|
||||
*
|
||||
*
|
||||
* @since 0.0.3
|
||||
*/
|
||||
export function first<Value>(arr: Value[], defaultValue?: Value) {
|
||||
return arr[0] ?? defaultValue;
|
||||
}
|
||||
return arr[0] ?? defaultValue;
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ export * from './cluster';
|
||||
export * from './first';
|
||||
export * from './last';
|
||||
export * from './sum';
|
||||
export * from './unique';
|
||||
export * from './unique';
|
||||
|
||||
@@ -20,4 +20,4 @@ describe('last', () => {
|
||||
expect(last([1, 2, 3], 42)).toBe(3);
|
||||
expect(last(['a', 'b', 'c'], 'default')).toBe('c');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
* @name last
|
||||
* @section Arrays
|
||||
* @description Gets the last element of an array
|
||||
*
|
||||
*
|
||||
* @param {Value[]} arr The array to get the last element of
|
||||
* @param {Value} [defaultValue] The default value to return if the array is empty
|
||||
* @returns {Value | undefined} The last element of the array, or the default value if the array is empty
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* last([1, 2, 3, 4, 5]); // => 5
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* last([], 3); // => 3
|
||||
*
|
||||
*
|
||||
* @since 0.0.3
|
||||
*/
|
||||
export function last<Value>(arr: Value[], defaultValue?: Value) {
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('sum', () => {
|
||||
});
|
||||
|
||||
it('return the sum of all elements using a getValue function', () => {
|
||||
const result = sum([{ value: 1 }, { value: 2 }, { value: 3 }], (item) => item.value);
|
||||
const result = sum([{ value: 1 }, { value: 2 }, { value: 3 }], item => item.value);
|
||||
|
||||
expect(result).toBe(6);
|
||||
});
|
||||
@@ -39,8 +39,8 @@ describe('sum', () => {
|
||||
});
|
||||
|
||||
it('handle arrays with a getValue function returning floating point numbers', () => {
|
||||
const result = sum([{ value: 1.5 }, { value: 2.5 }, { value: 3.5 }], (item) => item.value);
|
||||
|
||||
const result = sum([{ value: 1.5 }, { value: 2.5 }, { value: 3.5 }], item => item.value);
|
||||
|
||||
expect(result).toBe(7.5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
* @name sum
|
||||
* @category Arrays
|
||||
* @description Returns the sum of all the elements in an array
|
||||
*
|
||||
*
|
||||
* @param {Value[]} array - The array to sum
|
||||
* @param {(item: Value) => number} [getValue] - A function that returns the value to sum from each element in the array
|
||||
* @returns {number} The sum of all the elements in the array
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* sum([1, 2, 3, 4, 5]) // => 15
|
||||
*
|
||||
*
|
||||
* sum([{ value: 1 }, { value: 2 }, { value: 3 }], (item) => item.value) // => 6
|
||||
*
|
||||
*
|
||||
* @since 0.0.3
|
||||
*/
|
||||
export function sum<Value extends number>(array: Value[]): number;
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('unique', () => {
|
||||
it('return an array with unique objects based on id', () => {
|
||||
const result = unique(
|
||||
[{ id: 1 }, { id: 2 }, { id: 1 }],
|
||||
(item) => item.id,
|
||||
item => item.id,
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ id: 1 }, { id: 2 }]);
|
||||
@@ -33,7 +33,7 @@ describe('unique', () => {
|
||||
const sym1 = Symbol('a');
|
||||
const sym2 = Symbol('b');
|
||||
const result = unique([sym1, sym2, sym1]);
|
||||
|
||||
|
||||
expect(result).toEqual([sym1, sym2]);
|
||||
});
|
||||
|
||||
@@ -42,4 +42,4 @@ describe('unique', () => {
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,17 +5,17 @@ export type Extractor<Value, Key extends UniqueKey> = (value: Value) => Key;
|
||||
* @name unique
|
||||
* @category Arrays
|
||||
* @description Returns a new array with unique values from the original array
|
||||
*
|
||||
*
|
||||
* @param {Value[]} array - The array to filter
|
||||
* @param {Function} [extractor] - The function to extract the value to compare
|
||||
* @returns {Value[]} - The new array with unique values
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* unique([1, 2, 3, 3, 4, 5, 5, 6]) //=> [1, 2, 3, 4, 5, 6]
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* unique([{ id: 1 }, { id: 2 }, { id: 1 }], (a, b) => a.id === b.id) //=> [{ id: 1 }, { id: 2 }]
|
||||
*
|
||||
*
|
||||
* @since 0.0.3
|
||||
*/
|
||||
export function unique<Value, Key extends UniqueKey>(
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface AsyncPoolOptions {
|
||||
concurrency?: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,4 +16,4 @@ describe('sleep', () => {
|
||||
|
||||
expect(end - start).toBeGreaterThan(delay - 5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
* @name sleep
|
||||
* @category Async
|
||||
* @description Delays the execution of the current function by the specified amount of time
|
||||
*
|
||||
*
|
||||
* @param {number} ms - The amount of time to delay the execution of the current function
|
||||
* @returns {Promise<void>} - A promise that resolves after the specified amount of time
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* await sleep(1000);
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* sleep(1000).then(() => {
|
||||
* console.log('Hello, World!');
|
||||
* });
|
||||
*
|
||||
*
|
||||
* @since 0.0.3
|
||||
*/
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ describe('tryIt', () => {
|
||||
});
|
||||
|
||||
it('handle synchronous functions with errors', () => {
|
||||
const syncFn = (): void => { throw new Error('Test error') };
|
||||
const syncFn = (): void => {
|
||||
throw new Error('Test error');
|
||||
};
|
||||
const wrappedSyncFn = tryIt(syncFn);
|
||||
|
||||
const [error, result] = wrappedSyncFn();
|
||||
@@ -34,7 +36,9 @@ describe('tryIt', () => {
|
||||
});
|
||||
|
||||
it('handle asynchronous functions with errors', async () => {
|
||||
const asyncFn = async () => { throw new Error('Test error') };
|
||||
const asyncFn = async () => {
|
||||
throw new Error('Test error');
|
||||
};
|
||||
const wrappedAsyncFn = tryIt(asyncFn);
|
||||
|
||||
const [error, result] = await wrappedAsyncFn();
|
||||
@@ -64,4 +68,4 @@ describe('tryIt', () => {
|
||||
expect(error?.message).toBe('Test error');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,17 +8,17 @@ export type TryItReturn<Return> = Return extends Promise<any>
|
||||
* @name tryIt
|
||||
* @category Async
|
||||
* @description Wraps promise-based code in a try/catch block without forking the control flow
|
||||
*
|
||||
*
|
||||
* @param {Function} fn - The function to try
|
||||
* @returns {Function} - The function that will return a tuple with the error and the result
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const wrappedFetch = tryIt(fetch);
|
||||
* const [error, result] = await wrappedFetch('https://jsonplaceholder.typicode.com/todos/1');
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const [error, result] = await tryIt(fetch)('https://jsonplaceholder.typicode.com/todos/1');
|
||||
*
|
||||
*
|
||||
* @since 0.0.3
|
||||
*/
|
||||
export function tryIt<Args extends any[], Return>(
|
||||
@@ -30,11 +30,12 @@ export function tryIt<Args extends any[], Return>(
|
||||
|
||||
if (isPromise(result))
|
||||
return result
|
||||
.then((value) => [undefined, value])
|
||||
.catch((error) => [error, undefined]) as TryItReturn<Return>;
|
||||
.then(value => [undefined, value])
|
||||
.catch(error => [error, undefined]) as TryItReturn<Return>;
|
||||
|
||||
return [undefined, result] as TryItReturn<Return>;
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
return [error, undefined] as TryItReturn<Return>;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
* @name flagsGenerator
|
||||
* @category Bits
|
||||
* @description Create a function that generates unique flags
|
||||
*
|
||||
*
|
||||
* @returns {Function} A function that generates unique flags
|
||||
* @throws {RangeError} If more than 31 flags are created
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function flagsGenerator() {
|
||||
let lastFlag = 0;
|
||||
let lastFlag = 0;
|
||||
|
||||
return () => {
|
||||
// 31 flags is the maximum number of flags that can be created
|
||||
// (without zero) because of the 32-bit integer limit in bitwise operations
|
||||
if (lastFlag & 0x40000000)
|
||||
throw new RangeError('Cannot create more than 31 flags');
|
||||
return () => {
|
||||
// 31 flags is the maximum number of flags that can be created
|
||||
// (without zero) because of the 32-bit integer limit in bitwise operations
|
||||
if (lastFlag & 0x40000000)
|
||||
throw new RangeError('Cannot create more than 31 flags');
|
||||
|
||||
return (lastFlag = lastFlag === 0 ? 1 : lastFlag << 1);
|
||||
};
|
||||
return (lastFlag = lastFlag === 0 ? 1 : lastFlag << 1);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { and, or, not, has, is, unset, toggle } from '.';
|
||||
|
||||
|
||||
describe('flagsAnd', () => {
|
||||
it('no effect on zero flags', () => {
|
||||
const result = and();
|
||||
@@ -14,7 +13,7 @@ describe('flagsAnd', () => {
|
||||
|
||||
expect(result).toBe(0b1010);
|
||||
});
|
||||
|
||||
|
||||
it('perform bitwise AND operation on flags', () => {
|
||||
const result = and(0b1111, 0b1010, 0b1100);
|
||||
|
||||
@@ -30,15 +29,15 @@ describe('flagsOr', () => {
|
||||
});
|
||||
|
||||
it('source flag is returned if no flags are provided', () => {
|
||||
const result = or(0b1010);
|
||||
|
||||
expect(result).toBe(0b1010);
|
||||
const result = or(0b1010);
|
||||
|
||||
expect(result).toBe(0b1010);
|
||||
});
|
||||
|
||||
it('perform bitwise OR operation on flags', () => {
|
||||
const result = or(0b1111, 0b1010, 0b1100);
|
||||
|
||||
expect(result).toBe(0b1111);
|
||||
const result = or(0b1111, 0b1010, 0b1100);
|
||||
|
||||
expect(result).toBe(0b1111);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,9 +57,9 @@ describe('flagsHas', () => {
|
||||
});
|
||||
|
||||
it('check if a flag has a specific bit unset', () => {
|
||||
const result = has(0b1010, 0b0100);
|
||||
|
||||
expect(result).toBe(false);
|
||||
const result = has(0b1010, 0b0100);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,9 +71,9 @@ describe('flagsIs', () => {
|
||||
});
|
||||
|
||||
it('check if a flag is unset', () => {
|
||||
const result = is(0);
|
||||
|
||||
expect(result).toBe(false);
|
||||
const result = is(0);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,4 +91,4 @@ describe('flagsToggle', () => {
|
||||
|
||||
expect(result).toBe(0b0010);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @name and
|
||||
* @category Bits
|
||||
* @description Function to combine multiple flags using the AND operator
|
||||
*
|
||||
*
|
||||
* @param {number[]} flags - The flags to combine
|
||||
* @returns {number} The combined flags
|
||||
*
|
||||
@@ -16,7 +16,7 @@ export function and(...flags: number[]) {
|
||||
* @name or
|
||||
* @category Bits
|
||||
* @description Function to combine multiple flags using the OR operator
|
||||
*
|
||||
*
|
||||
* @param {number[]} flags - The flags to combine
|
||||
* @returns {number} The combined flags
|
||||
*
|
||||
@@ -30,7 +30,7 @@ export function or(...flags: number[]) {
|
||||
* @name not
|
||||
* @category Bits
|
||||
* @description Function to combine multiple flags using the XOR operator
|
||||
*
|
||||
*
|
||||
* @param {number} flag - The flag to apply the NOT operator to
|
||||
* @returns {number} The result of the NOT operator
|
||||
*
|
||||
@@ -44,7 +44,7 @@ export function not(flag: number) {
|
||||
* @name has
|
||||
* @category Bits
|
||||
* @description Function to make sure a flag has a specific bit set
|
||||
*
|
||||
*
|
||||
* @param {number} flag - The flag to check
|
||||
* @param {number} other - Flag to check
|
||||
* @returns {boolean} Whether the flag has the bit set
|
||||
@@ -59,7 +59,7 @@ export function has(flag: number, other: number) {
|
||||
* @name is
|
||||
* @category Bits
|
||||
* @description Function to check if a flag is set
|
||||
*
|
||||
*
|
||||
* @param {number} flag - The flag to check
|
||||
* @returns {boolean} Whether the flag is set
|
||||
*
|
||||
@@ -73,7 +73,7 @@ export function is(flag: number) {
|
||||
* @name unset
|
||||
* @category Bits
|
||||
* @description Function to unset a flag
|
||||
*
|
||||
*
|
||||
* @param {number} flag - Source flag
|
||||
* @param {number} other - Flag to unset
|
||||
* @returns {number} The new flag
|
||||
@@ -88,7 +88,7 @@ export function unset(flag: number, other: number) {
|
||||
* @name toggle
|
||||
* @category Bits
|
||||
* @description Function to toggle (xor) a flag
|
||||
*
|
||||
*
|
||||
* @param {number} flag - Source flag
|
||||
* @param {number} other - Flag to toggle
|
||||
* @returns {number} The new flag
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './flags';
|
||||
export * from './flags';
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('BitVector', () => {
|
||||
it('set and get bits correctly', () => {
|
||||
const bitVector = new BitVector(16);
|
||||
bitVector.setBit(5);
|
||||
|
||||
|
||||
expect(bitVector.getBit(5)).toBe(true);
|
||||
expect(bitVector.getBit(4)).toBe(false);
|
||||
});
|
||||
@@ -40,7 +40,7 @@ describe('BitVector', () => {
|
||||
const indices = [99, 88, 66, 65, 64, 63, 15, 14, 1, 0];
|
||||
const result = [];
|
||||
indices.forEach(index => bitVector.setBit(index));
|
||||
|
||||
|
||||
for (let i = bitVector.previousBit(100); i !== -1; i = bitVector.previousBit(i)) {
|
||||
result.push(i);
|
||||
}
|
||||
@@ -60,4 +60,4 @@ describe('BitVector', () => {
|
||||
|
||||
expect(() => bitVector.previousBit(24)).toThrow(new RangeError('Unreachable value'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface BitVectorLike {
|
||||
* @name BitVector
|
||||
* @category Bits
|
||||
* @description A bit vector is a vector of bits that can be used to store a collection of bits
|
||||
*
|
||||
*
|
||||
* @since 0.0.3
|
||||
*/
|
||||
export class BitVector extends Uint8Array implements BitVectorLike {
|
||||
@@ -58,4 +58,4 @@ export class BitVector extends Uint8Array implements BitVectorLike {
|
||||
|
||||
throw new RangeError('Unreachable value');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { Collection, Path } from '../../types';
|
||||
|
||||
export type ExtractFromObject<O extends Record<PropertyKey, unknown>, K> =
|
||||
K extends keyof O
|
||||
export type ExtractFromObject<O extends Record<PropertyKey, unknown>, K>
|
||||
= K extends keyof O
|
||||
? O[K]
|
||||
: K extends keyof NonNullable<O>
|
||||
? NonNullable<O>[K]
|
||||
: never;
|
||||
|
||||
export type ExtractFromArray<A extends readonly any[], K> =
|
||||
any[] extends A
|
||||
export type ExtractFromArray<A extends readonly any[], K>
|
||||
= any[] extends A
|
||||
? A extends ReadonlyArray<infer T>
|
||||
? T | undefined
|
||||
: undefined
|
||||
@@ -16,19 +16,19 @@ export type ExtractFromArray<A extends readonly any[], K> =
|
||||
? A[K]
|
||||
: undefined;
|
||||
|
||||
export type ExtractFromCollection<O, K> =
|
||||
K extends []
|
||||
? O
|
||||
: K extends [infer Key, ...infer Rest]
|
||||
? O extends Record<PropertyKey, unknown>
|
||||
? ExtractFromCollection<ExtractFromObject<O, Key>, Rest>
|
||||
: O extends readonly any[]
|
||||
? ExtractFromCollection<ExtractFromArray<O, Key>, Rest>
|
||||
: never
|
||||
: never;
|
||||
export type ExtractFromCollection<O, K>
|
||||
= K extends []
|
||||
? O
|
||||
: K extends [infer Key, ...infer Rest]
|
||||
? O extends Record<PropertyKey, unknown>
|
||||
? ExtractFromCollection<ExtractFromObject<O, Key>, Rest>
|
||||
: O extends readonly any[]
|
||||
? ExtractFromCollection<ExtractFromArray<O, Key>, Rest>
|
||||
: never
|
||||
: never;
|
||||
|
||||
type Get<O, K> = ExtractFromCollection<O, Path<K>>;
|
||||
|
||||
export function get<O extends Collection, K extends string>(obj: O, path: K) {
|
||||
return path.split('.').reduce((acc, key) => (acc as any)?.[key], obj) as Get<O, K> | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './get';
|
||||
export * from './get';
|
||||
|
||||
@@ -8,4 +8,4 @@ export * from './structs';
|
||||
export * from './sync';
|
||||
export * from './text';
|
||||
export * from './types';
|
||||
export * from './utils'
|
||||
export * from './utils';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe,it, expect } from 'vitest';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { clamp } from '.';
|
||||
|
||||
describe('clamp', () => {
|
||||
@@ -78,4 +78,4 @@ describe('clamp', () => {
|
||||
// min and max are -Infinity
|
||||
expect(clamp(50, -Infinity, -Infinity)).toBe(-Infinity);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @name clamp
|
||||
* @category Math
|
||||
* @description Clamps a number between a minimum and maximum value
|
||||
*
|
||||
*
|
||||
* @param {number} value The number to clamp
|
||||
* @param {number} min Minimum value
|
||||
* @param {number} max Maximum value
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {describe, it, expect} from 'vitest';
|
||||
import {inverseLerp, lerp} from '.';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { inverseLerp, lerp } from '.';
|
||||
|
||||
describe('lerp', () => {
|
||||
it('interpolates between two values', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {remap} from '.';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { remap } from '.';
|
||||
|
||||
describe('remap', () => {
|
||||
it('map values from one range to another', () => {
|
||||
@@ -43,4 +43,4 @@ describe('remap', () => {
|
||||
// input range is zero (should return output min)
|
||||
expect(remap(5, 0, 0, 0, 100)).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { clamp } from '../clamp';
|
||||
import {inverseLerp, lerp} from '../lerp';
|
||||
import { inverseLerp, lerp } from '../lerp';
|
||||
|
||||
/**
|
||||
* @name remap
|
||||
* @category Math
|
||||
* @description Map a value from one range to another
|
||||
*
|
||||
*
|
||||
* @param {number} value The value to map
|
||||
* @param {number} in_min The minimum value of the input range
|
||||
* @param {number} in_max The maximum value of the input range
|
||||
@@ -22,4 +22,4 @@ export function remap(value: number, in_min: number, in_max: number, out_min: nu
|
||||
const clampedValue = clamp(value, in_min, in_max);
|
||||
|
||||
return lerp(out_min, out_max, inverseLerp(in_min, in_max, clampedValue));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {describe, it, expect} from 'vitest';
|
||||
import {clampBigInt} from '.';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { clampBigInt } from '.';
|
||||
|
||||
describe('clampBigInt', () => {
|
||||
it('clamp a value within the given range', () => {
|
||||
@@ -32,4 +32,4 @@ describe('clampBigInt', () => {
|
||||
// negative range and value
|
||||
expect(clampBigInt(-10n, -100n, -5n)).toBe(-10n);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {minBigInt} from '../minBigInt';
|
||||
import {maxBigInt} from '../maxBigInt';
|
||||
import { minBigInt } from '../minBigInt';
|
||||
import { maxBigInt } from '../maxBigInt';
|
||||
|
||||
/**
|
||||
* @name clampBigInt
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {describe, it, expect} from 'vitest';
|
||||
import {inverseLerpBigInt, lerpBigInt} from '.';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { inverseLerpBigInt, lerpBigInt } from '.';
|
||||
|
||||
const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
|
||||
|
||||
|
||||
@@ -35,4 +35,4 @@ export function lerpBigInt(start: bigint, end: bigint, t: number) {
|
||||
*/
|
||||
export function inverseLerpBigInt(start: bigint, end: bigint, value: bigint) {
|
||||
return start === end ? 0 : Number((value - start) * SCALE_N / (end - start)) / SCALE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,4 +36,4 @@ describe('maxBigInt', () => {
|
||||
const result = maxBigInt(...values);
|
||||
expect(result).toBe(999n);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,4 +14,4 @@ export function maxBigInt(...values: bigint[]) {
|
||||
throw new TypeError('maxBigInt requires at least one argument');
|
||||
|
||||
return values.reduce((acc, val) => val > acc ? val : acc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {describe, it, expect} from 'vitest';
|
||||
import {minBigInt} from '.';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { minBigInt } from '.';
|
||||
|
||||
describe('minBigInt', () => {
|
||||
it('returns Infinity when no values are provided', () => {
|
||||
@@ -32,8 +32,8 @@ describe('minBigInt', () => {
|
||||
});
|
||||
|
||||
it('handles a large number of bigints', () => {
|
||||
const values = Array.from({length: 1000}, (_, i) => BigInt(i));
|
||||
const values = Array.from({ length: 1000 }, (_, i) => BigInt(i));
|
||||
const result = minBigInt(...values);
|
||||
expect(result).toBe(0n);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {remapBigInt} from '.';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { remapBigInt } from '.';
|
||||
|
||||
describe('remapBigInt', () => {
|
||||
it('map values from one range to another', () => {
|
||||
@@ -29,4 +29,4 @@ describe('remapBigInt', () => {
|
||||
// input range is zero (should return output min)
|
||||
expect(remapBigInt(5n, 0n, 0n, 0n, 100n)).toBe(0n);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { clampBigInt } from '../clampBigInt';
|
||||
import {inverseLerpBigInt, lerpBigInt} from '../lerpBigInt';
|
||||
import { inverseLerpBigInt, lerpBigInt } from '../lerpBigInt';
|
||||
|
||||
/**
|
||||
* @name remapBigInt
|
||||
* @category Math
|
||||
* @description Map a bigint value from one range to another
|
||||
*
|
||||
*
|
||||
* @param {bigint} value The value to map
|
||||
* @param {bigint} in_min The minimum value of the input range
|
||||
* @param {bigint} in_max The maximum value of the input range
|
||||
@@ -22,4 +22,4 @@ export function remapBigInt(value: bigint, in_min: bigint, in_max: bigint, out_m
|
||||
const clampedValue = clampBigInt(value, in_min, in_max);
|
||||
|
||||
return lerpBigInt(out_min, out_max, inverseLerpBigInt(in_min, in_max, clampedValue));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,4 +47,4 @@ describe('omit', () => {
|
||||
expect(emptyTarget).toEqual({});
|
||||
expect(emptyKeys).toEqual({ a: 1 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,22 +5,22 @@ import type { Arrayable } from '../../types';
|
||||
* @name omit
|
||||
* @category Objects
|
||||
* @description Returns a new object with the specified keys omitted
|
||||
*
|
||||
*
|
||||
* @param {object} target - The object to omit keys from
|
||||
* @param {Arrayable<keyof Target>} keys - The keys to omit
|
||||
* @returns {Omit<Target, Key>} The new object with the specified keys omitted
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* omit({ a: 1, b: 2, c: 3 }, 'a') // => { b: 2, c: 3 }
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* omit({ a: 1, b: 2, c: 3 }, ['a', 'b']) // => { c: 3 }
|
||||
*
|
||||
*
|
||||
* @since 0.0.3
|
||||
*/
|
||||
export function omit<Target extends object, Key extends keyof Target>(
|
||||
target: Target,
|
||||
keys: Arrayable<Key>
|
||||
keys: Arrayable<Key>,
|
||||
): Omit<Target, Key> {
|
||||
const result = { ...target };
|
||||
|
||||
@@ -31,7 +31,8 @@ export function omit<Target extends object, Key extends keyof Target>(
|
||||
for (const key of keys) {
|
||||
delete result[key];
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
delete result[keys];
|
||||
}
|
||||
|
||||
|
||||
@@ -33,4 +33,4 @@ describe('pick', () => {
|
||||
expect(emptyTarget).toEqual({});
|
||||
expect(emptyKeys).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,22 +5,22 @@ import type { Arrayable } from '../../types';
|
||||
* @name pick
|
||||
* @category Objects
|
||||
* @description Returns a partial copy of an object containing only the keys specified
|
||||
*
|
||||
*
|
||||
* @param {object} target - The object to pick keys from
|
||||
* @param {Arrayable<keyof Target>} keys - The keys to pick
|
||||
* @returns {Pick<Target, Key>} The new object with the specified keys picked
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* pick({ a: 1, b: 2, c: 3 }, 'a') // => { a: 1 }
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* pick({ a: 1, b: 2, c: 3 }, ['a', 'b']) // => { a: 1, b: 2 }
|
||||
*
|
||||
*
|
||||
* @since 0.0.3
|
||||
*/
|
||||
export function pick<Target extends object, Key extends keyof Target>(
|
||||
target: Target,
|
||||
keys: Arrayable<Key>
|
||||
keys: Arrayable<Key>,
|
||||
): Pick<Target, Key> {
|
||||
const result = {} as Pick<Target, Key>;
|
||||
|
||||
@@ -31,9 +31,10 @@ export function pick<Target extends object, Key extends keyof Target>(
|
||||
for (const key of keys) {
|
||||
result[key] = target[key];
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
result[keys] = target[keys];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,11 +205,11 @@ describe('asyncCommandHistory', () => {
|
||||
function addItemAsync(item: string): AsyncCommand {
|
||||
return {
|
||||
execute: async () => {
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
await new Promise(r => setTimeout(r, 5));
|
||||
items.push(item);
|
||||
},
|
||||
undo: async () => {
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
await new Promise(r => setTimeout(r, 5));
|
||||
items.pop();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -40,7 +40,7 @@ export class CommandHistory extends BaseCommandHistory<Command> {
|
||||
|
||||
batch(commands: Command[]): void {
|
||||
const macro: Command = {
|
||||
execute: () => commands.forEach((c) => c.execute()),
|
||||
execute: () => commands.forEach(c => c.execute()),
|
||||
undo: () => {
|
||||
for (let i = commands.length - 1; i >= 0; i--)
|
||||
commands[i]!.undo();
|
||||
|
||||
@@ -115,4 +115,4 @@ describe('pubsub', () => {
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
* @description Simple PubSub implementation
|
||||
*
|
||||
* @since 0.0.2
|
||||
*
|
||||
*
|
||||
* @template Events - Event map where keys are event names and values are listener signatures
|
||||
*/
|
||||
export class PubSub<Events extends Record<string, (...args: any[]) => any>> {
|
||||
/**
|
||||
* Events map
|
||||
*
|
||||
*
|
||||
* @private
|
||||
* @type {Map<keyof Events, Set<Events[keyof Events]>>}
|
||||
*/
|
||||
@@ -25,7 +25,7 @@ export class PubSub<Events extends Record<string, (...args: any[]) => any>> {
|
||||
|
||||
/**
|
||||
* Subscribe to an event
|
||||
*
|
||||
*
|
||||
* @template {keyof Events} K
|
||||
* @param {K} event Name of the event
|
||||
* @param {Events[K]} listener Listener function
|
||||
@@ -44,7 +44,7 @@ export class PubSub<Events extends Record<string, (...args: any[]) => any>> {
|
||||
|
||||
/**
|
||||
* Unsubscribe from an event
|
||||
*
|
||||
*
|
||||
* @template {keyof Events} K
|
||||
* @param {K} event Name of the event
|
||||
* @param {Events[K]} listener Listener function
|
||||
@@ -61,7 +61,7 @@ export class PubSub<Events extends Record<string, (...args: any[]) => any>> {
|
||||
|
||||
/**
|
||||
* Subscribe to an event only once
|
||||
*
|
||||
*
|
||||
* @template {keyof Events} K
|
||||
* @param {K} event Name of the event
|
||||
* @param {Events[K]} listener Listener function
|
||||
@@ -69,8 +69,8 @@ export class PubSub<Events extends Record<string, (...args: any[]) => any>> {
|
||||
*/
|
||||
public once<K extends keyof Events>(event: K, listener: Events[K]) {
|
||||
const onceListener = (...args: Parameters<Events[K]>) => {
|
||||
this.off(event, onceListener as Events[K]);
|
||||
listener(...args);
|
||||
this.off(event, onceListener as Events[K]);
|
||||
listener(...args);
|
||||
};
|
||||
|
||||
this.on(event, onceListener as Events[K]);
|
||||
@@ -80,7 +80,7 @@ export class PubSub<Events extends Record<string, (...args: any[]) => any>> {
|
||||
|
||||
/**
|
||||
* Emit an event
|
||||
*
|
||||
*
|
||||
* @template {keyof Events} K
|
||||
* @param {K} event Name of the event
|
||||
* @param {...Parameters<Events[K]>} args Arguments for the listener
|
||||
@@ -92,14 +92,14 @@ export class PubSub<Events extends Record<string, (...args: any[]) => any>> {
|
||||
if (!listeners)
|
||||
return false;
|
||||
|
||||
listeners.forEach((listener) => listener(...args));
|
||||
listeners.forEach(listener => listener(...args));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all listeners for an event
|
||||
*
|
||||
*
|
||||
* @template {keyof Events} K
|
||||
* @param {K} event Name of the event
|
||||
* @returns {this}
|
||||
|
||||
@@ -39,7 +39,8 @@ export class AsyncStateMachine<
|
||||
|
||||
if (isString(transition)) {
|
||||
target = transition;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
if (transition.guard && !(await transition.guard(this.context)))
|
||||
return this.currentState;
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ describe('stateMachine', () => {
|
||||
on: {
|
||||
FAIL: {
|
||||
target: 'idle',
|
||||
guard: (ctx) => ctx.retries < 3,
|
||||
guard: ctx => ctx.retries < 3,
|
||||
},
|
||||
SUCCESS: 'done',
|
||||
},
|
||||
@@ -255,7 +255,7 @@ describe('stateMachine', () => {
|
||||
on: {
|
||||
UNLOCK: {
|
||||
target: 'unlocked',
|
||||
guard: (ctx) => ctx.unlocked,
|
||||
guard: ctx => ctx.unlocked,
|
||||
},
|
||||
},
|
||||
exit: exitHook,
|
||||
@@ -374,7 +374,7 @@ describe('stateMachine', () => {
|
||||
on: {
|
||||
NEXT: {
|
||||
target: 'c',
|
||||
guard: (ctx) => ctx.step === 1,
|
||||
guard: ctx => ctx.step === 1,
|
||||
action: (ctx) => { ctx.step = 2; },
|
||||
},
|
||||
},
|
||||
@@ -434,7 +434,7 @@ describe('asyncStateMachine', () => {
|
||||
on: {
|
||||
GO: {
|
||||
target: 'active',
|
||||
guard: async (ctx) => ctx.allowed,
|
||||
guard: async ctx => ctx.allowed,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -456,7 +456,7 @@ describe('asyncStateMachine', () => {
|
||||
on: {
|
||||
GO: {
|
||||
target: 'active',
|
||||
guard: async (ctx) => ctx.allowed,
|
||||
guard: async ctx => ctx.allowed,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -483,7 +483,7 @@ describe('asyncStateMachine', () => {
|
||||
FETCH: {
|
||||
target: 'done',
|
||||
action: async (ctx) => {
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
ctx.data = 'fetched';
|
||||
order.push('action');
|
||||
},
|
||||
@@ -513,13 +513,13 @@ describe('asyncStateMachine', () => {
|
||||
a: {
|
||||
on: { GO: 'b' },
|
||||
exit: async () => {
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
order.push('exit-a');
|
||||
},
|
||||
},
|
||||
b: {
|
||||
entry: async () => {
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
order.push('entry-b');
|
||||
},
|
||||
},
|
||||
@@ -544,7 +544,7 @@ describe('asyncStateMachine', () => {
|
||||
on: {
|
||||
UNLOCK: {
|
||||
target: 'unlocked',
|
||||
guard: async (ctx) => ctx.unlocked,
|
||||
guard: async ctx => ctx.unlocked,
|
||||
},
|
||||
},
|
||||
exit: exitHook,
|
||||
@@ -573,7 +573,7 @@ describe('asyncStateMachine', () => {
|
||||
on: {
|
||||
GO: {
|
||||
target: 'active',
|
||||
guard: async (ctx) => ctx.ready,
|
||||
guard: async ctx => ctx.ready,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -667,7 +667,7 @@ describe('asyncStateMachine', () => {
|
||||
on: {
|
||||
GO: {
|
||||
target: 'active',
|
||||
guard: (ctx) => ctx.count === 0,
|
||||
guard: ctx => ctx.count === 0,
|
||||
action: (ctx) => { ctx.count++; },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -39,7 +39,8 @@ export class StateMachine<
|
||||
|
||||
if (isString(transition)) {
|
||||
target = transition;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
if (transition.guard && !transition.guard(this.context))
|
||||
return this.currentState;
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './behavioral/Command';
|
||||
export * from './behavioral/PubSub';
|
||||
export * from './behavioral/StateMachine';
|
||||
export * from './behavioral/StateMachine';
|
||||
|
||||
@@ -3,227 +3,227 @@ import { describe, expect, it } from 'vitest';
|
||||
import { BinaryHeap } from '.';
|
||||
|
||||
describe('BinaryHeap', () => {
|
||||
describe('constructor', () => {
|
||||
it('should create an empty heap', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
describe('constructor', () => {
|
||||
it('should create an empty heap', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
|
||||
expect(heap.length).toBe(0);
|
||||
expect(heap.isEmpty).toBe(true);
|
||||
});
|
||||
|
||||
it('should create a heap from single value', () => {
|
||||
const heap = new BinaryHeap(42);
|
||||
|
||||
expect(heap.length).toBe(1);
|
||||
expect(heap.peek()).toBe(42);
|
||||
});
|
||||
|
||||
it('should create a heap from array (heapify)', () => {
|
||||
const heap = new BinaryHeap([5, 3, 8, 1, 4]);
|
||||
|
||||
expect(heap.length).toBe(5);
|
||||
expect(heap.peek()).toBe(1);
|
||||
});
|
||||
|
||||
it('should accept a custom comparator for max-heap', () => {
|
||||
const heap = new BinaryHeap([5, 3, 8, 1, 4], {
|
||||
comparator: (a, b) => b - a,
|
||||
});
|
||||
|
||||
expect(heap.peek()).toBe(8);
|
||||
});
|
||||
expect(heap.length).toBe(0);
|
||||
expect(heap.isEmpty).toBe(true);
|
||||
});
|
||||
|
||||
describe('push', () => {
|
||||
it('should insert elements maintaining heap property', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
it('should create a heap from single value', () => {
|
||||
const heap = new BinaryHeap(42);
|
||||
|
||||
heap.push(5);
|
||||
heap.push(3);
|
||||
heap.push(8);
|
||||
heap.push(1);
|
||||
|
||||
expect(heap.peek()).toBe(1);
|
||||
expect(heap.length).toBe(4);
|
||||
});
|
||||
|
||||
it('should handle duplicate values', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
|
||||
heap.push(3);
|
||||
heap.push(3);
|
||||
heap.push(3);
|
||||
|
||||
expect(heap.length).toBe(3);
|
||||
expect(heap.peek()).toBe(3);
|
||||
});
|
||||
expect(heap.length).toBe(1);
|
||||
expect(heap.peek()).toBe(42);
|
||||
});
|
||||
|
||||
describe('pop', () => {
|
||||
it('should return undefined for empty heap', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
it('should create a heap from array (heapify)', () => {
|
||||
const heap = new BinaryHeap([5, 3, 8, 1, 4]);
|
||||
|
||||
expect(heap.pop()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should extract elements in min-heap order', () => {
|
||||
const heap = new BinaryHeap([5, 3, 8, 1, 4, 2, 7, 6]);
|
||||
const sorted: number[] = [];
|
||||
|
||||
while (!heap.isEmpty) {
|
||||
sorted.push(heap.pop()!);
|
||||
}
|
||||
|
||||
expect(sorted).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
});
|
||||
|
||||
it('should extract elements in max-heap order with custom comparator', () => {
|
||||
const heap = new BinaryHeap([5, 3, 8, 1, 4], {
|
||||
comparator: (a, b) => b - a,
|
||||
});
|
||||
const sorted: number[] = [];
|
||||
|
||||
while (!heap.isEmpty) {
|
||||
sorted.push(heap.pop()!);
|
||||
}
|
||||
|
||||
expect(sorted).toEqual([8, 5, 4, 3, 1]);
|
||||
});
|
||||
|
||||
it('should handle single element', () => {
|
||||
const heap = new BinaryHeap(42);
|
||||
|
||||
expect(heap.pop()).toBe(42);
|
||||
expect(heap.isEmpty).toBe(true);
|
||||
});
|
||||
expect(heap.length).toBe(5);
|
||||
expect(heap.peek()).toBe(1);
|
||||
});
|
||||
|
||||
describe('peek', () => {
|
||||
it('should return undefined for empty heap', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
it('should accept a custom comparator for max-heap', () => {
|
||||
const heap = new BinaryHeap([5, 3, 8, 1, 4], {
|
||||
comparator: (a, b) => b - a,
|
||||
});
|
||||
|
||||
expect(heap.peek()).toBeUndefined();
|
||||
});
|
||||
expect(heap.peek()).toBe(8);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return root without removing it', () => {
|
||||
const heap = new BinaryHeap([5, 3, 1]);
|
||||
describe('push', () => {
|
||||
it('should insert elements maintaining heap property', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
|
||||
expect(heap.peek()).toBe(1);
|
||||
expect(heap.length).toBe(3);
|
||||
});
|
||||
heap.push(5);
|
||||
heap.push(3);
|
||||
heap.push(8);
|
||||
heap.push(1);
|
||||
|
||||
expect(heap.peek()).toBe(1);
|
||||
expect(heap.length).toBe(4);
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('should remove all elements', () => {
|
||||
const heap = new BinaryHeap([1, 2, 3]);
|
||||
it('should handle duplicate values', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
|
||||
const result = heap.clear();
|
||||
heap.push(3);
|
||||
heap.push(3);
|
||||
heap.push(3);
|
||||
|
||||
expect(heap.length).toBe(0);
|
||||
expect(heap.isEmpty).toBe(true);
|
||||
expect(result).toBe(heap);
|
||||
});
|
||||
expect(heap.length).toBe(3);
|
||||
expect(heap.peek()).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pop', () => {
|
||||
it('should return undefined for empty heap', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
|
||||
expect(heap.pop()).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('toArray', () => {
|
||||
it('should return empty array for empty heap', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
it('should extract elements in min-heap order', () => {
|
||||
const heap = new BinaryHeap([5, 3, 8, 1, 4, 2, 7, 6]);
|
||||
const sorted: number[] = [];
|
||||
|
||||
expect(heap.toArray()).toEqual([]);
|
||||
});
|
||||
while (!heap.isEmpty) {
|
||||
sorted.push(heap.pop()!);
|
||||
}
|
||||
|
||||
it('should return a shallow copy', () => {
|
||||
const heap = new BinaryHeap([3, 1, 2]);
|
||||
const arr = heap.toArray();
|
||||
|
||||
arr.push(99);
|
||||
|
||||
expect(heap.length).toBe(3);
|
||||
});
|
||||
expect(sorted).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
});
|
||||
|
||||
describe('toString', () => {
|
||||
it('should return formatted string', () => {
|
||||
const heap = new BinaryHeap([1, 2, 3]);
|
||||
it('should extract elements in max-heap order with custom comparator', () => {
|
||||
const heap = new BinaryHeap([5, 3, 8, 1, 4], {
|
||||
comparator: (a, b) => b - a,
|
||||
});
|
||||
const sorted: number[] = [];
|
||||
|
||||
expect(heap.toString()).toBe('BinaryHeap(3)');
|
||||
});
|
||||
while (!heap.isEmpty) {
|
||||
sorted.push(heap.pop()!);
|
||||
}
|
||||
|
||||
expect(sorted).toEqual([8, 5, 4, 3, 1]);
|
||||
});
|
||||
|
||||
describe('iterator', () => {
|
||||
it('should iterate over heap elements', () => {
|
||||
const heap = new BinaryHeap([5, 3, 8, 1]);
|
||||
const elements = [...heap];
|
||||
it('should handle single element', () => {
|
||||
const heap = new BinaryHeap(42);
|
||||
|
||||
expect(elements.length).toBe(4);
|
||||
expect(elements[0]).toBe(1);
|
||||
});
|
||||
expect(heap.pop()).toBe(42);
|
||||
expect(heap.isEmpty).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('peek', () => {
|
||||
it('should return undefined for empty heap', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
|
||||
expect(heap.peek()).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('custom comparator', () => {
|
||||
it('should work with string length comparator', () => {
|
||||
const heap = new BinaryHeap(['banana', 'apple', 'kiwi', 'fig'], {
|
||||
comparator: (a, b) => a.length - b.length,
|
||||
});
|
||||
it('should return root without removing it', () => {
|
||||
const heap = new BinaryHeap([5, 3, 1]);
|
||||
|
||||
expect(heap.pop()).toBe('fig');
|
||||
expect(heap.pop()).toBe('kiwi');
|
||||
});
|
||||
expect(heap.peek()).toBe(1);
|
||||
expect(heap.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with object comparator', () => {
|
||||
interface Task {
|
||||
priority: number;
|
||||
name: string;
|
||||
}
|
||||
describe('clear', () => {
|
||||
it('should remove all elements', () => {
|
||||
const heap = new BinaryHeap([1, 2, 3]);
|
||||
|
||||
const heap = new BinaryHeap<Task>(
|
||||
[
|
||||
{ priority: 3, name: 'low' },
|
||||
{ priority: 1, name: 'high' },
|
||||
{ priority: 2, name: 'medium' },
|
||||
],
|
||||
{ comparator: (a, b) => a.priority - b.priority },
|
||||
);
|
||||
const result = heap.clear();
|
||||
|
||||
expect(heap.pop()?.name).toBe('high');
|
||||
expect(heap.pop()?.name).toBe('medium');
|
||||
expect(heap.pop()?.name).toBe('low');
|
||||
});
|
||||
expect(heap.length).toBe(0);
|
||||
expect(heap.isEmpty).toBe(true);
|
||||
expect(result).toBe(heap);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toArray', () => {
|
||||
it('should return empty array for empty heap', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
|
||||
expect(heap.toArray()).toEqual([]);
|
||||
});
|
||||
|
||||
describe('heapify', () => {
|
||||
it('should correctly heapify large arrays', () => {
|
||||
const values = Array.from({ length: 1000 }, () => Math.random() * 1000 | 0);
|
||||
const heap = new BinaryHeap(values);
|
||||
const sorted: number[] = [];
|
||||
it('should return a shallow copy', () => {
|
||||
const heap = new BinaryHeap([3, 1, 2]);
|
||||
const arr = heap.toArray();
|
||||
|
||||
while (!heap.isEmpty) {
|
||||
sorted.push(heap.pop()!);
|
||||
}
|
||||
arr.push(99);
|
||||
|
||||
const expected = [...values].sort((a, b) => a - b);
|
||||
expect(heap.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
expect(sorted).toEqual(expected);
|
||||
});
|
||||
describe('toString', () => {
|
||||
it('should return formatted string', () => {
|
||||
const heap = new BinaryHeap([1, 2, 3]);
|
||||
|
||||
expect(heap.toString()).toBe('BinaryHeap(3)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('iterator', () => {
|
||||
it('should iterate over heap elements', () => {
|
||||
const heap = new BinaryHeap([5, 3, 8, 1]);
|
||||
const elements = [...heap];
|
||||
|
||||
expect(elements.length).toBe(4);
|
||||
expect(elements[0]).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom comparator', () => {
|
||||
it('should work with string length comparator', () => {
|
||||
const heap = new BinaryHeap(['banana', 'apple', 'kiwi', 'fig'], {
|
||||
comparator: (a, b) => a.length - b.length,
|
||||
});
|
||||
|
||||
expect(heap.pop()).toBe('fig');
|
||||
expect(heap.pop()).toBe('kiwi');
|
||||
});
|
||||
|
||||
describe('interleaved operations', () => {
|
||||
it('should maintain heap property with mixed push and pop', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
it('should work with object comparator', () => {
|
||||
interface Task {
|
||||
priority: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
heap.push(10);
|
||||
heap.push(5);
|
||||
expect(heap.pop()).toBe(5);
|
||||
const heap = new BinaryHeap<Task>(
|
||||
[
|
||||
{ priority: 3, name: 'low' },
|
||||
{ priority: 1, name: 'high' },
|
||||
{ priority: 2, name: 'medium' },
|
||||
],
|
||||
{ comparator: (a, b) => a.priority - b.priority },
|
||||
);
|
||||
|
||||
heap.push(3);
|
||||
heap.push(7);
|
||||
expect(heap.pop()).toBe(3);
|
||||
|
||||
heap.push(1);
|
||||
expect(heap.pop()).toBe(1);
|
||||
expect(heap.pop()).toBe(7);
|
||||
expect(heap.pop()).toBe(10);
|
||||
expect(heap.pop()).toBeUndefined();
|
||||
});
|
||||
expect(heap.pop()?.name).toBe('high');
|
||||
expect(heap.pop()?.name).toBe('medium');
|
||||
expect(heap.pop()?.name).toBe('low');
|
||||
});
|
||||
});
|
||||
|
||||
describe('heapify', () => {
|
||||
it('should correctly heapify large arrays', () => {
|
||||
const values = Array.from({ length: 1000 }, () => Math.random() * 1000 | 0);
|
||||
const heap = new BinaryHeap(values);
|
||||
const sorted: number[] = [];
|
||||
|
||||
while (!heap.isEmpty) {
|
||||
sorted.push(heap.pop()!);
|
||||
}
|
||||
|
||||
const expected = [...values].sort((a, b) => a - b);
|
||||
|
||||
expect(sorted).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interleaved operations', () => {
|
||||
it('should maintain heap property with mixed push and pop', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
|
||||
heap.push(10);
|
||||
heap.push(5);
|
||||
expect(heap.pop()).toBe(5);
|
||||
|
||||
heap.push(3);
|
||||
heap.push(7);
|
||||
expect(heap.pop()).toBe(3);
|
||||
|
||||
heap.push(1);
|
||||
expect(heap.pop()).toBe(1);
|
||||
expect(heap.pop()).toBe(7);
|
||||
expect(heap.pop()).toBe(10);
|
||||
expect(heap.pop()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { BinaryHeapLike, Comparator } from './types';
|
||||
export type { BinaryHeapLike, Comparator } from './types';
|
||||
|
||||
export interface BinaryHeapOptions<T> {
|
||||
comparator?: Comparator<T>;
|
||||
comparator?: Comparator<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,194 +27,194 @@ const defaultComparator: Comparator<any> = (a: number, b: number) => a - b;
|
||||
* @template T The type of elements stored in the heap
|
||||
*/
|
||||
export class BinaryHeap<T> implements BinaryHeapLike<T> {
|
||||
/**
|
||||
/**
|
||||
* The comparator function used to order elements
|
||||
*
|
||||
* @private
|
||||
* @type {Comparator<T>}
|
||||
*/
|
||||
private readonly comparator: Comparator<T>;
|
||||
private readonly comparator: Comparator<T>;
|
||||
|
||||
/**
|
||||
/**
|
||||
* Internal flat array backing the heap
|
||||
*
|
||||
* @private
|
||||
* @type {T[]}
|
||||
*/
|
||||
private readonly heap: T[] = [];
|
||||
private readonly heap: T[] = [];
|
||||
|
||||
/**
|
||||
/**
|
||||
* Creates an instance of BinaryHeap
|
||||
*
|
||||
* @param {(T[] | T)} [initialValues] The initial values to heapify
|
||||
* @param {BinaryHeapOptions<T>} [options] Heap configuration
|
||||
*/
|
||||
constructor(initialValues?: T[] | T, options?: BinaryHeapOptions<T>) {
|
||||
this.comparator = options?.comparator ?? defaultComparator;
|
||||
constructor(initialValues?: T[] | T, options?: BinaryHeapOptions<T>) {
|
||||
this.comparator = options?.comparator ?? defaultComparator;
|
||||
|
||||
if (initialValues !== null && initialValues !== undefined) {
|
||||
const items = isArray(initialValues) ? initialValues : [initialValues];
|
||||
this.heap.push(...items);
|
||||
this.heapify();
|
||||
}
|
||||
if (initialValues !== null && initialValues !== undefined) {
|
||||
const items = isArray(initialValues) ? initialValues : [initialValues];
|
||||
this.heap.push(...items);
|
||||
this.heapify();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Gets the number of elements in the heap
|
||||
* @returns {number} The number of elements in the heap
|
||||
*/
|
||||
public get length(): number {
|
||||
return this.heap.length;
|
||||
}
|
||||
public get length(): number {
|
||||
return this.heap.length;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Checks if the heap is empty
|
||||
* @returns {boolean} `true` if the heap is empty, `false` otherwise
|
||||
*/
|
||||
public get isEmpty(): boolean {
|
||||
return this.heap.length === 0;
|
||||
}
|
||||
public get isEmpty(): boolean {
|
||||
return this.heap.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Pushes an element into the heap
|
||||
* @param {T} element The element to insert
|
||||
*/
|
||||
public push(element: T): void {
|
||||
this.heap.push(element);
|
||||
this.siftUp(this.heap.length - 1);
|
||||
}
|
||||
public push(element: T): void {
|
||||
this.heap.push(element);
|
||||
this.siftUp(this.heap.length - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Removes and returns the root element (min or max depending on comparator)
|
||||
* @returns {T | undefined} The root element, or `undefined` if the heap is empty
|
||||
*/
|
||||
public pop(): T | undefined {
|
||||
if (this.heap.length === 0) return undefined;
|
||||
public pop(): T | undefined {
|
||||
if (this.heap.length === 0) return undefined;
|
||||
|
||||
const root = first(this.heap)!;
|
||||
const last = this.heap.pop()!;
|
||||
const root = first(this.heap)!;
|
||||
const last = this.heap.pop()!;
|
||||
|
||||
if (this.heap.length > 0) {
|
||||
this.heap[0] = last;
|
||||
this.siftDown(0);
|
||||
}
|
||||
|
||||
return root;
|
||||
if (this.heap.length > 0) {
|
||||
this.heap[0] = last;
|
||||
this.siftDown(0);
|
||||
}
|
||||
|
||||
/**
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root element without removing it
|
||||
* @returns {T | undefined} The root element, or `undefined` if the heap is empty
|
||||
*/
|
||||
public peek(): T | undefined {
|
||||
return first(this.heap);
|
||||
}
|
||||
public peek(): T | undefined {
|
||||
return first(this.heap);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Removes all elements from the heap
|
||||
* @returns {this} The heap instance for chaining
|
||||
*/
|
||||
public clear(): this {
|
||||
this.heap.length = 0;
|
||||
return this;
|
||||
}
|
||||
public clear(): this {
|
||||
this.heap.length = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns a shallow copy of the heap elements as an array (heap order, not sorted)
|
||||
* @returns {T[]} Array of elements in heap order
|
||||
*/
|
||||
public toArray(): T[] {
|
||||
return this.heap.slice();
|
||||
}
|
||||
public toArray(): T[] {
|
||||
return this.heap.slice();
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns a string representation of the heap
|
||||
* @returns {string} String representation
|
||||
*/
|
||||
public toString(): string {
|
||||
return `BinaryHeap(${this.heap.length})`;
|
||||
}
|
||||
public toString(): string {
|
||||
return `BinaryHeap(${this.heap.length})`;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Iterator over heap elements in heap order
|
||||
*/
|
||||
public *[Symbol.iterator](): Iterator<T> {
|
||||
yield* this.heap;
|
||||
}
|
||||
public* [Symbol.iterator](): Iterator<T> {
|
||||
yield* this.heap;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Async iterator over heap elements in heap order
|
||||
*/
|
||||
public async *[Symbol.asyncIterator](): AsyncIterator<T> {
|
||||
for (const element of this.heap)
|
||||
yield element;
|
||||
}
|
||||
public async* [Symbol.asyncIterator](): AsyncIterator<T> {
|
||||
for (const element of this.heap)
|
||||
yield element;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Restores heap property by sifting an element up
|
||||
*
|
||||
* @private
|
||||
* @param {number} index The index of the element to sift up
|
||||
*/
|
||||
private siftUp(index: number): void {
|
||||
const heap = this.heap;
|
||||
const cmp = this.comparator;
|
||||
private siftUp(index: number): void {
|
||||
const heap = this.heap;
|
||||
const cmp = this.comparator;
|
||||
|
||||
while (index > 0) {
|
||||
const parent = (index - 1) >> 1;
|
||||
while (index > 0) {
|
||||
const parent = (index - 1) >> 1;
|
||||
|
||||
if (cmp(heap[index]!, heap[parent]!) >= 0) break;
|
||||
if (cmp(heap[index]!, heap[parent]!) >= 0) break;
|
||||
|
||||
const temp = heap[index]!;
|
||||
heap[index] = heap[parent]!;
|
||||
heap[parent] = temp;
|
||||
const temp = heap[index]!;
|
||||
heap[index] = heap[parent]!;
|
||||
heap[parent] = temp;
|
||||
|
||||
index = parent;
|
||||
}
|
||||
index = parent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Restores heap property by sifting an element down
|
||||
*
|
||||
* @private
|
||||
* @param {number} index The index of the element to sift down
|
||||
*/
|
||||
private siftDown(index: number): void {
|
||||
const heap = this.heap;
|
||||
const cmp = this.comparator;
|
||||
const length = heap.length;
|
||||
private siftDown(index: number): void {
|
||||
const heap = this.heap;
|
||||
const cmp = this.comparator;
|
||||
const length = heap.length;
|
||||
|
||||
while (true) {
|
||||
let smallest = index;
|
||||
const left = 2 * index + 1;
|
||||
const right = 2 * index + 2;
|
||||
while (true) {
|
||||
let smallest = index;
|
||||
const left = 2 * index + 1;
|
||||
const right = 2 * index + 2;
|
||||
|
||||
if (left < length && cmp(heap[left]!, heap[smallest]!) < 0) {
|
||||
smallest = left;
|
||||
}
|
||||
if (left < length && cmp(heap[left]!, heap[smallest]!) < 0) {
|
||||
smallest = left;
|
||||
}
|
||||
|
||||
if (right < length && cmp(heap[right]!, heap[smallest]!) < 0) {
|
||||
smallest = right;
|
||||
}
|
||||
if (right < length && cmp(heap[right]!, heap[smallest]!) < 0) {
|
||||
smallest = right;
|
||||
}
|
||||
|
||||
if (smallest === index) break;
|
||||
if (smallest === index) break;
|
||||
|
||||
const temp = heap[index]!;
|
||||
heap[index] = heap[smallest]!;
|
||||
heap[smallest] = temp;
|
||||
const temp = heap[index]!;
|
||||
heap[index] = heap[smallest]!;
|
||||
heap[smallest] = temp;
|
||||
|
||||
index = smallest;
|
||||
}
|
||||
index = smallest;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Builds heap from unordered array in O(n) using Floyd's algorithm
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private heapify(): void {
|
||||
for (let i = (this.heap.length >> 1) - 1; i >= 0; i--) {
|
||||
this.siftDown(i);
|
||||
}
|
||||
private heapify(): void {
|
||||
for (let i = (this.heap.length >> 1) - 1; i >= 0; i--) {
|
||||
this.siftDown(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ export class CircularBuffer<T> implements CircularBufferLike<T> {
|
||||
*
|
||||
* @returns {AsyncIterableIterator<T>}
|
||||
*/
|
||||
async *[Symbol.asyncIterator]() {
|
||||
async* [Symbol.asyncIterator]() {
|
||||
for (const element of this)
|
||||
yield element;
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ export class Deque<T> implements DequeLike<T> {
|
||||
*
|
||||
* @returns {AsyncIterableIterator<T>}
|
||||
*/
|
||||
async *[Symbol.asyncIterator]() {
|
||||
async* [Symbol.asyncIterator]() {
|
||||
for (const element of this.buffer)
|
||||
yield element;
|
||||
}
|
||||
|
||||
@@ -3,404 +3,404 @@ import { describe, expect, it } from 'vitest';
|
||||
import { LinkedList } from '.';
|
||||
|
||||
describe('LinkedList', () => {
|
||||
describe('constructor', () => {
|
||||
it('should create an empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
describe('constructor', () => {
|
||||
it('should create an empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
expect(list.length).toBe(0);
|
||||
expect(list.isEmpty).toBe(true);
|
||||
expect(list.head).toBeUndefined();
|
||||
expect(list.tail).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create a list from single value', () => {
|
||||
const list = new LinkedList(42);
|
||||
|
||||
expect(list.length).toBe(1);
|
||||
expect(list.peekFront()).toBe(42);
|
||||
expect(list.peekBack()).toBe(42);
|
||||
});
|
||||
|
||||
it('should create a list from array', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
expect(list.length).toBe(3);
|
||||
expect(list.peekFront()).toBe(1);
|
||||
expect(list.peekBack()).toBe(3);
|
||||
});
|
||||
expect(list.length).toBe(0);
|
||||
expect(list.isEmpty).toBe(true);
|
||||
expect(list.head).toBeUndefined();
|
||||
expect(list.tail).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('pushBack', () => {
|
||||
it('should append to empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
it('should create a list from single value', () => {
|
||||
const list = new LinkedList(42);
|
||||
|
||||
const node = list.pushBack(1);
|
||||
|
||||
expect(list.length).toBe(1);
|
||||
expect(node.value).toBe(1);
|
||||
expect(list.head).toBe(node);
|
||||
expect(list.tail).toBe(node);
|
||||
});
|
||||
|
||||
it('should append to non-empty list', () => {
|
||||
const list = new LinkedList([1, 2]);
|
||||
|
||||
list.pushBack(3);
|
||||
|
||||
expect(list.length).toBe(3);
|
||||
expect(list.peekBack()).toBe(3);
|
||||
expect(list.peekFront()).toBe(1);
|
||||
});
|
||||
|
||||
it('should return the created node', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
const node = list.pushBack(5);
|
||||
|
||||
expect(node.value).toBe(5);
|
||||
expect(node.prev).toBeUndefined();
|
||||
expect(node.next).toBeUndefined();
|
||||
});
|
||||
expect(list.length).toBe(1);
|
||||
expect(list.peekFront()).toBe(42);
|
||||
expect(list.peekBack()).toBe(42);
|
||||
});
|
||||
|
||||
describe('pushFront', () => {
|
||||
it('should prepend to empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
it('should create a list from array', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
const node = list.pushFront(1);
|
||||
expect(list.length).toBe(3);
|
||||
expect(list.peekFront()).toBe(1);
|
||||
expect(list.peekBack()).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
expect(list.length).toBe(1);
|
||||
expect(list.head).toBe(node);
|
||||
expect(list.tail).toBe(node);
|
||||
});
|
||||
describe('pushBack', () => {
|
||||
it('should append to empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
it('should prepend to non-empty list', () => {
|
||||
const list = new LinkedList([2, 3]);
|
||||
const node = list.pushBack(1);
|
||||
|
||||
list.pushFront(1);
|
||||
|
||||
expect(list.length).toBe(3);
|
||||
expect(list.peekFront()).toBe(1);
|
||||
expect(list.peekBack()).toBe(3);
|
||||
});
|
||||
expect(list.length).toBe(1);
|
||||
expect(node.value).toBe(1);
|
||||
expect(list.head).toBe(node);
|
||||
expect(list.tail).toBe(node);
|
||||
});
|
||||
|
||||
describe('popBack', () => {
|
||||
it('should return undefined for empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
it('should append to non-empty list', () => {
|
||||
const list = new LinkedList([1, 2]);
|
||||
|
||||
expect(list.popBack()).toBeUndefined();
|
||||
});
|
||||
list.pushBack(3);
|
||||
|
||||
it('should remove and return last value', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
expect(list.popBack()).toBe(3);
|
||||
expect(list.length).toBe(2);
|
||||
expect(list.peekBack()).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle single element', () => {
|
||||
const list = new LinkedList(1);
|
||||
|
||||
expect(list.popBack()).toBe(1);
|
||||
expect(list.isEmpty).toBe(true);
|
||||
expect(list.head).toBeUndefined();
|
||||
expect(list.tail).toBeUndefined();
|
||||
});
|
||||
expect(list.length).toBe(3);
|
||||
expect(list.peekBack()).toBe(3);
|
||||
expect(list.peekFront()).toBe(1);
|
||||
});
|
||||
|
||||
describe('popFront', () => {
|
||||
it('should return undefined for empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
it('should return the created node', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
expect(list.popFront()).toBeUndefined();
|
||||
});
|
||||
const node = list.pushBack(5);
|
||||
|
||||
it('should remove and return first value', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
expect(node.value).toBe(5);
|
||||
expect(node.prev).toBeUndefined();
|
||||
expect(node.next).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
expect(list.popFront()).toBe(1);
|
||||
expect(list.length).toBe(2);
|
||||
expect(list.peekFront()).toBe(2);
|
||||
});
|
||||
describe('pushFront', () => {
|
||||
it('should prepend to empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
it('should handle single element', () => {
|
||||
const list = new LinkedList(1);
|
||||
const node = list.pushFront(1);
|
||||
|
||||
expect(list.popFront()).toBe(1);
|
||||
expect(list.isEmpty).toBe(true);
|
||||
expect(list.head).toBeUndefined();
|
||||
expect(list.tail).toBeUndefined();
|
||||
});
|
||||
expect(list.length).toBe(1);
|
||||
expect(list.head).toBe(node);
|
||||
expect(list.tail).toBe(node);
|
||||
});
|
||||
|
||||
describe('peekBack', () => {
|
||||
it('should return undefined for empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
it('should prepend to non-empty list', () => {
|
||||
const list = new LinkedList([2, 3]);
|
||||
|
||||
expect(list.peekBack()).toBeUndefined();
|
||||
});
|
||||
list.pushFront(1);
|
||||
|
||||
it('should return last value without removing', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
expect(list.length).toBe(3);
|
||||
expect(list.peekFront()).toBe(1);
|
||||
expect(list.peekBack()).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
expect(list.peekBack()).toBe(3);
|
||||
expect(list.length).toBe(3);
|
||||
});
|
||||
describe('popBack', () => {
|
||||
it('should return undefined for empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
expect(list.popBack()).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('peekFront', () => {
|
||||
it('should return undefined for empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
it('should remove and return last value', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
expect(list.peekFront()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return first value without removing', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
expect(list.peekFront()).toBe(1);
|
||||
expect(list.length).toBe(3);
|
||||
});
|
||||
expect(list.popBack()).toBe(3);
|
||||
expect(list.length).toBe(2);
|
||||
expect(list.peekBack()).toBe(2);
|
||||
});
|
||||
|
||||
describe('insertBefore', () => {
|
||||
it('should insert before head', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const node = list.pushBack(2);
|
||||
it('should handle single element', () => {
|
||||
const list = new LinkedList(1);
|
||||
|
||||
list.insertBefore(node, 1);
|
||||
expect(list.popBack()).toBe(1);
|
||||
expect(list.isEmpty).toBe(true);
|
||||
expect(list.head).toBeUndefined();
|
||||
expect(list.tail).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
expect(list.peekFront()).toBe(1);
|
||||
expect(list.peekBack()).toBe(2);
|
||||
expect(list.length).toBe(2);
|
||||
});
|
||||
describe('popFront', () => {
|
||||
it('should return undefined for empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
it('should insert before middle node', () => {
|
||||
const list = new LinkedList([1, 3]);
|
||||
const tail = list.tail!;
|
||||
|
||||
list.insertBefore(tail, 2);
|
||||
|
||||
expect(list.toArray()).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('should return the created node', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const existing = list.pushBack(2);
|
||||
|
||||
const newNode = list.insertBefore(existing, 1);
|
||||
|
||||
expect(newNode.value).toBe(1);
|
||||
expect(newNode.next).toBe(existing);
|
||||
});
|
||||
expect(list.popFront()).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('insertAfter', () => {
|
||||
it('should insert after tail', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const node = list.pushBack(1);
|
||||
it('should remove and return first value', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
list.insertAfter(node, 2);
|
||||
|
||||
expect(list.peekFront()).toBe(1);
|
||||
expect(list.peekBack()).toBe(2);
|
||||
expect(list.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should insert after middle node', () => {
|
||||
const list = new LinkedList([1, 3]);
|
||||
const head = list.head!;
|
||||
|
||||
list.insertAfter(head, 2);
|
||||
|
||||
expect(list.toArray()).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('should return the created node', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const existing = list.pushBack(1);
|
||||
|
||||
const newNode = list.insertAfter(existing, 2);
|
||||
|
||||
expect(newNode.value).toBe(2);
|
||||
expect(newNode.prev).toBe(existing);
|
||||
});
|
||||
expect(list.popFront()).toBe(1);
|
||||
expect(list.length).toBe(2);
|
||||
expect(list.peekFront()).toBe(2);
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should remove head node', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
const head = list.head!;
|
||||
it('should handle single element', () => {
|
||||
const list = new LinkedList(1);
|
||||
|
||||
const value = list.remove(head);
|
||||
expect(list.popFront()).toBe(1);
|
||||
expect(list.isEmpty).toBe(true);
|
||||
expect(list.head).toBeUndefined();
|
||||
expect(list.tail).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
expect(value).toBe(1);
|
||||
expect(list.length).toBe(2);
|
||||
expect(list.peekFront()).toBe(2);
|
||||
});
|
||||
describe('peekBack', () => {
|
||||
it('should return undefined for empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
it('should remove tail node', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
const tail = list.tail!;
|
||||
|
||||
const value = list.remove(tail);
|
||||
|
||||
expect(value).toBe(3);
|
||||
expect(list.length).toBe(2);
|
||||
expect(list.peekBack()).toBe(2);
|
||||
});
|
||||
|
||||
it('should remove middle node', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
const middle = list.head!.next!;
|
||||
|
||||
const value = list.remove(middle);
|
||||
|
||||
expect(value).toBe(2);
|
||||
expect(list.toArray()).toEqual([1, 3]);
|
||||
});
|
||||
|
||||
it('should remove single element', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const node = list.pushBack(1);
|
||||
|
||||
list.remove(node);
|
||||
|
||||
expect(list.isEmpty).toBe(true);
|
||||
expect(list.head).toBeUndefined();
|
||||
expect(list.tail).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should detach the removed node', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
const middle = list.head!.next!;
|
||||
|
||||
list.remove(middle);
|
||||
|
||||
expect(middle.prev).toBeUndefined();
|
||||
expect(middle.next).toBeUndefined();
|
||||
});
|
||||
expect(list.peekBack()).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('should remove all elements', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
it('should return last value without removing', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
const result = list.clear();
|
||||
expect(list.peekBack()).toBe(3);
|
||||
expect(list.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
expect(list.length).toBe(0);
|
||||
expect(list.isEmpty).toBe(true);
|
||||
expect(list.head).toBeUndefined();
|
||||
expect(list.tail).toBeUndefined();
|
||||
expect(result).toBe(list);
|
||||
});
|
||||
describe('peekFront', () => {
|
||||
it('should return undefined for empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
expect(list.peekFront()).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('toArray', () => {
|
||||
it('should return empty array for empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
it('should return first value without removing', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
expect(list.toArray()).toEqual([]);
|
||||
});
|
||||
expect(list.peekFront()).toBe(1);
|
||||
expect(list.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return values from head to tail', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
describe('insertBefore', () => {
|
||||
it('should insert before head', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const node = list.pushBack(2);
|
||||
|
||||
expect(list.toArray()).toEqual([1, 2, 3]);
|
||||
});
|
||||
list.insertBefore(node, 1);
|
||||
|
||||
expect(list.peekFront()).toBe(1);
|
||||
expect(list.peekBack()).toBe(2);
|
||||
expect(list.length).toBe(2);
|
||||
});
|
||||
|
||||
describe('toString', () => {
|
||||
it('should return comma-separated values', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
it('should insert before middle node', () => {
|
||||
const list = new LinkedList([1, 3]);
|
||||
const tail = list.tail!;
|
||||
|
||||
expect(list.toString()).toBe('1,2,3');
|
||||
});
|
||||
list.insertBefore(tail, 2);
|
||||
|
||||
expect(list.toArray()).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
describe('iterator', () => {
|
||||
it('should iterate from head to tail', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
it('should return the created node', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const existing = list.pushBack(2);
|
||||
|
||||
expect([...list]).toEqual([1, 2, 3]);
|
||||
});
|
||||
const newNode = list.insertBefore(existing, 1);
|
||||
|
||||
it('should yield nothing for empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
expect(newNode.value).toBe(1);
|
||||
expect(newNode.next).toBe(existing);
|
||||
});
|
||||
});
|
||||
|
||||
expect([...list]).toEqual([]);
|
||||
});
|
||||
describe('insertAfter', () => {
|
||||
it('should insert after tail', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const node = list.pushBack(1);
|
||||
|
||||
list.insertAfter(node, 2);
|
||||
|
||||
expect(list.peekFront()).toBe(1);
|
||||
expect(list.peekBack()).toBe(2);
|
||||
expect(list.length).toBe(2);
|
||||
});
|
||||
|
||||
describe('async iterator', () => {
|
||||
it('should async iterate from head to tail', async () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
const result: number[] = [];
|
||||
it('should insert after middle node', () => {
|
||||
const list = new LinkedList([1, 3]);
|
||||
const head = list.head!;
|
||||
|
||||
for await (const value of list)
|
||||
result.push(value);
|
||||
list.insertAfter(head, 2);
|
||||
|
||||
expect(result).toEqual([1, 2, 3]);
|
||||
});
|
||||
expect(list.toArray()).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
describe('node linking', () => {
|
||||
it('should maintain correct prev/next references', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const a = list.pushBack(1);
|
||||
const b = list.pushBack(2);
|
||||
const c = list.pushBack(3);
|
||||
it('should return the created node', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const existing = list.pushBack(1);
|
||||
|
||||
expect(a.next).toBe(b);
|
||||
expect(b.prev).toBe(a);
|
||||
expect(b.next).toBe(c);
|
||||
expect(c.prev).toBe(b);
|
||||
expect(a.prev).toBeUndefined();
|
||||
expect(c.next).toBeUndefined();
|
||||
});
|
||||
const newNode = list.insertAfter(existing, 2);
|
||||
|
||||
it('should update links after removal', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const a = list.pushBack(1);
|
||||
const b = list.pushBack(2);
|
||||
const c = list.pushBack(3);
|
||||
expect(newNode.value).toBe(2);
|
||||
expect(newNode.prev).toBe(existing);
|
||||
});
|
||||
});
|
||||
|
||||
list.remove(b);
|
||||
describe('remove', () => {
|
||||
it('should remove head node', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
const head = list.head!;
|
||||
|
||||
expect(a.next).toBe(c);
|
||||
expect(c.prev).toBe(a);
|
||||
});
|
||||
const value = list.remove(head);
|
||||
|
||||
expect(value).toBe(1);
|
||||
expect(list.length).toBe(2);
|
||||
expect(list.peekFront()).toBe(2);
|
||||
});
|
||||
|
||||
describe('interleaved operations', () => {
|
||||
it('should handle mixed push/pop from both ends', () => {
|
||||
const list = new LinkedList<number>();
|
||||
it('should remove tail node', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
const tail = list.tail!;
|
||||
|
||||
list.pushBack(1);
|
||||
list.pushBack(2);
|
||||
list.pushFront(0);
|
||||
const value = list.remove(tail);
|
||||
|
||||
expect(list.popFront()).toBe(0);
|
||||
expect(list.popBack()).toBe(2);
|
||||
expect(list.popFront()).toBe(1);
|
||||
expect(list.isEmpty).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle insert and remove by node reference', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const a = list.pushBack(1);
|
||||
const c = list.pushBack(3);
|
||||
const b = list.insertAfter(a, 2);
|
||||
const d = list.insertBefore(c, 2.5);
|
||||
|
||||
expect(list.toArray()).toEqual([1, 2, 2.5, 3]);
|
||||
|
||||
list.remove(b);
|
||||
list.remove(d);
|
||||
|
||||
expect(list.toArray()).toEqual([1, 3]);
|
||||
});
|
||||
expect(value).toBe(3);
|
||||
expect(list.length).toBe(2);
|
||||
expect(list.peekBack()).toBe(2);
|
||||
});
|
||||
|
||||
it('should remove middle node', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
const middle = list.head!.next!;
|
||||
|
||||
const value = list.remove(middle);
|
||||
|
||||
expect(value).toBe(2);
|
||||
expect(list.toArray()).toEqual([1, 3]);
|
||||
});
|
||||
|
||||
it('should remove single element', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const node = list.pushBack(1);
|
||||
|
||||
list.remove(node);
|
||||
|
||||
expect(list.isEmpty).toBe(true);
|
||||
expect(list.head).toBeUndefined();
|
||||
expect(list.tail).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should detach the removed node', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
const middle = list.head!.next!;
|
||||
|
||||
list.remove(middle);
|
||||
|
||||
expect(middle.prev).toBeUndefined();
|
||||
expect(middle.next).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('should remove all elements', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
const result = list.clear();
|
||||
|
||||
expect(list.length).toBe(0);
|
||||
expect(list.isEmpty).toBe(true);
|
||||
expect(list.head).toBeUndefined();
|
||||
expect(list.tail).toBeUndefined();
|
||||
expect(result).toBe(list);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toArray', () => {
|
||||
it('should return empty array for empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
expect(list.toArray()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return values from head to tail', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
expect(list.toArray()).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toString', () => {
|
||||
it('should return comma-separated values', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
expect(list.toString()).toBe('1,2,3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('iterator', () => {
|
||||
it('should iterate from head to tail', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
expect([...list]).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('should yield nothing for empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
expect([...list]).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('async iterator', () => {
|
||||
it('should async iterate from head to tail', async () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
const result: number[] = [];
|
||||
|
||||
for await (const value of list)
|
||||
result.push(value);
|
||||
|
||||
expect(result).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('node linking', () => {
|
||||
it('should maintain correct prev/next references', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const a = list.pushBack(1);
|
||||
const b = list.pushBack(2);
|
||||
const c = list.pushBack(3);
|
||||
|
||||
expect(a.next).toBe(b);
|
||||
expect(b.prev).toBe(a);
|
||||
expect(b.next).toBe(c);
|
||||
expect(c.prev).toBe(b);
|
||||
expect(a.prev).toBeUndefined();
|
||||
expect(c.next).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should update links after removal', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const a = list.pushBack(1);
|
||||
const b = list.pushBack(2);
|
||||
const c = list.pushBack(3);
|
||||
|
||||
list.remove(b);
|
||||
|
||||
expect(a.next).toBe(c);
|
||||
expect(c.prev).toBe(a);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interleaved operations', () => {
|
||||
it('should handle mixed push/pop from both ends', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
list.pushBack(1);
|
||||
list.pushBack(2);
|
||||
list.pushFront(0);
|
||||
|
||||
expect(list.popFront()).toBe(0);
|
||||
expect(list.popBack()).toBe(2);
|
||||
expect(list.popFront()).toBe(1);
|
||||
expect(list.isEmpty).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle insert and remove by node reference', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const a = list.pushBack(1);
|
||||
const c = list.pushBack(3);
|
||||
const b = list.insertAfter(a, 2);
|
||||
const d = list.insertBefore(c, 2.5);
|
||||
|
||||
expect(list.toArray()).toEqual([1, 2, 2.5, 3]);
|
||||
|
||||
list.remove(b);
|
||||
list.remove(d);
|
||||
|
||||
expect(list.toArray()).toEqual([1, 3]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ export type { LinkedListLike, LinkedListNode } from './types';
|
||||
* @returns {LinkedListNode<T>} The created node
|
||||
*/
|
||||
function createNode<T>(value: T): LinkedListNode<T> {
|
||||
return { value, prev: undefined, next: undefined };
|
||||
return { value, prev: undefined, next: undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,301 +24,307 @@ function createNode<T>(value: T): LinkedListNode<T> {
|
||||
* @template T The type of elements stored in the list
|
||||
*/
|
||||
export class LinkedList<T> implements LinkedListLike<T> {
|
||||
/**
|
||||
/**
|
||||
* The number of elements in the list
|
||||
*
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
private count = 0;
|
||||
private count = 0;
|
||||
|
||||
/**
|
||||
/**
|
||||
* The first node in the list
|
||||
*
|
||||
* @private
|
||||
* @type {LinkedListNode<T> | undefined}
|
||||
*/
|
||||
private first: LinkedListNode<T> | undefined;
|
||||
private first: LinkedListNode<T> | undefined;
|
||||
|
||||
/**
|
||||
/**
|
||||
* The last node in the list
|
||||
*
|
||||
* @private
|
||||
* @type {LinkedListNode<T> | undefined}
|
||||
*/
|
||||
private last: LinkedListNode<T> | undefined;
|
||||
private last: LinkedListNode<T> | undefined;
|
||||
|
||||
/**
|
||||
/**
|
||||
* Creates an instance of LinkedList
|
||||
*
|
||||
* @param {(T[] | T)} [initialValues] The initial values to add to the list
|
||||
*/
|
||||
constructor(initialValues?: T[] | T) {
|
||||
if (initialValues !== null && initialValues !== undefined) {
|
||||
const items = isArray(initialValues) ? initialValues : [initialValues];
|
||||
constructor(initialValues?: T[] | T) {
|
||||
if (initialValues !== null && initialValues !== undefined) {
|
||||
const items = isArray(initialValues) ? initialValues : [initialValues];
|
||||
|
||||
for (const item of items)
|
||||
this.pushBack(item);
|
||||
}
|
||||
for (const item of items)
|
||||
this.pushBack(item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Gets the number of elements in the list
|
||||
* @returns {number} The number of elements in the list
|
||||
*/
|
||||
public get length(): number {
|
||||
return this.count;
|
||||
}
|
||||
public get length(): number {
|
||||
return this.count;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Checks if the list is empty
|
||||
* @returns {boolean} `true` if the list is empty, `false` otherwise
|
||||
*/
|
||||
public get isEmpty(): boolean {
|
||||
return this.count === 0;
|
||||
}
|
||||
public get isEmpty(): boolean {
|
||||
return this.count === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Gets the first node
|
||||
* @returns {LinkedListNode<T> | undefined} The first node, or `undefined` if the list is empty
|
||||
*/
|
||||
public get head(): LinkedListNode<T> | undefined {
|
||||
return this.first;
|
||||
}
|
||||
public get head(): LinkedListNode<T> | undefined {
|
||||
return this.first;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Gets the last node
|
||||
* @returns {LinkedListNode<T> | undefined} The last node, or `undefined` if the list is empty
|
||||
*/
|
||||
public get tail(): LinkedListNode<T> | undefined {
|
||||
return this.last;
|
||||
}
|
||||
public get tail(): LinkedListNode<T> | undefined {
|
||||
return this.last;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Appends a value to the end of the list
|
||||
* @param {T} value The value to append
|
||||
* @returns {LinkedListNode<T>} The created node
|
||||
*/
|
||||
public pushBack(value: T): LinkedListNode<T> {
|
||||
const node = createNode(value);
|
||||
public pushBack(value: T): LinkedListNode<T> {
|
||||
const node = createNode(value);
|
||||
|
||||
if (this.last) {
|
||||
node.prev = this.last;
|
||||
this.last.next = node;
|
||||
this.last = node;
|
||||
} else {
|
||||
this.first = node;
|
||||
this.last = node;
|
||||
}
|
||||
|
||||
this.count++;
|
||||
|
||||
return node;
|
||||
if (this.last) {
|
||||
node.prev = this.last;
|
||||
this.last.next = node;
|
||||
this.last = node;
|
||||
}
|
||||
else {
|
||||
this.first = node;
|
||||
this.last = node;
|
||||
}
|
||||
|
||||
/**
|
||||
this.count++;
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends a value to the beginning of the list
|
||||
* @param {T} value The value to prepend
|
||||
* @returns {LinkedListNode<T>} The created node
|
||||
*/
|
||||
public pushFront(value: T): LinkedListNode<T> {
|
||||
const node = createNode(value);
|
||||
public pushFront(value: T): LinkedListNode<T> {
|
||||
const node = createNode(value);
|
||||
|
||||
if (this.first) {
|
||||
node.next = this.first;
|
||||
this.first.prev = node;
|
||||
this.first = node;
|
||||
} else {
|
||||
this.first = node;
|
||||
this.last = node;
|
||||
}
|
||||
|
||||
this.count++;
|
||||
|
||||
return node;
|
||||
if (this.first) {
|
||||
node.next = this.first;
|
||||
this.first.prev = node;
|
||||
this.first = node;
|
||||
}
|
||||
else {
|
||||
this.first = node;
|
||||
this.last = node;
|
||||
}
|
||||
|
||||
/**
|
||||
this.count++;
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns the last value
|
||||
* @returns {T | undefined} The last value, or `undefined` if the list is empty
|
||||
*/
|
||||
public popBack(): T | undefined {
|
||||
if (!this.last) return undefined;
|
||||
public popBack(): T | undefined {
|
||||
if (!this.last) return undefined;
|
||||
|
||||
const node = this.last;
|
||||
const node = this.last;
|
||||
|
||||
this.detach(node);
|
||||
this.detach(node);
|
||||
|
||||
return node.value;
|
||||
}
|
||||
return node.value;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Removes and returns the first value
|
||||
* @returns {T | undefined} The first value, or `undefined` if the list is empty
|
||||
*/
|
||||
public popFront(): T | undefined {
|
||||
if (!this.first) return undefined;
|
||||
public popFront(): T | undefined {
|
||||
if (!this.first) return undefined;
|
||||
|
||||
const node = this.first;
|
||||
const node = this.first;
|
||||
|
||||
this.detach(node);
|
||||
this.detach(node);
|
||||
|
||||
return node.value;
|
||||
}
|
||||
return node.value;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns the last value without removing it
|
||||
* @returns {T | undefined} The last value, or `undefined` if the list is empty
|
||||
*/
|
||||
public peekBack(): T | undefined {
|
||||
return this.last?.value;
|
||||
}
|
||||
public peekBack(): T | undefined {
|
||||
return this.last?.value;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns the first value without removing it
|
||||
* @returns {T | undefined} The first value, or `undefined` if the list is empty
|
||||
*/
|
||||
public peekFront(): T | undefined {
|
||||
return this.first?.value;
|
||||
}
|
||||
public peekFront(): T | undefined {
|
||||
return this.first?.value;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Inserts a value before the given node
|
||||
* @param {LinkedListNode<T>} node The reference node
|
||||
* @param {T} value The value to insert
|
||||
* @returns {LinkedListNode<T>} The created node
|
||||
*/
|
||||
public insertBefore(node: LinkedListNode<T>, value: T): LinkedListNode<T> {
|
||||
const newNode = createNode(value);
|
||||
public insertBefore(node: LinkedListNode<T>, value: T): LinkedListNode<T> {
|
||||
const newNode = createNode(value);
|
||||
|
||||
newNode.next = node;
|
||||
newNode.prev = node.prev;
|
||||
newNode.next = node;
|
||||
newNode.prev = node.prev;
|
||||
|
||||
if (node.prev) {
|
||||
node.prev.next = newNode;
|
||||
} else {
|
||||
this.first = newNode;
|
||||
}
|
||||
|
||||
node.prev = newNode;
|
||||
this.count++;
|
||||
|
||||
return newNode;
|
||||
if (node.prev) {
|
||||
node.prev.next = newNode;
|
||||
}
|
||||
else {
|
||||
this.first = newNode;
|
||||
}
|
||||
|
||||
/**
|
||||
node.prev = newNode;
|
||||
this.count++;
|
||||
|
||||
return newNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a value after the given node
|
||||
* @param {LinkedListNode<T>} node The reference node
|
||||
* @param {T} value The value to insert
|
||||
* @returns {LinkedListNode<T>} The created node
|
||||
*/
|
||||
public insertAfter(node: LinkedListNode<T>, value: T): LinkedListNode<T> {
|
||||
const newNode = createNode(value);
|
||||
public insertAfter(node: LinkedListNode<T>, value: T): LinkedListNode<T> {
|
||||
const newNode = createNode(value);
|
||||
|
||||
newNode.prev = node;
|
||||
newNode.next = node.next;
|
||||
newNode.prev = node;
|
||||
newNode.next = node.next;
|
||||
|
||||
if (node.next) {
|
||||
node.next.prev = newNode;
|
||||
} else {
|
||||
this.last = newNode;
|
||||
}
|
||||
|
||||
node.next = newNode;
|
||||
this.count++;
|
||||
|
||||
return newNode;
|
||||
if (node.next) {
|
||||
node.next.prev = newNode;
|
||||
}
|
||||
else {
|
||||
this.last = newNode;
|
||||
}
|
||||
|
||||
/**
|
||||
node.next = newNode;
|
||||
this.count++;
|
||||
|
||||
return newNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a node from the list by reference in O(1)
|
||||
* @param {LinkedListNode<T>} node The node to remove
|
||||
* @returns {T} The value of the removed node
|
||||
*/
|
||||
public remove(node: LinkedListNode<T>): T {
|
||||
this.detach(node);
|
||||
public remove(node: LinkedListNode<T>): T {
|
||||
this.detach(node);
|
||||
|
||||
return node.value;
|
||||
}
|
||||
return node.value;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Removes all elements from the list
|
||||
* @returns {this} The list instance for chaining
|
||||
*/
|
||||
public clear(): this {
|
||||
this.first = undefined;
|
||||
this.last = undefined;
|
||||
this.count = 0;
|
||||
public clear(): this {
|
||||
this.first = undefined;
|
||||
this.last = undefined;
|
||||
this.count = 0;
|
||||
|
||||
return this;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns a shallow copy of the list values as an array
|
||||
* @returns {T[]} Array of values from head to tail
|
||||
*/
|
||||
public toArray(): T[] {
|
||||
const result = Array.from<T>({ length: this.count });
|
||||
let current = this.first;
|
||||
let i = 0;
|
||||
public toArray(): T[] {
|
||||
const result = Array.from<T>({ length: this.count });
|
||||
let current = this.first;
|
||||
let i = 0;
|
||||
|
||||
while (current) {
|
||||
result[i++] = current.value;
|
||||
current = current.next;
|
||||
}
|
||||
|
||||
return result;
|
||||
while (current) {
|
||||
result[i++] = current.value;
|
||||
current = current.next;
|
||||
}
|
||||
|
||||
/**
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the list
|
||||
* @returns {string} String representation
|
||||
*/
|
||||
public toString(): string {
|
||||
return this.toArray().toString();
|
||||
}
|
||||
public toString(): string {
|
||||
return this.toArray().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Iterator over list values from head to tail
|
||||
*/
|
||||
public *[Symbol.iterator](): Iterator<T> {
|
||||
let current = this.first;
|
||||
public* [Symbol.iterator](): Iterator<T> {
|
||||
let current = this.first;
|
||||
|
||||
while (current) {
|
||||
yield current.value;
|
||||
current = current.next;
|
||||
}
|
||||
while (current) {
|
||||
yield current.value;
|
||||
current = current.next;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Async iterator over list values from head to tail
|
||||
*/
|
||||
public async *[Symbol.asyncIterator](): AsyncIterator<T> {
|
||||
for (const value of this)
|
||||
yield value;
|
||||
}
|
||||
public async* [Symbol.asyncIterator](): AsyncIterator<T> {
|
||||
for (const value of this)
|
||||
yield value;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Detaches a node from the list, updating head/tail and count
|
||||
*
|
||||
* @private
|
||||
* @param {LinkedListNode<T>} node The node to detach
|
||||
*/
|
||||
private detach(node: LinkedListNode<T>): void {
|
||||
if (node.prev) {
|
||||
node.prev.next = node.next;
|
||||
} else {
|
||||
this.first = node.next;
|
||||
}
|
||||
|
||||
if (node.next) {
|
||||
node.next.prev = node.prev;
|
||||
} else {
|
||||
this.last = node.prev;
|
||||
}
|
||||
|
||||
node.prev = undefined;
|
||||
node.next = undefined;
|
||||
this.count--;
|
||||
private detach(node: LinkedListNode<T>): void {
|
||||
if (node.prev) {
|
||||
node.prev.next = node.next;
|
||||
}
|
||||
else {
|
||||
this.first = node.next;
|
||||
}
|
||||
|
||||
if (node.next) {
|
||||
node.next.prev = node.prev;
|
||||
}
|
||||
else {
|
||||
this.last = node.prev;
|
||||
}
|
||||
|
||||
node.prev = undefined;
|
||||
node.next = undefined;
|
||||
this.count--;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
export interface LinkedListNode<T> {
|
||||
value: T;
|
||||
prev: LinkedListNode<T> | undefined;
|
||||
next: LinkedListNode<T> | undefined;
|
||||
value: T;
|
||||
prev: LinkedListNode<T> | undefined;
|
||||
next: LinkedListNode<T> | undefined;
|
||||
}
|
||||
|
||||
export interface LinkedListLike<T> extends Iterable<T>, AsyncIterable<T> {
|
||||
readonly length: number;
|
||||
readonly isEmpty: boolean;
|
||||
readonly length: number;
|
||||
readonly isEmpty: boolean;
|
||||
|
||||
readonly head: LinkedListNode<T> | undefined;
|
||||
readonly tail: LinkedListNode<T> | undefined;
|
||||
readonly head: LinkedListNode<T> | undefined;
|
||||
readonly tail: LinkedListNode<T> | undefined;
|
||||
|
||||
pushBack(value: T): LinkedListNode<T>;
|
||||
pushFront(value: T): LinkedListNode<T>;
|
||||
popBack(): T | undefined;
|
||||
popFront(): T | undefined;
|
||||
peekBack(): T | undefined;
|
||||
peekFront(): T | undefined;
|
||||
pushBack(value: T): LinkedListNode<T>;
|
||||
pushFront(value: T): LinkedListNode<T>;
|
||||
popBack(): T | undefined;
|
||||
popFront(): T | undefined;
|
||||
peekBack(): T | undefined;
|
||||
peekFront(): T | undefined;
|
||||
|
||||
insertBefore(node: LinkedListNode<T>, value: T): LinkedListNode<T>;
|
||||
insertAfter(node: LinkedListNode<T>, value: T): LinkedListNode<T>;
|
||||
remove(node: LinkedListNode<T>): T;
|
||||
insertBefore(node: LinkedListNode<T>, value: T): LinkedListNode<T>;
|
||||
insertAfter(node: LinkedListNode<T>, value: T): LinkedListNode<T>;
|
||||
remove(node: LinkedListNode<T>): T;
|
||||
|
||||
clear(): this;
|
||||
toArray(): T[];
|
||||
toString(): string;
|
||||
clear(): this;
|
||||
toArray(): T[];
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
@@ -3,211 +3,211 @@ import { describe, expect, it } from 'vitest';
|
||||
import { PriorityQueue } from '.';
|
||||
|
||||
describe('PriorityQueue', () => {
|
||||
describe('constructor', () => {
|
||||
it('should create an empty queue', () => {
|
||||
const pq = new PriorityQueue<number>();
|
||||
describe('constructor', () => {
|
||||
it('should create an empty queue', () => {
|
||||
const pq = new PriorityQueue<number>();
|
||||
|
||||
expect(pq.length).toBe(0);
|
||||
expect(pq.isEmpty).toBe(true);
|
||||
expect(pq.isFull).toBe(false);
|
||||
});
|
||||
|
||||
it('should create a queue from single value', () => {
|
||||
const pq = new PriorityQueue(42);
|
||||
|
||||
expect(pq.length).toBe(1);
|
||||
expect(pq.peek()).toBe(42);
|
||||
});
|
||||
|
||||
it('should create a queue from array', () => {
|
||||
const pq = new PriorityQueue([5, 3, 8, 1, 4]);
|
||||
|
||||
expect(pq.length).toBe(5);
|
||||
expect(pq.peek()).toBe(1);
|
||||
});
|
||||
|
||||
it('should throw if initial values exceed maxSize', () => {
|
||||
expect(() => new PriorityQueue([1, 2, 3], { maxSize: 2 }))
|
||||
.toThrow('Initial values exceed maxSize');
|
||||
});
|
||||
expect(pq.length).toBe(0);
|
||||
expect(pq.isEmpty).toBe(true);
|
||||
expect(pq.isFull).toBe(false);
|
||||
});
|
||||
|
||||
describe('enqueue', () => {
|
||||
it('should enqueue elements by priority', () => {
|
||||
const pq = new PriorityQueue<number>();
|
||||
it('should create a queue from single value', () => {
|
||||
const pq = new PriorityQueue(42);
|
||||
|
||||
pq.enqueue(5);
|
||||
pq.enqueue(1);
|
||||
pq.enqueue(3);
|
||||
|
||||
expect(pq.peek()).toBe(1);
|
||||
expect(pq.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should throw when queue is full', () => {
|
||||
const pq = new PriorityQueue<number>(undefined, { maxSize: 2 });
|
||||
|
||||
pq.enqueue(1);
|
||||
pq.enqueue(2);
|
||||
|
||||
expect(() => pq.enqueue(3)).toThrow('PriorityQueue is full');
|
||||
});
|
||||
expect(pq.length).toBe(1);
|
||||
expect(pq.peek()).toBe(42);
|
||||
});
|
||||
|
||||
describe('dequeue', () => {
|
||||
it('should return undefined for empty queue', () => {
|
||||
const pq = new PriorityQueue<number>();
|
||||
it('should create a queue from array', () => {
|
||||
const pq = new PriorityQueue([5, 3, 8, 1, 4]);
|
||||
|
||||
expect(pq.dequeue()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should dequeue elements in priority order (min-heap)', () => {
|
||||
const pq = new PriorityQueue([5, 3, 8, 1, 4]);
|
||||
const result: number[] = [];
|
||||
|
||||
while (!pq.isEmpty) {
|
||||
result.push(pq.dequeue()!);
|
||||
}
|
||||
|
||||
expect(result).toEqual([1, 3, 4, 5, 8]);
|
||||
});
|
||||
|
||||
it('should dequeue elements in priority order (max-heap)', () => {
|
||||
const pq = new PriorityQueue([5, 3, 8, 1, 4], {
|
||||
comparator: (a, b) => b - a,
|
||||
});
|
||||
const result: number[] = [];
|
||||
|
||||
while (!pq.isEmpty) {
|
||||
result.push(pq.dequeue()!);
|
||||
}
|
||||
|
||||
expect(result).toEqual([8, 5, 4, 3, 1]);
|
||||
});
|
||||
expect(pq.length).toBe(5);
|
||||
expect(pq.peek()).toBe(1);
|
||||
});
|
||||
|
||||
describe('peek', () => {
|
||||
it('should return undefined for empty queue', () => {
|
||||
const pq = new PriorityQueue<number>();
|
||||
it('should throw if initial values exceed maxSize', () => {
|
||||
expect(() => new PriorityQueue([1, 2, 3], { maxSize: 2 }))
|
||||
.toThrow('Initial values exceed maxSize');
|
||||
});
|
||||
});
|
||||
|
||||
expect(pq.peek()).toBeUndefined();
|
||||
});
|
||||
describe('enqueue', () => {
|
||||
it('should enqueue elements by priority', () => {
|
||||
const pq = new PriorityQueue<number>();
|
||||
|
||||
it('should return highest-priority element without removing', () => {
|
||||
const pq = new PriorityQueue([5, 1, 3]);
|
||||
pq.enqueue(5);
|
||||
pq.enqueue(1);
|
||||
pq.enqueue(3);
|
||||
|
||||
expect(pq.peek()).toBe(1);
|
||||
expect(pq.length).toBe(3);
|
||||
});
|
||||
expect(pq.peek()).toBe(1);
|
||||
expect(pq.length).toBe(3);
|
||||
});
|
||||
|
||||
describe('isFull', () => {
|
||||
it('should be false when no maxSize', () => {
|
||||
const pq = new PriorityQueue([1, 2, 3]);
|
||||
it('should throw when queue is full', () => {
|
||||
const pq = new PriorityQueue<number>(undefined, { maxSize: 2 });
|
||||
|
||||
expect(pq.isFull).toBe(false);
|
||||
});
|
||||
pq.enqueue(1);
|
||||
pq.enqueue(2);
|
||||
|
||||
it('should be true when at maxSize', () => {
|
||||
const pq = new PriorityQueue([1, 2], { maxSize: 2 });
|
||||
expect(() => pq.enqueue(3)).toThrow('PriorityQueue is full');
|
||||
});
|
||||
});
|
||||
|
||||
expect(pq.isFull).toBe(true);
|
||||
});
|
||||
describe('dequeue', () => {
|
||||
it('should return undefined for empty queue', () => {
|
||||
const pq = new PriorityQueue<number>();
|
||||
|
||||
it('should become false after dequeue', () => {
|
||||
const pq = new PriorityQueue([1, 2], { maxSize: 2 });
|
||||
|
||||
pq.dequeue();
|
||||
|
||||
expect(pq.isFull).toBe(false);
|
||||
});
|
||||
expect(pq.dequeue()).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('should remove all elements', () => {
|
||||
const pq = new PriorityQueue([1, 2, 3]);
|
||||
it('should dequeue elements in priority order (min-heap)', () => {
|
||||
const pq = new PriorityQueue([5, 3, 8, 1, 4]);
|
||||
const result: number[] = [];
|
||||
|
||||
const result = pq.clear();
|
||||
while (!pq.isEmpty) {
|
||||
result.push(pq.dequeue()!);
|
||||
}
|
||||
|
||||
expect(pq.length).toBe(0);
|
||||
expect(pq.isEmpty).toBe(true);
|
||||
expect(result).toBe(pq);
|
||||
});
|
||||
expect(result).toEqual([1, 3, 4, 5, 8]);
|
||||
});
|
||||
|
||||
describe('toArray', () => {
|
||||
it('should return empty array for empty queue', () => {
|
||||
const pq = new PriorityQueue<number>();
|
||||
it('should dequeue elements in priority order (max-heap)', () => {
|
||||
const pq = new PriorityQueue([5, 3, 8, 1, 4], {
|
||||
comparator: (a, b) => b - a,
|
||||
});
|
||||
const result: number[] = [];
|
||||
|
||||
expect(pq.toArray()).toEqual([]);
|
||||
});
|
||||
while (!pq.isEmpty) {
|
||||
result.push(pq.dequeue()!);
|
||||
}
|
||||
|
||||
it('should return a shallow copy', () => {
|
||||
const pq = new PriorityQueue([3, 1, 2]);
|
||||
const arr = pq.toArray();
|
||||
expect(result).toEqual([8, 5, 4, 3, 1]);
|
||||
});
|
||||
});
|
||||
|
||||
arr.push(99);
|
||||
describe('peek', () => {
|
||||
it('should return undefined for empty queue', () => {
|
||||
const pq = new PriorityQueue<number>();
|
||||
|
||||
expect(pq.length).toBe(3);
|
||||
});
|
||||
expect(pq.peek()).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('toString', () => {
|
||||
it('should return formatted string', () => {
|
||||
const pq = new PriorityQueue([1, 2, 3]);
|
||||
it('should return highest-priority element without removing', () => {
|
||||
const pq = new PriorityQueue([5, 1, 3]);
|
||||
|
||||
expect(pq.toString()).toBe('PriorityQueue(3)');
|
||||
});
|
||||
expect(pq.peek()).toBe(1);
|
||||
expect(pq.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFull', () => {
|
||||
it('should be false when no maxSize', () => {
|
||||
const pq = new PriorityQueue([1, 2, 3]);
|
||||
|
||||
expect(pq.isFull).toBe(false);
|
||||
});
|
||||
|
||||
describe('iterator', () => {
|
||||
it('should iterate over elements', () => {
|
||||
const pq = new PriorityQueue([5, 3, 1]);
|
||||
const elements = [...pq];
|
||||
it('should be true when at maxSize', () => {
|
||||
const pq = new PriorityQueue([1, 2], { maxSize: 2 });
|
||||
|
||||
expect(elements.length).toBe(3);
|
||||
});
|
||||
expect(pq.isFull).toBe(true);
|
||||
});
|
||||
|
||||
describe('custom comparator', () => {
|
||||
it('should work with object priority', () => {
|
||||
interface Job {
|
||||
priority: number;
|
||||
name: string;
|
||||
}
|
||||
it('should become false after dequeue', () => {
|
||||
const pq = new PriorityQueue([1, 2], { maxSize: 2 });
|
||||
|
||||
const pq = new PriorityQueue<Job>(
|
||||
[
|
||||
{ priority: 3, name: 'low' },
|
||||
{ priority: 1, name: 'critical' },
|
||||
{ priority: 2, name: 'normal' },
|
||||
],
|
||||
{ comparator: (a, b) => a.priority - b.priority },
|
||||
);
|
||||
pq.dequeue();
|
||||
|
||||
expect(pq.dequeue()?.name).toBe('critical');
|
||||
expect(pq.dequeue()?.name).toBe('normal');
|
||||
expect(pq.dequeue()?.name).toBe('low');
|
||||
});
|
||||
expect(pq.isFull).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('should remove all elements', () => {
|
||||
const pq = new PriorityQueue([1, 2, 3]);
|
||||
|
||||
const result = pq.clear();
|
||||
|
||||
expect(pq.length).toBe(0);
|
||||
expect(pq.isEmpty).toBe(true);
|
||||
expect(result).toBe(pq);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toArray', () => {
|
||||
it('should return empty array for empty queue', () => {
|
||||
const pq = new PriorityQueue<number>();
|
||||
|
||||
expect(pq.toArray()).toEqual([]);
|
||||
});
|
||||
|
||||
describe('interleaved operations', () => {
|
||||
it('should maintain priority with mixed enqueue and dequeue', () => {
|
||||
const pq = new PriorityQueue<number>();
|
||||
it('should return a shallow copy', () => {
|
||||
const pq = new PriorityQueue([3, 1, 2]);
|
||||
const arr = pq.toArray();
|
||||
|
||||
pq.enqueue(10);
|
||||
pq.enqueue(5);
|
||||
expect(pq.dequeue()).toBe(5);
|
||||
arr.push(99);
|
||||
|
||||
pq.enqueue(3);
|
||||
pq.enqueue(7);
|
||||
expect(pq.dequeue()).toBe(3);
|
||||
|
||||
pq.enqueue(1);
|
||||
expect(pq.dequeue()).toBe(1);
|
||||
expect(pq.dequeue()).toBe(7);
|
||||
expect(pq.dequeue()).toBe(10);
|
||||
expect(pq.dequeue()).toBeUndefined();
|
||||
});
|
||||
expect(pq.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toString', () => {
|
||||
it('should return formatted string', () => {
|
||||
const pq = new PriorityQueue([1, 2, 3]);
|
||||
|
||||
expect(pq.toString()).toBe('PriorityQueue(3)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('iterator', () => {
|
||||
it('should iterate over elements', () => {
|
||||
const pq = new PriorityQueue([5, 3, 1]);
|
||||
const elements = [...pq];
|
||||
|
||||
expect(elements.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom comparator', () => {
|
||||
it('should work with object priority', () => {
|
||||
interface Job {
|
||||
priority: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const pq = new PriorityQueue<Job>(
|
||||
[
|
||||
{ priority: 3, name: 'low' },
|
||||
{ priority: 1, name: 'critical' },
|
||||
{ priority: 2, name: 'normal' },
|
||||
],
|
||||
{ comparator: (a, b) => a.priority - b.priority },
|
||||
);
|
||||
|
||||
expect(pq.dequeue()?.name).toBe('critical');
|
||||
expect(pq.dequeue()?.name).toBe('normal');
|
||||
expect(pq.dequeue()?.name).toBe('low');
|
||||
});
|
||||
});
|
||||
|
||||
describe('interleaved operations', () => {
|
||||
it('should maintain priority with mixed enqueue and dequeue', () => {
|
||||
const pq = new PriorityQueue<number>();
|
||||
|
||||
pq.enqueue(10);
|
||||
pq.enqueue(5);
|
||||
expect(pq.dequeue()).toBe(5);
|
||||
|
||||
pq.enqueue(3);
|
||||
pq.enqueue(7);
|
||||
expect(pq.dequeue()).toBe(3);
|
||||
|
||||
pq.enqueue(1);
|
||||
expect(pq.dequeue()).toBe(1);
|
||||
expect(pq.dequeue()).toBe(7);
|
||||
expect(pq.dequeue()).toBe(10);
|
||||
expect(pq.dequeue()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,8 +5,8 @@ export type { PriorityQueueLike } from './types';
|
||||
export type { Comparator } from './types';
|
||||
|
||||
export interface PriorityQueueOptions<T> {
|
||||
comparator?: Comparator<T>;
|
||||
maxSize?: number;
|
||||
comparator?: Comparator<T>;
|
||||
maxSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,126 +19,126 @@ export interface PriorityQueueOptions<T> {
|
||||
* @template T The type of elements stored in the queue
|
||||
*/
|
||||
export class PriorityQueue<T> implements PriorityQueueLike<T> {
|
||||
/**
|
||||
/**
|
||||
* The maximum number of elements the queue can hold
|
||||
*
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
private readonly maxSize: number;
|
||||
private readonly maxSize: number;
|
||||
|
||||
/**
|
||||
/**
|
||||
* Internal binary heap backing the queue
|
||||
*
|
||||
* @private
|
||||
* @type {BinaryHeap<T>}
|
||||
*/
|
||||
private readonly heap: BinaryHeap<T>;
|
||||
private readonly heap: BinaryHeap<T>;
|
||||
|
||||
/**
|
||||
/**
|
||||
* Creates an instance of PriorityQueue
|
||||
*
|
||||
* @param {(T[] | T)} [initialValues] The initial values to add to the queue
|
||||
* @param {PriorityQueueOptions<T>} [options] Queue configuration
|
||||
*/
|
||||
constructor(initialValues?: T[] | T, options?: PriorityQueueOptions<T>) {
|
||||
this.maxSize = options?.maxSize ?? Infinity;
|
||||
this.heap = new BinaryHeap(initialValues, { comparator: options?.comparator });
|
||||
constructor(initialValues?: T[] | T, options?: PriorityQueueOptions<T>) {
|
||||
this.maxSize = options?.maxSize ?? Infinity;
|
||||
this.heap = new BinaryHeap(initialValues, { comparator: options?.comparator });
|
||||
|
||||
if (this.heap.length > this.maxSize) {
|
||||
throw new RangeError('Initial values exceed maxSize');
|
||||
}
|
||||
if (this.heap.length > this.maxSize) {
|
||||
throw new RangeError('Initial values exceed maxSize');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Gets the number of elements in the queue
|
||||
* @returns {number} The number of elements in the queue
|
||||
*/
|
||||
public get length(): number {
|
||||
return this.heap.length;
|
||||
}
|
||||
public get length(): number {
|
||||
return this.heap.length;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Checks if the queue is empty
|
||||
* @returns {boolean} `true` if the queue is empty, `false` otherwise
|
||||
*/
|
||||
public get isEmpty(): boolean {
|
||||
return this.heap.isEmpty;
|
||||
}
|
||||
public get isEmpty(): boolean {
|
||||
return this.heap.isEmpty;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Checks if the queue is full
|
||||
* @returns {boolean} `true` if the queue has reached maxSize, `false` otherwise
|
||||
*/
|
||||
public get isFull(): boolean {
|
||||
return this.heap.length >= this.maxSize;
|
||||
}
|
||||
public get isFull(): boolean {
|
||||
return this.heap.length >= this.maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Enqueues an element by priority
|
||||
* @param {T} element The element to enqueue
|
||||
* @throws {RangeError} If the queue is full
|
||||
*/
|
||||
public enqueue(element: T): void {
|
||||
if (this.isFull)
|
||||
throw new RangeError('PriorityQueue is full');
|
||||
public enqueue(element: T): void {
|
||||
if (this.isFull)
|
||||
throw new RangeError('PriorityQueue is full');
|
||||
|
||||
this.heap.push(element);
|
||||
}
|
||||
this.heap.push(element);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Dequeues the highest-priority element
|
||||
* @returns {T | undefined} The highest-priority element, or `undefined` if empty
|
||||
*/
|
||||
public dequeue(): T | undefined {
|
||||
return this.heap.pop();
|
||||
}
|
||||
public dequeue(): T | undefined {
|
||||
return this.heap.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns the highest-priority element without removing it
|
||||
* @returns {T | undefined} The highest-priority element, or `undefined` if empty
|
||||
*/
|
||||
public peek(): T | undefined {
|
||||
return this.heap.peek();
|
||||
}
|
||||
public peek(): T | undefined {
|
||||
return this.heap.peek();
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Removes all elements from the queue
|
||||
* @returns {this} The queue instance for chaining
|
||||
*/
|
||||
public clear(): this {
|
||||
this.heap.clear();
|
||||
return this;
|
||||
}
|
||||
public clear(): this {
|
||||
this.heap.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns a shallow copy of elements in heap order
|
||||
* @returns {T[]} Array of elements
|
||||
*/
|
||||
public toArray(): T[] {
|
||||
return this.heap.toArray();
|
||||
}
|
||||
public toArray(): T[] {
|
||||
return this.heap.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns a string representation of the queue
|
||||
* @returns {string} String representation
|
||||
*/
|
||||
public toString(): string {
|
||||
return `PriorityQueue(${this.heap.length})`;
|
||||
}
|
||||
public toString(): string {
|
||||
return `PriorityQueue(${this.heap.length})`;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Iterator over queue elements in heap order
|
||||
*/
|
||||
public *[Symbol.iterator](): Iterator<T> {
|
||||
yield* this.heap;
|
||||
}
|
||||
public* [Symbol.iterator](): Iterator<T> {
|
||||
yield* this.heap;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Async iterator over queue elements in heap order
|
||||
*/
|
||||
public async *[Symbol.asyncIterator](): AsyncIterator<T> {
|
||||
for (const element of this.heap)
|
||||
yield element;
|
||||
}
|
||||
public async* [Symbol.asyncIterator](): AsyncIterator<T> {
|
||||
for (const element of this.heap)
|
||||
yield element;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { Comparator } from '../BinaryHeap';
|
||||
|
||||
export interface PriorityQueueLike<T> extends Iterable<T>, AsyncIterable<T> {
|
||||
readonly length: number;
|
||||
readonly isEmpty: boolean;
|
||||
readonly isFull: boolean;
|
||||
readonly length: number;
|
||||
readonly isEmpty: boolean;
|
||||
readonly isFull: boolean;
|
||||
|
||||
enqueue(element: T): void;
|
||||
dequeue(): T | undefined;
|
||||
peek(): T | undefined;
|
||||
clear(): this;
|
||||
toArray(): T[];
|
||||
toString(): string;
|
||||
enqueue(element: T): void;
|
||||
dequeue(): T | undefined;
|
||||
peek(): T | undefined;
|
||||
clear(): this;
|
||||
toArray(): T[];
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
export type { Comparator };
|
||||
|
||||
@@ -133,7 +133,7 @@ export class Queue<T> implements QueueLike<T> {
|
||||
*
|
||||
* @returns {AsyncIterableIterator<T>}
|
||||
*/
|
||||
async *[Symbol.asyncIterator]() {
|
||||
async* [Symbol.asyncIterator]() {
|
||||
for (const element of this.deque)
|
||||
yield element;
|
||||
}
|
||||
|
||||
@@ -112,8 +112,8 @@ describe('stack', () => {
|
||||
for await (const element of stack) {
|
||||
elements.push(element);
|
||||
}
|
||||
|
||||
|
||||
expect(elements).toEqual([3, 2, 1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { StackLike } from './types';
|
||||
export type { StackLike } from './types';
|
||||
|
||||
export interface StackOptions {
|
||||
maxSize?: number;
|
||||
maxSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -18,138 +18,138 @@ export interface StackOptions {
|
||||
* @template T The type of elements stored in the stack
|
||||
*/
|
||||
export class Stack<T> implements StackLike<T> {
|
||||
/**
|
||||
/**
|
||||
* The maximum number of elements that the stack can hold
|
||||
*
|
||||
*
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
private readonly maxSize: number;
|
||||
private readonly maxSize: number;
|
||||
|
||||
/**
|
||||
/**
|
||||
* The stack data structure
|
||||
*
|
||||
*
|
||||
* @private
|
||||
* @type {T[]}
|
||||
*/
|
||||
private readonly stack: T[];
|
||||
private readonly stack: T[];
|
||||
|
||||
/**
|
||||
/**
|
||||
* Creates an instance of Stack
|
||||
*
|
||||
*
|
||||
* @param {(T[] | T)} [initialValues] The initial values to add to the stack
|
||||
* @param {StackOptions} [options] The options for the stack
|
||||
* @memberof Stack
|
||||
*/
|
||||
constructor(initialValues?: T[] | T, options?: StackOptions) {
|
||||
this.maxSize = options?.maxSize ?? Infinity;
|
||||
this.stack = isArray(initialValues) ? initialValues : initialValues ? [initialValues] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
constructor(initialValues?: T[] | T, options?: StackOptions) {
|
||||
this.maxSize = options?.maxSize ?? Infinity;
|
||||
this.stack = isArray(initialValues) ? initialValues : initialValues ? [initialValues] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of elements in the stack
|
||||
* @returns {number} The number of elements in the stack
|
||||
*/
|
||||
public get length() {
|
||||
return this.stack.length;
|
||||
}
|
||||
public get length() {
|
||||
return this.stack.length;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Checks if the stack is empty
|
||||
* @returns {boolean} `true` if the stack is empty, `false` otherwise
|
||||
*/
|
||||
public get isEmpty() {
|
||||
return this.stack.length === 0;
|
||||
}
|
||||
public get isEmpty() {
|
||||
return this.stack.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Checks if the stack is full
|
||||
* @returns {boolean} `true` if the stack is full, `false` otherwise
|
||||
*/
|
||||
public get isFull() {
|
||||
return this.stack.length === this.maxSize;
|
||||
}
|
||||
public get isFull() {
|
||||
return this.stack.length === this.maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Pushes an element onto the stack
|
||||
* @param {T} element The element to push onto the stack
|
||||
* @returns {this}
|
||||
* @throws {RangeError} If the stack is full
|
||||
*/
|
||||
public push(element: T) {
|
||||
if (this.isFull)
|
||||
throw new RangeError('Stack is full');
|
||||
|
||||
this.stack.push(element);
|
||||
public push(element: T) {
|
||||
if (this.isFull)
|
||||
throw new RangeError('Stack is full');
|
||||
|
||||
return this;
|
||||
}
|
||||
this.stack.push(element);
|
||||
|
||||
/**
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops an element from the stack
|
||||
* @returns {T | undefined} The element popped from the stack
|
||||
*/
|
||||
public pop() {
|
||||
return this.stack.pop();
|
||||
}
|
||||
public pop() {
|
||||
return this.stack.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Peeks at the top element of the stack
|
||||
* @returns {T | undefined} The top element of the stack
|
||||
*/
|
||||
public peek() {
|
||||
if (this.isEmpty)
|
||||
return undefined;
|
||||
|
||||
return last(this.stack);
|
||||
}
|
||||
public peek() {
|
||||
if (this.isEmpty)
|
||||
return undefined;
|
||||
|
||||
/**
|
||||
return last(this.stack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the stack
|
||||
*
|
||||
*
|
||||
* @returns {this}
|
||||
*/
|
||||
public clear() {
|
||||
this.stack.length = 0;
|
||||
public clear() {
|
||||
this.stack.length = 0;
|
||||
|
||||
return this;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Converts the stack to an array
|
||||
*
|
||||
*
|
||||
* @returns {T[]}
|
||||
*/
|
||||
public toArray() {
|
||||
return this.stack.toReversed();
|
||||
}
|
||||
public toArray() {
|
||||
return this.stack.toReversed();
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns a string representation of the stack
|
||||
*
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
public toString() {
|
||||
return this.toArray().toString();
|
||||
}
|
||||
public toString() {
|
||||
return this.toArray().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns an iterator for the stack
|
||||
*
|
||||
*
|
||||
* @returns {IterableIterator<T>}
|
||||
*/
|
||||
public [Symbol.iterator]() {
|
||||
return this.toArray()[Symbol.iterator]();
|
||||
}
|
||||
public [Symbol.iterator]() {
|
||||
return this.toArray()[Symbol.iterator]();
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns an async iterator for the stack
|
||||
*
|
||||
*
|
||||
* @returns {AsyncIterableIterator<T>}
|
||||
*/
|
||||
public async *[Symbol.asyncIterator]() {
|
||||
for (const element of this.toArray()) {
|
||||
yield element;
|
||||
}
|
||||
public async* [Symbol.asyncIterator]() {
|
||||
for (const element of this.toArray()) {
|
||||
yield element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ export * from './Deque';
|
||||
export * from './LinkedList';
|
||||
export * from './PriorityQueue';
|
||||
export * from './Queue';
|
||||
export * from './Stack';
|
||||
export * from './Stack';
|
||||
|
||||
@@ -14,21 +14,21 @@ describe('SyncMutex', () => {
|
||||
|
||||
it('lock the mutex', () => {
|
||||
mutex.lock();
|
||||
|
||||
|
||||
expect(mutex.isLocked).toBe(true);
|
||||
});
|
||||
|
||||
it('remain locked when locked multiple times', () => {
|
||||
mutex.lock();
|
||||
mutex.lock();
|
||||
|
||||
|
||||
expect(mutex.isLocked).toBe(true);
|
||||
});
|
||||
|
||||
it('unlock a locked mutex', () => {
|
||||
mutex.lock();
|
||||
mutex.unlock();
|
||||
|
||||
|
||||
expect(mutex.isLocked).toBe(false);
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('SyncMutex', () => {
|
||||
it('execute a callback when unlocked', async () => {
|
||||
const callback = vi.fn(() => 'done');
|
||||
const result = await mutex.execute(callback);
|
||||
|
||||
|
||||
expect(result).toBe('done');
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
@@ -58,7 +58,7 @@ describe('SyncMutex', () => {
|
||||
it('execute a promise callback when unlocked', async () => {
|
||||
const callback = vi.fn(() => Promise.resolve('done'));
|
||||
const result = await mutex.execute(callback);
|
||||
|
||||
|
||||
expect(result).toBe('done');
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
@@ -71,7 +71,7 @@ describe('SyncMutex', () => {
|
||||
mutex.execute(callback),
|
||||
mutex.execute(callback),
|
||||
]);
|
||||
|
||||
|
||||
expect(result).toEqual(['done', undefined, undefined]);
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -88,7 +88,7 @@ describe('SyncMutex', () => {
|
||||
it('unlocks after executing a callback', async () => {
|
||||
const callback = vi.fn(() => 'done');
|
||||
await mutex.execute(callback);
|
||||
|
||||
|
||||
expect(mutex.isLocked).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
* @name SyncMutex
|
||||
* @category Utils
|
||||
* @description A simple synchronous mutex to provide more readable locking and unlocking of code blocks
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const mutex = new SyncMutex();
|
||||
*
|
||||
*
|
||||
* mutex.lock();
|
||||
*
|
||||
* mutex.unlock();
|
||||
*
|
||||
*
|
||||
* const result = await mutex.execute(() => {
|
||||
* // do something
|
||||
* return Promise.resolve('done');
|
||||
* return Promise.resolve('done');
|
||||
* });
|
||||
*
|
||||
*
|
||||
* @since 0.0.5
|
||||
*/
|
||||
export class SyncMutex {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './levenshtein-distance';
|
||||
export * from './trigram-distance';
|
||||
// TODO: Template is not implemented yet
|
||||
// export * from './template';
|
||||
// export * from './template';
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {levenshteinDistance} from '.';
|
||||
|
||||
describe('levenshteinDistance', () => {
|
||||
it('calculate edit distance between two strings', () => {
|
||||
// just one substitution I at the beginning
|
||||
expect(levenshteinDistance('islander', 'slander')).toBe(1);
|
||||
|
||||
// substitution M->K, T->M and add an A to the end
|
||||
expect(levenshteinDistance('mart', 'karma')).toBe(3);
|
||||
|
||||
// substitution K->S, E->I and insert G at the end
|
||||
expect(levenshteinDistance('kitten', 'sitting')).toBe(3);
|
||||
|
||||
// should add 4 letters FOOT at the beginning
|
||||
expect(levenshteinDistance('ball', 'football')).toBe(4);
|
||||
|
||||
// should delete 4 letters FOOT at the beginning
|
||||
expect(levenshteinDistance('football', 'foot')).toBe(4);
|
||||
|
||||
// needs to substitute the first 5 chars INTEN->EXECU
|
||||
expect(levenshteinDistance('intention', 'execution')).toBe(5);
|
||||
});
|
||||
import { levenshteinDistance } from '.';
|
||||
|
||||
it('handle empty strings', () => {
|
||||
expect(levenshteinDistance('', '')).toBe(0);
|
||||
expect(levenshteinDistance('a', '')).toBe(1);
|
||||
expect(levenshteinDistance('', 'a')).toBe(1);
|
||||
expect(levenshteinDistance('abc', '')).toBe(3);
|
||||
expect(levenshteinDistance('', 'abc')).toBe(3);
|
||||
});
|
||||
});
|
||||
describe('levenshteinDistance', () => {
|
||||
it('calculate edit distance between two strings', () => {
|
||||
// just one substitution I at the beginning
|
||||
expect(levenshteinDistance('islander', 'slander')).toBe(1);
|
||||
|
||||
// substitution M->K, T->M and add an A to the end
|
||||
expect(levenshteinDistance('mart', 'karma')).toBe(3);
|
||||
|
||||
// substitution K->S, E->I and insert G at the end
|
||||
expect(levenshteinDistance('kitten', 'sitting')).toBe(3);
|
||||
|
||||
// should add 4 letters FOOT at the beginning
|
||||
expect(levenshteinDistance('ball', 'football')).toBe(4);
|
||||
|
||||
// should delete 4 letters FOOT at the beginning
|
||||
expect(levenshteinDistance('football', 'foot')).toBe(4);
|
||||
|
||||
// needs to substitute the first 5 chars INTEN->EXECU
|
||||
expect(levenshteinDistance('intention', 'execution')).toBe(5);
|
||||
});
|
||||
|
||||
it('handle empty strings', () => {
|
||||
expect(levenshteinDistance('', '')).toBe(0);
|
||||
expect(levenshteinDistance('a', '')).toBe(1);
|
||||
expect(levenshteinDistance('', 'a')).toBe(1);
|
||||
expect(levenshteinDistance('abc', '')).toBe(3);
|
||||
expect(levenshteinDistance('', 'abc')).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @name levenshteinDistance
|
||||
* @category Text
|
||||
* @description Calculate the Levenshtein distance between two strings
|
||||
*
|
||||
*
|
||||
* @param {string} left First string
|
||||
* @param {string} right Second string
|
||||
* @returns {number} The Levenshtein distance between the two strings
|
||||
@@ -10,37 +10,37 @@
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export function levenshteinDistance(left: string, right: string): number {
|
||||
if (left === right) return 0;
|
||||
if (left === right) return 0;
|
||||
|
||||
if (left.length === 0) return right.length;
|
||||
if (right.length === 0) return left.length;
|
||||
if (left.length === 0) return right.length;
|
||||
if (right.length === 0) return left.length;
|
||||
|
||||
// Create empty edit distance matrix for all possible modifications of
|
||||
// substrings of left to substrings of right
|
||||
const distanceMatrix = Array(right.length + 1).fill(null).map(() => Array(left.length + 1).fill(null));
|
||||
// Create empty edit distance matrix for all possible modifications of
|
||||
// substrings of left to substrings of right
|
||||
const distanceMatrix = Array(right.length + 1).fill(null).map(() => Array(left.length + 1).fill(null));
|
||||
|
||||
// Fill the first row of the matrix
|
||||
// If this is the first row, we're transforming from an empty string to left
|
||||
// In this case, the number of operations equals the length of left substring
|
||||
for (let i = 0; i <= left.length; i++)
|
||||
distanceMatrix[0]![i]! = i;
|
||||
// Fill the first row of the matrix
|
||||
// If this is the first row, we're transforming from an empty string to left
|
||||
// In this case, the number of operations equals the length of left substring
|
||||
for (let i = 0; i <= left.length; i++)
|
||||
distanceMatrix[0]![i]! = i;
|
||||
|
||||
// Fill the first column of the matrix
|
||||
// If this is the first column, we're transforming empty string to right
|
||||
// In this case, the number of operations equals the length of right substring
|
||||
for (let j = 0; j <= right.length; j++)
|
||||
distanceMatrix[j]![0]! = j;
|
||||
// Fill the first column of the matrix
|
||||
// If this is the first column, we're transforming empty string to right
|
||||
// In this case, the number of operations equals the length of right substring
|
||||
for (let j = 0; j <= right.length; j++)
|
||||
distanceMatrix[j]![0]! = j;
|
||||
|
||||
for (let j = 1; j <= right.length; j++) {
|
||||
for (let i = 1; i <= left.length; i++) {
|
||||
const indicator = left[i - 1] === right[j - 1] ? 0 : 1;
|
||||
distanceMatrix[j]![i]! = Math.min(
|
||||
distanceMatrix[j]![i - 1]! + 1, // deletion
|
||||
distanceMatrix[j - 1]![i]! + 1, // insertion
|
||||
distanceMatrix[j - 1]![i - 1]! + indicator // substitution
|
||||
);
|
||||
}
|
||||
for (let j = 1; j <= right.length; j++) {
|
||||
for (let i = 1; i <= left.length; i++) {
|
||||
const indicator = left[i - 1] === right[j - 1] ? 0 : 1;
|
||||
distanceMatrix[j]![i]! = Math.min(
|
||||
distanceMatrix[j]![i - 1]! + 1, // deletion
|
||||
distanceMatrix[j - 1]![i]! + 1, // insertion
|
||||
distanceMatrix[j - 1]![i - 1]! + indicator, // substitution
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return distanceMatrix[right.length]![left.length]!;
|
||||
return distanceMatrix[right.length]![left.length]!;
|
||||
}
|
||||
|
||||
@@ -102,4 +102,4 @@ describe.skip('template', () => {
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,16 +33,16 @@ describe.skip('templateObject', () => {
|
||||
});
|
||||
|
||||
it('replace template placeholders with nested values from args', () => {
|
||||
const result = templateObject('Hello {{user.name}, your address {user.addresses.0.street}', {
|
||||
user: {
|
||||
name: 'John Doe',
|
||||
addresses: [
|
||||
{ street: '123 Main St', city: 'Springfield'},
|
||||
{ street: '456 Elm St', city: 'Shelbyville'}
|
||||
]
|
||||
}
|
||||
});
|
||||
const result = templateObject('Hello {{user.name}, your address {user.addresses.0.street}', {
|
||||
user: {
|
||||
name: 'John Doe',
|
||||
addresses: [
|
||||
{ street: '123 Main St', city: 'Springfield' },
|
||||
{ street: '456 Elm St', city: 'Shelbyville' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe('Hello {John Doe, your address 123 Main St');
|
||||
expect(result).toBe('Hello {John Doe, your address 123 Main St');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,14 +19,14 @@ const TEMPLATE_PLACEHOLDER = /\{\s*([^{}]+?)\s*\}/gm;
|
||||
|
||||
/**
|
||||
* Removes the placeholder syntax from a template string.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* type Base = ClearPlaceholder<'{user.name}'>; // 'user.name'
|
||||
* type Unbalanced = ClearPlaceholder<'{user.name'>; // 'user.name'
|
||||
* type Spaces = ClearPlaceholder<'{ user.name }'>; // 'user.name'
|
||||
*/
|
||||
export type ClearPlaceholder<In extends string> =
|
||||
In extends `${string}{${infer Template}`
|
||||
export type ClearPlaceholder<In extends string>
|
||||
= In extends `${string}{${infer Template}`
|
||||
? ClearPlaceholder<Template>
|
||||
: In extends `${infer Template}}${string}`
|
||||
? ClearPlaceholder<Template>
|
||||
@@ -34,12 +34,12 @@ export type ClearPlaceholder<In extends string> =
|
||||
|
||||
/**
|
||||
* Extracts all placeholders from a template string.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* type Base = ExtractPlaceholders<'Hello {user.name}, {user.addresses.0.street}'>; // 'user.name' | 'user.addresses.0.street'
|
||||
*/
|
||||
export type ExtractPlaceholders<In extends string> =
|
||||
In extends `${infer Before}}${infer After}`
|
||||
export type ExtractPlaceholders<In extends string>
|
||||
= In extends `${infer Before}}${infer After}`
|
||||
? Before extends `${string}{${infer Placeholder}`
|
||||
? ClearPlaceholder<Placeholder> | ExtractPlaceholders<After>
|
||||
: ExtractPlaceholders<After>
|
||||
@@ -47,7 +47,7 @@ export type ExtractPlaceholders<In extends string> =
|
||||
|
||||
/**
|
||||
* Generates a type for a template string with placeholders.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* type Base = GenerateTypes<'Hello {user.name}, your address {user.addresses.0.street}'>; // { user: { name: string; addresses: { 0: { street: string; }; }; }; }
|
||||
* type WithTarget = GenerateTypes<'Hello {user.age}', number>; // { user: { age: number; }; }
|
||||
@@ -56,12 +56,12 @@ export type GenerateTypes<T extends string, Target = string> = UnionToIntersecti
|
||||
|
||||
export function templateObject<
|
||||
T extends string,
|
||||
A extends GenerateTypes<ExtractPlaceholders<T>, TemplateValue> & Collection
|
||||
A extends GenerateTypes<ExtractPlaceholders<T>, TemplateValue> & Collection,
|
||||
>(template: T, args: A, fallback?: TemplateFallback) {
|
||||
return template.replace(TEMPLATE_PLACEHOLDER, (_, key) => {
|
||||
const value = get(args, key)?.toString();
|
||||
return value !== undefined ? value : (isFunction(fallback) ? fallback(key) : '');
|
||||
});
|
||||
return template.replace(TEMPLATE_PLACEHOLDER, (_, key) => {
|
||||
const value = get(args, key)?.toString();
|
||||
return value !== undefined ? value : (isFunction(fallback) ? fallback(key) : '');
|
||||
});
|
||||
}
|
||||
|
||||
templateObject('Hello {user.name}, your address {user.addresses.0.city}', {
|
||||
|
||||
@@ -2,92 +2,92 @@ import { describe, it, expect } from 'vitest';
|
||||
import { trigramDistance, trigramProfile } from '.';
|
||||
|
||||
describe('trigramProfile', () => {
|
||||
it('trigram profile of a text with different trigrams', () => {
|
||||
const different_trigrams = 'hello world';
|
||||
const profile1 = trigramProfile(different_trigrams);
|
||||
it('trigram profile of a text with different trigrams', () => {
|
||||
const different_trigrams = 'hello world';
|
||||
const profile1 = trigramProfile(different_trigrams);
|
||||
|
||||
expect(profile1).toEqual(new Map([
|
||||
['\n\nh', 1],
|
||||
['\nhe', 1],
|
||||
['hel', 1],
|
||||
['ell', 1],
|
||||
['llo', 1],
|
||||
['lo ', 1],
|
||||
['o w', 1],
|
||||
[' wo', 1],
|
||||
['wor', 1],
|
||||
['orl', 1],
|
||||
['rld', 1],
|
||||
['ld\n', 1],
|
||||
['d\n\n', 1]
|
||||
]));
|
||||
});
|
||||
expect(profile1).toEqual(new Map([
|
||||
['\n\nh', 1],
|
||||
['\nhe', 1],
|
||||
['hel', 1],
|
||||
['ell', 1],
|
||||
['llo', 1],
|
||||
['lo ', 1],
|
||||
['o w', 1],
|
||||
[' wo', 1],
|
||||
['wor', 1],
|
||||
['orl', 1],
|
||||
['rld', 1],
|
||||
['ld\n', 1],
|
||||
['d\n\n', 1],
|
||||
]));
|
||||
});
|
||||
|
||||
it('trigram profile of a text with repeated trigrams', () => {
|
||||
const repeated_trigrams = 'hello hello';
|
||||
const profile2 = trigramProfile(repeated_trigrams);
|
||||
it('trigram profile of a text with repeated trigrams', () => {
|
||||
const repeated_trigrams = 'hello hello';
|
||||
const profile2 = trigramProfile(repeated_trigrams);
|
||||
|
||||
expect(profile2).toEqual(new Map([
|
||||
['\n\nh', 1],
|
||||
['\nhe', 1],
|
||||
['hel', 2],
|
||||
['ell', 2],
|
||||
['llo', 2],
|
||||
['lo ', 1],
|
||||
['o h', 1],
|
||||
[' he', 1],
|
||||
['lo\n', 1],
|
||||
['o\n\n', 1]
|
||||
]));
|
||||
});
|
||||
expect(profile2).toEqual(new Map([
|
||||
['\n\nh', 1],
|
||||
['\nhe', 1],
|
||||
['hel', 2],
|
||||
['ell', 2],
|
||||
['llo', 2],
|
||||
['lo ', 1],
|
||||
['o h', 1],
|
||||
[' he', 1],
|
||||
['lo\n', 1],
|
||||
['o\n\n', 1],
|
||||
]));
|
||||
});
|
||||
|
||||
it('trigram profile of an empty text', () => {
|
||||
const text = '';
|
||||
const profile = trigramProfile(text);
|
||||
it('trigram profile of an empty text', () => {
|
||||
const text = '';
|
||||
const profile = trigramProfile(text);
|
||||
|
||||
expect(profile).toEqual(new Map([
|
||||
['\n\n\n', 2],
|
||||
]));
|
||||
});
|
||||
expect(profile).toEqual(new Map([
|
||||
['\n\n\n', 2],
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('trigramDistance', () => {
|
||||
it('zero when comparing the same text', () => {
|
||||
const profile1 = trigramProfile('hello world');
|
||||
const profile2 = trigramProfile('hello world');
|
||||
it('zero when comparing the same text', () => {
|
||||
const profile1 = trigramProfile('hello world');
|
||||
const profile2 = trigramProfile('hello world');
|
||||
|
||||
expect(trigramDistance(profile1, profile2)).toBe(0);
|
||||
});
|
||||
expect(trigramDistance(profile1, profile2)).toBe(0);
|
||||
});
|
||||
|
||||
it('one for completely different text', () => {
|
||||
const profile1 = trigramProfile('hello world');
|
||||
const profile2 = trigramProfile('lorem ipsum');
|
||||
it('one for completely different text', () => {
|
||||
const profile1 = trigramProfile('hello world');
|
||||
const profile2 = trigramProfile('lorem ipsum');
|
||||
|
||||
expect(trigramDistance(profile1, profile2)).toBe(1);
|
||||
});
|
||||
expect(trigramDistance(profile1, profile2)).toBe(1);
|
||||
});
|
||||
|
||||
it('one for empty text and non-empty text', () => {
|
||||
const profile1 = trigramProfile('hello world');
|
||||
const profile2 = trigramProfile('');
|
||||
it('one for empty text and non-empty text', () => {
|
||||
const profile1 = trigramProfile('hello world');
|
||||
const profile2 = trigramProfile('');
|
||||
|
||||
expect(trigramDistance(profile1, profile2)).toBe(1);
|
||||
});
|
||||
expect(trigramDistance(profile1, profile2)).toBe(1);
|
||||
});
|
||||
|
||||
it('approximately 0.5 for similar text', () => {
|
||||
const profile1 = trigramProfile('hello world');
|
||||
const profile2 = trigramProfile('hello lorem');
|
||||
it('approximately 0.5 for similar text', () => {
|
||||
const profile1 = trigramProfile('hello world');
|
||||
const profile2 = trigramProfile('hello lorem');
|
||||
|
||||
const approx = trigramDistance(profile1, profile2);
|
||||
const approx = trigramDistance(profile1, profile2);
|
||||
|
||||
expect(approx).toBeGreaterThan(0.45);
|
||||
expect(approx).toBeLessThan(0.55);
|
||||
});
|
||||
expect(approx).toBeGreaterThan(0.45);
|
||||
expect(approx).toBeLessThan(0.55);
|
||||
});
|
||||
|
||||
it('triangle inequality', () => {
|
||||
const A = trigramDistance(trigramProfile('metric'), trigramProfile('123ric'));
|
||||
const B = trigramDistance(trigramProfile('123ric'), trigramProfile('123456'));
|
||||
const C = trigramDistance(trigramProfile('metric'), trigramProfile('123456'));
|
||||
it('triangle inequality', () => {
|
||||
const A = trigramDistance(trigramProfile('metric'), trigramProfile('123ric'));
|
||||
const B = trigramDistance(trigramProfile('123ric'), trigramProfile('123456'));
|
||||
const C = trigramDistance(trigramProfile('metric'), trigramProfile('123456'));
|
||||
|
||||
expect(A + B).toBeGreaterThanOrEqual(C);
|
||||
});
|
||||
});
|
||||
expect(A + B).toBeGreaterThanOrEqual(C);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,31 +4,31 @@ export type Trigrams = Map<string, number>;
|
||||
* @name trigramProfile
|
||||
* @category Text
|
||||
* @description Extracts trigrams from a text and returns a map of trigram to count
|
||||
*
|
||||
*
|
||||
* @param {string} text The text to extract trigrams
|
||||
* @returns {Trigrams} A map of trigram to count
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export function trigramProfile(text: string): Trigrams {
|
||||
text = `\n\n${text}\n\n`;
|
||||
text = `\n\n${text}\n\n`;
|
||||
|
||||
const trigrams = new Map<string, number>();
|
||||
const trigrams = new Map<string, number>();
|
||||
|
||||
for (let i = 0; i < text.length - 2; i++) {
|
||||
const trigram = text.slice(i, i + 3);
|
||||
const count = trigrams.get(trigram) ?? 0;
|
||||
trigrams.set(trigram, count + 1);
|
||||
}
|
||||
for (let i = 0; i < text.length - 2; i++) {
|
||||
const trigram = text.slice(i, i + 3);
|
||||
const count = trigrams.get(trigram) ?? 0;
|
||||
trigrams.set(trigram, count + 1);
|
||||
}
|
||||
|
||||
return trigrams;
|
||||
return trigrams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name trigramDistance
|
||||
* @category Text
|
||||
* @description Calculates the trigram distance between two strings
|
||||
*
|
||||
*
|
||||
* @param {Trigrams} left First text trigram profile
|
||||
* @param {Trigrams} right Second text trigram profile
|
||||
* @returns {number} The trigram distance between the two strings
|
||||
@@ -36,22 +36,22 @@ export function trigramProfile(text: string): Trigrams {
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export function trigramDistance(left: Trigrams, right: Trigrams): number {
|
||||
let distance = -4;
|
||||
let total = -4;
|
||||
let distance = -4;
|
||||
let total = -4;
|
||||
|
||||
for (const [trigram, left_count] of left) {
|
||||
total += left_count;
|
||||
const right_count = right.get(trigram) ?? 0;
|
||||
distance += Math.abs(left_count - right_count);
|
||||
}
|
||||
for (const [trigram, left_count] of left) {
|
||||
total += left_count;
|
||||
const right_count = right.get(trigram) ?? 0;
|
||||
distance += Math.abs(left_count - right_count);
|
||||
}
|
||||
|
||||
for (const [trigram, right_count] of right) {
|
||||
total += right_count;
|
||||
const left_count = left.get(trigram) ?? 0;
|
||||
distance += Math.abs(left_count - right_count);
|
||||
}
|
||||
for (const [trigram, right_count] of right) {
|
||||
total += right_count;
|
||||
const left_count = left.get(trigram) ?? 0;
|
||||
distance += Math.abs(left_count - right_count);
|
||||
}
|
||||
|
||||
if (distance < 0) return 0;
|
||||
if (distance < 0) return 0;
|
||||
|
||||
return distance / total;
|
||||
}
|
||||
return distance / total;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './js';
|
||||
export * from './ts';
|
||||
export * from './ts';
|
||||
|
||||
@@ -27,4 +27,4 @@ describe('casts', () => {
|
||||
expect(toString(new WeakSet())).toBe('[object WeakSet]');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
* @name toString
|
||||
* @category Types
|
||||
* @description To string any value
|
||||
*
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {string}
|
||||
*
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const toString = (value: any): string => Object.prototype.toString.call(value);
|
||||
export const toString = (value: any): string => Object.prototype.toString.call(value);
|
||||
|
||||
@@ -7,7 +7,7 @@ describe('complex', () => {
|
||||
expect(isArray([])).toBe(true);
|
||||
expect(isArray([1, 2, 3])).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('false if the value is not an array', () => {
|
||||
expect(isArray('')).toBe(false);
|
||||
expect(isArray(123)).toBe(false);
|
||||
@@ -19,13 +19,13 @@ describe('complex', () => {
|
||||
expect(isArray(new Set())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('isObject', () => {
|
||||
it('true if the value is an object', () => {
|
||||
expect(isObject({})).toBe(true);
|
||||
expect(isObject({ key: 'value' })).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('false if the value is not an object', () => {
|
||||
expect(isObject('')).toBe(false);
|
||||
expect(isObject(123)).toBe(false);
|
||||
@@ -37,13 +37,13 @@ describe('complex', () => {
|
||||
expect(isObject(new Set())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('isRegExp', () => {
|
||||
it('true if the value is a regexp', () => {
|
||||
expect(isRegExp(/test/)).toBe(true);
|
||||
expect(isRegExp(new RegExp('test'))).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('false if the value is not a regexp', () => {
|
||||
expect(isRegExp('')).toBe(false);
|
||||
expect(isRegExp(123)).toBe(false);
|
||||
@@ -56,12 +56,12 @@ describe('complex', () => {
|
||||
expect(isRegExp(new Set())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('isDate', () => {
|
||||
it('true if the value is a date', () => {
|
||||
expect(isDate(new Date())).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('false if the value is not a date', () => {
|
||||
expect(isDate('')).toBe(false);
|
||||
expect(isDate(123)).toBe(false);
|
||||
@@ -74,12 +74,12 @@ describe('complex', () => {
|
||||
expect(isDate(new Set())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('isError', () => {
|
||||
it('true if the value is an error', () => {
|
||||
expect(isError(new Error('test'))).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('false if the value is not an error', () => {
|
||||
expect(isError('')).toBe(false);
|
||||
expect(isError(123)).toBe(false);
|
||||
@@ -92,12 +92,12 @@ describe('complex', () => {
|
||||
expect(isError(new Set())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('isPromise', () => {
|
||||
it('true if the value is a promise', () => {
|
||||
expect(isPromise(new Promise(() => {}))).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('false if the value is not a promise', () => {
|
||||
expect(isPromise('')).toBe(false);
|
||||
expect(isPromise(123)).toBe(false);
|
||||
@@ -110,12 +110,12 @@ describe('complex', () => {
|
||||
expect(isPromise(new Set())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('isMap', () => {
|
||||
it('true if the value is a map', () => {
|
||||
expect(isMap(new Map())).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('false if the value is not a map', () => {
|
||||
expect(isMap('')).toBe(false);
|
||||
expect(isMap(123)).toBe(false);
|
||||
@@ -127,12 +127,12 @@ describe('complex', () => {
|
||||
expect(isMap(new Set())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('isSet', () => {
|
||||
it('true if the value is a set', () => {
|
||||
expect(isSet(new Set())).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('false if the value is not a set', () => {
|
||||
expect(isSet('')).toBe(false);
|
||||
expect(isSet(123)).toBe(false);
|
||||
@@ -144,12 +144,12 @@ describe('complex', () => {
|
||||
expect(isSet(new Map())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('isWeakMap', () => {
|
||||
it('true if the value is a weakmap', () => {
|
||||
expect(isWeakMap(new WeakMap())).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('false if the value is not a weakmap', () => {
|
||||
expect(isWeakMap('')).toBe(false);
|
||||
expect(isWeakMap(123)).toBe(false);
|
||||
@@ -162,12 +162,12 @@ describe('complex', () => {
|
||||
expect(isWeakMap(new Set())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('isWeakSet', () => {
|
||||
it('true if the value is a weakset', () => {
|
||||
expect(isWeakSet(new WeakSet())).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('false if the value is not a weakset', () => {
|
||||
expect(isWeakSet('')).toBe(false);
|
||||
expect(isWeakSet(123)).toBe(false);
|
||||
@@ -180,4 +180,4 @@ describe('complex', () => {
|
||||
expect(isWeakSet(new Set())).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,10 +4,10 @@ import { toString } from './casts';
|
||||
* @name isFunction
|
||||
* @category Types
|
||||
* @description Check if a value is an array
|
||||
*
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is any[]}
|
||||
*
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isArray = (value: any): value is any[] => Array.isArray(value);
|
||||
@@ -16,10 +16,10 @@ export const isArray = (value: any): value is any[] => Array.isArray(value);
|
||||
* @name isObject
|
||||
* @category Types
|
||||
* @description Check if a value is an object
|
||||
*
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is object}
|
||||
*
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isObject = (value: any): value is object => toString(value) === '[object Object]';
|
||||
@@ -31,7 +31,7 @@ export const isObject = (value: any): value is object => toString(value) === '[o
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is RegExp}
|
||||
*
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isRegExp = (value: any): value is RegExp => toString(value) === '[object RegExp]';
|
||||
@@ -43,7 +43,7 @@ export const isRegExp = (value: any): value is RegExp => toString(value) === '[o
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is Date}
|
||||
*
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isDate = (value: any): value is Date => toString(value) === '[object Date]';
|
||||
@@ -52,10 +52,10 @@ export const isDate = (value: any): value is Date => toString(value) === '[objec
|
||||
* @name isError
|
||||
* @category Types
|
||||
* @description Check if a value is an error
|
||||
*
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is Error}
|
||||
*
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isError = (value: any): value is Error => toString(value) === '[object Error]';
|
||||
@@ -64,10 +64,10 @@ export const isError = (value: any): value is Error => toString(value) === '[obj
|
||||
* @name isPromise
|
||||
* @category Types
|
||||
* @description Check if a value is a promise
|
||||
*
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is Promise<any>}
|
||||
*
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isPromise = (value: any): value is Promise<any> => toString(value) === '[object Promise]';
|
||||
@@ -76,10 +76,10 @@ export const isPromise = (value: any): value is Promise<any> => toString(value)
|
||||
* @name isMap
|
||||
* @category Types
|
||||
* @description Check if a value is a map
|
||||
*
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is Map<any, any>}
|
||||
*
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isMap = (value: any): value is Map<any, any> => toString(value) === '[object Map]';
|
||||
@@ -88,10 +88,10 @@ export const isMap = (value: any): value is Map<any, any> => toString(value) ===
|
||||
* @name isSet
|
||||
* @category Types
|
||||
* @description Check if a value is a set
|
||||
*
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is Set<any>}
|
||||
*
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isSet = (value: any): value is Set<any> => toString(value) === '[object Set]';
|
||||
@@ -100,10 +100,10 @@ export const isSet = (value: any): value is Set<any> => toString(value) === '[ob
|
||||
* @name isWeakMap
|
||||
* @category Types
|
||||
* @description Check if a value is a weakmap
|
||||
*
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is WeakMap<object, any>}
|
||||
*
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isWeakMap = (value: any): value is WeakMap<object, any> => toString(value) === '[object WeakMap]';
|
||||
@@ -112,10 +112,10 @@ export const isWeakMap = (value: any): value is WeakMap<object, any> => toString
|
||||
* @name isWeakSet
|
||||
* @category Types
|
||||
* @description Check if a value is a weakset
|
||||
*
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is WeakSet<object>}
|
||||
*
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isWeakSet = (value: any): value is WeakSet<object> => toString(value) === '[object WeakSet]';
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './casts';
|
||||
export * from './primitives';
|
||||
export * from './complex';
|
||||
export * from './complex';
|
||||
|
||||
@@ -7,95 +7,95 @@ describe('primitives', () => {
|
||||
expect(isBoolean(true)).toBe(true);
|
||||
expect(isBoolean(false)).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('false if the value is not a boolean', () => {
|
||||
expect(isBoolean(0)).toBe(false);
|
||||
expect(isBoolean('true')).toBe(false);
|
||||
expect(isBoolean(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('isFunction', () => {
|
||||
it('true if the value is a function', () => {
|
||||
expect(isFunction(() => {})).toBe(true);
|
||||
expect(isFunction(function() {})).toBe(true);
|
||||
expect(isFunction(function () {})).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('false if the value is not a function', () => {
|
||||
expect(isFunction(123)).toBe(false);
|
||||
expect(isFunction('function')).toBe(false);
|
||||
expect(isFunction(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('isNumber', () => {
|
||||
it('true if the value is a number', () => {
|
||||
expect(isNumber(123)).toBe(true);
|
||||
expect(isNumber(3.14)).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('false if the value is not a number', () => {
|
||||
expect(isNumber('123')).toBe(false);
|
||||
expect(isNumber(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('isBigInt', () => {
|
||||
it('true if the value is a bigint', () => {
|
||||
expect(isBigInt(BigInt(123))).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('false if the value is not a bigint', () => {
|
||||
expect(isBigInt(123)).toBe(false);
|
||||
expect(isBigInt('123')).toBe(false);
|
||||
expect(isBigInt(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('isString', () => {
|
||||
it('true if the value is a string', () => {
|
||||
expect(isString('hello')).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('false if the value is not a string', () => {
|
||||
expect(isString(123)).toBe(false);
|
||||
expect(isString(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('isSymbol', () => {
|
||||
it('true if the value is a symbol', () => {
|
||||
expect(isSymbol(Symbol())).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('false if the value is not a symbol', () => {
|
||||
expect(isSymbol(123)).toBe(false);
|
||||
expect(isSymbol('symbol')).toBe(false);
|
||||
expect(isSymbol(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('isUndefined', () => {
|
||||
it('true if the value is undefined', () => {
|
||||
expect(isUndefined(undefined)).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('false if the value is not undefined', () => {
|
||||
expect(isUndefined(null)).toBe(false);
|
||||
expect(isUndefined(123)).toBe(false);
|
||||
expect(isUndefined('undefined')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('isNull', () => {
|
||||
it('true if the value is null', () => {
|
||||
expect(isNull(null)).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('false if the value is not null', () => {
|
||||
expect(isNull(undefined)).toBe(false);
|
||||
expect(isNull(123)).toBe(false);
|
||||
expect(isNull('null')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,10 +4,10 @@ import { toString } from './casts';
|
||||
* @name isObject
|
||||
* @category Types
|
||||
* @description Check if a value is a boolean
|
||||
*
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is boolean}
|
||||
*
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isBoolean = (value: any): value is boolean => typeof value === 'boolean';
|
||||
@@ -16,10 +16,10 @@ export const isBoolean = (value: any): value is boolean => typeof value === 'boo
|
||||
* @name isFunction
|
||||
* @category Types
|
||||
* @description Check if a value is a function
|
||||
*
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is Function}
|
||||
*
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isFunction = <T extends Function>(value: any): value is T => typeof value === 'function';
|
||||
@@ -28,10 +28,10 @@ export const isFunction = <T extends Function>(value: any): value is T => typeof
|
||||
* @name isNumber
|
||||
* @category Types
|
||||
* @description Check if a value is a number
|
||||
*
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is number}
|
||||
*
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isNumber = (value: any): value is number => typeof value === 'number';
|
||||
@@ -40,10 +40,10 @@ export const isNumber = (value: any): value is number => typeof value === 'numbe
|
||||
* @name isBigInt
|
||||
* @category Types
|
||||
* @description Check if a value is a bigint
|
||||
*
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is bigint}
|
||||
*
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isBigInt = (value: any): value is bigint => typeof value === 'bigint';
|
||||
@@ -52,10 +52,10 @@ export const isBigInt = (value: any): value is bigint => typeof value === 'bigin
|
||||
* @name isString
|
||||
* @category Types
|
||||
* @description Check if a value is a string
|
||||
*
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is string}
|
||||
*
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isString = (value: any): value is string => typeof value === 'string';
|
||||
@@ -64,10 +64,10 @@ export const isString = (value: any): value is string => typeof value === 'strin
|
||||
* @name isSymbol
|
||||
* @category Types
|
||||
* @description Check if a value is a symbol
|
||||
*
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is symbol}
|
||||
*
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isSymbol = (value: any): value is symbol => typeof value === 'symbol';
|
||||
@@ -76,10 +76,10 @@ export const isSymbol = (value: any): value is symbol => typeof value === 'symbo
|
||||
* @name isUndefined
|
||||
* @category Types
|
||||
* @description Check if a value is a undefined
|
||||
*
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is undefined}
|
||||
*
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isUndefined = (value: any): value is undefined => toString(value) === '[object Undefined]';
|
||||
@@ -88,10 +88,10 @@ export const isUndefined = (value: any): value is undefined => toString(value) =
|
||||
* @name isNull
|
||||
* @category Types
|
||||
* @description Check if a value is a null
|
||||
*
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is null}
|
||||
*
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isNull = (value: any): value is null => toString(value) === '[object Null]';
|
||||
|
||||
@@ -68,4 +68,4 @@ describe('collections', () => {
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,18 +6,18 @@ export type Collection = Record<PropertyKey, any> | any[];
|
||||
/**
|
||||
* Parse a collection path string into an array of keys
|
||||
*/
|
||||
export type Path<T> =
|
||||
T extends `${infer Key}.${infer Rest}`
|
||||
export type Path<T>
|
||||
= T extends `${infer Key}.${infer Rest}`
|
||||
? [Key, ...Path<Rest>]
|
||||
: T extends `${infer Key}`
|
||||
? [Key]
|
||||
: [];
|
||||
: T extends `${infer Key}`
|
||||
? [Key]
|
||||
: [];
|
||||
|
||||
/**
|
||||
* Convert a collection path array into a Target type
|
||||
*/
|
||||
export type PathToType<T extends string[], Target = unknown> =
|
||||
T extends [infer Head, ...infer Rest]
|
||||
export type PathToType<T extends string[], Target = unknown>
|
||||
= T extends [infer Head, ...infer Rest]
|
||||
? Head extends `${number}`
|
||||
? Rest extends string[]
|
||||
? Array<PathToType<Rest, Target>>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * from './array';
|
||||
export * from './collections';
|
||||
export * from './function';
|
||||
export * from './promise';
|
||||
export * from './promise';
|
||||
export * from './string';
|
||||
export * from './union';
|
||||
|
||||
@@ -2,53 +2,53 @@ import { describe, expectTypeOf, it } from 'vitest';
|
||||
import type { HasSpaces, Trim, Stringable } from './string';
|
||||
|
||||
describe('string', () => {
|
||||
describe('Stringable', () => {
|
||||
it('should be a string', () => {
|
||||
expectTypeOf(Number(1)).toExtend<Stringable>();
|
||||
expectTypeOf(String(1)).toExtend<Stringable>();
|
||||
expectTypeOf(Symbol()).toExtend<Stringable>();
|
||||
expectTypeOf([1]).toExtend<Stringable>();
|
||||
expectTypeOf(new Object()).toExtend<Stringable>();
|
||||
expectTypeOf(new Date()).toExtend<Stringable>();
|
||||
});
|
||||
describe('Stringable', () => {
|
||||
it('should be a string', () => {
|
||||
expectTypeOf(Number(1)).toExtend<Stringable>();
|
||||
expectTypeOf(String(1)).toExtend<Stringable>();
|
||||
expectTypeOf(Symbol()).toExtend<Stringable>();
|
||||
expectTypeOf([1]).toExtend<Stringable>();
|
||||
expectTypeOf(new Object()).toExtend<Stringable>();
|
||||
expectTypeOf(new Date()).toExtend<Stringable>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Trim', () => {
|
||||
it('remove leading and trailing spaces from a string', () => {
|
||||
type actual = Trim<' hello '>;
|
||||
type expected = 'hello';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
describe('Trim', () => {
|
||||
it('remove leading and trailing spaces from a string', () => {
|
||||
type actual = Trim<' hello '>;
|
||||
type expected = 'hello';
|
||||
it('remove only leading spaces from a string', () => {
|
||||
type actual = Trim<' hello'>;
|
||||
type expected = 'hello';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('remove only leading spaces from a string', () => {
|
||||
type actual = Trim<' hello'>;
|
||||
type expected = 'hello';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('remove only trailing spaces from a string', () => {
|
||||
type actual = Trim<'hello '>;
|
||||
type expected = 'hello';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
describe('HasSpaces', () => {
|
||||
it('check if a string has spaces', () => {
|
||||
type actual = HasSpaces<'hello world'>;
|
||||
type expected = true;
|
||||
it('remove only trailing spaces from a string', () => {
|
||||
type actual = Trim<'hello '>;
|
||||
type expected = 'hello';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('check if a string has no spaces', () => {
|
||||
type actual = HasSpaces<'helloworld'>;
|
||||
type expected = false;
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('HasSpaces', () => {
|
||||
it('check if a string has spaces', () => {
|
||||
type actual = HasSpaces<'hello world'>;
|
||||
type expected = true;
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('check if a string has no spaces', () => {
|
||||
type actual = HasSpaces<'helloworld'>;
|
||||
type expected = false;
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
*/
|
||||
export type UnionToIntersection<Union> = (
|
||||
Union extends unknown
|
||||
? (distributedUnion: Union) => void
|
||||
: never
|
||||
? (distributedUnion: Union) => void
|
||||
: never
|
||||
) extends ((mergedIntersection: infer Intersection) => void)
|
||||
? Intersection & Union
|
||||
: never;
|
||||
? Intersection & Union
|
||||
: never;
|
||||
|
||||
@@ -4,4 +4,4 @@ import { sharedConfig } from '@robonen/tsdown';
|
||||
export default defineConfig({
|
||||
...sharedConfig,
|
||||
entry: ['src/index.ts'],
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user