Repository: qwertie/btree-typescript Branch: master Commit: 8782fd6af524 Files: 59 Total size: 595.3 KB Directory structure: gitextract_6aq6pf7a/ ├── .gitignore ├── .vscode/ │ └── launch.json ├── LICENSE ├── b+tree.d.ts ├── b+tree.js ├── b+tree.ts ├── benchmarks.ts ├── extended/ │ ├── bulkLoad.d.ts │ ├── bulkLoad.js │ ├── bulkLoad.ts │ ├── decompose.d.ts │ ├── decompose.js │ ├── decompose.ts │ ├── diffAgainst.d.ts │ ├── diffAgainst.js │ ├── diffAgainst.ts │ ├── forEachKeyInBoth.d.ts │ ├── forEachKeyInBoth.js │ ├── forEachKeyInBoth.ts │ ├── forEachKeyNotIn.d.ts │ ├── forEachKeyNotIn.js │ ├── forEachKeyNotIn.ts │ ├── index.d.ts │ ├── index.js │ ├── index.ts │ ├── intersect.d.ts │ ├── intersect.js │ ├── intersect.ts │ ├── parallelWalk.d.ts │ ├── parallelWalk.js │ ├── parallelWalk.ts │ ├── shared.d.ts │ ├── shared.js │ ├── shared.ts │ ├── subtract.d.ts │ ├── subtract.js │ ├── subtract.ts │ ├── union.d.ts │ ├── union.js │ └── union.ts ├── interfaces.d.ts ├── package.json ├── readme.md ├── scripts/ │ ├── minify.js │ └── size-report.js ├── sorted-array.d.ts ├── sorted-array.js ├── sorted-array.ts ├── test/ │ ├── b+tree.test.ts │ ├── bulkLoad.test.ts │ ├── diffAgainst.test.ts │ ├── intersect.test.ts │ ├── setOperationFuzz.test.ts │ ├── shared.d.ts │ ├── shared.js │ ├── shared.ts │ ├── subtract.test.ts │ └── union.test.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Generated files *.js.map *.bundle.js *.out.js *.min.js .testpack/ # Misc benchmarks.js benchmarks.d.ts interfaces.js # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Typescript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Jest Tests", "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", "args": [ "--runInBand", "--ci" ], //"preLaunchTask": "build", "internalConsoleOptions": "openOnSessionStart", "outFiles": [ "${workspaceRoot}/dist/**/*" ], "env": { "CI": "true" } //"envFile": "${workspaceRoot}/.env" }, { "type": "node", "request": "launch", "name": "Debug Jest Tests", "runtimeArgs": [ "--nolazy", "--inspect-brk", "${workspaceRoot}/node_modules/jest/bin/jest.js", "--runInBand", "--coverage", "false", "--ci" ], "console": "integratedTerminal", //"internalConsoleOptions": "openOnSessionStart", "port": 9229, "stopOnEntry": true }, { "type": "node", "request": "launch", "name": "Debug Benchmarks", "runtimeArgs": [ "--nolazy", "--inspect-brk", "${workspaceRoot}/node_modules/ts-node/dist/bin.js", "${workspaceRoot}/benchmarks.ts" ], "console": "integratedTerminal", "port": 9229, } ] } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 David Piepgrass Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: b+tree.d.ts ================================================ import { ISortedMap, ISortedMapF, ISortedSet } from './interfaces'; export { ISetSource, ISetSink, ISet, ISetF, ISortedSetSource, ISortedSet, ISortedSetF, IMapSource, IMapSink, IMap, IMapF, ISortedMapSource, ISortedMap, ISortedMapF } from './interfaces'; export declare type EditRangeResult = { value?: V; break?: R; delete?: boolean; }; /** * Types that BTree supports by default */ export declare type DefaultComparable = number | string | Date | boolean | null | undefined | (number | string)[] | { valueOf: () => number | string | Date | boolean | null | undefined | (number | string)[]; }; /** * Compares DefaultComparables to form a strict partial ordering. * * Handles +/-0 and NaN like Map: NaN is equal to NaN, and -0 is equal to +0. * * Arrays are compared using '<' and '>', which may cause unexpected equality: * for example [1] will be considered equal to ['1']. * * Two objects with equal valueOf compare the same, but compare unequal to * primitives that have the same value. */ export declare function defaultComparator(a: DefaultComparable, b: DefaultComparable): number; /** * Compares items using the < and > operators. This function is probably slightly * faster than the defaultComparator for Dates and strings, but has not been benchmarked. * Unlike defaultComparator, this comparator doesn't support mixed types correctly, * i.e. use it with `BTree` or `BTree` but not `BTree`. * * NaN is not supported. * * Note: null is treated like 0 when compared with numbers or Date, but in general * null is not ordered with respect to strings (neither greater nor less), and * undefined is not ordered with other types. */ export declare function simpleComparator(a: string, b: string): number; export declare function simpleComparator(a: number | null, b: number | null): number; export declare function simpleComparator(a: Date | null, b: Date | null): number; export declare function simpleComparator(a: (number | string)[], b: (number | string)[]): number; /** * A reasonably fast collection of key-value pairs with a powerful API. * Largely compatible with the standard Map. BTree is a B+ tree data structure, * so the collection is sorted by key. * * B+ trees tend to use memory more efficiently than hashtables such as the * standard Map, especially when the collection contains a large number of * items. However, maintaining the sort order makes them modestly slower: * O(log size) rather than O(1). This B+ tree implementation supports O(1) * fast cloning. It also supports freeze(), which can be used to ensure that * a BTree is not changed accidentally. * * Confusingly, the ES6 Map.forEach(c) method calls c(value,key) instead of * c(key,value), in contrast to other methods such as set() and entries() * which put the key first. I can only assume that the order was reversed on * the theory that users would usually want to examine values and ignore keys. * BTree's forEach() therefore works the same way, but a second method * `.forEachPair((key,value)=>{...})` is provided which sends you the key * first and the value second; this method is slightly faster because it is * the "native" for-each method for this class. * * Out of the box, BTree supports keys that are numbers, strings, arrays of * numbers/strings, Date, and objects that have a valueOf() method returning a * number or string. Other data types, such as arrays of Date or custom * objects, require a custom comparator, which you must pass as the second * argument to the constructor (the first argument is an optional list of * initial items). Symbols cannot be used as keys because they are unordered * (one Symbol is never "greater" or "less" than another). * * @example * Given a {name: string, age: number} object, you can create a tree sorted by * name and then by age like this: * * var tree = new BTree(undefined, (a, b) => { * if (a.name > b.name) * return 1; // Return a number >0 when a > b * else if (a.name < b.name) * return -1; // Return a number <0 when a < b * else // names are equal (or incomparable) * return a.age - b.age; // Return >0 when a.age > b.age * }); * * tree.set({name:"Bill", age:17}, "happy"); * tree.set({name:"Fran", age:40}, "busy & stressed"); * tree.set({name:"Bill", age:55}, "recently laid off"); * tree.forEachPair((k, v) => { * console.log(`Name: ${k.name} Age: ${k.age} Status: ${v}`); * }); * * @description * The "range" methods (`forEach, forRange, editRange`) will return the number * of elements that were scanned. In addition, the callback can return {break:R} * to stop early and return R from the outer function. * * - TODO: Test performance of preallocating values array at max size * - TODO: Add fast initialization when a sorted array is provided to constructor * * For more documentation see https://github.com/qwertie/btree-typescript * * Are you a C# developer? You might like the similar data structures I made for C#: * BDictionary, BList, etc. See http://core.loyc.net/collections/ * * @author David Piepgrass */ export default class BTree implements ISortedMapF, ISortedMap { private _root; _maxNodeSize: number; /** * provides a total order over keys (and a strict partial order over the type K) * @returns a negative value if a < b, 0 if a === b and a positive value if a > b */ _compare: (a: K, b: K) => number; /** * Initializes an empty B+ tree. * @param compare Custom function to compare pairs of elements in the tree. * If not specified, defaultComparator will be used which is valid as long as K extends DefaultComparable. * @param entries A set of key-value pairs to initialize the tree * @param maxNodeSize Branching factor (maximum items or children per node) * Must be in range 4..256. If undefined or <4 then default is used; if >256 then 256. */ constructor(entries?: [K, V][], compare?: (a: K, b: K) => number, maxNodeSize?: number); /** Gets the number of key-value pairs in the tree. */ get size(): number; /** Gets the number of key-value pairs in the tree. */ get length(): number; /** Returns true iff the tree contains no key-value pairs. */ get isEmpty(): boolean; /** Releases the tree so that its size is 0. */ clear(): void; forEach(callback: (v: V, k: K, tree: BTree) => void, thisArg?: any): number; /** Runs a function for each key-value pair, in order from smallest to * largest key. The callback can return {break:R} (where R is any value * except undefined) to stop immediately and return R from forEachPair. * @param onFound A function that is called for each key-value pair. This * function can return {break:R} to stop early with result R. * The reason that you must return {break:R} instead of simply R * itself is for consistency with editRange(), which allows * multiple actions, not just breaking. * @param initialCounter This is the value of the third argument of * `onFound` the first time it is called. The counter increases * by one each time `onFound` is called. Default value: 0 * @returns the number of pairs sent to the callback (plus initialCounter, * if you provided one). If the callback returned {break:R} then * the R value is returned instead. */ forEachPair(callback: (k: K, v: V, counter: number) => { break?: R; } | void, initialCounter?: number): R | number; /** * Finds a pair in the tree and returns the associated value. * @param defaultValue a value to return if the key was not found. * @returns the value, or defaultValue if the key was not found. * @description Computational complexity: O(log size) */ get(key: K, defaultValue?: V): V | undefined; /** * Adds or overwrites a key-value pair in the B+ tree. * @param key the key is used to determine the sort order of * data in the tree. * @param value data to associate with the key (optional) * @param overwrite Whether to overwrite an existing key-value pair * (default: true). If this is false and there is an existing * key-value pair then this method has no effect. * @returns true if a new key-value pair was added. * @description Computational complexity: O(log size) * Note: when overwriting a previous entry, the key is updated * as well as the value. This has no effect unless the new key * has data that does not affect its sort order. */ set(key: K, value: V, overwrite?: boolean): boolean; /** * Returns true if the key exists in the B+ tree, false if not. * Use get() for best performance; use has() if you need to * distinguish between "undefined value" and "key not present". * @param key Key to detect * @description Computational complexity: O(log size) */ has(key: K): boolean; /** * Removes a single key-value pair from the B+ tree. * @param key Key to find * @returns true if a pair was found and removed, false otherwise. * @description Computational complexity: O(log size) */ delete(key: K): boolean; /** Returns a copy of the tree with the specified key set (the value is undefined). */ with(key: K): BTree; /** Returns a copy of the tree with the specified key-value pair set. */ with(key: K, value: V2, overwrite?: boolean): BTree; /** Returns a copy of the tree with the specified key-value pairs set. */ withPairs(pairs: [K, V | V2][], overwrite: boolean): BTree; /** Returns a copy of the tree with the specified keys present. * @param keys The keys to add. If a key is already present in the tree, * neither the existing key nor the existing value is modified. * @param returnThisIfUnchanged if true, returns this if all keys already * existed. Performance note: due to the architecture of this class, all * node(s) leading to existing keys are cloned even if the collection is * ultimately unchanged. */ withKeys(keys: K[], returnThisIfUnchanged?: boolean): BTree; /** Returns a copy of the tree with the specified key removed. * @param returnThisIfUnchanged if true, returns this if the key didn't exist. * Performance note: due to the architecture of this class, node(s) leading * to where the key would have been stored are cloned even when the key * turns out not to exist and the collection is unchanged. */ without(key: K, returnThisIfUnchanged?: boolean): this; /** Returns a copy of the tree with the specified keys removed. * @param returnThisIfUnchanged if true, returns this if none of the keys * existed. Performance note: due to the architecture of this class, * node(s) leading to where the key would have been stored are cloned * even when the key turns out not to exist. */ withoutKeys(keys: K[], returnThisIfUnchanged?: boolean): this; /** Returns a copy of the tree with the specified range of keys removed. */ withoutRange(low: K, high: K, includeHigh: boolean, returnThisIfUnchanged?: boolean): this; /** Returns a copy of the tree with pairs removed whenever the callback * function returns false. `where()` is a synonym for this method. */ filter(callback: (k: K, v: V, counter: number) => boolean, returnThisIfUnchanged?: boolean): this; /** Returns a copy of the tree with all values altered by a callback function. */ mapValues(callback: (v: V, k: K, counter: number) => R): BTree; /** Performs a reduce operation like the `reduce` method of `Array`. * It is used to combine all pairs into a single value, or perform * conversions. `reduce` is best understood by example. For example, * `tree.reduce((P, pair) => P * pair[0], 1)` multiplies all keys * together. It means "start with P=1, and for each pair multiply * it by the key in pair[0]". Another example would be converting * the tree to a Map (in this example, note that M.set returns M): * * var M = tree.reduce((M, pair) => M.set(pair[0],pair[1]), new Map()) * * **Note**: the same array is sent to the callback on every iteration. */ reduce(callback: (previous: R, currentPair: [K, V], counter: number, tree: BTree) => R, initialValue: R): R; reduce(callback: (previous: R | undefined, currentPair: [K, V], counter: number, tree: BTree) => R): R | undefined; /** Returns an iterator that provides items in order (ascending order if * the collection's comparator uses ascending order, as is the default.) * @param lowestKey First key to be iterated, or undefined to start at * minKey(). If the specified key doesn't exist then iteration * starts at the next higher key (according to the comparator). * @param reusedArray Optional array used repeatedly to store key-value * pairs, to avoid creating a new array on every iteration. */ entries(lowestKey?: K, reusedArray?: (K | V)[]): IterableIterator<[K, V]>; /** Returns an iterator that provides items in reversed order. * @param highestKey Key at which to start iterating, or undefined to * start at maxKey(). If the specified key doesn't exist then iteration * starts at the next lower key (according to the comparator). * @param reusedArray Optional array used repeatedly to store key-value * pairs, to avoid creating a new array on every iteration. * @param skipHighest Iff this flag is true and the highestKey exists in the * collection, the pair matching highestKey is skipped, not iterated. */ entriesReversed(highestKey?: K, reusedArray?: (K | V)[], skipHighest?: boolean): IterableIterator<[K, V]>; private findPath; /** Returns a new iterator for iterating the keys of each pair in ascending order. * @param firstKey: Minimum key to include in the output. */ keys(firstKey?: K): IterableIterator; /** Returns a new iterator for iterating the values of each pair in order by key. * @param firstKey: Minimum key whose associated value is included in the output. */ values(firstKey?: K): IterableIterator; /** Returns the maximum number of children/values before nodes will split. */ get maxNodeSize(): number; /** Gets the lowest key in the tree. Complexity: O(log size) */ minKey(): K | undefined; /** Gets the highest key in the tree. Complexity: O(1) */ maxKey(): K | undefined; /** Quickly clones the tree by marking the root node as shared. * Both copies remain editable. When you modify either copy, any * nodes that are shared (or potentially shared) between the two * copies are cloned so that the changes do not affect other copies. * This is known as copy-on-write behavior, or "lazy copying". */ clone(): this; /** Performs a greedy clone, immediately duplicating any nodes that are * not currently marked as shared, in order to avoid marking any * additional nodes as shared. * @param force Clone all nodes, even shared ones. */ greedyClone(force?: boolean): this; /** Gets an array filled with the contents of the tree, sorted by key */ toArray(maxLength?: number): [K, V][]; /** Gets an array of all keys, sorted */ keysArray(): K[]; /** Gets an array of all values, sorted by key */ valuesArray(): V[]; /** Gets a string representing the tree's data based on toArray(). */ toString(): string; /** Stores a key-value pair only if the key doesn't already exist in the tree. * @returns true if a new key was added */ setIfNotPresent(key: K, value: V): boolean; /** Returns the next pair whose key is larger than the specified key (or undefined if there is none). * If key === undefined, this function returns the lowest pair. * @param key The key to search for. * @param reusedArray Optional array used repeatedly to store key-value pairs, to * avoid creating a new array on every iteration. */ nextHigherPair(key: K | undefined, reusedArray?: [K, V]): [K, V] | undefined; /** Returns the next key larger than the specified key, or undefined if there is none. * Also, nextHigherKey(undefined) returns the lowest key. */ nextHigherKey(key: K | undefined): K | undefined; /** Returns the next pair whose key is smaller than the specified key (or undefined if there is none). * If key === undefined, this function returns the highest pair. * @param key The key to search for. * @param reusedArray Optional array used repeatedly to store key-value pairs, to * avoid creating a new array each time you call this method. */ nextLowerPair(key: K | undefined, reusedArray?: [K, V]): [K, V] | undefined; /** Returns the next key smaller than the specified key, or undefined if there is none. * Also, nextLowerKey(undefined) returns the highest key. */ nextLowerKey(key: K | undefined): K | undefined; /** Returns the key-value pair associated with the supplied key if it exists * or the pair associated with the next lower pair otherwise. If there is no * next lower pair, undefined is returned. * @param key The key to search for. * @param reusedArray Optional array used repeatedly to store key-value pairs, to * avoid creating a new array each time you call this method. * */ getPairOrNextLower(key: K, reusedArray?: [K, V]): [K, V] | undefined; /** Returns the key-value pair associated with the supplied key if it exists * or the pair associated with the next lower pair otherwise. If there is no * next lower pair, undefined is returned. * @param key The key to search for. * @param reusedArray Optional array used repeatedly to store key-value pairs, to * avoid creating a new array each time you call this method. * */ getPairOrNextHigher(key: K, reusedArray?: [K, V]): [K, V] | undefined; /** Edits the value associated with a key in the tree, if it already exists. * @returns true if the key existed, false if not. */ changeIfPresent(key: K, value: V): boolean; /** * Builds an array of pairs from the specified range of keys, sorted by key. * Each returned pair is also an array: pair[0] is the key, pair[1] is the value. * @param low The first key in the array will be greater than or equal to `low`. * @param high This method returns when a key larger than this is reached. * @param includeHigh If the `high` key is present, its pair will be included * in the output if and only if this parameter is true. Note: if the * `low` key is present, it is always included in the output. * @param maxLength Length limit. getRange will stop scanning the tree when * the array reaches this size. * @description Computational complexity: O(result.length + log size) */ getRange(low: K, high: K, includeHigh?: boolean, maxLength?: number): [K, V][]; /** Adds all pairs from a list of key-value pairs. * @param pairs Pairs to add to this tree. If there are duplicate keys, * later pairs currently overwrite earlier ones (e.g. [[0,1],[0,7]] * associates 0 with 7.) * @param overwrite Whether to overwrite pairs that already exist (if false, * pairs[i] is ignored when the key pairs[i][0] already exists.) * @returns The number of pairs added to the collection. * @description Computational complexity: O(pairs.length * log(size + pairs.length)) */ setPairs(pairs: [K, V][], overwrite?: boolean): number; forRange(low: K, high: K, includeHigh: boolean, onFound?: (k: K, v: V, counter: number) => void, initialCounter?: number): number; /** * Scans and potentially modifies values for a subsequence of keys. * Note: the callback `onFound` should ideally be a pure function. * Specfically, it must not insert items, call clone(), or change * the collection except via return value; out-of-band editing may * cause an exception or may cause incorrect data to be sent to * the callback (duplicate or missed items). It must not cause a * clone() of the collection, otherwise the clone could be modified * by changes requested by the callback. * @param low The first key scanned will be greater than or equal to `low`. * @param high Scanning stops when a key larger than this is reached. * @param includeHigh If the `high` key is present, `onFound` is called for * that final pair if and only if this parameter is true. * @param onFound A function that is called for each key-value pair. This * function can return `{value:v}` to change the value associated * with the current key, `{delete:true}` to delete the current pair, * `{break:R}` to stop early with result R, or it can return nothing * (undefined or {}) to cause no effect and continue iterating. * `{break:R}` can be combined with one of the other two commands. * The third argument `counter` is the number of items iterated * previously; it equals 0 when `onFound` is called the first time. * @returns The number of values scanned, or R if the callback returned * `{break:R}` to stop early. * @description * Computational complexity: O(number of items scanned + log size) * Note: if the tree has been cloned with clone(), any shared * nodes are copied before `onFound` is called. This takes O(n) time * where n is proportional to the amount of shared data scanned. */ editRange(low: K, high: K, includeHigh: boolean, onFound: (k: K, v: V, counter: number) => EditRangeResult | void, initialCounter?: number): R | number; /** Same as `editRange` except that the callback is called for all pairs. */ editAll(onFound: (k: K, v: V, counter: number) => EditRangeResult | void, initialCounter?: number): R | number; /** * Removes a range of key-value pairs from the B+ tree. * @param low The first key scanned will be greater than or equal to `low`. * @param high Scanning stops when a key larger than this is reached. * @param includeHigh Specifies whether the `high` key, if present, is deleted. * @returns The number of key-value pairs that were deleted. * @description Computational complexity: O(log size + number of items deleted) */ deleteRange(low: K, high: K, includeHigh: boolean): number; /** Deletes a series of keys from the collection. */ deleteKeys(keys: K[]): number; /** Gets the height of the tree: the number of internal nodes between the * BTree object and its leaf nodes (zero if there are no internal nodes). */ get height(): number; /** Makes the object read-only to ensure it is not accidentally modified. * Freezing does not have to be permanent; unfreeze() reverses the effect. * This is accomplished by replacing mutator functions with a function * that throws an Error. Compared to using a property (e.g. this.isFrozen) * this implementation gives better performance in non-frozen BTrees. */ freeze(): void; /** Ensures mutations are allowed, reversing the effect of freeze(). */ unfreeze(): void; /** Returns true if the tree appears to be frozen. */ get isFrozen(): boolean; /** 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). This method validates cached size * information and, optionally, the ordering of keys (including leaves), which * takes more time to check (O(size), which is technically the same big-O). */ checkValid(checkOrdering?: boolean): void; } /** A TypeScript helper function that simply returns its argument, typed as * `ISortedSet` if the BTree implements it, as it does if `V extends undefined`. * If `V` cannot be `undefined`, it returns `unknown` instead. Or at least, that * was the intention, but TypeScript is acting weird and may return `ISortedSet` * even if `V` can't be `undefined` (discussion: btree-typescript issue #14) */ export declare function asSet(btree: BTree): undefined extends V ? ISortedSet : unknown; /** A BTree frozen in the empty state. */ export declare const EmptyBTree: BTree; ================================================ FILE: b+tree.js ================================================ "use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.EmptyBTree = exports.check = exports.areOverlapping = exports.sumChildSizes = exports.BNodeInternal = exports.BNode = exports.asSet = exports.fixMaxSize = exports.simpleComparator = exports.defaultComparator = void 0; /** * Compares DefaultComparables to form a strict partial ordering. * * Handles +/-0 and NaN like Map: NaN is equal to NaN, and -0 is equal to +0. * * Arrays are compared using '<' and '>', which may cause unexpected equality: * for example [1] will be considered equal to ['1']. * * Two objects with equal valueOf compare the same, but compare unequal to * primitives that have the same value. */ function defaultComparator(a, b) { // Special case finite numbers first for performance. // Note that the trick of using 'a - b' and checking for NaN to detect non-numbers // does not work if the strings are numeric (ex: "5"). This would leading most // comparison functions using that approach to fail to have transitivity. if (Number.isFinite(a) && Number.isFinite(b)) { return a - b; } // The default < and > operators are not totally ordered. To allow types to be mixed // in a single collection, compare types and order values of different types by type. var ta = typeof a; var tb = typeof b; if (ta !== tb) { return ta < tb ? -1 : 1; } if (ta === 'object') { // standardized JavaScript bug: null is not an object, but typeof says it is if (a === null) return b === null ? 0 : -1; else if (b === null) return 1; a = a.valueOf(); b = b.valueOf(); ta = typeof a; tb = typeof b; // Deal with the two valueOf()s producing different types if (ta !== tb) { return ta < tb ? -1 : 1; } } // a and b are now the same type, and will be a number, string or array // (which we assume holds numbers or strings), or something unsupported. if (a < b) return -1; if (a > b) return 1; if (a === b) return 0; // Order NaN less than other numbers if (Number.isNaN(a)) return Number.isNaN(b) ? 0 : -1; else if (Number.isNaN(b)) return 1; // This could be two objects (e.g. [7] and ['7']) that aren't ordered return Array.isArray(a) ? 0 : Number.NaN; } exports.defaultComparator = defaultComparator; ; function simpleComparator(a, b) { return a > b ? 1 : a < b ? -1 : 0; } exports.simpleComparator = simpleComparator; ; /** Sanitizes a requested max node size. * @internal */ function fixMaxSize(maxNodeSize) { return maxNodeSize >= 4 ? Math.min(maxNodeSize | 0, 256) : 32; } exports.fixMaxSize = fixMaxSize; /** * A reasonably fast collection of key-value pairs with a powerful API. * Largely compatible with the standard Map. BTree is a B+ tree data structure, * so the collection is sorted by key. * * B+ trees tend to use memory more efficiently than hashtables such as the * standard Map, especially when the collection contains a large number of * items. However, maintaining the sort order makes them modestly slower: * O(log size) rather than O(1). This B+ tree implementation supports O(1) * fast cloning. It also supports freeze(), which can be used to ensure that * a BTree is not changed accidentally. * * Confusingly, the ES6 Map.forEach(c) method calls c(value,key) instead of * c(key,value), in contrast to other methods such as set() and entries() * which put the key first. I can only assume that the order was reversed on * the theory that users would usually want to examine values and ignore keys. * BTree's forEach() therefore works the same way, but a second method * `.forEachPair((key,value)=>{...})` is provided which sends you the key * first and the value second; this method is slightly faster because it is * the "native" for-each method for this class. * * Out of the box, BTree supports keys that are numbers, strings, arrays of * numbers/strings, Date, and objects that have a valueOf() method returning a * number or string. Other data types, such as arrays of Date or custom * objects, require a custom comparator, which you must pass as the second * argument to the constructor (the first argument is an optional list of * initial items). Symbols cannot be used as keys because they are unordered * (one Symbol is never "greater" or "less" than another). * * @example * Given a {name: string, age: number} object, you can create a tree sorted by * name and then by age like this: * * var tree = new BTree(undefined, (a, b) => { * if (a.name > b.name) * return 1; // Return a number >0 when a > b * else if (a.name < b.name) * return -1; // Return a number <0 when a < b * else // names are equal (or incomparable) * return a.age - b.age; // Return >0 when a.age > b.age * }); * * tree.set({name:"Bill", age:17}, "happy"); * tree.set({name:"Fran", age:40}, "busy & stressed"); * tree.set({name:"Bill", age:55}, "recently laid off"); * tree.forEachPair((k, v) => { * console.log(`Name: ${k.name} Age: ${k.age} Status: ${v}`); * }); * * @description * The "range" methods (`forEach, forRange, editRange`) will return the number * of elements that were scanned. In addition, the callback can return {break:R} * to stop early and return R from the outer function. * * - TODO: Test performance of preallocating values array at max size * - TODO: Add fast initialization when a sorted array is provided to constructor * * For more documentation see https://github.com/qwertie/btree-typescript * * Are you a C# developer? You might like the similar data structures I made for C#: * BDictionary, BList, etc. See http://core.loyc.net/collections/ * * @author David Piepgrass */ var BTree = /** @class */ (function () { /** * Initializes an empty B+ tree. * @param compare Custom function to compare pairs of elements in the tree. * If not specified, defaultComparator will be used which is valid as long as K extends DefaultComparable. * @param entries A set of key-value pairs to initialize the tree * @param maxNodeSize Branching factor (maximum items or children per node) * Must be in range 4..256. If undefined or <4 then default is used; if >256 then 256. */ function BTree(entries, compare, maxNodeSize) { this._root = EmptyLeaf; this._maxNodeSize = fixMaxSize(maxNodeSize); this._compare = compare || defaultComparator; if (entries) this.setPairs(entries); } Object.defineProperty(BTree.prototype, "size", { ///////////////////////////////////////////////////////////////////////////// // ES6 Map methods ///////////////////////////////////////////////////// /** Gets the number of key-value pairs in the tree. */ 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; }, enumerable: false, configurable: true }); Object.defineProperty(BTree.prototype, "isEmpty", { /** Returns true iff the tree contains no key-value pairs. */ 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; }; /** Runs a function for each key-value pair, in order from smallest to * largest key. For compatibility with ES6 Map, the argument order to * the callback is backwards: value first, then key. Call forEachPair * instead to receive the key as the first argument. * @param thisArg If provided, this parameter is assigned as the `this` * value for each callback. * @returns the number of values that were sent to the callback, * or the R value if the callback returned {break:R}. */ BTree.prototype.forEach = function (callback, thisArg) { var _this = this; if (thisArg !== undefined) callback = callback.bind(thisArg); return this.forEachPair(function (k, v) { return callback(v, k, _this); }); }; /** Runs a function for each key-value pair, in order from smallest to * largest key. The callback can return {break:R} (where R is any value * except undefined) to stop immediately and return R from forEachPair. * @param onFound A function that is called for each key-value pair. This * function can return {break:R} to stop early with result R. * The reason that you must return {break:R} instead of simply R * itself is for consistency with editRange(), which allows * multiple actions, not just breaking. * @param initialCounter This is the value of the third argument of * `onFound` the first time it is called. The counter increases * by one each time `onFound` is called. Default value: 0 * @returns the number of pairs sent to the callback (plus initialCounter, * if you provided one). If the callback returned {break:R} then * the R value is returned instead. */ BTree.prototype.forEachPair = function (callback, initialCounter) { var low = this.minKey(), high = this.maxKey(); return this.forRange(low, high, true, callback, initialCounter); }; /** * Finds a pair in the tree and returns the associated value. * @param defaultValue a value to return if the key was not found. * @returns the value, or defaultValue if the key was not found. * @description Computational complexity: O(log size) */ BTree.prototype.get = function (key, defaultValue) { return this._root.get(key, defaultValue, this); }; /** * Adds or overwrites a key-value pair in the B+ tree. * @param key the key is used to determine the sort order of * data in the tree. * @param value data to associate with the key (optional) * @param overwrite Whether to overwrite an existing key-value pair * (default: true). If this is false and there is an existing * key-value pair then this method has no effect. * @returns true if a new key-value pair was added. * @description Computational complexity: O(log size) * Note: when overwriting a previous entry, the key is updated * as well as the value. This has no effect unless the new key * has data that does not affect its sort order. */ BTree.prototype.set = function (key, value, overwrite) { if (this._root.isShared) this._root = this._root.clone(); var result = this._root.set(key, value, overwrite, this); if (result === true || result === false) return result; // Root node has split, so create a new root node. var children = [this._root, result]; this._root = new BNodeInternal(children, sumChildSizes(children)); return true; }; /** * Returns true if the key exists in the B+ tree, false if not. * Use get() for best performance; use has() if you need to * distinguish between "undefined value" and "key not present". * @param key Key to detect * @description Computational complexity: O(log size) */ BTree.prototype.has = function (key) { return this.forRange(key, key, true, undefined) !== 0; }; /** * Removes a single key-value pair from the B+ tree. * @param key Key to find * @returns true if a pair was found and removed, false otherwise. * @description Computational complexity: O(log size) */ BTree.prototype.delete = function (key) { return this.editRange(key, key, true, DeleteRange) !== 0; }; BTree.prototype.with = function (key, value, overwrite) { var nu = this.clone(); return nu.set(key, value, overwrite) || overwrite ? nu : this; }; /** Returns a copy of the tree with the specified key-value pairs set. */ BTree.prototype.withPairs = function (pairs, overwrite) { var nu = this.clone(); return nu.setPairs(pairs, overwrite) !== 0 || overwrite ? nu : this; }; /** Returns a copy of the tree with the specified keys present. * @param keys The keys to add. If a key is already present in the tree, * neither the existing key nor the existing value is modified. * @param returnThisIfUnchanged if true, returns this if all keys already * existed. Performance note: due to the architecture of this class, all * node(s) leading to existing keys are cloned even if the collection is * ultimately unchanged. */ BTree.prototype.withKeys = function (keys, returnThisIfUnchanged) { var nu = this.clone(), changed = false; for (var i = 0; i < keys.length; i++) changed = nu.set(keys[i], undefined, false) || changed; return returnThisIfUnchanged && !changed ? this : nu; }; /** Returns a copy of the tree with the specified key removed. * @param returnThisIfUnchanged if true, returns this if the key didn't exist. * Performance note: due to the architecture of this class, node(s) leading * to where the key would have been stored are cloned even when the key * turns out not to exist and the collection is unchanged. */ BTree.prototype.without = function (key, returnThisIfUnchanged) { return this.withoutRange(key, key, true, returnThisIfUnchanged); }; /** Returns a copy of the tree with the specified keys removed. * @param returnThisIfUnchanged if true, returns this if none of the keys * existed. Performance note: due to the architecture of this class, * node(s) leading to where the key would have been stored are cloned * even when the key turns out not to exist. */ BTree.prototype.withoutKeys = function (keys, returnThisIfUnchanged) { var nu = this.clone(); return nu.deleteKeys(keys) || !returnThisIfUnchanged ? nu : this; }; /** Returns a copy of the tree with the specified range of keys removed. */ BTree.prototype.withoutRange = function (low, high, includeHigh, returnThisIfUnchanged) { var nu = this.clone(); if (nu.deleteRange(low, high, includeHigh) === 0 && returnThisIfUnchanged) return this; return nu; }; /** Returns a copy of the tree with pairs removed whenever the callback * function returns false. `where()` is a synonym for this method. */ BTree.prototype.filter = function (callback, returnThisIfUnchanged) { var nu = this.greedyClone(); var del; nu.editAll(function (k, v, i) { if (!callback(k, v, i)) return del = Delete; }); if (!del && returnThisIfUnchanged) return this; return nu; }; /** Returns a copy of the tree with all values altered by a callback function. */ BTree.prototype.mapValues = function (callback) { var tmp = {}; var nu = this.greedyClone(); nu.editAll(function (k, v, i) { return tmp.value = callback(v, k, i), tmp; }); return nu; }; BTree.prototype.reduce = function (callback, initialValue) { var i = 0, p = initialValue; var it = this.entries(this.minKey(), ReusedArray), next; while (!(next = it.next()).done) p = callback(p, next.value, i++, this); return p; }; ///////////////////////////////////////////////////////////////////////////// // Iterator methods ///////////////////////////////////////////////////////// /** Returns an iterator that provides items in order (ascending order if * the collection's comparator uses ascending order, as is the default.) * @param lowestKey First key to be iterated, or undefined to start at * minKey(). If the specified key doesn't exist then iteration * starts at the next higher key (according to the comparator). * @param reusedArray Optional array used repeatedly to store key-value * pairs, to avoid creating a new array on every iteration. */ BTree.prototype.entries = function (lowestKey, reusedArray) { var info = this.findPath(lowestKey); if (info === undefined) return iterator(); var nodequeue = info.nodequeue, nodeindex = info.nodeindex, leaf = info.leaf; var state = reusedArray !== undefined ? 1 : 0; var i = (lowestKey === undefined ? -1 : leaf.indexOf(lowestKey, 0, this._compare) - 1); return iterator(function () { jump: for (;;) { switch (state) { case 0: if (++i < leaf.keys.length) return { done: false, value: [leaf.keys[i], leaf.values[i]] }; state = 2; continue; case 1: if (++i < leaf.keys.length) { reusedArray[0] = leaf.keys[i], reusedArray[1] = leaf.values[i]; return { done: false, value: reusedArray }; } state = 2; case 2: // Advance to the next leaf node for (var level = -1;;) { if (++level >= nodequeue.length) { state = 3; continue jump; } if (++nodeindex[level] < nodequeue[level].length) break; } for (; level > 0; level--) { nodequeue[level - 1] = nodequeue[level][nodeindex[level]].children; nodeindex[level - 1] = 0; } leaf = nodequeue[0][nodeindex[0]]; i = -1; state = reusedArray !== undefined ? 1 : 0; continue; case 3: return { done: true, value: undefined }; } } }); }; /** Returns an iterator that provides items in reversed order. * @param highestKey Key at which to start iterating, or undefined to * start at maxKey(). If the specified key doesn't exist then iteration * starts at the next lower key (according to the comparator). * @param reusedArray Optional array used repeatedly to store key-value * pairs, to avoid creating a new array on every iteration. * @param skipHighest Iff this flag is true and the highestKey exists in the * collection, the pair matching highestKey is skipped, not iterated. */ BTree.prototype.entriesReversed = function (highestKey, reusedArray, skipHighest) { if (highestKey === undefined) { highestKey = this.maxKey(); skipHighest = undefined; if (highestKey === undefined) return iterator(); // collection is empty } var _a = this.findPath(highestKey) || this.findPath(this.maxKey()), nodequeue = _a.nodequeue, nodeindex = _a.nodeindex, leaf = _a.leaf; check(!nodequeue[0] || leaf === nodequeue[0][nodeindex[0]], "wat!"); var i = leaf.indexOf(highestKey, 0, this._compare); if (!skipHighest && i < leaf.keys.length && this._compare(leaf.keys[i], highestKey) <= 0) i++; var state = reusedArray !== undefined ? 1 : 0; return iterator(function () { jump: for (;;) { switch (state) { case 0: if (--i >= 0) return { done: false, value: [leaf.keys[i], leaf.values[i]] }; state = 2; continue; case 1: if (--i >= 0) { reusedArray[0] = leaf.keys[i], reusedArray[1] = leaf.values[i]; return { done: false, value: reusedArray }; } state = 2; case 2: // Advance to the next leaf node for (var level = -1;;) { if (++level >= nodequeue.length) { state = 3; continue jump; } if (--nodeindex[level] >= 0) break; } for (; level > 0; level--) { nodequeue[level - 1] = nodequeue[level][nodeindex[level]].children; nodeindex[level - 1] = nodequeue[level - 1].length - 1; } leaf = nodequeue[0][nodeindex[0]]; i = leaf.keys.length; state = reusedArray !== undefined ? 1 : 0; continue; case 3: return { done: true, value: undefined }; } } }); }; /* Used by entries() and entriesReversed() to prepare to start iterating. * It develops a "node queue" for each non-leaf level of the tree. * Levels are numbered "bottom-up" so that level 0 is a list of leaf * nodes from a low-level non-leaf node. The queue at a given level L * consists of nodequeue[L] which is the children of a BNodeInternal, * and nodeindex[L], the current index within that child list, such * such that nodequeue[L-1] === nodequeue[L][nodeindex[L]].children. * (However inside this function the order is reversed.) */ BTree.prototype.findPath = function (key) { var nextnode = this._root; var nodequeue, nodeindex; if (nextnode.isLeaf) { nodequeue = EmptyArray, nodeindex = EmptyArray; // avoid allocations } else { nodequeue = [], nodeindex = []; for (var d = 0; !nextnode.isLeaf; d++) { nodequeue[d] = nextnode.children; nodeindex[d] = key === undefined ? 0 : nextnode.indexOf(key, 0, this._compare); if (nodeindex[d] >= nodequeue[d].length) return; // first key > maxKey() nextnode = nodequeue[d][nodeindex[d]]; } nodequeue.reverse(); nodeindex.reverse(); } return { nodequeue: nodequeue, nodeindex: nodeindex, leaf: nextnode }; }; /** Returns a new iterator for iterating the keys of each pair in ascending order. * @param firstKey: Minimum key to include in the output. */ BTree.prototype.keys = function (firstKey) { var it = this.entries(firstKey, ReusedArray); return iterator(function () { var n = it.next(); if (n.value) n.value = n.value[0]; return n; }); }; /** Returns a new iterator for iterating the values of each pair in order by key. * @param firstKey: Minimum key whose associated value is included in the output. */ BTree.prototype.values = function (firstKey) { var it = this.entries(firstKey, ReusedArray); return iterator(function () { var n = it.next(); if (n.value) n.value = n.value[1]; return n; }); }; Object.defineProperty(BTree.prototype, "maxNodeSize", { ///////////////////////////////////////////////////////////////////////////// // Additional methods /////////////////////////////////////////////////////// /** Returns the maximum number of children/values before nodes will split. */ get: function () { return this._maxNodeSize; }, enumerable: false, configurable: true }); /** Gets the lowest key in the tree. Complexity: O(log size) */ BTree.prototype.minKey = function () { return this._root.minKey(); }; /** Gets the highest key in the tree. Complexity: O(1) */ BTree.prototype.maxKey = function () { return this._root.maxKey(); }; /** Quickly clones the tree by marking the root node as shared. * Both copies remain editable. When you modify either copy, any * nodes that are shared (or potentially shared) between the two * copies are cloned so that the changes do not affect other copies. * This is known as copy-on-write behavior, or "lazy copying". */ BTree.prototype.clone = function () { this._root.isShared = true; var result = new BTree(undefined, this._compare, this._maxNodeSize); result._root = this._root; return result; }; /** Performs a greedy clone, immediately duplicating any nodes that are * not currently marked as shared, in order to avoid marking any * additional nodes as shared. * @param force Clone all nodes, even shared ones. */ BTree.prototype.greedyClone = function (force) { var result = new BTree(undefined, this._compare, this._maxNodeSize); result._root = this._root.greedyClone(force); return result; }; /** Gets an array filled with the contents of the tree, sorted by key */ BTree.prototype.toArray = function (maxLength) { if (maxLength === void 0) { maxLength = 0x7FFFFFFF; } var min = this.minKey(), max = this.maxKey(); if (min !== undefined) return this.getRange(min, max, true, maxLength); return []; }; /** Gets an array of all keys, sorted */ BTree.prototype.keysArray = function () { var results = []; this._root.forRange(this.minKey(), this.maxKey(), true, false, this, 0, function (k, v) { results.push(k); }); return results; }; /** Gets an array of all values, sorted by key */ BTree.prototype.valuesArray = function () { var results = []; this._root.forRange(this.minKey(), this.maxKey(), true, false, this, 0, function (k, v) { results.push(v); }); return results; }; /** Gets a string representing the tree's data based on toArray(). */ BTree.prototype.toString = function () { return this.toArray().toString(); }; /** Stores a key-value pair only if the key doesn't already exist in the tree. * @returns true if a new key was added */ BTree.prototype.setIfNotPresent = function (key, value) { return this.set(key, value, false); }; /** Returns the next pair whose key is larger than the specified key (or undefined if there is none). * If key === undefined, this function returns the lowest pair. * @param key The key to search for. * @param reusedArray Optional array used repeatedly to store key-value pairs, to * avoid creating a new array on every iteration. */ BTree.prototype.nextHigherPair = function (key, reusedArray) { reusedArray = reusedArray || []; if (key === undefined) { return this._root.minPair(reusedArray); } return this._root.getPairOrNextHigher(key, this._compare, false, reusedArray); }; /** Returns the next key larger than the specified key, or undefined if there is none. * Also, nextHigherKey(undefined) returns the lowest key. */ BTree.prototype.nextHigherKey = function (key) { var p = this.nextHigherPair(key, ReusedArray); return p && p[0]; }; /** Returns the next pair whose key is smaller than the specified key (or undefined if there is none). * If key === undefined, this function returns the highest pair. * @param key The key to search for. * @param reusedArray Optional array used repeatedly to store key-value pairs, to * avoid creating a new array each time you call this method. */ BTree.prototype.nextLowerPair = function (key, reusedArray) { reusedArray = reusedArray || []; if (key === undefined) { return this._root.maxPair(reusedArray); } return this._root.getPairOrNextLower(key, this._compare, false, reusedArray); }; /** Returns the next key smaller than the specified key, or undefined if there is none. * Also, nextLowerKey(undefined) returns the highest key. */ BTree.prototype.nextLowerKey = function (key) { var p = this.nextLowerPair(key, ReusedArray); return p && p[0]; }; /** Returns the key-value pair associated with the supplied key if it exists * or the pair associated with the next lower pair otherwise. If there is no * next lower pair, undefined is returned. * @param key The key to search for. * @param reusedArray Optional array used repeatedly to store key-value pairs, to * avoid creating a new array each time you call this method. * */ BTree.prototype.getPairOrNextLower = function (key, reusedArray) { return this._root.getPairOrNextLower(key, this._compare, true, reusedArray || []); }; /** Returns the key-value pair associated with the supplied key if it exists * or the pair associated with the next lower pair otherwise. If there is no * next lower pair, undefined is returned. * @param key The key to search for. * @param reusedArray Optional array used repeatedly to store key-value pairs, to * avoid creating a new array each time you call this method. * */ BTree.prototype.getPairOrNextHigher = function (key, reusedArray) { return this._root.getPairOrNextHigher(key, this._compare, true, reusedArray || []); }; /** Edits the value associated with a key in the tree, if it already exists. * @returns true if the key existed, false if not. */ BTree.prototype.changeIfPresent = function (key, value) { return this.editRange(key, key, true, function (k, v) { return ({ value: value }); }) !== 0; }; /** * Builds an array of pairs from the specified range of keys, sorted by key. * Each returned pair is also an array: pair[0] is the key, pair[1] is the value. * @param low The first key in the array will be greater than or equal to `low`. * @param high This method returns when a key larger than this is reached. * @param includeHigh If the `high` key is present, its pair will be included * in the output if and only if this parameter is true. Note: if the * `low` key is present, it is always included in the output. * @param maxLength Length limit. getRange will stop scanning the tree when * the array reaches this size. * @description Computational complexity: O(result.length + log size) */ BTree.prototype.getRange = function (low, high, includeHigh, maxLength) { if (maxLength === void 0) { maxLength = 0x3FFFFFF; } var results = []; this._root.forRange(low, high, includeHigh, false, this, 0, function (k, v) { results.push([k, v]); return results.length > maxLength ? Break : undefined; }); return results; }; /** Adds all pairs from a list of key-value pairs. * @param pairs Pairs to add to this tree. If there are duplicate keys, * later pairs currently overwrite earlier ones (e.g. [[0,1],[0,7]] * associates 0 with 7.) * @param overwrite Whether to overwrite pairs that already exist (if false, * pairs[i] is ignored when the key pairs[i][0] already exists.) * @returns The number of pairs added to the collection. * @description Computational complexity: O(pairs.length * log(size + pairs.length)) */ BTree.prototype.setPairs = function (pairs, overwrite) { var added = 0; for (var i = 0; i < pairs.length; i++) if (this.set(pairs[i][0], pairs[i][1], overwrite)) added++; return added; }; /** * Scans the specified range of keys, in ascending order by key. * Note: the callback `onFound` must not insert or remove items in the * collection. Doing so may cause incorrect data to be sent to the * callback afterward. * @param low The first key scanned will be greater than or equal to `low`. * @param high Scanning stops when a key larger than this is reached. * @param includeHigh If the `high` key is present, `onFound` is called for * that final pair if and only if this parameter is true. * @param onFound A function that is called for each key-value pair. This * function can return {break:R} to stop early with result R. * @param initialCounter Initial third argument of onFound. This value * increases by one each time `onFound` is called. Default: 0 * @returns The number of values found, or R if the callback returned * `{break:R}` to stop early. * @description Computational complexity: O(number of items scanned + log size) */ BTree.prototype.forRange = function (low, high, includeHigh, onFound, initialCounter) { var r = this._root.forRange(low, high, includeHigh, false, this, initialCounter || 0, onFound); return typeof r === "number" ? r : r.break; }; /** * Scans and potentially modifies values for a subsequence of keys. * Note: the callback `onFound` should ideally be a pure function. * Specfically, it must not insert items, call clone(), or change * the collection except via return value; out-of-band editing may * cause an exception or may cause incorrect data to be sent to * the callback (duplicate or missed items). It must not cause a * clone() of the collection, otherwise the clone could be modified * by changes requested by the callback. * @param low The first key scanned will be greater than or equal to `low`. * @param high Scanning stops when a key larger than this is reached. * @param includeHigh If the `high` key is present, `onFound` is called for * that final pair if and only if this parameter is true. * @param onFound A function that is called for each key-value pair. This * function can return `{value:v}` to change the value associated * with the current key, `{delete:true}` to delete the current pair, * `{break:R}` to stop early with result R, or it can return nothing * (undefined or {}) to cause no effect and continue iterating. * `{break:R}` can be combined with one of the other two commands. * The third argument `counter` is the number of items iterated * previously; it equals 0 when `onFound` is called the first time. * @returns The number of values scanned, or R if the callback returned * `{break:R}` to stop early. * @description * Computational complexity: O(number of items scanned + log size) * Note: if the tree has been cloned with clone(), any shared * nodes are copied before `onFound` is called. This takes O(n) time * where n is proportional to the amount of shared data scanned. */ BTree.prototype.editRange = function (low, high, includeHigh, onFound, initialCounter) { var root = this._root; if (root.isShared) this._root = root = root.clone(); try { var r = root.forRange(low, high, includeHigh, true, this, initialCounter || 0, onFound); return typeof r === "number" ? r : r.break; } finally { var isShared = void 0; while (root.keys.length <= 1 && !root.isLeaf) { isShared || (isShared = root.isShared); this._root = root = root.keys.length === 0 ? EmptyLeaf : root.children[0]; } // If any ancestor of the new root was shared, the new root must also be shared if (isShared) { root.isShared = true; } } }; /** Same as `editRange` except that the callback is called for all pairs. */ BTree.prototype.editAll = function (onFound, initialCounter) { return this.editRange(this.minKey(), this.maxKey(), true, onFound, initialCounter); }; /** * Removes a range of key-value pairs from the B+ tree. * @param low The first key scanned will be greater than or equal to `low`. * @param high Scanning stops when a key larger than this is reached. * @param includeHigh Specifies whether the `high` key, if present, is deleted. * @returns The number of key-value pairs that were deleted. * @description Computational complexity: O(log size + number of items deleted) */ BTree.prototype.deleteRange = function (low, high, includeHigh) { return this.editRange(low, high, includeHigh, DeleteRange); }; /** Deletes a series of keys from the collection. */ BTree.prototype.deleteKeys = function (keys) { for (var i = 0, r = 0; i < keys.length; i++) if (this.delete(keys[i])) r++; return r; }; Object.defineProperty(BTree.prototype, "height", { /** Gets the height of the tree: the number of internal nodes between the * BTree object and its leaf nodes (zero if there are no internal nodes). */ get: function () { var node = this._root; var height = -1; while (node) { height++; node = node.isLeaf ? undefined : node.children[0]; } return height; }, enumerable: false, configurable: true }); /** Makes the object read-only to ensure it is not accidentally modified. * Freezing does not have to be permanent; unfreeze() reverses the effect. * This is accomplished by replacing mutator functions with a function * that throws an Error. Compared to using a property (e.g. this.isFrozen) * this implementation gives better performance in non-frozen BTrees. */ BTree.prototype.freeze = function () { var t = this; // Note: all other mutators ultimately call set() or editRange() // so we don't need to override those others. t.clear = t.set = t.editRange = function () { throw new Error("Attempted to modify a frozen BTree"); }; }; /** Ensures mutations are allowed, reversing the effect of freeze(). */ BTree.prototype.unfreeze = function () { // @ts-ignore "The operand of a 'delete' operator must be optional." // (wrong: delete does not affect the prototype.) delete this.clear; // @ts-ignore delete this.set; // @ts-ignore delete this.editRange; }; Object.defineProperty(BTree.prototype, "isFrozen", { /** Returns true if the tree appears to be frozen. */ get: function () { return this.hasOwnProperty('editRange'); }, enumerable: false, configurable: true }); /** 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). This method validates cached size * information and, optionally, the ordering of keys (including leaves), which * takes more time to check (O(size), which is technically the same big-O). */ BTree.prototype.checkValid = function (checkOrdering) { if (checkOrdering === void 0) { checkOrdering = false; } var size = this._root.checkValid(0, this, 0, checkOrdering)[0]; check(size === this.size, "size mismatch: counted ", size, "but stored", this.size); }; return BTree; }()); exports.default = BTree; /** A TypeScript helper function that simply returns its argument, typed as * `ISortedSet` if the BTree implements it, as it does if `V extends undefined`. * If `V` cannot be `undefined`, it returns `unknown` instead. Or at least, that * was the intention, but TypeScript is acting weird and may return `ISortedSet` * even if `V` can't be `undefined` (discussion: btree-typescript issue #14) */ function asSet(btree) { return btree; } exports.asSet = asSet; if (Symbol && Symbol.iterator) // iterator is equivalent to entries() BTree.prototype[Symbol.iterator] = BTree.prototype.entries; BTree.prototype.where = BTree.prototype.filter; BTree.prototype.setRange = BTree.prototype.setPairs; BTree.prototype.add = BTree.prototype.set; // for compatibility with ISetSink function iterator(next) { if (next === void 0) { next = (function () { return ({ done: true, value: undefined }); }); } var result = { next: next }; if (Symbol && Symbol.iterator) result[Symbol.iterator] = function () { return this; }; return result; } /** @internal */ var BNode = /** @class */ (function () { function BNode(keys, values) { if (keys === void 0) { keys = []; } this.keys = keys; this.values = values || undefVals; this.isShared = undefined; } Object.defineProperty(BNode.prototype, "isLeaf", { get: function () { return this.children === undefined; }, enumerable: false, configurable: true }); BNode.prototype.size = function () { return this.keys.length; }; /////////////////////////////////////////////////////////////////////////// // Shared methods ///////////////////////////////////////////////////////// BNode.prototype.maxKey = function () { return this.keys[this.keys.length - 1]; }; // If key not found, returns i^failXor where i is the insertion index. // Callers that don't care whether there was a match will set failXor=0. BNode.prototype.indexOf = function (key, failXor, cmp) { var keys = this.keys; var lo = 0, hi = keys.length, mid = hi >> 1; while (lo < hi) { var c = cmp(keys[mid], key); if (c < 0) lo = mid + 1; else if (c > 0) // key < keys[mid] hi = mid; else if (c === 0) return mid; else { // c is NaN or otherwise invalid if (key === key) // at least the search key is not NaN return keys.length; else throw new Error("BTree: NaN was used as a key"); } mid = (lo + hi) >> 1; } return mid ^ failXor; // Unrolled version: benchmarks show same speed, not worth using /*var i = 1, c: number = 0, sum = 0; if (keys.length >= 4) { i = 3; if (keys.length >= 8) { i = 7; if (keys.length >= 16) { i = 15; if (keys.length >= 32) { i = 31; if (keys.length >= 64) { i = 127; i += (c = i < keys.length ? cmp(keys[i], key) : 1) < 0 ? 64 : -64; sum += c; i += (c = i < keys.length ? cmp(keys[i], key) : 1) < 0 ? 32 : -32; sum += c; } i += (c = i < keys.length ? cmp(keys[i], key) : 1) < 0 ? 16 : -16; sum += c; } i += (c = i < keys.length ? cmp(keys[i], key) : 1) < 0 ? 8 : -8; sum += c; } i += (c = i < keys.length ? cmp(keys[i], key) : 1) < 0 ? 4 : -4; sum += c; } i += (c = i < keys.length ? cmp(keys[i], key) : 1) < 0 ? 2 : -2; sum += c; } i += (c = i < keys.length ? cmp(keys[i], key) : 1) < 0 ? 1 : -1; c = i < keys.length ? cmp(keys[i], key) : 1; sum += c; if (c < 0) { ++i; c = i < keys.length ? cmp(keys[i], key) : 1; sum += c; } if (sum !== sum) { if (key === key) // at least the search key is not NaN return keys.length ^ failXor; else throw new Error("BTree: NaN was used as a key"); } return c === 0 ? i : i ^ failXor;*/ }; ///////////////////////////////////////////////////////////////////////////// // Leaf Node: misc ////////////////////////////////////////////////////////// BNode.prototype.minKey = function () { return this.keys[0]; }; BNode.prototype.minPair = function (reusedArray) { if (this.keys.length === 0) return undefined; reusedArray[0] = this.keys[0]; reusedArray[1] = this.values[0]; return reusedArray; }; BNode.prototype.maxPair = function (reusedArray) { if (this.keys.length === 0) return undefined; var lastIndex = this.keys.length - 1; reusedArray[0] = this.keys[lastIndex]; reusedArray[1] = this.values[lastIndex]; return reusedArray; }; BNode.prototype.clone = function () { var v = this.values; return new BNode(this.keys.slice(0), v === undefVals ? v : v.slice(0)); }; BNode.prototype.greedyClone = function (force) { return this.isShared && !force ? this : this.clone(); }; BNode.prototype.get = function (key, defaultValue, tree) { var i = this.indexOf(key, -1, tree._compare); return i < 0 ? defaultValue : this.values[i]; }; BNode.prototype.getPairOrNextLower = function (key, compare, inclusive, reusedArray) { var i = this.indexOf(key, -1, compare); var indexOrLower = i < 0 ? ~i - 1 : (inclusive ? i : i - 1); if (indexOrLower >= 0) { reusedArray[0] = this.keys[indexOrLower]; reusedArray[1] = this.values[indexOrLower]; return reusedArray; } return undefined; }; BNode.prototype.getPairOrNextHigher = function (key, compare, inclusive, reusedArray) { var i = this.indexOf(key, -1, compare); var indexOrLower = i < 0 ? ~i : (inclusive ? i : i + 1); var keys = this.keys; if (indexOrLower < keys.length) { reusedArray[0] = keys[indexOrLower]; reusedArray[1] = this.values[indexOrLower]; return reusedArray; } return undefined; }; BNode.prototype.checkValid = function (depth, tree, baseIndex, checkOrdering) { 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); // Note: we don't check for "node too small" because sometimes a node // can legitimately have size 1. This occurs if there is a batch // deletion, leaving a node of size 1, and the siblings are full so // 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); if (checkOrdering === true) { 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 ////////////////////////////////////////// BNode.prototype.set = function (key, value, overwrite, tree) { var i = this.indexOf(key, -1, tree._compare); if (i < 0) { // key does not exist yet i = ~i; if (this.keys.length < tree._maxNodeSize) { return this.insertInLeaf(i, key, value, tree); } else { // This leaf node is full and must split var newRightSibling = this.splitOffRightSide(), target = this; if (i > this.keys.length) { i -= this.keys.length; target = newRightSibling; } target.insertInLeaf(i, key, value, tree); return newRightSibling; } } else { // Key already exists if (overwrite !== false) { if (value !== undefined) this.reifyValues(); // usually this is a no-op, but some users may wish to edit the key this.keys[i] = key; this.values[i] = value; } return false; } }; BNode.prototype.reifyValues = function () { if (this.values === undefVals) return this.values = this.values.slice(0, this.keys.length); return this.values; }; BNode.prototype.insertInLeaf = function (i, key, value, tree) { this.keys.splice(i, 0, key); if (this.values === undefVals) { while (undefVals.length < tree._maxNodeSize) undefVals.push(undefined); if (value === undefined) { return true; } else { this.values = undefVals.slice(0, this.keys.length - 1); } } this.values.splice(i, 0, value); return true; }; BNode.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> 1, keys = this.keys.splice(half); var values = this.values === undefVals ? undefVals : this.values.splice(half); return new BNode(keys, values); }; ///////////////////////////////////////////////////////////////////////////// // Leaf Node: scanning & deletions ////////////////////////////////////////// BNode.prototype.forRange = function (low, high, includeHigh, editMode, tree, count, onFound) { var cmp = tree._compare; var iLow, iHigh; if (high === low) { if (!includeHigh) return count; iHigh = (iLow = this.indexOf(low, -1, cmp)) + 1; if (iLow < 0) return count; } else { iLow = this.indexOf(low, 0, cmp); iHigh = this.indexOf(high, -1, cmp); if (iHigh < 0) iHigh = ~iHigh; else if (includeHigh === true) iHigh++; } var keys = this.keys, values = this.values; if (onFound !== undefined) { for (var i = iLow; i < iHigh; i++) { var key = keys[i]; var result = onFound(key, values[i], count++); if (result !== undefined) { if (editMode === true) { if (key !== keys[i] || this.isShared === true) throw new Error("BTree illegally changed or cloned in editRange"); if (result.delete) { this.keys.splice(i, 1); if (this.values !== undefVals) this.values.splice(i, 1); i--; iHigh--; } else if (result.hasOwnProperty('value')) { values[i] = result.value; } } if (result.break !== undefined) return result; } } } else count += iHigh - iLow; return count; }; /** Adds entire contents of right-hand sibling (rhs is left unchanged) */ BNode.prototype.mergeSibling = function (rhs, _) { this.keys.push.apply(this.keys, rhs.keys); if (this.values === undefVals) { if (rhs.values === undefVals) return; this.values = this.values.slice(0, this.keys.length); } this.values.push.apply(this.values, rhs.reifyValues()); }; return BNode; }()); exports.BNode = BNode; /** Internal node (non-leaf node) ********************************************/ /** @internal */ var BNodeInternal = /** @class */ (function (_super) { __extends(BNodeInternal, _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, size, keys) { var _this = this; if (!keys) { keys = []; for (var i = 0; i < children.length; i++) keys[i] = children[i].maxKey(); } _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._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._size, this.keys.slice(0)); for (var i = 0; i < nu.children.length; i++) nu.children[i] = nu.children[i].greedyClone(force); return nu; }; BNodeInternal.prototype.minKey = function () { return this.children[0].minKey(); }; BNodeInternal.prototype.minPair = function (reusedArray) { return this.children[0].minPair(reusedArray); }; BNodeInternal.prototype.maxPair = function (reusedArray) { return this.children[this.children.length - 1].maxPair(reusedArray); }; BNodeInternal.prototype.get = function (key, defaultValue, tree) { var i = this.indexOf(key, 0, tree._compare), children = this.children; return i < children.length ? children[i].get(key, defaultValue, tree) : undefined; }; BNodeInternal.prototype.getPairOrNextLower = function (key, compare, inclusive, reusedArray) { var i = this.indexOf(key, 0, compare), children = this.children; if (i >= children.length) return this.maxPair(reusedArray); var result = children[i].getPairOrNextLower(key, compare, inclusive, reusedArray); if (result === undefined && i > 0) { return children[i - 1].maxPair(reusedArray); } return result; }; BNodeInternal.prototype.getPairOrNextHigher = function (key, compare, inclusive, reusedArray) { var i = this.indexOf(key, 0, compare), children = this.children, length = children.length; if (i >= length) return undefined; var result = children[i].getPairOrNextHigher(key, compare, inclusive, reusedArray); if (result === undefined && i < length - 1) { return children[i + 1].minPair(reusedArray); } return result; }; BNodeInternal.prototype.checkValid = function (depth, tree, baseIndex, checkOrdering) { var 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); var size = 0, c = this.children, k = this.keys, childSize = 0; var prevMinKey = undefined; var prevMaxKey = undefined; for (var i = 0; i < cL; i++) { var child = c[i]; var _a = child.checkValid(depth + 1, tree, baseIndex + size, checkOrdering), 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 && checkOrdering) { 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 === 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, this.minKey(), this.maxKey()]; }; ///////////////////////////////////////////////////////////////////////////// // Internal Node: set & node splitting ////////////////////////////////////// BNodeInternal.prototype.set = function (key, value, overwrite, tree) { var c = this.children, max = tree._maxNodeSize, cmp = tree._compare; var i = Math.min(this.indexOf(key, 0, cmp), c.length - 1), child = c[i]; if (child.isShared) c[i] = child = child.clone(); if (child.keys.length >= max) { // child is full; inserting anything else will cause a split. // Shifting an item to the left or right sibling may avoid a split. // We can do a shift if the adjacent node is not full and if the // current key can still be placed in the same node after the shift. var other; if (i > 0 && (other = c[i - 1]).keys.length < max && cmp(child.keys[0], key) < 0) { if (other.isShared) c[i - 1] = other = other.clone(); other.takeFromRight(child); this.keys[i - 1] = other.maxKey(); } else if ((other = c[i + 1]) !== undefined && other.keys.length < max && cmp(child.maxKey(), key) < 0) { if (other.isShared) c[i + 1] = other = other.clone(); other.takeFromLeft(child); 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(); if (result === true) return true; // The child has split and `result` is a new right child... does it fit? if (this.keys.length < max) { // yes this.insert(i + 1, result); return true; } else { // no, we must split also var newRightSibling = this.splitOffRightSide(), target = this; if (cmp(result.maxKey(), this.maxKey()) > 0) { target = newRightSibling; i -= this.keys.length; } target.insert(i + 1, result); return newRightSibling; } }; /** * Inserts `child` at index `i`. * This does not mark `child` as shared, so it is the responsibility of the caller * to ensure that either child is marked shared, or it is not included in another tree. */ 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. * Modifies this to remove the second half of the items, returning a separate node containing them. */ BNodeInternal.prototype.splitOffRightSide = function () { // assert !this.isShared; var half = this.children.length >> 1; 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> 1; if (iLow > 0) iLow--; for (i = iHigh; i >= iLow; i--) { if (children[i].keys.length <= half) { if (children[i].keys.length !== 0) { this.tryMerge(i, tree._maxNodeSize); } else { // child is empty! delete it! keys.splice(i, 1); var removed = children.splice(i, 1); check(removed[0].size() === 0, "emptiness cleanup"); } } } if (children.length !== 0 && children[0].keys.length === 0) check(false, "emptiness bug"); } } return count; }; /** Merges child i with child i+1 if their combined size is not too large */ BNodeInternal.prototype.tryMerge = function (i, maxSize) { var children = this.children; if (i >= 0 && i + 1 < children.length) { if (children[i].keys.length + children[i + 1].keys.length <= maxSize) { if (children[i].isShared) // cloned already UNLESS i is outside scan range children[i] = children[i].clone(); children[i].mergeSibling(children[i + 1], maxSize); children.splice(i + 1, 1); this.keys.splice(i + 1, 1); this.keys[i] = children[i].maxKey(); return true; } } return false; }; /** * Move children from `rhs` into this. * `rhs` must be part of this tree, and be removed from it after this call * (otherwise isShared for its children could be incorrect). */ BNodeInternal.prototype.mergeSibling = function (rhs, maxNodeSize) { // assert !this.isShared; var oldLength = this.keys.length; this.keys.push.apply(this.keys, rhs.keys); var rhsChildren = rhs.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 // parent is not shared, they must now be explicitly marked as shared. for (var i = 0; i < rhsChildren.length; i++) rhsChildren[i].isShared = true; } // If our children are themselves almost empty due to a mass-delete, // they may need to be merged too (but only the oldLength-1 and its // right sibling should need this). this.tryMerge(oldLength - 1, maxNodeSize); }; return BNodeInternal; }(BNode)); exports.BNodeInternal = BNodeInternal; // Optimization: this array of `undefined`s is used instead of a normal // array of values in nodes where `undefined` is the only value. // Its length is extended to max node size on first use; since it can // be shared between trees with different maximums, its length can only // increase, never decrease. Its type should be undefined[] but strangely // TypeScript won't allow the comparison V[] === undefined[]. To prevent // users from making this array too large, BTree has a maximum node size. // // FAQ: undefVals[i] is already undefined, so why increase the array size? // Reading outside the bounds of an array is relatively slow because it // has the side effect of scanning the prototype chain. var undefVals = []; /** * Sums the sizes of the given child nodes. * @param children the child nodes * @returns the total size * @internal */ function sumChildSizes(children) { var total = 0; for (var i = 0; i < children.length; i++) total += children[i].size(); return total; } exports.sumChildSizes = sumChildSizes; /** * Determines whether two nodes are overlapping in key range. * @internal */ function areOverlapping(aMin, aMax, bMin, bMax, cmp) { return cmp(aMin, bMax) <= 0 && cmp(aMax, bMin) >= 0; } exports.areOverlapping = areOverlapping; var Delete = { delete: true }, DeleteRange = function () { return Delete; }; var Break = { break: true }; var EmptyLeaf = (function () { var n = new BNode(); n.isShared = true; return n; })(); var EmptyArray = []; var ReusedArray = []; // assumed thread-local /** @internal */ function check(fact) { var args = []; for (var _i = 1; _i < arguments.length; _i++) { args[_i - 1] = arguments[_i]; } if (!fact) { args.unshift('B+ tree'); // at beginning of message throw new Error(args.join(' ')); } } exports.check = check; /** A BTree frozen in the empty state. */ exports.EmptyBTree = (function () { var t = new BTree(); t.freeze(); return t; })(); ================================================ FILE: b+tree.ts ================================================ // B+ tree by David Piepgrass. License: MIT import { ISortedMap, ISortedMapF, ISortedSet } from './interfaces'; export { ISetSource, ISetSink, ISet, ISetF, ISortedSetSource, ISortedSet, ISortedSetF, IMapSource, IMapSink, IMap, IMapF, ISortedMapSource, ISortedMap, ISortedMapF } from './interfaces'; export type EditRangeResult = {value?:V, break?:R, delete?:boolean}; type index = number; // Informative microbenchmarks & stuff: // http://www.jayconrod.com/posts/52/a-tour-of-v8-object-representation (very educational) // https://blog.mozilla.org/luke/2012/10/02/optimizing-javascript-variable-access/ (local vars are faster than properties) // http://benediktmeurer.de/2017/12/13/an-introduction-to-speculative-optimization-in-v8/ (other stuff) // https://jsperf.com/js-in-operator-vs-alternatives (avoid 'in' operator; `.p!==undefined` faster than `hasOwnProperty('p')` in all browsers) // https://jsperf.com/instanceof-vs-typeof-vs-constructor-vs-member (speed of type tests varies wildly across browsers) // https://jsperf.com/detecting-arrays-new (a.constructor===Array is best across browsers, assuming a is an object) // https://jsperf.com/shallow-cloning-methods (a constructor is faster than Object.create; hand-written clone faster than Object.assign) // https://jsperf.com/ways-to-fill-an-array (slice-and-replace is fastest) // https://jsperf.com/math-min-max-vs-ternary-vs-if (Math.min/max is slow on Edge) // https://jsperf.com/array-vs-property-access-speed (v.x/v.y is faster than a[0]/a[1] in major browsers IF hidden class is constant) // https://jsperf.com/detect-not-null-or-undefined (`x==null` slightly slower than `x===null||x===undefined` on all browsers) // Overall, microbenchmarks suggest Firefox is the fastest browser for JavaScript and Edge is the slowest. // Lessons from https://v8project.blogspot.com/2017/09/elements-kinds-in-v8.html: // - Avoid holes in arrays. Avoid `new Array(N)`, it will be "holey" permanently. // - Don't read outside bounds of an array (it scans prototype chain). // - Small integer arrays are stored differently from doubles // - Adding non-numbers to an array deoptimizes it permanently into a general array // - Objects can be used like arrays (e.g. have length property) but are slower // - V8 source (NewElementsCapacity in src/objects.h): arrays grow by 50% + 16 elements /** * Types that BTree supports by default */ export type DefaultComparable = number | string | Date | boolean | null | undefined | (number | string)[] | { valueOf: () => number | string | Date | boolean | null | undefined | (number | string)[] }; /** * Compares DefaultComparables to form a strict partial ordering. * * Handles +/-0 and NaN like Map: NaN is equal to NaN, and -0 is equal to +0. * * Arrays are compared using '<' and '>', which may cause unexpected equality: * for example [1] will be considered equal to ['1']. * * Two objects with equal valueOf compare the same, but compare unequal to * primitives that have the same value. */ export function defaultComparator(a: DefaultComparable, b: DefaultComparable): number { // Special case finite numbers first for performance. // Note that the trick of using 'a - b' and checking for NaN to detect non-numbers // does not work if the strings are numeric (ex: "5"). This would leading most // comparison functions using that approach to fail to have transitivity. if (Number.isFinite(a as any) && Number.isFinite(b as any)) { return a as number - (b as number); } // The default < and > operators are not totally ordered. To allow types to be mixed // in a single collection, compare types and order values of different types by type. let ta = typeof a; let tb = typeof b; if (ta !== tb) { return ta < tb ? -1 : 1; } if (ta === 'object') { // standardized JavaScript bug: null is not an object, but typeof says it is if (a === null) return b === null ? 0 : -1; else if (b === null) return 1; a = a!.valueOf() as DefaultComparable; b = b!.valueOf() as DefaultComparable; ta = typeof a; tb = typeof b; // Deal with the two valueOf()s producing different types if (ta !== tb) { return ta < tb ? -1 : 1; } } // a and b are now the same type, and will be a number, string or array // (which we assume holds numbers or strings), or something unsupported. if (a! < b!) return -1; if (a! > b!) return 1; if (a === b) return 0; // Order NaN less than other numbers if (Number.isNaN(a as any)) return Number.isNaN(b as any) ? 0 : -1; else if (Number.isNaN(b as any)) return 1; // This could be two objects (e.g. [7] and ['7']) that aren't ordered return Array.isArray(a) ? 0 : Number.NaN; }; /** * Compares items using the < and > operators. This function is probably slightly * faster than the defaultComparator for Dates and strings, but has not been benchmarked. * Unlike defaultComparator, this comparator doesn't support mixed types correctly, * i.e. use it with `BTree` or `BTree` but not `BTree`. * * NaN is not supported. * * Note: null is treated like 0 when compared with numbers or Date, but in general * null is not ordered with respect to strings (neither greater nor less), and * undefined is not ordered with other types. */ export function simpleComparator(a: string, b:string): number; export function simpleComparator(a: number|null, b:number|null): number; export function simpleComparator(a: Date|null, b:Date|null): number; export function simpleComparator(a: (number|string)[], b:(number|string)[]): number; export function simpleComparator(a: any, b: any): number { return a > b ? 1 : a < b ? -1 : 0; }; /** Sanitizes a requested max node size. * @internal */ export function fixMaxSize(maxNodeSize?: number) { return maxNodeSize! >= 4 ? Math.min(maxNodeSize! | 0, 256) : 32; } /** * A reasonably fast collection of key-value pairs with a powerful API. * Largely compatible with the standard Map. BTree is a B+ tree data structure, * so the collection is sorted by key. * * B+ trees tend to use memory more efficiently than hashtables such as the * standard Map, especially when the collection contains a large number of * items. However, maintaining the sort order makes them modestly slower: * O(log size) rather than O(1). This B+ tree implementation supports O(1) * fast cloning. It also supports freeze(), which can be used to ensure that * a BTree is not changed accidentally. * * Confusingly, the ES6 Map.forEach(c) method calls c(value,key) instead of * c(key,value), in contrast to other methods such as set() and entries() * which put the key first. I can only assume that the order was reversed on * the theory that users would usually want to examine values and ignore keys. * BTree's forEach() therefore works the same way, but a second method * `.forEachPair((key,value)=>{...})` is provided which sends you the key * first and the value second; this method is slightly faster because it is * the "native" for-each method for this class. * * Out of the box, BTree supports keys that are numbers, strings, arrays of * numbers/strings, Date, and objects that have a valueOf() method returning a * number or string. Other data types, such as arrays of Date or custom * objects, require a custom comparator, which you must pass as the second * argument to the constructor (the first argument is an optional list of * initial items). Symbols cannot be used as keys because they are unordered * (one Symbol is never "greater" or "less" than another). * * @example * Given a {name: string, age: number} object, you can create a tree sorted by * name and then by age like this: * * var tree = new BTree(undefined, (a, b) => { * if (a.name > b.name) * return 1; // Return a number >0 when a > b * else if (a.name < b.name) * return -1; // Return a number <0 when a < b * else // names are equal (or incomparable) * return a.age - b.age; // Return >0 when a.age > b.age * }); * * tree.set({name:"Bill", age:17}, "happy"); * tree.set({name:"Fran", age:40}, "busy & stressed"); * tree.set({name:"Bill", age:55}, "recently laid off"); * tree.forEachPair((k, v) => { * console.log(`Name: ${k.name} Age: ${k.age} Status: ${v}`); * }); * * @description * The "range" methods (`forEach, forRange, editRange`) will return the number * of elements that were scanned. In addition, the callback can return {break:R} * to stop early and return R from the outer function. * * - TODO: Test performance of preallocating values array at max size * - TODO: Add fast initialization when a sorted array is provided to constructor * * For more documentation see https://github.com/qwertie/btree-typescript * * Are you a C# developer? You might like the similar data structures I made for C#: * BDictionary, BList, etc. See http://core.loyc.net/collections/ * * @author David Piepgrass */ class BTree implements ISortedMapF, ISortedMap { private _root: BNode = EmptyLeaf as BNode; _maxNodeSize: number; /** * provides a total order over keys (and a strict partial order over the type K) * @returns a negative value if a < b, 0 if a === b and a positive value if a > b */ _compare: (a:K, b:K) => number; /** * Initializes an empty B+ tree. * @param compare Custom function to compare pairs of elements in the tree. * If not specified, defaultComparator will be used which is valid as long as K extends DefaultComparable. * @param entries A set of key-value pairs to initialize the tree * @param maxNodeSize Branching factor (maximum items or children per node) * Must be in range 4..256. If undefined or <4 then default is used; if >256 then 256. */ public constructor(entries?: [K,V][], compare?: (a: K, b: K) => number, maxNodeSize?: number) { this._maxNodeSize = fixMaxSize(maxNodeSize); this._compare = compare || defaultComparator as any as (a: K, b: K) => number; if (entries) this.setPairs(entries); } ///////////////////////////////////////////////////////////////////////////// // ES6 Map methods ///////////////////////////////////////////////////// /** Gets the number of key-value pairs in the tree. */ get size(): number { return this._root.size(); } /** Gets the number of key-value pairs in the tree. */ get length(): number { return this.size; } /** Returns true iff the tree contains no key-value pairs. */ get isEmpty(): boolean { return this._root.size() === 0; } /** Releases the tree so that its size is 0. */ clear() { this._root = EmptyLeaf as BNode; } forEach(callback: (v:V, k:K, tree:BTree) => void, thisArg?: any): number; /** Runs a function for each key-value pair, in order from smallest to * largest key. For compatibility with ES6 Map, the argument order to * the callback is backwards: value first, then key. Call forEachPair * instead to receive the key as the first argument. * @param thisArg If provided, this parameter is assigned as the `this` * value for each callback. * @returns the number of values that were sent to the callback, * or the R value if the callback returned {break:R}. */ forEach(callback: (v:V, k:K, tree:BTree) => {break?:R}|void, thisArg?: any): R|number { if (thisArg !== undefined) callback = callback.bind(thisArg); return this.forEachPair((k, v) => callback(v, k, this)); } /** Runs a function for each key-value pair, in order from smallest to * largest key. The callback can return {break:R} (where R is any value * except undefined) to stop immediately and return R from forEachPair. * @param onFound A function that is called for each key-value pair. This * function can return {break:R} to stop early with result R. * The reason that you must return {break:R} instead of simply R * itself is for consistency with editRange(), which allows * multiple actions, not just breaking. * @param initialCounter This is the value of the third argument of * `onFound` the first time it is called. The counter increases * by one each time `onFound` is called. Default value: 0 * @returns the number of pairs sent to the callback (plus initialCounter, * if you provided one). If the callback returned {break:R} then * the R value is returned instead. */ forEachPair(callback: (k:K, v:V, counter:number) => {break?:R}|void, initialCounter?: number): R|number { var low = this.minKey(), high = this.maxKey(); return this.forRange(low!, high!, true, callback, initialCounter); } /** * Finds a pair in the tree and returns the associated value. * @param defaultValue a value to return if the key was not found. * @returns the value, or defaultValue if the key was not found. * @description Computational complexity: O(log size) */ get(key: K, defaultValue?: V): V | undefined { return this._root.get(key, defaultValue, this); } /** * Adds or overwrites a key-value pair in the B+ tree. * @param key the key is used to determine the sort order of * data in the tree. * @param value data to associate with the key (optional) * @param overwrite Whether to overwrite an existing key-value pair * (default: true). If this is false and there is an existing * key-value pair then this method has no effect. * @returns true if a new key-value pair was added. * @description Computational complexity: O(log size) * Note: when overwriting a previous entry, the key is updated * as well as the value. This has no effect unless the new key * has data that does not affect its sort order. */ set(key: K, value: V, overwrite?: boolean): boolean { if (this._root.isShared) this._root = this._root.clone(); var result = this._root.set(key, value, overwrite, this); if (result === true || result === false) return result; // Root node has split, so create a new root node. const children = [this._root, result]; this._root = new BNodeInternal(children, sumChildSizes(children)); return true; } /** * Returns true if the key exists in the B+ tree, false if not. * Use get() for best performance; use has() if you need to * distinguish between "undefined value" and "key not present". * @param key Key to detect * @description Computational complexity: O(log size) */ has(key: K): boolean { return this.forRange(key, key, true, undefined) !== 0; } /** * Removes a single key-value pair from the B+ tree. * @param key Key to find * @returns true if a pair was found and removed, false otherwise. * @description Computational complexity: O(log size) */ delete(key: K): boolean { return this.editRange(key, key, true, DeleteRange) !== 0; } ///////////////////////////////////////////////////////////////////////////// // Clone-mutators /////////////////////////////////////////////////////////// /** Returns a copy of the tree with the specified key set (the value is undefined). */ with(key: K): BTree; /** Returns a copy of the tree with the specified key-value pair set. */ with(key: K, value: V2, overwrite?: boolean): BTree; with(key: K, value?: V2, overwrite?: boolean): BTree { let nu = this.clone() as BTree; return nu.set(key, value, overwrite) || overwrite ? nu : this; } /** Returns a copy of the tree with the specified key-value pairs set. */ withPairs(pairs: [K,V|V2][], overwrite: boolean): BTree { let nu = this.clone() as BTree; return nu.setPairs(pairs, overwrite) !== 0 || overwrite ? nu : this; } /** Returns a copy of the tree with the specified keys present. * @param keys The keys to add. If a key is already present in the tree, * neither the existing key nor the existing value is modified. * @param returnThisIfUnchanged if true, returns this if all keys already * existed. Performance note: due to the architecture of this class, all * node(s) leading to existing keys are cloned even if the collection is * ultimately unchanged. */ withKeys(keys: K[], returnThisIfUnchanged?: boolean): BTree { let nu = this.clone() as BTree, changed = false; for (var i = 0; i < keys.length; i++) changed = nu.set(keys[i], undefined, false) || changed; return returnThisIfUnchanged && !changed ? this : nu; } /** Returns a copy of the tree with the specified key removed. * @param returnThisIfUnchanged if true, returns this if the key didn't exist. * Performance note: due to the architecture of this class, node(s) leading * to where the key would have been stored are cloned even when the key * turns out not to exist and the collection is unchanged. */ without(key: K, returnThisIfUnchanged?: boolean): this { return this.withoutRange(key, key, true, returnThisIfUnchanged); } /** Returns a copy of the tree with the specified keys removed. * @param returnThisIfUnchanged if true, returns this if none of the keys * existed. Performance note: due to the architecture of this class, * node(s) leading to where the key would have been stored are cloned * even when the key turns out not to exist. */ withoutKeys(keys: K[], returnThisIfUnchanged?: boolean): this { let nu = this.clone(); return nu.deleteKeys(keys) || !returnThisIfUnchanged ? nu : this; } /** Returns a copy of the tree with the specified range of keys removed. */ withoutRange(low: K, high: K, includeHigh: boolean, returnThisIfUnchanged?: boolean): this { let nu = this.clone(); if (nu.deleteRange(low, high, includeHigh) === 0 && returnThisIfUnchanged) return this; return nu; } /** Returns a copy of the tree with pairs removed whenever the callback * function returns false. `where()` is a synonym for this method. */ filter(callback: (k:K,v:V,counter:number) => boolean, returnThisIfUnchanged?: boolean): this { var nu = this.greedyClone(); var del: any; nu.editAll((k,v,i) => { if (!callback(k, v, i)) return del = Delete; }); if (!del && returnThisIfUnchanged) return this; return nu; } /** Returns a copy of the tree with all values altered by a callback function. */ mapValues(callback: (v:V,k:K,counter:number) => R): BTree { var tmp = {} as {value:R}; var nu = this.greedyClone(); nu.editAll((k,v,i) => { return tmp.value = callback(v, k, i), tmp as any; }); return nu as any as BTree; } /** Performs a reduce operation like the `reduce` method of `Array`. * It is used to combine all pairs into a single value, or perform * conversions. `reduce` is best understood by example. For example, * `tree.reduce((P, pair) => P * pair[0], 1)` multiplies all keys * together. It means "start with P=1, and for each pair multiply * it by the key in pair[0]". Another example would be converting * the tree to a Map (in this example, note that M.set returns M): * * var M = tree.reduce((M, pair) => M.set(pair[0],pair[1]), new Map()) * * **Note**: the same array is sent to the callback on every iteration. */ reduce(callback: (previous:R,currentPair:[K,V],counter:number,tree:BTree) => R, initialValue: R): R; reduce(callback: (previous:R|undefined,currentPair:[K,V],counter:number,tree:BTree) => R): R|undefined; reduce(callback: (previous:R|undefined,currentPair:[K,V],counter:number,tree:BTree) => R, initialValue?: R): R|undefined { let i = 0, p = initialValue; var it = this.entries(this.minKey(), ReusedArray), next; while (!(next = it.next()).done) p = callback(p, next.value, i++, this); return p; } ///////////////////////////////////////////////////////////////////////////// // Iterator methods ///////////////////////////////////////////////////////// /** Returns an iterator that provides items in order (ascending order if * the collection's comparator uses ascending order, as is the default.) * @param lowestKey First key to be iterated, or undefined to start at * minKey(). If the specified key doesn't exist then iteration * starts at the next higher key (according to the comparator). * @param reusedArray Optional array used repeatedly to store key-value * pairs, to avoid creating a new array on every iteration. */ entries(lowestKey?: K, reusedArray?: (K|V)[]): IterableIterator<[K,V]> { var info = this.findPath(lowestKey); if (info === undefined) return iterator<[K,V]>(); var {nodequeue, nodeindex, leaf} = info; var state = reusedArray !== undefined ? 1 : 0; var i = (lowestKey === undefined ? -1 : leaf.indexOf(lowestKey, 0, this._compare) - 1); return iterator<[K,V]>(() => { jump: for (;;) { switch(state) { case 0: if (++i < leaf.keys.length) return {done: false, value: [leaf.keys[i], leaf.values[i]]}; state = 2; continue; case 1: if (++i < leaf.keys.length) { reusedArray![0] = leaf.keys[i], reusedArray![1] = leaf.values[i]; return {done: false, value: reusedArray as [K,V]}; } state = 2; case 2: // Advance to the next leaf node for (var level = -1;;) { if (++level >= nodequeue.length) { state = 3; continue jump; } if (++nodeindex[level] < nodequeue[level].length) break; } for (; level > 0; level--) { nodequeue[level-1] = (nodequeue[level][nodeindex[level]] as BNodeInternal).children; nodeindex[level-1] = 0; } leaf = nodequeue[0][nodeindex[0]]; i = -1; state = reusedArray !== undefined ? 1 : 0; continue; case 3: return {done: true, value: undefined}; } } }); } /** Returns an iterator that provides items in reversed order. * @param highestKey Key at which to start iterating, or undefined to * start at maxKey(). If the specified key doesn't exist then iteration * starts at the next lower key (according to the comparator). * @param reusedArray Optional array used repeatedly to store key-value * pairs, to avoid creating a new array on every iteration. * @param skipHighest Iff this flag is true and the highestKey exists in the * collection, the pair matching highestKey is skipped, not iterated. */ entriesReversed(highestKey?: K, reusedArray?: (K|V)[], skipHighest?: boolean): IterableIterator<[K,V]> { if (highestKey === undefined) { highestKey = this.maxKey(); skipHighest = undefined; if (highestKey === undefined) return iterator<[K,V]>(); // collection is empty } var {nodequeue,nodeindex,leaf} = this.findPath(highestKey) || this.findPath(this.maxKey())!; check(!nodequeue[0] || leaf === nodequeue[0][nodeindex[0]], "wat!"); var i = leaf.indexOf(highestKey, 0, this._compare); if (!skipHighest && i < leaf.keys.length && this._compare(leaf.keys[i], highestKey) <= 0) i++; var state = reusedArray !== undefined ? 1 : 0; return iterator<[K,V]>(() => { jump: for (;;) { switch(state) { case 0: if (--i >= 0) return {done: false, value: [leaf.keys[i], leaf.values[i]]}; state = 2; continue; case 1: if (--i >= 0) { reusedArray![0] = leaf.keys[i], reusedArray![1] = leaf.values[i]; return {done: false, value: reusedArray as [K,V]}; } state = 2; case 2: // Advance to the next leaf node for (var level = -1;;) { if (++level >= nodequeue.length) { state = 3; continue jump; } if (--nodeindex[level] >= 0) break; } for (; level > 0; level--) { nodequeue[level-1] = (nodequeue[level][nodeindex[level]] as BNodeInternal).children; nodeindex[level-1] = nodequeue[level-1].length-1; } leaf = nodequeue[0][nodeindex[0]]; i = leaf.keys.length; state = reusedArray !== undefined ? 1 : 0; continue; case 3: return {done: true, value: undefined}; } } }); } /* Used by entries() and entriesReversed() to prepare to start iterating. * It develops a "node queue" for each non-leaf level of the tree. * Levels are numbered "bottom-up" so that level 0 is a list of leaf * nodes from a low-level non-leaf node. The queue at a given level L * consists of nodequeue[L] which is the children of a BNodeInternal, * and nodeindex[L], the current index within that child list, such * such that nodequeue[L-1] === nodequeue[L][nodeindex[L]].children. * (However inside this function the order is reversed.) */ private findPath(key?: K): { nodequeue: BNode[][], nodeindex: number[], leaf: BNode } | undefined { var nextnode = this._root; var nodequeue: BNode[][], nodeindex: number[]; if (nextnode.isLeaf) { nodequeue = EmptyArray, nodeindex = EmptyArray; // avoid allocations } else { nodequeue = [], nodeindex = []; for (var d = 0; !nextnode.isLeaf; d++) { nodequeue[d] = (nextnode as BNodeInternal).children; nodeindex[d] = key === undefined ? 0 : nextnode.indexOf(key, 0, this._compare); if (nodeindex[d] >= nodequeue[d].length) return; // first key > maxKey() nextnode = nodequeue[d][nodeindex[d]]; } nodequeue.reverse(); nodeindex.reverse(); } return {nodequeue, nodeindex, leaf:nextnode}; } /** Returns a new iterator for iterating the keys of each pair in ascending order. * @param firstKey: Minimum key to include in the output. */ keys(firstKey?: K): IterableIterator { var it = this.entries(firstKey, ReusedArray); return iterator(() => { var n: IteratorResult = it.next(); if (n.value) n.value = n.value[0]; return n; }); } /** Returns a new iterator for iterating the values of each pair in order by key. * @param firstKey: Minimum key whose associated value is included in the output. */ values(firstKey?: K): IterableIterator { var it = this.entries(firstKey, ReusedArray); return iterator(() => { var n: IteratorResult = it.next(); if (n.value) n.value = n.value[1]; return n; }); } ///////////////////////////////////////////////////////////////////////////// // Additional methods /////////////////////////////////////////////////////// /** Returns the maximum number of children/values before nodes will split. */ get maxNodeSize() { return this._maxNodeSize; } /** Gets the lowest key in the tree. Complexity: O(log size) */ minKey(): K | undefined { return this._root.minKey(); } /** Gets the highest key in the tree. Complexity: O(1) */ maxKey(): K | undefined { return this._root.maxKey(); } /** Quickly clones the tree by marking the root node as shared. * Both copies remain editable. When you modify either copy, any * nodes that are shared (or potentially shared) between the two * copies are cloned so that the changes do not affect other copies. * This is known as copy-on-write behavior, or "lazy copying". */ clone(): this { this._root.isShared = true; var result = new BTree(undefined, this._compare, this._maxNodeSize); result._root = this._root; return result as this; } /** Performs a greedy clone, immediately duplicating any nodes that are * not currently marked as shared, in order to avoid marking any * additional nodes as shared. * @param force Clone all nodes, even shared ones. */ greedyClone(force?: boolean): this { var result = new BTree(undefined, this._compare, this._maxNodeSize); result._root = this._root.greedyClone(force); return result as this; } /** Gets an array filled with the contents of the tree, sorted by key */ toArray(maxLength: number = 0x7FFFFFFF): [K,V][] { let min = this.minKey(), max = this.maxKey(); if (min !== undefined) return this.getRange(min, max!, true, maxLength) return []; } /** Gets an array of all keys, sorted */ keysArray() { var results: K[] = []; this._root.forRange(this.minKey()!, this.maxKey()!, true, false, this, 0, (k,v) => { results.push(k); }); return results; } /** Gets an array of all values, sorted by key */ valuesArray() { var results: V[] = []; this._root.forRange(this.minKey()!, this.maxKey()!, true, false, this, 0, (k,v) => { results.push(v); }); return results; } /** Gets a string representing the tree's data based on toArray(). */ toString() { return this.toArray().toString(); } /** Stores a key-value pair only if the key doesn't already exist in the tree. * @returns true if a new key was added */ setIfNotPresent(key: K, value: V): boolean { return this.set(key, value, false); } /** Returns the next pair whose key is larger than the specified key (or undefined if there is none). * If key === undefined, this function returns the lowest pair. * @param key The key to search for. * @param reusedArray Optional array used repeatedly to store key-value pairs, to * avoid creating a new array on every iteration. */ nextHigherPair(key: K|undefined, reusedArray?: [K,V]): [K,V]|undefined { reusedArray = reusedArray || ([] as unknown as [K,V]); if (key === undefined) { return this._root.minPair(reusedArray); } return this._root.getPairOrNextHigher(key, this._compare, false, reusedArray); } /** Returns the next key larger than the specified key, or undefined if there is none. * Also, nextHigherKey(undefined) returns the lowest key. */ nextHigherKey(key: K|undefined): K|undefined { var p = this.nextHigherPair(key, ReusedArray as [K,V]); return p && p[0]; } /** Returns the next pair whose key is smaller than the specified key (or undefined if there is none). * If key === undefined, this function returns the highest pair. * @param key The key to search for. * @param reusedArray Optional array used repeatedly to store key-value pairs, to * avoid creating a new array each time you call this method. */ nextLowerPair(key: K|undefined, reusedArray?: [K,V]): [K,V]|undefined { reusedArray = reusedArray || ([] as unknown as [K,V]); if (key === undefined) { return this._root.maxPair(reusedArray); } return this._root.getPairOrNextLower(key, this._compare, false, reusedArray); } /** Returns the next key smaller than the specified key, or undefined if there is none. * Also, nextLowerKey(undefined) returns the highest key. */ nextLowerKey(key: K|undefined): K|undefined { var p = this.nextLowerPair(key, ReusedArray as [K,V]); return p && p[0]; } /** Returns the key-value pair associated with the supplied key if it exists * or the pair associated with the next lower pair otherwise. If there is no * next lower pair, undefined is returned. * @param key The key to search for. * @param reusedArray Optional array used repeatedly to store key-value pairs, to * avoid creating a new array each time you call this method. * */ getPairOrNextLower(key: K, reusedArray?: [K,V]): [K,V]|undefined { return this._root.getPairOrNextLower(key, this._compare, true, reusedArray || ([] as unknown as [K,V])); } /** Returns the key-value pair associated with the supplied key if it exists * or the pair associated with the next lower pair otherwise. If there is no * next lower pair, undefined is returned. * @param key The key to search for. * @param reusedArray Optional array used repeatedly to store key-value pairs, to * avoid creating a new array each time you call this method. * */ getPairOrNextHigher(key: K, reusedArray?: [K,V]): [K,V]|undefined { return this._root.getPairOrNextHigher(key, this._compare, true, reusedArray || ([] as unknown as [K,V])); } /** Edits the value associated with a key in the tree, if it already exists. * @returns true if the key existed, false if not. */ changeIfPresent(key: K, value: V): boolean { return this.editRange(key, key, true, (k,v) => ({value})) !== 0; } /** * Builds an array of pairs from the specified range of keys, sorted by key. * Each returned pair is also an array: pair[0] is the key, pair[1] is the value. * @param low The first key in the array will be greater than or equal to `low`. * @param high This method returns when a key larger than this is reached. * @param includeHigh If the `high` key is present, its pair will be included * in the output if and only if this parameter is true. Note: if the * `low` key is present, it is always included in the output. * @param maxLength Length limit. getRange will stop scanning the tree when * the array reaches this size. * @description Computational complexity: O(result.length + log size) */ getRange(low: K, high: K, includeHigh?: boolean, maxLength: number = 0x3FFFFFF): [K,V][] { var results: [K,V][] = []; this._root.forRange(low, high, includeHigh, false, this, 0, (k,v) => { results.push([k,v]) return results.length > maxLength ? Break : undefined; }); return results; } /** Adds all pairs from a list of key-value pairs. * @param pairs Pairs to add to this tree. If there are duplicate keys, * later pairs currently overwrite earlier ones (e.g. [[0,1],[0,7]] * associates 0 with 7.) * @param overwrite Whether to overwrite pairs that already exist (if false, * pairs[i] is ignored when the key pairs[i][0] already exists.) * @returns The number of pairs added to the collection. * @description Computational complexity: O(pairs.length * log(size + pairs.length)) */ setPairs(pairs: [K,V][], overwrite?: boolean): number { var added = 0; for (var i = 0; i < pairs.length; i++) if (this.set(pairs[i][0], pairs[i][1], overwrite)) added++; return added; } forRange(low: K, high: K, includeHigh: boolean, onFound?: (k:K,v:V,counter:number) => void, initialCounter?: number): number; /** * Scans the specified range of keys, in ascending order by key. * Note: the callback `onFound` must not insert or remove items in the * collection. Doing so may cause incorrect data to be sent to the * callback afterward. * @param low The first key scanned will be greater than or equal to `low`. * @param high Scanning stops when a key larger than this is reached. * @param includeHigh If the `high` key is present, `onFound` is called for * that final pair if and only if this parameter is true. * @param onFound A function that is called for each key-value pair. This * function can return {break:R} to stop early with result R. * @param initialCounter Initial third argument of onFound. This value * increases by one each time `onFound` is called. Default: 0 * @returns The number of values found, or R if the callback returned * `{break:R}` to stop early. * @description Computational complexity: O(number of items scanned + log size) */ forRange(low: K, high: K, includeHigh: boolean, onFound?: (k:K,v:V,counter:number) => {break?:R}|void, initialCounter?: number): R|number { var r = this._root.forRange(low, high, includeHigh, false, this, initialCounter || 0, onFound); return typeof r === "number" ? r : r.break!; } /** * Scans and potentially modifies values for a subsequence of keys. * Note: the callback `onFound` should ideally be a pure function. * Specfically, it must not insert items, call clone(), or change * the collection except via return value; out-of-band editing may * cause an exception or may cause incorrect data to be sent to * the callback (duplicate or missed items). It must not cause a * clone() of the collection, otherwise the clone could be modified * by changes requested by the callback. * @param low The first key scanned will be greater than or equal to `low`. * @param high Scanning stops when a key larger than this is reached. * @param includeHigh If the `high` key is present, `onFound` is called for * that final pair if and only if this parameter is true. * @param onFound A function that is called for each key-value pair. This * function can return `{value:v}` to change the value associated * with the current key, `{delete:true}` to delete the current pair, * `{break:R}` to stop early with result R, or it can return nothing * (undefined or {}) to cause no effect and continue iterating. * `{break:R}` can be combined with one of the other two commands. * The third argument `counter` is the number of items iterated * previously; it equals 0 when `onFound` is called the first time. * @returns The number of values scanned, or R if the callback returned * `{break:R}` to stop early. * @description * Computational complexity: O(number of items scanned + log size) * Note: if the tree has been cloned with clone(), any shared * nodes are copied before `onFound` is called. This takes O(n) time * where n is proportional to the amount of shared data scanned. */ editRange(low: K, high: K, includeHigh: boolean, onFound: (k:K,v:V,counter:number) => EditRangeResult|void, initialCounter?: number): R|number { var root = this._root; if (root.isShared) this._root = root = root.clone(); try { var r = root.forRange(low, high, includeHigh, true, this, initialCounter || 0, onFound); return typeof r === "number" ? r : r.break!; } finally { let isShared; while (root.keys.length <= 1 && !root.isLeaf) { isShared ||= root.isShared; this._root = root = root.keys.length === 0 ? EmptyLeaf : (root as any as BNodeInternal).children[0]; } // If any ancestor of the new root was shared, the new root must also be shared if (isShared) { root.isShared = true; } } } /** Same as `editRange` except that the callback is called for all pairs. */ editAll(onFound: (k:K,v:V,counter:number) => EditRangeResult|void, initialCounter?: number): R|number { return this.editRange(this.minKey()!, this.maxKey()!, true, onFound, initialCounter); } /** * Removes a range of key-value pairs from the B+ tree. * @param low The first key scanned will be greater than or equal to `low`. * @param high Scanning stops when a key larger than this is reached. * @param includeHigh Specifies whether the `high` key, if present, is deleted. * @returns The number of key-value pairs that were deleted. * @description Computational complexity: O(log size + number of items deleted) */ deleteRange(low: K, high: K, includeHigh: boolean): number { return this.editRange(low, high, includeHigh, DeleteRange); } /** Deletes a series of keys from the collection. */ deleteKeys(keys: K[]): number { for (var i = 0, r = 0; i < keys.length; i++) if (this.delete(keys[i])) r++; return r; } /** Gets the height of the tree: the number of internal nodes between the * BTree object and its leaf nodes (zero if there are no internal nodes). */ get height(): number { let node: BNode | undefined = this._root; let height = -1; while (node) { height++; node = node.isLeaf ? undefined : (node as unknown as BNodeInternal).children[0]; } return height; } /** Makes the object read-only to ensure it is not accidentally modified. * Freezing does not have to be permanent; unfreeze() reverses the effect. * This is accomplished by replacing mutator functions with a function * that throws an Error. Compared to using a property (e.g. this.isFrozen) * this implementation gives better performance in non-frozen BTrees. */ freeze() { var t = this as any; // Note: all other mutators ultimately call set() or editRange() // so we don't need to override those others. t.clear = t.set = t.editRange = function() { throw new Error("Attempted to modify a frozen BTree"); }; } /** Ensures mutations are allowed, reversing the effect of freeze(). */ unfreeze() { // @ts-ignore "The operand of a 'delete' operator must be optional." // (wrong: delete does not affect the prototype.) delete this.clear; // @ts-ignore delete this.set; // @ts-ignore delete this.editRange; } /** Returns true if the tree appears to be frozen. */ get isFrozen() { return this.hasOwnProperty('editRange'); } /** 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). This method validates cached size * information and, optionally, the ordering of keys (including leaves), which * takes more time to check (O(size), which is technically the same big-O). */ checkValid(checkOrdering = false) { var [size] = this._root.checkValid(0, this, 0, checkOrdering); check(size === this.size, "size mismatch: counted ", size, "but stored", this.size); } } export { BTree, BTree as default }; /** A TypeScript helper function that simply returns its argument, typed as * `ISortedSet` if the BTree implements it, as it does if `V extends undefined`. * If `V` cannot be `undefined`, it returns `unknown` instead. Or at least, that * was the intention, but TypeScript is acting weird and may return `ISortedSet` * even if `V` can't be `undefined` (discussion: btree-typescript issue #14) */ export function asSet(btree: BTree): undefined extends V ? ISortedSet : unknown { return btree as any; } declare const Symbol: any; if (Symbol && Symbol.iterator) // iterator is equivalent to entries() (BTree as any).prototype[Symbol.iterator] = BTree.prototype.entries; (BTree as any).prototype.where = BTree.prototype.filter; (BTree as any).prototype.setRange = BTree.prototype.setPairs; (BTree as any).prototype.add = BTree.prototype.set; // for compatibility with ISetSink function iterator(next: () => IteratorResult = (() => ({ done:true, value:undefined }))): IterableIterator { var result: any = { next }; if (Symbol && Symbol.iterator) result[Symbol.iterator] = function() { return this; }; return result; } /** @internal */ export class BNode { // If this is an internal node, _keys[i] is the highest key in children[i]. keys: K[]; values: V[]; // True if this node might be within multiple `BTree`s (or have multiple parents). // If so, it must be cloned before being mutated to avoid changing an unrelated tree. // This is transitive: if it's true, children are also shared even if `isShared!=true` // in those children. (Certain operations will propagate isShared=true to children.) isShared: true | undefined; get isLeaf() { return (this as any).children === undefined; } constructor(keys: K[] = [], values?: V[]) { this.keys = keys; this.values = values || undefVals as any[]; this.isShared = undefined; } size(): number { return this.keys.length; } /////////////////////////////////////////////////////////////////////////// // Shared methods ///////////////////////////////////////////////////////// maxKey() { return this.keys[this.keys.length-1]; } // If key not found, returns i^failXor where i is the insertion index. // Callers that don't care whether there was a match will set failXor=0. indexOf(key: K, failXor: number, cmp: (a:K, b:K) => number): index { const keys = this.keys; var lo = 0, hi = keys.length, mid = hi >> 1; while(lo < hi) { var c = cmp(keys[mid], key); if (c < 0) lo = mid + 1; else if (c > 0) // key < keys[mid] hi = mid; else if (c === 0) return mid; else { // c is NaN or otherwise invalid if (key === key) // at least the search key is not NaN return keys.length; else throw new Error("BTree: NaN was used as a key"); } mid = (lo + hi) >> 1; } return mid ^ failXor; // Unrolled version: benchmarks show same speed, not worth using /*var i = 1, c: number = 0, sum = 0; if (keys.length >= 4) { i = 3; if (keys.length >= 8) { i = 7; if (keys.length >= 16) { i = 15; if (keys.length >= 32) { i = 31; if (keys.length >= 64) { i = 127; i += (c = i < keys.length ? cmp(keys[i], key) : 1) < 0 ? 64 : -64; sum += c; i += (c = i < keys.length ? cmp(keys[i], key) : 1) < 0 ? 32 : -32; sum += c; } i += (c = i < keys.length ? cmp(keys[i], key) : 1) < 0 ? 16 : -16; sum += c; } i += (c = i < keys.length ? cmp(keys[i], key) : 1) < 0 ? 8 : -8; sum += c; } i += (c = i < keys.length ? cmp(keys[i], key) : 1) < 0 ? 4 : -4; sum += c; } i += (c = i < keys.length ? cmp(keys[i], key) : 1) < 0 ? 2 : -2; sum += c; } i += (c = i < keys.length ? cmp(keys[i], key) : 1) < 0 ? 1 : -1; c = i < keys.length ? cmp(keys[i], key) : 1; sum += c; if (c < 0) { ++i; c = i < keys.length ? cmp(keys[i], key) : 1; sum += c; } if (sum !== sum) { if (key === key) // at least the search key is not NaN return keys.length ^ failXor; else throw new Error("BTree: NaN was used as a key"); } return c === 0 ? i : i ^ failXor;*/ } ///////////////////////////////////////////////////////////////////////////// // Leaf Node: misc ////////////////////////////////////////////////////////// minKey(): K | undefined { return this.keys[0]; } minPair(reusedArray: [K,V]): [K,V] | undefined { if (this.keys.length === 0) return undefined; reusedArray[0] = this.keys[0]; reusedArray[1] = this.values[0]; return reusedArray; } maxPair(reusedArray: [K,V]): [K,V] | undefined { if (this.keys.length === 0) return undefined; const lastIndex = this.keys.length - 1; reusedArray[0] = this.keys[lastIndex]; reusedArray[1] = this.values[lastIndex]; return reusedArray; } clone(): BNode { var v = this.values; return new BNode(this.keys.slice(0), v === undefVals ? v : v.slice(0)); } greedyClone(force?: boolean): BNode { return this.isShared && !force ? this : this.clone(); } get(key: K, defaultValue: V|undefined, tree: BTree): V|undefined { var i = this.indexOf(key, -1, tree._compare); return i < 0 ? defaultValue : this.values[i]; } getPairOrNextLower(key: K, compare: (a: K, b: K) => number, inclusive: boolean, reusedArray: [K,V]): [K,V]|undefined { var i = this.indexOf(key, -1, compare); const indexOrLower = i < 0 ? ~i - 1 : (inclusive ? i : i - 1); if (indexOrLower >= 0) { reusedArray[0] = this.keys[indexOrLower]; reusedArray[1] = this.values[indexOrLower]; return reusedArray; } return undefined; } getPairOrNextHigher(key: K, compare: (a: K, b: K) => number, inclusive: boolean, reusedArray: [K,V]): [K,V]|undefined { var i = this.indexOf(key, -1, compare); const indexOrLower = i < 0 ? ~i : (inclusive ? i : i + 1); const keys = this.keys; if (indexOrLower < keys.length) { reusedArray[0] = keys[indexOrLower]; reusedArray[1] = this.values[indexOrLower]; return reusedArray; } return undefined; } checkValid(depth: number, tree: BTree, baseIndex: number, checkOrdering: boolean): [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); // Note: we don't check for "node too small" because sometimes a node // can legitimately have size 1. This occurs if there is a batch // deletion, leaving a node of size 1, and the siblings are full so // 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); if (checkOrdering === true) { 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 ////////////////////////////////////////// set(key: K, value: V, overwrite: boolean|undefined, tree: BTree): boolean|BNode { var i = this.indexOf(key, -1, tree._compare); if (i < 0) { // key does not exist yet i = ~i; if (this.keys.length < tree._maxNodeSize) { return this.insertInLeaf(i, key, value, tree); } else { // This leaf node is full and must split var newRightSibling = this.splitOffRightSide(), target: BNode = this; if (i > this.keys.length) { i -= this.keys.length; target = newRightSibling; } target.insertInLeaf(i, key, value, tree); return newRightSibling; } } else { // Key already exists if (overwrite !== false) { if (value !== undefined) this.reifyValues(); // usually this is a no-op, but some users may wish to edit the key this.keys[i] = key; this.values[i] = value; } return false; } } reifyValues() { if (this.values === undefVals) return this.values = this.values.slice(0, this.keys.length); return this.values; } insertInLeaf(i: index, key: K, value: V, tree: BTree) { this.keys.splice(i, 0, key); if (this.values === undefVals) { while (undefVals.length < tree._maxNodeSize) undefVals.push(undefined); if (value === undefined) { return true; } else { this.values = undefVals.slice(0, this.keys.length - 1); } } this.values.splice(i, 0, value); return true; } 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) { // 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 { // Reminder: parent node must update its copy of key for this node var half = this.keys.length >> 1, keys = this.keys.splice(half); var values = this.values === undefVals ? undefVals : this.values.splice(half); return new BNode(keys, values); } ///////////////////////////////////////////////////////////////////////////// // Leaf Node: scanning & deletions ////////////////////////////////////////// forRange(low: K, high: K, includeHigh: boolean|undefined, editMode: boolean, tree: BTree, count: number, onFound?: (k:K, v:V, counter:number) => EditRangeResult|void): EditRangeResult|number { var cmp = tree._compare; var iLow, iHigh; if (high === low) { if (!includeHigh) return count; iHigh = (iLow = this.indexOf(low, -1, cmp)) + 1; if (iLow < 0) return count; } else { iLow = this.indexOf(low, 0, cmp); iHigh = this.indexOf(high, -1, cmp); if (iHigh < 0) iHigh = ~iHigh; else if (includeHigh === true) iHigh++; } var keys = this.keys, values = this.values; if (onFound !== undefined) { for(var i = iLow; i < iHigh; i++) { var key = keys[i]; var result = onFound(key, values[i], count++); if (result !== undefined) { if (editMode === true) { if (key !== keys[i] || this.isShared === true) throw new Error("BTree illegally changed or cloned in editRange"); if (result.delete) { this.keys.splice(i, 1); if (this.values !== undefVals) this.values.splice(i, 1); i--; iHigh--; } else if (result.hasOwnProperty('value')) { values![i] = result.value!; } } if (result.break !== undefined) return result; } } } else count += iHigh - iLow; return count; } /** Adds entire contents of right-hand sibling (rhs is left unchanged) */ mergeSibling(rhs: BNode, _: number) { this.keys.push.apply(this.keys, rhs.keys); if (this.values === undefVals) { if (rhs.values === undefVals) return; this.values = this.values.slice(0, this.keys.length); } this.values.push.apply(this.values, rhs.reifyValues()); } } /** Internal node (non-leaf node) ********************************************/ /** @internal */ export class BNodeInternal extends BNode { // Note: conventionally B+ trees have one fewer key than the number of // 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[], size: number, keys?: K[]) { if (!keys) { keys = []; for (var i = 0; i < children.length; i++) keys[i] = children[i].maxKey(); } 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._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._size, this.keys.slice(0)); for (var i = 0; i < nu.children.length; i++) nu.children[i] = nu.children[i].greedyClone(force); return nu; } minKey() { return this.children[0].minKey(); } minPair(reusedArray: [K,V]): [K,V] | undefined { return this.children[0].minPair(reusedArray); } maxPair(reusedArray: [K,V]): [K,V] | undefined { return this.children[this.children.length - 1].maxPair(reusedArray); } get(key: K, defaultValue: V|undefined, tree: BTree): V|undefined { var i = this.indexOf(key, 0, tree._compare), children = this.children; return i < children.length ? children[i].get(key, defaultValue, tree) : undefined; } getPairOrNextLower(key: K, compare: (a: K, b: K) => number, inclusive: boolean, reusedArray: [K,V]): [K,V]|undefined { var i = this.indexOf(key, 0, compare), children = this.children; if (i >= children.length) return this.maxPair(reusedArray); const result = children[i].getPairOrNextLower(key, compare, inclusive, reusedArray); if (result === undefined && i > 0) { return children[i - 1].maxPair(reusedArray); } return result; } getPairOrNextHigher(key: K, compare: (a: K, b: K) => number, inclusive: boolean, reusedArray: [K,V]): [K,V]|undefined { var i = this.indexOf(key, 0, compare), children = this.children, length = children.length; if (i >= length) return undefined; const result = children[i].getPairOrNextHigher(key, compare, inclusive, reusedArray); if (result === undefined && i < length - 1) { return children[i + 1].minPair(reusedArray); } return result; } checkValid(depth: number, tree: BTree, baseIndex: number, checkOrdering: boolean): [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++) { var child = c[i]; var [subtreeSize, minKey, maxKey] = child.checkValid(depth + 1, tree, baseIndex + size, checkOrdering); 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 && checkOrdering) { 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 === 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, this.minKey()!, this.maxKey()]; } ///////////////////////////////////////////////////////////////////////////// // Internal Node: set & node splitting ////////////////////////////////////// set(key: K, value: V, overwrite: boolean|undefined, tree: BTree): boolean|BNodeInternal { var c = this.children, max = tree._maxNodeSize, cmp = tree._compare; var i = Math.min(this.indexOf(key, 0, cmp), c.length - 1), child = c[i]; if (child.isShared) c[i] = child = child.clone(); if (child.keys.length >= max) { // child is full; inserting anything else will cause a split. // Shifting an item to the left or right sibling may avoid a split. // We can do a shift if the adjacent node is not full and if the // current key can still be placed in the same node after the shift. var other: BNode; if (i > 0 && (other = c[i-1]).keys.length < max && cmp(child.keys[0], key) < 0) { if (other.isShared) c[i-1] = other = other.clone(); other.takeFromRight(child); this.keys[i-1] = other.maxKey(); } else if ((other = c[i+1]) !== undefined && other.keys.length < max && cmp(child.maxKey(), key) < 0) { if (other.isShared) c[i+1] = other = other.clone(); other.takeFromLeft(child); 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(); if (result === true) return true; // The child has split and `result` is a new right child... does it fit? if (this.keys.length < max) { // yes this.insert(i+1, result); return true; } else { // no, we must split also var newRightSibling = this.splitOffRightSide(), target: BNodeInternal = this; if (cmp(result.maxKey(), this.maxKey()) > 0) { target = newRightSibling; i -= this.keys.length; } target.insert(i+1, result); return newRightSibling; } } /** * Inserts `child` at index `i`. * This does not mark `child` as shared, so it is the responsibility of the caller * to ensure that either child is marked shared, or it is not included in another tree. */ insert(i: index, child: BNode) { this.children.splice(i, 0, child); this.keys.splice(i, 0, child.maxKey()); this._size += child.size(); } /** * Split this node. * Modifies this to remove the second half of the items, returning a separate node containing them. */ splitOffRightSide() { // assert !this.isShared; 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()!); 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(child); const size = child.size(); lhsInternal._size -= size; this._size += size; } ///////////////////////////////////////////////////////////////////////////// // Internal Node: scanning & deletions ////////////////////////////////////// // Note: `count` is the next value of the third argument to `onFound`. // A leaf node's `forRange` function returns a new value for this counter, // unless the operation is to stop early. forRange(low: K, high: K, includeHigh: boolean|undefined, editMode: boolean, tree: BTree, count: number, onFound?: (k:K, v:V, counter:number) => EditRangeResult|void): EditRangeResult|number { var cmp = tree._compare; var keys = this.keys, children = this.children; var iLow = this.indexOf(low, 0, cmp), i = iLow; var iHigh = Math.min(high === low ? iLow : this.indexOf(high, 0, cmp), keys.length-1); if (!editMode) { // Simple case for(; i <= iHigh; i++) { var result = children[i].forRange(low, high, includeHigh, editMode, tree, count, onFound); if (typeof result !== 'number') return result; count = result; } } else if (i <= iHigh) { try { for (; i <= iHigh; i++) { 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] = child.maxKey(); this._size += child.size() - beforeSize; if (typeof result !== 'number') return result; count = result; } } finally { // Deletions may have occurred, so look for opportunities to merge nodes. var half = tree._maxNodeSize >> 1; if (iLow > 0) iLow--; for (i = iHigh; i >= iLow; i--) { if (children[i].keys.length <= half) { if (children[i].keys.length !== 0) { this.tryMerge(i, tree._maxNodeSize); } else { // child is empty! delete it! keys.splice(i, 1); const removed = children.splice(i, 1); check(removed[0].size() === 0, "emptiness cleanup"); } } } if (children.length !== 0 && children[0].keys.length === 0) check(false, "emptiness bug"); } } return count; } /** Merges child i with child i+1 if their combined size is not too large */ tryMerge(i: index, maxSize: number): boolean { var children = this.children; if (i >= 0 && i + 1 < children.length) { if (children[i].keys.length + children[i+1].keys.length <= maxSize) { if (children[i].isShared) // cloned already UNLESS i is outside scan range children[i] = children[i].clone(); children[i].mergeSibling(children[i+1], maxSize); children.splice(i + 1, 1); this.keys.splice(i + 1, 1); this.keys[i] = children[i].maxKey(); return true; } } return false; } /** * Move children from `rhs` into this. * `rhs` must be part of this tree, and be removed from it after this call * (otherwise isShared for its children could be incorrect). */ mergeSibling(rhs: BNode, maxNodeSize: number) { // assert !this.isShared; var oldLength = this.keys.length; 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 // parent is not shared, they must now be explicitly marked as shared. for (var i = 0; i < rhsChildren.length; i++) rhsChildren[i].isShared = true; } // If our children are themselves almost empty due to a mass-delete, // they may need to be merged too (but only the oldLength-1 and its // right sibling should need this). this.tryMerge(oldLength-1, maxNodeSize); } } // Optimization: this array of `undefined`s is used instead of a normal // array of values in nodes where `undefined` is the only value. // Its length is extended to max node size on first use; since it can // be shared between trees with different maximums, its length can only // increase, never decrease. Its type should be undefined[] but strangely // TypeScript won't allow the comparison V[] === undefined[]. To prevent // users from making this array too large, BTree has a maximum node size. // // FAQ: undefVals[i] is already undefined, so why increase the array size? // Reading outside the bounds of an array is relatively slow because it // 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 { return cmp(aMin, bMax) <= 0 && cmp(aMax, bMin) >= 0; } const Delete = {delete: true}, DeleteRange = () => Delete; const Break = {break: true}; const EmptyLeaf = (function() { var n = new BNode(); n.isShared = true; return n; })(); const EmptyArray: any[] = []; const ReusedArray: any[] = []; // assumed thread-local /** @internal */ export function check(fact: boolean, ...args: any[]) { if (!fact) { args.unshift('B+ tree'); // at beginning of message throw new Error(args.join(' ')); } } /** A BTree frozen in the empty state. */ export const EmptyBTree = (() => { let t = new BTree(); t.freeze(); return t; })(); ================================================ FILE: benchmarks.ts ================================================ #!/usr/bin/env ts-node import BTree from '.'; import BTreeEx from './extended'; import SortedArray from './sorted-array'; import forEachKeyNotIn from './extended/forEachKeyNotIn'; import subtract from './extended/subtract'; // Note: The `bintrees` package also includes a `BinTree` type which turned // out to be an unbalanced binary tree. It is faster than `RBTree` for // randomized data, but it becomes extremely slow when filled with sorted // data, so it's not usually a good choice. import {RBTree} from 'bintrees'; import { logTreeNodeStats } from './test/shared'; import { performance } from 'perf_hooks'; // node.js only const SortedSet = require("collections/sorted-set"); // Bad type definition: missing 'length' const SortedMap = require("collections/sorted-map"); // No type definitions available const functionalTree = require("functional-red-black-tree"); // No type definitions available class Timer { start = perfNow(); ms() { return ((perfNow() - this.start) * 100 | 0) / 100; } restart() { var ms = this.ms(); this.start += ms; return ms; } } console.log("Benchmark results (milliseconds with integer keys/values)"); console.log("---------------------------------------------------------"); console.log(); console.log("### Insertions at random locations: sorted-btree vs the competition (millisec) ###"); for (let size of [1000, 10000, 100000, 1000000]) { console.log(); var keys = makeArray(size, true); measure(map => `Insert ${map.size} pairs in sorted-btree's BTree`, () => { let map = new BTree(); for (let k of keys) map.set(k, k); return map; }); measure(map => `Insert ${map.size} pairs in sorted-btree's BTree set (no values)`, () => { let map = new BTree(); for (let k of keys) map.set(k, undefined); return map; }); measure(map => `Insert ${map.length} pairs in collections' SortedMap`, () => { let map = new SortedMap(); for (let k of keys) map.set(k, k); return map; }); measure(set => `Insert ${set.length} pairs in collections' SortedSet (no values)`, () => { let set = new SortedSet(); for (let k of keys) set.push(k); return set; }); measure(set => `Insert ${set.length} pairs in functional-red-black-tree`, () => { let set = functionalTree(); for (let k of keys) set = set.insert(k, k); return set; }); measure(set => `Insert ${set.size} pairs in bintrees' RBTree (no values)`, () => { let set = new RBTree((a: any, b: any) => a - b); for (let k of keys) set.insert(k); return set; }); //measure(set => `Insert ${set.size} pairs in bintrees' BinTree (no values)`, () => { // let set = new BinTree((a: any, b: any) => a - b); // for (let k of keys) // set.insert(k); // return set; //}); } console.log(); console.log("### Insert in order, delete: sorted-btree vs the competition ###"); for (let size of [9999, 1000, 10000, 100000, 1000000]) { var log = (size === 9999 ? () => {} : console.log); log(); var keys = makeArray(size, false), i; let btree = measure(tree => `Insert ${tree.size} sorted pairs in B+ tree`, () => { let tree = new BTree(); for (let k of keys) tree.set(k, k * 10); return tree; }, 600, log); let btreeSet = measure(tree => `Insert ${tree.size} sorted keys in B+ tree set (no values)`, () => { let tree = new BTree(); for (let k of keys) tree.set(k, undefined); return tree; }, 600, log); // Another tree for the bulk-delete test let btreeSet2 = btreeSet.greedyClone(); let sMap = measure(map => `Insert ${map.length} sorted pairs in collections' SortedMap`, () => { let map = new SortedMap(); for (let k of keys) map.set(k, k * 10); return map; }, 600, log); let sSet = measure(set => `Insert ${set.length} sorted keys in collections' SortedSet (no values)`, () => { let set = new SortedSet(); for (let k of keys) set.push(k); return set; }, 600, log); let fTree = measure(map => `Insert ${map.length} sorted pairs in functional-red-black-tree`, () => { let map = functionalTree(); for (let k of keys) map = map.insert(k, k * 10); return map; }, 600, log); let rbTree = measure(set => `Insert ${set.size} sorted keys in bintrees' RBTree (no values)`, () => { let set = new RBTree((a: any, b: any) => a - b); for (let k of keys) set.insert(k); return set; }, 600, log); //let binTree = measure(set => `Insert ${set.size} sorted keys in bintrees' BinTree (no values)`, () => { // let set = new BinTree((a: any, b: any) => a - b); // for (let k of keys) // set.insert(k); // return set; //}); // Bug fix: can't use measure() for deletions because the // trees aren't the same on the second iteration var timer = new Timer(); for (i = 0; i < keys.length; i += 2) btree.delete(keys[i]); log(`${timer.restart()}\tDelete every second item in B+ tree`); for (i = 0; i < keys.length; i += 2) btreeSet.delete(keys[i]); log(`${timer.restart()}\tDelete every second item in B+ tree set`); btreeSet2.editRange(btreeSet2.minKey(), btreeSet2.maxKey(), true, (k,v,i) => { if ((i & 1) === 0) return {delete:true}; }); log(`${timer.restart()}\tBulk-delete every second item in B+ tree set`); for (i = 0; i < keys.length; i += 2) sMap.delete(keys[i]); log(`${timer.restart()}\tDelete every second item in collections' SortedMap`); for (i = 0; i < keys.length; i += 2) sSet.delete(keys[i]); log(`${timer.restart()}\tDelete every second item in collections' SortedSet`); for (i = 0; i < keys.length; i += 2) fTree = fTree.remove(keys[i]); log(`${timer.restart()}\tDelete every second item in functional-red-black-tree`); for (i = 0; i < keys.length; i += 2) rbTree.remove(keys[i]); log(`${timer.restart()}\tDelete every second item in bintrees' RBTree`); } console.log(); console.log("### Insertions at random locations: sorted-btree vs Array vs Map ###"); for (let size of [9999, 1000, 10000, 100000, 1000000]) { // Don't print anything in the first iteration (warm up the optimizer) var log = (size === 9999 ? () => {} : console.log); var keys = makeArray(size, true); log(); if (size <= 100000) { measure(list => `Insert ${list.size} pairs in sorted array`, () => { let list = new SortedArray(); for (let k of keys) list.set(k, k); return list; }, 600, log); } else { log(`SLOW!\tInsert ${size} pairs in sorted array`); } measure(tree => `Insert ${tree.size} pairs in B+ tree`, () => { let tree = new BTree(); for (let k of keys) tree.set(k, k); return tree; }, 600, log); measure(map => `Insert ${map.size} pairs in ES6 Map (hashtable)`, () => { let map = new Map(); for (let k of keys) map.set(k, k); return map; }, 600, log); } console.log(); console.log("### Insert in order, scan, delete: sorted-btree vs Array vs Map ###"); for (let size of [1000, 10000, 100000, 1000000]) { console.log(); var keys = makeArray(size, false), i; var list = measure(list => `Insert ${list.size} sorted pairs in array`, () => { let list = new SortedArray(); for (let k of keys) list.set(k, k * 10); return list; }); let tree = measure(tree => `Insert ${tree.size} sorted pairs in B+ tree`, () => { let tree = new BTree(); for (let k of keys) tree.set(k, k * 10); return tree; }); let map = measure(map => `Insert ${map.size} sorted pairs in Map hashtable`, () => { let map = new Map(); for (let k of keys) map.set(k, k * 10); return map; }); measure(sum => `Sum of all values with forEach in sorted array: ${sum}`, () => { var sum = 0; list.getArray().forEach(pair => sum += pair[1]); return sum; }); measure(sum => `Sum of all values with forEachPair in B+ tree: ${sum}`, () => { var sum = 0; tree.forEachPair((k, v) => sum += v); return sum; }); measure(sum => `Sum of all values with forEach in B+ tree: ${sum}`, () => { var sum = 0; tree.forEach(v => sum += v); return sum; }); measure(sum => `Sum of all values with iterator in B+ tree: ${sum}`, () => { var sum = 0; // entries() (instead of values()) with reused pair should be fastest // (not using for-of because tsc is in ES5 mode w/o --downlevelIteration) for (var it = tree.entries(undefined, []), next = it.next(); !next.done; next = it.next()) sum += next.value[1]; return sum; }); measure(sum => `Sum of all values with forEach in Map: ${sum}`, () => { var sum = 0; map.forEach(v => sum += v); return sum; }); if (keys.length <= 100000) { measure(() => `Delete every second item in sorted array`, () => { for (i = keys.length-1; i >= 0; i -= 2) list.delete(keys[i]); }); } else console.log(`SLOW!\tDelete every second item in sorted array`); measure(() => `Delete every second item in B+ tree`, () => { for (i = keys.length-1; i >= 0; i -= 2) tree.delete(keys[i]); }); measure(() => `Delete every second item in Map hashtable`, () => { for (i = keys.length-1; i >= 0; i -= 2) map.delete(keys[i]); }); } console.log(); console.log("### How max node size affects performance ###"); { console.log(); var keys = makeArray(100000, true); var timer = new Timer(); for (let nodeSize = 10; nodeSize <= 80; nodeSize += 2) { let tree = new BTree([], undefined, nodeSize); for (let i = 0; i < keys.length; i++) tree.set(keys[i], keys[i] + 1); console.log(`${timer.restart()}\tInsert ${tree.size} keys in B+tree with node size ${tree.maxNodeSize}`); } } console.log(); console.log("### BTree.diffAgainst()"); { console.log(); const sizes = [100, 1000, 10000, 100000, 1000000]; sizes.forEach((size, i) => { const tree = fillBTreeOfSize(size); sizes.slice(0, i).forEach(otherSize => { const otherTree = fillBTreeOfSize(otherSize); measure(() => `BTree.diffAgainst ${size} pairs vs ${otherSize} pairs`, () => { tree.diffAgainst(otherTree, inTree => {}, inOther => {}); }); }); }); console.log(); sizes.forEach((size, i) => { sizes.forEach(otherSize => { const keys = makeArray(size + otherSize, true); const tree = new BTreeEx(); for (let k of keys.slice(0, size)) tree.set(k, k * 2); const otherTree = tree.clone(); for (let k of keys.slice(size)) tree.set(k, k * 2); measure(() => `BTree.diffAgainst ${size} pairs vs cloned copy with ${otherSize} extra pairs`, () => { tree.diffAgainst(otherTree, inTree => {}, inOther => {}); }); }); }); } console.log(); console.log("### Accelerated union of B+ trees"); { console.log(); const sizes = [100, 1000, 10000, 100000]; const preferLeftUnion = (_k: number, leftValue: any, _rightValue: any) => leftValue; const measureUnionVsBaseline = ( baseTitle: string, tree1: BTreeEx, tree2: BTreeEx, includeBaseline = true, prefer = preferLeftUnion, ) => { const unionResult = measure(() => `union(): ${baseTitle}`, () => { return tree1.union(tree2, prefer); }); logTreeNodeStats('union(): ', unionResult); if (includeBaseline) { const baselineResult = measure(() => `baseline: ${baseTitle}`, () => { const result = tree1.clone(); tree2.forEachPair((k, v) => { result.set(k, v, false); }); return result; }); logTreeNodeStats('baseline:', baselineResult); } }; testNonOverlappingRanges('Union', sizes, measureUnionVsBaseline); testMaybeOverlappingRanges('Union', sizes, 1, (txt, t1, t2) => measureUnionVsBaseline(txt, t1, t2, false), "Adjacent ranges (one intersection point)"); console.log(); console.log("#### Interleaved ranges (two intersection points)"); sizes.forEach((size) => { const tree1 = new BTreeEx(); const tree2 = new BTreeEx(); // Tree1: 0 to size, 2*size to 3*size // Tree2: size to 2*size for (let i = 0; i <= size; i++) { tree1.set(i, i); tree1.set(i + 2 * size, i + 2 * size); tree2.set(i + size, i + size); } measureUnionVsBaseline(`Union ${size * 2}+${size} interleaved range trees`, tree1, tree2, false); }); testCompleteOverlap('Union trees', sizes, measureUnionVsBaseline, 'Complete overlap (all keys intersect)'); testPercentOverlap('Union trees', sizes, 10, measureUnionVsBaseline); testRandomOverlaps('Union', sizes, (t, t1, t2) => measureUnionVsBaseline(t, t1, t2, false), "Union random overlaps"); console.log(); console.log("#### Union with empty tree"); [100000].forEach((size) => { const tree1 = fillBTreeOfSize(size); const tree2 = new BTreeEx(); const baseTitle = `Union ${size}-key tree with empty tree`; measureUnionVsBaseline(baseTitle, tree1, tree2); }); testLargeSparseOverlap('Union', measureUnionVsBaseline); } console.log(); console.log("### Subtraction of B+ trees"); { console.log(); const sizes = [100, 1000, 10000, 100000]; const measureSubtractVsBaseline = ( baseTitle: string, includeTree: BTreeEx, excludeTree: BTreeEx, ) => { const subtractResult = measure(() => `subtract: ${baseTitle}`, () => { return subtract, number, number>(includeTree, excludeTree); }); logTreeNodeStats('subtract:', subtractResult); // Baseline const baselineResult = measure(() => `baseline: ${baseTitle}`, () => { const result = includeTree.clone(); excludeTree.forEachPair((key) => { result.delete(key); }); return result; }); logTreeNodeStats('baseline:', baselineResult); }; testNonOverlappingRanges('Subtract', sizes, measureSubtractVsBaseline, "Non-overlapping ranges (nothing removed)"); testPartialMiddleOverlap('Subtract', sizes, measureSubtractVsBaseline, "Partial overlap (middle segment removed)"); console.log(); console.log("#### Interleaved keys (every other key removed)"); sizes.forEach((size) => { const includeTree = fillBTreeOfSize(size * 2, 0, 1); const excludeTree = new BTreeEx(); for (let i = 0; i < size * 2; i += 2) excludeTree.set(i, i); const baseTitle = `Subtract ${includeTree.size}-${excludeTree.size} interleaved trees`; measureSubtractVsBaseline(baseTitle, includeTree, excludeTree); }); testCompleteOverlap('Subtract', sizes, measureSubtractVsBaseline, "Complete overlap (entire tree removed)"); console.log(); console.log("#### Random overlaps (~10% removed)"); sizes.forEach((size) => { const keysInclude = makeArray(size, true); const keysExclude = makeArray(size, true); const overlapCount = Math.max(1, Math.floor(size * 0.1)); for (let i = 0; i < overlapCount && i < keysInclude.length && i < keysExclude.length; i++) { keysExclude[i] = keysInclude[i]; } const includeTree = new BTreeEx(); const excludeTree = new BTreeEx(); for (const key of keysInclude) includeTree.set(key, key * 3); for (const key of keysExclude) excludeTree.set(key, key * 7); const baseTitle = `Subtract ${includeTree.size}-${excludeTree.size} random trees`; measureSubtractVsBaseline(baseTitle, includeTree, excludeTree); }); console.log(); console.log("#### Subtract empty tree"); sizes.forEach((size) => { const includeTree = fillBTreeOfSize(size, 0, 1, 1); const excludeTree = new BTreeEx(); measureSubtractVsBaseline(`Subtract ${includeTree.size}-0 keys`, includeTree, excludeTree); }); testLargeSparseOverlap('Subtract', measureSubtractVsBaseline, "Large sparse-overlap trees (1M keys each, 10 overlaps per 100k)"); } console.log(); console.log("### Intersection between B+ trees"); { console.log(); const sizes = [100, 1000, 10000, 100000]; const preferLeftIntersection = (_k: number, leftValue: number, _rightValue: number) => leftValue; const measureIntersectVsBaseline = ( baseTitle: string, tree1: BTreeEx, tree2: BTreeEx, combine = preferLeftIntersection, ) => { const intersectResult = measure(() => `intersect: ${baseTitle}`, () => { return tree1.intersect(tree2, combine); }); logTreeNodeStats('intersect:', intersectResult); // Baseline const baselineResult = measure(() => `baseline: ${baseTitle}`, () => { const result = new BTreeEx(undefined, tree1._compare, tree1._maxNodeSize); intersectBySorting(tree1, tree2, (key, leftValue, rightValue) => { const mergedValue = combine(key, leftValue, rightValue); result.set(key, mergedValue); }); return result; }); logTreeNodeStats('baseline: ', baselineResult); }; testNonOverlappingRanges('Intersect', sizes, measureIntersectVsBaseline); testPartialMiddleOverlap('Intersect', sizes, measureIntersectVsBaseline, "Partial overlap (middle segment shared)"); console.log(); console.log("#### Interleaved keys (every other key shared)"); sizes.forEach((size) => { const tree1 = new BTreeEx(); const tree2 = new BTreeEx(); for (let i = 0; i < size * 2; i++) { tree1.set(i, i); if (i % 2 === 0) tree2.set(i, i * 3); } measureIntersectVsBaseline(`Intersect ${tree1.size}+${tree2.size} interleaved trees`, tree1, tree2); }); console.log(); console.log("#### Complete overlap (all keys shared)"); sizes.forEach((size) => { const tree1 = fillBTreeOfSize(size, 0, 1, 1); const tree2 = fillBTreeOfSize(size, 0, 1, 5); measureIntersectVsBaseline(`Intersect ${tree1.size}+${tree2.size} identical trees`, tree1, tree2); }); testRandomOverlaps('Intersect', sizes, measureIntersectVsBaseline); console.log(); console.log("#### Intersection with empty tree"); sizes.forEach((size) => { const tree1 = fillBTreeOfSize(size, 0, 1, 1); const tree2 = new BTreeEx(); measureIntersectVsBaseline(`Intersect ${tree1.size}-key tree with empty tree`, tree1, tree2); }); testLargeSparseOverlap('Intersect', measureIntersectVsBaseline); } console.log(); console.log("### forEachKeyInBoth"); { const sizes = [100, 1000, 10000, 100000]; const timeForEachKeyInBothVsBaseline = ( baseTitle: string, tree1: BTreeEx, tree2: BTreeEx, forEachKeyInBothLabel = 'forEachKeyInBoth()', ) => { measure<{count: number, checksum: number }>( result => `forEachKeyInBoth: [count=${result.count}, checksum=${result.checksum}]`, function runForEachKeyInBoth() { let count = 0; let checksum = 0; tree1.forEachKeyInBoth(tree2, (_k, leftValue, rightValue) => { count++; checksum += leftValue + rightValue; }); return { count, checksum }; }); measure<{count: number, checksum: number }>( result => `Baseline method: [count=${result.count}, checksum=${result.checksum}]`, function runBaseline() { let count = 0; let checksum = 0; intersectBySorting(tree1, tree2, (_k, leftValue, rightValue) => { count++; checksum += leftValue + rightValue; }); return { count, checksum }; }); }; testNonOverlappingRanges('forEachKeyInBoth', sizes, timeForEachKeyInBothVsBaseline); test50PercentOverlappingRanges('forEachKeyInBoth', sizes, timeForEachKeyInBothVsBaseline); testCompleteOverlap('forEachKeyInBoth', sizes, timeForEachKeyInBothVsBaseline); testRandomOverlaps('forEachKeyInBoth', sizes, timeForEachKeyInBothVsBaseline); testLargeSparseOverlap('forEachKeyInBoth', timeForEachKeyInBothVsBaseline); } console.log(); console.log("### forEachKeyNotIn"); { const sizes = [100, 1000, 10000, 100000]; const measureForEachKeyNotInVsBaseline = ( baseTitle: string, includeTree: BTreeEx, excludeTree: BTreeEx, ) => { measure<{count: number, checksum: number }>( result => `forEachKeyNotIn: [count=${result.count}, checksum=${result.checksum}]`, function runForEachKeyNotIn() { let count = 0; let checksum = 0; forEachKeyNotIn(includeTree, excludeTree, (_key, value) => { count++; checksum += value; }); return { count, checksum }; }); measure<{count: number, checksum: number }>( result => `baseline method: [count=${result.count}, checksum=${result.checksum}]`, function runBaseline() { let count = 0; let checksum = 0; subtractBySorting(includeTree, excludeTree, (_key, value) => { count++; checksum += value; }); return { count, checksum }; }); }; testNonOverlappingRanges('forEachKeyNotIn', sizes, measureForEachKeyNotInVsBaseline, "Non-overlapping ranges (all keys survive)"); test50PercentOverlappingRanges('forEachKeyNotIn', sizes, measureForEachKeyNotInVsBaseline); testCompleteOverlap('forEachKeyNotIn', sizes, measureForEachKeyNotInVsBaseline, "Complete overlap (no keys survive)"); testRandomOverlaps('forEachKeyNotIn', sizes, measureForEachKeyNotInVsBaseline, "Random overlaps (~10% of include removed)"); testLargeSparseOverlap('forEachKeyNotIn', measureForEachKeyNotInVsBaseline); } //////////////////////////////////////////////////////////////////////////////////////////////////// //MARK: Shared test patterns function fillBTreeOfSize(size: number, first = 0, spacing?: number, valueMult = 2, randomOrder = false) { const tree = new BTreeEx(); for (let k of makeArray(size, randomOrder, first, spacing)) tree.set(k, k * valueMult); return tree; } type TwoTreeBenchmark = (baseTitle: string, tree1: BTreeEx, tree2: BTreeEx) => void; function testNonOverlappingRanges( labelPrefix: string, sizes: number[], run: TwoTreeBenchmark, heading = "Non-overlapping ranges (no shared keys)" ) { return testMaybeOverlappingRanges(labelPrefix, sizes, -100, run, heading); } function testMaybeOverlappingRanges( labelPrefix: string, sizes: number[], overlap: number, run: TwoTreeBenchmark, heading: string, ) { console.log(); console.log('#### ' + heading); sizes.forEach((size) => { const tree1 = fillBTreeOfSize(size, 0, 1, 1); const tree2 = fillBTreeOfSize(size, size - overlap, 1, 1); console.assert(tree1.minKey() === 0 && tree1.maxKey() === size - 1); console.assert(tree2.minKey() === size - overlap && tree2.maxKey() === size - overlap + size - 1); const descr = overlap > 0 ? `trees with ${overlap} keys overlaping` : `disjoint trees`; const baseTitle = `${labelPrefix} ${size}+${size} ${descr}`; run(baseTitle, tree1, tree2); }); } function test50PercentOverlappingRanges( labelPrefix: string, sizes: number[], run: TwoTreeBenchmark, heading: string = "50% overlapping ranges", ) { console.log(); console.log('#### ' + heading); sizes.forEach((size) => { const tree1 = fillBTreeOfSize(size, 0, 1, 1); const tree2 = fillBTreeOfSize(size, Math.floor(size / 2), 1, 2); const baseTitle = `${labelPrefix} ${tree1.size}+${tree2.size} half-overlapping trees`; run(baseTitle, tree1, tree2); }); } function testCompleteOverlap( labelPrefix: string, sizes: number[], run: TwoTreeBenchmark, heading: string = "Complete overlap (all keys shared)", ) { console.log(); console.log('#### ' + heading); sizes.forEach((size) => { const tree1 = fillBTreeOfSize(size, 0, 1, 1); const tree2 = fillBTreeOfSize(size, 0, 1, 3); console.assert(tree1.minKey() === tree2.minKey() && tree1.maxKey() === tree2.maxKey()); const baseTitle = `${labelPrefix} ${tree1.size}+${tree2.size} identical-key trees`; run(baseTitle, tree1, tree2); }); } function testPercentOverlap( labelPrefix: string, sizes: number[], percent: number, run: TwoTreeBenchmark, heading?: string, ) { console.log(); console.log('#### ' + (heading ?? `${percent}% overlap`)); sizes.forEach((size) => { const tree1 = fillBTreeOfSize(size, 0, 1, 1); const tree2 = fillBTreeOfSize(size, Math.floor(size * (1 - percent/100)), 1, 2); const baseTitle = `${labelPrefix} with ${percent}% overlap (${size}+${size} keys)`; run(baseTitle, tree1, tree2); }); } function testPartialMiddleOverlap( labelPrefix: string, sizes: number[], run: TwoTreeBenchmark, heading: string = "Partial overlap (middle segment)", ) { console.log(); console.log('#### ' + heading); sizes.forEach((size) => { const tree1 = fillBTreeOfSize(size, 0, 1, 1); const tree2 = fillBTreeOfSize(Math.floor(size / 2), Math.floor(size / 3), 1, 10); const baseTitle = `${labelPrefix} ${tree1.size}+${tree2.size} partially overlapping trees`; run(baseTitle, tree1, tree2); }); } function testRandomOverlaps( labelPrefix: string, sizes: number[], run: TwoTreeBenchmark, heading: string = "Random overlaps (~10% shared keys)", ) { console.log(); console.log('#### ' + heading); sizes.forEach((size) => { const keys1 = makeArray(size, true); const keys2 = makeArray(size, true); const overlapCount = Math.max(1, Math.floor(size * 0.1)); for (let i = 0; i < overlapCount && i < keys1.length && i < keys2.length; i++) { keys2[i] = keys1[i]; } const tree1 = new BTreeEx(); const tree2 = new BTreeEx(); for (let i = 0; i < keys1.length; i++) { const key = keys1[i]; tree1.set(key, key * 5); } for (let i = 0; i < keys2.length; i++) { const key = keys2[i]; tree2.set(key, key * 7); } const baseTitle = `${labelPrefix} ${tree1.size}+${tree2.size} random trees`; run(baseTitle, tree1, tree2); }); } function testLargeSparseOverlap( labelPrefix: string, run: TwoTreeBenchmark, heading: string = "Large sparse-overlap trees (1M keys each, 10 overlaps per 100k)", ) { console.log(); console.log('#### ' + heading); const totalKeys = 1_000_000; const overlapInterval = 100_000; const overlapPerInterval = 10; const tree1 = new BTreeEx(); for (let i = 0; i < totalKeys; i++) { tree1.set(i, i); } const tree2 = new BTreeEx(); for (let i = 0; i < totalKeys; i++) { if ((i % overlapInterval) < overlapPerInterval) { tree2.set(i, i * 7); } else { tree2.set(totalKeys + i, (totalKeys + i) * 7); } } const baseTitle = `${labelPrefix} ${tree1.size}+${tree2.size} sparse-overlap trees`; run(baseTitle, tree1, tree2); } //////////////////////////////////////////////////////////////////////////////////////////////////// //MARK: Baseline algorithms /** calls `callback` for each key and pair of values that is in both `tree1` and `tree2` (O(n)) */ function intersectBySorting( tree1: BTree, tree2: BTree, callback: (k: number, leftValue: number, rightValue: number) => void ) { const left = tree1.toArray(); const right = tree2.toArray(); let i = 0; let j = 0; const leftLen = left.length; const rightLen = right.length; while (i < leftLen && j < rightLen) { const [leftKey, leftValue] = left[i]; const [rightKey, rightValue] = right[j]; if (leftKey === rightKey) { callback(leftKey, leftValue, rightValue); i++; j++; } else if (leftKey < rightKey) { i++; } else { j++; } } } /** calls `callback` for each key and value that is in `tree1` but not `tree2` (O(n)) */ function subtractBySorting( includeTree: BTree, excludeTree: BTree, callback: (k: number, value: number) => void ) { const include = includeTree.toArray(); const exclude = excludeTree.toArray(); let i = 0; let j = 0; const includeLen = include.length; const excludeLen = exclude.length; while (i < includeLen) { const [includeKey, includeValue] = include[i]; while (j < excludeLen && exclude[j][0] < includeKey) j++; if (j < excludeLen && exclude[j][0] === includeKey) { i++; continue; } callback(includeKey, includeValue); i++; } } //////////////////////////////////////////////////////////////////////////////////////////////////// //MARK: Core functionality function perfNow(): number { return performance.now(); } function randInt(max: number) { return Math.random() * max | 0; } function swap(keys: any[], i: number, j: number) { var tmp = keys[i]; keys[i] = keys[j]; keys[j] = tmp; } /** Returns an array of numbers. * * @param size Array size * @param randomOrder Whether to randomize the order after constructing the array * @param spacing Max amount by which each number is bigger than the previous one (1 = no gaps) * @param lowest Lowest value in the array */ function makeArray(size: number, randomOrder: boolean, lowest = 0, spacing = 10) { var keys: number[] = [], i, n; for (i = 0, n = lowest; i < size; i++, n += 1 + randInt(spacing)) keys[i] = n; if (randomOrder) for (i = 0; i < size; i++) swap(keys, i, randInt(size)); return keys; } // Benchmark harness helper. // Runs the callback up to 6 times, using the first run as a warmup if the first run takes less // than `approxMillisec`. If multiple runs happen, the warmup run is excluded from measurement. function measure( message: (t:T) => string, callback: () => T, approxMillisec: number = 600, log = console.log ) { const timer = new Timer(); let result = callback(); let runCount = 1; const warmupEndMs = timer.ms(); for (; runCount < 10 && timer.ms() < approxMillisec; runCount++) callback(); let endMs = timer.ms(), measuredMs = endMs, measuredRuns = runCount; if (runCount > 1) { measuredMs = endMs - warmupEndMs; measuredRuns = runCount - 1; } const avgMs = measuredMs / measuredRuns; log((Math.round(avgMs * 100) / 100) + "\t" + message(result)); return result; } ================================================ FILE: extended/bulkLoad.d.ts ================================================ import BTree from '../b+tree'; /** * Loads a B-Tree from a sorted list of entries in bulk. This is faster than inserting * entries one at a time, and produces a more optimally balanced tree. * Time and space complexity: O(n). * @param keys Keys to load, sorted in strictly ascending order. * @param values Values corresponding to each key. * @param maxNodeSize The branching factor (maximum node size) for the resulting tree. * @param compare Function to compare keys. * @param loadFactor Desired load factor for created leaves. Must be between 0.5 and 1.0. * @returns A new BTree containing the given entries. * @throws Error if the entries are not sorted by key in strictly ascending order (duplicates disallowed) or if the load factor is out of the allowed range. */ export declare function bulkLoad(keys: K[], values: V[], maxNodeSize: number, compare: (a: K, b: K) => number, loadFactor?: number): BTree; ================================================ FILE: extended/bulkLoad.js ================================================ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.bulkLoadRoot = exports.bulkLoad = void 0; var b_tree_1 = __importStar(require("../b+tree")); var shared_1 = require("./shared"); /** * Loads a B-Tree from a sorted list of entries in bulk. This is faster than inserting * entries one at a time, and produces a more optimally balanced tree. * Time and space complexity: O(n). * @param keys Keys to load, sorted in strictly ascending order. * @param values Values corresponding to each key. * @param maxNodeSize The branching factor (maximum node size) for the resulting tree. * @param compare Function to compare keys. * @param loadFactor Desired load factor for created leaves. Must be between 0.5 and 1.0. * @returns A new BTree containing the given entries. * @throws Error if the entries are not sorted by key in strictly ascending order (duplicates disallowed) or if the load factor is out of the allowed range. */ function bulkLoad(keys, values, maxNodeSize, compare, loadFactor) { if (loadFactor === void 0) { loadFactor = 0.8; } var root = bulkLoadRoot(keys, values, maxNodeSize, compare, loadFactor); var tree = new b_tree_1.default(undefined, compare, maxNodeSize); var target = tree; target._root = root; return tree; } exports.bulkLoad = bulkLoad; /** * Bulk loads, returns the root node of the resulting tree. * @internal */ function bulkLoadRoot(keys, values, maxNodeSize, compare, loadFactor) { if (loadFactor === void 0) { loadFactor = 0.8; } if (loadFactor < 0.5 || loadFactor > 1.0) throw new Error("bulkLoad: loadFactor must be between 0.5 and 1.0"); if (keys.length !== values.length) throw new Error("bulkLoad: keys and values arrays must be the same length"); maxNodeSize = (0, b_tree_1.fixMaxSize)(maxNodeSize); // Verify keys are sorted var totalPairs = keys.length; if (totalPairs > 1) { var previousKey = keys[0]; for (var i = 1; i < totalPairs; i++) { var key = keys[i]; if (compare(previousKey, key) >= 0) throw new Error("bulkLoad: keys must be sorted in strictly ascending order"); previousKey = key; } } // Get ALL the leaf nodes with which the tree will be populated var currentNodes = []; (0, shared_1.makeLeavesFrom)(keys, values, maxNodeSize, loadFactor, currentNodes.push.bind(currentNodes)); if (currentNodes.length === 0) return new b_tree_1.BNode(); var targetNodeSize = Math.ceil(maxNodeSize * loadFactor); var isExactlyHalf = targetNodeSize === maxNodeSize / 2; var minSize = Math.floor(maxNodeSize / 2); for (var nextLevel = void 0; currentNodes.length > 1; currentNodes = nextLevel) { var nodeCount = currentNodes.length; if (nodeCount <= maxNodeSize && (nodeCount !== maxNodeSize || !isExactlyHalf)) { currentNodes = [new b_tree_1.BNodeInternal(currentNodes, (0, b_tree_1.sumChildSizes)(currentNodes))]; break; } var nextLevelCount = Math.ceil(nodeCount / targetNodeSize); (0, b_tree_1.check)(nextLevelCount > 1); nextLevel = new Array(nextLevelCount); var remainingNodes = nodeCount; var remainingParents = nextLevelCount; var childIndex = 0; for (var i = 0; i < nextLevelCount; i++) { var chunkSize = Math.ceil(remainingNodes / remainingParents); var children = new Array(chunkSize); var size = 0; for (var j = 0; j < chunkSize; j++) { var child = currentNodes[childIndex++]; children[j] = child; size += child.size(); } remainingNodes -= chunkSize; remainingParents--; nextLevel[i] = new b_tree_1.BNodeInternal(children, size); } // If last node is underfilled, balance with left sibling var secondLastNode = nextLevel[nextLevelCount - 2]; var lastNode = nextLevel[nextLevelCount - 1]; while (lastNode.children.length < minSize) lastNode.takeFromLeft(secondLastNode); } return currentNodes[0]; } exports.bulkLoadRoot = bulkLoadRoot; ================================================ FILE: extended/bulkLoad.ts ================================================ import BTree, { BNode, BNodeInternal, check, fixMaxSize, sumChildSizes } from '../b+tree'; import { makeLeavesFrom as makeAllLeafNodes, type BTreeWithInternals } from './shared'; /** * Loads a B-Tree from a sorted list of entries in bulk. This is faster than inserting * entries one at a time, and produces a more optimally balanced tree. * Time and space complexity: O(n). * @param keys Keys to load, sorted in strictly ascending order. * @param values Values corresponding to each key. * @param maxNodeSize The branching factor (maximum node size) for the resulting tree. * @param compare Function to compare keys. * @param loadFactor Desired load factor for created leaves. Must be between 0.5 and 1.0. * @returns A new BTree containing the given entries. * @throws Error if the entries are not sorted by key in strictly ascending order (duplicates disallowed) or if the load factor is out of the allowed range. */ export function bulkLoad( keys: K[], values: V[], maxNodeSize: number, compare: (a: K, b: K) => number, loadFactor = 0.8 ): BTree { const root = bulkLoadRoot(keys, values, maxNodeSize, compare, loadFactor); const tree = new BTree(undefined, compare, maxNodeSize); const target = tree as unknown as BTreeWithInternals; target._root = root; return tree; } /** * Bulk loads, returns the root node of the resulting tree. * @internal */ export function bulkLoadRoot( keys: K[], values: V[], maxNodeSize: number, compare: (a: K, b: K) => number, loadFactor = 0.8 ): BNode { if (loadFactor < 0.5 || loadFactor > 1.0) throw new Error("bulkLoad: loadFactor must be between 0.5 and 1.0"); if (keys.length !== values.length) throw new Error("bulkLoad: keys and values arrays must be the same length"); maxNodeSize = fixMaxSize(maxNodeSize); // Verify keys are sorted const totalPairs = keys.length; if (totalPairs > 1) { let previousKey = keys[0]; for (let i = 1; i < totalPairs; i++) { const key = keys[i]; if (compare(previousKey, key) >= 0) throw new Error("bulkLoad: keys must be sorted in strictly ascending order"); previousKey = key; } } // Get ALL the leaf nodes with which the tree will be populated let currentNodes: BNode[] = []; makeAllLeafNodes(keys, values, maxNodeSize, loadFactor, currentNodes.push.bind(currentNodes)); if (currentNodes.length === 0) return new BNode(); const targetNodeSize = Math.ceil(maxNodeSize * loadFactor); const isExactlyHalf = targetNodeSize === maxNodeSize / 2; const minSize = Math.floor(maxNodeSize / 2); for (let nextLevel; currentNodes.length > 1; currentNodes = nextLevel) { const nodeCount = currentNodes.length; if (nodeCount <= maxNodeSize && (nodeCount !== maxNodeSize || !isExactlyHalf)) { currentNodes = [new BNodeInternal(currentNodes, sumChildSizes(currentNodes))]; break; } const nextLevelCount = Math.ceil(nodeCount / targetNodeSize); check(nextLevelCount > 1); nextLevel = new Array>(nextLevelCount); let remainingNodes = nodeCount; let remainingParents = nextLevelCount; let childIndex = 0; for (let i = 0; i < nextLevelCount; i++) { const chunkSize = Math.ceil(remainingNodes / remainingParents); const children = new Array>(chunkSize); let size = 0; for (let j = 0; j < chunkSize; j++) { const child = currentNodes[childIndex++]; children[j] = child; size += child.size(); } remainingNodes -= chunkSize; remainingParents--; nextLevel[i] = new BNodeInternal(children, size); } // If last node is underfilled, balance with left sibling const secondLastNode = nextLevel[nextLevelCount - 2] as BNodeInternal; const lastNode = nextLevel[nextLevelCount - 1] as BNodeInternal; while (lastNode.children.length < minSize) lastNode.takeFromLeft(secondLastNode); } return currentNodes[0]; } ================================================ FILE: extended/decompose.d.ts ================================================ export {}; ================================================ FILE: extended/decompose.js ================================================ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.buildFromDecomposition = exports.decompose = void 0; var b_tree_1 = require("../b+tree"); var shared_1 = require("./shared"); var parallelWalk_1 = require("./parallelWalk"); var decomposeLoadFactor = 0.7; /** * Decomposes two trees into disjoint nodes. Reuses interior nodes when they do not overlap/intersect with any leaf nodes * in the other tree. Overlapping leaf nodes are broken down into new leaf nodes containing merged entries. * The algorithm is a parallel tree walk using two cursors. The trailing cursor (behind in key space) is walked forward * until it is at or after the leading cursor. As it does this, any whole nodes or subtrees it passes are guaranteed to * be disjoint. This is true because the leading cursor was also previously walked in this way, and is thus pointing to * the first key at or after the trailing cursor's previous position. * The cursor walk is efficient, meaning it skips over disjoint subtrees entirely rather than visiting every leaf. * Note: some of the returned leaves may be underfilled. * @internal */ function decompose(left, right, combineFn, ignoreRight) { if (ignoreRight === void 0) { ignoreRight = false; } var maxNodeSize = left._maxNodeSize; var cmp = left._compare; (0, b_tree_1.check)(left._root.size() > 0 && right._root.size() > 0, "decompose requires non-empty inputs"); // Holds the disjoint nodes that result from decomposition. // Stored as parallel arrays of (height, node) to avoid creating many tiny tuples var disjointHeights = []; var disjointNodes = []; // During the decomposition, leaves that are not disjoint are decomposed into individual entries // that accumulate in this array in sorted order. They are flushed into leaf nodes whenever a reused // disjoint subtree is added to the disjoint set. // Note that there are unavoidable cases in which this will generate underfilled leaves. // An example of this would be a leaf in one tree that contained keys [0, 100, 101, 102]. // In the other tree, there is a leaf that contains [2, 3, 4, 5]. This leaf can be reused entirely, // but the first tree's leaf must be decomposed into [0] and [100, 101, 102] var pendingKeys = []; var pendingValues = []; var tallestIndex = -1, tallestHeight = -1; // During the upward part of the cursor walk, this holds the highest disjoint node seen so far. // This is done because we cannot know immediately whether we can add the node to the disjoint set // because its ancestor may also be disjoint and should be reused instead. var highestDisjoint = undefined; var minSize = Math.floor(maxNodeSize / 2); var onLeafCreation = function (leaf) { var height = leaf.keys.length < minSize ? -1 : 0; disjointHeights.push(height); disjointNodes.push(leaf); }; var addSharedNodeToDisjointSet = function (node, height) { // flush pending entries (0, shared_1.makeLeavesFrom)(pendingKeys, pendingValues, maxNodeSize, decomposeLoadFactor, onLeafCreation); pendingKeys.length = 0; pendingValues.length = 0; // Don't share underfilled leaves, instead mark them as needing merging if (node.isLeaf && node.keys.length < minSize) { disjointHeights.push(-1); disjointNodes.push(node.clone()); } else { node.isShared = true; disjointHeights.push(height); disjointNodes.push(node); } if (height > tallestHeight) { tallestIndex = disjointHeights.length - 1; tallestHeight = height; } }; var addHighestDisjoint = function () { if (highestDisjoint !== undefined) { addSharedNodeToDisjointSet(highestDisjoint.node, highestDisjoint.height); highestDisjoint = undefined; } }; // Mark all nodes at or above depthFrom in the cursor spine as disqualified (non-disjoint) var disqualifySpine = function (cursor, depthFrom) { var spine = cursor.spine; for (var i = depthFrom; i >= 0; --i) { var payload = spine[i].payload; // Safe to early out because we always disqualify all ancestors of a disqualified node // That is correct because every ancestor of a non-disjoint node is also non-disjoint // because it must enclose the non-disjoint range. if (payload.disqualified) break; payload.disqualified = true; } }; // Cursor payload factory var makePayload = function () { return ({ disqualified: false }); }; var pushLeafRange = function (leaf, from, toExclusive) { var keys = leaf.keys; var values = leaf.values; for (var i = from; i < toExclusive; ++i) { pendingKeys.push(keys[i]); pendingValues.push(values[i]); } }; var onMoveInLeaf = function (leaf, payload, fromIndex, toIndex, startedEqual) { (0, b_tree_1.check)(payload.disqualified === true, "onMoveInLeaf: leaf must be disqualified"); var start = startedEqual ? fromIndex + 1 : fromIndex; if (start < toIndex) pushLeafRange(leaf, start, toIndex); }; var onExitLeaf = function (leaf, payload, startingIndex, startedEqual, cursorThis) { highestDisjoint = undefined; if (!payload.disqualified) { highestDisjoint = { node: leaf, height: 0 }; if (cursorThis.spine.length === 0) { // if we are exiting a leaf and there are no internal nodes, we will reach the end of the tree. // In this case we need to add the leaf now because step up will not be called. addHighestDisjoint(); } } else { var start = startedEqual ? startingIndex + 1 : startingIndex; var leafSize = leaf.keys.length; if (start < leafSize) pushLeafRange(leaf, start, leafSize); } }; var onStepUp = function (parent, height, payload, fromIndex, spineIndex, stepDownIndex, cursorThis) { var children = parent.children; var nextHeight = height - 1; if (stepDownIndex !== stepDownIndex /* NaN: still walking up */ || stepDownIndex === Number.POSITIVE_INFINITY /* target key is beyond edge of tree, done with walk */) { if (!payload.disqualified) { if (stepDownIndex === Number.POSITIVE_INFINITY) { // We have finished our walk, and we won't be stepping down, so add the root // Roots are allowed to be underfilled, so break the root up here if so to avoid // creating underfilled interior nodes during reconstruction. // Note: the main btree implementation allows underfilled nodes in general, this algorithm // guarantees that no additional underfilled nodes are created beyond what was already present. if (parent.keys.length < minSize) { for (var i = fromIndex; i < children.length; ++i) addSharedNodeToDisjointSet(children[i], nextHeight); } else { addSharedNodeToDisjointSet(parent, height); } highestDisjoint = undefined; } else { highestDisjoint = { node: parent, height: height }; } } else { addHighestDisjoint(); var len = children.length; for (var i = fromIndex + 1; i < len; ++i) addSharedNodeToDisjointSet(children[i], nextHeight); } } else { // We have a valid step down index, so we need to disqualify the spine if needed. // This is identical to the step down logic, but we must also perform it here because // in the case of stepping down into a leaf, the step down callback is never called. if (stepDownIndex > 0) { disqualifySpine(cursorThis, spineIndex); } addHighestDisjoint(); for (var i = fromIndex + 1; i < stepDownIndex; ++i) addSharedNodeToDisjointSet(children[i], nextHeight); } }; var onStepDown = function (node, height, spineIndex, stepDownIndex, cursorThis) { if (stepDownIndex > 0) { // When we step down into a node, we know that we have walked from a key that is less than our target. // Because of this, if we are not stepping down into the first child, we know that all children before // the stepDownIndex must overlap with the other tree because they must be before our target key. Since // the child we are stepping into has a key greater than our target key, this node must overlap. // If a child overlaps, the entire spine overlaps because a parent in a btree always encloses the range // of its children. disqualifySpine(cursorThis, spineIndex); var children = node.children; var nextHeight = height - 1; for (var i = 0; i < stepDownIndex; ++i) addSharedNodeToDisjointSet(children[i], nextHeight); } }; var onEnterLeaf = function (leaf, destIndex, cursorThis, cursorOther) { if (destIndex > 0 || (0, b_tree_1.areOverlapping)(leaf.minKey(), leaf.maxKey(), (0, parallelWalk_1.getKey)(cursorOther), cursorOther.leaf.maxKey(), cmp)) { // Similar logic to the step-down case, except in this case we also know the leaf in the other // tree overlaps a leaf in this tree (this leaf, specifically). Thus, we can disqualify both spines. cursorThis.leafPayload.disqualified = true; cursorOther.leafPayload.disqualified = true; disqualifySpine(cursorThis, cursorThis.spine.length - 1); disqualifySpine(cursorOther, cursorOther.spine.length - 1); pushLeafRange(leaf, 0, destIndex); } }; // Need the max key of both trees to perform the "finishing" walk of which ever cursor finishes second var maxKeyLeft = left._root.maxKey(); var maxKeyRight = right._root.maxKey(); var maxKey = cmp(maxKeyLeft, maxKeyRight) >= 0 ? maxKeyLeft : maxKeyRight; // Initialize cursors at minimum keys. var curA = (0, parallelWalk_1.createCursor)(left, makePayload, onEnterLeaf, onMoveInLeaf, onExitLeaf, onStepUp, onStepDown); var curB; if (ignoreRight) { var dummyPayload_1 = { disqualified: true }; var onStepUpIgnore = function (_1, _2, _3, _4, spineIndex, stepDownIndex, cursorThis) { if (stepDownIndex > 0) { disqualifySpine(cursorThis, spineIndex); } }; var onStepDownIgnore = function (_, __, spineIndex, stepDownIndex, cursorThis) { if (stepDownIndex > 0) { disqualifySpine(cursorThis, spineIndex); } }; var onEnterLeafIgnore = function (leaf, destIndex, _, cursorOther) { if (destIndex > 0 || (0, b_tree_1.areOverlapping)(leaf.minKey(), leaf.maxKey(), (0, parallelWalk_1.getKey)(cursorOther), cursorOther.leaf.maxKey(), cmp)) { cursorOther.leafPayload.disqualified = true; disqualifySpine(cursorOther, cursorOther.spine.length - 1); } }; curB = (0, parallelWalk_1.createCursor)(right, function () { return dummyPayload_1; }, onEnterLeafIgnore, parallelWalk_1.noop, parallelWalk_1.noop, onStepUpIgnore, onStepDownIgnore); } else { curB = (0, parallelWalk_1.createCursor)(right, makePayload, onEnterLeaf, onMoveInLeaf, onExitLeaf, onStepUp, onStepDown); } // The guarantee that no overlapping interior nodes are accidentally reused relies on the careful // alternating hopping walk of the cursors: WLOG, cursorA always--with one exception--walks from a key just behind (in key space) // the key of cursorB to the first key >= cursorB. Call this transition a "crossover point." All interior nodes that // overlap cause a crossover point, and all crossover points are guaranteed to be walked using this method. Thus, // all overlapping interior nodes will be found if they are checked for on step-down. // The one exception mentioned above is when they start at the same key. In this case, they are both advanced forward and then // their new ordering determines how they walk from there. // The one issue then is detecting any overlaps that occur based on their very initial position (minimum key of each tree). // This is handled by the initial disqualification step below, which essentially emulates the step down disqualification for each spine. // Initialize disqualification w.r.t. opposite leaf. var initDisqualify = function (cur, other) { var minKey = (0, parallelWalk_1.getKey)(cur); var otherMin = (0, parallelWalk_1.getKey)(other); var otherMax = other.leaf.maxKey(); if ((0, b_tree_1.areOverlapping)(minKey, cur.leaf.maxKey(), otherMin, otherMax, cmp)) cur.leafPayload.disqualified = true; for (var i = 0; i < cur.spine.length; ++i) { var entry = cur.spine[i]; // Since we are on the left side of the tree, we can use the leaf min key for every spine node if ((0, b_tree_1.areOverlapping)(minKey, entry.node.maxKey(), otherMin, otherMax, cmp)) entry.payload.disqualified = true; } }; initDisqualify(curA, curB); initDisqualify(curB, curA); var leading = curA; var trailing = curB; var order = cmp((0, parallelWalk_1.getKey)(leading), (0, parallelWalk_1.getKey)(trailing)); // Walk both cursors in alternating hops while (true) { var areEqual = order === 0; if (areEqual) { var key = (0, parallelWalk_1.getKey)(leading); var vA = curA.leaf.values[curA.leafIndex]; var vB = curB.leaf.values[curB.leafIndex]; // Perform the actual merge of values here. The cursors will avoid adding a duplicate of this key/value // to pending because they respect the areEqual flag during their moves. var combined = combineFn(key, vA, vB); if (combined !== undefined) { pendingKeys.push(key); pendingValues.push(combined); } var outTrailing = (0, parallelWalk_1.moveForwardOne)(trailing, leading); var outLeading = (0, parallelWalk_1.moveForwardOne)(leading, trailing); if (outTrailing || outLeading) { if (!outTrailing || !outLeading) { // In these cases, we pass areEqual=false because a return value of "out of tree" means // the cursor did not move. This must be true because they started equal and one of them had more tree // to walk (one is !out), so they cannot be equal at this point. if (outTrailing) { (0, parallelWalk_1.moveTo)(leading, trailing, maxKey, false, false); } else { (0, parallelWalk_1.moveTo)(trailing, leading, maxKey, false, false); } } break; } order = cmp((0, parallelWalk_1.getKey)(leading), (0, parallelWalk_1.getKey)(trailing)); } else { if (order < 0) { var tmp = trailing; trailing = leading; leading = tmp; } var _a = (0, parallelWalk_1.moveTo)(trailing, leading, (0, parallelWalk_1.getKey)(leading), true, areEqual), out = _a[0], nowEqual = _a[1]; if (out) { (0, parallelWalk_1.moveTo)(leading, trailing, maxKey, false, areEqual); break; } else if (nowEqual) { order = 0; } else { order = -1; } } } // Ensure any trailing non-disjoint entries are added (0, shared_1.makeLeavesFrom)(pendingKeys, pendingValues, maxNodeSize, decomposeLoadFactor, onLeafCreation); // In cases like full interleaving, no leaves may be created until now if (tallestHeight < 0 && disjointHeights.length > 0) { tallestIndex = 0; } return { heights: disjointHeights, nodes: disjointNodes, tallestIndex: tallestIndex }; } exports.decompose = decompose; /** * Constructs a B-Tree from the result of a decomposition (set of disjoint nodes). * @internal */ function buildFromDecomposition(constructor, branchingFactor, decomposed, cmp, maxNodeSize) { var heights = decomposed.heights, nodes = decomposed.nodes, tallestIndex = decomposed.tallestIndex; (0, b_tree_1.check)(heights.length === nodes.length, "Decompose result has mismatched heights and nodes."); var disjointEntryCount = heights.length; // Now we have a set of disjoint subtrees and we need to merge them into a single tree. // To do this, we start with the tallest subtree from the disjoint set and, for all subtrees // to the "right" and "left" of it in sorted order, we append them onto the appropriate side // of the current tree, splitting nodes as necessary to maintain balance. // A "side" is referred to as a frontier, as it is a linked list of nodes from the root down to // the leaf level on that side of the tree. Each appended subtree is appended to the node at the // same height as itself on the frontier. Each tree is guaranteed to be at most as tall as the // current frontier because we start from the tallest subtree and work outward. var initialRoot = nodes[tallestIndex]; var frontier = [initialRoot]; var rightContext = { branchingFactor: branchingFactor, spine: frontier, sideIndex: getRightmostIndex, sideInsertionIndex: getRightInsertionIndex, splitOffSide: splitOffRightSide, balanceLeaves: balanceLeavesRight, updateMax: updateRightMax, mergeLeaves: mergeRightEntries }; // Process all subtrees to the right of the tallest subtree if (tallestIndex + 1 <= disjointEntryCount - 1) { updateFrontier(rightContext, 0); processSide(heights, nodes, tallestIndex + 1, disjointEntryCount, 1, rightContext); } var leftContext = { branchingFactor: branchingFactor, spine: frontier, sideIndex: getLeftmostIndex, sideInsertionIndex: getLeftmostIndex, splitOffSide: splitOffLeftSide, balanceLeaves: balanceLeavesLeft, updateMax: parallelWalk_1.noop, mergeLeaves: mergeLeftEntries }; // Process all subtrees to the left of the current tree if (tallestIndex - 1 >= 0) { // Note we need to update the frontier here because the right-side processing may have grown the tree taller. updateFrontier(leftContext, 0); processSide(heights, nodes, tallestIndex - 1, -1, -1, leftContext); } var reconstructed = new constructor(undefined, cmp, maxNodeSize); reconstructed._root = frontier[0]; // Return the resulting tree return reconstructed; } exports.buildFromDecomposition = buildFromDecomposition; /** * Processes one side (left or right) of the disjoint subtree set during a reconstruction operation. * Merges each subtree in the disjoint set from start to end (exclusive) into the given spine. * @internal */ function processSide(heights, nodes, start, end, step, context) { var spine = context.spine, sideIndex = context.sideIndex; // Determine the depth of the first shared node on the frontier. // Appending subtrees to the frontier must respect the copy-on-write semantics by cloning // any shared nodes down to the insertion point. We track it by depth to avoid a log(n) walk of the // frontier for each insertion as that would fundamentally change our asymptotics. var isSharedFrontierDepth = 0; var cur = spine[0]; // Find the first shared node on the frontier while (!cur.isShared && isSharedFrontierDepth < spine.length - 1) { isSharedFrontierDepth++; cur = cur.children[sideIndex(cur)]; } // This array holds the sum of sizes of nodes that have been inserted but not yet propagated upward. // For example, if a subtree of size 5 is inserted at depth 2, then unflushedSizes[1] += 5. // These sizes are added to the depth above the insertion point because the insertion updates the direct parent of the insertion. // These sizes are flushed upward any time we need to insert at level higher than pending unflushed sizes. // E.g. in our example, if we later insert at depth 0, we will add 5 to the node at depth 1 and the root at depth 0 before inserting. // This scheme enables us to avoid a log(n) propagation of sizes for each insertion. var unflushedSizes = new Array(spine.length).fill(0); // pre-fill to avoid "holey" array for (var i = start; i != end; i += step) { var currentHeight = spine.length - 1; // height is number of internal levels; 0 means leaf var subtree = nodes[i]; var subtreeHeight = heights[i]; var isEntryInsertion = subtreeHeight === -1; (0, b_tree_1.check)(subtreeHeight <= currentHeight, "Subtree taller than spine during reconstruction."); // If subtree height is -1 (indicating underfilled leaf), then this indicates insertion into a leaf // otherwise, it points to a node whose children have height === subtreeHeight var insertionDepth = currentHeight - (subtreeHeight + 1); // Ensure path is unshared before mutation ensureNotShared(context, isSharedFrontierDepth, insertionDepth); var insertionCount = void 0; // non-recursive var insertionSize = void 0; // recursive if (isEntryInsertion) { (0, b_tree_1.check)(subtree.isShared !== true); insertionCount = insertionSize = subtree.keys.length; } else { insertionCount = 1; insertionSize = subtree.size(); } var cascadeEndDepth = findSplitCascadeEndDepth(context, insertionDepth, insertionCount); // Calculate expansion depth (first ancestor with capacity) var expansionDepth = Math.max(0, // -1 indicates we will cascade to new root cascadeEndDepth); // Update sizes on spine above the shared ancestor before we expand updateSizeAndMax(context, unflushedSizes, isSharedFrontierDepth, expansionDepth); var newRoot = undefined; var sizeChangeDepth = void 0; if (isEntryInsertion) { newRoot = splitUpwardsAndInsertEntries(context, insertionDepth, subtree); // if we are inserting entries, we don't have to update a cached size on the leaf as they simply return count of keys sizeChangeDepth = insertionDepth - 1; } else { newRoot = splitUpwardsAndInsert(context, insertionDepth, subtree)[0]; sizeChangeDepth = insertionDepth; } if (newRoot) { // Set the spine root to the highest up new node; the rest of the spine is updated below spine[0] = newRoot; unflushedSizes.push(0); // new root level, keep unflushed sizes in sync sizeChangeDepth++; // account for the spine lengthening } isSharedFrontierDepth = sizeChangeDepth + 1; unflushedSizes[sizeChangeDepth] += insertionSize; // Finally, update the frontier from the highest new node downward // Note that this is often the point where the new subtree is attached, // but in the case of cascaded splits it may be higher up. updateFrontier(context, expansionDepth); (0, b_tree_1.check)(isSharedFrontierDepth === spine.length - 1 || spine[isSharedFrontierDepth].isShared === true, "Non-leaf subtrees must be shared."); (0, b_tree_1.check)(unflushedSizes.length === spine.length, "Unflushed sizes length mismatch after root split."); // Useful for debugging: //updateSizeAndMax(context, unflushedSizes, spine.length - 1, 0); //spine[0].checkValid(0, { _compare: cmp } as unknown as BTree, 0); } // Finally, propagate any remaining unflushed sizes upward and update max keys updateSizeAndMax(context, unflushedSizes, isSharedFrontierDepth, 0); } ; /** * Cascade splits upward if capacity needed, then append a subtree at a given depth on the chosen side. * All un-propagated sizes must have already been applied to the spine up to the end of any cascading expansions. * This method guarantees that the size of the inserted subtree will not propagate upward beyond the insertion point. * Returns a new root if the root was split, otherwise undefined, and the node into which the subtree was inserted. */ function splitUpwardsAndInsert(context, insertionDepth, subtree) { var spine = context.spine, branchingFactor = context.branchingFactor, sideIndex = context.sideIndex, sideInsertionIndex = context.sideInsertionIndex, splitOffSide = context.splitOffSide, updateMax = context.updateMax; // We must take care to avoid accidental propagation upward of the size of the inserted subtree // To do this, we first split nodes upward from the insertion point until we find a node with capacity // or create a new root. Since all un-propagated sizes have already been applied to the spine up to this point, // inserting at the end ensures no accidental propagation. // Depth is -1 if the subtree is the same height as the current tree if (insertionDepth >= 0) { var carry = undefined; // Determine initially where to insert after any splits var insertTarget = spine[insertionDepth]; if (insertTarget.keys.length === branchingFactor) { insertTarget = carry = splitOffSide(insertTarget); } var d = insertionDepth - 1; while (carry && d >= 0) { var parent = spine[d]; var sideChildIndex = sideIndex(parent); // Refresh last key since child was split updateMax(parent, parent.children[sideChildIndex].maxKey()); if (parent.keys.length < branchingFactor) { // We have reached the end of the cascade insertNoCount(parent, sideInsertionIndex(parent), carry); carry = undefined; } else { // Splitting the parent here requires care to avoid incorrectly double counting sizes // Example: a node is at max capacity 4, with children each of size 4 for 16 total. // We split the node into two nodes of 2 children each, but this does *not* modify the size // of its parent. Therefore when we insert the carry into the torn-off node, we must not // increase its size or we will double-count the size of the carry subtree. var tornOff = splitOffSide(parent); insertNoCount(tornOff, sideInsertionIndex(tornOff), carry); carry = tornOff; } d--; } var newRoot = undefined; if (carry !== undefined) { // Expansion reached the root, need a new root to hold carry var oldRoot = spine[0]; newRoot = new b_tree_1.BNodeInternal([oldRoot], oldRoot.size() + carry.size()); insertNoCount(newRoot, sideInsertionIndex(newRoot), carry); } // Finally, insert the subtree at the insertion point insertNoCount(insertTarget, sideInsertionIndex(insertTarget), subtree); return [newRoot, insertTarget]; } else { // Insertion of subtree with equal height to current tree var oldRoot = spine[0]; var newRoot = new b_tree_1.BNodeInternal([oldRoot], oldRoot.size()); insertNoCount(newRoot, sideInsertionIndex(newRoot), subtree); return [newRoot, newRoot]; } } ; /** * Inserts an underfilled leaf (entryContainer), merging with its sibling if possible and splitting upward if not. */ function splitUpwardsAndInsertEntries(context, insertionDepth, entryContainer) { var branchingFactor = context.branchingFactor, spine = context.spine, balanceLeaves = context.balanceLeaves, mergeLeaves = context.mergeLeaves; var entryCount = entryContainer.keys.length; var parent = spine[insertionDepth]; var parentSize = parent.keys.length; if (parentSize + entryCount <= branchingFactor) { // Sibling has capacity, just merge into it mergeLeaves(parent, entryContainer); return undefined; } else { // As with the internal node splitUpwardsAndInsert method, this method also must make all structural changes // to the tree before inserting any new content. This is to avoid accidental propagation of sizes upward. var _a = splitUpwardsAndInsert(context, insertionDepth - 1, entryContainer), newRoot = _a[0], grandparent = _a[1]; var minSize = Math.floor(branchingFactor / 2); var toTake = minSize - entryCount; balanceLeaves(grandparent, entryContainer, toTake); return newRoot; } } /** * Clone along the spine from [isSharedFrontierDepth to depthTo] inclusive so path is safe to mutate. * Short-circuits if first shared node is deeper than depthTo (the insertion depth). */ function ensureNotShared(context, isSharedFrontierDepth, depthToInclusive) { var spine = context.spine, sideIndex = context.sideIndex; if (depthToInclusive < 0 /* new root case */) return; // nothing to clone when root is a leaf; equal-height case will handle this // Clone root if needed first (depth 0) if (isSharedFrontierDepth === 0) { var root = spine[0]; spine[0] = root.clone(); } // Clone downward along the frontier to 'depthToInclusive' for (var depth = Math.max(isSharedFrontierDepth, 1); depth <= depthToInclusive; depth++) { var parent = spine[depth - 1]; var childIndex = sideIndex(parent); var clone = parent.children[childIndex].clone(); parent.children[childIndex] = clone; spine[depth] = clone; } } ; /** * Propagates size updates and updates max keys for nodes in (isSharedFrontierDepth, depthTo) */ function updateSizeAndMax(context, unflushedSizes, isSharedFrontierDepth, depthUpToInclusive) { var spine = context.spine, updateMax = context.updateMax; // If isSharedFrontierDepth is <= depthUpToInclusive there is nothing to update because // the insertion point is inside a shared node which will always have correct sizes var maxKey = spine[isSharedFrontierDepth].maxKey(); var startDepth = isSharedFrontierDepth - 1; for (var depth = startDepth; depth >= depthUpToInclusive; depth--) { var sizeAtLevel = unflushedSizes[depth]; unflushedSizes[depth] = 0; // we are propagating it now if (depth > 0) { // propagate size upward, will be added lazily, either when a subtree is appended at or above that level or // at the end of processing the entire side unflushedSizes[depth - 1] += sizeAtLevel; } var node = spine[depth]; node._size += sizeAtLevel; // No-op if left side, as max keys in parents are unchanged by appending to the beginning of a node updateMax(node, maxKey); } } ; /** * Update a spine (frontier) from a specific depth down, inclusive. * Extends the frontier array if it is not already as long as the frontier. */ function updateFrontier(context, depthLastValid) { var frontier = context.spine, sideIndex = context.sideIndex; (0, b_tree_1.check)(frontier.length > depthLastValid, "updateFrontier: depthLastValid exceeds frontier height"); var startingAncestor = frontier[depthLastValid]; if (startingAncestor.isLeaf) return; var internal = startingAncestor; var cur = internal.children[sideIndex(internal)]; var depth = depthLastValid + 1; while (!cur.isLeaf) { var ni = cur; frontier[depth] = ni; cur = ni.children[sideIndex(ni)]; depth++; } frontier[depth] = cur; } ; /** * Find the first ancestor (starting at insertionDepth) with capacity. */ function findSplitCascadeEndDepth(context, insertionDepth, insertionCount) { var spine = context.spine, branchingFactor = context.branchingFactor; if (insertionDepth >= 0) { var depth = insertionDepth; if (spine[depth].keys.length + insertionCount <= branchingFactor) { return depth; } depth--; while (depth >= 0) { if (spine[depth].keys.length < branchingFactor) return depth; depth--; } } return -1; // no capacity, will need a new root } ; /** * Inserts the child without updating cached size counts. */ function insertNoCount(parent, index, child) { parent.children.splice(index, 0, child); parent.keys.splice(index, 0, child.maxKey()); } // ---- Side-specific delegates for merging subtrees into a frontier ---- function getLeftmostIndex() { return 0; } function getRightmostIndex(node) { return node.children.length - 1; } function getRightInsertionIndex(node) { return node.children.length; } function splitOffRightSide(node) { return node.splitOffRightSide(); } function splitOffLeftSide(node) { return node.splitOffLeftSide(); } function balanceLeavesRight(parent, underfilled, toTake) { var siblingIndex = parent.children.length - 2; var sibling = parent.children[siblingIndex]; var index = sibling.keys.length - toTake; var movedKeys = sibling.keys.splice(index); var movedValues = sibling.values.splice(index); underfilled.keys.unshift.apply(underfilled.keys, movedKeys); underfilled.values.unshift.apply(underfilled.values, movedValues); parent.keys[siblingIndex] = sibling.maxKey(); } function balanceLeavesLeft(parent, underfilled, toTake) { var sibling = parent.children[1]; var movedKeys = sibling.keys.splice(0, toTake); var movedValues = sibling.values.splice(0, toTake); underfilled.keys.push.apply(underfilled.keys, movedKeys); underfilled.values.push.apply(underfilled.values, movedValues); parent.keys[0] = underfilled.maxKey(); } function updateRightMax(node, maxBelow) { node.keys[node.keys.length - 1] = maxBelow; } function mergeRightEntries(leaf, entries) { leaf.keys.push.apply(leaf.keys, entries.keys); leaf.values.push.apply(leaf.values, entries.values); } function mergeLeftEntries(leaf, entries) { leaf.keys.unshift.apply(leaf.keys, entries.keys); leaf.values.unshift.apply(leaf.values, entries.values); } ================================================ FILE: extended/decompose.ts ================================================ import BTree, { areOverlapping, BNode, BNodeInternal, check } from '../b+tree'; import { BTreeConstructor, makeLeavesFrom, type BTreeWithInternals } from './shared'; import { createCursor, getKey, Cursor, moveForwardOne, moveTo, noop } from "./parallelWalk"; /** * A set of disjoint nodes, their heights, and the index of the tallest node. * A height of -1 indicates an underfilled non-shared node that must be merged. * Any shared nodes (including underfilled leaves) must have height >= 0. * @internal */ export type DecomposeResult = { heights: number[], nodes: BNode[], tallestIndex: number }; /** * Payload type used by decomposition cursors. */ type DecomposePayload = { disqualified: boolean }; const decomposeLoadFactor = 0.7; /** * Decomposes two trees into disjoint nodes. Reuses interior nodes when they do not overlap/intersect with any leaf nodes * in the other tree. Overlapping leaf nodes are broken down into new leaf nodes containing merged entries. * The algorithm is a parallel tree walk using two cursors. The trailing cursor (behind in key space) is walked forward * until it is at or after the leading cursor. As it does this, any whole nodes or subtrees it passes are guaranteed to * be disjoint. This is true because the leading cursor was also previously walked in this way, and is thus pointing to * the first key at or after the trailing cursor's previous position. * The cursor walk is efficient, meaning it skips over disjoint subtrees entirely rather than visiting every leaf. * Note: some of the returned leaves may be underfilled. * @internal */ export function decompose( left: BTreeWithInternals, right: BTreeWithInternals, combineFn: (key: K, leftValue: V, rightValue: V) => V | undefined, ignoreRight: boolean = false ): DecomposeResult { const maxNodeSize = left._maxNodeSize; const cmp = left._compare; check(left._root.size() > 0 && right._root.size() > 0, "decompose requires non-empty inputs"); // Holds the disjoint nodes that result from decomposition. // Stored as parallel arrays of (height, node) to avoid creating many tiny tuples const disjointHeights: number[] = []; const disjointNodes: BNode[] = []; // During the decomposition, leaves that are not disjoint are decomposed into individual entries // that accumulate in this array in sorted order. They are flushed into leaf nodes whenever a reused // disjoint subtree is added to the disjoint set. // Note that there are unavoidable cases in which this will generate underfilled leaves. // An example of this would be a leaf in one tree that contained keys [0, 100, 101, 102]. // In the other tree, there is a leaf that contains [2, 3, 4, 5]. This leaf can be reused entirely, // but the first tree's leaf must be decomposed into [0] and [100, 101, 102] const pendingKeys: K[] = []; const pendingValues: V[] = []; let tallestIndex = -1, tallestHeight = -1; // During the upward part of the cursor walk, this holds the highest disjoint node seen so far. // This is done because we cannot know immediately whether we can add the node to the disjoint set // because its ancestor may also be disjoint and should be reused instead. let highestDisjoint: { node: BNode, height: number } | undefined // Have to do this as cast to convince TS it's ever assigned = undefined as { node: BNode, height: number } | undefined; const minSize = Math.floor(maxNodeSize / 2); const onLeafCreation = (leaf: BNode) => { const height = leaf.keys.length < minSize ? -1 : 0; disjointHeights.push(height); disjointNodes.push(leaf); }; const addSharedNodeToDisjointSet = (node: BNode, height: number) => { // flush pending entries makeLeavesFrom(pendingKeys, pendingValues, maxNodeSize, decomposeLoadFactor, onLeafCreation); pendingKeys.length = 0; pendingValues.length = 0; // Don't share underfilled leaves, instead mark them as needing merging if (node.isLeaf && node.keys.length < minSize) { disjointHeights.push(-1); disjointNodes.push(node.clone()); } else { node.isShared = true; disjointHeights.push(height); disjointNodes.push(node); } if (height > tallestHeight) { tallestIndex = disjointHeights.length - 1; tallestHeight = height; } }; const addHighestDisjoint = () => { if (highestDisjoint !== undefined) { addSharedNodeToDisjointSet(highestDisjoint.node, highestDisjoint.height); highestDisjoint = undefined; } }; // Mark all nodes at or above depthFrom in the cursor spine as disqualified (non-disjoint) const disqualifySpine = (cursor: Cursor, depthFrom: number) => { const spine = cursor.spine; for (let i = depthFrom; i >= 0; --i) { const payload = spine[i].payload; // Safe to early out because we always disqualify all ancestors of a disqualified node // That is correct because every ancestor of a non-disjoint node is also non-disjoint // because it must enclose the non-disjoint range. if (payload.disqualified) break; payload.disqualified = true; } }; // Cursor payload factory const makePayload = (): DecomposePayload => ({ disqualified: false }); const pushLeafRange = (leaf: BNode, from: number, toExclusive: number) => { const keys = leaf.keys; const values = leaf.values; for (let i = from; i < toExclusive; ++i) { pendingKeys.push(keys[i]); pendingValues.push(values[i]); } }; const onMoveInLeaf = ( leaf: BNode, payload: DecomposePayload, fromIndex: number, toIndex: number, startedEqual: boolean ) => { check(payload.disqualified === true, "onMoveInLeaf: leaf must be disqualified"); const start = startedEqual ? fromIndex + 1 : fromIndex; if (start < toIndex) pushLeafRange(leaf, start, toIndex); }; const onExitLeaf = ( leaf: BNode, payload: DecomposePayload, startingIndex: number, startedEqual: boolean, cursorThis: Cursor, ) => { highestDisjoint = undefined; if (!payload.disqualified) { highestDisjoint = { node: leaf, height: 0 }; if (cursorThis.spine.length === 0) { // if we are exiting a leaf and there are no internal nodes, we will reach the end of the tree. // In this case we need to add the leaf now because step up will not be called. addHighestDisjoint(); } } else { const start = startedEqual ? startingIndex + 1 : startingIndex; const leafSize = leaf.keys.length; if (start < leafSize) pushLeafRange(leaf, start, leafSize); } }; const onStepUp = ( parent: BNodeInternal, height: number, payload: DecomposePayload, fromIndex: number, spineIndex: number, stepDownIndex: number, cursorThis: Cursor ) => { const children = parent.children; const nextHeight = height - 1; if (stepDownIndex !== stepDownIndex /* NaN: still walking up */ || stepDownIndex === Number.POSITIVE_INFINITY /* target key is beyond edge of tree, done with walk */) { if (!payload.disqualified) { if (stepDownIndex === Number.POSITIVE_INFINITY) { // We have finished our walk, and we won't be stepping down, so add the root // Roots are allowed to be underfilled, so break the root up here if so to avoid // creating underfilled interior nodes during reconstruction. // Note: the main btree implementation allows underfilled nodes in general, this algorithm // guarantees that no additional underfilled nodes are created beyond what was already present. if (parent.keys.length < minSize) { for (let i = fromIndex; i < children.length; ++i) addSharedNodeToDisjointSet(children[i], nextHeight); } else { addSharedNodeToDisjointSet(parent, height); } highestDisjoint = undefined; } else { highestDisjoint = { node: parent, height }; } } else { addHighestDisjoint(); const len = children.length; for (let i = fromIndex + 1; i < len; ++i) addSharedNodeToDisjointSet(children[i], nextHeight); } } else { // We have a valid step down index, so we need to disqualify the spine if needed. // This is identical to the step down logic, but we must also perform it here because // in the case of stepping down into a leaf, the step down callback is never called. if (stepDownIndex > 0) { disqualifySpine(cursorThis, spineIndex); } addHighestDisjoint(); for (let i = fromIndex + 1; i < stepDownIndex; ++i) addSharedNodeToDisjointSet(children[i], nextHeight); } }; const onStepDown = ( node: BNodeInternal, height: number, spineIndex: number, stepDownIndex: number, cursorThis: Cursor ) => { if (stepDownIndex > 0) { // When we step down into a node, we know that we have walked from a key that is less than our target. // Because of this, if we are not stepping down into the first child, we know that all children before // the stepDownIndex must overlap with the other tree because they must be before our target key. Since // the child we are stepping into has a key greater than our target key, this node must overlap. // If a child overlaps, the entire spine overlaps because a parent in a btree always encloses the range // of its children. disqualifySpine(cursorThis, spineIndex); const children = node.children; const nextHeight = height - 1; for (let i = 0; i < stepDownIndex; ++i) addSharedNodeToDisjointSet(children[i], nextHeight); } }; const onEnterLeaf = ( leaf: BNode, destIndex: number, cursorThis: Cursor, cursorOther: Cursor ) => { if (destIndex > 0 || areOverlapping(leaf.minKey()!, leaf.maxKey(), getKey(cursorOther), cursorOther.leaf.maxKey(), cmp)) { // Similar logic to the step-down case, except in this case we also know the leaf in the other // tree overlaps a leaf in this tree (this leaf, specifically). Thus, we can disqualify both spines. cursorThis.leafPayload.disqualified = true; cursorOther.leafPayload.disqualified = true; disqualifySpine(cursorThis, cursorThis.spine.length - 1); disqualifySpine(cursorOther, cursorOther.spine.length - 1); pushLeafRange(leaf, 0, destIndex); } }; // Need the max key of both trees to perform the "finishing" walk of which ever cursor finishes second const maxKeyLeft = left._root.maxKey() as K; const maxKeyRight = right._root.maxKey() as K; const maxKey = cmp(maxKeyLeft, maxKeyRight) >= 0 ? maxKeyLeft : maxKeyRight; // Initialize cursors at minimum keys. const curA = createCursor(left, makePayload, onEnterLeaf, onMoveInLeaf, onExitLeaf, onStepUp, onStepDown); let curB: typeof curA; if (ignoreRight) { const dummyPayload: DecomposePayload = { disqualified: true }; const onStepUpIgnore = ( _1: BNodeInternal, _2: number, _3: DecomposePayload, _4: number, spineIndex: number, stepDownIndex: number, cursorThis: Cursor ) => { if (stepDownIndex > 0) { disqualifySpine(cursorThis, spineIndex); } }; const onStepDownIgnore = ( _: BNodeInternal, __: number, spineIndex: number, stepDownIndex: number, cursorThis: Cursor ) => { if (stepDownIndex > 0) { disqualifySpine(cursorThis, spineIndex); } }; const onEnterLeafIgnore = ( leaf: BNode, destIndex: number, _: Cursor, cursorOther: Cursor ) => { if (destIndex > 0 || areOverlapping(leaf.minKey()!, leaf.maxKey(), getKey(cursorOther), cursorOther.leaf.maxKey(), cmp)) { cursorOther.leafPayload.disqualified = true; disqualifySpine(cursorOther, cursorOther.spine.length - 1); } }; curB = createCursor(right, () => dummyPayload, onEnterLeafIgnore, noop, noop, onStepUpIgnore, onStepDownIgnore); } else { curB = createCursor(right, makePayload, onEnterLeaf, onMoveInLeaf, onExitLeaf, onStepUp, onStepDown); } // The guarantee that no overlapping interior nodes are accidentally reused relies on the careful // alternating hopping walk of the cursors: WLOG, cursorA always--with one exception--walks from a key just behind (in key space) // the key of cursorB to the first key >= cursorB. Call this transition a "crossover point." All interior nodes that // overlap cause a crossover point, and all crossover points are guaranteed to be walked using this method. Thus, // all overlapping interior nodes will be found if they are checked for on step-down. // The one exception mentioned above is when they start at the same key. In this case, they are both advanced forward and then // their new ordering determines how they walk from there. // The one issue then is detecting any overlaps that occur based on their very initial position (minimum key of each tree). // This is handled by the initial disqualification step below, which essentially emulates the step down disqualification for each spine. // Initialize disqualification w.r.t. opposite leaf. const initDisqualify = (cur: Cursor, other: Cursor) => { const minKey = getKey(cur); const otherMin = getKey(other); const otherMax = other.leaf.maxKey(); if (areOverlapping(minKey, cur.leaf.maxKey(), otherMin, otherMax, cmp)) cur.leafPayload.disqualified = true; for (let i = 0; i < cur.spine.length; ++i) { const entry = cur.spine[i]; // Since we are on the left side of the tree, we can use the leaf min key for every spine node if (areOverlapping(minKey, entry.node.maxKey(), otherMin, otherMax, cmp)) entry.payload.disqualified = true; } }; initDisqualify(curA, curB); initDisqualify(curB, curA); let leading = curA; let trailing = curB; let order = cmp(getKey(leading), getKey(trailing)); // Walk both cursors in alternating hops while (true) { const areEqual = order === 0; if (areEqual) { const key = getKey(leading); const vA = curA.leaf.values[curA.leafIndex]; const vB = curB.leaf.values[curB.leafIndex]; // Perform the actual merge of values here. The cursors will avoid adding a duplicate of this key/value // to pending because they respect the areEqual flag during their moves. const combined = combineFn(key, vA, vB); if (combined !== undefined) { pendingKeys.push(key); pendingValues.push(combined); } const outTrailing = moveForwardOne(trailing, leading); const outLeading = moveForwardOne(leading, trailing); if (outTrailing || outLeading) { if (!outTrailing || !outLeading) { // In these cases, we pass areEqual=false because a return value of "out of tree" means // the cursor did not move. This must be true because they started equal and one of them had more tree // to walk (one is !out), so they cannot be equal at this point. if (outTrailing) { moveTo(leading, trailing, maxKey, false, false); } else { moveTo(trailing, leading, maxKey, false, false); } } break; } order = cmp(getKey(leading), getKey(trailing)); } else { if (order < 0) { const tmp = trailing; trailing = leading; leading = tmp; } const [out, nowEqual] = moveTo(trailing, leading, getKey(leading), true, areEqual); if (out) { moveTo(leading, trailing, maxKey, false, areEqual); break; } else if (nowEqual) { order = 0; } else { order = -1; } } } // Ensure any trailing non-disjoint entries are added makeLeavesFrom(pendingKeys, pendingValues, maxNodeSize, decomposeLoadFactor, onLeafCreation); // In cases like full interleaving, no leaves may be created until now if (tallestHeight < 0 && disjointHeights.length > 0) { tallestIndex = 0; } return { heights: disjointHeights, nodes: disjointNodes, tallestIndex }; } /** * Constructs a B-Tree from the result of a decomposition (set of disjoint nodes). * @internal */ export function buildFromDecomposition, K, V>( constructor: BTreeConstructor, branchingFactor: number, decomposed: DecomposeResult, cmp: (a: K, b: K) => number, maxNodeSize: number ): TBTree { const { heights, nodes, tallestIndex } = decomposed; check(heights.length === nodes.length, "Decompose result has mismatched heights and nodes."); const disjointEntryCount = heights.length; // Now we have a set of disjoint subtrees and we need to merge them into a single tree. // To do this, we start with the tallest subtree from the disjoint set and, for all subtrees // to the "right" and "left" of it in sorted order, we append them onto the appropriate side // of the current tree, splitting nodes as necessary to maintain balance. // A "side" is referred to as a frontier, as it is a linked list of nodes from the root down to // the leaf level on that side of the tree. Each appended subtree is appended to the node at the // same height as itself on the frontier. Each tree is guaranteed to be at most as tall as the // current frontier because we start from the tallest subtree and work outward. const initialRoot = nodes[tallestIndex]; const frontier: BNode[] = [initialRoot]; const rightContext: SideContext = { branchingFactor, spine: frontier, sideIndex: getRightmostIndex, sideInsertionIndex: getRightInsertionIndex, splitOffSide: splitOffRightSide, balanceLeaves: balanceLeavesRight, updateMax: updateRightMax, mergeLeaves: mergeRightEntries }; // Process all subtrees to the right of the tallest subtree if (tallestIndex + 1 <= disjointEntryCount - 1) { updateFrontier(rightContext, 0); processSide( heights, nodes, tallestIndex + 1, disjointEntryCount, 1, rightContext ); } const leftContext: SideContext = { branchingFactor, spine: frontier, sideIndex: getLeftmostIndex, sideInsertionIndex: getLeftmostIndex, splitOffSide: splitOffLeftSide, balanceLeaves: balanceLeavesLeft, updateMax: noop, // left side appending doesn't update max keys, mergeLeaves: mergeLeftEntries }; // Process all subtrees to the left of the current tree if (tallestIndex - 1 >= 0) { // Note we need to update the frontier here because the right-side processing may have grown the tree taller. updateFrontier(leftContext, 0); processSide( heights, nodes, tallestIndex - 1, -1, -1, leftContext ); } const reconstructed = new constructor(undefined, cmp, maxNodeSize); reconstructed._root = frontier[0]; // Return the resulting tree return reconstructed as unknown as TBTree; } /** * Processes one side (left or right) of the disjoint subtree set during a reconstruction operation. * Merges each subtree in the disjoint set from start to end (exclusive) into the given spine. * @internal */ function processSide( heights: number[], nodes: BNode[], start: number, end: number, step: number, context: SideContext ): void { const { spine, sideIndex } = context; // Determine the depth of the first shared node on the frontier. // Appending subtrees to the frontier must respect the copy-on-write semantics by cloning // any shared nodes down to the insertion point. We track it by depth to avoid a log(n) walk of the // frontier for each insertion as that would fundamentally change our asymptotics. let isSharedFrontierDepth = 0; let cur = spine[0]; // Find the first shared node on the frontier while (!cur.isShared && isSharedFrontierDepth < spine.length - 1) { isSharedFrontierDepth++; cur = (cur as BNodeInternal).children[sideIndex(cur as BNodeInternal)]; } // This array holds the sum of sizes of nodes that have been inserted but not yet propagated upward. // For example, if a subtree of size 5 is inserted at depth 2, then unflushedSizes[1] += 5. // These sizes are added to the depth above the insertion point because the insertion updates the direct parent of the insertion. // These sizes are flushed upward any time we need to insert at level higher than pending unflushed sizes. // E.g. in our example, if we later insert at depth 0, we will add 5 to the node at depth 1 and the root at depth 0 before inserting. // This scheme enables us to avoid a log(n) propagation of sizes for each insertion. const unflushedSizes: number[] = new Array(spine.length).fill(0); // pre-fill to avoid "holey" array for (let i = start; i != end; i += step) { const currentHeight = spine.length - 1; // height is number of internal levels; 0 means leaf const subtree = nodes[i]; const subtreeHeight = heights[i]; const isEntryInsertion = subtreeHeight === -1; check(subtreeHeight <= currentHeight, "Subtree taller than spine during reconstruction."); // If subtree height is -1 (indicating underfilled leaf), then this indicates insertion into a leaf // otherwise, it points to a node whose children have height === subtreeHeight const insertionDepth = currentHeight - (subtreeHeight + 1); // Ensure path is unshared before mutation ensureNotShared(context, isSharedFrontierDepth, insertionDepth); let insertionCount: number; // non-recursive let insertionSize: number; // recursive if (isEntryInsertion) { check(subtree.isShared !== true); insertionCount = insertionSize = subtree.keys.length; } else { insertionCount = 1; insertionSize = subtree.size(); } const cascadeEndDepth = findSplitCascadeEndDepth(context, insertionDepth, insertionCount); // Calculate expansion depth (first ancestor with capacity) const expansionDepth = Math.max( 0, // -1 indicates we will cascade to new root cascadeEndDepth ); // Update sizes on spine above the shared ancestor before we expand updateSizeAndMax(context, unflushedSizes, isSharedFrontierDepth, expansionDepth); let newRoot: BNodeInternal | undefined = undefined; let sizeChangeDepth: number; if (isEntryInsertion) { newRoot = splitUpwardsAndInsertEntries(context, insertionDepth, subtree); // if we are inserting entries, we don't have to update a cached size on the leaf as they simply return count of keys sizeChangeDepth = insertionDepth - 1; } else { [newRoot] = splitUpwardsAndInsert(context, insertionDepth, subtree); sizeChangeDepth = insertionDepth; } if (newRoot) { // Set the spine root to the highest up new node; the rest of the spine is updated below spine[0] = newRoot; unflushedSizes.push(0); // new root level, keep unflushed sizes in sync sizeChangeDepth++; // account for the spine lengthening } isSharedFrontierDepth = sizeChangeDepth + 1; unflushedSizes[sizeChangeDepth] += insertionSize; // Finally, update the frontier from the highest new node downward // Note that this is often the point where the new subtree is attached, // but in the case of cascaded splits it may be higher up. updateFrontier(context, expansionDepth); check(isSharedFrontierDepth === spine.length - 1 || spine[isSharedFrontierDepth].isShared === true, "Non-leaf subtrees must be shared."); check(unflushedSizes.length === spine.length, "Unflushed sizes length mismatch after root split."); // Useful for debugging: //updateSizeAndMax(context, unflushedSizes, spine.length - 1, 0); //spine[0].checkValid(0, { _compare: cmp } as unknown as BTree, 0); } // Finally, propagate any remaining unflushed sizes upward and update max keys updateSizeAndMax(context, unflushedSizes, isSharedFrontierDepth, 0); }; /** * Cascade splits upward if capacity needed, then append a subtree at a given depth on the chosen side. * All un-propagated sizes must have already been applied to the spine up to the end of any cascading expansions. * This method guarantees that the size of the inserted subtree will not propagate upward beyond the insertion point. * Returns a new root if the root was split, otherwise undefined, and the node into which the subtree was inserted. */ function splitUpwardsAndInsert( context: SideContext, insertionDepth: number, subtree: BNode ): [newRoot: BNodeInternal | undefined, insertTarget: BNodeInternal] { const { spine, branchingFactor, sideIndex, sideInsertionIndex, splitOffSide, updateMax } = context; // We must take care to avoid accidental propagation upward of the size of the inserted subtree // To do this, we first split nodes upward from the insertion point until we find a node with capacity // or create a new root. Since all un-propagated sizes have already been applied to the spine up to this point, // inserting at the end ensures no accidental propagation. // Depth is -1 if the subtree is the same height as the current tree if (insertionDepth >= 0) { let carry: BNode | undefined = undefined; // Determine initially where to insert after any splits let insertTarget: BNodeInternal = spine[insertionDepth] as BNodeInternal; if (insertTarget.keys.length === branchingFactor) { insertTarget = carry = splitOffSide(insertTarget); } let d = insertionDepth - 1; while (carry && d >= 0) { const parent = spine[d] as BNodeInternal; const sideChildIndex = sideIndex(parent); // Refresh last key since child was split updateMax(parent, parent.children[sideChildIndex].maxKey()); if (parent.keys.length < branchingFactor) { // We have reached the end of the cascade insertNoCount(parent, sideInsertionIndex(parent), carry); carry = undefined; } else { // Splitting the parent here requires care to avoid incorrectly double counting sizes // Example: a node is at max capacity 4, with children each of size 4 for 16 total. // We split the node into two nodes of 2 children each, but this does *not* modify the size // of its parent. Therefore when we insert the carry into the torn-off node, we must not // increase its size or we will double-count the size of the carry subtree. const tornOff = splitOffSide(parent); insertNoCount(tornOff, sideInsertionIndex(tornOff), carry); carry = tornOff; } d--; } let newRoot: BNodeInternal | undefined = undefined; if (carry !== undefined) { // Expansion reached the root, need a new root to hold carry const oldRoot = spine[0] as BNodeInternal; newRoot = new BNodeInternal([oldRoot], oldRoot.size() + carry.size()); insertNoCount(newRoot, sideInsertionIndex(newRoot), carry); } // Finally, insert the subtree at the insertion point insertNoCount(insertTarget, sideInsertionIndex(insertTarget), subtree); return [newRoot, insertTarget]; } else { // Insertion of subtree with equal height to current tree const oldRoot = spine[0] as BNodeInternal; const newRoot = new BNodeInternal([oldRoot], oldRoot.size()); insertNoCount(newRoot, sideInsertionIndex(newRoot), subtree); return [newRoot, newRoot]; } }; /** * Inserts an underfilled leaf (entryContainer), merging with its sibling if possible and splitting upward if not. */ function splitUpwardsAndInsertEntries( context: SideContext, insertionDepth: number, entryContainer: BNode ): BNodeInternal | undefined { const { branchingFactor, spine, balanceLeaves, mergeLeaves } = context; const entryCount = entryContainer.keys.length; const parent = spine[insertionDepth]; const parentSize = parent.keys.length; if (parentSize + entryCount <= branchingFactor) { // Sibling has capacity, just merge into it mergeLeaves(parent, entryContainer); return undefined; } else { // As with the internal node splitUpwardsAndInsert method, this method also must make all structural changes // to the tree before inserting any new content. This is to avoid accidental propagation of sizes upward. const [newRoot, grandparent] = splitUpwardsAndInsert( context, insertionDepth - 1, entryContainer ); const minSize = Math.floor(branchingFactor / 2); const toTake = minSize - entryCount; balanceLeaves(grandparent, entryContainer, toTake); return newRoot; } } /** * Clone along the spine from [isSharedFrontierDepth to depthTo] inclusive so path is safe to mutate. * Short-circuits if first shared node is deeper than depthTo (the insertion depth). */ function ensureNotShared( context: SideContext, isSharedFrontierDepth: number, depthToInclusive: number) { const { spine, sideIndex } = context; if (depthToInclusive < 0 /* new root case */) return; // nothing to clone when root is a leaf; equal-height case will handle this // Clone root if needed first (depth 0) if (isSharedFrontierDepth === 0) { const root = spine[0]; spine[0] = root.clone(); } // Clone downward along the frontier to 'depthToInclusive' for (let depth = Math.max(isSharedFrontierDepth, 1); depth <= depthToInclusive; depth++) { const parent = spine[depth - 1] as BNodeInternal; const childIndex = sideIndex(parent); const clone = parent.children[childIndex].clone(); parent.children[childIndex] = clone; spine[depth] = clone; } }; /** * Propagates size updates and updates max keys for nodes in (isSharedFrontierDepth, depthTo) */ function updateSizeAndMax( context: SideContext, unflushedSizes: number[], isSharedFrontierDepth: number, depthUpToInclusive: number) { const { spine, updateMax } = context; // If isSharedFrontierDepth is <= depthUpToInclusive there is nothing to update because // the insertion point is inside a shared node which will always have correct sizes const maxKey = spine[isSharedFrontierDepth].maxKey(); const startDepth = isSharedFrontierDepth - 1; for (let depth = startDepth; depth >= depthUpToInclusive; depth--) { const sizeAtLevel = unflushedSizes[depth]; unflushedSizes[depth] = 0; // we are propagating it now if (depth > 0) { // propagate size upward, will be added lazily, either when a subtree is appended at or above that level or // at the end of processing the entire side unflushedSizes[depth - 1] += sizeAtLevel; } const node = spine[depth] as BNodeInternal; node._size += sizeAtLevel; // No-op if left side, as max keys in parents are unchanged by appending to the beginning of a node updateMax(node, maxKey); } }; /** * Update a spine (frontier) from a specific depth down, inclusive. * Extends the frontier array if it is not already as long as the frontier. */ function updateFrontier(context: SideContext, depthLastValid: number): void { const { spine: frontier, sideIndex } = context; check(frontier.length > depthLastValid, "updateFrontier: depthLastValid exceeds frontier height"); const startingAncestor = frontier[depthLastValid]; if (startingAncestor.isLeaf) return; const internal = startingAncestor as BNodeInternal; let cur: BNode = internal.children[sideIndex(internal)]; let depth = depthLastValid + 1; while (!cur.isLeaf) { const ni = cur as BNodeInternal; frontier[depth] = ni; cur = ni.children[sideIndex(ni)]; depth++; } frontier[depth] = cur; }; /** * Find the first ancestor (starting at insertionDepth) with capacity. */ function findSplitCascadeEndDepth(context: SideContext, insertionDepth: number, insertionCount: number): number { const { spine, branchingFactor } = context; if (insertionDepth >= 0) { let depth = insertionDepth; if (spine[depth].keys.length + insertionCount <= branchingFactor) { return depth; } depth--; while (depth >= 0) { if (spine[depth].keys.length < branchingFactor) return depth; depth-- } } return -1; // no capacity, will need a new root }; /** * Inserts the child without updating cached size counts. */ function insertNoCount( parent: BNodeInternal, index: number, child: BNode ): void { parent.children.splice(index, 0, child); parent.keys.splice(index, 0, child.maxKey()); } type SideContext = { branchingFactor: number; spine: BNode[]; sideIndex: (node: BNodeInternal) => number; sideInsertionIndex: (node: BNodeInternal) => number; splitOffSide: (node: BNodeInternal) => BNodeInternal; updateMax: (node: BNodeInternal, maxBelow: K) => void; mergeLeaves: (leaf: BNode, entries: BNode) => void; balanceLeaves: (parent: BNodeInternal, underfilled: BNode, toTake: number) => void; }; // ---- Side-specific delegates for merging subtrees into a frontier ---- function getLeftmostIndex(): number { return 0; } function getRightmostIndex(node: BNodeInternal): number { return node.children.length - 1; } function getRightInsertionIndex(node: BNodeInternal): number { return node.children.length; } function splitOffRightSide(node: BNodeInternal): BNodeInternal { return node.splitOffRightSide(); } function splitOffLeftSide(node: BNodeInternal): BNodeInternal { return node.splitOffLeftSide(); } function balanceLeavesRight(parent: BNodeInternal, underfilled: BNode, toTake: number): void { const siblingIndex = parent.children.length - 2; const sibling = parent.children[siblingIndex]; const index = sibling.keys.length - toTake; const movedKeys = sibling.keys.splice(index); const movedValues = sibling.values.splice(index); underfilled.keys.unshift.apply(underfilled.keys, movedKeys); underfilled.values.unshift.apply(underfilled.values, movedValues); parent.keys[siblingIndex] = sibling.maxKey(); } function balanceLeavesLeft(parent: BNodeInternal, underfilled: BNode, toTake: number): void { const sibling = parent.children[1]; const movedKeys = sibling.keys.splice(0, toTake); const movedValues = sibling.values.splice(0, toTake); underfilled.keys.push.apply(underfilled.keys, movedKeys); underfilled.values.push.apply(underfilled.values, movedValues); parent.keys[0] = underfilled.maxKey(); } function updateRightMax(node: BNodeInternal, maxBelow: K): void { node.keys[node.keys.length - 1] = maxBelow; } function mergeRightEntries(leaf: BNode, entries: BNode): void { leaf.keys.push.apply(leaf.keys, entries.keys); leaf.values.push.apply(leaf.values, entries.values); } function mergeLeftEntries(leaf: BNode, entries: BNode): void{ leaf.keys.unshift.apply(leaf.keys, entries.keys); leaf.values.unshift.apply(leaf.values, entries.values); } ================================================ FILE: extended/diffAgainst.d.ts ================================================ import BTree from '../b+tree'; /** * Computes the differences between `treeA` and `treeB`. * For efficiency, the diff is returned via invocations of supplied handlers. * The computation is optimized for the case in which the two trees have large amounts of shared data * (obtained by calling the `clone` or `with` APIs) and will avoid any iteration of shared state. * The handlers can cause computation to early exit by returning `{ break: R }`. * Neither collection should be mutated during the comparison (inside your callbacks), as this method assumes they remain stable. * @param treeA The tree whose differences will be reported via the callbacks. * @param treeB The tree to compute a diff against. * @param onlyA Callback invoked for all keys only present in `treeA`. * @param onlyB Callback invoked for all keys only present in `treeB`. * @param different Callback invoked for all keys with differing values. * @returns The first `break` payload returned by a handler, or `undefined` if no handler breaks. * @throws Error if the supplied trees were created with different comparators. */ export default function diffAgainst(_treeA: BTree, _treeB: BTree, onlyA?: (k: K, v: V) => { break?: R; } | void, onlyB?: (k: K, v: V) => { break?: R; } | void, different?: (k: K, vThis: V, vOther: V) => { break?: R; } | void): R | undefined; ================================================ FILE: extended/diffAgainst.js ================================================ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var b_tree_1 = require("../b+tree"); /** * Computes the differences between `treeA` and `treeB`. * For efficiency, the diff is returned via invocations of supplied handlers. * The computation is optimized for the case in which the two trees have large amounts of shared data * (obtained by calling the `clone` or `with` APIs) and will avoid any iteration of shared state. * The handlers can cause computation to early exit by returning `{ break: R }`. * Neither collection should be mutated during the comparison (inside your callbacks), as this method assumes they remain stable. * @param treeA The tree whose differences will be reported via the callbacks. * @param treeB The tree to compute a diff against. * @param onlyA Callback invoked for all keys only present in `treeA`. * @param onlyB Callback invoked for all keys only present in `treeB`. * @param different Callback invoked for all keys with differing values. * @returns The first `break` payload returned by a handler, or `undefined` if no handler breaks. * @throws Error if the supplied trees were created with different comparators. */ function diffAgainst(_treeA, _treeB, onlyA, onlyB, different) { var treeA = _treeA; var treeB = _treeB; if (treeB._compare !== treeA._compare) { throw new Error('Tree comparators are not the same.'); } if (treeA.isEmpty || treeB.isEmpty) { if (_treeA.isEmpty && treeB.isEmpty) return undefined; if (treeA.isEmpty) { return onlyB === undefined ? undefined : stepToEnd(makeDiffCursor(treeB), onlyB); } return onlyA === undefined ? undefined : stepToEnd(makeDiffCursor(treeA), onlyA); } // Cursor-based diff algorithm is as follows: // - Until neither cursor has navigated to the end of the tree, do the following: // - If the `treeThis` cursor is "behind" the `treeOther` cursor (strictly <, via compare), advance it. // - Otherwise, advance the `treeOther` cursor. // - Any time a cursor is stepped, perform the following: // - If either cursor points to a key/value pair: // - If thisCursor === otherCursor and the values differ, it is a Different. // - If thisCursor > otherCursor and otherCursor is at a key/value pair, it is an OnlyB. // - If thisCursor < otherCursor and thisCursor is at a key/value pair, it is an OnlyA as long as the most recent // cursor step was *not* otherCursor advancing from a tie. The extra condition avoids erroneous OnlyB calls // that would occur due to otherCursor being the "leader". // - Otherwise, if both cursors point to nodes, compare them. If they are equal by reference (shared), skip // both cursors to the next node in the walk. // - Once one cursor has finished stepping, any remaining steps (if any) are taken and key/value pairs are logged // as OnlyB (if otherCursor is stepping) or OnlyA (if thisCursor is stepping). // This algorithm gives the critical guarantee that all locations (both nodes and key/value pairs) in both trees that // are identical by value (and possibly by reference) will be visited *at the same time* by the cursors. // This removes the possibility of emitting incorrect diffs, as well as allowing for skipping shared nodes. var compareKeys = treeA._compare; var thisCursor = makeDiffCursor(treeA); var otherCursor = makeDiffCursor(treeB); var thisSuccess = true; var otherSuccess = true; // It doesn't matter how thisSteppedLast is initialized. // Step order is only used when either cursor is at a leaf, and cursors always start at a node. var prevCursorOrder = compareDiffCursors(thisCursor, otherCursor, compareKeys); while (thisSuccess && otherSuccess) { var cursorOrder = compareDiffCursors(thisCursor, otherCursor, compareKeys); var thisLeaf = thisCursor.leaf, thisInternalSpine = thisCursor.internalSpine, thisLevelIndices = thisCursor.levelIndices; var otherLeaf = otherCursor.leaf, otherInternalSpine = otherCursor.internalSpine, otherLevelIndices = otherCursor.levelIndices; if (thisLeaf || otherLeaf) { // If the cursors were at the same location last step, then there is no work to be done. if (prevCursorOrder !== 0) { if (cursorOrder === 0) { if (thisLeaf && otherLeaf && different) { // Equal keys, check for modifications var valThis = thisLeaf.values[thisLevelIndices[thisLevelIndices.length - 1]]; var valOther = otherLeaf.values[otherLevelIndices[otherLevelIndices.length - 1]]; if (!Object.is(valThis, valOther)) { var result = different(thisCursor.currentKey, valThis, valOther); if (result && result.break) return result.break; } } } else if (cursorOrder > 0) { // If this is the case, we know that either: // 1. otherCursor stepped last from a starting position that trailed thisCursor, and is still behind, or // 2. thisCursor stepped last and leapfrogged otherCursor // Either of these cases is an "only other" if (otherLeaf && onlyB) { var otherVal = otherLeaf.values[otherLevelIndices[otherLevelIndices.length - 1]]; var result = onlyB(otherCursor.currentKey, otherVal); if (result && result.break) return result.break; } } else if (onlyA) { if (thisLeaf && prevCursorOrder !== 0) { var valThis = thisLeaf.values[thisLevelIndices[thisLevelIndices.length - 1]]; var result = onlyA(thisCursor.currentKey, valThis); if (result && result.break) return result.break; } } } } else if (!thisLeaf && !otherLeaf && cursorOrder === 0) { var lastThis = thisInternalSpine.length - 1; var lastOther = otherInternalSpine.length - 1; var nodeThis = thisInternalSpine[lastThis][thisLevelIndices[lastThis]]; var nodeOther = otherInternalSpine[lastOther][otherLevelIndices[lastOther]]; if (nodeOther === nodeThis) { prevCursorOrder = 0; thisSuccess = stepDiffCursor(thisCursor, true); otherSuccess = stepDiffCursor(otherCursor, true); continue; } } prevCursorOrder = cursorOrder; if (cursorOrder < 0) { thisSuccess = stepDiffCursor(thisCursor); } else { otherSuccess = stepDiffCursor(otherCursor); } } if (thisSuccess && onlyA) return finishCursorWalk(thisCursor, otherCursor, compareKeys, onlyA); if (otherSuccess && onlyB) return finishCursorWalk(otherCursor, thisCursor, compareKeys, onlyB); return undefined; } exports.default = diffAgainst; /** * Finishes walking `cursor` once the other cursor has already completed its walk. */ function finishCursorWalk(cursor, cursorFinished, compareKeys, callback) { var compared = compareDiffCursors(cursor, cursorFinished, compareKeys); if (compared === 0) { if (!stepDiffCursor(cursor)) return undefined; } else if (compared < 0) { (0, b_tree_1.check)(false, 'cursor walk terminated early'); } return stepToEnd(cursor, callback); } /** * Walks the cursor to the end of the tree, invoking the callback for each key/value pair. */ function stepToEnd(cursor, callback) { var canStep = true; while (canStep) { var leaf = cursor.leaf, levelIndices = cursor.levelIndices, currentKey = cursor.currentKey; if (leaf) { var value = leaf.values[levelIndices[levelIndices.length - 1]]; var result = callback(currentKey, value); if (result && result.break) return result.break; } canStep = stepDiffCursor(cursor); } return undefined; } function makeDiffCursor(internal) { var root = internal._root; return { height: internal.height, internalSpine: [[root]], levelIndices: [0], leaf: undefined, currentKey: root.maxKey() }; } /** * Advances the cursor to the next step in the walk of its tree. * Cursors are walked backwards in sort order, as this allows them to leverage maxKey() in order to be compared in O(1). */ function stepDiffCursor(cursor, stepToNode) { var internalSpine = cursor.internalSpine, levelIndices = cursor.levelIndices, leaf = cursor.leaf; if (stepToNode === true || leaf) { var levelsLength = levelIndices.length; // Step to the next node only if: // - We are explicitly directed to via stepToNode, or // - There are no key/value pairs left to step to in this leaf if (stepToNode === true || levelIndices[levelsLength - 1] === 0) { var spineLength = internalSpine.length; if (spineLength === 0) return false; // Walk back up the tree until we find a new subtree to descend into var nodeLevelIndex = spineLength - 1; var levelIndexWalkBack = nodeLevelIndex; while (levelIndexWalkBack >= 0) { if (levelIndices[levelIndexWalkBack] > 0) { if (levelIndexWalkBack < levelsLength - 1) { // Remove leaf state from cursor cursor.leaf = undefined; levelIndices.pop(); } // If we walked upwards past any internal node, slice them out if (levelIndexWalkBack < nodeLevelIndex) cursor.internalSpine = internalSpine.slice(0, levelIndexWalkBack + 1); cursor.currentKey = internalSpine[levelIndexWalkBack][--levelIndices[levelIndexWalkBack]].maxKey(); return true; } levelIndexWalkBack--; } // Cursor is in the far left leaf of the tree, no more nodes to enumerate return false; } else { // Move to new leaf value var valueIndex = --levelIndices[levelsLength - 1]; cursor.currentKey = leaf.keys[valueIndex]; return true; } } else { // Cursor does not point to a value in a leaf, so move downwards var nextLevel = internalSpine.length; var currentLevel = nextLevel - 1; var node = internalSpine[currentLevel][levelIndices[currentLevel]]; if (node.isLeaf) { cursor.leaf = node; var valueIndex = (levelIndices[nextLevel] = node.values.length - 1); cursor.currentKey = node.keys[valueIndex]; } else { var children = node.children; internalSpine[nextLevel] = children; var childIndex = children.length - 1; levelIndices[nextLevel] = childIndex; cursor.currentKey = children[childIndex].maxKey(); } return true; } } /** * Compares two cursors and returns which cursor is ahead in the traversal. * Note that cursors advance in reverse sort order. */ function compareDiffCursors(cursorA, cursorB, compareKeys) { var heightA = cursorA.height, currentKeyA = cursorA.currentKey, levelIndicesA = cursorA.levelIndices; var heightB = cursorB.height, currentKeyB = cursorB.currentKey, levelIndicesB = cursorB.levelIndices; // Reverse the comparison order, as cursors are advanced in reverse sorting order var keyComparison = compareKeys(currentKeyB, currentKeyA); if (keyComparison !== 0) return keyComparison; // Normalize depth values relative to the shortest tree. // This ensures that concurrent cursor walks of trees of differing heights can reliably land on shared nodes at the same time. // To accomplish this, a cursor that is on an internal node at depth D1 with maxKey X is considered "behind" a cursor on an // internal node at depth D2 with maxKey Y, when D1 < D2. Thus, always walking the cursor that is "behind" will allow the cursor // at shallower depth (but equal maxKey) to "catch up" and land on shared nodes. var heightMin = heightA < heightB ? heightA : heightB; var depthANormalized = levelIndicesA.length - (heightA - heightMin); var depthBNormalized = levelIndicesB.length - (heightB - heightMin); return depthANormalized - depthBNormalized; } ================================================ FILE: extended/diffAgainst.ts ================================================ import BTree from '../b+tree'; import { BNode, BNodeInternal, check } from '../b+tree'; import { type BTreeWithInternals } from './shared'; /** * Computes the differences between `treeA` and `treeB`. * For efficiency, the diff is returned via invocations of supplied handlers. * The computation is optimized for the case in which the two trees have large amounts of shared data * (obtained by calling the `clone` or `with` APIs) and will avoid any iteration of shared state. * The handlers can cause computation to early exit by returning `{ break: R }`. * Neither collection should be mutated during the comparison (inside your callbacks), as this method assumes they remain stable. * @param treeA The tree whose differences will be reported via the callbacks. * @param treeB The tree to compute a diff against. * @param onlyA Callback invoked for all keys only present in `treeA`. * @param onlyB Callback invoked for all keys only present in `treeB`. * @param different Callback invoked for all keys with differing values. * @returns The first `break` payload returned by a handler, or `undefined` if no handler breaks. * @throws Error if the supplied trees were created with different comparators. */ export default function diffAgainst( _treeA: BTree, _treeB: BTree, onlyA?: (k: K, v: V) => { break?: R } | void, onlyB?: (k: K, v: V) => { break?: R } | void, different?: (k: K, vThis: V, vOther: V) => { break?: R } | void ): R | undefined { const treeA = _treeA as unknown as BTreeWithInternals; const treeB = _treeB as unknown as BTreeWithInternals; if (treeB._compare !== treeA._compare) { throw new Error('Tree comparators are not the same.'); } if (treeA.isEmpty || treeB.isEmpty) { if (_treeA.isEmpty && treeB.isEmpty) return undefined; if (treeA.isEmpty) { return onlyB === undefined ? undefined : stepToEnd(makeDiffCursor(treeB), onlyB); } return onlyA === undefined ? undefined : stepToEnd(makeDiffCursor(treeA), onlyA); } // Cursor-based diff algorithm is as follows: // - Until neither cursor has navigated to the end of the tree, do the following: // - If the `treeThis` cursor is "behind" the `treeOther` cursor (strictly <, via compare), advance it. // - Otherwise, advance the `treeOther` cursor. // - Any time a cursor is stepped, perform the following: // - If either cursor points to a key/value pair: // - If thisCursor === otherCursor and the values differ, it is a Different. // - If thisCursor > otherCursor and otherCursor is at a key/value pair, it is an OnlyB. // - If thisCursor < otherCursor and thisCursor is at a key/value pair, it is an OnlyA as long as the most recent // cursor step was *not* otherCursor advancing from a tie. The extra condition avoids erroneous OnlyB calls // that would occur due to otherCursor being the "leader". // - Otherwise, if both cursors point to nodes, compare them. If they are equal by reference (shared), skip // both cursors to the next node in the walk. // - Once one cursor has finished stepping, any remaining steps (if any) are taken and key/value pairs are logged // as OnlyB (if otherCursor is stepping) or OnlyA (if thisCursor is stepping). // This algorithm gives the critical guarantee that all locations (both nodes and key/value pairs) in both trees that // are identical by value (and possibly by reference) will be visited *at the same time* by the cursors. // This removes the possibility of emitting incorrect diffs, as well as allowing for skipping shared nodes. const compareKeys = treeA._compare; const thisCursor = makeDiffCursor(treeA); const otherCursor = makeDiffCursor(treeB); let thisSuccess = true; let otherSuccess = true; // It doesn't matter how thisSteppedLast is initialized. // Step order is only used when either cursor is at a leaf, and cursors always start at a node. let prevCursorOrder = compareDiffCursors(thisCursor, otherCursor, compareKeys); while (thisSuccess && otherSuccess) { const cursorOrder = compareDiffCursors(thisCursor, otherCursor, compareKeys); const { leaf: thisLeaf, internalSpine: thisInternalSpine, levelIndices: thisLevelIndices } = thisCursor; const { leaf: otherLeaf, internalSpine: otherInternalSpine, levelIndices: otherLevelIndices } = otherCursor; if (thisLeaf || otherLeaf) { // If the cursors were at the same location last step, then there is no work to be done. if (prevCursorOrder !== 0) { if (cursorOrder === 0) { if (thisLeaf && otherLeaf && different) { // Equal keys, check for modifications const valThis = thisLeaf.values[thisLevelIndices[thisLevelIndices.length - 1]]; const valOther = otherLeaf.values[otherLevelIndices[otherLevelIndices.length - 1]]; if (!Object.is(valThis, valOther)) { const result = different(thisCursor.currentKey, valThis, valOther); if (result && result.break) return result.break; } } } else if (cursorOrder > 0) { // If this is the case, we know that either: // 1. otherCursor stepped last from a starting position that trailed thisCursor, and is still behind, or // 2. thisCursor stepped last and leapfrogged otherCursor // Either of these cases is an "only other" if (otherLeaf && onlyB) { const otherVal = otherLeaf.values[otherLevelIndices[otherLevelIndices.length - 1]]; const result = onlyB(otherCursor.currentKey, otherVal); if (result && result.break) return result.break; } } else if (onlyA) { if (thisLeaf && prevCursorOrder !== 0) { const valThis = thisLeaf.values[thisLevelIndices[thisLevelIndices.length - 1]]; const result = onlyA(thisCursor.currentKey, valThis); if (result && result.break) return result.break; } } } } else if (!thisLeaf && !otherLeaf && cursorOrder === 0) { const lastThis = thisInternalSpine.length - 1; const lastOther = otherInternalSpine.length - 1; const nodeThis = thisInternalSpine[lastThis][thisLevelIndices[lastThis]]; const nodeOther = otherInternalSpine[lastOther][otherLevelIndices[lastOther]]; if (nodeOther === nodeThis) { prevCursorOrder = 0; thisSuccess = stepDiffCursor(thisCursor, true); otherSuccess = stepDiffCursor(otherCursor, true); continue; } } prevCursorOrder = cursorOrder; if (cursorOrder < 0) { thisSuccess = stepDiffCursor(thisCursor); } else { otherSuccess = stepDiffCursor(otherCursor); } } if (thisSuccess && onlyA) return finishCursorWalk(thisCursor, otherCursor, compareKeys, onlyA); if (otherSuccess && onlyB) return finishCursorWalk(otherCursor, thisCursor, compareKeys, onlyB); return undefined; } /** * Finishes walking `cursor` once the other cursor has already completed its walk. */ function finishCursorWalk( cursor: DiffCursor, cursorFinished: DiffCursor, compareKeys: (a: K, b: K) => number, callback: (k: K, v: V) => { break?: R } | void ): R | undefined { const compared = compareDiffCursors(cursor, cursorFinished, compareKeys); if (compared === 0) { if (!stepDiffCursor(cursor)) return undefined; } else if (compared < 0) { check(false, 'cursor walk terminated early'); } return stepToEnd(cursor, callback); } /** * Walks the cursor to the end of the tree, invoking the callback for each key/value pair. */ function stepToEnd( cursor: DiffCursor, callback: (k: K, v: V) => { break?: R } | void ): R | undefined { let canStep = true; while (canStep) { const { leaf, levelIndices, currentKey } = cursor; if (leaf) { const value = leaf.values[levelIndices[levelIndices.length - 1]]; const result = callback(currentKey, value); if (result && result.break) return result.break; } canStep = stepDiffCursor(cursor); } return undefined; } function makeDiffCursor( internal: BTreeWithInternals ): DiffCursor { const root = internal._root; return { height: internal.height, internalSpine: [[root]], levelIndices: [0], leaf: undefined, currentKey: root.maxKey() }; } /** * Advances the cursor to the next step in the walk of its tree. * Cursors are walked backwards in sort order, as this allows them to leverage maxKey() in order to be compared in O(1). */ function stepDiffCursor(cursor: DiffCursor, stepToNode?: boolean): boolean { const { internalSpine, levelIndices, leaf } = cursor; if (stepToNode === true || leaf) { const levelsLength = levelIndices.length; // Step to the next node only if: // - We are explicitly directed to via stepToNode, or // - There are no key/value pairs left to step to in this leaf if (stepToNode === true || levelIndices[levelsLength - 1] === 0) { const spineLength = internalSpine.length; if (spineLength === 0) return false; // Walk back up the tree until we find a new subtree to descend into const nodeLevelIndex = spineLength - 1; let levelIndexWalkBack = nodeLevelIndex; while (levelIndexWalkBack >= 0) { if (levelIndices[levelIndexWalkBack] > 0) { if (levelIndexWalkBack < levelsLength - 1) { // Remove leaf state from cursor cursor.leaf = undefined; levelIndices.pop(); } // If we walked upwards past any internal node, slice them out if (levelIndexWalkBack < nodeLevelIndex) cursor.internalSpine = internalSpine.slice(0, levelIndexWalkBack + 1); cursor.currentKey = internalSpine[levelIndexWalkBack][--levelIndices[levelIndexWalkBack]].maxKey(); return true; } levelIndexWalkBack--; } // Cursor is in the far left leaf of the tree, no more nodes to enumerate return false; } else { // Move to new leaf value const valueIndex = --levelIndices[levelsLength - 1]; cursor.currentKey = (leaf as BNode).keys[valueIndex]; return true; } } else { // Cursor does not point to a value in a leaf, so move downwards const nextLevel = internalSpine.length; const currentLevel = nextLevel - 1; const node = internalSpine[currentLevel][levelIndices[currentLevel]]; if (node.isLeaf) { cursor.leaf = node; const valueIndex = (levelIndices[nextLevel] = node.values.length - 1); cursor.currentKey = node.keys[valueIndex]; } else { const children = (node as BNodeInternal).children; internalSpine[nextLevel] = children; const childIndex = children.length - 1; levelIndices[nextLevel] = childIndex; cursor.currentKey = children[childIndex].maxKey(); } return true; } } /** * Compares two cursors and returns which cursor is ahead in the traversal. * Note that cursors advance in reverse sort order. */ function compareDiffCursors( cursorA: DiffCursor, cursorB: DiffCursor, compareKeys: (a: K, b: K) => number ): number { const { height: heightA, currentKey: currentKeyA, levelIndices: levelIndicesA } = cursorA; const { height: heightB, currentKey: currentKeyB, levelIndices: levelIndicesB } = cursorB; // Reverse the comparison order, as cursors are advanced in reverse sorting order const keyComparison = compareKeys(currentKeyB, currentKeyA); if (keyComparison !== 0) return keyComparison; // Normalize depth values relative to the shortest tree. // This ensures that concurrent cursor walks of trees of differing heights can reliably land on shared nodes at the same time. // To accomplish this, a cursor that is on an internal node at depth D1 with maxKey X is considered "behind" a cursor on an // internal node at depth D2 with maxKey Y, when D1 < D2. Thus, always walking the cursor that is "behind" will allow the cursor // at shallower depth (but equal maxKey) to "catch up" and land on shared nodes. const heightMin = heightA < heightB ? heightA : heightB; const depthANormalized = levelIndicesA.length - (heightA - heightMin); const depthBNormalized = levelIndicesB.length - (heightB - heightMin); return depthANormalized - depthBNormalized; } /** * A walkable pointer into a BTree for computing efficient diffs between trees with shared data. * - A cursor points to either a key/value pair (KVP) or a node (which can be either a leaf or an internal node). * As a consequence, a cursor cannot be created for an empty tree. * - A cursor can be walked forwards using `step`. A cursor can be compared to another cursor to * determine which is ahead in advancement. * - A cursor is valid only for the tree it was created from, and only until the first edit made to * that tree since the cursor's creation. * - A cursor contains a key for the current location, which is the maxKey when the cursor points to a node * and a key corresponding to a value when pointing to a leaf. * - Leaf is only populated if the cursor points to a KVP. If this is the case, levelIndices.length === internalSpine.length + 1 * and levelIndices[levelIndices.length - 1] is the index of the value. */ type DiffCursor = { height: number; internalSpine: BNode[][]; levelIndices: number[]; leaf: BNode | undefined; currentKey: K; }; ================================================ FILE: extended/forEachKeyInBoth.d.ts ================================================ import BTree from '../b+tree'; /** * Calls the supplied `callback` for each key/value pair shared by both trees, in sorted key order. * Neither tree is modified. * * Complexity is O(N + M) when the trees overlap heavily, and additionally bounded by O(log(N + M) * D) * where `D` is the number of disjoint key ranges between the trees, because whole non-intersecting subtrees * are skipped. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param treeA First tree to compare. * @param treeB Second tree to compare. * @param callback Invoked for keys that appear in both trees. It can cause iteration to early exit by returning `{ break: R }`. * @returns The first `break` payload returned by the callback, or `undefined` if the walk finishes. * @throws Error if the trees were built with different comparators. */ export default function forEachKeyInBoth(treeA: BTree, treeB: BTree, callback: (key: K, leftValue: V, rightValue: V) => { break?: R; } | void): R | undefined; ================================================ FILE: extended/forEachKeyInBoth.js ================================================ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var shared_1 = require("./shared"); var parallelWalk_1 = require("./parallelWalk"); /** * Calls the supplied `callback` for each key/value pair shared by both trees, in sorted key order. * Neither tree is modified. * * Complexity is O(N + M) when the trees overlap heavily, and additionally bounded by O(log(N + M) * D) * where `D` is the number of disjoint key ranges between the trees, because whole non-intersecting subtrees * are skipped. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param treeA First tree to compare. * @param treeB Second tree to compare. * @param callback Invoked for keys that appear in both trees. It can cause iteration to early exit by returning `{ break: R }`. * @returns The first `break` payload returned by the callback, or `undefined` if the walk finishes. * @throws Error if the trees were built with different comparators. */ function forEachKeyInBoth(treeA, treeB, callback) { var _treeA = treeA; var _treeB = treeB; (0, shared_1.checkCanDoSetOperation)(_treeA, _treeB, true); if (treeB.size === 0 || treeA.size === 0) return; var cmp = treeA._compare; var makePayload = function () { return undefined; }; var cursorA = (0, parallelWalk_1.createCursor)(_treeA, makePayload, parallelWalk_1.noop, parallelWalk_1.noop, parallelWalk_1.noop, parallelWalk_1.noop, parallelWalk_1.noop); var cursorB = (0, parallelWalk_1.createCursor)(_treeB, makePayload, parallelWalk_1.noop, parallelWalk_1.noop, parallelWalk_1.noop, parallelWalk_1.noop, parallelWalk_1.noop); var leading = cursorA; var trailing = cursorB; var order = cmp((0, parallelWalk_1.getKey)(leading), (0, parallelWalk_1.getKey)(trailing)); // This walk is somewhat similar to a merge walk in that it does an alternating hop walk with cursors. // However, the only thing we care about is when the two cursors are equal (equality is intersection). // When they are not equal we just advance the trailing cursor. while (true) { var areEqual = order === 0; if (areEqual) { var key = (0, parallelWalk_1.getKey)(leading); var vA = cursorA.leaf.values[cursorA.leafIndex]; var vB = cursorB.leaf.values[cursorB.leafIndex]; var result = callback(key, vA, vB); if (result && result.break) { return result.break; } var outT = (0, parallelWalk_1.moveForwardOne)(trailing, leading); var outL = (0, parallelWalk_1.moveForwardOne)(leading, trailing); if (outT && outL) break; order = cmp((0, parallelWalk_1.getKey)(leading), (0, parallelWalk_1.getKey)(trailing)); } else { if (order < 0) { var tmp = trailing; trailing = leading; leading = tmp; } // At this point, leading is guaranteed to be ahead of trailing. var _a = (0, parallelWalk_1.moveTo)(trailing, leading, (0, parallelWalk_1.getKey)(leading), true, areEqual), out = _a[0], nowEqual = _a[1]; if (out) { // We've reached the end of one tree, so intersections are guaranteed to be done. break; } else if (nowEqual) { order = 0; } else { order = -1; // trailing is ahead of leading } } } } exports.default = forEachKeyInBoth; ================================================ FILE: extended/forEachKeyInBoth.ts ================================================ import BTree from '../b+tree'; import { type BTreeWithInternals, checkCanDoSetOperation } from './shared'; import { createCursor, moveForwardOne, moveTo, getKey, noop } from "./parallelWalk" /** * Calls the supplied `callback` for each key/value pair shared by both trees, in sorted key order. * Neither tree is modified. * * Complexity is O(N + M) when the trees overlap heavily, and additionally bounded by O(log(N + M) * D) * where `D` is the number of disjoint key ranges between the trees, because whole non-intersecting subtrees * are skipped. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param treeA First tree to compare. * @param treeB Second tree to compare. * @param callback Invoked for keys that appear in both trees. It can cause iteration to early exit by returning `{ break: R }`. * @returns The first `break` payload returned by the callback, or `undefined` if the walk finishes. * @throws Error if the trees were built with different comparators. */ export default function forEachKeyInBoth( treeA: BTree, treeB: BTree, callback: (key: K, leftValue: V, rightValue: V) => { break?: R } | void ): R | undefined { const _treeA = treeA as unknown as BTreeWithInternals; const _treeB = treeB as unknown as BTreeWithInternals; checkCanDoSetOperation(_treeA, _treeB, true); if (treeB.size === 0 || treeA.size === 0) return; const cmp = treeA._compare; const makePayload = (): undefined => undefined; let cursorA = createCursor(_treeA, makePayload, noop, noop, noop, noop, noop); let cursorB = createCursor(_treeB, makePayload, noop, noop, noop, noop, noop); let leading = cursorA; let trailing = cursorB; let order = cmp(getKey(leading), getKey(trailing)); // This walk is somewhat similar to a merge walk in that it does an alternating hop walk with cursors. // However, the only thing we care about is when the two cursors are equal (equality is intersection). // When they are not equal we just advance the trailing cursor. while (true) { const areEqual = order === 0; if (areEqual) { const key = getKey(leading); const vA = cursorA.leaf.values[cursorA.leafIndex]; const vB = cursorB.leaf.values[cursorB.leafIndex]; const result = callback(key, vA, vB); if (result && result.break) { return result.break; } const outT = moveForwardOne(trailing, leading); const outL = moveForwardOne(leading, trailing); if (outT && outL) break; order = cmp(getKey(leading), getKey(trailing)); } else { if (order < 0) { const tmp = trailing; trailing = leading; leading = tmp; } // At this point, leading is guaranteed to be ahead of trailing. const [out, nowEqual] = moveTo(trailing, leading, getKey(leading), true, areEqual) if (out) { // We've reached the end of one tree, so intersections are guaranteed to be done. break; } else if (nowEqual) { order = 0; } else { order = -1; // trailing is ahead of leading } } } } ================================================ FILE: extended/forEachKeyNotIn.d.ts ================================================ import BTree from '../b+tree'; /** * Calls the supplied `callback` for each key/value pair that is in `includeTree` but not in `excludeTree` * (set subtraction). The callback runs in sorted key order and neither tree is modified. * * Complexity is O(N + M) when the key ranges overlap heavily, and additionally bounded by O(log(N + M) * D) * where `D` is the number of disjoint ranges between the trees, because non-overlapping subtrees are skipped. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param includeTree The tree to iterate keys from. * @param excludeTree Keys present in this tree are omitted from the callback. * @param callback Invoked for keys that are in `includeTree` but not `excludeTree`. It can cause iteration to early exit by returning `{ break: R }`. * @returns The first `break` payload returned by the callback, or `undefined` if all qualifying keys are visited. * @throws Error if the trees were built with different comparators. */ export default function forEachKeyNotIn(includeTree: BTree, excludeTree: BTree, callback: (key: K, value: V) => { break?: R; } | void): R | undefined; ================================================ FILE: extended/forEachKeyNotIn.js ================================================ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var shared_1 = require("./shared"); var parallelWalk_1 = require("./parallelWalk"); /** * Calls the supplied `callback` for each key/value pair that is in `includeTree` but not in `excludeTree` * (set subtraction). The callback runs in sorted key order and neither tree is modified. * * Complexity is O(N + M) when the key ranges overlap heavily, and additionally bounded by O(log(N + M) * D) * where `D` is the number of disjoint ranges between the trees, because non-overlapping subtrees are skipped. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param includeTree The tree to iterate keys from. * @param excludeTree Keys present in this tree are omitted from the callback. * @param callback Invoked for keys that are in `includeTree` but not `excludeTree`. It can cause iteration to early exit by returning `{ break: R }`. * @returns The first `break` payload returned by the callback, or `undefined` if all qualifying keys are visited. * @throws Error if the trees were built with different comparators. */ function forEachKeyNotIn(includeTree, excludeTree, callback) { var _includeTree = includeTree; var _excludeTree = excludeTree; (0, shared_1.checkCanDoSetOperation)(_includeTree, _excludeTree, true); if (includeTree.size === 0) { return; } var finishWalk = function () { var out = false; do { var key = (0, parallelWalk_1.getKey)(cursorInclude); var value = cursorInclude.leaf.values[cursorInclude.leafIndex]; var result = callback(key, value); if (result && result.break) { return result.break; } out = (0, parallelWalk_1.moveForwardOne)(cursorInclude, cursorExclude); } while (!out); return undefined; }; var cmp = includeTree._compare; var makePayload = function () { return undefined; }; var cursorInclude = (0, parallelWalk_1.createCursor)(_includeTree, makePayload, parallelWalk_1.noop, parallelWalk_1.noop, parallelWalk_1.noop, parallelWalk_1.noop, parallelWalk_1.noop); if (excludeTree.size === 0) { return finishWalk(); } var cursorExclude = (0, parallelWalk_1.createCursor)(_excludeTree, makePayload, parallelWalk_1.noop, parallelWalk_1.noop, parallelWalk_1.noop, parallelWalk_1.noop, parallelWalk_1.noop); var order = cmp((0, parallelWalk_1.getKey)(cursorInclude), (0, parallelWalk_1.getKey)(cursorExclude)); while (true) { var areEqual = order === 0; if (areEqual) { // Keys are equal, so this key is in both trees and should be skipped. var outInclude = (0, parallelWalk_1.moveForwardOne)(cursorInclude, cursorExclude); if (outInclude) break; order = 1; // include is now ahead of exclude } else { if (order < 0) { var key = (0, parallelWalk_1.getKey)(cursorInclude); var value = cursorInclude.leaf.values[cursorInclude.leafIndex]; var result = callback(key, value); if (result && result.break) { return result.break; } var outInclude = (0, parallelWalk_1.moveForwardOne)(cursorInclude, cursorExclude); if (outInclude) { break; } order = cmp((0, parallelWalk_1.getKey)(cursorInclude), (0, parallelWalk_1.getKey)(cursorExclude)); } else { // At this point, include is guaranteed to be ahead of exclude. var _a = (0, parallelWalk_1.moveTo)(cursorExclude, cursorInclude, (0, parallelWalk_1.getKey)(cursorInclude), true, areEqual), out = _a[0], nowEqual = _a[1]; if (out) { // We've reached the end of exclude, so call for all remaining keys in include return finishWalk(); } else if (nowEqual) { order = 0; } else { order = -1; } } } } } exports.default = forEachKeyNotIn; ================================================ FILE: extended/forEachKeyNotIn.ts ================================================ import BTree from '../b+tree'; import { type BTreeWithInternals, checkCanDoSetOperation } from './shared'; import { createCursor, moveForwardOne, moveTo, getKey, noop } from "./parallelWalk" /** * Calls the supplied `callback` for each key/value pair that is in `includeTree` but not in `excludeTree` * (set subtraction). The callback runs in sorted key order and neither tree is modified. * * Complexity is O(N + M) when the key ranges overlap heavily, and additionally bounded by O(log(N + M) * D) * where `D` is the number of disjoint ranges between the trees, because non-overlapping subtrees are skipped. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param includeTree The tree to iterate keys from. * @param excludeTree Keys present in this tree are omitted from the callback. * @param callback Invoked for keys that are in `includeTree` but not `excludeTree`. It can cause iteration to early exit by returning `{ break: R }`. * @returns The first `break` payload returned by the callback, or `undefined` if all qualifying keys are visited. * @throws Error if the trees were built with different comparators. */ export default function forEachKeyNotIn( includeTree: BTree, excludeTree: BTree, callback: (key: K, value: V) => { break?: R } | void ): R | undefined { const _includeTree = includeTree as unknown as BTreeWithInternals; const _excludeTree = excludeTree as unknown as BTreeWithInternals; checkCanDoSetOperation(_includeTree, _excludeTree, true); if (includeTree.size === 0) { return; } const finishWalk = (): R | undefined => { let out = false; do { const key = getKey(cursorInclude); const value = cursorInclude.leaf.values[cursorInclude.leafIndex]; const result = callback(key, value); if (result && result.break) { return result.break; } out = moveForwardOne(cursorInclude, cursorExclude); } while (!out); return undefined; } const cmp = includeTree._compare; const makePayload = (): undefined => undefined; let cursorInclude = createCursor(_includeTree, makePayload, noop, noop, noop, noop, noop); if (excludeTree.size === 0) { return finishWalk(); } let cursorExclude = createCursor(_excludeTree, makePayload, noop, noop, noop, noop, noop); let order = cmp(getKey(cursorInclude), getKey(cursorExclude)); while (true) { const areEqual = order === 0; if (areEqual) { // Keys are equal, so this key is in both trees and should be skipped. const outInclude = moveForwardOne(cursorInclude, cursorExclude); if (outInclude) break; order = 1; // include is now ahead of exclude } else { if (order < 0) { const key = getKey(cursorInclude); const value = cursorInclude.leaf.values[cursorInclude.leafIndex]; const result = callback(key, value); if (result && result.break) { return result.break; } const outInclude = moveForwardOne(cursorInclude, cursorExclude); if (outInclude) { break; } order = cmp(getKey(cursorInclude), getKey(cursorExclude)); } else { // At this point, include is guaranteed to be ahead of exclude. const [out, nowEqual] = moveTo(cursorExclude, cursorInclude, getKey(cursorInclude), true, areEqual) if (out) { // We've reached the end of exclude, so call for all remaining keys in include return finishWalk(); } else if (nowEqual) { order = 0; } else { order = -1; } } } } } ================================================ FILE: extended/index.d.ts ================================================ import BTree from '../b+tree'; /** * An extended version of the `BTree` class that includes additional functionality * such as bulk loading, set operations, and diffing. * It is separated to keep the core BTree class small from a bundle size perspective. * Note: each additional functionality piece is available as a standalone function from the extended folder. * @extends BTree */ export declare class BTreeEx extends BTree { /** * Bulk loads a new `BTreeEx` from parallel arrays of sorted entries. * This reuses the same algorithm as `extended/bulkLoad`, but produces a `BTreeEx`. * Time and space complexity are O(n). * @param keys Keys to load, sorted by key in strictly ascending order. * @param values Values aligned with the supplied keys. * @param maxNodeSize The branching factor (maximum number of children per node). * @param compare Comparator to use. Defaults to the standard comparator if omitted. * @returns A fully built tree containing the supplied entries. * @throws Error if the entries are not strictly sorted or contain duplicate keys. */ static bulkLoad(keys: K[], values: V[], maxNodeSize: number, compare?: (a: K, b: K) => number): BTreeEx; /** See {@link BTree.clone}. */ clone(): this; /** See {@link BTree.greedyClone}. */ greedyClone(force?: boolean): this; /** * Computes the differences between `this` and `other`. * For efficiency, the diff is returned via invocations of supplied handlers. * The computation is optimized for the case in which the two trees have large amounts of shared data * (obtained by calling the `clone` or `with` APIs) and will avoid any iteration of shared state. * The handlers can cause computation to early exit by returning `{ break: R }`. * Neither collection should be mutated during the comparison (inside your callbacks), as this method assumes they remain stable. * @param other The tree to compute a diff against. * @param onlyThis Callback invoked for all keys only present in `this`. * @param onlyOther Callback invoked for all keys only present in `other`. * @param different Callback invoked for all keys with differing values. * @returns The first `break` payload returned by a handler, or `undefined` if no handler breaks. * @throws Error if the supplied trees were created with different comparators. */ diffAgainst(other: BTree, onlyThis?: (k: K, v: V) => { break?: R; } | void, onlyOther?: (k: K, v: V) => { break?: R; } | void, different?: (k: K, vThis: V, vOther: V) => { break?: R; } | void): R | undefined; /** * Calls the supplied `callback` for each key/value pair shared by this tree and `other`, in sorted key order. * Neither tree is modified. * * Complexity is O(N + M) when the trees overlap heavily, and additionally bounded by O(log(N + M) * D) * where `D` is the number of disjoint key ranges between the trees, because disjoint subtrees are skipped. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param other The other tree to compare with this one. * @param callback Called for keys that appear in both trees. It can cause iteration to early exit by returning `{ break: R }`. * @returns The first `break` payload returned by the callback, or `undefined` if the walk finishes. * @throws Error if the two trees were created with different comparators. */ forEachKeyInBoth(other: BTree, callback: (key: K, leftValue: V, rightValue: V) => { break?: R; } | void): R | undefined; /** * Calls the supplied `callback` for each key/value pair that exists in this tree but not in `other` * (set subtraction). The callback runs in sorted key order and neither tree is modified. * * Complexity is O(N + M) when the key ranges overlap heavily, and additionally bounded by O(log(N + M) * D) * where `D` is the number of disjoint ranges between the trees, because non-overlapping subtrees are skipped. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param other Keys present in this tree will be omitted from the callback. * @param callback Invoked for keys unique to `this`. It can cause iteration to early exit by returning `{ break: R }`. * @returns The first `break` payload returned by the callback, or `undefined` if all qualifying keys are visited. * @throws Error if the trees were created with different comparators. */ forEachKeyNotIn(other: BTree, callback: (key: K, value: V) => { break?: R; } | void): R | undefined; /** * Returns a new tree containing only keys present in both trees. * Neither tree is modified. * * Complexity is O(N + M) in the fully overlapping case and additionally bounded by O(log(N + M) * D), * where `D` is the number of disjoint key ranges, because disjoint subtrees are skipped entirely. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param other The other tree to intersect with this one. * @param combineFn Called for keys that appear in both trees. Return the desired value. * @returns A new `BTreeEx` populated with the intersection. * @throws Error if the trees were created with different comparators. */ intersect(other: BTreeEx, combineFn: (key: K, leftValue: V, rightValue: V) => V): BTreeEx; /** * Efficiently unions this tree with `other`, reusing subtrees wherever possible without modifying either input. * * Complexity is O(N + M) in the fully overlapping case, and additionally bounded by O(log(N + M) * D) * where `D` is the number of disjoint key ranges, because disjoint subtrees are skipped entirely. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param other The other tree to union with this one. * @param combineFn Called for keys that appear in both trees. Return the desired value, or `undefined` to omit the key. * @returns A new `BTreeEx` that contains the unioned key/value pairs. * @throws Error if the trees were created with different comparators or max node sizes. */ union(other: BTreeEx, combineFn: (key: K, leftValue: V, rightValue: V) => V | undefined): BTreeEx; /** * Returns a new tree containing only the keys that are present in this tree but not `other` (set subtraction). * Neither input tree is modified. * * Complexity is O(N + M) for time and O(N) for allocations in the worst case. Additionally, time is bounded by * O(log(N + M) * D1) and space by O(log N * D2) where `D1` is the number of disjoint key ranges between the trees * and `D2` is the number of disjoint ranges inside this tree. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param other The tree whose keys will be removed from the result. * @returns A new `BTreeEx` representing `this \ other`. * @throws Error if the trees were created with different comparators or max node sizes. */ subtract(other: BTreeEx): BTreeEx; } export interface BTreeEx { /** See {@link BTree.with}. */ with(key: K): BTreeEx; with(key: K, value: V2, overwrite?: boolean): BTreeEx; with(key: K, value?: V2, overwrite?: boolean): BTreeEx; /** See {@link BTree.withPairs}. */ withPairs(pairs: [K, V | V2][], overwrite: boolean): BTreeEx; /** See {@link BTree.withKeys}. */ withKeys(keys: K[], returnThisIfUnchanged?: boolean): BTreeEx; /** See {@link BTree.mapValues}. */ mapValues(callback: (v: V, k: K, counter: number) => R): BTreeEx; } export default BTreeEx; ================================================ FILE: extended/index.js ================================================ "use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BTreeEx = void 0; var b_tree_1 = __importStar(require("../b+tree")); var diffAgainst_1 = __importDefault(require("./diffAgainst")); var forEachKeyInBoth_1 = __importDefault(require("./forEachKeyInBoth")); var forEachKeyNotIn_1 = __importDefault(require("./forEachKeyNotIn")); var intersect_1 = __importDefault(require("./intersect")); var subtract_1 = __importDefault(require("./subtract")); var union_1 = __importDefault(require("./union")); var bulkLoad_1 = require("./bulkLoad"); /** * An extended version of the `BTree` class that includes additional functionality * such as bulk loading, set operations, and diffing. * It is separated to keep the core BTree class small from a bundle size perspective. * Note: each additional functionality piece is available as a standalone function from the extended folder. * @extends BTree */ var BTreeEx = /** @class */ (function (_super) { __extends(BTreeEx, _super); function BTreeEx() { return _super !== null && _super.apply(this, arguments) || this; } /** * Bulk loads a new `BTreeEx` from parallel arrays of sorted entries. * This reuses the same algorithm as `extended/bulkLoad`, but produces a `BTreeEx`. * Time and space complexity are O(n). * @param keys Keys to load, sorted by key in strictly ascending order. * @param values Values aligned with the supplied keys. * @param maxNodeSize The branching factor (maximum number of children per node). * @param compare Comparator to use. Defaults to the standard comparator if omitted. * @returns A fully built tree containing the supplied entries. * @throws Error if the entries are not strictly sorted or contain duplicate keys. */ BTreeEx.bulkLoad = function (keys, values, maxNodeSize, compare) { var cmp = compare !== null && compare !== void 0 ? compare : b_tree_1.defaultComparator; var root = (0, bulkLoad_1.bulkLoadRoot)(keys, values, maxNodeSize, cmp); var tree = new BTreeEx(undefined, cmp, maxNodeSize); var target = tree; target._root = root; return tree; }; /** See {@link BTree.clone}. */ BTreeEx.prototype.clone = function () { var source = this; source._root.isShared = true; var result = new BTreeEx(undefined, this._compare, this._maxNodeSize); var target = result; target._root = source._root; return result; }; /** See {@link BTree.greedyClone}. */ BTreeEx.prototype.greedyClone = function (force) { var source = this; var result = new BTreeEx(undefined, this._compare, this._maxNodeSize); var target = result; target._root = source._root.greedyClone(force); return result; }; /** * Computes the differences between `this` and `other`. * For efficiency, the diff is returned via invocations of supplied handlers. * The computation is optimized for the case in which the two trees have large amounts of shared data * (obtained by calling the `clone` or `with` APIs) and will avoid any iteration of shared state. * The handlers can cause computation to early exit by returning `{ break: R }`. * Neither collection should be mutated during the comparison (inside your callbacks), as this method assumes they remain stable. * @param other The tree to compute a diff against. * @param onlyThis Callback invoked for all keys only present in `this`. * @param onlyOther Callback invoked for all keys only present in `other`. * @param different Callback invoked for all keys with differing values. * @returns The first `break` payload returned by a handler, or `undefined` if no handler breaks. * @throws Error if the supplied trees were created with different comparators. */ BTreeEx.prototype.diffAgainst = function (other, onlyThis, onlyOther, different) { return (0, diffAgainst_1.default)(this, other, onlyThis, onlyOther, different); }; /** * Calls the supplied `callback` for each key/value pair shared by this tree and `other`, in sorted key order. * Neither tree is modified. * * Complexity is O(N + M) when the trees overlap heavily, and additionally bounded by O(log(N + M) * D) * where `D` is the number of disjoint key ranges between the trees, because disjoint subtrees are skipped. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param other The other tree to compare with this one. * @param callback Called for keys that appear in both trees. It can cause iteration to early exit by returning `{ break: R }`. * @returns The first `break` payload returned by the callback, or `undefined` if the walk finishes. * @throws Error if the two trees were created with different comparators. */ BTreeEx.prototype.forEachKeyInBoth = function (other, callback) { return (0, forEachKeyInBoth_1.default)(this, other, callback); }; /** * Calls the supplied `callback` for each key/value pair that exists in this tree but not in `other` * (set subtraction). The callback runs in sorted key order and neither tree is modified. * * Complexity is O(N + M) when the key ranges overlap heavily, and additionally bounded by O(log(N + M) * D) * where `D` is the number of disjoint ranges between the trees, because non-overlapping subtrees are skipped. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param other Keys present in this tree will be omitted from the callback. * @param callback Invoked for keys unique to `this`. It can cause iteration to early exit by returning `{ break: R }`. * @returns The first `break` payload returned by the callback, or `undefined` if all qualifying keys are visited. * @throws Error if the trees were created with different comparators. */ BTreeEx.prototype.forEachKeyNotIn = function (other, callback) { return (0, forEachKeyNotIn_1.default)(this, other, callback); }; /** * Returns a new tree containing only keys present in both trees. * Neither tree is modified. * * Complexity is O(N + M) in the fully overlapping case and additionally bounded by O(log(N + M) * D), * where `D` is the number of disjoint key ranges, because disjoint subtrees are skipped entirely. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param other The other tree to intersect with this one. * @param combineFn Called for keys that appear in both trees. Return the desired value. * @returns A new `BTreeEx` populated with the intersection. * @throws Error if the trees were created with different comparators. */ BTreeEx.prototype.intersect = function (other, combineFn) { return (0, intersect_1.default)(this, other, combineFn); }; /** * Efficiently unions this tree with `other`, reusing subtrees wherever possible without modifying either input. * * Complexity is O(N + M) in the fully overlapping case, and additionally bounded by O(log(N + M) * D) * where `D` is the number of disjoint key ranges, because disjoint subtrees are skipped entirely. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param other The other tree to union with this one. * @param combineFn Called for keys that appear in both trees. Return the desired value, or `undefined` to omit the key. * @returns A new `BTreeEx` that contains the unioned key/value pairs. * @throws Error if the trees were created with different comparators or max node sizes. */ BTreeEx.prototype.union = function (other, combineFn) { return (0, union_1.default)(this, other, combineFn); }; /** * Returns a new tree containing only the keys that are present in this tree but not `other` (set subtraction). * Neither input tree is modified. * * Complexity is O(N + M) for time and O(N) for allocations in the worst case. Additionally, time is bounded by * O(log(N + M) * D1) and space by O(log N * D2) where `D1` is the number of disjoint key ranges between the trees * and `D2` is the number of disjoint ranges inside this tree. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param other The tree whose keys will be removed from the result. * @returns A new `BTreeEx` representing `this \ other`. * @throws Error if the trees were created with different comparators or max node sizes. */ BTreeEx.prototype.subtract = function (other) { return (0, subtract_1.default)(this, other); }; return BTreeEx; }(b_tree_1.default)); exports.BTreeEx = BTreeEx; exports.default = BTreeEx; ================================================ FILE: extended/index.ts ================================================ import BTree, { defaultComparator } from '../b+tree'; import type { BTreeWithInternals } from './shared'; import diffAgainst from './diffAgainst'; import forEachKeyInBoth from './forEachKeyInBoth'; import forEachKeyNotIn from './forEachKeyNotIn'; import intersect from './intersect'; import subtract from './subtract'; import union from './union'; import { bulkLoadRoot } from './bulkLoad'; /** * An extended version of the `BTree` class that includes additional functionality * such as bulk loading, set operations, and diffing. * It is separated to keep the core BTree class small from a bundle size perspective. * Note: each additional functionality piece is available as a standalone function from the extended folder. * @extends BTree */ export class BTreeEx extends BTree { /** * Bulk loads a new `BTreeEx` from parallel arrays of sorted entries. * This reuses the same algorithm as `extended/bulkLoad`, but produces a `BTreeEx`. * Time and space complexity are O(n). * @param keys Keys to load, sorted by key in strictly ascending order. * @param values Values aligned with the supplied keys. * @param maxNodeSize The branching factor (maximum number of children per node). * @param compare Comparator to use. Defaults to the standard comparator if omitted. * @returns A fully built tree containing the supplied entries. * @throws Error if the entries are not strictly sorted or contain duplicate keys. */ static bulkLoad( keys: K[], values: V[], maxNodeSize: number, compare?: (a: K, b: K) => number ): BTreeEx { const cmp = compare ?? (defaultComparator as unknown as (a: K, b: K) => number); const root = bulkLoadRoot(keys, values, maxNodeSize, cmp); const tree = new BTreeEx(undefined, cmp, maxNodeSize); const target = tree as unknown as BTreeWithInternals; target._root = root; return tree; } /** See {@link BTree.clone}. */ clone(): this { const source = this as unknown as BTreeWithInternals; source._root.isShared = true; const result = new BTreeEx(undefined, this._compare, this._maxNodeSize); const target = result as unknown as BTreeWithInternals; target._root = source._root; return result as this; } /** See {@link BTree.greedyClone}. */ greedyClone(force?: boolean): this { const source = this as unknown as BTreeWithInternals; const result = new BTreeEx(undefined, this._compare, this._maxNodeSize); const target = result as unknown as BTreeWithInternals; target._root = source._root.greedyClone(force); return result as this; } /** * Computes the differences between `this` and `other`. * For efficiency, the diff is returned via invocations of supplied handlers. * The computation is optimized for the case in which the two trees have large amounts of shared data * (obtained by calling the `clone` or `with` APIs) and will avoid any iteration of shared state. * The handlers can cause computation to early exit by returning `{ break: R }`. * Neither collection should be mutated during the comparison (inside your callbacks), as this method assumes they remain stable. * @param other The tree to compute a diff against. * @param onlyThis Callback invoked for all keys only present in `this`. * @param onlyOther Callback invoked for all keys only present in `other`. * @param different Callback invoked for all keys with differing values. * @returns The first `break` payload returned by a handler, or `undefined` if no handler breaks. * @throws Error if the supplied trees were created with different comparators. */ diffAgainst( other: BTree, onlyThis?: (k: K, v: V) => { break?: R } | void, onlyOther?: (k: K, v: V) => { break?: R } | void, different?: (k: K, vThis: V, vOther: V) => { break?: R } | void ): R | undefined { return diffAgainst(this, other, onlyThis, onlyOther, different); } /** * Calls the supplied `callback` for each key/value pair shared by this tree and `other`, in sorted key order. * Neither tree is modified. * * Complexity is O(N + M) when the trees overlap heavily, and additionally bounded by O(log(N + M) * D) * where `D` is the number of disjoint key ranges between the trees, because disjoint subtrees are skipped. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param other The other tree to compare with this one. * @param callback Called for keys that appear in both trees. It can cause iteration to early exit by returning `{ break: R }`. * @returns The first `break` payload returned by the callback, or `undefined` if the walk finishes. * @throws Error if the two trees were created with different comparators. */ forEachKeyInBoth( other: BTree, callback: (key: K, leftValue: V, rightValue: V) => { break?: R } | void ): R | undefined { return forEachKeyInBoth(this, other, callback); } /** * Calls the supplied `callback` for each key/value pair that exists in this tree but not in `other` * (set subtraction). The callback runs in sorted key order and neither tree is modified. * * Complexity is O(N + M) when the key ranges overlap heavily, and additionally bounded by O(log(N + M) * D) * where `D` is the number of disjoint ranges between the trees, because non-overlapping subtrees are skipped. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param other Keys present in this tree will be omitted from the callback. * @param callback Invoked for keys unique to `this`. It can cause iteration to early exit by returning `{ break: R }`. * @returns The first `break` payload returned by the callback, or `undefined` if all qualifying keys are visited. * @throws Error if the trees were created with different comparators. */ forEachKeyNotIn( other: BTree, callback: (key: K, value: V) => { break?: R } | void ): R | undefined { return forEachKeyNotIn(this, other, callback); } /** * Returns a new tree containing only keys present in both trees. * Neither tree is modified. * * Complexity is O(N + M) in the fully overlapping case and additionally bounded by O(log(N + M) * D), * where `D` is the number of disjoint key ranges, because disjoint subtrees are skipped entirely. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param other The other tree to intersect with this one. * @param combineFn Called for keys that appear in both trees. Return the desired value. * @returns A new `BTreeEx` populated with the intersection. * @throws Error if the trees were created with different comparators. */ intersect(other: BTreeEx, combineFn: (key: K, leftValue: V, rightValue: V) => V): BTreeEx { return intersect, K, V>(this, other, combineFn); } /** * Efficiently unions this tree with `other`, reusing subtrees wherever possible without modifying either input. * * Complexity is O(N + M) in the fully overlapping case, and additionally bounded by O(log(N + M) * D) * where `D` is the number of disjoint key ranges, because disjoint subtrees are skipped entirely. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param other The other tree to union with this one. * @param combineFn Called for keys that appear in both trees. Return the desired value, or `undefined` to omit the key. * @returns A new `BTreeEx` that contains the unioned key/value pairs. * @throws Error if the trees were created with different comparators or max node sizes. */ union(other: BTreeEx, combineFn: (key: K, leftValue: V, rightValue: V) => V | undefined): BTreeEx { return union, K, V>(this, other, combineFn); } /** * Returns a new tree containing only the keys that are present in this tree but not `other` (set subtraction). * Neither input tree is modified. * * Complexity is O(N + M) for time and O(N) for allocations in the worst case. Additionally, time is bounded by * O(log(N + M) * D1) and space by O(log N * D2) where `D1` is the number of disjoint key ranges between the trees * and `D2` is the number of disjoint ranges inside this tree. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param other The tree whose keys will be removed from the result. * @returns A new `BTreeEx` representing `this \ other`. * @throws Error if the trees were created with different comparators or max node sizes. */ subtract(other: BTreeEx): BTreeEx { return subtract, K, V>(this, other); } } export interface BTreeEx { /** See {@link BTree.with}. */ with(key: K): BTreeEx; with(key: K, value: V2, overwrite?: boolean): BTreeEx; with(key: K, value?: V2, overwrite?: boolean): BTreeEx; /** See {@link BTree.withPairs}. */ withPairs(pairs: [K, V | V2][], overwrite: boolean): BTreeEx; /** See {@link BTree.withKeys}. */ withKeys(keys: K[], returnThisIfUnchanged?: boolean): BTreeEx; /** See {@link BTree.mapValues}. */ mapValues(callback: (v: V, k: K, counter: number) => R): BTreeEx; } export default BTreeEx; ================================================ FILE: extended/intersect.d.ts ================================================ import BTree from '../b+tree'; /** * Returns a new tree containing only keys present in both input trees. * Neither tree is modified. * * Complexity is O(N + M) in the fully overlapping case and additionally bounded by O(log(N + M) * D), * where `D` is the number of disjoint key ranges, because disjoint subtrees are skipped entirely. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param treeA First tree to intersect. * @param treeB Second tree to intersect. * @param combineFn Called for keys that appear in both trees. Return the desired value. * @returns A new tree populated with the intersection. * @throws Error if the trees were created with different comparators. */ export default function intersect, K, V>(treeA: TBTree, treeB: TBTree, combineFn: (key: K, leftValue: V, rightValue: V) => V): TBTree; ================================================ FILE: extended/intersect.js ================================================ "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); var shared_1 = require("./shared"); var forEachKeyInBoth_1 = __importDefault(require("./forEachKeyInBoth")); var bulkLoad_1 = require("./bulkLoad"); /** * Returns a new tree containing only keys present in both input trees. * Neither tree is modified. * * Complexity is O(N + M) in the fully overlapping case and additionally bounded by O(log(N + M) * D), * where `D` is the number of disjoint key ranges, because disjoint subtrees are skipped entirely. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param treeA First tree to intersect. * @param treeB Second tree to intersect. * @param combineFn Called for keys that appear in both trees. Return the desired value. * @returns A new tree populated with the intersection. * @throws Error if the trees were created with different comparators. */ function intersect(treeA, treeB, combineFn) { var _treeA = treeA; var _treeB = treeB; var branchingFactor = (0, shared_1.checkCanDoSetOperation)(_treeA, _treeB, true); if (_treeA._root.size() === 0) return treeA.clone(); if (_treeB._root.size() === 0) return treeB.clone(); var intersectedKeys = []; var intersectedValues = []; (0, forEachKeyInBoth_1.default)(treeA, treeB, function (key, leftValue, rightValue) { var mergedValue = combineFn(key, leftValue, rightValue); intersectedKeys.push(key); intersectedValues.push(mergedValue); }); // Intersected keys are guaranteed to be in order, so we can bulk load var constructor = treeA.constructor; var resultTree = new constructor(undefined, treeA._compare, branchingFactor); resultTree._root = (0, bulkLoad_1.bulkLoadRoot)(intersectedKeys, intersectedValues, branchingFactor, treeA._compare); return resultTree; } exports.default = intersect; ================================================ FILE: extended/intersect.ts ================================================ import BTree from '../b+tree'; import { checkCanDoSetOperation, type BTreeWithInternals, BTreeConstructor } from './shared'; import forEachKeyInBoth from './forEachKeyInBoth'; import { bulkLoadRoot } from './bulkLoad'; /** * Returns a new tree containing only keys present in both input trees. * Neither tree is modified. * * Complexity is O(N + M) in the fully overlapping case and additionally bounded by O(log(N + M) * D), * where `D` is the number of disjoint key ranges, because disjoint subtrees are skipped entirely. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param treeA First tree to intersect. * @param treeB Second tree to intersect. * @param combineFn Called for keys that appear in both trees. Return the desired value. * @returns A new tree populated with the intersection. * @throws Error if the trees were created with different comparators. */ export default function intersect, K, V>( treeA: TBTree, treeB: TBTree, combineFn: (key: K, leftValue: V, rightValue: V) => V ): TBTree { const _treeA = treeA as unknown as BTreeWithInternals; const _treeB = treeB as unknown as BTreeWithInternals; const branchingFactor = checkCanDoSetOperation(_treeA, _treeB, true); if (_treeA._root.size() === 0) return treeA.clone(); if (_treeB._root.size() === 0) return treeB.clone(); const intersectedKeys: K[] = []; const intersectedValues: V[] = []; forEachKeyInBoth(treeA, treeB, (key, leftValue, rightValue) => { const mergedValue = combineFn(key, leftValue, rightValue); intersectedKeys.push(key); intersectedValues.push(mergedValue); }); // Intersected keys are guaranteed to be in order, so we can bulk load const constructor = treeA.constructor as BTreeConstructor; const resultTree = new constructor(undefined, treeA._compare, branchingFactor); resultTree._root = bulkLoadRoot(intersectedKeys, intersectedValues, branchingFactor, treeA._compare); return resultTree as unknown as TBTree; } ================================================ FILE: extended/parallelWalk.d.ts ================================================ export {}; ================================================ FILE: extended/parallelWalk.js ================================================ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.noop = exports.moveTo = exports.getKey = exports.createCursor = exports.moveForwardOne = void 0; /** * Walks the cursor forward by one key. * Returns true if end-of-tree was reached (cursor not structurally mutated). * Optimized for this case over the more general `moveTo` function. * @internal */ function moveForwardOne(cur, other) { var leaf = cur.leaf; var nextIndex = cur.leafIndex + 1; if (nextIndex < leaf.keys.length) { // Still within current leaf cur.onMoveInLeaf(leaf, cur.leafPayload, cur.leafIndex, nextIndex, true); cur.leafIndex = nextIndex; return false; } // If our optimized step within leaf failed, use full moveTo logic // Pass isInclusive=false to ensure we walk forward to the key exactly after the current return moveTo(cur, other, getKey(cur), false, true)[0]; } exports.moveForwardOne = moveForwardOne; /** * Create a cursor pointing to the leftmost key of the supplied tree. * @internal */ function createCursor(tree, makePayload, onEnterLeaf, onMoveInLeaf, onExitLeaf, onStepUp, onStepDown) { var spine = []; var n = tree._root; while (!n.isLeaf) { var ni = n; var payload = makePayload(); spine.push({ node: ni, childIndex: 0, payload: payload }); n = ni.children[0]; } var leafPayload = makePayload(); var cur = { tree: tree, leaf: n, leafIndex: 0, spine: spine, leafPayload: leafPayload, makePayload: makePayload, onEnterLeaf: onEnterLeaf, onMoveInLeaf: onMoveInLeaf, onExitLeaf: onExitLeaf, onStepUp: onStepUp, onStepDown: onStepDown }; return cur; } exports.createCursor = createCursor; /** * Gets the key at the current cursor position. * @internal */ function getKey(c) { return c.leaf.keys[c.leafIndex]; } exports.getKey = getKey; /** * Move cursor strictly forward to the first key >= (inclusive) or > (exclusive) target. * Returns a boolean indicating if end-of-tree was reached (cursor not structurally mutated). * Also returns a boolean indicating if the target key was landed on exactly. * @internal */ function moveTo(cur, other, targetKey, isInclusive, startedEqual) { // Cache for perf var cmp = cur.tree._compare; var onMoveInLeaf = cur.onMoveInLeaf; // Fast path: destination within current leaf var leaf = cur.leaf; var leafPayload = cur.leafPayload; var i = leaf.indexOf(targetKey, -1, cmp); var destInLeaf; var targetExactlyReached; if (i < 0) { destInLeaf = ~i; targetExactlyReached = false; } else { if (isInclusive) { destInLeaf = i; targetExactlyReached = true; } else { destInLeaf = i + 1; targetExactlyReached = false; } } var leafKeyCount = leaf.keys.length; if (destInLeaf < leafKeyCount) { onMoveInLeaf(leaf, leafPayload, cur.leafIndex, destInLeaf, startedEqual); cur.leafIndex = destInLeaf; return [false, targetExactlyReached]; } // Find first ancestor with a viable right step var spine = cur.spine; var initialSpineLength = spine.length; var descentLevel = -1; var descentIndex = -1; for (var s = initialSpineLength - 1; s >= 0; s--) { var parent = spine[s].node; var indexOf = parent.indexOf(targetKey, -1, cmp); var stepDownIndex = void 0; if (indexOf < 0) { stepDownIndex = ~indexOf; } else { stepDownIndex = isInclusive ? indexOf : indexOf + 1; } // Note: when key not found, indexOf with failXor=0 already returns insertion index if (stepDownIndex < parent.keys.length) { descentLevel = s; descentIndex = stepDownIndex; break; } } // Exit leaf; even if no spine, we did walk out of it conceptually var startIndex = cur.leafIndex; cur.onExitLeaf(leaf, leafPayload, startIndex, startedEqual, cur); var onStepUp = cur.onStepUp; if (descentLevel < 0) { // No descent point; step up all the way; last callback gets infinity for (var depth = initialSpineLength - 1; depth >= 0; depth--) { var entry_1 = spine[depth]; var sd = depth === 0 ? Number.POSITIVE_INFINITY : Number.NaN; onStepUp(entry_1.node, initialSpineLength - depth, entry_1.payload, entry_1.childIndex, depth, sd, cur, other); } return [true, false]; } // Step up through ancestors above the descentLevel for (var depth = initialSpineLength - 1; depth > descentLevel; depth--) { var entry_2 = spine[depth]; onStepUp(entry_2.node, initialSpineLength - depth, entry_2.payload, entry_2.childIndex, depth, Number.NaN, cur, other); } var entry = spine[descentLevel]; onStepUp(entry.node, initialSpineLength - descentLevel, entry.payload, entry.childIndex, descentLevel, descentIndex, cur, other); entry.childIndex = descentIndex; var onStepDown = cur.onStepDown; var makePayload = cur.makePayload; // Descend, invoking onStepDown and creating payloads var height = initialSpineLength - descentLevel - 1; // calculate height before changing length spine.length = descentLevel + 1; var node = spine[descentLevel].node.children[descentIndex]; while (!node.isLeaf) { var ni = node; var keys = ni.keys; var stepDownIndex = ni.indexOf(targetKey, 0, cmp); if (!isInclusive && stepDownIndex < keys.length && cmp(keys[stepDownIndex], targetKey) === 0) stepDownIndex++; var payload = makePayload(); var spineIndex = spine.length; spine.push({ node: ni, childIndex: stepDownIndex, payload: payload }); onStepDown(ni, height, spineIndex, stepDownIndex, cur, other); node = ni.children[stepDownIndex]; height -= 1; } // Enter destination leaf var idx = node.indexOf(targetKey, -1, cmp); var destIndex; if (idx < 0) { destIndex = ~idx; targetExactlyReached = false; } else { if (isInclusive) { destIndex = idx; targetExactlyReached = true; } else { destIndex = idx + 1; targetExactlyReached = false; } } cur.leaf = node; cur.leafPayload = makePayload(); cur.leafIndex = destIndex; cur.onEnterLeaf(node, destIndex, cur, other); return [false, targetExactlyReached]; } exports.moveTo = moveTo; /** * A no-operation function. * @internal */ function noop() { } exports.noop = noop; ================================================ FILE: extended/parallelWalk.ts ================================================ import { BNode, BNodeInternal } from '../b+tree'; import type { BTreeWithInternals } from './shared'; /** * A walkable cursor for BTree set operations. * @internal */ export interface Cursor { tree: BTreeWithInternals; leaf: BNode; leafIndex: number; spine: Array<{ node: BNodeInternal, childIndex: number, payload: TPayload }>; leafPayload: TPayload; makePayload: () => TPayload; onMoveInLeaf: (leaf: BNode, payload: TPayload, fromIndex: number, toIndex: number, isInclusive: boolean) => void; onExitLeaf: (leaf: BNode, payload: TPayload, startingIndex: number, isInclusive: boolean, cursorThis: Cursor) => void; onStepUp: (parent: BNodeInternal, height: number, payload: TPayload, fromIndex: number, spineIndex: number, stepDownIndex: number, cursorThis: Cursor, cursorOther: Cursor) => void; onStepDown: (node: BNodeInternal, height: number, spineIndex: number, stepDownIndex: number, cursorThis: Cursor, cursorOther: Cursor) => void; onEnterLeaf: (leaf: BNode, destIndex: number, cursorThis: Cursor, cursorOther: Cursor) => void; } /** * Walks the cursor forward by one key. * Returns true if end-of-tree was reached (cursor not structurally mutated). * Optimized for this case over the more general `moveTo` function. * @internal */ export function moveForwardOne( cur: Cursor, other: Cursor ): boolean { const leaf = cur.leaf; const nextIndex = cur.leafIndex + 1; if (nextIndex < leaf.keys.length) { // Still within current leaf cur.onMoveInLeaf(leaf, cur.leafPayload, cur.leafIndex, nextIndex, true); cur.leafIndex = nextIndex; return false; } // If our optimized step within leaf failed, use full moveTo logic // Pass isInclusive=false to ensure we walk forward to the key exactly after the current return moveTo(cur, other, getKey(cur), false, true)[0]; } /** * Create a cursor pointing to the leftmost key of the supplied tree. * @internal */ export function createCursor( tree: BTreeWithInternals, makePayload: Cursor["makePayload"], onEnterLeaf: Cursor["onEnterLeaf"], onMoveInLeaf: Cursor["onMoveInLeaf"], onExitLeaf: Cursor["onExitLeaf"], onStepUp: Cursor["onStepUp"], onStepDown: Cursor["onStepDown"], ): Cursor { const spine: Array<{ node: BNodeInternal, childIndex: number, payload: TP }> = []; let n: BNode = tree._root; while (!n.isLeaf) { const ni = n as BNodeInternal; const payload = makePayload(); spine.push({ node: ni, childIndex: 0, payload }); n = ni.children[0]; } const leafPayload = makePayload(); const cur: Cursor = { tree, leaf: n, leafIndex: 0, spine, leafPayload, makePayload: makePayload, onEnterLeaf, onMoveInLeaf, onExitLeaf, onStepUp, onStepDown }; return cur; } /** * Gets the key at the current cursor position. * @internal */ export function getKey(c: Cursor): K { return c.leaf.keys[c.leafIndex]; } /** * Move cursor strictly forward to the first key >= (inclusive) or > (exclusive) target. * Returns a boolean indicating if end-of-tree was reached (cursor not structurally mutated). * Also returns a boolean indicating if the target key was landed on exactly. * @internal */ export function moveTo( cur: Cursor, other: Cursor, targetKey: K, isInclusive: boolean, startedEqual: boolean, ): [outOfTree: boolean, targetExactlyReached: boolean] { // Cache for perf const cmp = cur.tree._compare const onMoveInLeaf = cur.onMoveInLeaf; // Fast path: destination within current leaf const leaf = cur.leaf; const leafPayload = cur.leafPayload; const i = leaf.indexOf(targetKey, -1, cmp); let destInLeaf: number; let targetExactlyReached: boolean; if (i < 0) { destInLeaf = ~i; targetExactlyReached = false; } else { if (isInclusive) { destInLeaf = i; targetExactlyReached = true; } else { destInLeaf = i + 1; targetExactlyReached = false; } } const leafKeyCount = leaf.keys.length; if (destInLeaf < leafKeyCount) { onMoveInLeaf(leaf, leafPayload, cur.leafIndex, destInLeaf, startedEqual); cur.leafIndex = destInLeaf; return [false, targetExactlyReached]; } // Find first ancestor with a viable right step const spine = cur.spine; const initialSpineLength = spine.length; let descentLevel = -1; let descentIndex = -1; for (let s = initialSpineLength - 1; s >= 0; s--) { const parent = spine[s].node; const indexOf = parent.indexOf(targetKey, -1, cmp); let stepDownIndex: number; if (indexOf < 0) { stepDownIndex = ~indexOf; } else { stepDownIndex = isInclusive ? indexOf : indexOf + 1; } // Note: when key not found, indexOf with failXor=0 already returns insertion index if (stepDownIndex < parent.keys.length) { descentLevel = s; descentIndex = stepDownIndex; break; } } // Exit leaf; even if no spine, we did walk out of it conceptually const startIndex = cur.leafIndex; cur.onExitLeaf(leaf, leafPayload, startIndex, startedEqual, cur); const onStepUp = cur.onStepUp; if (descentLevel < 0) { // No descent point; step up all the way; last callback gets infinity for (let depth = initialSpineLength - 1; depth >= 0; depth--) { const entry = spine[depth]; const sd = depth === 0 ? Number.POSITIVE_INFINITY : Number.NaN; onStepUp(entry.node, initialSpineLength - depth, entry.payload, entry.childIndex, depth, sd, cur, other); } return [true, false]; } // Step up through ancestors above the descentLevel for (let depth = initialSpineLength - 1; depth > descentLevel; depth--) { const entry = spine[depth]; onStepUp(entry.node, initialSpineLength - depth, entry.payload, entry.childIndex, depth, Number.NaN, cur, other); } const entry = spine[descentLevel]; onStepUp(entry.node, initialSpineLength - descentLevel, entry.payload, entry.childIndex, descentLevel, descentIndex, cur, other); entry.childIndex = descentIndex; const onStepDown = cur.onStepDown; const makePayload = cur.makePayload; // Descend, invoking onStepDown and creating payloads let height = initialSpineLength - descentLevel - 1; // calculate height before changing length spine.length = descentLevel + 1; let node: BNode = spine[descentLevel].node.children[descentIndex]; while (!node.isLeaf) { const ni = node as BNodeInternal; const keys = ni.keys; let stepDownIndex = ni.indexOf(targetKey, 0, cmp); if (!isInclusive && stepDownIndex < keys.length && cmp(keys[stepDownIndex], targetKey) === 0) stepDownIndex++; const payload = makePayload(); const spineIndex = spine.length; spine.push({ node: ni, childIndex: stepDownIndex, payload }); onStepDown(ni, height, spineIndex, stepDownIndex, cur, other); node = ni.children[stepDownIndex]; height -= 1; } // Enter destination leaf const idx = node.indexOf(targetKey, -1, cmp); let destIndex: number; if (idx < 0) { destIndex = ~idx; targetExactlyReached = false; } else { if (isInclusive) { destIndex = idx; targetExactlyReached = true; } else { destIndex = idx + 1; targetExactlyReached = false; } } cur.leaf = node; cur.leafPayload = makePayload(); cur.leafIndex = destIndex; cur.onEnterLeaf(node, destIndex, cur, other); return [false, targetExactlyReached]; } /** * A no-operation function. * @internal */ export function noop(): void { } ================================================ FILE: extended/shared.d.ts ================================================ export {}; ================================================ FILE: extended/shared.js ================================================ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.checkCanDoSetOperation = exports.branchingFactorErrorMsg = exports.comparatorErrorMsg = exports.makeLeavesFrom = void 0; var b_tree_1 = require("../b+tree"); /** * Builds leaves from the given parallel arrays of entries. * The supplied load factor will be respected if possible, but may be exceeded * to ensure the 50% full rule is maintained. * Note: if < maxNodeSize entries are provided, only one leaf will be created, which may be underfilled. * @param keys The list of keys to build leaves from. * @param values The list of values to build leaves from. * @param maxNodeSize The maximum node size (branching factor) for the resulting leaves. * @param onLeafCreation Called when a new leaf is created. * @param loadFactor Desired load factor for created leaves. Must be between 0.5 and 1.0. * @internal */ function makeLeavesFrom(keys, values, maxNodeSize, loadFactor, onLeafCreation) { if (keys.length !== values.length) throw new Error("makeLeavesFrom: keys and values arrays must be the same length"); var totalPairs = keys.length; if (totalPairs === 0) return 0; var targetSize = Math.ceil(maxNodeSize * loadFactor); // This method creates as many evenly filled leaves as possible from // the pending entries. All will be > 50% full if we are creating more than one leaf. var remaining = totalPairs; var pairIndex = 0; var remainingLeaves = totalPairs <= maxNodeSize ? 1 : Math.ceil(totalPairs / targetSize); for (; remainingLeaves > 0; remainingLeaves--) { var chunkSize = Math.ceil(remaining / remainingLeaves); var nextIndex = pairIndex + chunkSize; var chunkKeys = keys.slice(pairIndex, nextIndex); var chunkVals = values.slice(pairIndex, nextIndex); pairIndex = nextIndex; remaining -= chunkSize; var leaf = new b_tree_1.BNode(chunkKeys, chunkVals); onLeafCreation(leaf); } } exports.makeLeavesFrom = makeLeavesFrom; ; /** * Error message used when comparators differ between trees. * @internal */ exports.comparatorErrorMsg = "Cannot perform set operations on BTrees with different comparators."; /** * Error message used when branching factors differ between trees. * @internal */ exports.branchingFactorErrorMsg = "Cannot perform set operations on BTrees with different max node sizes."; /** * Checks that two trees can be used together in a set operation. * @internal */ function checkCanDoSetOperation(treeA, treeB, supportsDifferentBranchingFactors) { if (treeA._compare !== treeB._compare) throw new Error(exports.comparatorErrorMsg); var branchingFactor = treeA._maxNodeSize; if (!supportsDifferentBranchingFactors && branchingFactor !== treeB._maxNodeSize) throw new Error(exports.branchingFactorErrorMsg); return branchingFactor; } exports.checkCanDoSetOperation = checkCanDoSetOperation; ================================================ FILE: extended/shared.ts ================================================ import BTree, { BNode } from '../b+tree'; /** * BTree with access to internal properties. * @internal */ export type BTreeWithInternals = BTree> = { _root: BNode; _maxNodeSize: number; _compare: (a: K, b: K) => number; } & Omit; /** * Builds leaves from the given parallel arrays of entries. * The supplied load factor will be respected if possible, but may be exceeded * to ensure the 50% full rule is maintained. * Note: if < maxNodeSize entries are provided, only one leaf will be created, which may be underfilled. * @param keys The list of keys to build leaves from. * @param values The list of values to build leaves from. * @param maxNodeSize The maximum node size (branching factor) for the resulting leaves. * @param onLeafCreation Called when a new leaf is created. * @param loadFactor Desired load factor for created leaves. Must be between 0.5 and 1.0. * @internal */ export function makeLeavesFrom( keys: K[], values: V[], maxNodeSize: number, loadFactor: number, onLeafCreation: (node: BNode) => void, ) { if (keys.length !== values.length) throw new Error("makeLeavesFrom: keys and values arrays must be the same length"); const totalPairs = keys.length; if (totalPairs === 0) return 0; const targetSize = Math.ceil(maxNodeSize * loadFactor); // This method creates as many evenly filled leaves as possible from // the pending entries. All will be > 50% full if we are creating more than one leaf. let remaining = totalPairs; let pairIndex = 0; let remainingLeaves = totalPairs <= maxNodeSize ? 1 : Math.ceil(totalPairs / targetSize); for (; remainingLeaves > 0; remainingLeaves--) { const chunkSize = Math.ceil(remaining / remainingLeaves); const nextIndex = pairIndex + chunkSize; const chunkKeys = keys.slice(pairIndex, nextIndex); const chunkVals = values.slice(pairIndex, nextIndex); pairIndex = nextIndex; remaining -= chunkSize; const leaf = new BNode(chunkKeys, chunkVals); onLeafCreation(leaf); } }; /** * Error message used when comparators differ between trees. * @internal */ export const comparatorErrorMsg = "Cannot perform set operations on BTrees with different comparators."; /** * Error message used when branching factors differ between trees. * @internal */ export const branchingFactorErrorMsg = "Cannot perform set operations on BTrees with different max node sizes."; /** * Checks that two trees can be used together in a set operation. * @internal */ export function checkCanDoSetOperation(treeA: BTreeWithInternals, treeB: BTreeWithInternals, supportsDifferentBranchingFactors: boolean): number { if (treeA._compare !== treeB._compare) throw new Error(comparatorErrorMsg); const branchingFactor = treeA._maxNodeSize; if (!supportsDifferentBranchingFactors && branchingFactor !== treeB._maxNodeSize) throw new Error(branchingFactorErrorMsg); return branchingFactor; } /** * Helper constructor signature used by set-operation helpers to create a result tree that preserves the input subtype. * @internal */ export type BTreeConstructor, K, V> = new (entries?: [K, V][], compare?: (a: K, b: K) => number, maxNodeSize?: number) => BTreeWithInternals; ================================================ FILE: extended/subtract.d.ts ================================================ import BTree from '../b+tree'; /** * Returns a new tree containing only the keys that are present in `targetTree` but not `subtractTree` (set subtraction). * Neither tree is modified. * * Complexity is O(N + M) for time and O(N) for allocations in the worst case. Additionally, time is bounded by * O(log(N + M) * D1) and space by O(log N * D2), where `D1` is the number of disjoint key ranges between the trees * and `D2` is the number of disjoint ranges inside `targetTree`, because disjoint subtrees are skipped entirely. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param targetTree The tree to subtract from. * @param subtractTree The tree whose keys will be removed from the result. * @returns A new tree that contains the subtraction result. * @throws Error if the trees were created with different comparators or max node sizes. */ export default function subtract, K, V>(targetTree: TBTree, subtractTree: TBTree): TBTree; ================================================ FILE: extended/subtract.js ================================================ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var shared_1 = require("./shared"); var decompose_1 = require("./decompose"); /** * Returns a new tree containing only the keys that are present in `targetTree` but not `subtractTree` (set subtraction). * Neither tree is modified. * * Complexity is O(N + M) for time and O(N) for allocations in the worst case. Additionally, time is bounded by * O(log(N + M) * D1) and space by O(log N * D2), where `D1` is the number of disjoint key ranges between the trees * and `D2` is the number of disjoint ranges inside `targetTree`, because disjoint subtrees are skipped entirely. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param targetTree The tree to subtract from. * @param subtractTree The tree whose keys will be removed from the result. * @returns A new tree that contains the subtraction result. * @throws Error if the trees were created with different comparators or max node sizes. */ function subtract(targetTree, subtractTree) { var _targetTree = targetTree; var _subtractTree = subtractTree; var branchingFactor = (0, shared_1.checkCanDoSetOperation)(_targetTree, _subtractTree, false); if (_targetTree._root.size() === 0 || _subtractTree._root.size() === 0) return targetTree.clone(); // Decompose target tree into disjoint subtrees leaves. // As many of these as possible will be reused from the original trees, and the remaining // will be leaves that are exploded (and filtered) due to intersecting leaves in subtractTree. var decomposed = (0, decompose_1.decompose)(_targetTree, _subtractTree, function () { return undefined; }, true); var constructor = targetTree.constructor; if (decomposed.heights.length === 0) { return new constructor(undefined, targetTree._compare, targetTree._maxNodeSize); } return (0, decompose_1.buildFromDecomposition)(constructor, branchingFactor, decomposed, targetTree._compare, targetTree._maxNodeSize); } exports.default = subtract; ================================================ FILE: extended/subtract.ts ================================================ import BTree from '../b+tree'; import { checkCanDoSetOperation, type BTreeWithInternals, BTreeConstructor } from './shared'; import { buildFromDecomposition, decompose } from './decompose'; /** * Returns a new tree containing only the keys that are present in `targetTree` but not `subtractTree` (set subtraction). * Neither tree is modified. * * Complexity is O(N + M) for time and O(N) for allocations in the worst case. Additionally, time is bounded by * O(log(N + M) * D1) and space by O(log N * D2), where `D1` is the number of disjoint key ranges between the trees * and `D2` is the number of disjoint ranges inside `targetTree`, because disjoint subtrees are skipped entirely. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param targetTree The tree to subtract from. * @param subtractTree The tree whose keys will be removed from the result. * @returns A new tree that contains the subtraction result. * @throws Error if the trees were created with different comparators or max node sizes. */ export default function subtract, K, V>( targetTree: TBTree, subtractTree: TBTree ): TBTree { const _targetTree = targetTree as unknown as BTreeWithInternals; const _subtractTree = subtractTree as unknown as BTreeWithInternals; const branchingFactor = checkCanDoSetOperation(_targetTree, _subtractTree, false); if (_targetTree._root.size() === 0 || _subtractTree._root.size() === 0) return targetTree.clone(); // Decompose target tree into disjoint subtrees leaves. // As many of these as possible will be reused from the original trees, and the remaining // will be leaves that are exploded (and filtered) due to intersecting leaves in subtractTree. const decomposed = decompose(_targetTree, _subtractTree, () => undefined, true); const constructor = targetTree.constructor as BTreeConstructor; if (decomposed.heights.length === 0) { return new constructor(undefined, targetTree._compare, targetTree._maxNodeSize) as unknown as TBTree; } return buildFromDecomposition(constructor, branchingFactor, decomposed, targetTree._compare, targetTree._maxNodeSize); } ================================================ FILE: extended/union.d.ts ================================================ import BTree from '../b+tree'; /** * Efficiently unions two trees, reusing subtrees wherever possible without mutating either input. * * Complexity is O(N + M) when the trees overlap heavily, and additionally bounded by O(log(N + M) * D) * where `D` is the number of disjoint key ranges, because disjoint subtrees are skipped entirely. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param treeA First tree to union. * @param treeB Second tree to union. * @param combineFn Called for keys that appear in both trees. Return the desired value, or * `undefined` to omit the key from the result. Note: symmetric difference can be achieved by always returning `undefined`. * @returns A new BTree that contains the unioned key/value pairs. * @throws Error if the trees were created with different comparators or max node sizes. */ export default function union, K, V>(treeA: TBTree, treeB: TBTree, combineFn: (key: K, leftValue: V, rightValue: V) => V | undefined): TBTree; ================================================ FILE: extended/union.js ================================================ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var shared_1 = require("./shared"); var decompose_1 = require("./decompose"); /** * Efficiently unions two trees, reusing subtrees wherever possible without mutating either input. * * Complexity is O(N + M) when the trees overlap heavily, and additionally bounded by O(log(N + M) * D) * where `D` is the number of disjoint key ranges, because disjoint subtrees are skipped entirely. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param treeA First tree to union. * @param treeB Second tree to union. * @param combineFn Called for keys that appear in both trees. Return the desired value, or * `undefined` to omit the key from the result. Note: symmetric difference can be achieved by always returning `undefined`. * @returns A new BTree that contains the unioned key/value pairs. * @throws Error if the trees were created with different comparators or max node sizes. */ function union(treeA, treeB, combineFn) { if (treeA === treeB) return treeA.clone(); var _treeA = treeA; var _treeB = treeB; var branchingFactor = (0, shared_1.checkCanDoSetOperation)(_treeA, _treeB, false); if (_treeA._root.size() === 0) return treeB.clone(); if (_treeB._root.size() === 0) return treeA.clone(); // Decompose both trees into disjoint subtrees leaves. // As many of these as possible will be reused from the original trees, and the remaining // will be leaves that are the result of merging intersecting leaves. var decomposed = (0, decompose_1.decompose)(_treeA, _treeB, combineFn); var constructor = treeA.constructor; return (0, decompose_1.buildFromDecomposition)(constructor, branchingFactor, decomposed, _treeA._compare, _treeA._maxNodeSize); } exports.default = union; ================================================ FILE: extended/union.ts ================================================ import BTree from '../b+tree'; import { BTreeConstructor, type BTreeWithInternals, checkCanDoSetOperation } from './shared'; import { decompose, buildFromDecomposition } from "./decompose"; /** * Efficiently unions two trees, reusing subtrees wherever possible without mutating either input. * * Complexity is O(N + M) when the trees overlap heavily, and additionally bounded by O(log(N + M) * D) * where `D` is the number of disjoint key ranges, because disjoint subtrees are skipped entirely. * In practice, that means for keys of random distribution the performance is linear and for keys with significant * numbers of non-overlapping key ranges it is much faster. * @param treeA First tree to union. * @param treeB Second tree to union. * @param combineFn Called for keys that appear in both trees. Return the desired value, or * `undefined` to omit the key from the result. Note: symmetric difference can be achieved by always returning `undefined`. * @returns A new BTree that contains the unioned key/value pairs. * @throws Error if the trees were created with different comparators or max node sizes. */ export default function union, K, V>( treeA: TBTree, treeB: TBTree, combineFn: (key: K, leftValue: V, rightValue: V) => V | undefined ): TBTree { if (treeA === treeB) return treeA.clone(); const _treeA = treeA as unknown as BTreeWithInternals; const _treeB = treeB as unknown as BTreeWithInternals; const branchingFactor = checkCanDoSetOperation(_treeA, _treeB, false); if (_treeA._root.size() === 0) return treeB.clone(); if (_treeB._root.size() === 0) return treeA.clone(); // Decompose both trees into disjoint subtrees leaves. // As many of these as possible will be reused from the original trees, and the remaining // will be leaves that are the result of merging intersecting leaves. const decomposed = decompose(_treeA, _treeB, combineFn); const constructor = treeA.constructor as BTreeConstructor; return buildFromDecomposition(constructor, branchingFactor, decomposed, _treeA._compare, _treeA._maxNodeSize); } ================================================ FILE: interfaces.d.ts ================================================ /** Read-only set interface (subinterface of IMapSource). * The word "set" usually means that each item in the collection is unique * (appears only once, based on a definition of equality used by the * collection.) Objects conforming to this interface aren't guaranteed not * to contain duplicates, but as an example, BTree implements this * interface and does not allow duplicates. */ export interface ISetSource { /** Returns the number of key/value pairs in the map object. */ size: number; /** Returns a boolean asserting whether the key exists in the map object or not. */ has(key: K): boolean; /** Returns a new iterator for iterating the items in the set (the order is implementation-dependent). */ keys(): IterableIterator; } /** Read-only map interface (i.e. a source of key-value pairs). */ export interface IMapSource extends ISetSource { /** Returns the number of key/value pairs in the map object. */ size: number; /** Returns the value associated to the key, or undefined if there is none. */ get(key: K): V|undefined; /** Returns a boolean asserting whether the key exists in the map object or not. */ has(key: K): boolean; /** Calls callbackFn once for each key-value pair present in the map object. * The ES6 Map class sends the value to the callback before the key, so * this interface must do likewise. */ forEach(callbackFn: (v:V, k:K, map:IMapSource) => void, thisArg: any): void; /** Returns an iterator that provides all key-value pairs from the collection (as arrays of length 2). */ entries(): IterableIterator<[K,V]>; /** Returns a new iterator for iterating the keys of each pair. */ keys(): IterableIterator; /** Returns a new iterator for iterating the values of each pair. */ values(): IterableIterator; // TypeScript compiler decided Symbol.iterator has type 'any' //[Symbol.iterator](): IterableIterator<[K,V]>; } /** Write-only set interface (the set cannot be queried, but items can be added to it.) * @description Note: BTree does not officially implement this interface, * but BTree can be used as an instance of ISetSink. */ export interface ISetSink { /** Adds the specified item to the set, if it was not in the set already. */ add(key: K): any; /** Returns true if an element in the map object existed and has been * removed, or false if the element did not exist. */ delete(key: K): boolean; /** Removes everything so that the set is empty. */ clear(): void; } /** Write-only map interface (i.e. a drain into which key-value pairs can be "sunk") */ export interface IMapSink { /** Returns true if an element in the map object existed and has been * removed, or false if the element did not exist. */ delete(key: K): boolean; /** Sets the value for the key in the map object (the return value is * boolean in BTree but Map returns the Map itself.) */ set(key: K, value: V): any; /** Removes all key/value pairs from the IMap object. */ clear(): void; } /** Set interface. * @description Note: BTree does not officially implement this interface, * but BTree can be used as an instance of ISet. */ export interface ISet extends ISetSource, ISetSink { } /** An interface compatible with ES6 Map and BTree. This interface does not * describe the complete interface of either class, but merely the common * interface shared by both. */ export interface IMap extends IMapSource, IMapSink { } /** An data source that provides read-only access to a set of items called * "keys" in sorted order. This is a subinterface of ISortedMapSource. */ export interface ISortedSetSource extends ISetSource { /** Gets the lowest key in the collection. */ minKey(): K | undefined; /** Gets the highest key in the collection. */ maxKey(): K | undefined; /** Returns the next key larger than the specified key (or undefined if there is none). * Also, nextHigherKey(undefined) returns the lowest key. */ nextHigherKey(key?: K): K|undefined; /** Returns the next key smaller than the specified key (or undefined if there is none). * Also, nextLowerKey(undefined) returns the highest key. */ nextLowerKey(key?: K): K|undefined; /** Calls `callback` on the specified range of keys, in ascending order by key. * @param low The first key scanned will be greater than or equal to `low`. * @param high Scanning stops when a key larger than this is reached. * @param includeHigh If the `high` key is present in the map, `onFound` is called * for that final pair if and only if this parameter is true. * @param onFound A function that is called for each key pair. Because this * is a subinterface of ISortedMapSource, if there is a value * associated with the key, it is passed as the second parameter. * @param initialCounter Initial third argument of `onFound`. This value * increases by one each time `onFound` is called. Default: 0 * @returns Number of pairs found and the number of times `onFound` was called. */ forRange(low: K, high: K, includeHigh: boolean, onFound?: (k:K,v:any,counter:number) => void, initialCounter?: number): number; /** Returns a new iterator for iterating the keys of each pair in ascending order. * @param firstKey: Minimum key to include in the output. */ keys(firstKey?: K): IterableIterator; } /** An data source that provides read-only access to items in sorted order. */ export interface ISortedMapSource extends IMapSource, ISortedSetSource { /** Returns the next pair whose key is larger than the specified key (or undefined * if there is none). If key === undefined, this function returns the lowest pair. */ nextHigherPair(key?: K): [K,V]|undefined; /** Returns the next pair whose key is smaller than the specified key (or undefined * if there is none). If key === undefined, this function returns the highest pair. */ nextLowerPair(key?: K): [K,V]|undefined; /** Builds an array of pairs from the specified range of keys, sorted by key. * Each returned pair is also an array: pair[0] is the key, pair[1] is the value. * @param low The first key in the array will be greater than or equal to `low`. * @param high This method returns when a key larger than this is reached. * @param includeHigh If the `high` key is present in the map, its pair will be * included in the output if and only if this parameter is true. Note: * if the `low` key is present, it is always included in the output. * @param maxLength Maximum length of the returned array (default: unlimited) * @description Computational complexity: O(result.length + log size) */ getRange(low: K, high: K, includeHigh?: boolean, maxLength?: number): [K,V][]; /** Calls `callback` on the specified range of keys, in ascending order by key. * @param low The first key scanned will be greater than or equal to `low`. * @param high Scanning stops when a key larger than this is reached. * @param includeHigh If the `high` key is present in the map, `onFound` is called * for that final pair if and only if this parameter is true. * @param onFound A function that is called for each key-value pair. * @param initialCounter Initial third argument of onFound. This value * increases by one each time `onFound` is called. Default: 0 * @returns Number of pairs found and the number of times `callback` was called. */ forRange(low: K, high: K, includeHigh: boolean, onFound?: (k:K,v:V,counter:number) => void, initialCounter?: number): number; /** Returns an iterator that provides items in order by key. * @param firstKey: Minimum key to include in the output. */ entries(firstKey?: K): IterableIterator<[K,V]>; /** Returns a new iterator for iterating the keys of each pair in ascending order. * @param firstKey: Minimum key to include in the output. */ keys(firstKey?: K): IterableIterator; /** Returns a new iterator for iterating the values of each pair in order by key. * @param firstKey: Minimum key whose associated value is included in the output. */ values(firstKey?: K): IterableIterator; // This method should logically be in IMapSource but is not supported by ES6 Map /** Performs a reduce operation like the `reduce` method of `Array`. * It is used to combine all pairs into a single value, or perform conversions. */ reduce(callback: (previous:R,currentPair:[K,V],counter:number,tree:IMapF) => R, initialValue: R): R; /** Performs a reduce operation like the `reduce` method of `Array`. * It is used to combine all pairs into a single value, or perform conversions. */ reduce(callback: (previous:R|undefined,currentPair:[K,V],counter:number,tree:IMapF) => R): R|undefined; } /** An interface for a set of keys (the combination of ISortedSetSource and ISetSink) */ export interface ISortedSet extends ISortedSetSource, ISetSink { } /** An interface for a sorted map (dictionary), * not including functional/persistent methods. */ export interface ISortedMap extends IMap, ISortedMapSource { // All of the following methods should be in IMap but are left out of IMap // so that IMap is compatible with ES6 Map. /** Adds or overwrites a key-value pair in the sorted map. * @param key the key is used to determine the sort order of data in the tree. * @param value data to associate with the key * @param overwrite Whether to overwrite an existing key-value pair * (default: true). If this is false and there is an existing * key-value pair then the call to this method has no effect. * @returns true if a new key-value pair was added, false if the key * already existed. */ set(key: K, value: V, overwrite?: boolean): boolean; /** Adds all pairs from a list of key-value pairs. * @param pairs Pairs to add to this tree. If there are duplicate keys, * later pairs currently overwrite earlier ones (e.g. [[0,1],[0,7]] * associates 0 with 7.) * @param overwrite Whether to overwrite pairs that already exist (if false, * pairs[i] is ignored when the key pairs[i][0] already exists.) * @returns The number of pairs added to the collection. */ setPairs(pairs: [K,V][], overwrite?: boolean): number; /** Deletes a series of keys from the collection. */ deleteKeys(keys: K[]): number; /** Removes a range of key-value pairs from the B+ tree. * @param low The first key deleted will be greater than or equal to `low`. * @param high Deleting stops when a key larger than this is reached. * @param includeHigh Specifies whether the `high` key, if present, is deleted. * @returns The number of key-value pairs that were deleted. */ deleteRange(low: K, high: K, includeHigh: boolean): number; // TypeScript requires these methods of ISortedMapSource to be repeated entries(firstKey?: K): IterableIterator<[K,V]>; keys(firstKey?: K): IterableIterator; values(firstKey?: K): IterableIterator; } /** An interface for a functional set, in which the set object could be read-only * but new versions of the set can be created by calling "with" or "without" * methods to add or remove keys. This is a subinterface of IMapF, * so the items in the set may be referred to as "keys". */ export interface ISetF extends ISetSource { /** Returns a copy of the set with the specified key included. * @description You might wonder why this method accepts only one key * instead of `...keys: K[]`. The reason is that the derived interface * IMapF expects the second parameter to be a value. Therefore * withKeys() is provided to set multiple keys at once. */ with(key: K): ISetF; /** Returns a copy of the set with the specified key removed. */ without(key: K): ISetF; /** Returns a copy of the tree with all the keys in the specified array present. * @param keys The keys to add. * @param returnThisIfUnchanged If true, the method returns `this` when * all of the keys are already present in the collection. The * default value may be true or false depending on the concrete * implementation of the interface (in BTree, the default is false.) */ withKeys(keys: K[], returnThisIfUnchanged?: boolean): ISetF; /** Returns a copy of the tree with all the keys in the specified array removed. */ withoutKeys(keys: K[], returnThisIfUnchanged?: boolean): ISetF; /** Returns a copy of the tree with items removed whenever the callback * function returns false. * @param callback A function to call for each item in the set. * The second parameter to `callback` exists because ISetF * is a subinterface of IMapF. If the object is a map, v * is the value associated with the key, otherwise v could be * undefined or another copy of the third parameter (counter). */ filter(callback: (k:K,v:any,counter:number) => boolean, returnThisIfUnchanged?: boolean): ISetF; } /** An interface for a functional map, in which the map object could be read-only * but new versions of the map can be created by calling "with" or "without" * methods to add or remove keys or key-value pairs. */ export interface IMapF extends IMapSource, ISetF { /** Returns a copy of the tree with the specified key set (the value is undefined). */ with(key: K): IMapF; /** Returns a copy of the tree with the specified key-value pair set. */ with(key: K, value: V2, overwrite?: boolean): IMapF; /** Returns a copy of the tree with the specified key-value pairs set. */ withPairs(pairs: [K,V|V2][], overwrite: boolean): IMapF; /** Returns a copy of the tree with all the keys in the specified array present. * @param keys The keys to add. If a key is already present in the tree, * neither the existing key nor the existing value is modified. * @param returnThisIfUnchanged If true, the method returns `this` when * all of the keys are already present in the collection. The * default value may be true or false depending on the concrete * implementation of the interface (in BTree, the default is false.) */ withKeys(keys: K[], returnThisIfUnchanged?: boolean): IMapF; /** Returns a copy of the tree with all values altered by a callback function. */ mapValues(callback: (v:V,k:K,counter:number) => R): IMapF; /** Performs a reduce operation like the `reduce` method of `Array`. * It is used to combine all pairs into a single value, or perform conversions. */ reduce(callback: (previous:R,currentPair:[K,V],counter:number,tree:IMapF) => R, initialValue: R): R; /** Performs a reduce operation like the `reduce` method of `Array`. * It is used to combine all pairs into a single value, or perform conversions. */ reduce(callback: (previous:R|undefined,currentPair:[K,V],counter:number,tree:IMapF) => R): R|undefined; // Update return types in ISetF without(key: K): IMapF; withoutKeys(keys: K[], returnThisIfUnchanged?: boolean): IMapF; /** Returns a copy of the tree with pairs removed whenever the callback * function returns false. */ filter(callback: (k:K,v:V,counter:number) => boolean, returnThisIfUnchanged?: boolean): IMapF; } /** An interface for a functional sorted set: a functional set in which the * keys (items) are sorted. This is a subinterface of ISortedMapF. */ export interface ISortedSetF extends ISetF, ISortedSetSource { // TypeScript requires this method of ISortedSetSource to be repeated keys(firstKey?: K): IterableIterator; } export interface ISortedMapF extends ISortedSetF, IMapF, ISortedMapSource { /** Returns a copy of the tree with the specified range of keys removed. */ withoutRange(low: K, high: K, includeHigh: boolean, returnThisIfUnchanged?: boolean): ISortedMapF; // TypeScript requires these methods of ISortedSetF and ISortedMapSource to be repeated entries(firstKey?: K): IterableIterator<[K,V]>; keys(firstKey?: K): IterableIterator; values(firstKey?: K): IterableIterator; forRange(low: K, high: K, includeHigh: boolean, onFound?: (k:K,v:V,counter:number) => void, initialCounter?: number): number; // Update the return value of methods from base interfaces with(key: K): ISortedMapF; with(key: K, value: V2, overwrite?: boolean): ISortedMapF; withKeys(keys: K[], returnThisIfUnchanged?: boolean): ISortedMapF; withPairs(pairs: [K,V|V2][], overwrite: boolean): ISortedMapF; mapValues(callback: (v:V,k:K,counter:number) => R): ISortedMapF; without(key: K): ISortedMapF; withoutKeys(keys: K[], returnThisIfUnchanged?: boolean): ISortedMapF; filter(callback: (k:K,v:any,counter:number) => boolean, returnThisIfUnchanged?: boolean): ISortedMapF; } export interface ISortedMapConstructor { new (entries?: [K,V][], compare?: (a: K, b: K) => number): ISortedMap; } export interface ISortedMapFConstructor { new (entries?: [K,V][], compare?: (a: K, b: K) => number): ISortedMapF; } ================================================ FILE: package.json ================================================ { "name": "sorted-btree", "version": "2.1.1", "description": "A sorted list of key-value pairs in a fast, typed in-memory B+ tree with a powerful API.", "sideEffects": false, "main": "b+tree.js", "typings": "b+tree", "scripts": { "test": "tsc && echo //ts-jest-issue-657 >interfaces.js && jest", "build": "tsc && npm run minify", "minify": "node scripts/minify.js", "sizes": "npm run build && node scripts/size-report.js", "prepare": "npm run build", "safePublish": "npm run build && testpack && npm publish", "benchmark": "npm run build && node benchmarks.js" }, "files": [ "b+tree.js", "b+tree.d.ts", "b+tree.min.js", "extended/*.js", "extended/*.d.ts", "extended/*.min.js", "sorted-array.js", "sorted-array.d.ts", "interfaces.d.ts", "readme.md" ], "repository": { "type": "git", "url": "git+https://github.com/qwertie/btree-typescript.git" }, "keywords": [ "B+", "tree", "btree", "sorted", "set", "map", "list", "collection", "fast-cloning", "copy-on-write", "optimized" ], "author": "David Piepgrass", "license": "MIT", "bugs": { "url": "https://github.com/qwertie/btree-typescript/issues" }, "homepage": "https://github.com/qwertie/btree-typescript#readme", "devDependencies": { "@types/bintrees": "^1.0.2", "@types/collections": "^5.0.2", "@types/mersenne-twister": "^1.1.2", "@types/node": "^10.17.28", "babel-core": "^6.26.3", "bintrees": "^1.0.2", "collections": "^5.1.13", "functional-red-black-tree": "^1.0.1", "jest": "^26.6.2", "mersenne-twister": "^1.1.0", "testpack-cli": "^1.1.4", "ts-jest": "^26.4.3", "ts-node": "^7.0.1", "typescript": "^4.0.8", "uglify-js": "^3.11.4" }, "dependencies": {}, "jest": { "globals": { "ts-jest": { "diagnostics": { "//": "There are errors accessing internal symbols during safePublish; treat them as warnings", "warnOnly": true } } }, "transform": { "^.+\\.tsx?$": "ts-jest" }, "testRegex": "(/tests/.*|(\\.|/)test)\\.(jsx?|tsx?)$", "testPathIgnorePatterns": [ ".*nontest.*", "/.testpack" ], "moduleFileExtensions": [ "ts", "tsx", "js", "jsx", "json" ], "verbose": true, "bail": false, "testEnvironment": "node" }, "testpack": { "packagejson": { "scripts": { "test": "echo //for ts-jest bug #618 > workaround.ts && jest" } }, "install": [ "ts-jest@26.4.3", "typescript@3.8.3" ], "verbose": true, "test-folder": ".testpack", "rmdir": true, "dirty": true, "replace-import//": [ "The first argument ensures tests run against the minified version of the library.", "The second and third patterns are similar to defaults in testpack, but only replace relative imports that navigate", "outside the test directory. This is necessary because the test directory is not published with the package." ], "replace-import": [ "|\\./b\\+tree|$P/b+tree|", "|\\.\\.|$P|", "|\\.\\.([/\\\\].*)|$P$1|" ] } } ================================================ FILE: readme.md ================================================ B+ tree ======= B+ trees are ordered collections of key-value pairs, sorted by key. This is a fast B+ tree implementation, largely compatible with the standard Map, but with a much more diverse and powerful API. To use it, `import BTree from 'sorted-btree'`. `BTree` is faster and/or uses less memory than other popular JavaScript sorted trees (see Benchmarks). However, data structures in JavaScript tend to be slower than the built-in `Array` and `Map` data structures in typical cases, because the built-in data structures are mostly implemented in a faster language such as C++. Even so, if you have a large amount of data that you want to keep sorted, the built-in data structures will not serve you well, and `BTree` offers a variety of features like fast cloning and diffing, which the built-in types don't. Use `npm install sorted-btree` in a terminal to install it in your npm-based project. Features -------- - Requires ES5 only (`Symbol.iterator` is not required but is used if defined.) - Includes typings (`BTree` was written in TypeScript) - API similar to ES6 `Map` with methods such as `size(), clear()`, `forEach((v,k,tree)=>{}), get(K), set(K,V), has(K), delete(K)`, plus iterator functions `keys()`, `values()` and `entries()`. - Supports keys that are numbers, strings, arrays of numbers/strings, `Date`, and objects that have a `valueOf()` method that returns a number or string. - Other data types can also be supported with a custom comparator (second constructor argument). - Supports O(1) fast cloning with subtree sharing. This works by marking the root node as "shared between instances". This makes the tree read-only with copy-on-edit behavior; both copies of the tree remain mutable. I call this category of data structure "dynamically persistent" or "mutably persistent" because AFAIK no one else has given it a name; it walks the line between mutating and [persistent](https://en.wikipedia.org/wiki/Persistent_data_structure). - Includes persistent methods such as `with` and `without`, which return a modified tree without changing the original (in O(log(size)) time). - When a node fills up, items are shifted to siblings when possible to keep nodes near their capacity, to improve memory utilization. - Efficiently supports sets (keys without values). The collection does not allocate memory for values if the value `undefined` is associated with all keys in a given node. - Includes neat stuff such as `Range` methods for batch operations - Throws an exception if you try to use `NaN` as a key, but infinity is allowed. - No dependencies. 19.5K minified, 5.5K gzipped (plus an extra 22.8K minified / 9.2K gzipped if you use `BTreeEx`) - Includes a lattice of interfaces for TypeScript users (see below) - Supports diffing computation between two trees that is highly optimized for the case in which a majority of nodes are shared (such as when persistent methods are used). - Supports fast union & shared-key iteration via `forEachKeyInBoth` with asymptotic speedups when large disjoint ranges of keys are present. The union operation generates a new tree that shares nodes with the original trees when possible. ### Additional operations supported on this `BTree` ### - Set a value only if the key does not already exist: `t.setIfNotPresent(k,v)` - Set a value only if the key already exists: `t.changeIfPresent(k,v)` - Iterate in backward order: `for (pair of t.entriesReversed()) {}` - Iterate from a particular first element: `for (let p of t.entries(first)) {}` - Convert to an array: `t.toArray()`, `t.keysArray()`, `t.valuesArray()` - Get pairs for a range of keys ([K,V][]): `t.getRange(loK, hiK, includeHi)` - Delete a range of keys and their values: `t.deleteRange(loK, hiK, includeHi)` - Scan all items: `t.forEachPair((key, value, index) => {...})` - Scan a range of items: `t.forRange(lowKey, highKey, includeHiFlag, (k,v) => {...})` - Count the number of keys in a range: `c = t.forRange(loK, hiK, includeHi, undefined)` - Get smallest or largest key: `t.minKey()`, `t.maxKey()` - Get next larger key/pair than `k`: `t.nextHigherKey(k)`, `t.nextHigherPair(k)` - Get largest key/pair that is lower than `k`: `t.nextLowerKey(k)`, `t.nextLowerPair(k)` - Freeze to prevent modifications: `t.freeze()` (you can also `t.unfreeze()`) - Fast clone: `t.clone()` - For more information, **see [full documentation](https://github.com/qwertie/btree-typescript/blob/master/b%2Btree.ts) in the source code.** **Note:** Confusingly, the ES6 `Map.forEach(c)` method calls `c(value,key)` instead of `c(key,value)`, in contrast to other methods such as `set()` and `entries()` which put the key first. I can only assume that they reversed the order on the hypothesis that users would usually want to examine values and ignore keys. BTree's `forEach()` therefore works the same way, but there is a second method `.forEachPair((key,value)=>{...})` which sends you the key first and the value second; this method is slightly faster because it is the "native" for-each method for this class. **Note:** Duplicate keys are not allowed (supporting duplicates properly is complex). The "scanning" methods (`forEach, forRange, editRange, deleteRange`) will normally return the number of elements that were scanned. However, the callback can return `{break:R}` to stop iterating early and return a value `R` from the scanning method. #### Functional methods - Get a copy of the tree including only items fitting a criteria: `t.filter((k,v) => k.fitsCriteria())` - Get a copy of the tree with all values modified: `t.mapValues((v,k) => v.toString())` - Reduce a tree (see below): `t.reduce((acc, pair) => acc+pair[1], 0)` #### Persistent methods - Get a new tree with one pair changed: `t.with(key, value)` - Get a new tree with multiple pairs changed: `t.withPairs([[k1,v1], [k2,v2]])` - Ensure that specified keys exist in a new tree: `t.withKeys([k1,k2])` - Get a new tree with one pair removed: `t.without(key)` - Get a new tree with specific pairs removed: `t.withoutKeys(keys)` - Get a new tree with a range of keys removed: `t.withoutRange(low, high, includeHi)` - Get a new tree that is the result of a union: `t.union(other, unionFn)` **Things to keep in mind:** I ran a test which suggested `t.with` is three times slower than `t.set`. These methods do not return a frozen tree even if the original tree was frozen (for performance reasons, e.g. frozen trees use slightly more memory.) ### Additional optimized operations in `BTreeEx` - Find differences between two trees, quickly skipping shared subtrees: `tree1.diffAgainst(tree2, function tree1Only(k, v) {}, function tree2Only(k, v) {}, function different(k, v1, v2) {})` (standalone: `diffAgainst(treeA, treeB, ...)`) - Examine keys shared between two trees: `tree1.forEachKeyInBoth(tree2, (k, val1, val2) => {...})` - Examine keys unique to this tree: `tree1.forEachKeyNotIn(tree2, (k, v) => {...})` - Get the intersection (overlap) between two trees: `tree1.intersect(tree2, (k, val1, val2) => val1)` - Get the union (combination) of two trees: `tree1.union(tree2, (k, val1, val2) => val1)` - Get a copy without keys from another tree: `tree1.subtract(tree2)` - Fast bulk load: `BTreeEx.bulkLoad(entries, 32)` - For more information, **see [full documentation](https://github.com/qwertie/btree-typescript/blob/master/extended/index.ts) in the source code.** ### Two ways to use the extra algorithms The default export gives you the core tree and functionality; import `BTreeEx` to get an extended `BTreeEx` class with all the extra goodies: ```ts import BTreeEx from 'sorted-btree/extended'; ``` Alternately, you can continue using `BTree` but import individual algorithms instead: ```ts import diffAgainst from 'sorted-btree/extended/diffAgainst'; ``` `BTreeEx` is a drop-in subclass of `BTree` that keeps advanced helpers on the instance, while the standalone `diffAgainst` entry point lets bundlers include only that function when you don't need the rest of the extended surface. Examples -------- ### Custom comparator ### Given a set of `{name: string, age: number}` objects, you can create a tree sorted by name and then by age like this: ~~~js // First constructor argument is an optional list of pairs ([K,V][]) var tree = new BTree(undefined, (a, b) => { if (a.name > b.name) return 1; // Return a number >0 when a > b else if (a.name < b.name) return -1; // Return a number <0 when a < b else // names are equal (or incomparable) return a.age - b.age; // Return >0 when a.age > b.age }); tree.set({name:"Bill", age:17}, "happy"); tree.set({name:"Fran", age:40}, "busy & stressed"); tree.set({name:"Bill", age:55}, "recently laid off"); tree.forEachPair((k, v) => { console.log(`Name: ${k.name} Age: ${k.age} Status: ${v}`); }); ~~~ ### reduce ### The `reduce` method performs a reduction operation, like the `reduce` method of `Array`. It is used to combine all keys, values or pairs into a single value, or to perform type conversions conversions. `reduce` is best understood by example. So here's how you can multiply all the keys in a tree together: var product = tree.reduce((p, pair) => p * pair[0], 1) It means "start with `p=1`, and for each pair change `p` to `p * pair[0]`" (`pair[0]` is the key). You may be thinking "hey, wouldn't it make more sense if the `1` argument came _first_?" Yes it would, but in `Array` the parameter is second, so it must also be second in `BTree` for consistency. Here's a similar example that adds all values together: var total = tree.reduce((sum, pair) => sum + pair[1], 0) This final example converts the tree to a Map: var map = tree.reduce((m, pair) => m.set(pair[0], pair[1]), new Map())` Remember that `m.set` returns `m`, which is different from `BTree` where `tree.set` returns a boolean indicating whether a new key was added. ### editRange ### You can scan a range of items and selectively delete or change some of them using `t.editRange`. For example, the following code adds an exclamation mark to each non-boring value and deletes key number 4: ~~~js var t = new BTree().setRange([[1,"fun"],[2,"yay"],[4,"whee"],[8,"zany"],[10,"boring"]); t.editRange(t.minKey(), t.maxKey(), true, (k, v) => { if (k === 4) return {delete: true}; if (v !== "boring") return {value: v + '!'}; }) ~~~ Interface lattice ----------------- BTree includes a [lattice of interface types](https://github.com/qwertie/btree-typescript/blob/master/interfaces.d.ts) representing subsets of BTree's interface. I would encourage other authors of map/dictionary/tree/hashtable types to utilize these interfaces. These interfaces can be divided along three dimensions: ### 1. Read/write access ### I have defined several kinds of interfaces along the read/write access dimension: - **Source**: A "source" is a read-only interface (`ISetSource` and `IMapSource`). At minimum, sources include a `size` property and methods `get`, `has`, `forEach`, and `keys`. - **Sink**: A "sink" is a write-only interface (`ISetSink` and `IMapSink`). At minimum, sinks have `set`, `delete` and `clear` methods. - **Mutable**: An interface that combines the source and sink interfaces (`ISet` and `IMap`). - **Functional**: An interface for [persistent](https://en.wikipedia.org/wiki/Persistent_data_structure) data structures. It combines a read-only interface with methods that return a modified copy of the collection. The functional interfaces end with `F` (`ISetF` and `IMapF`). ### 2. Sorted versus unsorted ### The `Sorted` interfaces extend the non-sorted interfaces with queries that only a sorted collection can perform efficiently, such as `minKey()` and `nextHigherKey(k)`. At minimum, sorted interfaces add methods `minKey`, `maxKey`, `nextHigherKey`, `nextLowerKey`, and `forRange`, plus iterators that return keys/values/pairs in sorted order and accept a `firstKey` parameter to control the starting point of iteration. **Note:** in sorted-btree ≤ v1.7.x, these interfaces have methods `nextHigherKey(key: K)` and `nextLowerKey(key: K)` which should be `nextHigherKey(key: K|undefined)` and `nextLowerKey(key: K|undefined)`. These signatures are changed in the next version. ### 3. Set versus map ### A map is a collection of keys with values, while a set is a collection of keys without values. For the most part, each `Set` interface is a subset of the corresponding `Map` interface with "values" removed. For example, `MapF` extends `SetF`. An exception to this is that `IMapSink` could not be derived from `ISetSink` (and thus `IMap` is not derived from `ISet`) because the type `V` does not necessarily include `undefined`. Therefore you can write `set.set(key)` to add a key to a set, but you cannot write `map.set(key)` without specifying a value (in TypeScript this is true _even if `V` includes undefined_.) ### List of interfaces ### All of these [interfaces](https://github.com/qwertie/btree-typescript/blob/master/interfaces.d.ts) use `any` as the default type of `K` and `V`. - `ISetSource` - `ISetSink` - `ISet extends ISetSource, ISetSink` - `IMapSource extends ISetSource` - `IMapSink` - `IMap extends IMapSource, IMapSink` - `ISortedSetSource extends ISetSource` - `ISortedSet extends ISortedSetSource, ISetSink` - `ISortedMapSource extends IMapSource, ISortedSetSource` - `ISortedMap extends IMap, ISortedMapSource` - `ISetF extends ISetSource` - `IMapF extends IMapSource, ISetF` - `ISortedSetF extends ISetF, ISortedSetSource` - `ISortedMapF extends ISortedSetF, IMapF, ISortedMapSource` If the lattice were complete there would be 16 interfaces (`4*2*2`). In fact there are only 14 interfaces because `ISortedMapSink` and `ISortedSetSink` don't exist, because sorted sinks are indistinguishable from unsorted sinks. `BTree` implements all of these interfaces except `ISetSink`, `ISet`, and `ISortedSet`. However, `BTree` may be _compatible_ with these interfaces even if TypeScript doesn't realize it. Therefore, if `V` includes `undefined`, the `BTree.asSet` property is provided to cast the `BTree` to a set type. The `asSet` property returns the same `BTree` as type `ISortedSet` (which is assignable to `ISetSink`, `ISet` and `ISortedSetSource`). ### ES6 Map/Set compatibility ### The `IMap` interface is compatible with the ES6 `Map` type as well as `BTree`. In order to accomplish this, compromises had to be made: - The `set(k,v)` method returns `any` for compatibility with both `BTree` and `Map`, since `BTree` returns `boolean` (true if an item was added or false if it already existed), while `Map` returns `this`. - ES6's `Map.forEach(c)` method calls `c(value,key)` instead of `c(key,value)`, unlike all other methods which put the key first. Therefore `IMap` works the same way. Unfortunately, this means that `ISetSource`, the supertype of `IMapSource`, cannot sanely have a `forEach` method because if it did, the first parameter to the callback would be unused. - The batch operations `setPairs`, `deletePairs` and `reduce` are left out because they are not defined by `Map`. Instead, these methods are defined in `ISortedMap`. - Likewise, the functional operations `reduce`, `filter` and `mapValues` are not included in `IMap`, but they are defined in `IMapF` and (except `mapValues`) `ISetF`. Similarly, `ISet` is compatible with ES6 `Set`. Again there are compromises: - The `set` method is renamed `add` in `Set` and `ISet`, so `add` exists on `BTree.prototype` as a synonym for `set`. - There is no `forEach` method for reasons alluded to above. Use `keys()` instead. - There is no `filter` or `reduce` because `Set` doesn't support them. Although `BTree` doesn't directly implement `ISet`, it does implement `ISetSource` and it is safe to cast `BTree` to `ISet` or `ISortedSet` provided that `V` is allowed to be undefined. Ukraine is still under attack ----------------------------- I wrote this on March 24, 2022: the one-month anniversary of the full-scale invasion of Ukraine. ![Mariupol](http://david.loyc.net/misc/ukraine/Mariupol-from-above.webp) This is the city of Mariupol, which Russia badly damaged after cutting off electricity, water and heat on March 1. Pre-war population: 431,859. Do you see any military targets here? No, these are homes that many people still live in. Russia has even made it [dangerous to leave](https://www.bbc.com/news/world-europe-60629851). [An official told NPR:](https://www.dailymail.co.uk/news/article-10580113/All-people-die-Zelensky-slams-NATOs-refusal-establish-no-fly-zone.html) 'When the people organised in evacuation points, they [Russians] started attack on evacuation points. Not all the city. Just evacuation points.' Officially, [as of 9 days ago, 2,400 civilians had been killed](https://www.nytimes.com/2022/03/15/world/europe/mariupol-death-toll-ukraine.html), but this is said to be an underestimate and the actual number of murders may have been as high as 20,000... nine days ago. Now, I'm just a lowly programming-language designer with no real following on [Twitter](https://twitter.com/DPiepgrass), so I'm venting here. I have donated to the Red Cross in support of Ukraine [update: reports have said this is not an effective charity], and also to AVD-Info and Meduza in order to help give Russians access to information (the Russian internet is heavily censored, and independent media are banned). For more donation ideas, [see here](https://forum.effectivealtruism.org/posts/qkhoBJRNQT4EFWos7/what-are-effective-ways-to-help-ukrainians-right-now). [As of 2025, I still find it hard to find effective charities. The most impactful thing you could do is probably to request that your government support Ukraine financially. Trump has cut aid almost to zero, invited Putin to Alaska and proposed restoring economic ties to Russia, which has emboldened Putin to fight harder. I remind people that Putin is wanted by the ICC for mass-kidnapping of Ukrainian children. Don't forget the Bucha massacre, the [genocidal rhetoric](https://www.youtube.com/watch?v=I5yvjyJdDW0), the poison-gas attacks, or the executions on video of Ukrainian POWs. Don't forget the Human Safari (Documentary [one](https://www.youtube.com/watch?v=yaAUV3JmxwM), [two](https://khersonhumansafari.com/), [three](https://www.youtube.com/watch?v=xZ60RwJk88A)). Don't forget that Russia still maintains enormous demands that the Ukrainian people can't accept, including giving up the fortress belt that protects Ukraine from Russian advances in the Donbass region, the Ukrainian-held city of Zaporizhzhia (pre-war population: over 700,000), the Ukrainian-held city of Kherson, two large bridgeheads over the Dnipro river, and most recently, [the Russians claim "Novorussiya"](https://x.com/DPiepgrass/status/1903806027872809231) ― two extra provinces, to turn Ukraine into a landlocked country in order to destroy Ukraine's economy. They also want to prevent Ukraine from having effective guarantees against further attacks. Don't forget that Russia destroys every city before taking it, because they don't want infrastructure, they want a Russian empire bordered by a Europe filled with Ukrainian refugees. In short, they continue to insist that Ukraine give up almost everything valuable and place themselves at the mercy of Russia, and this extremism is why we must ensure that they fail. And now, back to 2022:] Without electricity, reports from Mariupol have been limited, but certainly there is enough information to see that the situation is very bad. ![Mariupol apartment bombed](http://david.loyc.net/misc/ukraine/Mariupol-explosion.webp) Here you can see the famous Donetsk Academic Regional Drama Theatre, labeled "дети" ("children") in huge letters, which held over 1,000 civilians. Russia bombed it anyway. ![Mariupol theatre](http://david.loyc.net/misc/ukraine/Mariupol-theatre-children.webp) ![Mariupol theatre before](http://david.loyc.net/misc/ukraine/Mariupol-theatre-children-before.jpg) For more images and stories from Mariupol, [see here](https://twitter.com/DPiepgrass/status/1506642788074536965). Meanwhile, I hope you don't live in an apartment in [Borodyanka](https://euromaidanpress.com/2022/03/06/close-the-sky-or-how-russia-bombed-out-my-town-of-borodyanka), near Kyiv... ![Borodyanka 1](http://david.loyc.net/misc/ukraine/central-Borodyanka-after.jpeg) ![Borodyanka 2](http://david.loyc.net/misc/ukraine/Borodyanka.png) Or in these other places... ![Before/after](http://david.loyc.net/misc/ukraine/before-after.jpg) ![Chernihiv](http://david.loyc.net/misc/ukraine/Chernihiv-before-after.webp) ![Irpin](http://david.loyc.net/misc/ukraine/Irpin-burns.webp) ![Kharkiv](http://david.loyc.net/misc/ukraine/Kharkiv-firefighters-rubble.webp) ![Kharkiv](http://david.loyc.net/misc/ukraine/Kharkiv-two-dead.webp) It was true in 2022 and remains true in 2025: **Russia will not stop until it is stopped.** Democracies are on the decline globally ― as India, Hungary and Turkey slide into authoritarianism, the internet fills with dis/misinformation, some of it generated by dictatorships, while China prepares to blockade and invade Taiwan. Russia has turned totalitarian, and now Russia wants to destroy yet another democracy after previously invading Chechnya, Georgia, and Ukraine (in 2014). Please care about this, because democracies need to stick together. The intensity of the war hasn't slowed down after three years; instead, Russia spent a majority of its cash reserves to ramp up attacks (see [Inside Russia](https://www.youtube.com/@INSIDERUSSIA) and [Ukraine Matters](https://www.youtube.com/@UkraineMatters)). Ukrainians want peace, but they also want to keep their democracy, their homes, their land and their livelihoods (see [Ukrainian public opinion survey](https://www.ipsos.com/en/survey-ukranian-citizens)). They have fought long and hard, and remain willing, but can only succeed with strong western support. Russian cash will not last forever, and our economies are much stronger than Russia's. Ukrainians have the only army in the world that knows how to fight a modern war, and they manufacture more drones than any country besides China and maybe Russia. If we let Ukraine lose, we lose their military strength and production capacity at a time when we might well need it ourselves, for Ukraine is not the only territory that Putin believes belongs to him, nor is Putin the only one who considers starting wars. Ukrainians offer to teach our militaries something they don't know: how to fight a modern drone war. All they want in exchange is to keep existing as a free people, and we in the west can grant them that. Slava Ukrayini, i dyakuyu. Benchmarks (in milliseconds for integer keys/values) ---------------------------------------------------- - These benchmark results were gathered on my PC in Node v20.11.1, December 2025 - `BTree` is 3 to 5 times faster than `SortedMap` and `SortedSet` in the `collections` package - `BTree` has similar speed to `RBTree` at smaller sizes, but is faster at very large sizes and uses less memory because it packs many keys into one array instead of allocating an extra heap object for every key. - If you need [functional persistence](https://en.wikipedia.org/wiki/Persistent_data_structure), `functional-red-black-tree` is remarkably fast for a persistent tree, but `BTree` should require less memory _unless_ you frequently use `clone/with/without` and are saving snapshots of the old tree to prevent garbage collection. - B+ trees normally use less memory than hashtables (such as the standard `Map`), although in JavaScript this is not guaranteed because the B+ tree's memory efficiency depends on avoiding wasted space in the arrays for each node, and JavaScript provides no way to detect or control the capacity of an array's underlying memory area. Also, `Map` should be faster because it does not sort its keys. - "Sorted array" refers to `SortedArray`, a wrapper class for an array of `[K,V]` pairs. Benchmark results were not gathered for sorted arrays with one million elements (it takes too long). - "Baseline algorithms" below are typically based on converting the B+ tree to an array (`toArray()`) and doing the operation in that array. ### Insertions at random locations: sorted-btree vs the competition (millisec) ### 1.16 Insert 1000 pairs in sorted-btree's BTree 0.39 Insert 1000 pairs in sorted-btree's BTree set (no values) 3.07 Insert 1000 pairs in collections' SortedMap 2.44 Insert 1000 pairs in collections' SortedSet (no values) 1.86 Insert 1000 pairs in functional-red-black-tree 1.22 Insert 1000 pairs in bintrees' RBTree (no values) 3.25 Insert 10000 pairs in sorted-btree's BTree 2.31 Insert 10000 pairs in sorted-btree's BTree set (no values) 21.08 Insert 10000 pairs in collections' SortedMap 12.66 Insert 10000 pairs in collections' SortedSet (no values) 4.14 Insert 10000 pairs in functional-red-black-tree 1.84 Insert 10000 pairs in bintrees' RBTree (no values) 37.29 Insert 100000 pairs in sorted-btree's BTree 27.88 Insert 100000 pairs in sorted-btree's BTree set (no values) 329.64 Insert 100000 pairs in collections' SortedMap 206.27 Insert 100000 pairs in collections' SortedSet (no values) 81.06 Insert 100000 pairs in functional-red-black-tree 28.54 Insert 100000 pairs in bintrees' RBTree (no values) 625.89 Insert 1000000 pairs in sorted-btree's BTree 437.59 Insert 1000000 pairs in sorted-btree's BTree set (no values) 5673.62 Insert 1000000 pairs in collections' SortedMap 3496.35 Insert 1000000 pairs in collections' SortedSet (no values) 1892.51 Insert 1000000 pairs in functional-red-black-tree 834.54 Insert 1000000 pairs in bintrees' RBTree (no values) ### Insert in order, delete: sorted-btree vs the competition ### 0.16 Insert 1000 sorted pairs in B+ tree 0.14 Insert 1000 sorted keys in B+ tree set (no values) 0.4 Insert 1000 sorted pairs in collections' SortedMap 0.2 Insert 1000 sorted keys in collections' SortedSet (no values) 0.24 Insert 1000 sorted pairs in functional-red-black-tree 0.78 Insert 1000 sorted keys in bintrees' RBTree (no values) 0.41 Delete every second item in B+ tree 0.66 Delete every second item in B+ tree set 0.42 Bulk-delete every second item in B+ tree set 0.79 Delete every second item in collections' SortedMap 0.6 Delete every second item in collections' SortedSet 1.66 Delete every second item in functional-red-black-tree 1.23 Delete every second item in bintrees' RBTree 2.22 Insert 10000 sorted pairs in B+ tree 1.52 Insert 10000 sorted keys in B+ tree set (no values) 3.28 Insert 10000 sorted pairs in collections' SortedMap 2.4 Insert 10000 sorted keys in collections' SortedSet (no values) 5.77 Insert 10000 sorted pairs in functional-red-black-tree 2.48 Insert 10000 sorted keys in bintrees' RBTree (no values) 2.15 Delete every second item in B+ tree 2.01 Delete every second item in B+ tree set 1.26 Bulk-delete every second item in B+ tree set 5.17 Delete every second item in collections' SortedMap 4.46 Delete every second item in collections' SortedSet 7.33 Delete every second item in functional-red-black-tree 1.82 Delete every second item in bintrees' RBTree 22.26 Insert 100000 sorted pairs in B+ tree 17 Insert 100000 sorted keys in B+ tree set (no values) 53.62 Insert 100000 sorted pairs in collections' SortedMap 34.65 Insert 100000 sorted keys in collections' SortedSet (no values) 70.5 Insert 100000 sorted pairs in functional-red-black-tree 29.61 Insert 100000 sorted keys in bintrees' RBTree (no values) 18.91 Delete every second item in B+ tree 16.52 Delete every second item in B+ tree set 5.31 Bulk-delete every second item in B+ tree set 50.01 Delete every second item in collections' SortedMap 33.12 Delete every second item in collections' SortedSet 28.09 Delete every second item in functional-red-black-tree 13.79 Delete every second item in bintrees' RBTree 239.15 Insert 1000000 sorted pairs in B+ tree 194.53 Insert 1000000 sorted keys in B+ tree set (no values) 652.82 Insert 1000000 sorted pairs in collections' SortedMap 364.85 Insert 1000000 sorted keys in collections' SortedSet (no values) 833.39 Insert 1000000 sorted pairs in functional-red-black-tree 367.05 Insert 1000000 sorted keys in bintrees' RBTree (no values) 177.48 Delete every second item in B+ tree 137.5 Delete every second item in B+ tree set 43.13 Bulk-delete every second item in B+ tree set 407.36 Delete every second item in collections' SortedMap 349.39 Delete every second item in collections' SortedSet 337.67 Delete every second item in functional-red-black-tree 116.74 Delete every second item in bintrees' RBTree ### Insertions at random locations: sorted-btree vs Array vs Map ### 0.16 Insert 1000 pairs in sorted array 0.2 Insert 1000 pairs in B+ tree 0.03 Insert 1000 pairs in ES6 Map (hashtable) 6.04 Insert 10000 pairs in sorted array 2.66 Insert 10000 pairs in B+ tree 0.7 Insert 10000 pairs in ES6 Map (hashtable) 1852.77 Insert 100000 pairs in sorted array 36.07 Insert 100000 pairs in B+ tree 8.64 Insert 100000 pairs in ES6 Map (hashtable) SLOW! Insert 1000000 pairs in sorted array 613.34 Insert 1000000 pairs in B+ tree 133.13 Insert 1000000 pairs in ES6 Map (hashtable) ### Insert in order, scan, delete: sorted-btree vs Array vs Map ### 0.16 Insert 1000 sorted pairs in array 0.27 Insert 1000 sorted pairs in B+ tree 0.09 Insert 1000 sorted pairs in Map hashtable 0.03 Sum of all values with forEach in sorted array: 27414400 0.09 Sum of all values with forEachPair in B+ tree: 27414400 0.1 Sum of all values with forEach in B+ tree: 27414400 0.18 Sum of all values with iterator in B+ tree: 27414400 0.03 Sum of all values with forEach in Map: 27414400 0.19 Delete every second item in sorted array 0.32 Delete every second item in B+ tree 0.06 Delete every second item in Map hashtable 1.48 Insert 10000 sorted pairs in array 2.02 Insert 10000 sorted pairs in B+ tree 0.7 Insert 10000 sorted pairs in Map hashtable 0.17 Sum of all values with forEach in sorted array: 2727131580 0.13 Sum of all values with forEachPair in B+ tree: 2727131580 0.15 Sum of all values with forEach in B+ tree: 2727131580 1.05 Sum of all values with iterator in B+ tree: 2727131580 0.11 Sum of all values with forEach in Map: 2727131580 0.36 Delete every second item in sorted array 0.62 Delete every second item in B+ tree 0.18 Delete every second item in Map hashtable 16.46 Insert 100000 sorted pairs in array 23.9 Insert 100000 sorted pairs in B+ tree 8.07 Insert 100000 sorted pairs in Map hashtable 0.95 Sum of all values with forEach in sorted array: 274463815510 1.4 Sum of all values with forEachPair in B+ tree: 274463815510 1.37 Sum of all values with forEach in B+ tree: 274463815510 1.61 Sum of all values with iterator in B+ tree: 274463815510 0.82 Sum of all values with forEach in Map: 274463815510 1478.85 Delete every second item in sorted array 7.69 Delete every second item in B+ tree 2.32 Delete every second item in Map hashtable 298.73 Insert 1000000 sorted pairs in array 241.94 Insert 1000000 sorted pairs in B+ tree 125.53 Insert 1000000 sorted pairs in Map hashtable 12.13 Sum of all values with forEach in sorted array: 27511905926210 15.43 Sum of all values with forEachPair in B+ tree: 27511905926210 15.72 Sum of all values with forEach in B+ tree: 27511905926210 13.65 Sum of all values with iterator in B+ tree: 27511905926210 8.72 Sum of all values with forEach in Map: 27511905926210 SLOW! Delete every second item in sorted array 79.73 Delete every second item in B+ tree 85.45 Delete every second item in Map hashtable ### BTree.diffAgainst() ### 0.3 BTree.diffAgainst 1000 pairs vs 100 pairs 0.83 BTree.diffAgainst 10000 pairs vs 100 pairs 0.18 BTree.diffAgainst 10000 pairs vs 1000 pairs 1.15 BTree.diffAgainst 100000 pairs vs 100 pairs 2.29 BTree.diffAgainst 100000 pairs vs 1000 pairs 1.31 BTree.diffAgainst 100000 pairs vs 10000 pairs 13.04 BTree.diffAgainst 1000000 pairs vs 100 pairs 13.14 BTree.diffAgainst 1000000 pairs vs 1000 pairs 14.12 BTree.diffAgainst 1000000 pairs vs 10000 pairs 15.79 BTree.diffAgainst 1000000 pairs vs 100000 pairs 0.03 BTree.diffAgainst 100 pairs vs cloned copy with 100 extra pairs 0.14 BTree.diffAgainst 100 pairs vs cloned copy with 1000 extra pairs 0.31 BTree.diffAgainst 100 pairs vs cloned copy with 10000 extra pairs 2.09 BTree.diffAgainst 100 pairs vs cloned copy with 100000 extra pairs 26.64 BTree.diffAgainst 100 pairs vs cloned copy with 1000000 extra pairs 0.15 BTree.diffAgainst 1000 pairs vs cloned copy with 100 extra pairs 0.27 BTree.diffAgainst 1000 pairs vs cloned copy with 1000 extra pairs 0.39 BTree.diffAgainst 1000 pairs vs cloned copy with 10000 extra pairs 2.29 BTree.diffAgainst 1000 pairs vs cloned copy with 100000 extra pairs 29.45 BTree.diffAgainst 1000 pairs vs cloned copy with 1000000 extra pairs 0.09 BTree.diffAgainst 10000 pairs vs cloned copy with 100 extra pairs 0.4 BTree.diffAgainst 10000 pairs vs cloned copy with 1000 extra pairs 0.58 BTree.diffAgainst 10000 pairs vs cloned copy with 10000 extra pairs 2.99 BTree.diffAgainst 10000 pairs vs cloned copy with 100000 extra pairs 29.59 BTree.diffAgainst 10000 pairs vs cloned copy with 1000000 extra pairs 0.2 BTree.diffAgainst 100000 pairs vs cloned copy with 100 extra pairs 1.02 BTree.diffAgainst 100000 pairs vs cloned copy with 1000 extra pairs 3.71 BTree.diffAgainst 100000 pairs vs cloned copy with 10000 extra pairs 7.58 BTree.diffAgainst 100000 pairs vs cloned copy with 100000 extra pairs 34.55 BTree.diffAgainst 100000 pairs vs cloned copy with 1000000 extra pairs 0.38 BTree.diffAgainst 1000000 pairs vs cloned copy with 100 extra pairs 4.29 BTree.diffAgainst 1000000 pairs vs cloned copy with 1000 extra pairs 18.86 BTree.diffAgainst 1000000 pairs vs cloned copy with 10000 extra pairs 48.21 BTree.diffAgainst 1000000 pairs vs cloned copy with 100000 extra pairs 90.29 BTree.diffAgainst 1000000 pairs vs cloned copy with 1000000 extra pairs ### Accelerated union of B+ trees (vs non-accelerated baseline algorithm) ### #### Adjacent ranges (one intersection point) 0.04 union(): Union 100+100 trees with 1 keys overlaping union(): 6/10 shared nodes, 0/10 underfilled nodes, 65.00% average load factor 0.2 union(): Union 1000+1000 trees with 1 keys overlaping union(): 62/70 shared nodes, 0/70 underfilled nodes, 92.32% average load factor 0.22 union(): Union 10000+10000 trees with 1 keys overlaping union(): 641/650 shared nodes, 0/650 underfilled nodes, 99.27% average load factor 0.1 union(): Union 100000+100000 trees with 1 keys overlaping union(): 6446/6459 shared nodes, 0/6459 underfilled nodes, 99.89% average load factor #### 10% overlap 0.01 union(): Union trees with 10% overlap (100+100 keys) union(): 6/9 shared nodes, 0/9 underfilled nodes, 68.75% average load factor 0.02 baseline: Union trees with 10% overlap (100+100 keys) baseline: 2/7 shared nodes, 0/7 underfilled nodes, 87.50% average load factor 0.03 union(): Union trees with 10% overlap (1000+1000 keys) union(): 56/66 shared nodes, 0/66 underfilled nodes, 93.04% average load factor 0.21 baseline: Union trees with 10% overlap (1000+1000 keys) baseline: 28/63 shared nodes, 0/63 underfilled nodes, 97.32% average load factor 0.12 union(): Union trees with 10% overlap (10000+10000 keys) union(): 578/630 shared nodes, 0/630 underfilled nodes, 97.37% average load factor 2.33 baseline: Union trees with 10% overlap (10000+10000 keys) baseline: 289/614 shared nodes, 0/614 underfilled nodes, 99.82% average load factor 1.42 union(): Union trees with 10% overlap (100000+100000 keys) union(): 5803/6276 shared nodes, 0/6276 underfilled nodes, 97.73% average load factor 24.69 baseline: Union trees with 10% overlap (100000+100000 keys) baseline: 2901/6131 shared nodes, 0/6131 underfilled nodes, 99.97% average load factor #### Large sparse-overlap trees (1M keys each, 10 overlaps per 100k) 0.5 union(): Union 1000000+1000000 sparse-overlap trees union(): 64461/64552 shared nodes, 0/64552 underfilled nodes, 99.94% average load factor 288.2 baseline: Union 1000000+1000000 sparse-overlap trees baseline: 32223/64516 shared nodes, 0/64516 underfilled nodes, 100.00% average load factor ### Subtraction of B+ trees (vs non-accelerated baseline algorithm) ### #### Non-overlapping ranges (nothing removed) 0.01 subtract: Subtract 100+100 disjoint trees subtract: 4/5 shared nodes, 0/5 underfilled nodes, 65.00% average load factor 0.02 baseline: Subtract 100+100 disjoint trees baseline: 4/5 shared nodes, 0/5 underfilled nodes, 65.00% average load factor 0.01 subtract: Subtract 1000+1000 disjoint trees subtract: 33/33 shared nodes, 0/33 underfilled nodes, 97.73% average load factor 0.18 baseline: Subtract 1000+1000 disjoint trees baseline: 32/33 shared nodes, 0/33 underfilled nodes, 97.73% average load factor 0.01 subtract: Subtract 10000+10000 disjoint trees subtract: 323/324 shared nodes, 0/324 underfilled nodes, 99.57% average load factor 0.38 baseline: Subtract 10000+10000 disjoint trees baseline: 323/324 shared nodes, 0/324 underfilled nodes, 99.57% average load factor 0.01 subtract: Subtract 100000+100000 disjoint trees subtract: 3227/3228 shared nodes, 0/3228 underfilled nodes, 99.93% average load factor 2.55 baseline: Subtract 100000+100000 disjoint trees baseline: 3227/3228 shared nodes, 0/3228 underfilled nodes, 99.93% average load factor #### Partial overlap (middle segment removed) 0.03 subtract: Subtract 100+50 partially overlapping trees subtract: 1/3 shared nodes, 0/3 underfilled nodes, 54.17% average load factor 0.03 baseline: Subtract 100+50 partially overlapping trees baseline: 1/3 shared nodes, 0/3 underfilled nodes, 54.17% average load factor 0.23 subtract: Subtract 1000+500 partially overlapping trees subtract: 15/18 shared nodes, 0/18 underfilled nodes, 89.76% average load factor 0.31 baseline: Subtract 1000+500 partially overlapping trees baseline: 15/18 shared nodes, 1/18 underfilled nodes, 89.76% average load factor 0.94 subtract: Subtract 10000+5000 partially overlapping trees subtract: 159/164 shared nodes, 0/164 underfilled nodes, 98.38% average load factor 2.12 baseline: Subtract 10000+5000 partially overlapping trees baseline: 160/163 shared nodes, 0/163 underfilled nodes, 98.96% average load factor 3.82 subtract: Subtract 100000+50000 partially overlapping trees subtract: 1608/1619 shared nodes, 0/1619 underfilled nodes, 99.63% average load factor 17.3 baseline: Subtract 100000+50000 partially overlapping trees baseline: 1610/1616 shared nodes, 0/1616 underfilled nodes, 99.81% average load factor #### Interleaved keys (every other key removed) 0.02 subtract: Subtract 200-100 interleaved trees subtract: 0/6 shared nodes, 0/6 underfilled nodes, 54.69% average load factor 0.08 baseline: Subtract 200-100 interleaved trees baseline: 0/5 shared nodes, 1/5 underfilled nodes, 65.00% average load factor 0.15 subtract: Subtract 2000-1000 interleaved trees subtract: 0/47 shared nodes, 0/47 underfilled nodes, 69.55% average load factor 0.54 baseline: Subtract 2000-1000 interleaved trees baseline: 0/33 shared nodes, 0/33 underfilled nodes, 97.73% average load factor 1.94 subtract: Subtract 20000-10000 interleaved trees subtract: 0/463 shared nodes, 0/463 underfilled nodes, 70.61% average load factor 3.14 baseline: Subtract 20000-10000 interleaved trees baseline: 0/324 shared nodes, 0/324 underfilled nodes, 99.57% average load factor 20.97 subtract: Subtract 200000-100000 interleaved trees subtract: 0/4636 shared nodes, 0/4636 underfilled nodes, 70.53% average load factor 39.68 baseline: Subtract 200000-100000 interleaved trees baseline: 0/3229 shared nodes, 2/3229 underfilled nodes, 99.90% average load factor #### Large sparse-overlap trees (1M keys each, 10 overlaps per 100k) 0.25 subtract: Subtract 1000000+1000000 sparse-overlap trees subtract: 32208/32291 shared nodes, 0/32291 underfilled nodes, 99.89% average load factor 49.97 baseline: Subtract 1000000+1000000 sparse-overlap trees baseline: 32228/32259 shared nodes, 0/32259 underfilled nodes, 99.99% average load factor ### Intersection between B+ trees (vs non-accelerated baseline algorithm) ### #### Non-overlapping ranges (no shared keys) 0.01 intersect: Intersect 100+100 disjoint trees intersect: 0/0 shared nodes, 0/0 underfilled nodes, 0.00% average load factor 0.04 baseline: Intersect 100+100 disjoint trees baseline: 0/0 shared nodes, 0/0 underfilled nodes, 0.00% average load factor 0.01 intersect: Intersect 1000+1000 disjoint trees intersect: 0/0 shared nodes, 0/0 underfilled nodes, 0.00% average load factor 0.08 baseline: Intersect 1000+1000 disjoint trees baseline: 0/0 shared nodes, 0/0 underfilled nodes, 0.00% average load factor 0 intersect: Intersect 10000+10000 disjoint trees intersect: 0/0 shared nodes, 0/0 underfilled nodes, 0.00% average load factor 0.57 baseline: Intersect 10000+10000 disjoint trees baseline: 0/0 shared nodes, 0/0 underfilled nodes, 0.00% average load factor 0 intersect: Intersect 100000+100000 disjoint trees intersect: 0/0 shared nodes, 0/0 underfilled nodes, 0.00% average load factor 10.02 baseline: Intersect 100000+100000 disjoint trees baseline: 0/0 shared nodes, 0/0 underfilled nodes, 0.00% average load factor #### Partial overlap (middle segment shared) 0.02 intersect: Intersect 100+50 partially overlapping trees intersect: 0/3 shared nodes, 0/3 underfilled nodes, 54.17% average load factor 0.02 baseline: Intersect 100+50 partially overlapping trees baseline: 0/3 shared nodes, 0/3 underfilled nodes, 54.17% average load factor 0.14 intersect: Intersect 1000+500 partially overlapping trees intersect: 0/21 shared nodes, 0/21 underfilled nodes, 77.38% average load factor 0.51 baseline: Intersect 1000+500 partially overlapping trees baseline: 0/17 shared nodes, 0/17 underfilled nodes, 94.85% average load factor 0.62 intersect: Intersect 10000+5000 partially overlapping trees intersect: 0/202 shared nodes, 0/202 underfilled nodes, 80.46% average load factor 1.71 baseline: Intersect 10000+5000 partially overlapping trees baseline: 0/163 shared nodes, 0/163 underfilled nodes, 98.96% average load factor 4.65 intersect: Intersect 100000+50000 partially overlapping trees intersect: 0/2002 shared nodes, 0/2002 underfilled nodes, 81.17% average load factor 17.16 baseline: Intersect 100000+50000 partially overlapping trees baseline: 0/1615 shared nodes, 0/1615 underfilled nodes, 99.87% average load factor #### Interleaved keys (every other key shared) 0.01 intersect: Intersect 200+100 interleaved trees intersect: 0/5 shared nodes, 0/5 underfilled nodes, 65.00% average load factor 0.02 baseline: Intersect 200+100 interleaved trees baseline: 0/5 shared nodes, 0/5 underfilled nodes, 65.00% average load factor 0.17 intersect: Intersect 2000+1000 interleaved trees intersect: 0/42 shared nodes, 0/42 underfilled nodes, 77.46% average load factor 0.28 baseline: Intersect 2000+1000 interleaved trees baseline: 0/33 shared nodes, 0/33 underfilled nodes, 97.73% average load factor 1.11 intersect: Intersect 20000+10000 interleaved trees intersect: 0/401 shared nodes, 0/401 underfilled nodes, 81.05% average load factor 3.16 baseline: Intersect 20000+10000 interleaved trees baseline: 0/324 shared nodes, 0/324 underfilled nodes, 99.57% average load factor 12.65 intersect: Intersect 200000+100000 interleaved trees intersect: 0/4002 shared nodes, 0/4002 underfilled nodes, 81.21% average load factor 73.05 baseline: Intersect 200000+100000 interleaved trees baseline: 0/3228 shared nodes, 0/3228 underfilled nodes, 99.93% average load factor #### Large sparse-overlap trees (1M keys each, 10 overlaps per 100k) 0.02 intersect: Intersect 1000000+1000000 sparse-overlap trees intersect: 0/5 shared nodes, 0/5 underfilled nodes, 65.00% average load factor 327.57 baseline: Intersect 1000000+1000000 sparse-overlap trees baseline: 0/5 shared nodes, 0/5 underfilled nodes, 65.00% average load factor ### forEachKeyInBoth ### #### Non-overlapping ranges (no shared keys) 0 forEachKeyInBoth: [count=0, checksum=0] 0 forEachKeyInBoth: [count=0, checksum=0] 0 forEachKeyInBoth: [count=0, checksum=0] 0 forEachKeyInBoth: [count=0, checksum=0] #### 50% overlapping ranges 0.01 forEachKeyInBoth: [count=50, checksum=11175] 0.09 forEachKeyInBoth: [count=500, checksum=1124250] 0.89 forEachKeyInBoth: [count=5000, checksum=112492500] 2.14 forEachKeyInBoth: [count=50000, checksum=11249925000] #### Random overlaps (~10% shared keys) 0.01 forEachKeyInBoth: [count=22, checksum=70248] 0.09 forEachKeyInBoth: [count=252, checksum=7524384] 1.32 forEachKeyInBoth: [count=2409, checksum=772269240] 9.69 forEachKeyInBoth: [count=24895, checksum=80459452812] #### Large sparse-overlap trees (1M keys each, 10 overlaps per 100k) 0.01 forEachKeyInBoth: [count=100, checksum=360003600] ### forEachKeyNotIn ### #### Non-overlapping ranges (all keys survive) 0.02 forEachKeyNotIn: [count=100, checksum=4950] 0.41 forEachKeyNotIn: [count=1000, checksum=499500] 3.04 forEachKeyNotIn: [count=10000, checksum=49995000] 4.93 forEachKeyNotIn: [count=100000, checksum=4999950000] #### 50% overlapping ranges 0.03 forEachKeyNotIn: [count=50, checksum=1225] 0.18 forEachKeyNotIn: [count=500, checksum=124750] 0.99 forEachKeyNotIn: [count=5000, checksum=12497500] 7.02 forEachKeyNotIn: [count=50000, checksum=1249975000] #### Random overlaps (~10% of include removed) 0.01 forEachKeyNotIn: [count=67, checksum=83085] 0.07 forEachKeyNotIn: [count=743, checksum=10109320] 1.5 forEachKeyNotIn: [count=7492, checksum=1035797435] 8.29 forEachKeyNotIn: [count=75400, checksum=104318180340] #### Large sparse-overlap trees (1M keys each, 10 overlaps per 100k) 33.03 forEachKeyNotIn: [count=999900, checksum=499954499550] Version history --------------- ### v2.1.0 ### - Introduced the new `sorted-btree/extended` entry point that holds `BTreeEx`. The default `sorted-btree` export stays lean (tree-shakable) while the extended build keeps parity with the old API surface. - Thanks to Microsoft, Taylor Williams and Craig Macomber for these new features. - Added a dedicated `sorted-btree/extended/diffAgainst` entry so apps can import just the standalone diff helper without pulling in `BTreeEx`. - **Breaking change:** `diffAgainst` is no longer available on the default `BTree` export. Switch to `BTreeEx#diffAgainst` (imported from `sorted-btree/extended`) or the standalone `diffAgainst(treeA, treeB, ...)` helper to continue using the diff API. - `checkValid` now has a parameter `checkOrdering = false` for more thorough checking ### v1.8.0 ### - Argument of `ISortedSetSource.nextHigherKey(key: K)` changed to `key?: K` - Argument of `ISortedSetSource.nextLowerKey(key: K)` changed to `key?: K` - Argument of `ISortedMapSource.nextHigherPair(key: K)` changed to `key?: K` - Argument of `ISortedMapSource.nextLowerPair(key: K)` changed to `key?: K` ### v1.7.0 ### - Added `asSet` method, defined as follows: `asSet(btree: BTree): undefined extends V ? ISortedSet : unknown { return btree as any; }` ### v1.6.2 ### - Bug fixes: two rare situations were discovered in which shared nodes could fail to be marked as shared, and as a result, mutations could affect copies that should have been completely separate. - Bug fix: greedyClone(true) did not clone shared nodes recursively. ### v1.6.0 ### - Added `BTree.getPairOrNextLower` and `BTree.getPairOrNextHigher` methods (PR #23) - Added optional second parameter `reusedArray` to `nextHigherPair` and `nextLowerPair` (PR #23) - Optimizations added in `diffAgainst` (PR #24) and `nextLowerPair` (PR #23) ### v1.5.0 ### - Added `BTree.diffAgainst` method (PR #16) - Added `simpleComparator` function (PR #15) - Improved `defaultComparator` (PR #15) to support edge cases better. Most notably, heterogenous key types will no longer cause trouble such as failure to find keys that are, in fact, present in the tree. `BTree` is slightly slower using the new default comparator, but the benchmarks above have not been refreshed. For maximum performance, use `simpleComparator` or a custom comparator as the second constructor parameter. The simplest possible comparator is `(a, b) => a - b`, which works for finite numbers only. ### v1.4.0 ### - Now built as CommonJS module instead of UMD module, for better compatibility with webpack. No semantic change. ### v1.3.0 ### - Now built with TypeScript v3.8.3. No semantic change. ### v1.2.4 ### - Issue #9 fixed: `nextLowerPair(0)` was being treated like `nextLowerPair(undefined)`, and `nextLowerPair(undefined)` was returning the second-highest pair when it should have returned the highest pair. ### v1.2.3 ### - Important bug fix in deletion code avoids occasional tree corruption that can occur after a series of delete operations - Add `typings` option in package.json so that `tsc` works for end-users ### v1.2 ### - Added a complete lattice of interfaces as described above. - Interfaces have been moved to a separate *interfaces.d.ts* file which is re-exported by the main module in *b+tree.d.ts*. ### v1.1 ### - Added `isEmpty` property getter - Added `nextHigherPair`, `nextHigherKey`, `nextLowerPair`, `nextLowerKey` methods - Added `editAll`, which is like `editRange` but touches all keys - Added `deleteKeys` for deleting a sequence of keys (iterable) - Added persistent methods `with`, `withPairs`, `withKeys`, `without`, `withoutKeys`, `withoutRange` - Added functional methods `filter`, `reduce`, `mapValues` - Added `greedyClone` for cloning nodes immediately, to avoid marking the original tree as shared which slows it down. - Relaxed type constraint on second parameter of `entries`/`entriesReversed` - Renamed `setRange` to `setPairs` for logical consistency with `withoutPairs` and `withoutRange`. The old name is deprecated but added to the `prototype` as a synonym. `setPairs` returns the number of pairs added instead of `this`. - Added export `EmptyBTree`, a frozen empty tree ### v1.0: Initial version ### - With fast cloning and all that good stuff ### Endnote ### ♥ This package was made to help people [learn TypeScript & React](http://typescript-react-primer.loyc.net/). Are you a C# developer? You might like the similar data structures I made for C# ([BDictionary, BList, etc.](core.loyc.net/collections/alists-part2)), and other [dynamically persistent collection types](http://core.loyc.net/collections/). You might think that the package name "sorted btree" is overly redundant, but I _did_ make a data structure similar to B+ Tree that is _not_ sorted. I called it the [A-List](http://core.loyc.net/collections/alists-part1) (C#). But yeah, the names `btree` and `bplustree` were already taken, so what was I supposed to do, right? ================================================ FILE: scripts/minify.js ================================================ const fs = require('fs'); const path = require('path'); const UglifyJS = require('uglify-js'); const rootDir = path.resolve(__dirname, '..'); const extendedDir = path.join(rootDir, 'extended'); function extendedTargets() { try { return fs .readdirSync(extendedDir) .filter((file) => file.endsWith('.js') && !file.endsWith('.min.js')) .map((file) => { const relative = path.join('extended', file); const output = path.join('extended', file.replace(/\.js$/, '.min.js')); return { input: relative, output }; }); } catch (err) { console.error(`Failed to read ${extendedDir}:`, err.message); process.exit(1); } } const targets = [{ input: 'b+tree.js', output: 'b+tree.min.js' }, ...extendedTargets()]; for (const { input, output } of targets) { const sourcePath = path.resolve(rootDir, input); const outPath = path.resolve(rootDir, output); const source = fs.readFileSync(sourcePath, 'utf8'); const result = UglifyJS.minify(source, { compress: true, mangle: true }); if (result.error) { console.error(`Failed to minify ${input}:`, result.error); process.exit(1); } fs.writeFileSync(outPath, `${result.code}\n`, 'utf8'); } ================================================ FILE: scripts/size-report.js ================================================ const fs = require('fs'); const path = require('path'); const zlib = require('zlib'); // // This script prints out a size report with raw, minified, and gzipped sizes of the JS modules. // Usage: node scripts\size-report.js // const rootDir = path.resolve(__dirname, '..'); const extendedDir = path.join(rootDir, 'extended'); function discoverExtendedEntries() { try { return fs .readdirSync(extendedDir) .filter((file) => file.endsWith('.js') && !file.endsWith('.min.js')) .sort() .map((file) => { const base = file.replace(/\.js$/, ''); const display = `extended/${base}`; const raw = path.join('extended', file); const min = path.join('extended', `${base}.min.js`); return { name: display, raw, min }; }); } catch (err) { console.error(`Cannot read ${extendedDir}: ${err.message}`); process.exitCode = 1; return []; } } const entryPoints = [{ name: 'btree', raw: 'b+tree.js', min: 'b+tree.min.js' }, ...discoverExtendedEntries()]; const nameColumnWidth = 30; function fileSize(relativePath) { const filePath = path.join(rootDir, relativePath); try { return fs.statSync(filePath).size; } catch (err) { console.error(`Cannot read ${relativePath}: ${err.message}`); process.exitCode = 1; return null; } } function gzipSize(relativePath) { const filePath = path.join(rootDir, relativePath); try { const buffer = fs.readFileSync(filePath); return zlib.gzipSync(buffer).length; } catch (err) { console.error(`Cannot gzip ${relativePath}: ${err.message}`); process.exitCode = 1; return null; } } function formatBytes(bytes) { if (typeof bytes !== 'number') return 'n/a'; if (bytes < 1024) return `${bytes} B`; const kb = bytes / 1024; if (kb < 1024) return `${kb.toFixed(2)} KB`; return `${(kb / 1024).toFixed(2)} MB`; } function pad(str, length) { const text = String(str); return text.length >= length ? `${text} ` : text.padEnd(length + 1, ' '); } const header = pad('Entry', nameColumnWidth) + pad('Raw JS Size', 13) + pad('Minified', 13) + 'Gzipped'; console.log(header); console.log('-'.repeat(header.length)); const btreeExTransitive = { raw: null, min: null, gz: null }; entryPoints.forEach((entry, index) => { const raw = fileSize(entry.raw); const min = fileSize(entry.min); const gz = gzipSize(entry.min); const line = pad(entry.name, nameColumnWidth) + pad(formatBytes(raw), 13) + pad(formatBytes(min), 13) + formatBytes(gz); console.log(line); if (index > 0) { // exclude BTree.ts from transitive if (raw) btreeExTransitive.raw = (btreeExTransitive.raw || 0) + raw; if (min) btreeExTransitive.min = (btreeExTransitive.min || 0) + min; if (gz) btreeExTransitive.gz = (btreeExTransitive.gz || 0) + gz; } }); if (entryPoints.length > 1) { const line = pad('BTreeEx transitive excl. BTree', nameColumnWidth) + pad(formatBytes(btreeExTransitive.raw), 13) + pad(formatBytes(btreeExTransitive.min), 13) + formatBytes(btreeExTransitive.gz); console.log('-'.repeat(header.length)); console.log(line); } if (process.exitCode) { process.exit(1); } ================================================ FILE: sorted-array.d.ts ================================================ import { IMap } from './interfaces'; /** A super-inefficient sorted list for testing purposes */ export default class SortedArray implements IMap { a: [K, V][]; cmp: (a: K, b: K) => number; constructor(entries?: [K, V][], compare?: (a: K, b: K) => number); get size(): number; get(key: K, defaultValue?: V): V | undefined; set(key: K, value: V, overwrite?: boolean): boolean; has(key: K): boolean; delete(key: K): boolean; clear(): void; getArray(): [K, V][]; minKey(): K | undefined; maxKey(): K | undefined; forEach(callbackFn: (v: V, k: K, list: SortedArray) => void): void; [Symbol.iterator](): IterableIterator<[K, V]>; entries(): IterableIterator<[K, V]>; keys(): IterableIterator; values(): IterableIterator; indexOf(key: K, failXor: number): number; } ================================================ FILE: sorted-array.js ================================================ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /** A super-inefficient sorted list for testing purposes */ var SortedArray = /** @class */ (function () { function SortedArray(entries, compare) { this.cmp = compare || (function (a, b) { return a < b ? -1 : a > b ? 1 : a === b ? 0 : a - b; }); this.a = []; if (entries !== undefined) for (var _i = 0, entries_1 = entries; _i < entries_1.length; _i++) { var e = entries_1[_i]; this.set(e[0], e[1]); } } Object.defineProperty(SortedArray.prototype, "size", { get: function () { return this.a.length; }, enumerable: false, configurable: true }); SortedArray.prototype.get = function (key, defaultValue) { var pair = this.a[this.indexOf(key, -1)]; return pair === undefined ? defaultValue : pair[1]; }; SortedArray.prototype.set = function (key, value, overwrite) { var i = this.indexOf(key, -1); if (i <= -1) this.a.splice(~i, 0, [key, value]); else this.a[i] = [key, value]; return i <= -1; }; SortedArray.prototype.has = function (key) { return this.indexOf(key, -1) >= 0; }; SortedArray.prototype.delete = function (key) { var i = this.indexOf(key, -1); if (i > -1) this.a.splice(i, 1); return i > -1; }; SortedArray.prototype.clear = function () { this.a = []; }; SortedArray.prototype.getArray = function () { return this.a; }; SortedArray.prototype.minKey = function () { return this.a[0][0]; }; SortedArray.prototype.maxKey = function () { return this.a[this.a.length - 1][0]; }; SortedArray.prototype.forEach = function (callbackFn) { var _this = this; this.a.forEach(function (pair) { return callbackFn(pair[1], pair[0], _this); }); }; // a.values() used to implement IMap but it's not actually available in Node v10.4 SortedArray.prototype[Symbol.iterator] = function () { return this.a.values(); }; SortedArray.prototype.entries = function () { return this.a.values(); }; SortedArray.prototype.keys = function () { return this.a.map(function (pair) { return pair[0]; }).values(); }; SortedArray.prototype.values = function () { return this.a.map(function (pair) { return pair[1]; }).values(); }; SortedArray.prototype.indexOf = function (key, failXor) { var lo = 0, hi = this.a.length, mid = hi >> 1; while (lo < hi) { var c = this.cmp(this.a[mid][0], key); if (c < 0) lo = mid + 1; else if (c > 0) // keys[mid] > key hi = mid; else if (c === 0) return mid; else throw new Error("Problem: compare failed"); mid = (lo + hi) >> 1; } return mid ^ failXor; }; return SortedArray; }()); exports.default = SortedArray; ================================================ FILE: sorted-array.ts ================================================ import {IMap} from './interfaces'; /** A super-inefficient sorted list for testing purposes */ export default class SortedArray implements IMap { a: [K,V][]; cmp: (a: K, b: K) => number; public constructor(entries?: [K,V][], compare?: (a: K, b: K) => number) { this.cmp = compare || ((a: any, b: any) => a < b ? -1 : a > b ? 1 : a === b ? 0 : a - b); this.a = []; if (entries !== undefined) for (var e of entries) this.set(e[0], e[1]); } get size() { return this.a.length; } get(key: K, defaultValue?: V): V | undefined { var pair = this.a[this.indexOf(key, -1)]; return pair === undefined ? defaultValue : pair[1]; } set(key: K, value: V, overwrite?: boolean): boolean { var i = this.indexOf(key, -1); if (i <= -1) this.a.splice(~i, 0, [key, value]); else this.a[i] = [key, value]; return i <= -1; } has(key: K): boolean { return this.indexOf(key, -1) >= 0; } delete(key: K): boolean { var i = this.indexOf(key, -1); if (i > -1) this.a.splice(i, 1); return i > -1; } clear() { this.a = []; } getArray() { return this.a; } minKey(): K | undefined { return this.a[0][0]; } maxKey(): K | undefined { return this.a[this.a.length-1][0]; } forEach(callbackFn: (v:V, k:K, list:SortedArray) => void) { this.a.forEach(pair => callbackFn(pair[1], pair[0], this)); } // a.values() used to implement IMap but it's not actually available in Node v10.4 [Symbol.iterator](): IterableIterator<[K,V]> { return this.a.values(); } entries(): IterableIterator<[K,V]> { return this.a.values(); } keys(): IterableIterator { return this.a.map(pair => pair[0]).values(); } values(): IterableIterator { return this.a.map(pair => pair[1]).values(); } indexOf(key: K, failXor: number): number { var lo = 0, hi = this.a.length, mid = hi >> 1; while(lo < hi) { var c = this.cmp(this.a[mid][0], key); if (c < 0) lo = mid + 1; else if (c > 0) // keys[mid] > key hi = mid; else if (c === 0) return mid; else throw new Error("Problem: compare failed"); mid = (lo + hi) >> 1; } return mid ^ failXor; } } ================================================ FILE: test/b+tree.test.ts ================================================ import BTree, { IMap, defaultComparator, simpleComparator, areOverlapping } from '../b+tree'; import BTreeEx from '../extended'; import SortedArray from '../sorted-array'; import { addToBoth, expectTreeEqualTo, randInt } from './shared'; var test: (name:string,f:()=>void)=>void = it; describe('defaultComparator', () => { const dateA = new Date(Date.UTC(96, 1, 2, 3, 4, 5)); const dateA2 = new Date(Date.UTC(96, 1, 2, 3, 4, 5)); const dateB = new Date(Date.UTC(96, 1, 2, 3, 4, 6)); const values = [ dateA, dateA2, dateB, dateA.valueOf(), '24x', '0', '1', '3', 'String', '10', 0, "NaN", NaN, Infinity, -0, -Infinity, 1, 10, 2, [], '[]', [1], ['1'] ]; const sorted = [-Infinity, -10, -1, -0, 0, 1, 2, 10, Infinity]; testComparison(defaultComparator, sorted, values, [[dateA, dateA2], [0, -0], [[1], ['1']]]); }); describe('simpleComparator with non-NaN numbers and null', () => { const sorted = [-Infinity, -10, -1, -0, 0, null, 1, 2, 10, Infinity]; testComparison(simpleComparator, sorted, sorted, [[-0, 0], [-0, null], [0, null]]); }); describe('simpleComparator with strings', () => { const values = [ '24x', '+0', '0.0', '0', '-0', '1', '3', 'String', '10', "NaN", ];; testComparison(simpleComparator, [], values, []); }); describe('simpleComparator with Date', () => { const dateA = new Date(Date.UTC(96, 1, 2, 3, 4, 5)); const dateA2 = new Date(Date.UTC(96, 1, 2, 3, 4, 5)); const dateB = new Date(Date.UTC(96, 1, 2, 3, 4, 6)); const values = [ dateA, dateA2, dateB, null, ]; testComparison(simpleComparator, [], values, [[dateA, dateA2]]); }); describe('simpleComparator arrays', () => { const values = [ [], [1], ['1'], [2], ]; testComparison<(number|string)[] >(simpleComparator, [], values, [[[1], ['1']]]); }); /** * Tests a comparison function, ensuring it produces a strict partial order over the provided values. * Additionally confirms that the comparison function has the correct definition of equality via expectedDuplicates. */ function testComparison(comparison: (a: T, b: T) => number, inOrder: T[], values: T[], expectedDuplicates: [T, T][] = []) { function compare(a: T, b: T): number { const v = comparison(a, b); expect(typeof v).toEqual('number'); if (v !== v) console.log('!!!', a, b); expect(v === v).toEqual(true); // Not NaN return Math.sign(v); } test('comparison has correct order', () => { expect([...inOrder].sort(comparison)).toMatchObject(inOrder); }); test('comparison deffierantes values', () => { let duplicates = []; for (let i = 0; i < values.length; i++) { for (let j = i + 1; j < values.length; j++) { if (compare(values[i], values[j]) === 0) { duplicates.push([values[i], values[j]]); } } } expect(duplicates).toMatchObject(expectedDuplicates); }); test('comparison forms a strict partial ordering', () => { // To be a strict partial order, the function must be: // irreflexive: not a < a // transitive: if a < b and b < c then a < c // asymmetric: if a < b then not b < a // Since our comparison has three outputs, we adjust that to, we need to tighten the rules that involve 'not a < b' (where we have two possible outputs) as follows: // irreflexive: compare(a, a) === 0 // transitive: if compare(a, b) < 0 and compare(b, c) < 0 then compare(a, c) < 0 // asymmetric: sign(compare(a, b)) === -sign(compare(b, a)) // This can is brute forced in O(n^3) time below: // Violations const irreflexive = [] const transitive = [] const asymmetric = [] for (const a of values) { // irreflexive: compare(a, a) === 0 if(compare(a, a) !== 0) irreflexive.push(a); for (const b of values) { for (const c of values) { // transitive: if compare(a, b) < 0 and compare(b, c) < 0 then compare(a, c) < 0 if (compare(a, b) < 0 && compare(b, c) < 0) { if(compare(a, c) !== -1) transitive.push([a, b, c]); } } // sign(compare(a, b)) === -sign(compare(b, a)) if(compare(a, b) !== -compare(b, a)) asymmetric.push([a, b]); } } expect(irreflexive).toEqual([]); expect(transitive).toEqual([]); expect(asymmetric).toEqual([]); }); } describe('areOverlapping', () => { const cmp: (a: number, b: number) => number = simpleComparator; const overlappingCases: { name: string, a: [number, number], b: [number, number] }[] = [ { name: 'aMax inside B (A starts before B)', a: [0, 5], b: [3, 8] }, { name: 'aMin inside B (A ends after B)', a: [4, 12], b: [1, 7] }, { name: 'A encloses B', a: [0, 10], b: [3, 7] }, { name: 'B encloses A', a: [4, 6], b: [1, 10] }, { name: 'shared boundary counts as overlap', a: [2, 6], b: [6, 9] }, { name: 'identical ranges overlap', a: [5, 5], b: [5, 5] }, ]; overlappingCases.forEach(({ name, a, b }) => { test(name, () => { expect(areOverlapping(a[0], a[1], b[0], b[1], cmp)).toBe(true); expect(areOverlapping(b[0], b[1], a[0], a[1], cmp)).toBe(true); }); }); const disjointCases: { name: string, a: [number, number], b: [number, number] }[] = [ { name: 'A entirely before B', a: [0, 2], b: [3, 5] }, { name: 'A entirely after B', a: [8, 9], b: [3, 5] }, ]; disjointCases.forEach(({ name, a, b }) => { test(name, () => { expect(areOverlapping(a[0], a[1], b[0], b[1], cmp)).toBe(false); expect(areOverlapping(b[0], b[1], a[0], a[1], cmp)).toBe(false); }); }); }); describe('height calculation', () => { test('Empty tree', () => { const tree = new BTree(); expect(tree.height).toEqual(0); }); test('Single node', () => { const tree = new BTree([[0, 0]]); expect(tree.height).toEqual(0); }); test('Multiple node, no internal nodes', () => { const tree = new BTree([[0, 0], [1, 1]], undefined, 32); expect(tree.height).toEqual(0); }); test('Multiple internal nodes', () => { for (let expectedHeight = 1; expectedHeight < 5; expectedHeight++) { for (let nodeSize = 4; nodeSize < 10; nodeSize++) { const numEntries = nodeSize ** expectedHeight; const entries: [number, number][] = []; for (let i = 0; i < numEntries; i++) { entries.push([i, i]); } const tree = new BTree(entries, undefined, nodeSize); expect(tree.height).toEqual(expectedHeight - 1); } } }); }); describe('cached sizes', () => { function buildTestTree(entryCount: number, maxNodeSize: number) { const tree = new BTree(undefined, undefined, maxNodeSize); for (let i = 0; i < entryCount; i++) { tree.set(i, i); } return tree; } function expectSize(tree: BTree, size: number) { expect(tree.size).toBe(size); tree.checkValid(); } [4, 6, 8, 16].forEach(nodeSize => { describe(`fanout ${nodeSize}`, () => { test('checkValid detects root size mismatch', () => { const tree = buildTestTree(nodeSize * 8, nodeSize); const root = (tree as any)._root; expect(root.isLeaf).toBe(false); (root as any).size = 0; expect(() => tree.checkValid()).toThrow(); }); test('checkValid detects mismatched child sizes', () => { const tree = buildTestTree(nodeSize * nodeSize * 4, nodeSize); const root = (tree as any)._root; expect(root.isLeaf).toBe(false); const internalChild = (root as any).children.find((child: any) => !child.isLeaf); expect(internalChild).toBeDefined(); (internalChild as any).size = 0; expect(() => tree.checkValid()).toThrow(); }); test('mutations preserve cached sizes', () => { const tree = buildTestTree(nodeSize * 4, nodeSize); const initialSize = tree.size; const expectedKeys = new Set(); for (let i = 0; i < initialSize; i++) expectedKeys.add(i); expectSize(tree, expectedKeys.size); // Insert sequential items const itemsToAdd = nodeSize * 2; for (let i = 0; i < itemsToAdd; i++) { const key = initialSize + i; tree.set(key, key); expectedKeys.add(key); } expectSize(tree, expectedKeys.size); // Delete every third new item let deleted = 0; for (let i = 0; i < itemsToAdd; i += 3) { const key = initialSize + i; if (tree.delete(key)) { deleted++; expectedKeys.delete(key); } } expectSize(tree, expectedKeys.size); // Bulk delete a middle range const low = Math.floor(initialSize / 2); const high = low + nodeSize; const rangeDeleted = tree.deleteRange(low, high, true); const toRemove = Array.from(expectedKeys).filter(k => k >= low && k <= high); expect(rangeDeleted).toBe(toRemove.length); toRemove.forEach(k => expectedKeys.delete(k)); expectSize(tree, expectedKeys.size); // Mix insertions and overwrites const extra = nodeSize * 5; for (let i = 0; i < extra; i++) { const insertKey = -i - 1; tree.set(insertKey, insertKey); expectedKeys.add(insertKey); const overwriteKey = i % (initialSize + 1); tree.set(overwriteKey, 42); // overwrite existing keys expectedKeys.add(overwriteKey); } expectSize(tree, expectedKeys.size); // Clone should preserve size and cached metadata const toClone = tree.clone(); expectSize(toClone, expectedKeys.size); // Edit range deletes some entries, patches others tree.editRange(-extra, extra, false, (k, v, counter) => { if (counter % 11 === 0) { expectedKeys.delete(k); return { delete: true }; } if (k % 5 === 0) return { value: v + 1 }; }); expectSize(tree, expectedKeys.size); }); }); }); }); describe('Simple tests on leaf nodes', () => { test('A few insertions (fanout 8)', insert8.bind(null, 8)); test('A few insertions (fanout 4)', insert8.bind(null, 4)); function insert8(maxNodeSize: number) { var items: [number,any][] = [[6,"six"],[7,7],[5,5],[2,"two"],[4,4],[1,"one"],[3,3],[8,8]]; var tree = new BTree(items, undefined, maxNodeSize); var list = new SortedArray(items, undefined); tree.checkValid(); expect(tree.keysArray()).toEqual([1,2,3,4,5,6,7,8]); expectTreeEqualTo(tree, list); } function forExpector(k:number, v:string, counter:number, i:number, first: number = 0) { expect(k).toEqual(v.length); expect(k - first).toEqual(counter); expect(k - first).toEqual(i); } { let tree = new BTree([[0,""],[1,"1"],[2,"to"],[3,"tri"],[4,"four"],[5,"five!"]]); test('forEach', () => { let i = 0; expect(tree.forEach(function(this:any, v, k, tree_) { expect(tree_).toBe(tree); expect((this as any).self).toBe("me"); forExpector(k, v, i, i++); }, {self:"me"})).toBe(6); }); test('forEachPair', () => { let i = 0; expect(tree.forEachPair(function(k,v,counter) { forExpector(k, v, counter - 10, i++); }, 10)).toBe(16); }); test('forRange', () => { let i = 0; expect(tree.forRange(2, 4, false, function(k,v,counter) { forExpector(k, v, counter - 10, i++, 2); }, 10)).toBe(12); i = 0; expect(tree.forRange(2, 4, true, function(k,v,counter) { forExpector(k, v, counter - 10, i++, 2); }, 10)).toBe(13); i = 0; expect(tree.forRange(0, 4.5, true, function(k,v,counter) { forExpector(k, v, counter - 10, i++); }, 10)).toBe(15); }); test('editRange', () => { let i = 0; expect(tree.editRange(1, 4, true, function(k,v,counter) { forExpector(k, v, counter - 10, i++, 1); }, 10)).toBe(14); i = 0; expect(tree.editRange(1, 9, true, function(k,v,counter) { forExpector(k, v, counter - 10, i++, 1); if (k & 1) return {delete:true}; if (k == 2) return {value:"TWO!"}; if (k >= 4) return {break:"STOP"}; }, 10)).toBe("STOP"); expect(tree.toArray()).toEqual([[0,""],[2,"TWO!"],[4,"four"],[5,"five!"]]) }); } { let items: [string,any][] = [["A",1],["B",2],["C",3],["D",4],["E",5],["F",6],["G",7],["H",8]]; let tree = new BTree(items); tree.checkValid(); test('has() in a leaf node of strings', () => { expect(tree.has("!")).toBe(false); expect(tree.has("A")).toBe(true); expect(tree.has("H")).toBe(true); expect(tree.has("Z")).toBe(false); }); test('get() in a leaf node of strings', () => { expect(tree.get("!", 7)).toBe(7); expect(tree.get("A", 7)).toBe(1); expect(tree.get("H", 7)).toBe(8); expect(tree.get("Z", 7)).toBe(7); }); test('getRange() in a leaf node', () => { expect(tree.getRange("#", "B", false)).toEqual([["A",1]]); expect(tree.getRange("#", "B", true)).toEqual([["A",1],["B",2]]); expect(tree.getRange("G", "S", true)).toEqual([["G",7],["H",8]]); }); test('iterators work on leaf nodes', () => { expect(Array.from(tree.entries())).toEqual(items); expect(Array.from(tree.keys())).toEqual(items.map(p => p[0])); expect(Array.from(tree.values())).toEqual(items.map(p => p[1])); }); test('try out the reverse iterator', () => { expect(Array.from(tree.entriesReversed())).toEqual(items.slice(0).reverse()); }); test('minKey() and maxKey()', () => { expect(tree.minKey()).toEqual("A"); expect(tree.maxKey()).toEqual("H"); }); test('delete() in a leaf node', () => { expect(tree.delete("C")).toBe(true); expect(tree.delete("C")).toBe(false); expect(tree.delete("H")).toBe(true); expect(tree.delete("H")).toBe(false); expect(tree.deleteRange(" ","A",false)).toBe(0); expect(tree.deleteRange(" ","A",true)).toBe(1); expectTreeEqualTo(tree, new SortedArray([["B",2],["D",4],["E",5],["F",6],["G",7]])); }); test('editRange() - again', () => { expect(tree.editRange(tree.minKey()!, "F", true, (k,v,counter) => { if (k == "D") return {value: 44}; if (k == "E" || k == "G") return {delete: true}; if (k >= "F") return {stop: counter+1}; })).toBe(4); expectTreeEqualTo(tree, new SortedArray([["B",2],["D",44],["F",6],["G",7]])); }); test("A clone is independent", () => { var tree2 = tree.clone(); expect(tree.delete("G")).toBe(true); expect(tree2.deleteRange("A", "F", false)).toBe(2); expect(tree2.deleteRange("A", "F", true)).toBe(1); expectTreeEqualTo(tree, new SortedArray([["B",2],["D",44],["F",6]])); expectTreeEqualTo(tree2, new SortedArray([["G",7]])); }); } test('Can be frozen and unfrozen', () => { var tree = new BTree([[1,"one"]]); expect(tree.isFrozen).toBe(false); tree.freeze(); expect(tree.isFrozen).toBe(true); expect(() => tree.set(2, "two")).toThrowError(/frozen/); expect(() => tree.setPairs([[2, "two"]])).toThrowError(/frozen/); expect(() => tree.clear()).toThrowError(/frozen/); expect(() => tree.delete(1)).toThrowError(/frozen/); expect(() => tree.editRange(0,10,true, ()=>{return {delete:true};})).toThrowError(/frozen/); expect(tree.toArray()).toEqual([[1, "one"]]); tree.unfreeze(); tree.set(2, "two"); tree.delete(1); expect(tree.toArray()).toEqual([[2, "two"]]); tree.clear(); expect(tree.keysArray()).toEqual([]); }); test('Custom comparator', () => { var tree = new BTree(undefined, (a, b) => { if (a.name > b.name) return 1; // Return a number >0 when a > b else if (a.name < b.name) return -1; // Return a number <0 when a < b else // names are equal (or incomparable) return a.age - b.age; // Return >0 when a.age > b.age }); tree.set({name:"Bill", age:17}, "happy"); tree.set({name:"Rose", age:40}, "busy & stressed"); tree.set({name:"Bill", age:55}, "recently laid off"); tree.set({name:"Rose", age:10}, "rambunctious"); tree.set({name:"Chad", age:18}, "smooth"); // Try editing a key tree.set({name: "Bill", age: 17, planet: "Earth"}, "happy"); var list: any[] = []; expect(tree.forEachPair((k, v) => { list.push(Object.assign({value: v}, k)); }, 10)).toBe(15); expect(list).toEqual([ { name: "Bill", age: 17, planet: "Earth", value: "happy" }, { name: "Bill", age: 55, value: "recently laid off" }, { name: "Chad", age: 18, value: "smooth" }, { name: "Rose", age: 10, value: "rambunctious" }, { name: "Rose", age: 40, value: "busy & stressed" }, ]); }); }); // Tests relating to `isShared` and cloning. // Tests on this subject that do not care about the specific interior structure of the tree // (and are thus maxNodeSize agnostic) can be added to testBTree to be testing on different branching factors instead. describe("cloning and sharing tests", () => { test("Regression test for failing to propagate shared when removing top two layers", () => { // This tests make a full 3 layer tree (height = 2), so use a small branching factor. const maxNodeSize = 4; const tree = new BTree( undefined, simpleComparator, maxNodeSize ); // Build a 3 layer complete tree, all values 0. for ( let index = 0; index < maxNodeSize * maxNodeSize * maxNodeSize; index++ ) { tree.set(index, 0); } // Leaf nodes don't count, so this is depth 2 expect(tree.height).toBe(2); // Edit the tree so it has a node in the second layer with exactly one child (key 0). tree.deleteRange(1, maxNodeSize * maxNodeSize, false); expect(tree.height).toBe(2); // Make a clone that should never be mutated. const clone = tree.clone(); // Mutate the original tree in such a way that clone gets mutated due to incorrect is shared tracking. // Delete everything outside of the internal node with only one child, so its child becomes the new root. tree.deleteRange(maxNodeSize, Number.POSITIVE_INFINITY, false); expect(tree.height).toBe(0); // Modify original tree.set(0, 1); // Check that clone is not modified as well: expect(clone.get(0)).toBe(0); }); test("Regression test for greedyClone(true) not copying all nodes", () => { const maxNodeSize = 4; const tree = new BTree( undefined, simpleComparator, maxNodeSize ); // Build a 3 layer tree. for ( let index = 0; index < maxNodeSize * maxNodeSize + 1; index++ ) { tree.set(index, 0); } // Leaf nodes don't count, so this is depth 2 expect(tree.height).toBe(2); // To trigger the bug, mark children of the root node as shared (not just the root) tree.clone().set(1, 1); const clone = tree.greedyClone(true); // The bug was that `force` was not passed down. This meant that non-shared nodes below the second layer would not be cloned. // Thus we check that the third layer of this tree did get cloned. // Since this depends on private APIs and types, // and this package currently has no way to expose them to tests without exporting them from the package, // do some private field access and any casts to make it work. expect((clone['_root'] as any).children[0].children[0]).not.toBe((tree['_root'] as any).children[0].children[0]); }); test("Regression test for mergeSibling setting isShared", () => { // This tests make a 3 layer tree (height = 2), so use a small branching factor. const maxNodeSize = 4; const tree = new BTreeEx( undefined, simpleComparator, maxNodeSize ); // Build a 3 layer tree const count = maxNodeSize * maxNodeSize * maxNodeSize; for ( let index = 0; index < count; index++ ) { tree.set(index, 0); } // Leaf nodes don't count, so this is depth 2 expect(tree.height).toBe(2); // Delete most of the keys so merging interior nodes is possible, marking all nodes as shared. for ( let index = 0; index < count; index++ ) { if (index % 4 !== 0) { tree.delete(index); } } const deepClone = tree.greedyClone(true); const cheapClone = tree.clone(); // These two clones should remain unchanged forever. // The bug this is testing for resulted in the cheap clone getting modified: // we will compare it against the deep clone to confirm it does not. // Delete a bunch more nodes, causing merging. for ( let index = 0; index < count; index++ ) { if (index % 16 !== 0) { tree.delete(index); } } const different: number[] = []; const onDiff = (k: number) => { different.push(k); } deepClone.diffAgainst(cheapClone, onDiff, onDiff, onDiff); expect(different).toEqual([]); }); }); describe('B+ tree with fanout 32', testBTree.bind(null, 32)); describe('B+ tree with fanout 10', testBTree.bind(null, 10)); describe('B+ tree with fanout 4', testBTree.bind(null, 4)); function testBTree(maxNodeSize: number) { for (let size of [8, 64, 512]) { let tree = new BTree(undefined, undefined, maxNodeSize); let list = new SortedArray(); test(`Insert randomly & toArray [size ${size}]`, () => { while (tree.size < size) { var key = randInt(size * 2); addToBoth(tree, list, key, key); expect(tree.size).toEqual(list.size); } expectTreeEqualTo(tree, list); }); test(`Iteration [size ${size}]`, () => { expect(tree.size).toBe(size); var it = tree.entries(); var array = list.getArray(), i = 0; for (let next = it.next(); !next.done; next = it.next(), i++) { expect(next.value).toEqual(array[i]); } expect(i).toBe(array.length); }); test(`Reverse iteration [size ${size}]`, () => { expect(tree.size).toBe(size); var it = tree.entriesReversed(); var array = list.getArray(), i = array.length-1; for (let next = it.next(); !next.done; next = it.next(), i--) { expect(next.value).toEqual(array[i]); } expect(i).toBe(-1); }); test(`Insert with few values [size ${size}]`, () => { let list = new SortedArray(); for (var i = 0; i < size; i++) { var key = randInt(size * 2); // Use a value only occasionally to stress out the no-values optimization list.set(key, key % 10 == 0 ? key.toString() : undefined); } let tree = new BTree(list.getArray(), undefined, maxNodeSize); expectTreeEqualTo(tree, list); }); } describe(`Next higher/lower methods`, () => { test(`nextLower/nextHigher methods return undefined in an empty tree`, () => { const tree = new BTree(undefined, undefined, maxNodeSize); expect(tree.nextLowerPair(undefined)).toEqual(undefined); expect(tree.nextHigherPair(undefined)).toEqual(undefined); expect(tree.getPairOrNextLower(1)).toEqual(undefined); expect(tree.getPairOrNextHigher(2)).toEqual(undefined); // This shouldn't make a difference tree.set(5, 55); tree.delete(5); expect(tree.nextLowerPair(undefined)).toEqual(undefined); expect(tree.nextHigherPair(undefined)).toEqual(undefined); expect(tree.nextLowerPair(3)).toEqual(undefined); expect(tree.nextHigherPair(4)).toEqual(undefined); expect(tree.getPairOrNextLower(5)).toEqual(undefined); expect(tree.getPairOrNextHigher(6)).toEqual(undefined); }); for (let size of [5, 10, 300]) { // Build a tree and list with pairs whose keys are even numbers: 0, 2, 4, 6, 8, 10... const tree = new BTree(undefined, undefined, maxNodeSize); const pairs: [number,number][] = []; for (let i = 0; i < size; i++) { const value = i; tree.set(i * 2, value); pairs.push([i * 2, value]); } test(`nextLowerPair/nextHigherPair for tree of size ${size}`, () => { expect(tree.nextHigherPair(undefined)).toEqual([tree.minKey()!, tree.get(tree.minKey()!)]); expect(tree.nextHigherPair(tree.maxKey())).toEqual(undefined); for (let i = 0; i < size * 2; i++) { if (i > 0) { expect(tree.nextLowerPair(i)).toEqual(pairs[((i + 1) >> 1) - 1]); } if (i < size - 1) { expect(tree.nextHigherPair(i)).toEqual(pairs[(i >> 1) + 1]); } } expect(tree.nextLowerPair(undefined)).toEqual([tree.maxKey()!, tree.get(tree.maxKey()!)]); expect(tree.nextLowerPair(tree.minKey())).toEqual(undefined); }) test(`getPairOrNextLower/getPairOrNextHigher for tree of size ${size}`, () => { for (let i = 0; i < size * 2; i++) { if (i > 0) { expect(tree.getPairOrNextLower(i)).toEqual(pairs[i >> 1]); } if (i < size - 1) { expect(tree.getPairOrNextHigher(i)).toEqual(pairs[(i + 1) >> 1]); } } }) } }); for (let size of [6, 36, 216]) { test(`setPairs & deleteRange [size ${size}]`, () => { // Store numbers in descending order var reverseComparator = (a:number, b:number) => b - a; // Prepare reference list var list = new SortedArray([], reverseComparator); for (var i = size-1; i >= 0; i--) list.set(i, i.toString()); // Add all to tree in the "wrong" order (ascending) var tree = new BTree(undefined, reverseComparator, maxNodeSize); tree.setPairs(list.getArray().slice(0).reverse()); expectTreeEqualTo(tree, list); // Remove most of the items expect(tree.deleteRange(size-2, 5, true)).toEqual(size-6); expectTreeEqualTo(tree, new SortedArray([ [size-1, (size-1).toString()], [4,"4"], [3,"3"], [2,"2"], [1,"1"], [0,"0"] ], reverseComparator)); expect(tree.deleteRange(size, 0, true)).toEqual(6); expect(tree.toArray()).toEqual([]); }); } for (let size of [5, 25, 125]) { // Ensure standard operations work for various list sizes test(`Various operations [starting size ${size}]`, () => { var tree = new BTree(undefined, undefined, maxNodeSize); var list = new SortedArray(); var i = 0, key; for (var i = 0; tree.size < size; i++) { addToBoth(tree, list, i, undefined); expect(list.size).toEqual(tree.size); } expectTreeEqualTo(tree, list); // Add some in the middle and try get() for (var i = size; i <= size + size/8; i += 0.5) { expect(tree.get(i)).toEqual(list.get(i)); addToBoth(tree, list, i, i); } expectTreeEqualTo(tree, list); expect(tree.get(-15, 12345)).toBe(12345); expect(tree.get(0.5, 12345)).toBe(12345); // Try all the iterators... expect(Array.from(tree.entries())).toEqual(list.getArray()); expect(Array.from(tree.keys())).toEqual(list.getArray().map(p => p[0])); expect(Array.from(tree.values())).toEqual(list.getArray().map(p => p[1])); // Try iterating from past the end... expect(Array.from(tree.entries(tree.maxKey()!+1))).toEqual([]); expect(Array.from(tree.keys(tree.maxKey()!+1))).toEqual([]); expect(Array.from(tree.values(tree.maxKey()!+1))).toEqual([]); expect(Array.from(tree.entriesReversed(tree.minKey()!-1))).toEqual([]); // Try some changes that should have no effect for (var i = size; i < size + size/8; i += 0.5) { expect(tree.setIfNotPresent(i, -i)).toBe(false); expect(tree.changeIfPresent(-i, -i)).toBe(false); } expectTreeEqualTo(tree, list); // Remove a few items and check against has() for (var i = 0; i < 10; i++) { key = randInt(size * 2) / 2; var has = tree.has(key); expect(has).toEqual(list.has(key)); expect(has).toEqual(tree.delete(key)); expect(has).toEqual(list.delete(key)); expectTreeEqualTo(tree, list); } expectTreeEqualTo(tree, list); }); } test('persistent and functional operations', () => { var tree = new BTree(undefined, undefined, maxNodeSize); var list = new SortedArray(); // Add keys 10 to 5000, step 10 for (var i = 1; i <= 500; i++) addToBoth(tree, list, i*10, i); // Test reduce() expect(tree.reduce((sum, pair) => sum + pair[1]!, 0)).toBe(501*250); // Test mapValues() tree.mapValues(v => v!*10).forEachPair((k, v) => { expect(v).toBe(k) }); // Perform various kinds of no-ops var t1 = tree; expect(t1.withKeys([10,20,30], true) ).toBe(tree); expect(t1.withKeys([10,20,30], false) ).not.toBe(tree); expect(t1.withoutKeys([5,105,205], true) ).toBe(tree); expect(t1.without(666, true) ).toBe(tree); expect(t1.withoutRange(1001, 1010, false, true)).toBe(tree); expect(t1.filter(() => true, true) ).toBe(tree); // Make a series of modifications in persistent mode var t2 = t1.with(5,5).with(999,999); var t3 = t2.without(777).without(7); var t4 = t3.withPairs([[60,66],[6,6.6]], false); var t5 = t4.withKeys([199,299,399], true); var t6 = t4.without(200).without(300).without(400); var t7 = t6.withoutKeys([10,20,30], true); var t8 = t7.withoutRange(100, 200, false, true); // Check that it all worked as expected expectTreeEqualTo(t1, list); list.set(5, 5); list.set(999, 999); expectTreeEqualTo(t2, list); list.delete(777); list.delete(7); expectTreeEqualTo(t3, list); list.set(6, 6.6); expectTreeEqualTo(t4, list); list.set(199, undefined); list.set(299, undefined); list.set(399, undefined); expectTreeEqualTo(t5, list); for(var k of [199, 299, 399, 200, 300, 400]) list.delete(k); expectTreeEqualTo(t6, list); for(var k of [10, 20, 30]) list.delete(k); expectTreeEqualTo(t7, list); for(var i = 100; i < 200; i++) list.delete(i); expectTreeEqualTo(t8, list); // Filter out all hundreds var t9 = t8.filter(k => k % 100 !== 0, true); for (let k = 0; k <= tree.maxKey()!; k += 100) list.delete(k); expectTreeEqualTo(t9, list); }); test("Issue #2 reproduction", () => { const tree = new BTree([], (a, b) => a - b, maxNodeSize); for (let i = 0; i <= 1999; i++) { tree.set(i, i); if (tree.size > 100 && i % 2 == 0) { const key = i / 2; tree.delete(key); tree.checkValid(); expect(tree.size).toBe(i / 2 + 50); } } }); test("entriesReversed when highest key does not exist", () => { const entries: [{ key: number}, number][] = [[{ key: 10 }, 0], [{ key: 20 }, 0], [{ key: 30 }, 0]]; const tree = new BTree<{ key: number }, number>(entries, (a, b) => a.key - b.key); expect(Array.from(tree.entriesReversed({ key: 40 }))).toEqual(entries.reverse()); }); test("nextLowerPair/nextHigherPair and issue #9: nextLowerPair returns highest pair if key is 0", () => { const tree = new BTree(undefined, undefined, maxNodeSize); tree.set(-2, 123); tree.set(0, 1234); tree.set(2, 12345); expect(tree.nextLowerPair(-2)).toEqual(undefined); expect(tree.nextLowerPair(-1)).toEqual([-2, 123]); expect(tree.nextLowerPair(0)).toEqual([-2, 123]); expect(tree.nextLowerKey(0)).toBe(-2); expect(tree.nextHigherPair(-1)).toEqual([0, 1234]); expect(tree.nextHigherPair(0)).toEqual([2, 12345]); expect(tree.nextHigherKey(0)).toBe(2); expect(tree.nextHigherPair(1)).toEqual([2, 12345]); expect(tree.nextHigherPair(2)).toEqual(undefined); expect(tree.nextLowerPair(undefined)).toEqual([2, 12345]); expect(tree.nextHigherPair(undefined)).toEqual([-2, 123]); for (let i = -10; i <= 300; i++) // embiggen the tree tree.set(i, i*2); expect(tree.nextLowerPair(-1)).toEqual([-2, -4]); expect(tree.nextLowerPair(0)).toEqual([-1, -2]); expect(tree.nextHigherPair(-1)).toEqual([0, 0]); expect(tree.nextHigherPair(0)).toEqual([1, 2]); expect(tree.nextLowerPair(undefined)).toEqual([300, 600]); expect(tree.nextHigherPair(undefined)).toEqual([-10, -20]); }); test('Regression test for invalid default comparator causing malformed trees', () => { const key = '24e26f0b-3c1a-47f8-a7a1-e8461ddb69ce6'; const tree = new BTree(undefined, undefined, maxNodeSize); // The defaultComparator was not transitive for these inputs due to comparing numeric strings to each other numerically, // but lexically when compared to non-numeric strings. This resulted in keys not being orderable, and the tree behaving incorrectly. const inputs: [string,{}][] = [ [key, {}], ['0', {}], ['1', {}], ['2', {}], ['3', {}], ['4', {}], ['Cheese', {}], ['10', {}], ['11', {}], ['12', {}], ['13', {}], ['15', {}], ['16', {}], ]; for (const [id, node] of inputs) { expect( tree.set(id, node)).toBeTruthy(); tree.checkValid(); expect(tree.get(key)).not.toBeUndefined(); } expect(tree.get(key)).not.toBeUndefined(); }); } ================================================ FILE: test/bulkLoad.test.ts ================================================ import BTree, { BNode, BNodeInternal } from '../b+tree'; import BTreeEx from '../extended'; import { bulkLoad } from '../extended/bulkLoad'; import MersenneTwister from 'mersenne-twister'; import { makeArray, randomInt } from './shared'; type Pair = [number, number]; const compareNumbers = (a: number, b: number) => a - b; const branchingFactors = [4, 10, 32, 128]; function sequentialPairs(count: number, start = 0, step = 1): Pair[] { const pairs: Pair[] = []; let key = start; for (let i = 0; i < count; i++) { pairs.push([key, key * 2]); key += step; } return pairs; } function pairsFromKeys(keys: number[]): Pair[] { return keys.map((key, index) => [key, index - key]); } function toParallelArrays(pairs: Pair[]): { keys: number[]; values: number[] } { const keys = new Array(pairs.length); const values = new Array(pairs.length); for (let i = 0; i < pairs.length; i++) { const [key, value] = pairs[i]; keys[i] = key; values[i] = value; } return { keys, values }; } function buildTreeFromPairs(maxNodeSize: number, pairs: Pair[], loadFactor: number) { const { keys, values } = toParallelArrays(pairs); const tree = bulkLoad(keys, values, maxNodeSize, compareNumbers, loadFactor); const root = tree['_root'] as BNode; return { tree, root }; } function expectTreeMatches(tree: BTree, expected: Pair[]) { tree.checkValid(true); expect(tree.size).toBe(expected.length); expect(tree.toArray()).toEqual(expected); } function collectLeaves(node: BNode): BNode[] { if (node.isLeaf) return [node]; const internal = node as unknown as BNodeInternal; const leaves: BNode[] = []; for (const child of internal.children) leaves.push(...collectLeaves(child as BNode)); return leaves; } function assertInternalNodeFanout(node: BNode, maxNodeSize: number, isRoot = true) { if (node.isLeaf) return; const internal = node as unknown as BNodeInternal; if (isRoot) { expect(internal.children.length).toBeGreaterThanOrEqual(2); } else { expect(internal.children.length).toBeGreaterThanOrEqual(Math.floor(maxNodeSize / 2)); } expect(internal.children.length).toBeLessThanOrEqual(maxNodeSize); for (const child of internal.children) assertInternalNodeFanout(child as BNode, maxNodeSize, false); } describe.each(branchingFactors)('bulkLoad fanout %i', (maxNodeSize) => { test('throws when keys are not strictly ascending', () => { const keys = [3, 2]; const values = [30, 20]; expect(() => bulkLoad(keys.slice(), values.slice(), maxNodeSize, compareNumbers)) .toThrow('bulkLoad: keys must be sorted in strictly ascending order'); }); test('empty input produces empty tree', () => { const { tree, root } = buildTreeFromPairs(maxNodeSize, [], 1.0); expect(root?.isLeaf).toBe(true); expect(root?.keys.length ?? 0).toBe(0); expectTreeMatches(tree, []); }); test('single entry stays in one leaf', () => { const pairs = sequentialPairs(1, 5); const { tree } = buildTreeFromPairs(maxNodeSize, pairs, 1.0); expectTreeMatches(tree, pairs); const root = tree['_root'] as BNode; expect(root.isLeaf).toBe(true); expect(root.keys).toEqual([5]); }); test('fills a single leaf up to capacity', () => { const pairs = sequentialPairs(maxNodeSize, 0, 2); const { tree } = buildTreeFromPairs(maxNodeSize, pairs, 1.0); expectTreeMatches(tree, pairs); const root = tree['_root'] as BNode; expect(root.isLeaf).toBe(true); expect(root.keys.length).toBe(maxNodeSize); }); test('does not produce underfilled nodes if possible', () => { const pairs = sequentialPairs(maxNodeSize, 0, 2); // despite asking for only 60% load factor, we should still get a full node // because splitting into > 1 leaf would cause underfilled nodes const { tree } = buildTreeFromPairs(maxNodeSize, pairs, 0.6); expectTreeMatches(tree, pairs); const root = tree['_root'] as BNode; expect(root.isLeaf).toBe(true); expect(root.keys.length).toBe(maxNodeSize); }); test('does not mutate the supplied entry list', () => { const pairs = sequentialPairs(maxNodeSize, 0, 2); const { keys, values } = toParallelArrays(pairs); const originalKeys = keys.slice(); const originalValues = values.slice(); const tree = bulkLoad(keys, values, maxNodeSize, compareNumbers, 0.6); expect(keys).toEqual(originalKeys); expect(values).toEqual(originalValues); expectTreeMatches(tree, pairs); }); test('throws when load factor is too low or too high', () => { const pairs = sequentialPairs(maxNodeSize, 0, 2); const { keys, values } = toParallelArrays(pairs); expect(() => bulkLoad(keys.slice(), values.slice(), maxNodeSize, compareNumbers, 0.3)).toThrow(); expect(() => bulkLoad(keys.slice(), values.slice(), maxNodeSize, compareNumbers, 1.1)).toThrow(); }); test('distributes keys nearly evenly across leaves when not divisible by fanout', () => { const inputSize = maxNodeSize * 3 + Math.floor(maxNodeSize / 2) + 1; const pairs = sequentialPairs(inputSize, 10, 3); const { tree } = buildTreeFromPairs(maxNodeSize, pairs, 0.8); expectTreeMatches(tree, pairs); const leaves = collectLeaves(tree['_root'] as BNode); const leafSizes = leaves.map((leaf) => leaf.keys.length); const min = Math.min.apply(Math, leafSizes); const max = Math.max.apply(Math, leafSizes); expect(max - min).toBeLessThanOrEqual(1); }); test('creates multiple internal layers when leaf count exceeds branching factor', () => { const inputSize = maxNodeSize * maxNodeSize + Math.floor(maxNodeSize / 2) + 1; const pairs = sequentialPairs(inputSize, 0, 1); const { tree } = buildTreeFromPairs(maxNodeSize, pairs, 0.8); expectTreeMatches(tree, pairs); const root = tree['_root'] as BNode; expect(root.isLeaf).toBe(false); assertInternalNodeFanout(root, maxNodeSize); }); test('loads 10000 entries and preserves all data', () => { const keys = makeArray(10000, false, 3); const pairs = pairsFromKeys(keys); const { tree } = buildTreeFromPairs(maxNodeSize, pairs, 1.0); expectTreeMatches(tree, pairs); const leaves = collectLeaves(tree['_root'] as BNode); expect(leaves.length).toBe(Math.ceil(pairs.length / maxNodeSize)); assertInternalNodeFanout(tree['_root'] as BNode, maxNodeSize); }); test('entries with 50% load factor, second layer with exactly half full nodes', () => { // Create enough entries to require a second layer that has exactly two nodes when maxNodeSize is even. const entryCount = Math.ceil(maxNodeSize / 2) * maxNodeSize; const keys = makeArray(entryCount, false, 3); const pairs = pairsFromKeys(keys); const { tree } = buildTreeFromPairs(maxNodeSize, pairs, 0.5); expectTreeMatches(tree, pairs); }); }); describe('BTreeEx.bulkLoad', () => { test.each(branchingFactors)('creates tree for fanout %i', (maxNodeSize) => { const pairs = sequentialPairs(maxNodeSize * 2 + 3, 7, 1); const { keys, values } = toParallelArrays(pairs); const tree = BTreeEx.bulkLoad(keys, values, maxNodeSize, compareNumbers); expect(tree).toBeInstanceOf(BTreeEx); expectTreeMatches(tree, pairs); }); }); describe('bulkLoad fuzz tests', () => { const FUZZ_SETTINGS = { branchingFactors, ooms: [0, 2, 3], iterationsPerOOM: 3, loadFactors: [0.5, 0.8, 1.0], timeoutMs: 30_000, } as const; jest.setTimeout(FUZZ_SETTINGS.timeoutMs); const rng = new MersenneTwister(0xB01C10AD); for (const maxNodeSize of FUZZ_SETTINGS.branchingFactors) { describe(`fanout ${maxNodeSize}`, () => { for (const oom of FUZZ_SETTINGS.ooms) { const baseSize = 5 * Math.pow(10, oom); for (let iteration = 0; iteration < FUZZ_SETTINGS.iterationsPerOOM; iteration++) { for (const loadFactor of FUZZ_SETTINGS.loadFactors) { const targetNodeSize = Math.ceil(maxNodeSize * loadFactor); const sizeJitter = randomInt(rng, baseSize); const size = baseSize + sizeJitter; test(`size ${size}, iteration ${iteration}`, () => { const keys = makeArray(size, false, 0, rng); const pairs = pairsFromKeys(keys).map(([key, value], index) => [key, value + index] as Pair); const { tree, root } = buildTreeFromPairs(maxNodeSize, pairs, loadFactor); expectTreeMatches(tree, pairs); const leaves = collectLeaves(root); const leafSizes = leaves.map((leaf) => leaf.keys.length); if (pairs.length >= maxNodeSize) { const expectedLeafCount = Math.ceil(pairs.length / targetNodeSize); expect(leaves.length).toBe(expectedLeafCount); } const minLeaf = Math.min(...leafSizes); const maxLeaf = Math.max(...leafSizes); expect(maxLeaf - minLeaf).toBeLessThanOrEqual(1); }); } } } }); } }); ================================================ FILE: test/diffAgainst.test.ts ================================================ import BTree from '../b+tree'; import BTreeEx from '../extended'; import diffAgainst from '../extended/diffAgainst'; var test: (name: string, f: () => void) => void = it; const FANOUTS = [32, 10, 4] as const; for (const fanout of FANOUTS) { describe(`BTree diffAgainst tests with fanout ${fanout}`, () => { runDiffAgainstSuite(fanout); }); } function runDiffAgainstSuite(maxNodeSize: number): void { describe('Diff computation', () => { let onlyThis: Map; let onlyOther: Map; let different: Map; function reset(): void { onlyOther = new Map(); onlyThis = new Map(); different = new Map(); } beforeEach(() => reset()); const OnlyThis = (k: number, v: number) => { onlyThis.set(k, v); }; const OnlyOther = (k: number, v: number) => { onlyOther.set(k, v); }; const Different = (k: number, vThis: number, vOther: number) => { different.set(k, `vThis: ${vThis}, vOther: ${vOther}`); }; const compare = (a: number, b: number) => a - b; function expectMapsEquals(mapA: Map, mapB: Map) { const onlyA = []; const onlyB = []; const different = []; mapA.forEach((valueA, keyA) => { const valueB = mapB.get(keyA); if (valueB === undefined) { onlyA.push([keyA, valueA]); } else if (!Object.is(valueB, valueB)) { different.push([keyA, valueA, valueB]); } }); mapB.forEach((valueB, keyB) => { const valueA = mapA.get(keyB); if (valueA === undefined) { onlyA.push([keyB, valueB]); } }); expect(onlyA.length).toEqual(0); expect(onlyB.length).toEqual(0); expect(different.length).toEqual(0); } function expectDiffCorrect(treeThis: BTreeEx, treeOther: BTreeEx): void { reset(); treeThis.diffAgainst(treeOther, OnlyThis, OnlyOther, Different); const onlyThisT: Map = new Map(); const onlyOtherT: Map = new Map(); const differentT: Map = new Map(); treeThis.forEachPair((kThis, vThis) => { if (!treeOther.has(kThis)) { onlyThisT.set(kThis, vThis); } else { const vOther = treeOther.get(kThis); if (!Object.is(vThis, vOther)) differentT.set(kThis, `vThis: ${vThis}, vOther: ${vOther}`); } }); treeOther.forEachPair((kOther, vOther) => { if (!treeThis.has(kOther)) { onlyOtherT.set(kOther, vOther); } }); expectMapsEquals(onlyThis, onlyThisT); expectMapsEquals(onlyOther, onlyOtherT); expectMapsEquals(different, differentT); } test('Diff of trees with different comparators is an error', () => { const treeA = new BTreeEx([], compare); const treeB = new BTreeEx([], (a, b) => b - a); expect(() => treeA.diffAgainst(treeB, OnlyThis, OnlyOther, Different)).toThrow('comparators'); }); test('Standalone diffAgainst works with core trees', () => { const treeA = new BTree([[1, 1], [2, 2], [4, 4]], compare, maxNodeSize); const treeB = new BTree([[1, 1], [2, 22], [3, 3]], compare, maxNodeSize); const onlyThisKeys: number[] = []; const onlyOtherKeys: number[] = []; const differentKeys: number[] = []; diffAgainst( treeA, treeB, (k) => { onlyThisKeys.push(k); }, (k) => { onlyOtherKeys.push(k); }, (k) => { differentKeys.push(k); } ); expect(onlyThisKeys).toEqual([4]); expect(onlyOtherKeys).toEqual([3]); expect(differentKeys).toEqual([2]); }); const entriesGroup: [number, number][][] = [[], [[1, 1], [2, 2], [3, 3], [4, 4], [5, 5]]]; entriesGroup.forEach(entries => { test(`Diff of the same tree ${entries.length > 0 ? '(non-empty)' : '(empty)'}`, () => { const tree = new BTreeEx(entries, compare, maxNodeSize); expectDiffCorrect(tree, tree); expect(onlyOther.size).toEqual(0); expect(onlyThis.size).toEqual(0); expect(different.size).toEqual(0); }); }); test('Diff of identical trees', () => { const treeA = new BTreeEx(entriesGroup[1], compare, maxNodeSize); const treeB = new BTreeEx(entriesGroup[1], compare, maxNodeSize); expectDiffCorrect(treeA, treeB); }); [entriesGroup, [...entriesGroup].reverse()].forEach(doubleEntries => { test(`Diff of an ${doubleEntries[0].length === 0 ? 'empty' : 'non-empty'} tree and a ${doubleEntries[1].length === 0 ? 'empty' : 'non-empty'} one`, () => { const treeA = new BTreeEx(doubleEntries[0], compare, maxNodeSize); const treeB = new BTreeEx(doubleEntries[1], compare, maxNodeSize); expectDiffCorrect(treeA, treeB); }); }); test('Diff of different trees', () => { const treeA = new BTreeEx(entriesGroup[1], compare, maxNodeSize); const treeB = new BTreeEx(entriesGroup[1], compare, maxNodeSize); treeB.set(-1, -1); treeB.delete(2); treeB.set(3, 4); treeB.set(10, 10); expectDiffCorrect(treeA, treeB); }); test('Diff of odds and evens', () => { const treeA = new BTreeEx([[1, 1], [3, 3], [5, 5], [7, 7]], compare, maxNodeSize); const treeB = new BTreeEx([[2, 2], [4, 4], [6, 6], [8, 8]], compare, maxNodeSize); expectDiffCorrect(treeA, treeB); expectDiffCorrect(treeB, treeA); }); function applyChanges(treeA: BTreeEx, duplicate: (tree: BTreeEx) => BTreeEx): void { const treeB = duplicate(treeA); const maxKey: number = treeA.maxKey()!; const onlyInA = -10; treeA.set(onlyInA, onlyInA); const onlyInBSmall = -1; treeB.set(onlyInBSmall, onlyInBSmall); const onlyInBLarge = maxKey + 1; treeB.set(onlyInBLarge, onlyInBLarge); const onlyInAFromDelete = 10; treeB.delete(onlyInAFromDelete); const differingValue = -100; const modifiedInB1 = 3; const modifiedInB2 = maxKey - 2; treeB.set(modifiedInB1, differingValue); treeB.set(modifiedInB2, differingValue); treeA.diffAgainst(treeB, OnlyThis, OnlyOther, Different); expectDiffCorrect(treeA, treeB); } function makeLargeTree(size?: number): BTreeEx { size = size ?? Math.pow(maxNodeSize, 3); const tree = new BTreeEx([], compare, maxNodeSize); for (let i = 0; i < size; i++) { tree.set(i, i); } return tree; } test('Diff of large trees', () => { const tree = makeLargeTree(); applyChanges(tree, tree => tree.greedyClone()); }); test('Diff of cloned trees', () => { const tree = makeLargeTree(); applyChanges(tree, tree => tree.clone()); }); test('Diff can early exit', () => { const tree = makeLargeTree(100); const tree2 = tree.clone(); tree2.set(-1, -1); tree2.delete(10); tree2.set(20, -1); tree2.set(110, -1); const ReturnKey = (key: number) => { return { break: key }; }; let val = tree.diffAgainst(tree2, OnlyThis, OnlyOther, ReturnKey); expect(onlyOther.size).toEqual(1); expect(onlyThis.size).toEqual(0); expect(val).toEqual(20); reset(); val = tree.diffAgainst(tree2, OnlyThis, ReturnKey, Different); expect(different.size).toEqual(0); expect(onlyThis.size).toEqual(0); expect(val).toEqual(110); reset(); val = tree.diffAgainst(tree2, ReturnKey, OnlyOther, Different); expect(different.size).toEqual(1); expect(onlyOther.size).toEqual(1); expect(val).toEqual(10); reset(); expectDiffCorrect(tree, tree2); }); }); } ================================================ FILE: test/intersect.test.ts ================================================ import BTreeEx from '../extended'; import intersect from '../extended/intersect'; import { comparatorErrorMsg } from '../extended/shared'; import MersenneTwister from 'mersenne-twister'; import { expectTreeMatchesEntries, forEachFuzzCase, populateFuzzTrees, SetOperationFuzzSettings, compareNumbers } from './shared'; type SharedCall = { key: number, leftValue: number, rightValue: number }; // Calls `assertion` on the results of both `forEachKeyInBoth` and `intersect`. // Also ensures that `intersect()` behaves self-consistently. const runForEachKeyInBothAndIntersect = ( left: BTreeEx, right: BTreeEx, assertion: (calls: SharedCall[]) => void ) => { const forEachCalls: SharedCall[] = []; left.forEachKeyInBoth(right, (key, leftValue, rightValue) => { forEachCalls.push({ key, leftValue, rightValue }); }); assertion(forEachCalls); const intersectionCalls: SharedCall[] = []; const resultTree = intersect, number, number>(left, right, (key, leftValue, rightValue) => { intersectionCalls.push({ key, leftValue, rightValue }); return leftValue; }); // Verify that intersect() produces a valid tree that matches its own calls to `combineFn` resultTree.checkValid(); const expectedEntries = intersectionCalls.map(({ key, leftValue }) => [key, leftValue] as [number, number]); expect(resultTree.toArray()).toEqual(expectedEntries); assertion(intersectionCalls); }; const expectForEachKeyInBothAndIntersectCalls = ( left: BTreeEx, right: BTreeEx, expected: Array<[number, number, number]> ) => { const expectedRecords = tuplesToRecords(expected); runForEachKeyInBothAndIntersect(left, right, (calls) => { expect(calls).toEqual(expectedRecords); }); }; const tuplesToRecords = (entries: Array<[number, number, number]>): SharedCall[] => entries.map(([key, leftValue, rightValue]) => ({ key, leftValue, rightValue })); const tuples = (...pairs: Array<[number, number]>) => pairs; const triples = (...triplets: Array<[number, number, number]>) => triplets; const buildTree = (entries: Array<[number, number]>, maxNodeSize: number) => new BTreeEx(entries, compareNumbers, maxNodeSize); describe.each([32, 10, 4])('BTree forEachKeyInBoth/intersect tests with fanout %i', (maxNodeSize) => { const buildTreeForFanout = (entries: Array<[number, number]>) => buildTree(entries, maxNodeSize); const BASIC_CASES: Array<{ name: string; left: Array<[number, number]>; right: Array<[number, number]>; expected: Array<[number, number, number]>; alsoCheckSwap?: boolean; }> = [ { name: 'forEachKeyInBoth/intersect two empty trees', left: tuples(), right: tuples(), expected: triples(), }, { name: 'forEachKeyInBoth/intersect empty tree with non-empty tree', left: tuples(), right: tuples([1, 10], [2, 20], [3, 30]), expected: triples(), alsoCheckSwap: true, }, { name: 'forEachKeyInBoth/intersect with no overlapping keys', left: tuples([1, 10], [3, 30], [5, 50]), right: tuples([2, 20], [4, 40], [6, 60]), expected: triples(), }, { name: 'forEachKeyInBoth/intersect with single overlapping key', left: tuples([1, 10], [2, 20], [3, 30]), right: tuples([0, 100], [2, 200], [4, 400]), expected: triples([2, 20, 200]), }, ]; BASIC_CASES.forEach(({ name, left, right, expected, alsoCheckSwap }) => { it(name, () => { const leftTree = buildTreeForFanout(left); const rightTree = buildTreeForFanout(right); expectForEachKeyInBothAndIntersectCalls(leftTree, rightTree, expected); if (alsoCheckSwap) { expectForEachKeyInBothAndIntersectCalls(rightTree, leftTree, expected); } }); }); it('forEachKeyInBoth/intersect with multiple overlapping keys maintains tree contents', () => { const leftEntries: Array<[number, number]> = [[1, 10], [2, 20], [3, 30], [4, 40], [5, 50]]; const rightEntries: Array<[number, number]> = [[0, 100], [2, 200], [4, 400], [6, 600]]; const tree1 = buildTreeForFanout(leftEntries); const tree2 = buildTreeForFanout(rightEntries); const leftBefore = tree1.toArray(); const rightBefore = tree2.toArray(); expectForEachKeyInBothAndIntersectCalls(tree1, tree2, triples([2, 20, 200], [4, 40, 400])); expect(tree1.toArray()).toEqual(leftBefore); expect(tree2.toArray()).toEqual(rightBefore); tree1.checkValid(); tree2.checkValid(); }); it('forEachKeyInBoth/intersect with contiguous overlap yields sorted keys', () => { const tree1 = buildTreeForFanout(tuples([1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6])); const tree2 = buildTreeForFanout(tuples([3, 30], [4, 40], [5, 50], [6, 60], [7, 70])); runForEachKeyInBothAndIntersect(tree1, tree2, (calls) => { expect(calls.map(c => c.key)).toEqual([3, 4, 5, 6]); expect(calls.map(c => c.leftValue)).toEqual([3, 4, 5, 6]); expect(calls.map(c => c.rightValue)).toEqual([30, 40, 50, 60]); }); }); it('forEachKeyInBoth/intersect large overlapping range counts each shared key once', () => { const size = 1000; const overlapStart = 500; const leftEntries = Array.from({ length: size }, (_, i) => [i, i * 3] as [number, number]); const rightEntries = Array.from({ length: size }, (_, i) => { const key = i + overlapStart; return [key, key * 7] as [number, number]; }); const tree1 = buildTreeForFanout(leftEntries); const tree2 = buildTreeForFanout(rightEntries); runForEachKeyInBothAndIntersect(tree1, tree2, (calls) => { expect(calls.length).toBe(size - overlapStart); expect(calls[0]).toEqual({ key: overlapStart, leftValue: overlapStart * 3, rightValue: overlapStart * 7 }); const lastCall = calls[calls.length - 1]; expect(lastCall.key).toBe(size - 1); expect(lastCall.leftValue).toBe((size - 1) * 3); expect(lastCall.rightValue).toBe((size - 1) * 7); }); }); it('forEachKeyInBoth/intersect tree with itself visits each key once', () => { const entries = Array.from({ length: 20 }, (_, i) => [i, i * 2] as [number, number]); const tree = buildTreeForFanout(entries); runForEachKeyInBothAndIntersect(tree, tree, (calls) => { expect(calls.length).toBe(entries.length); for (let i = 0; i < entries.length; i++) { const [key, value] = entries[i]; expect(calls[i]).toEqual({ key, leftValue: value, rightValue: value }); } }); }); it('forEachKeyInBoth/intersect arguments determine left/right values', () => { const tree1 = buildTreeForFanout(tuples([1, 100], [2, 200], [4, 400])); const tree2 = buildTreeForFanout(tuples([2, 20], [3, 30], [4, 40])); expectForEachKeyInBothAndIntersectCalls(tree1, tree2, triples([2, 200, 20], [4, 400, 40])); expectForEachKeyInBothAndIntersectCalls(tree2, tree1, triples([2, 20, 200], [4, 40, 400])); }); }); describe('BTree forEachKeyInBoth early exiting', () => { const buildTreeForEarlyExit = (entries: Array<[number, number]>) => buildTree(entries, 4); it('forEachKeyInBoth returns undefined when callback returns void', () => { const tree1 = buildTreeForEarlyExit(tuples([1, 10], [2, 20], [3, 30])); const tree2 = buildTreeForEarlyExit(tuples([0, 100], [2, 200], [3, 300], [4, 400])); const visited: number[] = []; const result = tree1.forEachKeyInBoth(tree2, key => { visited.push(key); }); expect(result).toBeUndefined(); expect(visited).toEqual([2, 3]); }); it('forEachKeyInBoth ignores undefined break values and completes traversal', () => { const tree1 = buildTreeForEarlyExit(tuples([1, 10], [2, 20], [3, 30])); const tree2 = buildTreeForEarlyExit(tuples([2, 200], [3, 300], [5, 500])); const visited: number[] = []; const result = tree1.forEachKeyInBoth(tree2, key => { visited.push(key); return { break: undefined }; }); expect(result).toBeUndefined(); expect(visited).toEqual([2, 3]); }); it('forEachKeyInBoth breaks early when callback returns a value', () => { const tree1 = buildTreeForEarlyExit(tuples([1, 10], [2, 20], [3, 30], [4, 40])); const tree2 = buildTreeForEarlyExit(tuples([2, 200], [3, 300], [4, 400], [5, 500])); const visited: number[] = []; const breakResult = tree1.forEachKeyInBoth(tree2, (key, leftValue, rightValue) => { visited.push(key); if (key === 3) { return { break: { key, sum: leftValue + rightValue } }; } }); expect(breakResult).toEqual({ key: 3, sum: 330 }); expect(visited).toEqual([2, 3]); }); }); describe('BTree forEachKeyInBoth and intersect input/output validation', () => { it('forEachKeyInBoth throws error when comparators differ', () => { const tree1 = new BTreeEx([[1, 10]], (a, b) => b + a); const tree2 = new BTreeEx([[2, 20]], (a, b) => b - a); expect(() => tree1.forEachKeyInBoth(tree2, () => { })).toThrow(comparatorErrorMsg); expect(() => intersect, number, number>(tree1, tree2, () => 0)).toThrow(comparatorErrorMsg); }); }); describe('BTree forEachKeyInBoth/intersect fuzz tests', () => { const FUZZ_SETTINGS: SetOperationFuzzSettings = { branchingFactors: [4, 5, 32], ooms: [2, 3], fractionsPerOOM: [0.1, 0.25, 0.5], removalChances: [0, 0.01, 0.1] }; const FUZZ_TIMEOUT_MS = 30_000; jest.setTimeout(FUZZ_TIMEOUT_MS); const rng = new MersenneTwister(0xC0FFEE); forEachFuzzCase(FUZZ_SETTINGS, ({ maxNodeSize, size, fractionA, fractionB, removalChance, removalLabel }) => { it(`branch ${maxNodeSize}, size ${size}, fractionA ${fractionA.toFixed(2)}, fractionB ${fractionB.toFixed(2)}, removal ${removalLabel}`, () => { const treeA = new BTreeEx([], compareNumbers, maxNodeSize); const treeB = new BTreeEx([], compareNumbers, maxNodeSize); const [treeAEntries, treeBEntries] = populateFuzzTrees( [ { tree: treeA, fraction: fractionA, removalChance }, { tree: treeB, fraction: fractionB, removalChance } ], { rng, size, compare: compareNumbers, maxNodeSize, minAssignmentsPerKey: 1 } ); const bMap = new Map(treeBEntries); const expectedTuples: Array<[number, number, number]> = []; for (const [key, leftValue] of treeAEntries) { const rightValue = bMap.get(key); if (rightValue !== undefined) expectedTuples.push([key, leftValue, rightValue]); } expectForEachKeyInBothAndIntersectCalls(treeA, treeB, expectedTuples); const swappedExpected = expectedTuples.map(([key, leftValue, rightValue]) => [key, rightValue, leftValue] as [number, number, number]); expectForEachKeyInBothAndIntersectCalls(treeB, treeA, swappedExpected); expectTreeMatchesEntries(treeA, treeAEntries); expectTreeMatchesEntries(treeB, treeBEntries); treeA.checkValid(true); treeB.checkValid(true); }); }); }); ================================================ FILE: test/setOperationFuzz.test.ts ================================================ import BTreeEx from '../extended'; import MersenneTwister from 'mersenne-twister'; import { expectTreeMatchesEntries, forEachFuzzCase, populateFuzzTrees, SetOperationFuzzSettings } from './shared'; const compare = (a: number, b: number) => a - b; describe('Set operation fuzz tests', () => { const FUZZ_SETTINGS: SetOperationFuzzSettings = { branchingFactors: [4, 5, 32], ooms: [2, 3], fractionsPerOOM: [0.1, 0.25, 0.5], removalChances: [0.01, 0.1] }; const FUZZ_TIMEOUT_MS = 30_000; jest.setTimeout(FUZZ_TIMEOUT_MS); const rng = new MersenneTwister(0xC0FFEE); forEachFuzzCase(FUZZ_SETTINGS, ({ maxNodeSize, size, fractionA, fractionB, removalChance, removalLabel }) => { it(`branch ${maxNodeSize}, size ${size}, fractionA ${fractionA.toFixed(2)}, fractionB ${fractionB.toFixed(2)}, removal ${removalLabel}`, () => { const treeA = new BTreeEx([], compare, maxNodeSize); const treeB = new BTreeEx([], compare, maxNodeSize); const treeC = new BTreeEx([], compare, maxNodeSize); const [treeAEntries, treeBEntries, treeCEntries] = populateFuzzTrees( [ { tree: treeA, fraction: fractionA, removalChance }, { tree: treeB, fraction: fractionB, removalChance }, { tree: treeC, fraction: 0.5 } ], { rng, size, compare, maxNodeSize } ); const keepEither = (_k: number, left: number, _right: number) => left; const dropValue = () => undefined; const combineSum = (_k: number, left: number, right: number) => left + right; const unionDrop = treeA.union(treeB, dropValue); const unionKeep = treeA.union(treeB, keepEither); const intersection = treeA.intersect(treeB, keepEither); const diffAB = treeA.subtract(treeB); const diffBA = treeB.subtract(treeA); // 1. Partition of A: A = (A\B) ∪ (A∩B) and parts are disjoint. const partition = diffAB.union(intersection, keepEither); expect(partition.toArray()).toEqual(treeAEntries); expect(diffAB.intersect(intersection, keepEither).size).toBe(0); // 2. Recover B from union and A\B: (A∪B)\(A\B) = B. expect(unionKeep.subtract(diffAB).toArray()).toEqual(treeBEntries); // 3. Symmetric difference two ways. const symFromDiffs = diffAB.union(diffBA, keepEither); const symFromUnion = unionKeep.subtract(intersection); expect(symFromDiffs.toArray()).toEqual(symFromUnion.toArray()); // 4. Intersection via difference: A∩B = A \ (A\B). expect(intersection.toArray()).toEqual(treeA.subtract(diffAB).toArray()); // 5. Difference via intersection: A\B = A \ (A∩B). expect(diffAB.toArray()).toEqual(treeA.subtract(intersection).toArray()); // 6. Idempotence. expect(treeA.union(treeA, keepEither).toArray()).toEqual(treeAEntries); expect(treeA.intersect(treeA, keepEither).toArray()).toEqual(treeAEntries); expect(diffAB.subtract(treeB).toArray()).toEqual(diffAB.toArray()); // 7. Commutativity. expect(intersection.toArray()).toEqual(treeB.intersect(treeA, keepEither).toArray()); const commUT = treeA.union(treeB, combineSum); const commTU = treeB.union(treeA, combineSum); expect(commUT.toArray()).toEqual(commTU.toArray()); // 8. Associativity. const assocLeft = treeA.intersect(treeB, keepEither).intersect(treeC, keepEither); const assocRight = treeA.intersect(treeB.intersect(treeC, keepEither), keepEither); expect(assocLeft.toArray()).toEqual(assocRight.toArray()); const assocSumLeft = treeA.union(treeB, combineSum).union(treeC, combineSum); const assocSumRight = treeA.union(treeB.union(treeC, combineSum), combineSum); expect(assocSumLeft.toArray()).toEqual(assocSumRight.toArray()); // 9. Absorption. expect(treeA.intersect(treeA.union(treeB, keepEither), keepEither).toArray()).toEqual(treeAEntries); expect(treeA.union(treeA.intersect(treeB, keepEither), keepEither).toArray()).toEqual(treeAEntries); // 10. Distributivity. const distIntersect = treeA.intersect(treeB.union(treeC, keepEither), keepEither); const distRight = treeA.intersect(treeB, keepEither).union(treeA.intersect(treeC, keepEither), keepEither); expect(distIntersect.toArray()).toEqual(distRight.toArray()); const distSubtract = treeA.subtract(treeB.union(treeC, keepEither)); const distSubtractRight = treeA.subtract(treeB).subtract(treeC); expect(distSubtract.toArray()).toEqual(distSubtractRight.toArray()); const distIntersectDiff = treeA.intersect(treeB, keepEither).subtract(treeC); const distDiffIntersect = treeA.subtract(treeC).intersect(treeB, keepEither); expect(distIntersectDiff.toArray()).toEqual(distDiffIntersect.toArray()); // 11. Superset sanity. expect(treeA.subtract(treeA.union(treeB, keepEither)).size).toBe(0); expect(diffAB.intersect(treeB, keepEither).size).toBe(0); // 12. Cardinality relations. expect(unionKeep.size).toBe(treeA.size + treeB.size - intersection.size); expect(diffAB.size).toBe(treeA.size - intersection.size); expect(treeA.size).toBe(diffAB.size + intersection.size); partition.checkValid(true); unionDrop.checkValid(true); unionKeep.checkValid(true); intersection.checkValid(true); diffAB.checkValid(true); diffBA.checkValid(true); treeA.checkValid(true); treeB.checkValid(true); treeC.checkValid(true); expectTreeMatchesEntries(treeA, treeAEntries); expectTreeMatchesEntries(treeB, treeBEntries); expectTreeMatchesEntries(treeC, treeCEntries); }); }); }); ================================================ FILE: test/shared.d.ts ================================================ import BTree, { IMap } from '../b+tree'; import SortedArray from '../sorted-array'; import MersenneTwister from 'mersenne-twister'; import BTreeEx from '../extended'; export declare const compareNumbers: (a: number, b: number) => number; export declare type TreeNodeStats = { total: number; shared: number; newUnderfilled: number; averageLoadFactor: number; }; export declare type TreeEntries = Array<[number, number]>; export declare type SetOperationFuzzSettings = { branchingFactors: number[]; ooms: number[]; fractionsPerOOM: number[]; removalChances: number[]; }; export declare type FuzzCase = { maxNodeSize: number; oom: number; size: number; fractionA: number; fractionB: number; removalChance: number; removalLabel: string; }; export declare function countTreeNodeStats(tree: BTree): TreeNodeStats; export declare function logTreeNodeStats(prefix: string, stats: BTreeEx | TreeNodeStats): void; export declare function randInt(max: number): number; export declare function expectTreeEqualTo(tree: BTree, list: SortedArray): void; export declare function addToBoth(a: IMap, b: IMap, k: K, v: V): void; export declare function makeArray(size: number, randomOrder: boolean, spacing?: number, rng?: MersenneTwister): number[]; export declare const randomInt: (rng: MersenneTwister, maxExclusive: number) => number; export declare function buildEntriesFromMap(entriesMap: Map, compareFn?: (a: number, b: number) => number): TreeEntries; export declare type FuzzTreeSpec = { tree: BTree; fraction: number; removalChance?: number; }; export declare type PopulateFuzzTreesOptions = { size: number; rng: MersenneTwister; compare: (a: number, b: number) => number; maxNodeSize: number; minAssignmentsPerKey?: number; }; export declare function populateFuzzTrees(specs: FuzzTreeSpec[], { size, rng, compare, maxNodeSize, minAssignmentsPerKey }: PopulateFuzzTreesOptions): TreeEntries[]; export declare function applyRemovalRunsToTree(tree: BTree, entries: TreeEntries, removalChance: number, branchingFactor: number, rng: MersenneTwister): TreeEntries; export declare function expectTreeMatchesEntries(tree: BTree, entries: TreeEntries): void; export declare function forEachFuzzCase(settings: SetOperationFuzzSettings, callback: (testCase: FuzzCase) => void): void; ================================================ FILE: test/shared.js ================================================ "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.forEachFuzzCase = exports.expectTreeMatchesEntries = exports.applyRemovalRunsToTree = exports.populateFuzzTrees = exports.buildEntriesFromMap = exports.randomInt = exports.makeArray = exports.addToBoth = exports.expectTreeEqualTo = exports.randInt = exports.logTreeNodeStats = exports.countTreeNodeStats = exports.compareNumbers = void 0; var b_tree_1 = __importDefault(require("../b+tree")); var mersenne_twister_1 = __importDefault(require("mersenne-twister")); var rand = new mersenne_twister_1.default(1234); var compareNumbers = function (a, b) { return a - b; }; exports.compareNumbers = compareNumbers; function countTreeNodeStats(tree) { var root = tree._root; if (tree.size === 0 || !root) return { total: 0, shared: 0, newUnderfilled: 0, averageLoadFactor: 0 }; var maxNodeSize = tree.maxNodeSize; var minNodeSize = Math.floor(maxNodeSize / 2); var visit = function (node, ancestorShared, isRoot) { if (!node) return { total: 0, shared: 0, newUnderfilled: 0, loadFactorSum: 0 }; var selfShared = node.isShared === true || ancestorShared; var children = node.children; var occupancy = children ? children.length : node.keys.length; var isUnderfilled = !isRoot && occupancy < minNodeSize; var loadFactor = occupancy / maxNodeSize; var shared = selfShared ? 1 : 0; var total = 1; var newUnderfilled = !selfShared && isUnderfilled ? 1 : 0; var loadFactorSum = loadFactor; if (children) { for (var _i = 0, children_1 = children; _i < children_1.length; _i++) { var child = children_1[_i]; var stats = visit(child, selfShared, false); total += stats.total; shared += stats.shared; newUnderfilled += stats.newUnderfilled; loadFactorSum += stats.loadFactorSum; } } return { total: total, shared: shared, newUnderfilled: newUnderfilled, loadFactorSum: loadFactorSum }; }; var result = visit(root, false, true); var averageLoadFactor = result.total === 0 ? 0 : result.loadFactorSum / result.total; return { total: result.total, shared: result.shared, newUnderfilled: result.newUnderfilled, averageLoadFactor: averageLoadFactor }; } exports.countTreeNodeStats = countTreeNodeStats; function logTreeNodeStats(prefix, stats) { if (stats instanceof b_tree_1.default) stats = countTreeNodeStats(stats); var percent = (stats.averageLoadFactor * 100).toFixed(2); console.log("\t".concat(prefix, " ").concat(stats.shared, "/").concat(stats.total, " shared nodes, ") + "".concat(stats.newUnderfilled, "/").concat(stats.total, " underfilled nodes, ").concat(percent, "% average load factor")); } exports.logTreeNodeStats = logTreeNodeStats; function randInt(max) { return rand.random_int() % max; } exports.randInt = randInt; function expectTreeEqualTo(tree, list) { tree.checkValid(); expect(tree.toArray()).toEqual(list.getArray()); } exports.expectTreeEqualTo = expectTreeEqualTo; function addToBoth(a, b, k, v) { expect(a.set(k, v)).toEqual(b.set(k, v)); } exports.addToBoth = addToBoth; function makeArray(size, randomOrder, spacing, rng) { if (spacing === void 0) { spacing = 10; } var randomizer = rng !== null && rng !== void 0 ? rng : rand; var useGlobalRand = rng === undefined; var randomFloat = function () { if (typeof randomizer.random === 'function') return randomizer.random(); return Math.random(); }; var randomIntWithMax = function (max) { if (max <= 0) return 0; if (useGlobalRand) return randInt(max); return Math.floor(randomFloat() * max); }; var keys = []; var current = 0; for (var i = 0; i < size; i++) { current += 1 + randomIntWithMax(spacing); keys[i] = current; } if (randomOrder) { for (var i = 0; i < size; i++) swap(keys, i, randomIntWithMax(size)); } return keys; } exports.makeArray = makeArray; var randomInt = function (rng, maxExclusive) { return Math.floor(rng.random() * maxExclusive); }; exports.randomInt = randomInt; function swap(keys, i, j) { var tmp = keys[i]; keys[i] = keys[j]; keys[j] = tmp; } function buildEntriesFromMap(entriesMap, compareFn) { if (compareFn === void 0) { compareFn = function (a, b) { return a - b; }; } var entries = Array.from(entriesMap.entries()); entries.sort(function (a, b) { return compareFn(a[0], b[0]); }); return entries; } exports.buildEntriesFromMap = buildEntriesFromMap; function populateFuzzTrees(specs, _a) { var size = _a.size, rng = _a.rng, compare = _a.compare, maxNodeSize = _a.maxNodeSize, _b = _a.minAssignmentsPerKey, minAssignmentsPerKey = _b === void 0 ? 0 : _b; if (specs.length === 0) return []; var keys = makeArray(size, true, 1, rng); var entriesMaps = specs.map(function () { return new Map(); }); var assignments = new Array(specs.length); var requiredAssignments = Math.min(minAssignmentsPerKey, specs.length); for (var _i = 0, keys_1 = keys; _i < keys_1.length; _i++) { var value = keys_1[_i]; var assignedCount = 0; for (var i = 0; i < specs.length; i++) { assignments[i] = rng.random() < specs[i].fraction; if (assignments[i]) assignedCount++; } while (assignedCount < requiredAssignments && specs.length > 0) { var index = (0, exports.randomInt)(rng, specs.length); if (!assignments[index]) { assignments[index] = true; assignedCount++; } } for (var i = 0; i < specs.length; i++) { if (assignments[i]) { specs[i].tree.set(value, value); entriesMaps[i].set(value, value); } } } return specs.map(function (spec, index) { var _a; var entries = buildEntriesFromMap(entriesMaps[index], compare); var removalChance = (_a = spec.removalChance) !== null && _a !== void 0 ? _a : 0; if (removalChance > 0) entries = applyRemovalRunsToTree(spec.tree, entries, removalChance, maxNodeSize, rng); return entries; }); } exports.populateFuzzTrees = populateFuzzTrees; function applyRemovalRunsToTree(tree, entries, removalChance, branchingFactor, rng) { if (removalChance <= 0 || entries.length === 0) return entries; var remaining = []; var index = 0; while (index < entries.length) { var _a = entries[index], key = _a[0], value = _a[1]; if (rng.random() < removalChance) { tree.delete(key); index++; while (index < entries.length) { var candidateKey = entries[index][0]; if (rng.random() < (1 / branchingFactor)) break; tree.delete(candidateKey); index++; } } else { remaining.push([key, value]); index++; } } return remaining; } exports.applyRemovalRunsToTree = applyRemovalRunsToTree; function expectTreeMatchesEntries(tree, entries) { var index = 0; tree.forEachPair(function (key, value) { var expected = entries[index++]; expect([key, value]).toEqual(expected); }); expect(index).toBe(entries.length); } exports.expectTreeMatchesEntries = expectTreeMatchesEntries; function validateFuzzSettings(settings) { settings.fractionsPerOOM.forEach(function (fraction) { if (fraction < 0 || fraction > 1) throw new Error('fractionsPerOOM values must be between 0 and 1'); }); settings.removalChances.forEach(function (chance) { if (chance < 0 || chance > 1) throw new Error('removalChances values must be between 0 and 1'); }); } function forEachFuzzCase(settings, callback) { validateFuzzSettings(settings); for (var _i = 0, _a = settings.branchingFactors; _i < _a.length; _i++) { var maxNodeSize = _a[_i]; for (var _b = 0, _c = settings.removalChances; _b < _c.length; _b++) { var removalChance = _c[_b]; var removalLabel = removalChance.toFixed(3); for (var _d = 0, _e = settings.ooms; _d < _e.length; _d++) { var oom = _e[_d]; var size = 5 * Math.pow(10, oom); for (var _f = 0, _g = settings.fractionsPerOOM; _f < _g.length; _f++) { var fractionA = _g[_f]; var fractionB = 1 - fractionA; callback({ maxNodeSize: maxNodeSize, oom: oom, size: size, fractionA: fractionA, fractionB: fractionB, removalChance: removalChance, removalLabel: removalLabel }); } } } } } exports.forEachFuzzCase = forEachFuzzCase; ================================================ FILE: test/shared.ts ================================================ import BTree, { BNode, BNodeInternal, IMap } from '../b+tree'; import SortedArray from '../sorted-array'; import MersenneTwister from 'mersenne-twister'; import type { BTreeWithInternals } from '../extended/shared'; import BTreeEx from '../extended'; const rand = new MersenneTwister(1234); export const compareNumbers = (a: number, b: number) => a - b; export type TreeNodeStats = { total: number; shared: number; newUnderfilled: number; averageLoadFactor: number; }; export type TreeEntries = Array<[number, number]>; export type SetOperationFuzzSettings = { branchingFactors: number[]; ooms: number[]; fractionsPerOOM: number[]; removalChances: number[]; }; export type FuzzCase = { maxNodeSize: number; oom: number; size: number; fractionA: number; fractionB: number; removalChance: number; removalLabel: string; }; export function countTreeNodeStats(tree: BTree): TreeNodeStats { const root = (tree as unknown as BTreeWithInternals)._root; if (tree.size === 0 || !root) return { total: 0, shared: 0, newUnderfilled: 0, averageLoadFactor: 0 }; const maxNodeSize = tree.maxNodeSize; const minNodeSize = Math.floor(maxNodeSize / 2); type StatsAccumulator = { total: number; shared: number; newUnderfilled: number; loadFactorSum: number; }; const visit = (node: BNode, ancestorShared: boolean, isRoot: boolean): StatsAccumulator => { if (!node) return { total: 0, shared: 0, newUnderfilled: 0, loadFactorSum: 0 }; const selfShared = node.isShared === true || ancestorShared; const children: BNode[] | undefined = (node as BNodeInternal).children; const occupancy = children ? children.length : node.keys.length; const isUnderfilled = !isRoot && occupancy < minNodeSize; const loadFactor = occupancy / maxNodeSize; let shared = selfShared ? 1 : 0; let total = 1; let newUnderfilled = !selfShared && isUnderfilled ? 1 : 0; let loadFactorSum = loadFactor; if (children) { for (const child of children) { const stats = visit(child, selfShared, false); total += stats.total; shared += stats.shared; newUnderfilled += stats.newUnderfilled; loadFactorSum += stats.loadFactorSum; } } return { total, shared, newUnderfilled, loadFactorSum }; }; const result = visit(root, false, true); const averageLoadFactor = result.total === 0 ? 0 : result.loadFactorSum / result.total; return { total: result.total, shared: result.shared, newUnderfilled: result.newUnderfilled, averageLoadFactor }; } export function logTreeNodeStats(prefix: string, stats: BTreeEx|TreeNodeStats): void { if (stats instanceof BTree) stats = countTreeNodeStats(stats); const percent = (stats.averageLoadFactor * 100).toFixed(2); console.log(`\t${prefix} ${stats.shared}/${stats.total} shared nodes, ` + `${stats.newUnderfilled}/${stats.total} underfilled nodes, ${percent}% average load factor`); } export function randInt(max: number): number { return rand.random_int() % max; } export function expectTreeEqualTo(tree: BTree, list: SortedArray): void { tree.checkValid(); expect(tree.toArray()).toEqual(list.getArray()); } export function addToBoth(a: IMap, b: IMap, k: K, v: V): void { expect(a.set(k, v)).toEqual(b.set(k, v)); } export function makeArray( size: number, randomOrder: boolean, spacing = 10, rng?: MersenneTwister ): number[] { const randomizer = rng ?? rand; const useGlobalRand = rng === undefined; const randomFloat = () => { if (typeof randomizer.random === 'function') return randomizer.random(); return Math.random(); }; const randomIntWithMax = (max: number) => { if (max <= 0) return 0; if (useGlobalRand) return randInt(max); return Math.floor(randomFloat() * max); }; const keys: number[] = []; let current = 0; for (let i = 0; i < size; i++) { current += 1 + randomIntWithMax(spacing); keys[i] = current; } if (randomOrder) { for (let i = 0; i < size; i++) swap(keys, i, randomIntWithMax(size)); } return keys; } export const randomInt = (rng: MersenneTwister, maxExclusive: number) => Math.floor(rng.random() * maxExclusive); function swap(keys: any[], i: number, j: number) { const tmp = keys[i]; keys[i] = keys[j]; keys[j] = tmp; } export function buildEntriesFromMap( entriesMap: Map, compareFn: (a: number, b: number) => number = (a, b) => a - b ): TreeEntries { const entries = Array.from(entriesMap.entries()) as TreeEntries; entries.sort((a, b) => compareFn(a[0], b[0])); return entries; } export type FuzzTreeSpec = { tree: BTree; fraction: number; removalChance?: number; }; export type PopulateFuzzTreesOptions = { size: number; rng: MersenneTwister; compare: (a: number, b: number) => number; maxNodeSize: number; minAssignmentsPerKey?: number; }; export function populateFuzzTrees( specs: FuzzTreeSpec[], { size, rng, compare, maxNodeSize, minAssignmentsPerKey = 0 }: PopulateFuzzTreesOptions ): TreeEntries[] { if (specs.length === 0) return []; const keys = makeArray(size, true, 1, rng); const entriesMaps = specs.map(() => new Map()); const assignments = new Array(specs.length); const requiredAssignments = Math.min(minAssignmentsPerKey, specs.length); for (const value of keys) { let assignedCount = 0; for (let i = 0; i < specs.length; i++) { assignments[i] = rng.random() < specs[i].fraction; if (assignments[i]) assignedCount++; } while (assignedCount < requiredAssignments && specs.length > 0) { const index = randomInt(rng, specs.length); if (!assignments[index]) { assignments[index] = true; assignedCount++; } } for (let i = 0; i < specs.length; i++) { if (assignments[i]) { specs[i].tree.set(value, value); entriesMaps[i].set(value, value); } } } return specs.map((spec, index) => { let entries = buildEntriesFromMap(entriesMaps[index], compare); const removalChance = spec.removalChance ?? 0; if (removalChance > 0) entries = applyRemovalRunsToTree(spec.tree, entries, removalChance, maxNodeSize, rng); return entries; }); } export function applyRemovalRunsToTree( tree: BTree, entries: TreeEntries, removalChance: number, branchingFactor: number, rng: MersenneTwister ): TreeEntries { if (removalChance <= 0 || entries.length === 0) return entries; const remaining: TreeEntries = []; let index = 0; while (index < entries.length) { const [key, value] = entries[index]; if (rng.random() < removalChance) { tree.delete(key); index++; while (index < entries.length) { const [candidateKey] = entries[index]; if (rng.random() < (1 / branchingFactor)) break; tree.delete(candidateKey); index++; } } else { remaining.push([key, value]); index++; } } return remaining; } export function expectTreeMatchesEntries(tree: BTree, entries: TreeEntries): void { let index = 0; tree.forEachPair((key, value) => { const expected = entries[index++]!; expect([key, value]).toEqual(expected); }); expect(index).toBe(entries.length); } function validateFuzzSettings(settings: SetOperationFuzzSettings): void { settings.fractionsPerOOM.forEach(fraction => { if (fraction < 0 || fraction > 1) throw new Error('fractionsPerOOM values must be between 0 and 1'); }); settings.removalChances.forEach(chance => { if (chance < 0 || chance > 1) throw new Error('removalChances values must be between 0 and 1'); }); } export function forEachFuzzCase( settings: SetOperationFuzzSettings, callback: (testCase: FuzzCase) => void ): void { validateFuzzSettings(settings); for (const maxNodeSize of settings.branchingFactors) { for (const removalChance of settings.removalChances) { const removalLabel = removalChance.toFixed(3); for (const oom of settings.ooms) { const size = 5 * Math.pow(10, oom); for (const fractionA of settings.fractionsPerOOM) { const fractionB = 1 - fractionA; callback({ maxNodeSize, oom, size, fractionA, fractionB, removalChance, removalLabel }); } } } } } ================================================ FILE: test/subtract.test.ts ================================================ import BTreeEx from '../extended'; import forEachKeyNotIn from '../extended/forEachKeyNotIn'; import subtract from '../extended/subtract'; import { comparatorErrorMsg, branchingFactorErrorMsg } from '../extended/shared'; import MersenneTwister from 'mersenne-twister'; import { expectTreeMatchesEntries, forEachFuzzCase, populateFuzzTrees, SetOperationFuzzSettings, compareNumbers } from './shared'; type NotInCall = { key: number, value: number }; const runForEachKeyNotInAndSubtract = ( include: BTreeEx, exclude: BTreeEx, assertion: (calls: NotInCall[]) => void ) => { const forEachCalls: NotInCall[] = []; forEachKeyNotIn(include, exclude, (key, value) => { forEachCalls.push({ key, value }); }); assertion(forEachCalls); const resultTree = subtract, number, number>(include, exclude); const subtractCalls = resultTree.toArray().map(([key, value]) => ({ key, value })); expect(subtractCalls).toEqual(forEachCalls); resultTree.checkValid(true); assertion(subtractCalls); }; const expectForEachKeyNotInAndSubtractCalls = ( include: BTreeEx, exclude: BTreeEx, expected: Array<[number, number]> ) => { const expectedRecords = tuplesToRecords(expected); runForEachKeyNotInAndSubtract(include, exclude, (calls) => { expect(calls).toEqual(expectedRecords); }); }; const tuplesToRecords = (entries: Array<[number, number]>): NotInCall[] => entries.map(([key, value]) => ({ key, value })); const tuples = (...pairs: Array<[number, number]>) => pairs; const buildTree = (entries: Array<[number, number]>, maxNodeSize: number) => new BTreeEx(entries, compareNumbers, maxNodeSize); describe.each([32, 10, 4])('BTree forEachKeyNotIn/subtract tests with fanout %i', (maxNodeSize) => { const buildTreeForFanout = (entries: Array<[number, number]>) => buildTree(entries, maxNodeSize); const BASIC_CASES: Array<{ name: string; include: Array<[number, number]>; exclude: Array<[number, number]>; expected: Array<[number, number]>; }> = [ { name: 'forEachKeyNotIn/subtract two empty trees', include: tuples(), exclude: tuples(), expected: [], }, { name: 'forEachKeyNotIn/subtract include empty tree with non-empty tree', include: tuples(), exclude: tuples([1, 10], [2, 20], [3, 30]), expected: [], }, { name: 'forEachKeyNotIn/subtract exclude tree empty yields all include keys', include: tuples([1, 10], [3, 30], [5, 50]), exclude: tuples(), expected: tuples([1, 10], [3, 30], [5, 50]), }, { name: 'forEachKeyNotIn/subtract with no overlapping keys returns include tree contents', include: tuples([1, 10], [3, 30], [5, 50]), exclude: tuples([0, 100], [2, 200], [4, 400]), expected: tuples([1, 10], [3, 30], [5, 50]), }, { name: 'forEachKeyNotIn/subtract with overlapping keys excludes matches', include: tuples([1, 10], [2, 20], [3, 30], [4, 40], [5, 50]), exclude: tuples([0, 100], [2, 200], [4, 400], [6, 600]), expected: tuples([1, 10], [3, 30], [5, 50]), }, { name: 'forEachKeyNotIn/subtract excludes leading overlap then emits remaining keys', include: tuples([1, 10], [2, 20], [3, 30], [4, 40]), exclude: tuples([1, 100], [2, 200]), expected: tuples([3, 30], [4, 40]), }, { name: 'forEachKeyNotIn/subtract exclude superset yields empty result', include: tuples([2, 200], [3, 300]), exclude: tuples([1, 100], [2, 200], [3, 300], [4, 400]), expected: [], }, ]; BASIC_CASES.forEach(({ name, include, exclude, expected }) => { it(name, () => { const includeTree = buildTreeForFanout(include); const excludeTree = buildTreeForFanout(exclude); expectForEachKeyNotInAndSubtractCalls(includeTree, excludeTree, expected); }); }); it('forEachKeyNotIn/subtract maintains tree contents', () => { const includeEntries: Array<[number, number]> = [[1, 10], [2, 20], [3, 30], [4, 40], [5, 50]]; const excludeEntries: Array<[number, number]> = [[1, 100], [3, 300], [5, 500]]; const includeTree = buildTreeForFanout(includeEntries); const excludeTree = buildTreeForFanout(excludeEntries); const includeBefore = includeTree.toArray(); const excludeBefore = excludeTree.toArray(); expectForEachKeyNotInAndSubtractCalls(includeTree, excludeTree, tuples([2, 20], [4, 40])); expect(includeTree.toArray()).toEqual(includeBefore); expect(excludeTree.toArray()).toEqual(excludeBefore); includeTree.checkValid(); excludeTree.checkValid(); }); it('forEachKeyNotIn/subtract with contiguous overlap yields sorted survivors', () => { const includeTree = buildTreeForFanout(tuples([1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6])); const excludeTree = buildTreeForFanout(tuples([3, 30], [4, 40], [5, 50])); runForEachKeyNotInAndSubtract(includeTree, excludeTree, (calls) => { expect(calls.map(c => c.key)).toEqual([1, 2, 6]); expect(calls.map(c => c.value)).toEqual([1, 2, 6]); }); }); it('forEachKeyNotIn/subtract large subtraction leaves prefix and suffix ranges', () => { const size = 1000; const excludeStart = 200; const excludeSpan = 500; const includeEntries = Array.from({ length: size }, (_, i) => [i, i * 2] as [number, number]); const excludeEntries = Array.from({ length: excludeSpan }, (_, i) => { const key = i + excludeStart; return [key, key * 3] as [number, number]; }); const includeTree = buildTreeForFanout(includeEntries); const excludeTree = buildTreeForFanout(excludeEntries); runForEachKeyNotInAndSubtract(includeTree, excludeTree, (calls) => { expect(calls.length).toBe(size - excludeSpan); expect(calls[0]).toEqual({ key: 0, value: 0 }); const lastCall = calls[calls.length - 1]; expect(lastCall.key).toBe(size - 1); expect(lastCall.value).toBe((size - 1) * 2); expect(calls.filter(c => c.key >= excludeStart && c.key < excludeStart + excludeSpan)).toEqual([]); }); }); it('forEachKeyNotIn/subtract tree with itself visits no keys', () => { const entries = Array.from({ length: 20 }, (_, i) => [i, i * 2] as [number, number]); const tree = buildTreeForFanout(entries); expectForEachKeyNotInAndSubtractCalls(tree, tree, []); }); it('subtract returns a cloned tree when nothing is removed', () => { const includeTree = buildTreeForFanout(tuples([1, 10], [2, 20])); const excludeTree = buildTreeForFanout(tuples([3, 30])); const result = subtract, number, number>(includeTree, excludeTree); expect(result).not.toBe(includeTree); expect(result.toArray()).toEqual(includeTree.toArray()); expect(excludeTree.toArray()).toEqual(tuples([3, 30])); includeTree.checkValid(); result.checkValid(); excludeTree.checkValid(); }); it('forEachKeyNotIn/subtract arguments determine surviving keys', () => { const tree1 = buildTreeForFanout(tuples([1, 100], [2, 200], [4, 400])); const tree2 = buildTreeForFanout(tuples([2, 20], [3, 30], [4, 40])); expectForEachKeyNotInAndSubtractCalls(tree1, tree2, tuples([1, 100])); expectForEachKeyNotInAndSubtractCalls(tree2, tree1, tuples([3, 30])); }); }); describe('BTree forEachKeyNotIn early exiting', () => { const buildTreeForEarlyExit = (entries: Array<[number, number]>) => buildTree(entries, 4); it('forEachKeyNotIn returns undefined when callback returns void', () => { const includeTree = buildTreeForEarlyExit(tuples([1, 10], [2, 20], [3, 30])); const excludeTree = buildTreeForEarlyExit(tuples([2, 200])); const visited: number[] = []; const result = forEachKeyNotIn(includeTree, excludeTree, key => { visited.push(key); }); expect(result).toBeUndefined(); expect(visited).toEqual([1, 3]); }); it('forEachKeyNotIn ignores undefined break values and completes traversal', () => { const includeTree = buildTreeForEarlyExit(tuples([1, 10], [2, 20], [3, 30], [4, 40])); const excludeTree = buildTreeForEarlyExit(tuples([2, 200])); const visited: number[] = []; const result = forEachKeyNotIn(includeTree, excludeTree, key => { visited.push(key); return { break: undefined }; }); expect(result).toBeUndefined(); expect(visited).toEqual([1, 3, 4]); }); it('forEachKeyNotIn breaks early when callback returns a value', () => { const includeTree = buildTreeForEarlyExit(tuples([1, 10], [2, 20], [3, 30], [4, 40])); const excludeTree = buildTreeForEarlyExit(tuples([2, 200])); const visited: number[] = []; const breakResult = forEachKeyNotIn(includeTree, excludeTree, (key, value) => { visited.push(key); if (key === 3) { return { break: { key, value } }; } }); expect(breakResult).toEqual({ key: 3, value: 30 }); expect(visited).toEqual([1, 3]); }); }); describe('BTree forEachKeyNotIn and subtract input/output validation', () => { it('forEachKeyNotIn throws error when comparators differ', () => { const includeTree = new BTreeEx([[1, 10]], (a, b) => b - a); const excludeTree = new BTreeEx([[2, 20]], (a, b) => a + b); expect(() => forEachKeyNotIn(includeTree, excludeTree, () => { })).toThrow(comparatorErrorMsg); }); it('subtract throws error when comparators differ', () => { const includeTree = new BTreeEx([[1, 10]], (a, b) => b - a); const excludeTree = new BTreeEx([[2, 20]], (a, b) => a + b); expect(() => subtract, number, number>(includeTree, excludeTree)).toThrow(comparatorErrorMsg); }); it('subtract throws error when branching factors differ', () => { const includeTree = new BTreeEx([[1, 10]], (a, b) => a - b, 4); const excludeTree = new BTreeEx([[2, 20]], includeTree._compare, 8); expect(() => subtract, number, number>(includeTree, excludeTree)).toThrow(branchingFactorErrorMsg); }); }); describe('BTree forEachKeyNotIn/subtract fuzz tests', () => { const FUZZ_SETTINGS: SetOperationFuzzSettings = { branchingFactors: [4, 5, 32], ooms: [2, 3], fractionsPerOOM: [0.1, 0.25, 0.5], removalChances: [0, 0.01, 0.1] }; const FUZZ_TIMEOUT_MS = 30_000; jest.setTimeout(FUZZ_TIMEOUT_MS); const rng = new MersenneTwister(0xBAD_C0DE); forEachFuzzCase(FUZZ_SETTINGS, ({ maxNodeSize, size, fractionA, fractionB, removalChance, removalLabel }) => { it(`branch ${maxNodeSize}, size ${size}, fractionA ${fractionA.toFixed(2)}, fractionB ${fractionB.toFixed(2)}, removal ${removalLabel}`, () => { const treeA = new BTreeEx([], compareNumbers, maxNodeSize); const treeB = new BTreeEx([], compareNumbers, maxNodeSize); const [treeAEntries, treeBEntries] = populateFuzzTrees( [ { tree: treeA, fraction: fractionA, removalChance }, { tree: treeB, fraction: fractionB, removalChance } ], { rng, size, compare: compareNumbers, maxNodeSize, minAssignmentsPerKey: 1 } ); const bMap = new Map(treeBEntries); const aMap = new Map(treeAEntries); const expectedA = treeAEntries.filter(([key]) => !bMap.has(key)); const expectedB = treeBEntries.filter(([key]) => !aMap.has(key)); expectForEachKeyNotInAndSubtractCalls(treeA, treeB, expectedA); expectForEachKeyNotInAndSubtractCalls(treeB, treeA, expectedB); expectTreeMatchesEntries(treeA, treeAEntries); expectTreeMatchesEntries(treeB, treeBEntries); treeA.checkValid(true); treeB.checkValid(true); }); }); }); ================================================ FILE: test/union.test.ts ================================================ import BTree from '../b+tree'; import BTreeEx from '../extended'; import union from '../extended/union'; import { branchingFactorErrorMsg, comparatorErrorMsg } from '../extended/shared'; import MersenneTwister from 'mersenne-twister'; import { expectTreeMatchesEntries, forEachFuzzCase, makeArray, populateFuzzTrees, randomInt, SetOperationFuzzSettings, compareNumbers } from './shared'; type UnionFn = (key: number, leftValue: number, rightValue: number) => number | undefined; describe.each([32, 10, 4])('BTree union tests with fanout %i', (maxNodeSize) => { const sharesNode = (root: any, targetNode: any): boolean => { if (root === targetNode) return true; if (root.isLeaf) return false; const children = (root as any).children as any[]; for (let i = 0; i < children.length; i++) { if (sharesNode(children[i], targetNode)) return true; } return false; }; const buildTree = (keys: number[], valueScale = 1, valueOffset = 0) => { const tree = new BTreeEx([], compareNumbers, maxNodeSize); for (const key of keys) { tree.set(key, key * valueScale + valueOffset); } return tree; }; const expectRootLeafState = (tree: BTreeEx, expectedIsLeaf: boolean) => { const root = tree['_root'] as any; expect(root.isLeaf).toBe(expectedIsLeaf); }; const range = (start: number, endExclusive: number, step = 1): number[] => { const result: number[] = []; for (let i = start; i < endExclusive; i += step) result.push(i); return result; }; type UnionExpectationOptions = { after?: (ctx: { result: BTreeEx, expected: BTreeEx }) => void; expectedUnionFn?: UnionFn; }; const sumUnion: UnionFn = (_key, leftValue, rightValue) => leftValue + rightValue; const preferLeft: UnionFn = (_key, leftValue) => leftValue; const preferRight: UnionFn = (_key, _leftValue, rightValue) => rightValue; const failUnion = (message: string): UnionFn => () => { throw new Error(message); }; const naiveUnion = ( left: BTreeEx, right: BTreeEx, unionFn: UnionFn ) => { const expected = left.clone(); right.forEachPair((key, rightValue) => { if (expected.has(key)) { const leftValue = expected.get(key)!; const unionedValue = unionFn(key, leftValue, rightValue); if (unionedValue === undefined) { expected.delete(key); } else { expected.set(key, unionedValue); } } else { expected.set(key, rightValue); } }); return expected; }; const expectUnionMatchesBaseline = ( left: BTreeEx, right: BTreeEx, unionFn: UnionFn, options: UnionExpectationOptions = {} ) => { const { expectedUnionFn = unionFn, after } = options; const expected = naiveUnion(left, right, expectedUnionFn); const result = left.union(right, unionFn); expect(result.toArray()).toEqual(expected.toArray()); result.checkValid(); expected.checkValid(); after?.({ result, expected }); return { result, expected }; }; it('Union disjoint roots reuses roots', () => { // ensure the roots are not underfilled, as union will try to merge underfilled roots const size = maxNodeSize * maxNodeSize; const tree1 = buildTree(range(0, size), 1, 0); const offset = size * 5; const tree2 = buildTree(range(offset, offset + size), 2, 0); expectRootLeafState(tree1, false); expectRootLeafState(tree2, false); expectUnionMatchesBaseline(tree1, tree2, failUnion('Union callback should not run for disjoint roots'), { after: ({ result }) => { const resultRoot = result['_root'] as any; expect(sharesNode(resultRoot, tree1['_root'] as any)).toBe(true); expect(sharesNode(resultRoot, tree2['_root'] as any)).toBe(true); } }); }); it('Union leaf roots with intersecting keys uses union callback', () => { const tree1 = buildTree([1, 2, 4], 10, 0); const tree2 = buildTree([2, 3, 5], 100, 0); expectRootLeafState(tree1, true); expectRootLeafState(tree2, true); const calls: Array<{ key: number, leftValue: number, rightValue: number }> = []; expectUnionMatchesBaseline( tree1, tree2, (key, leftValue, rightValue) => { calls.push({ key, leftValue, rightValue }); return leftValue + rightValue; }, { expectedUnionFn: sumUnion } ); expect(calls).toEqual([{ key: 2, leftValue: 20, rightValue: 200 }]); }); it('Union leaf roots with disjoint keys', () => { const tree1 = buildTree([1, 3, 5], 1, 0); const tree2 = buildTree([2, 4, 6], 1, 1000); expectRootLeafState(tree1, true); expectRootLeafState(tree2, true); const { result } = expectUnionMatchesBaseline( tree1, tree2, failUnion('Union callback should not run for disjoint leaf roots') ); expect(result.toArray()).toEqual([ [1, 1], [2, 1002], [3, 3], [4, 1004], [5, 5], [6, 1006] ]); }); it('Union trees disjoint except for shared maximum key', () => { const size = maxNodeSize * 2; const tree1 = buildTree(range(0, size), 1, 0); const tree2 = buildTree(range(size - 1, size - 1 + size), 3, 0); expectRootLeafState(tree1, false); expectRootLeafState(tree2, false); let unionCalls = 0; const { result } = expectUnionMatchesBaseline( tree1, tree2, (_key, leftValue, rightValue) => { unionCalls++; return sumUnion(_key, leftValue, rightValue); }, { expectedUnionFn: sumUnion } ); expect(unionCalls).toBe(1); expect(result.get(size - 1)).toBe((size - 1) + (size - 1) * 3); expect(result.size).toBe(tree1.size + tree2.size - 1); }); it('Union trees where all leaves are disjoint and one tree straddles the other', () => { const straddleLength = 3 * 2 * maxNodeSize; // creates multiple leaves on both trees const tree1 = buildTree( range(0, straddleLength / 3).concat(range((straddleLength / 3) * 2, straddleLength)), 1 ); const tree2 = buildTree(range(straddleLength / 3, (straddleLength / 3) * 2), 3); expectRootLeafState(tree1, false); expectRootLeafState(tree2, false); const { result } = expectUnionMatchesBaseline( tree1, tree2, failUnion('Union callback should not run when all leaves are disjoint') ); expect(result.size).toBe(tree1.size + tree2.size); }); it('Union where two-leaf tree intersects leaf-root tree across both leaves', () => { const size = maxNodeSize + Math.max(3, Math.floor(maxNodeSize / 2)); const tree1 = buildTree(range(0, size), 2, 0); const tree2 = buildTree([1, Math.floor(size / 2), size - 1], 5, 0); expectRootLeafState(tree1, false); expectRootLeafState(tree2, true); const seenKeys: number[] = []; expectUnionMatchesBaseline( tree1, tree2, (key, _leftValue, rightValue) => { seenKeys.push(key); return rightValue; }, { expectedUnionFn: preferRight } ); expect(seenKeys.sort((a, b) => a - b)).toEqual([1, Math.floor(size / 2), size - 1]); }); it('Union where max key equals min key of other tree', () => { const size = maxNodeSize * 2; const tree1 = buildTree(range(0, size), 1, 0); const tree2 = buildTree(range(size - 1, size - 1 + size), 10, 0); expectRootLeafState(tree1, false); expectRootLeafState(tree2, false); let unionCalls = 0; const { result } = expectUnionMatchesBaseline( tree1, tree2, (_key, _leftValue, rightValue) => { unionCalls++; return rightValue; }, { expectedUnionFn: preferRight } ); expect(unionCalls).toBe(1); expect(result.get(size - 1)).toBe((size - 1) * 10); expect(result.size).toBe(tree1.size + tree2.size - 1); }); it('Union odd and even keyed trees', () => { const limit = maxNodeSize * 3; const treeOdd = buildTree(range(1, limit * 2, 2), 1, 0); const treeEven = buildTree(range(0, limit * 2, 2), 1, 100); expectRootLeafState(treeOdd, false); expectRootLeafState(treeEven, false); const { result } = expectUnionMatchesBaseline( treeOdd, treeEven, failUnion('Union callback should not be invoked for disjoint parity sets') ); expect(result.size).toBe(treeOdd.size + treeEven.size); }); it('Union merges disjoint leaf roots into a single leaf', () => { const perTree = Math.max(1, Math.floor(maxNodeSize / 2) - 1); const keysA = range(1, perTree).map(i => i); const keysB = keysA.map(k => k * 1000); const tree1 = buildTree(keysA); const tree2 = buildTree(keysB); expectRootLeafState(tree1, true); expectRootLeafState(tree2, true); const unioned = tree1.union(tree2, failUnion('Should not be called for disjoint keys')); const resultRoot = unioned['_root'] as any; const expectedKeys = keysA.concat(keysB).sort(compareNumbers); expect(resultRoot.isLeaf).toBe(true); expect(resultRoot.keys).toEqual(expectedKeys); }); it('Union combines underfilled non-leaf roots into a filled root', () => { const minChildren = Math.floor(maxNodeSize / 2); const targetLeavesPerTree = minChildren - 1; if (targetLeavesPerTree === 1) { return; // cannot test this case with only one leaf per tree } const entriesPerLeaf = maxNodeSize; const buildUnderfilledTree = (startKey: number) => { const keys: number[] = []; for (let leaf = 0; leaf < targetLeavesPerTree; leaf++) { for (let i = 0; i < entriesPerLeaf; i++) keys.push(startKey + leaf * entriesPerLeaf + i); } const tree = buildTree(keys); const root = tree['_root'] as any; expect(root.isLeaf).toBe(false); expect(root.children.length).toBeLessThan(minChildren); return { tree, nextKey: startKey + keys.length, childCount: root.children.length }; }; const first = buildUnderfilledTree(0); const second = buildUnderfilledTree(first.nextKey + maxNodeSize * 10); const unioned = first.tree.union(second.tree, failUnion('Should not be called for disjoint keys')); const resultRoot = unioned['_root'] as any; expect(resultRoot.isLeaf).toBe(false); expect(resultRoot.children.length).toBeGreaterThanOrEqual(minChildren); expect(resultRoot.children.length).toBe(first.childCount + second.childCount); }); it('Union overlapping prefix equal to branching factor', () => { const shared = maxNodeSize; const tree1Keys = [ ...range(0, shared), ...range(shared, shared + maxNodeSize) ]; const tree2Keys = [ ...range(0, shared), ...range(shared + maxNodeSize, shared + maxNodeSize * 2) ]; const tree1 = buildTree(tree1Keys, 1, 0); const tree2 = buildTree(tree2Keys, 2, 0); expectRootLeafState(tree1, false); expectRootLeafState(tree2, false); const unionedKeys: number[] = []; expectUnionMatchesBaseline( tree1, tree2, (key, leftValue, rightValue) => { unionedKeys.push(key); return leftValue + rightValue; }, { expectedUnionFn: sumUnion } ); expect(unionedKeys.sort((a, b) => a - b)).toEqual(range(0, shared)); }); it('Union two empty trees', () => { const tree1 = new BTreeEx([], compareNumbers, maxNodeSize); const tree2 = new BTreeEx([], compareNumbers, maxNodeSize); const { result } = expectUnionMatchesBaseline(tree1, tree2, sumUnion); expect(result.size).toBe(0); }); it('Union empty tree with non-empty tree', () => { const tree1 = new BTreeEx([], compareNumbers, maxNodeSize); const tree2 = new BTreeEx([[1, 10], [2, 20], [3, 30]], compareNumbers, maxNodeSize); const { result: leftUnion } = expectUnionMatchesBaseline(tree1, tree2, sumUnion); expect(leftUnion.toArray()).toEqual(tree2.toArray()); const { result: rightUnion } = expectUnionMatchesBaseline(tree2, tree1, sumUnion); expect(rightUnion.toArray()).toEqual(tree2.toArray()); expect(tree1.toArray()).toEqual([]); expect(tree2.toArray()).toEqual([[1, 10], [2, 20], [3, 30]]); tree1.checkValid(); tree2.checkValid(); }); it('Union with no overlapping keys', () => { const tree1 = new BTreeEx([[1, 10], [3, 30], [5, 50]], compareNumbers, maxNodeSize); const tree2 = new BTreeEx([[2, 20], [4, 40], [6, 60]], compareNumbers, maxNodeSize); const { result } = expectUnionMatchesBaseline( tree1, tree2, failUnion('Should not be called for non-overlapping keys') ); expect(result.size).toBe(6); expect(result.toArray()).toEqual([[1, 10], [2, 20], [3, 30], [4, 40], [5, 50], [6, 60]]); }); it('Union with completely overlapping keys - sum values', () => { const tree1 = new BTreeEx([[1, 10], [2, 20], [3, 30]], compareNumbers, maxNodeSize); const tree2 = new BTreeEx([[1, 5], [2, 15], [3, 25]], compareNumbers, maxNodeSize); const { result } = expectUnionMatchesBaseline(tree1, tree2, sumUnion); expect(result.size).toBe(tree1.size); }); it('Union with completely overlapping keys - prefer left', () => { const tree1 = new BTreeEx([[1, 10], [2, 20], [3, 30]], compareNumbers, maxNodeSize); const tree2 = new BTreeEx([[1, 100], [2, 200], [3, 300]], compareNumbers, maxNodeSize); const { result } = expectUnionMatchesBaseline(tree1, tree2, preferLeft); expect(result.toArray()).toEqual(tree1.toArray()); }); it('Union with completely overlapping keys - prefer right', () => { const tree1 = new BTreeEx([[1, 10], [2, 20], [3, 30]], compareNumbers, maxNodeSize); const tree2 = new BTreeEx([[1, 100], [2, 200], [3, 300]], compareNumbers, maxNodeSize); const { result } = expectUnionMatchesBaseline(tree1, tree2, (_k, _v1, v2) => v2); expect(result.toArray()).toEqual(tree2.toArray()); }); it('Union with partially overlapping keys', () => { const tree1 = new BTreeEx([[1, 10], [2, 20], [3, 30], [4, 40]], compareNumbers, maxNodeSize); const tree2 = new BTreeEx([[3, 300], [4, 400], [5, 500], [6, 600]], compareNumbers, maxNodeSize); const unionedKeys: number[] = []; expectUnionMatchesBaseline( tree1, tree2, (key, v1, v2) => { unionedKeys.push(key); return v1 + v2; }, { expectedUnionFn: sumUnion } ); expect(unionedKeys.sort((a, b) => a - b)).toEqual([3, 4]); }); it('Union with overlapping keys can delete entries', () => { const tree1 = new BTreeEx([[1, 10], [2, 20], [3, 30], [4, 40]], compareNumbers, maxNodeSize); const tree2 = new BTreeEx([[2, 200], [3, 300], [4, 400], [5, 500]], compareNumbers, maxNodeSize); const { result } = expectUnionMatchesBaseline(tree1, tree2, (k, v1, v2) => { if (k === 3) return undefined; return v1 + v2; }); expect(result.has(3)).toBe(false); }); it('Union is called even when values are equal', () => { const tree1 = new BTreeEx([[1, 10], [2, 20]], compareNumbers, maxNodeSize); const tree2 = new BTreeEx([[2, 20], [3, 30]], compareNumbers, maxNodeSize); const unionCallLog: Array<{ k: number, v1: number, v2: number }> = []; expectUnionMatchesBaseline( tree1, tree2, (k, v1, v2) => { unionCallLog.push({ k, v1, v2 }); return v1; }, { expectedUnionFn: preferLeft } ); expect(unionCallLog).toEqual([{ k: 2, v1: 20, v2: 20 }]); }); it('Union does not mutate input trees', () => { const entries1: [number, number][] = [[1, 10], [2, 20], [3, 30]]; const entries2: [number, number][] = [[2, 200], [3, 300], [4, 400]]; const tree1 = new BTreeEx(entries1, compareNumbers, maxNodeSize); const tree2 = new BTreeEx(entries2, compareNumbers, maxNodeSize); const snapshot1 = tree1.toArray(); const snapshot2 = tree2.toArray(); expectUnionMatchesBaseline(tree1, tree2, sumUnion); expect(tree1.toArray()).toEqual(snapshot1); expect(tree2.toArray()).toEqual(snapshot2); tree1.checkValid(); tree2.checkValid(); }); it('Union large trees with some overlaps', () => { const entries1: [number, number][] = range(0, 1000).map(i => [i, i]); const entries2: [number, number][] = range(500, 1500).map(i => [i, i * 10]); const tree1 = new BTreeEx(entries1, compareNumbers, maxNodeSize); const tree2 = new BTreeEx(entries2, compareNumbers, maxNodeSize); let unionCount = 0; expectUnionMatchesBaseline( tree1, tree2, (k, v1, v2) => { unionCount++; return v1 + v2; }, { expectedUnionFn: sumUnion } ); expect(unionCount).toBe(500); }); it('Union with overlaps at boundaries', () => { const tree1 = new BTreeEx([], compareNumbers, maxNodeSize); const tree2 = new BTreeEx([], compareNumbers, maxNodeSize); for (let i = 0; i < 100; i++) { tree1.set(i * 2, i * 2); } for (let i = 50; i < 150; i++) { tree2.set(i, i * 10); } const unionedKeys: number[] = []; expectUnionMatchesBaseline( tree1, tree2, (key, v1, v2) => { unionedKeys.push(key); return v1 + v2; }, { expectedUnionFn: sumUnion } ); const expectedUnionedKeys = range(50, 150).filter(k => k % 2 === 0); expect(unionedKeys.sort((a, b) => a - b)).toEqual(expectedUnionedKeys); }); it('Union result can be modified without affecting inputs', () => { const tree1 = new BTreeEx([[1, 10], [2, 20]], compareNumbers, maxNodeSize); const tree2 = new BTreeEx([[3, 30], [4, 40]], compareNumbers, maxNodeSize); const { result } = expectUnionMatchesBaseline(tree1, tree2, sumUnion); result.set(1, 100); result.set(5, 50); result.delete(2); expect(tree1.get(1)).toBe(10); expect(tree1.get(2)).toBe(20); expect(tree1.has(5)).toBe(false); expect(tree2.get(3)).toBe(30); expect(tree2.get(4)).toBe(40); tree1.checkValid(); tree2.checkValid(); result.checkValid(); }); it('Union tree with itself returns a clone without invoking combineFn', () => { const size = maxNodeSize * 2 + 5; const tree = buildTree(range(0, size), 3, 1); let unionCalls = 0; const original = tree.toArray(); const result = tree.union(tree, (key, leftValue, rightValue) => { unionCalls++; return sumUnion(key, leftValue, rightValue); }); expect(unionCalls).toBe(0); expect(result).not.toBe(tree); expect(result.toArray()).toEqual(original); expect(tree.toArray()).toEqual(original); }); it('Standalone union short-circuits when given the same tree twice', () => { const size = maxNodeSize * 2 + 1; const tree = buildTree(range(0, size), 1, 0); let unionCalls = 0; const original = tree.toArray(); const result = union(tree, tree, (_key: number, _leftValue: number, _rightValue: number) => { unionCalls++; return undefined; }); expect(unionCalls).toBe(0); expect(result).not.toBe(tree); expect(result.toArray()).toEqual(original); expect(tree.toArray()).toEqual(original); }); it('Union with disjoint ranges', () => { const entries1: [number, number][] = []; for (let i = 1; i <= 100; i++) entries1.push([i, i]); for (let i = 201; i <= 300; i++) entries1.push([i, i]); const entries2: [number, number][] = []; for (let i = 101; i <= 200; i++) entries2.push([i, i]); const tree1 = new BTreeEx(entries1, compareNumbers, maxNodeSize); const tree2 = new BTreeEx(entries2, compareNumbers, maxNodeSize); const { result } = expectUnionMatchesBaseline( tree1, tree2, failUnion('Should not be called - no overlaps') ); expect(result.size).toBe(300); expect(result.get(1)).toBe(1); expect(result.get(100)).toBe(100); expect(result.get(101)).toBe(101); expect(result.get(200)).toBe(200); expect(result.get(201)).toBe(201); expect(result.get(300)).toBe(300); }); it('Union with single element trees', () => { const tree1 = new BTreeEx([[5, 50]], compareNumbers, maxNodeSize); const tree2 = new BTreeEx([[5, 500]], compareNumbers, maxNodeSize); const { result } = expectUnionMatchesBaseline(tree1, tree2, (_k, v1, v2) => Math.max(v1, v2)); expect(result.toArray()).toEqual([[5, 500]]); }); it('Union excluding all overlapping keys', () => { const tree1 = new BTreeEx([[1, 10], [2, 20], [3, 30]], compareNumbers, maxNodeSize); const tree2 = new BTreeEx([[2, 200], [3, 300], [4, 400]], compareNumbers, maxNodeSize); const { result } = expectUnionMatchesBaseline(tree1, tree2, () => undefined); expect(result.toArray()).toEqual([[1, 10], [4, 400]]); }); it('Union with large disjoint ranges', () => { const tree1 = new BTreeEx([], compareNumbers, maxNodeSize); const tree2 = new BTreeEx([], compareNumbers, maxNodeSize); for (let i = 0; i <= 10000; i++) tree1.set(i, i); for (let i = 10001; i <= 20000; i++) tree2.set(i, i); const { result } = expectUnionMatchesBaseline( tree1, tree2, failUnion('Union callback should not run for disjoint ranges') ); expect(result.size).toBe(tree1.size + tree2.size); expect(result.get(0)).toBe(0); expect(result.get(20000)).toBe(20000); }); it('Union trees with random overlap', () => { const size = 10000; const keys1 = makeArray(size, true); const keys2 = makeArray(size, true); const tree1 = new BTreeEx(); const tree2 = new BTreeEx(); for (let k of keys1) tree1.set(k, k); for (let k of keys2) tree2.set(k, k * 10); expectUnionMatchesBaseline(tree1, tree2, preferLeft); }); it('Union trees with ~10% overlap', () => { const size = 200; const offset = Math.floor(size * 0.9); const overlap = size - offset; const tree1 = new BTreeEx([], compareNumbers, maxNodeSize); const tree2 = new BTreeEx([], compareNumbers, maxNodeSize); for (let i = 0; i < size; i++) tree1.set(i, i); for (let i = 0; i < size; i++) { const key = offset + i; tree2.set(key, key * 10); } const { result } = expectUnionMatchesBaseline(tree1, tree2, preferLeft); expect(result.size).toBe(size + size - overlap); for (let i = 0; i < offset; i++) expect(result.get(i)).toBe(i); for (let i = offset; i < size; i++) expect(result.get(i)).toBe(i); const upperBound = offset + size; for (let i = size; i < upperBound; i++) expect(result.get(i)).toBe(i * 10); }); }); describe('BTree union input/output validation', () => { test('Union throws error when comparators differ', () => { const tree1 = new BTreeEx([[1, 10]], (a, b) => b + a); const tree2 = new BTreeEx([[2, 20]], (a, b) => b - a); expect(() => tree1.union(tree2, (_k, v1, v2) => v1 + v2)).toThrow(comparatorErrorMsg); }); test('Union throws error when max node sizes differ', () => { const tree1 = new BTreeEx([[1, 10]], compareNumbers, 32); const tree2 = new BTreeEx([[2, 20]], compareNumbers, 33); expect(() => tree1.union(tree2, (_k, v1, v2) => v1 + v2)).toThrow(branchingFactorErrorMsg); }); test('Union returns a tree of the same class', () => { expect(union(new BTreeEx(), new BTreeEx(), (_k, v1, v2) => v1)).toBeInstanceOf(BTreeEx); expect(union(new BTree(), new BTree(), (_k, v1, v2) => v1)).toBeInstanceOf(BTree); expect(union(new BTree(), new BTree(), (_k, v1, v2) => v1) instanceof BTreeEx).toBeFalsy(); }); }); describe('BTree union fuzz tests', () => { const unionFn = (_k: number, left: number, _right: number) => left; const FUZZ_SETTINGS: SetOperationFuzzSettings = { branchingFactors: [4, 5, 32], ooms: [0, 1, 2], // [0, 1, 2, 3], fractionsPerOOM: [0.1, 0.25, 0.5], // [0.0001, 0.01, 0.1, 0.25, 0.5], removalChances: [0, 0.01, 0.1] }; const RANDOM_EDITS_PER_TEST = 20; const TIMEOUT_MS = 30_000; jest.setTimeout(TIMEOUT_MS); const rng = new MersenneTwister(0xBEEFCAFE); forEachFuzzCase(FUZZ_SETTINGS, ({ maxNodeSize, size, fractionA, fractionB, removalChance, removalLabel }) => { test(`branch ${maxNodeSize}, size ${size}, fractionA ${fractionA.toFixed(2)}, fractionB ${fractionB.toFixed(2)}, removal ${removalLabel}`, () => { const treeA = new BTreeEx([], compareNumbers, maxNodeSize); const treeB = new BTreeEx([], compareNumbers, maxNodeSize); const [treeAEntries, treeBEntries] = populateFuzzTrees( [ { tree: treeA, fraction: fractionA, removalChance }, { tree: treeB, fraction: fractionB, removalChance } ], { rng, size, compare: compareNumbers, maxNodeSize, minAssignmentsPerKey: 1 } ); const unioned = treeA.union(treeB, unionFn); unioned.checkValid(true); const combinedKeys = new Set(); treeAEntries.forEach(([key]) => combinedKeys.add(key)); treeBEntries.forEach(([key]) => combinedKeys.add(key)); const expected = Array.from(combinedKeys).sort(compareNumbers).map(key => [key, key]); expect(unioned.toArray()).toEqual(expected); // Union should not have mutated inputs expectTreeMatchesEntries(treeA, treeAEntries); expectTreeMatchesEntries(treeB, treeBEntries); for (let edit = 0; edit < RANDOM_EDITS_PER_TEST; edit++) { const key = 1 + randomInt(rng, size); const action = rng.random(); if (action < 0.33) { unioned.set(key, key); } else if (action < 0.66) { unioned.set(key, -key); } else { unioned.delete(key); } } // Check for shared mutability issues expectTreeMatchesEntries(treeA, treeAEntries); expectTreeMatchesEntries(treeB, treeBEntries); }); }); }); ================================================ FILE: tsconfig.json ================================================ { // TypeScript configuration file: provides options to the TypeScript // compiler (tsc) and makes VSCode recognize this folder as a TS project, // enabling the VSCode build tasks "tsc: build" and "tsc: watch". "compilerOptions": { "target": "es5", // Compatible with older browsers "module": "commonjs", // Compatible with both Node.js and browser "moduleResolution": "node", // Tell tsc to look in node_modules for modules "sourceMap": false, // Whether to create *.js.map files "jsx": "react", // Causes inline XML (JSX code) to be expanded "strict": true, // Strict types, eg. prohibits `var x=0; x=null` "alwaysStrict": true, // Enable JavaScript's "use strict" mode "esModuleInterop": true, // CommonJS import behavior similar to Babel/mjs "declaration": true, // Generate d.ts files // Note: BTree does not rely on ES6 runtime APIs, just compile-time interfaces "lib": ["es6"], // APIs expected to exist at runtime "downlevelIteration": false, // for-of loops and yield statement in ES5 "stripInternal": true }, "include": ["**/*.ts"], "exclude": ["node_modules", "tests", "test"], }