├── .githooks └── pre-commit ├── .github ├── CONTRIBUTING.md ├── dependabot.yml └── workflows │ ├── checkReadme.yml │ └── test.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── atWrap.test.ts ├── atWrap.ts ├── batch.test.ts ├── batch.ts ├── bimap.test.ts ├── bimap.ts ├── bundle.test.ts ├── bundle.ts ├── callAll.test.ts ├── callAll.ts ├── case.test.ts ├── case.ts ├── clamp.test.ts ├── clamp.ts ├── clone.test.ts ├── clone.ts ├── debounce.test.ts ├── debounce.ts ├── deno.json ├── deno.lock ├── ds.ts ├── equal.test.ts ├── equal.ts ├── equality.ts ├── error.ts ├── except.ts ├── forward.test.ts ├── forward.ts ├── function.ts ├── ident.test.ts ├── ident.ts ├── import_map.json ├── invoke.test.ts ├── invoke.ts ├── isPromise.test.ts ├── isPromise.ts ├── iterable.ts ├── list.ts ├── map.test.ts ├── map.ts ├── math.ts ├── memoize.test.ts ├── memoize.ts ├── merge.test.ts ├── merge.ts ├── mod.ts ├── noop.test.ts ├── noop.ts ├── npm ├── .babelrc ├── package-lock.json ├── package.json └── tsconfig.json ├── nullish.ts ├── nullishChain.test.ts ├── nullishChain.ts ├── object.ts ├── omit.test.ts ├── omit.ts ├── oneOf.test.ts ├── oneOf.ts ├── partial.test.ts ├── partial.ts ├── partition.test.ts ├── partition.ts ├── path.ts ├── pick.test.ts ├── pick.ts ├── pipe.test.ts ├── pipe.ts ├── predicate.ts ├── prefix.test.ts ├── prefix.ts ├── promise.ts ├── promisify.test.ts ├── promisify.ts ├── queue.test.ts ├── queue.ts ├── range.test.ts ├── range.ts ├── repeat.test.ts ├── repeat.ts ├── scripts ├── buildNode ├── docs ├── docs.js ├── publishNpm ├── rewriteStarExports.mjs ├── setupGitHooks ├── test ├── testArg.js └── testImports ├── select.test.ts ├── select.ts ├── settled.test.ts ├── settled.ts ├── shuffle.test.ts ├── shuffle.ts ├── sortedArray.test.ts ├── sortedArray.ts ├── sortedMap.test.ts ├── sortedMap.ts ├── string.ts ├── suffix.test.ts ├── suffix.ts ├── surround.test.ts ├── surround.ts ├── take.test.ts ├── take.ts ├── throttle.test.ts ├── throttle.ts ├── truthy.test.ts ├── truthy.ts ├── types.test.ts ├── types.ts ├── unary.test.ts ├── unary.ts ├── unzip.test.ts ├── unzip.ts ├── zip.test.ts └── zip.ts /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | deno fmt --check *.ts 4 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Froebel 2 | 3 | Contributions are always welcome. Before considering making a contribution 4 | please take a moment to read the guidelines in this document. 5 | 6 | ## Feature Requests 7 | 8 | Feature requests should be submitted in the [issue tracker](https://github.com/MathisBullinger/froebel/issues). 9 | Please describe the expected behavior of the utility and what problem it solves. 10 | 11 | ## Pull Requests 12 | 13 | Before opening a pull request for a new utility, please make sure that an issue 14 | has been opened discussing its inclusion. 15 | 16 | Every utility must have a unit test. If the utility is in the file `myNewUtility.ts`, 17 | the tests must be in a file named `myNewUtility.test.ts`. 18 | 19 | The utilities are grouped into categories: `function`, `list`, `iterable`, 20 | `object`, `path`, `equality`, `promise`, `predicate`, `string`, `math`, 21 | and `ds` (data structures). Each category has a file by the same name. Every 22 | utility must be exported from at least one of those files. 23 | 24 | The readme is generated from those exports and their JSDoc comments. Every 25 | utility should have at a minimum a short description 26 | (``/** Utility transforms `a`, `b`, `c` into `d`... */``) and ideally should 27 | also have at least one example. 28 | 29 | Always regenerate the readme after making changes to the code by running 30 | `scripts/docs`. Before running that scripts, you will need to run `npm install` 31 | inside the `npm` directory. 32 | 33 | The code is formatted using the built-in formatter (`deno fmt *.ts`) using Deno 34 | v1.29.2. 35 | Other Deno versions may format the code slightly different so be sure to check 36 | formatting with the correct Deno version (it should match the version listed in 37 | `.github/workflows/test.yml`) to make sure the automated tests pass. 38 | 39 | Every utility must work in Deno, Node, and (modern) Browsers. 40 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/checkReadme.yml: -------------------------------------------------------------------------------- 1 | name: Check README 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: 14 14 | 15 | - name: Check README 16 | run: | 17 | cd npm && \ 18 | npm install && \ 19 | cd .. && \ 20 | scripts/docs && \ 21 | git diff --exit-code README.md 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v3 11 | 12 | - uses: denoland/setup-deno@v1 13 | with: 14 | deno-version: "1.29.2" 15 | 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 14 19 | 20 | - name: Check formatting 21 | run: deno fmt --check *.ts 22 | 23 | - name: Unit tests 24 | run: scripts/test 25 | 26 | - name: Test node imports 27 | run: | 28 | cd npm &&\ 29 | npm install &&\ 30 | cd .. &&\ 31 | scripts/buildNode &&\ 32 | scripts/testImports 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | tmp 4 | docs.json 5 | !.git/hooks 6 | /test* 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": false, 5 | "deno.importMap": "import_map.json", 6 | "[typescript]": { 7 | "editor.defaultFormatter": "denoland.vscode-deno" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License (ISC) 2 | Copyright (c) 2020, Mathis Bullinger 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 7 | -------------------------------------------------------------------------------- /atWrap.test.ts: -------------------------------------------------------------------------------- 1 | import atWrap from "./atWrap.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("atWrap", () => { 5 | const list = [0, 1, 2]; 6 | 7 | assertEquals(atWrap(list, 0), 0); 8 | assertEquals(atWrap(list, 1), 1); 9 | assertEquals(atWrap(list, 2), 2); 10 | assertEquals(atWrap(list, 3), 0); 11 | assertEquals(atWrap(list, 4), 1); 12 | assertEquals(atWrap(list, 13), 1); 13 | 14 | assertEquals(atWrap(list, -1), 2); 15 | assertEquals(atWrap(list, -2), 1); 16 | assertEquals(atWrap(list, -3), 0); 17 | assertEquals(atWrap(list, -4), 2); 18 | assertEquals(atWrap(list, -5), 1); 19 | assertEquals(atWrap(list, -14), 1); 20 | }); 21 | -------------------------------------------------------------------------------- /atWrap.ts: -------------------------------------------------------------------------------- 1 | /** Access list at `i % length`. Negative indexes start indexing the last 2 | * element as `[-1]` and wrap around to the back. */ 3 | const atWrap = (arr: T[], i: number): T => 4 | arr[i >= 0 ? i % arr.length : arr.length + (i % arr.length || -arr.length)]; 5 | 6 | export default atWrap; 7 | -------------------------------------------------------------------------------- /batch.test.ts: -------------------------------------------------------------------------------- 1 | import batch from "./batch.ts"; 2 | import { assertEquals, assertThrows } from "testing/asserts.ts"; 3 | 4 | Deno.test("batch", () => { 5 | assertEquals(batch([1, 2, 3, 4, 5], 2), [[1, 2], [3, 4], [5]]); 6 | assertEquals(batch([1, 2, 3, 4, 5], 6), [[1, 2, 3, 4, 5]]); 7 | assertEquals(batch([1, 2, 3, 4], 2), [[1, 2], [3, 4]]); 8 | assertEquals(batch([], 5), []); 9 | assertEquals(batch([1, 2], Infinity), [[1, 2]]); 10 | assertThrows(() => batch([1, 2], 0), RangeError); 11 | assertThrows(() => batch([1, 2], -1), RangeError); 12 | assertThrows(() => batch([1, 2], NaN), RangeError); 13 | }); 14 | -------------------------------------------------------------------------------- /batch.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "./except.ts"; 2 | 3 | /** 4 | * Takes a `list` and returns it in multiple smaller lists of the size 5 | * `batchSize`. 6 | * The last batch may be smaller than `batchSize` depending on if `list` size is 7 | * divisible by `batchSize`. 8 | * 9 | * @example 10 | * ``` 11 | * batch([1,2,3,4,5], 2) // -> [ [1,2], [3,4], [5] ] 12 | * ``` 13 | */ 14 | const batch = (list: T[], batchSize: number): T[][] => { 15 | assert( 16 | typeof batchSize === "number" && !Number.isNaN(batchSize) && batchSize > 0, 17 | "batch size must be > 0", 18 | RangeError, 19 | ); 20 | 21 | const size = Number.isFinite(batchSize) ? batchSize : list.length; 22 | 23 | return [...Array(Math.ceil(list.length / size))].map((_, i) => 24 | list.slice(i * size, (i + 1) * size) 25 | ); 26 | }; 27 | 28 | export default batch; 29 | -------------------------------------------------------------------------------- /bimap.test.ts: -------------------------------------------------------------------------------- 1 | import Bimap, { BiMap } from "./bimap.ts"; 2 | import { UniqueViolationError } from "./error.ts"; 3 | import { assert, assertEquals, assertThrows } from "testing/asserts.ts"; 4 | 5 | const testEntries = (map: BiMap, entries: unknown = []) => { 6 | assertEquals([ 7 | ...(map as unknown as { data: Map })["data"].entries(), 8 | ], entries); 9 | }; 10 | 11 | Deno.test("construct bimap", () => { 12 | const entries = [ 13 | ["a", 1], 14 | ["b", 2], 15 | ] as const; 16 | 17 | testEntries(new Bimap()); 18 | testEntries(new Bimap(entries), entries); 19 | testEntries(new Bimap(new Map(entries)), entries); 20 | testEntries(Bimap.from(Object.fromEntries(entries)), entries); 21 | testEntries(Bimap.from(new Set(["a", "b"]), new Set([1, 2])), entries); 22 | 23 | assertThrows( 24 | () => 25 | new Bimap([ 26 | [1, 2], 27 | [2, 2], 28 | ]), 29 | UniqueViolationError, 30 | ); 31 | assertThrows( 32 | () => 33 | new Bimap([ 34 | [1, 2], 35 | [1, 3], 36 | ]), 37 | UniqueViolationError, 38 | ); 39 | assertThrows( 40 | () => 41 | new Bimap( 42 | new Map([ 43 | [1, 2], 44 | [2, 2], 45 | ]), 46 | ), 47 | UniqueViolationError, 48 | ); 49 | 50 | assertThrows(() => Bimap.from(new Set([1, 2]), new Set("a")), TypeError); 51 | // @ts-expect-error 52 | assertThrows(() => Bimap.from(new Set(["foo"])), TypeError); 53 | }); 54 | 55 | const makeNumMap = ( 56 | data = [ 57 | [1, "one"], 58 | [2, "two"], 59 | ] as [number, string][], 60 | ) => [new Bimap(data, "number", "word"), data] as const; 61 | 62 | Deno.test("iterable", () => { 63 | const [numbers, lr] = makeNumMap(); 64 | const rl = lr.map(([l, r]) => [r, l]); 65 | const left = lr.map(([l]) => l); 66 | const right = rl.map(([r]) => r); 67 | 68 | assertEquals([...numbers], lr); 69 | assertEquals(Object.fromEntries(numbers), Object.fromEntries(lr)); 70 | 71 | assertEquals([...numbers.left], lr); 72 | assertEquals([...numbers.right], rl); 73 | 74 | assertEquals([...numbers.left.keys()], left); 75 | assertEquals([...numbers.right.keys()], right); 76 | 77 | assertEquals([...numbers.left.values()], right); 78 | assertEquals([...numbers.right.values()], left); 79 | }); 80 | 81 | Deno.test("alias sides", () => { 82 | const [numbers, data] = makeNumMap(); 83 | assertEquals([...numbers], data); 84 | assertEquals([...numbers.number], data); 85 | assertEquals([...numbers.word.keys()], ["one", "two"]); 86 | }); 87 | 88 | Deno.test("reverse", () => { 89 | const [numbers] = makeNumMap(); 90 | const reversed = numbers.reverse(); 91 | 92 | assertEquals([...reversed], [...numbers.right]); 93 | assertEquals([...reversed.right], [...numbers]); 94 | assertEquals([...reversed.number], [...numbers.number]); 95 | assertEquals([...reversed.word], [...numbers.word]); 96 | }); 97 | 98 | Deno.test("has", () => { 99 | const [numbers] = makeNumMap(); 100 | 101 | assert(numbers.left.has(1)); 102 | assertEquals(numbers.left.has(3), false); 103 | // @ts-expect-error 104 | assertEquals(numbers.left.has("a"), false); 105 | 106 | assert(numbers.right.has("one")); 107 | assertEquals(numbers.right.has("three"), false); 108 | // @ts-expect-error 109 | assertEquals(numbers.right.has(1), false); 110 | 111 | assert("one" in numbers.right); 112 | assertEquals("three" in numbers.right, false); 113 | assertEquals("keys" in numbers.right, false); 114 | 115 | assertEquals(1 in numbers.left, false); 116 | assertEquals("keys" in numbers.left, false); 117 | }); 118 | 119 | Deno.test("set left", () => { 120 | const bm = new Bimap(); 121 | assertEquals(bm.left.set("a", "b"), "b"); 122 | assertEquals(Object.fromEntries(bm), { a: "b" }); 123 | assertEquals(bm.left.set("c", "d"), "d"); 124 | assertEquals(Object.fromEntries(bm), { a: "b", c: "d" }); 125 | }); 126 | 127 | Deno.test("set right", () => { 128 | const bm = new Bimap(); 129 | assertEquals(bm.right.set("a", "b"), "b"); 130 | assertEquals(Object.fromEntries(bm), { b: "a" }); 131 | assertEquals(bm.right.set("c", "d"), "d"); 132 | assertEquals(Object.fromEntries(bm), { b: "a", d: "c" }); 133 | }); 134 | 135 | Deno.test("remap left", () => { 136 | const bm = new Bimap([ 137 | ["a", "b"], 138 | ["foo", "bar"], 139 | ]); 140 | assertEquals(bm.left.set("c", "b"), "b"); 141 | assertEquals(Object.fromEntries(bm), { foo: "bar", c: "b" }); 142 | }); 143 | 144 | Deno.test("remap right", () => { 145 | const bm = new Bimap([ 146 | ["a", "b"], 147 | ["foo", "bar"], 148 | ]); 149 | assertEquals(bm.right.set("c", "a"), "a"); 150 | assertEquals(Object.fromEntries(bm), { foo: "bar", a: "c" }); 151 | }); 152 | 153 | Deno.test("assign prop", () => { 154 | const s2s = new Bimap(); 155 | s2s.left.foo = "bar"; 156 | s2s.left.bar = "baz"; 157 | s2s.right.foo = "bar"; 158 | assertEquals(Object.fromEntries(s2s), { foo: "bar", bar: "foo" }); 159 | 160 | const s2n = new Bimap(); 161 | s2n.left.foo = 2; 162 | assertEquals(Object.fromEntries(s2n), { foo: 2 }); 163 | // @ts-expect-error 164 | s2n.left.foo = "bar"; 165 | // @ts-expect-error 166 | s2n.right.foo = "bar"; 167 | }); 168 | 169 | Deno.test("delete left", () => { 170 | const [bm] = makeNumMap(); 171 | assert(bm.left.delete(1)); 172 | assertEquals(bm.left.delete(3), false); 173 | assertEquals(Object.fromEntries(bm.right), { two: 2 }); 174 | }); 175 | 176 | Deno.test("delete right", () => { 177 | const [bm] = makeNumMap(); 178 | assert(bm.right.delete("two")); 179 | assertEquals(bm.right.delete("foo"), false); 180 | assertEquals(Object.fromEntries(bm.right), { one: 1 }); 181 | }); 182 | 183 | Deno.test("delete op left", () => { 184 | const bm = new Bimap([ 185 | ["a", "b"], 186 | ["c", "d"], 187 | ]); 188 | delete bm.left.a; 189 | assertEquals(Object.fromEntries(bm), { c: "d" }); 190 | 191 | // @ts-expect-error 192 | delete new Bimap().left.foo; 193 | }); 194 | 195 | Deno.test("delete op right", () => { 196 | const [bm] = makeNumMap(); 197 | delete bm.right.two; 198 | assertEquals(Object.fromEntries(bm.right), { one: 1 }); 199 | }); 200 | 201 | Deno.test("clear", () => { 202 | { 203 | const [bm] = makeNumMap(); 204 | assertEquals([...bm.clear()], []); 205 | assertEquals([...bm], []); 206 | } 207 | { 208 | const [bm] = makeNumMap(); 209 | assertEquals([...bm.left.clear()], []); 210 | assertEquals([...bm], []); 211 | } 212 | { 213 | const [bm] = makeNumMap(); 214 | assertEquals([...bm.right.clear()], []); 215 | assertEquals([...bm], []); 216 | } 217 | }); 218 | 219 | Deno.test("size", () => { 220 | const bm = new Bimap([[1, 2]]); 221 | assertEquals(bm.size, 1); 222 | assertEquals(bm.left.size, 1); 223 | assertEquals(bm.right.size, 1); 224 | bm.left.set(3, 4); 225 | assertEquals(bm.size, 2); 226 | assertEquals(bm.left.size, 2); 227 | assertEquals(bm.right.size, 2); 228 | }); 229 | 230 | Deno.test("nullish assign", () => { 231 | { 232 | const map = new Bimap(); 233 | assertEquals(map.left.val ??= "foo", "foo"); 234 | assertEquals(map.left.val ??= "bar", "foo"); 235 | assertEquals([...map], [["val", "foo"]]); 236 | } 237 | { 238 | const map = Bimap.alias("a", "b")(); 239 | assertEquals(map.b.val ??= "foo", "foo"); 240 | assertEquals(map.b.val ??= "bar", "foo"); 241 | assertEquals(Object.fromEntries(map), { foo: "val" }); 242 | } 243 | }); 244 | 245 | Deno.test("getOrSet", () => { 246 | const a = () => {}; 247 | const b = () => {}; 248 | const map = new Bimap([[a, a]]); 249 | 250 | assertEquals(map.left.getOrSet(a, b), a); 251 | assertEquals(map.left.getOrSet(b, b), b); 252 | assertEquals(map.left.set(a, b), b); 253 | 254 | assertEquals(map.right.getOrSet(b, b), a); 255 | assertEquals( 256 | map.right.getOrSet(() => {}, b), 257 | b, 258 | ); 259 | }); 260 | -------------------------------------------------------------------------------- /bimap.ts: -------------------------------------------------------------------------------- 1 | import { UniqueViolationError } from "./error.ts"; 2 | import zip from "./zip.ts"; 3 | 4 | type AliasConstr = { 5 | (data?: Map): BiMap; 6 | (data: T): BiMap< 7 | T[number] extends [infer L, any] ? L : never, 8 | T[number] extends [any, infer R] ? R : never, 9 | AL, 10 | AR 11 | >; 12 | (left: Set, right: Set): BiMap; 13 | , L extends string | symbol, R>( 14 | data: T, 15 | ): T extends Set ? never : BiMap; 16 | }; 17 | 18 | class BiMapImpl { 19 | private aliasLeft?: string; 20 | private aliasRight?: string; 21 | 22 | constructor( 23 | data?: Map | readonly (readonly [L, R])[], 24 | aliasLeft?: AL, 25 | aliasRight?: AR, 26 | ) { 27 | const entries = data instanceof Map ? data.entries() : data ?? []; 28 | const checkKeys = Array.isArray(data); 29 | 30 | const errDup = (side: string, k: any) => { 31 | throw new UniqueViolationError( 32 | `duplicate ${side} key ${JSON.stringify(k)}`, 33 | ); 34 | }; 35 | for (const [k, v] of entries) { 36 | if (checkKeys && this.data.has(k)) errDup("left", k); 37 | if (iterHas(this.data.values(), v)) errDup("right", v); 38 | this.data.set(k, v); 39 | } 40 | 41 | this.defineAlias(aliasLeft, aliasRight); 42 | } 43 | 44 | public static from: AliasConstr = (...args: any[]) => 45 | new BiMapImpl( 46 | !args[0] || args[0] instanceof Map || Array.isArray(args[0]) 47 | ? args[0] 48 | : args[0] instanceof Set 49 | ? BiMapImpl.fromSets(...(args as [any, any])) 50 | : Object.entries(args[0]), 51 | ); 52 | 53 | private static fromSets( 54 | left: Set, 55 | right: Set, 56 | ): BiMapImpl { 57 | if (!left || !right || left.size !== right.size) { 58 | throw new TypeError( 59 | "must have same number of keys on left and right side", 60 | ); 61 | } 62 | return new BiMapImpl(zip([...left.keys()], [...right.keys()])); 63 | } 64 | 65 | public static alias = ( 66 | left: LA, 67 | right: RA, 68 | ): AliasConstr => 69 | (...args: any[]) => { 70 | const map: any = BiMapImpl.from(...args); 71 | map.defineAlias(left, right); 72 | return map; 73 | }; 74 | 75 | private defineAlias(left?: string, right?: string) { 76 | if (left !== undefined) { 77 | this.aliasLeft = left; 78 | Object.defineProperty(this, left, { get: () => this.left }); 79 | } 80 | if (right !== undefined) { 81 | this.aliasRight = right; 82 | Object.defineProperty(this, right, { get: () => this.right }); 83 | } 84 | } 85 | 86 | public clone(): BiMapImpl { 87 | return new BiMapImpl([...this.left], this.aliasLeft, this.aliasRight); 88 | } 89 | 90 | public reverse(): BiMapImpl { 91 | return new BiMapImpl([...this.right], this.aliasRight, this.aliasLeft); 92 | } 93 | 94 | public clear() { 95 | this.data.clear(); 96 | return this; 97 | } 98 | 99 | public get size() { 100 | return this.data.size; 101 | } 102 | 103 | public [Symbol.iterator]() { 104 | return this.data[Symbol.iterator](); 105 | } 106 | 107 | private proxy(ltr: boolean) { 108 | const map = { 109 | keys: this.data[ltr ? "keys" : "values"].bind(this.data), 110 | values: this.data[ltr ? "values" : "keys"].bind(this.data), 111 | has: ltr 112 | ? (k: L) => this.data.has(k) 113 | : (k: R) => iterHas(this.data.values(), k), 114 | [Symbol.iterator]: ltr 115 | ? () => this.data[Symbol.iterator]() 116 | : () => reverseIterator(this.data[Symbol.iterator]()), 117 | get: ltr ? (k: L) => this.data.get(k) : (k: R) => { 118 | for (const entry of this.data.entries()) { 119 | if (entry[1] === k) return entry[0]; 120 | } 121 | }, 122 | set: ltr 123 | ? (k: L, v: R) => { 124 | for (const entry of this.data) { 125 | if (entry[1] !== v) continue; 126 | this.data.delete(entry[0]); 127 | break; 128 | } 129 | this.data.set(k, v); 130 | return v; 131 | } 132 | : (k: R, v: L) => { 133 | this.data.set(v, k); 134 | return v; 135 | }, 136 | getOrSet: ltr 137 | ? (k: L, v: R) => 138 | this.data.has(k) ? this.data.get(k) : (this.data.set(k, v), v) 139 | : (k: R, v: L) => { 140 | for (const entry of this.data.entries()) { 141 | if (entry[1] === k) return entry[0]; 142 | } 143 | this.data.set(v, k); 144 | return v; 145 | }, 146 | delete: ltr ? (k: L) => this.data.delete(k) : (k: R) => { 147 | for (const entry of this.data) { 148 | if (entry[1] === k) return this.data.delete(entry[0]); 149 | } 150 | return false; 151 | }, 152 | clear: () => (this.clear(), ltr ? this.left : this.right), 153 | }; 154 | 155 | return new Proxy(map, { 156 | get: (t: any, p) => { 157 | if (p in t) return t[p]; 158 | if (p === "size") return this.data.size; 159 | return t.get(p); 160 | }, 161 | has: (t, p) => t.has(p), 162 | set: (t, p, v) => (t.set(p, v), true), 163 | deleteProperty: (t, p) => (t.delete(p), true), 164 | }); 165 | } 166 | 167 | private readonly data = new Map(); 168 | public readonly left: MapLike = this.proxy(true); 169 | public readonly right: MapLike = this.proxy(false); 170 | } 171 | 172 | export default BiMapImpl as 173 | & (new < 174 | L, 175 | R, 176 | AL extends string = never, 177 | AR extends string = never, 178 | >( 179 | data?: Map | readonly (readonly [L, R])[], 180 | aliasLeft?: AL, 181 | aliasRight?: AR, 182 | ) => BiMap) 183 | & typeof BiMapImpl; 184 | 185 | /** 186 | * Bidirectional map. Maps two sets of keys in a one-to-one relation. 187 | * 188 | * Both sides are accessible (at .left & .right, or at their respective alias if 189 | * one was provided in the constructor) with an interface similar to that of the 190 | * built-in Map and the same iteration behavior. 191 | * 192 | * @example 193 | * ``` 194 | * const nums = BiMap.from({ one: 1, two: 2 }) 195 | * 196 | * // different ways of iterating over the entries 197 | * [...nums.left] // [['one',1], ['two',2]] 198 | * [...nums.right] // [[1,'one'], [2,'two']] 199 | * [...nums.left.keys()] // ['one', 'two'] 200 | * [...nums.left.values()] // [1, 2] 201 | * [...nums.right.keys()] // [1, 2] 202 | * [...nums.right.values()] // ['one', 'two'] 203 | * [...nums] // [['one',1], ['two',2]] 204 | * [...nums.right.entries()] // [[1,'one'], [2,'two']] 205 | * Object.fromEntries(nums.right) // { '1': 'one', '2': 'two' } 206 | * 207 | * // setting a value 208 | * nums.left.three = 3 209 | * // when accessing a property using bracket notation (i.e. nums.right[4]), 210 | * // JavaScript coerces the key to a string, so keys that aren't strings or 211 | * // symbols must be accessed using the same access methods known from Map. 212 | * nums.right.set(4, 'four') 213 | * 214 | * // remapping values 215 | * nums.left.tres = 3 // {one: 1, two: 2, tres: 3, four: 4} 216 | * nums.right.set(4, 'cuatro') // {one: 1, two: 2, tres: 3, cuatro: 4} 217 | * 218 | * // deleting 219 | * delete nums.left.tres // {one: 1, two: 2, cuatro: 4} 220 | * nums.right.delete(4) // {one: 1, two: 2} 221 | * 222 | * // reversing the map 223 | * const num2Name = nums.reverse() 224 | * console.log([...num2Name.left]) // [[1,'one'], [2,'two']] 225 | * console.log(Object.fromEntries(num2Name.right)) // {one: 1, two: 2} 226 | * 227 | * // other methods known from built-in Map 228 | * nums.size // 2 229 | * nums.[left|right].size // 2 230 | * nums.clear() // equivalent to nums.[left|right].clear() 231 | * console.log(nums.size) // 0 232 | * ``` 233 | * 234 | * @example 235 | * ``` 236 | * // giving aliases to both sides 237 | * const dictionary = new BiMap( 238 | * [ 239 | * ['hello', 'hallo'], 240 | * ['bye', 'tschüss'], 241 | * ], 242 | * 'en', 243 | * 'de' 244 | * ) 245 | * 246 | * dictionary.de.get('hallo') // 'hello' 247 | * dictionary.en.get('bye') // 'tschüss' 248 | * 249 | * delete dictionary.de.hallo 250 | * console.log(Object.fromEntries(dictionary.en)) // { bye: 'tschüss' } 251 | * 252 | * // you can also use the BiMap.alias method: 253 | * BiMap.alias('en', 'de')() 254 | * BiMap.alias('en', 'de')([['hello', 'hallo']]) 255 | * BiMap.alias('en', 'de')(new Map()) 256 | * BiMap.alias('en', 'de')({ hello: 'hallo' }) 257 | * BiMap.alias('en', 'de')(new Set(['hello']), new Set(['hallo'])) 258 | * 259 | * // the same arguments can be used with BiMap.from, e.g.: 260 | * BiMap.from(new Set(), new Set()) 261 | * ``` 262 | */ 263 | export type BiMap< 264 | L, 265 | R, 266 | A extends string = never, 267 | B extends string = never, 268 | > = 269 | & Omit, "clone" | "reverse"> 270 | & { 271 | clone(): BiMap; 272 | reverse(): BiMap; 273 | } 274 | & { [K in A]: MapLike } 275 | & { [K in B]: MapLike }; 276 | 277 | type MapLike = 278 | & { 279 | keys(): IterableIterator; 280 | values(): IterableIterator; 281 | has(key: K): boolean; 282 | get(key: K): V | undefined; 283 | set(key: K, value: T): T; 284 | getOrSet(key: K, value: V): V; 285 | delete(key: K): boolean; 286 | clear(): MapLike; 287 | size: number; 288 | } 289 | & { [SK in Extract]: V } 290 | & IterableIterator<[K, V]>; 291 | 292 | function reverseIterator( 293 | iter: IterableIterator<[L, R]>, 294 | ): IterableIterator<[R, L]> { 295 | return { 296 | next() { 297 | const { done, value } = iter.next(); 298 | return { done, value: !value ? value : [value[1], value[0]] }; 299 | }, 300 | [Symbol.iterator]() { 301 | return this; 302 | }, 303 | }; 304 | } 305 | 306 | function iterHas(iter: IterableIterator, value: T) { 307 | for (const entry of iter) if (entry === value) return true; 308 | return false; 309 | } 310 | -------------------------------------------------------------------------------- /bundle.test.ts: -------------------------------------------------------------------------------- 1 | import bundle, { bundleSync } from "./bundle.ts"; 2 | import { assertEquals, assertRejects, assertThrows } from "testing/asserts.ts"; 3 | import { assertSpyCallArgs, assertSpyCalls, spy } from "testing/mock.ts"; 4 | 5 | Deno.test("bundle", async () => { 6 | const funA = spy((a: number, _b: string) => a); 7 | const funB = spy((_a: number, b: string) => b); 8 | 9 | assertEquals(await bundle(funA, funB)(1, "a"), undefined); 10 | assertSpyCalls(funA, 1); 11 | 12 | assertSpyCalls(funB, 1); 13 | assertSpyCallArgs(funA, 0, [1, "a"]); 14 | assertSpyCallArgs(funB, 0, [1, "a"]); 15 | 16 | await assertRejects(() => 17 | bundle( 18 | funA, 19 | (_a, _b) => { 20 | throw new Error(); 21 | }, 22 | funB, 23 | )(0, "") 24 | ); 25 | assertSpyCalls(funA, 2); 26 | assertSpyCalls(funB, 2); 27 | assertSpyCallArgs(funA, 1, [0, ""]); 28 | assertSpyCallArgs(funB, 1, [0, ""]); 29 | }); 30 | 31 | Deno.test("bundle sync", () => { 32 | const funA = spy((a: number, _b: string) => a); 33 | const funB = spy((_a: number, b: string) => b); 34 | 35 | assertEquals(bundleSync(funA, funB)(1, "a"), undefined); 36 | assertSpyCalls(funA, 1); 37 | assertSpyCalls(funB, 1); 38 | assertSpyCallArgs(funA, 0, [1, "a"]); 39 | assertSpyCallArgs(funB, 0, [1, "a"]); 40 | 41 | assertThrows(() => 42 | bundleSync( 43 | funA, 44 | (_a, _b) => { 45 | throw new Error(); 46 | }, 47 | funB, 48 | )(0, "") 49 | ); 50 | assertSpyCalls(funA, 2); 51 | assertSpyCalls(funB, 1); 52 | assertSpyCallArgs(funA, 1, [0, ""]); 53 | }); 54 | -------------------------------------------------------------------------------- /bundle.ts: -------------------------------------------------------------------------------- 1 | import callAll from "./callAll.ts"; 2 | import type { λ } from "./types.ts"; 3 | 4 | /** 5 | * Given a list of functions that accept the same parameters, returns a function 6 | * that takes these parameters and invokes all of the given functions. 7 | * 8 | * The returned function returns a promise that resolves once all functions 9 | * returned/resolved and rejects if any of the functions throws/rejects - but 10 | * only after all returned promises have been settled. 11 | */ 12 | const bundle = 13 | (...funs: (λ | undefined)[]) => 14 | async (...args: T): Promise => { 15 | const res = await Promise.allSettled( 16 | funs.map((f) => (async () => await f?.(...args))()), 17 | ); 18 | res.forEach((v) => { 19 | if (v.status === "rejected") throw v.reason; 20 | }); 21 | }; 22 | 23 | /** 24 | * Same as {@link bundle}, but return synchronously. 25 | * 26 | * If any of the functions throws an error synchronously, none of the functions 27 | * after it will be invoked and the error will propagate. 28 | */ 29 | export const bundleSync = ( 30 | ...funs: (λ | undefined)[] 31 | ) => 32 | (...args: T) => 33 | void callAll(funs.filter((f) => f !== undefined) as λ[], ...args); 34 | 35 | export default bundle; 36 | -------------------------------------------------------------------------------- /callAll.test.ts: -------------------------------------------------------------------------------- 1 | import callAll from "./callAll.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | import { assertSpyCalls, spy } from "testing/mock.ts"; 4 | 5 | Deno.test("call all", () => { 6 | const square = spy((n: number) => n ** 2); 7 | const cube = spy((n: number) => n ** 3); 8 | 9 | assertEquals(callAll([square, cube], 2), [4, 8]); 10 | assertSpyCalls(square, 1); 11 | assertSpyCalls(cube, 1); 12 | 13 | // @ts-expect-error 14 | callAll([(_n: number) => 0]); 15 | 16 | // @ts-expect-error 17 | callAll([(_n: number) => 0], ""); 18 | 19 | // @ts-expect-error 20 | callAll([square, (_n: string) => 0], 2); 21 | 22 | // @ts-expect-error 23 | const _str: string[] = callAll([() => 2]); 24 | 25 | // @ts-expect-error 26 | const [_a, _b] = callAll([() => 2]); 27 | }); 28 | -------------------------------------------------------------------------------- /callAll.ts: -------------------------------------------------------------------------------- 1 | import type { λ } from "./types.ts"; 2 | 3 | /** 4 | * Take a list of functions that accept the same parameters and call them all 5 | * with the provided arguments. 6 | * 7 | * @example 8 | * ``` 9 | * const mult = (a: number, b: number) => a * b 10 | * const div = (a: number, b: number) => a / b 11 | * 12 | * // prints: [8, 2] 13 | * console.log( callAll([mult, div], 4, 2) ) 14 | * ``` 15 | */ 16 | const callAll =

