1
0
mirror of https://github.com/robonen/tools.git synced 2026-03-20 02:44:45 +00:00

Merge pull request #38 from robonen/platform

Platform
This commit is contained in:
2024-09-30 06:57:10 +07:00
committed by GitHub
82 changed files with 3673 additions and 917 deletions

View File

@@ -26,4 +26,4 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm run all:test
run: pnpm run all:build && pnpm run all:test

1
.gitignore vendored
View File

@@ -17,6 +17,7 @@ node_modules
.nuxt
.nitro
.cache
cache
out
build
dist

View File

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

14
docs/index.md Normal file
View File

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

View File

@@ -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 .",

View File

@@ -0,0 +1 @@
# @robonen/platform

View File

@@ -0,0 +1,5 @@
{
"name": "@robonen/platform",
"version": "0.0.0",
"exports": "./src/index.ts"
}

View File

@@ -0,0 +1,36 @@
{
"name": "@robonen/platform",
"private": true,
"version": "0.0.0",
"license": "UNLICENSED",
"description": "",
"keywords": [],
"author": "Robonen Andrew <robonenandrew@gmail.com>",
"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:*"
}
}

View File

@@ -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<FnArguments extends unknown[], FnReturn>(
fn: (...args: FnArguments) => PromiseLike<FnReturn> | 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> | 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<FnReturn>;
// Trailing call information
let trailingArgs: unknown[];
}

View File

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

View File

@@ -0,0 +1,2 @@
export * from './global';
// export * from './debounce';

View File

@@ -0,0 +1,3 @@
{
"extends": "@robonen/tsconfig/tsconfig.json"
}

View File

@@ -27,6 +27,6 @@
"test": "renovate-config-validator ./default.json"
},
"devDependencies": {
"renovate": "^38.100.0"
"renovate": "^38.101.1"
}
}

View File

@@ -2,9 +2,8 @@ import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
rollup: {
emitCJS: true,
esbuild: {
minify: true,
// minify: true,
},
},
});

View File

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

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

View File

@@ -0,0 +1 @@
export * from './getByPath';

View File

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

View File

@@ -2,4 +2,6 @@ export * from './text';
export * from './math';
export * from './patterns';
export * from './bits';
export * from './structs';
export * from './structs';
export * from './arrays';
export * from './types';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,2 +1,9 @@
export * from './clamp';
export * from './mapRange';
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';

View File

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

View File

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

View File

