1
0
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:
2026-03-07 18:07:22 +07:00
parent abd6605db3
commit 41d5e18f6b
286 changed files with 10295 additions and 5028 deletions

View File

@@ -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));

View File

@@ -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:"
}

View File

@@ -34,7 +34,7 @@ describe('cluster', () => {
it('return an empty array if the input array is empty', () => {
const result = cluster([], 3);
expect(result).toEqual([]);
});
});
});

View File

@@ -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[][] {

View File

@@ -20,4 +20,4 @@ describe('first', () => {
expect(first([1, 2, 3], 42)).toBe(1);
expect(first(['a', 'b', 'c'], 'default')).toBe('a');
});
});
});

View File

@@ -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;
}

View File

@@ -2,4 +2,4 @@ export * from './cluster';
export * from './first';
export * from './last';
export * from './sum';
export * from './unique';
export * from './unique';

View File

@@ -20,4 +20,4 @@ describe('last', () => {
expect(last([1, 2, 3], 42)).toBe(3);
expect(last(['a', 'b', 'c'], 'default')).toBe('c');
});
});
});

View File

@@ -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) {

View File

@@ -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);
});
});
});

View File

@@ -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;

View File

@@ -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([]);
});
});
});

View File

@@ -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>(

View File

@@ -1,3 +1,3 @@
export interface AsyncPoolOptions {
concurrency?: number;
}
}

View File

@@ -16,4 +16,4 @@ describe('sleep', () => {
expect(end - start).toBeGreaterThan(delay - 5);
});
});
});

View File

@@ -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));
}

View File

@@ -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();
});
});
});

View File

@@ -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>;
}
};

View File

@@ -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);
};
}

View File

@@ -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);
});
});
});

View File

@@ -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

View File

@@ -1 +1 @@
export * from './flags';
export * from './flags';

View File

@@ -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'));
});
});
});

View File

@@ -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');
}
}
}

View File

@@ -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;
}
}

View File

@@ -1 +1 @@
export * from './get';
export * from './get';

View File

@@ -8,4 +8,4 @@ export * from './structs';
export * from './sync';
export * from './text';
export * from './types';
export * from './utils'
export * from './utils';

View File

@@ -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);
});
});
});

View File

@@ -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

View File

@@ -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', () => {

View File

@@ -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);
});
});
});

View File

@@ -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));
}
}

View File

@@ -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);
});
});
});

View File

@@ -1,5 +1,5 @@
import {minBigInt} from '../minBigInt';
import {maxBigInt} from '../maxBigInt';
import { minBigInt } from '../minBigInt';
import { maxBigInt } from '../maxBigInt';
/**
* @name clampBigInt

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -36,4 +36,4 @@ describe('maxBigInt', () => {
const result = maxBigInt(...values);
expect(result).toBe(999n);
});
});
});

View File

@@ -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);
}
}

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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));
}
}

View File

@@ -47,4 +47,4 @@ describe('omit', () => {
expect(emptyTarget).toEqual({});
expect(emptyKeys).toEqual({ a: 1 });
});
});
});

View File

@@ -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];
}

View File

@@ -33,4 +33,4 @@ describe('pick', () => {
expect(emptyTarget).toEqual({});
expect(emptyKeys).toEqual({});
});
});
});

View File

@@ -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;
}
}

View File

@@ -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();
},
};

View File

@@ -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();

View File

@@ -115,4 +115,4 @@ describe('pubsub', () => {
expect(listener).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -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}

View File

@@ -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;

View File

@@ -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++; },
},
},

View File

@@ -39,7 +39,8 @@ export class StateMachine<
if (isString(transition)) {
target = transition;
} else {
}
else {
if (transition.guard && !transition.guard(this.context))
return this.currentState;

View File

@@ -1,3 +1,3 @@
export * from './behavioral/Command';
export * from './behavioral/PubSub';
export * from './behavioral/StateMachine';
export * from './behavioral/StateMachine';

View File

@@ -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();
});
});
});

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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]);
});
});
});

View File

@@ -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--;
}
}

View File

@@ -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;
}

View File

@@ -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();
});
});
});

View File

@@ -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;
}
}

View File

@@ -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 };

View File

@@ -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;
}

View File

@@ -112,8 +112,8 @@ describe('stack', () => {
for await (const element of stack) {
elements.push(element);
}
expect(elements).toEqual([3, 2, 1]);
});
});
});
});

View File

@@ -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;
}
}
}

View File

@@ -4,4 +4,4 @@ export * from './Deque';
export * from './LinkedList';
export * from './PriorityQueue';
export * from './Queue';
export * from './Stack';
export * from './Stack';

View File

@@ -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);
});
});

View File

@@ -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 {

View File

@@ -1,4 +1,4 @@
export * from './levenshtein-distance';
export * from './trigram-distance';
// TODO: Template is not implemented yet
// export * from './template';
// export * from './template';

View File

@@ -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);
});
});

View File

@@ -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]!;
}

View File

@@ -102,4 +102,4 @@ describe.skip('template', () => {
expectTypeOf<actual>().toEqualTypeOf<expected>();
});
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -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}', {

View File

@@ -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);
});
});

View File

@@ -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;
}

View File

@@ -1,2 +1,2 @@
export * from './js';
export * from './ts';
export * from './ts';

View File

@@ -27,4 +27,4 @@ describe('casts', () => {
expect(toString(new WeakSet())).toBe('[object WeakSet]');
});
});
});
});

View File

@@ -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);

View File

@@ -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);
});
});
});
});

View File

@@ -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]';

View File

@@ -1,3 +1,3 @@
export * from './casts';
export * from './primitives';
export * from './complex';
export * from './complex';

View File

@@ -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);
});
});
});
});

View File

@@ -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]';

View File

@@ -68,4 +68,4 @@ describe('collections', () => {
expectTypeOf<actual>().toEqualTypeOf<expected>();
});
});
});
});

View File

@@ -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>>

View File

@@ -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';

View File

@@ -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>();
});
});
});

View File

@@ -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;

View File

@@ -4,4 +4,4 @@ import { sharedConfig } from '@robonen/tsdown';
export default defineConfig({
...sharedConfig,
entry: ['src/index.ts'],
});
});