├── .gitignore ├── src └── ts │ ├── math │ ├── types.ts │ ├── index.ts │ ├── fractions.test.ts │ ├── fractions.ts │ ├── conversions.ts │ ├── conversions.test.ts │ ├── gcf.test.ts │ ├── gcf.ts │ ├── triangles.test.ts │ ├── circle.test.ts │ ├── circle.ts │ └── triangles.ts │ ├── sleep.test.ts │ ├── promises.ts │ ├── rand.ts │ ├── sleep.ts │ ├── rand.test.ts │ ├── is-letter.ts │ ├── promises.test.ts │ ├── is-number.ts │ ├── is-number.test.ts │ ├── types.ts │ ├── is-letter.test.ts │ ├── truncate.test.ts │ ├── time.test.ts │ ├── truncate.ts │ ├── stopwatch.test.ts │ ├── perishable-list.test.ts │ ├── strings.test.ts │ ├── lines.test.ts │ ├── dispatcher.test.ts │ ├── lines.ts │ ├── time.ts │ ├── array.ts │ ├── perishable-list.ts │ ├── array.test.ts │ ├── matcher.test.ts │ ├── strings.ts │ ├── stopwatch.ts │ ├── matcher.ts │ ├── ipv4-address.test.ts │ ├── dispatcher.ts │ ├── pad.test.ts │ ├── ipv4-address.ts │ ├── pad.ts │ ├── url.test.ts │ ├── url.ts │ ├── casing.test.ts │ ├── casing.ts │ ├── result.test.ts │ └── result.ts ├── rules └── typescript-utility-functions.mdc ├── typedoc.json ├── tsconfig.json ├── .changeset └── config.json ├── vite.config.ts ├── .github └── workflows │ ├── ci.yml │ ├── coverage.yml │ ├── tests.yml │ ├── publish.yml │ └── gh-pages.yml ├── package.json ├── LICENSE ├── CHANGELOG.md ├── badges ├── coverage-lines.svg ├── coverage-total.svg ├── coverage-branches.svg ├── coverage-functions.svg └── coverage-statements.svg ├── biome.json ├── README.md ├── jsrepo.config.ts └── registry.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /src/ts/math/types.ts: -------------------------------------------------------------------------------- 1 | export type Point = { 2 | x: number; 3 | y: number; 4 | }; 5 | -------------------------------------------------------------------------------- /rules/typescript-utility-functions.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | Before writing a new TypeScript utility check if the @ieedan/std registry has it already and if it does use it instead. 7 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | // theme 3 | "plugin": ["typedoc-material-theme"], 4 | "themeColor": "#cb9820", 5 | 6 | "entryPointStrategy": "expand", 7 | "entryPoints": ["./src"], 8 | "out": "docs", 9 | "exclude": ["**/*test.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/ts/math/index.ts: -------------------------------------------------------------------------------- 1 | import * as circles from './circle'; 2 | import * as conversions from './conversions'; 3 | import * as fractions from './fractions'; 4 | import { gcd, gcf } from './gcf'; 5 | import * as triangles from './triangles'; 6 | export * from './types'; 7 | 8 | export { gcf, gcd, fractions, conversions, triangles, circles }; 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "isolatedModules": true, 6 | "moduleResolution": "Bundler", 7 | "module": "ES2022", 8 | "target": "ES2022", 9 | "skipLibCheck": true, 10 | "strict": true 11 | }, 12 | "include": ["src/**/*.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /src/ts/sleep.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest'; 2 | import { sleep } from './sleep'; 3 | 4 | test('Expect time elapsed', async () => { 5 | const start = Date.now(); 6 | 7 | const duration = 50; 8 | 9 | await sleep(duration); 10 | 11 | const end = Date.now(); 12 | 13 | expect(end - start + 10).toBeGreaterThanOrEqual(duration); 14 | }); 15 | -------------------------------------------------------------------------------- /src/ts/math/fractions.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import * as math from '.'; 3 | 4 | describe('simplify', () => { 5 | it('simplifies fractions correctly', () => { 6 | expect(math.fractions.simplify(1920, 1080)).toStrictEqual([16, 9]); 7 | // like in usage 8 | expect(math.fractions.simplify(1920, 1080).join(':')).toBe('16:9'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/ts/promises.ts: -------------------------------------------------------------------------------- 1 | /** Returns a promise that immediately resolves to `T`, useful when you need to mix sync and async code. 2 | * 3 | * @param val 4 | * 5 | * ### Usage 6 | * ```ts 7 | * const promises: Promise[] = []; 8 | * 9 | * promises.push(immediate(10)); 10 | * ``` 11 | */ 12 | export function immediate(val: T): Promise { 13 | return new Promise((res) => res(val)); 14 | } 15 | -------------------------------------------------------------------------------- /src/ts/rand.ts: -------------------------------------------------------------------------------- 1 | /** Generate a random number that lies between the provided min and max. 2 | * 3 | * @param min 4 | * @param max 5 | * @returns 6 | * 7 | * ## Usage 8 | * ```ts 9 | * const num = rand(0, 10); // 0 >= num <= 10 10 | * ``` 11 | */ 12 | export function rand(min: number, max: number): number { 13 | if (min > max) { 14 | throw new Error('Max should not be greater than min!'); 15 | } 16 | 17 | return Math.random() * (max - min) + min; 18 | } 19 | -------------------------------------------------------------------------------- /src/ts/math/fractions.ts: -------------------------------------------------------------------------------- 1 | import { gcf } from './gcf'; 2 | 3 | /** Simplifies the fraction 4 | * 5 | * @param numerator 6 | * @param denominator 7 | * @returns 8 | * 9 | * ## Usage 10 | * ```ts 11 | * simplify(1920, 1080).join(":"); // 16:9 12 | * ``` 13 | */ 14 | export function simplify(numerator: number, denominator: number): [number, number] { 15 | const factor = gcf(numerator, denominator); 16 | 17 | return [numerator / factor, denominator / factor]; 18 | } 19 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": ["@svitejs/changesets-changelog-github-compact", { "repo": "ieedan/std" }], 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "prettier": false, 9 | "baseBranch": "main", 10 | "updateInternalDependencies": "patch", 11 | "ignore": [], 12 | "privatePackages": { 13 | "version": true, 14 | "tag": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ts/sleep.ts: -------------------------------------------------------------------------------- 1 | /** Await this to pause execution until the duration has passed. 2 | * 3 | * @param durationMs The duration in ms until the sleep in over 4 | * @returns 5 | * 6 | * ## Usage 7 | * ```ts 8 | * console.log(Date.now()) // 1725739228744 9 | * 10 | * await sleep(1000); 11 | * 12 | * console.log(Date.now()) // 1725739229744 13 | * ``` 14 | */ 15 | export function sleep(durationMs: number): Promise { 16 | return new Promise((res) => setTimeout(res, durationMs)); 17 | } 18 | -------------------------------------------------------------------------------- /src/ts/rand.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { rand } from './rand'; 3 | 4 | describe('rand', () => { 5 | it('Generates a random number between the given range', () => { 6 | for (let i = 0; i <= 10; i++) { 7 | const num = rand(0, 10); 8 | 9 | expect(num).toBeGreaterThanOrEqual(0); 10 | expect(num).toBeLessThanOrEqual(10); 11 | } 12 | }); 13 | 14 | it('Throws when max is greater than min', () => { 15 | expect(() => rand(10, 0)).toThrow(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/ts/is-letter.ts: -------------------------------------------------------------------------------- 1 | export const LETTER_REGEX = new RegExp(/[a-zA-Z]/); 2 | 3 | /** Checks if the provided character is a letter in the alphabet. 4 | * 5 | * @param char 6 | * @returns 7 | * 8 | * ## Usage 9 | * ```ts 10 | * isLetter('a'); 11 | * ``` 12 | */ 13 | export function isLetter(char: string): boolean { 14 | if (char.length > 1) { 15 | throw new Error( 16 | `You probably only meant to pass a character to this function. Instead you gave ${char}` 17 | ); 18 | } 19 | 20 | return LETTER_REGEX.test(char); 21 | } 22 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | all: true, 7 | reportOnFailure: true, 8 | provider: 'v8', 9 | reporter: ['json-summary', 'text'], 10 | thresholds: { 11 | lines: 100, 12 | functions: 100, 13 | branches: 100, 14 | statements: 100, 15 | }, 16 | exclude: [ 17 | 'node_modules/', 18 | '**/*.d.ts', 19 | '**/*.test.ts', 20 | 'vite.config.ts', 21 | 'jsrepo.config.ts', 22 | ], 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /src/ts/math/conversions.ts: -------------------------------------------------------------------------------- 1 | /** Converts degrees to radians 2 | * 3 | * @param degrees 4 | * @returns 5 | * 6 | * ## Usage 7 | * ```ts 8 | * dtr(180); // 3.14159268358979 9 | * ``` 10 | */ 11 | export function dtr(degrees: number): number { 12 | return degrees * (Math.PI / 180); 13 | } 14 | 15 | /** Converts radians to degrees 16 | * 17 | * @param radians 18 | * @returns 19 | * 20 | * ## Usage 21 | * ```ts 22 | * rtd(Math.PI); // 180 23 | * ``` 24 | */ 25 | export function rtd(radians: number): number { 26 | return radians * (180 / Math.PI); 27 | } 28 | -------------------------------------------------------------------------------- /src/ts/promises.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 2 | import * as promises from './promises'; 3 | 4 | describe('noopPromise', () => { 5 | beforeEach(() => { 6 | vi.useFakeTimers(); 7 | }); 8 | 9 | afterEach(() => { 10 | vi.useRealTimers(); 11 | }); 12 | 13 | it('Returns a promise that resolves immediately', async () => { 14 | const promise = promises.immediate(42); 15 | 16 | const start = Date.now(); 17 | 18 | await promise; 19 | 20 | const end = Date.now(); 21 | 22 | expect(end - start).toBe(0); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/ts/is-number.ts: -------------------------------------------------------------------------------- 1 | /** Checks if provided value is actually a number. 2 | * 3 | * @param num value to check 4 | * @returns 5 | * 6 | * ## Usage 7 | * 8 | * ```ts 9 | * isNumber("2"); // true 10 | * isNumber("1.11"); // true 11 | * isNumber("0xff"); // true 12 | * 13 | * isNumber("two"); // false 14 | * isNumber({ two: 2 }); // false 15 | * isNumber(Number.POSITIVE_INFINITY); // false 16 | * ``` 17 | */ 18 | export function isNumber(num: unknown): boolean { 19 | if (typeof num === 'number') return num - num === 0; 20 | 21 | if (typeof num === 'string' && num.trim() !== '') return Number.isFinite(+num); 22 | 23 | return false; 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: [next] 6 | 7 | jobs: 8 | CI: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: pnpm/action-setup@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: "20" 16 | 17 | - name: Install dependencies 18 | run: pnpm install 19 | 20 | - name: Check Types 21 | run: pnpm check:types 22 | 23 | - name: Lint 24 | run: pnpm check 25 | 26 | - name: Build 27 | run: pnpm build:registry 28 | -------------------------------------------------------------------------------- /src/ts/math/conversions.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import * as math from '.'; 3 | 4 | describe('dtr', () => { 5 | it('Correctly converts degrees to radians', () => { 6 | expect(math.conversions.dtr(90)).toBe(Math.PI / 2); 7 | expect(math.conversions.dtr(180)).toBe(Math.PI); 8 | expect(math.conversions.dtr(270)).toBe(Math.PI * 1.5); 9 | expect(math.conversions.dtr(360)).toBe(Math.PI * 2); 10 | }); 11 | }); 12 | 13 | describe('rtd', () => { 14 | it('Correctly converts radians to degrees', () => { 15 | expect(math.conversions.rtd(Math.PI / 2)).toBe(90); 16 | expect(math.conversions.rtd(Math.PI)).toBe(180); 17 | expect(math.conversions.rtd(Math.PI * 1.5)).toBe(270); 18 | expect(math.conversions.rtd(Math.PI * 2)).toBe(360); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | - uses: pnpm/action-setup@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: "20" 19 | 20 | - name: Install dependencies 21 | run: pnpm install 22 | 23 | - name: Run tests with coverage 24 | run: pnpm coverage 25 | 26 | - name: Coverage Badges 27 | uses: jpb06/coverage-badges-action@latest 28 | with: 29 | branches: main 30 | badges-icon: vitest 31 | -------------------------------------------------------------------------------- /src/ts/is-number.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { isNumber } from './is-number'; 3 | 4 | describe('isNumber', () => { 5 | it('Returns true for numbers', () => { 6 | expect(isNumber('1')).toBe(true); 7 | expect(isNumber('1.11')).toBe(true); 8 | expect(isNumber('-1')).toBe(true); 9 | expect(isNumber('0xff')).toBe(true); 10 | expect(isNumber(1)).toBe(true); 11 | expect(isNumber(1.11)).toBe(true); 12 | expect(isNumber(-1)).toBe(true); 13 | expect(isNumber(0xff)).toBe(true); 14 | }); 15 | 16 | it('Returns false for non-numbers', () => { 17 | expect(isNumber('one')).toBe(false); 18 | expect(isNumber('10 * 10')).toBe(false); 19 | expect(isNumber('test')).toBe(false); 20 | expect(isNumber({})).toBe(false); 21 | expect(isNumber({ two: 2 })).toBe(false); 22 | expect(isNumber(Number.POSITIVE_INFINITY)).toBe(false); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/ts/math/gcf.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import * as math from '.'; 3 | 4 | describe('gcf', () => { 5 | it('solves GCF correctly', () => { 6 | expect(math.gcf(1920, 1080)).toBe(120); 7 | expect(math.gcf(-1920, 1080)).toBe(120); 8 | expect(math.gcf(1080, 1920)).toBe(120); 9 | expect(math.gcf(2, 2)).toBe(2); 10 | }); 11 | 12 | it('throws an error when one of the numbers is 0', () => { 13 | expect(() => math.gcf(0, 100)).toThrow(); 14 | }); 15 | }); 16 | 17 | describe('gcd', () => { 18 | it('solves GCD correctly', () => { 19 | expect(math.gcd(1920, 1080)).toBe(120); 20 | expect(math.gcd(-1920, 1080)).toBe(120); 21 | expect(math.gcd(1080, 1920)).toBe(120); 22 | expect(math.gcd(2, 2)).toBe(2); 23 | }); 24 | 25 | it('throws an error when one of the numbers is 0', () => { 26 | expect(() => math.gcd(0, 100)).toThrow(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/ts/types.ts: -------------------------------------------------------------------------------- 1 | /** Allows you to have autocomplete on a string while still accepting any string value. 2 | * 3 | * ## Usage 4 | * ```ts 5 | * type Fruits = LooseAutocomplete<'apple' | 'orange' | 'pear'>; 6 | * 7 | * // you will still get autocomplete here 8 | * let fruit: Fruits = 'apple'; // valid 9 | * fruit = 'banana'; // valid 10 | * ``` 11 | */ 12 | export type LooseAutocomplete = T | (string & {}); 13 | 14 | /** Flattens a complex object type down into a single object. 15 | * Super useful when using joins with `Omit` and other helpers without needing to reveal the base type. 16 | */ 17 | export type Prettify = { 18 | [K in keyof T]: T[K]; 19 | } & {}; 20 | 21 | declare const brand: unique symbol; 22 | 23 | /** Allows you to create a branded type. 24 | * 25 | * ## Usage 26 | * ```ts 27 | * type Milliseconds = Brand; 28 | * ``` 29 | */ 30 | export type Brand = T & { [brand]: Brand }; 31 | -------------------------------------------------------------------------------- /src/ts/is-letter.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { isLetter } from './is-letter'; 3 | 4 | const validLetters = [ 5 | 'a', 6 | 'b', 7 | 'c', 8 | 'd', 9 | 'e', 10 | 'f', 11 | 'g', 12 | 'h', 13 | 'i', 14 | 'j', 15 | 'k', 16 | 'l', 17 | 'm', 18 | 'n', 19 | 'o', 20 | 'p', 21 | 'q', 22 | 'r', 23 | 's', 24 | 't', 25 | 'u', 26 | 'v', 27 | 'w', 28 | 'x', 29 | 'y', 30 | 'z', 31 | ]; 32 | 33 | describe('isLetter', () => { 34 | it('correctly identifies letters', () => { 35 | for (const letter of validLetters) { 36 | expect(isLetter(letter)).toBe(true); 37 | expect(isLetter(letter.toUpperCase())).toBe(true); 38 | } 39 | }); 40 | 41 | it('correctly identifies non-letters', () => { 42 | expect(isLetter('1')).toBe(false); 43 | expect(isLetter('|')).toBe(false); 44 | expect(isLetter(']')).toBe(false); 45 | }); 46 | 47 | it('throws if given more than 1 character', () => { 48 | expect(() => isLetter('ab')).toThrow(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/ts/math/gcf.ts: -------------------------------------------------------------------------------- 1 | /** Solves the GCF (Greatest Common Factor) using the **Euclidean Algorithm** 2 | * 3 | * @param a 4 | * @param b 5 | * @returns 6 | * 7 | * ## Usage 8 | * ```ts 9 | * gcf(1920, 1080); // 120 10 | * gcf(2, 2); // 2 11 | * ``` 12 | */ 13 | export function gcf(a: number, b: number): number { 14 | if (a === 0 || b === 0) throw new Error('Cannot get the GCF of 0'); 15 | 16 | // if they are negative we really just want the same thing 17 | let num1: number = Math.abs(a); 18 | let num2: number = Math.abs(b); 19 | 20 | while (num1 !== num2) { 21 | if (num1 > num2) { 22 | num1 -= num2; 23 | } else { 24 | num2 -= num1; 25 | } 26 | } 27 | 28 | return num1; 29 | } 30 | 31 | /** Solves the GCD (Greatest Common Divisor) using the **Euclidean Algorithm** (Alternate alias of `gcf`) 32 | * 33 | * @param a 34 | * @param b 35 | * @returns 36 | * 37 | * ## Usage 38 | * ```ts 39 | * gcd(1920, 1080); // 120 40 | * gcd(2, 2); // 2 41 | * ``` 42 | */ 43 | export const gcd = gcf; 44 | -------------------------------------------------------------------------------- /src/ts/truncate.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { truncate } from './truncate'; 3 | 4 | describe('truncate', () => { 5 | it('Returns string when string length is less than max length', () => { 6 | const str = truncate('Hello World!', 100); 7 | 8 | expect(str).toBe('Hello World!'); 9 | }); 10 | 11 | it('Correctly truncates forward', () => { 12 | const str = truncate('Hello World!', 5); 13 | 14 | expect(str).toBe('Hello'); 15 | }); 16 | 17 | it('Correctly truncates reverse', () => { 18 | const str = truncate('Hello World!', 6, { reverse: true }); 19 | 20 | expect(str).toBe('World!'); 21 | }); 22 | 23 | it('Adds ending to the end of forward truncated string', () => { 24 | const str = truncate('Hello World!', 5, { ending: '...' }); 25 | 26 | expect(str).toBe('Hello...'); 27 | }); 28 | 29 | it('Adds ending to the start of a reverse truncated string', () => { 30 | const str = truncate('Hello World!', 6, { ending: '...', reverse: true }); 31 | 32 | expect(str).toBe('...World!'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "std", 3 | "version": "5.3.2", 4 | "packageManager": "pnpm@10.4.1", 5 | "type": "module", 6 | "scripts": { 7 | "test": "vitest", 8 | "coverage": "vitest run --coverage", 9 | "format": "biome format --write", 10 | "lint": "biome lint --write", 11 | "check": "biome check", 12 | "check:types": "tsc --noEmit", 13 | "docs:generate": "typedoc", 14 | "build:registry": "jsrepo build", 15 | "ci:release": "pnpm jsrepo publish --verbose && changeset tag", 16 | "changeset:version": "changeset version && pnpm format" 17 | }, 18 | "devDependencies": { 19 | "@biomejs/biome": "1.9.4", 20 | "@changesets/cli": "^2.29.7", 21 | "@svitejs/changesets-changelog-github-compact": "^1.2.0", 22 | "@types/node": "^22.19.0", 23 | "@vitest/coverage-v8": "^3.2.4", 24 | "jsrepo": "3.0.8", 25 | "typedoc": "^0.28.14", 26 | "typescript": "^5.9.3", 27 | "vitest": "^3.2.4" 28 | }, 29 | "dependencies": { 30 | "typedoc-material-theme": "^1.4.1" 31 | }, 32 | "pnpm": { 33 | "onlyBuiltDependencies": ["@biomejs/biome", "esbuild"] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: "Test" 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | permissions: 10 | # Required to checkout the code 11 | contents: read 12 | # Required to put a comment into the pull-request 13 | pull-requests: write 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - uses: pnpm/action-setup@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: "20" 23 | 24 | - name: Install Deps 25 | run: pnpm install 26 | 27 | - name: Test 28 | run: pnpm vitest --coverage.enabled true 29 | 30 | - name: Report Coverage 31 | # Set if: always() to also generate the report if tests are failing 32 | # Only works if you set `reportOnFailure: true` in your vite config as specified above 33 | if: always() 34 | uses: davelosert/vitest-coverage-report-action@v2 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Aidan Bleser 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. 22 | -------------------------------------------------------------------------------- /src/ts/time.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { type Milliseconds, formatDuration, milliseconds } from './time'; 3 | 4 | describe('formatDuration', () => { 5 | it('correctly formats durations', () => { 6 | expect(formatDuration(500 as Milliseconds)).toBe('500ms'); 7 | expect(formatDuration(milliseconds.SECOND)).toBe('1s'); 8 | expect(formatDuration(milliseconds.MINUTE)).toBe('1min'); 9 | expect(formatDuration(milliseconds.HOUR)).toBe('1h'); 10 | expect(formatDuration(milliseconds.DAY)).toBe('24h'); 11 | }); 12 | 13 | it('correctly handles transform function', () => { 14 | expect( 15 | formatDuration((milliseconds.SECOND / 55) as Milliseconds, (num) => num.toFixed(2)) 16 | ).toBe('18.18ms'); 17 | expect( 18 | formatDuration((milliseconds.MINUTE / 55) as Milliseconds, (num) => num.toFixed(2)) 19 | ).toBe('1.09s'); 20 | expect( 21 | formatDuration((milliseconds.HOUR / 55) as Milliseconds, (num) => num.toFixed(2)) 22 | ).toBe('1.09min'); 23 | expect( 24 | formatDuration((milliseconds.DAY / 55) as Milliseconds, (num) => num.toFixed(2)) 25 | ).toBe('26.18min'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/ts/math/triangles.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import * as math from '.'; 3 | 4 | describe('right', () => { 5 | it('solves for triangle consistently', () => { 6 | expect(math.triangles.right.solve({ angle: 30, opposite: 5 })).toStrictEqual({ 7 | adjacent: 8.660254037844387, 8 | angle: 30, 9 | hypotenuse: 10.000000000000002, 10 | opposite: 5, 11 | }); 12 | 13 | expect( 14 | math.triangles.right.solve({ angle: 30, adjacent: 8.660254037844387 }) 15 | ).toStrictEqual({ 16 | adjacent: 8.660254037844387, 17 | angle: 30, 18 | hypotenuse: 10, 19 | opposite: 5, 20 | }); 21 | 22 | expect(math.triangles.right.solve({ angle: 30, hypotenuse: 10 })).toStrictEqual({ 23 | adjacent: 8.660254037844387, 24 | angle: 30, 25 | hypotenuse: 10, 26 | opposite: 4.999999999999999, 27 | }); 28 | }); 29 | 30 | it('throws if 0 angle provided', () => { 31 | expect(() => math.triangles.right.solve({ angle: 0, hypotenuse: 10 })).toThrow(); 32 | }); 33 | 34 | it('throws if incorrect arguments were provided due to ignored type error', () => { 35 | // @ts-ignore 36 | expect(() => math.triangles.right.solve({ angle: 10 })).toThrow(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/ts/truncate.ts: -------------------------------------------------------------------------------- 1 | type Options = { 2 | /** Reverses the truncate direction */ 3 | reverse: boolean; 4 | /** A string appended to the end (forward) or beginning (reverse) */ 5 | ending: string; 6 | }; 7 | 8 | /** Truncates the provided string to fit in the max character length if it truncates it will add the `ending` to the start or end of the string 9 | * 10 | * @param str String to be truncated 11 | * @param maxLength Max length of the string 12 | * @param param2 13 | * @returns 14 | * 15 | * ## Usage 16 | * ```ts 17 | * const str = truncate('Hello World!', 5, { ending: '...' }); 18 | * 19 | * console.log(str); // 'hello...' 20 | * ``` 21 | * 22 | * ### Reverse 23 | * ```ts 24 | * const str = truncate('Hello World!', 6, { ending: '...', reverse: true }); 25 | * 26 | * console.log(str); // '...World!' 27 | * ``` 28 | */ 29 | export function truncate( 30 | str: string, 31 | maxLength: number, 32 | { reverse = false, ending = '' }: Partial = { reverse: false, ending: '' } 33 | ): string { 34 | if (str.length <= maxLength) return str; 35 | 36 | if (reverse) { 37 | return `${ending}${str.slice(str.length - maxLength)}`; 38 | } 39 | 40 | return `${str.slice(0, maxLength)}${ending}`; 41 | } 42 | -------------------------------------------------------------------------------- /src/ts/stopwatch.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 2 | import { StopWatch } from './stopwatch'; 3 | 4 | describe('stopwatch', () => { 5 | beforeEach(() => { 6 | vi.useFakeTimers(); 7 | }); 8 | 9 | afterEach(() => { 10 | vi.useRealTimers(); 11 | }); 12 | 13 | it('Correctly indicates the elapsed time', async () => { 14 | const w = new StopWatch(); 15 | 16 | w.start(); 17 | 18 | vi.advanceTimersByTime(50); 19 | 20 | expect(w.elapsed()).toEqual(50); 21 | }); 22 | 23 | it('Correctly indicates the elapsed time', async () => { 24 | const w = new StopWatch(); 25 | 26 | w.start(); 27 | 28 | vi.advanceTimersByTime(50); 29 | 30 | w.stop(); 31 | 32 | vi.advanceTimersByTime(150); 33 | 34 | expect(w.elapsed()).toEqual(50); 35 | }); 36 | 37 | it('Will error if `elapsed` is called before `.start()`', async () => { 38 | const w = new StopWatch(); 39 | 40 | expect(() => w.elapsed()).toThrow(); 41 | }); 42 | 43 | it('Will reset when `.reset()` is called', async () => { 44 | const w = new StopWatch(); 45 | 46 | w.start(); 47 | 48 | w.stop(); 49 | 50 | w.elapsed(); 51 | 52 | w.reset(); 53 | 54 | expect(() => w.elapsed()).toThrow(); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: [main, next] 6 | 7 | concurrency: ${{ github.workflow }}-${{ github.ref }} 8 | 9 | jobs: 10 | release: 11 | name: Build & Publish Release 12 | if: github.repository == 'ieedan/std' 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: pnpm/action-setup@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: "20" 21 | cache: pnpm 22 | 23 | - name: Install dependencies 24 | run: pnpm install 25 | 26 | - name: Create Release Pull Request or Publish 27 | id: changesets 28 | uses: changesets/action@v1 29 | with: 30 | commit: "chore(release): version package" 31 | title: "chore(release): version package" 32 | publish: pnpm ci:release 33 | version: pnpm changeset:version 34 | env: 35 | JSREPO_TOKEN: ${{ secrets.JSREPO_TOKEN }} 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | NODE_ENV: production 38 | -------------------------------------------------------------------------------- /src/ts/perishable-list.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 2 | import { PerishableList } from './perishable-list'; 3 | 4 | describe('PerishableList', () => { 5 | beforeEach(() => { 6 | vi.useFakeTimers(); 7 | }); 8 | 9 | afterEach(() => { 10 | vi.useRealTimers(); 11 | }); 12 | 13 | it('Adds items', async () => { 14 | const list = new PerishableList(); 15 | 16 | list.add('Hello, World!', 100); 17 | list.add('Hello, Everyone!', 1500); 18 | 19 | expect(list.items).toStrictEqual(['Hello, World!', 'Hello, Everyone!']); 20 | 21 | vi.advanceTimersByTime(200); 22 | 23 | expect(list.items).toStrictEqual(['Hello, Everyone!']); 24 | }); 25 | 26 | it('Removes items', () => { 27 | const list = new PerishableList(); 28 | 29 | const key = list.add('Hello, World!', 100); 30 | 31 | list.remove(key); 32 | 33 | expect(list.items).toStrictEqual([]); 34 | }); 35 | 36 | it('Clears items', () => { 37 | const list = new PerishableList(); 38 | 39 | list.add('Hello, World!', 100); 40 | list.add('Hello, World!', 100); 41 | list.add('Hello, World!', 100); 42 | 43 | expect(list.items.length).toBe(3); 44 | 45 | list.clear(); 46 | 47 | expect(list.items.length).toBe(0); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/ts/strings.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import * as strings from './strings'; 3 | 4 | describe('equalsOneOf', () => { 5 | it('correctly identifies a string that equals one of the provided strings', () => { 6 | expect(strings.equalsOneOf('aa', ['aa', 'ba'])).toBe('aa'); 7 | expect(strings.equalsOneOf('aa', ['ba', 'ca'])).toBe(undefined); 8 | }); 9 | }); 10 | 11 | describe('startsWithOneOf', () => { 12 | it('correctly identifies a string that starts with one of the provided strings', () => { 13 | expect(strings.startsWithOneOf('aab', ['aa', 'ba'])).toBe('aa'); 14 | expect(strings.startsWithOneOf('aab', ['ba', 'ca'])).toBe(undefined); 15 | }); 16 | }); 17 | 18 | describe('endsWithOneOf', () => { 19 | it('correctly identifies a string that ends with one of the provided strings', () => { 20 | expect(strings.endsWithOneOf('baa', ['aa', 'ca'])).toBe('aa'); 21 | expect(strings.endsWithOneOf('baa', ['ab', 'ac'])).toBe(undefined); 22 | }); 23 | }); 24 | 25 | describe('iEqual', () => { 26 | it('returns true when strings are equal', () => { 27 | expect(strings.iEqual('A', 'a')).toBe(true); 28 | expect(strings.iEqual('Hello, World!', 'hello, world!')).toBe(true); 29 | }); 30 | 31 | it('returns false when strings are not equal', () => { 32 | expect(strings.iEqual('a', 'b')).toBe(false); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/ts/math/circle.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import * as math from '.'; 3 | 4 | describe('getPoint', () => { 5 | it('Gets points all around the circle correctly', () => { 6 | const radius = 1; 7 | 8 | expect(math.circles.getPoint(0, radius)).toStrictEqual({ x: 1, y: 0 } satisfies math.Point); 9 | expect(math.circles.getPoint(45, radius)).toStrictEqual({ 10 | x: 0.29289321881345254, 11 | y: 0.2928932188134524, 12 | } satisfies math.Point); 13 | expect(math.circles.getPoint(90, radius)).toStrictEqual({ 14 | x: 0, 15 | y: 1, 16 | } satisfies math.Point); 17 | expect(math.circles.getPoint(135, radius)).toStrictEqual({ 18 | x: 0.2928932188134524, 19 | y: 1.7071067811865475, 20 | } satisfies math.Point); 21 | expect(math.circles.getPoint(180, radius)).toStrictEqual({ 22 | x: 1, 23 | y: 2, 24 | } satisfies math.Point); 25 | expect(math.circles.getPoint(225, radius)).toStrictEqual({ 26 | x: 1.7071067811865475, 27 | y: 1.7071067811865475, 28 | } satisfies math.Point); 29 | expect(math.circles.getPoint(270, radius)).toStrictEqual({ 30 | x: 2, 31 | y: 1, 32 | } satisfies math.Point); 33 | expect(math.circles.getPoint(315, radius)).toStrictEqual({ 34 | x: 1.7071067811865475, 35 | y: 0.29289321881345254, 36 | } satisfies math.Point); 37 | expect(math.circles.getPoint(360, radius)).toStrictEqual({ 38 | x: 1, 39 | y: 0, 40 | } satisfies math.Point); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/ts/lines.test.ts: -------------------------------------------------------------------------------- 1 | import os from 'node:os'; 2 | import { describe, expect, it } from 'vitest'; 3 | import * as lines from './lines'; 4 | 5 | describe('get', () => { 6 | it('splits only on intended new lines', () => { 7 | const content = 'hello\\nhello\nhello'; 8 | 9 | expect(lines.get(content)).toStrictEqual(['hello\\nhello', 'hello']); 10 | }); 11 | 12 | it('splits correctly on windows EOL', () => { 13 | const content = 'hello\\nhello\r\nhello'; 14 | 15 | expect(lines.get(content)).toStrictEqual(['hello\\nhello', 'hello']); 16 | }); 17 | }); 18 | 19 | describe('join', () => { 20 | it('Joins correctly', () => { 21 | const ls = ['hello\\nhello', 'hello']; 22 | 23 | expect(lines.join(ls)).toStrictEqual(`hello\\nhello${os.EOL}hello`); 24 | }); 25 | 26 | it('Joins correctly with `lineNumbers`', () => { 27 | const ls = ['hello\\nhello', 'hello']; 28 | 29 | expect(lines.join(ls, { lineNumbers: true })).toStrictEqual( 30 | ` 1 hello\\nhello${os.EOL} 2 hello` 31 | ); 32 | }); 33 | 34 | it('Joins correctly with `prefix`', () => { 35 | const ls = ['hello\\nhello', 'hello']; 36 | 37 | expect(lines.join(ls, { prefix: () => ' + ' })).toStrictEqual( 38 | ` + hello\\nhello${os.EOL} + hello` 39 | ); 40 | }); 41 | 42 | it('Adds `prefix` before line numbers', () => { 43 | const ls = ['hello\\nhello', 'hello']; 44 | 45 | expect(lines.join(ls, { prefix: () => ' + ', lineNumbers: true })).toStrictEqual( 46 | ` + 1 hello\\nhello${os.EOL} + 2 hello` 47 | ); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/ts/dispatcher.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { Dispatcher } from './dispatcher'; 3 | 4 | describe('dispatcher', () => { 5 | it('Listen then un-listen', () => { 6 | const dispatcher = new Dispatcher(); 7 | 8 | let count = 0; 9 | 10 | const dispatcherId = dispatcher.addListener(() => { 11 | count += 2; 12 | }); 13 | 14 | dispatcher.emit(); 15 | 16 | expect(count).toBe(2); 17 | 18 | dispatcher.removeListener(dispatcherId); 19 | 20 | dispatcher.emit(); 21 | 22 | expect(count).toBe(2); 23 | }); 24 | 25 | it('All listen then un-listen', () => { 26 | const dispatcher = new Dispatcher(); 27 | 28 | let count = 0; 29 | 30 | dispatcher.addListener(() => { 31 | count += 2; 32 | }); 33 | dispatcher.addListener(() => { 34 | count += 2; 35 | }); 36 | 37 | dispatcher.emit(); 38 | 39 | expect(count).toBe(4); 40 | 41 | dispatcher.removeAllListeners(); 42 | 43 | dispatcher.emit(); 44 | 45 | expect(count).toBe(4); 46 | }); 47 | 48 | it('All listen then one un-listens', () => { 49 | const dispatcher = new Dispatcher(); 50 | 51 | let count = 0; 52 | 53 | const firstId = dispatcher.addListener(() => { 54 | count += 2; 55 | }); 56 | dispatcher.addListener(() => { 57 | count += 5; 58 | }); 59 | 60 | dispatcher.emit(); 61 | 62 | expect(count).toBe(7); 63 | 64 | dispatcher.removeListener(firstId); 65 | 66 | dispatcher.emit(); 67 | 68 | expect(count).toBe(12); 69 | }); 70 | 71 | it('All listeners receive params', () => { 72 | const dispatcher = new Dispatcher<{ currentCount: number }>(); 73 | 74 | let count = 2; 75 | 76 | dispatcher.addListener((opts) => { 77 | if (opts === undefined) return; 78 | 79 | count = opts.currentCount + 2; 80 | }); 81 | 82 | dispatcher.emit({ currentCount: count }); 83 | 84 | expect(count).toBe(4); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /src/ts/lines.ts: -------------------------------------------------------------------------------- 1 | import os from 'node:os'; 2 | import { leftPadMin } from './pad'; 3 | 4 | /** Regex used to split on new lines 5 | * 6 | * ``` 7 | * /\n|\r\n/g 8 | * ``` 9 | */ 10 | export const NEW_LINE_REGEX = /\n|\r\n/g; 11 | 12 | /** Splits str into an array of lines. 13 | * 14 | * @param str 15 | * @returns 16 | * 17 | * ## Usage 18 | * 19 | * ```ts 20 | * lines.split("hello\\nhello\nhello"); // ["hello\\nhello", "hello"] 21 | * ``` 22 | */ 23 | export function get(str: string): string[] { 24 | return str.split(NEW_LINE_REGEX); 25 | } 26 | 27 | export type Options = { 28 | lineNumbers: boolean; 29 | prefix: (line: number, lineCount: number) => string; 30 | }; 31 | 32 | /** Joins the array of lines back into a string using the platform specific EOL. 33 | * 34 | * @param lines 35 | * @returns 36 | * 37 | * ## Usage 38 | * 39 | * ```ts 40 | * lines.join(["1", "2", "3"]); // "1\n2\n3" or on windows "1\r\n2\r\n3" 41 | * 42 | * // add line numbers 43 | * lines.join(["import { } from '.'", "console.log('test')"], { lineNumbers: true }); 44 | * // 1 import { } from '.' 45 | * // 2 console.log('test') 46 | * 47 | * // add a custom prefix 48 | * lines.join(["import { } from '.'", "console.log('test')"], { prefix: () => " + " }); 49 | * // + import { } from '.' 50 | * // + console.log('test') 51 | * ``` 52 | */ 53 | export function join( 54 | lines: string[], 55 | { lineNumbers = false, prefix }: Partial = {} 56 | ): string { 57 | let transformed = lines; 58 | 59 | if (lineNumbers) { 60 | const length = lines.length.toString().length + 1; 61 | 62 | transformed = transformed.map((line, i) => `${leftPadMin(`${i + 1}`, length)} ${line}`); 63 | } 64 | 65 | if (prefix !== undefined) { 66 | transformed = transformed.map((line, i) => `${prefix(i, lines.length)}${line}`); 67 | } 68 | 69 | return transformed.join(os.EOL); 70 | } 71 | -------------------------------------------------------------------------------- /src/ts/time.ts: -------------------------------------------------------------------------------- 1 | import type { Brand } from './types'; 2 | 3 | export type Milliseconds = Brand; 4 | 5 | export const milliseconds = { 6 | /** Milliseconds in a second */ 7 | SECOND: 1000 as Milliseconds, 8 | /** Milliseconds in a minute */ 9 | MINUTE: (1000 * 60) as Milliseconds, 10 | /** Milliseconds in a hour */ 11 | HOUR: (1000 * 60 * 60) as Milliseconds, 12 | /** Milliseconds in a day */ 13 | DAY: (1000 * 60 * 60 * 24) as Milliseconds, 14 | /** Milliseconds in a year */ 15 | YEAR: (1000 * 60 * 60 * 24 * 365) as Milliseconds, 16 | } as const; 17 | 18 | export type Seconds = Brand; 19 | 20 | export const seconds = { 21 | /** Seconds in a minute */ 22 | MINUTE: 60 as Seconds, 23 | /** Seconds in a hour */ 24 | HOUR: (60 * 60) as Seconds, 25 | /** Seconds in a day */ 26 | DAY: (60 * 60 * 24) as Seconds, 27 | /** Seconds in a year */ 28 | YEAR: (60 * 60 * 24 * 365) as Seconds, 29 | } as const; 30 | 31 | /** Formats a time given in milliseconds with units. 32 | * 33 | * @param duration Time to be formatted in milliseconds 34 | * @param transform Runs before the num is formatted perfect place to put a `.toFixed()` 35 | * @returns 36 | * 37 | * ## Usage 38 | * ```ts 39 | * formatDuration(500); // 500ms 40 | * formatDuration(SECOND); // 1s 41 | * formatDuration(MINUTE); // 1min 42 | * formatDuration(HOUR); // 1h 43 | * ``` 44 | */ 45 | export function formatDuration( 46 | duration: Milliseconds, 47 | transform: (num: number) => string = (num) => num.toString() 48 | ): string { 49 | if (duration < milliseconds.SECOND) return `${transform(duration)}ms`; 50 | 51 | if (duration < milliseconds.MINUTE) return `${transform(duration / milliseconds.SECOND)}s`; 52 | 53 | if (duration < milliseconds.HOUR) return `${transform(duration / milliseconds.MINUTE)}min`; 54 | 55 | return `${duration / milliseconds.HOUR}h`; 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: pnpm/action-setup@v4 34 | - uses: actions/setup-node@v4 35 | with: 36 | node-version: "20" 37 | 38 | # we have to do this to load the typedoc plugins 39 | - name: Install Dependencies 40 | run: pnpm install 41 | 42 | - name: Generate Documentation 43 | run: pnpm docs:generate 44 | 45 | - name: Setup Pages 46 | uses: actions/configure-pages@v5 47 | 48 | - name: Upload artifact 49 | uses: actions/upload-pages-artifact@v3 50 | with: 51 | path: "./docs" 52 | 53 | - name: Deploy to GitHub Pages 54 | id: deployment 55 | uses: actions/deploy-pages@v4 56 | -------------------------------------------------------------------------------- /src/ts/array.ts: -------------------------------------------------------------------------------- 1 | /** Maps the provided map into an array using the provided mapping function. 2 | * 3 | * @param map Map to be entered into an array 4 | * @param fn A mapping function to transform each pair into an item 5 | * @returns 6 | * 7 | * ## Usage 8 | * ```ts 9 | * console.log(map); // Map(5) { 0 => 5, 1 => 4, 2 => 3, 3 => 2, 4 => 1 } 10 | * 11 | * const arr = fromMap(map, (_, value) => value); 12 | * 13 | * console.log(arr); // [5, 4, 3, 2, 1] 14 | * ``` 15 | */ 16 | export function fromMap(map: Map, fn: (key: K, value: V) => T): T[] { 17 | const items: T[] = []; 18 | 19 | for (const [key, value] of map) { 20 | items.push(fn(key, value)); 21 | } 22 | 23 | return items; 24 | } 25 | 26 | /** Calculates the sum of all elements in the array based on the provided function. 27 | * 28 | * @param arr Array of items to be summed. 29 | * @param fn Summing function 30 | * @returns 31 | * 32 | * ## Usage 33 | * 34 | * ```ts 35 | * const total = sum([1, 2, 3, 4, 5], (num) => num); 36 | * 37 | * console.log(total); // 15 38 | * ``` 39 | */ 40 | export function sum(arr: T[], fn: (item: T) => number): number { 41 | let total = 0; 42 | 43 | for (const item of arr) { 44 | total = total + fn(item); 45 | } 46 | 47 | return total; 48 | } 49 | 50 | /** Maps the provided array into a map 51 | * 52 | * @param arr Array of items to be entered into a map 53 | * @param fn A mapping function to transform each item into a key value pair 54 | * @returns 55 | * 56 | * ## Usage 57 | * ```ts 58 | * const map = toMap([5, 4, 3, 2, 1], (item, i) => [i, item]); 59 | * 60 | * console.log(map); // Map(5) { 0 => 5, 1 => 4, 2 => 3, 3 => 2, 4 => 1 } 61 | * ``` 62 | */ 63 | export function toMap( 64 | arr: T[], 65 | fn: (item: T, index: number) => [key: K, value: V] 66 | ): Map { 67 | const map = new Map(); 68 | 69 | for (let i = 0; i < arr.length; i++) { 70 | const [key, value] = fn(arr[i], i); 71 | 72 | map.set(key, value); 73 | } 74 | 75 | return map; 76 | } 77 | -------------------------------------------------------------------------------- /src/ts/perishable-list.ts: -------------------------------------------------------------------------------- 1 | /** A list class where items are removed after their given expiration time. 2 | * 3 | * ## Usage 4 | * 5 | * ```ts 6 | * const list = new PerishableList(); 7 | * 8 | * list.add("Hello, World!", 1000); 9 | * 10 | * console.log(list.items); // ["Hello, World!"]; 11 | * 12 | * await sleep(1000); 13 | * 14 | * console.log(list.items); // []; 15 | * ``` 16 | */ 17 | export class PerishableList { 18 | private index = -1; // start at -1 so it increments to 0 on first add 19 | #items: Map = new Map(); 20 | 21 | /** Adds an item to the list with an `expiresIn` time. The item will be removed from the list if the `expiresIn` time has passed. 22 | * 23 | * 24 | * @param item 25 | * @param expiresIn 26 | * @returns id of the item 27 | * 28 | * ## Usage 29 | * ```ts 30 | * list.add("Hello, World!", 1000); 31 | * ``` 32 | */ 33 | add(item: T, expiresIn: number): number { 34 | this.index++; 35 | 36 | this.#items.set(this.index, { expiration: Date.now() + expiresIn, item }); 37 | 38 | return this.index; 39 | } 40 | 41 | /** Removes an item from the list with the key returned from the `add` method. 42 | * 43 | * @param index 44 | * 45 | * ## Usage 46 | * ```ts 47 | * const key = list.add("Hello, World!", 1000); 48 | * 49 | * list.remove(key); 50 | * ``` 51 | */ 52 | remove(index: number) { 53 | this.#items.delete(index); 54 | } 55 | 56 | /** Removes all items from the list. 57 | * 58 | * ## Usage 59 | * 60 | * ```ts 61 | * list.clear(); 62 | * ``` 63 | */ 64 | clear() { 65 | this.#items.clear(); 66 | this.index = -1; 67 | } 68 | 69 | /** The un-expired items in the list */ 70 | get items(): T[] { 71 | const items: T[] = []; 72 | 73 | for (const [key, value] of this.#items) { 74 | if (Date.now() > value.expiration) { 75 | this.#items.delete(key); 76 | continue; 77 | } 78 | 79 | items.push(value.item); 80 | } 81 | 82 | return items; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/ts/array.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import * as array from './array'; 3 | 4 | describe('mapToArray', () => { 5 | it('Correctly maps the map to an array', () => { 6 | const initialMap = new Map(); 7 | initialMap.set(0, 1); 8 | initialMap.set(1, 2); 9 | initialMap.set(2, 3); 10 | 11 | const arr = array.fromMap(initialMap, (_, value) => value); 12 | 13 | expect(arr).toStrictEqual([1, 2, 3]); 14 | }); 15 | 16 | it('Returns an empty array for an empty map', () => { 17 | const initialMap = new Map(); 18 | 19 | const arr = array.fromMap(initialMap, (_, value) => value); 20 | 21 | expect(arr).toStrictEqual([]); 22 | }); 23 | }); 24 | 25 | describe('sum', () => { 26 | it('Correctly sums the array', () => { 27 | const total = array.sum([1, 2, 3, 4, 5], (num) => num); 28 | 29 | expect(total).toBe(15); 30 | }); 31 | 32 | it('Correctly sums negative and positive numbers', () => { 33 | const total = array.sum([1, -1], (num) => num); 34 | 35 | expect(total).toBe(0); 36 | }); 37 | 38 | it('Returns 0 when empty', () => { 39 | const total = array.sum([], (num) => num); 40 | 41 | expect(total).toBe(0); 42 | }); 43 | }); 44 | 45 | describe('toMap', () => { 46 | it('Maps array into map', () => { 47 | const expected = new Map(); 48 | expected.set(0, 1); 49 | expected.set(1, 2); 50 | expected.set(2, 3); 51 | 52 | const map = array.toMap([1, 2, 3], (item, index) => [index, item]); 53 | 54 | expect(map).toStrictEqual(expected); 55 | }); 56 | 57 | it('Ignores duplicate values in map', () => { 58 | const expected = new Map(); 59 | expected.set(1, 1); 60 | expected.set(2, 2); 61 | expected.set(3, 3); 62 | 63 | const map = array.toMap([1, 2, 3, 3], (item) => [item, item]); 64 | 65 | expect(map).toStrictEqual(expected); 66 | }); 67 | 68 | it('Returns empty when the map is empty', () => { 69 | const expected = new Map(); 70 | 71 | const map = array.toMap([], (item, index) => [index, item]); 72 | 73 | expect(map).toStrictEqual(expected); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/ts/matcher.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { type Matchable, Matcher } from './matcher'; 3 | 4 | describe('Matcher.select', () => { 5 | it('Selects the correct member', () => { 6 | interface Member extends Matchable { 7 | name: string; 8 | } 9 | 10 | const members: Member[] = [ 11 | { matches: (p) => p.endsWith('.ts'), name: 'typescript' }, 12 | { matches: (p) => p.endsWith('.js'), name: 'javascript' }, 13 | ]; 14 | 15 | const matcher = new Matcher(members); 16 | 17 | expect(matcher.select('test.js')?.name).toBe('javascript'); 18 | expect(matcher.select('test.ts')?.name).toBe('typescript'); 19 | }); 20 | }); 21 | 22 | describe('Matcher.selectOrDefault', () => { 23 | it('Selects the correct member', () => { 24 | interface Member extends Matchable { 25 | name: string; 26 | } 27 | 28 | const members: Member[] = [ 29 | { matches: (p) => p.endsWith('.ts'), name: 'typescript' }, 30 | { matches: (p) => p.endsWith('.js'), name: 'javascript' }, 31 | ]; 32 | 33 | const matcher = new Matcher(members); 34 | 35 | // matches correctly 36 | expect(matcher.selectOrDefault('test.js', members[0])?.name).toBe('javascript'); 37 | expect(matcher.selectOrDefault('test.ts', members[0])?.name).toBe('typescript'); 38 | 39 | // returns default when there's no match 40 | expect(matcher.selectOrDefault('test.oops', members[0])?.name).toBe('typescript'); 41 | }); 42 | }); 43 | 44 | describe('Matcher.match', () => { 45 | it('Matches the correct member', () => { 46 | interface Member extends Matchable { 47 | name: string; 48 | } 49 | 50 | const members: Member[] = [ 51 | { matches: (p) => p.endsWith('.ts'), name: 'typescript' }, 52 | { matches: (p) => p.endsWith('.js'), name: 'javascript' }, 53 | ]; 54 | 55 | const matcher = new Matcher(members); 56 | 57 | expect( 58 | matcher.match( 59 | 'test.js', 60 | (v) => v, 61 | () => { 62 | throw new Error('oh no!'); 63 | } 64 | ).name 65 | ).toBe('javascript'); 66 | expect( 67 | () => 68 | matcher.match( 69 | 'test.', 70 | (v) => v, 71 | () => { 72 | throw new Error('oh no!'); 73 | } 74 | ).name 75 | ).toThrow(); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /src/ts/strings.ts: -------------------------------------------------------------------------------- 1 | /** Returns the string that matches the string (if it exits). Great for matching string union types. 2 | * 3 | * @param str 4 | * @param strings 5 | * @returns 6 | * 7 | * ## Usage 8 | * ```ts 9 | * const methods = ['GET', 'PUT', 'POST', 'PATCH', 'DELETE'] as const; 10 | * 11 | * const methodStr: string = 'GET'; 12 | * 13 | * const method = equalsOneOf(methodStr, methods); 14 | * 15 | * if (method) { 16 | * // if method was just a string this would be a type error 17 | * methods.includes(method) 18 | * } 19 | * ``` 20 | */ 21 | export function equalsOneOf(str: string, strings: readonly T[]): T | undefined { 22 | for (const s of strings) { 23 | if (s === str) return s; 24 | } 25 | 26 | return undefined; 27 | } 28 | 29 | /** Returns the matched prefix for the string (if it exists). Great for matching string union types. 30 | * 31 | * @param str 32 | * @param strings 33 | * @returns 34 | * 35 | * ## Usage 36 | * ```ts 37 | * startsWithOneOf('ab', 'a', 'c'); // 'a' 38 | * startsWithOneOf('cc', 'a', 'b'); // undefined 39 | * ``` 40 | */ 41 | export function startsWithOneOf( 42 | str: string, 43 | strings: readonly T[] 44 | ): T | undefined { 45 | for (const s of strings) { 46 | if (str.startsWith(s)) return s; 47 | } 48 | 49 | return undefined; 50 | } 51 | 52 | /** Returns the matched suffix for the string (if it exists). Great for matching string union types. 53 | * 54 | * @param str 55 | * @param strings 56 | * @returns 57 | * 58 | * ## Usage 59 | * ```ts 60 | * endsWithOneOf('cb', 'a', 'b'); // 'b' 61 | * endsWithOneOf('cc', 'a', 'b'); // undefined 62 | * ``` 63 | */ 64 | export function endsWithOneOf(str: string, strings: readonly T[]): T | undefined { 65 | for (const s of strings) { 66 | if (str.endsWith(s)) return s; 67 | } 68 | 69 | return undefined; 70 | } 71 | 72 | /** Case insensitive equality. Returns true if `left.toLowerCase()` and `right.toLowerCase()` are equal. 73 | * 74 | * @param left 75 | * @param right 76 | * @returns 77 | * 78 | * ## Usage 79 | * ```ts 80 | * iEqual('Hello, World!', 'hello, World!'); // true 81 | * ``` 82 | */ 83 | export function iEqual(left: string, right: string): boolean { 84 | return left.toLowerCase() === right.toLowerCase(); 85 | } 86 | -------------------------------------------------------------------------------- /src/ts/stopwatch.ts: -------------------------------------------------------------------------------- 1 | /** Creates a new stopwatch instance. 2 | * 3 | * ## Usage 4 | * ```ts 5 | * const w = new Stopwatch(); 6 | * 7 | * w.start(); 8 | * 9 | * await sleep(1000); 10 | * 11 | * console.log(w.elapsed()); // 1000 12 | * ``` 13 | */ 14 | export class StopWatch { 15 | startedAt: number | undefined = undefined; 16 | endedAt: number | undefined = undefined; 17 | 18 | /** Start the stopwatch. 19 | * 20 | * @returns 21 | * 22 | * ## Usage 23 | * 24 | * ```ts 25 | * const w = stopwatch(); 26 | * 27 | * w.start(); // start counting 28 | * ``` 29 | */ 30 | start() { 31 | this.startedAt = Date.now(); 32 | } 33 | 34 | /** Stop the stopwatch. 35 | * 36 | * @returns 37 | * 38 | * ## Usage 39 | * 40 | * ```ts 41 | * const w = stopwatch(); 42 | * 43 | * w.start(); 44 | * 45 | * await sleep(1000); 46 | * 47 | * w.stop(); // stop counting 48 | * 49 | * await sleep(1000); 50 | * 51 | * console.log(w.elapsed()); // 1000 52 | * ``` 53 | */ 54 | stop() { 55 | this.endedAt = Date.now(); 56 | } 57 | 58 | /** Tries to get the elapsed ms. Throws if the Stopwatch has not been started. 59 | * 60 | * @returns 61 | * 62 | * ## Usage 63 | * 64 | * ```ts 65 | * const w = watch(); 66 | * 67 | * w.start(); 68 | * 69 | * await sleep(1000); 70 | * 71 | * // you don't have to call stop before accessing `.elapsed` 72 | * console.log(w.elapsed()); // 1000 73 | * ``` 74 | */ 75 | elapsed() { 76 | // if this hasn't been defined its always an error in the users code 77 | if (!this.startedAt) { 78 | throw new Error('Call `.start()` first!'); 79 | } 80 | 81 | let tempEndedAt = this.endedAt; 82 | 83 | // if the user hasn't called stop just give them the current time 84 | if (!tempEndedAt) { 85 | tempEndedAt = Date.now(); 86 | } 87 | 88 | return tempEndedAt - this.startedAt; 89 | } 90 | 91 | /** Reset the stopwatch. 92 | * 93 | * @returns 94 | * 95 | * ## Usage 96 | * 97 | * ```ts 98 | * const w = stopwatch(); 99 | * 100 | * w.start(); 101 | * 102 | * w.stop(); 103 | * 104 | * w.reset(); 105 | * 106 | * w.elapsed(); // Error: "Call `.start()` first!" 107 | * ``` 108 | */ 109 | reset() { 110 | this.endedAt = undefined; 111 | this.startedAt = undefined; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/ts/math/circle.ts: -------------------------------------------------------------------------------- 1 | import * as triangle from './triangles'; 2 | import type { Point } from './types'; 3 | 4 | /** Gets a point in reference to a grid with a coordinate system in line with the DOM. 5 | * You can use this to animate a point around a circle. 6 | * 7 | * **Example Grid:** 8 | * ``` 9 | * 0───────→ x 10 | * │ ● (1, 1) 11 | * │ ● (2, 2) 12 | * │ ● (2, 3) 13 | * ↓ 14 | * y 15 | * ``` 16 | * 17 | * @param angle 18 | * @param radius 19 | * @returns 20 | * 21 | * ```ts 22 | * // a program to move something around a circle 23 | * 24 | * const radius = 24; 25 | * const speed = 0.05; 26 | * let angle = 0; 27 | * 28 | * const update = () => { 29 | * const cords = getPoint(angle, radius); 30 | * 31 | * move(cords); // update position 32 | * 33 | * // increase angle to move point around the circle 34 | * if (angle >= 360) { 35 | * angle = speed; // start at speed because 360 is the same as 0 36 | * } else { 37 | * angle += speed; 38 | * } 39 | * 40 | * requestAnimationFrame(update) 41 | * } 42 | * 43 | * update(); 44 | * ``` 45 | */ 46 | export function getPoint(angle: number, radius: number): Point { 47 | if (angle > 0 && angle < 90) { 48 | const deg = angle; 49 | const { opposite, adjacent } = triangle.right.solve({ angle: deg, hypotenuse: radius }); 50 | 51 | return { x: radius - opposite, y: radius - adjacent }; 52 | } 53 | 54 | if (angle === 90) { 55 | return { x: 0, y: radius }; 56 | } 57 | 58 | if (angle > 90 && angle < 180) { 59 | const deg = angle - 90; 60 | const { opposite, adjacent } = triangle.right.solve({ angle: deg, hypotenuse: radius }); 61 | 62 | return { x: radius - adjacent, y: radius + opposite }; 63 | } 64 | 65 | if (angle === 180) { 66 | return { x: radius, y: radius * 2 }; 67 | } 68 | 69 | if (angle > 180 && angle < 270) { 70 | const deg = angle - 180; 71 | const { opposite, adjacent } = triangle.right.solve({ angle: deg, hypotenuse: radius }); 72 | 73 | return { x: radius + opposite, y: radius * 2 - radius + adjacent }; 74 | } 75 | 76 | if (angle === 270) { 77 | return { x: radius * 2, y: radius }; 78 | } 79 | 80 | if (angle > 270 && angle < 360) { 81 | const deg = angle - 270; 82 | const { opposite, adjacent } = triangle.right.solve({ angle: deg, hypotenuse: radius }); 83 | 84 | return { x: radius * 2 - radius + adjacent, y: radius - opposite }; 85 | } 86 | 87 | // must be 0degrees 88 | return { x: radius, y: 0 }; 89 | } 90 | -------------------------------------------------------------------------------- /src/ts/matcher.ts: -------------------------------------------------------------------------------- 1 | export interface Matchable { 2 | /** Returns true if the `s` matches this member. 3 | * 4 | * @param pattern 5 | * @returns 6 | */ 7 | matches: (pattern: U) => boolean; 8 | } 9 | 10 | /** An abstraction to help you create a simple pattern matching API 11 | * 12 | * ## Usage 13 | * ```ts 14 | * interface Member extends Matchable { 15 | * name: string; 16 | * } 17 | * 18 | * const members: Member[] = [ 19 | * { matches: (p) => p.endsWith('.ts'), name: 'typescript' }, 20 | * { matches: (p) => p.endsWith('.js'), name: 'javascript' }, 21 | * ]; 22 | * 23 | * const matcher = new Matcher(members); 24 | * 25 | * matcher.select('test.js')?.name; // javascript 26 | * ``` 27 | */ 28 | export class Matcher, U = T extends Matchable ? R : never> { 29 | constructor(readonly members: T[]) {} 30 | 31 | /** Iterate over the members and return the first member that matches `s`. 32 | * 33 | * @param pattern 34 | * @returns 35 | * 36 | * ## Usage 37 | * ```ts 38 | * const member = matcher.select('name'); 39 | * 40 | * member.doSomething(); 41 | * ``` 42 | */ 43 | select(pattern: U): T | null { 44 | for (const member of this.members) { 45 | if (member.matches(pattern)) return member; 46 | } 47 | 48 | return null; 49 | } 50 | 51 | /** Calls `.match()` to try and find a match if there is no match it returns the provided default value. 52 | * 53 | * @param pattern 54 | * @param defaultValue The value returned when the pattern provided doesn't match any member. 55 | * @returns 56 | * 57 | * ## Usage 58 | * ```ts 59 | * const member = matcher.select('', { matches: () => true, name: 'default' }); 60 | * ``` 61 | */ 62 | selectOrDefault(pattern: U, defaultValue: T): T { 63 | return this.match( 64 | pattern, 65 | (v) => v, 66 | () => defaultValue 67 | ); 68 | } 69 | 70 | /** Calls `.select()` to try and find a match for any of the members. If it can it calls `matches` otherwise it call `noMatch` 71 | * 72 | * @param pattern 73 | * @param matches Called with the matching member if one exists 74 | * @param noMatch Called when no match was found 75 | * @returns 76 | * 77 | * ## Usage 78 | * ```ts 79 | * matcher.match( 80 | * 'name', 81 | * (member) => member.doSomething(), 82 | * () => throw Error('Couldn't do anything!') 83 | * ) 84 | * ``` 85 | */ 86 | match(pattern: U, matches: (v: T) => W, noMatch: () => W): W { 87 | const selected = this.select(pattern); 88 | 89 | if (selected === null) return noMatch(); 90 | 91 | return matches(selected); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/ts/ipv4-address.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { formatToString, parse, validate } from './ipv4-address'; 3 | 4 | describe('parse', () => { 5 | it(`Allows '.' separator.`, () => { 6 | const expected = [192, 168, 100, 10]; 7 | 8 | expect(parse(expected.join('.')).unwrap()).toStrictEqual(expected); 9 | }); 10 | 11 | it(`Allows '_' separator.`, () => { 12 | const expected = [192, 168, 100, 10]; 13 | 14 | expect(parse(expected.join('_')).unwrap()).toStrictEqual(expected); 15 | }); 16 | 17 | it(`Allows ' ' separator.`, () => { 18 | const expected = [192, 168, 100, 10]; 19 | 20 | expect(parse(expected.join(' ')).unwrap()).toStrictEqual(expected); 21 | }); 22 | 23 | it('Does not allow non numbers.', () => { 24 | expect(parse('a.a.a.a').unwrapErr()).toStrictEqual({ 25 | message: "'a' is not a number.", 26 | octet: 1, 27 | }); 28 | }); 29 | 30 | it('Allows leading 0s', () => { 31 | expect(parse('001.001.001.001').unwrap()).toStrictEqual([1, 1, 1, 1]); 32 | }); 33 | 34 | it('Returns error when invalid', () => { 35 | expect(parse('192.168.256.10').unwrapErr()).toStrictEqual({ 36 | octet: 3, 37 | message: "'256' is out of range.", 38 | }); 39 | expect(parse('192.168.0').unwrapErr()).toStrictEqual({ 40 | message: "'192.168.0' is invalid as it doesn't contain 4 octets.", 41 | }); 42 | }); 43 | }); 44 | 45 | describe('validate', () => { 46 | it('Returns true when valid', () => { 47 | expect(validate([192, 168, 100, 10])).toBe(true); 48 | expect(validate('192.168.100.10')).toBe(true); 49 | expect(validate('192_168_100_10')).toBe(true); 50 | }); 51 | 52 | it('Returns false when invalid', () => { 53 | expect(validate([192, 168, 100, -10])).toBe(false); 54 | expect(validate([192, 168, 100, -56])).toBe(false); 55 | expect(validate('192.168.100.256')).toBe(false); 56 | expect(validate('192.168.100.-10')).toBe(false); 57 | }); 58 | }); 59 | 60 | describe('formatToString', () => { 61 | it('Returns the correct format', () => { 62 | expect(formatToString([192, 168, 100, 10]).unwrap()).toBe('192.168.100.10'); 63 | expect(formatToString([192, 168, 100, 10], ' ').unwrap()).toBe('192 168 100 10'); 64 | expect(formatToString([192, 168, 100, 10], '_').unwrap()).toBe('192_168_100_10'); 65 | expect(formatToString('192.168.100.10', '_').unwrap()).toBe('192_168_100_10'); 66 | expect(formatToString('192 168 100 10', '_').unwrap()).toBe('192_168_100_10'); 67 | expect(formatToString('192_168_100_10', '.').unwrap()).toBe('192.168.100.10'); 68 | }); 69 | 70 | it('Returns error when invalid', () => { 71 | expect(formatToString('256.2.2.2').unwrapErr()).toBe("'256' is out of range."); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # std 2 | 3 | ## 5.3.2 4 | ### Patch Changes 5 | 6 | 7 | - Mark all test files with role: 'test' so they are only installed when requested ([#117](https://github.com/ieedan/std/pull/117)) 8 | 9 | ## 5.3.1 10 | ### Patch Changes 11 | 12 | 13 | - feat: jsrepo v3 beta ([#110](https://github.com/ieedan/std/pull/110)) 14 | 15 | 16 | - bump jsrepo ([#110](https://github.com/ieedan/std/pull/110)) 17 | 18 | 19 | - bump jsrepo ([#110](https://github.com/ieedan/std/pull/110)) 20 | 21 | ## 5.3.1-beta.2 22 | ### Patch Changes 23 | 24 | 25 | - bump jsrepo ([#110](https://github.com/ieedan/std/pull/110)) 26 | 27 | ## 5.3.1-beta.1 28 | ### Patch Changes 29 | 30 | 31 | - bump jsrepo ([#110](https://github.com/ieedan/std/pull/110)) 32 | 33 | ## 5.3.1-beta.0 34 | ### Patch Changes 35 | 36 | 37 | - feat: jsrepo v3 beta ([#111](https://github.com/ieedan/std/pull/111)) 38 | 39 | ## 5.3.0 40 | ### Minor Changes 41 | 42 | 43 | - feat(ts/time): Use Branded types for format duration and units. ([#107](https://github.com/ieedan/std/pull/107)) 44 | 45 | 46 | - feat(ts/types): Add `Brand` utility type ([#107](https://github.com/ieedan/std/pull/107)) 47 | 48 | ## 5.2.2 49 | ### Patch Changes 50 | 51 | 52 | - chore: fix cursor rules config ([#105](https://github.com/ieedan/std/pull/105)) 53 | 54 | 55 | - chore: Add cursor rule config file ([#105](https://github.com/ieedan/std/pull/105)) 56 | 57 | ## 5.2.1 58 | ### Patch Changes 59 | 60 | 61 | - chore: Add cursor rule config file ([#103](https://github.com/ieedan/std/pull/103)) 62 | 63 | ## 5.2.0 64 | ### Minor Changes 65 | 66 | 67 | - feat: Add `equalsOneOf` utility ([#101](https://github.com/ieedan/std/pull/101)) 68 | 69 | 70 | - feat: Update `strings` API to return the matched string for `startsWithOneOf` and `endsWithOneOf` ([#101](https://github.com/ieedan/std/pull/101)) 71 | 72 | ## 5.1.0 73 | ### Minor Changes 74 | 75 | 76 | - feat: 🎉 New util `types` ([#98](https://github.com/ieedan/std/pull/98)) 77 | 78 | ## 5.0.3 79 | ### Patch Changes 80 | 81 | 82 | - chore: update readme badges ([#96](https://github.com/ieedan/std/pull/96)) 83 | 84 | ## 5.0.2 85 | ### Patch Changes 86 | 87 | 88 | - chore: update README links ([#94](https://github.com/ieedan/std/pull/94)) 89 | 90 | ## 5.0.1 91 | ### Patch Changes 92 | 93 | 94 | - chore: bump jsrepo to official release ([#91](https://github.com/ieedan/std/pull/91)) 95 | 96 | ## 5.0.0 97 | ### Major Changes 98 | 99 | 100 | - breaking: host on jsrepo.com. Now you can add with `jsrepo add --repo @ieedan/std` ([`61772bb`](https://github.com/ieedan/std/commit/61772bb04bd6817fe84460be8973d78d98c6e3d6)) 101 | -------------------------------------------------------------------------------- /src/ts/dispatcher.ts: -------------------------------------------------------------------------------- 1 | export type ListenerCallback = T extends undefined ? () => void : (opts: T) => void; 2 | 3 | /** Simplifies adding event listeners to your code. 4 | * 5 | * ## Usage 6 | * 7 | * ```ts 8 | * import { Dispatcher } from "./src/blocks/dispatcher"; 9 | * 10 | * const dispatcher = new Dispatcher(); 11 | * 12 | * let count = 0; 13 | * 14 | * dispatcher.addListener(() => { count += 2; }); 15 | * 16 | * dispatcher.emit(); 17 | * 18 | * console.log(count); // 2 19 | * 20 | * dispatcher.addListener(() => { count += 4; }); 21 | * 22 | * dispatcher.emit(); 23 | * 24 | * console.log(count); // 8 25 | * ``` 26 | * 27 | * ### Typed Options 28 | * 29 | * ```ts 30 | * import { Dispatcher } from "./src/blocks/dispatcher"; 31 | * 32 | * const dispatcher = new Dispatcher(); 33 | * 34 | * dispatcher.addListener((name) => { console.log(`Hello ${name}!`) }); 35 | * 36 | * dispatcher.emit("John"); // 'Hello John!' 37 | * ``` 38 | */ 39 | export class Dispatcher { 40 | nextListenerId = -1; 41 | private listeners = new Map>(); 42 | 43 | /** Adds an event listener 44 | * 45 | * @param callback 46 | * @returns 47 | * 48 | * ## Usage 49 | * 50 | * ```ts 51 | * const listenerId = dispatcher.addListener(() => { ... }); 52 | * ``` 53 | */ 54 | addListener(callback: ListenerCallback): number { 55 | this.nextListenerId++; 56 | this.listeners.set(this.nextListenerId, callback); 57 | return this.nextListenerId; 58 | } 59 | 60 | /** Removes an event listener 61 | * 62 | * @param id 63 | * @returns 64 | * 65 | * ## Usage 66 | * 67 | * ```ts 68 | * dispatcher.removeListener(listenerId); 69 | * ``` 70 | */ 71 | removeListener(listenerId: number) { 72 | this.listeners.delete(listenerId); 73 | } 74 | 75 | /** Emits an event to all listeners with the provided options. 76 | * 77 | * @param opts Options to be passed to the listener 78 | * @returns `void` 79 | * 80 | * ## Usage 81 | * 82 | * ```ts 83 | * dispatcher.emit(); 84 | * 85 | * dispatcher.emit(opts); 86 | * ``` 87 | * 88 | */ 89 | emit(opts?: T) { 90 | for (const [_, callback] of this.listeners) { 91 | if (opts === undefined) { 92 | (callback as ListenerCallback)(); 93 | } else { 94 | callback(opts); 95 | } 96 | } 97 | } 98 | 99 | /** Removes all event listeners from the dispatcher. 100 | * 101 | * @returns 102 | * 103 | * ## Usage 104 | * 105 | * ```ts 106 | * dispatcher.removeAllListeners(); 107 | * ``` 108 | */ 109 | removeAllListeners() { 110 | this.listeners.clear(); 111 | this.nextListenerId = 0; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/ts/pad.test.ts: -------------------------------------------------------------------------------- 1 | import { stripVTControlCharacters as stripAsni } from 'node:util'; 2 | import { describe, expect, it } from 'vitest'; 3 | import { centerPad, leftPad, leftPadMin, rightPad, rightPadMin } from './pad'; 4 | 5 | describe('leftPad', () => { 6 | it('Correctly pads', () => { 7 | expect(leftPad('Hello', 3)).toBe(' Hello'); 8 | }); 9 | 10 | it('Correctly pads with the padding character `padWith`', () => { 11 | expect(leftPad('Hello', 3, '.')).toBe('...Hello'); 12 | }); 13 | 14 | it('Correctly pads with padding set to 0', () => { 15 | expect(leftPad('Hello', 0)).toBe('Hello'); 16 | }); 17 | }); 18 | 19 | describe('leftPadMin', () => { 20 | it('Correctly pads', () => { 21 | expect(leftPadMin('1', 3)).toBe(' 1'); 22 | }); 23 | 24 | it('Correctly pads with the padding character `padWith`', () => { 25 | expect(leftPadMin('1', 3, '.')).toBe('..1'); 26 | }); 27 | 28 | it('Correctly pads with escape characters', () => { 29 | expect(stripAsni(leftPadMin('\x1b[1;31m1', 3, '.')).length).toBe(3); 30 | }); 31 | 32 | it('Errors when string length is greater than `length`', () => { 33 | expect(() => leftPadMin('Hello', 3)).toThrow(); 34 | }); 35 | }); 36 | 37 | describe('rightPad', () => { 38 | it('Correctly pads', () => { 39 | expect(rightPad('Hello', 3)).toBe('Hello '); 40 | }); 41 | 42 | it('Correctly pads with the padding character `padWith`', () => { 43 | expect(rightPad('Hello', 3, '.')).toBe('Hello...'); 44 | }); 45 | 46 | it('Correctly pads with padding set to 0', () => { 47 | expect(rightPad('Hello', 0)).toBe('Hello'); 48 | }); 49 | }); 50 | 51 | describe('rightPadMin', () => { 52 | it('Correctly pads', () => { 53 | expect(rightPadMin('1', 3)).toBe('1 '); 54 | }); 55 | 56 | it('Correctly pads with the padding character `padWith`', () => { 57 | expect(rightPadMin('1', 3, '.')).toBe('1..'); 58 | }); 59 | 60 | it('Correctly pads with escape characters', () => { 61 | expect(stripAsni(rightPadMin('\x1b[1;31m1', 3, '.')).length).toBe(3); 62 | }); 63 | 64 | it('Errors when string length is greater than `length`', () => { 65 | expect(() => rightPadMin('Hello', 3)).toThrow(); 66 | }); 67 | }); 68 | 69 | describe('centerPad', () => { 70 | it('Correctly pads', () => { 71 | expect(centerPad('1', 3)).toBe(' 1 '); 72 | }); 73 | 74 | it('Adds excess padding to right when padding is uneven', () => { 75 | expect(centerPad('1', 4)).toBe(' 1 '); 76 | }); 77 | 78 | it('Correctly pads with the padding character `padWith`', () => { 79 | expect(centerPad('1', 3, '.')).toBe('.1.'); 80 | }); 81 | 82 | it('Correctly pads with escape characters', () => { 83 | expect(stripAsni(centerPad('\x1b[1;31m1', 3, '.')).length).toBe(3); 84 | }); 85 | 86 | it('Errors when string length is greater than `length`', () => { 87 | expect(() => centerPad('Hello', 3)).toThrow(); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/ts/ipv4-address.ts: -------------------------------------------------------------------------------- 1 | import { isNumber } from './is-number'; 2 | import { Err, Ok, type Result } from './result'; 3 | 4 | export type Octets = [number, number, number, number]; 5 | 6 | export type IPv4Address = 7 | | Octets 8 | | `${number}.${number}.${number}.${number}` 9 | | `${number} ${number} ${number} ${number}` 10 | | `${number}_${number}_${number}_${number}`; 11 | 12 | export type ParseError = { 13 | octet?: number; 14 | message: string; 15 | }; 16 | 17 | /** Parses the ip address from a string in the format of `0.0.0.0` or `0 0 0 0` or `0_0_0_0` into an array of octets 18 | * 19 | * @param address 20 | * @returns 21 | * 22 | * ## Usage 23 | * 24 | * ```ts 25 | * parse("192.168.100.10").unwrap(); // [192, 168, 100, 10] 26 | * ``` 27 | */ 28 | export function parse(address: string): Result { 29 | let newAddress = address.trim(); 30 | 31 | newAddress = newAddress.replaceAll(' ', '.'); 32 | newAddress = newAddress.replaceAll('_', '.'); 33 | 34 | const octets = newAddress.split('.'); 35 | 36 | if (octets.length !== 4) 37 | return Err({ message: `'${address}' is invalid as it doesn't contain 4 octets.` }); 38 | 39 | const final: Octets = [0, 0, 0, 0]; 40 | 41 | for (let i = 0; i < octets.length; i++) { 42 | const octet = octets[i]; 43 | 44 | if (!isNumber(octet)) return Err({ octet: i + 1, message: `'${octet}' is not a number.` }); 45 | 46 | const num = Number.parseInt(octet); 47 | 48 | if (num < 0 || num > 255) 49 | return Err({ octet: i + 1, message: `'${octet}' is out of range.` }); 50 | 51 | final[i] = num; 52 | } 53 | 54 | return Ok(final); 55 | } 56 | 57 | /** Validates the provided address 58 | * 59 | * @param address 60 | * @returns 61 | * 62 | * ## Usage 63 | * 64 | * ```ts 65 | * validate("192.168.100.10"); // true 66 | * validate([192, 168, 100, 10]); // true 67 | * 68 | * validate("192.168.100.256"); // false 69 | * validate([192, 168, 100, 256]); // false 70 | * ``` 71 | */ 72 | export function validate(address: IPv4Address): boolean { 73 | if (typeof address === 'string') return parse(address).isOk(); 74 | 75 | for (let i = 0; i < address.length; i++) { 76 | const octet = address[i]; 77 | 78 | if (octet < 0 || octet > 255) return false; 79 | } 80 | 81 | return true; 82 | } 83 | 84 | /** Formats the provided address to a string with the provided separator 85 | * 86 | * @param address 87 | * @param separator @default "." 88 | * @returns 89 | * 90 | * ## Usage 91 | * 92 | * ```ts 93 | * formatToString([192, 168, 100, 10]); // "192.168.100.10" 94 | * ``` 95 | */ 96 | export function formatToString( 97 | address: IPv4Address, 98 | separator: '.' | '_' | ' ' = '.' 99 | ): Result { 100 | if (Array.isArray(address)) return Ok(address.join(separator)); 101 | 102 | const parsed = parse(address); 103 | 104 | if (parsed.isErr()) return Err(parsed.unwrapErr().message); 105 | 106 | return formatToString(parsed.unwrap(), separator); 107 | } 108 | -------------------------------------------------------------------------------- /badges/coverage-lines.svg: -------------------------------------------------------------------------------- 1 | lines: 100%lines100% -------------------------------------------------------------------------------- /badges/coverage-total.svg: -------------------------------------------------------------------------------- 1 | total: 100%total100% -------------------------------------------------------------------------------- /badges/coverage-branches.svg: -------------------------------------------------------------------------------- 1 | branches: 100%branches100% -------------------------------------------------------------------------------- /badges/coverage-functions.svg: -------------------------------------------------------------------------------- 1 | functions: 100%functions100% -------------------------------------------------------------------------------- /badges/coverage-statements.svg: -------------------------------------------------------------------------------- 1 | statements: 100%statements100% -------------------------------------------------------------------------------- /src/ts/math/triangles.ts: -------------------------------------------------------------------------------- 1 | import { dtr } from './conversions'; 2 | 3 | export type RightTriangle = { 4 | /** Angle in degrees */ 5 | angle: number; 6 | /** opposite length */ 7 | opposite: number; 8 | /** adjacent length */ 9 | adjacent: number; 10 | /** hypotenuse length */ 11 | hypotenuse: number; 12 | }; 13 | 14 | export type SolveOptions = 15 | | { 16 | angle: number; 17 | opposite: number; 18 | adjacent?: never; 19 | hypotenuse?: never; 20 | } 21 | | { 22 | angle: number; 23 | opposite?: never; 24 | adjacent: number; 25 | hypotenuse?: never; 26 | } 27 | | { 28 | angle: number; 29 | opposite?: never; 30 | adjacent?: never; 31 | hypotenuse: number; 32 | }; 33 | 34 | /** Solves the right triangle based on the angle given and any one of the sides 35 | * 36 | * @param param0 37 | * @returns 38 | */ 39 | function solveRight({ angle, opposite, adjacent, hypotenuse }: SolveOptions): RightTriangle { 40 | if (angle <= 0) throw new Error(`Invalid value (${angle}) for 'angle'`); 41 | 42 | if (typeof hypotenuse === 'number') { 43 | opposite = solveForOpposite({ angle, hypotenuse }); 44 | adjacent = solveForAdjacent({ angle, hypotenuse }); 45 | } else if (typeof opposite === 'number') { 46 | adjacent = solveForAdjacent({ angle, opposite }); 47 | hypotenuse = solveForHypotenuse({ angle, opposite }); 48 | } else if (typeof adjacent === 'number') { 49 | opposite = solveForOpposite({ angle, adjacent }); 50 | hypotenuse = solveForHypotenuse({ angle, adjacent }); 51 | } else { 52 | throw new Error( 53 | 'Incorrect arguments provided! expected opposite, adjacent, or hypotenuse to be a number' 54 | ); 55 | } 56 | 57 | return { 58 | angle, 59 | opposite, 60 | adjacent, 61 | hypotenuse, 62 | }; 63 | } 64 | 65 | type OppositeSolveOptions = 66 | | { angle: number; adjacent: number; hypotenuse?: never } 67 | | { angle: number; adjacent?: never; hypotenuse: number }; 68 | 69 | function solveForOpposite({ angle, adjacent, hypotenuse }: OppositeSolveOptions): number { 70 | if (typeof hypotenuse === 'number') { 71 | return Math.sin(dtr(angle)) * hypotenuse; 72 | } 73 | 74 | return Math.tan(dtr(angle)) * adjacent; 75 | } 76 | 77 | type AdjacentSolveOptions = 78 | | { angle: number; opposite: number; hypotenuse?: never } 79 | | { angle: number; opposite?: never; hypotenuse: number }; 80 | 81 | function solveForAdjacent({ angle, opposite, hypotenuse }: AdjacentSolveOptions): number { 82 | if (typeof opposite === 'number') { 83 | return opposite / Math.tan(dtr(angle)); 84 | } 85 | 86 | return hypotenuse * Math.cos(dtr(angle)); 87 | } 88 | 89 | type HypotenuseSolveOptions = 90 | | { angle: number; opposite: number; adjacent?: never } 91 | | { angle: number; opposite?: never; adjacent: number }; 92 | 93 | function solveForHypotenuse({ angle, opposite, adjacent }: HypotenuseSolveOptions): number { 94 | if (typeof opposite === 'number') { 95 | return opposite / Math.sin(dtr(angle)); 96 | } 97 | 98 | return adjacent / Math.cos(dtr(angle)); 99 | } 100 | 101 | /** Functions for working with right triangles */ 102 | const right = { solve: solveRight }; 103 | 104 | export { right }; 105 | -------------------------------------------------------------------------------- /src/ts/pad.ts: -------------------------------------------------------------------------------- 1 | import { stripVTControlCharacters as stripAsni } from 'node:util'; 2 | 3 | /** Adds the `padWith` (default `' '`) to the string the amount of times specified by the `space` argument 4 | * 5 | * @param str String to add padding to 6 | * @param space Whitespace to add 7 | * @param padWith Character to use to pad the string 8 | * @returns 9 | * 10 | * ## Usage 11 | * ```ts 12 | * const padded = leftPad("Hello", 3, "."); 13 | * 14 | * console.log(padded); // '...Hello' 15 | * ``` 16 | */ 17 | export function leftPad(str: string, space: number, padWith = ' '): string { 18 | return padWith.repeat(space) + str; 19 | } 20 | 21 | /** Adds the `padWith` until the string length matches the `length` 22 | * 23 | * @param str 24 | * @param length 25 | * @param padWith 26 | * 27 | * ## Usage 28 | * ```ts 29 | * const padded = leftPadMin("1", 3, "."); 30 | * 31 | * console.log(padded); // '..1' 32 | * ``` 33 | */ 34 | export function leftPadMin(str: string, length: number, padWith = ' '): string { 35 | const strippedLength = stripAsni(str).length; 36 | 37 | if (strippedLength > length) 38 | throw new Error('String length is greater than the length provided.'); 39 | 40 | return padWith.repeat(length - strippedLength) + str; 41 | } 42 | 43 | /** Adds the `padWith` (default `' '`) to the string the amount of times specified by the `space` argument 44 | * 45 | * @param str String to add padding to 46 | * @param space Whitespace to add 47 | * @param padWith Character to use to pad the string 48 | * @returns 49 | * 50 | * ## Usage 51 | * ```ts 52 | * const padded = rightPad("Hello", 3, "."); 53 | * 54 | * console.log(padded); // 'Hello...' 55 | * ``` 56 | */ 57 | export function rightPad(str: string, space: number, padWith = ' '): string { 58 | return str + padWith.repeat(space); 59 | } 60 | 61 | /** Adds the `padWith` until the string length matches the `length` 62 | * 63 | * @param str 64 | * @param length 65 | * @param padWith 66 | * 67 | * ## Usage 68 | * ```ts 69 | * const padded = rightPadMin("1", 3, "."); 70 | * 71 | * console.log(padded); // '1..' 72 | * ``` 73 | */ 74 | export function rightPadMin(str: string, length: number, padWith = ' '): string { 75 | const strippedLength = stripAsni(str).length; 76 | 77 | if (strippedLength > length) 78 | throw new Error('String length is greater than the length provided.'); 79 | 80 | return str + padWith.repeat(length - strippedLength); 81 | } 82 | 83 | /** Pads the string with the `padWith` so that it appears in the center of a new string with the provided length. 84 | * 85 | * @param str 86 | * @param length 87 | * @param padWith 88 | * @returns 89 | * 90 | * ## Usage 91 | * ```ts 92 | * const str = "Hello, World!"; 93 | * 94 | * const padded = centerPad(str, str.length + 4); 95 | * 96 | * console.log(padded); // ' Hello, World! ' 97 | * ``` 98 | */ 99 | export function centerPad(str: string, length: number, padWith = ' '): string { 100 | const strippedLength = stripAsni(str).length; 101 | 102 | if (strippedLength > length) { 103 | throw new Error('String length is greater than the length provided.'); 104 | } 105 | 106 | const overflow = length - strippedLength; 107 | 108 | const paddingLeft = Math.floor(overflow / 2); 109 | 110 | const paddingRight = Math.ceil(overflow / 2); 111 | 112 | return padWith.repeat(paddingLeft) + str + padWith.repeat(paddingRight); 113 | } 114 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", 3 | "formatter": { 4 | "enabled": true, 5 | "formatWithErrors": false, 6 | "indentStyle": "tab", 7 | "indentWidth": 4, 8 | "lineEnding": "lf", 9 | "lineWidth": 100, 10 | "attributePosition": "auto" 11 | }, 12 | "files": { 13 | "ignore": ["dist", "node_modules", "docs", "registry.json", "coverage"] 14 | }, 15 | "organizeImports": { "enabled": true }, 16 | "linter": { 17 | "enabled": true, 18 | "rules": { 19 | "recommended": true, 20 | "complexity": { 21 | "noExtraBooleanCast": "error", 22 | "noMultipleSpacesInRegularExpressionLiterals": "error", 23 | "noUselessCatch": "error", 24 | "noUselessTypeConstraint": "error", 25 | "noWith": "error" 26 | }, 27 | "correctness": { 28 | "noConstAssign": "error", 29 | "noConstantCondition": "error", 30 | "noEmptyCharacterClassInRegex": "error", 31 | "noEmptyPattern": "error", 32 | "noGlobalObjectCalls": "error", 33 | "noInvalidConstructorSuper": "error", 34 | "noInvalidNewBuiltin": "error", 35 | "noNonoctalDecimalEscape": "error", 36 | "noPrecisionLoss": "error", 37 | "noSelfAssign": "error", 38 | "noSetterReturn": "error", 39 | "noSwitchDeclarations": "error", 40 | "noUndeclaredVariables": "error", 41 | "noUnreachable": "error", 42 | "noUnreachableSuper": "error", 43 | "noUnsafeFinally": "error", 44 | "noUnsafeOptionalChaining": "error", 45 | "noUnusedLabels": "error", 46 | "noUnusedPrivateClassMembers": "error", 47 | "noUnusedVariables": "error", 48 | "useArrayLiterals": "off", 49 | "useIsNan": "error", 50 | "useValidForDirection": "error", 51 | "useYield": "error" 52 | }, 53 | "style": { "noNamespace": "error", "useAsConstAssertion": "error" }, 54 | "suspicious": { 55 | "noConsoleLog": "error", 56 | "noAsyncPromiseExecutor": "error", 57 | "noCatchAssign": "error", 58 | "noClassAssign": "error", 59 | "noCompareNegZero": "error", 60 | "noControlCharactersInRegex": "error", 61 | "noDebugger": "error", 62 | "noDuplicateCase": "error", 63 | "noDuplicateClassMembers": "error", 64 | "noDuplicateObjectKeys": "error", 65 | "noDuplicateParameters": "error", 66 | "noEmptyBlockStatements": "error", 67 | "noExplicitAny": "error", 68 | "noExtraNonNullAssertion": "error", 69 | "noFallthroughSwitchClause": "error", 70 | "noFunctionAssign": "error", 71 | "noGlobalAssign": "error", 72 | "noImportAssign": "error", 73 | "noMisleadingCharacterClass": "error", 74 | "noMisleadingInstantiator": "error", 75 | "noPrototypeBuiltins": "error", 76 | "noRedeclare": "error", 77 | "noShadowRestrictedNames": "error", 78 | "noUnsafeDeclarationMerging": "error", 79 | "noUnsafeNegation": "error", 80 | "useGetterReturn": "error", 81 | "useNamespaceKeyword": "error", 82 | "useValidTypeof": "error" 83 | } 84 | }, 85 | "ignore": ["**/dist/*", "**/node_modules/*"] 86 | }, 87 | "javascript": { 88 | "formatter": { 89 | "jsxQuoteStyle": "double", 90 | "quoteProperties": "asNeeded", 91 | "trailingCommas": "es5", 92 | "semicolons": "always", 93 | "arrowParentheses": "always", 94 | "bracketSpacing": true, 95 | "bracketSameLine": false, 96 | "quoteStyle": "single", 97 | "attributePosition": "auto" 98 | }, 99 | "globals": [] 100 | }, 101 | "overrides": [ 102 | { 103 | "include": ["*.md"], 104 | "formatter": { 105 | "indentStyle": "space", 106 | "indentWidth": 2, 107 | "lineWidth": 100 108 | } 109 | } 110 | ] 111 | } 112 | -------------------------------------------------------------------------------- /src/ts/url.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import * as url from './url'; 3 | 4 | describe('join', () => { 5 | it('correctly joins url segments', () => { 6 | expect(url.join('https://example.com/', '/api', '/examples/')).toBe( 7 | 'https://example.com/api/examples' 8 | ); 9 | }); 10 | 11 | it('handles empty segments', () => { 12 | expect(url.join('https://example.com', '', 'examples')).toBe( 13 | 'https://example.com/examples' 14 | ); 15 | }); 16 | 17 | it('handles single segment', () => { 18 | expect(url.join('https://example.com')).toBe('https://example.com'); 19 | }); 20 | }); 21 | 22 | describe('removeLeadingAndTrailingSlash', () => { 23 | it('removes both leading and trailing slashes', () => { 24 | expect(url.removeLeadingAndTrailingSlash('/example/')).toBe('example'); 25 | }); 26 | 27 | it('handles string with no slashes', () => { 28 | expect(url.removeLeadingAndTrailingSlash('example')).toBe('example'); 29 | }); 30 | 31 | it('handles string with only leading slash', () => { 32 | expect(url.removeLeadingAndTrailingSlash('/example')).toBe('example'); 33 | }); 34 | 35 | it('handles string with only trailing slash', () => { 36 | expect(url.removeLeadingAndTrailingSlash('example/')).toBe('example'); 37 | }); 38 | 39 | it('handles empty string', () => { 40 | expect(url.removeLeadingAndTrailingSlash('')).toBe(''); 41 | }); 42 | }); 43 | 44 | describe('addLeadingAndTrailingSlash', () => { 45 | it('adds both leading and trailing slashes', () => { 46 | expect(url.addLeadingAndTrailingSlash('example')).toBe('/example/'); 47 | }); 48 | 49 | it('handles string that already has slashes', () => { 50 | expect(url.addLeadingAndTrailingSlash('/example/')).toBe('/example/'); 51 | }); 52 | 53 | it('handles empty string', () => { 54 | expect(url.addLeadingAndTrailingSlash('')).toBe('//'); 55 | }); 56 | }); 57 | 58 | describe('removeLeadingSlash', () => { 59 | it('removes leading slash', () => { 60 | expect(url.removeLeadingSlash('/example')).toBe('example'); 61 | }); 62 | 63 | it('handles string without leading slash', () => { 64 | expect(url.removeLeadingSlash('example')).toBe('example'); 65 | }); 66 | 67 | it('handles empty string', () => { 68 | expect(url.removeLeadingSlash('')).toBe(''); 69 | }); 70 | }); 71 | 72 | describe('addLeadingSlash', () => { 73 | it('adds leading slash', () => { 74 | expect(url.addLeadingSlash('example')).toBe('/example'); 75 | }); 76 | 77 | it('handles string that already has leading slash', () => { 78 | expect(url.addLeadingSlash('/example')).toBe('/example'); 79 | }); 80 | 81 | it('handles empty string', () => { 82 | expect(url.addLeadingSlash('')).toBe('/'); 83 | }); 84 | }); 85 | 86 | describe('removeTrailingSlash', () => { 87 | it('removes trailing slash', () => { 88 | expect(url.removeTrailingSlash('example/')).toBe('example'); 89 | }); 90 | 91 | it('handles string without trailing slash', () => { 92 | expect(url.removeTrailingSlash('example')).toBe('example'); 93 | }); 94 | 95 | it('handles empty string', () => { 96 | expect(url.removeTrailingSlash('')).toBe(''); 97 | }); 98 | }); 99 | 100 | describe('addTrailingSlash', () => { 101 | it('adds trailing slash', () => { 102 | expect(url.addTrailingSlash('example')).toBe('example/'); 103 | }); 104 | 105 | it('handles string that already has trailing slash', () => { 106 | expect(url.addTrailingSlash('example/')).toBe('example/'); 107 | }); 108 | 109 | it('handles empty string', () => { 110 | expect(url.addTrailingSlash('')).toBe('/'); 111 | }); 112 | }); 113 | 114 | describe('upOneLevel', () => { 115 | it('removes the last segment', () => { 116 | expect(url.upOneLevel('/first/second')).toBe('/first'); 117 | }); 118 | 119 | it('will not go up from root', () => { 120 | expect(url.upOneLevel('/')).toBe('/'); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/ts/url.ts: -------------------------------------------------------------------------------- 1 | /** Joins the segments into a single url correctly handling leading and trailing slashes in each segment. 2 | * 3 | * @param segments 4 | * @returns 5 | * 6 | * ## Usage 7 | * ```ts 8 | * const url = join('https://example.com', '', 'api/', '/examples/'); 9 | * 10 | * console.log(url); // https://example.com/api/examples 11 | * ``` 12 | */ 13 | export function join(...segments: string[]): string { 14 | return segments 15 | .map((s) => removeLeadingAndTrailingSlash(s)) 16 | .filter(Boolean) 17 | .join('/'); 18 | } 19 | 20 | /** Removes the leading and trailing slash from the segment (if they exist) 21 | * 22 | * @param segment 23 | * @returns 24 | * 25 | * ## Usage 26 | * ```ts 27 | * const segment = removeLeadingAndTrailingSlash('/example/'); 28 | * 29 | * console.log(segment); // 'example' 30 | * ``` 31 | */ 32 | export function removeLeadingAndTrailingSlash(segment: string): string { 33 | const newSegment = removeLeadingSlash(segment); 34 | return removeTrailingSlash(newSegment); 35 | } 36 | 37 | /** Adds a leading and trailing to the beginning and end of the segment (if it doesn't already exist) 38 | * 39 | * @param segment 40 | * @returns 41 | * 42 | * ## Usage 43 | * ```ts 44 | * const segment = addLeadingAndTrailingSlash('example'); 45 | * 46 | * console.log(segment); // '/example/' 47 | * ``` 48 | */ 49 | export function addLeadingAndTrailingSlash(segment: string): string { 50 | // this is a weird case so feel free to handle it however you think it makes the most sense 51 | if (segment === '') return '//'; 52 | 53 | const newSegment = addLeadingSlash(segment); 54 | return addTrailingSlash(newSegment); 55 | } 56 | 57 | /** Removes the leading slash from the beginning of the segment (if it exists) 58 | * 59 | * @param segment 60 | * @returns 61 | * 62 | * ## Usage 63 | * ```ts 64 | * const segment = removeLeadingSlash('/example'); 65 | * 66 | * console.log(segment); // 'example' 67 | * ``` 68 | */ 69 | export function removeLeadingSlash(segment: string): string { 70 | let newSegment = segment; 71 | if (newSegment.startsWith('/')) { 72 | newSegment = newSegment.slice(1); 73 | } 74 | 75 | return newSegment; 76 | } 77 | 78 | /** Adds a leading slash to the beginning of the segment (if it doesn't already exist) 79 | * 80 | * @param segment 81 | * @returns 82 | * 83 | * ## Usage 84 | * ```ts 85 | * const segment = addLeadingSlash('example'); 86 | * 87 | * console.log(segment); // '/example' 88 | * ``` 89 | */ 90 | export function addLeadingSlash(segment: string): string { 91 | let newSegment = segment; 92 | if (!newSegment.startsWith('/')) { 93 | newSegment = `/${newSegment}`; 94 | } 95 | 96 | return newSegment; 97 | } 98 | 99 | /** Removes the trailing slash from the end of the segment (if it exists) 100 | * 101 | * @param segment 102 | * @returns 103 | * 104 | * ## Usage 105 | * ```ts 106 | * const segment = removeTrailingSlash('example/'); 107 | * 108 | * console.log(segment); // 'example' 109 | * ``` 110 | */ 111 | export function removeTrailingSlash(segment: string): string { 112 | let newSegment = segment; 113 | if (newSegment.endsWith('/')) { 114 | newSegment = newSegment.slice(0, newSegment.length - 1); 115 | } 116 | 117 | return newSegment; 118 | } 119 | 120 | /** Adds a trailing slash to the end of the segment (if it doesn't already exist) 121 | * 122 | * @param segment 123 | * @returns 124 | * 125 | * ## Usage 126 | * ```ts 127 | * const segment = addTrailingSlash('example'); 128 | * 129 | * console.log(segment); // 'example/' 130 | * ``` 131 | */ 132 | export function addTrailingSlash(segment: string): string { 133 | let newSegment = segment; 134 | if (!newSegment.endsWith('/')) { 135 | newSegment = `${newSegment}/`; 136 | } 137 | 138 | return newSegment; 139 | } 140 | 141 | /** Removes the last segment of the url. 142 | * 143 | * @param url 144 | * 145 | * ## Usage 146 | * ```ts 147 | * const url = upOneLevel('/first/second'); 148 | * 149 | * console.log(url); // '/first' 150 | * ``` 151 | */ 152 | export function upOneLevel(url: string): string { 153 | if (url === '/') return url; 154 | 155 | const lastIndex = removeTrailingSlash(url).lastIndexOf('/'); 156 | 157 | return url.slice(0, url.length - lastIndex - 1); 158 | } 159 | -------------------------------------------------------------------------------- /src/ts/casing.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import * as casing from './casing'; 3 | 4 | describe('pascalToSnake', () => { 5 | it('correctly converts to snake_case', () => { 6 | expect(casing.pascalToSnake('HelloWorld')).toBe('hello_world'); 7 | expect(casing.pascalToSnake('Hello')).toBe('hello'); 8 | expect(casing.pascalToSnake('_Hello')).toBe('_hello'); 9 | expect(casing.pascalToSnake('HelloH')).toBe('hello_h'); 10 | }); 11 | 12 | it('does not separate back to back capitals', () => { 13 | expect(casing.pascalToSnake('APIKey')).toBe('api_key'); 14 | }); 15 | }); 16 | 17 | describe('camelToSnake', () => { 18 | it('correctly converts to snake_case', () => { 19 | expect(casing.camelToSnake('helloWorld')).toBe('hello_world'); 20 | expect(casing.camelToSnake('hello')).toBe('hello'); 21 | expect(casing.camelToSnake('_hello')).toBe('_hello'); 22 | expect(casing.camelToSnake('helloH')).toBe('hello_h'); 23 | }); 24 | 25 | it('does not separate back to back capitals', () => { 26 | expect(casing.camelToSnake('APIKey')).toBe('api_key'); 27 | }); 28 | }); 29 | 30 | describe('pascalToKebab', () => { 31 | it('correctly converts to kebab-case', () => { 32 | expect(casing.pascalToKebab('HelloWorld')).toBe('hello-world'); 33 | expect(casing.pascalToKebab('Hello')).toBe('hello'); 34 | expect(casing.pascalToKebab('HelloH')).toBe('hello-h'); 35 | }); 36 | 37 | it('does not separate back to back capitals', () => { 38 | expect(casing.pascalToKebab('APIKey')).toBe('api-key'); 39 | }); 40 | }); 41 | 42 | describe('camelToKebab', () => { 43 | it('correctly converts to kebab-case', () => { 44 | expect(casing.camelToKebab('helloWorld')).toBe('hello-world'); 45 | expect(casing.camelToKebab('hello')).toBe('hello'); 46 | expect(casing.camelToKebab('helloH')).toBe('hello-h'); 47 | }); 48 | 49 | it('does not separate back to back capitals', () => { 50 | expect(casing.camelToKebab('APIKey')).toBe('api-key'); 51 | }); 52 | }); 53 | 54 | describe('pascalToCamel', () => { 55 | it('correctly converts to camelCase', () => { 56 | expect(casing.pascalToCamel('HelloWorld')).toBe('helloWorld'); 57 | expect(casing.pascalToCamel('Hello')).toBe('hello'); 58 | }); 59 | }); 60 | 61 | describe('camelToPascal', () => { 62 | it('correctly converts to PascalCase', () => { 63 | expect(casing.camelToPascal('helloWorld')).toBe('HelloWorld'); 64 | expect(casing.camelToPascal('hello')).toBe('Hello'); 65 | }); 66 | }); 67 | 68 | describe('snakeToCamel', () => { 69 | it('correctly converts to camelCase', () => { 70 | expect(casing.snakeToCamel('hello_world')).toBe('helloWorld'); 71 | expect(casing.snakeToCamel('HELLO_WORLD')).toBe('helloWorld'); 72 | expect(casing.snakeToCamel('hello')).toBe('hello'); 73 | }); 74 | 75 | it('leaves leading underscore', () => { 76 | expect(casing.snakeToCamel('_hello_world')).toBe('_helloWorld'); 77 | expect(casing.snakeToCamel('___hello_world')).toBe('___helloWorld'); 78 | }); 79 | 80 | it('Correctly handles trailing underscore', () => { 81 | expect(casing.snakeToCamel('_hello_world_')).toBe('_helloWorld_'); 82 | }); 83 | }); 84 | 85 | describe('snakeToPascal', () => { 86 | it('correctly converts to PascalCase', () => { 87 | expect(casing.snakeToPascal('hello_world')).toBe('HelloWorld'); 88 | expect(casing.snakeToPascal('HELLO_WORLD')).toBe('HelloWorld'); 89 | expect(casing.snakeToPascal('hello')).toBe('Hello'); 90 | }); 91 | 92 | it('leaves leading underscore', () => { 93 | expect(casing.snakeToPascal('_hello_world')).toBe('_HelloWorld'); 94 | expect(casing.snakeToPascal('___hello_world')).toBe('___HelloWorld'); 95 | }); 96 | 97 | it('Correctly handles trailing underscore', () => { 98 | expect(casing.snakeToPascal('_hello_world_')).toBe('_HelloWorld_'); 99 | }); 100 | }); 101 | 102 | describe('kebabToCamel', () => { 103 | it('correctly converts to camelCase', () => { 104 | expect(casing.kebabToCamel('hello-world')).toBe('helloWorld'); 105 | expect(casing.kebabToCamel('hello')).toBe('hello'); 106 | }); 107 | 108 | it('Removes trailing dash', () => { 109 | expect(casing.kebabToCamel('hello-world-')).toBe('helloWorld'); 110 | }); 111 | }); 112 | 113 | describe('kebabToPascal', () => { 114 | it('correctly converts to PascalCase', () => { 115 | expect(casing.kebabToPascal('hello-world')).toBe('HelloWorld'); 116 | expect(casing.kebabToPascal('hello')).toBe('Hello'); 117 | }); 118 | 119 | it('Removes trailing dash', () => { 120 | expect(casing.kebabToPascal('hello-world-')).toBe('HelloWorld'); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # std 2 | 3 | [![jsrepo](https://jsrepo.com/badges/@ieedan/std)](https://jsrepo.com/@ieedan/std) [![jsrepo](https://jsrepo.com/badges/@ieedan/std/dm)](https://jsrepo.com/@ieedan/std) 4 | 5 | Types and utility functions brokered with [jsrepo](https://jsrepo.dev). If I have wrote it twice it's probably here. 6 | 7 | ## Setup 8 | 9 | Add as a repo in config: 10 | 11 | ```bash 12 | pnpm dlx jsrepo init @ieedan/std 13 | ``` 14 | 15 | then add your blocks: 16 | 17 | ```bash 18 | pnpm dlx jsrepo add ts/result 19 | ``` 20 | 21 | # Blocks 22 | 23 | | Block | Status | 24 | | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | 25 | | [ts/result](https://ieedan.github.io/std/classes/result.Result.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 26 | | [ts/array](https://ieedan.github.io/std/modules/array.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 27 | | [ts/casing](https://ieedan.github.io/std/modules/casing.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 28 | | [ts/dispatcher](https://ieedan.github.io/std/classes/dispatcher.Dispatcher.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 29 | | [ts/ipv4-address](https://ieedan.github.io/std/modules/ipv4-address.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 30 | | [ts/is-letter](https://ieedan.github.io/std/functions/is-letter.isLetter.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 31 | | [ts/is-number](https://ieedan.github.io/std/functions/is-number.isNumber.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 32 | | [ts/lines](https://ieedan.github.io/std/modules/lines.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 33 | | [ts/matcher](https://ieedan.github.io/std/classes/matcher.Matcher.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 34 | | [ts/math](https://ieedan.github.io/std/modules/math.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 35 | | [ts/pad](https://ieedan.github.io/std/functions/pad.leftPad.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 36 | | [ts/perishable-list](https://ieedan.github.io/std/classes/perishable-list.PerishableList.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 37 | | [ts/promises](https://ieedan.github.io/std/modules/promises.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 38 | | [ts/rand](https://ieedan.github.io/std/functions/rand.rand.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 39 | | [ts/sleep](https://ieedan.github.io/std/functions/sleep.sleep.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 40 | | [ts/stopwatch](https://ieedan.github.io/std/classes/stopwatch.StopWatch.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 41 | | [ts/strings](https://ieedan.github.io/std/modules/strings.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 42 | | [ts/time](https://ieedan.github.io/std/functions/time.formatDuration.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 43 | | [ts/truncate](https://ieedan.github.io/std/functions/truncate.truncate.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 44 | | [ts/types](https://ieedan.github.io/std/modules/types.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 45 | | [ts/url](https://ieedan.github.io/std/functions/url.join.html) | ![Tests](https://raw.githubusercontent.com/ieedan/std/refs/heads/main/badges/coverage-total.svg) | 46 | -------------------------------------------------------------------------------- /jsrepo.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'jsrepo'; 2 | import { repository } from 'jsrepo/outputs'; 3 | 4 | export default defineConfig({ 5 | registry: { 6 | name: '@ieedan/std', 7 | version: 'package', 8 | authors: ['Aidan Bleser'], 9 | bugs: 'https://github.com/ieedan/std/issues', 10 | description: 'Fully tested and documented TypeScript utilities brokered by jsrepo.', 11 | homepage: 'https://ieedan.github.io/std/', 12 | repository: 'https://github.com/ieedan/std', 13 | tags: ['typescript', 'std', 'utilities'], 14 | outputs: [repository({ format: true })], 15 | items: [ 16 | { 17 | name: 'result', 18 | type: 'util', 19 | files: [ 20 | { path: 'src/ts/result.ts' }, 21 | { path: 'src/ts/result.test.ts', role: 'test' }, 22 | ], 23 | }, 24 | { 25 | name: 'array', 26 | type: 'util', 27 | files: [ 28 | { path: 'src/ts/array.ts' }, 29 | { path: 'src/ts/array.test.ts', role: 'test' }, 30 | ], 31 | }, 32 | { 33 | name: 'casing', 34 | type: 'util', 35 | files: [ 36 | { path: 'src/ts/casing.ts' }, 37 | { path: 'src/ts/casing.test.ts', role: 'test' }, 38 | ], 39 | }, 40 | { 41 | name: 'dispatcher', 42 | type: 'util', 43 | files: [ 44 | { path: 'src/ts/dispatcher.ts' }, 45 | { path: 'src/ts/dispatcher.test.ts', role: 'test' }, 46 | ], 47 | }, 48 | { 49 | name: 'ipv4-address', 50 | type: 'util', 51 | files: [ 52 | { path: 'src/ts/ipv4-address.ts' }, 53 | { path: 'src/ts/ipv4-address.test.ts', role: 'test' }, 54 | ], 55 | }, 56 | { 57 | name: 'is-letter', 58 | type: 'util', 59 | files: [ 60 | { path: 'src/ts/is-letter.ts' }, 61 | { path: 'src/ts/is-letter.test.ts', role: 'test' }, 62 | ], 63 | }, 64 | { 65 | name: 'is-number', 66 | type: 'util', 67 | files: [ 68 | { path: 'src/ts/is-number.ts' }, 69 | { path: 'src/ts/is-number.test.ts', role: 'test' }, 70 | ], 71 | }, 72 | { 73 | name: 'math', 74 | type: 'util', 75 | files: [ 76 | { path: 'src/ts/math/circle.ts' }, 77 | { path: 'src/ts/math/circle.test.ts', role: 'test' }, 78 | { path: 'src/ts/math/conversions.ts' }, 79 | { path: 'src/ts/math/conversions.test.ts', role: 'test' }, 80 | { path: 'src/ts/math/fractions.ts' }, 81 | { path: 'src/ts/math/fractions.test.ts', role: 'test' }, 82 | { path: 'src/ts/math/gcf.ts' }, 83 | { path: 'src/ts/math/gcf.test.ts', role: 'test' }, 84 | { path: 'src/ts/math/triangles.ts' }, 85 | { path: 'src/ts/math/triangles.test.ts', role: 'test' }, 86 | { path: 'src/ts/math/types.ts' }, 87 | { path: 'src/ts/math/index.ts' }, 88 | ], 89 | }, 90 | { 91 | name: 'pad', 92 | type: 'util', 93 | files: [{ path: 'src/ts/pad.ts' }, { path: 'src/ts/pad.test.ts', role: 'test' }], 94 | }, 95 | { 96 | name: 'perishable-list', 97 | type: 'util', 98 | files: [ 99 | { path: 'src/ts/perishable-list.ts' }, 100 | { path: 'src/ts/perishable-list.test.ts', role: 'test' }, 101 | ], 102 | }, 103 | { 104 | name: 'promises', 105 | type: 'util', 106 | files: [ 107 | { path: 'src/ts/promises.ts' }, 108 | { path: 'src/ts/promises.test.ts', role: 'test' }, 109 | ], 110 | }, 111 | { 112 | name: 'rand', 113 | type: 'util', 114 | files: [{ path: 'src/ts/rand.ts' }, { path: 'src/ts/rand.test.ts', role: 'test' }], 115 | }, 116 | { 117 | name: 'sleep', 118 | type: 'util', 119 | files: [ 120 | { path: 'src/ts/sleep.ts' }, 121 | { path: 'src/ts/sleep.test.ts', role: 'test' }, 122 | ], 123 | }, 124 | { 125 | name: 'stopwatch', 126 | type: 'util', 127 | files: [ 128 | { path: 'src/ts/stopwatch.ts' }, 129 | { path: 'src/ts/stopwatch.test.ts', role: 'test' }, 130 | ], 131 | }, 132 | { 133 | name: 'strings', 134 | type: 'util', 135 | files: [ 136 | { path: 'src/ts/strings.ts' }, 137 | { path: 'src/ts/strings.test.ts', role: 'test' }, 138 | ], 139 | }, 140 | { 141 | name: 'time', 142 | type: 'util', 143 | files: [{ path: 'src/ts/time.ts' }, { path: 'src/ts/time.test.ts', role: 'test' }], 144 | }, 145 | { 146 | name: 'truncate', 147 | type: 'util', 148 | files: [ 149 | { path: 'src/ts/truncate.ts' }, 150 | { path: 'src/ts/truncate.test.ts', role: 'test' }, 151 | ], 152 | }, 153 | { 154 | name: 'types', 155 | type: 'util', 156 | files: [{ path: 'src/ts/types.ts' }], 157 | }, 158 | { 159 | name: 'url', 160 | type: 'util', 161 | files: [{ path: 'src/ts/url.ts' }, { path: 'src/ts/url.test.ts', role: 'test' }], 162 | }, 163 | { 164 | name: 'Cursor Rule', 165 | type: 'rule', 166 | add: 'optionally-on-init', 167 | dependencyResolution: 'manual', 168 | files: [ 169 | { 170 | path: 'rules/typescript-utility-functions.mdc', 171 | target: '.cursor/rules/typescript-utility-functions.mdc', 172 | }, 173 | ], 174 | }, 175 | ], 176 | }, 177 | }); 178 | -------------------------------------------------------------------------------- /src/ts/casing.ts: -------------------------------------------------------------------------------- 1 | import { isLetter } from './is-letter'; 2 | 3 | /** Converts a `camelCase` string to a `snake_case` string 4 | * 5 | * @param str 6 | * @returns 7 | * 8 | * ## Usage 9 | * ```ts 10 | * camelToSnake('helloWorld'); // hello_world 11 | * ``` 12 | */ 13 | export function camelToSnake(str: string): string { 14 | let newStr = ''; 15 | 16 | for (let i = 0; i < str.length; i++) { 17 | // is uppercase letter 18 | if (isLetter(str[i]) && str[i].toUpperCase() === str[i]) { 19 | let l = i; 20 | 21 | while (l < str.length && isLetter(str[l]) && str[l].toUpperCase() === str[l]) { 22 | l++; 23 | } 24 | 25 | newStr += `${str.slice(i, l - 1).toLocaleLowerCase()}_${str[l - 1].toLocaleLowerCase()}`; 26 | 27 | i = l - 1; 28 | 29 | continue; 30 | } 31 | 32 | newStr += str[i].toLocaleLowerCase(); 33 | } 34 | 35 | return newStr; 36 | } 37 | 38 | /** Converts a `PascalCase` string to a `snake_case` string 39 | * 40 | * @param str 41 | * @returns 42 | * 43 | * ## Usage 44 | * ```ts 45 | * camelToSnake('HelloWorld'); // hello_world 46 | * ``` 47 | */ 48 | export function pascalToSnake(str: string): string { 49 | let newStr = ''; 50 | 51 | let firstLetter: number | undefined; 52 | 53 | for (let i = 0; i < str.length; i++) { 54 | if (firstLetter === undefined && isLetter(str[i])) { 55 | firstLetter = i; 56 | } 57 | 58 | // is uppercase letter (ignoring the first) 59 | if ( 60 | firstLetter !== undefined && 61 | i > firstLetter && 62 | isLetter(str[i]) && 63 | str[i].toUpperCase() === str[i] 64 | ) { 65 | let l = i; 66 | 67 | while (l < str.length && isLetter(str[l]) && str[l].toUpperCase() === str[l]) { 68 | l++; 69 | } 70 | 71 | newStr += `${str.slice(i, l - 1).toLocaleLowerCase()}_${str[l - 1].toLocaleLowerCase()}`; 72 | 73 | i = l - 1; 74 | 75 | continue; 76 | } 77 | 78 | newStr += str[i].toLocaleLowerCase(); 79 | } 80 | 81 | return newStr; 82 | } 83 | 84 | /** Converts a `camelCase` string to a `kebab-case` string 85 | * 86 | * @param str 87 | * @returns 88 | * 89 | * ## Usage 90 | * ```ts 91 | * camelToSnake('helloWorld'); // hello-world 92 | * ``` 93 | */ 94 | export function camelToKebab(str: string): string { 95 | let newStr = ''; 96 | 97 | for (let i = 0; i < str.length; i++) { 98 | // is uppercase letter 99 | if (i > 0 && isLetter(str[i]) && str[i].toUpperCase() === str[i]) { 100 | let l = i; 101 | 102 | while (l < str.length && isLetter(str[l]) && str[l].toUpperCase() === str[l]) { 103 | l++; 104 | } 105 | 106 | newStr += `${str.slice(i, l - 1).toLocaleLowerCase()}-${str[l - 1].toLocaleLowerCase()}`; 107 | 108 | i = l - 1; 109 | 110 | continue; 111 | } 112 | 113 | newStr += str[i].toLocaleLowerCase(); 114 | } 115 | 116 | return newStr; 117 | } 118 | 119 | /** Converts a `PascalCase` string to a `kebab-case` string 120 | * 121 | * @param str 122 | * @returns 123 | * 124 | * ## Usage 125 | * ```ts 126 | * camelToSnake('HelloWorld'); // hello-world 127 | * ``` 128 | */ 129 | export function pascalToKebab(str: string): string { 130 | let newStr = ''; 131 | 132 | for (let i = 0; i < str.length; i++) { 133 | // is uppercase letter (ignoring the first) 134 | if (i > 0 && isLetter(str[i]) && str[i].toUpperCase() === str[i]) { 135 | let l = i; 136 | 137 | while (l < str.length && isLetter(str[l]) && str[l].toUpperCase() === str[l]) { 138 | l++; 139 | } 140 | 141 | newStr += `${str.slice(i, l - 1).toLocaleLowerCase()}-${str[l - 1].toLocaleLowerCase()}`; 142 | 143 | i = l - 1; 144 | 145 | continue; 146 | } 147 | 148 | newStr += str[i].toLocaleLowerCase(); 149 | } 150 | 151 | return newStr; 152 | } 153 | 154 | /** Converts a `camelCase` string to a `PascalCase` string (makes first letter lowercase) 155 | * 156 | * @param str 157 | * @returns 158 | * 159 | * ## Usage 160 | * ```ts 161 | * camelToPascal('helloWorld'); // HelloWorld 162 | * ``` 163 | */ 164 | export function camelToPascal(str: string): string { 165 | return `${str[0].toLocaleUpperCase()}${str.slice(1)}`; 166 | } 167 | 168 | /** Converts a `PascalCase` string to a `camelCase` string (makes first letter uppercase) 169 | * 170 | * @param str 171 | * @returns 172 | * 173 | * ## Usage 174 | * ```ts 175 | * camelToPascal('HelloWorld'); // helloWorld 176 | * ``` 177 | */ 178 | export function pascalToCamel(str: string): string { 179 | return `${str[0].toLocaleLowerCase()}${str.slice(1)}`; 180 | } 181 | 182 | /** Converts a `snake_case` string to a `PascalCase` string 183 | * 184 | * 185 | * @param str 186 | * @returns 187 | * 188 | * ## Usage 189 | * ```ts 190 | * snakeToPascal('hello_world'); // HelloWorld 191 | * snakeToPascal('HELLO_WORLD'); // HelloWorld 192 | * ``` 193 | */ 194 | export function snakeToPascal(str: string): string { 195 | let newStr = ''; 196 | 197 | let firstLetter = true; 198 | 199 | for (let i = 0; i < str.length; i++) { 200 | // capitalize first letter 201 | if (firstLetter && isLetter(str[i])) { 202 | firstLetter = false; 203 | newStr += str[i].toUpperCase(); 204 | continue; 205 | } 206 | 207 | // capitalize first after a _ (ignoring the first) 208 | if (!firstLetter && str[i] === '_') { 209 | i++; 210 | if (i <= str.length - 1) { 211 | newStr += str[i].toUpperCase(); 212 | } else { 213 | newStr += '_'; 214 | } 215 | continue; 216 | } 217 | 218 | newStr += str[i].toLocaleLowerCase(); 219 | } 220 | 221 | return newStr; 222 | } 223 | 224 | /** Converts a `snake_case` string to a `camelCase` string 225 | * 226 | * 227 | * @param str 228 | * @returns 229 | * 230 | * ## Usage 231 | * ```ts 232 | * snakeToCamel('hello_world'); // helloWorld 233 | * snakeToCamel('HELLO_WORLD'); // helloWorld 234 | * ``` 235 | */ 236 | export function snakeToCamel(str: string): string { 237 | let newStr = ''; 238 | 239 | let firstLetter = true; 240 | 241 | for (let i = 0; i < str.length; i++) { 242 | // capitalize first letter 243 | if (firstLetter && isLetter(str[i])) { 244 | firstLetter = false; 245 | newStr += str[i].toLowerCase(); 246 | continue; 247 | } 248 | 249 | // capitalize first after a _ (ignoring the first) 250 | if (!firstLetter && str[i] === '_') { 251 | i++; 252 | if (i <= str.length - 1) { 253 | newStr += str[i].toUpperCase(); 254 | } else { 255 | newStr += '_'; 256 | } 257 | continue; 258 | } 259 | 260 | newStr += str[i].toLocaleLowerCase(); 261 | } 262 | 263 | return newStr; 264 | } 265 | 266 | /** Converts a `kebab-case` string to a `PascalCase` string 267 | * 268 | * @param str 269 | * @returns 270 | * 271 | * ## Usage 272 | * ```ts 273 | * kebabToPascal('hello-world'); // HelloWorld 274 | * ``` 275 | */ 276 | export function kebabToPascal(str: string): string { 277 | let newStr = ''; 278 | 279 | for (let i = 0; i < str.length; i++) { 280 | // capitalize first 281 | if (i === 0) { 282 | newStr += str[i].toUpperCase(); 283 | continue; 284 | } 285 | 286 | // capitalize first after a - 287 | if (str[i] === '-') { 288 | i++; 289 | if (i <= str.length - 1) { 290 | newStr += str[i].toUpperCase(); 291 | } 292 | continue; 293 | } 294 | 295 | newStr += str[i].toLocaleLowerCase(); 296 | } 297 | 298 | return newStr; 299 | } 300 | 301 | /** Converts a `kebab-case` string to a `camelCase` string 302 | * 303 | * 304 | * @param str 305 | * @returns 306 | * 307 | * ## Usage 308 | * ```ts 309 | * kebabToCamel('hello-world'); // helloWorld 310 | * ``` 311 | */ 312 | export function kebabToCamel(str: string): string { 313 | let newStr = ''; 314 | 315 | for (let i = 0; i < str.length; i++) { 316 | // capitalize first after a - 317 | if (str[i] === '-') { 318 | i++; 319 | if (i <= str.length - 1) { 320 | newStr += str[i].toUpperCase(); 321 | } 322 | continue; 323 | } 324 | 325 | newStr += str[i].toLocaleLowerCase(); 326 | } 327 | 328 | return newStr; 329 | } 330 | -------------------------------------------------------------------------------- /src/ts/result.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { Err, Ok, type Result } from './result'; 3 | 4 | const failingFunction = (err: E): Result => Err(err); 5 | 6 | const passingFunction = (val: T): Result => Ok(val); 7 | 8 | describe('Result', () => { 9 | // --- match --- 10 | 11 | it('Result.match: Expect pass value from match', () => { 12 | const expected = true; 13 | 14 | const res = passingFunction(expected).match( 15 | (val) => val, 16 | () => { 17 | throw new Error('This should not throw'); 18 | } 19 | ); 20 | 21 | expect(res).toBe(expected); 22 | }); 23 | 24 | it('Result.match: Expect fail value from match', () => { 25 | const expected = true; 26 | 27 | const res = failingFunction(expected).match( 28 | () => { 29 | throw new Error('This should not throw'); 30 | }, 31 | (err) => err 32 | ); 33 | 34 | expect(res).toBe(expected); 35 | }); 36 | 37 | // --- isOk / isErr --- 38 | 39 | it('Result.isOk / isErr: Expect correct `Ok` boolean assertions', () => { 40 | const result = passingFunction(undefined); 41 | 42 | expect(result.isOk()).toBe(true); 43 | expect(result.isErr()).toBe(false); 44 | }); 45 | 46 | it('Result.isOk / isErr: Expect correct `Err` boolean assertions', () => { 47 | const result = failingFunction(undefined); 48 | 49 | expect(result.isOk()).toBe(false); 50 | expect(result.isErr()).toBe(true); 51 | }); 52 | 53 | // --- unwrap --- 54 | 55 | it('Result.unwrap: Expect correct value', () => { 56 | const expected = 'Success'; 57 | 58 | const result = passingFunction(expected); 59 | 60 | expect(result.unwrap()).toBe(expected); 61 | }); 62 | 63 | it('Result.unwrap: Should throw if failed', () => { 64 | const result = failingFunction('oops!'); 65 | 66 | expect(() => result.unwrap()).toThrow(); 67 | }); 68 | 69 | // --- unwrapOr --- 70 | 71 | it('Result.unwrapOr: Expect correct value if err', () => { 72 | const expected = true; 73 | 74 | const result = failingFunction('oops!'); 75 | 76 | expect(result.unwrapOr(expected)).toBe(expected); 77 | }); 78 | 79 | it('Result.unwrapOr: Expect correct value is ok', () => { 80 | const expected = true; 81 | 82 | const result = passingFunction(expected); 83 | 84 | expect(result.unwrapOr(false)).toBe(expected); 85 | }); 86 | 87 | // --- unwrapOrElse --- 88 | 89 | it('Result.unwrapOrElse: Expect correct value if err', () => { 90 | const expected = true; 91 | 92 | const result = failingFunction('oops!'); 93 | 94 | expect(result.unwrapOrElse(() => expected)).toBe(expected); 95 | }); 96 | 97 | it('Result.unwrapOrElse: Expect correct value is ok', () => { 98 | const expected = true; 99 | 100 | const result = passingFunction(expected); 101 | 102 | expect(result.unwrapOrElse(() => false)).toBe(expected); 103 | }); 104 | 105 | // --- unwrapErr --- 106 | 107 | it('Result.unwrapErr: Expect correct error', () => { 108 | const expected = 'I failed!'; 109 | 110 | const result = failingFunction(expected); 111 | 112 | expect(result.unwrapErr()).toBe(expected); 113 | }); 114 | 115 | it('Result.unwrapErr: Should throw if passed', () => { 116 | const result = passingFunction(true); 117 | 118 | expect(() => result.unwrapErr()).toThrow(); 119 | }); 120 | 121 | // --- unwrapErrOr --- 122 | 123 | it('Result.unwrapErrOr: Expect correct error', () => { 124 | const expected = 'I failed!'; 125 | 126 | const result = passingFunction(expected); 127 | 128 | expect(result.unwrapErrOr(expected)).toBe(expected); 129 | }); 130 | 131 | it('Result.unwrapErrOr: Expect correct error on fail', () => { 132 | const expected = 'I failed!'; 133 | 134 | const result = failingFunction(expected); 135 | 136 | expect(result.unwrapErrOr(expected)).toBe(expected); 137 | }); 138 | 139 | // --- unwrapErrOrElse --- 140 | 141 | it('Result.unwrapErrOrElse: Expect correct value on err', () => { 142 | const expected = 'I failed!'; 143 | 144 | const result = failingFunction(expected); 145 | 146 | expect(result.unwrapErrOrElse(() => 'nope')).toBe(expected); 147 | }); 148 | 149 | it('Result.unwrapErrOrElse: Expect correct error on fail', () => { 150 | const expected = 'I failed!'; 151 | 152 | const result = passingFunction('nope'); 153 | 154 | expect(result.unwrapErrOrElse(() => expected)).toBe(expected); 155 | }); 156 | 157 | // --- expect --- 158 | 159 | it('Result.expect: Expect correct value', () => { 160 | const expected = 'Success'; 161 | 162 | const result = passingFunction(expected); 163 | 164 | expect(result.expect('Oh no!')).toBe(expected); 165 | }); 166 | 167 | it('Result.expect: Should throw if failed', () => { 168 | const result = failingFunction('oops!'); 169 | 170 | expect(() => result.expect('Oh no')).toThrow('Oh no'); 171 | }); 172 | 173 | // --- expectErr --- 174 | 175 | it('Result.expectErr: Expect correct value', () => { 176 | const expected = 'Failure'; 177 | 178 | const result = failingFunction(expected); 179 | 180 | expect(result.expectErr('Oh no!')).toBe(expected); 181 | }); 182 | 183 | it('Result.expectErr: Should throw if ok', () => { 184 | const result = passingFunction('oops!'); 185 | 186 | expect(() => result.expectErr('Oh no')).toThrow('Oh no'); 187 | }); 188 | 189 | // --- map --- 190 | 191 | it('Result.map: Should map correctly if ok', () => { 192 | const expected = 'Something'; 193 | const result = passingFunction(expected); 194 | 195 | expect(result.map((val) => val.length).unwrap()).toBe(expected.length); 196 | }); 197 | 198 | it('Result.map: Should map correctly if err', () => { 199 | const expected = 'Something'; 200 | const result = failingFunction(expected); 201 | 202 | expect(result.map(() => 'foo'.length).unwrapErr()).toBe(expected); 203 | }); 204 | 205 | // --- mapOr --- 206 | 207 | it('Result.mapOr: Should map correctly if ok', () => { 208 | const expected = 'Something'; 209 | const result = passingFunction(expected); 210 | 211 | expect(result.mapOr(1, (val) => val.length)).toBe(expected.length); 212 | }); 213 | 214 | it('Result.mapOr: Should map correctly if err', () => { 215 | const expected = 'Something'; 216 | const result = failingFunction('error'); 217 | 218 | expect(result.mapOr(expected.length, () => 1)).toBe(expected.length); 219 | }); 220 | 221 | // --- mapOrElse --- 222 | 223 | it('Result.mapOrElse: Should map correctly if ok', () => { 224 | const expected = 'Something'; 225 | const result = passingFunction(expected); 226 | 227 | expect( 228 | result.mapOrElse( 229 | () => 0, 230 | (val) => val.length 231 | ) 232 | ).toBe(expected.length); 233 | }); 234 | 235 | it('Result.mapOrElse: Should map correctly if err', () => { 236 | const expected = 'Something'; 237 | const result = failingFunction('error'); 238 | 239 | expect( 240 | result.mapOrElse( 241 | () => expected.length, 242 | () => 1 243 | ) 244 | ).toBe(expected.length); 245 | }); 246 | 247 | // --- mapErr --- 248 | 249 | it('Result.mapErr: Should map correctly if err', () => { 250 | const expected = 'Something'; 251 | const result = failingFunction(expected); 252 | 253 | expect(result.mapErr((err) => err.length).unwrapErr()).toBe(expected.length); 254 | }); 255 | 256 | it('Result.mapErr: Should map correctly if ok', () => { 257 | const expected = 'foo'; 258 | const result = passingFunction(expected); 259 | 260 | expect(result.mapErr(() => expected).unwrap()).toBe(expected); 261 | }); 262 | 263 | // --- mapErrOr --- 264 | 265 | it('Result.mapErrOr: Should map correctly if err', () => { 266 | const expected = 'Something'; 267 | const result = failingFunction(expected); 268 | 269 | expect(result.mapErrOr(1, (val) => val.length)).toBe(expected.length); 270 | }); 271 | 272 | it('Result.mapErrOr: Should map correctly if ok', () => { 273 | const expected = 'Something'; 274 | const result = passingFunction('error'); 275 | 276 | expect(result.mapErrOr(expected.length, () => 1)).toBe(expected.length); 277 | }); 278 | 279 | // --- mapErrOrElse --- 280 | 281 | it('Result.mapErrOrElse: Should map correctly if err', () => { 282 | const expected = 'Something'; 283 | const result = failingFunction(expected); 284 | 285 | expect( 286 | result.mapErrOrElse( 287 | () => 1, 288 | (val) => val.length 289 | ) 290 | ).toBe(expected.length); 291 | }); 292 | 293 | it('Result.mapErrOrElse: Should map correctly if ok', () => { 294 | const expected = 'Something'; 295 | const result = passingFunction('error'); 296 | 297 | expect( 298 | result.mapErrOrElse( 299 | () => expected.length, 300 | () => 1 301 | ) 302 | ).toBe(expected.length); 303 | }); 304 | }); 305 | -------------------------------------------------------------------------------- /src/ts/result.ts: -------------------------------------------------------------------------------- 1 | /** This is just a helper type used only within this file */ 2 | type _Result = { ok: true; val: T } | { ok: false; err: E }; 3 | 4 | /** Result allows you to show to a consumer that a function might throw and force them to handle it. 5 | * 6 | * `T` Value type 7 | * 8 | * `E` Error type 9 | * 10 | * ## Usage 11 | * 12 | * ```ts 13 | * function functionThatMightFail(): Result; 14 | * ``` 15 | * 16 | * ## Usage 17 | * 18 | * ```ts 19 | * const functionThatMightFail = (): Result => Ok("Hello, World!"); 20 | * 21 | * const result = functionThatMightFail(); 22 | * 23 | * console.log(result.unwrap()); // "Hello, World!" 24 | * ``` 25 | */ 26 | class Result { 27 | private readonly _result: _Result; 28 | 29 | constructor(result: _Result) { 30 | this._result = result; 31 | } 32 | 33 | /** Allows you to run callbacks based on the result. 34 | * 35 | * @param success callback to be run when result is success 36 | * @param failure callback to be run when result is failure 37 | * @returns 38 | * 39 | * ## Usage 40 | * 41 | * ```ts 42 | * result.match( 43 | * (val) => val, 44 | * () => { 45 | * throw new Error('oops!') 46 | * } 47 | * ); 48 | * ``` 49 | * 50 | * ## Usage 51 | * 52 | * ```ts 53 | * const functionThatMightFail = (): Result => Ok("Hello, World!"); 54 | * 55 | * const result = functionThatMightFail(); 56 | * 57 | * const val = result.match( 58 | * (val) => val, 59 | * () => { 60 | * throw new Error('oops!') 61 | * } 62 | * ); 63 | * 64 | * console.log(val); // "Hello, World!" 65 | * ``` 66 | */ 67 | match(success: (val: T) => A, failure: (err: E) => B): A | B { 68 | if (!this._result.ok) { 69 | return failure(this._result.err); 70 | } 71 | 72 | return success(this._result.val); 73 | } 74 | 75 | /** Maps `Result` to `Result` using the passed mapping function 76 | * 77 | * @param fn Mapping function 78 | * @returns 79 | * 80 | * ## Usage 81 | * 82 | * ```ts 83 | * result.map((val) => val.length); 84 | * ``` 85 | * 86 | * ## Usage 87 | * 88 | * ```ts 89 | * const functionThatMightFail = (): Result => Ok("Hello, World!"); 90 | * 91 | * const result = functionThatMightFail(); 92 | * 93 | * const hello = result.map((val) => val.slice(0, 5)); 94 | * 95 | * console.log(hello.unwrap()); // "Hello" 96 | * ``` 97 | */ 98 | map(fn: (val: T) => A): Result { 99 | return this.match( 100 | (val) => Ok(fn(val)), 101 | (err) => Err(err) 102 | ); 103 | } 104 | 105 | /** In the `Ok` case returns the mapped value using the function else returns `defaultVal` 106 | * 107 | * @param defaultVal Value to be returned when `Err` 108 | * @param fn Mapping function to map in case of `Ok` 109 | * @returns 110 | * 111 | * ## Usage 112 | * 113 | * ```ts 114 | * result.mapOr(1, (val) => val.length); 115 | * ``` 116 | * 117 | * ## Usage 118 | * 119 | * ### When `Ok` 120 | * 121 | * ```ts 122 | * const functionThatMightFail = (): Result => Ok("foo"); 123 | * 124 | * const result = functionThatMightFail(); 125 | * 126 | * const length = result.mapOr(1, (val) => val.length); 127 | * 128 | * console.log(length); // 3 129 | * ``` 130 | * 131 | * ### When `Err` 132 | * 133 | * ```ts 134 | * const functionThatMightFail = (): Result => Err("oops!"); 135 | * 136 | * const result = functionThatMightFail(); 137 | * 138 | * const length = result.mapOr(1, (val) => val.length); 139 | * 140 | * console.log(length); // 1 141 | * ``` 142 | */ 143 | mapOr(defaultVal: A, fn: (val: T) => A): A { 144 | return this.match( 145 | (val) => fn(val), 146 | (_) => defaultVal 147 | ); 148 | } 149 | 150 | /** In the `Ok` case returns the mapped value using `fn` else returns value of `def` 151 | * 152 | * @param def Mapping function called when `Err` 153 | * @param fn Mapping function called when `Ok` 154 | * @returns 155 | * 156 | * ## Usage 157 | * 158 | * ```ts 159 | * result.mapOrElse(() => 1, (val) => val.length); 160 | * ``` 161 | * 162 | * ## Usage 163 | * 164 | * ### When `Ok` 165 | * 166 | * ```ts 167 | * const functionThatMightFail = (): Result => Ok("foo"); 168 | * 169 | * const result = functionThatMightFail(); 170 | * 171 | * const length = result.mapOrElse(() => 1, (val) => val.length); 172 | * 173 | * console.log(length); // 3 174 | * ``` 175 | * 176 | * ### When `Err` 177 | * 178 | * ```ts 179 | * const functionThatMightFail = (): Result => Err("oops!"); 180 | * 181 | * const result = functionThatMightFail(); 182 | * 183 | * const length = result.mapOr(() => 1, (val) => val.length); 184 | * 185 | * console.log(length); // 1 186 | * ``` 187 | */ 188 | mapOrElse(def: (err: E) => A, fn: (val: T) => A): A { 189 | return this.match( 190 | (val) => fn(val), 191 | (err) => def(err) 192 | ); 193 | } 194 | 195 | /** Maps `Result` to `Result` using the passed mapping function 196 | * 197 | * @param fn Mapping function 198 | * @returns 199 | * 200 | * ## Usage 201 | * 202 | * ```ts 203 | * result.mapErr((err) => getCodeMsg(err)); 204 | * ``` 205 | * 206 | * ## Usage 207 | * 208 | * ```ts 209 | * const functionThatMightFail = (): Result => Err(10); 210 | * 211 | * const result = functionThatMightFail(); 212 | * 213 | * const message = result.mapErr(() => "Error"); 214 | * 215 | * console.log(message); // "Error" 216 | * ``` 217 | */ 218 | mapErr(fn: (err: E) => A): Result { 219 | return this.match( 220 | (val) => Ok(val), 221 | (err) => Err(fn(err)) 222 | ); 223 | } 224 | 225 | /** In the `Err` case returns the mapped value using the function else returns `defaultVal` 226 | * 227 | * @param defaultVal Value to be returned when `Ok` 228 | * @param fn Mapping function to map in case of `Err` 229 | * @returns 230 | * 231 | * ## Usage 232 | * 233 | * ```ts 234 | * result.mapErrOr("Should've been error", (err) => getCodeMsg(err)); 235 | * ``` 236 | * 237 | * ## Usage 238 | * 239 | * ### When `Ok` 240 | * 241 | * ```ts 242 | * const functionThatMightFail = (): Result => Ok("foo"); 243 | * 244 | * const result = functionThatMightFail(); 245 | * 246 | * const message = result.mapErrOr("Should've been error", () => "Error"); 247 | * 248 | * console.log(message); // "Should've been error" 249 | * ``` 250 | * 251 | * ### When `Err` 252 | * 253 | * ```ts 254 | * const functionThatMightFail = (): Result => Err(10); 255 | * 256 | * const result = functionThatMightFail(); 257 | * 258 | * const message = result.mapErrOr("Should've been error", () => "Error"); 259 | * 260 | * console.log(message); // "Error" 261 | * ``` 262 | */ 263 | mapErrOr(defaultVal: A, fn: (err: E) => A): A { 264 | return this.match( 265 | (_) => defaultVal, 266 | (err) => fn(err) 267 | ); 268 | } 269 | 270 | /** In the `Err` case returns the mapped value using the function else returns value of `def` 271 | * 272 | * @param def Mapping function called when `Ok` 273 | * @param fn Mapping function called when `Err` 274 | * @returns 275 | * 276 | * ## Usage 277 | * 278 | * ```ts 279 | * result.mapErrOrElse(() => "Value", (_) => "Error!"); 280 | * ``` 281 | * 282 | * ## Usage 283 | * 284 | * ### When `Ok` 285 | * 286 | * ```ts 287 | * const functionThatMightFail = (): Result => Ok("foo"); 288 | * 289 | * const result = functionThatMightFail(); 290 | * 291 | * const length = result.mapErrOrElse(() => 1, (val) => val.length); 292 | * 293 | * console.log(length); // 1 294 | * ``` 295 | * 296 | * ### When `Err` 297 | * 298 | * ```ts 299 | * const functionThatMightFail = (): Result => Err("oops!"); 300 | * 301 | * const result = functionThatMightFail(); 302 | * 303 | * const length = result.mapOr(() => 1, (val) => val.length); 304 | * 305 | * console.log(length); // 4 306 | * ``` 307 | */ 308 | mapErrOrElse(def: (val: T) => A, fn: (err: E) => A): A { 309 | return this.match( 310 | (val) => def(val), 311 | (err) => fn(err) 312 | ); 313 | } 314 | 315 | /** Returns true if result is `Ok` 316 | * 317 | * @returns 318 | * 319 | * ## Usage 320 | * 321 | * ```ts 322 | * result.isOk(); 323 | * ``` 324 | */ 325 | isOk(): boolean { 326 | return this.match( 327 | () => true, 328 | () => false 329 | ); 330 | } 331 | 332 | /** Returns true if result is `Err` 333 | * 334 | * @returns 335 | * 336 | * ## Usage 337 | * 338 | * ```ts 339 | * result.isErr(); 340 | * ``` 341 | */ 342 | isErr(): boolean { 343 | return this.match( 344 | () => false, 345 | () => true 346 | ); 347 | } 348 | 349 | /** Tries to return value if value is `Err` throws generic error message. 350 | * 351 | * @returns 352 | * 353 | * ## Usage 354 | * 355 | * ```ts 356 | * result.unwrap(); 357 | * ``` 358 | * 359 | * ## Usage 360 | * 361 | * ### When `Ok` 362 | * 363 | * ```ts 364 | * const functionThatMightFail = (): Result => Ok("Hello!"); 365 | * 366 | * const result = functionThatMightFail(); 367 | * 368 | * console.log(result.unwrap()); // "Hello!" 369 | * ``` 370 | * 371 | * ### When `Err` 372 | * 373 | * ```ts 374 | * const functionThatMightFail = (): Result => Err("oops!"); 375 | * 376 | * const result = functionThatMightFail(); 377 | * 378 | * result.unwrap(); // Error: Attempted to call `.unwrap()` on a non `Ok` value. 379 | * ``` 380 | */ 381 | unwrap(): T { 382 | return this.match( 383 | (val) => val, 384 | () => { 385 | throw new Error('Attempted to call `.unwrap()` on a non `Ok` value.'); 386 | } 387 | ); 388 | } 389 | 390 | /** Tries to return err if value is `Ok` throws generic error message. 391 | * 392 | * @returns 393 | * 394 | * ## Usage 395 | * 396 | * ```ts 397 | * result.unwrapErr(); 398 | * ``` 399 | * 400 | * ## Usage 401 | * 402 | * ### When `Ok` 403 | * 404 | * ```ts 405 | * const functionThatMightFail = (): Result => Ok("Hello!"); 406 | * 407 | * const result = functionThatMightFail(); 408 | * 409 | * result.unwrapErr(); // Error: Attempted to call `.unwrapErr()` on a non `Err` value. 410 | * ``` 411 | * 412 | * ### When `Err` 413 | * 414 | * ```ts 415 | * const functionThatMightFail = (): Result => Err("oops!"); 416 | * 417 | * const result = functionThatMightFail(); 418 | * 419 | * console.log(result.unwrapErr()); // "oops!" 420 | * ``` 421 | */ 422 | unwrapErr(): E { 423 | return this.match( 424 | () => { 425 | throw new Error('Attempted to call `.unwrapErr()` on a non `Err` value.'); 426 | }, 427 | (err) => err 428 | ); 429 | } 430 | 431 | /** Tries to unwrap the value if value is `Err` returns `defaultVal` 432 | * 433 | * @param defaultVal Value to be returned if `Err` 434 | * @returns 435 | * 436 | * ## Usage 437 | * 438 | * ```ts 439 | * result.unwrapOr(7); 440 | * ``` 441 | * 442 | * ## Usage 443 | * 444 | * ### When `Ok` 445 | * 446 | * ```ts 447 | * const functionThatMightFail = (): Result => Ok("Hello!"); 448 | * 449 | * const result = functionThatMightFail(); 450 | * 451 | * console.log(result.unwrapOr("Yellow!")); // "Hello!" 452 | * ``` 453 | * 454 | * ### When `Err` 455 | * 456 | * ```ts 457 | * const functionThatMightFail = (): Result => Err("oops!"); 458 | * 459 | * const result = functionThatMightFail(); 460 | * 461 | * console.log(result.unwrapOr("Yellow!")); // "Yellow!" 462 | * ``` 463 | */ 464 | unwrapOr(defaultVal: T): T { 465 | return this.match( 466 | (val) => val, 467 | (_) => defaultVal 468 | ); 469 | } 470 | 471 | /** Tries to unwrap the error if vale is `Ok` returns `defaultVal` 472 | * 473 | * @param defaultVal 474 | * @returns 475 | * 476 | * ## Usage 477 | * 478 | * ```ts 479 | * result.unwrapErrOr("Error"); 480 | * ``` 481 | * 482 | * ## Usage 483 | * 484 | * ### When `Ok` 485 | * 486 | * ```ts 487 | * const functionThatMightFail = (): Result => Ok("Hello!"); 488 | * 489 | * const result = functionThatMightFail(); 490 | * 491 | * console.log(result.unwrapErrOr("Yellow!")); // "Yellow!" 492 | * ``` 493 | * 494 | * ### When `Err` 495 | * 496 | * ```ts 497 | * const functionThatMightFail = (): Result => Err("oops!"); 498 | * 499 | * const result = functionThatMightFail(); 500 | * 501 | * console.log(result.unwrapErrOr("Yellow!")); // "oops!" 502 | * ``` 503 | */ 504 | unwrapErrOr(defaultVal: E): E { 505 | return this.match( 506 | () => defaultVal, 507 | (err) => err 508 | ); 509 | } 510 | 511 | /** Tries to return the value if value is `Err` calls `fn` 512 | * 513 | * @param fn Function called if `Err` 514 | * 515 | * ## Usage 516 | * 517 | * ```ts 518 | * result.unwrapOrElse(() => "Hello!"); 519 | * ``` 520 | * 521 | * ## Usage 522 | * 523 | * ### When `Ok` 524 | * 525 | * ```ts 526 | * const functionThatMightFail = (): Result => Ok("Hello!"); 527 | * 528 | * const result = functionThatMightFail(); 529 | * 530 | * console.log(result.unwrapOrElse(() => "oops!")); // "Hello!" 531 | * ``` 532 | * 533 | * ### When `Err` 534 | * 535 | * ```ts 536 | * const functionThatMightFail = (): Result => Err("oops!"); 537 | * 538 | * const result = functionThatMightFail(); 539 | * 540 | * console.log(result.unwrapOrElse(() => "Hello!")); // "Hello!" 541 | * ``` 542 | * 543 | */ 544 | unwrapOrElse(fn: (err: E) => T): T { 545 | return this.match( 546 | (val) => val, 547 | (err) => fn(err) 548 | ); 549 | } 550 | 551 | /** Tries to return the error if value is `Ok` calls `fn` 552 | * 553 | * @param fn Function called if `Ok` 554 | * 555 | * ## Usage 556 | * 557 | * ```ts 558 | * result.unwrapErrOrElse(() => "Error!"); 559 | * ``` 560 | * 561 | * ## Usage 562 | * 563 | * ### When `Ok` 564 | * 565 | * ```ts 566 | * const functionThatMightFail = (): Result => Ok("Hello!"); 567 | * 568 | * const result = functionThatMightFail(); 569 | * 570 | * console.log(result.unwrapErrOrElse(() => "oops!")); // "oops!" 571 | * ``` 572 | * 573 | * ### When `Err` 574 | * 575 | * ```ts 576 | * const functionThatMightFail = (): Result => Err("oops!"); 577 | * 578 | * const result = functionThatMightFail(); 579 | * 580 | * console.log(result.unwrapErrOrElse(() => "Hello!")); // "oops!" 581 | * ``` 582 | * 583 | */ 584 | unwrapErrOrElse(fn: (val: T) => E): E { 585 | return this.match( 586 | (val) => fn(val), 587 | (err) => err 588 | ); 589 | } 590 | 591 | /** Tries to return value if value is `Err` throws custom error message. 592 | * 593 | * @param message Message to show when value is `Err` 594 | * @returns 595 | * 596 | * ## Usage 597 | * 598 | * ```ts 599 | * result.expect("Custom message"); 600 | * ``` 601 | * 602 | * ## Usage 603 | * 604 | * ### When `Ok` 605 | * 606 | * ```ts 607 | * const functionThatMightFail = (): Result => Ok("Hello!"); 608 | * 609 | * const result = functionThatMightFail(); 610 | * 611 | * console.log(result.expect("I failed!")); // "Hello!" 612 | * ``` 613 | * 614 | * ### When `Err` 615 | * 616 | * ```ts 617 | * const functionThatMightFail = (): Result => Err("oops!"); 618 | * 619 | * const result = functionThatMightFail(); 620 | * 621 | * result.expect("I failed!"); // Error: I failed! 622 | * ``` 623 | */ 624 | expect(message: string): T { 625 | return this.match( 626 | (val) => val, 627 | () => { 628 | throw new Error(message); 629 | } 630 | ); 631 | } 632 | 633 | /** Tries to return error value if value is `Ok` throws custom error message 634 | * 635 | * @param message 636 | * @returns 637 | * 638 | * ## Usage 639 | * 640 | * ```ts 641 | * result.expectErr("Custom message"); 642 | * ``` 643 | * 644 | * ## Usage 645 | * 646 | * ### When `Ok` 647 | * 648 | * ```ts 649 | * const functionThatMightFail = (): Result => Ok("Hello!"); 650 | * 651 | * const result = functionThatMightFail(); 652 | * 653 | * console.log(result.expectErr("I failed!")); // Error: I failed! 654 | * ``` 655 | * 656 | * ### When `Err` 657 | * 658 | * ```ts 659 | * const functionThatMightFail = (): Result => Err("oops!"); 660 | * 661 | * const result = functionThatMightFail(); 662 | * 663 | * console.log(result.expectErr("I failed!")); // "oops!" 664 | * ``` 665 | */ 666 | expectErr(message: string): E { 667 | return this.match( 668 | () => { 669 | throw new Error(message); 670 | }, 671 | (err) => err 672 | ); 673 | } 674 | } 675 | 676 | /** Returns a new `Ok` result type with the provided value 677 | * 678 | * @param val Value of the result 679 | * @returns 680 | * 681 | * ## Usage 682 | * 683 | * ```ts 684 | * Ok(true); 685 | * ``` 686 | * 687 | * ## Usage 688 | * 689 | * ```ts 690 | * const functionThatCanFail = (condition) => { 691 | * if (condition) { 692 | * Ok("Success") 693 | * } 694 | * 695 | * return Err("Failure"); 696 | * } 697 | * ``` 698 | */ 699 | export function Ok(val: T): Result { 700 | return new Result({ ok: true, val }); 701 | } 702 | 703 | /** Returns a new `Err` result type with the provided error 704 | * 705 | * @param err Error of the result 706 | * @returns 707 | * 708 | * ## Usage 709 | * 710 | * ```ts 711 | * Err("I failed!"); 712 | * ``` 713 | * 714 | * ## Usage 715 | * 716 | * ```ts 717 | * const functionThatCanFail = (condition) => { 718 | * if (condition) { 719 | * Ok("Success") 720 | * } 721 | * 722 | * return Err("Failure"); 723 | * } 724 | * ``` 725 | */ 726 | export function Err(err: E): Result { 727 | return new Result({ ok: false, err }); 728 | } 729 | 730 | export type { Result }; 731 | -------------------------------------------------------------------------------- /registry.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ieedan/std", 3 | "authors": [ 4 | "Aidan Bleser" 5 | ], 6 | "bugs": "https://github.com/ieedan/std/issues", 7 | "description": "Fully tested and documented TypeScript utilities brokered by jsrepo.", 8 | "homepage": "https://ieedan.github.io/std/", 9 | "repository": "https://github.com/ieedan/std", 10 | "tags": [ 11 | "typescript", 12 | "std", 13 | "utilities" 14 | ], 15 | "version": "package", 16 | "type": "repository", 17 | "items": [ 18 | { 19 | "name": "result", 20 | "type": "util", 21 | "add": "when-added", 22 | "registryDependencies": [], 23 | "dependencies": [], 24 | "devDependencies": [], 25 | "files": [ 26 | { 27 | "type": "util", 28 | "role": "file", 29 | "path": "result.ts", 30 | "relativePath": "src/ts/result.ts", 31 | "_imports_": [], 32 | "registryDependencies": [], 33 | "dependencies": [], 34 | "devDependencies": [] 35 | }, 36 | { 37 | "type": "util", 38 | "role": "test", 39 | "path": "result.test.ts", 40 | "relativePath": "src/ts/result.test.ts", 41 | "_imports_": [], 42 | "registryDependencies": [], 43 | "dependencies": [], 44 | "devDependencies": [ 45 | { 46 | "ecosystem": "js", 47 | "name": "vitest", 48 | "version": "^3.2.4" 49 | } 50 | ] 51 | } 52 | ] 53 | }, 54 | { 55 | "name": "array", 56 | "type": "util", 57 | "add": "when-added", 58 | "registryDependencies": [], 59 | "dependencies": [], 60 | "devDependencies": [], 61 | "files": [ 62 | { 63 | "type": "util", 64 | "role": "file", 65 | "path": "array.ts", 66 | "relativePath": "src/ts/array.ts", 67 | "_imports_": [], 68 | "registryDependencies": [], 69 | "dependencies": [], 70 | "devDependencies": [] 71 | }, 72 | { 73 | "type": "util", 74 | "role": "test", 75 | "path": "array.test.ts", 76 | "relativePath": "src/ts/array.test.ts", 77 | "_imports_": [], 78 | "registryDependencies": [], 79 | "dependencies": [], 80 | "devDependencies": [ 81 | { 82 | "ecosystem": "js", 83 | "name": "vitest", 84 | "version": "^3.2.4" 85 | } 86 | ] 87 | } 88 | ] 89 | }, 90 | { 91 | "name": "casing", 92 | "type": "util", 93 | "add": "when-added", 94 | "registryDependencies": [ 95 | "is-letter" 96 | ], 97 | "dependencies": [], 98 | "devDependencies": [], 99 | "files": [ 100 | { 101 | "type": "util", 102 | "role": "file", 103 | "path": "casing.ts", 104 | "relativePath": "src/ts/casing.ts", 105 | "_imports_": [ 106 | { 107 | "import": "./is-letter", 108 | "item": "is-letter", 109 | "file": { 110 | "type": "util", 111 | "path": "is-letter.ts" 112 | }, 113 | "meta": {} 114 | } 115 | ], 116 | "registryDependencies": [], 117 | "dependencies": [], 118 | "devDependencies": [] 119 | }, 120 | { 121 | "type": "util", 122 | "role": "test", 123 | "path": "casing.test.ts", 124 | "relativePath": "src/ts/casing.test.ts", 125 | "_imports_": [], 126 | "registryDependencies": [], 127 | "dependencies": [], 128 | "devDependencies": [ 129 | { 130 | "ecosystem": "js", 131 | "name": "vitest", 132 | "version": "^3.2.4" 133 | } 134 | ] 135 | } 136 | ] 137 | }, 138 | { 139 | "name": "dispatcher", 140 | "type": "util", 141 | "add": "when-added", 142 | "registryDependencies": [], 143 | "dependencies": [], 144 | "devDependencies": [], 145 | "files": [ 146 | { 147 | "type": "util", 148 | "role": "file", 149 | "path": "dispatcher.ts", 150 | "relativePath": "src/ts/dispatcher.ts", 151 | "_imports_": [], 152 | "registryDependencies": [], 153 | "dependencies": [], 154 | "devDependencies": [] 155 | }, 156 | { 157 | "type": "util", 158 | "role": "test", 159 | "path": "dispatcher.test.ts", 160 | "relativePath": "src/ts/dispatcher.test.ts", 161 | "_imports_": [], 162 | "registryDependencies": [], 163 | "dependencies": [], 164 | "devDependencies": [ 165 | { 166 | "ecosystem": "js", 167 | "name": "vitest", 168 | "version": "^3.2.4" 169 | } 170 | ] 171 | } 172 | ] 173 | }, 174 | { 175 | "name": "ipv4-address", 176 | "type": "util", 177 | "add": "when-added", 178 | "registryDependencies": [ 179 | "is-number", 180 | "result" 181 | ], 182 | "dependencies": [], 183 | "devDependencies": [], 184 | "files": [ 185 | { 186 | "type": "util", 187 | "role": "file", 188 | "path": "ipv4-address.ts", 189 | "relativePath": "src/ts/ipv4-address.ts", 190 | "_imports_": [ 191 | { 192 | "import": "./is-number", 193 | "item": "is-number", 194 | "file": { 195 | "type": "util", 196 | "path": "is-number.ts" 197 | }, 198 | "meta": {} 199 | }, 200 | { 201 | "import": "./result", 202 | "item": "result", 203 | "file": { 204 | "type": "util", 205 | "path": "result.ts" 206 | }, 207 | "meta": {} 208 | } 209 | ], 210 | "registryDependencies": [], 211 | "dependencies": [], 212 | "devDependencies": [] 213 | }, 214 | { 215 | "type": "util", 216 | "role": "test", 217 | "path": "ipv4-address.test.ts", 218 | "relativePath": "src/ts/ipv4-address.test.ts", 219 | "_imports_": [], 220 | "registryDependencies": [], 221 | "dependencies": [], 222 | "devDependencies": [ 223 | { 224 | "ecosystem": "js", 225 | "name": "vitest", 226 | "version": "^3.2.4" 227 | } 228 | ] 229 | } 230 | ] 231 | }, 232 | { 233 | "name": "is-letter", 234 | "type": "util", 235 | "add": "when-added", 236 | "registryDependencies": [], 237 | "dependencies": [], 238 | "devDependencies": [], 239 | "files": [ 240 | { 241 | "type": "util", 242 | "role": "file", 243 | "path": "is-letter.ts", 244 | "relativePath": "src/ts/is-letter.ts", 245 | "_imports_": [], 246 | "registryDependencies": [], 247 | "dependencies": [], 248 | "devDependencies": [] 249 | }, 250 | { 251 | "type": "util", 252 | "role": "test", 253 | "path": "is-letter.test.ts", 254 | "relativePath": "src/ts/is-letter.test.ts", 255 | "_imports_": [], 256 | "registryDependencies": [], 257 | "dependencies": [], 258 | "devDependencies": [ 259 | { 260 | "ecosystem": "js", 261 | "name": "vitest", 262 | "version": "^3.2.4" 263 | } 264 | ] 265 | } 266 | ] 267 | }, 268 | { 269 | "name": "is-number", 270 | "type": "util", 271 | "add": "when-added", 272 | "registryDependencies": [], 273 | "dependencies": [], 274 | "devDependencies": [], 275 | "files": [ 276 | { 277 | "type": "util", 278 | "role": "file", 279 | "path": "is-number.ts", 280 | "relativePath": "src/ts/is-number.ts", 281 | "_imports_": [], 282 | "registryDependencies": [], 283 | "dependencies": [], 284 | "devDependencies": [] 285 | }, 286 | { 287 | "type": "util", 288 | "role": "test", 289 | "path": "is-number.test.ts", 290 | "relativePath": "src/ts/is-number.test.ts", 291 | "_imports_": [], 292 | "registryDependencies": [], 293 | "dependencies": [], 294 | "devDependencies": [ 295 | { 296 | "ecosystem": "js", 297 | "name": "vitest", 298 | "version": "^3.2.4" 299 | } 300 | ] 301 | } 302 | ] 303 | }, 304 | { 305 | "name": "math", 306 | "type": "util", 307 | "add": "when-added", 308 | "registryDependencies": [], 309 | "dependencies": [], 310 | "devDependencies": [], 311 | "files": [ 312 | { 313 | "type": "util", 314 | "role": "file", 315 | "path": "circle.ts", 316 | "relativePath": "src/ts/math/circle.ts", 317 | "_imports_": [], 318 | "registryDependencies": [], 319 | "dependencies": [], 320 | "devDependencies": [] 321 | }, 322 | { 323 | "type": "util", 324 | "role": "test", 325 | "path": "circle.test.ts", 326 | "relativePath": "src/ts/math/circle.test.ts", 327 | "_imports_": [], 328 | "registryDependencies": [], 329 | "dependencies": [], 330 | "devDependencies": [ 331 | { 332 | "ecosystem": "js", 333 | "name": "vitest", 334 | "version": "^3.2.4" 335 | } 336 | ] 337 | }, 338 | { 339 | "type": "util", 340 | "role": "file", 341 | "path": "conversions.ts", 342 | "relativePath": "src/ts/math/conversions.ts", 343 | "_imports_": [], 344 | "registryDependencies": [], 345 | "dependencies": [], 346 | "devDependencies": [] 347 | }, 348 | { 349 | "type": "util", 350 | "role": "test", 351 | "path": "conversions.test.ts", 352 | "relativePath": "src/ts/math/conversions.test.ts", 353 | "_imports_": [], 354 | "registryDependencies": [], 355 | "dependencies": [], 356 | "devDependencies": [ 357 | { 358 | "ecosystem": "js", 359 | "name": "vitest", 360 | "version": "^3.2.4" 361 | } 362 | ] 363 | }, 364 | { 365 | "type": "util", 366 | "role": "file", 367 | "path": "fractions.ts", 368 | "relativePath": "src/ts/math/fractions.ts", 369 | "_imports_": [], 370 | "registryDependencies": [], 371 | "dependencies": [], 372 | "devDependencies": [] 373 | }, 374 | { 375 | "type": "util", 376 | "role": "test", 377 | "path": "fractions.test.ts", 378 | "relativePath": "src/ts/math/fractions.test.ts", 379 | "_imports_": [], 380 | "registryDependencies": [], 381 | "dependencies": [], 382 | "devDependencies": [ 383 | { 384 | "ecosystem": "js", 385 | "name": "vitest", 386 | "version": "^3.2.4" 387 | } 388 | ] 389 | }, 390 | { 391 | "type": "util", 392 | "role": "file", 393 | "path": "gcf.ts", 394 | "relativePath": "src/ts/math/gcf.ts", 395 | "_imports_": [], 396 | "registryDependencies": [], 397 | "dependencies": [], 398 | "devDependencies": [] 399 | }, 400 | { 401 | "type": "util", 402 | "role": "test", 403 | "path": "gcf.test.ts", 404 | "relativePath": "src/ts/math/gcf.test.ts", 405 | "_imports_": [], 406 | "registryDependencies": [], 407 | "dependencies": [], 408 | "devDependencies": [ 409 | { 410 | "ecosystem": "js", 411 | "name": "vitest", 412 | "version": "^3.2.4" 413 | } 414 | ] 415 | }, 416 | { 417 | "type": "util", 418 | "role": "file", 419 | "path": "triangles.ts", 420 | "relativePath": "src/ts/math/triangles.ts", 421 | "_imports_": [], 422 | "registryDependencies": [], 423 | "dependencies": [], 424 | "devDependencies": [] 425 | }, 426 | { 427 | "type": "util", 428 | "role": "test", 429 | "path": "triangles.test.ts", 430 | "relativePath": "src/ts/math/triangles.test.ts", 431 | "_imports_": [], 432 | "registryDependencies": [], 433 | "dependencies": [], 434 | "devDependencies": [ 435 | { 436 | "ecosystem": "js", 437 | "name": "vitest", 438 | "version": "^3.2.4" 439 | } 440 | ] 441 | }, 442 | { 443 | "type": "util", 444 | "role": "file", 445 | "path": "types.ts", 446 | "relativePath": "src/ts/math/types.ts", 447 | "_imports_": [], 448 | "registryDependencies": [], 449 | "dependencies": [], 450 | "devDependencies": [] 451 | }, 452 | { 453 | "type": "util", 454 | "role": "file", 455 | "path": "index.ts", 456 | "relativePath": "src/ts/math/index.ts", 457 | "_imports_": [], 458 | "registryDependencies": [], 459 | "dependencies": [], 460 | "devDependencies": [] 461 | } 462 | ] 463 | }, 464 | { 465 | "name": "pad", 466 | "type": "util", 467 | "add": "when-added", 468 | "registryDependencies": [], 469 | "dependencies": [], 470 | "devDependencies": [], 471 | "files": [ 472 | { 473 | "type": "util", 474 | "role": "file", 475 | "path": "pad.ts", 476 | "relativePath": "src/ts/pad.ts", 477 | "_imports_": [], 478 | "registryDependencies": [], 479 | "dependencies": [], 480 | "devDependencies": [] 481 | }, 482 | { 483 | "type": "util", 484 | "role": "test", 485 | "path": "pad.test.ts", 486 | "relativePath": "src/ts/pad.test.ts", 487 | "_imports_": [], 488 | "registryDependencies": [], 489 | "dependencies": [], 490 | "devDependencies": [ 491 | { 492 | "ecosystem": "js", 493 | "name": "vitest", 494 | "version": "^3.2.4" 495 | } 496 | ] 497 | } 498 | ] 499 | }, 500 | { 501 | "name": "perishable-list", 502 | "type": "util", 503 | "add": "when-added", 504 | "registryDependencies": [], 505 | "dependencies": [], 506 | "devDependencies": [], 507 | "files": [ 508 | { 509 | "type": "util", 510 | "role": "file", 511 | "path": "perishable-list.ts", 512 | "relativePath": "src/ts/perishable-list.ts", 513 | "_imports_": [], 514 | "registryDependencies": [], 515 | "dependencies": [], 516 | "devDependencies": [] 517 | }, 518 | { 519 | "type": "util", 520 | "role": "test", 521 | "path": "perishable-list.test.ts", 522 | "relativePath": "src/ts/perishable-list.test.ts", 523 | "_imports_": [], 524 | "registryDependencies": [], 525 | "dependencies": [], 526 | "devDependencies": [ 527 | { 528 | "ecosystem": "js", 529 | "name": "vitest", 530 | "version": "^3.2.4" 531 | } 532 | ] 533 | } 534 | ] 535 | }, 536 | { 537 | "name": "promises", 538 | "type": "util", 539 | "add": "when-added", 540 | "registryDependencies": [], 541 | "dependencies": [], 542 | "devDependencies": [], 543 | "files": [ 544 | { 545 | "type": "util", 546 | "role": "file", 547 | "path": "promises.ts", 548 | "relativePath": "src/ts/promises.ts", 549 | "_imports_": [], 550 | "registryDependencies": [], 551 | "dependencies": [], 552 | "devDependencies": [] 553 | }, 554 | { 555 | "type": "util", 556 | "role": "test", 557 | "path": "promises.test.ts", 558 | "relativePath": "src/ts/promises.test.ts", 559 | "_imports_": [], 560 | "registryDependencies": [], 561 | "dependencies": [], 562 | "devDependencies": [ 563 | { 564 | "ecosystem": "js", 565 | "name": "vitest", 566 | "version": "^3.2.4" 567 | } 568 | ] 569 | } 570 | ] 571 | }, 572 | { 573 | "name": "rand", 574 | "type": "util", 575 | "add": "when-added", 576 | "registryDependencies": [], 577 | "dependencies": [], 578 | "devDependencies": [], 579 | "files": [ 580 | { 581 | "type": "util", 582 | "role": "file", 583 | "path": "rand.ts", 584 | "relativePath": "src/ts/rand.ts", 585 | "_imports_": [], 586 | "registryDependencies": [], 587 | "dependencies": [], 588 | "devDependencies": [] 589 | }, 590 | { 591 | "type": "util", 592 | "role": "test", 593 | "path": "rand.test.ts", 594 | "relativePath": "src/ts/rand.test.ts", 595 | "_imports_": [], 596 | "registryDependencies": [], 597 | "dependencies": [], 598 | "devDependencies": [ 599 | { 600 | "ecosystem": "js", 601 | "name": "vitest", 602 | "version": "^3.2.4" 603 | } 604 | ] 605 | } 606 | ] 607 | }, 608 | { 609 | "name": "sleep", 610 | "type": "util", 611 | "add": "when-added", 612 | "registryDependencies": [], 613 | "dependencies": [], 614 | "devDependencies": [], 615 | "files": [ 616 | { 617 | "type": "util", 618 | "role": "file", 619 | "path": "sleep.ts", 620 | "relativePath": "src/ts/sleep.ts", 621 | "_imports_": [], 622 | "registryDependencies": [], 623 | "dependencies": [], 624 | "devDependencies": [] 625 | }, 626 | { 627 | "type": "util", 628 | "role": "test", 629 | "path": "sleep.test.ts", 630 | "relativePath": "src/ts/sleep.test.ts", 631 | "_imports_": [], 632 | "registryDependencies": [], 633 | "dependencies": [], 634 | "devDependencies": [ 635 | { 636 | "ecosystem": "js", 637 | "name": "vitest", 638 | "version": "^3.2.4" 639 | } 640 | ] 641 | } 642 | ] 643 | }, 644 | { 645 | "name": "stopwatch", 646 | "type": "util", 647 | "add": "when-added", 648 | "registryDependencies": [], 649 | "dependencies": [], 650 | "devDependencies": [], 651 | "files": [ 652 | { 653 | "type": "util", 654 | "role": "file", 655 | "path": "stopwatch.ts", 656 | "relativePath": "src/ts/stopwatch.ts", 657 | "_imports_": [], 658 | "registryDependencies": [], 659 | "dependencies": [], 660 | "devDependencies": [] 661 | }, 662 | { 663 | "type": "util", 664 | "role": "test", 665 | "path": "stopwatch.test.ts", 666 | "relativePath": "src/ts/stopwatch.test.ts", 667 | "_imports_": [], 668 | "registryDependencies": [], 669 | "dependencies": [], 670 | "devDependencies": [ 671 | { 672 | "ecosystem": "js", 673 | "name": "vitest", 674 | "version": "^3.2.4" 675 | } 676 | ] 677 | } 678 | ] 679 | }, 680 | { 681 | "name": "strings", 682 | "type": "util", 683 | "add": "when-added", 684 | "registryDependencies": [], 685 | "dependencies": [], 686 | "devDependencies": [], 687 | "files": [ 688 | { 689 | "type": "util", 690 | "role": "file", 691 | "path": "strings.ts", 692 | "relativePath": "src/ts/strings.ts", 693 | "_imports_": [], 694 | "registryDependencies": [], 695 | "dependencies": [], 696 | "devDependencies": [] 697 | }, 698 | { 699 | "type": "util", 700 | "role": "test", 701 | "path": "strings.test.ts", 702 | "relativePath": "src/ts/strings.test.ts", 703 | "_imports_": [], 704 | "registryDependencies": [], 705 | "dependencies": [], 706 | "devDependencies": [ 707 | { 708 | "ecosystem": "js", 709 | "name": "vitest", 710 | "version": "^3.2.4" 711 | } 712 | ] 713 | } 714 | ] 715 | }, 716 | { 717 | "name": "time", 718 | "type": "util", 719 | "add": "when-added", 720 | "registryDependencies": [ 721 | "types" 722 | ], 723 | "dependencies": [], 724 | "devDependencies": [], 725 | "files": [ 726 | { 727 | "type": "util", 728 | "role": "file", 729 | "path": "time.ts", 730 | "relativePath": "src/ts/time.ts", 731 | "_imports_": [ 732 | { 733 | "import": "./types", 734 | "item": "types", 735 | "file": { 736 | "type": "util", 737 | "path": "types.ts" 738 | }, 739 | "meta": {} 740 | } 741 | ], 742 | "registryDependencies": [], 743 | "dependencies": [], 744 | "devDependencies": [] 745 | }, 746 | { 747 | "type": "util", 748 | "role": "test", 749 | "path": "time.test.ts", 750 | "relativePath": "src/ts/time.test.ts", 751 | "_imports_": [], 752 | "registryDependencies": [], 753 | "dependencies": [], 754 | "devDependencies": [ 755 | { 756 | "ecosystem": "js", 757 | "name": "vitest", 758 | "version": "^3.2.4" 759 | } 760 | ] 761 | } 762 | ] 763 | }, 764 | { 765 | "name": "truncate", 766 | "type": "util", 767 | "add": "when-added", 768 | "registryDependencies": [], 769 | "dependencies": [], 770 | "devDependencies": [], 771 | "files": [ 772 | { 773 | "type": "util", 774 | "role": "file", 775 | "path": "truncate.ts", 776 | "relativePath": "src/ts/truncate.ts", 777 | "_imports_": [], 778 | "registryDependencies": [], 779 | "dependencies": [], 780 | "devDependencies": [] 781 | }, 782 | { 783 | "type": "util", 784 | "role": "test", 785 | "path": "truncate.test.ts", 786 | "relativePath": "src/ts/truncate.test.ts", 787 | "_imports_": [], 788 | "registryDependencies": [], 789 | "dependencies": [], 790 | "devDependencies": [ 791 | { 792 | "ecosystem": "js", 793 | "name": "vitest", 794 | "version": "^3.2.4" 795 | } 796 | ] 797 | } 798 | ] 799 | }, 800 | { 801 | "name": "types", 802 | "type": "util", 803 | "add": "when-added", 804 | "registryDependencies": [], 805 | "dependencies": [], 806 | "devDependencies": [], 807 | "files": [ 808 | { 809 | "type": "util", 810 | "role": "file", 811 | "path": "types.ts", 812 | "relativePath": "src/ts/types.ts", 813 | "_imports_": [], 814 | "registryDependencies": [], 815 | "dependencies": [], 816 | "devDependencies": [] 817 | } 818 | ] 819 | }, 820 | { 821 | "name": "url", 822 | "type": "util", 823 | "add": "when-added", 824 | "registryDependencies": [], 825 | "dependencies": [], 826 | "devDependencies": [], 827 | "files": [ 828 | { 829 | "type": "util", 830 | "role": "file", 831 | "path": "url.ts", 832 | "relativePath": "src/ts/url.ts", 833 | "_imports_": [], 834 | "registryDependencies": [], 835 | "dependencies": [], 836 | "devDependencies": [] 837 | }, 838 | { 839 | "type": "util", 840 | "role": "test", 841 | "path": "url.test.ts", 842 | "relativePath": "src/ts/url.test.ts", 843 | "_imports_": [], 844 | "registryDependencies": [], 845 | "dependencies": [], 846 | "devDependencies": [ 847 | { 848 | "ecosystem": "js", 849 | "name": "vitest", 850 | "version": "^3.2.4" 851 | } 852 | ] 853 | } 854 | ] 855 | }, 856 | { 857 | "name": "Cursor Rule", 858 | "type": "rule", 859 | "add": "optionally-on-init", 860 | "registryDependencies": [], 861 | "dependencies": [], 862 | "devDependencies": [], 863 | "files": [ 864 | { 865 | "type": "rule", 866 | "role": "file", 867 | "path": "typescript-utility-functions.mdc", 868 | "relativePath": "rules/typescript-utility-functions.mdc", 869 | "_imports_": [], 870 | "target": ".cursor/rules/typescript-utility-functions.mdc", 871 | "registryDependencies": [], 872 | "dependencies": [], 873 | "devDependencies": [] 874 | } 875 | ] 876 | } 877 | ] 878 | } --------------------------------------------------------------------------------