├── .all-contributorsrc
├── .github
├── .kodiak.toml
├── renovate.json
└── workflows
│ └── push.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── benchmark.sh
├── dvx.json
├── esbuild.js
├── eslint.config.cjs
├── jest.config.js
├── package.json
├── src
├── average
│ ├── average.spec.ts
│ └── average.ts
├── benchmarkHelper.ts
├── chunk
│ ├── chunk.spec.ts
│ └── chunk.ts
├── clamp
│ ├── clamp.spec.ts
│ └── clamp.ts
├── clone
│ ├── clone.spec.ts
│ └── clone.ts
├── compact
│ ├── compact.spec.ts
│ └── compact.ts
├── debounce
│ ├── debounce.spec.ts
│ └── debounce.ts
├── duplicates
│ ├── duplicates.spec.ts
│ └── duplicates.ts
├── escapeRegExp
│ ├── escapeRegExp.spec.ts
│ └── escapeRegExp.ts
├── flatten
│ ├── flatten.spec.ts
│ └── flatten.ts
├── generateDocs.ts
├── get
│ ├── BENCHMARK.md
│ ├── get.benchmark.ts
│ ├── get.spec.ts
│ └── get.ts
├── hash
│ ├── BENCHMARK.md
│ ├── hash.benchmark.ts
│ ├── hash.spec.ts
│ └── hash.ts
├── identifier
│ ├── identifier.spec.ts
│ └── identifier.ts
├── index.spec.ts
├── index.ts
├── matchAll
│ ├── matchAll.spec.ts
│ └── matchAll.ts
├── max
│ ├── max.spec.ts
│ └── max.ts
├── memoize
│ ├── BENCHMARK.md
│ ├── memoize.benchmark.ts
│ ├── memoize.spec.ts
│ └── memoize.ts
├── min
│ ├── min.spec.ts
│ └── min.ts
├── omit
│ ├── BENCHMARK.md
│ ├── omit.benchmark.ts
│ ├── omit.spec.ts
│ └── omit.ts
├── parseModules.ts
├── percentile
│ ├── percentile.spec.ts
│ └── percentile.ts
├── pick
│ ├── BENCHMARK.md
│ ├── pick.benchmark.ts
│ ├── pick.spec.ts
│ └── pick.ts
├── promisePool
│ ├── promisePool.spec.ts
│ └── promisePool.ts
├── promiseTimeout
│ ├── promiseTimeout.spec.ts
│ └── promiseTimeout.ts
├── random
│ ├── random.spec.ts
│ └── random.ts
├── randomString
│ ├── randomString.spec.ts
│ └── randomString.ts
├── range
│ ├── range.spec.ts
│ └── range.ts
├── roundTo
│ ├── roundTo.spec.ts
│ └── roundTo.ts
├── sample
│ ├── sample.spec.ts
│ └── sample.ts
├── shuffle
│ ├── shuffle.spec.ts
│ └── shuffle.ts
├── sleep
│ ├── sleep.spec.ts
│ └── sleep.ts
├── slugify
│ ├── slugify.spec.ts
│ └── slugify.ts
├── sum
│ ├── sum.spec.ts
│ └── sum.ts
├── testHelpers.ts
├── throttle
│ ├── throttle.spec.ts
│ └── throttle.ts
├── toMap
│ ├── toMap.spec.ts
│ └── toMap.ts
├── typeHelpers.ts
├── unflatten
│ ├── unflatten.spec.ts
│ └── unflatten.ts
└── unique
│ ├── BENCHMARK.md
│ ├── unique.benchmark.ts
│ ├── unique.spec.ts
│ └── unique.ts
├── tsconfig.json
└── yarn.lock
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "imageSize": 75,
3 | "projectName": "flocky",
4 | "projectOwner": "devoxa",
5 | "repoType": "github",
6 | "repoHost": "https://github.com",
7 | "skipCi": true,
8 | "contributors": [
9 | {
10 | "login": "queicherius",
11 | "name": "David Reeß",
12 | "avatar_url": "https://avatars3.githubusercontent.com/u/4615516?v=4",
13 | "profile": "https://www.david-reess.de",
14 | "contributions": ["code", "doc", "test"]
15 | },
16 | {
17 | "login": "atjeff",
18 | "name": "Jeff Hage",
19 | "avatar_url": "https://avatars1.githubusercontent.com/u/10563763?v=4",
20 | "profile": "https://github.com/atjeff",
21 | "contributions": ["code"]
22 | },
23 | {
24 | "login": "darthmaim",
25 | "name": "darthmaim",
26 | "avatar_url": "https://avatars2.githubusercontent.com/u/2511547?v=4",
27 | "profile": "https://gw2treasures.com/",
28 | "contributions": ["code"]
29 | }
30 | ],
31 | "files": ["README.md"],
32 | "contributorsPerLine": 7
33 | }
34 |
--------------------------------------------------------------------------------
/.github/.kodiak.toml:
--------------------------------------------------------------------------------
1 | version = 1
2 |
3 | [merge]
4 | delete_branch_on_merge = true
5 | priority_merge_label = "priority-automerge"
6 |
7 | [merge.automerge_dependencies]
8 | usernames = ["renovate"]
9 | versions = ["minor", "patch"]
10 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["github>devoxa/renovate-config"]
3 | }
4 |
--------------------------------------------------------------------------------
/.github/workflows/push.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Integration
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 |
9 | concurrency:
10 | group: ${{ github.workflow }}-${{ github.ref }}
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | test-and-build:
15 | name: 'Test & build'
16 | runs-on: buildjet-4vcpu-ubuntu-2204
17 | timeout-minutes: 30
18 |
19 | steps:
20 | - name: 'Checkout the repository'
21 | uses: actions/checkout@v4
22 |
23 | - name: 'Setup Node.JS'
24 | uses: actions/setup-node@v4
25 | with:
26 | node-version: '20.19.0'
27 |
28 | - name: 'Cache dependencies'
29 | uses: buildjet/cache@v4
30 | with:
31 | path: '**/node_modules'
32 | key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}
33 |
34 | - name: 'Install dependencies'
35 | run: yarn install --frozen-lockfile
36 |
37 | - name: 'Run tests'
38 | run: yarn test --coverage
39 |
40 | - name: 'Save test coverage'
41 | uses: codecov/codecov-action@v5
42 |
43 | - name: 'Check code formatting'
44 | run: yarn format:check
45 |
46 | - name: 'Run linter'
47 | run: yarn lint
48 |
49 | - name: 'Build package'
50 | run: yarn build
51 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Output
2 | dist/
3 |
4 | # Tests
5 | coverage/
6 |
7 | # Dependencies
8 | node_modules/
9 | yarn-debug.log*
10 | yarn-error.log*
11 |
12 | # Development Environments
13 | .history/
14 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Tests
2 | coverage/
3 |
4 | # Dependencies
5 | node_modules/
6 | yarn-debug.log*
7 | yarn-error.log*
8 |
9 | # Development Environments
10 | .history/
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Devoxa Limited
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | flocky
4 |
5 |
6 |
7 |
8 | A utility library with clarity and efficiency at the core and no dependencies
9 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
20 |
21 |
25 |
26 |
27 |
28 |
32 |
33 |
34 |
35 |
36 |
37 | Installation •
38 | Usage •
39 | API Reference •
40 | Contributors •
41 | License
42 |
43 |
44 |
45 |
46 | ## Installation
47 |
48 | ```bash
49 | yarn add @devoxa/flocky
50 | ```
51 |
52 | ## Usage
53 |
54 | ```ts
55 | import { sum } from '@devoxa/flocky'
56 | sum([1, 2, 3])
57 | // -> 6
58 | ```
59 |
60 | ## API Reference
61 |
62 |
63 |
64 | ### average(array)
65 |
66 | Compute the average of the values in an array.
67 |
68 | ```js
69 | flocky.average([1, 4, 2, -4, 0])
70 | // -> 0.6
71 | ```
72 |
73 | [Source](./src/average/average.ts) • Minify: 77 B • Minify & GZIP: 79 B
74 |
75 | ### chunk(array, size)
76 |
77 | Split an array of elements into groups of `size`.
78 | If the array can't be split evenly, the final chunk will contain the remaining elements.
79 |
80 | ```js
81 | flocky.chunk([1, 2, 3, 4, 5, 6, 7], 3)
82 | // -> [[1, 2, 3], [4, 5, 6], [7]]
83 | ```
84 |
85 | [Source](./src/chunk/chunk.ts) • Minify: 105 B • Minify & GZIP: 101 B
86 |
87 | ### clamp(value, min, max)
88 |
89 | Clamps a value within a minimum and maximum range (inclusive).
90 |
91 | ```js
92 | flocky.clamp(3, 0, 5)
93 | // -> 3
94 |
95 | flocky.clamp(10, 0, 5)
96 | // -> 5
97 |
98 | flocky.clamp(-10, 0, 5)
99 | // -> 0
100 | ```
101 |
102 | [Source](./src/clamp/clamp.ts) • Minify: 69 B • Minify & GZIP: 66 B
103 |
104 | ### clone(value)
105 |
106 | Create a deep clone of `value`.
107 | This method only supports types native to JSON, so all primitive types, arrays and objects.
108 |
109 | ```js
110 | const original = [{ a: 1 }, { b: 2 }]
111 | const clone = flocky.clone(original)
112 | original[0] === clone[0]
113 | // -> false
114 | ```
115 |
116 | [Source](./src/clone/clone.ts) • Minify: 69 B • Minify & GZIP: 69 B
117 |
118 | ### compact(array)
119 |
120 | Create an array with all falsy (`undefined`, `null`, `false`, `0`, `NaN`, `''`) values removed.
121 |
122 | ```js
123 | flocky.compact([1, 2, 3, null, 4, false, 0, NaN, 5, ''])
124 | // -> [1, 2, 3, 4, 5]
125 | ```
126 |
127 | [Source](./src/compact/compact.ts) • Minify: 61 B • Minify & GZIP: 64 B
128 |
129 | ### debounce(func, wait)
130 |
131 | Create a debounced function that delays invoking `func` until `wait` milliseconds
132 | have elapsed since the last time the debounced function was invoked.
133 |
134 | ```js
135 | const func = () => console.log('Heavy processing happening')
136 | const debouncedFunc = flocky.debounce(func, 250)
137 | ```
138 |
139 | [Source](./src/debounce/debounce.ts) • Minify: 131 B • Minify & GZIP: 113 B
140 |
141 | ### duplicates(array, identity?)
142 |
143 | Create a version of an array, in which only the duplicated elements are kept.
144 | The order of result values is determined by the order they occur in the array.
145 | Can be passed an optional `identity` function to select the identifying part of objects.
146 |
147 | ```js
148 | flocky.duplicates([1, 1, 2, 4, 2, 1, 6])
149 | // -> [1, 2, 1]
150 |
151 | flocky.duplicates(['foo', 'bar', 'foo', 'foobar'])
152 | // -> ['foo']
153 |
154 | const input = [{ id: 1, a: 1 }, { id: 1, a: 2 }, { id: 2, a: 3 }, { id: 1, a: 4 }]
155 | flocky.duplicates(input, (element) => element.id)
156 | // -> [{ id: 1, a: 2 }, { id: 1, a: 4 }]
157 | ```
158 |
159 | [Source](./src/duplicates/duplicates.ts) • Minify: 277 B • Minify & GZIP: 147 B
160 |
161 | ### escapeRegExp(string)
162 |
163 | Escape special characters in a string for use in a regular expression.
164 |
165 | ```js
166 | flocky.escapeRegExp('Hey. (1 + 1 = 2)')
167 | // -> 'Hey\\. \\(1 \\+ 1 = 2\\)'
168 | ```
169 |
170 | [Source](./src/escapeRegExp/escapeRegExp.ts) • Minify: 93 B • Minify & GZIP: 90 B
171 |
172 | ### flatten(object)
173 |
174 | Flattens a nested object into an object with dot notation keys.
175 |
176 | ```js
177 | flocky.flatten({ a: { b: 1, c: 2 }, d: { e: { f: 3 } } })
178 | // -> { 'a.b': 1, 'a.c': 2, 'd.e.f': 3 }
179 | ```
180 |
181 | [Source](./src/flatten/flatten.ts) • Minify: 172 B • Minify & GZIP: 136 B
182 |
183 | ### get(object, path, defaultValue?)
184 |
185 | Get the value at a `path` of an `object` (with an optional `defaultValue`)
186 |
187 | :warning: **Using this method will ignore type information, and you will have
188 | to type the return type yourself. If you can, it is always better to access
189 | properties directly, for example with the "optional chaining" operator.**
190 |
191 | ```js
192 | const object = {a: {b: {c: 1}}}
193 | flocky.get(object, 'a.b.c')
194 | // -> 1
195 |
196 | const object = {a: {b: {c: 1}}}
197 | flocky.get(object, 'x.x.x')
198 | // -> undefined
199 |
200 | const object = {a: {b: {c: 1}}}
201 | flocky.get(object, 'x.x.x', 'default')
202 | // -> 'default'
203 | ```
204 |
205 | [Source](./src/get/get.ts) • [Benchmark](./src/get/BENCHMARK.md) • Minify: 424 B • Minify & GZIP: 266 B
206 |
207 | ### hash(data)
208 |
209 | Create a hashed string representation of the passed in data.
210 |
211 | :warning: **This function is not cryptographically secure, use
212 | [`Argon2id`, `scrypt` or `bcrypt`](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#password-hashing-algorithms)
213 | for anything security related.**
214 |
215 | ```js
216 | flocky.hash('some really long string')
217 | // -> 'x1nr7uiv'
218 |
219 | flocky.hash({id: 'AAA', name: 'BBB'})
220 | // -> 'x16mynva'
221 | ```
222 |
223 |
224 | Implementation Details
225 |
226 | This method uses Murmur3 because it is small, fast and has fairly good
227 | collision characteristics (about 1 in 36000).
228 |
229 | - https://softwareengineering.stackexchange.com/questions/49550
230 | - https://github.com/VowpalWabbit/vowpal_wabbit/wiki/murmur2-vs-murmur3
231 | - https://en.wikipedia.org/wiki/MurmurHash
232 | - https://github.com/whitequark/murmurhash3-js/blob/master/murmurhash3.js
233 |
234 |
235 | [Source](./src/hash/hash.ts) • [Benchmark](./src/hash/BENCHMARK.md) • Minify: 552 B • Minify & GZIP: 332 B
236 |
237 | ### identifier()
238 |
239 | Generate a random identifier with UUID v4 format.
240 |
241 | ```js
242 | flocky.identifier()
243 | // -> 'bfc8d57e-b9ab-4245-836e-d1fd99602e30'
244 | ```
245 |
246 | [Source](./src/identifier/identifier.ts) • Minify: 275 B • Minify & GZIP: 196 B
247 |
248 | ### matchAll(regExp, string)
249 |
250 | Find all matches of a regular expression in a string.
251 |
252 | ```js
253 | flocky.matchAll(/f(o+)/g, 'foo bar baz foooo bar')
254 | // -> [
255 | // -> { match: 'foo', subMatches: ['oo'], index: 0 },
256 | // -> { match: 'foooo', subMatches: ['oooo'], index: 12 },
257 | // -> ]
258 | ```
259 |
260 | [Source](./src/matchAll/matchAll.ts) • Minify: 178 B • Minify & GZIP: 133 B
261 |
262 | ### max(array)
263 |
264 | Compute the maximum of the values in an array.
265 |
266 | ```js
267 | flocky.max([1, 4, 2, -3, 0])
268 | // -> 4
269 | ```
270 |
271 | [Source](./src/max/max.ts) • Minify: 58 B • Minify & GZIP: 63 B
272 |
273 | ### memoize(func, options?)
274 |
275 | Create a function that memoizes the return value of `func`.
276 |
277 | ```js
278 | const func = (a, b) => a + b
279 | const memoizedFunc = flocky.memoize(func)
280 | const memoizedFuncWithTtl = flocky.memoize(func, { ttl: 30 * 1000 })
281 | memoizedFunc(1, 2)
282 | // -> 3
283 | ```
284 |
285 |
286 | Implementation Details
287 |
288 | This method's implementation is based on [fast-memoize](https://github.com/caiogondim/fast-memoize.js),
289 | with some improvements for variadic performance and additional support for a TTL based cache.
290 |
291 |
292 | [Source](./src/memoize/memoize.ts) • [Benchmark](./src/memoize/BENCHMARK.md) • Minify: 962 B • Minify & GZIP: 441 B
293 |
294 | ### min(array)
295 |
296 | Compute the minimum of the values in an array.
297 |
298 | ```js
299 | flocky.min([1, 4, 2, -3, 0])
300 | // -> -3
301 | ```
302 |
303 | [Source](./src/min/min.ts) • Minify: 58 B • Minify & GZIP: 63 B
304 |
305 | ### omit(object, keys)
306 |
307 | Create an object composed of all existing keys that are not specified in `keys`.
308 |
309 | ```js
310 | const object = { a: 1, b: 2, c: 3 }
311 | flocky.omit(object, ['a'])
312 | // -> { b: 2, c: 3 }
313 | ```
314 |
315 | [Source](./src/omit/omit.ts) • [Benchmark](./src/omit/BENCHMARK.md) • Minify: 143 B • Minify & GZIP: 129 B
316 |
317 | ### percentile(array, k)
318 |
319 | Compute the kth percentile of the values in an array.
320 |
321 | ```js
322 | flocky.percentile([90, 85, 65, 72, 82, 96, 70, 79, 68, 84], 0.9)
323 | // -> 90.6
324 | ```
325 |
326 | [Source](./src/percentile/percentile.ts) • Minify: 217 B • Minify & GZIP: 156 B
327 |
328 | ### pick(object, keys)
329 |
330 | Create an object composed of the specified `keys`.
331 |
332 | ```js
333 | const object = { a: 1, b: 2, c: 3 }
334 | flocky.pick(object, ['a', 'c'])
335 | // -> { a: 1, c: 3 }
336 | ```
337 |
338 | [Source](./src/pick/pick.ts) • [Benchmark](./src/pick/BENCHMARK.md) • Minify: 97 B • Minify & GZIP: 93 B
339 |
340 | ### promisePool(promiseFunctions, limit)
341 |
342 | Run multiple promise-returning functions in parallel with limited concurrency.
343 |
344 | ```js
345 | await flocky.promisePool([
346 | () => Promise.resolve(1),
347 | () => Promise.resolve(2),
348 | () => Promise.resolve(3),
349 | () => Promise.resolve(4),
350 | ], 2)
351 | // -> [1, 2, 3, 4]
352 | ```
353 |
354 | [Source](./src/promisePool/promisePool.ts) • Minify: 182 B • Minify & GZIP: 142 B
355 |
356 | ### promiseTimeout(promise, timeoutMs)
357 |
358 | Reject a promise if it does not resolve within `timeoutMs`.
359 |
360 | :warning: **When the timeout is hit, a promise rejection will be thrown. However,
361 | since promises are not cancellable, the execution of the promise itself will continue
362 | until it resolves or rejects.**
363 |
364 | ```js
365 | await flocky.promiseTimeout(Promise.resolve(1), 10)
366 | // -> 1
367 | ```
368 |
369 | [Source](./src/promiseTimeout/promiseTimeout.ts) • Minify: 305 B • Minify & GZIP: 181 B
370 |
371 | ### random(lower, upper, float?)
372 |
373 | Generate a random number between `lower` and `upper` (inclusive).
374 | If `float` is true or `lower` or `upper` is a float, a float is returned instead of an integer.
375 |
376 | ```js
377 | flocky.random(1, 10)
378 | // -> 8
379 |
380 | flocky.random(1, 20, true)
381 | // -> 14.94849340769861
382 |
383 | flocky.random(2.5, 3.5)
384 | // -> 3.2341312319841373
385 | ```
386 |
387 | [Source](./src/random/random.ts) • Minify: 219 B • Minify & GZIP: 128 B
388 |
389 | ### randomString(length)
390 |
391 | Generate a random alphanumeric string with `length` characters.
392 |
393 | ```js
394 | flocky.randomString(5)
395 | // -> 'tfl0g'
396 | ```
397 |
398 | [Source](./src/randomString/randomString.ts) • Minify: 230 B • Minify & GZIP: 201 B
399 |
400 | ### range(start, end, step?)
401 |
402 | Generate an array of numbers progressing from `start` up to and including `end`.
403 |
404 | ```js
405 | flocky.range(0, 5)
406 | // -> [0, 1, 2, 3, 4, 5]
407 |
408 | flocky.range(-5, -10)
409 | // -> [-5, -6, -7, -8, -9, -10]
410 |
411 | flocky.range(-6, -12, 2)
412 | // -> [-6, -8, -10, -12]
413 | ```
414 |
415 | [Source](./src/range/range.ts) • Minify: 131 B • Minify & GZIP: 111 B
416 |
417 | ### roundTo(number, precision)
418 |
419 | Round a floating point number to `precision` decimal places.
420 |
421 | ```js
422 | flocky.roundTo(3.141592653589, 4)
423 | // -> 3.1416
424 |
425 | flocky.roundTo(1.005, 2)
426 | // -> 1.01
427 |
428 | flocky.roundTo(1111.1, -2)
429 | // -> 1100
430 | ```
431 |
432 |
433 | Implementation Details
434 |
435 | This method avoids floating-point errors by adjusting the exponent part of
436 | the string representation of a number instead of multiplying and dividing
437 | with powers of 10. The implementation is based on [this example](https://stackoverflow.com/a/60098416)
438 | by Lam Wei Li.
439 |
440 |
441 | [Source](./src/roundTo/roundTo.ts) • Minify: 209 B • Minify & GZIP: 150 B
442 |
443 | ### sample(array)
444 |
445 | Get a random element from the `array`.
446 |
447 | ```js
448 | flocky.sample([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
449 | // -> 8
450 | ```
451 |
452 | [Source](./src/sample/sample.ts) • Minify: 79 B • Minify & GZIP: 79 B
453 |
454 | ### shuffle(array)
455 |
456 | Create an array of shuffled values.
457 |
458 | ```js
459 | flocky.shuffle([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
460 | // -> [3, 7, 2, 1, 10, 4, 6, 9, 5, 8]
461 | ```
462 |
463 |
464 | Implementation Details
465 |
466 | This method uses a modern version of the
467 | [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm).
468 |
469 |
470 | [Source](./src/shuffle/shuffle.ts) • Minify: 152 B • Minify & GZIP: 131 B
471 |
472 | ### sleep(ms)
473 |
474 | Return a promise that waits for `ms` milliseconds before resolving.
475 |
476 | ```js
477 | await flocky.sleep(25)
478 | ```
479 |
480 | [Source](./src/sleep/sleep.ts) • Minify: 73 B • Minify & GZIP: 78 B
481 |
482 | ### slugify(string)
483 |
484 | Generate a URL-safe slug of a string.
485 |
486 | ```js
487 | flocky.slugify(' Issue #123 is _important_! :)')
488 | // -> 'issue-123-is-important'
489 | ```
490 |
491 | [Source](./src/slugify/slugify.ts) • Minify: 114 B • Minify & GZIP: 104 B
492 |
493 | ### sum(array)
494 |
495 | Compute the sum of the values in an array.
496 |
497 | ```js
498 | flocky.sum([1, 4, 2, -4, 0])
499 | // -> 3
500 | ```
501 |
502 | [Source](./src/sum/sum.ts) • Minify: 60 B • Minify & GZIP: 67 B
503 |
504 | ### throttle(func, wait)
505 |
506 | Create a throttled function that invokes `func` at most every `wait` milliseconds.
507 | If the invocation is throttled, `func` will be invoked with the last arguments provided.
508 |
509 | ```js
510 | const func = () => console.log('Heavy processing happening')
511 | const throttledFunc = flocky.throttle(func, 250)
512 | ```
513 |
514 | [Source](./src/throttle/throttle.ts) • Minify: 209 B • Minify & GZIP: 156 B
515 |
516 | ### toMap(array, key, target?)
517 |
518 | Create a lookup map out of an `array` of objects, with a lookup `key` and an optional `target`.
519 |
520 | ```js
521 | flocky.toMap(
522 | [
523 | { id: 1, name: 'Stanley', age: 64 },
524 | { id: 2, name: 'Juliet', age: 57 },
525 | { id: 3, name: 'Alex', age: 19 }
526 | ],
527 | 'id'
528 | )
529 | // -> {
530 | // -> 1: { id: 1, name: 'Stanley', age: 64 },
531 | // -> 2: { id: 2, name: 'Juliet', age: 57 },
532 | // -> 3: { id: 3, name: 'Alex', age: 19 }
533 | // -> }
534 |
535 | flocky.toMap(
536 | [
537 | { id: 1, name: 'Stanley', age: 64 },
538 | { id: 2, name: 'Juliet', age: 57 },
539 | { id: 3, name: 'Alex', age: 19 }
540 | ],
541 | 'name',
542 | 'age'
543 | )
544 | // -> { Stanley: 64, Juliet: 57, Alex: 19 }
545 | ```
546 |
547 | [Source](./src/toMap/toMap.ts) • Minify: 95 B • Minify & GZIP: 95 B
548 |
549 | ### unflatten(object)
550 |
551 | Unflattens an object with dot notation keys into a nested object.
552 |
553 | ```js
554 | flocky.unflatten({ 'a.b': 1, 'a.c': 2, 'd.e.f': 3 })
555 | // -> { a: { b: 1, c: 2 }, d: { e: { f: 3 } } }
556 | ```
557 |
558 | [Source](./src/unflatten/unflatten.ts) • Minify: 199 B • Minify & GZIP: 145 B
559 |
560 | ### unique(array, identity?)
561 |
562 | Create a duplicate-free version of an array, in which only the first occurrence of each element is kept.
563 | The order of result values is determined by the order they occur in the array.
564 | Can be passed an optional `identity` function to select the identifying part of objects.
565 |
566 | ```js
567 | flocky.unique([1, 1, 2, 4, 2, 1, 6])
568 | // -> [1, 2, 4, 6]
569 |
570 | flocky.unique(['foo', 'bar', 'foo', 'foobar'])
571 | // -> ['foo', 'bar', 'foobar']
572 |
573 | const input = [{ id: 1, a: 1 }, { id: 1, a: 2 }, { id: 2, a: 3 }, { id: 1, a: 4 }]
574 | flocky.unique(input, (element) => element.id)
575 | // -> [{ id: 1, a: 1 }, { id: 2, a: 3 }]
576 | ```
577 |
578 | [Source](./src/unique/unique.ts) • [Benchmark](./src/unique/BENCHMARK.md) • Minify: 238 B • Minify & GZIP: 153 B
579 |
580 |
581 |
582 |
583 | ## Contributors
584 |
585 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
586 |
587 |
588 |
589 |
590 |
597 |
598 |
599 |
600 |
601 |
602 |
603 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors)
604 | specification. Contributions of any kind welcome!
605 |
606 | ## License
607 |
608 | MIT
609 |
--------------------------------------------------------------------------------
/benchmark.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -e
3 |
4 | find src/ -name "*.benchmark.ts" -type f | while read -r file; do
5 | echo ">> Running benchmark: $file"
6 | ./node_modules/.bin/ts-node --transpile-only -O '{"module":"commonjs"}' $file
7 | done
8 |
9 | echo ">> All benchmarks completed"
10 |
--------------------------------------------------------------------------------
/dvx.json:
--------------------------------------------------------------------------------
1 | {
2 | "npmDependencyRules": [
3 | // Allow dependencies for benchmark comparisons that are usually explicitly denied
4 | { "action": "allow", "name": "fast-memoize", "levels": ["dev"] },
5 | { "action": "allow", "name": "lodash", "levels": ["dev"] }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/esbuild.js:
--------------------------------------------------------------------------------
1 | const { build } = require('esbuild')
2 | const fastGlob = require('fast-glob')
3 |
4 | // Output the commonjs file
5 | build({
6 | entryPoints: ['./src/index.ts'],
7 | outfile: 'dist/index.js',
8 | format: 'cjs',
9 | bundle: true,
10 | sourcemap: true,
11 | target: 'node20.9',
12 | }).catch(() => process.exit(1))
13 |
14 | // Output the es modules files
15 | build({
16 | entryPoints: fastGlob.sync('./src/**/*.ts'),
17 | outdir: 'dist/esm',
18 | format: 'esm',
19 | sourcemap: true,
20 | target: 'chrome90',
21 | }).catch(() => process.exit(1))
22 |
--------------------------------------------------------------------------------
/eslint.config.cjs:
--------------------------------------------------------------------------------
1 | const config = require('@devoxa/eslint-config')
2 |
3 | module.exports = config({
4 | ignoreFiles: ['.gitignore'],
5 | })
6 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | transform: {
3 | '^.+\\.tsx?$': ['@swc/jest', { jsc: { transform: { react: { runtime: 'automatic' } } } }],
4 | },
5 | coverageProvider: 'v8',
6 | testEnvironment: 'jsdom',
7 | modulePathIgnorePatterns: ['/dist'],
8 | collectCoverageFrom: [
9 | '/src/**/*',
10 | '!/src/benchmarkHelper.ts',
11 | '!/src/generateDocs.ts',
12 | '!/src/parseModules.ts',
13 | '!/src/testHelpers.ts',
14 | '!/src/typeHelpers.ts',
15 | '!/src/**/*.benchmark.ts',
16 | ],
17 | coverageThreshold: { global: { branches: 100, functions: 100, lines: 100, statements: 100 } },
18 | }
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@devoxa/flocky",
3 | "description": "A utility library with clarity and efficiency at the core and no dependencies",
4 | "version": "3.1.0",
5 | "main": "dist/index.js",
6 | "module": "dist/esm/index.js",
7 | "license": "MIT",
8 | "repository": {
9 | "url": "https://github.com/devoxa/flocky"
10 | },
11 | "scripts": {
12 | "test": "jest",
13 | "benchmark": "sh benchmark.sh",
14 | "format": "prettier --ignore-path='.gitignore' --list-different --write .",
15 | "format:check": "prettier --ignore-path='.gitignore' --check .",
16 | "lint": "eslint '{src,tests}/**/*.ts'",
17 | "build": "yarn build:esbuild && yarn build:docs",
18 | "build:esbuild": "rm -rf dist/ && node esbuild.js && tsc --emitDeclarationOnly",
19 | "build:docs": "ts-node -O '{\"module\":\"commonjs\"}' src/generateDocs.ts",
20 | "preversion": "yarn build"
21 | },
22 | "prettier": "@devoxa/prettier-config",
23 | "devDependencies": {
24 | "@devoxa/eslint-config": "4.0.2",
25 | "@devoxa/prettier-config": "2.0.3",
26 | "@swc/core": "1.11.29",
27 | "@swc/jest": "0.2.38",
28 | "@types/benchmark": "2.1.5",
29 | "@types/jest": "29.5.14",
30 | "@types/lodash": "4.17.15",
31 | "@types/node": "20.9.5",
32 | "@types/pako": "2.0.3",
33 | "benchmark": "2.1.4",
34 | "esbuild": "0.25.1",
35 | "eslint": "9.24.0",
36 | "fast-glob": "3.3.3",
37 | "fast-memoize": "2.5.2",
38 | "filesize": "10.1.6",
39 | "jest": "29.7.0",
40 | "jest-environment-jsdom": "29.7.0",
41 | "lodash": "4.17.21",
42 | "pako": "2.1.0",
43 | "prettier": "3.5.3",
44 | "terser": "5.39.1",
45 | "ts-node": "10.9.2",
46 | "typescript": "5.8.2"
47 | },
48 | "publishConfig": {
49 | "access": "public"
50 | },
51 | "volta": {
52 | "node": "20.9.0"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/average/average.spec.ts:
--------------------------------------------------------------------------------
1 | import { average } from './average'
2 |
3 | describe('average', () => {
4 | test('calculates the average of an array of numbers', () => {
5 | expect(average([1, 4, 2, 0])).toEqual(1.75)
6 | expect(average([9, 4, 3, 7, 79, -60])).toEqual(7)
7 | expect(average([])).toEqual(NaN)
8 | })
9 |
10 | test('does not mutate the input', () => {
11 | const input = [1, 2, 3]
12 | average(input)
13 | expect(input).toEqual([1, 2, 3])
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/src/average/average.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### average(array)
3 | *
4 | * Compute the average of the values in an array.
5 | *
6 | * ```js
7 | * flocky.average([1, 4, 2, -4, 0])
8 | * // -> 0.6
9 | * ```
10 | */
11 |
12 | export function average(array: Array): number {
13 | return array.reduce((a, b) => a + b, 0) / array.length
14 | }
15 |
--------------------------------------------------------------------------------
/src/benchmarkHelper.ts:
--------------------------------------------------------------------------------
1 | import { Event, Suite } from 'benchmark'
2 | import fs from 'fs'
3 | import path from 'path'
4 | import { max } from './max/max'
5 | import { unique } from './unique/unique'
6 |
7 | interface BenchmarkSample {
8 | library: string
9 | input: string
10 | func: () => void
11 | }
12 |
13 | interface BenchmarkResult {
14 | library: string
15 | input: string
16 | opsPerSec: number
17 | }
18 |
19 | export class Benchmark {
20 | private readonly name: string
21 | private suite: Suite
22 | private results: Array
23 |
24 | constructor(name: string) {
25 | this.name = name
26 | this.suite = new Suite()
27 | this.results = []
28 | }
29 |
30 | add(options: BenchmarkSample): void {
31 | this.suite.add(options.library + ' | ' + options.input, options.func)
32 | }
33 |
34 | run(): void {
35 | this.suite.on('cycle', (event: Event) => this.addResult(event))
36 | this.suite.on('complete', () => this.writeResults())
37 | this.suite.run({ async: true })
38 | }
39 |
40 | // Parse the string output of BenchmarkJS because that seems easier than trying
41 | // to understand their API and doing all the calculations ourselves...
42 | addResult(event: Event): void {
43 | const eventString = String(event.target) // eslint-disable-line @typescript-eslint/no-base-to-string
44 |
45 | const [name, runtimeInfo] = eventString.split(' x ')
46 | const [library, input] = name.split(' | ')
47 |
48 | let [opsPerSec] = runtimeInfo.split(' ops/sec')
49 | opsPerSec = opsPerSec.replace(/,/g, '')
50 |
51 | this.results.push({
52 | library,
53 | input,
54 | opsPerSec: parseInt(opsPerSec, 10),
55 | })
56 | }
57 |
58 | writeResults(): void {
59 | // TODO: Add system information at the bottom
60 |
61 | const content = [
62 | `### Benchmark for \`${this.name}\``,
63 | '',
64 | `[Source for this benchmark](./${this.name}.benchmark.ts)`,
65 | '',
66 | this.generateResultTable(),
67 | '',
68 | [
69 | '',
70 | 'Generated at ' + new Date().toISOString().replace(/T.*$/, ''),
71 | ' with Node.JS ' + process.version,
72 | '',
73 | ].join(''),
74 | ].join('\n')
75 |
76 | fs.writeFileSync(path.join(__dirname, `./${this.name}/BENCHMARK.md`), content, 'utf-8')
77 | }
78 |
79 | generateResultTable(): string {
80 | const table: Array = []
81 | const inputs = unique(this.results.map((x) => x.input))
82 | const libraries = unique(this.results.map((x) => x.library))
83 |
84 | // Generate the header (libraries from left to right)
85 | table.push(`| | ${libraries.join(' | ')} |`)
86 | table.push(`| --- | ${libraries.map(() => '---').join(' | ')} |`)
87 |
88 | // Generate the table content (inputs from top to bottom)
89 | inputs.forEach((input) => {
90 | const inputResults = this.results.filter((x) => x.input === input)
91 | const bestResult = max(inputResults.map((x) => x.opsPerSec))
92 |
93 | const formattedInputResults = inputResults.map((result) => {
94 | const percent = (result.opsPerSec / bestResult) * 100
95 | const formattedOps = result.opsPerSec.toLocaleString()
96 |
97 | return result.opsPerSec === bestResult
98 | ? `**${formattedOps} ops/sec (100.00%)**`
99 | : `${formattedOps} ops/sec (${percent.toFixed(2)}%)`
100 | })
101 |
102 | table.push(`| ${input} | ${formattedInputResults.join(' | ')} |`)
103 | })
104 |
105 | return table.join('\n')
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/chunk/chunk.spec.ts:
--------------------------------------------------------------------------------
1 | import { chunk } from './chunk'
2 |
3 | describe('chunk', () => {
4 | test('splits an array of numbers into chunks', () => {
5 | const chunks = chunk([1, 2, 3, 4, 5, 6, 7], 3)
6 | expect(chunks).toEqual([[1, 2, 3], [4, 5, 6], [7]])
7 | })
8 |
9 | test('splits an array of strings into chunks', () => {
10 | expect(chunk(['1', '2', '3', '4', '5', '6', '7'], 2)).toEqual([
11 | ['1', '2'],
12 | ['3', '4'],
13 | ['5', '6'],
14 | ['7'],
15 | ])
16 | })
17 |
18 | test('splits an array of objects into chunks', () => {
19 | expect(
20 | chunk([{ a: '1' }, { a: '2' }, { a: '3' }, { a: '4' }, { a: '5' }, { a: '6' }, { a: '7' }], 4)
21 | ).toEqual([
22 | [{ a: '1' }, { a: '2' }, { a: '3' }, { a: '4' }],
23 | [{ a: '5' }, { a: '6' }, { a: '7' }],
24 | ])
25 | })
26 |
27 | test('does not mutate the input', () => {
28 | const input = [1, 2, 3]
29 | chunk(input, 1)
30 | expect(input).toEqual([1, 2, 3])
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/src/chunk/chunk.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### chunk(array, size)
3 | *
4 | * Split an array of elements into groups of `size`.
5 | * If the array can't be split evenly, the final chunk will contain the remaining elements.
6 | *
7 | * ```js
8 | * flocky.chunk([1, 2, 3, 4, 5, 6, 7], 3)
9 | * // -> [[1, 2, 3], [4, 5, 6], [7]]
10 | * ```
11 | */
12 |
13 | export function chunk(array: Array, size: number): Array> {
14 | const chunked: Array> = []
15 |
16 | for (let i = 0; i < array.length; i += size) {
17 | chunked.push(array.slice(i, i + size))
18 | }
19 |
20 | return chunked
21 | }
22 |
--------------------------------------------------------------------------------
/src/clamp/clamp.spec.ts:
--------------------------------------------------------------------------------
1 | import { clamp } from './clamp'
2 |
3 | describe('clamp', () => {
4 | test('clamps a value between a minimum and maximum range (inside range)', () => {
5 | expect(clamp(0, 0, 5)).toEqual(0)
6 | expect(clamp(1, 0, 5)).toEqual(1)
7 | expect(clamp(3, 0, 5)).toEqual(3)
8 | expect(clamp(5, 0, 5)).toEqual(5)
9 | })
10 |
11 | test('clamps a value between a minimum and maximum range (below min)', () => {
12 | expect(clamp(-0.1, 0, 5)).toEqual(0)
13 | expect(clamp(-1, 0, 5)).toEqual(0)
14 | expect(clamp(-10, 0, 5)).toEqual(0)
15 | })
16 |
17 | test('clamps a value between a minimum and maximum range (above max)', () => {
18 | expect(clamp(5.1, 0, 5)).toEqual(5)
19 | expect(clamp(6, 0, 5)).toEqual(5)
20 | expect(clamp(10, 0, 5)).toEqual(5)
21 | })
22 |
23 | test('handles very large and small numbers', () => {
24 | expect(clamp(Number.MAX_SAFE_INTEGER, 0, 100)).toEqual(100)
25 | expect(clamp(Number.MIN_SAFE_INTEGER, -100, 0)).toEqual(-100)
26 | expect(clamp(0, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER)).toEqual(0)
27 | })
28 |
29 | test('handles cases where min equals max', () => {
30 | expect(clamp(0, 5, 5)).toEqual(5)
31 | expect(clamp(10, 5, 5)).toEqual(5)
32 | expect(clamp(-10, 5, 5)).toEqual(5)
33 | })
34 |
35 | test('handles negative ranges correctly', () => {
36 | expect(clamp(-3, -5, -1)).toEqual(-3)
37 | expect(clamp(0, -5, -1)).toEqual(-1)
38 | expect(clamp(-10, -5, -1)).toEqual(-5)
39 | })
40 |
41 | test('handles decimal numbers correctly', () => {
42 | expect(clamp(1.5, 1, 2)).toEqual(1.5)
43 | expect(clamp(0.5, 1, 2)).toEqual(1)
44 | expect(clamp(2.5, 1, 2)).toEqual(2)
45 | expect(clamp(1.5, 1.1, 1.9)).toEqual(1.5)
46 | })
47 |
48 | test('handles invalid input where min is greater than max', () => {
49 | expect(clamp(3, 5, 0)).toEqual(0)
50 | expect(clamp(3, 10, 5)).toEqual(5)
51 | expect(clamp(-1, 2, -2)).toEqual(-2)
52 | })
53 | })
54 |
--------------------------------------------------------------------------------
/src/clamp/clamp.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### clamp(value, min, max)
3 | *
4 | * Clamps a value within a minimum and maximum range (inclusive).
5 | *
6 | * ```js
7 | * flocky.clamp(3, 0, 5)
8 | * // -> 3
9 | *
10 | * flocky.clamp(10, 0, 5)
11 | * // -> 5
12 | *
13 | * flocky.clamp(-10, 0, 5)
14 | * // -> 0
15 | * ```
16 | */
17 |
18 | export function clamp(value: number, min: number, max: number): number {
19 | return Math.min(Math.max(value, min), max)
20 | }
21 |
--------------------------------------------------------------------------------
/src/clone/clone.spec.ts:
--------------------------------------------------------------------------------
1 | import { clone } from './clone'
2 |
3 | describe('clone', () => {
4 | test('clones primitive types', () => {
5 | expect(clone('AAA')).toEqual('AAA')
6 | expect(clone(123)).toEqual(123)
7 | expect(clone(true)).toEqual(true)
8 | expect(clone(null)).toEqual(null)
9 |
10 | // Handles undefined
11 | const x = { a: undefined, b: null }
12 | expect(clone(x)).toEqual({ b: null })
13 | })
14 |
15 | test('deep clones objects', () => {
16 | const original = { a: { b: 1 } }
17 | const cloned = clone(original)
18 | cloned.a.b = 2
19 |
20 | expect(original).toEqual({ a: { b: 1 } })
21 | expect(cloned).toEqual({ a: { b: 2 } })
22 | })
23 |
24 | test('deep clones arrays', () => {
25 | const original = [{ a: 1 }, { b: 1 }]
26 | const cloned = clone(original)
27 | cloned[0].a = 2
28 |
29 | expect(original).toEqual([{ a: 1 }, { b: 1 }])
30 | expect(cloned).toEqual([{ a: 2 }, { b: 1 }])
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/src/clone/clone.ts:
--------------------------------------------------------------------------------
1 | import { JSONValue } from '../typeHelpers'
2 |
3 | /**
4 | * ### clone(value)
5 | *
6 | * Create a deep clone of `value`.
7 | * This method only supports types native to JSON, so all primitive types, arrays and objects.
8 | *
9 | * ```js
10 | * const original = [{ a: 1 }, { b: 2 }]
11 | * const clone = flocky.clone(original)
12 | * original[0] === clone[0]
13 | * // -> false
14 | * ```
15 | */
16 |
17 | export function clone(value: T): T {
18 | return JSON.parse(JSON.stringify(value)) as T
19 | }
20 |
--------------------------------------------------------------------------------
/src/compact/compact.spec.ts:
--------------------------------------------------------------------------------
1 | import { compact } from './compact'
2 |
3 | describe('compact', () => {
4 | test('compacts numeric arrays', () => {
5 | const original = [1, false, 2, null, 3, 0, 4, '', 5, undefined]
6 | const compacted = compact(original)
7 |
8 | expect(original).toEqual([1, false, 2, null, 3, 0, 4, '', 5, undefined])
9 | expect(compacted).toEqual([1, 2, 3, 4, 5])
10 | })
11 |
12 | test('compacts string arrays', () => {
13 | const original = ['a', false, 'b', null, 'c', 0, 'd', '', 'e', undefined]
14 | const compacted = compact(original)
15 |
16 | expect(original).toEqual(['a', false, 'b', null, 'c', 0, 'd', '', 'e', undefined])
17 | expect(compacted).toEqual(['a', 'b', 'c', 'd', 'e'])
18 | })
19 |
20 | test('compacts object arrays', () => {
21 | const original = [
22 | { a: 1 },
23 | false,
24 | { b: 1 },
25 | null,
26 | { c: 1 },
27 | 0,
28 | { d: 1 },
29 | '',
30 | { e: 1 },
31 | undefined,
32 | ]
33 | const compacted = compact(original)
34 |
35 | expect(original).toEqual([
36 | { a: 1 },
37 | false,
38 | { b: 1 },
39 | null,
40 | { c: 1 },
41 | 0,
42 | { d: 1 },
43 | '',
44 | { e: 1 },
45 | undefined,
46 | ])
47 | expect(compacted).toEqual([{ a: 1 }, { b: 1 }, { c: 1 }, { d: 1 }, { e: 1 }])
48 | })
49 |
50 | test('compacts completely falsy arrays', () => {
51 | const original = [false, null, 0, NaN, undefined, '']
52 | const compacted = compact(original)
53 |
54 | expect(original).toEqual([false, null, 0, NaN, undefined, ''])
55 | expect(compacted).toEqual([])
56 | })
57 | })
58 |
--------------------------------------------------------------------------------
/src/compact/compact.ts:
--------------------------------------------------------------------------------
1 | type Falsy = undefined | null | false | 0 | ''
2 |
3 | /**
4 | * ### compact(array)
5 | *
6 | * Create an array with all falsy (`undefined`, `null`, `false`, `0`, `NaN`, `''`) values removed.
7 | *
8 | * ```js
9 | * flocky.compact([1, 2, 3, null, 4, false, 0, NaN, 5, ''])
10 | * // -> [1, 2, 3, 4, 5]
11 | * ```
12 | */
13 |
14 | export function compact(array: Array): Array {
15 | return array.filter(Boolean) as Array
16 | }
17 |
--------------------------------------------------------------------------------
/src/debounce/debounce.spec.ts:
--------------------------------------------------------------------------------
1 | import { sleep } from '../sleep/sleep'
2 | import { debounce } from './debounce'
3 |
4 | describe('debounce', () => {
5 | test('debounces the function call', async () => {
6 | const func = jest.fn()
7 |
8 | const debouncedFunc = debounce(func, 25)
9 | debouncedFunc('a')
10 | debouncedFunc('ab')
11 | debouncedFunc('abc')
12 | debouncedFunc('abcd')
13 |
14 | expect(func.mock.calls).toEqual([])
15 |
16 | await sleep(25)
17 | debouncedFunc('abcde')
18 | await sleep(25)
19 |
20 | expect(func.mock.calls).toEqual([['abcd'], ['abcde']])
21 | })
22 |
23 | test('has the correct type', () => {
24 | const func = (a: number, b: number): number => {
25 | return a + b
26 | }
27 |
28 | const debouncedFunc = debounce(func, 250)
29 |
30 | // @ts-expect-error The first argument has to be a number
31 | debouncedFunc('a', 2)
32 |
33 | // @ts-expect-error The second argument has to be a number
34 | debouncedFunc(2, 'a')
35 |
36 | // @ts-expect-error The return value is void
37 | debouncedFunc(2, 2)?.concat()
38 |
39 | // @ts-expect-error The return value is void
40 | debouncedFunc(2, 2)?.toFixed()
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/src/debounce/debounce.ts:
--------------------------------------------------------------------------------
1 | import { TAnyFunction } from '../typeHelpers'
2 |
3 | /**
4 | * ### debounce(func, wait)
5 | *
6 | * Create a debounced function that delays invoking `func` until `wait` milliseconds
7 | * have elapsed since the last time the debounced function was invoked.
8 | *
9 | * ```js
10 | * const func = () => console.log('Heavy processing happening')
11 | * const debouncedFunc = flocky.debounce(func, 250)
12 | * ```
13 | */
14 |
15 | type FunctionWithVoidReturn> = (...args: Parameters) => void
16 |
17 | export function debounce>(
18 | func: TFunc,
19 | wait: number
20 | ): FunctionWithVoidReturn {
21 | let timeoutID: NodeJS.Timeout | null = null
22 |
23 | return function (this: unknown, ...args: Array) {
24 | if (timeoutID) clearTimeout(timeoutID)
25 | timeoutID = setTimeout(() => func.apply(this, args), wait)
26 | } as FunctionWithVoidReturn
27 | }
28 |
--------------------------------------------------------------------------------
/src/duplicates/duplicates.spec.ts:
--------------------------------------------------------------------------------
1 | import { duplicates } from './duplicates'
2 |
3 | describe('duplicates', () => {
4 | test('filters the duplicate occurrences of an array of numbers', () => {
5 | expect(duplicates([1, 4, 2, 0])).toEqual([])
6 | expect(duplicates([1, 1, 4, 2, 0, 2, 0])).toEqual([1, 2, 0])
7 | })
8 |
9 | test('filters the duplicate occurrences of an array of strings', () => {
10 | expect(duplicates(['foo', 'bar', 'foo', 'foobar', 'foo', 'foo'])).toEqual(['foo', 'foo', 'foo'])
11 | })
12 |
13 | test('filters the duplicate occurrences of an array of objects with an identity function', () => {
14 | const input = [
15 | { id: 1, name: 'Foo1' },
16 | { id: 2, name: 'Foo2' },
17 | { id: 1, name: 'Foo3' },
18 | { id: 3, name: 'Foo4' },
19 | { id: 2, name: 'Foo5' },
20 | ]
21 |
22 | const expected = [
23 | { id: 1, name: 'Foo3' },
24 | { id: 2, name: 'Foo5' },
25 | ]
26 |
27 | expect(duplicates(input, (element) => element.id)).toEqual(expected)
28 | })
29 |
30 | test('does not mutate the input', () => {
31 | const input = [1, 2, 3, 1]
32 | duplicates(input)
33 | expect(input).toEqual([1, 2, 3, 1])
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/src/duplicates/duplicates.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### duplicates(array, identity?)
3 | *
4 | * Create a version of an array, in which only the duplicated elements are kept.
5 | * The order of result values is determined by the order they occur in the array.
6 | * Can be passed an optional `identity` function to select the identifying part of objects.
7 | *
8 | * ```js
9 | * flocky.duplicates([1, 1, 2, 4, 2, 1, 6])
10 | * // -> [1, 2, 1]
11 | *
12 | * flocky.duplicates(['foo', 'bar', 'foo', 'foobar'])
13 | * // -> ['foo']
14 | *
15 | * const input = [{ id: 1, a: 1 }, { id: 1, a: 2 }, { id: 2, a: 3 }, { id: 1, a: 4 }]
16 | * flocky.duplicates(input, (element) => element.id)
17 | * // -> [{ id: 1, a: 2 }, { id: 1, a: 4 }]
18 | * ```
19 | */
20 |
21 | export function duplicates(array: Array, identity?: (x: T) => unknown): Array {
22 | if (!identity) {
23 | return primitiveDuplicates(array)
24 | }
25 |
26 | return objectDuplicates(array, identity)
27 | }
28 |
29 | function primitiveDuplicates(array: Array): Array {
30 | return array.filter((x, i, self) => self.indexOf(x) !== i)
31 | }
32 |
33 | function objectDuplicates(array: Array, identity: (x: T) => unknown): Array {
34 | const identities = array.map((x) => identity(x))
35 | return array.filter((_, i) => identities.indexOf(identities[i]) !== i)
36 | }
37 |
--------------------------------------------------------------------------------
/src/escapeRegExp/escapeRegExp.spec.ts:
--------------------------------------------------------------------------------
1 | import { escapeRegExp } from './escapeRegExp'
2 |
3 | describe('escapeRegExp', () => {
4 | test('can escape the special characters in a string', () => {
5 | expect(escapeRegExp('How much $$$ for this?')).toEqual('How much \\$\\$\\$ for this\\?')
6 | expect(escapeRegExp('\\')).toEqual('\\\\')
7 | expect(escapeRegExp('^^')).toEqual('\\^\\^')
8 | })
9 | })
10 |
--------------------------------------------------------------------------------
/src/escapeRegExp/escapeRegExp.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### escapeRegExp(string)
3 | *
4 | * Escape special characters in a string for use in a regular expression.
5 | *
6 | * ```js
7 | * flocky.escapeRegExp('Hey. (1 + 1 = 2)')
8 | * // -> 'Hey\\. \\(1 \\+ 1 = 2\\)'
9 | * ```
10 | */
11 |
12 | export function escapeRegExp(string: string): string {
13 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
14 | }
15 |
--------------------------------------------------------------------------------
/src/flatten/flatten.spec.ts:
--------------------------------------------------------------------------------
1 | import { flatten } from './flatten'
2 |
3 | describe('flatten', () => {
4 | test('flattens the object', () => {
5 | expect(flatten({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 })
6 |
7 | expect(flatten({ a: { b: 1 } })).toEqual({ 'a.b': 1 })
8 | expect(flatten({ a: { b: 1, c: 2 } })).toEqual({ 'a.b': 1, 'a.c': 2 })
9 | expect(flatten({ a: { b: 1, c: 2 }, d: { e: 3 } })).toEqual({ 'a.b': 1, 'a.c': 2, 'd.e': 3 })
10 |
11 | expect(flatten({ a: { b: { c: 1 } } })).toEqual({ 'a.b.c': 1 })
12 | expect(flatten({ a: { b: { c: 1, d: 2 } } })).toEqual({ 'a.b.c': 1, 'a.b.d': 2 })
13 | expect(flatten({ a: { b: { c: 1, d: 2 } }, e: { f: { g: 3 } } })).toEqual({
14 | 'a.b.c': 1,
15 | 'a.b.d': 2,
16 | 'e.f.g': 3,
17 | })
18 |
19 | // Check that the types are correct
20 | const result = flatten({ a: { b: { c: 1, d: 2 } }, e: { f: { g: 3 } } })
21 | expect(result['a.b.c'] + 1).toEqual(2)
22 | expect(result['a.b.d'] + 1).toEqual(3)
23 | expect(result['e.f.g'] + 1).toEqual(4)
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/src/flatten/flatten.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### flatten(object)
3 | *
4 | * Flattens a nested object into an object with dot notation keys.
5 | *
6 | * ```js
7 | * flocky.flatten({ a: { b: 1, c: 2 }, d: { e: { f: 3 } } })
8 | * // -> { 'a.b': 1, 'a.c': 2, 'd.e.f': 3 }
9 | * ```
10 | */
11 |
12 | type Entry = { key: string; value: unknown; optional: boolean }
13 |
14 | type Explode = _Explode ? { '0': T[number] } : T>
15 |
16 | type _Explode = T extends object
17 | ? {
18 | [K in keyof T]-?: K extends string
19 | ? Explode extends infer E
20 | ? E extends Entry
21 | ? {
22 | key: `${K}${E['key'] extends '' ? '' : '.'}${E['key']}`
23 | value: E['value']
24 | optional: E['key'] extends ''
25 | ? object extends Pick
26 | ? true
27 | : false
28 | : E['optional']
29 | }
30 | : never
31 | : never
32 | : never
33 | }[keyof T]
34 | : { key: ''; value: T; optional: false }
35 |
36 | type Collapse = {
37 | [E in Extract as E['key']]: E['value']
38 | } & Partial<{ [E in Extract as E['key']]: E['value'] }> extends infer O
39 | ? { [K in keyof O]: O[K] }
40 | : never
41 |
42 | type FlattenObject = Collapse>
43 |
44 | export function flatten>(
45 | object: TObject
46 | ): FlattenObject {
47 | const result: Record = {}
48 |
49 | function recurse(current: Record, prefix = ''): void {
50 | for (const key in current) {
51 | const value = current[key]
52 | const nextKey = prefix ? `${prefix}.${key}` : key
53 |
54 | if (typeof value === 'object' && value !== null) {
55 | recurse(value as Record, nextKey)
56 | } else {
57 | result[nextKey] = value
58 | }
59 | }
60 | }
61 |
62 | recurse(object)
63 | return result as FlattenObject
64 | }
65 |
--------------------------------------------------------------------------------
/src/generateDocs.ts:
--------------------------------------------------------------------------------
1 | import { filesize as fileSize } from 'filesize'
2 | import fs from 'fs'
3 | import pako from 'pako'
4 | import path from 'path'
5 | import * as terser from 'terser'
6 | import { parseModules } from './parseModules'
7 |
8 | const START_TOKEN = ''
9 | const END_TOKEN = ''
10 |
11 | run().catch((err) => console.error(err))
12 |
13 | async function run(): Promise {
14 | // Parse the module documentations
15 | let modules = parseModules().sort((a, b) => a.name.localeCompare(b.name))
16 |
17 | // Generate the subtext for each module (source link, benchmark link, module size)
18 | modules = await Promise.all(
19 | modules.map(async (module) => {
20 | const subText: Array = []
21 |
22 | // Link to the source
23 | subText.push(`[Source](./src/${module.name}/${module.name}.ts)`)
24 |
25 | // Link to the benchmark
26 | const benchmarkPath = path.join(__dirname, `./${module.name}/BENCHMARK.md`)
27 | const hasBenchmark = fs.existsSync(benchmarkPath)
28 | if (hasBenchmark) {
29 | subText.push(`[Benchmark](./src/${module.name}/BENCHMARK.md)`)
30 | }
31 |
32 | // Calculate the module
33 | const sizes = await calculateModuleSizes(module.name)
34 | subText.push(`Minify: ${sizes.minSize}`)
35 | subText.push(`Minify & GZIP: ${sizes.minZipSize}`)
36 |
37 | // Append to the docs for this module
38 | module.docs += `\n${subText.join(' • ')}\n`
39 | return module
40 | })
41 | )
42 |
43 | // Replace the documentation for the individual modules in the readme
44 | let README = fs.readFileSync('./README.md', 'utf-8')
45 | README = README.replace(
46 | new RegExp(`${START_TOKEN}.*${END_TOKEN}`, 's'),
47 | [START_TOKEN, modules.map((x) => x.docs).join('\n'), END_TOKEN].join('\n')
48 | )
49 |
50 | fs.writeFileSync('./README.md', README, 'utf-8')
51 | }
52 |
53 | async function calculateModuleSizes(
54 | name: string
55 | ): Promise<{ size: string; minSize: string; minZipSize: string }> {
56 | const content = fs.readFileSync(path.join(__dirname, `../dist/esm/${name}/${name}.js`), 'utf-8')
57 |
58 | const contentMin = (await terser.minify(content)).code || ''
59 | const contentMinZip = pako.deflate(contentMin)
60 |
61 | return {
62 | size: fileSize(content.length),
63 | minSize: fileSize(contentMin.length),
64 | minZipSize: fileSize(contentMinZip.length),
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/get/BENCHMARK.md:
--------------------------------------------------------------------------------
1 | ### Benchmark for `get`
2 |
3 | [Source for this benchmark](./get.benchmark.ts)
4 |
5 | | | lodash | flocky |
6 | | ----------- | -------------------------- | ------------------------------- |
7 | | array path | 1,090,098 ops/sec (94.41%) | **1,154,597 ops/sec (100.00%)** |
8 | | string path | 384,031 ops/sec (42.10%) | **912,167 ops/sec (100.00%)** |
9 |
10 | Generated at 2024-07-20 with Node.JS v20.9.0
11 |
--------------------------------------------------------------------------------
/src/get/get.benchmark.ts:
--------------------------------------------------------------------------------
1 | import lodash from 'lodash'
2 | import { Benchmark } from '../benchmarkHelper'
3 | import { get } from './get'
4 |
5 | const OBJECT = { foo: { bar: { herp: 123 } } }
6 |
7 | const benchmark = new Benchmark('get')
8 |
9 | benchmark.add({
10 | library: 'lodash',
11 | input: 'array path',
12 | func: (): unknown => lodash.get(OBJECT, ['foo', 'bar', 'herp' + Math.random()]),
13 | })
14 |
15 | benchmark.add({
16 | library: 'lodash',
17 | input: 'string path',
18 | func: (): unknown => lodash.get(OBJECT, 'foo.bar.herp' + Math.random()),
19 | })
20 |
21 | benchmark.add({
22 | library: 'flocky',
23 | input: 'array path',
24 | func: () => get(OBJECT, ['foo', 'bar', 'herp' + Math.random()]),
25 | })
26 |
27 | benchmark.add({
28 | library: 'flocky',
29 | input: 'string path',
30 | func: () => get(OBJECT, 'foo.bar.herp' + Math.random()),
31 | })
32 |
33 | benchmark.run()
34 |
--------------------------------------------------------------------------------
/src/get/get.spec.ts:
--------------------------------------------------------------------------------
1 | import { get } from './get'
2 |
3 | describe('get', () => {
4 | test('should work with the path of an object', () => {
5 | const object = { foo: { bar: { herp: 123, derp: 0 } } }
6 | expect(get(object, 'foo.bar.herp')).toEqual(123)
7 | expect(get(object, ['foo', 'bar', 'herp'])).toEqual(123)
8 | expect(get(object, 'foo.bar')).toEqual({ herp: 123, derp: 0 })
9 | expect(get(object, 'foo[bar]')).toEqual({ herp: 123, derp: 0 })
10 | expect(get(object, 'foo[bar[herp]]')).toEqual(123)
11 | expect(get(object, 'foo.bar.derp')).toEqual(0)
12 |
13 | // Does not mutate input
14 | expect(object).toEqual({ foo: { bar: { herp: 123, derp: 0 } } })
15 | })
16 |
17 | test('should return `undefined` if the path of an object does not exist', () => {
18 | const object = { foo: { bar: { herp: 123 } } }
19 | expect(get(object, 'foo.sup.flerp')).toEqual(undefined)
20 | expect(get(object, 'foo.sup.flerp.derp.merp')).toEqual(undefined)
21 |
22 | // Does not mutate input
23 | expect(object).toEqual({ foo: { bar: { herp: 123 } } })
24 | })
25 |
26 | test('should return `undefined` if part of an path of an object is undefined', () => {
27 | const object = { foo: null }
28 | expect(get(object, 'foo')).toEqual(null)
29 | expect(get(object, 'foo.bar')).toEqual(undefined)
30 | expect(get(object, 'bar')).toEqual(undefined)
31 |
32 | // Does not mutate input
33 | expect(object).toEqual({ foo: null })
34 |
35 | const object2 = { foo: false }
36 | expect(get(object2, 'foo')).toEqual(false)
37 | expect(get(object2, 'foo.bar')).toEqual(undefined)
38 | expect(get(object2, 'bar')).toEqual(undefined)
39 |
40 | // Does not mutate input
41 | expect(object2).toEqual({ foo: false })
42 | })
43 |
44 | test('should return `undefined` if part of an path of an object is not an object', () => {
45 | const object = { foo: 'herp?' }
46 | expect(get(object, 'foo')).toEqual('herp?')
47 | expect(get(object, 'foo.bar')).toEqual(undefined)
48 |
49 | // Does not mutate input
50 | expect(object).toEqual({ foo: 'herp?' })
51 | })
52 |
53 | test('should return the default if the path of an object does not exist', () => {
54 | const object = { foo: { bar: { herp: 123 } } }
55 | expect(get(object, 'foo.sup.flerp', 'the default')).toEqual('the default')
56 |
57 | const object2 = { foo: { bar: { herp: null } } }
58 | expect(get(object2, 'foo.bar.herp.derp', 'default')).toEqual('default')
59 | expect(get(object2, 'foo.bar.herp', null)).toEqual(null)
60 |
61 | // Does not mutate input
62 | expect(object).toEqual({ foo: { bar: { herp: 123 } } })
63 | })
64 |
65 | test('should work with the path of array elements', () => {
66 | const object = { foo: { bar: ['hi', { herp: 123 }] } }
67 | expect(get(object, 'foo.bar[1].herp')).toEqual(123)
68 | expect(get(object, 'foo.bar.[1].herp')).toEqual(123)
69 | expect(get(object, ['foo', 'bar', '1', 'herp'])).toEqual(123)
70 | expect(get(object, ['foo', 'bar', 1, 'herp'])).toEqual(123)
71 | expect(get(object, 'foo.bar.1.herp')).toEqual(123)
72 |
73 | // Does not mutate input
74 | expect(object).toEqual({ foo: { bar: ['hi', { herp: 123 }] } })
75 | })
76 |
77 | test('should work at the start of a path of array elements', () => {
78 | const object = [{ foo: { bar: ['hi', { herp: 123 }] } }]
79 | expect(get(object, '[0].foo.bar[1].herp')).toEqual(123)
80 | expect(get(object, '[0].foo.bar.1.herp')).toEqual(123)
81 | expect(get(object, '0.foo.bar[1].herp')).toEqual(123)
82 | expect(get(object, '0.foo.bar.1.herp')).toEqual(123)
83 |
84 | // Does not mutate input
85 | expect(object).toEqual([{ foo: { bar: ['hi', { herp: 123 }] } }])
86 | })
87 |
88 | test('should return `undefined` if the path of array elements does not exist', () => {
89 | const object = { foo: { bar: [{ herp: 123 }] } }
90 | expect(get(object, 'foo.bar[1].herp')).toEqual(undefined)
91 | expect(get(object, 'foo.bar.1.herp')).toEqual(undefined)
92 | expect(get(object, 'foo.sup[0].herp')).toEqual(undefined)
93 | expect(get(object, 'foo.sup.0.herp')).toEqual(undefined)
94 | expect(get(object, 'foo.bar[0].flerp')).toEqual(undefined)
95 | expect(get(object, 'foo.bar.0.flerp')).toEqual(undefined)
96 |
97 | // Does not mutate input
98 | expect(object).toEqual({ foo: { bar: [{ herp: 123 }] } })
99 | })
100 |
101 | test('should return the default if the path of array elements does not exist', () => {
102 | const object = { foo: { bar: [{ herp: 123 }] } }
103 | expect(get(object, 'foo.bar[1].herp', 'the default')).toEqual('the default')
104 | expect(get(object, 'foo.bar.1.herp', 'the default')).toEqual('the default')
105 | expect(get(object, 'foo.sup[0].herp', 'the default')).toEqual('the default')
106 | expect(get(object, 'foo.sup.0.herp', 'the default')).toEqual('the default')
107 | expect(get(object, 'foo.bar[0].flerp', 'default')).toEqual('default')
108 | expect(get(object, 'foo.bar.0.flerp', 'the default')).toEqual('the default')
109 | expect(object).toEqual({ foo: { bar: [{ herp: 123 }] } })
110 | })
111 |
112 | test('should return `undefined` if object does not exist', () => {
113 | expect(get(null, 'foo.bar')).toEqual(undefined)
114 | expect(get(undefined, 'foo.bar')).toEqual(undefined)
115 | })
116 |
117 | test('should return `undefined` if object is not an object', () => {
118 | // @ts-expect-error Ignore the type check for the first argument and test this,
119 | // because this misuse can happen very easily in JavaScript land
120 | expect(get('wat', 'foo.bar')).toEqual(undefined)
121 | })
122 |
123 | test('should return `undefined` if the path is malformed', () => {
124 | const object = { foo: { bar: [{ herp: 123 }] } }
125 | expect(get(object, 'foo..')).toEqual(undefined)
126 | expect(get(object, 'foo.[].bar')).toEqual(undefined)
127 | expect(get(object, 'foo..bar')).toEqual(undefined)
128 | expect(get(object, '.foo.bar')).toEqual(undefined)
129 | expect(get(object, 'foo......[2][1][0].bar')).toEqual(undefined)
130 | expect(get(object, 'foo......[[1][0].bar')).toEqual(undefined)
131 | expect(get(object, 'foo......[[1][0].bar')).toEqual(undefined)
132 | })
133 | })
134 |
--------------------------------------------------------------------------------
/src/get/get.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### get(object, path, defaultValue?)
3 | *
4 | * Get the value at a `path` of an `object` (with an optional `defaultValue`)
5 | *
6 | * :warning: **Using this method will ignore type information, and you will have
7 | * to type the return type yourself. If you can, it is always better to access
8 | * properties directly, for example with the "optional chaining" operator.**
9 | *
10 | * ```js
11 | * const object = {a: {b: {c: 1}}}
12 | * flocky.get(object, 'a.b.c')
13 | * // -> 1
14 | *
15 | * const object = {a: {b: {c: 1}}}
16 | * flocky.get(object, 'x.x.x')
17 | * // -> undefined
18 | *
19 | * const object = {a: {b: {c: 1}}}
20 | * flocky.get(object, 'x.x.x', 'default')
21 | * // -> 'default'
22 | * ```
23 | */
24 |
25 | const REPLACE_EMPTY_REGEX = /]|^\[/g
26 | const REPLACE_DOT_REGEX = /\.?\[/g
27 |
28 | export function get(
29 | object: object | null | undefined,
30 | path: string | Array,
31 | defaultValue?: unknown
32 | ): unknown {
33 | // Handle the case that the object is undefined or not an object
34 | if (!object || Object(object) !== object) {
35 | return defaultValue
36 | }
37 |
38 | // A) If the path is an array, we can just use that
39 | // B) If the path is a string, convert it into an array by migrating
40 | // array-style `[foo]` accessors into object-style `.foo` accessors
41 | const arrayPath = Array.isArray(path) ? path : parsePath(path)
42 | const result = getWithArrayPath(object, arrayPath)
43 | return result !== undefined ? result : defaultValue
44 | }
45 |
46 | function parsePath(path: string): Array {
47 | return path.replace(REPLACE_EMPTY_REGEX, '').replace(REPLACE_DOT_REGEX, '.').split('.')
48 | }
49 |
50 | function getWithArrayPath(object: object, path: Array): unknown {
51 | const length = path.length
52 | let index = 0
53 | let current = object as Record | null
54 |
55 | while (current != null && index < length) {
56 | current = current[path[index++]] as Record | null
57 | }
58 |
59 | return index === length ? current : undefined
60 | }
61 |
--------------------------------------------------------------------------------
/src/hash/BENCHMARK.md:
--------------------------------------------------------------------------------
1 | ### Benchmark for `hash`
2 |
3 | [Source for this benchmark](./hash.benchmark.ts)
4 |
5 | | | flocky |
6 | | ---------- | ----------------------------- |
7 | | 1KB String | **160,315 ops/sec (100.00%)** |
8 | | 1MB String | **172 ops/sec (100.00%)** |
9 |
10 | Generated at 2024-07-20 with Node.JS v20.9.0
11 |
--------------------------------------------------------------------------------
/src/hash/hash.benchmark.ts:
--------------------------------------------------------------------------------
1 | import { Benchmark } from '../benchmarkHelper'
2 | import { hash } from './hash'
3 |
4 | function makeString(length: number): string {
5 | let text = ''
6 | for (let i = 0; i < length; i++) {
7 | text += 'A'
8 | }
9 | return text
10 | }
11 |
12 | const ONE_KB_STRING = makeString(1024)
13 | const ONE_MB_STRING = makeString(1024 * 1024)
14 |
15 | const benchmark = new Benchmark('hash')
16 |
17 | benchmark.add({
18 | library: 'flocky',
19 | input: '1KB String',
20 | func: () => hash(ONE_KB_STRING),
21 | })
22 |
23 | benchmark.add({
24 | library: 'flocky',
25 | input: '1MB String',
26 | func: () => hash(ONE_MB_STRING),
27 | })
28 |
29 | benchmark.run()
30 |
--------------------------------------------------------------------------------
/src/hash/hash.spec.ts:
--------------------------------------------------------------------------------
1 | import { hash } from './hash'
2 |
3 | describe('hash', () => {
4 | test('consistently hashes the input', () => {
5 | expect(hash('Never gonna give you up')).toEqual('x1bvo9jc')
6 | expect(hash('Never gonna give you up')).toEqual('x1bvo9jc')
7 | expect(hash('Never gonna give you up')).toEqual('x1bvo9jc')
8 | })
9 |
10 | test('can hash any input type', () => {
11 | expect(hash('string')).toEqual('xcmbrgs')
12 | expect(hash(7)).toEqual('x1fq1y7a')
13 | expect(hash(true)).toEqual('x1rowxc0')
14 |
15 | expect(hash({ object: true })).toEqual('x1qx0xkp')
16 | expect(hash(['array'])).toEqual('xr7mzub')
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/src/hash/hash.ts:
--------------------------------------------------------------------------------
1 | import { JSONValue } from '../typeHelpers'
2 |
3 | /**
4 | * ### hash(data)
5 | *
6 | * Create a hashed string representation of the passed in data.
7 | *
8 | * :warning: **This function is not cryptographically secure, use
9 | * [`Argon2id`, `scrypt` or `bcrypt`](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#password-hashing-algorithms)
10 | * for anything security related.**
11 | *
12 | * ```js
13 | * flocky.hash('some really long string')
14 | * // -> 'x1nr7uiv'
15 | *
16 | * flocky.hash({id: 'AAA', name: 'BBB'})
17 | * // -> 'x16mynva'
18 | * ```
19 | *
20 | *
21 | * Implementation Details
22 | *
23 | * This method uses Murmur3 because it is small, fast and has fairly good
24 | * collision characteristics (about 1 in 36000).
25 | *
26 | * - https://softwareengineering.stackexchange.com/questions/49550
27 | * - https://github.com/VowpalWabbit/vowpal_wabbit/wiki/murmur2-vs-murmur3
28 | * - https://en.wikipedia.org/wiki/MurmurHash
29 | * - https://github.com/whitequark/murmurhash3-js/blob/master/murmurhash3.js
30 | *
31 | */
32 |
33 | export function hash(data: JSONValue): string {
34 | // Convert any data into a string
35 | data = JSON.stringify(data)
36 |
37 | // Setup length, seed and chunk looping
38 | const len = data.length
39 | let hash = len ^ len
40 | const roundedEnd = len & ~0x1
41 |
42 | // Go through 4-byte chunks
43 | for (let i = 0; i < roundedEnd; i += 2) {
44 | let chunk = data.charCodeAt(i) | (data.charCodeAt(i + 1) << 16)
45 |
46 | chunk = mul32(chunk, 0xcc9e2d51)
47 | chunk = ((chunk & 0x1ffff) << 15) | (chunk >>> 17)
48 | chunk = mul32(chunk, 0x1b873593)
49 |
50 | hash ^= chunk
51 | hash = ((hash & 0x7ffff) << 13) | (hash >>> 19)
52 | hash = (hash * 5 + 0xe6546b64) | 0
53 | }
54 |
55 | // Handle remaining bytes
56 | if (len % 2 === 1) {
57 | let remaining = data.charCodeAt(roundedEnd)
58 |
59 | remaining = mul32(remaining, 0xcc9e2d51)
60 | remaining = ((remaining & 0x1ffff) << 15) | (remaining >>> 17)
61 | remaining = mul32(remaining, 0x1b873593)
62 |
63 | hash ^= remaining
64 | }
65 |
66 | // Finalize
67 | hash ^= len << 1
68 | hash ^= hash >>> 16
69 | hash = mul32(hash, 0x85ebca6b)
70 | hash ^= hash >>> 13
71 | hash = mul32(hash, 0xc2b2ae35)
72 | hash ^= hash >>> 16
73 |
74 | // Convert to string, ensuring start with character
75 | return 'x' + (hash >>> 0).toString(36)
76 | }
77 |
78 | // Multiply two 32-bit numbers
79 | function mul32(m: number, n: number): number {
80 | const nLow = n & 0xffff
81 | const nHigh = n - nLow
82 |
83 | return (((nHigh * m) | 0) + ((nLow * m) | 0)) | 0
84 | }
85 |
--------------------------------------------------------------------------------
/src/identifier/identifier.spec.ts:
--------------------------------------------------------------------------------
1 | import { dateNow, mathRandom } from '../testHelpers'
2 | import { identifier } from './identifier'
3 |
4 | const UUID_FORMAT = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i
5 |
6 | describe('identifier', () => {
7 | beforeEach(() => {
8 | mathRandom.setup()
9 | dateNow.setup()
10 | })
11 |
12 | afterEach(() => {
13 | mathRandom.reset()
14 | dateNow.reset()
15 | })
16 |
17 | test('generates a random identifier', () => {
18 | expect(identifier()).toEqual('bfc8d57e-b9ab-4245-836e-d1fd99602e30')
19 | })
20 | })
21 |
22 | describe('identifier (fuzzing)', () => {
23 | const ITERATIONS = 1000
24 |
25 | test('generates only valid UUIDs', () => {
26 | for (let i = 0; i !== ITERATIONS; i++) {
27 | const id = identifier()
28 | expect(id.match(UUID_FORMAT)).toBeTruthy()
29 | }
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/src/identifier/identifier.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### identifier()
3 | *
4 | * Generate a random identifier with UUID v4 format.
5 | *
6 | * ```js
7 | * flocky.identifier()
8 | * // -> 'bfc8d57e-b9ab-4245-836e-d1fd99602e30'
9 | * ```
10 | */
11 |
12 | export function identifier(): string {
13 | let seed = Date.now()
14 | let uuid = ''
15 |
16 | for (let i = 0; i !== 36; i++) {
17 | // These spots have to be dashes
18 | // xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
19 | // ^ ^ ^ ^
20 | if (i === 8 || i === 13 || i === 18 || i === 23) {
21 | uuid += '-'
22 | continue
23 | }
24 |
25 | // This spot has to be a "4"
26 | // xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
27 | // ^
28 | if (i === 14) {
29 | uuid += '4'
30 | continue
31 | }
32 |
33 | // Generate a random byte and adjust the seed for the next random byte
34 | const randomByte = (seed + Math.random() * 16) % 16 | 0
35 | seed = Math.floor(seed / 16)
36 |
37 | // This spot has to be either "8", "9", "a" or "b". The bit-shifting
38 | // magic below achieves exactly that.
39 | // xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
40 | // ^
41 | if (i === 19) {
42 | uuid += ((randomByte & 0x3) | 0x8).toString(16)
43 | continue
44 | }
45 |
46 | // These spots can just be random bytes (0-9, A-F)
47 | // xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
48 | // ^^^^^^^^ ^^^^ ^^^ ^^^ ^^^^^^^^^^^^
49 | uuid += randomByte.toString(16)
50 | }
51 |
52 | return uuid
53 | }
54 |
--------------------------------------------------------------------------------
/src/index.spec.ts:
--------------------------------------------------------------------------------
1 | import * as _flocky from './index'
2 | import { parseModules } from './parseModules'
3 | import { dateNow, mathRandom } from './testHelpers'
4 |
5 | // This is here because the "import" is not a global variable for "eval"
6 | const flocky = _flocky
7 |
8 | describe('index file', () => {
9 | test('exports the modules', () => {
10 | expect(Object.keys(flocky).length).toBeGreaterThanOrEqual(5)
11 | })
12 |
13 | test('exports custom errors', () => {
14 | expect(new _flocky.PromiseTimeoutError()).toBeInstanceOf(Error)
15 | })
16 | })
17 |
18 | describe('documentation examples', () => {
19 | beforeEach(() => {
20 | mathRandom.setup()
21 | dateNow.setup()
22 | })
23 |
24 | afterEach(() => {
25 | mathRandom.reset()
26 | dateNow.reset()
27 | })
28 |
29 | const modules = parseModules()
30 | modules.forEach((module) => {
31 | const { name, examples } = module
32 |
33 | describe(name, () => {
34 | examples.forEach((example, index) => {
35 | test(`Example #${index + 1}`, async () => {
36 | const actual = await eval(example.code)
37 | expect(actual).toEqual(example.expected)
38 | })
39 | })
40 | })
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { average } from './average/average'
2 | export { chunk } from './chunk/chunk'
3 | export { clamp } from './clamp/clamp'
4 | export { clone } from './clone/clone'
5 | export { compact } from './compact/compact'
6 | export { debounce } from './debounce/debounce'
7 | export { duplicates } from './duplicates/duplicates'
8 | export { escapeRegExp } from './escapeRegExp/escapeRegExp'
9 | export { flatten } from './flatten/flatten'
10 | export { get } from './get/get'
11 | export { hash } from './hash/hash'
12 | export { identifier } from './identifier/identifier'
13 | export { matchAll } from './matchAll/matchAll'
14 | export { max } from './max/max'
15 | export { memoize } from './memoize/memoize'
16 | export { min } from './min/min'
17 | export { omit } from './omit/omit'
18 | export { percentile } from './percentile/percentile'
19 | export { pick } from './pick/pick'
20 | export { promisePool } from './promisePool/promisePool'
21 | export { PromiseTimeoutError, promiseTimeout } from './promiseTimeout/promiseTimeout'
22 | export { random } from './random/random'
23 | export { randomString } from './randomString/randomString'
24 | export { range } from './range/range'
25 | export { roundTo } from './roundTo/roundTo'
26 | export { sample } from './sample/sample'
27 | export { shuffle } from './shuffle/shuffle'
28 | export { sleep } from './sleep/sleep'
29 | export { slugify } from './slugify/slugify'
30 | export { sum } from './sum/sum'
31 | export { throttle } from './throttle/throttle'
32 | export { toMap } from './toMap/toMap'
33 | export { unflatten } from './unflatten/unflatten'
34 | export { unique } from './unique/unique'
35 |
--------------------------------------------------------------------------------
/src/matchAll/matchAll.spec.ts:
--------------------------------------------------------------------------------
1 | import { matchAll } from './matchAll'
2 |
3 | describe('matchAll', () => {
4 | test('matches all occurrences of the regular expression', () => {
5 | expect(matchAll(/\d+/g, '$200 and $400')).toEqual([
6 | { match: '200', subMatches: [], index: 1 },
7 | { match: '400', subMatches: [], index: 10 },
8 | ])
9 | })
10 |
11 | test('matches all occurrences of the regular expression with submatches', () => {
12 | expect(matchAll(/(\d+)/g, '$200 and $400')).toEqual([
13 | { match: '200', subMatches: ['200'], index: 1 },
14 | { match: '400', subMatches: ['400'], index: 10 },
15 | ])
16 | })
17 |
18 | test('matches all occurrences of the regular expression with multiple submatches', () => {
19 | expect(matchAll(/(\d+) and (\d+)/g, '200 and 400 or also 300 and 500')).toEqual([
20 | { match: '200 and 400', subMatches: ['200', '400'], index: 0 },
21 | { match: '300 and 500', subMatches: ['300', '500'], index: 20 },
22 | ])
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/src/matchAll/matchAll.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### matchAll(regExp, string)
3 | *
4 | * Find all matches of a regular expression in a string.
5 | *
6 | * ```js
7 | * flocky.matchAll(/f(o+)/g, 'foo bar baz foooo bar')
8 | * // -> [
9 | * // -> { match: 'foo', subMatches: ['oo'], index: 0 },
10 | * // -> { match: 'foooo', subMatches: ['oooo'], index: 12 },
11 | * // -> ]
12 | * ```
13 | */
14 |
15 | export interface MatchAllMatch {
16 | match: string
17 | subMatches: Array
18 | index: number
19 | }
20 |
21 | export function matchAll(regExp: RegExp, string: string): Array {
22 | return Array.from(string.matchAll(regExp)).map(formatMatch)
23 | }
24 |
25 | function formatMatch(match: RegExpMatchArray): MatchAllMatch {
26 | return {
27 | match: match[0],
28 | subMatches: match.slice(1, match.length),
29 | index: match.index!,
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/max/max.spec.ts:
--------------------------------------------------------------------------------
1 | import { max } from './max'
2 |
3 | describe('max', () => {
4 | test('calculates the maximum of an array of numbers', () => {
5 | expect(max([4, 2, 6])).toEqual(6)
6 | expect(max([9, 4, 3, 7, 79, -60])).toEqual(79)
7 | expect(max([])).toEqual(-Infinity)
8 | })
9 |
10 | test('does not mutate the input', () => {
11 | const input = [1, 2, 3]
12 | max(input)
13 | expect(input).toEqual([1, 2, 3])
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/src/max/max.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### max(array)
3 | *
4 | * Compute the maximum of the values in an array.
5 | *
6 | * ```js
7 | * flocky.max([1, 4, 2, -3, 0])
8 | * // -> 4
9 | * ```
10 | */
11 |
12 | export function max(array: Array): number {
13 | return Math.max.apply(null, array)
14 | }
15 |
--------------------------------------------------------------------------------
/src/memoize/BENCHMARK.md:
--------------------------------------------------------------------------------
1 | ### Benchmark for `memoize`
2 |
3 | [Source for this benchmark](./memoize.benchmark.ts)
4 |
5 | | | lodash | fast-memoize | flocky |
6 | | -------------------- | --------------------------- | --------------------------------- | ------------------------------- |
7 | | monadic (primitive) | 85,503,748 ops/sec (41.94%) | **203,874,391 ops/sec (100.00%)** | 198,755,703 ops/sec (97.49%) |
8 | | monadic (serialized) | 3,292,794 ops/sec (44.06%) | 2,607,224 ops/sec (34.89%) | **7,473,005 ops/sec (100.00%)** |
9 | | variadic | 3,222,679 ops/sec (79.33%) | 1,620,215 ops/sec (39.88%) | **4,062,624 ops/sec (100.00%)** |
10 |
11 | Generated at 2024-07-20 with Node.JS v20.9.0
12 |
--------------------------------------------------------------------------------
/src/memoize/memoize.benchmark.ts:
--------------------------------------------------------------------------------
1 | import fastMemoize from 'fast-memoize'
2 | import lodash from 'lodash'
3 | import { Benchmark } from '../benchmarkHelper'
4 | import { memoize } from './memoize'
5 |
6 | function monadicPrimitiveFunc(a: number): number {
7 | return a + 1
8 | }
9 |
10 | function monadicSerializedFunc(a: string): string {
11 | return a + '1'
12 | }
13 |
14 | function variadicFunc(a: number, b: number): number {
15 | return a + b
16 | }
17 |
18 | const benchmark = new Benchmark('memoize')
19 |
20 | const lodashMonadicPrimitiveFunc = lodash.memoize(monadicPrimitiveFunc)
21 | benchmark.add({
22 | library: 'lodash',
23 | input: 'monadic (primitive)',
24 | func: () => lodashMonadicPrimitiveFunc(1),
25 | })
26 |
27 | const fastMemoizeMonadicPrimitiveFunc = fastMemoize(monadicPrimitiveFunc, {
28 | strategy: fastMemoize.strategies.monadic,
29 | })
30 | benchmark.add({
31 | library: 'fast-memoize',
32 | input: 'monadic (primitive)',
33 | func: () => fastMemoizeMonadicPrimitiveFunc(1),
34 | })
35 |
36 | const flockyMonadicPrimitiveFunc = memoize(monadicPrimitiveFunc, { strategy: 'monadic' })
37 | benchmark.add({
38 | library: 'flocky',
39 | input: 'monadic (primitive)',
40 | func: () => flockyMonadicPrimitiveFunc(1),
41 | })
42 |
43 | const lodashMonadicSerializedFunc = lodash.memoize(monadicSerializedFunc, (...args) =>
44 | JSON.stringify(args)
45 | )
46 | benchmark.add({
47 | library: 'lodash',
48 | input: 'monadic (serialized)',
49 | func: () => lodashMonadicSerializedFunc('1'),
50 | })
51 |
52 | const fastMemoizeMonadicSerializedFunc = fastMemoize(monadicSerializedFunc, {
53 | strategy: fastMemoize.strategies.monadic,
54 | })
55 | benchmark.add({
56 | library: 'fast-memoize',
57 | input: 'monadic (serialized)',
58 | func: () => fastMemoizeMonadicSerializedFunc('1'),
59 | })
60 |
61 | const flockyMonadicSerializedFunc = memoize(monadicSerializedFunc, { strategy: 'monadic' })
62 | benchmark.add({
63 | library: 'flocky',
64 | input: 'monadic (serialized)',
65 | func: () => flockyMonadicSerializedFunc('1'),
66 | })
67 |
68 | const lodashVariadicFunc = lodash.memoize(variadicFunc, (...args) => JSON.stringify(args))
69 | benchmark.add({
70 | library: 'lodash',
71 | input: 'variadic',
72 | func: () => lodashVariadicFunc(1, 2),
73 | })
74 |
75 | const fastMemoizeVariadicFunc = fastMemoize(variadicFunc, {
76 | strategy: fastMemoize.strategies.variadic,
77 | })
78 | benchmark.add({
79 | library: 'fast-memoize',
80 | input: 'variadic',
81 | func: () => fastMemoizeVariadicFunc(1, 2),
82 | })
83 |
84 | const flockyVariadicFunc = memoize(variadicFunc, { strategy: 'variadic' })
85 | benchmark.add({
86 | library: 'flocky',
87 | input: 'variadic',
88 | func: () => flockyVariadicFunc(1, 2),
89 | })
90 |
91 | benchmark.run()
92 |
--------------------------------------------------------------------------------
/src/memoize/memoize.spec.ts:
--------------------------------------------------------------------------------
1 | import { sleep } from '../sleep/sleep'
2 | import { MemoizeOptions, memoize } from './memoize'
3 |
4 | type NumberObject = { n: number }
5 |
6 | describe('memoize', () => {
7 | test('memoizes function calls with no arguments', () => {
8 | const calls: Array<[]> = []
9 | const func = (): number => {
10 | calls.push([])
11 | return 1
12 | }
13 |
14 | const memoizedFunc = memoize(func)
15 |
16 | expect(memoizedFunc()).toEqual(1)
17 | expect(memoizedFunc()).toEqual(1)
18 | expect(calls).toEqual([[]])
19 | })
20 |
21 | test('memoizes function calls with a single primitive argument', () => {
22 | const calls: Array> = []
23 | const func = (x: number): number => {
24 | calls.push([x])
25 | return x + 1
26 | }
27 |
28 | const memoizedFunc = memoize(func)
29 |
30 | expect(memoizedFunc(1)).toEqual(2)
31 | expect(memoizedFunc(1)).toEqual(2)
32 | expect(memoizedFunc(2)).toEqual(3)
33 | expect(memoizedFunc(1)).toEqual(2)
34 | expect(calls).toEqual([[1], [2]])
35 | })
36 |
37 | test('memoizes function calls with a single non-primitive argument', () => {
38 | const calls: Array> = []
39 | const func = (x: NumberObject): number => {
40 | calls.push([x])
41 | return x.n + 1
42 | }
43 |
44 | const memoizedFunc = memoize(func)
45 |
46 | expect(memoizedFunc({ n: 1 })).toEqual(2)
47 | expect(memoizedFunc({ n: 1 })).toEqual(2)
48 | expect(memoizedFunc({ n: 2 })).toEqual(3)
49 | expect(memoizedFunc({ n: 1 })).toEqual(2)
50 | expect(calls).toEqual([[{ n: 1 }], [{ n: 2 }]])
51 | })
52 |
53 | test('memoizes function calls with multiple primitive arguments', () => {
54 | const calls: Array> = []
55 | const func = (a: number, b: number): number => {
56 | calls.push([a, b])
57 | return a + b
58 | }
59 |
60 | const memoizedFunc = memoize(func)
61 |
62 | expect(memoizedFunc(1, 2)).toEqual(3)
63 | expect(memoizedFunc(1, 2)).toEqual(3)
64 | expect(memoizedFunc(3, 4)).toEqual(7)
65 | expect(memoizedFunc(1, 2)).toEqual(3)
66 | expect(memoizedFunc(1, 4)).toEqual(5)
67 | expect(calls).toEqual([
68 | [1, 2],
69 | [3, 4],
70 | [1, 4],
71 | ])
72 | })
73 |
74 | test('memoizes function calls with multiple non-primitive arguments', () => {
75 | const calls: Array> = []
76 | const func = (a: NumberObject, b: NumberObject): number => {
77 | calls.push([a, b])
78 | return a.n + b.n
79 | }
80 |
81 | const memoizedFunc = memoize(func)
82 |
83 | expect(memoizedFunc({ n: 1 }, { n: 2 })).toEqual(3)
84 | expect(memoizedFunc({ n: 1 }, { n: 2 })).toEqual(3)
85 | expect(memoizedFunc({ n: 3 }, { n: 4 })).toEqual(7)
86 | expect(memoizedFunc({ n: 1 }, { n: 2 })).toEqual(3)
87 | expect(memoizedFunc({ n: 1 }, { n: 4 })).toEqual(5)
88 | expect(calls).toEqual([
89 | [{ n: 1 }, { n: 2 }],
90 | [{ n: 3 }, { n: 4 }],
91 | [{ n: 1 }, { n: 4 }],
92 | ])
93 | })
94 |
95 | test('memoizes function calls with spread primitive arguments', () => {
96 | const calls: Array> = []
97 | const func = (multiplier: number, ...numbers: Array): Array => {
98 | calls.push([multiplier, ...numbers])
99 | return numbers.map((x) => multiplier * x)
100 | }
101 |
102 | const memoizedFunc = memoize(func, {
103 | strategy: 'variadic',
104 | })
105 |
106 | expect(memoizedFunc(2, 1, 2, 3)).toEqual([2, 4, 6])
107 | expect(memoizedFunc(3, 4, 5, 6)).toEqual([12, 15, 18])
108 | expect(memoizedFunc(2, 1, 2, 3)).toEqual([2, 4, 6])
109 | expect(calls).toEqual([
110 | [2, 1, 2, 3],
111 | [3, 4, 5, 6],
112 | ])
113 | })
114 |
115 | test('memoizes function calls with spread non-primitive arguments', () => {
116 | const calls: Array> = []
117 | const func = (multiplier: NumberObject, ...numbers: Array): Array => {
118 | calls.push([multiplier, ...numbers])
119 | return numbers.map((x) => multiplier.n * x.n)
120 | }
121 |
122 | const memoizedFunc = memoize(func, {
123 | strategy: 'variadic',
124 | })
125 |
126 | expect(memoizedFunc({ n: 2 }, { n: 1 }, { n: 2 })).toEqual([2, 4])
127 | expect(memoizedFunc({ n: 3 }, { n: 4 }, { n: 5 })).toEqual([12, 15])
128 | expect(memoizedFunc({ n: 2 }, { n: 1 }, { n: 2 })).toEqual([2, 4])
129 | expect(calls).toEqual([
130 | [{ n: 2 }, { n: 1 }, { n: 2 }],
131 | [{ n: 3 }, { n: 4 }, { n: 5 }],
132 | ])
133 | })
134 |
135 | test('passes arguments as their original primitive', () => {
136 | const func = (x: unknown): string =>
137 | typeof x === 'object' && x ? x.constructor.name : typeof x
138 |
139 | const memoizedFunc = memoize(func)
140 |
141 | expect(memoizedFunc(1)).toEqual('number')
142 | expect(memoizedFunc('2')).toEqual('string')
143 | expect(memoizedFunc({ n: 3 })).toEqual('Object')
144 | })
145 |
146 | test('can define a custom strategy (monadic)', () => {
147 | const calls: Array> = []
148 | const func = (a: number, b: number): number => {
149 | calls.push([a, b])
150 | return a
151 | }
152 |
153 | const memoizedFunc = memoize(func, {
154 | strategy: 'monadic',
155 | })
156 |
157 | expect(memoizedFunc(1, 2)).toEqual(1)
158 | expect(memoizedFunc(1, 2)).toEqual(1)
159 | expect(memoizedFunc(1, 3)).toEqual(1)
160 | expect(memoizedFunc(3, 4)).toEqual(3)
161 | expect(memoizedFunc(1, 2)).toEqual(1)
162 | expect(memoizedFunc(1, 4)).toEqual(1)
163 | expect(calls).toEqual([
164 | [1, undefined],
165 | [3, undefined],
166 | ])
167 | })
168 |
169 | test('can define a custom strategy (variadic)', () => {
170 | const calls: Array> = []
171 | const func = (x: number): number => {
172 | calls.push([x])
173 | return x + 1
174 | }
175 |
176 | const memoizedFunc = memoize(func, {
177 | strategy: 'variadic',
178 | })
179 |
180 | expect(memoizedFunc(1)).toEqual(2)
181 | expect(memoizedFunc(1)).toEqual(2)
182 | expect(memoizedFunc(2)).toEqual(3)
183 | expect(memoizedFunc(1)).toEqual(2)
184 | expect(calls).toEqual([[1], [2]])
185 | })
186 |
187 | test('can define a custom serializer', () => {
188 | let serializerCalls = 0
189 | function serializer(data: unknown): string {
190 | serializerCalls++
191 | return '__' + JSON.stringify(data) + '__'
192 | }
193 |
194 | const func = (a: number, b: number): number => {
195 | return a + b
196 | }
197 |
198 | const memoizedFunc = memoize(func, { serializer })
199 |
200 | expect(memoizedFunc(1, 2)).toEqual(3)
201 | expect(memoizedFunc(1, 2)).toEqual(3)
202 | expect(memoizedFunc(3, 4)).toEqual(7)
203 | expect(memoizedFunc(1, 2)).toEqual(3)
204 | expect(memoizedFunc(1, 4)).toEqual(5)
205 | expect(serializerCalls).toEqual(5)
206 | })
207 |
208 | test('memoizes function calls that return promises', async () => {
209 | let calls = 0
210 | const func = async (): Promise => {
211 | calls++
212 | await sleep(10)
213 | return 'A'
214 | }
215 |
216 | const memoizedFunc = memoize(func)
217 |
218 | const a = memoizedFunc()
219 | expect(a).toBeInstanceOf(Promise)
220 | const b = memoizedFunc()
221 | expect(b).toBeInstanceOf(Promise)
222 | expect(calls).toEqual(1)
223 |
224 | expect(await a).toEqual('A')
225 | expect(await b).toEqual('A')
226 | expect(calls).toEqual(1)
227 |
228 | const c = memoizedFunc()
229 | expect(await c).toEqual('A')
230 | expect(calls).toEqual(1)
231 | })
232 |
233 | test('memoizes function calls with a maximum TTL', async () => {
234 | const calls: Array> = []
235 | const func = (a: number, b: number): number => {
236 | calls.push([a, b])
237 | return a + b
238 | }
239 |
240 | const memoizedFunc = memoize(func, {
241 | ttl: 10,
242 | })
243 |
244 | expect(memoizedFunc(1, 2)).toEqual(3)
245 | expect(memoizedFunc(1, 2)).toEqual(3)
246 | expect(memoizedFunc(3, 4)).toEqual(7)
247 | expect(memoizedFunc(1, 2)).toEqual(3)
248 | expect(calls).toEqual([
249 | [1, 2],
250 | [3, 4],
251 | ])
252 |
253 | await sleep(20)
254 | expect(memoizedFunc(1, 2)).toEqual(3)
255 | expect(calls).toEqual([
256 | [1, 2],
257 | [3, 4],
258 | [1, 2],
259 | ])
260 | })
261 |
262 | test.each([
263 | ['monadic', { strategy: 'monadic' }],
264 | ['variadic', { strategy: 'variadic' }],
265 | ['ttl', { ttl: 50 }],
266 | ] as Array<[string, MemoizeOptions]>)(
267 | 'smartly memoizes function calls that return promise rejections (%s)',
268 | async (_: string, options: MemoizeOptions) => {
269 | let calls = 0
270 | const func = async (): Promise => {
271 | calls++
272 | await sleep(10)
273 |
274 | if (calls === 1) {
275 | throw new Error('Uh oh')
276 | }
277 |
278 | return 'A'
279 | }
280 |
281 | const memoizedFunc = memoize(func, options)
282 |
283 | // We want to memoize the second call because the Promise is still pending
284 | const a = memoizedFunc()
285 | expect(a).toBeInstanceOf(Promise)
286 | const b = memoizedFunc()
287 | expect(b).toBeInstanceOf(Promise)
288 | expect(calls).toEqual(1)
289 |
290 | await expect(a).rejects.toThrow('Uh oh')
291 | await expect(b).rejects.toThrow('Uh oh')
292 | expect(calls).toEqual(1)
293 |
294 | // We don't want to memoize the third call because the Promise has rejected
295 | const c = memoizedFunc()
296 | expect(await c).toEqual('A')
297 | expect(calls).toEqual(2)
298 | }
299 | )
300 |
301 | test('has the correct type', () => {
302 | const func = (a: number, b: number): number => {
303 | return a + b
304 | }
305 |
306 | const memoizedFunc = memoize(func)
307 |
308 | // @ts-expect-error The first argument has to be a number
309 | memoizedFunc('a', 2)
310 |
311 | // @ts-expect-error The second argument has to be a number
312 | memoizedFunc(2, 'a')
313 |
314 | // @ts-expect-error The return value is a number
315 | expect(() => memoizedFunc(2, 2).concat()).toThrow()
316 |
317 | // This one is okay
318 | memoizedFunc(2, 2).toFixed(2)
319 | })
320 | })
321 |
--------------------------------------------------------------------------------
/src/memoize/memoize.ts:
--------------------------------------------------------------------------------
1 | import { TAnyFunction } from '../typeHelpers'
2 |
3 | export interface MemoizeOptions {
4 | strategy?: MemoizeStrategy
5 | serializer?: MemoizeSerializer
6 | ttl?: number
7 | }
8 |
9 | type MemoizeStrategy = 'monadic' | 'variadic'
10 |
11 | type MemoizeSerializer = (data: unknown) => string
12 |
13 | /**
14 | * ### memoize(func, options?)
15 | *
16 | * Create a function that memoizes the return value of `func`.
17 | *
18 | * ```js
19 | * const func = (a, b) => a + b
20 | * const memoizedFunc = flocky.memoize(func)
21 | * const memoizedFuncWithTtl = flocky.memoize(func, { ttl: 30 * 1000 })
22 | * memoizedFunc(1, 2)
23 | * // -> 3
24 | * ```
25 | *
26 | *
27 | * Implementation Details
28 | *
29 | * This method's implementation is based on [fast-memoize](https://github.com/caiogondim/fast-memoize.js),
30 | * with some improvements for variadic performance and additional support for a TTL based cache.
31 | *
32 | */
33 |
34 | export function memoize>(
35 | this: TThis,
36 | func: TFunc,
37 | options: MemoizeOptions = {}
38 | ): TFunc {
39 | const strategy =
40 | options.strategy === 'monadic' || (options.strategy !== 'variadic' && func.length <= 1)
41 | ? monadic
42 | : variadic
43 | const cache = options.ttl ? ttlCache(options.ttl) : defaultCache()
44 | const serializer = options.serializer ? options.serializer : defaultSerializer
45 |
46 | return strategy.bind(this, func, cache, serializer) as TFunc
47 | }
48 |
49 | function isPrimitive(value: unknown): value is string {
50 | // We can not treat strings as primitive, because they overwrite numbers
51 | return value == null || typeof value === 'number' || typeof value === 'boolean'
52 | }
53 |
54 | function monadic>(
55 | this: TThis,
56 | func: TFunc,
57 | cache: MemoizeCache,
58 | serializer: MemoizeSerializer,
59 | arg: unknown
60 | ): TReturn {
61 | const cacheKey = isPrimitive(arg) ? arg : serializer(arg)
62 |
63 | let value = cache.get(cacheKey)
64 | if (typeof value === 'undefined') {
65 | value = func.call(this, arg)
66 |
67 | if (value instanceof Promise) {
68 | value.catch(() => cache.remove(cacheKey))
69 | }
70 |
71 | cache.set(cacheKey, value)
72 | }
73 |
74 | return value
75 | }
76 |
77 | function variadic>(
78 | this: TThis,
79 | func: TFunc,
80 | cache: MemoizeCache,
81 | serializer: MemoizeSerializer,
82 | ...args: Array
83 | ): TReturn {
84 | const cacheKey = serializer(args)
85 |
86 | let value = cache.get(cacheKey)
87 | if (typeof value === 'undefined') {
88 | value = func.apply(this, args)
89 |
90 | if (value instanceof Promise) {
91 | value.catch(() => cache.remove(cacheKey))
92 | }
93 |
94 | cache.set(cacheKey, value)
95 | }
96 |
97 | return value
98 | }
99 |
100 | function defaultSerializer(data: unknown): string {
101 | return JSON.stringify(data)
102 | }
103 |
104 | interface MemoizeCache {
105 | get: (key: string) => TReturn | undefined
106 | set: (key: string, value: TReturn) => void
107 | remove: (key: string) => void
108 | }
109 |
110 | function defaultCache(): MemoizeCache {
111 | const cache = Object.create(null) as Record
112 |
113 | return {
114 | get: (key) => cache[key],
115 | set: (key, value): void => {
116 | cache[key] = value
117 | },
118 | remove: (key) => delete cache[key],
119 | }
120 | }
121 |
122 | function ttlCache(ttl: number): MemoizeCache {
123 | const cache = Object.create(null) as Record
124 |
125 | return {
126 | get: (key) => cache[key],
127 | set: (key, value): void => {
128 | cache[key] = value
129 |
130 | // Note: We do not need to clear the timeout because we never set a key
131 | // if it still exists in the cache.
132 | setTimeout(() => {
133 | delete cache[key]
134 | }, ttl)
135 | },
136 | remove: (key) => delete cache[key],
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/min/min.spec.ts:
--------------------------------------------------------------------------------
1 | import { min } from './min'
2 |
3 | describe('min', () => {
4 | test('calculates the minimum of an array of numbers', () => {
5 | expect(min([4, 2, 6])).toEqual(2)
6 | expect(min([9, 4, 3, 7, 79, -60])).toEqual(-60)
7 | expect(min([])).toEqual(Infinity)
8 | })
9 |
10 | test('does not mutate the input', () => {
11 | const input = [1, 2, 3]
12 | min(input)
13 | expect(input).toEqual([1, 2, 3])
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/src/min/min.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### min(array)
3 | *
4 | * Compute the minimum of the values in an array.
5 | *
6 | * ```js
7 | * flocky.min([1, 4, 2, -3, 0])
8 | * // -> -3
9 | * ```
10 | */
11 |
12 | export function min(array: Array): number {
13 | return Math.min.apply(null, array)
14 | }
15 |
--------------------------------------------------------------------------------
/src/omit/BENCHMARK.md:
--------------------------------------------------------------------------------
1 | ### Benchmark for `omit`
2 |
3 | [Source for this benchmark](./omit.benchmark.ts)
4 |
5 | | | flocky |
6 | | -------------------------- | ------------------------------- |
7 | | 5 properties / 3 omitted | **8,553,947 ops/sec (100.00%)** |
8 | | 26 properties / 3 omitted | **460,434 ops/sec (100.00%)** |
9 | | 10 properties / 55 omitted | **1,238,164 ops/sec (100.00%)** |
10 |
11 | Generated at 2024-07-20 with Node.JS v20.9.0
12 |
--------------------------------------------------------------------------------
/src/omit/omit.benchmark.ts:
--------------------------------------------------------------------------------
1 | import { Benchmark } from '../benchmarkHelper'
2 | import { randomString } from '../randomString/randomString'
3 | import { omit } from './omit'
4 |
5 | const benchmark = new Benchmark('omit')
6 |
7 | const omitKeys: Array = []
8 | for (let i = 0; i !== 55; i++) {
9 | omitKeys.push(randomString(10))
10 | }
11 |
12 | benchmark.add({
13 | library: 'flocky',
14 | input: '5 properties / 3 omitted',
15 | func: () => omit({ a: 1, b: 2, c: 3, d: 4, e: 5 }, ['a', 'b', 'd']),
16 | })
17 |
18 | benchmark.add({
19 | library: 'flocky',
20 | input: '26 properties / 3 omitted',
21 | func: () => {
22 | const object = {
23 | a: 1,
24 | b: 2,
25 | c: 3,
26 | d: 4,
27 | e: 5,
28 | f: 6,
29 | g: 7,
30 | h: 8,
31 | i: 9,
32 | j: 10,
33 | k: 11,
34 | l: 12,
35 | m: 13,
36 | n: 14,
37 | o: 15,
38 | p: 16,
39 | q: 17,
40 | r: 18,
41 | s: 19,
42 | t: 20,
43 | u: 21,
44 | v: 22,
45 | w: 23,
46 | x: 24,
47 | y: 25,
48 | z: 26,
49 | }
50 |
51 | return omit(object, ['a', 'b', 'd'])
52 | },
53 | })
54 |
55 | benchmark.add({
56 | library: 'flocky',
57 | input: '10 properties / 55 omitted',
58 | func: () => {
59 | const object = {
60 | a: 1,
61 | b: 2,
62 | c: 3,
63 | d: 4,
64 | e: 5,
65 | f: 6,
66 | g: 7,
67 | h: 8,
68 | i: 9,
69 | j: 10,
70 | }
71 |
72 | return omit(object, omitKeys as Array)
73 | },
74 | })
75 |
76 | benchmark.run()
77 |
--------------------------------------------------------------------------------
/src/omit/omit.spec.ts:
--------------------------------------------------------------------------------
1 | import { omit } from './omit'
2 |
3 | describe('omit', () => {
4 | test('omits the specified keys of the object', () => {
5 | const original = { a: 1, b: 2, c: 3, d: 4, e: 5 }
6 | const omitted = omit(original, ['a', 'b', 'd'])
7 |
8 | expect(original).toEqual({ a: 1, b: 2, c: 3, d: 4, e: 5 })
9 | expect(omitted).toEqual({ c: 3, e: 5 })
10 | })
11 |
12 | test('has the correct type behavior', () => {
13 | interface StackProps {
14 | children: string
15 | gap: string
16 | }
17 |
18 | function omitFromProps(props: StackProps): Omit {
19 | return omit(props, ['gap'])
20 | }
21 |
22 | expect(omitFromProps({ children: 'foo', gap: 'bar' })).toEqual({ children: 'foo' })
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/src/omit/omit.ts:
--------------------------------------------------------------------------------
1 | type Omit = Pick>
2 |
3 | /**
4 | * ### omit(object, keys)
5 | *
6 | * Create an object composed of all existing keys that are not specified in `keys`.
7 | *
8 | * ```js
9 | * const object = { a: 1, b: 2, c: 3 }
10 | * flocky.omit(object, ['a'])
11 | * // -> { b: 2, c: 3 }
12 | * ```
13 | */
14 |
15 | export function omit(object: T, keys: Array): Omit {
16 | const objectKeys = Object.keys(object) as Array
17 |
18 | const result: Partial = {}
19 | for (let i = 0; i !== objectKeys.length; i++) {
20 | const objectKey = objectKeys[i] as K
21 |
22 | if (keys.indexOf(objectKey) === -1) {
23 | result[objectKey] = object[objectKey]
24 | }
25 | }
26 |
27 | return result as Omit
28 | }
29 |
--------------------------------------------------------------------------------
/src/parseModules.ts:
--------------------------------------------------------------------------------
1 | import glob from 'fast-glob'
2 | import fs from 'fs'
3 | import path from 'path'
4 |
5 | const EXAMPLE_REGEX = /```js\n(.*?)\n```/s
6 |
7 | interface ModuleFile {
8 | filePath: string
9 | name: string
10 | docs: string
11 | examples: Array
12 | }
13 |
14 | interface Example {
15 | code: string
16 | expected: unknown
17 | }
18 |
19 | export function parseModules(): Array {
20 | return getModulePaths()
21 | .map(parseModule)
22 | .filter((x): x is ModuleFile => Boolean(x))
23 | }
24 |
25 | function getModulePaths(): Array {
26 | const paths = glob.sync(path.join(__dirname, '../src/*/*.ts'))
27 | return paths.filter((path) => path.match(/src\/(.*?)\/\1.ts/))
28 | }
29 |
30 | function parseModule(filePath: string): ModuleFile | false {
31 | const name = parseModuleName(filePath)
32 | const docs = parseModuleDocs(filePath)
33 | const examples = parseModuleExamples(filePath, docs)
34 |
35 | return { filePath, name, docs, examples }
36 | }
37 |
38 | function parseModuleName(filePath: string): string {
39 | return filePath.replace(/^.*\/src\/(.*?)\/\1.ts$/, '$1')
40 | }
41 |
42 | function parseModuleDocs(filePath: string): string {
43 | const fileContent = fs.readFileSync(filePath, 'utf-8')
44 |
45 | if (!fileContent.includes('/**')) {
46 | throw new Error(`The module at path '${filePath}' has no JSDoc`)
47 | }
48 |
49 | // Parse the initial JSDoc comment as the documentation
50 | return fileContent
51 | .split('\n')
52 | .filter((line) => line.startsWith(' *'))
53 | .map((line) => line.replace(/^ \*[ /]?/, ''))
54 | .join('\n')
55 | }
56 |
57 | function parseModuleExamples(filePath: string, docs: string): Array {
58 | const exampleMatch = docs.match(EXAMPLE_REGEX)
59 |
60 | if (!exampleMatch) {
61 | throw new Error(`The module at path '${filePath}' has no examples`)
62 | }
63 |
64 | return exampleMatch[1].split('\n\n').map(parseModuleExample)
65 | }
66 |
67 | function parseModuleExample(example: string): Example {
68 | // Get the code (the parts without "// -> " at the start)
69 | const code = example
70 | .split('\n')
71 | .filter((x) => !x.startsWith('// -> '))
72 | .map((x) => x.replace('await ', ''))
73 | .join('\n')
74 |
75 | // Get the expected output (the parts with "// -> " at the start)
76 | const expected = example
77 | .split('\n')
78 | .filter((x) => x.startsWith('// -> '))
79 | .map((x) => x.replace('// -> ', ''))
80 | .join('\n')
81 |
82 | return {
83 | code,
84 | expected: expected === '' || expected === 'undefined' ? undefined : eval(`(${expected})`),
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/percentile/percentile.spec.ts:
--------------------------------------------------------------------------------
1 | import { percentile } from './percentile'
2 |
3 | describe('percentile', () => {
4 | test('calculates the percentile of an array of numbers', () => {
5 | expect(percentile([1, 4, 2, 4, 0], 0.5)).toEqual(2)
6 |
7 | const values = [90, 85, 65, 72, 82, 96, 70, 79, 68, 84]
8 | expect(percentile(values, 1)).toEqual(96)
9 | expect(percentile(values, 0.95)).toEqual(93.3)
10 | expect(percentile(values, 0.9)).toEqual(90.6)
11 | expect(percentile(values, 0.8)).toEqual(86)
12 | expect(percentile(values, 0.75)).toEqual(84.75)
13 | expect(percentile(values, 0.7)).toEqual(84.3)
14 | expect(percentile(values, 0.6)).toEqual(82.8)
15 | expect(percentile(values, 0.5)).toEqual(80.5)
16 | expect(percentile(values, 0.25)).toEqual(70.5)
17 | expect(percentile(values, 0)).toEqual(65)
18 |
19 | expect(percentile([0], 0.5)).toEqual(0)
20 | expect(percentile([], 0.5)).toEqual(NaN)
21 | })
22 |
23 | test('does not mutate the input', () => {
24 | const input = [3, 1, 2, 3]
25 | percentile(input, 0.5)
26 | expect(input).toEqual([3, 1, 2, 3])
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/src/percentile/percentile.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### percentile(array, k)
3 | *
4 | * Compute the kth percentile of the values in an array.
5 | *
6 | * ```js
7 | * flocky.percentile([90, 85, 65, 72, 82, 96, 70, 79, 68, 84], 0.9)
8 | * // -> 90.6
9 | * ```
10 | */
11 |
12 | export function percentile(array: Array, k: number): number {
13 | const values = Array.from(array).sort((a, b) => a - b)
14 | const position = (values.length - 1) * k
15 | const baseIndex = Math.floor(position)
16 |
17 | if (values[baseIndex] === undefined) {
18 | return NaN
19 | }
20 |
21 | if (values[baseIndex + 1] === undefined) {
22 | return values[baseIndex]
23 | }
24 |
25 | const restPosition = position - baseIndex
26 | return values[baseIndex] + (values[baseIndex + 1] - values[baseIndex]) * restPosition
27 | }
28 |
--------------------------------------------------------------------------------
/src/pick/BENCHMARK.md:
--------------------------------------------------------------------------------
1 | ### Benchmark for `pick`
2 |
3 | [Source for this benchmark](./pick.benchmark.ts)
4 |
5 | | | flocky |
6 | | ------------------------ | -------------------------------- |
7 | | 5 properties / 3 picked | **10,428,966 ops/sec (100.00%)** |
8 | | 26 properties / 3 picked | **7,488,734 ops/sec (100.00%)** |
9 |
10 | Generated at 2024-07-20 with Node.JS v20.9.0
11 |
--------------------------------------------------------------------------------
/src/pick/pick.benchmark.ts:
--------------------------------------------------------------------------------
1 | import { Benchmark } from '../benchmarkHelper'
2 | import { pick } from './pick'
3 |
4 | const benchmark = new Benchmark('pick')
5 |
6 | benchmark.add({
7 | library: 'flocky',
8 | input: '5 properties / 3 picked',
9 | func: () => pick({ a: 1, b: 2, c: 3, d: 4, e: 5 }, ['a', 'b', 'd']),
10 | })
11 |
12 | benchmark.add({
13 | library: 'flocky',
14 | input: '26 properties / 3 picked',
15 | func: () => {
16 | const object = {
17 | a: 1,
18 | b: 2,
19 | c: 3,
20 | d: 4,
21 | e: 5,
22 | f: 6,
23 | g: 7,
24 | h: 8,
25 | i: 9,
26 | j: 10,
27 | k: 11,
28 | l: 12,
29 | m: 13,
30 | n: 14,
31 | o: 15,
32 | p: 16,
33 | q: 17,
34 | r: 18,
35 | s: 19,
36 | t: 20,
37 | u: 21,
38 | v: 22,
39 | w: 23,
40 | x: 24,
41 | y: 25,
42 | z: 26,
43 | }
44 |
45 | return pick(object, ['a', 'b', 'd'])
46 | },
47 | })
48 |
49 | benchmark.run()
50 |
--------------------------------------------------------------------------------
/src/pick/pick.spec.ts:
--------------------------------------------------------------------------------
1 | import { pick } from './pick'
2 |
3 | describe('pick', () => {
4 | test('picks the specified keys of the object', () => {
5 | const original = { a: 1, b: 2, c: 3, d: 4, e: 5 }
6 | const picked = pick(original, ['a', 'b', 'd'])
7 |
8 | expect(original).toEqual({ a: 1, b: 2, c: 3, d: 4, e: 5 })
9 | expect(picked).toEqual({ a: 1, b: 2, d: 4 })
10 | })
11 |
12 | test('has the correct type behavior', () => {
13 | interface StackProps {
14 | children: string
15 | gap: string
16 | }
17 |
18 | function pickFromProps(props: StackProps): Pick {
19 | return pick(props, ['gap'])
20 | }
21 |
22 | expect(pickFromProps({ children: 'foo', gap: 'bar' })).toEqual({ gap: 'bar' })
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/src/pick/pick.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### pick(object, keys)
3 | *
4 | * Create an object composed of the specified `keys`.
5 | *
6 | * ```js
7 | * const object = { a: 1, b: 2, c: 3 }
8 | * flocky.pick(object, ['a', 'c'])
9 | * // -> { a: 1, c: 3 }
10 | * ```
11 | */
12 |
13 | export function pick(object: T, keys: Array): Pick {
14 | const result: Partial = {}
15 |
16 | for (let i = 0; i !== keys.length; i++) {
17 | result[keys[i]] = object[keys[i]]
18 | }
19 |
20 | return result as Pick
21 | }
22 |
--------------------------------------------------------------------------------
/src/promisePool/promisePool.spec.ts:
--------------------------------------------------------------------------------
1 | import { sleep } from '../sleep/sleep'
2 | import { expectApproximateDuration } from '../testHelpers'
3 | import { promisePool } from './promisePool'
4 |
5 | const PROMISE_FUNCTIONS = new Array(9).fill('').map((_, i) => async (): Promise => {
6 | await sleep(100)
7 | return i
8 | })
9 |
10 | describe('promisePool', () => {
11 | test('runs the promise functions in parallel', async () => {
12 | const start = new Date()
13 | const result = await promisePool(PROMISE_FUNCTIONS, 100)
14 | const end = new Date()
15 |
16 | expect(result).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8])
17 | expectApproximateDuration(start, end, 100)
18 | })
19 |
20 | test('runs the promise functions in parallel with a limit', async () => {
21 | const start = new Date()
22 | const result = await promisePool(PROMISE_FUNCTIONS, 3)
23 | const end = new Date()
24 |
25 | expect(result).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8])
26 | expectApproximateDuration(start, end, 3 * 100)
27 | })
28 |
29 | test('runs the promise functions in series', async () => {
30 | const start = new Date()
31 | const result = await promisePool(PROMISE_FUNCTIONS, 1)
32 | const end = new Date()
33 |
34 | expect(result).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8])
35 | expectApproximateDuration(start, end, 9 * 100)
36 | })
37 |
38 | test('errors when the first promise function errors', async () => {
39 | const ERRORING_PROMISE_FUNCTIONS = Object.assign([], PROMISE_FUNCTIONS, {
40 | 5: () => Promise.reject(new Error('Something went wrong.')),
41 | })
42 |
43 | const start = new Date()
44 | let error: Error | undefined
45 | try {
46 | await promisePool(ERRORING_PROMISE_FUNCTIONS, 2)
47 | } catch (err) {
48 | if (!(err instanceof Error)) throw err
49 | error = err
50 | }
51 | const end = new Date()
52 |
53 | expect(error?.message).toEqual('Something went wrong.')
54 | expectApproximateDuration(start, end, 2 * 100)
55 | })
56 | })
57 |
--------------------------------------------------------------------------------
/src/promisePool/promisePool.ts:
--------------------------------------------------------------------------------
1 | type PromiseFunction = () => Promise
2 |
3 | /**
4 | * ### promisePool(promiseFunctions, limit)
5 | *
6 | * Run multiple promise-returning functions in parallel with limited concurrency.
7 | *
8 | * ```js
9 | * await flocky.promisePool([
10 | * () => Promise.resolve(1),
11 | * () => Promise.resolve(2),
12 | * () => Promise.resolve(3),
13 | * () => Promise.resolve(4),
14 | * ], 2)
15 | * // -> [1, 2, 3, 4]
16 | * ```
17 | */
18 |
19 | export async function promisePool(
20 | promiseFunctions: Array>,
21 | limit: number
22 | ): Promise> {
23 | const results: Array = []
24 |
25 | const iterator = promiseFunctions.entries()
26 | const workers = new Array(limit).fill(iterator).map(async (it: typeof iterator) => {
27 | for (const [index, func] of it) {
28 | results[index] = await func()
29 | }
30 | })
31 |
32 | await Promise.all(workers)
33 | return results
34 | }
35 |
--------------------------------------------------------------------------------
/src/promiseTimeout/promiseTimeout.spec.ts:
--------------------------------------------------------------------------------
1 | import { sleep } from '../sleep/sleep'
2 | import { expectApproximateDuration } from '../testHelpers'
3 | import { promiseTimeout } from './promiseTimeout'
4 |
5 | async function promiseWithDuration(value: T, duration: number): Promise {
6 | await sleep(duration)
7 | return value
8 | }
9 |
10 | describe('promiseTimeout', () => {
11 | test('runs the promise without hitting the timeout', async () => {
12 | const start = new Date()
13 | const result = await promiseTimeout(promiseWithDuration({ foo: 'bar' }, 100), 200)
14 | const end = new Date()
15 |
16 | expect(result).toEqual({ foo: 'bar' })
17 | expectApproximateDuration(start, end, 100)
18 |
19 | // The return type has this property
20 | result.foo.toString()
21 |
22 | // @ts-expect-error The return type does not have this property
23 | expect(result.bar).toBeUndefined()
24 | })
25 |
26 | test('rejects the promise when hitting the timeout', async () => {
27 | let error: Error | undefined
28 |
29 | const start = new Date()
30 | try {
31 | await promiseTimeout(promiseWithDuration('foo', 200), 100)
32 | } catch (err) {
33 | if (!(err instanceof Error)) throw err
34 | error = err
35 | }
36 | const end = new Date()
37 |
38 | expect(error?.message).toEqual('Promise timed out')
39 | expectApproximateDuration(start, end, 100)
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/src/promiseTimeout/promiseTimeout.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### promiseTimeout(promise, timeoutMs)
3 | *
4 | * Reject a promise if it does not resolve within `timeoutMs`.
5 | *
6 | * :warning: **When the timeout is hit, a promise rejection will be thrown. However,
7 | * since promises are not cancellable, the execution of the promise itself will continue
8 | * until it resolves or rejects.**
9 | *
10 | * ```js
11 | * await flocky.promiseTimeout(Promise.resolve(1), 10)
12 | * // -> 1
13 | * ```
14 | */
15 |
16 | export async function promiseTimeout(promise: Promise, timeoutMs: number): Promise {
17 | let timeoutId: NodeJS.Timeout
18 | const timeoutPromise = new Promise((_, reject) => {
19 | timeoutId = setTimeout(() => reject(new PromiseTimeoutError()), timeoutMs)
20 | })
21 |
22 | return Promise.race([promise, timeoutPromise]).then((result) => {
23 | clearTimeout(timeoutId)
24 | return result as T
25 | })
26 | }
27 |
28 | export class PromiseTimeoutError extends Error {
29 | constructor() {
30 | super('Promise timed out')
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/random/random.spec.ts:
--------------------------------------------------------------------------------
1 | import { mathRandom } from '../testHelpers'
2 | import { random } from './random'
3 |
4 | describe('random', () => {
5 | beforeEach(() => {
6 | mathRandom.setup()
7 | })
8 |
9 | afterEach(() => {
10 | mathRandom.reset()
11 | })
12 |
13 | test('generates a random integer', () => {
14 | expect(random(0, 10)).toEqual(8)
15 | })
16 |
17 | test('generates a random float', () => {
18 | expect(random(0, 10, true)).toEqual(7.341312319841373)
19 | expect(random(3.5, 4.5)).toEqual(4.0153569814278525)
20 | })
21 | })
22 |
23 | describe('random (fuzzing)', () => {
24 | const ITERATIONS = 1000
25 |
26 | test('generates valid integers', () => {
27 | for (let i = 0; i !== ITERATIONS; i++) {
28 | const number = random(2, 10)
29 | expect(number >= 2 && number <= 10).toBeTruthy()
30 | expect(number % 1 === 0).toBeTruthy()
31 | }
32 | })
33 |
34 | test('generates valid floats', () => {
35 | for (let i = 0; i !== ITERATIONS; i++) {
36 | const number = random(2.5, 5.75)
37 | expect(number >= 2.5 && number <= 5.75).toBeTruthy()
38 | }
39 | })
40 |
41 | test('generates valid integers with minimum and maximum bounds', () => {
42 | const isInBounds = (x: number): boolean =>
43 | x >= Number.MIN_SAFE_INTEGER && x <= Number.MAX_SAFE_INTEGER
44 |
45 | for (let i = 0; i !== ITERATIONS; i++) {
46 | const number = random(Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER)
47 |
48 | expect(Number.isNaN(number)).toBeFalsy()
49 | expect(isInBounds(number)).toBeTruthy()
50 | }
51 | })
52 | })
53 |
--------------------------------------------------------------------------------
/src/random/random.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### random(lower, upper, float?)
3 | *
4 | * Generate a random number between `lower` and `upper` (inclusive).
5 | * If `float` is true or `lower` or `upper` is a float, a float is returned instead of an integer.
6 | *
7 | * ```js
8 | * flocky.random(1, 10)
9 | * // -> 8
10 | *
11 | * flocky.random(1, 20, true)
12 | * // -> 14.94849340769861
13 | *
14 | * flocky.random(2.5, 3.5)
15 | * // -> 3.2341312319841373
16 | * ```
17 | */
18 |
19 | export function random(lower: number, upper: number, float?: boolean): number {
20 | if (float || lower % 1 || upper % 1) {
21 | return randomFloat(lower, upper)
22 | }
23 |
24 | return randomInteger(lower, upper)
25 | }
26 |
27 | function randomFloat(lower: number, upper: number): number {
28 | return Math.random() * (upper - lower) + lower
29 | }
30 |
31 | function randomInteger(lower: number, upper: number): number {
32 | return Math.floor(Math.random() * (upper - lower + 1) + lower)
33 | }
34 |
--------------------------------------------------------------------------------
/src/randomString/randomString.spec.ts:
--------------------------------------------------------------------------------
1 | import { mathRandom } from '../testHelpers'
2 | import { randomString } from './randomString'
3 |
4 | describe('randomString', () => {
5 | beforeEach(() => {
6 | mathRandom.setup()
7 | })
8 |
9 | afterEach(() => {
10 | mathRandom.reset()
11 | })
12 |
13 | test('generates a random string', () => {
14 | expect(randomString(5)).toEqual('tfl0g')
15 | expect(randomString(10)).toEqual('9JmILmsJRU')
16 | expect(randomString(3)).toEqual('vPY')
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/src/randomString/randomString.ts:
--------------------------------------------------------------------------------
1 | const CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
2 |
3 | /**
4 | * ### randomString(length)
5 | *
6 | * Generate a random alphanumeric string with `length` characters.
7 | *
8 | * ```js
9 | * flocky.randomString(5)
10 | * // -> 'tfl0g'
11 | * ```
12 | */
13 |
14 | export function randomString(length: number): string {
15 | let string = ''
16 |
17 | for (let i = 0; i !== length; i++) {
18 | const index = Math.floor(Math.random() * CHARACTERS.length)
19 | string += CHARACTERS.charAt(index)
20 | }
21 |
22 | return string
23 | }
24 |
--------------------------------------------------------------------------------
/src/range/range.spec.ts:
--------------------------------------------------------------------------------
1 | import { range } from './range'
2 |
3 | describe('range', () => {
4 | test('generates a range of numbers', () => {
5 | expect(range(0, 1)).toEqual([0, 1])
6 | expect(range(1, 10)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
7 |
8 | expect(range(-4, -1)).toEqual([-4, -3, -2, -1])
9 | expect(range(-1, -6)).toEqual([-1, -2, -3, -4, -5, -6])
10 | })
11 |
12 | test('generates a range of numbers with a custom step', () => {
13 | expect(range(0, 10, 2)).toEqual([0, 2, 4, 6, 8, 10])
14 | expect(range(-1, -11, 2)).toEqual([-1, -3, -5, -7, -9, -11])
15 | })
16 |
17 | test('does not generate an out of bounds range', () => {
18 | expect(range(0, 0)).toEqual([0])
19 | expect(range(0, 12, 10)).toEqual([0, 10])
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/src/range/range.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### range(start, end, step?)
3 | *
4 | * Generate an array of numbers progressing from `start` up to and including `end`.
5 | *
6 | * ```js
7 | * flocky.range(0, 5)
8 | * // -> [0, 1, 2, 3, 4, 5]
9 | *
10 | * flocky.range(-5, -10)
11 | * // -> [-5, -6, -7, -8, -9, -10]
12 | *
13 | * flocky.range(-6, -12, 2)
14 | * // -> [-6, -8, -10, -12]
15 | * ```
16 | */
17 |
18 | export function range(start: number, end: number, step = 1): Array {
19 | const array = []
20 | const positive = start <= end
21 | let value = start
22 |
23 | if (positive) {
24 | while (value <= end) {
25 | array.push(value)
26 | value += step
27 | }
28 | } else {
29 | while (value >= end) {
30 | array.push(value)
31 | value -= step
32 | }
33 | }
34 |
35 | return array
36 | }
37 |
--------------------------------------------------------------------------------
/src/roundTo/roundTo.spec.ts:
--------------------------------------------------------------------------------
1 | import { roundTo } from './roundTo'
2 |
3 | describe('roundTo', () => {
4 | test('rounds the number to the expected precision', () => {
5 | // General usage with integers
6 | expect(roundTo(1, 0)).toEqual(1)
7 | expect(roundTo(1, 3)).toEqual(1)
8 |
9 | // General usage with floats
10 | expect(roundTo(0.129, 1)).toEqual(0.1)
11 | expect(roundTo(0.129, 2)).toEqual(0.13)
12 | expect(roundTo(0.129, 3)).toEqual(0.129)
13 |
14 | // Edge cases for floating point weirdness
15 | expect(roundTo(1.005, 2)).toEqual(1.01)
16 | expect(roundTo(1.005, 0)).toEqual(1)
17 |
18 | // Negative precision
19 | expect(roundTo(111.1, -2)).toEqual(100)
20 |
21 | // Negative numbers
22 | expect(roundTo(-3.51, 1)).toEqual(-3.5)
23 | expect(roundTo(-0.375, 2)).toEqual(-0.38)
24 |
25 | // Edge cases for maximum floating point precision
26 | expect(roundTo(1e20, 1)).toEqual(1e20)
27 | expect(roundTo(10000000000000.123, 8)).toEqual(10000000000000.123)
28 | expect(roundTo(0.37542323423423432432432432432, 8)).toEqual(0.37542323) // eslint-disable-line no-loss-of-precision
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/src/roundTo/roundTo.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### roundTo(number, precision)
3 | *
4 | * Round a floating point number to `precision` decimal places.
5 | *
6 | * ```js
7 | * flocky.roundTo(3.141592653589, 4)
8 | * // -> 3.1416
9 | *
10 | * flocky.roundTo(1.005, 2)
11 | * // -> 1.01
12 | *
13 | * flocky.roundTo(1111.1, -2)
14 | * // -> 1100
15 | * ```
16 | *
17 | *
18 | * Implementation Details
19 | *
20 | * This method avoids floating-point errors by adjusting the exponent part of
21 | * the string representation of a number instead of multiplying and dividing
22 | * with powers of 10. The implementation is based on [this example](https://stackoverflow.com/a/60098416)
23 | * by Lam Wei Li.
24 | *
25 | */
26 |
27 | export function roundTo(number: number, precision: number): number {
28 | const isNegative = number < 0
29 |
30 | // We can only work with positive numbers in the next steps, use the absolute
31 | number = Math.abs(number)
32 |
33 | // Shift the decimal point to the right by `precision` places and round the result
34 | // e.g. 1.456745 with precision 3 -> 1456.745 -> 1457
35 | number = Math.round(shift(number, precision))
36 |
37 | // Shift the decimal point back to the left by `precision` places
38 | // e.g. 1457 with precision 3 -> 1.457
39 | number = shift(number, -precision)
40 |
41 | // If the original number was negative, the rounded number is too
42 | if (isNegative) {
43 | number = -number
44 | }
45 |
46 | return number
47 | }
48 |
49 | // Shift the decimal point of a `number` by `exponent` places
50 | function shift(number: number, exponent: number): number {
51 | const [numberBase, numberExponent] = `${number}e`.split('e')
52 | return Number(`${numberBase}e${Number(numberExponent) + exponent}`)
53 | }
54 |
--------------------------------------------------------------------------------
/src/sample/sample.spec.ts:
--------------------------------------------------------------------------------
1 | import { mathRandom } from '../testHelpers'
2 | import { sample } from './sample'
3 |
4 | describe('sample', () => {
5 | beforeEach(() => {
6 | mathRandom.setup()
7 | })
8 |
9 | afterEach(() => {
10 | mathRandom.reset()
11 | })
12 |
13 | test('samples primitive arrays', () => {
14 | const original = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
15 | const element = sample(original)
16 |
17 | expect(original).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
18 | expect(element).toEqual(8)
19 | })
20 |
21 | test('samples object arrays', () => {
22 | const original = [{ a: 1 }, { b: 1 }, { c: 1 }, { d: 1 }, { e: 1 }]
23 | const element = sample(original)
24 |
25 | expect(original).toEqual([{ a: 1 }, { b: 1 }, { c: 1 }, { d: 1 }, { e: 1 }])
26 | expect(element).toEqual({ d: 1 })
27 | })
28 | })
29 |
30 | describe('sample (fuzzing)', () => {
31 | const ITERATIONS = 1000
32 |
33 | test('does not get out of bound elements', () => {
34 | for (let i = 0; i !== ITERATIONS; i++) {
35 | const number = sample([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
36 | expect(number).toBeTruthy()
37 | }
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/src/sample/sample.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### sample(array)
3 | *
4 | * Get a random element from the `array`.
5 | *
6 | * ```js
7 | * flocky.sample([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
8 | * // -> 8
9 | * ```
10 | */
11 |
12 | export function sample(array: Array): T {
13 | const index = Math.floor(Math.random() * array.length)
14 | return array[index]
15 | }
16 |
--------------------------------------------------------------------------------
/src/shuffle/shuffle.spec.ts:
--------------------------------------------------------------------------------
1 | import { mathRandom } from '../testHelpers'
2 | import { shuffle } from './shuffle'
3 |
4 | describe('shuffle', () => {
5 | beforeEach(() => {
6 | mathRandom.setup()
7 | })
8 |
9 | afterEach(() => {
10 | mathRandom.reset()
11 | })
12 |
13 | test('shuffles primitive arrays', () => {
14 | const original = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
15 | const shuffled = shuffle(original)
16 |
17 | expect(original).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
18 | expect(shuffled).toEqual([3, 7, 2, 1, 10, 4, 6, 9, 5, 8])
19 | })
20 |
21 | test('shuffles object arrays', () => {
22 | const original = [{ a: 1 }, { b: 1 }, { c: 1 }, { d: 1 }, { e: 1 }]
23 | const shuffled = shuffle(original)
24 |
25 | expect(original).toEqual([{ a: 1 }, { b: 1 }, { c: 1 }, { d: 1 }, { e: 1 }])
26 | expect(shuffled).toEqual([{ a: 1 }, { e: 1 }, { b: 1 }, { c: 1 }, { d: 1 }])
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/src/shuffle/shuffle.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### shuffle(array)
3 | *
4 | * Create an array of shuffled values.
5 | *
6 | * ```js
7 | * flocky.shuffle([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
8 | * // -> [3, 7, 2, 1, 10, 4, 6, 9, 5, 8]
9 | * ```
10 | *
11 | *
12 | * Implementation Details
13 | *
14 | * This method uses a modern version of the
15 | * [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm).
16 | *
17 | */
18 |
19 | export function shuffle(array: Array): Array {
20 | // Create a copy of the array so we don't mutate the input
21 | array = array.concat()
22 |
23 | // Treat the end of the array as shuffled and the start as unshuffled.
24 | // The shuffling is accomplished by going through the array from end to start,
25 | // generating a random index up to the current index, and then swapping the
26 | // place of the element at that random index with the current index.
27 | //
28 | // ['a', 'b', 'c', 'd', 'e']
29 | // ^ Random index between 0 - 4 (e.g. 2: swap 'e' with 'c')
30 | // ['a', 'b', 'e', 'd', 'c']
31 | // ^ Random index between 0 - 3 (e.g. 1: swap 'd' with 'b')
32 | // ['a', 'd', 'e', 'b', 'c']
33 | // ^ Random index between 0 - 2 (e.g. 0: swap 'e' with 'a')
34 | // ['e', 'd', 'a', 'b', 'c']
35 | // ^ Random index between 0 - 1 (e.g. 0: swap 'd' with 'e')
36 | // ['d', 'e', 'a', 'b', 'c']
37 | for (let i = array.length - 1; i > 0; i--) {
38 | const j = Math.floor(Math.random() * (i + 1))
39 |
40 | const tmp = array[i]
41 | array[i] = array[j]
42 | array[j] = tmp
43 | }
44 |
45 | return array
46 | }
47 |
--------------------------------------------------------------------------------
/src/sleep/sleep.spec.ts:
--------------------------------------------------------------------------------
1 | import { expectApproximateDuration } from '../testHelpers'
2 | import { sleep } from './sleep'
3 |
4 | describe('sleep', () => {
5 | test('waits for the specified time', async () => {
6 | const start = new Date()
7 | await sleep(100)
8 | const end = new Date()
9 |
10 | expectApproximateDuration(start, end, 100)
11 | })
12 | })
13 |
--------------------------------------------------------------------------------
/src/sleep/sleep.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### sleep(ms)
3 | *
4 | * Return a promise that waits for `ms` milliseconds before resolving.
5 | *
6 | * ```js
7 | * await flocky.sleep(25)
8 | * ```
9 | */
10 |
11 | export function sleep(ms: number): Promise {
12 | return new Promise((resolve) => setTimeout(resolve, ms))
13 | }
14 |
--------------------------------------------------------------------------------
/src/slugify/slugify.spec.ts:
--------------------------------------------------------------------------------
1 | import { slugify } from './slugify'
2 |
3 | describe('slugify', () => {
4 | test('generates correct slugs', () => {
5 | expect(slugify('Hi There')).toEqual('hi-there')
6 | expect(slugify('Hi-- There')).toEqual('hi-there')
7 | expect(slugify(' Hi 123_ There ')).toEqual('hi-123-there')
8 | expect(slugify(' #;123-0sdg--+++asofinasg.d/f23XXX£948// ')).toEqual(
9 | '123-0sdg-asofinasg-d-f23xxx-948'
10 | )
11 | expect(slugify('unicode ♥ is ☢')).toEqual('unicode-is')
12 | expect(slugify('---trim---')).toEqual('trim')
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/src/slugify/slugify.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### slugify(string)
3 | *
4 | * Generate a URL-safe slug of a string.
5 | *
6 | * ```js
7 | * flocky.slugify(' Issue #123 is _important_! :)')
8 | * // -> 'issue-123-is-important'
9 | * ```
10 | */
11 |
12 | export function slugify(string: string): string {
13 | return string
14 | .trim()
15 | .toLowerCase()
16 | .replace(/[^a-z0-9]+/g, '-') // Replace all clusters of non-word characters with a single "-"
17 | .replace(/^-|-$/g, '') // Trim "-" from start and end
18 | }
19 |
--------------------------------------------------------------------------------
/src/sum/sum.spec.ts:
--------------------------------------------------------------------------------
1 | import { sum } from './sum'
2 |
3 | describe('sum', () => {
4 | test('calculates the sum of an array of numbers', () => {
5 | expect(sum([1, 4, 2, 0])).toEqual(7)
6 | expect(sum([9, 4, 3, 7, 79, -60])).toEqual(42)
7 | expect(sum([])).toEqual(0)
8 | })
9 |
10 | test('does not mutate the input', () => {
11 | const input = [1, 2, 3]
12 | sum(input)
13 | expect(input).toEqual([1, 2, 3])
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/src/sum/sum.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### sum(array)
3 | *
4 | * Compute the sum of the values in an array.
5 | *
6 | * ```js
7 | * flocky.sum([1, 4, 2, -4, 0])
8 | * // -> 3
9 | * ```
10 | */
11 |
12 | export function sum(array: Array): number {
13 | return array.reduce((a, b) => a + b, 0)
14 | }
15 |
--------------------------------------------------------------------------------
/src/testHelpers.ts:
--------------------------------------------------------------------------------
1 | const RANDOM_OUTPUT = [
2 | 0.7341312319841373, 0.5153569814278522, 0.6039244693324568, 0.8485123814547479,
3 | 0.5305323323446391, 0.9936904462715297, 0.152741118641178, 0.6284452050928289, 0.1333952722446896,
4 | 0.19323989178442735, 0.6210946032149509, 0.715356956262492, 0.1601444486850756,
5 | 0.2826657106671773, 0.3360975043209571, 0.7686446730864509, 0.2482518764495567,
6 | 0.39590294569110873, 0.8810226597671855, 0.8645898520605233, 0.08819117131655063,
7 | 0.9746880765364763, 0.8191862710617215, 0.6093775445189631, 0.5680783823402815,
8 | 0.4094440218703661, 0.03730391719730841, 0.17578915337316725, 0.8865419358339801,
9 | 0.2227008300658413,
10 | ]
11 |
12 | let randomIndex = 0
13 | let globalMathRandom = Math.random
14 |
15 | export const mathRandom = {
16 | setup: (): void => {
17 | randomIndex = 0
18 | globalMathRandom = Math.random
19 | Math.random = (): number => RANDOM_OUTPUT[randomIndex++]
20 | },
21 | reset: (): void => {
22 | Math.random = globalMathRandom
23 | },
24 | }
25 |
26 | let globalDateNow = Date.now
27 |
28 | export const dateNow = {
29 | setup: (): void => {
30 | globalDateNow = Date.now
31 | Date.now = (): number => 1551647486832
32 | },
33 | reset: (): void => {
34 | Date.now = globalDateNow
35 | },
36 | }
37 |
38 | export function expectApproximateDuration(start: Date, end: Date, duration: number): void {
39 | const BUFFER = 25
40 |
41 | expect(end.getTime() - start.getTime()).toBeGreaterThan(duration - BUFFER)
42 | expect(end.getTime() - start.getTime()).toBeLessThan(duration + BUFFER)
43 | }
44 |
--------------------------------------------------------------------------------
/src/throttle/throttle.spec.ts:
--------------------------------------------------------------------------------
1 | import { sleep } from '../sleep/sleep'
2 | import { throttle } from './throttle'
3 |
4 | describe('throttle', () => {
5 | test('throttles the function call', async () => {
6 | const func = jest.fn()
7 |
8 | const throttledFunc = throttle(func, 25)
9 | throttledFunc('a')
10 | throttledFunc('ab')
11 | throttledFunc('abc')
12 | throttledFunc('abcd')
13 |
14 | expect(func.mock.calls).toEqual([['a']])
15 | await sleep(50)
16 | expect(func.mock.calls).toEqual([['a'], ['abcd']])
17 |
18 | throttledFunc('abcde')
19 | throttledFunc('abcdef')
20 |
21 | expect(func.mock.calls).toEqual([['a'], ['abcd'], ['abcde']])
22 | await sleep(25)
23 | expect(func.mock.calls).toEqual([['a'], ['abcd'], ['abcde'], ['abcdef']])
24 | })
25 |
26 | test('has the correct type', () => {
27 | const func = (a: number, b: number): number => {
28 | return a + b
29 | }
30 |
31 | const throttledFunc = throttle(func, 250)
32 |
33 | // @ts-expect-error The first argument has to be a number
34 | throttledFunc('a', 2)
35 |
36 | // @ts-expect-error The second argument has to be a number
37 | throttledFunc(2, 'a')
38 |
39 | // @ts-expect-error The return value is void
40 | throttledFunc(2, 2)?.concat()
41 |
42 | // @ts-expect-error The return value is void
43 | throttledFunc(2, 2)?.toFixed()
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/src/throttle/throttle.ts:
--------------------------------------------------------------------------------
1 | import { TAnyFunction } from '../typeHelpers'
2 |
3 | /**
4 | * ### throttle(func, wait)
5 | *
6 | * Create a throttled function that invokes `func` at most every `wait` milliseconds.
7 | * If the invocation is throttled, `func` will be invoked with the last arguments provided.
8 | *
9 | * ```js
10 | * const func = () => console.log('Heavy processing happening')
11 | * const throttledFunc = flocky.throttle(func, 250)
12 | * ```
13 | */
14 |
15 | type FunctionWithVoidReturn> = (...args: Parameters) => void
16 |
17 | export function throttle>(
18 | func: TFunc,
19 | wait: number
20 | ): FunctionWithVoidReturn {
21 | let timeoutID: NodeJS.Timeout | null = null
22 | let lastCall = 0
23 |
24 | return function (this: unknown, ...args: Array) {
25 | if (timeoutID) clearTimeout(timeoutID)
26 |
27 | const remainingWait = wait - (Date.now() - lastCall)
28 |
29 | const callFunc = (): void => {
30 | func.apply(this, args)
31 | lastCall = Date.now()
32 | }
33 |
34 | // Immediately call the function if we don't have to throttle it
35 | if (remainingWait <= 0 || remainingWait > wait) {
36 | return callFunc()
37 | }
38 |
39 | timeoutID = setTimeout(() => callFunc(), remainingWait)
40 | } as FunctionWithVoidReturn
41 | }
42 |
--------------------------------------------------------------------------------
/src/toMap/toMap.spec.ts:
--------------------------------------------------------------------------------
1 | import { toMap } from './toMap'
2 |
3 | describe('toMap', () => {
4 | test('generates a map with no target', () => {
5 | const input = [
6 | { id: 1, name: 'Anna', age: 64 },
7 | { id: 2, name: 'Bertha', age: 57 },
8 | {
9 | id: 3,
10 | name: 'Chris',
11 | age: 19,
12 | alignment: { evil: 3, neutral: 1, good: 0 },
13 | },
14 | ]
15 |
16 | expect(toMap(input, 'id')).toEqual({
17 | 1: { id: 1, name: 'Anna', age: 64 },
18 | 2: { id: 2, name: 'Bertha', age: 57 },
19 | 3: {
20 | id: 3,
21 | name: 'Chris',
22 | age: 19,
23 | alignment: { evil: 3, neutral: 1, good: 0 },
24 | },
25 | })
26 |
27 | expect(toMap(input, 'name')).toEqual({
28 | Anna: { id: 1, name: 'Anna', age: 64 },
29 | Bertha: { id: 2, name: 'Bertha', age: 57 },
30 | Chris: {
31 | id: 3,
32 | name: 'Chris',
33 | age: 19,
34 | alignment: { evil: 3, neutral: 1, good: 0 },
35 | },
36 | })
37 |
38 | expect(toMap(input, 'age')).toEqual({
39 | 64: { id: 1, name: 'Anna', age: 64 },
40 | 57: { id: 2, name: 'Bertha', age: 57 },
41 | 19: {
42 | id: 3,
43 | name: 'Chris',
44 | age: 19,
45 | alignment: { evil: 3, neutral: 1, good: 0 },
46 | },
47 | })
48 | })
49 |
50 | test('generates a map with a target', () => {
51 | const input = [
52 | { id: 1, name: 'Anna', age: 64 },
53 | { id: 2, name: 'Bertha', age: 57 },
54 | { id: 3, name: 'Chris', age: 19 },
55 | ]
56 |
57 | expect(toMap(input, 'id', 'age')).toEqual({
58 | 1: 64,
59 | 2: 57,
60 | 3: 19,
61 | })
62 |
63 | expect(toMap(input, 'name', 'name')).toEqual({
64 | Anna: 'Anna',
65 | Bertha: 'Bertha',
66 | Chris: 'Chris',
67 | })
68 |
69 | expect(toMap(input, 'age', 'id')).toEqual({
70 | 64: 1,
71 | 57: 2,
72 | 19: 3,
73 | })
74 | })
75 |
76 | test('filters elements if the key does not exist', () => {
77 | const input = [
78 | { id: 1, name: 'Anna', age: 64 },
79 | { id: 2, name: 'Bertha', age: 57 },
80 | { name: 'Chris Clone', age: 80 },
81 | { id: 3, name: 'Chris', age: 19 },
82 | ]
83 |
84 | expect(toMap(input, 'id', 'age')).toEqual({
85 | 1: 64,
86 | 2: 57,
87 | 3: 19,
88 | })
89 | })
90 |
91 | test('does not mutate the input', () => {
92 | const input = [
93 | { id: 1, name: 'Anna' },
94 | { id: 2, name: 'Bertha' },
95 | { id: 3, name: 'Chris' },
96 | ]
97 |
98 | toMap(input, 'id')
99 | expect(input).toEqual([
100 | { id: 1, name: 'Anna' },
101 | { id: 2, name: 'Bertha' },
102 | { id: 3, name: 'Chris' },
103 | ])
104 | })
105 | })
106 |
--------------------------------------------------------------------------------
/src/toMap/toMap.ts:
--------------------------------------------------------------------------------
1 | // Return all keys of an object that have value types we can use as a map key
2 | type MappableKeys = NonNullable<
3 | {
4 | [K in keyof T]: T[K] extends string | number | undefined ? K : never
5 | }[keyof T]
6 | >
7 |
8 | /**
9 | * ### toMap(array, key, target?)
10 | *
11 | * Create a lookup map out of an `array` of objects, with a lookup `key` and an optional `target`.
12 | *
13 | * ```js
14 | * flocky.toMap(
15 | * [
16 | * { id: 1, name: 'Stanley', age: 64 },
17 | * { id: 2, name: 'Juliet', age: 57 },
18 | * { id: 3, name: 'Alex', age: 19 }
19 | * ],
20 | * 'id'
21 | * )
22 | * // -> {
23 | * // -> 1: { id: 1, name: 'Stanley', age: 64 },
24 | * // -> 2: { id: 2, name: 'Juliet', age: 57 },
25 | * // -> 3: { id: 3, name: 'Alex', age: 19 }
26 | * // -> }
27 | *
28 | * flocky.toMap(
29 | * [
30 | * { id: 1, name: 'Stanley', age: 64 },
31 | * { id: 2, name: 'Juliet', age: 57 },
32 | * { id: 3, name: 'Alex', age: 19 }
33 | * ],
34 | * 'name',
35 | * 'age'
36 | * )
37 | * // -> { Stanley: 64, Juliet: 57, Alex: 19 }
38 | * ```
39 | */
40 |
41 | export function toMap>(
42 | array: Array,
43 | key: Key
44 | ): { [key: string]: Element | undefined }
45 |
46 | export function toMap<
47 | Element extends object,
48 | Key extends MappableKeys,
49 | Target extends keyof Element,
50 | >(array: Array, key: Key, target: Target): { [key: string]: Element[Target] | undefined }
51 |
52 | export function toMap<
53 | Element extends object,
54 | Key extends MappableKeys,
55 | Target extends keyof Element,
56 | >(
57 | array: Array,
58 | key: Key,
59 | target?: Target
60 | ): { [key: string]: Element | Element[Target] | undefined } {
61 | const map: { [key: string]: Element | Element[Target] | undefined } = {}
62 |
63 | array.map((element) => {
64 | if (!element[key]) return
65 | map[element[key] as unknown as string] = target ? element[target] : element
66 | })
67 |
68 | return map
69 | }
70 |
--------------------------------------------------------------------------------
/src/typeHelpers.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 |
3 | export type JSONValue = string | number | boolean | null | undefined | JSONObject | JSONArray
4 | type JSONObject = { [key: string]: JSONValue }
5 | type JSONArray = Array
6 |
7 | export type TAnyFunction = (...args: Array) => TReturn
8 |
9 | export type Simplify = T extends unknown ? { [K in keyof T]: Simplify } : never
10 |
11 | export type RecursiveUnionToIntersection = {
12 | [TKey in keyof TObjectWithUnions]: TObjectWithUnions[TKey] extends object
13 | ? RecursiveUnionToIntersection>
14 | : TObjectWithUnions[TKey]
15 | }
16 |
17 | export type UnionToIntersection = (
18 | TUnion extends any ? (k: TUnion) => void : never
19 | ) extends (k: infer TIntersection) => void
20 | ? TIntersection
21 | : never
22 |
--------------------------------------------------------------------------------
/src/unflatten/unflatten.spec.ts:
--------------------------------------------------------------------------------
1 | import { unflatten } from './unflatten'
2 |
3 | describe('unflatten', () => {
4 | test('unflattens the object', () => {
5 | expect(unflatten({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 })
6 |
7 | expect(unflatten({ 'a.b': 1 })).toEqual({ a: { b: 1 } })
8 | expect(unflatten({ 'a.b': 1, 'a.c': 2 })).toEqual({ a: { b: 1, c: 2 } })
9 | expect(unflatten({ 'a.b': 1, 'a.c': 2, 'd.e': 3 })).toEqual({ a: { b: 1, c: 2 }, d: { e: 3 } })
10 |
11 | expect(unflatten({ 'a.b.c': 1 })).toEqual({ a: { b: { c: 1 } } })
12 | expect(unflatten({ 'a.b.c': 1, 'a.b.d': 2 })).toEqual({ a: { b: { c: 1, d: 2 } } })
13 | expect(unflatten({ 'a.b.c': 1, 'a.b.d': 2, 'e.f.g': 3 })).toEqual({
14 | a: { b: { c: 1, d: 2 } },
15 | e: { f: { g: 3 } },
16 | })
17 |
18 | // Check that the types are correct
19 | const result = unflatten({ 'a.a.a': 1, 'a.b.c': 1, 'a.b.d': 2, 'e.f.g': 3 })
20 | expect(result.a.a.a + 1).toEqual(2)
21 | expect(result.a.b.c + 1).toEqual(2)
22 | expect(result.a.b.d + 1).toEqual(3)
23 | expect(result.e.f.g + 1).toEqual(4)
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/src/unflatten/unflatten.ts:
--------------------------------------------------------------------------------
1 | import { RecursiveUnionToIntersection, Simplify } from '../typeHelpers'
2 |
3 | /**
4 | * ### unflatten(object)
5 | *
6 | * Unflattens an object with dot notation keys into a nested object.
7 | *
8 | * ```js
9 | * flocky.unflatten({ 'a.b': 1, 'a.c': 2, 'd.e.f': 3 })
10 | * // -> { a: { b: 1, c: 2 }, d: { e: { f: 3 } } }
11 | * ```
12 | */
13 |
14 | type UnflattenObject> = {
15 | [TKey in keyof TObject as TKey extends `${infer TKeyPrefix}.${string}`
16 | ? TKeyPrefix
17 | : TKey]: TKey extends `${string}.${infer TKeySuffix}`
18 | ? UnflattenObject<{ [key in TKeySuffix]: TObject[TKey] }>
19 | : TObject[TKey]
20 | } extends infer TResult
21 | ? { [TResultKey in keyof TResult]: TResult[TResultKey] }
22 | : never
23 |
24 | export function unflatten>(
25 | object: TObject
26 | ): Simplify>> {
27 | const result: Record = {}
28 |
29 | for (const key in object) {
30 | const keys = key.split('.')
31 | let current = result
32 |
33 | for (let i = 0; i < keys.length; i++) {
34 | const part = keys[i]
35 |
36 | if (i === keys.length - 1) {
37 | current[part] = object[key]
38 | } else {
39 | if (!current[part]) current[part] = {}
40 | current = current[part] as Record
41 | }
42 | }
43 | }
44 |
45 | return result as Simplify>>
46 | }
47 |
--------------------------------------------------------------------------------
/src/unique/BENCHMARK.md:
--------------------------------------------------------------------------------
1 | ### Benchmark for `unique`
2 |
3 | [Source for this benchmark](./unique.benchmark.ts)
4 |
5 | | | es6 filter | lodash | flocky |
6 | | ----------- | ------------------------ | ------------------------ | ----------------------------- |
7 | | large array | 40 ops/sec (2.76%) | 1,313 ops/sec (90.49%) | **1,451 ops/sec (100.00%)** |
8 | | small array | 205,293 ops/sec (85.34%) | 187,378 ops/sec (77.89%) | **240,559 ops/sec (100.00%)** |
9 |
10 | Generated at 2024-07-20 with Node.JS v20.9.0
11 |
--------------------------------------------------------------------------------
/src/unique/unique.benchmark.ts:
--------------------------------------------------------------------------------
1 | import lodash from 'lodash'
2 | import { Benchmark } from '../benchmarkHelper'
3 | import { unique } from './unique'
4 |
5 | function generateArrayOfSize(size: number): Array {
6 | const array: Array = []
7 |
8 | for (let i = 0; i !== size; i++) {
9 | array.push(Math.random())
10 | }
11 |
12 | return array
13 | }
14 |
15 | const LARGE_ARRAY = generateArrayOfSize(10000)
16 | const SMALL_ARRAY = generateArrayOfSize(100)
17 | console.log('Setup finished')
18 |
19 | const benchmark = new Benchmark('unique')
20 |
21 | benchmark.add({
22 | library: 'es6 filter',
23 | input: 'large array',
24 | func: () => LARGE_ARRAY.filter((x, i, self) => self.indexOf(x) === i),
25 | })
26 |
27 | benchmark.add({
28 | library: 'es6 filter',
29 | input: 'small array',
30 | func: () => SMALL_ARRAY.filter((x, i, self) => self.indexOf(x) === i),
31 | })
32 |
33 | benchmark.add({
34 | library: 'lodash',
35 | input: 'large array',
36 | func: () => lodash.uniq(LARGE_ARRAY),
37 | })
38 |
39 | benchmark.add({
40 | library: 'lodash',
41 | input: 'small array',
42 | func: () => lodash.uniq(SMALL_ARRAY),
43 | })
44 |
45 | benchmark.add({
46 | library: 'flocky',
47 | input: 'large array',
48 | func: () => unique(LARGE_ARRAY),
49 | })
50 |
51 | benchmark.add({
52 | library: 'flocky',
53 | input: 'small array',
54 | func: () => unique(SMALL_ARRAY),
55 | })
56 |
57 | benchmark.run()
58 |
--------------------------------------------------------------------------------
/src/unique/unique.spec.ts:
--------------------------------------------------------------------------------
1 | import { unique } from './unique'
2 |
3 | describe('unique', () => {
4 | test('filters the unique occurrences of an array of numbers', () => {
5 | expect(unique([1, 4, 2, 0])).toEqual([1, 4, 2, 0])
6 | expect(unique([1, 1, 4, 2, 0, 2, 0])).toEqual([1, 4, 2, 0])
7 | })
8 |
9 | test('filters the unique occurrences of an array of strings', () => {
10 | expect(unique(['foo', 'bar', 'foo', 'foobar', 'foo', 'foo'])).toEqual(['foo', 'bar', 'foobar'])
11 | })
12 |
13 | test('filters the unique occurrences of an array of objects with an identity function', () => {
14 | const input = [
15 | { id: 1, name: 'Foo1' },
16 | { id: 2, name: 'Foo2' },
17 | { id: 1, name: 'Foo3' },
18 | { id: 3, name: 'Foo4' },
19 | { id: 2, name: 'Foo5' },
20 | ]
21 |
22 | const expected = [
23 | { id: 1, name: 'Foo1' },
24 | { id: 2, name: 'Foo2' },
25 | { id: 3, name: 'Foo4' },
26 | ]
27 |
28 | expect(unique(input, (element) => element.id)).toEqual(expected)
29 | })
30 |
31 | test('does not mutate the input', () => {
32 | const input = [1, 2, 3, 1]
33 | unique(input)
34 | expect(input).toEqual([1, 2, 3, 1])
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/src/unique/unique.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ### unique(array, identity?)
3 | *
4 | * Create a duplicate-free version of an array, in which only the first occurrence of each element is kept.
5 | * The order of result values is determined by the order they occur in the array.
6 | * Can be passed an optional `identity` function to select the identifying part of objects.
7 | *
8 | * ```js
9 | * flocky.unique([1, 1, 2, 4, 2, 1, 6])
10 | * // -> [1, 2, 4, 6]
11 | *
12 | * flocky.unique(['foo', 'bar', 'foo', 'foobar'])
13 | * // -> ['foo', 'bar', 'foobar']
14 | *
15 | * const input = [{ id: 1, a: 1 }, { id: 1, a: 2 }, { id: 2, a: 3 }, { id: 1, a: 4 }]
16 | * flocky.unique(input, (element) => element.id)
17 | * // -> [{ id: 1, a: 1 }, { id: 2, a: 3 }]
18 | * ```
19 | */
20 |
21 | export function unique(array: Array, identity?: (x: T) => unknown): Array {
22 | if (!identity) {
23 | return primitiveUnique(array)
24 | }
25 |
26 | return objectUnique(array, identity)
27 | }
28 |
29 | function primitiveUnique(array: Array): Array {
30 | return Array.from(new Set(array))
31 | }
32 |
33 | function objectUnique(array: Array, identity: (x: T) => unknown): Array {
34 | const identities = array.map((x) => identity(x))
35 | return array.filter((_, i) => identities.indexOf(identities[i]) === i)
36 | }
37 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Output Options
4 | "target": "es2022",
5 | "module": "esnext",
6 | "jsx": "react-jsx",
7 | "noEmitOnError": true,
8 | "newLine": "lf",
9 | "outDir": "dist/",
10 | "baseUrl": "./",
11 | "declaration": true,
12 | "sourceMap": true,
13 | "removeComments": false,
14 | "emitDecoratorMetadata": true,
15 | "experimentalDecorators": true,
16 | "incremental": true,
17 |
18 | // Type-Checking Options
19 | "lib": ["es2023", "dom"],
20 | "strict": true,
21 | "skipLibCheck": true,
22 | "strictPropertyInitialization": false,
23 | "noUnusedLocals": true,
24 | "noUnusedParameters": true,
25 |
26 | // Module Resolution Options
27 | "moduleResolution": "node",
28 | "allowSyntheticDefaultImports": true,
29 | "esModuleInterop": true,
30 | "forceConsistentCasingInFileNames": true,
31 | "resolveJsonModule": true,
32 | "allowJs": false
33 | },
34 | "include": ["src/"]
35 | }
36 |
--------------------------------------------------------------------------------