From b2829023dd19147c4df521fed7b8fa8fe064e8dc Mon Sep 17 00:00:00 2001 From: Taylor Williams Date: Fri, 21 Nov 2025 14:31:16 -0800 Subject: [PATCH 1/2] add cached size and more robust checkValid --- b+tree.d.ts | 1 - b+tree.js | 169 ++++++++++++++++++++++++++++++++++++++---------- b+tree.ts | 181 ++++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 284 insertions(+), 67 deletions(-) diff --git a/b+tree.d.ts b/b+tree.d.ts index a8ff453..57fddd7 100644 --- a/b+tree.d.ts +++ b/b+tree.d.ts @@ -105,7 +105,6 @@ export declare function simpleComparator(a: (number | string)[], b: (number | st */ export default class BTree implements ISortedMapF, ISortedMap { private _root; - _size: number; _maxNodeSize: number; /** * provides a total order over keys (and a strict partial order over the type K) diff --git a/b+tree.js b/b+tree.js index df9add2..637db72 100644 --- a/b+tree.js +++ b/b+tree.js @@ -15,7 +15,7 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -exports.EmptyBTree = exports.check = exports.BNodeInternal = exports.BNode = exports.asSet = exports.simpleComparator = exports.defaultComparator = void 0; +exports.EmptyBTree = exports.check = exports.areOverlapping = exports.sumChildSizes = exports.BNodeInternal = exports.BNode = exports.asSet = exports.simpleComparator = exports.defaultComparator = void 0; /** * Compares DefaultComparables to form a strict partial ordering. * @@ -155,7 +155,6 @@ var BTree = /** @class */ (function () { */ function BTree(entries, compare, maxNodeSize) { this._root = EmptyLeaf; - this._size = 0; this._maxNodeSize = maxNodeSize >= 4 ? Math.min(maxNodeSize, 256) : 32; this._compare = compare || defaultComparator; if (entries) @@ -165,26 +164,25 @@ var BTree = /** @class */ (function () { ///////////////////////////////////////////////////////////////////////////// // ES6 Map methods ///////////////////////////////////////////////////// /** Gets the number of key-value pairs in the tree. */ - get: function () { return this._size; }, + get: function () { return this._root.size(); }, enumerable: false, configurable: true }); Object.defineProperty(BTree.prototype, "length", { /** Gets the number of key-value pairs in the tree. */ - get: function () { return this._size; }, + get: function () { return this.size; }, enumerable: false, configurable: true }); Object.defineProperty(BTree.prototype, "isEmpty", { /** Returns true iff the tree contains no key-value pairs. */ - get: function () { return this._size === 0; }, + get: function () { return this._root.size() === 0; }, enumerable: false, configurable: true }); /** Releases the tree so that its size is 0. */ BTree.prototype.clear = function () { this._root = EmptyLeaf; - this._size = 0; }; /** Runs a function for each key-value pair, in order from smallest to * largest key. For compatibility with ES6 Map, the argument order to @@ -248,7 +246,8 @@ var BTree = /** @class */ (function () { if (result === true || result === false) return result; // Root node has split, so create a new root node. - this._root = new BNodeInternal([this._root, result]); + var children = [this._root, result]; + this._root = new BNodeInternal(children, sumChildSizes(children)); return true; }; /** @@ -537,7 +536,6 @@ var BTree = /** @class */ (function () { this._root.isShared = true; var result = new BTree(undefined, this._compare, this._maxNodeSize); result._root = this._root; - result._size = this._size; return result; }; /** Performs a greedy clone, immediately duplicating any nodes that are @@ -548,7 +546,6 @@ var BTree = /** @class */ (function () { BTree.prototype.greedyClone = function (force) { var result = new BTree(undefined, this._compare, this._maxNodeSize); result._root = this._root.greedyClone(force); - result._size = this._size; return result; }; /** Gets an array filled with the contents of the tree, sorted by key */ @@ -830,7 +827,7 @@ var BTree = /** @class */ (function () { * skips the most expensive test - whether all keys are sorted - but it * does check that maxKey() of the children of internal nodes are sorted. */ BTree.prototype.checkValid = function () { - var size = this._root.checkValid(0, this, 0); + var size = this._root.checkValid(0, this, 0)[0]; check(size === this.size, "size mismatch: counted ", size, "but stored", this.size); }; return BTree; @@ -870,6 +867,9 @@ var BNode = /** @class */ (function () { enumerable: false, configurable: true }); + BNode.prototype.size = function () { + return this.keys.length; + }; /////////////////////////////////////////////////////////////////////////// // Shared methods ///////////////////////////////////////////////////////// BNode.prototype.maxKey = function () { @@ -1004,7 +1004,11 @@ var BNode = /** @class */ (function () { // it can't be merged with adjacent nodes. However, the parent will // verify that the average node size is at least half of the maximum. check(depth == 0 || kL > 0, "empty leaf at depth", depth, "and baseIndex", baseIndex); - return kL; + for (var i = 1; i < kL; i++) { + var c = tree._compare(this.keys[i - 1], this.keys[i]); + check(c < 0, "keys out of order at depth", depth, "and baseIndex", baseIndex + i - 1, ": ", this.keys[i - 1], " !< ", this.keys[i]); + } + return [kL, this.keys[0], this.keys[kL - 1]]; }; ///////////////////////////////////////////////////////////////////////////// // Leaf Node: set & node splitting ////////////////////////////////////////// @@ -1013,7 +1017,6 @@ var BNode = /** @class */ (function () { if (i < 0) { // key does not exist yet i = ~i; - tree._size++; if (this.keys.length < tree._maxNodeSize) { return this.insertInLeaf(i, key, value, tree); } @@ -1129,7 +1132,6 @@ var BNode = /** @class */ (function () { this.keys.splice(i, 1); if (this.values !== undefVals) this.values.splice(i, 1); - tree._size--; i--; iHigh--; } @@ -1167,7 +1169,7 @@ var BNodeInternal = /** @class */ (function (_super) { * This does not mark `children` as shared, so it is the responsibility of the caller * to ensure children are either marked shared, or aren't included in another tree. */ - function BNodeInternal(children, keys) { + function BNodeInternal(children, size, keys) { var _this = this; if (!keys) { keys = []; @@ -1176,18 +1178,22 @@ var BNodeInternal = /** @class */ (function (_super) { } _this = _super.call(this, keys) || this; _this.children = children; + _this._size = size; return _this; } BNodeInternal.prototype.clone = function () { var children = this.children.slice(0); for (var i = 0; i < children.length; i++) children[i].isShared = true; - return new BNodeInternal(children, this.keys.slice(0)); + return new BNodeInternal(children, this._size, this.keys.slice(0)); + }; + BNodeInternal.prototype.size = function () { + return this._size; }; BNodeInternal.prototype.greedyClone = function (force) { if (this.isShared && !force) return this; - var nu = new BNodeInternal(this.children.slice(0), this.keys.slice(0)); + var nu = new BNodeInternal(this.children.slice(0), this._size, this.keys.slice(0)); for (var i = 0; i < nu.children.length; i++) nu.children[i] = nu.children[i].greedyClone(force); return nu; @@ -1230,22 +1236,35 @@ var BNodeInternal = /** @class */ (function (_super) { check(kL === cL, "keys/children length mismatch: depth", depth, "lengths", kL, cL, "baseIndex", baseIndex); check(kL > 1 || depth > 0, "internal node has length", kL, "at depth", depth, "baseIndex", baseIndex); var size = 0, c = this.children, k = this.keys, childSize = 0; + var prevMinKey = undefined; + var prevMaxKey = undefined; for (var i = 0; i < cL; i++) { - size += c[i].checkValid(depth + 1, tree, baseIndex + size); - childSize += c[i].keys.length; + var child = c[i]; + var _a = child.checkValid(depth + 1, tree, baseIndex + size), subtreeSize = _a[0], minKey = _a[1], maxKey = _a[2]; + check(subtreeSize === child.size(), "cached size mismatch at depth", depth, "index", i, "baseIndex", baseIndex); + check(subtreeSize === 1 || tree._compare(minKey, maxKey) < 0, "child node keys not sorted at depth", depth, "index", i, "baseIndex", baseIndex); + if (prevMinKey !== undefined && prevMaxKey !== undefined) { + check(!areOverlapping(prevMinKey, prevMaxKey, minKey, maxKey, tree._compare), "children keys not sorted at depth", depth, "index", i, "baseIndex", baseIndex, ": ", prevMaxKey, " !< ", minKey); + check(tree._compare(prevMaxKey, minKey) < 0, "children keys not sorted at depth", depth, "index", i, "baseIndex", baseIndex, ": ", prevMaxKey, " !< ", minKey); + } + prevMinKey = minKey; + prevMaxKey = maxKey; + size += subtreeSize; + childSize += child.keys.length; check(size >= childSize, "wtf", baseIndex); // no way this will ever fail - check(i === 0 || c[i - 1].constructor === c[i].constructor, "type mismatch, baseIndex:", baseIndex); - if (c[i].maxKey() != k[i]) - check(false, "keys[", i, "] =", k[i], "is wrong, should be ", c[i].maxKey(), "at depth", depth, "baseIndex", baseIndex); + check(i === 0 || c[i - 1].constructor === child.constructor, "type mismatch, baseIndex:", baseIndex); + if (child.maxKey() != k[i]) + check(false, "keys[", i, "] =", k[i], "is wrong, should be ", child.maxKey(), "at depth", depth, "baseIndex", baseIndex); if (!(i === 0 || tree._compare(k[i - 1], k[i]) < 0)) check(false, "sort violation at depth", depth, "index", i, "keys", k[i - 1], k[i]); } + check(this._size === size, "internal node cached size mismatch at depth", depth, "baseIndex", baseIndex, "cached", this._size, "actual", size); // 2020/08: BTree doesn't always avoid grossly undersized nodes, // but AFAIK such nodes are pretty harmless, so accept them. var toofew = childSize === 0; // childSize < (tree.maxNodeSize >> 1)*cL; if (toofew || childSize > tree.maxNodeSize * cL) check(false, toofew ? "too few" : "too many", "children (", childSize, size, ") at depth", depth, "maxNodeSize:", tree.maxNodeSize, "children.length:", cL, "baseIndex:", baseIndex); - return size; + return [size, this.minKey(), this.maxKey()]; }; ///////////////////////////////////////////////////////////////////////////// // Internal Node: set & node splitting ////////////////////////////////////// @@ -1273,7 +1292,9 @@ var BNodeInternal = /** @class */ (function (_super) { this.keys[i] = c[i].maxKey(); } } + var oldSize = child.size(); var result = child.set(key, value, overwrite, tree); + this._size += child.size() - oldSize; if (result === false) return false; this.keys[i] = child.maxKey(); @@ -1302,6 +1323,7 @@ var BNodeInternal = /** @class */ (function (_super) { BNodeInternal.prototype.insert = function (i, child) { this.children.splice(i, 0, child); this.keys.splice(i, 0, child.maxKey()); + this._size += child.size(); }; /** * Split this node. @@ -1310,21 +1332,50 @@ var BNodeInternal = /** @class */ (function (_super) { BNodeInternal.prototype.splitOffRightSide = function () { // assert !this.isShared; var half = this.children.length >> 1; - return new BNodeInternal(this.children.splice(half), this.keys.splice(half)); + var newChildren = this.children.splice(half); + var newKeys = this.keys.splice(half); + var sizePrev = this._size; + this._size = sumChildSizes(this.children); + var newNode = new BNodeInternal(newChildren, sizePrev - this._size, newKeys); + return newNode; + }; + /** + * Split this node. + * Modifies this to remove the first half of the items, returning a separate node containing them. + */ + BNodeInternal.prototype.splitOffLeftSide = function () { + // assert !this.isShared; + var half = this.children.length >> 1; + var newChildren = this.children.splice(0, half); + var newKeys = this.keys.splice(0, half); + var sizePrev = this._size; + this._size = sumChildSizes(this.children); + var newNode = new BNodeInternal(newChildren, sizePrev - this._size, newKeys); + return newNode; }; BNodeInternal.prototype.takeFromRight = function (rhs) { // Reminder: parent node must update its copy of key for this node // assert: neither node is shared // assert rhs.keys.length > (maxNodeSize/2 && this.keys.length (maxNodeSize/2 && this.keys.length= 0 && aMinBMax <= 0) { + // case 2 or 4 + return true; + } + var aMaxBMin = cmp(aMax, bMin); + var aMaxBMax = cmp(aMax, bMax); + if (aMaxBMin >= 0 && aMaxBMax <= 0) { + // case 1 + return true; + } + // case 3 or no overlap + return aMinBMin <= 0 && aMaxBMax >= 0; +} +exports.areOverlapping = areOverlapping; var Delete = { delete: true }, DeleteRange = function () { return Delete; }; var Break = { break: true }; var EmptyLeaf = (function () { diff --git a/b+tree.ts b/b+tree.ts index 084055c..c0d80c7 100644 --- a/b+tree.ts +++ b/b+tree.ts @@ -184,7 +184,6 @@ export function simpleComparator(a: any, b: any): number { export default class BTree implements ISortedMapF, ISortedMap { private _root: BNode = EmptyLeaf as BNode; - _size: number = 0; _maxNodeSize: number; /** @@ -212,16 +211,15 @@ export default class BTree implements ISortedMapF, ISortedMap // ES6 Map methods ///////////////////////////////////////////////////// /** Gets the number of key-value pairs in the tree. */ - get size() { return this._size; } + get size(): number { return this._root.size(); } /** Gets the number of key-value pairs in the tree. */ - get length() { return this._size; } + get length(): number { return this.size; } /** Returns true iff the tree contains no key-value pairs. */ - get isEmpty() { return this._size === 0; } + get isEmpty(): boolean { return this._root.size() === 0; } /** Releases the tree so that its size is 0. */ clear() { this._root = EmptyLeaf as BNode; - this._size = 0; } forEach(callback: (v:V, k:K, tree:BTree) => void, thisArg?: any): number; @@ -290,7 +288,8 @@ export default class BTree implements ISortedMapF, ISortedMap if (result === true || result === false) return result; // Root node has split, so create a new root node. - this._root = new BNodeInternal([this._root, result]); + const children = [this._root, result]; + this._root = new BNodeInternal(children, sumChildSizes(children)); return true; } @@ -615,7 +614,6 @@ export default class BTree implements ISortedMapF, ISortedMap this._root.isShared = true; var result = new BTree(undefined, this._compare, this._maxNodeSize); result._root = this._root; - result._size = this._size; return result as this; } @@ -627,7 +625,6 @@ export default class BTree implements ISortedMapF, ISortedMap greedyClone(force?: boolean): this { var result = new BTree(undefined, this._compare, this._maxNodeSize); result._root = this._root.greedyClone(force); - result._size = this._size; return result as this; } @@ -926,7 +923,7 @@ export default class BTree implements ISortedMapF, ISortedMap * skips the most expensive test - whether all keys are sorted - but it * does check that maxKey() of the children of internal nodes are sorted. */ checkValid() { - var size = this._root.checkValid(0, this, 0); + var [size] = this._root.checkValid(0, this, 0); check(size === this.size, "size mismatch: counted ", size, "but stored", this.size); } } @@ -973,6 +970,10 @@ export class BNode { this.isShared = undefined; } + size(): number { + return this.keys.length; + } + /////////////////////////////////////////////////////////////////////////// // Shared methods ///////////////////////////////////////////////////////// @@ -1111,7 +1112,7 @@ export class BNode { return undefined; } - checkValid(depth: number, tree: BTree, baseIndex: number): number { + checkValid(depth: number, tree: BTree, baseIndex: number): [size: number, min: K, max: K] { var kL = this.keys.length, vL = this.values.length; check(this.values === undefVals ? kL <= vL : kL === vL, "keys/values length mismatch: depth", depth, "with lengths", kL, vL, "and baseIndex", baseIndex); @@ -1121,7 +1122,12 @@ export class BNode { // it can't be merged with adjacent nodes. However, the parent will // verify that the average node size is at least half of the maximum. check(depth == 0 || kL > 0, "empty leaf at depth", depth, "and baseIndex", baseIndex); - return kL; + for (var i = 1; i < kL; i++) { + var c = tree._compare(this.keys[i-1], this.keys[i]); + check(c < 0, "keys out of order at depth", depth, "and baseIndex", baseIndex + i - 1, + ": ", this.keys[i-1], " !< ", this.keys[i]); + } + return [kL, this.keys[0], this.keys[kL - 1]]; } ///////////////////////////////////////////////////////////////////////////// @@ -1132,8 +1138,6 @@ export class BNode { if (i < 0) { // key does not exist yet i = ~i; - tree._size++; - if (this.keys.length < tree._maxNodeSize) { return this.insertInLeaf(i, key, value, tree); } else { @@ -1251,7 +1255,6 @@ export class BNode { this.keys.splice(i, 1); if (this.values !== undefVals) this.values.splice(i, 1); - tree._size--; i--; iHigh--; } else if (result.hasOwnProperty('value')) { @@ -1286,12 +1289,13 @@ export class BNodeInternal extends BNode { // children, but I find it easier to keep the array lengths equal: each // keys[i] caches the value of children[i].maxKey(). children: BNode[]; + _size: number; /** * This does not mark `children` as shared, so it is the responsibility of the caller * to ensure children are either marked shared, or aren't included in another tree. */ - constructor(children: BNode[], keys?: K[]) { + constructor(children: BNode[], size: number, keys?: K[]) { if (!keys) { keys = []; for (var i = 0; i < children.length; i++) @@ -1299,19 +1303,24 @@ export class BNodeInternal extends BNode { } super(keys); this.children = children; + this._size = size; } clone(): BNode { var children = this.children.slice(0); for (var i = 0; i < children.length; i++) children[i].isShared = true; - return new BNodeInternal(children, this.keys.slice(0)); + return new BNodeInternal(children, this._size, this.keys.slice(0)); + } + + size(): number { + return this._size; } greedyClone(force?: boolean): BNode { if (this.isShared && !force) return this; - var nu = new BNodeInternal(this.children.slice(0), this.keys.slice(0)); + var nu = new BNodeInternal(this.children.slice(0), this._size, this.keys.slice(0)); for (var i = 0; i < nu.children.length; i++) nu.children[i] = nu.children[i].greedyClone(force); return nu; @@ -1356,27 +1365,42 @@ export class BNodeInternal extends BNode { return result; } - checkValid(depth: number, tree: BTree, baseIndex: number): number { + checkValid(depth: number, tree: BTree, baseIndex: number): [size: number, min: K, max: K] { let kL = this.keys.length, cL = this.children.length; check(kL === cL, "keys/children length mismatch: depth", depth, "lengths", kL, cL, "baseIndex", baseIndex); check(kL > 1 || depth > 0, "internal node has length", kL, "at depth", depth, "baseIndex", baseIndex); let size = 0, c = this.children, k = this.keys, childSize = 0; + let prevMinKey: K | undefined = undefined; + let prevMaxKey: K | undefined = undefined; for (var i = 0; i < cL; i++) { - size += c[i].checkValid(depth + 1, tree, baseIndex + size); - childSize += c[i].keys.length; + var child = c[i]; + var [subtreeSize, minKey, maxKey] = child.checkValid(depth + 1, tree, baseIndex + size); + check(subtreeSize === child.size(), "cached size mismatch at depth", depth, "index", i, "baseIndex", baseIndex); + check(subtreeSize === 1 || tree._compare(minKey, maxKey) < 0, "child node keys not sorted at depth", depth, "index", i, "baseIndex", baseIndex); + if (prevMinKey !== undefined && prevMaxKey !== undefined) { + check(!areOverlapping(prevMinKey, prevMaxKey, minKey, maxKey, tree._compare), "children keys not sorted at depth", depth, "index", i, "baseIndex", baseIndex, + ": ", prevMaxKey, " !< ", minKey); + check(tree._compare(prevMaxKey, minKey) < 0, "children keys not sorted at depth", depth, "index", i, "baseIndex", baseIndex, + ": ", prevMaxKey, " !< ", minKey); + } + prevMinKey = minKey; + prevMaxKey = maxKey; + size += subtreeSize; + childSize += child.keys.length; check(size >= childSize, "wtf", baseIndex); // no way this will ever fail - check(i === 0 || c[i-1].constructor === c[i].constructor, "type mismatch, baseIndex:", baseIndex); - if (c[i].maxKey() != k[i]) - check(false, "keys[", i, "] =", k[i], "is wrong, should be ", c[i].maxKey(), "at depth", depth, "baseIndex", baseIndex); + check(i === 0 || c[i-1].constructor === child.constructor, "type mismatch, baseIndex:", baseIndex); + if (child.maxKey() != k[i]) + check(false, "keys[", i, "] =", k[i], "is wrong, should be ", child.maxKey(), "at depth", depth, "baseIndex", baseIndex); if (!(i === 0 || tree._compare(k[i-1], k[i]) < 0)) check(false, "sort violation at depth", depth, "index", i, "keys", k[i-1], k[i]); } + check(this._size === size, "internal node cached size mismatch at depth", depth, "baseIndex", baseIndex, "cached", this._size, "actual", size); // 2020/08: BTree doesn't always avoid grossly undersized nodes, // but AFAIK such nodes are pretty harmless, so accept them. let toofew = childSize === 0; // childSize < (tree.maxNodeSize >> 1)*cL; if (toofew || childSize > tree.maxNodeSize*cL) check(false, toofew ? "too few" : "too many", "children (", childSize, size, ") at depth", depth, "maxNodeSize:", tree.maxNodeSize, "children.length:", cL, "baseIndex:", baseIndex); - return size; + return [size, this.minKey()!, this.maxKey()]; } ///////////////////////////////////////////////////////////////////////////// @@ -1407,7 +1431,9 @@ export class BNodeInternal extends BNode { } } + var oldSize = child.size(); var result = child.set(key, value, overwrite, tree); + this._size += child.size() - oldSize; if (result === false) return false; this.keys[i] = child.maxKey(); @@ -1437,6 +1463,7 @@ export class BNodeInternal extends BNode { insert(i: index, child: BNode) { this.children.splice(i, 0, child); this.keys.splice(i, 0, child.maxKey()); + this._size += child.size(); } /** @@ -1445,24 +1472,54 @@ export class BNodeInternal extends BNode { */ splitOffRightSide() { // assert !this.isShared; - var half = this.children.length >> 1; - return new BNodeInternal(this.children.splice(half), this.keys.splice(half)); + const half = this.children.length >> 1; + const newChildren = this.children.splice(half); + const newKeys = this.keys.splice(half); + const sizePrev = this._size; + this._size = sumChildSizes(this.children); + const newNode = new BNodeInternal(newChildren, sizePrev - this._size, newKeys); + return newNode; + } + + /** + * Split this node. + * Modifies this to remove the first half of the items, returning a separate node containing them. + */ + splitOffLeftSide() { + // assert !this.isShared; + const half = this.children.length >> 1; + const newChildren = this.children.splice(0, half); + const newKeys = this.keys.splice(0, half); + const sizePrev = this._size; + this._size = sumChildSizes(this.children); + const newNode = new BNodeInternal(newChildren, sizePrev - this._size, newKeys); + return newNode; } takeFromRight(rhs: BNode) { // Reminder: parent node must update its copy of key for this node // assert: neither node is shared // assert rhs.keys.length > (maxNodeSize/2 && this.keys.length; this.keys.push(rhs.keys.shift()!); - this.children.push((rhs as BNodeInternal).children.shift()!); + const child = rhsInternal.children.shift()!; + this.children.push(child); + const size = child.size(); + rhsInternal._size -= size; + this._size += size; } takeFromLeft(lhs: BNode) { // Reminder: parent node must update its copy of key for this node // assert: neither node is shared // assert rhs.keys.length > (maxNodeSize/2 && this.keys.length; + const child = lhsInternal.children.pop()!; this.keys.unshift(lhs.keys.pop()!); - this.children.unshift((lhs as BNodeInternal).children.pop()!); + this.children.unshift(child); + const size = child.size(); + lhsInternal._size -= size; + this._size += size; } ///////////////////////////////////////////////////////////////////////////// @@ -1489,12 +1546,15 @@ export class BNodeInternal extends BNode { } else if (i <= iHigh) { try { for (; i <= iHigh; i++) { - if (children[i].isShared) - children[i] = children[i].clone(); - var result = children[i].forRange(low, high, includeHigh, editMode, tree, count, onFound); + let child = children[i]; + if (child.isShared) + children[i] = child = child.clone(); + const beforeSize = child.size(); + const result = child.forRange(low, high, includeHigh, editMode, tree, count, onFound); // Note: if children[i] is empty then keys[i]=undefined. // This is an invalid state, but it is fixed below. - keys[i] = children[i].maxKey(); + keys[i] = child.maxKey(); + this._size += child.size() - beforeSize; if (typeof result !== 'number') return result; count = result; @@ -1510,7 +1570,8 @@ export class BNodeInternal extends BNode { this.tryMerge(i, tree._maxNodeSize); } else { // child is empty! delete it! keys.splice(i, 1); - children.splice(i, 1); + const removed = children.splice(i, 1); + check(removed[0].size() === 0, "emptiness cleanup"); } } } @@ -1549,6 +1610,7 @@ export class BNodeInternal extends BNode { this.keys.push.apply(this.keys, rhs.keys); const rhsChildren = (rhs as any as BNodeInternal).children; this.children.push.apply(this.children, rhsChildren); + this._size += rhs.size(); if (rhs.isShared && !this.isShared) { // All children of a shared node are implicitly shared, and since their new @@ -1577,6 +1639,59 @@ export class BNodeInternal extends BNode { // has the side effect of scanning the prototype chain. var undefVals: any[] = []; +/** + * Sums the sizes of the given child nodes. + * @param children the child nodes + * @returns the total size + * @internal + */ +export function sumChildSizes(children: BNode[]): number { + var total = 0; + for (var i = 0; i < children.length; i++) + total += children[i].size(); + return total; +} + +/** + * Determines whether two nodes are overlapping in key range. + * @internal + */ +export function areOverlapping( + aMin: K, + aMax: K, + bMin: K, + bMax: K, + cmp: (x:K,y:K)=>number +): boolean { + // There are 4 possibilities: + // 1. aMin.........aMax + // bMin.........bMax + // (aMax between bMin and bMax) + // 2. aMin.........aMax + // bMin.........bMax + // (aMin between bMin and bMax) + // 3. aMin.............aMax + // bMin....bMax + // (aMin and aMax enclose bMin and bMax; note this includes equality cases) + // 4. aMin....aMax + // bMin.............bMax + // (bMin and bMax enclose aMin and aMax; note equality cases are identical to case 3) + const aMinBMin = cmp(aMin, bMin); + const aMinBMax = cmp(aMin, bMax); + if (aMinBMin >= 0 && aMinBMax <= 0) { + // case 2 or 4 + return true; + } + const aMaxBMin = cmp(aMax, bMin); + const aMaxBMax = cmp(aMax, bMax); + if (aMaxBMin >= 0 && aMaxBMax <= 0) { + // case 1 + return true; + } + // case 3 or no overlap + return aMinBMin <= 0 && aMaxBMax >= 0; +} + const Delete = {delete: true}, DeleteRange = () => Delete; const Break = {break: true}; const EmptyLeaf = (function() { From 21f63ce18b2d62e73660dd54b47256b33ae4dfb8 Mon Sep 17 00:00:00 2001 From: Taylor Williams Date: Tue, 25 Nov 2025 07:50:06 -0800 Subject: [PATCH 2/2] cleanup from BTreeEx --- b+tree.d.ts | 3 +-- b+tree.js | 3 +-- b+tree.ts | 3 +-- extended/index.js | 2 -- extended/index.ts | 2 -- extended/shared.d.ts | 3 +-- extended/shared.ts | 3 +-- 7 files changed, 5 insertions(+), 14 deletions(-) diff --git a/b+tree.d.ts b/b+tree.d.ts index 57fddd7..0905a6c 100644 --- a/b+tree.d.ts +++ b/b+tree.d.ts @@ -415,8 +415,7 @@ export default class BTree implements ISortedMapF, ISort /** Scans the tree for signs of serious bugs (e.g. this.size doesn't match * number of elements, internal nodes not caching max element properly...) * Computational complexity: O(number of nodes), i.e. O(size). This method - * skips the most expensive test - whether all keys are sorted - but it - * does check that maxKey() of the children of internal nodes are sorted. */ + * validates ordering of keys (including leaves) and cached size information. */ checkValid(): void; } /** A TypeScript helper function that simply returns its argument, typed as diff --git a/b+tree.js b/b+tree.js index 637db72..4eef0f0 100644 --- a/b+tree.js +++ b/b+tree.js @@ -824,8 +824,7 @@ var BTree = /** @class */ (function () { /** Scans the tree for signs of serious bugs (e.g. this.size doesn't match * number of elements, internal nodes not caching max element properly...) * Computational complexity: O(number of nodes), i.e. O(size). This method - * skips the most expensive test - whether all keys are sorted - but it - * does check that maxKey() of the children of internal nodes are sorted. */ + * validates ordering of keys (including leaves) and cached size information. */ BTree.prototype.checkValid = function () { var size = this._root.checkValid(0, this, 0)[0]; check(size === this.size, "size mismatch: counted ", size, "but stored", this.size); diff --git a/b+tree.ts b/b+tree.ts index c0d80c7..eea1e34 100644 --- a/b+tree.ts +++ b/b+tree.ts @@ -920,8 +920,7 @@ export default class BTree implements ISortedMapF, ISortedMap /** Scans the tree for signs of serious bugs (e.g. this.size doesn't match * number of elements, internal nodes not caching max element properly...) * Computational complexity: O(number of nodes), i.e. O(size). This method - * skips the most expensive test - whether all keys are sorted - but it - * does check that maxKey() of the children of internal nodes are sorted. */ + * validates ordering of keys (including leaves) and cached size information. */ checkValid() { var [size] = this._root.checkValid(0, this, 0); check(size === this.size, "size mismatch: counted ", size, "but stored", this.size); diff --git a/extended/index.js b/extended/index.js index 9d9ba2d..5c83b36 100644 --- a/extended/index.js +++ b/extended/index.js @@ -32,7 +32,6 @@ var BTreeEx = /** @class */ (function (_super) { var result = new BTreeEx(undefined, this._compare, this._maxNodeSize); var target = result; target._root = source._root; - target._size = source._size; return result; }; BTreeEx.prototype.greedyClone = function (force) { @@ -40,7 +39,6 @@ var BTreeEx = /** @class */ (function (_super) { var result = new BTreeEx(undefined, this._compare, this._maxNodeSize); var target = result; target._root = source._root.greedyClone(force); - target._size = source._size; return result; }; BTreeEx.prototype.diffAgainst = function (other, onlyThis, onlyOther, different) { diff --git a/extended/index.ts b/extended/index.ts index 0d6edf9..9846511 100644 --- a/extended/index.ts +++ b/extended/index.ts @@ -9,7 +9,6 @@ export class BTreeEx extends BTree { const result = new BTreeEx(undefined, this._compare, this._maxNodeSize); const target = result as unknown as BTreeWithInternals; target._root = source._root; - target._size = source._size; return result as this; } @@ -18,7 +17,6 @@ export class BTreeEx extends BTree { const result = new BTreeEx(undefined, this._compare, this._maxNodeSize); const target = result as unknown as BTreeWithInternals; target._root = source._root.greedyClone(force); - target._size = source._size; return result as this; } diff --git a/extended/shared.d.ts b/extended/shared.d.ts index 4527a4e..dd131ba 100644 --- a/extended/shared.d.ts +++ b/extended/shared.d.ts @@ -2,7 +2,6 @@ import type { BNode } from '../b+tree'; import BTree from '../b+tree'; export declare type BTreeWithInternals = { _root: BNode; - _size: number; _maxNodeSize: number; _compare: (a: K, b: K) => number; -} & Omit, '_root' | '_size' | '_maxNodeSize' | '_compare'>; +} & Omit, '_root' | '_maxNodeSize' | '_compare'>; diff --git a/extended/shared.ts b/extended/shared.ts index 58c7982..1061363 100644 --- a/extended/shared.ts +++ b/extended/shared.ts @@ -3,7 +3,6 @@ import BTree from '../b+tree'; export type BTreeWithInternals = { _root: BNode; - _size: number; _maxNodeSize: number; _compare: (a: K, b: K) => number; -} & Omit, '_root' | '_size' | '_maxNodeSize' | '_compare'>; +} & Omit, '_root' | '_maxNodeSize' | '_compare'>;