@weborigami/async-tree 0.6.16 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/async-tree",
3
- "version": "0.6.16",
3
+ "version": "0.6.17",
4
4
  "description": "Asynchronous tree drivers based on standard JavaScript classes",
5
5
  "type": "module",
6
6
  "main": "./main.js",
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
+ }
@@ -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 {boolean?} reshuffle
16
+ * @param {{ randoms?: (() => number) }?} options
12
17
  * @returns {Promise<AsyncMap>}
13
18
  */
14
- export default async function shuffle(maplike, reshuffle = false) {
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 || reshuffle) {
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 j = Math.floor(Math.random() * (i + 1));
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;
@@ -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
+ }
@@ -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
  });