[]>( 17 | funs: [...F], 18 | ...args: P 19 | ): ReturnTypes => (funs.map((cb) => cb(...args)) ?? []) as any; 20 | 21 | type ReturnTypes = { 22 | [K in keyof T]: T[K] extends λ ? I : never; 23 | }; 24 | 25 | export default callAll; 26 | -------------------------------------------------------------------------------- /case.test.ts: -------------------------------------------------------------------------------- 1 | import * as c from "./case.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("capitalize", () => { 5 | assertEquals(c.capitalize("foo"), "Foo"); 6 | 7 | // @ts-expect-error 8 | const _wrong: "FOO" = c.capitalize("foo"); 9 | }); 10 | 11 | Deno.test("uncapitalize", () => { 12 | assertEquals(c.uncapitalize("Foo"), "foo"); 13 | 14 | // @ts-expect-error 15 | const _wrong: "Foo" = c.uncapitalize("Foo"); 16 | }); 17 | 18 | Deno.test("uppercase", () => { 19 | assertEquals(c.upper("foo"), "FOO"); 20 | 21 | // @ts-expect-error 22 | const _wrong: "foo" = c.upper("foo"); 23 | }); 24 | 25 | Deno.test("lowercase", () => { 26 | assertEquals(c.lower("FOO"), "foo"); 27 | 28 | // @ts-expect-error 29 | const _wrong: "FOO" = c.lower("FOO"); 30 | }); 31 | 32 | Deno.test("snake case", () => { 33 | assertEquals(c.snake("fooBar"), "foo_bar"); 34 | assertEquals(c.snake("FooBar"), "foo_bar"); 35 | assertEquals(c.snake("fooBarABC0D"), "foo_bar_ABC0D"); 36 | assertEquals(c.snake("fooBarABC0DfooBar"), "foo_bar_ABC0D_foo_bar"); 37 | assertEquals(c.snake("fooBarABC0fooBar"), "foo_bar_ABC0_foo_bar"); 38 | assertEquals(c.snake("foo_Bar"), "foo_bar"); 39 | assertEquals(c.snake("foo_Bar"), "foo_bar"); 40 | assertEquals(c.snake("foo-bar"), "foo_bar"); 41 | assertEquals(c.snake("foo-Bar"), "foo_bar"); 42 | }); 43 | 44 | Deno.test("screaming snake case", () => { 45 | assertEquals(c.screamingSnake("fooBar"), "FOO_BAR"); 46 | }); 47 | 48 | Deno.test("kebab case", () => { 49 | assertEquals(c.kebab("fooBar"), "foo-bar"); 50 | assertEquals(c.kebab("FooBar"), "foo-bar"); 51 | assertEquals(c.kebab("fooBarABC0D"), "foo-bar-ABC0D"); 52 | assertEquals(c.kebab("fooBarABC0DfooBar"), "foo-bar-ABC0D-foo-bar"); 53 | assertEquals(c.kebab("fooBarABC0fooBar"), "foo-bar-ABC0-foo-bar"); 54 | assertEquals(c.kebab("foo_Bar"), "foo-bar"); 55 | assertEquals(c.kebab("foo_Bar"), "foo-bar"); 56 | assertEquals(c.kebab("foo-bar"), "foo-bar"); 57 | assertEquals(c.kebab("foo-Bar"), "foo-bar"); 58 | }); 59 | 60 | Deno.test("camel case", () => { 61 | assertEquals(c.camel("foo_bar"), "fooBar"); 62 | assertEquals(c.camel("foo-bar"), "fooBar"); 63 | assertEquals(c.camel("foo-Bar"), "fooBar"); 64 | assertEquals(c.camel("FooBar"), "fooBar"); 65 | assertEquals(c.camel("__foo_bar__baz__"), "__fooBar_Baz__"); 66 | assertEquals(c.camel("-_foo_bar-_baz_-"), "-_fooBar-Baz_-"); 67 | }); 68 | 69 | Deno.test("pascal case", () => { 70 | assertEquals(c.pascal("foo_bar"), "FooBar"); 71 | }); 72 | 73 | Deno.test("transform case", () => { 74 | assertEquals(c.transformCase("foo_bar", "camel"), "fooBar"); 75 | assertEquals(c.transformCase("foo_bar", "pascal"), "FooBar"); 76 | assertEquals(c.transformCase("fooBar", "snake"), "foo_bar"); 77 | assertEquals(c.transformCase("fooBar", "screaming-snake"), "FOO_BAR"); 78 | assertEquals(c.transformCase("fooBar", "kebab"), "foo-bar"); 79 | }); 80 | -------------------------------------------------------------------------------- /case.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CamelCase, 3 | KebabCase, 4 | PascalCase, 5 | ScreamingSnakeCase, 6 | SnakeCase, 7 | StringCase, 8 | λ, 9 | } from "./types.ts"; 10 | 11 | /** Upper-case first letter of string. */ 12 | export const capitalize = (str: T) => 13 | (str[0].toUpperCase() + str.slice(1)) as Capitalize; 14 | 15 | /** Lower-case first letter of string */ 16 | export const uncapitalize = (str: T) => 17 | (str[0].toLowerCase() + str.slice(1)) as Uncapitalize; 18 | 19 | /** Strictly typed `String.toUpperCase()`. */ 20 | export const upper = (str: T) => 21 | str.toUpperCase() as Uppercase; 22 | 23 | /** Strictly typed `String.toLowerCase()`. */ 24 | export const lower = (str: T) => 25 | str.toLowerCase() as Lowercase; 26 | 27 | /** 28 | * Transforms a variable name to snake case. 29 | * 30 | * Note: The rules for transforming anything to snake case are somewhat vague. 31 | * So use this only for very simple names where the resulting value is 32 | * absolutely unambiguous. For more examples of how names are transformed, have 33 | * a look at the test cases. 34 | * 35 | * @example 36 | * ``` 37 | * snake('fooBar') // 'foo_bar' 38 | * ``` 39 | */ 40 | export const snake = (str: T): SnakeCase => 41 | str 42 | .replace(/(\p{L})-(?=\p{L})/gu, "$1_") 43 | .replace(/(^|_)(\p{Lu})(?!\p{Lu})/gu, (_, a, b) => `${a}${b.toLowerCase()}`) 44 | .replace(/([^\p{Lu}])(\p{Lu})(?=\p{Lu})/gu, (_, a, b) => `${a}_${b}`) 45 | .replace( 46 | /([^\p{Lu}_0-9])(\p{Lu})/gu, 47 | (_, a, b) => `${a}_${b.toLowerCase()}`, 48 | ) 49 | .replace(/(\p{Lu}[^\p{L}_]*)(\p{Ll})/gu, (_, a, b) => `${a}_${b}`) as any; 50 | 51 | /** 52 | * Transforms a variable name to kebab case. 53 | * 54 | * Note: The rules for transforming anything to kebab case are somewhat vague. 55 | * So use this only for very simple names where the resulting value is 56 | * absolutely unambiguous. For more examples of how names are transformed, have 57 | * a look at the test cases. 58 | * 59 | * @example 60 | * ``` 61 | * kebab('fooBar') // 'foo-bar' 62 | * ``` 63 | */ 64 | export const kebab = (str: T): KebabCase => 65 | str 66 | .replace(/(\p{L})_(?=\p{L})/gu, "$1-") 67 | .replace(/(^|-)(\p{Lu})(?!\p{Lu})/gu, (_, a, b) => `${a}${b.toLowerCase()}`) 68 | .replace(/([^\p{Lu}])(\p{Lu})(?=\p{Lu})/gu, (_, a, b) => `${a}-${b}`) 69 | .replace( 70 | /([^\p{Lu}\-0-9])(\p{Lu})/gu, 71 | (_, a, b) => `${a}-${b.toLowerCase()}`, 72 | ) 73 | .replace(/(\p{Lu}[^\p{L}\-]*)(\p{Ll})/gu, (_, a, b) => `${a}-${b}`) as any; 74 | 75 | /** 76 | * Transforms a variable name to camel case. 77 | * 78 | * Note: The rules for transforming anything to camel case are somewhat vague. 79 | * So use this only for very simple names where the resulting value is 80 | * absolutely unambiguous. For more examples of how names are transformed, have 81 | * a look at the test cases. 82 | * 83 | * @example 84 | * ``` 85 | * camel('foo_bar') // 'fooBar' 86 | * ``` 87 | */ 88 | export const camel = (str: T): CamelCase => 89 | str 90 | .replace(/^\p{Lu}/u, (v) => v.toLowerCase()) 91 | .replace( 92 | /([^_-][_-]*)[_-](\p{L})/gu, 93 | (_, a, b) => a + b.toUpperCase(), 94 | ) as any; 95 | 96 | /** 97 | * Transforms a variable name to pascal case. 98 | * 99 | * Note: The rules for transforming anything to pascal case are somewhat vague. 100 | * So use this only for very simple names where the resulting value is 101 | * absolutely unambiguous. For more examples of how names are transformed, have 102 | * a look at the test cases. 103 | * 104 | * @example 105 | * ``` 106 | * pascal('foo_bar') // 'FooBar' 107 | * ``` 108 | */ 109 | export const pascal = (str: T): PascalCase => 110 | capitalize(camel(str)); 111 | 112 | /** 113 | * Transforms a variable name to screaming snake case. 114 | * 115 | * @see {@link snake} 116 | * 117 | * @example 118 | * ``` 119 | * screamingSnake('fooBar') // 'FOO_BAR' 120 | * ``` 121 | */ 122 | export const screamingSnake = ( 123 | str: T, 124 | ): ScreamingSnakeCase => upper(snake(str)); 125 | 126 | /** 127 | * Transform a variable name to `targetCase` 128 | * 129 | * @see {@link snake} 130 | * @see {@link kebab} 131 | * @see {@link camel} 132 | * @see {@link pascal} 133 | * @see {@link screamingSnake} 134 | */ 135 | export const transformCase = ( 136 | str: T, 137 | targetCase: C, 138 | ): C extends "snake" ? SnakeCase : never => { 139 | if (!(targetCase in converters)) { 140 | throw Error(`can't convert to ${targetCase} case`); 141 | } 142 | return converters[targetCase](str) as any; 143 | }; 144 | 145 | const converters: Record> = { 146 | camel, 147 | kebab, 148 | pascal, 149 | snake, 150 | "screaming-snake": screamingSnake, 151 | }; 152 | -------------------------------------------------------------------------------- /clamp.test.ts: -------------------------------------------------------------------------------- 1 | import clamp from "./clamp.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("clamp", () => { 5 | assertEquals(clamp(0, 2, 1), 1); 6 | assertEquals(clamp(-2, -Infinity, 10), -2); 7 | assertEquals(clamp(20, 25, 30), 25); 8 | assertEquals(clamp(30, 25, 20), 25); 9 | assertEquals(clamp(30, 50, 20), 30); 10 | }); 11 | -------------------------------------------------------------------------------- /clamp.ts: -------------------------------------------------------------------------------- 1 | /** Clamp `num` between `min` and `max` inclusively. */ 2 | const clamp = (min: number, num: number, max: number) => { 3 | if (min > max) [min, max] = [max, min]; 4 | return Math.max(Math.min(num, max), min); 5 | }; 6 | 7 | export default clamp; 8 | -------------------------------------------------------------------------------- /clone.test.ts: -------------------------------------------------------------------------------- 1 | import clone from "./clone.ts"; 2 | import { assertEquals, assertNotStrictEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("clone", () => { 5 | assertEquals(clone(1), 1); 6 | assertEquals(clone(null), null); 7 | assertEquals(clone("foo"), "foo"); 8 | 9 | { 10 | const v = [1, "a", { foo: ["bar", { baz: [[2, 3]] }, "a"] }]; 11 | assertEquals(clone(v), v); 12 | assertNotStrictEquals(clone(v), v); 13 | } 14 | 15 | { 16 | const v: any = {}; 17 | v.p = v; 18 | 19 | const cloned = clone(v); 20 | assertEquals(cloned, v); 21 | assertNotStrictEquals(cloned, v); 22 | 23 | assertEquals(cloned.p, cloned); 24 | assertNotStrictEquals(cloned.p, v); 25 | } 26 | 27 | { 28 | const v: any = { foo: {} }; 29 | v.foo.bar = v; 30 | 31 | const cloned = clone(v); 32 | assertEquals(cloned, v); 33 | assertNotStrictEquals(cloned, v); 34 | 35 | assertEquals(cloned.foo.bar, cloned); 36 | assertNotStrictEquals(cloned.foo.bar, v); 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /clone.ts: -------------------------------------------------------------------------------- 1 | import type { λ } from "./types.ts"; 2 | 3 | /** 4 | * Returns a copied version of `value`. 5 | * 6 | * If `value` is primitive, returns `value`. 7 | * Otherwise, properties of `value` are copied recursively. Only `value`'s own 8 | * enumerable properties are cloned. Arrays are cloned by mapping over their 9 | * elements. 10 | * 11 | * If a path in `value` references itself or a parent path, then in the 12 | * resulting object that path will also reference the path it referenced in the 13 | * original object (but now in the resuling object instead of the original). 14 | */ 15 | const clone: (value: T) => T = globalThis.structuredClone ?? cloneFallback; 16 | 17 | export default clone; 18 | 19 | function cloneFallback(value: T): T { 20 | const map = new Map(); 21 | const replacers: λ[] = []; 22 | const cloned = _clone(value, map, replacers, undefined as any); 23 | for (const f of replacers) f(); 24 | return cloned; 25 | } 26 | 27 | function _clone( 28 | v: any, 29 | visited: Map, 30 | replacers: λ[], 31 | replace: λ<[λ]>, 32 | ): any { 33 | if (typeof v !== "object" || v === null) return v; 34 | if (visited.has(v)) return replace(v); 35 | visited.set(v, 0); 36 | 37 | const cloneNext = (v: any, r: (v: any) => void) => 38 | _clone(v, visited, replacers, (v: λ) => replacers.push(() => r(v))); 39 | 40 | const cloned: any = Array.isArray(v) 41 | ? v.map((e, i) => cloneNext(e, (v) => (cloned[i] = visited.get(v)))) 42 | : Object.fromEntries( 43 | Object.entries(v).map(([k, e]) => [ 44 | k, 45 | cloneNext(e, (v) => (cloned[k] = visited.get(v))), 46 | ]), 47 | ); 48 | 49 | visited.set(v, cloned); 50 | return cloned; 51 | } 52 | -------------------------------------------------------------------------------- /debounce.test.ts: -------------------------------------------------------------------------------- 1 | import debounce from "./debounce.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test( 5 | "debounce", 6 | () => 7 | new Promise((done) => { 8 | const args: number[] = []; 9 | 10 | const fun = (n: number) => { 11 | args.push(n); 12 | }; 13 | 14 | const debounced = debounce(fun, 50); 15 | 16 | let i = 0; 17 | const iid = setInterval(() => { 18 | debounced(++i); 19 | if (i === 3) clearInterval(iid); 20 | }, 5); 21 | 22 | setTimeout(() => { 23 | assertEquals(args.length, 1); 24 | assertEquals(args[0], 3); 25 | done(); 26 | }, 100); 27 | }), 28 | ); 29 | 30 | Deno.test( 31 | "cancel debounce", 32 | () => 33 | new Promise((done) => { 34 | const debounced = debounce(() => { 35 | throw Error(); 36 | }, 25); 37 | 38 | debounced(); 39 | setTimeout(() => debounced[debounce.cancel](), 10); 40 | 41 | setTimeout(done, 35); 42 | }), 43 | ); 44 | -------------------------------------------------------------------------------- /debounce.ts: -------------------------------------------------------------------------------- 1 | import type { λ } from "./types.ts"; 2 | 3 | export const cancel = Symbol("debounce.cancel"); 4 | 5 | /** 6 | * Creates a debounced function that delays invoking `fun` until `ms` milliseconds 7 | * have passed since the last invocation of the debounced function. 8 | * 9 | * `fun` is invoked with the last arguments passed to the debounced function. 10 | * 11 | * Calling `[debounce.cancel]()` on the debounced function will cancel the currently 12 | * scheduled invocation. 13 | */ 14 | const debounce = Object.assign( 15 | (fun: λ, ms: number) => { 16 | let toId: any; 17 | return Object.assign( 18 | (...args: any[]) => { 19 | clearTimeout(toId); 20 | toId = setTimeout(() => fun(...args), ms); 21 | }, 22 | { [cancel]: () => clearTimeout(toId) }, 23 | ) as any; 24 | }, 25 | { cancel }, 26 | ) as 27 | & (( 28 | fun: T, 29 | ms: number, 30 | ) => λ, void> & { [cancel](): void }) 31 | & { 32 | cancel: typeof cancel; 33 | }; 34 | 35 | export default debounce; 36 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "lint": { 3 | "files": { "include": ["src/"] }, 4 | "rules": { 5 | "exclude": [ 6 | "ban-ts-comment", 7 | "no-explicit-any", 8 | "require-await", 9 | "ban-types" 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2", 3 | "remote": { 4 | "https://deno.land/std@0.138.0/fmt/colors.ts": "30455035d6d728394781c10755351742dd731e3db6771b1843f9b9e490104d37", 5 | "https://deno.land/std@0.138.0/testing/_diff.ts": "029a00560b0d534bc0046f1bce4bd36b3b41ada3f2a3178c85686eb2ff5f1413", 6 | "https://deno.land/std@0.138.0/testing/_format.ts": "0d8dc79eab15b67cdc532826213bbe05bccfd276ca473a50a3fc7bbfb7260642", 7 | "https://deno.land/std@0.138.0/testing/asserts.ts": "dc7ab67b635063989b4aec8620dbcc6fa7c2465f2d9c856bddf8c0e7b45b4481", 8 | "https://deno.land/std@0.138.0/testing/mock.ts": "d9630b551fe59a81b3c214c1a6ccb67819b6e34fc20e3424a659528fe1f572c1" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ds.ts: -------------------------------------------------------------------------------- 1 | export { default as BiMap } from "./bimap.ts"; 2 | export { default as SortedArray } from "./sortedArray.ts"; 3 | export { default as SortedMap } from "./sortedMap.ts"; 4 | -------------------------------------------------------------------------------- /equal.test.ts: -------------------------------------------------------------------------------- 1 | import equal from "./equal.ts"; 2 | import { assert, assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("equal", () => { 5 | assert(equal(1, 1)); 6 | assertEquals(equal(1, 2), false); 7 | assertEquals(equal(1, "1"), false); 8 | assert(equal("1", "1")); 9 | 10 | assert(equal(null, null)); 11 | assertEquals(equal(null, undefined), false); 12 | assertEquals(equal(null, {}), false); 13 | 14 | assert(equal([], [])); 15 | assertEquals(equal([], {}), false); 16 | assertEquals(equal([], [1]), false); 17 | assert(equal([1], [1])); 18 | assertEquals(equal([1], [1, "a"]), false); 19 | assert(equal([1, "a"], [1, "a"])); 20 | assertEquals(equal([1, [2, [3, [4, 5], 6]]], []), false); 21 | assert(equal([1, [2, [3, [4, 5], 6]]], [1, [2, [3, [4, 5], 6]]])); 22 | 23 | { 24 | const obj = {}; 25 | assert(equal(obj, obj)); 26 | assert(equal(obj, {})); 27 | } 28 | 29 | assertEquals(equal({ foo: "bar" }, { foo: "baz" }), false); 30 | assert(equal({ foo: "bar" }, { foo: "bar" })); 31 | assertEquals(equal({ foo: "bar" }, { foo: "bar", bar: "baz" }), false); 32 | assertEquals(equal({ a: { b: { c: "d" }, e: "f" } }, {}), false); 33 | assertEquals( 34 | equal({ a: { b: { c: "d" }, e: "f" } }, { a: { b: { c: "d" }, e: "f" } }), 35 | true, 36 | ); 37 | assertEquals( 38 | equal({ a: [{ b: 1 }, { c: [[{ d: ["e"] }]] }] }, { foo: [] }), 39 | false, 40 | ); 41 | assertEquals( 42 | equal( 43 | { a: [{ b: 1 }, { c: [[{ d: ["e"] }]] }] }, 44 | { a: [{ b: 1 }, { c: [[{ d: ["e"] }]] }] }, 45 | ), 46 | true, 47 | ); 48 | 49 | { 50 | const fun = () => {}; 51 | assert(equal(fun, fun)); 52 | assertEquals( 53 | equal(fun, () => {}), 54 | false, 55 | ); 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /equal.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if `a` and `b` are structurally equal using the following algorithm: 3 | * 4 | * - primitives are compared by value 5 | * - functions are compared by reference 6 | * - objects (including arrays) are checked to have the same properties and 7 | * their values are compared recursively using the same algorithm 8 | */ 9 | export default function equal(a: unknown, b: unknown): boolean { 10 | if (typeof a !== "object" && typeof b !== "object") return a === b; 11 | if ((typeof a === "object") !== (typeof b === "object")) return false; 12 | if (a === b) return true; // null or ref equality 13 | if (a === null || b === null) return false; 14 | if (Array.isArray(a) !== Array.isArray(b)) return false; 15 | 16 | if (Object.keys(a as any).length !== Object.keys(b as any).length) { 17 | return false; 18 | } 19 | 20 | return Object.entries(a as any).every( 21 | ([k, v]) => k in (b as any) && equal(v, (b as any)[k]), 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /equality.ts: -------------------------------------------------------------------------------- 1 | export { default as oneOf } from "./oneOf.ts"; 2 | export { default as equal } from "./equal.ts"; 3 | export { default as clone } from "./clone.ts"; 4 | export { default as merge } from "./merge.ts"; 5 | -------------------------------------------------------------------------------- /error.ts: -------------------------------------------------------------------------------- 1 | export class UniqueViolationError extends Error { 2 | constructor(msg: string) { 3 | super(`[UniqueViolationError]: ${msg}`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /except.ts: -------------------------------------------------------------------------------- 1 | export const assert = (condition: unknown, message?: string, type = Error) => { 2 | if (!(typeof condition === "function" ? condition() : condition)) { 3 | throw new type(message); 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /forward.test.ts: -------------------------------------------------------------------------------- 1 | import forward from "./forward.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("forward arguments", () => { 5 | const fun = (a: string, b: number, c: boolean) => JSON.stringify([a, b, c]); 6 | 7 | const funA = forward(fun, 1, true); 8 | assertEquals(funA("a"), JSON.stringify(["a", 1, true])); 9 | 10 | const funAB = forward(fun, false); 11 | assertEquals(funAB("b", 2), JSON.stringify(["b", 2, false])); 12 | }); 13 | -------------------------------------------------------------------------------- /forward.ts: -------------------------------------------------------------------------------- 1 | import type { λ } from "./types.ts"; 2 | 3 | /** 4 | * Given a function and its nth..last arguments, return a function accepting 5 | * arguments 0..n-1. 6 | * 7 | * @example 8 | * ``` 9 | * const divide = (dividend: number, divisor: number) => dividend / divisor 10 | * 11 | * // (dividend: number) => number 12 | * const divideBy2 = forward(divide, 2) 13 | * 14 | * // prints: 0.5 15 | * console.log(divideBy2(1)) 16 | * ``` 17 | * 18 | * @example 19 | * ``` 20 | * const fetchUrl = async (protocol: string, domain: string, path: string) => 21 | * await fetch(`${protocol}://${domain}/${path}`) 22 | * 23 | * const fetchRepo = forward(fetchUrl, 'github.com', 'MathisBullinger/froebel') 24 | * 25 | * const viaHTTPS = await fetchRepo('https') 26 | * ``` 27 | */ 28 | const forward = (fun: T, ...argsRight: PR) => 29 | ( 30 | ...argsLeft: Parameters extends [...infer PL, ...PR] ? PL : never 31 | ): ReturnType => fun(...argsLeft, ...argsRight); 32 | 33 | export default forward; 34 | -------------------------------------------------------------------------------- /function.ts: -------------------------------------------------------------------------------- 1 | // deno-fmt-ignore-file 2 | export { default as ident } from "./ident.ts"; 3 | export { default as noop } from "./noop.ts"; 4 | export { default as partial } from "./partial.ts"; 5 | export { default as forward } from "./forward.ts"; 6 | export { default as unary } from "./unary.ts"; 7 | export { default as callAll } from "./callAll.ts"; 8 | export { default as pipe, applyPipe } from "./pipe.ts"; 9 | export { default as bundle, bundleSync } from "./bundle.ts"; 10 | export { nullishChain, asyncNullishChain } from "./nullishChain.ts"; 11 | export { default as throttle } from "./throttle.ts"; 12 | export { default as debounce } from "./debounce.ts"; 13 | export { default as memoize } from "./memoize.ts"; 14 | export { limitInvocations, once } from "./invoke.ts"; 15 | -------------------------------------------------------------------------------- /ident.test.ts: -------------------------------------------------------------------------------- 1 | import ident from "./ident.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("identity function", () => { 5 | assertEquals(ident(1), 1); 6 | const obj = {}; 7 | assertEquals(ident(obj), obj); 8 | }); 9 | -------------------------------------------------------------------------------- /ident.ts: -------------------------------------------------------------------------------- 1 | /** Identity function. */ 2 | const ident = (value: T): T => value; 3 | 4 | export default ident; 5 | -------------------------------------------------------------------------------- /import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "testing/": "https://deno.land/std@0.138.0/testing/" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /invoke.test.ts: -------------------------------------------------------------------------------- 1 | import { limitInvocations, once } from "./invoke.ts"; 2 | import { assertEquals, assertThrows } from "testing/asserts.ts"; 3 | 4 | Deno.test("once", () => { 5 | once(() => {}); 6 | once( 7 | () => {}, 8 | // @ts-expect-error 9 | (_arg) => {}, 10 | ); 11 | once( 12 | () => {}, 13 | () => {}, 14 | ); 15 | // @ts-expect-error 16 | once(() => 0); 17 | once( 18 | () => 0, 19 | () => 0, 20 | ); 21 | once( 22 | () => 0, 23 | // @ts-expect-error 24 | () => "", 25 | ); 26 | once( 27 | () => 0, 28 | // @ts-expect-error 29 | async () => 0, 30 | ); 31 | 32 | once(async () => {}); 33 | // @ts-expect-error 34 | once(async () => 0); 35 | once( 36 | async () => 0, 37 | async () => 0, 38 | ); 39 | once( 40 | async () => 0, 41 | () => 0, 42 | ); 43 | 44 | // @ts-expect-error 45 | once((_n: number) => {})(); 46 | once((_n: number) => {})(1); 47 | // @ts-expect-error 48 | once((_n: number) => {})(1, 2); 49 | // @ts-expect-error 50 | once((_a: number, _b: strin) => {})(1); 51 | once((_a: number, _b: string) => {})(1, "a"); 52 | 53 | const _rn: number = once( 54 | () => 1, 55 | () => 2, 56 | )(); 57 | // @ts-expect-error 58 | const _rs: string = once( 59 | () => 1, 60 | () => 2, 61 | )(); 62 | 63 | { 64 | const f = once( 65 | () => 1, 66 | () => 2, 67 | ); 68 | assertEquals(f(), 1); 69 | assertEquals(f(), 2); 70 | assertEquals(f(), 2); 71 | } 72 | }); 73 | 74 | Deno.test("limit", () => { 75 | const f = limitInvocations( 76 | () => "a", 77 | 2, 78 | () => "b", 79 | ); 80 | assertEquals(f(), "a"); 81 | assertEquals(f(), "a"); 82 | assertEquals(f(), "b"); 83 | 84 | const f3 = limitInvocations( 85 | () => 0, 86 | 3, 87 | () => { 88 | throw Error("4th invoke"); 89 | }, 90 | ); 91 | assertEquals(f3(), 0); 92 | assertEquals(f3(), 0); 93 | assertEquals(f3(), 0); 94 | assertThrows(() => f3(), Error, "4th invoke"); 95 | }); 96 | -------------------------------------------------------------------------------- /invoke.ts: -------------------------------------------------------------------------------- 1 | import type { λ } from "./types.ts"; 2 | import { assert } from "./except.ts"; 3 | 4 | /** 5 | * Returns a version of the function `fun` that can only be invoked `limit` 6 | * times. 7 | * An optional `except` function will be called with the same parameters on any 8 | * additional invocations. 9 | * 10 | * If `fun` returns anything but `void` (or `Promise`), supplying an 11 | * `except` function is mandatory. 12 | * 13 | * The `except` function must have the same return type as `fun`, or — if `fun` 14 | * returns a promise — it may return the type that the promise resolves to 15 | * synchronously. 16 | * 17 | * The `except` function may also throw instead of returning a value. 18 | */ 19 | export const limitInvocations = ( 20 | fun: T, 21 | limit: number, 22 | ...[except]: ExcS 23 | ): T => { 24 | assert(limit >= 1, "limit must be >= 1", RangeError); 25 | let invs = 0; 26 | return ((...args: Parameters) => { 27 | if (invs < limit) { 28 | invs++; 29 | return fun(...args); 30 | } 31 | if (typeof except === "function") return except(...args); 32 | }) as T; 33 | }; 34 | 35 | /** 36 | * Special case of {@link limitInvocations}. `fun` can only be invoked once. 37 | * 38 | * @see {@link limitInvocations} 39 | */ 40 | export const once = (fun: T, ...[except]: ExcS): T => { 41 | let invs = 0; 42 | return ((...args: Parameters) => 43 | ++invs > 1 ? except?.(...args) : fun(...args)) as T; 44 | }; 45 | 46 | type ExcS = ReturnType extends void | PromiseLike 47 | ? [except?: Exc] 48 | : [except: Exc]; 49 | type Exc = λ, OptProm>>; 50 | type OptProm = T extends Promise ? I | Promise : T; 51 | -------------------------------------------------------------------------------- /isPromise.test.ts: -------------------------------------------------------------------------------- 1 | import isPromise, { isNotPromise } from "./isPromise.ts"; 2 | import { assertEquals, assertNotEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("is promise", () => { 5 | const prom = new Promise(() => {}); 6 | 7 | assertEquals(isPromise(2), false); 8 | assertEquals(isPromise(""), false); 9 | assertEquals(isPromise(null), false); 10 | assertEquals(isPromise({}), false); 11 | assertEquals( 12 | isPromise(() => {}), 13 | false, 14 | ); 15 | assertEquals( 16 | isPromise(async () => {}), 17 | false, 18 | ); 19 | assertEquals(isPromise({ then: "" }), false); 20 | 21 | assertEquals(isPromise(prom), true); 22 | assertEquals(isPromise((async () => {})()), true); 23 | assertEquals(isPromise({ then() {} }), true); 24 | 25 | assertNotEquals(isPromise(1), isNotPromise(1)); 26 | assertNotEquals(isPromise(prom), isNotPromise(prom)); 27 | }); 28 | -------------------------------------------------------------------------------- /isPromise.ts: -------------------------------------------------------------------------------- 1 | /** Checks if `value` looks like a promise. */ 2 | const isPromise = (value: unknown): value is Promise => 3 | typeof value === "object" && 4 | value !== null && 5 | typeof (value as any).then === "function"; 6 | 7 | export default isPromise; 8 | 9 | /** 10 | * Checks if `value` is not a promise. 11 | * 12 | * @example 13 | * ``` 14 | * (value: number | Promise) => { 15 | * if (isNotPromise(value)) return value / 2 16 | * } 17 | * ``` 18 | */ 19 | export const isNotPromise = (value: T): value is Exclude> => 20 | !isPromise(value); 21 | -------------------------------------------------------------------------------- /iterable.ts: -------------------------------------------------------------------------------- 1 | export { default as repeat } from "./repeat.ts"; 2 | export { takeGenerator as take } from "./take.ts"; 3 | -------------------------------------------------------------------------------- /list.ts: -------------------------------------------------------------------------------- 1 | // deno-fmt-ignore-file 2 | export { default as atWrap } from "./atWrap.ts"; 3 | export { default as zip, zipWith } from "./zip.ts"; 4 | export { default as unzip, unzipWith } from "./unzip.ts"; 5 | export { default as batch } from "./batch.ts"; 6 | export { default as partition } from "./partition.ts"; 7 | export { default as shuffle, shuffleInPlace } from "./shuffle.ts"; 8 | export { takeList as take } from "./take.ts"; 9 | export { default as range, numberRange, alphaRange } from "./range.ts"; 10 | -------------------------------------------------------------------------------- /map.test.ts: -------------------------------------------------------------------------------- 1 | import map from "./map.ts"; 2 | import { assertEquals, assertThrows } from "testing/asserts.ts"; 3 | 4 | Deno.test("map", () => { 5 | { 6 | const input = { foo: 1, [Symbol("bar")]: 2 }; 7 | const mapped = map(input, (k, v) => [k, v]); 8 | type _0 = Expect>; 9 | assertEquals(mapped, input); 10 | // @ts-expect-error 11 | type _1 = Expect>; 12 | // @ts-expect-error 13 | type _2 = Expect>; 14 | } 15 | 16 | { 17 | const input = new Map([["1", "2"], ["3", "4"]]); 18 | const mapped: Map = map( 19 | input, 20 | (key, value) => [parseInt(key), parseInt(value)], 21 | ); 22 | assertEquals(mapped, new Map([[1, 2], [3, 4]])); 23 | } 24 | 25 | { 26 | const input = ["1", "2", "3"]; 27 | const mapped: number[] = map(input, (v) => parseInt(v)); 28 | assertEquals(mapped, [1, 2, 3]); 29 | } 30 | 31 | { 32 | const input = new Set(["1", "2", "3"]); 33 | const mapped: Set = map(input, (v) => parseInt(v)); 34 | assertEquals(mapped, new Set([1, 2, 3])); 35 | } 36 | 37 | // @ts-expect-error 38 | assertThrows(() => map(null, (k, v) => [k, v]), TypeError); 39 | }); 40 | 41 | type Expect = 0; 42 | -------------------------------------------------------------------------------- /map.ts: -------------------------------------------------------------------------------- 1 | type MapFun = { 2 | ( 3 | data: Map, 4 | callback: (key: IK, value: IV) => [OK, OV], 5 | ): Map; 6 | (data: T[], callback: (element: T) => O): O[]; 7 | (data: Set, callback: (element: T) => O): Set; 8 | < 9 | T extends Record, 10 | K extends string | number | symbol, 11 | V, 12 | >( 13 | data: T, 14 | callback: (key: keyof T, value: T[keyof T]) => [K, V], 15 | ): Record; 16 | }; 17 | 18 | /** 19 | * Map over `data`. `data` can be a regular object, a `Map`, a `Set`, or an 20 | * array. 21 | * 22 | * @example 23 | * ``` 24 | * // -> { a: 1, b: 2 } 25 | * map({ a: '1', b: '2' }, (key, value) => [key, parseInt(value)]) 26 | * ``` 27 | * 28 | * @example 29 | * ``` 30 | * // -> Map([ [2, 1], [4, 3] ]) 31 | * map(new Map([ [1, 2], [3, 4] ]), (key, value) => [key + 1, value - 1]) 32 | * ``` 33 | */ 34 | const map: MapFun = (data: any, cb: any): any => { 35 | if (typeof data !== "object" || data === null) { 36 | throw new TypeError(`cannot map over ${data}`); 37 | } 38 | 39 | if (data instanceof Map) { 40 | return new Map([...data].map(([key, value]) => cb(key, value))); 41 | } 42 | 43 | if (data instanceof Set) { 44 | return new Set([...data].map((el) => cb(el))); 45 | } 46 | 47 | if (Array.isArray(data)) { 48 | return data.map((el) => cb(el)); 49 | } 50 | 51 | return Object.fromEntries( 52 | Reflect.ownKeys(data).map((key) => cb(key, (data)[key])), 53 | ); 54 | }; 55 | 56 | export default map; 57 | -------------------------------------------------------------------------------- /math.ts: -------------------------------------------------------------------------------- 1 | export { default as clamp } from "./clamp.ts"; 2 | -------------------------------------------------------------------------------- /memoize.test.ts: -------------------------------------------------------------------------------- 1 | import memoize from "./memoize.ts"; 2 | import { assertEquals, assertThrows } from "testing/asserts.ts"; 3 | import { assertSpyCalls, spy } from "testing/mock.ts"; 4 | 5 | Deno.test("memoize", () => { 6 | const root = spy((n: number) => Math.sqrt(n)); 7 | 8 | const memRoot = memoize(root); 9 | 10 | assertEquals(memRoot(9), 3); 11 | assertSpyCalls(root, 1); 12 | 13 | assertEquals(memRoot(9), 3); 14 | assertSpyCalls(root, 1); 15 | 16 | assertEquals(memRoot(16), 4); 17 | assertSpyCalls(root, 2); 18 | 19 | assertEquals(memRoot(16), 4); 20 | assertSpyCalls(root, 2); 21 | 22 | memRoot.cache.clear(); 23 | assertEquals(memRoot(9), 3); 24 | assertSpyCalls(root, 3); 25 | 26 | assertEquals(memRoot(9), 3); 27 | assertSpyCalls(root, 3); 28 | 29 | const effect = spy((n: number) => n); 30 | const eff = memoize(effect, { key: (n) => n, limit: 1 }); 31 | 32 | assertEquals(eff(1), 1); 33 | assertSpyCalls(effect, 1); 34 | assertEquals(eff(1), 1); 35 | assertSpyCalls(effect, 1); 36 | assertEquals(eff(2), 2); 37 | assertSpyCalls(effect, 2); 38 | assertEquals(eff(2), 2); 39 | assertSpyCalls(effect, 2); 40 | assertEquals(eff(1), 1); 41 | assertSpyCalls(effect, 3); 42 | assertEquals(eff(2), 2); 43 | assertSpyCalls(effect, 4); 44 | assertEquals(eff(1), 1); 45 | assertSpyCalls(effect, 5); 46 | assertEquals(eff(1), 1); 47 | assertSpyCalls(effect, 5); 48 | 49 | const mem = memoize((n: number) => Math.sqrt(n), { 50 | key: (n) => n, 51 | weak: true, 52 | }); 53 | { 54 | // @ts-expect-error 55 | const _cache: Map = mem.cache; 56 | } 57 | { 58 | const _cache: Map = memRoot.cache; 59 | } 60 | 61 | assertThrows(() => 62 | memoize((n: number) => Math.sqrt(n), { 63 | key: (n) => n, 64 | weak: true, 65 | limit: 1, 66 | }) 67 | ); 68 | }); 69 | -------------------------------------------------------------------------------- /memoize.ts: -------------------------------------------------------------------------------- 1 | import type { λ } from "./types.ts"; 2 | import { assert } from "./except.ts"; 3 | 4 | /** 5 | * Returns a copy of `fun` that remembers its result for any given arguments and 6 | * only invokes `fun` for unknown arguments. 7 | * 8 | * The cache key is computed using the `key` function. The default `key` 9 | * function simply stringifies the arguments. 10 | * 11 | * If `limit` is specified, only the `limit`-last entries are kept in cache. 12 | * 13 | * The function's cache is available at `memoized.cache`. 14 | * 15 | * If `opt.weak` is `true`, non-primitive cache keys are stored in a WeakMap. 16 | * This behavior might for example be useful if you want to memoize some 17 | * calculation including a DOM Node without holding on to a reference of that 18 | * node. 19 | * Using weak keys prohibits setting a `limit`. 20 | * 21 | * @param fun - The function to be memoized. 22 | * @param opt - Optional additional parameters. 23 | * @returns The memoized function. 24 | * 25 | * @example 26 | * ``` 27 | * const expensiveCalculation = (a: number, b: number) => { 28 | * console.log(`calculate ${a} + ${b}`) 29 | * return a + b 30 | * } 31 | * const calc = memoize(expensiveCalculation) 32 | * 33 | * console.log( calc(1, 2) ) 34 | * // calculate 1 + 2 35 | * // 3 36 | * console.log( calc(20, 5) ) 37 | * // calculate 20 + 5 38 | * // 25 39 | * console.log( calc(20, 5) ) 40 | * // 25 41 | * console.log( calc(1, 2) ) 42 | * // 3 43 | * 44 | * calc.cache.clear() 45 | * console.log( calc(1, 2) ) 46 | * // calculate 1 + 2 47 | * // 3 48 | * ``` 49 | * 50 | * @example 51 | * ``` 52 | * const logIfDifferent = memoize( 53 | * (msg: string) => console.log(msg), 54 | * { 55 | * limit: 1, 56 | * key: msg => msg 57 | * } 58 | * ) 59 | * 60 | * logIfDifferent('a') 61 | * logIfDifferent('a') 62 | * logIfDifferent('b') 63 | * logIfDifferent('a') 64 | * 65 | * // a 66 | * // b 67 | * // a 68 | * ``` 69 | */ 70 | const memoize = ( 71 | fun: T, 72 | opt: { 73 | /** 74 | * How the cache key is computed. Defaults to `JSON.stringify`ing the arguments. 75 | */ 76 | key?: (...args: Parameters) => K; 77 | /** 78 | * The maximum number of results that can be kept in cache before discarding the oldest result. 79 | */ 80 | limit?: number; 81 | /** 82 | * Store non-primitive cache keys in a WeakMap. 83 | */ 84 | weak?: W; 85 | } = {}, 86 | ): T & { 87 | cache: W extends false ? Map> : Cache>; 88 | } => { 89 | opt.key ??= (...args) => JSON.stringify(args) as any; 90 | 91 | const cache = opt.weak 92 | ? new Cache>() 93 | : new Map>(); 94 | 95 | if (!Number.isFinite(opt.limit)) opt.limit = -1; 96 | 97 | assert( 98 | !opt.weak || opt.limit! <= 0, 99 | "can't set a limit when using weak keys", 100 | ); 101 | 102 | const hasLimit = (_cache: unknown): _cache is Map => 103 | opt.limit! > 0; 104 | 105 | return Object.assign( 106 | (...args: Parameters) => { 107 | const k = opt.key!(...args); 108 | if (cache.has(k)) return cache.get(k); 109 | if (hasLimit(cache) && opt.limit! <= cache.size) { 110 | const n = cache.size - opt.limit! + 1; 111 | for (let i = 0; i < n; i++) cache.delete(cache.keys().next().value); 112 | } 113 | const res = fun(...args); 114 | cache.set(k, res); 115 | return res; 116 | }, 117 | { 118 | cache, 119 | }, 120 | ) as any; 121 | }; 122 | 123 | export default memoize; 124 | 125 | class Cache { 126 | #primitives = new Map(); 127 | #objects = new (globalThis.WeakMap ?? Map)(); 128 | 129 | #getMap(key: K): WeakMap { 130 | if (typeof key === "object" && key !== null) return this.#objects; 131 | return this.#primitives; 132 | } 133 | 134 | delete(key: K) { 135 | return this.#getMap(key).delete(key); 136 | } 137 | 138 | has(key: K) { 139 | return this.#getMap(key).has(key); 140 | } 141 | 142 | get(key: K): V | undefined { 143 | return this.#getMap(key).get(key); 144 | } 145 | 146 | set(key: K, value: V) { 147 | this.#getMap(key).set(key, value); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /merge.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, assertEquals } from "testing/asserts.ts"; 2 | import { assertType } from "./types.ts"; 3 | import merge, { Merge } from "./merge.ts"; 4 | 5 | Deno.test("merge", () => { 6 | { 7 | const result: 2 = merge(1, 2); 8 | assertEquals(result, 2); 9 | } 10 | 11 | { 12 | const result = merge({ foo: "bar" }, null); 13 | assertType(); 14 | assertEquals(result, null); 15 | } 16 | 17 | { 18 | const result = merge(null, { foo: "bar" } as const); 19 | assertType(); 20 | assertEquals(result, { foo: "bar" }); 21 | } 22 | 23 | { 24 | const result = merge(null, { foo: "bar" } as const); 25 | assertType(); 26 | assertEquals(result, { foo: "bar" }); 27 | } 28 | 29 | { 30 | const result = merge( 31 | { a: 1, b: { foo: "bar", c: "d" } } as const, 32 | { a: 2, b: { foo: "baz", e: "f" } } as const, 33 | ); 34 | 35 | assertType< 36 | typeof result, 37 | { a: 2; b: { foo: "baz"; c: "d"; e: "f" } } 38 | >(); 39 | assertEquals( 40 | result, 41 | { a: 2, b: { foo: "baz", c: "d", e: "f" } }, 42 | ); 43 | } 44 | 45 | { 46 | const result = merge([1, 2] as const, [3, 4] as const); 47 | assertType(); 48 | assertEquals(result, [1, 2, 3, 4]); 49 | } 50 | 51 | { 52 | assertEquals(merge(new Set([1, 2]), new Set([2, 3])), new Set([1, 2, 3])); 53 | assertEquals(merge([1, 2], new Set([2, 3])), new Set([2, 3])); 54 | 55 | assertEquals( 56 | merge(new Map([[1, 2], [3, 4]]), new Map([[5, 6]])), 57 | new Map([[1, 2], [3, 4], [5, 6]]), 58 | ); 59 | 60 | assertType, Set>, Set>(); 61 | assertType< 62 | Merge<{ foo: any[] }, { foo: Set }>, 63 | { foo: Set } 64 | >(); 65 | assertType< 66 | Merge, Set>, 67 | Set | Set 68 | >(); 69 | assertType< 70 | Merge, Map>, 71 | Map 72 | >(); 73 | } 74 | 75 | { 76 | const v = { foo: "bar" }; 77 | assertEquals(merge({}, v), v); 78 | assert(merge({}, v) !== v); 79 | } 80 | 81 | { 82 | const v = [1, 2, 3]; 83 | assertEquals(merge({}, v), v); 84 | assert(merge({}, v) !== v); 85 | } 86 | 87 | { 88 | const v = new Set([1, 2, 3]); 89 | assertEquals(merge({}, v), v); 90 | assert(merge({}, v) !== v); 91 | } 92 | 93 | { 94 | const v = new Map([]); 95 | assertEquals(merge({}, v), v); 96 | assert(merge({}, v) !== v); 97 | } 98 | 99 | { 100 | type V = { foo: V }; 101 | const v = {} as V; 102 | v.foo = v; 103 | 104 | const result = merge(v, v); 105 | assertType(); 106 | assertEquals(result, v); 107 | assert(result.foo === result); 108 | assert(result.foo !== v); 109 | } 110 | 111 | { 112 | type V = [1, 2, V]; 113 | const v = [1, 2] as unknown as V; 114 | v.push(v); 115 | const result = merge(v, v); 116 | assertType(); 117 | assert(result[2] !== v); 118 | // @ts-ignore 119 | assert(result[2] === result); 120 | } 121 | 122 | { 123 | const v = new Set(); 124 | v.add(v); 125 | const ref = {}; 126 | v.add(ref); 127 | const result = merge(v, v); 128 | assert(!result.has(v)); 129 | assert(result.has(result)); 130 | assert(result.has(ref)); 131 | } 132 | 133 | { 134 | const a = new Map(); 135 | a.set(a, a); 136 | a.set("a", a); 137 | a.set("foo", a); 138 | const b = new Map(); 139 | b.set(b, b); 140 | b.set("b", b); 141 | b.set("foo", b); 142 | 143 | const result = merge(a, b); 144 | assertEquals(result.get("a"), result); 145 | assertEquals(result.get("b"), result); 146 | assertEquals(result.get("foo"), result); 147 | assertEquals([...result], [ 148 | ["a", result], 149 | ["foo", result], 150 | ["b", result], 151 | [result, result], 152 | ]); 153 | } 154 | 155 | assertType, (string | number)[]>(); 156 | assertType, {}>(); 157 | assertType, ".">(); 158 | 159 | assertType< 160 | Merge<{ foo: "bar" }, number | { foo: "baz" }>, 161 | number | { foo: "baz" } 162 | >(); 163 | assertType< 164 | Merge, 165 | { a: 1 } | { a: 1; foo: "bar" } 166 | >(); 167 | assertType< 168 | Merge, 169 | number | { a: 2; c: 3 } | { a: 2; c: 3; d: 4 } 170 | >(); 171 | 172 | assertType, { a: 2 }>(); 173 | assertType, { a: 1 | 2 | undefined }>(); 174 | 175 | assertType, { a?: 1 }>(); 176 | assertType, { a?: 1 | 2 }>(); 177 | 178 | assertType, number[] | [1, 2, 3, 4]>(); 179 | }); 180 | -------------------------------------------------------------------------------- /merge.ts: -------------------------------------------------------------------------------- 1 | import clone from "./clone.ts"; 2 | import { 3 | Mutable as Mutable_, 4 | OptionalKeys, 5 | Primitive, 6 | RequiredKeys, 7 | } from "./types.ts"; 8 | 9 | const merge_ = (a: any, b: any): any => { 10 | const visited = new Map(); 11 | return mergeItems(a, b, visited); 12 | }; 13 | 14 | const mergeItems = (a: any, b: any, visited: Map>): any => { 15 | if (isPrimitive(a) || isPrimitive(b)) return clone(b); 16 | 17 | if (!visited.has(a)) visited.set(a, new Map()); 18 | if (visited.get(a)!.has(b)) return visited.get(a)!.get(b); 19 | 20 | let result: any; 21 | 22 | if (Array.isArray(b)) { 23 | const clonedA = Array.isArray(a) ? clone(a) : []; 24 | const clonedB = clone(b); 25 | result = [...clonedA, ...clonedB]; 26 | for (let i = 0; i < result.length; i++) { 27 | if (result[i] === clonedA || result[i] === clonedB) result[i] = result; 28 | } 29 | } else if (b instanceof Set) { 30 | result = new Set([...a instanceof Set ? a : [], ...b]); 31 | if (result.has(a) || result.has(b)) { 32 | result.delete(a); 33 | result.delete(b); 34 | result.add(result); 35 | } 36 | } else if (b instanceof Map) { 37 | result = new Map([...a instanceof Map ? a : [], ...b]); 38 | const keys = [...result.keys()]; 39 | if (keys.includes(a)) { 40 | result.set(result, result.get(a)); 41 | result.delete(a); 42 | } 43 | if (keys.includes(b)) { 44 | result.set(result, result.get(b)); 45 | result.delete(b); 46 | } 47 | for (const [k, v] of result) { 48 | if (v === a || v === b) result.set(k, result); 49 | } 50 | } else { 51 | const keys = [...new Set([...Object.keys(a), ...Object.keys(b)])]; 52 | result = {}; 53 | visited.get(a)!.set(b, result); 54 | 55 | for (const key of keys) { 56 | result[key] = !(key in b) 57 | ? clone(a[key]) 58 | : !(key in a) 59 | ? clone(b[key]) 60 | : mergeItems(a[key], b[key], visited); 61 | } 62 | } 63 | 64 | visited.get(a)!.set(b, result); 65 | return result; 66 | }; 67 | 68 | /** 69 | * Recursively merges `A` and `B`. If a property in `A` and `B` is of a 70 | * different type (i.e. it's not an array, Set, Map, or plain object in both, 71 | * the value from `B` will be used in the result). 72 | * 73 | * If there are self-references in the cloned values, array / Set items, or Map 74 | * keys or values, they will also be self-referencing in the result. 75 | */ 76 | const merge = merge_ as (a: A, b: B) => Merge; 77 | export default merge; 78 | 79 | const isPrimitive = (value: unknown): value is Primitive => 80 | typeof value !== "object" || value === null; 81 | 82 | export type Merge = A extends Primitive ? Mutable 83 | : Merge_, Mutable>; 84 | 85 | type Merge_ = 86 | | Extract 87 | | MergeList 88 | | MergeSet 89 | | MergeMap 90 | | MergeObject; 91 | 92 | type MergeSet = B extends Set ? ( 93 | A extends Set ? Set : B 94 | ) 95 | : never; 96 | 97 | type MergeMap = B extends Map ? ( 98 | A extends Map ? Map : B 99 | ) 100 | : never; 101 | 102 | type MergeObject = B extends Record ? ( 103 | A extends Record ? MergeObject_ : never 104 | ) 105 | : never; 106 | 107 | type MergeObject_ = MakeOptional< 108 | { 109 | [K in keyof A | keyof B]: K extends keyof B 110 | ? (K extends keyof A 111 | ? (Merge | (K extends OptionalKeys ? A[K] : never)) 112 | : B[K]) 113 | : K extends keyof A ? A[K] 114 | : never; 115 | }, 116 | | Exclude, RequiredKeys> 117 | | Exclude, RequiredKeys> 118 | >; 119 | 120 | type MergeList = A extends unknown[] ? (B extends unknown[] ? ( 121 | B[number][] extends Required ? MergeArray 122 | : A[number][] extends Required ? MergeArray 123 | : MergeTuple 124 | ) 125 | : never) 126 | : never; 127 | 128 | type MergeTuple = B extends 129 | [infer H, ...infer T] ? MergeTuple<[...A, H], T> 130 | : A; 131 | 132 | type MergeArray = 133 | (A[number] | B[number])[]; 134 | 135 | type MakeOptional = FlattenIntersection< 136 | & Pick> 137 | & { [K in O]?: T[K] } 138 | >; 139 | 140 | type FlattenIntersection = { [K in keyof T]: T[K] }; 141 | 142 | type Mutable = T extends Set | Map ? T : Mutable_; 143 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./function.ts"; 2 | export * from "./list.ts"; 3 | // @ts-ignore 4 | export * from "./iterable.ts"; 5 | export * from "./object.ts"; 6 | export * from "./path.ts"; 7 | export * from "./equality.ts"; 8 | export * from "./promise.ts"; 9 | export * from "./predicate.ts"; 10 | export * from "./string.ts"; 11 | export * from "./math.ts"; 12 | export * from "./ds.ts"; 13 | -------------------------------------------------------------------------------- /noop.test.ts: -------------------------------------------------------------------------------- 1 | import noop from "./noop.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("noop", () => { 5 | assertEquals(noop(), undefined); 6 | // @ts-expect-error 7 | const _return: undefined = noop(); 8 | }); 9 | -------------------------------------------------------------------------------- /noop.ts: -------------------------------------------------------------------------------- 1 | const noop = () => {}; 2 | 3 | export default noop; 4 | -------------------------------------------------------------------------------- /npm/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | }, 9 | "modules": false 10 | } 11 | ] 12 | ], 13 | "plugins": [ 14 | "@babel/plugin-transform-typescript" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /npm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "froebel", 3 | "version": "0.23.2", 4 | "description": "TypeScript utility library", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "exports": { 8 | ".": { 9 | "require": "./index.js", 10 | "import": "./index.mjs" 11 | } 12 | }, 13 | "private": "true", 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/MathisBullinger/froebel.git" 17 | }, 18 | "author": "Mathis Bullinger", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/MathisBullinger/froebel/issues" 22 | }, 23 | "homepage": "https://github.com/MathisBullinger/froebel#readme", 24 | "devDependencies": { 25 | "@babel/cli": "^7.18.10", 26 | "@babel/core": "^7.18.10", 27 | "@babel/plugin-transform-typescript": "^7.18.12", 28 | "@babel/preset-env": "^7.18.10", 29 | "@types/node": "^17.0.35", 30 | "typedoc": "^0.22.17", 31 | "typescript": "^4.7.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /npm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "isolatedModules": true, 8 | "outDir": "../build", 9 | "strict": true, 10 | "strictNullChecks": true, 11 | "lib": ["ESNext", "DOM"], 12 | "types": ["node"], 13 | "downlevelIteration": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /nullish.ts: -------------------------------------------------------------------------------- 1 | /** Checks if `value` is nullish. Literal types are narrowed accordingly. */ 2 | export const nullish = (value: T): value is Nullish => 3 | value === undefined || value === null; 4 | 5 | type Nullish = PickNullish extends never ? Extract 6 | : PickNullish; 7 | 8 | type PickNullish = 9 | | (null extends T ? null : never) 10 | | (undefined extends T ? undefined : never); 11 | 12 | /** 13 | * Checks if `value` is not nullish. Literal types are narrowed accordingly. 14 | * 15 | * @example 16 | * ``` 17 | * const nums = (...values: (number | undefined)[]): number[] => values.filter(notNullish) 18 | * ``` 19 | */ 20 | export const notNullish = (value: T | null | undefined): value is T => 21 | value !== null && value !== undefined; 22 | -------------------------------------------------------------------------------- /nullishChain.test.ts: -------------------------------------------------------------------------------- 1 | import { asyncNullishChain, nullishChain } from "./nullishChain.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("nullish chain", () => { 5 | const numChain = nullishChain( 6 | (n: number) => (n === 1 ? n : null), 7 | (n) => (n === 2 ? n : undefined), 8 | (n) => (n === 3 ? n : null), 9 | ); 10 | 11 | assertEquals(numChain(1), 1); 12 | assertEquals(numChain(2), 2); 13 | assertEquals(numChain(3), 3); 14 | assertEquals(numChain(4), undefined); 15 | 16 | // @ts-expect-error 17 | // prettier-ignore 18 | nullishChain((_n: number) => 0, (_n: string) => 0); 19 | 20 | // @ts-expect-error 21 | const _str: string = nullishChain(() => 2)(); 22 | 23 | const _empty = nullishChain(); 24 | }); 25 | 26 | Deno.test("async nullish chain", async () => { 27 | const chain = asyncNullishChain( 28 | (n: number) => { 29 | if (n === 1) return "foo"; 30 | }, 31 | async (n) => { 32 | if (n === 2) return "bar"; 33 | }, 34 | (n) => 35 | new Promise((res) => 36 | setTimeout(() => res(n === 3 ? "baz" : null), 100) 37 | ), 38 | ); 39 | 40 | assertEquals(await chain(1), "foo"); 41 | assertEquals(await chain(2), "bar"); 42 | assertEquals(await chain(3), "baz"); 43 | assertEquals(await chain(4), undefined); 44 | }); 45 | -------------------------------------------------------------------------------- /nullishChain.ts: -------------------------------------------------------------------------------- 1 | import type { PromType, λ } from "./types.ts"; 2 | 3 | /** 4 | * Given a list of functions that accept the same parameters, returns a function 5 | * that given these arguments returns the result of the first function whose 6 | * result is not nullish. 7 | * 8 | * This is equivalent to chaining together invocations of the passed in 9 | * functions with the given arguments with nullish coalescing _(`??`)_ operators. 10 | * 11 | * @example 12 | * ``` 13 | * const isAdult = (age: number) => { if (n >= 18) return 'adult' } 14 | * const isToddler = (age: number) => { if (n <= 3) return 'toddler' } 15 | * 16 | * const ageGroup = nullishChain(isAdult, isToddler, () => 'child') 17 | * 18 | * // this is functionally equivalent to: 19 | * const ageGroup = age => isAdult(age) ?? isToddler(age) ?? 'child' 20 | * 21 | * ageGroup(1) // prints: 'toddler' 22 | * ageGroup(10) // prints: 'child' 23 | * ageGroup(50) // prints: 'adult' 24 | * ``` 25 | */ 26 | export const nullishChain = >[]>( 27 | ...[fun, ...rest]: [FF, ...FR] | [] 28 | ) => 29 | (...args: Parameters): ReturnType | undefined => 30 | !fun ? undefined : fun(...args) ?? nullishChain(...(rest as any))(...args); 31 | 32 | /** 33 | * Same as {@link nullishChain} but accept asynchronous functions too. 34 | * 35 | * @example 36 | * ``` 37 | * const readFromCache = (id: string): Resource => { if (id in cache) return cache[id] } 38 | * const readFromFile = (id: string): Resource => { if (fileExists(id)) return readFile(id) } 39 | * const fetchFromNet = async (id: string): Promise => await fetch(`someURL/${id}`) 40 | * 41 | * // async (id: string) => Promise 42 | * const getResource = asyncNullishChain(readFromCache, readFromFile, fetchFromNet) 43 | * ``` 44 | */ 45 | export const asyncNullishChain = >[]>( 46 | ...[fun, ...rest]: [FF, ...FR] | [] 47 | ) => 48 | async ( 49 | ...args: Parameters 50 | ): Promise> | undefined> => 51 | !fun ? undefined : (await fun(...args)) ?? 52 | (await asyncNullishChain(...(rest as any))(...args)); 53 | -------------------------------------------------------------------------------- /object.ts: -------------------------------------------------------------------------------- 1 | export { default as pick } from "./pick.ts"; 2 | export { default as omit } from "./omit.ts"; 3 | export { default as map } from "./map.ts"; 4 | -------------------------------------------------------------------------------- /omit.test.ts: -------------------------------------------------------------------------------- 1 | import omit from "./omit.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("omit", () => { 5 | const obj = { a: "foo", b: "bar", c: "baz" }; 6 | 7 | const foo = omit(obj, "b", "c"); 8 | assertEquals(foo, { a: "foo" }); 9 | 10 | const _a: string = foo.a; 11 | // @ts-expect-error 12 | const _b = foo.b; 13 | // @ts-expect-error 14 | const _c = foo.c; 15 | }); 16 | -------------------------------------------------------------------------------- /omit.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * From `obj`, create a new object that does not include `keys`. 3 | * 4 | * @example 5 | * ``` 6 | * omit({ a: 1, b: 2, c: 3 }, 'a', 'c') // { b: 2 } 7 | * ``` 8 | */ 9 | const omit = , K extends keyof T>( 10 | obj: T, 11 | ...keys: K[] 12 | ): Omit => 13 | Object.fromEntries( 14 | Object.entries(obj).filter(([k]) => !keys.includes(k as any)), 15 | ) as any; 16 | 17 | export default omit; 18 | -------------------------------------------------------------------------------- /oneOf.test.ts: -------------------------------------------------------------------------------- 1 | import oneOf from "./oneOf.ts"; 2 | import { assert } from "testing/asserts.ts"; 3 | 4 | Deno.test("oneOf", () => { 5 | { 6 | const unknown: unknown = 2; 7 | assert(oneOf(unknown as any, 1, 2, 3)); 8 | if (oneOf(unknown, 1, 2, 3)) { 9 | const _n: number = unknown; 10 | } else { 11 | // @ts-expect-error 12 | const _n: number = unknown; 13 | } 14 | } 15 | 16 | { 17 | const unknown: unknown = "a"; 18 | assert(oneOf(unknown as any, 1, 2, "a", "b")); 19 | if (oneOf(unknown, 1, 2, "a", "b")) { 20 | const _sOrN: string | number = unknown; 21 | } else { 22 | // @ts-expect-error 23 | const _sOrN: string | number = unknown; 24 | } 25 | } 26 | 27 | { 28 | const unknown: unknown = "foo"; 29 | if (oneOf(unknown, "foo", "bar")) { 30 | const _known: "foo" | "bar" = unknown; 31 | } 32 | } 33 | 34 | { 35 | const unknown: unknown = 2; 36 | if (oneOf(unknown, 1, 2)) { 37 | const _known: 1 | 2 = unknown; 38 | } 39 | } 40 | 41 | { 42 | const unknown: unknown = 1; 43 | if (oneOf(unknown, 1, 2, "a", "b")) { 44 | const _known: 1 | 2 | "a" | "b" = unknown; 45 | } 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /oneOf.ts: -------------------------------------------------------------------------------- 1 | /** Checks if `v` is one of `cmps`. */ 2 | const oneOf = < 3 | T, 4 | TT extends (T extends string ? string & T 5 | : T extends number ? number & T 6 | : any)[], 7 | >( 8 | value: T, 9 | ...cmps: TT 10 | ): value is TT[number] => cmps.includes(value as any); 11 | 12 | export default oneOf; 13 | -------------------------------------------------------------------------------- /partial.test.ts: -------------------------------------------------------------------------------- 1 | import partial from "./partial.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("partial application", () => { 5 | const fun = (a: string, b: number, c: boolean) => JSON.stringify([a, b, c]); 6 | 7 | const funABC = partial(fun); 8 | const funBC = partial(fun, "a"); 9 | const funC = partial(fun, "b", 2); 10 | const fun_ = partial(fun, "c", 3, true); 11 | 12 | assertEquals(funABC("_", 0, false), JSON.stringify(["_", 0, false])); 13 | assertEquals(funBC(1, true), JSON.stringify(["a", 1, true])); 14 | assertEquals(funC(false), JSON.stringify(["b", 2, false])); 15 | assertEquals(fun_(), JSON.stringify(["c", 3, true])); 16 | 17 | // @ts-expect-error 18 | partial((_str: "A" | "B", _v: boolean) => {}, "A")(); 19 | }); 20 | -------------------------------------------------------------------------------- /partial.ts: -------------------------------------------------------------------------------- 1 | import type { PartialList, λ } from "./types.ts"; 2 | 3 | /** 4 | * Partially apply a function. 5 | * 6 | * @example 7 | * ``` 8 | * const divide = (dividend: number, divisor: number) => dividend / divisor 9 | * 10 | * // (divisor: number) => number 11 | * const oneOver = partial(divide, 1) 12 | * 13 | * // prints: 0.25 14 | * console.log(oneOver(4)) 15 | * ``` 16 | */ 17 | const partial = >>( 18 | fun: T, 19 | ...argsLeft: PL 20 | ) => 21 | ( 22 | ...argsRight: Parameters extends [...PL, ...infer PR] ? PR : never 23 | ): ReturnType => fun(...argsLeft, ...argsRight); 24 | 25 | export default partial; 26 | -------------------------------------------------------------------------------- /partition.test.ts: -------------------------------------------------------------------------------- 1 | import partition from "./partition.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("partition", () => { 5 | const isStr = (v: unknown): v is string => typeof v === "string"; 6 | 7 | { 8 | const res: [["a", "b"], [1, 2]] = partition( 9 | ["a", 1, "b", 2] as const, 10 | isStr, 11 | ); 12 | assertEquals(res[0], ["a", "b"]); 13 | assertEquals(res[1], [1, 2]); 14 | } 15 | 16 | { 17 | const _res = partition(["a", 1, "b", 2], isStr); 18 | // @ts-expect-error 19 | const _: [[], []] = partition(["a", 1, "b", 2], isStr); 20 | } 21 | 22 | { 23 | const _: [(string | number)[], (string | number)[]] = partition( 24 | ["a", 1, "b", 2], 25 | (v) => typeof v === "string", 26 | ); 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /partition.ts: -------------------------------------------------------------------------------- 1 | type Partition = { 2 | (list: T[], predicate: (el: T) => el is S): [ 3 | S[], 4 | Exclude[], 5 | ]; 6 | ( 7 | list: T, 8 | predicate: (el: any) => el is S, 9 | ): Part; 10 | (list: T[], predicate: (el: T) => unknown): [T[], T[]]; 11 | }; 12 | 13 | /** 14 | * Takes a `list` and returns a pair of lists containing: the elements that 15 | * match the `predicate` and those that don't, respectively. 16 | * 17 | * Think of it as `filter`, but the elements that don't pass the filter aren't 18 | * discarded but returned in a separate list instead. 19 | * 20 | * @example 21 | * ``` 22 | * const [strings, numbers] = partition( 23 | * ['a', 'b', 1, 'c', 2, 3], 24 | * (el): el is string => typeof el === 'string' 25 | * ) 26 | * // strings: ["a", "b", "c"] 27 | * // numbers: [1, 2, 3] 28 | * ``` 29 | */ 30 | const partition: Partition = (list: T[], predicate: (el: T) => unknown) => 31 | list.reduce( 32 | ([t, f], c) => (predicate(c) ? [[...t, c], f] : [t, [...f, c]]) as any, 33 | [[], []], 34 | ) as any; 35 | 36 | export default partition; 37 | 38 | type Part = T extends readonly [ 39 | infer F, 40 | ...infer R, 41 | ] ? [ 42 | F extends S ? [F, ...Part[0]] : Part[0], 43 | F extends S ? Part[1] : [F, ...Part[1]], 44 | ] 45 | : [[], []]; 46 | -------------------------------------------------------------------------------- /path.ts: -------------------------------------------------------------------------------- 1 | export { default as select, none } from "./select.ts"; 2 | -------------------------------------------------------------------------------- /pick.test.ts: -------------------------------------------------------------------------------- 1 | import pick from "./pick.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("pick", () => { 5 | const obj = { a: "foo", b: "bar", c: "baz" }; 6 | 7 | const foo = pick(obj, "a", "b"); 8 | assertEquals(foo, { a: "foo", b: "bar" }); 9 | 10 | const _a: string = foo.a; 11 | const _b: string = foo.b; 12 | // @ts-expect-error 13 | const _c = foo.c; 14 | }); 15 | -------------------------------------------------------------------------------- /pick.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * From `obj`, create a new object that only includes `keys`. 3 | * 4 | * @example 5 | * ``` 6 | * pick({ a: 1, b: 2, c: 3 }, 'a', 'c') // { a: 1, c: 3 } 7 | * ``` 8 | */ 9 | const pick = , K extends keyof T>( 10 | obj: T, 11 | ...keys: K[] 12 | ): Pick => 13 | Object.fromEntries( 14 | Object.entries(obj).filter(([k]) => keys.includes(k as any)), 15 | ) as any; 16 | 17 | export default pick; 18 | -------------------------------------------------------------------------------- /pipe.test.ts: -------------------------------------------------------------------------------- 1 | import pipe, { applyPipe } from "./pipe.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("pipe", async () => { 5 | // @ts-expect-error 6 | pipe(); 7 | // @ts-expect-error 8 | pipe(add, add); 9 | // @ts-expect-error 10 | pipe(add, upper); 11 | 12 | const f0: (a: number, b: number) => string = pipe(add, square, toString); 13 | assertEquals(f0(2, 3), "25"); 14 | 15 | const f1: () => Promise = pipe( 16 | () => "10", 17 | parseInt, 18 | asyncSquare, 19 | toString, 20 | ); 21 | assertEquals(await f1(), "100"); 22 | 23 | const f2: () => Promise = pipe(() => 10, asyncSquare); 24 | assertEquals(await f2(), 100); 25 | const f3: () => Promise = pipe(async () => "10", parseInt); 26 | assertEquals(await f3(), 10); 27 | const f4: () => Promise = pipe(async () => 10, asyncSquare); 28 | assertEquals(await f4(), 100); 29 | 30 | assertEquals(pipe(join, parseInt)("1", "2", "3"), 123); 31 | 32 | // @ts-expect-error 33 | pipe(() => "10", parseInt, asyncSquare, parseInt); 34 | }); 35 | 36 | Deno.test("apply pipe", () => { 37 | // @ts-expect-error 38 | applyPipe(); 39 | // @ts-expect-error 40 | applyPipe(0 as any, add); 41 | // @ts-expect-error 42 | applyPipe(0, parseInt); 43 | 44 | assertEquals(applyPipe(1, toString), "1"); 45 | assertEquals(applyPipe(2, toString, parseInt, square), 4); 46 | 47 | // @ts-expect-error 48 | const _a: string = applyPipe(2, toString, parseInt); 49 | const _b: number = applyPipe(2, toString, parseInt); 50 | }); 51 | 52 | const add = (a: number, b: number) => a + b; 53 | const square = (n: number) => n ** 2; 54 | const toString = (n: number) => n.toString(); 55 | const upper = (str: string) => str.toUpperCase(); 56 | const join = (...args: string[]) => args.join(""); 57 | const asyncSquare = async (n: number) => 58 | await new Promise((res) => res(n ** 2)); 59 | -------------------------------------------------------------------------------- /pipe.ts: -------------------------------------------------------------------------------- 1 | import type { MakeProm, λ } from "./types.ts"; 2 | import isPromise from "./isPromise.ts"; 3 | 4 | /** 5 | * Given a list of functions returns a function that will execute the given 6 | * functions one after another, always passing the result of the previous 7 | * function as an argument to the next function. 8 | * 9 | * If one of the given functions returns a promise, the promise will be resolved 10 | * before being passed to the next function. 11 | * 12 | * @example 13 | * ``` 14 | * const join = (...chars: string[]) => chars.join('') 15 | * pipe(join, parseInt)('1', '2', '3') // -> 123 16 | * 17 | * const square = (n: number) => n ** 2 18 | * 19 | * // this is equivalent to: square(square(square(2))) 20 | * pipe(square, square, square)(2) // -> 256 21 | * 22 | * // also works with promises: 23 | * fetchNumber :: async () => Promise 24 | * pipe(fetchNumber, n => n.toString()) // async () => Promise 25 | * ``` 26 | */ 27 | const pipe = ( 28 | ...funs: PipeReturn extends never ? never : T 29 | ) => 30 | ((...args) => { 31 | let nextArgs: unknown[] = args; 32 | 33 | for (let i = 0; i < funs.length; i++) { 34 | const [result] = nextArgs = [funs[i](...nextArgs)]; 35 | if (isPromise(result)) return resolveAsync(result, funs.slice(i + 1)); 36 | } 37 | 38 | return nextArgs[0]; 39 | }) as PipedFun; 40 | 41 | export default pipe; 42 | 43 | /** 44 | * Like `pipe` but takes an argument as its first parameter and invokes the pipe 45 | * with it. 46 | * 47 | * Note: unlike in `pipe`, the first function of the pipe must take exactly one 48 | * argument. 49 | * 50 | * @see {@link pipe} 51 | * 52 | * @example 53 | * ``` 54 | * applyPipe(2, double, square, half) // -> 8 55 | * ``` 56 | */ 57 | export const applyPipe = , ...λ[]]>( 58 | arg: Parameters[0], 59 | ...funs: PipeReturn extends never ? never : T 60 | ): PipeReturn => (pipe(...funs) as any)(arg); 61 | 62 | const resolveAsync = async (result: unknown, funs: λ[]) => { 63 | for (const fun of funs) result = fun(await result); 64 | return await result; 65 | }; 66 | 67 | type PipedFun = PipeReturn extends never ? never 68 | : ((...args: Parameters) => PipeReturn); 69 | 70 | type PipeReturn = CheckPipe< 71 | F, 72 | CarryReturn, Parameters> 73 | >; 74 | 75 | type FunDef = [Return: any, Args: any[]]; 76 | 77 | type CheckPipe< 78 | F extends λ[], 79 | D extends FunDef[], 80 | Async extends boolean = false, 81 | > = F extends [any, any, ...any[]] 82 | ? (Resolved extends Parameters ? CheckPipe< 83 | F extends [any, ...infer F_] ? (F_ extends λ[] ? F_ : never) : never, 84 | D extends [any, ...infer D_] ? (D_ extends FunDef[] ? D_ : never) 85 | : never, 86 | Async extends true ? true 87 | : ReturnType extends Promise ? true 88 | : false 89 | > 90 | : never) 91 | : Resolved extends Parameters 92 | ? (Async extends true ? MakeProm> : ReturnType) 93 | : never; 94 | 95 | type Resolved = { 96 | [K in keyof T]: T[K] extends Promise ? I : T[K]; 97 | }; 98 | 99 | type ReturnTypes = { 100 | [K in keyof T]: ReturnType; 101 | }; 102 | 103 | type CarryReturn = Returns extends 104 | [infer A, ...infer B] ? [[A, Args], ...CarryReturn] 105 | : []; 106 | -------------------------------------------------------------------------------- /predicate.ts: -------------------------------------------------------------------------------- 1 | // deno-fmt-ignore-file 2 | export { default as isPromise, isNotPromise } from "./isPromise.ts"; 3 | export { truthy, falsy } from "./truthy.ts"; 4 | export { nullish, notNullish } from "./nullish.ts"; 5 | export { isFulfilled, isRejected } from "./settled.ts"; 6 | -------------------------------------------------------------------------------- /prefix.test.ts: -------------------------------------------------------------------------------- 1 | import prefix from "./prefix.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("prefix", () => { 5 | const pre1: "foobar" = prefix("foo", "bar"); 6 | assertEquals(pre1, "foobar"); 7 | 8 | const pre2: "fooBar" = prefix("foo", "bar", "camel"); 9 | assertEquals(pre2, "fooBar"); 10 | 11 | const pre3: "foo_bar" = prefix("foo", "bar", "snake"); 12 | assertEquals(pre3, "foo_bar"); 13 | }); 14 | -------------------------------------------------------------------------------- /prefix.ts: -------------------------------------------------------------------------------- 1 | import { capitalize } from "./case.ts"; 2 | import type { Prefix, StringCase } from "./types.ts"; 3 | 4 | /** 5 | * Returns `str` prefixed with `prefix`. Optionally, allows prefxing in camel 6 | * case, i.e. `prefix('foo', 'bar', 'camel') => 'fooBar'`, or snake case, i.e. 7 | * `prefix('foo', 'bar', 'snake') => 'foo_bar'`. 8 | * 9 | * The result is strictly typed, so `prefix('foo', 'bar')` will return the type 10 | * `'foobar'`, not just a generic `string`. 11 | */ 12 | const prefix = < 13 | T0 extends string, 14 | T1 extends string, 15 | C extends StringCase | void = void, 16 | >( 17 | prefix: T0, 18 | str: T1, 19 | caseMod?: C, 20 | ): Prefix => 21 | `${prefix}${caseMod === "snake" ? "_" : ""}${ 22 | caseMod === "camel" ? capitalize(str) : str 23 | }` as any; 24 | 25 | export default prefix; 26 | -------------------------------------------------------------------------------- /promise.ts: -------------------------------------------------------------------------------- 1 | export { default as promisify } from "./promisify.ts"; 2 | export { default as createQueue } from "./queue.ts"; 3 | export { default as isPromise, isNotPromise } from "./isPromise.ts"; 4 | -------------------------------------------------------------------------------- /promisify.test.ts: -------------------------------------------------------------------------------- 1 | import promisify from "./promisify.ts"; 2 | import type { λ } from "./types.ts"; 3 | import { assert, assertEquals, assertRejects } from "testing/asserts.ts"; 4 | 5 | Deno.test("promisify", async () => { 6 | { 7 | const callbackAPI = (cb: λ<[number]>) => { 8 | setTimeout(() => cb(1), 100); 9 | }; 10 | 11 | // @ts-expect-error 12 | const _promiseAPI: () => Promise = promisify(callbackAPI); 13 | const promiseAPI: () => Promise = promisify(callbackAPI); 14 | assertEquals(await promiseAPI(), 1); 15 | 16 | assertEquals(await promisify(callbackAPI, null)(), undefined); 17 | } 18 | 19 | { 20 | const callbackAPI = (_cb: λ) => { 21 | throw Error("foo"); 22 | }; 23 | 24 | await assertRejects(promisify(callbackAPI), Error, "foo"); 25 | } 26 | 27 | { 28 | const callbackAPI = async (_cb: λ) => { 29 | await new Promise((res) => setTimeout(res, 10)); 30 | throw Error("bar"); 31 | }; 32 | await assertRejects(promisify(callbackAPI), Error, "bar"); 33 | } 34 | 35 | { 36 | const callbackAPI = (cb: λ<[err?: Error, res?: string]>) => { 37 | cb(Error("baz")); 38 | }; 39 | await assertRejects(promisify(callbackAPI, 1, 0)); 40 | assert((await promisify(callbackAPI)()) instanceof Error); 41 | } 42 | 43 | await assertRejects( 44 | promisify( 45 | async (cb: λ) => { 46 | await new Promise((res) => setTimeout(res, 10)); 47 | cb(Error("foo")); 48 | throw Error("bar"); 49 | }, 50 | null, 51 | 0, 52 | ), 53 | Error, 54 | "foo", 55 | ); 56 | 57 | assertEquals( 58 | await promisify( 59 | async (cb: λ) => { 60 | await new Promise((res) => setTimeout(res, 10)); 61 | cb(null, 123); 62 | throw Error("foo"); 63 | }, 64 | 1, 65 | 0, 66 | )(), 67 | 123, 68 | ); 69 | 70 | { 71 | const callbackAdd = (a: number, b: number, cb: λ<[number]>) => 72 | setTimeout(() => cb(a + b), 200); 73 | 74 | assertEquals(await promisify(callbackAdd).callbackLast(1, 2), 3); 75 | // @ts-expect-error 76 | await promisify(callbackAdd).callbackLast(1, "a"); 77 | } 78 | 79 | { 80 | const callbackAdd = (cb: λ<[number]>, a: number, b: number) => 81 | setTimeout(() => cb(a + b), 200); 82 | 83 | assertEquals(await promisify(callbackAdd).callbackFirst(1, 2), 3); 84 | // @ts-expect-error 85 | await promisify(callbackAdd).callbackFirst(1, "a"); 86 | } 87 | }); 88 | -------------------------------------------------------------------------------- /promisify.ts: -------------------------------------------------------------------------------- 1 | import type { λ } from "./types.ts"; 2 | 3 | /** 4 | * Turns a function accepting a callback into a function returning a promise. 5 | * You can specify in which parameter (if any) the callback expects to receive 6 | * a result and in which it expects an error. 7 | * Pass `null` to `resultIndex` or `errorIndex` if no result or errors are 8 | * passed to the callback. By default the first argument passed to the callback 9 | * is interpreted as result and none of the arguments as error (if the function 10 | * accepting the callback throws or rejects, that will still result in the 11 | * promisified function rejecting). 12 | * 13 | * The `callbackFirst` property allows passing additional parameters after the 14 | * callback and `callbackLast` will pass additional parameters before the 15 | * callback. 16 | * 17 | * @param withCallback - The function accepting a callback that you want to turn 18 | * into a function returning a promise. 19 | * @param resultIndex - The index of the parameter of the callback that contains 20 | * the result. Defaults to `0`. 21 | * Pass `null` if the callback doesn't receive a result. 22 | * @param errorIndex - The index of the parameter of the callback that contains 23 | * an error. Pass `null` if the callback never receives an error. 24 | * Defaults to `null`. 25 | * 26 | * @example 27 | * ``` 28 | * const notify = (cb: (msg: string) => void) => { msg('something') } 29 | * const waitForMessage = promisify(notify) 30 | * await waitForMessage() // -> 'something' 31 | * 32 | * // here result is passed at index 1 and errors at index 0. 33 | * const callbackAPI = (cb: (error?: Error, data?: unknown) => void) => {} 34 | * const asyncAPI = promisify(callbackAPI, 1, 0) 35 | * ``` 36 | * 37 | * @example 38 | * ``` 39 | * const sleep = promisify(setTimeout).callbackFirst 40 | * await sleep(200) 41 | * ``` 42 | * 43 | * @example 44 | * ``` 45 | * const fs = require('node:fs'); 46 | * const stat = promisify(fs.stat, 1, 0).callbackLast 47 | * 48 | * try { 49 | * const stats = await stat('.'); 50 | * console.log(`This directory is owned by ${stats.uid}`); 51 | * } catch (err) { 52 | * console.error(err) 53 | * } 54 | * ``` 55 | */ 56 | const promisify = ( 57 | withCallback: T, 58 | resultIndex?: N, 59 | errorIndex: number | null = null, 60 | ): Promisified => { 61 | const promisified = (append?: boolean) => (...additional: unknown[]) => 62 | new Promise((res, rej) => 63 | withCallback(...append ? additional : [], (...args: unknown[]) => { 64 | const error = typeof errorIndex === "number" ? args[errorIndex] : null; 65 | if (error !== undefined && error !== null) return rej(error); 66 | res(resultIndex === null ? undefined : args[resultIndex ?? 0]); 67 | }, ...!append ? additional : [])?.catch?.(rej) 68 | ); 69 | 70 | return Object.assign(promisified(), { 71 | callbackFirst: promisified(false), 72 | callbackLast: promisified(true), 73 | }) as any; 74 | }; 75 | 76 | export default promisify; 77 | 78 | type Promisified = 79 | & (() => Promise< 80 | CallbackResult, N> 81 | >) 82 | & { 83 | callbackFirst: (...args: Trailing>) => Promise< 84 | CallbackResult, N> 85 | >; 86 | callbackLast: (...args: Leading>) => Promise< 87 | CallbackResult, N> 88 | >; 89 | }; 90 | 91 | type Callback = FindCallback>; 92 | 93 | type FindCallback = T extends [infer F, ...infer R] 94 | ? (F extends λ ? F : FindCallback) 95 | : never; 96 | 97 | type CallbackResult = N extends number 98 | ? (Parameters[N] extends undefined ? void : Parameters[N]) 99 | : void; 100 | 101 | type Leading = T extends [...infer L, λ] ? L : []; 102 | 103 | type Trailing = T extends [λ, ...infer T] ? T 104 | : []; 105 | -------------------------------------------------------------------------------- /queue.test.ts: -------------------------------------------------------------------------------- 1 | import createQueue from "./queue.ts"; 2 | import { assert, assertEquals, assertRejects } from "testing/asserts.ts"; 3 | 4 | Deno.test("queue", async () => { 5 | const logs: string[] = []; 6 | const queue = createQueue(); 7 | 8 | const returns = await Promise.all([ 9 | queue(async () => { 10 | logs.push("enter a"); 11 | await wait(10); 12 | logs.push("return a"); 13 | return "a"; 14 | }), 15 | queue(async () => { 16 | logs.push("enter b"); 17 | await wait(20); 18 | logs.push("return b"); 19 | return "b"; 20 | }), 21 | queue(() => { 22 | logs.push("enter c"); 23 | logs.push("return c"); 24 | return "c"; 25 | }), 26 | await queue(async () => { 27 | logs.push("enter d"); 28 | await wait(10); 29 | logs.push("return d"); 30 | return "d"; 31 | }), 32 | ]); 33 | 34 | assertEquals(returns, ["a", "b", "c", "d"]); 35 | assertEquals(logs, [ 36 | "enter a", 37 | "return a", 38 | "enter b", 39 | "return b", 40 | "enter c", 41 | "return c", 42 | "enter d", 43 | "return d", 44 | ]); 45 | }); 46 | 47 | Deno.test("queue (reject)", async () => { 48 | const queue = createQueue(); 49 | 50 | assertRejects(() => 51 | queue(() => { 52 | throw Error(); 53 | }) 54 | ); 55 | const a = queue(async () => { 56 | await wait(10); 57 | return 1; 58 | }); 59 | assertRejects(() => 60 | queue(async () => { 61 | await wait(10); 62 | throw Error(); 63 | }) 64 | ); 65 | const c = queue(async () => { 66 | await wait(10); 67 | return 3; 68 | }); 69 | 70 | const done = queue.done; 71 | assert(done instanceof Promise); 72 | assertEquals(await a, 1); 73 | assertEquals(await c, 3); 74 | assert(queue.done === true); 75 | assertEquals(await done, undefined); 76 | }); 77 | 78 | const wait = (ms: number) => new Promise((res) => setTimeout(res, ms)); 79 | -------------------------------------------------------------------------------- /queue.ts: -------------------------------------------------------------------------------- 1 | import type { MakeProm, λ } from "./types.ts"; 2 | import noop from "./noop.ts"; 3 | 4 | /** 5 | * Creates a `queue` function that accepts a function as it's only parameter. 6 | * When `queue` is invoked, the passed in function is executed after the last 7 | * function passed to `queue` has finished executing. The `queue` function 8 | * returns the result of the passed in function asynchronously. 9 | * 10 | * Reading `queue.done` is `true` if no functions are currently executing / 11 | * scheduled and otherwise a promise that resolves once the last function has 12 | * stopped executing and no futher functions are queued. 13 | * 14 | * @example 15 | * ``` 16 | * const queue = createQueue() 17 | * 18 | * queue(async () => { 19 | * console.log('start a') 20 | * await delay() 21 | * return 'end a' 22 | * }).then(console.log) 23 | * 24 | * queue(async () => { 25 | * console.log('start b') 26 | * await delay() 27 | * return 'end b' 28 | * }).then(console.log) 29 | * 30 | * queue(async () => { 31 | * console.log('start c') 32 | * await delay() 33 | * return 'end c' 34 | * }).then(console.log) 35 | * 36 | * await queue.done 37 | * 38 | * // start a 39 | * // end a 40 | * // start b 41 | * // end b 42 | * // start c 43 | * // end c 44 | * ``` 45 | */ 46 | const createQueue = (): Queue => { 47 | let last: Promise | null = null; 48 | 49 | const queued = (fun: λ) => 50 | last = (last ?? Promise.resolve()).catch(noop).then(fun).finally( 51 | () => { 52 | last = null; 53 | }, 54 | ); 55 | 56 | return Object.defineProperty(queued, "done", { 57 | get: () => last?.catch(noop).then(noop) ?? true, 58 | }) as Queue; 59 | }; 60 | 61 | export default createQueue; 62 | 63 | type Queue = (>(fun: T) => MakeProm>) & { 64 | done: Promise | true; 65 | }; 66 | -------------------------------------------------------------------------------- /range.test.ts: -------------------------------------------------------------------------------- 1 | import range, { alphaRange, numberRange } from "./range.ts"; 2 | import { assertEquals, assertThrows } from "testing/asserts.ts"; 3 | 4 | Deno.test("range", () => { 5 | assertEquals(range(1, 3), [1, 2, 3]); 6 | assertEquals(range(1, 3.5), [1, 2, 3]); 7 | assertEquals(range(1, 2.5), [1, 2]); 8 | 9 | assertEquals(range(3, 1), [3, 2, 1]); 10 | assertEquals(range(3, 0.5), [3, 2, 1]); 11 | assertEquals(range(3, 1.5), [3, 2]); 12 | 13 | assertEquals(range(0, 1, 0.2), [0, 0.2, 0.2 * 2, 0.2 * 3, 0.2 * 4, 0.2 * 5]); 14 | 15 | assertEquals(range("a", "d"), "abcd".split("")); 16 | assertEquals(range("d", "a"), "dcba".split("")); 17 | 18 | assertThrows(() => alphaRange("foo", "bar"), RangeError); 19 | assertThrows(() => numberRange(1, 2, -1), RangeError); 20 | assertThrows(() => numberRange(2, 1, 1), RangeError); 21 | }); 22 | -------------------------------------------------------------------------------- /range.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "./except.ts"; 2 | 3 | /** 4 | * Constructs a numeric between `start` and `end` inclusively. 5 | * 6 | * @param step - The step between items of the list. Must be `> 0` for ascending 7 | * and `< 0` for descending ranges. Defaults to `1` if ascending and `-1` if 8 | * descending. 9 | * 10 | * @example 11 | * ``` 12 | * range(2, 6) // -> [2, 3, 4, 5, 6] 13 | * range(8, 9, .3) // -> [8, 8.3, 8.6, 8.9] 14 | * range(3, -2) // -> [3, 2, 1, 0, -1, -2] 15 | * ``` 16 | */ 17 | export function numberRange( 18 | start: number, 19 | end: number, 20 | step = end > start ? 1 : -1, 21 | ): number[] { 22 | assert( 23 | Math.sign(step) === Math.sign(end - start), 24 | "step must be >0 for ascending and <0 descending ranges", 25 | RangeError, 26 | ); 27 | const sequence: number[] = []; 28 | for (let n = start; step > 0 ? n <= end : n >= end; n += step) { 29 | sequence.push(n); 30 | } 31 | return sequence; 32 | } 33 | 34 | /** 35 | * Constructs a range between characters. 36 | * 37 | * @example 38 | * ``` 39 | * range('a', 'd') // -> ['a', 'b', 'c', 'd'] 40 | * range('Z', 'W') // -> ['Z', 'Y', 'X', 'W'] 41 | * ``` 42 | */ 43 | export function alphaRange(start: string, end: string) { 44 | assert( 45 | start.length === 1 && end.length === 1, 46 | "alphabetical range can only be constructed between single-character strings", 47 | RangeError, 48 | ); 49 | 50 | return numberRange(start.charCodeAt(0), end.charCodeAt(0)).map((n) => 51 | String.fromCharCode(n) 52 | ); 53 | } 54 | 55 | type RangeSig = { 56 | (...args: Parameters): ReturnType; 57 | (...args: Parameters): ReturnType; 58 | }; 59 | 60 | /** 61 | * Creates a range between two values. 62 | * 63 | * @see {@link numberRange} 64 | * @see {@link alphaRange} 65 | */ 66 | const range: RangeSig = (...args: unknown[]) => 67 | ((typeof args[0] === "number" ? numberRange : alphaRange) as any)(...args); 68 | 69 | export default range; 70 | -------------------------------------------------------------------------------- /repeat.test.ts: -------------------------------------------------------------------------------- 1 | import repeat from "./repeat.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("repeat", () => { 5 | const result: number[] = []; 6 | for (const n of repeat(1, 2, 3)) { 7 | result.push(n); 8 | if (result.length >= 9) break; 9 | } 10 | assertEquals(result, [1, 2, 3, 1, 2, 3, 1, 2, 3]); 11 | }); 12 | -------------------------------------------------------------------------------- /repeat.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a generator that repeats `sequence`. 3 | * 4 | * @example 5 | * ``` 6 | * // prints: 1, 2, 3, 1, 2, 3, ... 7 | * for (const n of repeat(1, 2, 3)) 8 | * console.log(n) 9 | * ``` 10 | */ 11 | export default function* repeat(...sequence: [T, ...T[]]): Generator { 12 | while (true) for (const n of sequence) yield n; 13 | } 14 | -------------------------------------------------------------------------------- /scripts/buildNode: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf build tmp 4 | mkdir build tmp 5 | cp -r ./*.ts tmp 6 | cd tmp 7 | find . -name '*.test.ts' | xargs rm -r 8 | mv mod.ts index.ts 9 | find . -name '*.ts' | xargs sed -i.bak 's/.ts";/";/g' 10 | find . -name '*.ts' | xargs sed -i.bak 's/{ performance } = globalThis/{ performance } = "performance" in globalThis ? globalThis : require("perf_hooks")/g' 11 | cp ../npm/tsconfig.json . 12 | cp ../npm/.babelrc . 13 | ln -s ../npm/node_modules node_modules 14 | npx tsc -p tsconfig.json 15 | npx babel . --extensions .ts -d ../build --ignore node_modules 16 | cd ../build 17 | jq 'del(.scripts,.private)' ../npm/package.json > package.json 18 | for file in *.js; do 19 | BASE_NAME="$(basename -- "$file" .js)" 20 | mv -- "$file" "${BASE_NAME}.mjs" 21 | cat <<< $(jq ".exports[\"./${BASE_NAME}\"] = { \"require\": \"./${BASE_NAME}.js\", \"import\": \"./${BASE_NAME}.mjs\" }" package.json) > package.json 22 | done 23 | sed -E -i -e 's/from "([a-zA-Z\/\.]+)/from "\1\.mjs/g' *.mjs 24 | sed -E -i -e 's/require\("perf_hooks"\)/{performance:{now:\(\)=>Date.now\(\)}}/g' *.mjs 25 | sed -E -i -e 's/require\(([^)]+)\)/await import(\1)/g' *.mjs 26 | cd ../tmp 27 | cat <<< $(jq '.presets[0][1].modules = "cjs"' .babelrc) > .babelrc 28 | cat .babelrc 29 | npx babel . --extensions .ts -d ../build --ignore node_modules 30 | cd ../build 31 | echo -e "\nmodule.exports = Object.assign(exports.default || {}, exports);" | tee -a ./*.js 32 | # patch for babel redefining exports 33 | sed -E -i -e 's/if \(key in exports && .+/if \(key in exports\) return;/g' index.js 34 | node ../scripts/rewriteStarExports.mjs 35 | rm ./*-e 36 | rm -r ../tmp 37 | cp ../README.md . 38 | cp ../LICENSE . 39 | -------------------------------------------------------------------------------- /scripts/docs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | scripts/setupGitHooks 4 | rm -rf tmp 5 | mkdir tmp 6 | cp -r ./*.ts tmp 7 | cd tmp 8 | find . -name '*.test.ts' | xargs rm -r 9 | mv mod.ts index.ts 10 | find . -name '*.ts' | xargs sed -i.bak 's/.ts";/";/g' 11 | cp ../npm/tsconfig.json . 12 | ln -s ../npm/node_modules node_modules 13 | npx typedoc ./* --json docs.json 14 | cp ../scripts/docs.js . 15 | node docs.js 16 | cd .. 17 | rm -r tmp 18 | -------------------------------------------------------------------------------- /scripts/docs.js: -------------------------------------------------------------------------------- 1 | const docs = require('./docs.json') 2 | const package = require('../npm/package.json') 3 | const version = `v${package.version}` 4 | 5 | const repo = 'https://github.com/MathisBullinger/froebel' 6 | let readme = `# Froebel - a strictly typed TypeScript utility library. 7 | 8 | This is my (WIP) personal collection of TypeScript helper functions and utilities that 9 | I use across different projects. 10 | Think an opinionated version of lodash, but with first-class types. 11 | 12 | If you have an idea for a utility that might make a good addition to this collection, 13 | please open an issue and suggest its inclusion. 14 | 15 | Runs in Deno, Node.js, and the Browser. Get it from [deno.land](https://deno.land/x/froebel@${version}) 16 | or [npm](https://www.npmjs.com/package/froebel). 17 | 18 | ## Installation 19 | 20 | ### Using npm 21 | 22 | \`\`\`shell 23 | npm install froebel 24 | \`\`\` 25 | 26 | and — assuming a module-compatible system like webpack — import as: 27 | 28 | \`\`\`ts 29 | import { someUtility } from 'froebel'; 30 | // you can also import the utility you need directly: 31 | import memoize from 'froebel/memoize'; 32 | \`\`\` 33 | 34 | ### Using Deno 35 | 36 | \`\`\`ts 37 | import { someUtility } from "https://deno.land/x/froebel@${version}/mod.ts"; 38 | // or import just the utility you need: 39 | import memoize from "https://deno.land/x/froebel@${version}/memoize.ts" 40 | \`\`\` 41 | 42 | --- 43 | 44 | ## Available Utilities 45 | 46 | Each category also has a file exporting only the utilities in that category, so 47 | if you want to only import utilities from one category, you could import them as 48 | 49 | \`\`\`ts 50 | import { throttle, debounce } from "froebel/function"; 51 | \`\`\` 52 | 53 | A few utils are exported from multiple categories but will only be listed here 54 | once. For example \`isPromise\` is exported from both the \`promise\` and the 55 | \`predicate\` category. 56 | 57 | ### Table of Contents 58 | 59 | ` 60 | const paramReplace = { __namedParameters: 'funs' } 61 | 62 | const indCont = require('fs').readFileSync( 63 | require('path').resolve(__dirname, './index.ts'), 64 | 'utf8' 65 | ) 66 | 67 | const resImport = ({ fileName, line } = {}) => { 68 | if (!fileName) return 'tmp/string.ts' 69 | if (!fileName.endsWith('index.ts')) return fileName 70 | return `src/${indCont.split('\n')[line - 1].match(/\w+(?=')/)[0]}.ts` 71 | } 72 | 73 | const modules = new Set( 74 | docs.children 75 | .find(({ name }) => name === 'index') 76 | .children.filter(v => v.kindString === 'Reference') 77 | .map(v => resImport(v.sources?.[0])) 78 | ) 79 | 80 | const exps = [...modules].flatMap(name => 81 | docs.children 82 | .find(child => child.name === name.split('/').pop().replace(/\.ts$/, '')) 83 | .children.filter(v => v.kindString === 'Reference') 84 | .map(v => [v.id, resImport(v.sources?.[0])]) 85 | ) 86 | 87 | let cats = exps.reduce( 88 | (a, [id, file]) => ({ ...a, [file]: [...(a[file] ?? []), id] }), 89 | {} 90 | ) 91 | 92 | const defaultExports = docs.children 93 | .find(({ name }) => name === 'index') 94 | .children.filter(v => v.kindString === 'Reference') 95 | .map(v => [v.id, resImport(v.sources?.[0])]) 96 | .reduce((a, [id, file]) => ({ ...a, [file]: [...(a[file] ?? []), id] }), {}) 97 | 98 | const moduleOrder = Object.entries(defaultExports) 99 | .sort(([, a], [, b]) => Math.min(...a) - Math.min(...b)) 100 | .map(([v]) => v) 101 | 102 | const alias = { ds: 'Data Structures' } 103 | const seen = new Set() 104 | cats = Object.entries(cats) 105 | .sort(([a], [b]) => moduleOrder.indexOf(a) - moduleOrder.indexOf(b)) 106 | .map(([file, ids]) => { 107 | const [name] = file.match(/\w+?(?=\.ts$)/) 108 | return [ 109 | alias[name] ?? name[0].toUpperCase() + name.slice(1), 110 | ids 111 | .filter(v => { 112 | const [, info] = getNode(v) 113 | if (seen.has(info.id)) return false 114 | seen.add(info.id) 115 | return info.kindString !== 'Variable' || info.type.type !== 'query' 116 | }) 117 | .sort(), 118 | file 119 | ] 120 | }) 121 | 122 | const catByName = {} 123 | const catById = {} 124 | 125 | for (const [, ids, file] of cats) { 126 | for (const id of ids) { 127 | const [name] = getNode(id) 128 | if (!(name in catByName)) catByName[name] = [] 129 | const fileName = file.replace(/^tmp\//, '') 130 | catByName[name].push(fileName) 131 | catById[id] = fileName 132 | } 133 | } 134 | 135 | readme += '\n\n' 136 | for (const [name, ids] of cats) { 137 | readme += `- __\`${name.toLowerCase()}\`__\n` 138 | ids.forEach(id => { 139 | const name = getNode(id)[0] 140 | readme += ` - [${name}](#${name.toLowerCase()})\n` 141 | }) 142 | } 143 | readme += '\n\n' 144 | 145 | for (const [name, ids] of cats) 146 | readme += `\n## ${name}\n\n${ids.map(docItem).join('\n\n---\n\n')}` 147 | 148 | function docItem(id) { 149 | const [name, info] = getNode(id) 150 | const node = info.signatures?.[0] ?? info 151 | 152 | let docNode 153 | try { 154 | docNode = node.comment 155 | ? node 156 | : getNode(node.type.types[0].declaration.signatures[0].type.id)[1] 157 | } catch (e) { 158 | console.log('no doc node for', name) 159 | } 160 | 161 | let descr = docNode?.comment?.shortText ?? '' 162 | if (descr && docNode?.comment?.text) descr += `\n\n${docNode.comment.text}` 163 | const see = docNode?.comment?.tags?.filter(({ tag }) => tag === 'see') ?? [] 164 | if (see.length) 165 | descr += `\n\nsee ${new Intl.ListFormat('en').format( 166 | see.map(({ text }) => text.replace(/\n*$/, '')) 167 | )}` 168 | 169 | if (descr) 170 | descr = descr 171 | .replace(/(?<=^|\n)(.?)/g, '> $1') 172 | .replace(/\{@link\s(\w+)\}/g, (_, $1) => `[${$1}](#${$1.toLowerCase()})`) 173 | 174 | const parenthHeur = expr => 175 | expr.includes('=>') && !/^[{(\[]/.test(expr) ? `(${expr})` : expr 176 | 177 | const postProcess = str => 178 | str 179 | .replace(/λ<([^>]+),\s*any>/g, 'λ<$1>') 180 | .replace(/\[\.{3}([A-Z])\[\]\]/g, '$1[]') 181 | 182 | const formatNode = (node, name) => postProcess(_formatNode(node, name)) 183 | 184 | function _formatNode(node, name) { 185 | if (node.target) 186 | node = 187 | typeof node.target === 'number' ? getNode(node.target) : node.target 188 | 189 | if (node.name === 'default') node.name = name 190 | 191 | if (['Call signature', 'Constructor signature'].includes(node.kindString)) { 192 | const isClass = node.kindString === 'Constructor signature' 193 | if (isClass) delete node.type.typeArguments 194 | let ret = formatNode(node.type, name) 195 | if (ret === 'undefined') ret = 'void' 196 | const argStr = `(${ 197 | node.parameters 198 | ?.map( 199 | v => 200 | `${v.flags?.isRest ? '...' : ''}${ 201 | paramReplace[v.name] ?? v.name 202 | }${v.flags.isOptional ? '?' : ''}: ${formatNode(v, name)}` 203 | ) 204 | .join(', ') ?? '' 205 | })` 206 | if (!isClass) return `${argStr} => ${ret}` 207 | const gen = !node.typeParameter 208 | ? '' 209 | : `<${node.typeParameter 210 | .filter(v => !v.default) 211 | .map(v => v.name) 212 | .join(', ')}>` 213 | return `class ${ret}${gen}${argStr}` 214 | } 215 | 216 | if (!node.type) { 217 | if (node.signatures?.length) return formatNode(node.signatures[0], name) 218 | if (node.children) { 219 | if (node.children[0].name === 'constructor') 220 | return formatNode(node.children[0], name) 221 | return `{${node.children 222 | .map( 223 | v => `${v.name}: ${formatNode(v.type ?? v.signatures?.[0])}`, 224 | name 225 | ) 226 | .join(', ')}}` 227 | } 228 | throw node 229 | } 230 | 231 | if (typeof node.type === 'object') return formatNode(node.type, name) 232 | if (node.type === 'reference') { 233 | const name = node.name 234 | if (name === 'default') console.log('default', node) 235 | if (!node.typeArguments?.length) return name 236 | return `${name}<${node.typeArguments 237 | .map(v => formatNode(v, name)) 238 | .join(', ')}>` 239 | } 240 | if (node.type === 'intrinsic') return node.name 241 | if (node.type === 'literal') return JSON.stringify(node.value) 242 | if (node.type === 'predicate') 243 | return `${node.name} is ${formatNode(node.targetType, name)}` 244 | if (node.type === 'indexedAccess') 245 | return `${formatNode(node.objectType, name)}[${formatNode( 246 | node.indexType, 247 | name 248 | )}]` 249 | if (node.type === 'array') return `${formatNode(node.elementType, name)}[]` 250 | if (node.type === 'reflection') return formatNode(node.declaration, name) 251 | if (node.type === 'tuple') 252 | return `[${ 253 | node.elements?.map(v => formatNode(v, name)).join(', ') ?? '' 254 | }]` 255 | if (node.type === 'rest') return `...${formatNode(node.elementType, name)}` 256 | if (node.type === 'template-literal') return `\`\${string}\`` 257 | if (node.type === 'mapped') 258 | return `{[${node.parameter} in ${formatNode( 259 | node.parameterType, 260 | name 261 | )}]: ${formatNode(node.templateType, name)}}` 262 | if (node.type === 'conditional') { 263 | if ( 264 | (node.trueType.name === 'never') !== 265 | (node.falseType.name === 'never') 266 | ) 267 | return formatNode( 268 | node.falseType.name === 'never' ? node.trueType : node.falseType, 269 | name 270 | ) 271 | return `${formatNode(node.checkType)}${ 272 | node.extendsType ? ` extends ${formatNode(node.extendsType, name)}` : '' 273 | } ? ${formatNode(node.trueType, name)} : ${formatNode( 274 | node.falseType, 275 | name 276 | )}` 277 | } 278 | if (node.type === 'intersection') { 279 | if (node.types.every(({ type }) => type === 'reflection')) 280 | return formatNode(node.types[0], name) 281 | return node.types 282 | .filter(({ type }) => type !== 'query') 283 | .map(v => formatNode(v, name)) 284 | .map(parenthHeur) 285 | .join(' & ') 286 | } 287 | if (node.type === 'union') 288 | return node.types 289 | .map(v => formatNode(v, name)) 290 | .filter(v => v !== 'undefined') 291 | .map(parenthHeur) 292 | .join(' | ') 293 | 294 | if (node.type === 'query') return `<${node.queryType.name}>` 295 | 296 | if (node.type === 'named-tuple-member') throw 0 297 | 298 | console.warn(`unknown node type ${node.type}:`, node) 299 | return '???' 300 | } 301 | 302 | function examples(sig) { 303 | const examples = sig?.comment?.tags?.filter(({ tag }) => tag === 'example') 304 | if (!examples?.length) return '' 305 | return `\n\n#### Example${examples.length > 1 ? 's' : ''}\n${examples 306 | .map( 307 | ({ text }) => 308 | `\`\`\`ts\n${text.replace(/(^[`\n]+)|([`\n]+$)/g, '')}\n\`\`\`` 309 | ) 310 | .join('\n\n')}` 311 | } 312 | 313 | const srcs = findSources(info) 314 | const fileName = srcs[0].fileName.replace('tmp/', '') 315 | const src = srcs 316 | ? `_[source](${repo}/blob/main/${fileName}#L${srcs[0].line})_ | _[tests](${repo}/blob/main/${fileName.replace(/\.ts$/, '.test.ts')})_` 317 | : (console.warn(`couldn't find source for ${name} ${id}`), '') 318 | 319 | const importPart = `import ${info.name === 'default' ? name : `{ ${name} }`} from` 320 | let importPath = catByName[name].length > 1 ? catById[id] : fileName 321 | 322 | let code = '' 323 | try { 324 | code = `\`\`\`hs\n${formatNode(info, name)}\n\`\`\`` 325 | } catch (e) { 326 | if (e !== 0) throw e 327 | } 328 | 329 | 330 | return `#### \`${name}\` 331 | 332 | ${code} 333 | 334 | ${src} 335 | 336 | ${descr ?? ''} 337 | 338 | 339 | #### Import 340 | 341 | \`\`\`ts 342 | /* Node: */ ${importPart} "froebel/${importPath.replace(/\.ts$/, '')}"; 343 | /* Deno: */ ${importPart} "https://deno.land/x/froebel@${version}/${importPath}"; 344 | \`\`\` 345 | 346 | 347 | ${examples(docNode)}` 348 | } 349 | 350 | require('fs').writeFileSync( 351 | require('path').resolve(__dirname, '../README.md'), 352 | readme 353 | ) 354 | 355 | function getNode(id, node = docs, path = []) { 356 | if (node?.id === id) 357 | return [ 358 | node.name, 359 | ...(!node.target 360 | ? [node, path] 361 | : getNode(node.target, docs, [...path, node]).slice(1)), 362 | ] 363 | 364 | if (!node.children?.length) return 365 | for (const child of node.children) { 366 | let match = getNode(id, child, [...path, node]) 367 | if (match) return match 368 | } 369 | } 370 | 371 | function findSources(node) { 372 | if (node.sources) return node.sources 373 | return getNode(node.type.types[0].declaration.signatures[0].type.id)[1] 374 | .sources 375 | } 376 | -------------------------------------------------------------------------------- /scripts/publishNpm: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | scripts/test 4 | scripts/buildNode 5 | cd build 6 | npm publish 7 | -------------------------------------------------------------------------------- /scripts/rewriteStarExports.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { fileURLToPath } from 'url'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = path.dirname(__filename); 7 | const indexPath = path.resolve(__dirname, '../build/index.mjs') 8 | let content = fs.readFileSync(indexPath, 'utf-8') 9 | let match 10 | 11 | while (match = content.match(/export \* from "([^"]+).+/)) { 12 | const fileExports = Object.keys(await import(path.resolve(__dirname, '../build', match[1]))) 13 | const individualExports = fileExports.map(name => `export { ${name} } from "${match[1]}";`) 14 | content = content.slice(0, match.index) + individualExports.join('\n') + content.slice(match.index + match[0].length) 15 | } 16 | 17 | content = content.split('\n').filter(Boolean).filter((line, i, lines) => lines.findIndex(v => v.startsWith(line.split('from ')[0])) === i).join('\n') 18 | 19 | fs.writeFileSync(indexPath, content) 20 | -------------------------------------------------------------------------------- /scripts/setupGitHooks: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! $CI ] 4 | then 5 | git config core.hooksPath .githooks 6 | fi 7 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | scripts/setupGitHooks 4 | deno lint ./*.ts 5 | deno test --importmap=import_map.json ./*.test.ts 6 | -------------------------------------------------------------------------------- /scripts/testArg.js: -------------------------------------------------------------------------------- 1 | module.exports.arg = (target = {}) => 2 | new Proxy(target, { 3 | get(_, prop) { 4 | if (prop === Symbol.toPrimitive) return () => {}; 5 | if (prop in new String(" ")) return " "[prop]; 6 | if (prop in []) return [][prop]; 7 | 8 | return target; 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /scripts/testImports: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf testImport 4 | mkdir testImport 5 | echo 'const { arg } = require("../scripts/testArg");' > testImport/commonjs.js 6 | echo 'import { arg } from "../scripts/testArg.js";' > testImport/esmodule.mjs 7 | 8 | testJS='try { \ 9 | console.log(\2.name || \2); \ 10 | \2(arg(), arg()); \ 11 | } catch(e) { \ 12 | try { \ 13 | \2(arg(() => {}), arg(() => {})); \ 14 | } catch { \ 15 | if (e instanceof TypeError) throw e; \ 16 | } \ 17 | }' 18 | 19 | grep -E '\/\* Node:' build/README.md\ 20 | | sed -E "s/.*import (\{ )?([a-zA-Z]+)( })? from[^\"]*([^;]+);/var \1\2\3 = require(\4);\n${testJS}/"\ 21 | >> testImport/commonjs.js 22 | 23 | grep -E '\/\* Node:' build/README.md\ 24 | | sed -E "s/.*import (\{ )?([a-zA-Z]+)( })? from[^\"]*([^;]+);/import \1\2\3 from \4;\n${testJS}/"\ 25 | >> testImport/esmodule.mjs 26 | 27 | sed -E -i -e 's/import \{ ([a-zA-Z]+) \} from "froebel\/([a-z]+)";/import { \1 as \1_\2 } from "froebel\/\2";\nvar \1 = \1_\2;/g' testImport/esmodule.mjs 28 | 29 | sed -E -i -e 's/ ([A-Z][a-zA-Z]+\()[^;]+/ new \1)/g' testImport/commonjs.js 30 | sed -E -i -e 's/ ([A-Z][a-zA-Z]+\()[^;]+/ new \1)/g' testImport/esmodule.mjs 31 | 32 | cd build 33 | npm link 34 | 35 | cd ../testImport 36 | npm link froebel 37 | 38 | node commonjs.js 39 | node esmodule.mjs 40 | 41 | cd .. && rm -r testImport 42 | -------------------------------------------------------------------------------- /select.test.ts: -------------------------------------------------------------------------------- 1 | import select, { none } from "./select.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("select path", () => { 5 | const ref = {}; 6 | 7 | const obj = { 8 | a: { 9 | b: "c", 10 | }, 11 | d: "e", 12 | f: undefined, 13 | g: [{ foo: "bar" }, { foo: "baz" }], 14 | map: new Map([["a", "b"], ["c", "d"]]), 15 | map2: new Map(), 16 | boolMap: new Map(), 17 | nestMap: new Map([["a", [ 18 | 0, 19 | new Map([[ref, { foo: "bar" }]]), 20 | ]]]), 21 | } as const; 22 | 23 | assertEquals(select(obj, "a"), { b: "c" }); 24 | assertEquals(select(obj, "a", "b"), "c"); 25 | assertEquals(select(obj, "f"), undefined); 26 | assertEquals(select(obj, "z"), none); 27 | assertEquals(select(obj, "a", "z"), none); 28 | assertEquals(select(obj, "d", "e"), none); 29 | assertEquals(select(obj, "g", 0, "foo"), "bar"); 30 | assertEquals(select(obj, "g", 1, "foo"), "baz"); 31 | assertEquals(select(obj, "map", "a"), "b"); 32 | assertEquals(select(obj, "map", "c"), "d"); 33 | assertEquals(select(obj, "map", "e"), none); 34 | assertEquals(select(obj, "nestMap", "a", 1, ref, "foo"), "bar"); 35 | 36 | { 37 | const _a_b: "c" = select(obj, "a", "b"); 38 | const _a_z: typeof none = select(obj, "a", "z"); 39 | } 40 | { 41 | // @ts-expect-error 42 | const _a_b2: "c" = select(obj as unknown, "a", "b"); 43 | const _a_b1: unknown = select(obj as unknown, "a", "b"); 44 | 45 | // @ts-expect-error 46 | const _a_z1: typeof none = select(obj as unknown, "a", "z"); 47 | const _a_z2: unknown = select(obj as unknown, "a", "z"); 48 | } 49 | { 50 | // @ts-expect-error 51 | const _bm_f1: number = select(obj, "boolMap", false); 52 | // @ts-expect-error 53 | const _bm_f2: string | typeof none = select(obj, "boolMap", false); 54 | const _bm_f3: number | typeof none = select(obj, "boolMap", false); 55 | 56 | const _m_k1: typeof none = select(obj, "map", 2); 57 | const _m_k2: string | typeof none = select(obj, "map", "foo"); 58 | 59 | const _m2_k1: string | typeof none = select(obj, "map2", "a"); 60 | const _m2_k2: string | typeof none = select(obj, "map2", 2); 61 | } 62 | }); 63 | -------------------------------------------------------------------------------- /select.ts: -------------------------------------------------------------------------------- 1 | export const none = Symbol("value.none"); 2 | 3 | /** 4 | * Returns the value in `obj` at `path`. If the given path does not exist, 5 | * the symbol `none` is returned. 6 | * 7 | * @example 8 | * ``` 9 | * // -> 'something' 10 | * select( 11 | * { a: { deeply: [{ nested: { object: 'something' } }] } }, 12 | * 'a', 'deeply', 0, 'nested', 'object' 13 | * ) 14 | * ``` 15 | */ 16 | const select = < 17 | T, 18 | P extends ( 19 | AnyNarrow 20 | )[], 21 | >( 22 | obj: T, 23 | ...path: P 24 | ): PickPath => 25 | path.length === 0 26 | ? obj 27 | : obj instanceof Map 28 | ? (obj.has(path[0]) ? select(obj.get(path[0]), ...path.slice(1)) : none) 29 | : typeof obj !== "object" || obj === null || 30 | typeof path[0] !== "string" && typeof path[0] !== "number" && 31 | typeof path[0] !== "symbol" || 32 | !(path[0] in obj) 33 | ? none 34 | : (select(obj[path[0] as keyof T], ...path.slice(1)) as any); 35 | 36 | export default select; 37 | 38 | type AnyNarrow = 39 | | string 40 | | number 41 | | symbol 42 | | boolean 43 | | undefined 44 | | null 45 | | Record; 46 | 47 | type PickPath = P extends [] ? T 48 | : T extends Map 49 | ? (P[0] extends K ? V | typeof none : typeof none) 50 | : P[0] extends keyof T 51 | ? PickPath 52 | : unknown extends T ? unknown 53 | : typeof none; 54 | -------------------------------------------------------------------------------- /settled.test.ts: -------------------------------------------------------------------------------- 1 | import { isFulfilled, isRejected } from "./settled.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("settled predicate", async () => { 5 | const proms: Promise[] = [Promise.resolve(1), Promise.reject("foo")]; 6 | const [a, b] = await Promise.allSettled(proms); 7 | 8 | assertEquals([a, b].map(isFulfilled), [true, false]); 9 | assertEquals([a, b].map(isRejected), [false, true]); 10 | 11 | if (isFulfilled(a)) { 12 | const _val: number = a.value; 13 | // @ts-expect-error 14 | const _str: string = a.value; 15 | // @ts-expect-error 16 | const _err = a.reason; 17 | } 18 | if (isRejected(a)) { 19 | const _err = a.reason; 20 | // @ts-expect-error 21 | const _val = a.value; 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /settled.ts: -------------------------------------------------------------------------------- 1 | /** Checks if `result` (returned from `Promise.allSettled`) is fulfilled. */ 2 | export const isFulfilled = ( 3 | result: PromiseSettledResult, 4 | ): result is PromiseFulfilledResult => result.status === "fulfilled"; 5 | 6 | /** Checks if `result` (returned from `Promise.allSettled`) is rejected. */ 7 | export const isRejected = ( 8 | result: PromiseSettledResult, 9 | ): result is PromiseRejectedResult => result.status === "rejected"; 10 | -------------------------------------------------------------------------------- /shuffle.test.ts: -------------------------------------------------------------------------------- 1 | import shuffle from "./shuffle.ts"; 2 | import { assert, assertEquals, assertNotEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("shuffle", () => { 5 | const list = [...Array(1e5)].map((_, i) => i); 6 | const org = [...list]; 7 | const shuffled = shuffle(list); 8 | 9 | assertEquals(list, org); 10 | assertNotEquals(list, shuffled); 11 | assert(list.length === shuffled.length); 12 | assertEquals(shuffled.sort((a, b) => a - b), list); 13 | }); 14 | -------------------------------------------------------------------------------- /shuffle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Shuffles `list` using the Fisher-Yates shuffle algorithm. 3 | * The original `list` is not modified and the shuffled list is returned. 4 | */ 5 | const shuffle = (list: T[]): T[] => { 6 | const shuffled = [...list]; 7 | shuffleInPlace(shuffled); 8 | return shuffled; 9 | }; 10 | 11 | export default shuffle; 12 | 13 | /** 14 | * Same as {@link shuffle} but shuffles `list` in place. 15 | */ 16 | export const shuffleInPlace = (list: unknown[]): void => { 17 | for (let i = list.length - 1; i > 0; i--) { 18 | const j = Math.floor(Math.random() * (i + 1)); 19 | const tmp = list[j]; 20 | list[j] = list[i]; 21 | list[i] = tmp; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /sortedArray.test.ts: -------------------------------------------------------------------------------- 1 | import Sorted, { SortedArray } from "./sortedArray.ts"; 2 | import { assert, assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("SortedArray", () => { 5 | const cmp = (a: number, b: number) => a - b; 6 | 7 | assertEquals([...new Sorted(cmp, 2, 1, 3)], [1, 2, 3]); 8 | 9 | { 10 | const arr = new Sorted(cmp); 11 | arr.add(2); 12 | arr.add(1); 13 | arr.add(3); 14 | assertEquals([...arr], [1, 2, 3]); 15 | } 16 | 17 | const arr = new Sorted(cmp); 18 | 19 | arr.add(2, 1, 3); 20 | assertEquals([...arr], [1, 2, 3]); 21 | 22 | assertEquals(arr.indexOf(2), 1); 23 | 24 | assertEquals(arr instanceof Sorted, true); 25 | 26 | arr.add(5, 6, 4); 27 | assertEquals([...arr], [1, 2, 3, 4, 5, 6]); 28 | 29 | assertEquals(arr.delete(1, 3), [2, 4]); 30 | assertEquals([...arr], [1, 3, 5, 6]); 31 | assertEquals(arr.delete(3, 0, 2), [6, 1, 5]); 32 | assertEquals([...arr], [3]); 33 | assertEquals(arr.delete(0), [3]); 34 | assertEquals([...arr], []); 35 | 36 | arr.add(0, 1, 2, 3, 4); 37 | arr.delete(0, -1); 38 | assertEquals([...arr], [1, 2, 3]); 39 | 40 | // @ts-ignore 41 | delete arr[1]; 42 | assertEquals([...arr], [1, 3]); 43 | 44 | assertEquals(arr.length, 2); 45 | arr.clear(); 46 | assertEquals([...arr], []); 47 | assertEquals(arr.length, 0); 48 | 49 | arr.add(1, 2, 3); 50 | 51 | // @ts-expect-error 52 | arr.find((_value: number, _index: number, _obj: number[]) => false); 53 | arr.find((_value: number, _index: number, _obj: SortedArray) => 54 | false 55 | ); 56 | let fArgs: any[] = []; 57 | arr.find((...args) => { 58 | fArgs = args; 59 | }); 60 | assertEquals(fArgs[0], 3); 61 | assertEquals(fArgs[1], 2); 62 | assert(fArgs[2] instanceof Sorted); 63 | 64 | { 65 | const sliced = arr.slice(1); 66 | assertEquals([...sliced], [2, 3]); 67 | assert(sliced instanceof Sorted); 68 | } 69 | 70 | { 71 | const org = Sorted.from(cmp, arr); 72 | org.add(3); 73 | const filtered = org.filter((n, i, a) => n % 2 && a.indexOf(n) === i); 74 | assertEquals([...filtered], [1, 3]); 75 | assert(filtered instanceof Sorted); 76 | } 77 | 78 | const _valNum: number = arr[0]; 79 | // @ts-expect-error 80 | const _valStr: string = arr[0]; 81 | // @ts-expect-error 82 | arr[0] = 1; 83 | }); 84 | -------------------------------------------------------------------------------- /sortedArray.ts: -------------------------------------------------------------------------------- 1 | import type { FilterKeys, λ } from "./types.ts"; 2 | 3 | class SortedArrayImpl { 4 | #data: T[] = []; 5 | #cmp: Cmp; 6 | 7 | constructor(compare: Cmp, ...values: T[]) { 8 | this.#cmp = compare; 9 | if (values.length) this.#data = values.sort(compare); 10 | return wrap(this); 11 | } 12 | 13 | static from(compare: Cmp, source: Iterable): SortedArrayImpl { 14 | return new SortedArrayImpl(compare, ...source); 15 | } 16 | 17 | add(...values: T[]) { 18 | values.map(this.#addValue); 19 | } 20 | 21 | #addValue = ((value: T) => { 22 | for (let i = 0; i < this.#data.length; i++) { 23 | if (this.#cmp(value, this.#data[i]) >= 0) continue; 24 | this.#data.splice(i, 0, value); 25 | return; 26 | } 27 | this.#data.push(value); 28 | }).bind(this); 29 | 30 | delete = (...indices: number[]) => 31 | indices 32 | .map((i) => (i >= 0 ? i : this.#data.length + i)) 33 | .map((i, j, a) => { 34 | for (let k = j + 1; k < a.length; k++) if (a[k] > i) a[k]--; 35 | return this.#data.splice(i, 1)[0]; 36 | }); 37 | 38 | clear() { 39 | this.#data = []; 40 | } 41 | 42 | #ref, I extends number>( 43 | method: K, 44 | i: I, 45 | ): SetArg, I, SortedArray>> { 46 | return ((f: λ, ...rest: any[]) => 47 | (this.#data[method] as any)( 48 | (...args: any[]) => f(...args.slice(0, i), this, ...args.slice(i + 1)), 49 | ...rest, 50 | )) as any; 51 | } 52 | 53 | #bind = >(method: K) => 54 | ((...args: any[]) => (this.#data as any)[method](...args)) as T[][K]; 55 | 56 | #wrap = 57 | >(f: F) => (...args: Parameters): SortedArray => 58 | new SortedArrayImpl(this.#cmp, ...f(...(args as any))) as any; 59 | 60 | every = this.#ref("every", 2); 61 | filter = this.#wrap(this.#ref("filter", 2)); 62 | find = this.#ref("find", 2); 63 | findIndex = this.#ref("findIndex", 2); 64 | forEach = this.#ref("forEach", 2); 65 | includes = this.#bind("includes"); 66 | indexOf = this.#bind("indexOf"); 67 | join = this.#bind("join"); 68 | lastIndexOf = this.#bind("lastIndexOf"); 69 | map = this.#ref("map", 2); 70 | pop = this.#bind("pop"); 71 | reduce = this.#ref("reduce", 3); 72 | reduceRight = this.#ref("reduceRight", 3); 73 | shift = this.#bind("shift"); 74 | slice = this.#wrap(this.#bind("slice")); 75 | some = this.#ref("some", 2); 76 | 77 | at = (this.#data as any).at?.bind(this.#data) ?? 78 | ((i: number) => this.#data[i >= 0 ? i : this.#data.length + i]); 79 | 80 | get length() { 81 | return this.#data.length; 82 | } 83 | 84 | public [Symbol.iterator]() { 85 | return this.#data[Symbol.iterator](); 86 | } 87 | } 88 | 89 | const wrap = (v: T): T => 90 | new Proxy(v as any, { 91 | get(t, k) { 92 | if (k in t || typeof k !== "string") { 93 | return typeof t[k] === "function" ? t[k].bind(t) : t[k]; 94 | } 95 | for (const c of k) if (c < "0" || c > "9") return; 96 | return t.at(parseInt(k)); 97 | }, 98 | deleteProperty(t, k) { 99 | if (k in t || typeof k !== "string") return delete t[k]; 100 | for (const c of k) if (c < "0" || c > "9") return true; 101 | t.delete(parseInt(k)); 102 | return true; 103 | }, 104 | }); 105 | 106 | /** 107 | * Sorted array. Behaves much like a regular array but its elements remain 108 | * sorted using the `compare` function supplied in the constructor. 109 | * 110 | * Contains most of the methods defined on regular JavaScript arrays as long as 111 | * they don't modify the array's content in place. 112 | * 113 | * New elements are added using the `add(...values)` method. 114 | * 115 | * Elements can still be accessed using bracket notation as in plain JavaScript 116 | * arrays but can't be assigned to using bracket notation (as that could change 117 | * the element's sort position). 118 | * 119 | * Elements can be removed using the `delete(...indices)` method, which returns 120 | * an array containing the deleted values. 121 | * Deleting an element using `delete sorted[index]` will also work, but results 122 | * in a TypeScript error because element access is marked readonly. 123 | * 124 | * Array methods that pass a reference of the array to a callback (e.g. `map`, 125 | * `reduce`, `find`) will pass a reference to the SortedArray instance instead. 126 | * 127 | * The `filter` and `slice` methods will return SortedArray instances instead of 128 | * plain arrays. 129 | */ 130 | export interface SortedArray extends SortedArrayImpl { 131 | readonly [K: number]: T; 132 | } 133 | 134 | export default SortedArrayImpl as unknown as 135 | & (new ( 136 | compare: Cmp, 137 | ...value: T[] 138 | ) => SortedArray) 139 | & { from: typeof SortedArrayImpl.from }; 140 | 141 | type Cmp = (a: T, b: T) => number; 142 | 143 | type SetI = Tup< 144 | { 145 | [K in keyof T]: K extends `${I}` ? V : T[K]; 146 | } 147 | >; 148 | 149 | type Tup = T extends any[] ? T : never; 150 | 151 | type SetArg = T extends λ 152 | ? (...args: SetI, I, V>) => ReturnType 153 | : never; 154 | 155 | type FA = T extends (..._: [infer F, ...any[]]) => any ? F : never; 156 | -------------------------------------------------------------------------------- /sortedMap.test.ts: -------------------------------------------------------------------------------- 1 | import SortedMap from "./sortedMap.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("SortedMap", () => { 5 | const entries: [string, string][] = [ 6 | ["a", "foo"], 7 | ["b", "bar"], 8 | ["c", "baz"], 9 | ]; 10 | const keys = () => entries.map(([k]) => k); 11 | const values = () => entries.map(([, v]) => v); 12 | 13 | assertEquals( 14 | [ 15 | ...new SortedMap( 16 | (_va, _vb, a, b) => a.localeCompare(b), 17 | [entries[1], entries[0], entries[2]], 18 | ), 19 | ], 20 | entries, 21 | ); 22 | 23 | const map = new SortedMap((_va, _vb, a, b) => 24 | a.localeCompare(b) 25 | ); 26 | 27 | map.set(...entries[1]); 28 | map.set(...entries[0]); 29 | map.set(...entries[2]); 30 | 31 | assertEquals([...map], entries); 32 | assertEquals([...map.entries()], entries); 33 | assertEquals([...map.keys()], keys()); 34 | assertEquals([...map.values()], values()); 35 | assertEquals( 36 | map.map((v) => v), 37 | values(), 38 | ); 39 | assertEquals( 40 | map.map((_, k) => k), 41 | keys(), 42 | ); 43 | 44 | const fe: any[] = []; 45 | map.forEach((v, k) => fe.push([k, v])); 46 | assertEquals(fe, entries); 47 | 48 | map.delete("b"); 49 | entries.splice(1, 1); 50 | assertEquals([...map], entries); 51 | assertEquals([...map.entries()], entries); 52 | assertEquals([...map.keys()], keys()); 53 | assertEquals([...map.values()], values()); 54 | 55 | const nums = new SortedMap( 56 | (a, b) => a.value - b.value, 57 | ); 58 | nums.set("a", { value: 2 }); 59 | nums.set("b", { value: 1 }); 60 | nums.set("c", { value: 3 }); 61 | 62 | assertEquals([...nums.keys()], ["b", "a", "c"]); 63 | nums.set("a", { value: 0 }); 64 | assertEquals([...nums.keys()], ["a", "b", "c"]); 65 | 66 | nums.get("c")!.value = -1; 67 | assertEquals([...nums.keys()], ["a", "b", "c"]); 68 | assertEquals(nums.update("c"), true); 69 | nums.get("c")!.value = -2; 70 | assertEquals(nums.update("c"), false); 71 | assertEquals([...nums.keys()], ["c", "a", "b"]); 72 | }); 73 | -------------------------------------------------------------------------------- /sortedMap.ts: -------------------------------------------------------------------------------- 1 | import type { FilterKeys, λ } from "./types.ts"; 2 | 3 | /** 4 | * Behaves like a regular JavaScript `Map`, but its iteration order is dependant 5 | * on the `compare` function supplied in the constructor. 6 | * 7 | * Note: The item's sort position is only computed automatically on insertion. 8 | * If you update one of the values that the `compare` function depends on, you 9 | * must call the `update(key)` method afterwards to ensure the map stays sorted. 10 | */ 11 | export default class SortedMap implements Map { 12 | constructor( 13 | compare: Cmp, 14 | entries?: readonly (readonly [K, V])[] | null, 15 | ) { 16 | this.#cmp = compare; 17 | if (!entries?.length) return; 18 | const sorted = [...entries].sort(([ka, va], [kb, vb]) => 19 | compare(va, vb, ka, kb) 20 | ); 21 | for (let i = 0; i < entries.length; i++) { 22 | this.#map.set(...sorted[i]); 23 | this.#order.push(i); 24 | } 25 | } 26 | 27 | #cmp: Cmp; 28 | #map = new Map(); 29 | #order: number[] = []; 30 | 31 | #bind = , λ>>(method: T) => 32 | this.#map[method].bind(this.#map) as Map[T]; 33 | 34 | clear = this.#bind("clear"); 35 | get = this.#bind("get"); 36 | has = this.#bind("has"); 37 | 38 | set(key: K, value: V): this { 39 | if (this.#map.has(key)) this.delete(key); 40 | let i = 0; 41 | for (const [k, v] of this) { 42 | if (this.#cmp(value, v, key, k) < 0) break; 43 | i++; 44 | } 45 | this.#order.splice(i, 0, this.size); 46 | this.#map.set(key, value); 47 | return this; 48 | } 49 | 50 | delete(key: K) { 51 | if (this.#map.has(key)) { 52 | const i = [...this.#map.keys()].indexOf(key); 53 | this.#order.splice(this.#order.indexOf(i), 1); 54 | this.#order = this.#order.map((n) => (n < i ? n : n - 1)); 55 | } 56 | return this.#map.delete(key); 57 | } 58 | 59 | /** 60 | * Update the sort position of the element at `key` if necessary. 61 | * 62 | * This method should be called to notify the SortedMap that one of the 63 | * parameters that the `compare` function depends on has been updated and 64 | * consequently the sort order must be verified/updated. 65 | * 66 | * @returns `true` if the sort position of the element with `key` had to be 67 | * updated, `false` if not. 68 | */ 69 | update(key: K) { 70 | const entries = [...this.#map.entries()]; 71 | const i = entries.findIndex(([k]) => k === key); 72 | const oi = this.#order.indexOf(i); 73 | const e = entries[i]; 74 | const l = entries[this.#order[oi - 1]]; 75 | const r = entries[this.#order[oi + 1]]; 76 | 77 | if ( 78 | (!l || this.#cmp(l[1], e[1], l[0], e[0]) <= 0) && 79 | (!r || this.#cmp(e[1], r[1], e[0], r[0]) <= 0) 80 | ) { 81 | return false; 82 | } 83 | 84 | this.set(...e); 85 | return true; 86 | } 87 | 88 | forEach(callback: (value: V, key: K, map: SortedMap) => void) { 89 | for (const [k, v] of this.entries()) callback(v, k, this); 90 | } 91 | 92 | map(callback: (value: V, key: K, map: SortedMap) => T): T[] { 93 | return [...this].map(([k, v]) => callback(v, k, this)); 94 | } 95 | 96 | get size() { 97 | return this.#map.size; 98 | } 99 | 100 | #orderIter(iter: Iterable): IterableIterator { 101 | const items = [...iter]; 102 | let i = -1; 103 | return { 104 | next: () => { 105 | i++; 106 | return { 107 | done: i >= items.length, 108 | value: items[this.#order[i]], 109 | }; 110 | }, 111 | [Symbol.iterator]() { 112 | return this; 113 | }, 114 | }; 115 | } 116 | 117 | public [Symbol.iterator] = () => this.#orderIter(this.#map); 118 | entries = () => this.#orderIter(this.#map.entries()); 119 | keys = () => this.#orderIter(this.#map.keys()); 120 | values = () => this.#orderIter(this.#map.values()); 121 | 122 | public [Symbol.toStringTag] = this.#map[Symbol.toStringTag]; 123 | } 124 | 125 | type Cmp = (valueA: V, valueB: V, keyA: K, keyB: K) => number; 126 | -------------------------------------------------------------------------------- /string.ts: -------------------------------------------------------------------------------- 1 | export * from "./case.ts"; 2 | export { default as prefix } from "./prefix.ts"; 3 | export { default as suffix } from "./suffix.ts"; 4 | export { default as surround } from "./surround.ts"; 5 | -------------------------------------------------------------------------------- /suffix.test.ts: -------------------------------------------------------------------------------- 1 | import suffix from "./suffix.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("suffix", () => { 5 | const suf1: "foobar" = suffix("foo", "bar"); 6 | assertEquals(suf1, "foobar"); 7 | 8 | const suf2: "fooBar" = suffix("foo", "bar", "camel"); 9 | assertEquals(suf2, "fooBar"); 10 | 11 | const suf3: "foo_bar" = suffix("foo", "bar", "snake"); 12 | assertEquals(suf3, "foo_bar"); 13 | }); 14 | -------------------------------------------------------------------------------- /suffix.ts: -------------------------------------------------------------------------------- 1 | import type { StringCase, Suffix } from "./types.ts"; 2 | import prefix from "./prefix.ts"; 3 | 4 | /** 5 | * Returns `str` suffixed with `suffix`. Same case and type behavior as 6 | * {@link prefix}. 7 | */ 8 | const suffix = < 9 | T0 extends string, 10 | T1 extends string, 11 | C extends StringCase | void = void, 12 | >( 13 | str: T1, 14 | suffix: T0, 15 | caseMod?: C, 16 | ): Suffix => prefix(str, suffix, caseMod); 17 | 18 | export default suffix; 19 | -------------------------------------------------------------------------------- /surround.test.ts: -------------------------------------------------------------------------------- 1 | import surround from "./surround.ts"; 2 | import { assertEquals, assertThrows } from "testing/asserts.ts"; 3 | 4 | Deno.test("surround", () => { 5 | assertEquals(surround("foo", "()"), "(foo)"); 6 | assertEquals(surround("foo", "([])"), "([foo])"); 7 | assertEquals(surround("foo", ""), "foo"); 8 | assertThrows(() => surround("foo", "abc")); 9 | 10 | const _0: "[({foo})]" = surround("foo", "[({})]"); 11 | // @ts-expect-error 12 | const _1: "[{(foo)}]" = surround("foo", "[({})]"); 13 | const _2: "foo" = surround("foo", ""); 14 | }); 15 | -------------------------------------------------------------------------------- /surround.ts: -------------------------------------------------------------------------------- 1 | import { IsEvenLength, Surround } from "./types.ts"; 2 | import { assert } from "./except.ts"; 3 | 4 | /** 5 | * Surrounds the `str` with `surrounding`. `surrounding` must have an even length. 6 | * 7 | * @example 8 | * ``` 9 | * surround("foo", "()") // "(foo)" 10 | * surround("foo", "({[]})") // "({[foo]})" 11 | * ``` 12 | */ 13 | export const surround = ( 14 | str: A, 15 | surrounding: B, 16 | ): B extends "" ? A 17 | : IsEvenLength extends true ? Surround 18 | : never => { 19 | if (typeof surrounding !== "string") return str as any; 20 | assert( 21 | surrounding.length % 2 === 0, 22 | "surrounding string must have even length", 23 | ); 24 | const half = surrounding.length / 2; 25 | return `${surrounding.slice(0, half)}${str}${ 26 | surrounding.slice(half, half * 2) 27 | }` as any; 28 | }; 29 | 30 | export default surround; 31 | -------------------------------------------------------------------------------- /take.test.ts: -------------------------------------------------------------------------------- 1 | import { takeList as take } from "./take.ts"; 2 | import repeat from "./repeat.ts"; 3 | import { assertEquals } from "testing/asserts.ts"; 4 | 5 | Deno.test("take", () => { 6 | { 7 | let i = 0; 8 | const iter: IterableIterator = { 9 | next: () => ({ 10 | value: i++, 11 | }), 12 | [Symbol.iterator]() { 13 | return this; 14 | }, 15 | }; 16 | 17 | assertEquals(take(5, iter), [0, 1, 2, 3, 4]); 18 | } 19 | 20 | assertEquals(take(5, repeat(1, 2)), [1, 2, 1, 2, 1]); 21 | 22 | { 23 | let i = 0; 24 | const iter = { 25 | next: () => (++i <= 3 ? { value: i - 1 } : { done: true }), 26 | [Symbol.iterator]() { 27 | return this; 28 | }, 29 | }; 30 | 31 | const list = take(5, iter as any); 32 | assertEquals(list, [0, 1, 2]); 33 | } 34 | 35 | assertEquals(take(5, [1, 2, 3, 4, 5, 6]), [1, 2, 3, 4, 5]); 36 | assertEquals(take(5, [1, 2, 3]), [1, 2, 3]); 37 | }); 38 | -------------------------------------------------------------------------------- /take.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Takes `n` elements from the iterable `list` and returns them as an array. 3 | * 4 | * @example 5 | * ``` 6 | * take(5, repeat(1, 2)) // -> [1, 2, 1, 2, 1] 7 | * take(3, [1, 2, 3, 4]) // -> [1, 2, 3] 8 | * take(3, [1, 2]) // -> [1, 2] 9 | * ``` 10 | */ 11 | export const takeList = (n: number, list: Iterable): T[] => [ 12 | ...takeGenerator(n, list), 13 | ]; 14 | 15 | /** 16 | * Takes `n` elements from the iterable `list` and returns them as a generator. 17 | * 18 | * @example 19 | * ``` 20 | * [...take(5, repeat(1, 2))] // -> [1, 2, 1, 2, 1] 21 | * [...take(3, [1, 2, 3, 4])] // -> [1, 2, 3] 22 | * [...take(3, [1, 2])] // -> [1, 2] 23 | * ``` 24 | */ 25 | export function* takeGenerator(n: number, list: Iterable): Generator { 26 | let i = 0; 27 | for (const el of list) { 28 | if (i++ >= n) return; 29 | yield el; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /throttle.test.ts: -------------------------------------------------------------------------------- 1 | import throttle from "./throttle.ts"; 2 | import { assert, assertEquals } from "testing/asserts.ts"; 3 | 4 | const expectTimes = (times: number[], ...expected: number[]) => { 5 | assertEquals(times.length, expected.length); 6 | const margin = 32; 7 | for (let i = 0; i < times.length; i++) { 8 | assert(times[i] > expected[i] - margin); 9 | assert(times[i] < expected[i] + margin); 10 | } 11 | }; 12 | 13 | const runTest = ( 14 | interval: number, 15 | { 16 | leading = false, 17 | trailing = false, 18 | }: { 19 | leading?: boolean; 20 | trailing?: boolean; 21 | }, 22 | ...expected: number[] 23 | ) => 24 | new Promise((done) => { 25 | const t0 = performance.now(); 26 | const invocations: number[] = []; 27 | 28 | const fun = () => { 29 | invocations.push(performance.now() - t0); 30 | }; 31 | 32 | const throttled = throttle(fun, 50, { leading, trailing }); 33 | 34 | const iid = setInterval(throttled, interval); 35 | 36 | setTimeout(() => { 37 | clearInterval(iid); 38 | setTimeout(() => { 39 | try { 40 | expectTimes(invocations, ...expected); 41 | } finally { 42 | done(); 43 | } 44 | }, 100); 45 | }, 150); 46 | }); 47 | 48 | Deno.test("throttle (leading + trailing)", () => 49 | runTest(10, { leading: true, trailing: true }, 10, 60, 110, 160)); 50 | 51 | Deno.test("throttle (trailing)", () => 52 | runTest(10, { leading: false, trailing: true }, 60, 110, 160)); 53 | 54 | Deno.test("throttle (leading)", () => 55 | runTest(16, { leading: true, trailing: false }, 16, 80, 144)); 56 | -------------------------------------------------------------------------------- /throttle.ts: -------------------------------------------------------------------------------- 1 | import type { λ } from "./types.ts"; 2 | 3 | const { performance } = globalThis; 4 | export const cancel = Symbol("throttle.cancel"); 5 | 6 | /** 7 | * Create a throttled function that invokes `fun` at most every `ms` milliseconds. 8 | * 9 | * `fun` is invoked with the last arguments passed to the throttled function. 10 | * 11 | * Calling `[throttle.cancel]()` on the throttled function will cancel the currently 12 | * scheduled invocation. 13 | */ 14 | const throttle = Object.assign( 15 | (fun: λ, ms: number, { leading = true, trailing = true } = {}) => { 16 | let toId: any; 17 | let lastInvoke = -Infinity; 18 | let lastArgs: any[] | undefined; 19 | 20 | const invoke = () => { 21 | lastInvoke = performance.now(); 22 | toId = undefined; 23 | fun(...lastArgs!); 24 | }; 25 | 26 | return Object.assign( 27 | (...args: any[]) => { 28 | if (!leading && !trailing) return; 29 | lastArgs = args; 30 | const dt = performance.now() - lastInvoke; 31 | 32 | if (dt >= ms && toId === undefined && leading) invoke(); 33 | else if (toId === undefined && trailing) { 34 | toId = setTimeout(invoke, dt >= ms ? ms : ms - dt); 35 | } 36 | }, 37 | { [cancel]: () => clearTimeout(toId) }, 38 | ); 39 | }, 40 | { cancel }, 41 | ) as 42 | & (( 43 | fun: T, 44 | ms: number, 45 | opts?: { leading?: boolean; trailing: boolean }, 46 | ) => λ, void> & { [cancel](): void }) 47 | & { 48 | cancel: typeof cancel; 49 | }; 50 | 51 | export default throttle; 52 | -------------------------------------------------------------------------------- /truthy.test.ts: -------------------------------------------------------------------------------- 1 | import { falsy, truthy } from "./truthy.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("truthy & falsy", () => { 5 | type Num = 0 | 1 | 2; 6 | 7 | { 8 | const num: Num = 2 as any; 9 | 10 | assertEquals(truthy(num), true); 11 | assertEquals(falsy(num), false); 12 | 13 | if (truthy(num)) { 14 | // @ts-expect-error 15 | const _nf: 0 = num; 16 | const _nt: 1 | 2 = num; 17 | } else { 18 | const _nf: 0 = num; 19 | // @ts-expect-error 20 | const _nt: 1 | 2 = num; 21 | } 22 | 23 | if (falsy(num)) { 24 | const _nf: 0 = num; 25 | // @ts-expect-error 26 | const _nt: 1 | 2 = num; 27 | } else { 28 | // @ts-expect-error 29 | const _nf: 0 = num; 30 | const _nt: 1 | 2 = num; 31 | } 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /truthy.ts: -------------------------------------------------------------------------------- 1 | /** Checks if `value` is truthy. Literal types are narrowed accordingly. */ 2 | export const truthy = (value: T): value is PickTruthy => !!value; 3 | 4 | /** Checks if `value` is falsy. Literal types are narrowed accordingly. */ 5 | export const falsy = (value: T): value is PickFalsy => !value; 6 | 7 | type PickTruthy = 8 | | (true extends T ? true : never) 9 | | (T extends string ? Exclude : never) 10 | | (T extends number ? Exclude : never) 11 | | Exclude; 12 | 13 | type PickFalsy = 14 | | (null extends T ? null : never) 15 | | (undefined extends T ? undefined : never) 16 | | (false extends T ? false : never) 17 | | (0 extends T ? 0 : never) 18 | | ("" extends T ? "" : never); 19 | -------------------------------------------------------------------------------- /types.test.ts: -------------------------------------------------------------------------------- 1 | import { assertNotType, assertType } from "./types.ts"; 2 | import type { 3 | AND, 4 | BitAnd, 5 | BitNand, 6 | BitNor, 7 | BitNot, 8 | BitOr, 9 | BitXnor, 10 | BitXor, 11 | CamelCase, 12 | Extends, 13 | IfElse, 14 | IntRange, 15 | IsEvenLength, 16 | IsIntegerString, 17 | IsNegative, 18 | IsNumberString, 19 | IsTuple, 20 | IsUnion, 21 | Join, 22 | KebabCase, 23 | Length, 24 | LogicFalse, 25 | LogicNull, 26 | LogicTrue, 27 | NAND, 28 | NarrowList, 29 | NOR, 30 | NOT, 31 | OptionalKeys, 32 | OR, 33 | ParseInt, 34 | PascalCase, 35 | RequiredKeys, 36 | ScreamingSnakeCase, 37 | SnakeCase, 38 | SplitAt, 39 | SplitEven, 40 | SplitString, 41 | Surround, 42 | Take, 43 | TakeLast, 44 | ToString, 45 | TupleOfSize, 46 | XNOR, 47 | XOR, 48 | } from "./types.ts"; 49 | 50 | Deno.test("static type tests", () => { 51 | { 52 | const symA = Symbol("a"); 53 | const symB = Symbol("b"); 54 | type SymA = typeof symA; 55 | type SymB = typeof symB; 56 | 57 | assertType(); 58 | assertType(); 59 | assertType(); 60 | assertType<[string], [string]>(); 61 | assertType(); 62 | assertType(); 63 | assertType<1, 1>(); 64 | assertNotType<1, 2>(); 65 | assertType(); 66 | assertType<{ a?: string }, { a?: string }>(); 67 | assertType<{ a?: string }, { a?: string | undefined }>(); 68 | 69 | // @ts-expect-error 70 | assertType(); 71 | // @ts-expect-error 72 | assertType(); 73 | // @ts-expect-error 74 | assertType(); 75 | // @ts-expect-error 76 | assertType(); 77 | // @ts-expect-error 78 | assertType(); 79 | // @ts-expect-error 80 | assertType(); 81 | // @ts-expect-error 82 | assertType<[string, number], [string, number?]>(); 83 | // @ts-expect-error 84 | assertType(); 85 | // @ts-expect-error 86 | assertType(); 87 | // @ts-expect-error 88 | assertType(); 89 | // @ts-expect-error 90 | assertType(); 91 | // @ts-expect-error 92 | assertType<"foo", string>(); 93 | // @ts-expect-error 94 | assertType(); 95 | // @ts-expect-error 96 | assertType<1, number>(); 97 | // @ts-expect-error 98 | assertType<2, 3>(); 99 | // @ts-expect-error 100 | assertNotType<1, 1>(); 101 | // @ts-expect-error 102 | assertType(); 103 | // @ts-expect-error 104 | assertType(); 105 | // @ts-expect-error 106 | assertType<{ a?: string }, { a: string }>(); 107 | // @ts-expect-error 108 | assertType<{ a?: string }, { a: string | undefined }>(); 109 | } 110 | 111 | { 112 | assertType, never>(); 113 | assertType, never>(); 114 | 115 | assertType, []>(); 116 | assertType, []>(); 117 | assertType, []>(); 118 | 119 | assertType, []>(); 120 | assertType, [number]>(); 121 | assertType, [number, string]>(); 122 | assertType, [number, string]>(); 123 | 124 | assertType, []>(); 125 | assertType, [number?]>(); 126 | assertType, [number?]>(); 127 | 128 | assertType, []>(); 129 | assertType, [string]>(); 130 | assertType, [string, number?]>(); 131 | 132 | assertType, []>(); 133 | assertType, [string?]>(); 134 | assertType, [string?, number?]>(); 135 | assertType, [string?, number?]>(); 136 | 137 | assertType, []>(); 138 | assertType, []>(); 139 | assertType, []>(); 140 | assertType, []>(); 141 | assertType, []>(); 142 | 143 | type Tup15 = TupleOfSize<15, null>; 144 | assertType, [null, null, null, null]>(); 145 | assertType, Tup15>(); 146 | assertType, Tup15>(); 147 | assertType, [any, any]>(); 148 | } 149 | 150 | { 151 | type Full = [string, number, boolean, null]; 152 | 153 | assertType, []>(); 154 | assertType, [null]>(); 155 | assertType, [boolean, null]>(); 156 | assertType, [number, boolean, null]>(); 157 | assertType, [string, number, boolean, null]>(); 158 | assertType, never>(); 159 | } 160 | 161 | { 162 | type Full = [string, number, boolean, null]; 163 | 164 | assertType, [[string, number, boolean, null], []]>(); 165 | assertType, [[string, number, boolean], [null]]>(); 166 | assertType, [[string, number], [boolean, null]]>(); 167 | assertType, [[string], [number, boolean, null]]>(); 168 | assertType, [[], [string, number, boolean, null]]>(); 169 | } 170 | 171 | { 172 | type Strict = ["A" | "B", 1 | 2 | 3, number]; 173 | 174 | assertType< 175 | NarrowList, 176 | ["A" | "B", 1 | 2 | 3, 5] 177 | >(); 178 | } 179 | 180 | { 181 | assertType, "fooBar">(); 182 | assertType, "fooBar">(); 183 | assertType, "__fooBar_Baz__">(); 184 | assertType, "-_fooBar-Baz_-">(); 185 | assertType, "fooBar">(); 186 | } 187 | 188 | { 189 | assertType, "FooBar">(); 190 | } 191 | 192 | { 193 | assertType, "foo_bar">(); 194 | assertType, "foo_bar">(); 195 | assertType, "foo_bar_ABC0D">(); 196 | assertType, "foo_bar_ABC0D_foo_bar">(); 197 | assertType, "foo_bar">(); 198 | assertType, "foo_bar">(); 199 | // @ts-expect-error 200 | assertType, "">(); 201 | assertType, "foo_bar">(); 202 | assertType, "foo_bar">(); 203 | } 204 | 205 | { 206 | assertType, "FOO_BAR">(); 207 | } 208 | 209 | { 210 | assertType, "foo-bar">(); 211 | assertType, "foo-bar">(); 212 | assertType, "foo-bar-ABC0D">(); 213 | assertType, "foo-bar-ABC0D-foo-bar">(); 214 | assertType, "foo-bar">(); 215 | assertType, "foo-bar">(); 216 | // @ts-expect-error 217 | assertType, "">(); 218 | assertType, "foo-bar">(); 219 | assertType, "foo-bar">(); 220 | } 221 | 222 | { 223 | assertType, false>(); 224 | assertType, false>(); 225 | assertType, true>(); 226 | assertType, false>(); 227 | assertType, true>(); 228 | assertType, false>(); 229 | } 230 | 231 | { 232 | assertType, ["f", "o", "o", "b", "a", "r"]>(); 233 | } 234 | 235 | { 236 | assertType, 0>(); 237 | assertType, 1>(); 238 | assertType, 0 | 1>(); 239 | assertType, number>(); 240 | assertType, number>(); 241 | } 242 | 243 | { 244 | assertType, []>(); 245 | assertType, [any]>(); 246 | assertType, [number, number]>(); 247 | assertType, [string | number]>(); 248 | } 249 | 250 | { 251 | assertType, true>(); 252 | assertType, false>(); 253 | assertType, false>(); 254 | assertType, true>(); 255 | assertType, true>(); 256 | assertType, true>(); 257 | assertType, true>(); 258 | assertType, true>(); 259 | assertType, true>(); 260 | assertType, false>(); 261 | assertType, true>(); 262 | assertType, true>(); 263 | assertType, true>(); 264 | assertType, true>(); 265 | assertType, true>(); 266 | assertType, true>(); 267 | } 268 | 269 | { 270 | assertType, false>(); 271 | assertType, true>(); 272 | assertType, true>(); 273 | assertType, false>(); 274 | assertType, true>(); 275 | assertType, false>(); 276 | } 277 | 278 | { 279 | assertType, 0>(); 280 | assertType, 0 | 1 | 2 | 3 | 4>(); 281 | assertType, never>(); 282 | } 283 | 284 | { 285 | assertType, never>(); 286 | assertType, 10>(); 287 | assertType, 10>(); 288 | assertType, 10>(); 289 | assertType, never>(); 290 | assertType, never>(); 291 | assertType, never>(); 292 | assertType, 5>(); 293 | assertType, 5>(); 294 | } 295 | 296 | { 297 | assertType, never>(); 298 | assertType, false>(); 299 | assertType, true>(); 300 | assertType, false>(); 301 | assertType, true>(); 302 | } 303 | 304 | { 305 | assertType, true>(); 306 | assertType, true>(); 307 | assertType, true>(); 308 | assertType, true>(); 309 | assertType, false>(); 310 | assertType, false>(); 311 | assertType, true>(); 312 | assertType, false>(); 313 | assertType, true>(); 314 | assertType, false>(); 315 | assertType, false>(); 316 | assertType, false>(); 317 | assertType, true>(); 318 | assertType, false>(); 319 | assertType, true>(); 320 | assertType, false>(); 321 | assertType, false>(); 322 | assertType, false>(); 323 | assertType, true>(); 324 | assertType, true>(); 325 | assertType, true>(); 326 | assertType, true>(); 327 | assertType, false>(); 328 | assertType, false>(); 329 | assertType, false>(); 330 | assertType, false>(); 331 | assertType, false>(); 332 | assertType, false>(); 333 | assertType, true>(); 334 | assertType, true>(); 335 | assertType, true>(); 336 | assertType, true>(); 337 | assertType, true>(); 338 | assertType, true>(); 339 | assertType, false>(); 340 | assertType, false>(); 341 | assertType, false>(); 342 | assertType, false>(); 343 | } 344 | 345 | { 346 | type A = [string, number]; 347 | type B = [string, string | number]; 348 | type C = [null, any]; 349 | 350 | assertType, A[0] extends A[1] ? true : false>(); 351 | assertType, B[0] extends B[1] ? true : false>(); 352 | assertType, B[1] extends B[0] ? true : false>(); 353 | assertType, C[0] extends C[1] ? true : false>(); 354 | assertType, C[1] extends C[0] ? true : false>(); 355 | 356 | assertType, A extends A ? true : false>(); 357 | assertType, A extends B ? true : false>(); 358 | assertType, B extends A ? true : false>(); 359 | 360 | assertType, true>(); 361 | assertType, false>(); 362 | assertType, true>(); 363 | assertType, false>(); 364 | } 365 | 366 | { 367 | assertType, "a">(); 368 | assertType, "b">(); 369 | assertType, "a" | "b">(); 370 | assertType, "a" | "b">(); 371 | } 372 | 373 | { 374 | type _0 = false; 375 | type _1 = true; 376 | type _n = boolean; 377 | 378 | assertType, _1>(); 379 | assertType, _0>(); 380 | 381 | assertType, _1>(); 382 | assertType, _0>(); 383 | assertType, _n>(); 384 | 385 | assertType, _0>(); 386 | assertType, _0>(); 387 | assertType, _0>(); 388 | assertType, _1>(); 389 | 390 | assertType, _0>(); 391 | assertType, _0>(); 392 | assertType, _0>(); 393 | assertType, _0>(); 394 | assertType, _1>(); 395 | assertType, _n>(); 396 | assertType, _0>(); 397 | assertType, _n>(); 398 | assertType, _n>(); 399 | 400 | assertType, _0>(); 401 | assertType, _1>(); 402 | assertType, _1>(); 403 | assertType, _1>(); 404 | 405 | assertType, _0>(); 406 | assertType, _1>(); 407 | assertType, _n>(); 408 | assertType, _1>(); 409 | assertType, _1>(); 410 | assertType, _1>(); 411 | assertType, _n>(); 412 | assertType, _1>(); 413 | assertType, _n>(); 414 | 415 | assertType, _0>(); 416 | assertType, _1>(); 417 | assertType, _1>(); 418 | assertType, _0>(); 419 | 420 | assertType, _0>(); 421 | assertType, _1>(); 422 | assertType, _n>(); 423 | assertType, _1>(); 424 | assertType, _0>(); 425 | assertType, _n>(); 426 | assertType, _n>(); 427 | assertType, _n>(); 428 | assertType, _n>(); 429 | 430 | assertType, _1>(); 431 | assertType, _0>(); 432 | assertType, _0>(); 433 | assertType, _1>(); 434 | 435 | assertType, _1>(); 436 | assertType, _0>(); 437 | assertType, _n>(); 438 | assertType, _0>(); 439 | assertType, _1>(); 440 | assertType, _n>(); 441 | assertType, _n>(); 442 | assertType, _n>(); 443 | assertType, _n>(); 444 | 445 | assertType, _1>(); 446 | assertType, _0>(); 447 | assertType, _0>(); 448 | assertType, _0>(); 449 | 450 | assertType, _1>(); 451 | assertType, _0>(); 452 | assertType, _n>(); 453 | assertType, _0>(); 454 | assertType, _0>(); 455 | assertType, _0>(); 456 | assertType, _n>(); 457 | assertType, _0>(); 458 | assertType, _n>(); 459 | 460 | assertType, _1>(); 461 | assertType, _1>(); 462 | assertType, _1>(); 463 | assertType, _0>(); 464 | 465 | assertType, _1>(); 466 | assertType, _1>(); 467 | assertType, _1>(); 468 | assertType, _1>(); 469 | assertType, _0>(); 470 | assertType, _n>(); 471 | assertType, _1>(); 472 | assertType, _n>(); 473 | assertType, _n>(); 474 | } 475 | 476 | { 477 | assertType, true>(); 478 | assertType, false>(); 479 | assertType, false>(); 480 | 481 | assertType, false>(); 482 | assertType, true>(); 483 | assertType, false>(); 484 | 485 | assertType, false>(); 486 | assertType, false>(); 487 | assertType, true>(); 488 | } 489 | 490 | { 491 | type List1 = [ 492 | undefined, 493 | "a", 494 | 1, 495 | [2, [4, undefined, Record, [5, 6]], 7], 496 | "b", 497 | null, 498 | Record, 499 | [], 500 | 8, 501 | ]; 502 | 503 | assertType< 504 | Join, 505 | "a12,4,,[object Object],5,6,7b[object Object]8" 506 | >(); 507 | 508 | assertType< 509 | Join, 510 | " | a | 1 | 2,4,,[object Object],5,6,7 | b | | [object Object] | | 8" 511 | >(); 512 | 513 | assertType, "123">(); 514 | assertType, "">(); 515 | 516 | type List2 = [Map, Set, Uint16Array, DataView]; 517 | 518 | assertType< 519 | Join, 520 | `[object Map] [object Set] ${string} [object DataView]` 521 | >(); 522 | } 523 | 524 | { 525 | assertType, "foo">(); 526 | assertType, "123">(); 527 | assertType, "true">(); 528 | assertType, "false">(); 529 | assertType>, "[object Object]">(); 530 | assertType, "[object Object]">(); 531 | assertType, "">(); 532 | assertType, "1,2,3">(); 533 | assertType, "1,2,3,4,5">(); 534 | assertType>, "[object WeakMap]">(); 535 | assertType, string>(); 536 | assertType, string>(); 537 | assertType, string>(); 538 | assertType, string>(); 539 | assertType, string>(); 540 | assertType, "[object ArrayBuffer]">(); 541 | assertType, "[object SharedArrayBuffer]">(); 542 | assertType, "[object DataView]">(); 543 | assertType>, "[object Promise]">(); 544 | assertType 2>, string>(); 545 | assertType, string>(); 546 | assertType, "[object Atomics]">(); 547 | assertType, "[object Intl.Collator]">(); 548 | assertType, string>(); 549 | assertType, "foo">(); 550 | } 551 | 552 | { 553 | assertType, ["a", "b"]>(); 554 | assertType, ["ab", "cd"]>(); 555 | assertType, ["abc", "def"]>(); 556 | } 557 | 558 | { 559 | assertType, "(foo)">(); 560 | assertType, "([foo])">(); 561 | assertType, "([{foo}])">(); 562 | } 563 | 564 | { 565 | type Foo = { 566 | a?: string; 567 | b?: number; 568 | c: string; 569 | d: number; 570 | e: number | undefined; 571 | }; 572 | 573 | assertType, "c" | "d" | "e">(); 574 | assertType, "a" | "b">(); 575 | } 576 | }); 577 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | export const assertType = ( 2 | ..._TYPE: IsEqualType< 3 | A, 4 | B, 5 | [], 6 | [TYPE_ERROR: [first: Print, second: Print]] 7 | > 8 | ) => {}; 9 | 10 | export const assertNotType = ( 11 | ..._TYPE: IsEqualType 12 | ) => {}; 13 | 14 | export type IsEqualType = 15 | (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) 16 | ? ((() => T extends B ? 1 : 2) extends (() => T extends A ? 1 : 2) 17 | ? True 18 | : False) 19 | : False; 20 | 21 | export type Print = T extends λ ? λ 22 | : T extends infer I ? { [K in keyof I]: I[K] } 23 | : T; 24 | 25 | export type λ = (...args: TA) => TR; 26 | export type Fn = (...args: TA) => TR; 27 | 28 | /** @deprecated use `Fn` or `λ` instead */ 29 | export type Fun = λ; 30 | 31 | export type Primitive = string | number | boolean | symbol | null | undefined; 32 | 33 | export type FilterKeys = Extract< 34 | keyof T, 35 | keyof { 36 | [K in keyof T as T[K] extends F ? K : never]: 0; 37 | } 38 | >; 39 | 40 | export type RequiredKeys = Exclude< 41 | { [K in keyof T]: T[K] extends Required[K] ? K : never }[keyof T], 42 | undefined 43 | >; 44 | 45 | export type OptionalKeys = Exclude>; 46 | 47 | /** Any list of the `n`-first elements of `T`. */ 48 | export type PartialList = T extends [infer L, ...infer R] 49 | ? [] | [Widen] | [Widen, ...PartialList] 50 | : []; 51 | 52 | export type NarrowList = { 53 | [I in keyof TLoose]: I extends keyof TStrict 54 | ? TStrict[I] extends TLoose[I] ? TStrict[I] 55 | : TLoose[I] 56 | : TLoose[I]; 57 | }; 58 | 59 | export type Length = T extends { length: infer I } ? I : never; 60 | 61 | export type IsLength = T extends 62 | { length: infer I } ? I extends N ? true : false : false; 63 | 64 | // 65 | 66 | /** 67 | * Return a tuple containing the `N` first elements from `T`. If `T` is a tuple 68 | * its labels will be preserved unless it contains variadic elements. 69 | */ 70 | export type Take = Countable extends never 71 | ? never 72 | : Take_; 73 | 74 | /** Same as `Take` but doesn't validate that `N` is a valid length. */ 75 | export type Take_ = 76 | // generic array type 77 | IsTuple extends false ? TupleOfSize 78 | // N >= length of T 79 | : TupleOfSize extends Required<[...T, ...any[]]> ? T 80 | // contains variadic items 81 | : number extends T["length"] ? TakeFromVariadic 82 | // regular tuple 83 | : TakeFromTuple; 84 | 85 | type TakeFromTuple = T extends [] ? T 86 | : Length> extends N ? T 87 | : T extends [...infer H, any?] ? TakeFromTuple 88 | : never; 89 | 90 | type TakeFromVariadic = 91 | T extends [] ? F 92 | : Length extends N ? F 93 | : T extends [infer A, ...infer B] ? TakeFromVariadic 94 | : never; 95 | 96 | type Countable = IfElse< 97 | OnlyContains<`${T}`, "0123456789">, 98 | TrimFront<`${T}`, 0>, 99 | never 100 | >; 101 | 102 | export type TakeLast = Length extends I 103 | ? T 104 | : T extends [any, ...infer S] ? TakeLast 105 | : never; 106 | 107 | export type IsTuple = T[number][] extends Required 108 | ? false 109 | : true; 110 | 111 | export type IsUnion = T extends unknown 112 | ? [U] extends [T] ? false : true 113 | : false; 114 | 115 | export type SplitAt< 116 | Front extends any[], 117 | I extends number, 118 | End extends any[] = [], 119 | > = Length extends I ? [Front, End] 120 | : Front extends [...infer F_, infer L] ? SplitAt 121 | : never; 122 | 123 | export type Slice< 124 | T extends any[], 125 | N extends number, 126 | D extends "front" | "back" = "front", 127 | S extends any[] = [], 128 | > = Length extends N ? T 129 | : T extends ( 130 | D extends "front" ? [infer F, ...infer R] : [...infer R, infer F] 131 | ) ? Slice 132 | : T; 133 | 134 | type Widen = T extends string ? string : T extends number ? number : T; 135 | 136 | /** Inverse of `Readonly` */ 137 | export type Mutable = { -readonly [K in keyof T]: T[K] }; 138 | 139 | /** If `T` is promise then the type it resolves to, otherwise `T`. */ 140 | export type PromType = T extends PromiseLike ? I : T; 141 | 142 | export type MakeProm = Promise ? I : T>; 143 | 144 | export type Async = ( 145 | ...args: Parameters 146 | ) => MakeProm>; 147 | 148 | export type StringCase = 149 | | "camel" 150 | | "kebab" 151 | | "pascal" 152 | | "screaming-snake" 153 | | "snake"; 154 | 155 | export type Prefix< 156 | STR extends string, 157 | PRE extends string, 158 | CASE extends StringCase | void = void, 159 | > = `${PRE}${CASE extends "snake" ? "_" : ""}${CASE extends "camel" 160 | ? Capitalize 161 | : STR}`; 162 | 163 | export type Suffix< 164 | STR extends string, 165 | SUF extends string, 166 | CASE extends StringCase | void = void, 167 | > = Prefix; 168 | 169 | export type CamelCase = T extends `_${infer R}` 170 | ? `_${CamelCase}` 171 | : T extends `-${infer R}` ? `-${CamelCase}` 172 | : _Camel>; 173 | 174 | type _Camel = T extends `${infer A}${infer B}${infer C}` 175 | ? A extends "_" | "-" ? B extends "_" | "-" ? `${A}${_Camel<`${B}${C}`>}` 176 | : `${Uppercase}${_Camel}` 177 | : `${A}${_Camel<`${B}${C}`>}` 178 | : T; 179 | 180 | export type PascalCase = Capitalize>; 181 | 182 | export type SnakeCase = DelimitedCase; 183 | 184 | export type ScreamingSnakeCase = Uppercase>; 185 | 186 | export type KebabCase = DelimitedCase; 187 | 188 | type DelimitedCase = T extends 189 | `${infer A}${infer B}${infer C}${infer R}` 190 | ? Lowercase extends A 191 | ? A extends ("_" | "-") 192 | ? `${D}${Lowercase}${DelimitedCase<`${C}${R}`, D>}` 193 | : Lowercase extends B ? `${A}${DelimitedCase<`${B}${C}${R}`, D>}` 194 | : Lowercase extends C 195 | ? `${A}${D}${Lowercase}${C}${DelimitedCase}` 196 | : `${A}${D}${InvertDelimited<`${B}${C}${R}`, D>}` 197 | : `${Lowercase}${DelimitedCase<`${B}${C}${R}`, D>}` 198 | : T; 199 | 200 | type InvertDelimited = T extends 201 | `${infer A}${infer B}` 202 | ? Uppercase extends A ? `${A}${InvertDelimited}` 203 | : `${D}${DelimitedCase<`${A}${B}`, D>}` 204 | : T; 205 | 206 | export type IsEvenLength = T extends 207 | `${string}${string}${infer S}` ? (S extends "" ? true : IsEvenLength) 208 | : false; 209 | 210 | export type Surround = SplitEven extends 211 | [infer A, infer B] 212 | ? `${A extends string ? A : never}${T}${B extends string ? B : never}` 213 | : never; 214 | 215 | export type SplitString = T extends 216 | `${infer H}${infer T}` ? SplitString : A; 217 | 218 | export type SplitEven = EvenLength> extends 219 | [infer A, infer B] ? [ 220 | Join, 221 | Join, 222 | ] 223 | : never; 224 | 225 | type EvenLength = A extends [] 226 | ? (B extends [] ? [B, A] : never) 227 | : (A extends { length: infer LA } 228 | ? (B extends { length: infer LB } ? (LA extends LB ? [B, A] : ( 229 | A extends [infer H, ...infer T] ? EvenLength : never 230 | )) 231 | : never) 232 | : never); 233 | 234 | export type TupleOfSize< 235 | Length extends number, 236 | Type = any, 237 | > = TupleOfSize_; 238 | 239 | type TupleOfSize_< 240 | L extends number, 241 | T, 242 | R extends unknown[] = [], 243 | > = Length extends L ? R : TupleOfSize_; 244 | 245 | // string types 246 | 247 | type Contains = T extends `${any}${C}${any}` 248 | ? true 249 | : false; 250 | 251 | type TrimFront = T extends 252 | `${C}${infer R}` ? TrimFront 253 | : T; 254 | 255 | type Chars = T extends 256 | `${infer A}${infer B}` ? Chars : C; 257 | 258 | type OnlyContains = OnlyContains_< 259 | T, 260 | Chars<`${C}`> 261 | >; 262 | 263 | type OnlyContains_ = T extends 264 | `${C}${infer R}` ? OnlyContains_ : T extends "" ? true : false; 265 | 266 | export type IsNumberString = IsNumberString_< 267 | T extends `-${infer R}` ? R : T 268 | >; 269 | 270 | type IsNumberString_ = T extends `-${any}` ? false 271 | : T extends `${number}` ? true 272 | : false; 273 | 274 | export type IsIntegerString = IsNumberString extends false 275 | ? false 276 | : BitNot>; 277 | 278 | // math types 279 | 280 | export type IsNegative = number extends T ? never 281 | : IfElse, never, Extends<`${T}`, `-${string}`>>; 282 | 283 | /** Create a union containing the integers 0..`T` */ 284 | export type IntRange = IsNegative extends true ? never 285 | : ConstructIntRange; 286 | 287 | type ConstructIntRange< 288 | L extends number, 289 | U extends number = 0, 290 | T extends any[] = [any], 291 | > = L extends U ? U : ConstructIntRange; 292 | 293 | /** Parse `T` into number if `T` is a positive base 10 integer. */ 294 | export type ParseInt = T extends 295 | "" ? never 296 | : OnlyContains extends true 297 | ? ParseInt_, MAX, []> 298 | : never; 299 | 300 | type ParseInt_ = 301 | `${L["length"]}` extends T ? L["length"] 302 | : L["length"] extends M ? never 303 | : ParseInt_; 304 | 305 | // logic types 306 | 307 | export type NOT = boolean extends T ? T : BitNot; 308 | export type BitNot = T extends true ? false : true; 309 | 310 | export type AND = Switch<[ 311 | [BitOr, LogicFalse>, false], 312 | [BitOr, LogicNull>, boolean], 313 | true, 314 | ]>; 315 | export type BitAnd = A extends false ? false : B; 316 | 317 | export type OR = Switch<[ 318 | [BitOr, LogicTrue>, true], 319 | [BitOr, LogicNull>, boolean], 320 | false, 321 | ]>; 322 | export type BitOr = A extends true ? true : B; 323 | 324 | export type XOR = Switch<[ 325 | [BitOr, LogicNull>, boolean], 326 | BitXor, 327 | ]>; 328 | export type BitXor = A extends B ? false : true; 329 | 330 | export type XNOR = Switch<[ 331 | [BitOr, LogicNull>, boolean], 332 | BitXnor, 333 | ]>; 334 | export type BitXnor = A extends B ? true : false; 335 | 336 | export type NOR = Switch<[ 337 | [BitAnd, LogicFalse>, true], 338 | [BitOr, LogicTrue>, false], 339 | boolean, 340 | ]>; 341 | export type BitNor = A extends true ? false : BitNot; 342 | 343 | export type NAND = Switch<[ 344 | [BitOr, LogicFalse>, true], 345 | [BitOr, LogicNull>, boolean], 346 | false, 347 | ]>; 348 | export type BitNand = A extends false ? true : BitNot; 349 | 350 | export type LogicTrue = boolean extends T ? false : T; 351 | 352 | export type LogicFalse = boolean extends T ? false : BitNot; 353 | 354 | export type LogicNull = boolean extends T ? true 355 | : T extends boolean ? false 356 | : true; 357 | 358 | /** Equivalent to `A extends B ? true : false` */ 359 | export type Extends = IfElse< 360 | IsUnion, 361 | [A] extends [B] ? true : false, 362 | A extends B ? true : false 363 | >; 364 | 365 | /** Returns `A` if `T` is `true`, `B` if `false`, and `A` | `B` otherwise. */ 366 | export type IfElse = T extends true ? A : B; 367 | 368 | export type Switch = T extends [infer A, ...infer B] ? ( 369 | A extends [boolean, any] 370 | ? (A[0] extends true ? A[1] : Switch) 371 | : A 372 | ) 373 | : never; 374 | 375 | // Array.join types 376 | 377 | export type Join = 378 | Array extends [infer A, ...infer B] ? (B extends [] ? ToString 379 | : `${ToString}${Separator}${Join}`) 380 | : ""; 381 | 382 | type JoinInnerArray = Join; 383 | 384 | export type ToString = T extends undefined | null | [] ? "" 385 | : T extends PositiveInfinity ? "Infinity" 386 | : T extends NegativeInfinity ? "-Infinity" 387 | : T extends string | number | boolean ? `${T}` 388 | : T extends Map ? "[object Map]" 389 | : T extends Set ? "[object Set]" 390 | : T extends Set ? "[object Set]" 391 | : T extends unknown[] ? JoinInnerArray 392 | : T extends WeakMap ? "[object WeakMap]" 393 | : T extends WeakSet ? "[object WeakSet]" 394 | : T extends 395 | | TypedArray 396 | | bigint 397 | | symbol 398 | | ((...args: unknown[]) => unknown) 399 | | RegExp 400 | | Error 401 | | Intl.NumberFormat 402 | | typeof globalThis ? string 403 | : T extends Promise ? "[object Promise]" 404 | : T extends DataView ? "[object DataView]" 405 | : T extends SharedArrayBuffer ? "[object SharedArrayBuffer]" 406 | : T extends ArrayBuffer ? "[object ArrayBuffer]" 407 | : T extends Atomics ? "[object Atomics]" 408 | : T extends Intl.Collator ? "[object Intl.Collator]" 409 | : T extends Intl.DateTimeFormat ? "[object Intl.DateTimeFormat]" 410 | : T extends Intl.ListFormat ? "[object Intl.ListFormat]" 411 | : T extends Intl.NumberFormat ? "[object Intl.NumberFormat]" 412 | : T extends Intl.PluralRules ? "[object Intl.PluralRules]" 413 | : T extends Intl.RelativeTimeFormat ? "[object Intl.RelativeTimeFormat]" 414 | : T extends { toString(): infer R } 415 | ? (string extends R ? "[object Object]" : ReturnType) 416 | // deno-lint-ignore ban-types 417 | : T extends object ? "[object Object]" 418 | : never; 419 | 420 | export type TypedArray = 421 | | Int8Array 422 | | Uint8Array 423 | | Uint8ClampedArray 424 | | Int16Array 425 | | Uint16Array 426 | | Int32Array 427 | | Uint32Array 428 | | Float32Array 429 | | Float64Array 430 | | BigInt64Array 431 | | BigUint64Array; 432 | 433 | export type PositiveInfinity = 1e999; 434 | export type NegativeInfinity = -1e999; 435 | -------------------------------------------------------------------------------- /unary.test.ts: -------------------------------------------------------------------------------- 1 | import unary from "./unary.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("unary", async () => { 5 | // @ts-expect-error 6 | unary(() => {}); 7 | unary((_a: any) => {}); 8 | unary((_a?: any) => {}); 9 | unary((_a: any, _b?: any) => {}); 10 | // @ts-expect-error 11 | unary((_a: any, _b: any) => {}); 12 | unary((..._args: any[]) => {}); 13 | unary((_a: any, ..._args: any[]) => {}); 14 | unary((_a?: any, ..._args: any[]) => {}); 15 | unary((_a: any, _b?: any, ..._args: any[]) => {}); 16 | // @ts-expect-error 17 | unary((_a: any, _b: any, ..._args: any[]) => {}); 18 | 19 | const parseInt10 = unary(parseInt); 20 | 21 | assertEquals(parseInt10("10"), 10); 22 | // @ts-expect-error 23 | assertEquals(parseInt10("10", 16), 10); 24 | // @ts-expect-error 25 | assertEquals(parseInt10(), NaN); 26 | assertEquals(["1", "2", "3"].map(parseInt10), [1, 2, 3]); 27 | 28 | const asyncTest = async (a: number, b = 2) => 29 | await new Promise((res) => setTimeout(() => res(a * b))); 30 | const asyncUnary = unary(asyncTest); 31 | assertEquals(await asyncUnary(3), 6); 32 | }); 33 | -------------------------------------------------------------------------------- /unary.ts: -------------------------------------------------------------------------------- 1 | import { Take_, λ } from "./types.ts"; 2 | 3 | /** 4 | * Turns `fun` into a unary function (a function that only accepts one 5 | * argument). 6 | * 7 | * Note: `fun` must accept at least one argument and must not require more than 8 | * one argument. 9 | * 10 | * @example 11 | * ``` 12 | * ['1', '2', '3'].map(unary(parseInt)) // -> [1, 2, 3] 13 | * ``` 14 | */ 15 | const unary = >( 16 | fun: Parameters extends [] ? never : T, 17 | ): Unary => ((arg: unknown) => (fun as any)(arg)) as any; 18 | 19 | export default unary; 20 | 21 | type Unary> = λ, 1>, ReturnType>; 22 | -------------------------------------------------------------------------------- /unzip.test.ts: -------------------------------------------------------------------------------- 1 | import unzip, { unzipWith } from "./unzip.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("unzip", () => { 5 | { 6 | const unzipped: [number[], string[]] = unzip([1, "a"], [2, "b"], [3, "c"]); 7 | assertEquals(unzipped, [ 8 | [1, 2, 3], 9 | ["a", "b", "c"], 10 | ]); 11 | } 12 | 13 | { 14 | const unzipped = unzip([1, true, "a"], [2, false, "b"], [3, true, "c"]); 15 | assertEquals(unzipped, [ 16 | [1, 2, 3], 17 | [true, false, true], 18 | ["a", "b", "c"], 19 | ]); 20 | } 21 | 22 | // @ts-expect-error 23 | unzip([1, "a"], [2]); 24 | }); 25 | 26 | Deno.test("unzipWith", () => { 27 | const [nums, str]: [number[], string] = unzipWith( 28 | [ 29 | [1, "a"], 30 | [2, "b"], 31 | [3, "c"], 32 | ], 33 | (n, a: number[] = []) => [...a, n], 34 | (c, str = "") => str + c, 35 | ); 36 | 37 | assertEquals(nums, [1, 2, 3]); 38 | assertEquals(str, "abc"); 39 | }); 40 | -------------------------------------------------------------------------------- /unzip.ts: -------------------------------------------------------------------------------- 1 | import type { λ } from "./types.ts"; 2 | 3 | type Unzip = { [I in keyof T]: T[I][] }; 4 | 5 | /** 6 | * Reverse of {@link zip}. Takes a list of tuples and deconstructs them into 7 | * an array (of length of the tuples length) of lists each containing all the 8 | * elements in all tuples at the lists index. 9 | * 10 | * @example 11 | * const [nums, chars] = unzip([1,'a'], [2,'b'], [3,'c']) 12 | * console.log(nums) // prints: [1, 2, 3] 13 | * console.log(chars) // prints: ['a','b','c'] 14 | */ 15 | const unzip = (...zipped: [...T][]): Unzip => 16 | zipped.reduce((a, c) => c.map((v, i) => [...(a[i] ?? []), v]), [] as any); 17 | 18 | export default unzip; 19 | 20 | /** 21 | * Same as {@link unzip} but accepts an `unzipper` function for each tuple 22 | * index. The `unzipper`'s return value is used as the value in the list at 23 | * that index returned from `unzipWith`. 24 | * 25 | * The `unzipper` takes the current element as its first argument, an 26 | * accumulator as second argument (initially `undefined`) and its return value 27 | * is the accumulator passed into the next invocation. 28 | * 29 | * @example 30 | * const [nums, str] = unzipWith( 31 | * [ [1,'a'], [2,'b'], [3,'c'] ], 32 | * (n, acc: number[] = []) => [...acc, n], 33 | * (c, str = '') => str + c 34 | * ) 35 | * 36 | * console.log(nums) // prints: [1, 2, 3] 37 | * console.log(str) // prints: 'abc' 38 | */ 39 | export const unzipWith = < 40 | T extends unknown[], 41 | U extends { 42 | [I in keyof T]: λ<[cur: T[I], acc: any]>; 43 | }, 44 | >( 45 | zipped: [...T][], 46 | ...unzippers: U 47 | ): { [I in keyof U]: ReturnType } => 48 | zipped.reduce((a, c) => c.map((v, i) => unzippers[i](v, a[i])), [] as any); 49 | -------------------------------------------------------------------------------- /zip.test.ts: -------------------------------------------------------------------------------- 1 | import zip, { zipWith } from "./zip.ts"; 2 | import { assertEquals } from "testing/asserts.ts"; 3 | 4 | Deno.test("zip", () => { 5 | { 6 | const pairs: [number, string][] = zip([1, 2, 3], ["a", "b", "c"]); 7 | assertEquals(pairs, [ 8 | [1, "a"], 9 | [2, "b"], 10 | [3, "c"], 11 | ]); 12 | } 13 | 14 | assertEquals(zip([1], ["a", "b"]), [[1, "a"]]); 15 | 16 | assertEquals(zip([1, 2, 3], [true, false, true], ["a", "b", "c"]), [ 17 | [1, true, "a"], 18 | [2, false, "b"], 19 | [3, true, "c"], 20 | ]); 21 | 22 | assertEquals(zip([1, 2, 3]), [[1], [2], [3]]); 23 | }); 24 | 25 | Deno.test("zipWith", () => { 26 | const sums: number[] = zipWith((a, b) => a + b, [1, 2, 3], [4, 5, 6]); 27 | assertEquals(sums, [5, 7, 9]); 28 | 29 | // @ts-expect-error 30 | zipWith((n: string) => n, [1]); 31 | }); 32 | -------------------------------------------------------------------------------- /zip.ts: -------------------------------------------------------------------------------- 1 | export type Zip = { 2 | [I in keyof T]: T[I] extends (infer U)[] ? U : never; 3 | }[]; 4 | 5 | /** 6 | * Takes multiple lists and returns a list of tuples containing the value in 7 | * each list at the current index. If the lists are of different lengths, the 8 | * returned list of tuples has the length of the shortest passed in list. 9 | * 10 | * @example 11 | * ``` 12 | * const pairs = zip([1,2,3], ['a','b','c']) 13 | * console.log(pairs) // prints: [[1,'a'], [2,'b'], [3,'c']] 14 | * ``` 15 | */ 16 | const zip = (...lists: T): Zip => 17 | [...Array(Math.min(...lists.map(({ length }) => length)))].map((_, i) => 18 | lists.map((l) => l[i]) 19 | ) as any; 20 | 21 | export default zip; 22 | 23 | /** 24 | * Same as {@link zip} but also takes a `zipper` function, that is called for 25 | * each index with the element at current index in each list as arguments. The 26 | * result of `zipper` is the element at current index in the list returned from 27 | * `zipWith`. 28 | * 29 | * @example 30 | * ``` 31 | * const sums = zipWith((a,b) => a+b, [1,2,3], [4,5,6]) 32 | * console.log(sums) // prints: [5,7,9] 33 | * ``` 34 | */ 35 | export const zipWith = ( 36 | zipper: (...args: Zip[0]) => U, 37 | ...lists: T 38 | ): U[] => 39 | [...Array(Math.min(...lists.map(({ length }) => length)))].map((_, i) => 40 | zipper(...(lists.map((l) => l[i]) as any)) 41 | ); 42 | --------------------------------------------------------------------------------