diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 49cfa64..791c5d8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -26,4 +26,4 @@ jobs: run: pnpm install --frozen-lockfile - name: Test - run: pnpm run all:test \ No newline at end of file + run: pnpm run all:build && pnpm run all:test \ No newline at end of file diff --git a/.gitignore b/.gitignore index b41a2d0..a84ed06 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ node_modules .nuxt .nitro .cache +cache out build dist diff --git a/.vitepress/config.ts b/docs/.vitepress/config.ts similarity index 83% rename from .vitepress/config.ts rename to docs/.vitepress/config.ts index c590284..0b4cd01 100644 --- a/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -2,8 +2,8 @@ import { defineConfig } from 'vitepress'; export default defineConfig({ lang: 'ru-RU', - title: "Tools", - description: "A set of tools and utilities for web development", + title: "Toolkit", + description: "A collection of typescript and javascript development tools", rewrites: { 'packages/:pkg/README.md': 'packages/:pkg/index.md', }, diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..66e79bd --- /dev/null +++ b/docs/index.md @@ -0,0 +1,14 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: Toolkit + tagline: A collection of typescript and javascript development tools + actions: + - theme: brand + text: Get Started + link: / + - theme: alt + text: View on GitHub + link: / \ No newline at end of file diff --git a/package.json b/package.json index 098ff94..7bd97d0 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "vitepress": "^1.3.4" }, "scripts": { + "all:build": "pnpm -r build", "all:test": "pnpm -r test", "create": "jiti ./cli.ts", "docs:dev": "vitepress dev .", diff --git a/packages/platform/README.md b/packages/platform/README.md new file mode 100644 index 0000000..629b30e --- /dev/null +++ b/packages/platform/README.md @@ -0,0 +1 @@ +# @robonen/platform \ No newline at end of file diff --git a/packages/platform/jsr.json b/packages/platform/jsr.json new file mode 100644 index 0000000..8e8f5bf --- /dev/null +++ b/packages/platform/jsr.json @@ -0,0 +1,5 @@ +{ + "name": "@robonen/platform", + "version": "0.0.0", + "exports": "./src/index.ts" +} \ No newline at end of file diff --git a/packages/platform/package.json b/packages/platform/package.json new file mode 100644 index 0000000..d06b59a --- /dev/null +++ b/packages/platform/package.json @@ -0,0 +1,36 @@ +{ + "name": "@robonen/platform", + "private": true, + "version": "0.0.0", + "license": "UNLICENSED", + "description": "", + "keywords": [], + "author": "Robonen Andrew ", + "repository": { + "type": "git", + "url": "git+https://github.com/robonen/tools.git", + "directory": "./packages/platform" + }, + "packageManager": "pnpm@9.11.0", + "engines": { + "node": ">=20.13.1" + }, + "type": "module", + "files": [ + "dist" + ], + "main": "./dist/index.umd.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.umd.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": {}, + "devDependencies": { + "@robonen/tsconfig": "workspace:*" + } +} diff --git a/packages/platform/src/multi/debounce/index.ts b/packages/platform/src/multi/debounce/index.ts new file mode 100644 index 0000000..bb803be --- /dev/null +++ b/packages/platform/src/multi/debounce/index.ts @@ -0,0 +1,47 @@ +export interface DebounceOptions { + /** + * Call the function on the leading edge of the timeout, instead of waiting for the trailing edge + */ + readonly immediate?: boolean; + + /** + * Call the function on the trailing edge with the last used arguments. + * Result of call is from previous call + */ + readonly trailing?: boolean; +} + +const DEFAULT_DEBOUNCE_OPTIONS: DebounceOptions = { + trailing: true, +} + +export function debounce( + fn: (...args: FnArguments) => PromiseLike | FnReturn, + timeout: number = 20, + options: DebounceOptions = {}, +) { + options = { + ...DEFAULT_DEBOUNCE_OPTIONS, + ...options, + }; + + if (!Number.isFinite(timeout) || timeout <= 0) + throw new TypeError('Debounce timeout must be a positive number'); + + // Last result for leading edge + let leadingValue: PromiseLike | FnReturn; + + // Debounce timeout id + let timeoutId: NodeJS.Timeout; + + // Promises to be resolved when debounce is finished + let resolveList: Array<(value: unknown) => void> = []; + + // State of currently resolving promise + let currentResolve: Promise; + + // Trailing call information + let trailingArgs: unknown[]; + + +} diff --git a/packages/platform/src/multi/global/index.ts b/packages/platform/src/multi/global/index.ts new file mode 100644 index 0000000..6cad097 --- /dev/null +++ b/packages/platform/src/multi/global/index.ts @@ -0,0 +1,19 @@ +// TODO: tests + +/** + * @name _global + * @category Multi + * @description Global object that works in any environment + * + * @since 0.0.2 + */ +export const _global = + typeof globalThis !== 'undefined' + ? globalThis + : typeof window !== 'undefined' + ? window + : typeof global !== 'undefined' + ? global + : typeof self !== 'undefined' + ? self + : undefined; diff --git a/packages/platform/src/multi/index.ts b/packages/platform/src/multi/index.ts new file mode 100644 index 0000000..8774985 --- /dev/null +++ b/packages/platform/src/multi/index.ts @@ -0,0 +1,2 @@ +export * from './global'; +// export * from './debounce'; \ No newline at end of file diff --git a/packages/platform/tsconfig.json b/packages/platform/tsconfig.json new file mode 100644 index 0000000..d6d22e4 --- /dev/null +++ b/packages/platform/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@robonen/tsconfig/tsconfig.json" +} \ No newline at end of file diff --git a/packages/renovate/package.json b/packages/renovate/package.json index 05eb87e..fc03a0e 100644 --- a/packages/renovate/package.json +++ b/packages/renovate/package.json @@ -27,6 +27,6 @@ "test": "renovate-config-validator ./default.json" }, "devDependencies": { - "renovate": "^38.100.0" + "renovate": "^38.101.1" } } diff --git a/packages/stdlib/build.config.ts b/packages/stdlib/build.config.ts index f86264a..9195cb6 100644 --- a/packages/stdlib/build.config.ts +++ b/packages/stdlib/build.config.ts @@ -2,9 +2,8 @@ import { defineBuildConfig } from 'unbuild'; export default defineBuildConfig({ rollup: { - emitCJS: true, esbuild: { - minify: true, + // minify: true, }, }, }); \ No newline at end of file diff --git a/packages/stdlib/package.json b/packages/stdlib/package.json index 875749d..6f85dcc 100644 --- a/packages/stdlib/package.json +++ b/packages/stdlib/package.json @@ -43,9 +43,9 @@ }, "devDependencies": { "@robonen/tsconfig": "workspace:*", - "@vitest/coverage-v8": "^2.1.1", - "pathe": "^1.1.2", - "unbuild": "^2.0.0", - "vitest": "^2.1.1" + "@vitest/coverage-v8": "catalog:", + "pathe": "catalog:", + "unbuild": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/stdlib/src/arrays/getByPath/index.ts b/packages/stdlib/src/arrays/getByPath/index.ts new file mode 100644 index 0000000..6ceea36 --- /dev/null +++ b/packages/stdlib/src/arrays/getByPath/index.ts @@ -0,0 +1,84 @@ +type Exist = T extends undefined | null ? never : T; + +type ExtractFromObject, K> = + K extends keyof O + ? O[K] + : K extends keyof Exist + ? Exist[K] + : never; + +type ExtractFromArray = any[] extends A + ? A extends readonly (infer T)[] + ? T | undefined + : undefined + : K extends keyof A + ? A[K] + : undefined; + +type GetWithArray = K extends [] + ? O + : K extends [infer Key, ...infer Rest] + ? O extends Record + ? GetWithArray, Rest> + : O extends readonly any[] + ? GetWithArray, Rest> + : never + : never; + +type Path = T extends `${infer Key}.${infer Rest}` + ? [Key, ...Path] + : 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 extends unknown + ? (distributedUnion: Union) => void + : never +) extends ((mergedIntersection: infer Intersection) => void) + ? Intersection & Union + : never; + + +type PathToType = T extends [infer Head, ...infer Rest] + ? Head extends string + ? Head extends `${number}` + ? Rest extends string[] + ? PathToType[] + : never + : Rest extends string[] + ? { [K in Head & string]: PathToType } + : never + : never + : string; + +export type Generate = UnionToIntersection>>; +type Get = GetWithArray>; + +export function getByPath(obj: O, path: K): Get; +export function getByPath(obj: Record, 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; + } + + return currentObj; +} diff --git a/packages/stdlib/src/arrays/index.ts b/packages/stdlib/src/arrays/index.ts new file mode 100644 index 0000000..64eab40 --- /dev/null +++ b/packages/stdlib/src/arrays/index.ts @@ -0,0 +1 @@ +export * from './getByPath'; \ No newline at end of file diff --git a/packages/stdlib/src/bits/flags/index.ts b/packages/stdlib/src/bits/flags/index.ts index 772f4cd..1e54fc7 100644 --- a/packages/stdlib/src/bits/flags/index.ts +++ b/packages/stdlib/src/bits/flags/index.ts @@ -3,6 +3,8 @@ * * @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; @@ -22,6 +24,8 @@ export function flagsGenerator() { * * @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); @@ -32,6 +36,8 @@ export function and(...flags: number[]) { * * @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); @@ -42,6 +48,8 @@ export function or(...flags: number[]) { * * @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; @@ -51,7 +59,10 @@ export function not(flag: number) { * 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; @@ -62,6 +73,8 @@ export function has(flag: number, other: number) { * * @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; @@ -73,6 +86,8 @@ export function is(flag: number) { * @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; @@ -84,6 +99,8 @@ export function unset(flag: number, other: number) { * @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; diff --git a/packages/stdlib/src/index.ts b/packages/stdlib/src/index.ts index e076937..58694d8 100644 --- a/packages/stdlib/src/index.ts +++ b/packages/stdlib/src/index.ts @@ -2,4 +2,6 @@ export * from './text'; export * from './math'; export * from './patterns'; export * from './bits'; -export * from './structs'; \ No newline at end of file +export * from './structs'; +export * from './arrays'; +export * from './types'; diff --git a/packages/stdlib/src/math/clamp/index.test.ts b/packages/stdlib/src/math/basic/clamp/index.test.ts similarity index 100% rename from packages/stdlib/src/math/clamp/index.test.ts rename to packages/stdlib/src/math/basic/clamp/index.test.ts diff --git a/packages/stdlib/src/math/basic/clamp/index.ts b/packages/stdlib/src/math/basic/clamp/index.ts new file mode 100644 index 0000000..bd57a9c --- /dev/null +++ b/packages/stdlib/src/math/basic/clamp/index.ts @@ -0,0 +1,13 @@ +/** + * 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); +} diff --git a/packages/stdlib/src/math/basic/lerp/index.test.ts b/packages/stdlib/src/math/basic/lerp/index.test.ts new file mode 100644 index 0000000..0e0dd74 --- /dev/null +++ b/packages/stdlib/src/math/basic/lerp/index.test.ts @@ -0,0 +1,61 @@ +import {describe, it, expect} from 'vitest'; +import {inverseLerp, lerp} from './index'; + +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); + }); +}); diff --git a/packages/stdlib/src/math/basic/lerp/index.ts b/packages/stdlib/src/math/basic/lerp/index.ts new file mode 100644 index 0000000..42cf4e3 --- /dev/null +++ b/packages/stdlib/src/math/basic/lerp/index.ts @@ -0,0 +1,27 @@ +/** + * 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); +} + +/** + * 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); +} diff --git a/packages/stdlib/src/math/basic/remap/index.test.ts b/packages/stdlib/src/math/basic/remap/index.test.ts new file mode 100644 index 0000000..0597f80 --- /dev/null +++ b/packages/stdlib/src/math/basic/remap/index.test.ts @@ -0,0 +1,46 @@ +import {describe, expect, it} from 'vitest'; +import {remap} from './index'; + +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); + }); +}); \ No newline at end of file diff --git a/packages/stdlib/src/math/mapRange/index.ts b/packages/stdlib/src/math/basic/remap/index.ts similarity index 52% rename from packages/stdlib/src/math/mapRange/index.ts rename to packages/stdlib/src/math/basic/remap/index.ts index f82dd41..c2dbc83 100644 --- a/packages/stdlib/src/math/mapRange/index.ts +++ b/packages/stdlib/src/math/basic/remap/index.ts @@ -1,4 +1,5 @@ -import { clamp } from "../clamp"; +import { clamp } from '../clamp'; +import {inverseLerp, lerp} from '../lerp'; /** * Map a value from one range to another @@ -9,15 +10,14 @@ import { clamp } from "../clamp"; * @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 mapRange(value: number, in_min: number, in_max: number, out_min: number, out_max: number): number { - // Zero input range means invalid input, so return lowest output range value +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; - - // To ensure the value is within the input range, clamp it + const clampedValue = clamp(value, in_min, in_max); - // Finally, map the value from the input range to the output range - return (clampedValue - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; + return lerp(out_min, out_max, inverseLerp(in_min, in_max, clampedValue)); } \ No newline at end of file diff --git a/packages/stdlib/src/math/bigint/clampBigInt/index.test.ts b/packages/stdlib/src/math/bigint/clampBigInt/index.test.ts new file mode 100644 index 0000000..9a59501 --- /dev/null +++ b/packages/stdlib/src/math/bigint/clampBigInt/index.test.ts @@ -0,0 +1,35 @@ +import {describe, it, expect} from 'vitest'; +import {clampBigInt} from './index'; + +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); + }); +}); \ No newline at end of file diff --git a/packages/stdlib/src/math/bigint/clampBigInt/index.ts b/packages/stdlib/src/math/bigint/clampBigInt/index.ts new file mode 100644 index 0000000..42a3fac --- /dev/null +++ b/packages/stdlib/src/math/bigint/clampBigInt/index.ts @@ -0,0 +1,16 @@ +import {minBigInt} from '../minBigInt'; +import {maxBigInt} from '../maxBigInt'; + +/** + * 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); +} diff --git a/packages/stdlib/src/math/bigint/lerpBigInt/index.test.ts b/packages/stdlib/src/math/bigint/lerpBigInt/index.test.ts new file mode 100644 index 0000000..4d40c94 --- /dev/null +++ b/packages/stdlib/src/math/bigint/lerpBigInt/index.test.ts @@ -0,0 +1,83 @@ +import {describe, it, expect} from 'vitest'; +import {inverseLerpBigInt, lerpBigInt} from './index'; + +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); + }); +}); diff --git a/packages/stdlib/src/math/bigint/lerpBigInt/index.ts b/packages/stdlib/src/math/bigint/lerpBigInt/index.ts new file mode 100644 index 0000000..5006038 --- /dev/null +++ b/packages/stdlib/src/math/bigint/lerpBigInt/index.ts @@ -0,0 +1,27 @@ +/** + * 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; +} + +/** + * 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; +} \ No newline at end of file diff --git a/packages/stdlib/src/math/bigint/maxBigInt/index.test.ts b/packages/stdlib/src/math/bigint/maxBigInt/index.test.ts new file mode 100644 index 0000000..8f4dbe5 --- /dev/null +++ b/packages/stdlib/src/math/bigint/maxBigInt/index.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from 'vitest'; +import { maxBigInt } from './index'; + +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); + }); +}); \ No newline at end of file diff --git a/packages/stdlib/src/math/bigint/maxBigInt/index.ts b/packages/stdlib/src/math/bigint/maxBigInt/index.ts new file mode 100644 index 0000000..f47f015 --- /dev/null +++ b/packages/stdlib/src/math/bigint/maxBigInt/index.ts @@ -0,0 +1,15 @@ +/** + * 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); +} \ No newline at end of file diff --git a/packages/stdlib/src/math/bigint/minBigInt/index.test.ts b/packages/stdlib/src/math/bigint/minBigInt/index.test.ts new file mode 100644 index 0000000..fbfb73a --- /dev/null +++ b/packages/stdlib/src/math/bigint/minBigInt/index.test.ts @@ -0,0 +1,39 @@ +import {describe, it, expect} from 'vitest'; +import {minBigInt} from './index'; + +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); + }); +}); \ No newline at end of file diff --git a/packages/stdlib/src/math/bigint/minBigInt/index.ts b/packages/stdlib/src/math/bigint/minBigInt/index.ts new file mode 100644 index 0000000..03af83e --- /dev/null +++ b/packages/stdlib/src/math/bigint/minBigInt/index.ts @@ -0,0 +1,15 @@ +/** + * 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); +} diff --git a/packages/stdlib/src/math/bigint/remapBigInt/index.test.ts b/packages/stdlib/src/math/bigint/remapBigInt/index.test.ts new file mode 100644 index 0000000..c3cfc29 --- /dev/null +++ b/packages/stdlib/src/math/bigint/remapBigInt/index.test.ts @@ -0,0 +1,32 @@ +import {describe, expect, it} from 'vitest'; +import {remapBigInt} from './index'; + +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); + }); +}); \ No newline at end of file diff --git a/packages/stdlib/src/math/bigint/remapBigInt/index.ts b/packages/stdlib/src/math/bigint/remapBigInt/index.ts new file mode 100644 index 0000000..0530ed8 --- /dev/null +++ b/packages/stdlib/src/math/bigint/remapBigInt/index.ts @@ -0,0 +1,23 @@ +import { clampBigInt } from '../clampBigInt'; +import {inverseLerpBigInt, lerpBigInt} from '../lerpBigInt'; + +/** + * 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)); +} \ No newline at end of file diff --git a/packages/stdlib/src/math/clamp/index.ts b/packages/stdlib/src/math/clamp/index.ts deleted file mode 100644 index b8532eb..0000000 --- a/packages/stdlib/src/math/clamp/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * 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 - */ -export function clamp(value: number, min: number, max: number): number { - // The clamp function takes a value, a minimum, and a maximum as parameters. - // It ensures that the value falls within the range defined by the minimum and maximum values. - // If the value is less than the minimum, it returns the minimum value. - // If the value is greater than the maximum, it returns the maximum value. - // Otherwise, it returns the original value. - return Math.min(Math.max(value, min), max); -} diff --git a/packages/stdlib/src/math/index.ts b/packages/stdlib/src/math/index.ts index 701b72a..96e0fd7 100644 --- a/packages/stdlib/src/math/index.ts +++ b/packages/stdlib/src/math/index.ts @@ -1,2 +1,9 @@ -export * from './clamp'; -export * from './mapRange'; \ No newline at end of file +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'; diff --git a/packages/stdlib/src/math/mapRange/index.test.ts b/packages/stdlib/src/math/mapRange/index.test.ts deleted file mode 100644 index 17a4292..0000000 --- a/packages/stdlib/src/math/mapRange/index.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { mapRange } from './index'; - -describe('mapRange', () => { - it('map values from one range to another', () => { - // value at midpoint - expect(mapRange(5, 0, 10, 0, 100)).toBe(50); - - // value at min - expect(mapRange(0, 0, 10, 0, 100)).toBe(0); - - // value at max - expect(mapRange(10, 0, 10, 0, 100)).toBe(100); - - // value outside range (below) - expect(mapRange(-5, 0, 10, 0, 100)).toBe(0); - - // value outside range (above) - expect(mapRange(15, 0, 10, 0, 100)).toBe(100); - - // value at midpoint of negative range - expect(mapRange(75, 50, 100, -50, 50)).toBe(0); - - // value at midpoint of negative range - expect(mapRange(-25, -50, 0, 0, 100)).toBe(50); - }); - - it('handle floating-point numbers correctly', () => { - // floating-point value - expect(mapRange(3.5, 0, 10, 0, 100)).toBe(35); - - // positive floating-point ranges - expect(mapRange(1.25, 0, 2.5, 0, 100)).toBe(50); - - // negative floating-point value - expect(mapRange(-2.5, -5, 0, 0, 100)).toBe(50); - - // negative floating-point ranges - expect(mapRange(-1.25, -2.5, 0, 0, 100)).toBe(50); - }); - - it('handle edge cases', () => { - // input range is zero (should return output min) - expect(mapRange(5, 0, 0, 0, 100)).toBe(0); - }); -}); \ No newline at end of file diff --git a/packages/stdlib/src/patterns/behavioral/pubsub/index.test.ts b/packages/stdlib/src/patterns/behavioral/pubsub/index.test.ts index 629e23c..8a67b57 100644 --- a/packages/stdlib/src/patterns/behavioral/pubsub/index.test.ts +++ b/packages/stdlib/src/patterns/behavioral/pubsub/index.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { PubSub } from './index'; -describe('PubSub', () => { +describe('pubsub', () => { let eventBus: PubSub<{ event1: (arg: string) => void; event2: () => void diff --git a/packages/stdlib/src/patterns/behavioral/pubsub/index.ts b/packages/stdlib/src/patterns/behavioral/pubsub/index.ts index 0d54f20..8e31a3a 100644 --- a/packages/stdlib/src/patterns/behavioral/pubsub/index.ts +++ b/packages/stdlib/src/patterns/behavioral/pubsub/index.ts @@ -3,6 +3,8 @@ export type EventsRecord = Record; /** * Simple PubSub implementation + * + * @since 0.0.2 * * @template {EventsRecord} Events */ @@ -85,7 +87,7 @@ export class PubSub { * @param {...Parameters} args Arguments for the listener * @returns {boolean} */ - public emit(event: K, ...args: Parameters): boolean { + public emit(event: K, ...args: Parameters) { const listeners = this.events.get(event); if (!listeners) diff --git a/packages/stdlib/src/structs/stack/index.test.ts b/packages/stdlib/src/structs/stack/index.test.ts index b66dab2..748cabe 100644 --- a/packages/stdlib/src/structs/stack/index.test.ts +++ b/packages/stdlib/src/structs/stack/index.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { Stack } from './index'; -describe('Stack', () => { +describe('stack', () => { describe('constructor', () => { it('create an empty stack if no initial values are provided', () => { const stack = new Stack(); diff --git a/packages/stdlib/src/structs/stack/index.ts b/packages/stdlib/src/structs/stack/index.ts index 39a30f6..07a37f1 100644 --- a/packages/stdlib/src/structs/stack/index.ts +++ b/packages/stdlib/src/structs/stack/index.ts @@ -4,6 +4,9 @@ export type StackOptions = { /** * Represents a stack data structure + * + * @since 0.0.2 + * * @template T The type of elements stored in the stack */ export class Stack implements Iterable, AsyncIterable { @@ -13,7 +16,7 @@ export class Stack implements Iterable, AsyncIterable { * @private * @type {number} */ - private maxSize: number; + private readonly maxSize: number; /** * The stack data structure @@ -21,7 +24,7 @@ export class Stack implements Iterable, AsyncIterable { * @private * @type {T[]} */ - private stack: T[]; + private readonly stack: T[]; /** * Creates an instance of Stack @@ -39,7 +42,7 @@ export class Stack implements Iterable, AsyncIterable { * Gets the number of elements in the stack * @returns {number} The number of elements in the stack */ - public get length(): number { + public get length() { return this.stack.length; } @@ -47,7 +50,7 @@ export class Stack implements Iterable, AsyncIterable { * Checks if the stack is empty * @returns {boolean} `true` if the stack is empty, `false` otherwise */ - public get isEmpty(): boolean { + public get isEmpty() { return this.stack.length === 0; } @@ -55,7 +58,7 @@ export class Stack implements Iterable, AsyncIterable { * Checks if the stack is full * @returns {boolean} `true` if the stack is full, `false` otherwise */ - public get isFull(): boolean { + public get isFull() { return this.stack.length === this.maxSize; } @@ -78,7 +81,7 @@ export class Stack implements Iterable, AsyncIterable { * Pops an element from the stack * @returns {T} The element popped from the stack */ - public pop(): T | undefined { + public pop() { return this.stack.pop(); } @@ -86,7 +89,7 @@ export class Stack implements Iterable, AsyncIterable { * Peeks at the top element of the stack * @returns {T} The top element of the stack */ - public peek(): T | undefined { + public peek() { if (this.isEmpty) throw new RangeError('Stack is empty'); @@ -109,7 +112,7 @@ export class Stack implements Iterable, AsyncIterable { * * @returns {T[]} */ - public toArray(): T[] { + public toArray() { return this.stack.toReversed(); } diff --git a/packages/stdlib/src/text/index.ts b/packages/stdlib/src/text/index.ts index c740f6d..1a0231f 100644 --- a/packages/stdlib/src/text/index.ts +++ b/packages/stdlib/src/text/index.ts @@ -1,2 +1,4 @@ export * from './levenshtein-distance'; -export * from './trigram-distance'; \ No newline at end of file +export * from './trigram-distance'; +// TODO: Template is not implemented yet +// export * from './template'; \ No newline at end of file diff --git a/packages/stdlib/src/text/levenshtein-distance/index.ts b/packages/stdlib/src/text/levenshtein-distance/index.ts index 06957a8..69bf150 100644 --- a/packages/stdlib/src/text/levenshtein-distance/index.ts +++ b/packages/stdlib/src/text/levenshtein-distance/index.ts @@ -4,12 +4,12 @@ * @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 the strings are equal, the distance is 0 if (left === right) return 0; - // If either string is empty, the distance is the length of the other string if (left.length === 0) return right.length; if (right.length === 0) return left.length; diff --git a/packages/stdlib/src/text/template/index.test-d.ts b/packages/stdlib/src/text/template/index.test-d.ts new file mode 100644 index 0000000..7842c4c --- /dev/null +++ b/packages/stdlib/src/text/template/index.test-d.ts @@ -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().toEqualTypeOf(); + }); + + it('removes all balanced braces from placeholders', () => { + type actual1 = ClearPlaceholder<'{name}'>; + type actual2 = ClearPlaceholder<'{{name}}'>; + type actual3 = ClearPlaceholder<'{{{name}}}'>; + type expected = 'name'; + + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); + + it('removes all unbalanced braces from placeholders', () => { + type actual1 = ClearPlaceholder<'{name}}'>; + type actual2 = ClearPlaceholder<'{{name}}}'>; + type expected = 'name'; + + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); + }); + + describe('ExtractPlaceholders', () => { + it('string without placeholders', () => { + type actual = ExtractPlaceholders<'Hello name, how are?'>; + type expected = never; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with one idexed placeholder', () => { + type actual = ExtractPlaceholders<'Hello {0}, how are you?'>; + type expected = '0'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with two indexed placeholders', () => { + type actual = ExtractPlaceholders<'Hello {0}, my name is {1}'>; + type expected = '0' | '1'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with one key placeholder', () => { + type actual = ExtractPlaceholders<'Hello {name}, how are you?'>; + type expected = 'name'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with two key placeholders', () => { + type actual = ExtractPlaceholders<'Hello {name}, my name is {managers.0.name}'>; + type expected = 'name' | 'managers.0.name'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with mixed placeholders', () => { + type actual = ExtractPlaceholders<'Hello {0}, how are you? My name is {1.name}'>; + type expected = '0' | '1.name'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with nested placeholder and balanced braces', () => { + type actual = ExtractPlaceholders<'Hello {{name}}, how are you?'>; + type expected = 'name'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with nested placeholder and unbalanced braces', () => { + type actual = ExtractPlaceholders<'Hello {{{name}, how are you?'>; + type expected = 'name'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with nested placeholders and balanced braces', () => { + type actual = ExtractPlaceholders<'Hello {{{name}{positions}}}, how are you?'>; + type expected = 'name' | 'positions'; + + expectTypeOf().toEqualTypeOf(); + }); + + it('string with nested placeholders and unbalanced braces', () => { + type actual = ExtractPlaceholders<'Hello {{{name}{positions}, how are you?'>; + type expected = 'name' | 'positions'; + + expectTypeOf().toEqualTypeOf(); + }); + }); +}); \ No newline at end of file diff --git a/packages/stdlib/src/text/template/index.test.ts b/packages/stdlib/src/text/template/index.test.ts new file mode 100644 index 0000000..b896be1 --- /dev/null +++ b/packages/stdlib/src/text/template/index.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest'; +import { templateObject } from './index'; + +describe('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'); + }); +}); \ No newline at end of file diff --git a/packages/stdlib/src/text/template/index.ts b/packages/stdlib/src/text/template/index.ts new file mode 100644 index 0000000..473cadd --- /dev/null +++ b/packages/stdlib/src/text/template/index.ts @@ -0,0 +1,66 @@ +import { getByPath, type Generate } from '../../arrays'; +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}{${infer Template}` + ? ClearPlaceholder