@wizhut_tech/wizjs 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.yml +59 -0
- package/__tests__/internal_exceptions.test.js +29 -0
- package/__tests__/io_files.test.js +41 -0
- 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_flow.test.js +49 -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/__tests__/package_exports.test.js +79 -0
- 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 +15 -2
- package/readme.md +22 -3
- package/src/io/files.js +1 -1
- 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
- package/CLAUDE.md +0 -54
|
@@ -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
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const t = require('tap');
|
|
2
|
+
|
|
3
|
+
const { BadlyInitializedError } = require('../src/internal/exceptions.js');
|
|
4
|
+
const root = require('@wizhut_tech/wizjs');
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
t.test('exceptions/BadlyInitializedError behaves like an Error', (t) => {
|
|
8
|
+
const err = new BadlyInitializedError('not ready');
|
|
9
|
+
|
|
10
|
+
t.type(err, Error, 'extends Error');
|
|
11
|
+
t.type(err, BadlyInitializedError);
|
|
12
|
+
t.equal(err.message, 'not ready');
|
|
13
|
+
t.ok(err.stack, 'carries a stack');
|
|
14
|
+
t.end();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
t.test('exceptions/BadlyInitializedError ignores trailing arguments', (t) => {
|
|
19
|
+
const err = new BadlyInitializedError('only the message', 'extra', 42);
|
|
20
|
+
|
|
21
|
+
t.equal(err.message, 'only the message');
|
|
22
|
+
t.end();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
t.test('exceptions/BadlyInitializedError is the one the root import exposes', (t) => {
|
|
27
|
+
t.equal(root.exceptions.BadlyInitializedError, BadlyInitializedError);
|
|
28
|
+
t.end();
|
|
29
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const t = require('tap');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const { loadFully } = require('../src/io/files.js');
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
t.test('files/loadFully returns null for a file that is not there', async (t) => {
|
|
8
|
+
const missing = path.join(t.testdir(), 'no-such-file.txt');
|
|
9
|
+
|
|
10
|
+
t.equal(await loadFully(missing), null);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
t.test('files/loadFully returns null when the path is a directory', async (t) => {
|
|
15
|
+
const dir = t.testdir({ 'sub': {} });
|
|
16
|
+
|
|
17
|
+
t.equal(await loadFully(path.join(dir, 'sub')), null);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
t.test('files/loadFully returns the whole file as text', async (t) => {
|
|
22
|
+
const dir = t.testdir({ 'hello.txt': 'contents\n' });
|
|
23
|
+
|
|
24
|
+
t.equal(await loadFully(path.join(dir, 'hello.txt')), 'contents\n');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
t.test('files/loadFully returns an empty string for an empty file', async (t) => {
|
|
29
|
+
const dir = t.testdir({ 'empty.txt': '' });
|
|
30
|
+
|
|
31
|
+
t.equal(await loadFully(path.join(dir, 'empty.txt')), '',
|
|
32
|
+
'an empty file is distinguishable from a failed read');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
t.test('files/loadFully preserves multi-line and unicode content', async (t) => {
|
|
37
|
+
const body = 'first\nsecond\n\u03b1\u03b2\u03b3\n';
|
|
38
|
+
const dir = t.testdir({ 'multi.txt': body });
|
|
39
|
+
|
|
40
|
+
t.equal(await loadFully(path.join(dir, 'multi.txt')), body);
|
|
41
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const t = require('tap');
|
|
2
|
+
|
|
3
|
+
const { if_exception } = require('../src/lang/flow.js');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
t.test('flow/if_exception runs the success branch', (t) => {
|
|
7
|
+
const calls = [];
|
|
8
|
+
|
|
9
|
+
if_exception(
|
|
10
|
+
() => calls.push('body'),
|
|
11
|
+
() => calls.push('ok'),
|
|
12
|
+
() => calls.push('failed')
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
t.same(calls, ['body', 'ok']);
|
|
16
|
+
t.end();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
t.test('flow/if_exception runs the failure branch with the error', (t) => {
|
|
21
|
+
const calls = [];
|
|
22
|
+
const boom = new Error('boom');
|
|
23
|
+
let seen = null;
|
|
24
|
+
|
|
25
|
+
if_exception(
|
|
26
|
+
() => { calls.push('body'); throw boom; },
|
|
27
|
+
() => calls.push('ok'),
|
|
28
|
+
(error) => { calls.push('failed'); seen = error; }
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
t.same(calls, ['body', 'failed'], 'the success callback is skipped');
|
|
32
|
+
t.equal(seen, boom, 'the thrown error is handed to the failure callback');
|
|
33
|
+
t.end();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
t.test('flow/if_exception catches a throw from the success callback', (t) => {
|
|
38
|
+
const boom = new Error('from fn_noexc');
|
|
39
|
+
let seen = null;
|
|
40
|
+
|
|
41
|
+
if_exception(
|
|
42
|
+
() => {},
|
|
43
|
+
() => { throw boom; },
|
|
44
|
+
(error) => { seen = error; }
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
t.equal(seen, boom, 'fn_noexc runs inside the same try block');
|
|
48
|
+
t.end();
|
|
49
|
+
});
|
|
@@ -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
|
+
});
|