@wizhut_tech/wizjs 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.yml +59 -0
- package/CLAUDE.md +11 -9
- package/__tests__/lang_arrays.test.js +44 -2
- package/__tests__/lang_checks.test.js +63 -2
- package/__tests__/lang_collections.test.js +90 -0
- package/__tests__/lang_functools.test.js +123 -2
- package/__tests__/lang_itertools.test.js +210 -3
- package/__tests__/lang_objects.test.js +71 -0
- package/__tests__/math_numbers.test.js +40 -1
- package/docs/lang_arrays.md +4 -1
- package/docs/lang_checks.md +4 -0
- package/docs/lang_collections.md +24 -0
- package/docs/lang_functools.md +8 -1
- package/docs/lang_itertools.md +20 -1
- package/docs/lang_objects.md +5 -0
- package/docs/math_numbers.md +4 -1
- package/index.js +5 -1
- package/package.json +2 -2
- package/readme.md +6 -2
- package/src/lang/arrays.js +74 -3
- package/src/lang/checks.js +42 -2
- package/src/lang/collections.js +191 -0
- package/src/lang/functools.js +135 -2
- package/src/lang/itertools.js +511 -8
- package/src/lang/objects.js +123 -0
- package/src/math/numbers.js +59 -2
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Tag-driven: pushing v<x.y.z> runs the suite, publishes @wizhut_tech/wizjs to
|
|
4
|
+
# npm and opens a GitHub release. Cut one with:
|
|
5
|
+
# npm version patch && git push --follow-tags
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags:
|
|
9
|
+
- "v*"
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: write # gh release create
|
|
13
|
+
id-token: write # npm trusted publishing (OIDC)
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
release:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v7
|
|
21
|
+
with:
|
|
22
|
+
fetch-depth: 0 # --generate-notes diffs against the previous tag
|
|
23
|
+
|
|
24
|
+
# Do not set registry-url here: setup-node would write an .npmrc with an
|
|
25
|
+
# empty _authToken and that short-circuits npm OIDC trusted publishing
|
|
26
|
+
# (the CLI then 404s the PUT as if the package did not exist).
|
|
27
|
+
- uses: actions/setup-node@v7
|
|
28
|
+
with:
|
|
29
|
+
node-version: "24"
|
|
30
|
+
|
|
31
|
+
# `npm version` keeps these in step; a hand-made tag might not.
|
|
32
|
+
- name: Check the tag matches package.json
|
|
33
|
+
run: |
|
|
34
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
35
|
+
pkg="$(node -p "require('./package.json').version")"
|
|
36
|
+
if [ "$tag" != "$pkg" ]; then
|
|
37
|
+
echo "::error::tag $GITHUB_REF_NAME does not match package.json version $pkg"
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
- run: npm ci
|
|
42
|
+
- run: npm test
|
|
43
|
+
|
|
44
|
+
# Trusted publishing: npmjs.com is configured to trust this repo's
|
|
45
|
+
# release.yml, so there is no NPM_TOKEN — the OIDC id-token is the
|
|
46
|
+
# credential, and npm attaches a provenance attestation. Needs npm
|
|
47
|
+
# >= 11.5.1, hence the upgrade.
|
|
48
|
+
- name: Publish to npm
|
|
49
|
+
run: |
|
|
50
|
+
npm install -g npm@latest
|
|
51
|
+
npm publish --access public
|
|
52
|
+
|
|
53
|
+
- name: Publish the GitHub release
|
|
54
|
+
env:
|
|
55
|
+
GH_TOKEN: ${{ github.token }}
|
|
56
|
+
run: |
|
|
57
|
+
gh release create "$GITHUB_REF_NAME" \
|
|
58
|
+
--title "wizjs $GITHUB_REF_NAME" \
|
|
59
|
+
--generate-notes
|
package/CLAUDE.md
CHANGED
|
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
4
4
|
|
|
5
5
|
## What this is
|
|
6
6
|
|
|
7
|
-
`@wizhut_tech/wizjs` is a small, curated JavaScript utility library —
|
|
7
|
+
`@wizhut_tech/wizjs` is a small, curated JavaScript utility library — Python idioms and selected stdlib helpers (`itertools`, `functools`, `collections`) for everyday JS, explicitly *not* trying to be lodash. Helpers are grouped into top-level namespaces (`io`, `lang`, `math`) with focused sub-areas under each. It is plain JavaScript (no TypeScript), CommonJS (no `type: "module"`, no build step), and ships its source directly to npm. It pulls in no runtime dependencies; the only third-party use is Node's built-in `fs` in `io/files`, so anything outside that namespace is runtime-agnostic.
|
|
8
8
|
|
|
9
9
|
## Commands
|
|
10
10
|
|
|
@@ -16,7 +16,7 @@ npx tap __tests__/lang_arrays.test.js # run a SINGLE test file
|
|
|
16
16
|
|
|
17
17
|
There is **no build, lint, or typecheck step** — `test` is the only npm script. The test runner is [`tap`](https://node-tap.org) (v21). Tests live in `__tests__/`, one file per namespace sub-area, named `<namespace>_<area>.test.js` (e.g. `lang_arrays.test.js`, `math_numbers.test.js`).
|
|
18
18
|
|
|
19
|
-
Publishing
|
|
19
|
+
Publishing goes to the scoped package `@wizhut_tech/wizjs` and is tag-driven, from CI: `npm version patch` then `git push --follow-tags`, and the `v<x.y.z>` tag triggers [`.github/workflows/release.yml`](.github/workflows/release.yml) — tag/`package.json` check, `npm ci && npm test`, `npm publish --access public`, GitHub release. Authentication is npm OIDC trusted publishing, so there is no token anywhere; don't publish from a laptop, as hand-published versions lose the provenance attestation. There is no `files` allowlist or `.npmignore`, so the published tarball includes `index.js` + `src/`. The repo declares the MIT license in `package.json` but currently has **no LICENSE file**.
|
|
20
20
|
|
|
21
21
|
## Architecture
|
|
22
22
|
|
|
@@ -24,13 +24,15 @@ The single entry point is `index.js`. It `require`s each leaf module from `src/`
|
|
|
24
24
|
|
|
25
25
|
```
|
|
26
26
|
io.files → src/io/files.js (loadFully)
|
|
27
|
-
lang.arrays → src/lang/arrays.js (compact, accumulate, Operator)
|
|
27
|
+
lang.arrays → src/lang/arrays.js (compact, accumulate, Operator, unique, chunk, flatten)
|
|
28
28
|
lang.singleton → src/lang/singleton.js (singleton, getInstance)
|
|
29
29
|
lang.flow → src/lang/flow.js (if_exception)
|
|
30
|
-
lang.checks → src/lang/checks.js (isNil, isArray, isObject, isString, isNumber)
|
|
31
|
-
lang.functools → src/lang/functools.js (reflexive)
|
|
32
|
-
lang.itertools → src/lang/itertools.js (count, repeat)
|
|
33
|
-
|
|
30
|
+
lang.checks → src/lang/checks.js (isNil, isArray, isObject, isString, isNumber, isBoolean, isFunction, isInteger, isEmpty)
|
|
31
|
+
lang.functools → src/lang/functools.js (reflexive, partial, compose, once, memoize, cache, reduce)
|
|
32
|
+
lang.itertools → src/lang/itertools.js (count, repeat, cycle, chain, take, zip, range, enumerate, islice, takewhile, dropwhile, filterfalse, zip_longest, product, groupby, starmap, batched, pairwise, compress)
|
|
33
|
+
lang.objects → src/lang/objects.js (pick, omit, get)
|
|
34
|
+
lang.collections → src/lang/collections.js (Counter, DefaultDict)
|
|
35
|
+
math.numbers → src/math/numbers.js (toInteger, toNumber, clamp, mod)
|
|
34
36
|
exceptions → src/internal/exceptions.js (BadlyInitializedError)
|
|
35
37
|
```
|
|
36
38
|
|
|
@@ -40,7 +42,7 @@ Directory layout mirrors the namespace path exactly: `src/<top-namespace>/<area>
|
|
|
40
42
|
|
|
41
43
|
Where a helper belongs:
|
|
42
44
|
- I/O / filesystem / external resources → `src/io/`
|
|
43
|
-
- General language utilities → `src/lang/` (`checks` = type/predicate tests, `arrays` = array ops, `flow` = control-flow wrappers, `functools` = function helpers, `itertools` = lazy generators, `singleton` = the singleton hack)
|
|
45
|
+
- General language utilities → `src/lang/` (`checks` = type/predicate tests, `arrays` = array ops, `flow` = control-flow wrappers, `functools` = function helpers, `itertools` = lazy generators, `objects` = plain-object pick/omit/get, `collections` = Python `Counter` / `DefaultDict`, `singleton` = the singleton hack)
|
|
44
46
|
- Numeric helpers → `src/math/`
|
|
45
47
|
- Shared internals not meant as public API → `src/internal/`
|
|
46
48
|
|
|
@@ -49,6 +51,6 @@ Cross-module reuse is direct relative `require` (e.g. `arrays.js` and `numbers.j
|
|
|
49
51
|
## Conventions
|
|
50
52
|
|
|
51
53
|
- **Module format:** CommonJS only — `require(...)` / `module.exports`. No ESM, no transpilation.
|
|
52
|
-
- **Naming:** functions are camelCase (`isNil`, `loadFully`, `toInteger`);
|
|
54
|
+
- **Naming:** functions are camelCase (`isNil`, `loadFully`, `toInteger`); Python stdlib names are kept as in Python (`if_exception`, `takewhile`, `zip_longest`, `groupby`). Classes are PascalCase (`Operator`, `BadlyInitializedError`, `Counter`, `DefaultDict`). Lazy/iterator helpers in `itertools` are generator functions.
|
|
53
55
|
- **Adding a helper to an existing area:** write the function in the matching `src/<ns>/<area>.js`, add it to that file's `module.exports`, and add cases to the corresponding `__tests__/<ns>_<area>.test.js`. It becomes available automatically since `index.js` references the whole `module.exports`.
|
|
54
56
|
- **Adding a new area or namespace:** create `src/<ns>/<area>.js`, then `require` it in `index.js` and mount it under the right namespace object. Add a `docs/<ns>_<area>.md` page and a matching `__tests__/<ns>_<area>.test.js`. Each namespace sub-area has a doc page under `docs/` linked from `readme.md` (lowercase filename).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const t = require('tap');
|
|
2
2
|
|
|
3
|
-
const { compact, accumulate, Operator } = require('../src/lang/arrays.js');
|
|
3
|
+
const { compact, accumulate, Operator, unique, chunk, flatten } = require('../src/lang/arrays.js');
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
t.test('arrays/compact', (t) => {
|
|
@@ -22,4 +22,46 @@ t.test('arrays/accumulate', (t) => {
|
|
|
22
22
|
t.equal(accumulate(arr, Operator.multiply, 0), 0);
|
|
23
23
|
t.equal(accumulate(arr, Operator.divide, 0), 0);
|
|
24
24
|
t.end();
|
|
25
|
-
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
t.test('arrays/unique', (t) => {
|
|
29
|
+
t.match(unique([1, 2, 2, 3, 1]), [1, 2, 3]);
|
|
30
|
+
t.match(unique(['a', 'b', 'a']), ['a', 'b']);
|
|
31
|
+
t.match(unique([NaN, NaN, 1]), [NaN, 1]);
|
|
32
|
+
t.match(unique([0, false, 0, false]), [0, false]);
|
|
33
|
+
t.match(unique([]), []);
|
|
34
|
+
t.match(unique(null), []);
|
|
35
|
+
t.match(unique(undefined), []);
|
|
36
|
+
|
|
37
|
+
const a = { id: 1 };
|
|
38
|
+
const b = { id: 1 };
|
|
39
|
+
t.match(unique([a, b, a]), [a, b]);
|
|
40
|
+
t.end();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
t.test('arrays/chunk', (t) => {
|
|
45
|
+
t.match(chunk([1, 2, 3, 4, 5], 2), [[1, 2], [3, 4], [5]]);
|
|
46
|
+
t.match(chunk([1, 2, 3], 3), [[1, 2, 3]]);
|
|
47
|
+
t.match(chunk([1, 2, 3], 5), [[1, 2, 3]]);
|
|
48
|
+
t.match(chunk([], 2), []);
|
|
49
|
+
t.match(chunk(null, 2), []);
|
|
50
|
+
t.match(chunk(undefined, 2), []);
|
|
51
|
+
t.match(chunk([1, 2, 3], 0), []);
|
|
52
|
+
t.match(chunk([1, 2, 3], -1), []);
|
|
53
|
+
t.match(chunk([1, 2, 3], '2'), [[1, 2], [3]]);
|
|
54
|
+
t.end();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
t.test('arrays/flatten', (t) => {
|
|
59
|
+
t.match(flatten([1, [2, 3], 4]), [1, 2, 3, 4]);
|
|
60
|
+
t.match(flatten([1, [2, [3]], 4]), [1, 2, [3], 4]);
|
|
61
|
+
t.match(flatten([1, 2, 3]), [1, 2, 3]);
|
|
62
|
+
t.match(flatten([]), []);
|
|
63
|
+
t.match(flatten(null), []);
|
|
64
|
+
t.match(flatten(undefined), []);
|
|
65
|
+
t.match(flatten([[], [1], 2]), [1, 2]);
|
|
66
|
+
t.end();
|
|
67
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const t = require('tap');
|
|
2
2
|
|
|
3
|
-
const { isNil, isArray, isObject, isNumber, isString } = require('../src/lang/checks.js');
|
|
3
|
+
const { isNil, isArray, isObject, isNumber, isString, isBoolean, isFunction, isInteger, isEmpty } = require('../src/lang/checks.js');
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
@@ -53,4 +53,65 @@ t.test('checks/isString', (t) => {
|
|
|
53
53
|
t.equal(isString(1.1), false);
|
|
54
54
|
t.equal(isString('misc'), true);
|
|
55
55
|
t.end();
|
|
56
|
-
})
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
t.test('checks/isBoolean', (t) => {
|
|
59
|
+
t.equal(isBoolean(true), true);
|
|
60
|
+
t.equal(isBoolean(false), true);
|
|
61
|
+
t.equal(isBoolean(null), false);
|
|
62
|
+
t.equal(isBoolean(undefined), false);
|
|
63
|
+
t.equal(isBoolean(0), false);
|
|
64
|
+
t.equal(isBoolean(1), false);
|
|
65
|
+
t.equal(isBoolean('true'), false);
|
|
66
|
+
t.equal(isBoolean({}), false);
|
|
67
|
+
t.end();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
t.test('checks/isFunction', (t) => {
|
|
71
|
+
t.equal(isFunction(function () {}), true);
|
|
72
|
+
t.equal(isFunction(() => 1), true);
|
|
73
|
+
t.equal(isFunction(async () => 1), true);
|
|
74
|
+
t.equal(isFunction(function* () {}), true);
|
|
75
|
+
t.equal(isFunction(class Foo {}), true);
|
|
76
|
+
t.equal(isFunction(null), false);
|
|
77
|
+
t.equal(isFunction(undefined), false);
|
|
78
|
+
t.equal(isFunction(1), false);
|
|
79
|
+
t.equal(isFunction('fn'), false);
|
|
80
|
+
t.equal(isFunction({}), false);
|
|
81
|
+
t.end();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
t.test('checks/isInteger', (t) => {
|
|
85
|
+
t.equal(isInteger(1), true);
|
|
86
|
+
t.equal(isInteger(0), true);
|
|
87
|
+
t.equal(isInteger(-4), true);
|
|
88
|
+
t.equal(isInteger(1.0), true);
|
|
89
|
+
t.equal(isInteger(1.1), false);
|
|
90
|
+
t.equal(isInteger(NaN), false);
|
|
91
|
+
t.equal(isInteger(Infinity), false);
|
|
92
|
+
t.equal(isInteger('1'), false);
|
|
93
|
+
t.equal(isInteger(null), false);
|
|
94
|
+
t.equal(isInteger(undefined), false);
|
|
95
|
+
t.equal(isInteger(true), false);
|
|
96
|
+
t.end();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
t.test('checks/isEmpty', (t) => {
|
|
100
|
+
t.equal(isEmpty(null), true);
|
|
101
|
+
t.equal(isEmpty(undefined), true);
|
|
102
|
+
t.equal(isEmpty(''), true);
|
|
103
|
+
t.equal(isEmpty([]), true);
|
|
104
|
+
t.equal(isEmpty({}), true);
|
|
105
|
+
t.equal(isEmpty(new Map()), true);
|
|
106
|
+
t.equal(isEmpty(new Set()), true);
|
|
107
|
+
|
|
108
|
+
t.equal(isEmpty(' '), false);
|
|
109
|
+
t.equal(isEmpty([0]), false);
|
|
110
|
+
t.equal(isEmpty({ a: 1 }), false);
|
|
111
|
+
t.equal(isEmpty(new Map([['a', 1]])), false);
|
|
112
|
+
t.equal(isEmpty(new Set([1])), false);
|
|
113
|
+
t.equal(isEmpty(0), false);
|
|
114
|
+
t.equal(isEmpty(false), false);
|
|
115
|
+
t.equal(isEmpty(() => {}), false);
|
|
116
|
+
t.end();
|
|
117
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const t = require('tap');
|
|
2
|
+
|
|
3
|
+
const { Counter, DefaultDict } = require('../src/lang/collections.js');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
t.test('collections/Counter', (t) => {
|
|
7
|
+
const fromString = new Counter('aab');
|
|
8
|
+
t.equal(fromString.get('a'), 2);
|
|
9
|
+
t.equal(fromString.get('b'), 1);
|
|
10
|
+
t.equal(fromString.get('z'), 0);
|
|
11
|
+
t.equal(fromString.total(), 3);
|
|
12
|
+
t.equal(fromString.size, 2);
|
|
13
|
+
t.match([...fromString], ['a', 'b']);
|
|
14
|
+
t.match([...fromString.keys()], ['a', 'b']);
|
|
15
|
+
t.match([...fromString.values()], [2, 1]);
|
|
16
|
+
t.match([...fromString.entries()], [['a', 2], ['b', 1]]);
|
|
17
|
+
t.match([...fromString.elements()], ['a', 'a', 'b']);
|
|
18
|
+
|
|
19
|
+
const fromObj = new Counter({ a: 4, b: 2 });
|
|
20
|
+
t.equal(fromObj.get('a'), 4);
|
|
21
|
+
t.match(fromObj.mostCommon(1), [['a', 4]]);
|
|
22
|
+
t.match(fromObj.mostCommon(), [['a', 4], ['b', 2]]);
|
|
23
|
+
t.match(fromObj.mostCommon(0), []);
|
|
24
|
+
|
|
25
|
+
const fromArr = new Counter([1, 1, 2]);
|
|
26
|
+
t.equal(fromArr.get(1), 2);
|
|
27
|
+
fromArr.add(1);
|
|
28
|
+
t.equal(fromArr.get(1), 3);
|
|
29
|
+
fromArr.add(3, 5);
|
|
30
|
+
t.equal(fromArr.get(3), 5);
|
|
31
|
+
fromArr.set(2, 0);
|
|
32
|
+
t.equal(fromArr.get(2), 0);
|
|
33
|
+
t.ok(fromArr.delete(2));
|
|
34
|
+
t.equal(fromArr.get(2), 0);
|
|
35
|
+
|
|
36
|
+
const zeros = new Counter();
|
|
37
|
+
zeros.set('gone', 0);
|
|
38
|
+
zeros.set('neg', -1);
|
|
39
|
+
zeros.set('keep', 2);
|
|
40
|
+
t.match([...zeros.elements()], ['keep', 'keep']);
|
|
41
|
+
|
|
42
|
+
const fromMap = new Counter(new Map([['x', 3], ['y', 1]]));
|
|
43
|
+
t.equal(fromMap.get('x'), 3);
|
|
44
|
+
|
|
45
|
+
const copy = new Counter(fromString);
|
|
46
|
+
t.equal(copy.get('a'), 2);
|
|
47
|
+
copy.add('a');
|
|
48
|
+
t.equal(fromString.get('a'), 2);
|
|
49
|
+
|
|
50
|
+
const empty = new Counter();
|
|
51
|
+
t.equal(empty.total(), 0);
|
|
52
|
+
t.match([...empty.elements()], []);
|
|
53
|
+
|
|
54
|
+
const nan = new Counter([NaN, NaN]);
|
|
55
|
+
t.equal(nan.get(NaN), 2);
|
|
56
|
+
t.end();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
t.test('collections/DefaultDict', (t) => {
|
|
61
|
+
const groups = new DefaultDict(Array);
|
|
62
|
+
groups.get('a').push(1);
|
|
63
|
+
groups.get('a').push(2);
|
|
64
|
+
groups.get('b').push(3);
|
|
65
|
+
t.match(groups.get('a'), [1, 2]);
|
|
66
|
+
t.match(groups.get('b'), [3]);
|
|
67
|
+
t.equal(groups.size, 2);
|
|
68
|
+
t.ok(groups.has('a'));
|
|
69
|
+
t.equal(groups.peek('missing', 'nope'), 'nope');
|
|
70
|
+
t.match(groups.peek('a'), [1, 2]);
|
|
71
|
+
t.equal(groups.has('missing'), false);
|
|
72
|
+
|
|
73
|
+
groups.set('c', [9]);
|
|
74
|
+
t.match(groups.get('c'), [9]);
|
|
75
|
+
t.ok(groups.delete('c'));
|
|
76
|
+
t.equal(groups.has('c'), false);
|
|
77
|
+
|
|
78
|
+
t.match([...groups.keys()], ['a', 'b']);
|
|
79
|
+
t.equal([...groups.values()].length, 2);
|
|
80
|
+
t.equal([...groups.entries()].length, 2);
|
|
81
|
+
|
|
82
|
+
const lists = new DefaultDict(() => []);
|
|
83
|
+
lists.get('k').push('x');
|
|
84
|
+
t.match(lists.get('k'), ['x']);
|
|
85
|
+
|
|
86
|
+
const noFactory = new DefaultDict();
|
|
87
|
+
t.equal(noFactory.get('x'), undefined);
|
|
88
|
+
t.equal(noFactory.has('x'), false);
|
|
89
|
+
t.end();
|
|
90
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const t = require('tap');
|
|
2
2
|
|
|
3
|
-
const { reflexive } = require('../src/lang/functools.js');
|
|
3
|
+
const { reflexive, partial, compose, once, memoize, cache, reduce } = require('../src/lang/functools.js');
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
t.test('functools/reflexive', (t) => {
|
|
@@ -9,4 +9,125 @@ t.test('functools/reflexive', (t) => {
|
|
|
9
9
|
t.equal(reflexive('abcde'), 'abcde');
|
|
10
10
|
t.equal(reflexive(2.1), 2.1);
|
|
11
11
|
t.end();
|
|
12
|
-
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
t.test('functools/partial', (t) => {
|
|
16
|
+
const add = (a, b, c) => a + b + c;
|
|
17
|
+
const add10 = partial(add, 10);
|
|
18
|
+
t.equal(add10(1, 2), 13);
|
|
19
|
+
|
|
20
|
+
const add10and20 = partial(add, 10, 20);
|
|
21
|
+
t.equal(add10and20(3), 33);
|
|
22
|
+
|
|
23
|
+
const greet = function (greeting, name) {
|
|
24
|
+
return `${greeting} ${name}, ${this.title}`;
|
|
25
|
+
};
|
|
26
|
+
const hello = partial(greet, 'hello');
|
|
27
|
+
t.equal(hello.call({ title: 'Dr' }, 'Ada'), 'hello Ada, Dr');
|
|
28
|
+
t.end();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
t.test('functools/compose', (t) => {
|
|
33
|
+
const inc = (n) => n + 1;
|
|
34
|
+
const double = (n) => n * 2;
|
|
35
|
+
t.equal(compose(double, inc)(3), 8);
|
|
36
|
+
t.equal(compose(inc, double)(3), 7);
|
|
37
|
+
t.equal(compose(inc)(3), 4);
|
|
38
|
+
t.equal(compose()(3), 3);
|
|
39
|
+
|
|
40
|
+
const first = (...args) => args.join('-');
|
|
41
|
+
const wrap = (s) => `[${s}]`;
|
|
42
|
+
t.equal(compose(wrap, first)('a', 'b'), '[a-b]');
|
|
43
|
+
t.end();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
t.test('functools/once', (t) => {
|
|
48
|
+
let calls = 0;
|
|
49
|
+
const fn = (x) => {
|
|
50
|
+
calls += 1;
|
|
51
|
+
return x * 2;
|
|
52
|
+
};
|
|
53
|
+
const onceFn = once(fn);
|
|
54
|
+
|
|
55
|
+
t.equal(onceFn(4), 8);
|
|
56
|
+
t.equal(onceFn(9), 8);
|
|
57
|
+
t.equal(calls, 1);
|
|
58
|
+
t.end();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
t.test('functools/memoize', (t) => {
|
|
63
|
+
let calls = 0;
|
|
64
|
+
const fn = (a, b) => {
|
|
65
|
+
calls += 1;
|
|
66
|
+
return a + b;
|
|
67
|
+
};
|
|
68
|
+
const mem = memoize(fn);
|
|
69
|
+
|
|
70
|
+
t.equal(mem(1, 2), 3);
|
|
71
|
+
t.equal(mem(1, 2), 3);
|
|
72
|
+
t.equal(calls, 1);
|
|
73
|
+
t.equal(mem(1, 3), 4);
|
|
74
|
+
t.equal(calls, 2);
|
|
75
|
+
|
|
76
|
+
// different arity is a different cache entry
|
|
77
|
+
t.equal(mem(1, 2, undefined), 3);
|
|
78
|
+
t.equal(calls, 3);
|
|
79
|
+
|
|
80
|
+
// NaN is a valid key (SameValueZero)
|
|
81
|
+
let nanCalls = 0;
|
|
82
|
+
const nanFn = memoize((n) => {
|
|
83
|
+
nanCalls += 1;
|
|
84
|
+
return n;
|
|
85
|
+
});
|
|
86
|
+
t.ok(Number.isNaN(nanFn(NaN)));
|
|
87
|
+
t.ok(Number.isNaN(nanFn(NaN)));
|
|
88
|
+
t.equal(nanCalls, 1);
|
|
89
|
+
|
|
90
|
+
// throws are not cached
|
|
91
|
+
let throws = 0;
|
|
92
|
+
const boom = memoize((ok) => {
|
|
93
|
+
throws += 1;
|
|
94
|
+
if (!ok) {
|
|
95
|
+
throw new Error('nope');
|
|
96
|
+
}
|
|
97
|
+
return 'ok';
|
|
98
|
+
});
|
|
99
|
+
t.throws(() => boom(false));
|
|
100
|
+
t.throws(() => boom(false));
|
|
101
|
+
t.equal(throws, 2);
|
|
102
|
+
t.equal(boom(true), 'ok');
|
|
103
|
+
t.equal(boom(true), 'ok');
|
|
104
|
+
t.equal(throws, 3);
|
|
105
|
+
|
|
106
|
+
// custom resolver
|
|
107
|
+
let resolved = 0;
|
|
108
|
+
const byName = memoize((obj) => {
|
|
109
|
+
resolved += 1;
|
|
110
|
+
return obj.v;
|
|
111
|
+
}, (obj) => obj.id);
|
|
112
|
+
t.equal(byName({ id: 1, v: 10 }), 10);
|
|
113
|
+
t.equal(byName({ id: 1, v: 99 }), 10);
|
|
114
|
+
t.equal(resolved, 1);
|
|
115
|
+
|
|
116
|
+
mem.cache.clear();
|
|
117
|
+
t.equal(mem(1, 2), 3);
|
|
118
|
+
t.equal(calls, 4);
|
|
119
|
+
t.equal(cache, memoize);
|
|
120
|
+
t.end();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
t.test('functools/reduce', (t) => {
|
|
125
|
+
t.equal(reduce((a, b) => a + b, [1, 2, 3, 4]), 10);
|
|
126
|
+
t.equal(reduce((a, b) => a + b, [1, 2, 3, 4], 10), 20);
|
|
127
|
+
t.equal(reduce((a, b) => a + b, [], 0), 0);
|
|
128
|
+
t.equal(reduce((a, b) => a * b, (function* () { yield 2; yield 3; yield 4; })()), 24);
|
|
129
|
+
t.throws(() => reduce((a, b) => a + b, []));
|
|
130
|
+
t.throws(() => reduce((a, b) => a + b, null));
|
|
131
|
+
t.equal(reduce((a, b) => a + b, null, 5), 5);
|
|
132
|
+
t.end();
|
|
133
|
+
});
|