mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 10:54:44 +00:00
refactor: change separate tools by category
This commit is contained in:
40
core/stdlib/src/arrays/cluster/index.test.ts
Normal file
40
core/stdlib/src/arrays/cluster/index.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { cluster } from '.';
|
||||
|
||||
describe('cluster', () => {
|
||||
it('cluster an array into subarrays of a specific size', () => {
|
||||
const result = cluster([1, 2, 3, 4, 5, 6, 7, 8], 3);
|
||||
|
||||
expect(result).toEqual([[1, 2, 3], [4, 5, 6], [7, 8]]);
|
||||
});
|
||||
|
||||
it('handle arrays that are not perfectly divisible by the size', () => {
|
||||
const result = cluster([1, 2, 3, 4, 5], 2);
|
||||
|
||||
expect(result).toEqual([[1, 2], [3, 4], [5]]);
|
||||
});
|
||||
|
||||
it('return an array with each element in its own subarray if size is 1', () => {
|
||||
const result = cluster([1, 2, 3, 4], 1);
|
||||
|
||||
expect(result).toEqual([[1], [2], [3], [4]]);
|
||||
});
|
||||
|
||||
it('return an array with a single subarray if size is greater than the array length', () => {
|
||||
const result = cluster([1, 2, 3], 5);
|
||||
|
||||
expect(result).toEqual([[1, 2, 3]]);
|
||||
});
|
||||
|
||||
it('return an empty array if size is less than or equal to 0', () => {
|
||||
const result = cluster([1, 2, 3, 4], -1);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('return an empty array if the input array is empty', () => {
|
||||
const result = cluster([], 3);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
24
core/stdlib/src/arrays/cluster/index.ts
Normal file
24
core/stdlib/src/arrays/cluster/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @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[][] {
|
||||
if (size <= 0) return [];
|
||||
|
||||
const clusterLength = Math.ceil(arr.length / size);
|
||||
|
||||
return Array.from({ length: clusterLength }, (_, i) => arr.slice(i * size, i * size + size));
|
||||
}
|
||||
23
core/stdlib/src/arrays/first/index.test.ts
Normal file
23
core/stdlib/src/arrays/first/index.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { first } from '.';
|
||||
|
||||
describe('first', () => {
|
||||
it('return the first element of a non-empty array', () => {
|
||||
expect(first([1, 2, 3])).toBe(1);
|
||||
expect(first(['a', 'b', 'c'])).toBe('a');
|
||||
});
|
||||
|
||||
it('return undefined for an empty array without a default value', () => {
|
||||
expect(first([])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('return the default value for an empty array with a default value', () => {
|
||||
expect(first([], 42)).toBe(42);
|
||||
expect(first([], 'default')).toBe('default');
|
||||
});
|
||||
|
||||
it('return the first element even if a default value is provided', () => {
|
||||
expect(first([1, 2, 3], 42)).toBe(1);
|
||||
expect(first(['a', 'b', 'c'], 'default')).toBe('a');
|
||||
});
|
||||
});
|
||||
20
core/stdlib/src/arrays/first/index.ts
Normal file
20
core/stdlib/src/arrays/first/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
5
core/stdlib/src/arrays/index.ts
Normal file
5
core/stdlib/src/arrays/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './cluster';
|
||||
export * from './first';
|
||||
export * from './last';
|
||||
export * from './sum';
|
||||
export * from './unique';
|
||||
23
core/stdlib/src/arrays/last/index.test.ts
Normal file
23
core/stdlib/src/arrays/last/index.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { last } from '.';
|
||||
|
||||
describe('last', () => {
|
||||
it('return the last element of a non-empty array', () => {
|
||||
expect(last([1, 2, 3, 4, 5])).toBe(5);
|
||||
expect(last(['a', 'b', 'c'])).toBe('c');
|
||||
});
|
||||
|
||||
it('return undefined if the array is empty and no default value is provided', () => {
|
||||
expect(last([])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('return the default value for an empty array with a default value', () => {
|
||||
expect(last([], 42)).toBe(42);
|
||||
expect(last([], 'default')).toBe('default');
|
||||
});
|
||||
|
||||
it('return the first element even if a default value is provided', () => {
|
||||
expect(last([1, 2, 3], 42)).toBe(3);
|
||||
expect(last(['a', 'b', 'c'], 'default')).toBe('c');
|
||||
});
|
||||
});
|
||||
20
core/stdlib/src/arrays/last/index.ts
Normal file
20
core/stdlib/src/arrays/last/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @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) {
|
||||
return arr[arr.length - 1] ?? defaultValue;
|
||||
}
|
||||
46
core/stdlib/src/arrays/sum/index.test.ts
Normal file
46
core/stdlib/src/arrays/sum/index.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { sum } from '.';
|
||||
|
||||
describe('sum', () => {
|
||||
it('return the sum of all elements in a number array', () => {
|
||||
const result = sum([1, 2, 3, 4, 5]);
|
||||
|
||||
expect(result).toBe(15);
|
||||
});
|
||||
|
||||
it('return 0 for an empty array', () => {
|
||||
const result = sum([]);
|
||||
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('return the sum of all elements using a getValue function', () => {
|
||||
const result = sum([{ value: 1 }, { value: 2 }, { value: 3 }], (item) => item.value);
|
||||
|
||||
expect(result).toBe(6);
|
||||
});
|
||||
|
||||
it('handle arrays with negative numbers', () => {
|
||||
const result = sum([-1, -2, -3, -4, -5]);
|
||||
|
||||
expect(result).toBe(-15);
|
||||
});
|
||||
|
||||
it('handle arrays with mixed positive and negative numbers', () => {
|
||||
const result = sum([1, -2, 3, -4, 5]);
|
||||
|
||||
expect(result).toBe(3);
|
||||
});
|
||||
|
||||
it('handle arrays with floating point numbers', () => {
|
||||
const result = sum([1.5, 2.5, 3.5]);
|
||||
|
||||
expect(result).toBe(7.5);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
expect(result).toBe(7.5);
|
||||
});
|
||||
});
|
||||
26
core/stdlib/src/arrays/sum/index.ts
Normal file
26
core/stdlib/src/arrays/sum/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @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;
|
||||
export function sum<Value>(array: Value[], getValue: (item: Value) => number): number;
|
||||
export function sum<Value>(array: Value[], getValue?: (item: Value) => number): number {
|
||||
// This check is necessary because the overload without the getValue argument
|
||||
// makes tree-shaking based on argument types possible
|
||||
if (!getValue)
|
||||
return array.reduce((acc, item) => acc + (item as number), 0);
|
||||
|
||||
return array.reduce((acc, item) => acc + getValue(item), 0);
|
||||
}
|
||||
45
core/stdlib/src/arrays/unique/index.test.ts
Normal file
45
core/stdlib/src/arrays/unique/index.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { unique } from '.';
|
||||
|
||||
describe('unique', () => {
|
||||
it('return an array with unique numbers', () => {
|
||||
const result = unique([1, 2, 3, 3, 4, 5, 5, 6]);
|
||||
|
||||
expect(result).toEqual([1, 2, 3, 4, 5, 6]);
|
||||
});
|
||||
|
||||
it('return an array with unique objects based on id', () => {
|
||||
const result = unique(
|
||||
[{ id: 1 }, { id: 2 }, { id: 1 }],
|
||||
(item) => item.id,
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ id: 1 }, { id: 2 }]);
|
||||
});
|
||||
|
||||
it('return the same array if all elements are unique', () => {
|
||||
const result = unique([1, 2, 3, 4, 5]);
|
||||
|
||||
expect(result).toEqual([1, 2, 3, 4, 5]);
|
||||
});
|
||||
|
||||
it('handle arrays with different types of values', () => {
|
||||
const result = unique([1, '1', 2, '2', 2, 3, '3']);
|
||||
|
||||
expect(result).toEqual([1, '1', 2, '2', 3, '3']);
|
||||
});
|
||||
|
||||
it('handle arrays with symbols', () => {
|
||||
const sym1 = Symbol('a');
|
||||
const sym2 = Symbol('b');
|
||||
const result = unique([sym1, sym2, sym1]);
|
||||
|
||||
expect(result).toEqual([sym1, sym2]);
|
||||
});
|
||||
|
||||
it('return an empty array when given an empty array', () => {
|
||||
const result = unique([]);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
33
core/stdlib/src/arrays/unique/index.ts
Normal file
33
core/stdlib/src/arrays/unique/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export type UniqueKey = string | number | symbol;
|
||||
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>(
|
||||
array: Value[],
|
||||
extractor?: Extractor<Value, Key>,
|
||||
) {
|
||||
const values = new Map<Key, Value>();
|
||||
|
||||
for (const value of array) {
|
||||
const key = extractor ? extractor(value) : value as any;
|
||||
values.set(key, value);
|
||||
}
|
||||
|
||||
return Array.from(values.values());
|
||||
}
|
||||
2
core/stdlib/src/async/index.ts
Normal file
2
core/stdlib/src/async/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './sleep';
|
||||
export * from './tryIt';
|
||||
3
core/stdlib/src/async/pool/index.ts
Normal file
3
core/stdlib/src/async/pool/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type AsyncPoolOptions = {
|
||||
concurrency?: number;
|
||||
}
|
||||
38
core/stdlib/src/async/retry/index.ts
Normal file
38
core/stdlib/src/async/retry/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export interface RetryOptions {
|
||||
times?: number;
|
||||
delay?: number;
|
||||
backoff: (options: RetryOptions & { count: number }) => number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name retry
|
||||
* @category Async
|
||||
* @description Retries a function a specified number of times with a delay between each retry
|
||||
*
|
||||
* @param {Promise<unknown>} fn - The function to retry
|
||||
* @param {RetryOptions} options - The options for the retry
|
||||
* @returns {Promise<unknown>} - The result of the function
|
||||
*
|
||||
* @example
|
||||
* const result = await retry(() => {
|
||||
* return fetch('https://jsonplaceholder.typicode.com/todos/1')
|
||||
* .then(response => response.json())
|
||||
* });
|
||||
*
|
||||
* @example
|
||||
* const result = await retry(() => {
|
||||
* return fetch('https://jsonplaceholder.typicode.com/todos/1')
|
||||
* .then(response => response.json())
|
||||
* }, { times: 3, delay: 1000 });
|
||||
*
|
||||
*/
|
||||
export async function retry<Return>(
|
||||
fn: () => Promise<Return>,
|
||||
options: RetryOptions
|
||||
) {
|
||||
const {
|
||||
times = 3,
|
||||
} = options;
|
||||
|
||||
let count = 0;
|
||||
}
|
||||
19
core/stdlib/src/async/sleep/index.test.ts
Normal file
19
core/stdlib/src/async/sleep/index.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { sleep } from '.';
|
||||
|
||||
describe('sleep', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
});
|
||||
|
||||
it('delay execution by the specified amount of time', async () => {
|
||||
const start = performance.now();
|
||||
const delay = 100;
|
||||
|
||||
await sleep(delay);
|
||||
|
||||
const end = performance.now();
|
||||
|
||||
expect(end - start).toBeGreaterThan(delay - 5);
|
||||
});
|
||||
});
|
||||
21
core/stdlib/src/async/sleep/index.ts
Normal file
21
core/stdlib/src/async/sleep/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @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));
|
||||
}
|
||||
67
core/stdlib/src/async/tryIt/index.test.ts
Normal file
67
core/stdlib/src/async/tryIt/index.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { tryIt } from '.';
|
||||
|
||||
describe('tryIt', () => {
|
||||
it('handle synchronous functions without errors', () => {
|
||||
const syncFn = (x: number) => x * 2;
|
||||
const wrappedSyncFn = tryIt(syncFn);
|
||||
|
||||
const [error, result] = wrappedSyncFn(2);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(result).toBe(4);
|
||||
});
|
||||
|
||||
it('handle synchronous functions with errors', () => {
|
||||
const syncFn = (): void => { throw new Error('Test error') };
|
||||
const wrappedSyncFn = tryIt(syncFn);
|
||||
|
||||
const [error, result] = wrappedSyncFn();
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error?.message).toBe('Test error');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('handle asynchronous functions without errors', async () => {
|
||||
const asyncFn = async (x: number) => x * 2;
|
||||
const wrappedAsyncFn = tryIt(asyncFn);
|
||||
|
||||
const [error, result] = await wrappedAsyncFn(2);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(result).toBe(4);
|
||||
});
|
||||
|
||||
it('handle asynchronous functions with errors', async () => {
|
||||
const asyncFn = async () => { throw new Error('Test error') };
|
||||
const wrappedAsyncFn = tryIt(asyncFn);
|
||||
|
||||
const [error, result] = await wrappedAsyncFn();
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error?.message).toBe('Test error');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('handle promise-based functions without errors', async () => {
|
||||
const promiseFn = (x: number) => Promise.resolve(x * 2);
|
||||
const wrappedPromiseFn = tryIt(promiseFn);
|
||||
|
||||
const [error, result] = await wrappedPromiseFn(2);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(result).toBe(4);
|
||||
});
|
||||
|
||||
it('handle promise-based functions with errors', async () => {
|
||||
const promiseFn = () => Promise.reject(new Error('Test error'));
|
||||
const wrappedPromiseFn = tryIt(promiseFn);
|
||||
|
||||
const [error, result] = await wrappedPromiseFn();
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error?.message).toBe('Test error');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
41
core/stdlib/src/async/tryIt/index.ts
Normal file
41
core/stdlib/src/async/tryIt/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { isPromise } from '../../types';
|
||||
|
||||
export type TryItReturn<Return> = Return extends Promise<any>
|
||||
? Promise<[Error, undefined] | [undefined, Awaited<Return>]>
|
||||
: [Error, undefined] | [undefined, Return];
|
||||
|
||||
/**
|
||||
* @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>(
|
||||
fn: (...args: Args) => Return,
|
||||
) {
|
||||
return (...args: Args): TryItReturn<Return> => {
|
||||
try {
|
||||
const result = fn(...args);
|
||||
|
||||
if (isPromise(result))
|
||||
return result
|
||||
.then((value) => [undefined, value])
|
||||
.catch((error) => [error, undefined]) as TryItReturn<Return>;
|
||||
|
||||
return [undefined, result] as TryItReturn<Return>;
|
||||
} catch (error) {
|
||||
return [error, undefined] as TryItReturn<Return>;
|
||||
}
|
||||
};
|
||||
}
|
||||
26
core/stdlib/src/bits/flags/index.test.ts
Normal file
26
core/stdlib/src/bits/flags/index.test.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { flagsGenerator } from '.';
|
||||
|
||||
describe('flagsGenerator', () => {
|
||||
it('generate unique flags', () => {
|
||||
const generateFlag = flagsGenerator();
|
||||
|
||||
const flag1 = generateFlag();
|
||||
const flag2 = generateFlag();
|
||||
const flag3 = generateFlag();
|
||||
|
||||
expect(flag1).toBe(1);
|
||||
expect(flag2).toBe(2);
|
||||
expect(flag3).toBe(4);
|
||||
});
|
||||
|
||||
it('throw an error if more than 31 flags are created', () => {
|
||||
const generateFlag = flagsGenerator();
|
||||
|
||||
for (let i = 0; i < 31; i++) {
|
||||
generateFlag();
|
||||
}
|
||||
|
||||
expect(() => generateFlag()).toThrow(new RangeError('Cannot create more than 31 flags'));
|
||||
});
|
||||
});
|
||||
22
core/stdlib/src/bits/flags/index.ts
Normal file
22
core/stdlib/src/bits/flags/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @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;
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
||||
95
core/stdlib/src/bits/helpers/index.test.ts
Normal file
95
core/stdlib/src/bits/helpers/index.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
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();
|
||||
|
||||
expect(result).toBe(-1);
|
||||
});
|
||||
|
||||
it('source flag is returned if no flags are provided', () => {
|
||||
const result = and(0b1010);
|
||||
|
||||
expect(result).toBe(0b1010);
|
||||
});
|
||||
|
||||
it('perform bitwise AND operation on flags', () => {
|
||||
const result = and(0b1111, 0b1010, 0b1100);
|
||||
|
||||
expect(result).toBe(0b1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flagsOr', () => {
|
||||
it('no effect on zero flags', () => {
|
||||
const result = or();
|
||||
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('source flag is returned if no flags are provided', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flagsNot', () => {
|
||||
it('perform bitwise NOT operation on a flag', () => {
|
||||
const result = not(0b101);
|
||||
|
||||
expect(result).toBe(-0b110);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flagsHas', () => {
|
||||
it('check if a flag has a specific bit set', () => {
|
||||
const result = has(0b1010, 0b1000);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('check if a flag has a specific bit unset', () => {
|
||||
const result = has(0b1010, 0b0100);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flagsIs', () => {
|
||||
it('check if a flag is set', () => {
|
||||
const result = is(0b1010);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('check if a flag is unset', () => {
|
||||
const result = is(0);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flagsUnset', () => {
|
||||
it('unset a flag', () => {
|
||||
const result = unset(0b1010, 0b1000);
|
||||
|
||||
expect(result).toBe(0b0010);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flagsToggle', () => {
|
||||
it('toggle a flag', () => {
|
||||
const result = toggle(0b1010, 0b1000);
|
||||
|
||||
expect(result).toBe(0b0010);
|
||||
});
|
||||
});
|
||||
100
core/stdlib/src/bits/helpers/index.ts
Normal file
100
core/stdlib/src/bits/helpers/index.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @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
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function and(...flags: number[]) {
|
||||
return flags.reduce((acc, flag) => acc & flag, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function or(...flags: number[]) {
|
||||
return flags.reduce((acc, flag) => acc | flag, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function not(flag: number) {
|
||||
return ~flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function has(flag: number, other: number) {
|
||||
return (flag & other) === other;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function is(flag: number) {
|
||||
return flag !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function unset(flag: number, other: number) {
|
||||
return flag & ~other;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function toggle(flag: number, other: number) {
|
||||
return flag ^ other;
|
||||
}
|
||||
1
core/stdlib/src/bits/index.ts
Normal file
1
core/stdlib/src/bits/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './flags';
|
||||
63
core/stdlib/src/bits/vector/index.test.ts
Normal file
63
core/stdlib/src/bits/vector/index.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { BitVector } from '.';
|
||||
|
||||
describe('BitVector', () => {
|
||||
it('initialize with the correct size', () => {
|
||||
const size = 16;
|
||||
const expectedSize = Math.ceil(size / 8);
|
||||
const bitVector = new BitVector(size);
|
||||
|
||||
expect(bitVector.length).toBe(expectedSize);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('get out of bounds bits correctly', () => {
|
||||
const bitVector = new BitVector(16);
|
||||
|
||||
expect(bitVector.getBit(155)).toBe(false);
|
||||
});
|
||||
|
||||
it('clear bits correctly', () => {
|
||||
const bitVector = new BitVector(16);
|
||||
bitVector.setBit(5);
|
||||
|
||||
expect(bitVector.getBit(5)).toBe(true);
|
||||
|
||||
bitVector.clearBit(5);
|
||||
|
||||
expect(bitVector.getBit(5)).toBe(false);
|
||||
});
|
||||
|
||||
it('find the previous bit correctly', () => {
|
||||
const bitVector = new BitVector(100);
|
||||
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);
|
||||
}
|
||||
|
||||
expect(result).toEqual(indices);
|
||||
});
|
||||
|
||||
it('return -1 when no previous bit is found', () => {
|
||||
const bitVector = new BitVector(16);
|
||||
|
||||
expect(bitVector.previousBit(0)).toBe(-1);
|
||||
});
|
||||
|
||||
it('throw RangeError when previousBit is called with an unreachable value', () => {
|
||||
const bitVector = new BitVector(16);
|
||||
bitVector.setBit(5);
|
||||
|
||||
expect(() => bitVector.previousBit(24)).toThrow(new RangeError('Unreachable value'));
|
||||
});
|
||||
});
|
||||
61
core/stdlib/src/bits/vector/index.ts
Normal file
61
core/stdlib/src/bits/vector/index.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
export interface BitVector {
|
||||
getBit(index: number): boolean;
|
||||
setBit(index: number): void;
|
||||
clearBit(index: number): void;
|
||||
previousBit(index: number): number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 BitVector {
|
||||
constructor(size: number) {
|
||||
super(Math.ceil(size / 8));
|
||||
}
|
||||
|
||||
getBit(index: number) {
|
||||
const value = this[index >> 3]! & (1 << (index & 7));
|
||||
return value !== 0;
|
||||
}
|
||||
|
||||
setBit(index: number) {
|
||||
this[index >> 3]! |= 1 << (index & 7);
|
||||
}
|
||||
|
||||
clearBit(index: number): void {
|
||||
this[index >> 3]! &= ~(1 << (index & 7));
|
||||
}
|
||||
|
||||
previousBit(index: number): number {
|
||||
while (index !== ((index >> 3) << 3)) {
|
||||
--index;
|
||||
|
||||
if (this.getBit(index)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
let byteIndex = (index >> 3) - 1;
|
||||
|
||||
while (byteIndex >= 0 && this[byteIndex] === 0)
|
||||
--byteIndex;
|
||||
|
||||
if (byteIndex < 0)
|
||||
return -1;
|
||||
|
||||
index = (byteIndex << 3) + 7;
|
||||
|
||||
while (index >= (byteIndex << 3)) {
|
||||
if (this.getBit(index))
|
||||
return index;
|
||||
|
||||
--index;
|
||||
}
|
||||
|
||||
throw new RangeError('Unreachable value');
|
||||
}
|
||||
}
|
||||
84
core/stdlib/src/collections/getByPath/index.ts
Normal file
84
core/stdlib/src/collections/getByPath/index.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
type Exist<T> = T extends undefined | null ? never : T;
|
||||
|
||||
type ExtractFromObject<O extends Record<PropertyKey, unknown>, K> =
|
||||
K extends keyof O
|
||||
? O[K]
|
||||
: K extends keyof Exist<O>
|
||||
? Exist<O>[K]
|
||||
: never;
|
||||
|
||||
type ExtractFromArray<A extends readonly any[], K> = any[] extends A
|
||||
? A extends readonly (infer T)[]
|
||||
? T | undefined
|
||||
: undefined
|
||||
: K extends keyof A
|
||||
? A[K]
|
||||
: undefined;
|
||||
|
||||
type GetWithArray<O, K> = K extends []
|
||||
? O
|
||||
: K extends [infer Key, ...infer Rest]
|
||||
? O extends Record<PropertyKey, unknown>
|
||||
? GetWithArray<ExtractFromObject<O, Key>, Rest>
|
||||
: O extends readonly any[]
|
||||
? GetWithArray<ExtractFromArray<O, Key>, Rest>
|
||||
: never
|
||||
: never;
|
||||
|
||||
type Path<T> = T extends `${infer Key}.${infer Rest}`
|
||||
? [Key, ...Path<Rest>]
|
||||
: T extends `${infer Key}`
|
||||
? [Key]
|
||||
: [];
|
||||
|
||||
// Type that generate a type of a value by a path;
|
||||
// e.g. ['a', 'b', 'c'] => { a: { b: { c: PropertyKey } } }
|
||||
// e.g. ['a', 'b', 'c', 'd'] => { a: { b: { c: { d: PropertyKey } } } }
|
||||
// e.g. ['a'] => { a: PropertyKey }
|
||||
// e.g. ['a', '0'], => { a: [PropertyKey] }
|
||||
// e.g. ['a', '0', 'b'] => { a: [{ b: PropertyKey }] }
|
||||
// e.g. ['a', '0', 'b', '0'] => { a: [{ b: [PropertyKey] }] }
|
||||
// e/g/ ['0', 'a'] => [{ a: PropertyKey }]
|
||||
//
|
||||
// Input: ['a', 'b', 'c'], constrain: PropertyKey
|
||||
// Output: { a: { b: { c: PropertyKey } } }
|
||||
|
||||
export type UnionToIntersection<Union> = (
|
||||
Union extends unknown
|
||||
? (distributedUnion: Union) => void
|
||||
: never
|
||||
) extends ((mergedIntersection: infer Intersection) => void)
|
||||
? Intersection & Union
|
||||
: never;
|
||||
|
||||
|
||||
type PathToType<T extends string[]> = T extends [infer Head, ...infer Rest]
|
||||
? Head extends string
|
||||
? Head extends `${number}`
|
||||
? Rest extends string[]
|
||||
? PathToType<Rest>[]
|
||||
: never
|
||||
: Rest extends string[]
|
||||
? { [K in Head & string]: PathToType<Rest> }
|
||||
: never
|
||||
: never
|
||||
: string;
|
||||
|
||||
export type Generate<T extends string> = UnionToIntersection<PathToType<Path<T>>>;
|
||||
type Get<O, K> = GetWithArray<O, Path<K>>;
|
||||
|
||||
export function getByPath<O, K extends string>(obj: O, path: K): Get<O, K>;
|
||||
export function getByPath(obj: Record<string, unknown>, path: string): unknown {
|
||||
const keys = path.split('.');
|
||||
let currentObj = obj;
|
||||
|
||||
for (const key of keys) {
|
||||
const value = currentObj[key];
|
||||
|
||||
if (value === undefined || value === null) return undefined;
|
||||
|
||||
currentObj = value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
return currentObj;
|
||||
}
|
||||
1
core/stdlib/src/collections/index.ts
Normal file
1
core/stdlib/src/collections/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './getByPath';
|
||||
11
core/stdlib/src/index.ts
Normal file
11
core/stdlib/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export * from './arrays';
|
||||
export * from './async';
|
||||
export * from './bits';
|
||||
export * from './math';
|
||||
export * from './objects';
|
||||
export * from './patterns';
|
||||
export * from './structs';
|
||||
export * from './sync';
|
||||
export * from './text';
|
||||
export * from './types';
|
||||
export * from './utils'
|
||||
81
core/stdlib/src/math/basic/clamp/index.test.ts
Normal file
81
core/stdlib/src/math/basic/clamp/index.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { describe,it, expect } from 'vitest';
|
||||
import { clamp } from '.';
|
||||
|
||||
describe('clamp', () => {
|
||||
it('clamp a value within the given range', () => {
|
||||
// value < min
|
||||
expect(clamp(-10, 0, 100)).toBe(0);
|
||||
|
||||
// value > max
|
||||
expect(clamp(200, 0, 100)).toBe(100);
|
||||
|
||||
// value within range
|
||||
expect(clamp(50, 0, 100)).toBe(50);
|
||||
|
||||
// value at min
|
||||
expect(clamp(0, 0, 100)).toBe(0);
|
||||
|
||||
// value at max
|
||||
expect(clamp(100, 0, 100)).toBe(100);
|
||||
|
||||
// value at midpoint
|
||||
expect(clamp(50, 100, 100)).toBe(100);
|
||||
});
|
||||
|
||||
it('handle floating-point numbers correctly', () => {
|
||||
// floating-point value within range
|
||||
expect(clamp(3.14, 0, 5)).toBe(3.14);
|
||||
|
||||
// floating-point value < min
|
||||
expect(clamp(-1.5, 0, 10)).toBe(0);
|
||||
|
||||
// floating-point value > max
|
||||
expect(clamp(15.75, 0, 10)).toBe(10);
|
||||
});
|
||||
|
||||
it('handle edge cases', () => {
|
||||
// all values are the same
|
||||
expect(clamp(5, 5, 5)).toBe(5);
|
||||
|
||||
// min > max
|
||||
expect(clamp(10, 100, 50)).toBe(50);
|
||||
|
||||
// negative range and value
|
||||
expect(clamp(-10, -100, -5)).toBe(-10);
|
||||
});
|
||||
|
||||
it('handle NaN and Infinity', () => {
|
||||
// value is NaN
|
||||
expect(clamp(NaN, 0, 100)).toBe(NaN);
|
||||
|
||||
// min is NaN
|
||||
expect(clamp(50, NaN, 100)).toBe(NaN);
|
||||
|
||||
// max is NaN
|
||||
expect(clamp(50, 0, NaN)).toBe(NaN);
|
||||
|
||||
// value is Infinity
|
||||
expect(clamp(Infinity, 0, 100)).toBe(100);
|
||||
|
||||
// min is Infinity
|
||||
expect(clamp(50, Infinity, 100)).toBe(100);
|
||||
|
||||
// max is Infinity
|
||||
expect(clamp(50, 0, Infinity)).toBe(50);
|
||||
|
||||
// min and max are Infinity
|
||||
expect(clamp(50, Infinity, Infinity)).toBe(Infinity);
|
||||
|
||||
// value is -Infinity
|
||||
expect(clamp(-Infinity, 0, 100)).toBe(0);
|
||||
|
||||
// min is -Infinity
|
||||
expect(clamp(50, -Infinity, 100)).toBe(50);
|
||||
|
||||
// max is -Infinity
|
||||
expect(clamp(50, 0, -Infinity)).toBe(-Infinity);
|
||||
|
||||
// min and max are -Infinity
|
||||
expect(clamp(50, -Infinity, -Infinity)).toBe(-Infinity);
|
||||
});
|
||||
});
|
||||
15
core/stdlib/src/math/basic/clamp/index.ts
Normal file
15
core/stdlib/src/math/basic/clamp/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @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
|
||||
* @returns {number} The clamped number
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
61
core/stdlib/src/math/basic/lerp/index.test.ts
Normal file
61
core/stdlib/src/math/basic/lerp/index.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {describe, it, expect} from 'vitest';
|
||||
import {inverseLerp, lerp} from '.';
|
||||
|
||||
describe('lerp', () => {
|
||||
it('interpolates between two values', () => {
|
||||
const result = lerp(0, 10, 0.5);
|
||||
expect(result).toBe(5);
|
||||
});
|
||||
|
||||
it('returns start value when t is 0', () => {
|
||||
const result = lerp(0, 10, 0);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('returns end value when t is 1', () => {
|
||||
const result = lerp(0, 10, 1);
|
||||
expect(result).toBe(10);
|
||||
});
|
||||
|
||||
it('handles negative interpolation values', () => {
|
||||
const result = lerp(0, 10, -0.5);
|
||||
expect(result).toBe(-5);
|
||||
});
|
||||
|
||||
it('handles interpolation values greater than 1', () => {
|
||||
const result = lerp(0, 10, 1.5);
|
||||
expect(result).toBe(15);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inverseLerp', () => {
|
||||
it('returns 0 when value is start', () => {
|
||||
const result = inverseLerp(0, 10, 0);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('returns 1 when value is end', () => {
|
||||
const result = inverseLerp(0, 10, 10);
|
||||
expect(result).toBe(1);
|
||||
});
|
||||
|
||||
it('interpolates correctly between two values', () => {
|
||||
const result = inverseLerp(0, 10, 5);
|
||||
expect(result).toBe(0.5);
|
||||
});
|
||||
|
||||
it('handles values less than start', () => {
|
||||
const result = inverseLerp(0, 10, -5);
|
||||
expect(result).toBe(-0.5);
|
||||
});
|
||||
|
||||
it('handles values greater than end', () => {
|
||||
const result = inverseLerp(0, 10, 15);
|
||||
expect(result).toBe(1.5);
|
||||
});
|
||||
|
||||
it('handles same start and end values', () => {
|
||||
const result = inverseLerp(10, 10, 10);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
});
|
||||
31
core/stdlib/src/math/basic/lerp/index.ts
Normal file
31
core/stdlib/src/math/basic/lerp/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @name lerp
|
||||
* @category Math
|
||||
* @description Linearly interpolates between two values
|
||||
*
|
||||
* @param {number} start The start value
|
||||
* @param {number} end The end value
|
||||
* @param {number} t The interpolation value
|
||||
* @returns {number} The interpolated value
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function lerp(start: number, end: number, t: number) {
|
||||
return start + t * (end - start);
|
||||
}
|
||||
|
||||
/**
|
||||
* @name inverseLerp
|
||||
* @category Math
|
||||
* @description Inverse linear interpolation between two values
|
||||
*
|
||||
* @param {number} start The start value
|
||||
* @param {number} end The end value
|
||||
* @param {number} value The value to interpolate
|
||||
* @returns {number} The interpolated value
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function inverseLerp(start: number, end: number, value: number) {
|
||||
return start === end ? 0 : (value - start) / (end - start);
|
||||
}
|
||||
46
core/stdlib/src/math/basic/remap/index.test.ts
Normal file
46
core/stdlib/src/math/basic/remap/index.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {remap} from '.';
|
||||
|
||||
describe('remap', () => {
|
||||
it('map values from one range to another', () => {
|
||||
// value at midpoint
|
||||
expect(remap(5, 0, 10, 0, 100)).toBe(50);
|
||||
|
||||
// value at min
|
||||
expect(remap(0, 0, 10, 0, 100)).toBe(0);
|
||||
|
||||
// value at max
|
||||
expect(remap(10, 0, 10, 0, 100)).toBe(100);
|
||||
|
||||
// value outside range (below)
|
||||
expect(remap(-5, 0, 10, 0, 100)).toBe(0);
|
||||
|
||||
// value outside range (above)
|
||||
expect(remap(15, 0, 10, 0, 100)).toBe(100);
|
||||
|
||||
// value at midpoint of negative range
|
||||
expect(remap(75, 50, 100, -50, 50)).toBe(0);
|
||||
|
||||
// value at midpoint of negative range
|
||||
expect(remap(-25, -50, 0, 0, 100)).toBe(50);
|
||||
});
|
||||
|
||||
it('handle floating-point numbers correctly', () => {
|
||||
// floating-point value
|
||||
expect(remap(3.5, 0, 10, 0, 100)).toBe(35);
|
||||
|
||||
// positive floating-point ranges
|
||||
expect(remap(1.25, 0, 2.5, 0, 100)).toBe(50);
|
||||
|
||||
// negative floating-point value
|
||||
expect(remap(-2.5, -5, 0, 0, 100)).toBe(50);
|
||||
|
||||
// negative floating-point ranges
|
||||
expect(remap(-1.25, -2.5, 0, 0, 100)).toBe(50);
|
||||
});
|
||||
|
||||
it('handle edge cases', () => {
|
||||
// input range is zero (should return output min)
|
||||
expect(remap(5, 0, 0, 0, 100)).toBe(0);
|
||||
});
|
||||
});
|
||||
25
core/stdlib/src/math/basic/remap/index.ts
Normal file
25
core/stdlib/src/math/basic/remap/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { clamp } from '../clamp';
|
||||
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
|
||||
* @param {number} out_min The minimum value of the output range
|
||||
* @param {number} out_max The maximum value of the output range
|
||||
* @returns {number} The mapped value
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export function remap(value: number, in_min: number, in_max: number, out_min: number, out_max: number) {
|
||||
if (in_min === in_max)
|
||||
return out_min;
|
||||
|
||||
const clampedValue = clamp(value, in_min, in_max);
|
||||
|
||||
return lerp(out_min, out_max, inverseLerp(in_min, in_max, clampedValue));
|
||||
}
|
||||
35
core/stdlib/src/math/bigint/clampBigInt/index.test.ts
Normal file
35
core/stdlib/src/math/bigint/clampBigInt/index.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import {describe, it, expect} from 'vitest';
|
||||
import {clampBigInt} from '.';
|
||||
|
||||
describe('clampBigInt', () => {
|
||||
it('clamp a value within the given range', () => {
|
||||
// value < min
|
||||
expect(clampBigInt(-10n, 0n, 100n)).toBe(0n);
|
||||
|
||||
// value > max
|
||||
expect(clampBigInt(200n, 0n, 100n)).toBe(100n);
|
||||
|
||||
// value within range
|
||||
expect(clampBigInt(50n, 0n, 100n)).toBe(50n);
|
||||
|
||||
// value at min
|
||||
expect(clampBigInt(0n, 0n, 100n)).toBe(0n);
|
||||
|
||||
// value at max
|
||||
expect(clampBigInt(100n, 0n, 100n)).toBe(100n);
|
||||
|
||||
// value at midpoint
|
||||
expect(clampBigInt(50n, 100n, 100n)).toBe(100n);
|
||||
});
|
||||
|
||||
it('handle edge cases', () => {
|
||||
// all values are the same
|
||||
expect(clampBigInt(5n, 5n, 5n)).toBe(5n);
|
||||
|
||||
// min > max
|
||||
expect(clampBigInt(10n, 100n, 50n)).toBe(50n);
|
||||
|
||||
// negative range and value
|
||||
expect(clampBigInt(-10n, -100n, -5n)).toBe(-10n);
|
||||
});
|
||||
});
|
||||
18
core/stdlib/src/math/bigint/clampBigInt/index.ts
Normal file
18
core/stdlib/src/math/bigint/clampBigInt/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {minBigInt} from '../minBigInt';
|
||||
import {maxBigInt} from '../maxBigInt';
|
||||
|
||||
/**
|
||||
* @name clampBigInt
|
||||
* @category Math
|
||||
* @description Clamps a bigint between a minimum and maximum value
|
||||
*
|
||||
* @param {bigint} value The number to clamp
|
||||
* @param {bigint} min Minimum value
|
||||
* @param {bigint} max Maximum value
|
||||
* @returns {bigint} The clamped number
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function clampBigInt(value: bigint, min: bigint, max: bigint) {
|
||||
return minBigInt(maxBigInt(value, min), max);
|
||||
}
|
||||
83
core/stdlib/src/math/bigint/lerpBigInt/index.test.ts
Normal file
83
core/stdlib/src/math/bigint/lerpBigInt/index.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import {describe, it, expect} from 'vitest';
|
||||
import {inverseLerpBigInt, lerpBigInt} from '.';
|
||||
|
||||
const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
|
||||
|
||||
describe('lerpBigInt', () => {
|
||||
it('interpolates between two bigint values', () => {
|
||||
const result = lerpBigInt(0n, 10n, 0.5);
|
||||
expect(result).toBe(5n);
|
||||
});
|
||||
|
||||
it('returns start value when t is 0', () => {
|
||||
const result = lerpBigInt(0n, 10n, 0);
|
||||
expect(result).toBe(0n);
|
||||
});
|
||||
|
||||
it('returns end value when t is 1', () => {
|
||||
const result = lerpBigInt(0n, 10n, 1);
|
||||
expect(result).toBe(10n);
|
||||
});
|
||||
|
||||
it('handles negative interpolation values', () => {
|
||||
const result = lerpBigInt(0n, 10n, -0.5);
|
||||
expect(result).toBe(-5n);
|
||||
});
|
||||
|
||||
it('handles interpolation values greater than 1', () => {
|
||||
const result = lerpBigInt(0n, 10n, 1.5);
|
||||
expect(result).toBe(15n);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inverseLerpBigInt', () => {
|
||||
it('returns 0 when value is start', () => {
|
||||
const result = inverseLerpBigInt(0n, 10n, 0n);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('returns 1 when value is end', () => {
|
||||
const result = inverseLerpBigInt(0n, 10n, 10n);
|
||||
expect(result).toBe(1);
|
||||
});
|
||||
|
||||
it('interpolates correctly between two bigint values', () => {
|
||||
const result = inverseLerpBigInt(0n, 10n, 5n);
|
||||
expect(result).toBe(0.5);
|
||||
});
|
||||
|
||||
it('handles values less than start', () => {
|
||||
const result = inverseLerpBigInt(0n, 10n, -5n);
|
||||
expect(result).toBe(-0.5);
|
||||
});
|
||||
|
||||
it('handles values greater than end', () => {
|
||||
const result = inverseLerpBigInt(0n, 10n, 15n);
|
||||
expect(result).toBe(1.5);
|
||||
});
|
||||
|
||||
it('handles same start and end values', () => {
|
||||
const result = inverseLerpBigInt(10n, 10n, 10n);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('handles the maximum safe integer correctly', () => {
|
||||
const result = inverseLerpBigInt(0n, MAX_SAFE_INTEGER, MAX_SAFE_INTEGER);
|
||||
expect(result).toBe(1);
|
||||
});
|
||||
|
||||
it('handles values just above the maximum safe integer correctly', () => {
|
||||
const result = inverseLerpBigInt(0n, MAX_SAFE_INTEGER, 0n);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('handles values just below the maximum safe integer correctly', () => {
|
||||
const result = inverseLerpBigInt(0n, MAX_SAFE_INTEGER, MAX_SAFE_INTEGER);
|
||||
expect(result).toBe(1);
|
||||
});
|
||||
|
||||
it('handles values just above the maximum safe integer correctly', () => {
|
||||
const result = inverseLerpBigInt(0n, 2n ** 128n, 2n ** 127n);
|
||||
expect(result).toBe(0.5);
|
||||
});
|
||||
});
|
||||
31
core/stdlib/src/math/bigint/lerpBigInt/index.ts
Normal file
31
core/stdlib/src/math/bigint/lerpBigInt/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @name lerpBigInt
|
||||
* @category Math
|
||||
* @description Linearly interpolates between bigint values
|
||||
*
|
||||
* @param {bigint} start The start value
|
||||
* @param {bigint} end The end value
|
||||
* @param {number} t The interpolation value
|
||||
* @returns {bigint} The interpolated value
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function lerpBigInt(start: bigint, end: bigint, t: number) {
|
||||
return start + ((end - start) * BigInt(t * 10000)) / 10000n;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name inverseLerpBigInt
|
||||
* @category Math
|
||||
* @description Inverse linear interpolation between two bigint values
|
||||
*
|
||||
* @param {bigint} start The start value
|
||||
* @param {bigint} end The end value
|
||||
* @param {bigint} value The value to interpolate
|
||||
* @returns {number} The interpolated value
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function inverseLerpBigInt(start: bigint, end: bigint, value: bigint) {
|
||||
return start === end ? 0 : Number((value - start) * 10000n / (end - start)) / 10000;
|
||||
}
|
||||
39
core/stdlib/src/math/bigint/maxBigInt/index.test.ts
Normal file
39
core/stdlib/src/math/bigint/maxBigInt/index.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { maxBigInt } from '.';
|
||||
|
||||
describe('maxBigInt', () => {
|
||||
it('returns -Infinity when no values are provided', () => {
|
||||
expect(() => maxBigInt()).toThrow(new TypeError('maxBigInt requires at least one argument'));
|
||||
});
|
||||
|
||||
it('returns the largest value from a list of positive bigints', () => {
|
||||
const result = maxBigInt(10n, 20n, 5n, 15n);
|
||||
expect(result).toBe(20n);
|
||||
});
|
||||
|
||||
it('returns the largest value from a list of negative bigints', () => {
|
||||
const result = maxBigInt(-10n, -20n, -5n, -15n);
|
||||
expect(result).toBe(-5n);
|
||||
});
|
||||
|
||||
it('returns the largest value from a list of mixed positive and negative bigints', () => {
|
||||
const result = maxBigInt(10n, -20n, 5n, -15n);
|
||||
expect(result).toBe(10n);
|
||||
});
|
||||
|
||||
it('returns the value itself when only one bigint is provided', () => {
|
||||
const result = maxBigInt(10n);
|
||||
expect(result).toBe(10n);
|
||||
});
|
||||
|
||||
it('returns the largest value when all values are the same', () => {
|
||||
const result = maxBigInt(10n, 10n, 10n);
|
||||
expect(result).toBe(10n);
|
||||
});
|
||||
|
||||
it('handles a large number of bigints', () => {
|
||||
const values = Array.from({ length: 1000 }, (_, i) => BigInt(i));
|
||||
const result = maxBigInt(...values);
|
||||
expect(result).toBe(999n);
|
||||
});
|
||||
});
|
||||
17
core/stdlib/src/math/bigint/maxBigInt/index.ts
Normal file
17
core/stdlib/src/math/bigint/maxBigInt/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @name maxBigInt
|
||||
* @category Math
|
||||
* @description Like `Math.max` but for BigInts
|
||||
*
|
||||
* @param {...bigint} values The values to compare
|
||||
* @returns {bigint} The largest value
|
||||
* @throws {TypeError} If no arguments are provided
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function maxBigInt(...values: bigint[]) {
|
||||
if (!values.length)
|
||||
throw new TypeError('maxBigInt requires at least one argument');
|
||||
|
||||
return values.reduce((acc, val) => val > acc ? val : acc);
|
||||
}
|
||||
39
core/stdlib/src/math/bigint/minBigInt/index.test.ts
Normal file
39
core/stdlib/src/math/bigint/minBigInt/index.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {describe, it, expect} from 'vitest';
|
||||
import {minBigInt} from '.';
|
||||
|
||||
describe('minBigInt', () => {
|
||||
it('returns Infinity when no values are provided', () => {
|
||||
expect(() => minBigInt()).toThrow(new TypeError('minBigInt requires at least one argument'));
|
||||
});
|
||||
|
||||
it('returns the smallest value from a list of positive bigints', () => {
|
||||
const result = minBigInt(10n, 20n, 5n, 15n);
|
||||
expect(result).toBe(5n);
|
||||
});
|
||||
|
||||
it('returns the smallest value from a list of negative bigints', () => {
|
||||
const result = minBigInt(-10n, -20n, -5n, -15n);
|
||||
expect(result).toBe(-20n);
|
||||
});
|
||||
|
||||
it('returns the smallest value from a list of mixed positive and negative bigints', () => {
|
||||
const result = minBigInt(10n, -20n, 5n, -15n);
|
||||
expect(result).toBe(-20n);
|
||||
});
|
||||
|
||||
it('returns the value itself when only one bigint is provided', () => {
|
||||
const result = minBigInt(10n);
|
||||
expect(result).toBe(10n);
|
||||
});
|
||||
|
||||
it('returns the smallest value when all values are the same', () => {
|
||||
const result = minBigInt(10n, 10n, 10n);
|
||||
expect(result).toBe(10n);
|
||||
});
|
||||
|
||||
it('handles a large number of bigints', () => {
|
||||
const values = Array.from({length: 1000}, (_, i) => BigInt(i));
|
||||
const result = minBigInt(...values);
|
||||
expect(result).toBe(0n);
|
||||
});
|
||||
});
|
||||
17
core/stdlib/src/math/bigint/minBigInt/index.ts
Normal file
17
core/stdlib/src/math/bigint/minBigInt/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @name minBigInt
|
||||
* @category Math
|
||||
* @description Like `Math.min` but for BigInts
|
||||
*
|
||||
* @param {...bigint} values The values to compare
|
||||
* @returns {bigint} The smallest value
|
||||
* @throws {TypeError} If no arguments are provided
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export function minBigInt(...values: bigint[]) {
|
||||
if (!values.length)
|
||||
throw new TypeError('minBigInt requires at least one argument');
|
||||
|
||||
return values.reduce((acc, val) => val < acc ? val : acc);
|
||||
}
|
||||
32
core/stdlib/src/math/bigint/remapBigInt/index.test.ts
Normal file
32
core/stdlib/src/math/bigint/remapBigInt/index.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {remapBigInt} from '.';
|
||||
|
||||
describe('remapBigInt', () => {
|
||||
it('map values from one range to another', () => {
|
||||
// value at midpoint
|
||||
expect(remapBigInt(5n, 0n, 10n, 0n, 100n)).toBe(50n);
|
||||
|
||||
// value at min
|
||||
expect(remapBigInt(0n, 0n, 10n, 0n, 100n)).toBe(0n);
|
||||
|
||||
// value at max
|
||||
expect(remapBigInt(10n, 0n, 10n, 0n, 100n)).toBe(100n);
|
||||
|
||||
// value outside range (below)
|
||||
expect(remapBigInt(-5n, 0n, 10n, 0n, 100n)).toBe(0n);
|
||||
|
||||
// value outside range (above)
|
||||
expect(remapBigInt(15n, 0n, 10n, 0n, 100n)).toBe(100n);
|
||||
|
||||
// value at midpoint of negative range
|
||||
expect(remapBigInt(75n, 50n, 100n, -50n, 50n)).toBe(0n);
|
||||
|
||||
// value at midpoint of negative range
|
||||
expect(remapBigInt(-25n, -50n, 0n, 0n, 100n)).toBe(50n);
|
||||
});
|
||||
|
||||
it('handle edge cases', () => {
|
||||
// input range is zero (should return output min)
|
||||
expect(remapBigInt(5n, 0n, 0n, 0n, 100n)).toBe(0n);
|
||||
});
|
||||
});
|
||||
25
core/stdlib/src/math/bigint/remapBigInt/index.ts
Normal file
25
core/stdlib/src/math/bigint/remapBigInt/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { clampBigInt } from '../clampBigInt';
|
||||
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
|
||||
* @param {bigint} out_min The minimum value of the output range
|
||||
* @param {bigint} out_max The maximum value of the output range
|
||||
* @returns {bigint} The mapped value
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export function remapBigInt(value: bigint, in_min: bigint, in_max: bigint, out_min: bigint, out_max: bigint) {
|
||||
if (in_min === in_max)
|
||||
return out_min;
|
||||
|
||||
const clampedValue = clampBigInt(value, in_min, in_max);
|
||||
|
||||
return lerpBigInt(out_min, out_max, inverseLerpBigInt(in_min, in_max, clampedValue));
|
||||
}
|
||||
9
core/stdlib/src/math/index.ts
Normal file
9
core/stdlib/src/math/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from './basic/clamp';
|
||||
export * from './basic/lerp';
|
||||
export * from './basic/remap';
|
||||
|
||||
export * from './bigint/clampBigInt';
|
||||
export * from './bigint/lerpBigInt';
|
||||
export * from './bigint/maxBigInt';
|
||||
export * from './bigint/minBigInt';
|
||||
export * from './bigint/remapBigInt';
|
||||
2
core/stdlib/src/objects/index.ts
Normal file
2
core/stdlib/src/objects/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './omit';
|
||||
export * from './pick';
|
||||
50
core/stdlib/src/objects/omit/index.test.ts
Normal file
50
core/stdlib/src/objects/omit/index.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { omit } from '.';
|
||||
|
||||
describe('omit', () => {
|
||||
it('omit a single key from the object', () => {
|
||||
const result = omit({ a: 1, b: 2, c: 3 }, 'a');
|
||||
|
||||
expect(result).toEqual({ b: 2, c: 3 });
|
||||
});
|
||||
|
||||
it('omit multiple keys from the object', () => {
|
||||
const result = omit({ a: 1, b: 2, c: 3 }, ['a', 'b']);
|
||||
|
||||
expect(result).toEqual({ c: 3 });
|
||||
});
|
||||
|
||||
it('return the same object if no keys are omitted', () => {
|
||||
const result = omit({ a: 1, b: 2, c: 3 }, []);
|
||||
|
||||
expect(result).toEqual({ a: 1, b: 2, c: 3 });
|
||||
});
|
||||
|
||||
it('not modify the original object', () => {
|
||||
const obj = { a: 1, b: 2, c: 3 };
|
||||
const result = omit(obj, 'a');
|
||||
|
||||
expect(obj).toEqual({ a: 1, b: 2, c: 3 });
|
||||
expect(result).toEqual({ b: 2, c: 3 });
|
||||
});
|
||||
|
||||
it('handle an empty object', () => {
|
||||
const result = omit({}, 'a' as any);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('handle non-existent keys gracefully', () => {
|
||||
const result = omit({ a: 1, b: 2, c: 3 } as Record<string, number>, 'd');
|
||||
|
||||
expect(result).toEqual({ a: 1, b: 2, c: 3 });
|
||||
});
|
||||
|
||||
it('handle null gracefully', () => {
|
||||
const emptyTarget = omit(null as any, 'a');
|
||||
const emptyKeys = omit({ a: 1 }, null as any);
|
||||
|
||||
expect(emptyTarget).toEqual({});
|
||||
expect(emptyKeys).toEqual({ a: 1 });
|
||||
});
|
||||
});
|
||||
38
core/stdlib/src/objects/omit/index.ts
Normal file
38
core/stdlib/src/objects/omit/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { isArray, 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>
|
||||
): Omit<Target, Key> {
|
||||
const result = { ...target };
|
||||
|
||||
if (!target || !keys)
|
||||
return result;
|
||||
|
||||
if (isArray(keys)) {
|
||||
for (const key of keys) {
|
||||
delete result[key];
|
||||
}
|
||||
} else {
|
||||
delete result[keys];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
36
core/stdlib/src/objects/pick/index.test.ts
Normal file
36
core/stdlib/src/objects/pick/index.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { pick } from '.';
|
||||
|
||||
describe('pick', () => {
|
||||
it('pick a single key', () => {
|
||||
const result = pick({ a: 1, b: 2, c: 3 }, 'a');
|
||||
|
||||
expect(result).toEqual({ a: 1 });
|
||||
});
|
||||
|
||||
it('pick multiple keys', () => {
|
||||
const result = pick({ a: 1, b: 2, c: 3 }, ['a', 'b']);
|
||||
|
||||
expect(result).toEqual({ a: 1, b: 2 });
|
||||
});
|
||||
|
||||
it('return an empty object when no keys are provided', () => {
|
||||
const result = pick({ a: 1, b: 2 }, []);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('handle non-existent keys by setting them to undefined', () => {
|
||||
const result = pick({ a: 1, b: 2 }, ['a', 'c'] as any);
|
||||
|
||||
expect(result).toEqual({ a: 1, c: undefined });
|
||||
});
|
||||
|
||||
it('return an empty object if target is null or undefined', () => {
|
||||
const emptyTarget = pick(null as any, 'a');
|
||||
const emptyKeys = pick({ a: 1 }, null as any);
|
||||
|
||||
expect(emptyTarget).toEqual({});
|
||||
expect(emptyKeys).toEqual({});
|
||||
});
|
||||
});
|
||||
38
core/stdlib/src/objects/pick/index.ts
Normal file
38
core/stdlib/src/objects/pick/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { isArray, 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>
|
||||
): Pick<Target, Key> {
|
||||
const result = {} as Pick<Target, Key>;
|
||||
|
||||
if (!target || !keys)
|
||||
return result;
|
||||
|
||||
if (isArray(keys)) {
|
||||
for (const key of keys) {
|
||||
result[key] = target[key];
|
||||
}
|
||||
} else {
|
||||
result[keys] = target[keys];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
118
core/stdlib/src/patterns/behavioral/pubsub/index.test.ts
Normal file
118
core/stdlib/src/patterns/behavioral/pubsub/index.test.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { PubSub } from '.';
|
||||
|
||||
describe('pubsub', () => {
|
||||
const event3 = Symbol('event3');
|
||||
|
||||
let eventBus: PubSub<{
|
||||
event1: (arg: string) => void;
|
||||
event2: () => void;
|
||||
[event3]: () => void;
|
||||
}>;
|
||||
|
||||
beforeEach(() => {
|
||||
eventBus = new PubSub();
|
||||
});
|
||||
|
||||
it('add a listener and emit an event', () => {
|
||||
const listener = vi.fn();
|
||||
|
||||
eventBus.on('event1', listener);
|
||||
eventBus.emit('event1', 'Hello');
|
||||
|
||||
expect(listener).toHaveBeenCalledWith('Hello');
|
||||
});
|
||||
|
||||
it('add multiple listeners and emit an event', () => {
|
||||
const listener1 = vi.fn();
|
||||
const listener2 = vi.fn();
|
||||
|
||||
eventBus.on('event1', listener1);
|
||||
eventBus.on('event1', listener2);
|
||||
eventBus.emit('event1', 'Hello');
|
||||
|
||||
expect(listener1).toHaveBeenCalledWith('Hello');
|
||||
expect(listener2).toHaveBeenCalledWith('Hello');
|
||||
});
|
||||
|
||||
it('emit symbol event', () => {
|
||||
const listener = vi.fn();
|
||||
|
||||
eventBus.on(event3, listener);
|
||||
eventBus.emit(event3);
|
||||
|
||||
expect(listener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('add a one-time listener and emit an event', () => {
|
||||
const listener = vi.fn();
|
||||
|
||||
eventBus.once('event1', listener);
|
||||
eventBus.emit('event1', 'Hello');
|
||||
eventBus.emit('event1', 'World');
|
||||
|
||||
expect(listener).toHaveBeenCalledWith('Hello');
|
||||
expect(listener).not.toHaveBeenCalledWith('World');
|
||||
});
|
||||
|
||||
it('add once listener and emit multiple events', () => {
|
||||
const listener = vi.fn();
|
||||
|
||||
eventBus.once('event1', listener);
|
||||
eventBus.emit('event1', 'Hello');
|
||||
eventBus.emit('event1', 'World');
|
||||
eventBus.emit('event1', '!');
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
expect(listener).toHaveBeenCalledWith('Hello');
|
||||
});
|
||||
|
||||
it('remove a listener', () => {
|
||||
const listener = vi.fn();
|
||||
|
||||
eventBus.on('event1', listener);
|
||||
eventBus.off('event1', listener);
|
||||
eventBus.emit('event1', 'Hello');
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clear all listeners for an event', () => {
|
||||
const listener1 = vi.fn();
|
||||
const listener2 = vi.fn();
|
||||
|
||||
eventBus.on('event1', listener1);
|
||||
eventBus.on('event1', listener2);
|
||||
eventBus.clear('event1');
|
||||
eventBus.emit('event1', 'Hello');
|
||||
|
||||
expect(listener1).not.toHaveBeenCalled();
|
||||
expect(listener2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('return true when emitting an event with listeners', () => {
|
||||
const listener = vi.fn();
|
||||
|
||||
eventBus.on('event1', listener);
|
||||
const result = eventBus.emit('event1', 'Hello');
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('return false when emitting an event without listeners', () => {
|
||||
const result = eventBus.emit('event1', 'Hello');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('calls listener only once when the same function is registered multiple times', () => {
|
||||
const listener = vi.fn();
|
||||
|
||||
eventBus.on('event1', listener);
|
||||
eventBus.on('event1', listener);
|
||||
eventBus.on('event1', listener);
|
||||
eventBus.emit('event1', 'Hello');
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
118
core/stdlib/src/patterns/behavioral/pubsub/index.ts
Normal file
118
core/stdlib/src/patterns/behavioral/pubsub/index.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import type { AnyFunction } from '../../../types';
|
||||
|
||||
export type Subscriber = AnyFunction;
|
||||
|
||||
export type EventHandlerMap = Record<PropertyKey, Subscriber>;
|
||||
|
||||
/**
|
||||
* @name PubSub
|
||||
* @category Patterns
|
||||
* @description Simple PubSub implementation
|
||||
*
|
||||
* @since 0.0.2
|
||||
*
|
||||
* @template Events - Event map where all values are function types
|
||||
*/
|
||||
export class PubSub<Events extends EventHandlerMap> {
|
||||
/**
|
||||
* Events map
|
||||
*
|
||||
* @private
|
||||
* @type {Map<keyof Events, Set<Events[keyof Events]>>}
|
||||
*/
|
||||
private events: Map<keyof Events, Set<Events[keyof Events]>>;
|
||||
|
||||
/**
|
||||
* Creates an instance of PubSub
|
||||
*/
|
||||
constructor() {
|
||||
this.events = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to an event
|
||||
*
|
||||
* @template {keyof Events} K
|
||||
* @param {K} event Name of the event
|
||||
* @param {Events[K]} listener Listener function
|
||||
* @returns {this}
|
||||
*/
|
||||
public on<K extends keyof Events>(event: K, listener: Events[K]) {
|
||||
const listeners = this.events.get(event);
|
||||
|
||||
if (listeners)
|
||||
listeners.add(listener);
|
||||
else
|
||||
this.events.set(event, new Set([listener]));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from an event
|
||||
*
|
||||
* @template {keyof Events} K
|
||||
* @param {K} event Name of the event
|
||||
* @param {Events[K]} listener Listener function
|
||||
* @returns {this}
|
||||
*/
|
||||
public off<K extends keyof Events>(event: K, listener: Events[K]) {
|
||||
const listeners = this.events.get(event);
|
||||
|
||||
if (listeners)
|
||||
listeners.delete(listener);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to an event only once
|
||||
*
|
||||
* @template {keyof Events} K
|
||||
* @param {K} event Name of the event
|
||||
* @param {Events[K]} listener Listener function
|
||||
* @returns {this}
|
||||
*/
|
||||
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.on(event, onceListener as Events[K]);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event
|
||||
*
|
||||
* @template {keyof Events} K
|
||||
* @param {K} event Name of the event
|
||||
* @param {...Parameters<Events[K]>} args Arguments for the listener
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>) {
|
||||
const listeners = this.events.get(event);
|
||||
|
||||
if (!listeners)
|
||||
return false;
|
||||
|
||||
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}
|
||||
*/
|
||||
public clear<K extends keyof Events>(event: K) {
|
||||
this.events.delete(event);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
1
core/stdlib/src/patterns/index.ts
Normal file
1
core/stdlib/src/patterns/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './behavioral/pubsub';
|
||||
1
core/stdlib/src/structs/index.ts
Normal file
1
core/stdlib/src/structs/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './stack';
|
||||
119
core/stdlib/src/structs/stack/index.test.ts
Normal file
119
core/stdlib/src/structs/stack/index.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Stack } from '.';
|
||||
|
||||
describe('stack', () => {
|
||||
describe('constructor', () => {
|
||||
it('create an empty stack if no initial values are provided', () => {
|
||||
const stack = new Stack<number>();
|
||||
|
||||
expect(stack.length).toBe(0);
|
||||
expect(stack.isEmpty).toBe(true);
|
||||
});
|
||||
|
||||
it('create a stack with the provided initial values', () => {
|
||||
const initialValues = [1, 2, 3];
|
||||
const stack = new Stack(initialValues);
|
||||
|
||||
expect(stack.length).toBe(initialValues.length);
|
||||
expect(stack.peek()).toBe(initialValues.at(-1));
|
||||
});
|
||||
|
||||
it('create a stack with the provided initial value', () => {
|
||||
const initialValue = 1;
|
||||
const stack = new Stack(initialValue);
|
||||
|
||||
expect(stack.length).toBe(1);
|
||||
expect(stack.peek()).toBe(initialValue);
|
||||
});
|
||||
|
||||
it('create a stack with the provided options', () => {
|
||||
const options = { maxSize: 5 };
|
||||
const stack = new Stack<number>(undefined, options);
|
||||
|
||||
expect(stack.length).toBe(0);
|
||||
expect(stack.isFull).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('push', () => {
|
||||
it('push an element onto the stack', () => {
|
||||
const stack = new Stack<number>();
|
||||
stack.push(1);
|
||||
|
||||
expect(stack.length).toBe(1);
|
||||
expect(stack.peek()).toBe(1);
|
||||
});
|
||||
|
||||
it('throw an error if the stack is full', () => {
|
||||
const options = { maxSize: 1 };
|
||||
const stack = new Stack<number>(undefined, options);
|
||||
stack.push(1);
|
||||
|
||||
expect(() => stack.push(2)).toThrow(new RangeError('Stack is full'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('pop', () => {
|
||||
it('pop an element from the stack', () => {
|
||||
const stack = new Stack<number>([1, 2, 3]);
|
||||
const poppedElement = stack.pop();
|
||||
|
||||
expect(poppedElement).toBe(3);
|
||||
expect(stack.length).toBe(2);
|
||||
});
|
||||
|
||||
it('return undefined if the stack is empty', () => {
|
||||
const stack = new Stack<number>();
|
||||
const poppedElement = stack.pop();
|
||||
|
||||
expect(poppedElement).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('peek', () => {
|
||||
it('return the top element of the stack', () => {
|
||||
const stack = new Stack<number>([1, 2, 3]);
|
||||
const topElement = stack.peek();
|
||||
|
||||
expect(topElement).toBe(3);
|
||||
expect(stack.length).toBe(3);
|
||||
});
|
||||
|
||||
it('return undefined if the stack is empty', () => {
|
||||
const stack = new Stack<number>();
|
||||
const topElement = stack.peek();
|
||||
|
||||
expect(topElement).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('clear the stack', () => {
|
||||
const stack = new Stack<number>([1, 2, 3]);
|
||||
stack.clear();
|
||||
|
||||
expect(stack.length).toBe(0);
|
||||
expect(stack.isEmpty).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('iteration', () => {
|
||||
it('iterate over the stack in LIFO order', () => {
|
||||
const stack = new Stack<number>([1, 2, 3]);
|
||||
const elements = [...stack];
|
||||
|
||||
expect(elements).toEqual([3, 2, 1]);
|
||||
});
|
||||
|
||||
it('iterate over the stack asynchronously in LIFO order', async () => {
|
||||
const stack = new Stack<number>([1, 2, 3]);
|
||||
const elements: number[] = [];
|
||||
|
||||
for await (const element of stack) {
|
||||
elements.push(element);
|
||||
}
|
||||
|
||||
expect(elements).toEqual([3, 2, 1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
152
core/stdlib/src/structs/stack/index.ts
Normal file
152
core/stdlib/src/structs/stack/index.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { last } from '../../arrays';
|
||||
import { isArray } from '../../types';
|
||||
|
||||
export type StackOptions = {
|
||||
maxSize?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* @name Stack
|
||||
* @category Data Structures
|
||||
* @description Represents a stack data structure
|
||||
*
|
||||
* @since 0.0.2
|
||||
*
|
||||
* @template T The type of elements stored in the stack
|
||||
*/
|
||||
export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
|
||||
/**
|
||||
* The maximum number of elements that the stack can hold
|
||||
*
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
private readonly maxSize: number;
|
||||
|
||||
/**
|
||||
* The stack data structure
|
||||
*
|
||||
* @private
|
||||
* @type {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] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of elements in the stack
|
||||
* @returns {number} The number of elements in the stack
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops an element from the stack
|
||||
* @returns {T | undefined} The element popped from the stack
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the stack
|
||||
*
|
||||
* @returns {this}
|
||||
*/
|
||||
public clear() {
|
||||
this.stack.length = 0;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the stack to an array
|
||||
*
|
||||
* @returns {T[]}
|
||||
*/
|
||||
public toArray() {
|
||||
return this.stack.toReversed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the stack
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
public toString() {
|
||||
return this.toArray().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator for the stack
|
||||
*
|
||||
* @returns {IterableIterator<T>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
core/stdlib/src/sync/index.ts
Normal file
1
core/stdlib/src/sync/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './mutex';
|
||||
94
core/stdlib/src/sync/mutex/index.test.ts
Normal file
94
core/stdlib/src/sync/mutex/index.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { SyncMutex } from '.';
|
||||
|
||||
describe('SyncMutex', () => {
|
||||
let mutex: SyncMutex;
|
||||
|
||||
beforeEach(() => {
|
||||
mutex = new SyncMutex();
|
||||
});
|
||||
|
||||
it('unlocked by default', () => {
|
||||
expect(mutex.isLocked).toBe(false);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('remain unlocked when unlocked multiple times', () => {
|
||||
mutex.unlock();
|
||||
mutex.unlock();
|
||||
|
||||
expect(mutex.isLocked).toBe(false);
|
||||
});
|
||||
|
||||
it('reflect the current lock state', () => {
|
||||
expect(mutex.isLocked).toBe(false);
|
||||
mutex.lock();
|
||||
expect(mutex.isLocked).toBe(true);
|
||||
mutex.unlock();
|
||||
expect(mutex.isLocked).toBe(false);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
it('execute concurrent callbacks only one at a time', async () => {
|
||||
const callback = vi.fn(() => Promise.resolve('done'));
|
||||
|
||||
const result = await Promise.all([
|
||||
mutex.execute(callback),
|
||||
mutex.execute(callback),
|
||||
mutex.execute(callback),
|
||||
]);
|
||||
|
||||
expect(result).toEqual(['done', undefined, undefined]);
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not execute a callback when locked', async () => {
|
||||
const callback = vi.fn(() => 'done');
|
||||
mutex.lock();
|
||||
const result = await mutex.execute(callback);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('unlocks after executing a callback', async () => {
|
||||
const callback = vi.fn(() => 'done');
|
||||
await mutex.execute(callback);
|
||||
|
||||
expect(mutex.isLocked).toBe(false);
|
||||
});
|
||||
});
|
||||
47
core/stdlib/src/sync/mutex/index.ts
Normal file
47
core/stdlib/src/sync/mutex/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { MaybePromise } from "../../types";
|
||||
|
||||
/**
|
||||
* @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');
|
||||
* });
|
||||
*
|
||||
* @since 0.0.5
|
||||
*/
|
||||
export class SyncMutex {
|
||||
private state = false;
|
||||
|
||||
public get isLocked() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public lock() {
|
||||
this.state = true;
|
||||
}
|
||||
|
||||
public unlock() {
|
||||
this.state = false;
|
||||
}
|
||||
|
||||
public async execute<T>(callback: () => T) {
|
||||
if (this.isLocked)
|
||||
return;
|
||||
|
||||
this.lock();
|
||||
const result = await callback();
|
||||
this.unlock();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
4
core/stdlib/src/text/index.ts
Normal file
4
core/stdlib/src/text/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './levenshtein-distance';
|
||||
export * from './trigram-distance';
|
||||
// TODO: Template is not implemented yet
|
||||
// export * from './template';
|
||||
32
core/stdlib/src/text/levenshtein-distance/index.test.ts
Normal file
32
core/stdlib/src/text/levenshtein-distance/index.test.ts
Normal file
@@ -0,0 +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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
46
core/stdlib/src/text/levenshtein-distance/index.ts
Normal file
46
core/stdlib/src/text/levenshtein-distance/index.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @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
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export function levenshteinDistance(left: string, right: string): number {
|
||||
if (left === right) return 0;
|
||||
|
||||
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));
|
||||
|
||||
// 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;
|
||||
|
||||
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]!;
|
||||
}
|
||||
105
core/stdlib/src/text/template/index.test-d.ts
Normal file
105
core/stdlib/src/text/template/index.test-d.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { describe, expectTypeOf, it } from "vitest";
|
||||
import type { ClearPlaceholder, ExtractPlaceholders } from "./index";
|
||||
|
||||
describe('template', () => {
|
||||
describe('ClearPlaceholder', () => {
|
||||
it('ignores strings without braces', () => {
|
||||
type actual = ClearPlaceholder<'name'>;
|
||||
type expected = 'name';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('removes all balanced braces from placeholders', () => {
|
||||
type actual1 = ClearPlaceholder<'{name}'>;
|
||||
type actual2 = ClearPlaceholder<'{{name}}'>;
|
||||
type actual3 = ClearPlaceholder<'{{{name}}}'>;
|
||||
type expected = 'name';
|
||||
|
||||
expectTypeOf<actual1>().toEqualTypeOf<expected>();
|
||||
expectTypeOf<actual2>().toEqualTypeOf<expected>();
|
||||
expectTypeOf<actual3>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('removes all unbalanced braces from placeholders', () => {
|
||||
type actual1 = ClearPlaceholder<'{name}}'>;
|
||||
type actual2 = ClearPlaceholder<'{{name}}}'>;
|
||||
type expected = 'name';
|
||||
|
||||
expectTypeOf<actual1>().toEqualTypeOf<expected>();
|
||||
expectTypeOf<actual2>().toEqualTypeOf<expected>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ExtractPlaceholders', () => {
|
||||
it('string without placeholders', () => {
|
||||
type actual = ExtractPlaceholders<'Hello name, how are?'>;
|
||||
type expected = never;
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('string with one idexed placeholder', () => {
|
||||
type actual = ExtractPlaceholders<'Hello {0}, how are you?'>;
|
||||
type expected = '0';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('string with two indexed placeholders', () => {
|
||||
type actual = ExtractPlaceholders<'Hello {0}, my name is {1}'>;
|
||||
type expected = '0' | '1';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('string with one key placeholder', () => {
|
||||
type actual = ExtractPlaceholders<'Hello {name}, how are you?'>;
|
||||
type expected = 'name';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('string with two key placeholders', () => {
|
||||
type actual = ExtractPlaceholders<'Hello {name}, my name is {managers.0.name}'>;
|
||||
type expected = 'name' | 'managers.0.name';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('string with mixed placeholders', () => {
|
||||
type actual = ExtractPlaceholders<'Hello {0}, how are you? My name is {1.name}'>;
|
||||
type expected = '0' | '1.name';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('string with nested placeholder and balanced braces', () => {
|
||||
type actual = ExtractPlaceholders<'Hello {{name}}, how are you?'>;
|
||||
type expected = 'name';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('string with nested placeholder and unbalanced braces', () => {
|
||||
type actual = ExtractPlaceholders<'Hello {{{name}, how are you?'>;
|
||||
type expected = 'name';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('string with nested placeholders and balanced braces', () => {
|
||||
type actual = ExtractPlaceholders<'Hello {{{name}{positions}}}, how are you?'>;
|
||||
type expected = 'name' | 'positions';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
|
||||
it('string with nested placeholders and unbalanced braces', () => {
|
||||
type actual = ExtractPlaceholders<'Hello {{{name}{positions}, how are you?'>;
|
||||
type expected = 'name' | 'positions';
|
||||
|
||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||
});
|
||||
});
|
||||
});
|
||||
48
core/stdlib/src/text/template/index.test.ts
Normal file
48
core/stdlib/src/text/template/index.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { templateObject } from '.';
|
||||
|
||||
describe.todo('templateObject', () => {
|
||||
it('replace template placeholders with corresponding values from args', () => {
|
||||
const template = 'Hello, {names.0}!';
|
||||
const args = { names: ['John'] };
|
||||
const result = templateObject(template, args);
|
||||
expect(result).toBe('Hello, John!');
|
||||
});
|
||||
|
||||
it('replace template placeholders with corresponding values from args', () => {
|
||||
const template = 'Hello, {name}!';
|
||||
const args = { name: 'John' };
|
||||
const result = templateObject(template, args);
|
||||
expect(result).toBe('Hello, John!');
|
||||
});
|
||||
|
||||
it('replace template placeholders with fallback value if corresponding value is undefined', () => {
|
||||
const template = 'Hello, {name}!';
|
||||
const args = { age: 25 };
|
||||
const fallback = 'Guest';
|
||||
const result = templateObject(template, args, fallback);
|
||||
expect(result).toBe('Hello, Guest!');
|
||||
});
|
||||
|
||||
it(' replace template placeholders with fallback value returned by fallback function if corresponding value is undefined', () => {
|
||||
const template = 'Hello, {name}!';
|
||||
const args = { age: 25 };
|
||||
const fallback = (key: string) => `Unknown ${key}`;
|
||||
const result = templateObject(template, args, fallback);
|
||||
expect(result).toBe('Hello, Unknown name!');
|
||||
});
|
||||
|
||||
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'}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
expect(result).toBe('Hello {John Doe, your address 123 Main St');
|
||||
});
|
||||
});
|
||||
65
core/stdlib/src/text/template/index.ts
Normal file
65
core/stdlib/src/text/template/index.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { getByPath, type Generate } from '../../collections';
|
||||
import { isFunction } from '../../types';
|
||||
|
||||
/**
|
||||
* Type of a value that will be used to replace a placeholder in a template.
|
||||
*/
|
||||
type StringPrimitive = string | number | bigint | null | undefined;
|
||||
|
||||
/**
|
||||
* Type of a fallback value when a template key is not found.
|
||||
*/
|
||||
type TemplateFallback = string | ((key: string) => string);
|
||||
|
||||
/**
|
||||
* Type of an object that will be used to replace placeholders in a template.
|
||||
*/
|
||||
type TemplateArgsObject = StringPrimitive[] | { [key: string]: TemplateArgsObject | StringPrimitive };
|
||||
|
||||
/**
|
||||
* Type of a template string with placeholders.
|
||||
*/
|
||||
const TEMPLATE_PLACEHOLDER = /{([^{}]+)}/gm;
|
||||
|
||||
/**
|
||||
* Removes the placeholder syntax from a template string.
|
||||
*
|
||||
* @example
|
||||
* type Base = ClearPlaceholder<'{user.name}'>; // 'user.name'
|
||||
* type Unbalanced = ClearPlaceholder<'{user.name'>; // 'user.name'
|
||||
*/
|
||||
export type ClearPlaceholder<T extends string> =
|
||||
T extends `${string}{${infer Template}`
|
||||
? ClearPlaceholder<Template>
|
||||
: T extends `${infer Template}}${string}`
|
||||
? ClearPlaceholder<Template>
|
||||
: T;
|
||||
|
||||
/**
|
||||
* 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<T extends string> =
|
||||
T extends `${infer Before}}${infer After}`
|
||||
? Before extends `${string}{${infer Placeholder}`
|
||||
? ClearPlaceholder<Placeholder> | ExtractPlaceholders<After>
|
||||
: ExtractPlaceholders<After>
|
||||
: never;
|
||||
|
||||
export function templateObject<T extends string, A extends Generate<ExtractPlaceholders<T>>>(template: T, args: A, fallback?: TemplateFallback): string {
|
||||
return template.replace(TEMPLATE_PLACEHOLDER, (_, key) => {
|
||||
const value = getByPath(args, key) as string;
|
||||
return value !== undefined ? value : (isFunction(fallback) ? fallback(key) : '');
|
||||
});
|
||||
}
|
||||
|
||||
// templateObject('Hello {user.name}, your address {user.addresses.0.street}', {
|
||||
// user: {
|
||||
// name: 'John',
|
||||
// addresses: [
|
||||
// { city: 'New York', street: '5th Avenue' },
|
||||
// ],
|
||||
// },
|
||||
// });
|
||||
93
core/stdlib/src/text/trigram-distance/index.test.ts
Normal file
93
core/stdlib/src/text/trigram-distance/index.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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');
|
||||
|
||||
expect(trigramDistance(profile1, profile2)).toBe(0);
|
||||
});
|
||||
|
||||
it('one for completely different text', () => {
|
||||
const profile1 = trigramProfile('hello world');
|
||||
const profile2 = trigramProfile('lorem ipsum');
|
||||
|
||||
expect(trigramDistance(profile1, profile2)).toBe(1);
|
||||
});
|
||||
|
||||
it('one for empty text and non-empty text', () => {
|
||||
const profile1 = trigramProfile('hello world');
|
||||
const profile2 = trigramProfile('');
|
||||
|
||||
expect(trigramDistance(profile1, profile2)).toBe(1);
|
||||
});
|
||||
|
||||
it('approximately 0.5 for similar text', () => {
|
||||
const profile1 = trigramProfile('hello world');
|
||||
const profile2 = trigramProfile('hello lorem');
|
||||
|
||||
const approx = trigramDistance(profile1, profile2);
|
||||
|
||||
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'));
|
||||
|
||||
expect(A + B).toBeGreaterThanOrEqual(C);
|
||||
});
|
||||
});
|
||||
57
core/stdlib/src/text/trigram-distance/index.ts
Normal file
57
core/stdlib/src/text/trigram-distance/index.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
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';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export function trigramDistance(left: Trigrams, right: Trigrams): number {
|
||||
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, 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;
|
||||
|
||||
return distance / total;
|
||||
}
|
||||
2
core/stdlib/src/types/index.ts
Normal file
2
core/stdlib/src/types/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './js';
|
||||
export * from './ts';
|
||||
30
core/stdlib/src/types/js/casts.test.ts
Normal file
30
core/stdlib/src/types/js/casts.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { toString } from './casts';
|
||||
|
||||
describe('casts', () => {
|
||||
describe('toString', () => {
|
||||
it('correct string representation of a value', () => {
|
||||
// Primitives
|
||||
expect(toString(true)).toBe('[object Boolean]');
|
||||
expect(toString(() => {})).toBe('[object Function]');
|
||||
expect(toString(5)).toBe('[object Number]');
|
||||
expect(toString(BigInt(5))).toBe('[object BigInt]');
|
||||
expect(toString('hello')).toBe('[object String]');
|
||||
expect(toString(Symbol('foo'))).toBe('[object Symbol]');
|
||||
|
||||
// Complex
|
||||
expect(toString([])).toBe('[object Array]');
|
||||
expect(toString({})).toBe('[object Object]');
|
||||
expect(toString(undefined)).toBe('[object Undefined]');
|
||||
expect(toString(null)).toBe('[object Null]');
|
||||
expect(toString(/abc/)).toBe('[object RegExp]');
|
||||
expect(toString(new Date())).toBe('[object Date]');
|
||||
expect(toString(new Error())).toBe('[object Error]');
|
||||
expect(toString(new Promise(() => {}))).toBe('[object Promise]');
|
||||
expect(toString(new Map())).toBe('[object Map]');
|
||||
expect(toString(new Set())).toBe('[object Set]');
|
||||
expect(toString(new WeakMap())).toBe('[object WeakMap]');
|
||||
expect(toString(new WeakSet())).toBe('[object WeakSet]');
|
||||
});
|
||||
});
|
||||
});
|
||||
11
core/stdlib/src/types/js/casts.ts
Normal file
11
core/stdlib/src/types/js/casts.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @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);
|
||||
183
core/stdlib/src/types/js/complex.test.ts
Normal file
183
core/stdlib/src/types/js/complex.test.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { isArray, isObject, isRegExp, isDate, isError, isPromise, isMap, isSet, isWeakMap, isWeakSet } from './complex';
|
||||
|
||||
describe('complex', () => {
|
||||
describe('isArray', () => {
|
||||
it('true if the value is an array', () => {
|
||||
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);
|
||||
expect(isArray(true)).toBe(false);
|
||||
expect(isArray(null)).toBe(false);
|
||||
expect(isArray(undefined)).toBe(false);
|
||||
expect(isArray({})).toBe(false);
|
||||
expect(isArray(new Map())).toBe(false);
|
||||
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);
|
||||
expect(isObject(true)).toBe(false);
|
||||
expect(isObject(null)).toBe(false);
|
||||
expect(isObject(undefined)).toBe(false);
|
||||
expect(isObject([])).toBe(false);
|
||||
expect(isObject(new Map())).toBe(false);
|
||||
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);
|
||||
expect(isRegExp(true)).toBe(false);
|
||||
expect(isRegExp(null)).toBe(false);
|
||||
expect(isRegExp(undefined)).toBe(false);
|
||||
expect(isRegExp([])).toBe(false);
|
||||
expect(isRegExp({})).toBe(false);
|
||||
expect(isRegExp(new Map())).toBe(false);
|
||||
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);
|
||||
expect(isDate(true)).toBe(false);
|
||||
expect(isDate(null)).toBe(false);
|
||||
expect(isDate(undefined)).toBe(false);
|
||||
expect(isDate([])).toBe(false);
|
||||
expect(isDate({})).toBe(false);
|
||||
expect(isDate(new Map())).toBe(false);
|
||||
expect(isDate(new Set())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isError', () => {
|
||||
it('true if the value is an error', () => {
|
||||
expect(isError(new Error())).toBe(true);
|
||||
});
|
||||
|
||||
it('false if the value is not an error', () => {
|
||||
expect(isError('')).toBe(false);
|
||||
expect(isError(123)).toBe(false);
|
||||
expect(isError(true)).toBe(false);
|
||||
expect(isError(null)).toBe(false);
|
||||
expect(isError(undefined)).toBe(false);
|
||||
expect(isError([])).toBe(false);
|
||||
expect(isError({})).toBe(false);
|
||||
expect(isError(new Map())).toBe(false);
|
||||
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);
|
||||
expect(isPromise(true)).toBe(false);
|
||||
expect(isPromise(null)).toBe(false);
|
||||
expect(isPromise(undefined)).toBe(false);
|
||||
expect(isPromise([])).toBe(false);
|
||||
expect(isPromise({})).toBe(false);
|
||||
expect(isPromise(new Map())).toBe(false);
|
||||
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);
|
||||
expect(isMap(true)).toBe(false);
|
||||
expect(isMap(null)).toBe(false);
|
||||
expect(isMap(undefined)).toBe(false);
|
||||
expect(isMap([])).toBe(false);
|
||||
expect(isMap({})).toBe(false);
|
||||
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);
|
||||
expect(isSet(true)).toBe(false);
|
||||
expect(isSet(null)).toBe(false);
|
||||
expect(isSet(undefined)).toBe(false);
|
||||
expect(isSet([])).toBe(false);
|
||||
expect(isSet({})).toBe(false);
|
||||
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);
|
||||
expect(isWeakMap(true)).toBe(false);
|
||||
expect(isWeakMap(null)).toBe(false);
|
||||
expect(isWeakMap(undefined)).toBe(false);
|
||||
expect(isWeakMap([])).toBe(false);
|
||||
expect(isWeakMap({})).toBe(false);
|
||||
expect(isWeakMap(new Map())).toBe(false);
|
||||
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);
|
||||
expect(isWeakSet(true)).toBe(false);
|
||||
expect(isWeakSet(null)).toBe(false);
|
||||
expect(isWeakSet(undefined)).toBe(false);
|
||||
expect(isWeakSet([])).toBe(false);
|
||||
expect(isWeakSet({})).toBe(false);
|
||||
expect(isWeakSet(new Map())).toBe(false);
|
||||
expect(isWeakSet(new Set())).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
121
core/stdlib/src/types/js/complex.ts
Normal file
121
core/stdlib/src/types/js/complex.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { toString } from '.';
|
||||
|
||||
/**
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* @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]';
|
||||
|
||||
/**
|
||||
* @name isRegExp
|
||||
* @category Types
|
||||
* @description Check if a value is a regexp
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is RegExp}
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isRegExp = (value: any): value is RegExp => toString(value) === '[object RegExp]';
|
||||
|
||||
/**
|
||||
* @name isDate
|
||||
* @category Types
|
||||
* @description Check if a value is a date
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {value is Date}
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const isDate = (value: any): value is Date => toString(value) === '[object Date]';
|
||||
|
||||
/**
|
||||
* @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]';
|
||||
|
||||
/**
|
||||
* @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]';
|
||||
|
||||
/**
|
||||
* @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]';
|
||||
|
||||
/**
|
||||
* @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]';
|
||||
|
||||
/**
|
||||
* @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]';
|
||||
|
||||
/**
|
||||
* @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]';
|
||||
3
core/stdlib/src/types/js/index.ts
Normal file
3
core/stdlib/src/types/js/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './casts';
|
||||
export * from './primitives';
|
||||
export * from './complex';
|
||||
101
core/stdlib/src/types/js/primitives.test.ts
Normal file
101
core/stdlib/src/types/js/primitives.test.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { isBoolean, isFunction, isNumber, isBigInt, isString, isSymbol, isUndefined, isNull } from './primitives';
|
||||
|
||||
describe('primitives', () => {
|
||||
describe('isBoolean', () => {
|
||||
it('true if the value is a boolean', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
97
core/stdlib/src/types/js/primitives.ts
Normal file
97
core/stdlib/src/types/js/primitives.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { toString } from '.';
|
||||
|
||||
/**
|
||||
* @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';
|
||||
|
||||
/**
|
||||
* @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';
|
||||
|
||||
/**
|
||||
* @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';
|
||||
|
||||
/**
|
||||
* @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';
|
||||
|
||||
/**
|
||||
* @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';
|
||||
|
||||
/**
|
||||
* @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';
|
||||
|
||||
/**
|
||||
* @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]';
|
||||
|
||||
/**
|
||||
* @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]';
|
||||
4
core/stdlib/src/types/ts/array.ts
Normal file
4
core/stdlib/src/types/ts/array.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* A type that can be either a single value or an array of values
|
||||
*/
|
||||
export type Arrayable<T> = T | T[];
|
||||
9
core/stdlib/src/types/ts/function.ts
Normal file
9
core/stdlib/src/types/ts/function.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Any function
|
||||
*/
|
||||
export type AnyFunction = (...args: any[]) => any;
|
||||
|
||||
/**
|
||||
* Void function
|
||||
*/
|
||||
export type VoidFunction = () => void;
|
||||
4
core/stdlib/src/types/ts/index.ts
Normal file
4
core/stdlib/src/types/ts/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './array';
|
||||
export * from './function';
|
||||
export * from './promise';
|
||||
export * from './string';
|
||||
4
core/stdlib/src/types/ts/promise.ts
Normal file
4
core/stdlib/src/types/ts/promise.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Represents a value that may be a promise.
|
||||
*/
|
||||
export type MaybePromise<T> = T | Promise<T>;
|
||||
43
core/stdlib/src/types/ts/string.test-d.ts
Normal file
43
core/stdlib/src/types/ts/string.test-d.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest';
|
||||
import type { HasSpaces, Trim } from './string';
|
||||
|
||||
describe('string', () => {
|
||||
describe('Trim', () => {
|
||||
it('remove leading and trailing 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>();
|
||||
});
|
||||
});
|
||||
|
||||
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>();
|
||||
});
|
||||
});
|
||||
});
|
||||
9
core/stdlib/src/types/ts/string.ts
Normal file
9
core/stdlib/src/types/ts/string.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Trim leading and trailing whitespace from `S`
|
||||
*/
|
||||
export type Trim<S extends string> = S extends ` ${infer R}` ? Trim<R> : S extends `${infer L} ` ? Trim<L> : S;
|
||||
|
||||
/**
|
||||
* Check if `S` has any spaces
|
||||
*/
|
||||
export type HasSpaces<S extends string> = S extends `${string} ${string}` ? true : false;
|
||||
21
core/stdlib/src/utils/index.ts
Normal file
21
core/stdlib/src/utils/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @name timestamp
|
||||
* @category Utils
|
||||
* @description Returns the current timestamp
|
||||
*
|
||||
* @returns {number} The current timestamp
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const timestamp = () => Date.now();
|
||||
|
||||
/**
|
||||
* @name noop
|
||||
* @category Utils
|
||||
* @description A function that does nothing
|
||||
*
|
||||
* @returns {void} Nothing
|
||||
*
|
||||
* @since 0.0.2
|
||||
*/
|
||||
export const noop = () => {};
|
||||
Reference in New Issue
Block a user