@@ -3,6 +3,8 @@ export type EventsRecord = Record<string, Subscriber>;
/**
* Simple PubSub implementation
*
* @since 0.0.2
*
* @template {EventsRecord} Events
*/
@@ -85,7 +87,7 @@ export class PubSub<Events extends EventsRecord> {
* @param {...Parameters<Events[K]>} args Arguments for the listener
* @returns {boolean}
*/
public emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>): boolean {
public emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>) {
const listeners = this.events.get(event);
if (!listeners)

View File

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

View File

@@ -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<T> implements Iterable<T>, AsyncIterable<T> {
@@ -13,7 +16,7 @@ export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
* @private
* @type {number}
*/
private maxSize: number;
private readonly maxSize: number;
/**
* The stack data structure
@@ -21,7 +24,7 @@ export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
* @private
* @type {T[]}
*/
private stack: T[];
private readonly stack: T[];
/**
* Creates an instance of Stack
@@ -39,7 +42,7 @@ export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
* 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<T> implements Iterable<T>, AsyncIterable<T> {
* 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<T> implements Iterable<T>, AsyncIterable<T> {
* 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<T> implements Iterable<T>, AsyncIterable<T> {
* 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<T> implements Iterable<T>, AsyncIterable<T> {
* 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<T> implements Iterable<T>, AsyncIterable<T> {
*
* @returns {T[]}
*/
public toArray(): T[] {
public toArray() {
return this.stack.toReversed();
}

View File

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

View File

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

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

View File

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

View File

@@ -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> =
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 Doe',
// addresses: [
// { street: '123 Main St', city: 'Springfield'},
// { street: '456 Elm St', city: 'Shelbyville'},
// ],
// },
// });

View File

@@ -5,6 +5,8 @@ export type Trigrams = Map<string, number>;
*
* @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';
@@ -26,6 +28,8 @@ export function trigramProfile(text: string): Trigrams {
* @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;

View File

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

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

View File

@@ -0,0 +1,7 @@
/**
* To string any value.
*
* @param {any} value
* @returns {string}
*/
export const toString = (value: any): string => Object.prototype.toString.call(value);

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

View File

@@ -0,0 +1,81 @@
import { toString } from '.';
/**
* Check if a value is an array.
*
* @param {any} value
* @returns {value is any[]}
*/
export const isArray = (value: any): value is any[] => Array.isArray(value);
/**
* Check if a value is an object.
*
* @param {any} value
* @returns {value is object}
*/
export const isObject = (value: any): value is object => toString(value) === '[object Object]';
/**
* Check if a value is a regexp.
*
* @param {any} value
* @returns {value is RegExp}
*/
export const isRegExp = (value: any): value is RegExp => toString(value) === '[object RegExp]';
/**
* Check if a value is a date.
*
* @param {any} value
* @returns {value is Date}
*/
export const isDate = (value: any): value is Date => toString(value) === '[object Date]';
/**
* Check if a value is an error.
*
* @param {any} value
* @returns {value is Error}
*/
export const isError = (value: any): value is Error => toString(value) === '[object Error]';
/**
* Check if a value is a promise.
*
* @param {any} value
* @returns {value is Promise<any>}
*/
export const isPromise = (value: any): value is Promise<any> => toString(value) === '[object Promise]';
/**
* Check if a value is a map.
*
* @param {any} value
* @returns {value is Map<any, any>}
*/
export const isMap = (value: any): value is Map<any, any> => toString(value) === '[object Map]';
/**
* Check if a value is a set.
*
* @param {any} value
* @returns {value is Set<any>}
*/
export const isSet = (value: any): value is Set<any> => toString(value) === '[object Set]';
/**
* Check if a value is a weakmap.
*
* @param {any} value
* @returns {value is WeakMap<object, any>}
*/
export const isWeakMap = (value: any): value is WeakMap<object, any> => toString(value) === '[object WeakMap]';
/**
* Check if a value is a weakset.
*
* @param {any} value
* @returns {value is WeakSet<object>}
*/
export const isWeakSet = (value: any): value is WeakSet<object> => toString(value) === '[object WeakSet]';

View File

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

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

View File

@@ -0,0 +1,65 @@
import { toString } from '.';
/**
* Check if a value is a boolean.
*
* @param {any} value
* @returns {value is boolean}
*/
export const isBoolean = (value: any): value is boolean => typeof value === 'boolean';
/**
* Check if a value is a function.
*
* @param {any} value
* @returns {value is Function}
*/
export const isFunction = <T extends Function>(value: any): value is T => typeof value === 'function';
/**
* Check if a value is a number.
*
* @param {any} value
* @returns {value is number}
*/
export const isNumber = (value: any): value is number => typeof value === 'number';
/**
* Check if a value is a bigint.
*
* @param {any} value
* @returns {value is bigint}
*/
export const isBigInt = (value: any): value is bigint => typeof value === 'bigint';
/**
* Check if a value is a string.
*
* @param {any} value
* @returns {value is string}
*/
export const isString = (value: any): value is string => typeof value === 'string';
/**
* Check if a value is a symbol.
*
* @param {any} value
* @returns {value is symbol}
*/
export const isSymbol = (value: any): value is symbol => typeof value === 'symbol';
/**
* Check if a value is a undefined.
*
* @param {any} value
* @returns {value is undefined}
*/
export const isUndefined = (value: any): value is undefined => toString(value) === '[object Undefined]';
/**
* Check if a value is a null.
*
* @param {any} value
* @returns {value is null}
*/
export const isNull = (value: any): value is null => toString(value) === '[object Null]';

View File

@@ -0,0 +1 @@
export * from './string';

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

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

View File

@@ -31,7 +31,7 @@ pnpm install -D @robonen/tsconfig
"resolveJsonModule": true, // разрешить импортировать файлы JSON
"moduleDetection": "force", // заставляет TypeScript рассматривать все файлы как модули. Это помогает избежать ошибок cannot redeclare block-scoped variable»
"isolatedModules": true, // орабатывать каждый файл, как отдельный изолированный модуль
"removeComments": true, // удалять комментарии из исходного кода
"removeComments": false, // удалять комментарии из исходного кода
"verbatimModuleSyntax": true, // сохранять синтаксис модулей в исходном коде (важно при импорте типов)
"useDefineForClassFields": true, // использование классов стандарта TC39, а не TypeScript
"strict": true, // включить все строгие проверки (noImplicitAny, noImplicitThis, alwaysStrict, strictNullChecks, strictFunctionTypes, strictPropertyInitialization)

View File

@@ -17,7 +17,7 @@
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"removeComments": true,
"removeComments": false,
"verbatimModuleSyntax": true,
"useDefineForClassFields": true,

1
packages/vue/README.md Normal file
View File

@@ -0,0 +1 @@
# @robonen/vue

5
packages/vue/jsr.json Normal file
View File

@@ -0,0 +1,5 @@
{
"name": "@robonen/vue",
"version": "0.0.0",
"exports": "./src/index.ts"
}

46
packages/vue/package.json Normal file
View File

@@ -0,0 +1,46 @@
{
"name": "@robonen/vue",
"private": true,
"version": "0.0.0",
"license": "UNLICENSED",
"description": "",
"keywords": [],
"author": "Robonen Andrew <robonenandrew@gmail.com>",
"repository": {
"type": "git",
"url": "git+https://github.com/robonen/tools.git",
"directory": "./packages/vue"
},
"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": {
"test": "vitest run",
"dev": "vitest dev"
},
"devDependencies": {
"@robonen/tsconfig": "workspace:*",
"@vue/test-utils": "catalog:",
"jsdom": "catalog:",
"vitest": "catalog:"
},
"dependencies": {
"@robonen/stdlib": "workspace:*",
"vue": "catalog:"
}
}

View File

@@ -0,0 +1,5 @@
export * from './useCached';
export * from './useCounter';
export * from './useRenderCount';
export * from './useSyncRefs';
export * from './useToggle';

View File

@@ -0,0 +1,44 @@
import { describe, expect, it } from 'vitest';
import { ref, nextTick } from 'vue';
import { useCached } from '.';
const arrayEquals = (a: number[], b: number[]) => a.length === b.length && a.every((v, i) => v === b[i]);
describe('useCached', () => {
it('default comparator', async () => {
const externalValue = ref(0);
const cachedValue = useCached(externalValue);
expect(cachedValue.value).toBe(0);
externalValue.value = 1;
await nextTick();
expect(cachedValue.value).toBe(1);
externalValue.value = 10;
await nextTick();
expect(cachedValue.value).toBe(10);
});
it('custom array comparator', async () => {
const externalValue = ref([1]);
const initialValue = externalValue.value;
const cachedValue = useCached(externalValue, arrayEquals);
expect(cachedValue.value).toEqual(initialValue);
externalValue.value = initialValue;
await nextTick();
expect(cachedValue.value).toEqual(initialValue);
externalValue.value = [1];
await nextTick();
expect(cachedValue.value).toEqual(initialValue);
externalValue.value = [2];
await nextTick();
expect(cachedValue.value).not.toEqual(initialValue);
expect(cachedValue.value).toEqual([2]);
});
});

View File

@@ -0,0 +1,36 @@
import { ref, watch, type Ref, type WatchOptions } from 'vue';
export type Comparator<Value> = (a: Value, b: Value) => boolean;
/**
* @name useCached
* @category Reactivity
* @description Caches the value of an external ref and updates it only when the value changes
*
* @param {Ref<T>} externalValue Ref to cache
* @param {Comparator<T>} comparator Comparator function to compare the values
* @param {WatchOptions} watchOptions Watch options
* @returns {Ref<T>} Cached ref
*
* @example
* const externalValue = ref(0);
* const cachedValue = useCached(externalValue);
*
* @example
* const externalValue = ref(0);
* const cachedValue = useCached(externalValue, (a, b) => a === b, { immediate: true });
*/
export function useCached<Value = unknown>(
externalValue: Ref<Value>,
comparator: Comparator<Value> = (a, b) => a === b,
watchOptions?: WatchOptions,
): Ref<Value> {
const cached = ref(externalValue.value) as Ref<Value>;
watch(() => externalValue.value, (value) => {
if (!comparator(value, cached.value))
cached.value = value;
}, watchOptions);
return cached;
}

View File

@@ -0,0 +1,8 @@
<script setup lang="ts">
</script>
<template>
<div>
</div>
</template>

View File

@@ -0,0 +1,76 @@
import { it, expect, describe } from 'vitest';
import { ref } from 'vue';
import { useCounter } from '.';
describe('useCounter', () => {
it('initialize count with the provided initial value', () => {
const { count } = useCounter(5);
expect(count.value).toBe(5);
});
it('initialize count with the provided initial value from a ref', () => {
const { count } = useCounter(ref(5));
expect(count.value).toBe(5);
});
it('increment count by 1 by default', () => {
const { count, increment } = useCounter(0);
increment();
expect(count.value).toBe(1);
});
it('increment count by the specified delta', () => {
const { count, increment } = useCounter(0);
increment(5);
expect(count.value).toBe(5);
});
it('decrement count by 1 by default', () => {
const { count, decrement } = useCounter(5);
decrement();
expect(count.value).toBe(4);
});
it('decrement count by the specified delta', () => {
const { count, decrement } = useCounter(10);
decrement(5);
expect(count.value).toBe(5);
});
it('set count to the specified value', () => {
const { count, set } = useCounter(0);
set(10);
expect(count.value).toBe(10);
});
it('get the current count value', () => {
const { get } = useCounter(5);
expect(get()).toBe(5);
});
it('reset count to the initial value', () => {
const { count, reset } = useCounter(10);
count.value = 5;
reset();
expect(count.value).toBe(10);
});
it('reset count to the specified value', () => {
const { count, reset } = useCounter(10);
count.value = 5;
reset(20);
expect(count.value).toBe(20);
});
it('clamp count to the minimum value', () => {
const { count, decrement } = useCounter(Number.MIN_SAFE_INTEGER);
decrement();
expect(count.value).toBe(Number.MIN_SAFE_INTEGER);
});
it('clamp count to the maximum value', () => {
const { count, increment } = useCounter(Number.MAX_SAFE_INTEGER);
increment();
expect(count.value).toBe(Number.MAX_SAFE_INTEGER);
});
});

View File

@@ -0,0 +1,71 @@
import { ref, unref, type MaybeRef, type Ref } from 'vue';
import { clamp } from '@robonen/stdlib';
export interface UseCounterOptions {
min?: number;
max?: number;
}
export interface UseConterReturn {
count: Ref<number>;
increment: (delta?: number) => void;
decrement: (delta?: number) => void;
set: (value: number) => void;
get: () => number;
reset: (value?: number) => void;
}
/**
* @name useCounter
* @category Utilities
* @description A composable that provides a counter with increment, decrement, set, get, and reset functions
*
* @param {MaybeRef<number>} [initialValue=0] The initial value of the counter
* @param {UseCounterOptions} [options={}] The options for the counter
* @param {number} [options.min=Number.MIN_SAFE_INTEGER] The minimum value of the counter
* @param {number} [options.max=Number.MAX_SAFE_INTEGER] The maximum value of the counter
* @returns {UseConterReturn} The counter object
*
* @example
* const { count, increment } = useCounter(0);
*
* @example
* const { count, increment, decrement, set, get, reset } = useCounter(0, { min: 0, max: 10 });
*/
export function useCounter(
initialValue: MaybeRef<number> = 0,
options: UseCounterOptions = {},
): UseConterReturn {
let _initialValue = unref(initialValue);
const count = ref(initialValue);
const {
min = Number.MIN_SAFE_INTEGER,
max = Number.MAX_SAFE_INTEGER,
} = options;
const increment = (delta = 1) =>
count.value = clamp(count.value + delta, min, max);
const decrement = (delta = 1) =>
count.value = clamp(count.value - delta, min, max);
const set = (value: number) =>
count.value = clamp(value, min, max);
const get = () => count.value;
const reset = (value = _initialValue) => {
_initialValue = value;
return set(value);
};
return {
count,
increment,
decrement,
set,
get,
reset,
};
};

View File

@@ -0,0 +1,75 @@
import { describe, expect, it } from 'vitest';
import { defineComponent, nextTick, ref } from 'vue';
import { mount } from '@vue/test-utils'
import { useRenderCount } from '.';
const ComponentStub = defineComponent({
setup() {
const count = useRenderCount();
const visibleCount = ref(0);
const hiddenCount = ref(0);
return { count, visibleCount, hiddenCount };
},
template: `<div>{{ visibleCount }}</div>`,
});
describe('useRenderCount', () => {
it('return the number of times the component has been rendered', async () => {
const component = mount(ComponentStub);
// Initial render
expect(component.vm.count).toBe(0);
component.vm.hiddenCount = 1;
await nextTick();
// Will not trigger a render
expect(component.vm.count).toBe(0);
expect(component.text()).toBe('0');
component.vm.visibleCount++;
await nextTick();
// Will trigger a render
expect(component.vm.count).toBe(1);
expect(component.text()).toBe('1');
component.vm.visibleCount++;
component.vm.visibleCount++;
await nextTick();
// Will trigger a single render for both updates
expect(component.vm.count).toBe(2);
expect(component.text()).toBe('3');
});
it('can be used with a specific component instance', async () => {
const component = mount(ComponentStub);
const instance = component.vm.$;
const count = useRenderCount(instance);
// Initial render
expect(count.value).toBe(0);
component.vm.hiddenCount = 1;
await nextTick();
// Will not trigger a render
expect(count.value).toBe(0);
component.vm.visibleCount++;
await nextTick();
// Will trigger a render
expect(count.value).toBe(1);
component.vm.visibleCount++;
component.vm.visibleCount++;
await nextTick();
// Will trigger a single render for both updates
expect(count.value).toBe(2);
});
});

View File

@@ -0,0 +1,25 @@
import { onUpdated, readonly, type ComponentInternalInstance } from 'vue';
import { useCounter } from '../useCounter';
import { getLifeCycleTarger } from '../../utils';
/**
* @name useRenderCount
* @category Components
* @description Returns the number of times the component has been rendered into the DOM
*
* @param {ComponentInternalInstance} [instance] The component instance to track the render count for
* @returns {Readonly<Ref<number>>} The number of times the component has been rendered
*
* @example
* const count = useRenderCount();
*
* @example
* const count = useRenderCount(getCurrentInstance());
*/
export function useRenderCount(instance?: ComponentInternalInstance) {
const { count, increment } = useCounter(0);
onUpdated(increment, getLifeCycleTarger(instance));
return readonly(count);
}

View File

@@ -0,0 +1,39 @@
import { describe, expect, it } from 'vitest';
import { ref } from 'vue';
import { useSyncRefs } from '.';
describe('useSyncRefs', () => {
it('sync the value of a source ref with multiple target refs', () => {
const source = ref(0);
const target1 = ref(0);
const target2 = ref(0);
useSyncRefs(source, [target1, target2]);
source.value = 10;
expect(target1.value).toBe(10);
expect(target2.value).toBe(10);
});
it('sync the value of a source ref with a single target ref', () => {
const source = ref(0);
const target = ref(0);
useSyncRefs(source, target);
source.value = 20;
expect(target.value).toBe(20);
});
it('stop watching when the stop handle is called', () => {
const source = ref(0);
const target = ref(0);
const stop = useSyncRefs(source, target);
source.value = 30;
stop();
source.value = 40;
expect(target.value).toBe(30);
});
});

View File

@@ -0,0 +1,44 @@
import { watch, type Ref, type WatchOptions, type WatchSource } from 'vue';
import { isArray } from '@robonen/stdlib';
/**
* @name useSyncRefs
* @category Reactivity
* @description Syncs the value of a source ref with multiple target refs
*
* @param {WatchSource<T>} source Source ref to sync
* @param {Ref<T> | Ref<T>[]} targets Target refs to sync
* @param {WatchOptions} watchOptions Watch options
* @returns {WatchStopHandle} Watch stop handle
*
* @example
* const source = ref(0);
* const target1 = ref(0);
* const target2 = ref(0);
* useSyncRefs(source, [target1, target2]);
*
* @example
* const source = ref(0);
* const target1 = ref(0);
* useSyncRefs(source, target1, { immediate: true });
*/
export function useSyncRefs<T = unknown>(
source: WatchSource<T>,
targets: Ref<T> | Ref<T>[],
watchOptions: WatchOptions = {},
) {
const {
flush = 'sync',
deep = false,
immediate = true,
} = watchOptions;
if (!isArray(targets))
targets = [targets];
return watch(
source,
(value) => targets.forEach((target) => target.value = value),
{ flush, deep, immediate },
);
}

View File

@@ -0,0 +1,49 @@
import { isRef, ref, toValue, type MaybeRefOrGetter, type MaybeRef, type Ref } from 'vue';
export interface UseToggleOptions<Enabled, Disabled> {
enabledValue?: MaybeRefOrGetter<Enabled>,
disabledValue?: MaybeRefOrGetter<Disabled>,
}
// two overloads
// 1. const [state, toggle] = useToggle(nonRefValue, options)
// 2. const toggle = useToggle(refValue, options)
// 3. const [state, toggle] = useToggle() // true, false by default
export function useToggle<V extends Enabled | Disabled, Enabled = true, Disabled = false>(
initialValue: Ref<V>,
options?: UseToggleOptions<Enabled, Disabled>,
): (value?: V) => V;
export function useToggle<V extends Enabled | Disabled, Enabled = true, Disabled = false>(
initialValue?: V,
options?: UseToggleOptions<Enabled, Disabled>,
): [Ref<V>, (value?: V) => V];
export function useToggle<V extends Enabled | Disabled, Enabled = true, Disabled = false>(
initialValue: MaybeRef<V> = false,
options: UseToggleOptions<Enabled, Disabled> = {},
) {
const {
enabledValue = false,
disabledValue = true,
} = options;
const state = ref(initialValue) as Ref<V>;
const toggle = (value?: V) => {
if (arguments.length) {
state.value = value!;
return state.value;
}
const enabled = toValue(enabledValue);
const disabled = toValue(disabledValue);
state.value = state.value === enabled ? disabled : enabled;
return state.value;
};
return isRef(initialValue) ? toggle : [state, toggle];
}

View File

@@ -0,0 +1,2 @@
export * from './composables';
export * from './utils';

View File

@@ -0,0 +1,5 @@
import { getCurrentInstance, type ComponentInternalInstance } from 'vue';
export function getLifeCycleTarger(target?: ComponentInternalInstance) {
return target || getCurrentInstance();
}

View File

@@ -0,0 +1 @@
export * from './components';

View File

@@ -0,0 +1,3 @@
{
"extends": "@robonen/tsconfig/tsconfig.json"
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
},
});

2439
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1,10 @@
packages:
- packages/*
- packages/*
catalog:
'@vitest/coverage-v8': ^2.1.1
'@vue/test-utils': ^2.4.6
jsdom: ^25.0.1
pathe: ^1.1.2
unbuild: 3.0.0-rc.8
vitest: ^2.1.1
vue: ^3.5.10