@weborigami/async-tree 0.6.15 → 0.6.17
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/package.json +1 -1
- package/src/Tree.js +1 -0
- package/src/operations/combine.js +59 -0
- package/src/operations/deepEntriesIterator.js +5 -4
- package/src/operations/map.js +1 -1
- package/src/operations/shuffle.js +20 -7
- package/src/operations/traverseOrThrow.js +3 -1
- package/test/operations/combine.test.js +35 -0
- package/test/operations/deepEntriesIterator.test.js +1 -1
- package/test/operations/flat.test.js +1 -1
- package/test/operations/shuffle.test.js +25 -0
package/package.json
CHANGED
package/src/Tree.js
CHANGED
|
@@ -8,6 +8,7 @@ export { default as cache } from "./operations/cache.js";
|
|
|
8
8
|
export { default as calendar } from "./operations/calendar.js";
|
|
9
9
|
export { default as child } from "./operations/child.js";
|
|
10
10
|
export { default as clear } from "./operations/clear.js";
|
|
11
|
+
export { default as combine } from "./operations/combine.js";
|
|
11
12
|
export { default as concat } from "./operations/concat.js";
|
|
12
13
|
export { default as constant } from "./operations/constant.js";
|
|
13
14
|
export { default as deepArrays } from "./operations/deepArrays.js";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as args from "../utilities/args.js";
|
|
2
|
+
import isUnpackable from "../utilities/isUnpackable.js";
|
|
3
|
+
import isMap from "./isMap.js";
|
|
4
|
+
import keys from "./keys.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Does a pairwise invocation of `combineFn` for each value in the two trees. If
|
|
8
|
+
* one tree has a key that the other doesn't, the `combineFn` will be invoked
|
|
9
|
+
* with `undefined` for the missing value.
|
|
10
|
+
*
|
|
11
|
+
* This returns a new tree of all the results of the `combineFn` invocations
|
|
12
|
+
* that were not `undefined`. If all results were `undefined`, the overall
|
|
13
|
+
* result is itself `undefined`.
|
|
14
|
+
*
|
|
15
|
+
* @typedef {import("@weborigami/async-tree").Maplike} Maplike
|
|
16
|
+
*
|
|
17
|
+
* @param {Maplike} maplike1
|
|
18
|
+
* @param {Maplike} maplike2
|
|
19
|
+
* @param {function} combineFn
|
|
20
|
+
*/
|
|
21
|
+
export default async function combine(maplike1, maplike2, combineFn) {
|
|
22
|
+
const tree1 = await args.map(maplike1, "Tree.combine", {
|
|
23
|
+
deep: true,
|
|
24
|
+
position: 1,
|
|
25
|
+
});
|
|
26
|
+
const tree2 = await args.map(maplike2, "Tree.combine", {
|
|
27
|
+
deep: true,
|
|
28
|
+
position: 2,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (isUnpackable(combineFn)) {
|
|
32
|
+
combineFn = await combineFn.unpack();
|
|
33
|
+
}
|
|
34
|
+
const fn = args.fn(combineFn, "Tree.combine", {
|
|
35
|
+
position: 3,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const keys1 = await keys(tree1);
|
|
39
|
+
const keys2 = await keys(tree2);
|
|
40
|
+
const combinedKeys = new Set([...keys1, ...keys2]);
|
|
41
|
+
|
|
42
|
+
const result = {};
|
|
43
|
+
|
|
44
|
+
for (const key of combinedKeys) {
|
|
45
|
+
const value1 = await tree1.get(key);
|
|
46
|
+
const value2 = await tree2.get(key);
|
|
47
|
+
|
|
48
|
+
const combination =
|
|
49
|
+
isMap(value1) && isMap(value2)
|
|
50
|
+
? await combine(value1, value2, fn)
|
|
51
|
+
: await fn(value1, value2);
|
|
52
|
+
|
|
53
|
+
if (combination !== undefined) {
|
|
54
|
+
result[key] = combination;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
59
|
+
}
|
|
@@ -13,7 +13,7 @@ import isMaplike from "./isMaplike.js";
|
|
|
13
13
|
* specified depth. A depth of 0 will yield values only at the tree's top level.
|
|
14
14
|
*
|
|
15
15
|
* @param {import("../../index.ts").Maplike} maplike
|
|
16
|
-
* @param {{ depth?: number, expand?: boolean }} [options]
|
|
16
|
+
* @param {{ depth?: number, expand?: boolean, unpack?: boolean }} [options]
|
|
17
17
|
* @returns {AsyncGenerator<[any, any], void, undefined>}
|
|
18
18
|
*/
|
|
19
19
|
export default async function* deepEntriesIterator(maplike, options = {}) {
|
|
@@ -22,12 +22,13 @@ export default async function* deepEntriesIterator(maplike, options = {}) {
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
const depth = options.depth ?? Infinity;
|
|
25
|
-
const expand = options.expand ??
|
|
25
|
+
const expand = options.expand ?? true;
|
|
26
|
+
const unpack = options.unpack ?? false;
|
|
26
27
|
|
|
27
28
|
for await (let [key, value] of tree.entries()) {
|
|
28
29
|
value = await value;
|
|
29
30
|
|
|
30
|
-
if (
|
|
31
|
+
if (unpack && isUnpackable(value)) {
|
|
31
32
|
value = await value.unpack();
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -37,7 +38,7 @@ export default async function* deepEntriesIterator(maplike, options = {}) {
|
|
|
37
38
|
(isMap(value) ||
|
|
38
39
|
(expand && typeof value !== "function" && isMaplike(value)));
|
|
39
40
|
if (recurse) {
|
|
40
|
-
yield* deepEntriesIterator(value, { depth: depth - 1, expand });
|
|
41
|
+
yield* deepEntriesIterator(value, { depth: depth - 1, expand, unpack });
|
|
41
42
|
} else {
|
|
42
43
|
yield [key, value];
|
|
43
44
|
}
|
package/src/operations/map.js
CHANGED
|
@@ -250,7 +250,7 @@ function validateOptions(options) {
|
|
|
250
250
|
// Set defaults for options not specified. We don't set a default value for
|
|
251
251
|
// `deep` because a false value is a stronger signal than undefined.
|
|
252
252
|
description ??= "key/value map";
|
|
253
|
-
keyNeedsSourceValue ??= true;
|
|
253
|
+
keyNeedsSourceValue ??= keyFn?.needsSourceValue ?? true;
|
|
254
254
|
|
|
255
255
|
return {
|
|
256
256
|
deep,
|
|
@@ -3,16 +3,22 @@ import * as args from "../utilities/args.js";
|
|
|
3
3
|
import keys from "./keys.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Return a new tree with the original's keys shuffled
|
|
6
|
+
* Return a new tree with the original's keys shuffled.
|
|
7
|
+
*
|
|
8
|
+
* The `randoms` option allows you to provide a function that either returns a
|
|
9
|
+
* random number between 0 and 1 (like `Math.random`) or a random integer. This
|
|
10
|
+
* can be used to create deterministic shuffling.
|
|
7
11
|
*
|
|
8
12
|
* @typedef {import("../../index.ts").Maplike} Maplike
|
|
13
|
+
* @typedef {import("../../index.ts").Stringlike} Stringlike
|
|
9
14
|
*
|
|
10
15
|
* @param {Maplike} maplike
|
|
11
|
-
* @param {
|
|
16
|
+
* @param {{ randoms?: (() => number) }?} options
|
|
12
17
|
* @returns {Promise<AsyncMap>}
|
|
13
18
|
*/
|
|
14
|
-
export default async function shuffle(maplike,
|
|
19
|
+
export default async function shuffle(maplike, options = {}) {
|
|
15
20
|
const source = await args.map(maplike, "Tree.shuffle");
|
|
21
|
+
const randoms = options?.randoms ?? Math.random;
|
|
16
22
|
|
|
17
23
|
let mapKeys;
|
|
18
24
|
|
|
@@ -24,9 +30,9 @@ export default async function shuffle(maplike, reshuffle = false) {
|
|
|
24
30
|
},
|
|
25
31
|
|
|
26
32
|
async *keys() {
|
|
27
|
-
if (!mapKeys
|
|
33
|
+
if (!mapKeys) {
|
|
28
34
|
mapKeys = await keys(source);
|
|
29
|
-
shuffleArray(mapKeys);
|
|
35
|
+
shuffleArray(mapKeys, randoms);
|
|
30
36
|
}
|
|
31
37
|
yield* mapKeys;
|
|
32
38
|
},
|
|
@@ -42,10 +48,17 @@ export default async function shuffle(maplike, reshuffle = false) {
|
|
|
42
48
|
*
|
|
43
49
|
* Performs a Fisher-Yates shuffle. From http://sedition.com/perl/javascript-fy.html
|
|
44
50
|
*/
|
|
45
|
-
export function shuffleArray(array) {
|
|
51
|
+
export function shuffleArray(array, randoms) {
|
|
46
52
|
let i = array.length;
|
|
47
53
|
while (--i >= 0) {
|
|
48
|
-
const
|
|
54
|
+
const random = randoms();
|
|
55
|
+
|
|
56
|
+
const j =
|
|
57
|
+
random < 1
|
|
58
|
+
? // Like Math.random
|
|
59
|
+
Math.floor(random * (i + 1))
|
|
60
|
+
: // Random number
|
|
61
|
+
Math.floor(random) % (i + 1);
|
|
49
62
|
const temp = array[i];
|
|
50
63
|
array[i] = array[j];
|
|
51
64
|
array[j] = temp;
|
|
@@ -86,7 +86,9 @@ export default async function traverseOrThrow(maplike, ...keys) {
|
|
|
86
86
|
const message =
|
|
87
87
|
value === undefined
|
|
88
88
|
? "A path tried to unpack a value that doesn't exist."
|
|
89
|
-
:
|
|
89
|
+
: isPacked(value)
|
|
90
|
+
? "A path tried to unpack data but there's no unpack function."
|
|
91
|
+
: "A path tried to unpack data that's already unpacked.";
|
|
90
92
|
throw new TraverseError(message, {
|
|
91
93
|
head: maplike,
|
|
92
94
|
lastValue,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import combine from "../../src/operations/combine.js";
|
|
4
|
+
|
|
5
|
+
describe("combine", () => {
|
|
6
|
+
test("combines two trees", async () => {
|
|
7
|
+
const oldTree = {
|
|
8
|
+
a: {
|
|
9
|
+
b: "old",
|
|
10
|
+
c: "old",
|
|
11
|
+
d: "old",
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
const newTree = {
|
|
15
|
+
a: {
|
|
16
|
+
b: "new",
|
|
17
|
+
c: "old",
|
|
18
|
+
},
|
|
19
|
+
e: "new",
|
|
20
|
+
};
|
|
21
|
+
const combination = await combine(oldTree, newTree, compareFn);
|
|
22
|
+
assert.deepEqual(combination, {
|
|
23
|
+
"a/": {
|
|
24
|
+
b: ["old", "new"],
|
|
25
|
+
c: ["old", "old"],
|
|
26
|
+
d: ["old", undefined],
|
|
27
|
+
},
|
|
28
|
+
e: [undefined, "new"],
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function compareFn(a, b) {
|
|
34
|
+
return [a, b];
|
|
35
|
+
}
|
|
@@ -67,7 +67,7 @@ describe("deepEntriesIterator", () => {
|
|
|
67
67
|
packed,
|
|
68
68
|
};
|
|
69
69
|
const entries = [];
|
|
70
|
-
for await (const entry of deepEntriesIterator(tree, {
|
|
70
|
+
for await (const entry of deepEntriesIterator(tree, { unpack: true })) {
|
|
71
71
|
entries.push(entry);
|
|
72
72
|
}
|
|
73
73
|
assert.deepEqual(entries, [
|
|
@@ -6,7 +6,7 @@ import plain from "../../src/operations/plain.js";
|
|
|
6
6
|
|
|
7
7
|
describe("flat", () => {
|
|
8
8
|
test("flattens an array one level by default", async () => {
|
|
9
|
-
const result = await flat([1, 2, [3], [[4, [5]]]]
|
|
9
|
+
const result = await flat([1, 2, [3], [[4, [5]]]]);
|
|
10
10
|
assert.deepEqual(await plain(result), [1, 2, 3, [4, [5]]]);
|
|
11
11
|
});
|
|
12
12
|
|
|
@@ -16,4 +16,29 @@ describe("shuffle", () => {
|
|
|
16
16
|
const treeKeys = await keys(result);
|
|
17
17
|
assert.deepEqual(treeKeys.sort(), Object.keys(obj).sort());
|
|
18
18
|
});
|
|
19
|
+
|
|
20
|
+
test("accepts a randoms function for deterministic shuffling", async () => {
|
|
21
|
+
const obj = {
|
|
22
|
+
a: 1,
|
|
23
|
+
b: 2,
|
|
24
|
+
c: 3,
|
|
25
|
+
d: 4,
|
|
26
|
+
e: 5,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function count() {
|
|
30
|
+
let index = 0;
|
|
31
|
+
return function () {
|
|
32
|
+
return index++;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const result1 = await shuffle(obj, { randoms: count() });
|
|
37
|
+
const keys1 = await keys(result1);
|
|
38
|
+
|
|
39
|
+
const result2 = await shuffle(obj, { randoms: count() });
|
|
40
|
+
const keys2 = await keys(result2);
|
|
41
|
+
|
|
42
|
+
assert.deepEqual(keys1, keys2);
|
|
43
|
+
});
|
|
19
44
|
});
|