@oviirup/utils 0.0.1-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +17 -0
- package/.gitattributes +6 -0
- package/.github/codeowners +1 -0
- package/.github/workflows/ci.yaml +26 -0
- package/.husky/post-checkout +3 -0
- package/.husky/pre-commit +2 -0
- package/.vscode/settings.json +9 -0
- package/biome.json +37 -0
- package/bun.lock +428 -0
- package/license +21 -0
- package/package.json +66 -0
- package/readme.md +7 -0
- package/src/__equal +0 -0
- package/src/__promise +0 -0
- package/src/array.ts +122 -0
- package/src/assertions.ts +70 -0
- package/src/index.ts +6 -0
- package/src/nanoid.ts +28 -0
- package/src/number.ts +59 -0
- package/src/object.ts +45 -0
- package/src/string.ts +34 -0
- package/tests/array.test.ts +22 -0
- package/tests/nanoid.test.ts +82 -0
- package/tests/number.test.ts +70 -0
- package/tests/string.test.ts +43 -0
- package/tsconfig.json +15 -0
package/license
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Avirup Ghosh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oviirup/utils",
|
|
3
|
+
"version": "0.0.1-canary.0",
|
|
4
|
+
"description": "Collection of common JavaScript / TypeScript utilities bt @oviirup",
|
|
5
|
+
"repository": "oviirup/utils",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Avirup Ghosh (oviirup)",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./array": {
|
|
15
|
+
"types": "./dist/array.d.ts",
|
|
16
|
+
"import": "./dist/array.mjs",
|
|
17
|
+
"require": "./dist/array.js"
|
|
18
|
+
},
|
|
19
|
+
"./assertions": {
|
|
20
|
+
"types": "./dist/assertions.d.ts",
|
|
21
|
+
"import": "./dist/assertions.mjs",
|
|
22
|
+
"require": "./dist/assertions.js"
|
|
23
|
+
},
|
|
24
|
+
"./nanoid": {
|
|
25
|
+
"types": "./dist/nanoid.d.ts",
|
|
26
|
+
"import": "./dist/nanoid.mjs",
|
|
27
|
+
"require": "./dist/nanoid.js"
|
|
28
|
+
},
|
|
29
|
+
"./number": {
|
|
30
|
+
"types": "./dist/number.d.ts",
|
|
31
|
+
"import": "./dist/number.mjs",
|
|
32
|
+
"require": "./dist/number.js"
|
|
33
|
+
},
|
|
34
|
+
"./object": {
|
|
35
|
+
"types": "./dist/object.d.ts",
|
|
36
|
+
"import": "./dist/object.mjs",
|
|
37
|
+
"require": "./dist/object.js"
|
|
38
|
+
},
|
|
39
|
+
"./string": {
|
|
40
|
+
"types": "./dist/string.d.ts",
|
|
41
|
+
"import": "./dist/string.mjs",
|
|
42
|
+
"require": "./dist/string.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "bunchee --minify --no-sourcemap",
|
|
47
|
+
"format": "biome format --write",
|
|
48
|
+
"lint": "biome lint",
|
|
49
|
+
"prepare": "husky",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"typecheck": "tsc --noEmit"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@biomejs/biome": "^2",
|
|
55
|
+
"@types/bun": "^1",
|
|
56
|
+
"@types/node": "^24",
|
|
57
|
+
"bunchee": "^6",
|
|
58
|
+
"husky": "^9",
|
|
59
|
+
"lint-staged": "^16",
|
|
60
|
+
"typescript": "^5",
|
|
61
|
+
"vitest": "^4"
|
|
62
|
+
},
|
|
63
|
+
"lint-staged": {
|
|
64
|
+
"*.{js,mjs,ts,json,md}": "biome format --write"
|
|
65
|
+
}
|
|
66
|
+
}
|
package/readme.md
ADDED
package/src/__equal
ADDED
|
File without changes
|
package/src/__promise
ADDED
|
File without changes
|
package/src/array.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { isEmptyArray } from "./assertions";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts a given value to represent itself in an array
|
|
5
|
+
* @param value The value to convert to an array
|
|
6
|
+
* @category Array
|
|
7
|
+
*/
|
|
8
|
+
export function toArray<T>(array: T | T[]): T[] {
|
|
9
|
+
array = array ?? [];
|
|
10
|
+
return Array.isArray(array) ? array : [array];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Matcher<T> = (left: T, right: T) => boolean;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create an array with all unique items
|
|
17
|
+
* @param value The array to make unique
|
|
18
|
+
* @param equals The matcher function to use to determine if two items are the same
|
|
19
|
+
* @category Array
|
|
20
|
+
*/
|
|
21
|
+
export function unique<T>(value: T[]): T[];
|
|
22
|
+
export function unique<T>(value: T[], equals: Matcher<T>): T[];
|
|
23
|
+
export function unique<T>(value: T[], equals?: Matcher<T>): T[] {
|
|
24
|
+
if (typeof equals !== "function") {
|
|
25
|
+
return Array.from(new Set(value));
|
|
26
|
+
}
|
|
27
|
+
return value.reduce<T[]>((acc, item) => {
|
|
28
|
+
const index = acc.findIndex((e) => equals(e, item));
|
|
29
|
+
if (index === -1) acc.push(item);
|
|
30
|
+
return acc;
|
|
31
|
+
}, []);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get nth item of Array. Negative for backward
|
|
36
|
+
* @param array The array to get the item from
|
|
37
|
+
* @param index The index of the item to get.
|
|
38
|
+
* @category Array
|
|
39
|
+
*/
|
|
40
|
+
export function at(array: readonly [], index: number): undefined;
|
|
41
|
+
export function at<T>(array: readonly T[], index: number): T;
|
|
42
|
+
export function at<T>(array: readonly T[] | [], index: number): T | undefined {
|
|
43
|
+
const len = array.length;
|
|
44
|
+
if (!len) return undefined;
|
|
45
|
+
if (index < 0) index += len;
|
|
46
|
+
return array[index];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get last item
|
|
51
|
+
* @category Array
|
|
52
|
+
*/
|
|
53
|
+
export function last(array: readonly []): undefined;
|
|
54
|
+
export function last<T>(array: readonly T[]): T;
|
|
55
|
+
export function last<T>(array: readonly T[]): T | undefined {
|
|
56
|
+
return at(array, -1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get first item
|
|
61
|
+
* @category Array
|
|
62
|
+
*/
|
|
63
|
+
export function first(array: readonly []): undefined;
|
|
64
|
+
export function first<T>(array: readonly T[]): T;
|
|
65
|
+
export function first<T>(array: readonly T[]): T | undefined {
|
|
66
|
+
return at(array, 0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generate a range array of numbers.
|
|
71
|
+
* @category Array
|
|
72
|
+
*/
|
|
73
|
+
export function range(stop: number): number[];
|
|
74
|
+
export function range(start: number, stop: number, step?: number): number[];
|
|
75
|
+
export function range(...args: any): number[] {
|
|
76
|
+
let start = 0;
|
|
77
|
+
let stop: number;
|
|
78
|
+
let step = 1;
|
|
79
|
+
if (args.length === 1) {
|
|
80
|
+
[stop] = args;
|
|
81
|
+
} else {
|
|
82
|
+
[start, stop, step = 1] = args;
|
|
83
|
+
}
|
|
84
|
+
const array: number[] = [];
|
|
85
|
+
let current = start;
|
|
86
|
+
while (current < stop) {
|
|
87
|
+
array.push(current);
|
|
88
|
+
current += step;
|
|
89
|
+
}
|
|
90
|
+
return array;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type Predicate<T> = (item: T, index: number, array: T[]) => boolean;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Filter an array in place (faster than Array.filter)
|
|
97
|
+
* @param array The array to filter
|
|
98
|
+
* @param predicate The predicate function to use to filter the array
|
|
99
|
+
* @category Array
|
|
100
|
+
*/
|
|
101
|
+
export function toFiltered<T>(array: T[], predicate: Predicate<T>): T[] {
|
|
102
|
+
for (let i = array.length; i--; i >= 0) {
|
|
103
|
+
if (!predicate(array[i], i, array)) array.splice(i, 1);
|
|
104
|
+
}
|
|
105
|
+
return array;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Move an item in an array to a new position
|
|
110
|
+
* @param array The array to move the item in
|
|
111
|
+
* @param from The index of the item to move
|
|
112
|
+
* @param to The index to move the item to
|
|
113
|
+
* @category Array
|
|
114
|
+
*/
|
|
115
|
+
export function move<T>(array: T[], from: number, to: number): T[] {
|
|
116
|
+
if (isEmptyArray(array)) return array;
|
|
117
|
+
if (from === to) return array;
|
|
118
|
+
const item = array[from];
|
|
119
|
+
array.splice(from, 1);
|
|
120
|
+
array.splice(to, 0, item);
|
|
121
|
+
return array;
|
|
122
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
type Dict<T = unknown> = Record<string, T>;
|
|
2
|
+
|
|
3
|
+
/** Check if given value is a string */
|
|
4
|
+
export function isString(val: unknown): val is string {
|
|
5
|
+
return typeof val === "string";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Check if the given value is a number */
|
|
9
|
+
export function isNumber(val: unknown): val is number {
|
|
10
|
+
return typeof val === "number" && !Number.isNaN(val);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Check if the given value is an integer */
|
|
14
|
+
export function isInteger(val: unknown): val is number {
|
|
15
|
+
return isNumber(val) && Number.isInteger(val);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Check if the given value is a float */
|
|
19
|
+
export function isFloat(val: unknown): val is number {
|
|
20
|
+
return isNumber(val) && !Number.isInteger(val);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Check if given value is an array */
|
|
24
|
+
export function isArray<T = any>(value: unknown): value is T[] {
|
|
25
|
+
return Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Check if given array is empty */
|
|
29
|
+
export function isEmptyArray(value: any[]): boolean {
|
|
30
|
+
return isArray(value) && value.length === 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Check if given value is an object */
|
|
34
|
+
export function isObject<T extends object = Dict>(val: unknown): val is T {
|
|
35
|
+
return val !== null && typeof val === "object" && !Array.isArray(val);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Check if given object is empty */
|
|
39
|
+
export function isEmptyObject<T extends object = Dict>(val: T): boolean {
|
|
40
|
+
return isObject(val) && Object.keys(val).length === 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Check if the given value is empty, null, undefined, or a string with no content */
|
|
44
|
+
export function isEmpty(val: unknown): boolean {
|
|
45
|
+
if (val === null || val === undefined) return true;
|
|
46
|
+
if (typeof val === "string" && val.trim() === "") return true;
|
|
47
|
+
if (isArray(val)) return isEmptyArray(val);
|
|
48
|
+
if (isObject(val)) return isEmptyObject(val);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type Func<T> = (...args: any[]) => T;
|
|
53
|
+
/** Check if the given object is a function */
|
|
54
|
+
export function isFunction<T = unknown>(val: unknown): val is Func<T> {
|
|
55
|
+
return typeof val === "function";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Check if the given value is a regex */
|
|
59
|
+
export function isRegex(value: unknown): value is RegExp {
|
|
60
|
+
return value instanceof RegExp;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Check if the given value is truthy */
|
|
64
|
+
export function isTruthy(val: unknown): boolean {
|
|
65
|
+
return !!val;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function isBrowser(): boolean {
|
|
69
|
+
return typeof window !== "undefined";
|
|
70
|
+
}
|
package/src/index.ts
ADDED
package/src/nanoid.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const charset = `abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789`;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate a secure nanoid string using Node.js crypto module.
|
|
7
|
+
* @param length - Length of the ID (default: 21)
|
|
8
|
+
* @param alphabets - Alphabet to use for the ID
|
|
9
|
+
* @returns A cryptographically secure unique ID string
|
|
10
|
+
* @source https://github.com/ai/nanoid
|
|
11
|
+
*/
|
|
12
|
+
export function nanoid(length = 21, alphabets = charset): string {
|
|
13
|
+
const size = alphabets.length;
|
|
14
|
+
if (size === 0 || size > 255) {
|
|
15
|
+
throw new Error("Alphabet must contain less than 255 characters");
|
|
16
|
+
}
|
|
17
|
+
const mask = (2 << Math.floor(Math.log2(size - 1))) - 1;
|
|
18
|
+
const step = Math.ceil((1.6 * mask * length) / size);
|
|
19
|
+
let id = "";
|
|
20
|
+
while (id.length < length) {
|
|
21
|
+
const bytes = randomBytes(step);
|
|
22
|
+
for (let i = 0; i < step && id.length < length; i++) {
|
|
23
|
+
const index = bytes[i] & mask;
|
|
24
|
+
if (index < size) id += alphabets[index];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return id;
|
|
28
|
+
}
|
package/src/number.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { isNumber, isObject } from "./assertions";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Clamps a number between a minimum and maximum value
|
|
5
|
+
* @category Number
|
|
6
|
+
*/
|
|
7
|
+
export function clamp(val: number, min: number, max: number): number {
|
|
8
|
+
return Math.min(Math.max(val, min), max);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const baseAbbreviationSymbols = ["", "K", "M", "B", "T"];
|
|
12
|
+
|
|
13
|
+
export type AbbreviationSymbols = Record<string, number> | string[];
|
|
14
|
+
export type AbbreviateOptions = {
|
|
15
|
+
symbols?: AbbreviationSymbols;
|
|
16
|
+
precision?: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Abbreviates a number to a string with a symbol and a precision
|
|
21
|
+
* @param value - The number to abbreviate
|
|
22
|
+
* @param arg - The precision or options to use for the abbreviation
|
|
23
|
+
* @category Number
|
|
24
|
+
*/
|
|
25
|
+
export function abbreviate(value: number, precision?: number): string;
|
|
26
|
+
export function abbreviate(value: number, options?: AbbreviateOptions): string;
|
|
27
|
+
export function abbreviate(
|
|
28
|
+
value: number,
|
|
29
|
+
arg?: number | AbbreviateOptions | undefined,
|
|
30
|
+
): string {
|
|
31
|
+
if (!isNumber(value)) return "0";
|
|
32
|
+
// parse arguments
|
|
33
|
+
let precision = 1;
|
|
34
|
+
let symbols: AbbreviationSymbols = baseAbbreviationSymbols;
|
|
35
|
+
if (typeof arg === "number") {
|
|
36
|
+
precision = arg;
|
|
37
|
+
} else if (typeof arg === "object") {
|
|
38
|
+
precision = arg.precision ?? 1;
|
|
39
|
+
symbols = arg.symbols ?? baseAbbreviationSymbols;
|
|
40
|
+
}
|
|
41
|
+
// get threshold symbols as tuples
|
|
42
|
+
const thresholds: [string, number][] = isObject(symbols)
|
|
43
|
+
? Object.entries(symbols)
|
|
44
|
+
: symbols.map((symbol, i) => [symbol, 10 ** (i * 3)]);
|
|
45
|
+
// sort thresholds from largest to smallest
|
|
46
|
+
thresholds.sort((a, b) => b[1] - a[1]);
|
|
47
|
+
// handle negative values
|
|
48
|
+
const sign = value < 0 ? "-" : "";
|
|
49
|
+
const abs = Math.abs(value);
|
|
50
|
+
// find the appropriate symbol
|
|
51
|
+
for (const [symbol, threshold] of thresholds) {
|
|
52
|
+
const frac = (abs / threshold).toFixed(precision);
|
|
53
|
+
if (Number(frac) < 1) continue;
|
|
54
|
+
return `${sign}${frac}${symbol}`;
|
|
55
|
+
}
|
|
56
|
+
// if the number is an integer, return it as is
|
|
57
|
+
if (Math.floor(abs) === abs) return value.toString();
|
|
58
|
+
return `${sign}${abs.toFixed(precision)}`;
|
|
59
|
+
}
|
package/src/object.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { toArray } from "./array";
|
|
2
|
+
import { isObject } from "./assertions";
|
|
3
|
+
|
|
4
|
+
type Dict<T = unknown> = Record<string, T>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Checks if a given object has a specified key
|
|
8
|
+
* @category Object
|
|
9
|
+
*/
|
|
10
|
+
export function keyInObject<T extends object = Dict>(
|
|
11
|
+
val: T,
|
|
12
|
+
key: keyof T | (string & {}),
|
|
13
|
+
): boolean {
|
|
14
|
+
return isObject(val) && key in val;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Picks a set of keys from an object
|
|
19
|
+
* @category Object
|
|
20
|
+
*/
|
|
21
|
+
export function pick<T extends object, K extends keyof T = keyof T>(
|
|
22
|
+
input: T,
|
|
23
|
+
keys: K | K[],
|
|
24
|
+
): Pick<T, K> {
|
|
25
|
+
const picked = {} as Pick<T, K>;
|
|
26
|
+
for (const key of toArray(keys)) {
|
|
27
|
+
if (key in input) picked[key] = input[key];
|
|
28
|
+
}
|
|
29
|
+
return picked;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Omits a set of keys from an object
|
|
34
|
+
* @category Object
|
|
35
|
+
*/
|
|
36
|
+
export function omit<T extends object, K extends keyof T = keyof T>(
|
|
37
|
+
input: T,
|
|
38
|
+
keys: K | K[],
|
|
39
|
+
): Omit<T, K> {
|
|
40
|
+
const partial = { ...input } as T;
|
|
41
|
+
for (const key of toArray(keys)) {
|
|
42
|
+
if (key in input) delete partial[key];
|
|
43
|
+
}
|
|
44
|
+
return partial as Omit<T, K>;
|
|
45
|
+
}
|
package/src/string.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replace backslash to slash
|
|
3
|
+
* @category String
|
|
4
|
+
*/
|
|
5
|
+
export function slash(str: string) {
|
|
6
|
+
return str.replace(/\\/g, "/");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const DEFAULT_TRUNCATE_LENGTH = 80;
|
|
10
|
+
/**
|
|
11
|
+
* Truncates a string to the specified length, adding "..." if it was longer.
|
|
12
|
+
* @param text The string to truncate
|
|
13
|
+
* @param length Maximum allowed length before truncation
|
|
14
|
+
* @category String
|
|
15
|
+
*/
|
|
16
|
+
export function truncate(
|
|
17
|
+
input: string,
|
|
18
|
+
length = DEFAULT_TRUNCATE_LENGTH,
|
|
19
|
+
): string {
|
|
20
|
+
if (!input) return input;
|
|
21
|
+
const text = input.trim();
|
|
22
|
+
const maxLength = Math.max(3, length);
|
|
23
|
+
if (text.length <= maxLength) return text;
|
|
24
|
+
return `${text.slice(0, maxLength - 3)}...`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Capitalizes the first letter of a string
|
|
29
|
+
* @param input The string to capitalize
|
|
30
|
+
* @category String
|
|
31
|
+
*/
|
|
32
|
+
export function capitalize(input: string): string {
|
|
33
|
+
return input.charAt(0).toUpperCase() + input.slice(1);
|
|
34
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it, test } from "vitest";
|
|
2
|
+
import * as array from "../src/array";
|
|
3
|
+
|
|
4
|
+
describe("toArray", () => {
|
|
5
|
+
it.each([
|
|
6
|
+
{ input: 1, expected: [1] },
|
|
7
|
+
{ input: [1, 2, 3], expected: [1, 2, 3] },
|
|
8
|
+
{ input: null, expected: [] },
|
|
9
|
+
{ input: undefined, expected: [] },
|
|
10
|
+
{ input: "foo", expected: ["foo"] },
|
|
11
|
+
{ input: ["foo"], expected: ["foo"] },
|
|
12
|
+
])("should convert $input to $expected", ({ input, expected }) => {
|
|
13
|
+
expect(array.toArray(input)).toEqual(expected);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("range", () => {
|
|
18
|
+
expect(array.range(0)).toEqual([]);
|
|
19
|
+
expect(array.range(5)).toEqual([0, 1, 2, 3, 4]);
|
|
20
|
+
expect(array.range(2, 5)).toEqual([2, 3, 4]);
|
|
21
|
+
expect(array.range(2, 10, 2)).toEqual([2, 4, 6, 8]);
|
|
22
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { charset, nanoid } from "../src/nanoid";
|
|
3
|
+
|
|
4
|
+
describe("nanoid", () => {
|
|
5
|
+
it("should be ready for 0 size", () => {
|
|
6
|
+
expect(nanoid(0)).toBe("");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("should generates URL-friendly IDs", () => {
|
|
10
|
+
const charset = `abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789`;
|
|
11
|
+
for (let i = 0; i < 100; i++) {
|
|
12
|
+
const id = nanoid();
|
|
13
|
+
expect(id.length).toBe(21);
|
|
14
|
+
expect(typeof id).toBe("string");
|
|
15
|
+
for (const char of id) expect(charset.includes(char)).toBe(true);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should generate IDs with with given length", () => {
|
|
20
|
+
expect(nanoid(10).length).toBe(10);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should not have any collisions", () => {
|
|
24
|
+
const ids = new Set<string>();
|
|
25
|
+
const iterations = 100_000;
|
|
26
|
+
for (let i = 0; i < iterations; i++) {
|
|
27
|
+
const id = nanoid();
|
|
28
|
+
expect(ids.has(id)).toBe(false);
|
|
29
|
+
ids.add(id);
|
|
30
|
+
}
|
|
31
|
+
expect(ids.size).toBe(iterations);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should generate IDs with with given alphabet", () => {
|
|
35
|
+
const alphabet = "0123456789abcdef";
|
|
36
|
+
const id = nanoid(21, alphabet);
|
|
37
|
+
expect(id.length).toBe(21);
|
|
38
|
+
for (const char of id) expect(alphabet.includes(char)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should avoid pool pollution, infinite loop", () => {
|
|
42
|
+
nanoid(2.1);
|
|
43
|
+
const second = nanoid();
|
|
44
|
+
const third = nanoid();
|
|
45
|
+
expect(second).not.toBe(third);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should have flat distribution", () => {
|
|
49
|
+
const length = 10;
|
|
50
|
+
const iterations = 100_000;
|
|
51
|
+
const charCounts: Record<string, number> = {};
|
|
52
|
+
// Initialize counts
|
|
53
|
+
for (const char of charset) {
|
|
54
|
+
charCounts[char] = 0;
|
|
55
|
+
}
|
|
56
|
+
// Generate IDs and count character occurrences
|
|
57
|
+
for (let i = 0; i < iterations; i++) {
|
|
58
|
+
const id = nanoid(length, charset);
|
|
59
|
+
for (const char of id) charCounts[char]++;
|
|
60
|
+
}
|
|
61
|
+
// Calculate expected frequency (each character should appear roughly equally)
|
|
62
|
+
const totalChars = iterations * length;
|
|
63
|
+
const expectedFrequency = totalChars / charset.length;
|
|
64
|
+
const tolerance = expectedFrequency * 0.1; // 10% tolerance
|
|
65
|
+
// Check that each character appears roughly equally
|
|
66
|
+
for (const char of charset) {
|
|
67
|
+
const actualFrequency = charCounts[char];
|
|
68
|
+
expect(actualFrequency).toBeGreaterThan(expectedFrequency - tolerance);
|
|
69
|
+
expect(actualFrequency).toBeLessThan(expectedFrequency + tolerance);
|
|
70
|
+
}
|
|
71
|
+
// Check that the distribution is uniform
|
|
72
|
+
let max = 0;
|
|
73
|
+
let min = Number.MAX_SAFE_INTEGER;
|
|
74
|
+
for (const char of charset) {
|
|
75
|
+
const observed = charCounts[char];
|
|
76
|
+
const dist = (observed * charset.length) / (iterations * length);
|
|
77
|
+
if (dist > max) max = dist;
|
|
78
|
+
if (dist < min) min = dist;
|
|
79
|
+
}
|
|
80
|
+
expect(max - min).toBeLessThan(0.05);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { AbbreviationSymbols, abbreviate } from "../src/number";
|
|
3
|
+
|
|
4
|
+
describe("abbreviate", () => {
|
|
5
|
+
it.each([
|
|
6
|
+
[1_742, "1.7K"],
|
|
7
|
+
[1_8364, "18.4K"],
|
|
8
|
+
[1_0701, "10.7K"],
|
|
9
|
+
[1_500_000, "1.5M"],
|
|
10
|
+
[9_876_543, "9.9M"],
|
|
11
|
+
[1_234_567_890, "1.2B"],
|
|
12
|
+
[5_876_543_210, "5.9B"],
|
|
13
|
+
[3_500_000_000_000, "3.5T"],
|
|
14
|
+
[7_654_321_987_654, "7.7T"],
|
|
15
|
+
])("positive values: %d > %s", (value, expected) => {
|
|
16
|
+
expect(abbreviate(value)).toBe(expected);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it.each([
|
|
20
|
+
[-2_345, "-2.3K"],
|
|
21
|
+
[-15_432, "-15.4K"],
|
|
22
|
+
[-43_600, "-43.6K"],
|
|
23
|
+
[-3_892_400, "-3.9M"],
|
|
24
|
+
[-24_187_543, "-24.2M"],
|
|
25
|
+
[-4_584_321_987, "-4.6B"],
|
|
26
|
+
[-98_000_000_101, "-98.0B"],
|
|
27
|
+
[-1_700_234_567_890, "-1.7T"],
|
|
28
|
+
[-3_190_000_000_000, "-3.2T"],
|
|
29
|
+
])("negative values: %d -> %s", (value, expected) => {
|
|
30
|
+
expect(abbreviate(value)).toBe(expected);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should handle custom precision", () => {
|
|
34
|
+
expect(abbreviate(1234, 0)).toBe("1K");
|
|
35
|
+
expect(abbreviate(1500, 2)).toBe("1.50K");
|
|
36
|
+
expect(abbreviate(9_876_543, 3)).toBe("9.877M");
|
|
37
|
+
expect(abbreviate(56_789, 2)).toBe("56.79K");
|
|
38
|
+
expect(abbreviate(-1_234_567, 0)).toBe("-1M");
|
|
39
|
+
expect(abbreviate(-2_345_678, 2)).toBe("-2.35M");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should handle custom precision options", () => {
|
|
43
|
+
expect(abbreviate(1234567, { precision: 3 })).toBe("1.235M");
|
|
44
|
+
expect(abbreviate(999500, { precision: 0 })).toBe("1M");
|
|
45
|
+
expect(abbreviate(-8765432, { precision: 2 })).toBe("-8.77M");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should handle custom symbols", () => {
|
|
49
|
+
const bytes_symbol: AbbreviationSymbols = {
|
|
50
|
+
B: 1,
|
|
51
|
+
KB: 1024,
|
|
52
|
+
MB: 1024 ** 2,
|
|
53
|
+
GB: 1024 ** 3,
|
|
54
|
+
TB: 1024 ** 4,
|
|
55
|
+
};
|
|
56
|
+
expect(abbreviate(1234567, { symbols: bytes_symbol })).toBe("1.2MB");
|
|
57
|
+
expect(abbreviate(999500, { symbols: bytes_symbol })).toBe("1.0MB");
|
|
58
|
+
expect(abbreviate(8765432, { symbols: bytes_symbol })).toBe("8.4MB");
|
|
59
|
+
|
|
60
|
+
const mass_symbols: AbbreviationSymbols = {
|
|
61
|
+
g: 1,
|
|
62
|
+
kg: 1e3,
|
|
63
|
+
ton: 1e6,
|
|
64
|
+
};
|
|
65
|
+
expect(abbreviate(500, { symbols: mass_symbols })).toBe("500.0g");
|
|
66
|
+
expect(abbreviate(1500, { symbols: mass_symbols })).toBe("1.5kg");
|
|
67
|
+
expect(abbreviate(1234567, { symbols: mass_symbols })).toBe("1.2ton");
|
|
68
|
+
expect(abbreviate(-1500, { symbols: mass_symbols })).toBe("-1.5kg");
|
|
69
|
+
});
|
|
70
|
+
});
|