@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/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
@@ -0,0 +1,7 @@
1
+ # @oviirup/utils ![](https://img.shields.io/badge/WIP-gold)
2
+
3
+ Collection of common JavaScript / TypeScript utilities by [@oviirup](https://github.com/oviirup).
4
+
5
+ - Type-safe utilities for everyday coding
6
+ - Zero dependencies
7
+ - Thoroughly tested
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
@@ -0,0 +1,6 @@
1
+ export * from "./array";
2
+ export * from "./assertions";
3
+ export * from "./nanoid";
4
+ export * from "./number";
5
+ export * from "./object";
6
+ export * from "./string";
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
+ });