mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 10:54:44 +00:00
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -26,4 +26,4 @@ jobs:
|
|||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: pnpm run all:test
|
run: pnpm run all:build && pnpm run all:test
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,6 +17,7 @@ node_modules
|
|||||||
.nuxt
|
.nuxt
|
||||||
.nitro
|
.nitro
|
||||||
.cache
|
.cache
|
||||||
|
cache
|
||||||
out
|
out
|
||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { defineConfig } from 'vitepress';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
lang: 'ru-RU',
|
lang: 'ru-RU',
|
||||||
title: "Tools",
|
title: "Toolkit",
|
||||||
description: "A set of tools and utilities for web development",
|
description: "A collection of typescript and javascript development tools",
|
||||||
rewrites: {
|
rewrites: {
|
||||||
'packages/:pkg/README.md': 'packages/:pkg/index.md',
|
'packages/:pkg/README.md': 'packages/:pkg/index.md',
|
||||||
},
|
},
|
||||||
14
docs/index.md
Normal file
14
docs/index.md
Normal 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: /
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
"vitepress": "^1.3.4"
|
"vitepress": "^1.3.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"all:build": "pnpm -r build",
|
||||||
"all:test": "pnpm -r test",
|
"all:test": "pnpm -r test",
|
||||||
"create": "jiti ./cli.ts",
|
"create": "jiti ./cli.ts",
|
||||||
"docs:dev": "vitepress dev .",
|
"docs:dev": "vitepress dev .",
|
||||||
|
|||||||
1
packages/platform/README.md
Normal file
1
packages/platform/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# @robonen/platform
|
||||||
5
packages/platform/jsr.json
Normal file
5
packages/platform/jsr.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "@robonen/platform",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"exports": "./src/index.ts"
|
||||||
|
}
|
||||||
36
packages/platform/package.json
Normal file
36
packages/platform/package.json
Normal 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:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
47
packages/platform/src/multi/debounce/index.ts
Normal file
47
packages/platform/src/multi/debounce/index.ts
Normal 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[];
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
19
packages/platform/src/multi/global/index.ts
Normal file
19
packages/platform/src/multi/global/index.ts
Normal 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;
|
||||||
2
packages/platform/src/multi/index.ts
Normal file
2
packages/platform/src/multi/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './global';
|
||||||
|
// export * from './debounce';
|
||||||
3
packages/platform/tsconfig.json
Normal file
3
packages/platform/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "@robonen/tsconfig/tsconfig.json"
|
||||||
|
}
|
||||||
@@ -27,6 +27,6 @@
|
|||||||
"test": "renovate-config-validator ./default.json"
|
"test": "renovate-config-validator ./default.json"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"renovate": "^38.100.0"
|
"renovate": "^38.101.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import { defineBuildConfig } from 'unbuild';
|
|||||||
|
|
||||||
export default defineBuildConfig({
|
export default defineBuildConfig({
|
||||||
rollup: {
|
rollup: {
|
||||||
emitCJS: true,
|
|
||||||
esbuild: {
|
esbuild: {
|
||||||
minify: true,
|
// minify: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -43,9 +43,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@robonen/tsconfig": "workspace:*",
|
"@robonen/tsconfig": "workspace:*",
|
||||||
"@vitest/coverage-v8": "^2.1.1",
|
"@vitest/coverage-v8": "catalog:",
|
||||||
"pathe": "^1.1.2",
|
"pathe": "catalog:",
|
||||||
"unbuild": "^2.0.0",
|
"unbuild": "catalog:",
|
||||||
"vitest": "^2.1.1"
|
"vitest": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
84
packages/stdlib/src/arrays/getByPath/index.ts
Normal file
84
packages/stdlib/src/arrays/getByPath/index.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
type Exist<T> = T extends undefined | null ? never : T;
|
||||||
|
|
||||||
|
type ExtractFromObject<O extends Record<PropertyKey, unknown>, K> =
|
||||||
|
K extends keyof O
|
||||||
|
? O[K]
|
||||||
|
: K extends keyof Exist<O>
|
||||||
|
? Exist<O>[K]
|
||||||
|
: never;
|
||||||
|
|
||||||
|
type ExtractFromArray<A extends readonly any[], K> = any[] extends A
|
||||||
|
? A extends readonly (infer T)[]
|
||||||
|
? T | undefined
|
||||||
|
: undefined
|
||||||
|
: K extends keyof A
|
||||||
|
? A[K]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
type GetWithArray<O, K> = K extends []
|
||||||
|
? O
|
||||||
|
: K extends [infer Key, ...infer Rest]
|
||||||
|
? O extends Record<PropertyKey, unknown>
|
||||||
|
? GetWithArray<ExtractFromObject<O, Key>, Rest>
|
||||||
|
: O extends readonly any[]
|
||||||
|
? GetWithArray<ExtractFromArray<O, Key>, Rest>
|
||||||
|
: never
|
||||||
|
: never;
|
||||||
|
|
||||||
|
type Path<T> = T extends `${infer Key}.${infer Rest}`
|
||||||
|
? [Key, ...Path<Rest>]
|
||||||
|
: T extends `${infer Key}`
|
||||||
|
? [Key]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Type that generate a type of a value by a path;
|
||||||
|
// e.g. ['a', 'b', 'c'] => { a: { b: { c: PropertyKey } } }
|
||||||
|
// e.g. ['a', 'b', 'c', 'd'] => { a: { b: { c: { d: PropertyKey } } } }
|
||||||
|
// e.g. ['a'] => { a: PropertyKey }
|
||||||
|
// e.g. ['a', '0'], => { a: [PropertyKey] }
|
||||||
|
// e.g. ['a', '0', 'b'] => { a: [{ b: PropertyKey }] }
|
||||||
|
// e.g. ['a', '0', 'b', '0'] => { a: [{ b: [PropertyKey] }] }
|
||||||
|
// e/g/ ['0', 'a'] => [{ a: PropertyKey }]
|
||||||
|
//
|
||||||
|
// Input: ['a', 'b', 'c'], constrain: PropertyKey
|
||||||
|
// Output: { a: { b: { c: PropertyKey } } }
|
||||||
|
|
||||||
|
export type UnionToIntersection<Union> = (
|
||||||
|
Union extends unknown
|
||||||
|
? (distributedUnion: Union) => void
|
||||||
|
: never
|
||||||
|
) extends ((mergedIntersection: infer Intersection) => void)
|
||||||
|
? Intersection & Union
|
||||||
|
: never;
|
||||||
|
|
||||||
|
|
||||||
|
type PathToType<T extends string[]> = T extends [infer Head, ...infer Rest]
|
||||||
|
? Head extends string
|
||||||
|
? Head extends `${number}`
|
||||||
|
? Rest extends string[]
|
||||||
|
? PathToType<Rest>[]
|
||||||
|
: never
|
||||||
|
: Rest extends string[]
|
||||||
|
? { [K in Head & string]: PathToType<Rest> }
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: string;
|
||||||
|
|
||||||
|
export type Generate<T extends string> = UnionToIntersection<PathToType<Path<T>>>;
|
||||||
|
type Get<O, K> = GetWithArray<O, Path<K>>;
|
||||||
|
|
||||||
|
export function getByPath<O, K extends string>(obj: O, path: K): Get<O, K>;
|
||||||
|
export function getByPath(obj: Record<string, unknown>, path: string): unknown {
|
||||||
|
const keys = path.split('.');
|
||||||
|
let currentObj = obj;
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = currentObj[key];
|
||||||
|
|
||||||
|
if (value === undefined || value === null) return undefined;
|
||||||
|
|
||||||
|
currentObj = value as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentObj;
|
||||||
|
}
|
||||||
1
packages/stdlib/src/arrays/index.ts
Normal file
1
packages/stdlib/src/arrays/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './getByPath';
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
*
|
*
|
||||||
* @returns {Function} A function that generates unique flags
|
* @returns {Function} A function that generates unique flags
|
||||||
* @throws {RangeError} If more than 31 flags are created
|
* @throws {RangeError} If more than 31 flags are created
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export function flagsGenerator() {
|
export function flagsGenerator() {
|
||||||
let lastFlag = 0;
|
let lastFlag = 0;
|
||||||
@@ -22,6 +24,8 @@ export function flagsGenerator() {
|
|||||||
*
|
*
|
||||||
* @param {number[]} flags - The flags to combine
|
* @param {number[]} flags - The flags to combine
|
||||||
* @returns {number} The combined flags
|
* @returns {number} The combined flags
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export function and(...flags: number[]) {
|
export function and(...flags: number[]) {
|
||||||
return flags.reduce((acc, flag) => acc & flag, -1);
|
return flags.reduce((acc, flag) => acc & flag, -1);
|
||||||
@@ -32,6 +36,8 @@ export function and(...flags: number[]) {
|
|||||||
*
|
*
|
||||||
* @param {number[]} flags - The flags to combine
|
* @param {number[]} flags - The flags to combine
|
||||||
* @returns {number} The combined flags
|
* @returns {number} The combined flags
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export function or(...flags: number[]) {
|
export function or(...flags: number[]) {
|
||||||
return flags.reduce((acc, flag) => acc | flag, 0);
|
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
|
* @param {number} flag - The flag to apply the NOT operator to
|
||||||
* @returns {number} The result of the NOT operator
|
* @returns {number} The result of the NOT operator
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export function not(flag: number) {
|
export function not(flag: number) {
|
||||||
return ~flag;
|
return ~flag;
|
||||||
@@ -51,7 +59,10 @@ export function not(flag: number) {
|
|||||||
* Function to make sure a flag has a specific bit set
|
* Function to make sure a flag has a specific bit set
|
||||||
*
|
*
|
||||||
* @param {number} flag - The flag to check
|
* @param {number} flag - The flag to check
|
||||||
|
* @param {number} other - Flag to check
|
||||||
* @returns {boolean} Whether the flag has the bit set
|
* @returns {boolean} Whether the flag has the bit set
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export function has(flag: number, other: number) {
|
export function has(flag: number, other: number) {
|
||||||
return (flag & other) === other;
|
return (flag & other) === other;
|
||||||
@@ -62,6 +73,8 @@ export function has(flag: number, other: number) {
|
|||||||
*
|
*
|
||||||
* @param {number} flag - The flag to check
|
* @param {number} flag - The flag to check
|
||||||
* @returns {boolean} Whether the flag is set
|
* @returns {boolean} Whether the flag is set
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export function is(flag: number) {
|
export function is(flag: number) {
|
||||||
return flag !== 0;
|
return flag !== 0;
|
||||||
@@ -73,6 +86,8 @@ export function is(flag: number) {
|
|||||||
* @param {number} flag - Source flag
|
* @param {number} flag - Source flag
|
||||||
* @param {number} other - Flag to unset
|
* @param {number} other - Flag to unset
|
||||||
* @returns {number} The new flag
|
* @returns {number} The new flag
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export function unset(flag: number, other: number) {
|
export function unset(flag: number, other: number) {
|
||||||
return flag & ~other;
|
return flag & ~other;
|
||||||
@@ -84,6 +99,8 @@ export function unset(flag: number, other: number) {
|
|||||||
* @param {number} flag - Source flag
|
* @param {number} flag - Source flag
|
||||||
* @param {number} other - Flag to toggle
|
* @param {number} other - Flag to toggle
|
||||||
* @returns {number} The new flag
|
* @returns {number} The new flag
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export function toggle(flag: number, other: number) {
|
export function toggle(flag: number, other: number) {
|
||||||
return flag ^ other;
|
return flag ^ other;
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ export * from './math';
|
|||||||
export * from './patterns';
|
export * from './patterns';
|
||||||
export * from './bits';
|
export * from './bits';
|
||||||
export * from './structs';
|
export * from './structs';
|
||||||
|
export * from './arrays';
|
||||||
|
export * from './types';
|
||||||
|
|||||||
13
packages/stdlib/src/math/basic/clamp/index.ts
Normal file
13
packages/stdlib/src/math/basic/clamp/index.ts
Normal 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);
|
||||||
|
}
|
||||||
61
packages/stdlib/src/math/basic/lerp/index.test.ts
Normal file
61
packages/stdlib/src/math/basic/lerp/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
27
packages/stdlib/src/math/basic/lerp/index.ts
Normal file
27
packages/stdlib/src/math/basic/lerp/index.ts
Normal 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);
|
||||||
|
}
|
||||||
46
packages/stdlib/src/math/basic/remap/index.test.ts
Normal file
46
packages/stdlib/src/math/basic/remap/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
|
* 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_min The minimum value of the output range
|
||||||
* @param {number} out_max The maximum value of the output range
|
* @param {number} out_max The maximum value of the output range
|
||||||
* @returns {number} The mapped value
|
* @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 {
|
export function remap(value: number, in_min: number, in_max: number, out_min: number, out_max: number) {
|
||||||
// Zero input range means invalid input, so return lowest output range value
|
|
||||||
if (in_min === in_max)
|
if (in_min === in_max)
|
||||||
return out_min;
|
return out_min;
|
||||||
|
|
||||||
// To ensure the value is within the input range, clamp it
|
|
||||||
const clampedValue = clamp(value, in_min, in_max);
|
const clampedValue = clamp(value, in_min, in_max);
|
||||||
|
|
||||||
// Finally, map the value from the input range to the output range
|
return lerp(out_min, out_max, inverseLerp(in_min, in_max, clampedValue));
|
||||||
return (clampedValue - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
|
|
||||||
}
|
}
|
||||||
35
packages/stdlib/src/math/bigint/clampBigInt/index.test.ts
Normal file
35
packages/stdlib/src/math/bigint/clampBigInt/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
16
packages/stdlib/src/math/bigint/clampBigInt/index.ts
Normal file
16
packages/stdlib/src/math/bigint/clampBigInt/index.ts
Normal 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);
|
||||||
|
}
|
||||||
83
packages/stdlib/src/math/bigint/lerpBigInt/index.test.ts
Normal file
83
packages/stdlib/src/math/bigint/lerpBigInt/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
27
packages/stdlib/src/math/bigint/lerpBigInt/index.ts
Normal file
27
packages/stdlib/src/math/bigint/lerpBigInt/index.ts
Normal 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;
|
||||||
|
}
|
||||||
39
packages/stdlib/src/math/bigint/maxBigInt/index.test.ts
Normal file
39
packages/stdlib/src/math/bigint/maxBigInt/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
15
packages/stdlib/src/math/bigint/maxBigInt/index.ts
Normal file
15
packages/stdlib/src/math/bigint/maxBigInt/index.ts
Normal 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);
|
||||||
|
}
|
||||||
39
packages/stdlib/src/math/bigint/minBigInt/index.test.ts
Normal file
39
packages/stdlib/src/math/bigint/minBigInt/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
15
packages/stdlib/src/math/bigint/minBigInt/index.ts
Normal file
15
packages/stdlib/src/math/bigint/minBigInt/index.ts
Normal 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);
|
||||||
|
}
|
||||||
32
packages/stdlib/src/math/bigint/remapBigInt/index.test.ts
Normal file
32
packages/stdlib/src/math/bigint/remapBigInt/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
23
packages/stdlib/src/math/bigint/remapBigInt/index.ts
Normal file
23
packages/stdlib/src/math/bigint/remapBigInt/index.ts
Normal 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));
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -1,2 +1,9 @@
|
|||||||
export * from './clamp';
|
export * from './basic/clamp';
|
||||||
export * from './mapRange';
|
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';
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import { PubSub } from './index';
|
import { PubSub } from './index';
|
||||||
|
|
||||||
describe('PubSub', () => {
|
describe('pubsub', () => {
|
||||||
let eventBus: PubSub<{
|
let eventBus: PubSub<{
|
||||||
event1: (arg: string) => void;
|
event1: (arg: string) => void;
|
||||||
event2: () => void
|
event2: () => void
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ export type EventsRecord = Record<string, Subscriber>;
|
|||||||
/**
|
/**
|
||||||
* Simple PubSub implementation
|
* Simple PubSub implementation
|
||||||
*
|
*
|
||||||
|
* @since 0.0.2
|
||||||
|
*
|
||||||
* @template {EventsRecord} Events
|
* @template {EventsRecord} Events
|
||||||
*/
|
*/
|
||||||
export class PubSub<Events extends EventsRecord> {
|
export class PubSub<Events extends EventsRecord> {
|
||||||
@@ -85,7 +87,7 @@ export class PubSub<Events extends EventsRecord> {
|
|||||||
* @param {...Parameters<Events[K]>} args Arguments for the listener
|
* @param {...Parameters<Events[K]>} args Arguments for the listener
|
||||||
* @returns {boolean}
|
* @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);
|
const listeners = this.events.get(event);
|
||||||
|
|
||||||
if (!listeners)
|
if (!listeners)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { Stack } from './index';
|
import { Stack } from './index';
|
||||||
|
|
||||||
describe('Stack', () => {
|
describe('stack', () => {
|
||||||
describe('constructor', () => {
|
describe('constructor', () => {
|
||||||
it('create an empty stack if no initial values are provided', () => {
|
it('create an empty stack if no initial values are provided', () => {
|
||||||
const stack = new Stack<number>();
|
const stack = new Stack<number>();
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ export type StackOptions = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a stack data structure
|
* Represents a stack data structure
|
||||||
|
*
|
||||||
|
* @since 0.0.2
|
||||||
|
*
|
||||||
* @template T The type of elements stored in the stack
|
* @template T The type of elements stored in the stack
|
||||||
*/
|
*/
|
||||||
export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
|
export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
|
||||||
@@ -13,7 +16,7 @@ export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
|
|||||||
* @private
|
* @private
|
||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
private maxSize: number;
|
private readonly maxSize: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The stack data structure
|
* The stack data structure
|
||||||
@@ -21,7 +24,7 @@ export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
|
|||||||
* @private
|
* @private
|
||||||
* @type {T[]}
|
* @type {T[]}
|
||||||
*/
|
*/
|
||||||
private stack: T[];
|
private readonly stack: T[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of Stack
|
* 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
|
* Gets the number of elements in the stack
|
||||||
* @returns {number} 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;
|
return this.stack.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +50,7 @@ export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
|
|||||||
* Checks if the stack is empty
|
* Checks if the stack is empty
|
||||||
* @returns {boolean} `true` if the stack is empty, `false` otherwise
|
* @returns {boolean} `true` if the stack is empty, `false` otherwise
|
||||||
*/
|
*/
|
||||||
public get isEmpty(): boolean {
|
public get isEmpty() {
|
||||||
return this.stack.length === 0;
|
return this.stack.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +58,7 @@ export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
|
|||||||
* Checks if the stack is full
|
* Checks if the stack is full
|
||||||
* @returns {boolean} `true` if the stack is full, `false` otherwise
|
* @returns {boolean} `true` if the stack is full, `false` otherwise
|
||||||
*/
|
*/
|
||||||
public get isFull(): boolean {
|
public get isFull() {
|
||||||
return this.stack.length === this.maxSize;
|
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
|
* Pops an element from the stack
|
||||||
* @returns {T} The element popped from the stack
|
* @returns {T} The element popped from the stack
|
||||||
*/
|
*/
|
||||||
public pop(): T | undefined {
|
public pop() {
|
||||||
return this.stack.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
|
* Peeks at the top element of the stack
|
||||||
* @returns {T} The top element of the stack
|
* @returns {T} The top element of the stack
|
||||||
*/
|
*/
|
||||||
public peek(): T | undefined {
|
public peek() {
|
||||||
if (this.isEmpty)
|
if (this.isEmpty)
|
||||||
throw new RangeError('Stack is empty');
|
throw new RangeError('Stack is empty');
|
||||||
|
|
||||||
@@ -109,7 +112,7 @@ export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
|
|||||||
*
|
*
|
||||||
* @returns {T[]}
|
* @returns {T[]}
|
||||||
*/
|
*/
|
||||||
public toArray(): T[] {
|
public toArray() {
|
||||||
return this.stack.toReversed();
|
return this.stack.toReversed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
export * from './levenshtein-distance';
|
export * from './levenshtein-distance';
|
||||||
export * from './trigram-distance';
|
export * from './trigram-distance';
|
||||||
|
// TODO: Template is not implemented yet
|
||||||
|
// export * from './template';
|
||||||
@@ -4,12 +4,12 @@
|
|||||||
* @param {string} left First string
|
* @param {string} left First string
|
||||||
* @param {string} right Second string
|
* @param {string} right Second string
|
||||||
* @returns {number} The Levenshtein distance between the two strings
|
* @returns {number} The Levenshtein distance between the two strings
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
*/
|
*/
|
||||||
export function levenshteinDistance(left: string, right: string): number {
|
export function levenshteinDistance(left: string, right: string): number {
|
||||||
// If the strings are equal, the distance is 0
|
|
||||||
if (left === right) return 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 (left.length === 0) return right.length;
|
||||||
if (right.length === 0) return left.length;
|
if (right.length === 0) return left.length;
|
||||||
|
|
||||||
|
|||||||
105
packages/stdlib/src/text/template/index.test-d.ts
Normal file
105
packages/stdlib/src/text/template/index.test-d.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { describe, expectTypeOf, it } from "vitest";
|
||||||
|
import type { ClearPlaceholder, ExtractPlaceholders } from "./index";
|
||||||
|
|
||||||
|
describe('template', () => {
|
||||||
|
describe('ClearPlaceholder', () => {
|
||||||
|
it('ignores strings without braces', () => {
|
||||||
|
type actual = ClearPlaceholder<'name'>;
|
||||||
|
type expected = 'name';
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes all balanced braces from placeholders', () => {
|
||||||
|
type actual1 = ClearPlaceholder<'{name}'>;
|
||||||
|
type actual2 = ClearPlaceholder<'{{name}}'>;
|
||||||
|
type actual3 = ClearPlaceholder<'{{{name}}}'>;
|
||||||
|
type expected = 'name';
|
||||||
|
|
||||||
|
expectTypeOf<actual1>().toEqualTypeOf<expected>();
|
||||||
|
expectTypeOf<actual2>().toEqualTypeOf<expected>();
|
||||||
|
expectTypeOf<actual3>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes all unbalanced braces from placeholders', () => {
|
||||||
|
type actual1 = ClearPlaceholder<'{name}}'>;
|
||||||
|
type actual2 = ClearPlaceholder<'{{name}}}'>;
|
||||||
|
type expected = 'name';
|
||||||
|
|
||||||
|
expectTypeOf<actual1>().toEqualTypeOf<expected>();
|
||||||
|
expectTypeOf<actual2>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ExtractPlaceholders', () => {
|
||||||
|
it('string without placeholders', () => {
|
||||||
|
type actual = ExtractPlaceholders<'Hello name, how are?'>;
|
||||||
|
type expected = never;
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('string with one idexed placeholder', () => {
|
||||||
|
type actual = ExtractPlaceholders<'Hello {0}, how are you?'>;
|
||||||
|
type expected = '0';
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('string with two indexed placeholders', () => {
|
||||||
|
type actual = ExtractPlaceholders<'Hello {0}, my name is {1}'>;
|
||||||
|
type expected = '0' | '1';
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('string with one key placeholder', () => {
|
||||||
|
type actual = ExtractPlaceholders<'Hello {name}, how are you?'>;
|
||||||
|
type expected = 'name';
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('string with two key placeholders', () => {
|
||||||
|
type actual = ExtractPlaceholders<'Hello {name}, my name is {managers.0.name}'>;
|
||||||
|
type expected = 'name' | 'managers.0.name';
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('string with mixed placeholders', () => {
|
||||||
|
type actual = ExtractPlaceholders<'Hello {0}, how are you? My name is {1.name}'>;
|
||||||
|
type expected = '0' | '1.name';
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('string with nested placeholder and balanced braces', () => {
|
||||||
|
type actual = ExtractPlaceholders<'Hello {{name}}, how are you?'>;
|
||||||
|
type expected = 'name';
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('string with nested placeholder and unbalanced braces', () => {
|
||||||
|
type actual = ExtractPlaceholders<'Hello {{{name}, how are you?'>;
|
||||||
|
type expected = 'name';
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('string with nested placeholders and balanced braces', () => {
|
||||||
|
type actual = ExtractPlaceholders<'Hello {{{name}{positions}}}, how are you?'>;
|
||||||
|
type expected = 'name' | 'positions';
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('string with nested placeholders and unbalanced braces', () => {
|
||||||
|
type actual = ExtractPlaceholders<'Hello {{{name}{positions}, how are you?'>;
|
||||||
|
type expected = 'name' | 'positions';
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
48
packages/stdlib/src/text/template/index.test.ts
Normal file
48
packages/stdlib/src/text/template/index.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
66
packages/stdlib/src/text/template/index.ts
Normal file
66
packages/stdlib/src/text/template/index.ts
Normal 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'},
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// });
|
||||||
@@ -5,6 +5,8 @@ export type Trigrams = Map<string, number>;
|
|||||||
*
|
*
|
||||||
* @param {string} text The text to extract trigrams
|
* @param {string} text The text to extract trigrams
|
||||||
* @returns {Trigrams} A map of trigram to count
|
* @returns {Trigrams} A map of trigram to count
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
*/
|
*/
|
||||||
export function trigramProfile(text: string): Trigrams {
|
export function trigramProfile(text: string): Trigrams {
|
||||||
text = '\n\n' + text + '\n\n';
|
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} left First text trigram profile
|
||||||
* @param {Trigrams} right Second text trigram profile
|
* @param {Trigrams} right Second text trigram profile
|
||||||
* @returns {number} The trigram distance between the two strings
|
* @returns {number} The trigram distance between the two strings
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
*/
|
*/
|
||||||
export function trigramDistance(left: Trigrams, right: Trigrams): number {
|
export function trigramDistance(left: Trigrams, right: Trigrams): number {
|
||||||
let distance = -4;
|
let distance = -4;
|
||||||
|
|||||||
2
packages/stdlib/src/types/index.ts
Normal file
2
packages/stdlib/src/types/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './js';
|
||||||
|
export * from './ts';
|
||||||
30
packages/stdlib/src/types/js/casts.test.ts
Normal file
30
packages/stdlib/src/types/js/casts.test.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { toString } from './casts';
|
||||||
|
|
||||||
|
describe('casts', () => {
|
||||||
|
describe('toString', () => {
|
||||||
|
it('correct string representation of a value', () => {
|
||||||
|
// Primitives
|
||||||
|
expect(toString(true)).toBe('[object Boolean]');
|
||||||
|
expect(toString(() => {})).toBe('[object Function]');
|
||||||
|
expect(toString(5)).toBe('[object Number]');
|
||||||
|
expect(toString(BigInt(5))).toBe('[object BigInt]');
|
||||||
|
expect(toString('hello')).toBe('[object String]');
|
||||||
|
expect(toString(Symbol('foo'))).toBe('[object Symbol]');
|
||||||
|
|
||||||
|
// Complex
|
||||||
|
expect(toString([])).toBe('[object Array]');
|
||||||
|
expect(toString({})).toBe('[object Object]');
|
||||||
|
expect(toString(undefined)).toBe('[object Undefined]');
|
||||||
|
expect(toString(null)).toBe('[object Null]');
|
||||||
|
expect(toString(/abc/)).toBe('[object RegExp]');
|
||||||
|
expect(toString(new Date())).toBe('[object Date]');
|
||||||
|
expect(toString(new Error())).toBe('[object Error]');
|
||||||
|
expect(toString(new Promise(() => {}))).toBe('[object Promise]');
|
||||||
|
expect(toString(new Map())).toBe('[object Map]');
|
||||||
|
expect(toString(new Set())).toBe('[object Set]');
|
||||||
|
expect(toString(new WeakMap())).toBe('[object WeakMap]');
|
||||||
|
expect(toString(new WeakSet())).toBe('[object WeakSet]');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
7
packages/stdlib/src/types/js/casts.ts
Normal file
7
packages/stdlib/src/types/js/casts.ts
Normal 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);
|
||||||
183
packages/stdlib/src/types/js/complex.test.ts
Normal file
183
packages/stdlib/src/types/js/complex.test.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { isArray, isObject, isRegExp, isDate, isError, isPromise, isMap, isSet, isWeakMap, isWeakSet } from './complex';
|
||||||
|
|
||||||
|
describe('complex', () => {
|
||||||
|
describe('isArray', () => {
|
||||||
|
it('true if the value is an array', () => {
|
||||||
|
expect(isArray([])).toBe(true);
|
||||||
|
expect(isArray([1, 2, 3])).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false if the value is not an array', () => {
|
||||||
|
expect(isArray('')).toBe(false);
|
||||||
|
expect(isArray(123)).toBe(false);
|
||||||
|
expect(isArray(true)).toBe(false);
|
||||||
|
expect(isArray(null)).toBe(false);
|
||||||
|
expect(isArray(undefined)).toBe(false);
|
||||||
|
expect(isArray({})).toBe(false);
|
||||||
|
expect(isArray(new Map())).toBe(false);
|
||||||
|
expect(isArray(new Set())).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isObject', () => {
|
||||||
|
it('true if the value is an object', () => {
|
||||||
|
expect(isObject({})).toBe(true);
|
||||||
|
expect(isObject({ key: 'value' })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false if the value is not an object', () => {
|
||||||
|
expect(isObject('')).toBe(false);
|
||||||
|
expect(isObject(123)).toBe(false);
|
||||||
|
expect(isObject(true)).toBe(false);
|
||||||
|
expect(isObject(null)).toBe(false);
|
||||||
|
expect(isObject(undefined)).toBe(false);
|
||||||
|
expect(isObject([])).toBe(false);
|
||||||
|
expect(isObject(new Map())).toBe(false);
|
||||||
|
expect(isObject(new Set())).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isRegExp', () => {
|
||||||
|
it('true if the value is a regexp', () => {
|
||||||
|
expect(isRegExp(/test/)).toBe(true);
|
||||||
|
expect(isRegExp(new RegExp('test'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false if the value is not a regexp', () => {
|
||||||
|
expect(isRegExp('')).toBe(false);
|
||||||
|
expect(isRegExp(123)).toBe(false);
|
||||||
|
expect(isRegExp(true)).toBe(false);
|
||||||
|
expect(isRegExp(null)).toBe(false);
|
||||||
|
expect(isRegExp(undefined)).toBe(false);
|
||||||
|
expect(isRegExp([])).toBe(false);
|
||||||
|
expect(isRegExp({})).toBe(false);
|
||||||
|
expect(isRegExp(new Map())).toBe(false);
|
||||||
|
expect(isRegExp(new Set())).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isDate', () => {
|
||||||
|
it('true if the value is a date', () => {
|
||||||
|
expect(isDate(new Date())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false if the value is not a date', () => {
|
||||||
|
expect(isDate('')).toBe(false);
|
||||||
|
expect(isDate(123)).toBe(false);
|
||||||
|
expect(isDate(true)).toBe(false);
|
||||||
|
expect(isDate(null)).toBe(false);
|
||||||
|
expect(isDate(undefined)).toBe(false);
|
||||||
|
expect(isDate([])).toBe(false);
|
||||||
|
expect(isDate({})).toBe(false);
|
||||||
|
expect(isDate(new Map())).toBe(false);
|
||||||
|
expect(isDate(new Set())).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isError', () => {
|
||||||
|
it('true if the value is an error', () => {
|
||||||
|
expect(isError(new Error())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false if the value is not an error', () => {
|
||||||
|
expect(isError('')).toBe(false);
|
||||||
|
expect(isError(123)).toBe(false);
|
||||||
|
expect(isError(true)).toBe(false);
|
||||||
|
expect(isError(null)).toBe(false);
|
||||||
|
expect(isError(undefined)).toBe(false);
|
||||||
|
expect(isError([])).toBe(false);
|
||||||
|
expect(isError({})).toBe(false);
|
||||||
|
expect(isError(new Map())).toBe(false);
|
||||||
|
expect(isError(new Set())).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isPromise', () => {
|
||||||
|
it('true if the value is a promise', () => {
|
||||||
|
expect(isPromise(new Promise(() => {}))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false if the value is not a promise', () => {
|
||||||
|
expect(isPromise('')).toBe(false);
|
||||||
|
expect(isPromise(123)).toBe(false);
|
||||||
|
expect(isPromise(true)).toBe(false);
|
||||||
|
expect(isPromise(null)).toBe(false);
|
||||||
|
expect(isPromise(undefined)).toBe(false);
|
||||||
|
expect(isPromise([])).toBe(false);
|
||||||
|
expect(isPromise({})).toBe(false);
|
||||||
|
expect(isPromise(new Map())).toBe(false);
|
||||||
|
expect(isPromise(new Set())).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isMap', () => {
|
||||||
|
it('true if the value is a map', () => {
|
||||||
|
expect(isMap(new Map())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false if the value is not a map', () => {
|
||||||
|
expect(isMap('')).toBe(false);
|
||||||
|
expect(isMap(123)).toBe(false);
|
||||||
|
expect(isMap(true)).toBe(false);
|
||||||
|
expect(isMap(null)).toBe(false);
|
||||||
|
expect(isMap(undefined)).toBe(false);
|
||||||
|
expect(isMap([])).toBe(false);
|
||||||
|
expect(isMap({})).toBe(false);
|
||||||
|
expect(isMap(new Set())).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isSet', () => {
|
||||||
|
it('true if the value is a set', () => {
|
||||||
|
expect(isSet(new Set())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false if the value is not a set', () => {
|
||||||
|
expect(isSet('')).toBe(false);
|
||||||
|
expect(isSet(123)).toBe(false);
|
||||||
|
expect(isSet(true)).toBe(false);
|
||||||
|
expect(isSet(null)).toBe(false);
|
||||||
|
expect(isSet(undefined)).toBe(false);
|
||||||
|
expect(isSet([])).toBe(false);
|
||||||
|
expect(isSet({})).toBe(false);
|
||||||
|
expect(isSet(new Map())).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isWeakMap', () => {
|
||||||
|
it('true if the value is a weakmap', () => {
|
||||||
|
expect(isWeakMap(new WeakMap())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false if the value is not a weakmap', () => {
|
||||||
|
expect(isWeakMap('')).toBe(false);
|
||||||
|
expect(isWeakMap(123)).toBe(false);
|
||||||
|
expect(isWeakMap(true)).toBe(false);
|
||||||
|
expect(isWeakMap(null)).toBe(false);
|
||||||
|
expect(isWeakMap(undefined)).toBe(false);
|
||||||
|
expect(isWeakMap([])).toBe(false);
|
||||||
|
expect(isWeakMap({})).toBe(false);
|
||||||
|
expect(isWeakMap(new Map())).toBe(false);
|
||||||
|
expect(isWeakMap(new Set())).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isWeakSet', () => {
|
||||||
|
it('true if the value is a weakset', () => {
|
||||||
|
expect(isWeakSet(new WeakSet())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false if the value is not a weakset', () => {
|
||||||
|
expect(isWeakSet('')).toBe(false);
|
||||||
|
expect(isWeakSet(123)).toBe(false);
|
||||||
|
expect(isWeakSet(true)).toBe(false);
|
||||||
|
expect(isWeakSet(null)).toBe(false);
|
||||||
|
expect(isWeakSet(undefined)).toBe(false);
|
||||||
|
expect(isWeakSet([])).toBe(false);
|
||||||
|
expect(isWeakSet({})).toBe(false);
|
||||||
|
expect(isWeakSet(new Map())).toBe(false);
|
||||||
|
expect(isWeakSet(new Set())).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
81
packages/stdlib/src/types/js/complex.ts
Normal file
81
packages/stdlib/src/types/js/complex.ts
Normal 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]';
|
||||||
3
packages/stdlib/src/types/js/index.ts
Normal file
3
packages/stdlib/src/types/js/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './casts';
|
||||||
|
export * from './primitives';
|
||||||
|
export * from './complex';
|
||||||
101
packages/stdlib/src/types/js/primitives.test.ts
Normal file
101
packages/stdlib/src/types/js/primitives.test.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { isBoolean, isFunction, isNumber, isBigInt, isString, isSymbol, isUndefined, isNull } from './primitives';
|
||||||
|
|
||||||
|
describe('primitives', () => {
|
||||||
|
describe('isBoolean', () => {
|
||||||
|
it('true if the value is a boolean', () => {
|
||||||
|
expect(isBoolean(true)).toBe(true);
|
||||||
|
expect(isBoolean(false)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false if the value is not a boolean', () => {
|
||||||
|
expect(isBoolean(0)).toBe(false);
|
||||||
|
expect(isBoolean('true')).toBe(false);
|
||||||
|
expect(isBoolean(null)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isFunction', () => {
|
||||||
|
it('true if the value is a function', () => {
|
||||||
|
expect(isFunction(() => {})).toBe(true);
|
||||||
|
expect(isFunction(function() {})).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false if the value is not a function', () => {
|
||||||
|
expect(isFunction(123)).toBe(false);
|
||||||
|
expect(isFunction('function')).toBe(false);
|
||||||
|
expect(isFunction(null)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isNumber', () => {
|
||||||
|
it('true if the value is a number', () => {
|
||||||
|
expect(isNumber(123)).toBe(true);
|
||||||
|
expect(isNumber(3.14)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false if the value is not a number', () => {
|
||||||
|
expect(isNumber('123')).toBe(false);
|
||||||
|
expect(isNumber(null)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isBigInt', () => {
|
||||||
|
it('true if the value is a bigint', () => {
|
||||||
|
expect(isBigInt(BigInt(123))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false if the value is not a bigint', () => {
|
||||||
|
expect(isBigInt(123)).toBe(false);
|
||||||
|
expect(isBigInt('123')).toBe(false);
|
||||||
|
expect(isBigInt(null)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isString', () => {
|
||||||
|
it('true if the value is a string', () => {
|
||||||
|
expect(isString('hello')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false if the value is not a string', () => {
|
||||||
|
expect(isString(123)).toBe(false);
|
||||||
|
expect(isString(null)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isSymbol', () => {
|
||||||
|
it('true if the value is a symbol', () => {
|
||||||
|
expect(isSymbol(Symbol())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false if the value is not a symbol', () => {
|
||||||
|
expect(isSymbol(123)).toBe(false);
|
||||||
|
expect(isSymbol('symbol')).toBe(false);
|
||||||
|
expect(isSymbol(null)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isUndefined', () => {
|
||||||
|
it('true if the value is undefined', () => {
|
||||||
|
expect(isUndefined(undefined)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false if the value is not undefined', () => {
|
||||||
|
expect(isUndefined(null)).toBe(false);
|
||||||
|
expect(isUndefined(123)).toBe(false);
|
||||||
|
expect(isUndefined('undefined')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isNull', () => {
|
||||||
|
it('true if the value is null', () => {
|
||||||
|
expect(isNull(null)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false if the value is not null', () => {
|
||||||
|
expect(isNull(undefined)).toBe(false);
|
||||||
|
expect(isNull(123)).toBe(false);
|
||||||
|
expect(isNull('null')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
65
packages/stdlib/src/types/js/primitives.ts
Normal file
65
packages/stdlib/src/types/js/primitives.ts
Normal 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]';
|
||||||
1
packages/stdlib/src/types/ts/index.ts
Normal file
1
packages/stdlib/src/types/ts/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './string';
|
||||||
43
packages/stdlib/src/types/ts/string.test-d.ts
Normal file
43
packages/stdlib/src/types/ts/string.test-d.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { describe, expectTypeOf, it } from 'vitest';
|
||||||
|
import type { HasSpaces, Trim } from './string';
|
||||||
|
|
||||||
|
describe('string', () => {
|
||||||
|
describe('Trim', () => {
|
||||||
|
it('remove leading and trailing spaces from a string', () => {
|
||||||
|
type actual = Trim<' hello '>;
|
||||||
|
type expected = 'hello';
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('remove only leading spaces from a string', () => {
|
||||||
|
type actual = Trim<' hello'>;
|
||||||
|
type expected = 'hello';
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('remove only trailing spaces from a string', () => {
|
||||||
|
type actual = Trim<'hello '>;
|
||||||
|
type expected = 'hello';
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('HasSpaces', () => {
|
||||||
|
it('check if a string has spaces', () => {
|
||||||
|
type actual = HasSpaces<'hello world'>;
|
||||||
|
type expected = true;
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('check if a string has no spaces', () => {
|
||||||
|
type actual = HasSpaces<'helloworld'>;
|
||||||
|
type expected = false;
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
9
packages/stdlib/src/types/ts/string.ts
Normal file
9
packages/stdlib/src/types/ts/string.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Trim leading and trailing whitespace from `S`
|
||||||
|
*/
|
||||||
|
export type Trim<S extends string> = S extends ` ${infer R}` ? Trim<R> : S extends `${infer L} ` ? Trim<L> : S;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if `S` has any spaces
|
||||||
|
*/
|
||||||
|
export type HasSpaces<S extends string> = S extends `${string} ${string}` ? true : false;
|
||||||
@@ -31,7 +31,7 @@ pnpm install -D @robonen/tsconfig
|
|||||||
"resolveJsonModule": true, // разрешить импортировать файлы JSON
|
"resolveJsonModule": true, // разрешить импортировать файлы JSON
|
||||||
"moduleDetection": "force", // заставляет TypeScript рассматривать все файлы как модули. Это помогает избежать ошибок cannot redeclare block-scoped variable»
|
"moduleDetection": "force", // заставляет TypeScript рассматривать все файлы как модули. Это помогает избежать ошибок cannot redeclare block-scoped variable»
|
||||||
"isolatedModules": true, // орабатывать каждый файл, как отдельный изолированный модуль
|
"isolatedModules": true, // орабатывать каждый файл, как отдельный изолированный модуль
|
||||||
"removeComments": true, // удалять комментарии из исходного кода
|
"removeComments": false, // удалять комментарии из исходного кода
|
||||||
"verbatimModuleSyntax": true, // сохранять синтаксис модулей в исходном коде (важно при импорте типов)
|
"verbatimModuleSyntax": true, // сохранять синтаксис модулей в исходном коде (важно при импорте типов)
|
||||||
"useDefineForClassFields": true, // использование классов стандарта TC39, а не TypeScript
|
"useDefineForClassFields": true, // использование классов стандарта TC39, а не TypeScript
|
||||||
"strict": true, // включить все строгие проверки (noImplicitAny, noImplicitThis, alwaysStrict, strictNullChecks, strictFunctionTypes, strictPropertyInitialization)
|
"strict": true, // включить все строгие проверки (noImplicitAny, noImplicitThis, alwaysStrict, strictNullChecks, strictFunctionTypes, strictPropertyInitialization)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"removeComments": true,
|
"removeComments": false,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
|
|
||||||
|
|||||||
1
packages/vue/README.md
Normal file
1
packages/vue/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# @robonen/vue
|
||||||
5
packages/vue/jsr.json
Normal file
5
packages/vue/jsr.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "@robonen/vue",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"exports": "./src/index.ts"
|
||||||
|
}
|
||||||
46
packages/vue/package.json
Normal file
46
packages/vue/package.json
Normal 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:"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
packages/vue/src/composables/index.ts
Normal file
5
packages/vue/src/composables/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './useCached';
|
||||||
|
export * from './useCounter';
|
||||||
|
export * from './useRenderCount';
|
||||||
|
export * from './useSyncRefs';
|
||||||
|
export * from './useToggle';
|
||||||
44
packages/vue/src/composables/useCached/index.test.ts
Normal file
44
packages/vue/src/composables/useCached/index.test.ts
Normal 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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
36
packages/vue/src/composables/useCached/index.ts
Normal file
36
packages/vue/src/composables/useCached/index.ts
Normal 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;
|
||||||
|
}
|
||||||
8
packages/vue/src/composables/useCounter/demo.vue
Normal file
8
packages/vue/src/composables/useCounter/demo.vue
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
76
packages/vue/src/composables/useCounter/index.test.ts
Normal file
76
packages/vue/src/composables/useCounter/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
71
packages/vue/src/composables/useCounter/index.ts
Normal file
71
packages/vue/src/composables/useCounter/index.ts
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
75
packages/vue/src/composables/useRenderCount/index.test.ts
Normal file
75
packages/vue/src/composables/useRenderCount/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
25
packages/vue/src/composables/useRenderCount/index.ts
Normal file
25
packages/vue/src/composables/useRenderCount/index.ts
Normal 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);
|
||||||
|
}
|
||||||
39
packages/vue/src/composables/useSyncRefs/index.test.ts
Normal file
39
packages/vue/src/composables/useSyncRefs/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
44
packages/vue/src/composables/useSyncRefs/index.ts
Normal file
44
packages/vue/src/composables/useSyncRefs/index.ts
Normal 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 },
|
||||||
|
);
|
||||||
|
}
|
||||||
49
packages/vue/src/composables/useToggle/index.ts
Normal file
49
packages/vue/src/composables/useToggle/index.ts
Normal 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];
|
||||||
|
}
|
||||||
2
packages/vue/src/index.ts
Normal file
2
packages/vue/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './composables';
|
||||||
|
export * from './utils';
|
||||||
5
packages/vue/src/utils/components.ts
Normal file
5
packages/vue/src/utils/components.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { getCurrentInstance, type ComponentInternalInstance } from 'vue';
|
||||||
|
|
||||||
|
export function getLifeCycleTarger(target?: ComponentInternalInstance) {
|
||||||
|
return target || getCurrentInstance();
|
||||||
|
}
|
||||||
1
packages/vue/src/utils/index.ts
Normal file
1
packages/vue/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './components';
|
||||||
3
packages/vue/tsconfig.json
Normal file
3
packages/vue/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "@robonen/tsconfig/tsconfig.json"
|
||||||
|
}
|
||||||
7
packages/vue/vitest.config.ts
Normal file
7
packages/vue/vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
},
|
||||||
|
});
|
||||||
2439
pnpm-lock.yaml
generated
2439
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,10 @@
|
|||||||
packages:
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user