@bablr/btree 0.4.1 → 0.4.3

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/README.md CHANGED
@@ -1,18 +1,19 @@
1
1
  # @bablr/btree
2
2
 
3
- Functional utilities for working with btrees such as those used in agAST.
3
+ This library offers a set of functional utilities for working with immutable btrees such as those used in agAST.
4
4
 
5
5
  ```js
6
- expect(push(['a', 'b'], 'c')).toEqual([
7
- 3,
8
- [['a'], ['b', 'c']],
9
- ]);
6
+ import * as btree from '@bablr/btree';
10
7
 
11
- expect(addAt(0, [3, [['x'], ['y', 'z']]], 'w')).toEqual([
12
- 4,
13
- [
14
- ['w', 'x'],
15
- ['y', 'z'],
16
- ],
17
- ]);
8
+ let tree = btree.fromValues([]);
9
+ tree = btree.push('a');
10
+ tree = btree.push('b');
11
+ tree = btree.push('c');
12
+ let tree3 = tree;
13
+ tree = btree.concat(tree, tree);
14
+ tree = btree.concat(tree3, tree);
15
+
16
+ btree.getSize(tree); // 9
17
+ btree.getAt(-2, tree); // 'b'
18
+ [...btree.traverse(tree)]; // ['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b', 'c']
18
19
  ```
@@ -1,17 +1,31 @@
1
- import emptyStack from '@iter-tools/imm-stack';
2
-
3
1
  const { isArray } = Array;
4
2
  const { freeze } = Object;
5
3
  const { isFinite } = Number;
6
4
 
7
5
  export const defaultNodeSize = 8;
8
6
 
9
- export const buildModule = (
10
- NODE_SIZE = defaultNodeSize,
11
- statsReducer,
12
- getSumsInitial,
13
- finalizeSums,
14
- ) => {
7
+ export const deepFreeze = (object) => {
8
+ let stack = [object];
9
+ while (stack.length) {
10
+ let item = stack[stack.length - 1];
11
+ stack.pop();
12
+
13
+ for (const value of Object.values(item)) {
14
+ if (value && typeof value === 'object') {
15
+ if (
16
+ !(Array.isArray(value) || [Object.prototype, null].includes(Object.getPrototypeOf(value)))
17
+ )
18
+ throw new Error();
19
+ stack.push(value);
20
+ }
21
+ }
22
+
23
+ Object.freeze(item);
24
+ }
25
+ return object;
26
+ };
27
+
28
+ export const buildModule = (NODE_SIZE = defaultNodeSize, buildStats) => {
15
29
  const sumNodes = (nodes) => {
16
30
  return nodes.map(getSize).reduce((a, b) => a + b, 0);
17
31
  };
@@ -29,7 +43,8 @@ export const buildModule = (
29
43
  };
30
44
 
31
45
  const treeFrom = (...values) => {
32
- if (!statsReducer && values.length <= NODE_SIZE) {
46
+ validateValues(values);
47
+ if (!buildStats && values.length <= NODE_SIZE) {
33
48
  return freeze(values);
34
49
  } else {
35
50
  let tree = [];
@@ -40,19 +55,14 @@ export const buildModule = (
40
55
  }
41
56
  };
42
57
 
43
- const buildStats = (values) => {
44
- let sums = values.reduce(statsReducer, getSumsInitial());
45
- sums = finalizeSums(sums);
46
- return sums;
47
- };
48
-
49
58
  const treeFromValues = (values) => {
50
- if (values.includes(undefined)) throw new Error();
51
- if (!statsReducer && values.length <= NODE_SIZE) {
59
+ validateValues(values);
60
+
61
+ if (!buildStats && values.length <= NODE_SIZE) {
52
62
  return isArray(values[0]) ? freeze([sumNodes(values), freeze(values)]) : freeze(values);
53
63
  } else {
54
64
  if (values.length <= NODE_SIZE) {
55
- if (statsReducer) {
65
+ if (buildStats) {
56
66
  return freeze([sumNodes(values), freeze(values), buildStats(values)]);
57
67
  } else {
58
68
  return freeze([sumNodes(values), freeze(values)]);
@@ -91,11 +101,11 @@ export const buildModule = (
91
101
 
92
102
  let midIndex;
93
103
 
94
- if (isLeaf) {
95
- midIndex = Math.floor(values.length / 2 + 0.01);
96
- } else {
97
- midIndex = findBalancePoint(values);
98
- }
104
+ // if (isLeaf) {
105
+ midIndex = Math.floor(values.length / 2 + 0.01);
106
+ // } else {
107
+ // midIndex = findBalancePoint(values);
108
+ // }
99
109
 
100
110
  let leftValues = values.slice(0, midIndex);
101
111
  let rightValues = values.slice(midIndex);
@@ -125,60 +135,46 @@ export const buildModule = (
125
135
  let firstValues = getValues(first);
126
136
  let secondValues = getValues(second);
127
137
 
138
+ if (!secondValues.length) return first;
139
+ if (!firstValues.length) return second;
140
+
128
141
  if (firstHeight === secondHeight) {
129
- if (firstValues.length + secondValues.length < NODE_SIZE) {
142
+ if (firstValues.length + secondValues.length <= NODE_SIZE) {
130
143
  return treeFromValues([...firstValues, ...secondValues]);
131
144
  } else {
132
- return treeFromValues([first, second]);
145
+ let { leftValues, rightValues } = splitValues([...firstValues, ...secondValues]);
146
+ return treeFromValues([treeFromValues(leftValues), treeFromValues(rightValues)]);
133
147
  }
134
148
  } else {
135
- let targetDepth = Math.abs(firstHeight - secondHeight);
136
- let path =
137
- firstHeight > secondHeight
138
- ? findPath(Infinity, first, targetDepth)
139
- : findPath(0, second, targetDepth);
140
-
141
- let { node, index } = path.value;
142
-
149
+ let tree = firstHeight <= secondHeight ? second : first;
143
150
  let pushout = firstHeight > secondHeight ? second : first;
144
151
 
145
- let values = getValues(node);
146
-
147
- for (;;) {
148
- if (pushout) {
149
- values = values.slice();
150
-
151
- let finiteIndex = index === Infinity ? values.length : index;
152
-
153
- if (!isFinite(finiteIndex)) throw new Error();
154
- if (values.length + getValues(pushout).length > NODE_SIZE) {
155
- values.splice(finiteIndex, 0, pushout);
156
- const { leftValues, rightValues } = splitValues(values);
157
-
158
- pushout = treeFromValues(leftValues);
159
- node = treeFromValues(rightValues);
160
- } else {
161
- values.splice(finiteIndex, 0, pushout);
162
- node = setValues(node, values);
163
- pushout = null;
164
- }
165
- }
152
+ if (!nodeCollapses(pushout)) {
153
+ let targetIndex = firstHeight > secondHeight ? getSize(tree) : 0;
154
+ return addAt(targetIndex, tree, pushout);
155
+ } else {
156
+ let depth = Math.abs(firstHeight - secondHeight) - 1;
157
+ let targetIndex = firstHeight > secondHeight ? getSize(tree) - 1 : 0;
158
+ let targetPath = findPath(targetIndex, tree, depth);
159
+ let lastIndex = targetPath.length - 1;
160
+ let targetNode = getValues(targetPath[lastIndex].node)[targetPath[lastIndex].index];
161
+ let combinedValues =
162
+ firstHeight > secondHeight
163
+ ? [...getValues(targetNode), ...getValues(pushout)]
164
+ : [...getValues(pushout), ...getValues(targetNode)];
165
+ if (getValues(pushout).length + getValues(targetNode).length <= NODE_SIZE) {
166
+ return replaceAt(targetIndex, tree, treeFromValues(combinedValues));
167
+ } else {
168
+ let { leftValues, rightValues } = splitValues(combinedValues);
166
169
 
167
- if (path.size === 1) {
168
- if (pushout) {
169
- return treeFromValues([pushout, node]);
170
+ if (firstHeight > secondHeight) {
171
+ return replaceAt(targetIndex, addAt(targetIndex + 1, tree, rightValues), leftValues);
172
+ // return addAt(targetIndex + 1, replaceAt(targetIndex, tree, leftValues), rightValues);
170
173
  } else {
171
- return node;
174
+ // TODO change order of operations like above?
175
+ return addAt(targetIndex, replaceAt(targetIndex, tree, rightValues), leftValues);
172
176
  }
173
177
  }
174
-
175
- const poppedNode = node;
176
- path = path.pop();
177
- ({ node, index } = path.value);
178
-
179
- node = setValuesAt(index, node, poppedNode);
180
- values = getValues(node);
181
- path = path.replace({ node, index });
182
178
  }
183
179
  }
184
180
  };
@@ -188,9 +184,10 @@ export const buildModule = (
188
184
  if (!isArray(tree)) throw new Error();
189
185
  if (!value) throw new Error();
190
186
 
191
- let path = findPath(idx, tree);
187
+ let path = findPath(idx, tree, getHeight(tree) - getHeight(value) - 1);
192
188
 
193
- let { node, index } = path.value;
189
+ let pathIdx = path.length - 1;
190
+ let { node, index } = path[pathIdx];
194
191
 
195
192
  let pushout = value;
196
193
 
@@ -203,7 +200,7 @@ export const buildModule = (
203
200
  let finiteIndex = index === Infinity ? values.length : index;
204
201
 
205
202
  if (!isFinite(finiteIndex)) throw new Error();
206
- if (values.length + getValues(pushout).length > NODE_SIZE) {
203
+ if (values.length + (pushout ? 1 : 0) > NODE_SIZE) {
207
204
  values.splice(finiteIndex, 0, pushout);
208
205
  const { leftValues, rightValues } = splitValues(values);
209
206
 
@@ -216,7 +213,7 @@ export const buildModule = (
216
213
  }
217
214
  }
218
215
 
219
- if (path.size === 1) {
216
+ if (pathIdx === 0) {
220
217
  if (pushout) {
221
218
  return treeFromValues([pushout, node]);
222
219
  } else {
@@ -225,12 +222,11 @@ export const buildModule = (
225
222
  }
226
223
 
227
224
  const poppedNode = node;
228
- path = path.pop();
229
- ({ node, index } = path.value);
225
+ pathIdx--;
226
+ ({ node, index } = path[pathIdx]);
230
227
 
231
228
  node = setValuesAt(index, node, poppedNode);
232
229
  values = getValues(node);
233
- path = path.replace({ node, index });
234
230
  }
235
231
  };
236
232
 
@@ -239,8 +235,12 @@ export const buildModule = (
239
235
  return size ? addAt(size, tree, value) : treeFromValues([value]);
240
236
  };
241
237
 
238
+ const unshift = (value, tree) => {
239
+ return addAt(0, tree, value);
240
+ };
241
+
242
242
  const collapses = (size) => {
243
- return size > NODE_SIZE / 2 + 0.01;
243
+ return size < NODE_SIZE / 2 - 0.01;
244
244
  };
245
245
 
246
246
  const nodeCollapses = (node) => {
@@ -248,13 +248,16 @@ export const buildModule = (
248
248
  };
249
249
 
250
250
  const nodeCanDonate = (node) => {
251
- return collapses(getValues(node).length - 1);
251
+ return !collapses(getValues(node).length - 1);
252
252
  };
253
253
 
254
254
  const removeAt = (idx, tree) => {
255
+ if (idx > getSize(tree)) throw new Error('Index exceeds tree bounds');
256
+
255
257
  let path = findPath(idx, tree);
256
258
 
257
- let { node, index } = path.value;
259
+ let pathIdx = path.length - 1;
260
+ let { node, index } = path[pathIdx];
258
261
 
259
262
  const initialValues = [...getValues(node)];
260
263
 
@@ -266,8 +269,8 @@ export const buildModule = (
266
269
  let values = getValues(returnValue);
267
270
  let adjustSibling = null;
268
271
 
269
- if (path.size > 1 && nodeCollapses(returnValue)) {
270
- let { node: parentNode, index: parentIndex } = path.prev.value;
272
+ if (pathIdx >= 1 && nodeCollapses(returnValue)) {
273
+ let { node: parentNode, index: parentIndex } = path[pathIdx - 1];
271
274
  const prevSibling = getValues(parentNode)[parentIndex - 1];
272
275
  const nextSibling = getValues(parentNode)[parentIndex + 1];
273
276
  let targetSibling = nodeCanDonate(prevSibling)
@@ -297,15 +300,12 @@ export const buildModule = (
297
300
  }
298
301
  }
299
302
 
300
- if (path.size === 1) {
301
- if (getSize(returnValue) <= NODE_SIZE) {
302
- returnValue = setValues(returnValue, getValues(returnValue).flat());
303
- }
303
+ if (pathIdx === 0) {
304
304
  return returnValue;
305
305
  }
306
306
 
307
- path = path.pop();
308
- ({ node, index } = path.value);
307
+ pathIdx--;
308
+ ({ node, index } = path[pathIdx]);
309
309
 
310
310
  values = getValues(node).slice();
311
311
 
@@ -328,6 +328,24 @@ export const buildModule = (
328
328
  return removeAt(-1, tree);
329
329
  };
330
330
 
331
+ const shift = (tree) => {
332
+ return removeAt(0, tree);
333
+ };
334
+
335
+ const validateValues = (values) => {
336
+ if (values.length > NODE_SIZE) throw new Error();
337
+ if (!values.length) return;
338
+
339
+ let nodeIsLeaf = !isArray(values[0]);
340
+ for (let value of values) {
341
+ if (!nodeIsLeaf && nodeCollapses(value)) {
342
+ throw new Error('invalid btree: uncollapsed node');
343
+ } else if (!nodeIsLeaf && value == null) {
344
+ throw new Error('invalid btree: nil sibling');
345
+ }
346
+ }
347
+ };
348
+
331
349
  const isValidNode = (node) => {
332
350
  if (!isArray(node)) return false;
333
351
  const values = getValues(node);
@@ -347,22 +365,27 @@ export const buildModule = (
347
365
 
348
366
  const getSums = (node) => {
349
367
  if (!isValidNode(node)) throw new Error();
350
- return node[2];
368
+ let sums = Number.isFinite(node[0]) ? node[2] : null;
369
+ return sums || (buildStats ? buildStats(getValues(node)) : null);
351
370
  };
352
371
 
353
372
  const setValues = (node, values) => {
354
- if (values.length > NODE_SIZE) throw new Error();
373
+ if (isFinite(node[0]) || buildStats) {
374
+ return treeFromValues(values);
375
+ }
355
376
 
356
- return isFinite(node[0]) || statsReducer ? treeFromValues(values) : freeze(values);
377
+ validateValues(values);
378
+
379
+ return freeze(values);
357
380
  };
358
381
 
359
382
  function* traverse(tree) {
360
- let states = emptyStack.push({ node: tree, i: 0 });
383
+ let states = [{ node: tree, i: 0 }];
361
384
 
362
385
  assertValidNode(tree);
363
386
 
364
- stack: while (states.size) {
365
- const s = states.value;
387
+ stack: while (states.length) {
388
+ const s = states[states.length - 1];
366
389
  const { node } = s;
367
390
 
368
391
  const values = getValues(node);
@@ -372,7 +395,7 @@ export const buildModule = (
372
395
  if (isArray(value)) {
373
396
  let node = value;
374
397
  assertValidNode(node);
375
- states = states.push({ node, i: 0 });
398
+ states.push({ node, i: 0 });
376
399
  i = ++s.i;
377
400
  continue stack;
378
401
  } else {
@@ -381,7 +404,7 @@ export const buildModule = (
381
404
  }
382
405
  }
383
406
 
384
- states = states.pop();
407
+ states.pop();
385
408
  }
386
409
  }
387
410
 
@@ -399,15 +422,33 @@ export const buildModule = (
399
422
 
400
423
  const findPath = (idx, tree, depth = Infinity) => {
401
424
  if (idx == null) throw new Error();
425
+ if (tree && !isArray(tree)) throw new Error();
426
+
427
+ let path = [];
428
+ let node = tree;
402
429
 
403
- let path = emptyStack;
430
+ if (isArray(idx)) {
431
+ for (let seg of idx) {
432
+ let index = typeof seg !== 'object' ? seg : seg.index;
433
+ if (typeof index === 'string') throw new Error();
434
+ if (!isArray(node)) return null;
435
+ let index_ = index < 0 ? getSize(node) + index : index;
436
+ path.push({ index: index_, node });
437
+ node = getValues(node)[index_];
438
+ if (node && !isArray(node)) {
439
+ return freeze(path);
440
+ }
441
+ if (!node) return null;
442
+ }
443
+
444
+ return freeze(path);
445
+ }
404
446
 
405
447
  let treeSum = getSize(tree);
406
448
  let currentIdx = idx < 0 ? treeSum - 1 : 0;
407
449
  let direction = idx < 0 ? -1 : 1;
408
450
  let targetIdx = idx < 0 ? treeSum + idx : idx;
409
451
 
410
- let node = tree;
411
452
  stack: while (node) {
412
453
  assertValidNode(node);
413
454
 
@@ -422,17 +463,16 @@ export const buildModule = (
422
463
  i += increment
423
464
  ) {
424
465
  let value = values[i];
425
- if (isArray(value) && path.size < depth - 1) {
466
+ if (isArray(value) && path.length < depth) {
426
467
  candidateNode = value;
427
468
 
428
469
  const sum = getSize(candidateNode);
429
-
430
- const nextCount = currentIdx + sum * direction;
470
+ const nextIndex = currentIdx + sum * direction;
431
471
  if (
432
- (backwards ? nextCount <= targetIdx : nextCount > targetIdx) ||
433
- (backwards ? nextCount < 0 : nextCount >= treeSum)
472
+ (backwards ? nextIndex < targetIdx : nextIndex > targetIdx) ||
473
+ (backwards ? nextIndex < 0 : nextIndex >= treeSum)
434
474
  ) {
435
- path = path.push({ index: i, node });
475
+ path.push({ index: i, node });
436
476
  node = candidateNode;
437
477
  continue stack;
438
478
  } else {
@@ -440,35 +480,43 @@ export const buildModule = (
440
480
  }
441
481
  } else {
442
482
  const sum = getSize(value);
443
- const nextCount = currentIdx + sum * direction;
483
+ const nextIndex = currentIdx + sum * direction;
444
484
  if (!isFinite(targetIdx)) {
445
- return path.push({ index: targetIdx, node });
446
- } else if (currentIdx === targetIdx) {
447
- return path.push({ index: i, node });
485
+ path.push({ index: targetIdx, node });
486
+
487
+ return freeze(path);
488
+ } else if (backwards ? nextIndex < targetIdx : nextIndex > targetIdx) {
489
+ path.push({ index: i, node });
490
+
491
+ return freeze(path);
448
492
  } else if (
449
493
  backwards
450
- ? nextCount < targetIdx || nextCount < 0
451
- : nextCount > targetIdx || nextCount >= treeSum
494
+ ? nextIndex < targetIdx || nextIndex < 0
495
+ : nextIndex > targetIdx || nextIndex >= treeSum
452
496
  ) {
453
497
  break;
454
498
  } else {
455
- currentIdx += direction;
499
+ currentIdx += direction * sum;
456
500
  }
457
501
  }
458
502
  }
459
- return path.push({ index: backwards ? -Infinity : Infinity, node });
503
+
504
+ path.push({ index: backwards ? -Infinity : Infinity, node });
505
+
506
+ return freeze(path);
460
507
  }
461
508
 
462
509
  return null;
463
510
  };
464
511
 
465
512
  const getAt = (idx, tree) => {
466
- const v = findPath(idx, tree)?.value;
467
- return v && getValues(v.node)[v.index];
513
+ const path = findPath(idx, tree);
514
+ let seg = path && path[path.length - 1];
515
+ return seg && getValues(seg.node)[seg.index];
468
516
  };
469
517
 
470
518
  const replaceAt = (idx, tree, value) => {
471
- let path = findPath(idx, tree);
519
+ let path = findPath(idx, tree, getHeight(tree) - getHeight(value) - 1);
472
520
 
473
521
  if (getSize(tree) < idx) {
474
522
  throw new Error('Cannot add past the end of a list');
@@ -476,16 +524,17 @@ export const buildModule = (
476
524
  return addAt(idx, tree, value);
477
525
  }
478
526
 
479
- let { node, index } = path.value;
527
+ let pathIndex = path.length - 1;
528
+ let { node, index } = path[pathIndex];
480
529
 
481
530
  let returnValue = setValuesAt(index, node, value);
482
531
 
483
532
  for (;;) {
484
- ({ node, index } = path.value);
533
+ ({ node, index } = path[pathIndex]);
485
534
 
486
- if (path.size > 1) {
487
- path = path.pop();
488
- ({ node, index } = path.value);
535
+ if (pathIndex > 0) {
536
+ pathIndex--;
537
+ ({ node, index } = path[pathIndex]);
489
538
 
490
539
  returnValue = setValuesAt(index, node, returnValue);
491
540
  } else {
@@ -505,10 +554,13 @@ export const buildModule = (
505
554
  collapses,
506
555
  nodeCollapses,
507
556
  nodeCanDonate,
557
+ unshift,
508
558
  pop,
509
559
  removeAt,
510
560
  push,
511
561
  addAt,
562
+ shift,
563
+ unshift,
512
564
  concat,
513
565
  isValidNode,
514
566
  assertValidNode,
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { defaultNodeSize, buildModule } from './enhanceable.js';
1
+ import { defaultNodeSize, deepFreeze, buildModule } from './enhanceable.js';
2
2
 
3
- export { defaultNodeSize };
3
+ export { defaultNodeSize, deepFreeze };
4
4
 
5
5
  export const {
6
6
  btreeFrom,
@@ -14,7 +14,10 @@ export const {
14
14
  nodeCanDonate,
15
15
  pop,
16
16
  push,
17
+ concat,
17
18
  addAt,
19
+ shift,
20
+ unshift,
18
21
  isValidNode,
19
22
  assertValidNode,
20
23
  getValues,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bablr/btree",
3
3
  "description": "Functional utilities for working with btrees such as those used in agAST",
4
- "version": "0.4.1",
4
+ "version": "0.4.3",
5
5
  "author": "Conrad Buck<conartist6@gmail.com>",
6
6
  "type": "module",
7
7
  "files": [
@@ -15,9 +15,7 @@
15
15
  "scripts": {
16
16
  "test": "mocha test/*.test.js"
17
17
  },
18
- "dependencies": {
19
- "@iter-tools/imm-stack": "1.2.0"
20
- },
18
+ "dependencies": {},
21
19
  "devDependencies": {
22
20
  "@bablr/eslint-config-base": "github:bablr-lang/eslint-config-base#c97bfa4b3663f8378e9b3e42bb5a41e685406cf9",
23
21
  "enhanced-resolve": "^5.12.0",
@@ -29,7 +27,10 @@
29
27
  "mocha": "10.7.3",
30
28
  "prettier": "^2.6.2"
31
29
  },
32
- "repository": "github:bablr-lang/btree",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/bablr-lang/btree.git"
33
+ },
33
34
  "homepage": "https://github.com/bablr-lang/btree",
34
35
  "license": "MIT"
35
36
  }