Showing preview only (619K chars total). Download the full file or copy to clipboard to get everything.
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<V, R = number> = {
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<string>` or `BTree<number>` but not `BTree<string|number>`.
*
* 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<K = any, V = any> implements ISortedMapF<K, V>, ISortedMap<K, V> {
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<K, V>) => 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<R = number>(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<K, V | undefined>;
/** Returns a copy of the tree with the specified key-value pair set. */
with<V2>(key: K, value: V2, overwrite?: boolean): BTree<K, V | V2>;
/** Returns a copy of the tree with the specified key-value pairs set. */
withPairs<V2>(pairs: [K, V | V2][], overwrite: boolean): BTree<K, V | V2>;
/** 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<K, V | undefined>;
/** 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<R>(callback: (v: V, k: K, counter: number) => R): BTree<K, 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` 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<R>(callback: (previous: R, currentPair: [K, V], counter: number, tree: BTree<K, V>) => R, initialValue: R): R;
reduce<R>(callback: (previous: R | undefined, currentPair: [K, V], counter: number, tree: BTree<K, V>) => 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<K>;
/** 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<V>;
/** 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<R = V>(low: K, high: K, includeHigh: boolean, onFound: (k: K, v: V, counter: number) => EditRangeResult<V, R> | void, initialCounter?: number): R | number;
/** Same as `editRange` except that the callback is called for all pairs. */
editAll<R = V>(onFound: (k: K, v: V, counter: number) => EditRangeResult<V, R> | 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<K>` 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<K>`
* even if `V` can't be `undefined` (discussion: btree-typescript issue #14) */
export declare function asSet<K, V>(btree: BTree<K, V>): undefined extends V ? ISortedSet<K> : unknown;
/** A BTree frozen in the empty state. */
export declare const EmptyBTree: BTree<any, any>;
================================================
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<K,V> 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<K>` 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<K>`
* 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<K>
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)
var v = this.values;
if (rhs.values === undefVals) {
if (v !== undefVals)
v.push(undefined);
}
else {
v = this.reifyValues();
v.push(rhs.values.shift());
}
this.keys.push(rhs.keys.shift());
};
BNode.prototype.takeFromLeft = function (lhs) {
// 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)
var v = this.values;
if (lhs.values === undefVals) {
if (v !== undefVals)
v.unshift(undefined);
}
else {
v = this.reifyValues();
v.unshift(lhs.values.pop());
}
this.keys.unshift(lhs.keys.pop());
};
BNode.prototype.splitOffRightSide = function () {
// 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 //////////////////////////////////////////
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)
var rhsInternal = rhs;
this.keys.push(rhs.keys.shift());
var child = rhsInternal.children.shift();
this.children.push(child);
var size = child.size();
rhsInternal._size -= size;
this._size += size;
};
BNodeInternal.prototype.takeFromLeft = function (lhs) {
// 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)
var lhsInternal = lhs;
var child = lhsInternal.children.pop();
this.keys.unshift(lhs.keys.pop());
this.children.unshift(child);
var 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.
BNodeInternal.prototype.forRange = function (low, high, includeHigh, editMode, tree, count, onFound) {
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++) {
var child = children[i];
if (child.isShared)
children[i] = child = child.clone();
var beforeSize = child.size();
var result_1 = 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_1 !== 'number')
return result_1;
count = result_1;
}
}
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);
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<V,R=number> = {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<string>` or `BTree<number>` but not `BTree<string|number>`.
*
* 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<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap<K,V>
{
private _root: BNode<K, V> = EmptyLeaf as BNode<K,V>;
_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<K,V> 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<K,V>;
}
forEach(callback: (v:V, k:K, tree:BTree<K,V>) => 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<R=number>(callback: (v:V, k:K, tree:BTree<K,V>) => {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<R=number>(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<K,V>(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<K,V|undefined>;
/** Returns a copy of the tree with the specified key-value pair set. */
with<V2>(key: K, value: V2, overwrite?: boolean): BTree<K,V|V2>;
with<V2>(key: K, value?: V2, overwrite?: boolean): BTree<K,V|V2|undefined> {
let nu = this.clone() as BTree<K,V|V2|undefined>;
return nu.set(key, value, overwrite) || overwrite ? nu : this;
}
/** Returns a copy of the tree with the specified key-value pairs set. */
withPairs<V2>(pairs: [K,V|V2][], overwrite: boolean): BTree<K,V|V2> {
let nu = this.clone() as BTree<K,V|V2>;
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<K,V|undefined> {
let nu = this.clone() as BTree<K,V|undefined>, 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<R>(callback: (v:V,k:K,counter:number) => R): BTree<K,R> {
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<K,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` 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<R>(callback: (previous:R,currentPair:[K,V],counter:number,tree:BTree<K,V>) => R, initialValue: R): R;
reduce<R>(callback: (previous:R|undefined,currentPair:[K,V],counter:number,tree:BTree<K,V>) => R): R|undefined;
reduce<R>(callback: (previous:R|undefined,currentPair:[K,V],counter:number,tree:BTree<K,V>) => 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<K,V>).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<K,V>).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<K,V>[][], nodeindex: number[], leaf: BNode<K,V> } | undefined
{
var nextnode = this._root;
var nodequeue: BNode<K,V>[][], 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<K,V>).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<K> {
var it = this.entries(firstKey, ReusedArray);
return iterator<K>(() => {
var n: IteratorResult<any> = 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<V> {
var it = this.entries(firstKey, ReusedArray);
return iterator<V>(() => {
var n: IteratorResult<any> = 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<K,V>(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<K,V>(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<R=number>(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<R=V>(low: K, high: K, includeHigh: boolean, onFound: (k:K,v:V,counter:number) => EditRangeResult<V,R>|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<K,V>).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<R=V>(onFound: (k:K,v:V,counter:number) => EditRangeResult<V,R>|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<K, V> | undefined = this._root;
let height = -1;
while (node) {
height++;
node = node.isLeaf ? undefined : (node as unknown as BNodeInternal<K, V>).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<K>` 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<K>`
* even if `V` can't be `undefined` (discussion: btree-typescript issue #14) */
export function asSet<K,V>(btree: BTree<K,V>): undefined extends V ? ISortedSet<K> : 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<K>
function iterator<T>(next: () => IteratorResult<T> = (() => ({ done:true, value:undefined }))): IterableIterator<T> {
var result: any = { next };
if (Symbol && Symbol.iterator)
result[Symbol.iterator] = function() { return this; };
return result;
}
/** @internal */
export class BNode<K,V> {
// 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<K,V> {
var v = this.values;
return new BNode<K,V>(this.keys.slice(0), v === undefVals ? v : v.slice(0));
}
greedyClone(force?: boolean): BNode<K,V> {
return this.isShared && !force ? this : this.clone();
}
get(key: K, defaultValue: V|undefined, tree: BTree<K,V>): 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<K,V>, 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<K,V>): boolean|BNode<K,V> {
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<K,V> = 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<K,V>) {
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<K,V>) {
// 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)
var v = this.values;
if (rhs.values === undefVals) {
if (v !== undefVals)
v.push(undefined as any);
} else {
v = this.reifyValues();
v.push(rhs.values.shift()!);
}
this.keys.push(rhs.keys.shift()!);
}
takeFromLeft(lhs: BNode<K,V>) {
// 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)
var v = this.values;
if (lhs.values === undefVals) {
if (v !== undefVals)
v.unshift(undefined as any);
} else {
v = this.reifyValues();
v.unshift(lhs.values.pop()!);
}
this.keys.unshift(lhs.keys.pop()!);
}
splitOffRightSide(): BNode<K,V> {
// 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<K,V>(keys, values);
}
/////////////////////////////////////////////////////////////////////////////
// Leaf Node: scanning & deletions //////////////////////////////////////////
forRange<R>(low: K, high: K, includeHigh: boolean|undefined, editMode: boolean, tree: BTree<K,V>, count: number,
onFound?: (k:K, v:V, counter:number) => EditRangeResult<V,R>|void): EditRangeResult<V,R>|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<K,V>, _: 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<K,V> extends BNode<K,V> {
// 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<K,V>[];
_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<K,V>[], 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<K,V> {
var children = this.children.slice(0);
for (var i = 0; i < children.length; i++)
children[i].isShared = true;
return new BNodeInternal<K,V>(children, this._size, this.keys.slice(0));
}
size(): number {
return this._size;
}
greedyClone(force?: boolean): BNode<K,V> {
if (this.isShared && !force)
return this;
var nu = new BNodeInternal<K,V>(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<K,V>): 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<K,V>, 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<K,V>): boolean|BNodeInternal<K,V> {
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<K,V>;
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<K,V> = 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<K,V>) {
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<K,V>(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<K,V>(newChildren, sizePrev - this._size, newKeys);
return newNode;
}
takeFromRight(rhs: BNode<K,V>) {
// 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)
const rhsInternal = rhs as BNodeInternal<K,V>;
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<K,V>) {
// 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)
const lhsInternal = lhs as BNodeInternal<K,V>;
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<R>(low: K, high: K, includeHigh: boolean|undefined, editMode: boolean, tree: BTree<K,V>, count: number,
onFound?: (k:K, v:V, counter:number) => EditRangeResult<V,R>|void): EditRangeResult<V,R>|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<K,V>, 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<K,V>).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<K,V>(children: BNode<K,V>[]): 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<K,V>(
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<any,any>(); 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<number, number>,
tree2: BTreeEx<number, number>,
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<number, number>();
const tree2 = new BTreeEx<number, number>();
// 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<number, number>();
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<number, number>,
excludeTree: BTreeEx<number, number>,
) => {
const subtractResult = measure(() => `subtract: ${baseTitle}`, () => {
return subtract<BTreeEx<number, number>, 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<number, number>();
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<number, number>();
const excludeTree = new BTreeEx<number, number>();
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<number, number>();
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<number, number>,
tree2: BTreeEx<number, number>,
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<number, number>(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<number, number>();
const tree2 = new BTreeEx<number, number>();
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<number, number>();
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<number, number>,
tree2: BTreeEx<number, number>,
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<number, number>,
excludeTree: BTreeEx<number, number>,
) => {
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<number, number>, tree2: BTreeEx<number, number>) => 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<number, number>();
const tree2 = new BTreeEx<number, number>();
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<number, number>();
for (let i = 0; i < totalKeys; i++) {
tree1.set(i, i);
}
const tree2 = new BTreeEx<number, number>();
for (let i = 0; i < totalKeys; i++) {
if ((i % overlapInterval) < overlapPerInterval) {
tree2.set(i, i * 7);
} els
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
SYMBOL INDEX (346 symbols across 42 files)
FILE: b+tree.d.ts
type EditRangeResult (line 3) | type EditRangeResult<V, R = number> = {
type DefaultComparable (line 11) | type DefaultComparable = number | string | Date | boolean | null | undef...
class BTree (line 106) | class BTree<K = any, V = any> implements ISortedMapF<K, V>, ISortedMap<K...
FILE: b+tree.js
function __ (line 13) | function __() { this.constructor = d; }
function defaultComparator (line 30) | function defaultComparator(a, b) {
function simpleComparator (line 78) | function simpleComparator(a, b) {
function fixMaxSize (line 85) | function fixMaxSize(maxNodeSize) {
function BTree (line 162) | function BTree(entries, compare, maxNodeSize) {
function asSet (line 848) | function asSet(btree) {
function iterator (line 857) | function iterator(next) {
function BNode (line 866) | function BNode(keys, values) {
function BNodeInternal (line 1181) | function BNodeInternal(children, size, keys) {
function sumChildSizes (line 1508) | function sumChildSizes(children) {
function areOverlapping (line 1519) | function areOverlapping(aMin, aMax, bMin, bMax, cmp) {
function check (line 1533) | function check(fact) {
FILE: b+tree.ts
type EditRangeResult (line 9) | type EditRangeResult<V,R=number> = {value?:V, break?:R, delete?:boolean};
type index (line 11) | type index = number;
type DefaultComparable (line 37) | type DefaultComparable = number | string | Date | boolean | null | undef...
function defaultComparator (line 51) | function defaultComparator(a: DefaultComparable, b: DefaultComparable): ...
function simpleComparator (line 116) | function simpleComparator(a: any, b: any): number {
function fixMaxSize (line 122) | function fixMaxSize(maxNodeSize?: number) {
class BTree (line 190) | class BTree<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap<K,V>
method constructor (line 209) | public constructor(entries?: [K,V][], compare?: (a: K, b: K) => number...
method size (line 220) | get size(): number { return this._root.size(); }
method length (line 222) | get length(): number { return this.size; }
method isEmpty (line 224) | get isEmpty(): boolean { return this._root.size() === 0; }
method clear (line 227) | clear() {
method forEach (line 241) | forEach<R=number>(callback: (v:V, k:K, tree:BTree<K,V>) => {break?:R}|...
method forEachPair (line 261) | forEachPair<R=number>(callback: (k:K, v:V, counter:number) => {break?:...
method get (line 272) | get(key: K, defaultValue?: V): V | undefined {
method set (line 290) | set(key: K, value: V, overwrite?: boolean): boolean {
method has (line 309) | has(key: K): boolean {
method delete (line 319) | delete(key: K): boolean {
method with (line 330) | with<V2>(key: K, value?: V2, overwrite?: boolean): BTree<K,V|V2|undefi...
method withPairs (line 336) | withPairs<V2>(pairs: [K,V|V2][], overwrite: boolean): BTree<K,V|V2> {
method withKeys (line 349) | withKeys(keys: K[], returnThisIfUnchanged?: boolean): BTree<K,V|undefi...
method without (line 362) | without(key: K, returnThisIfUnchanged?: boolean): this {
method withoutKeys (line 372) | withoutKeys(keys: K[], returnThisIfUnchanged?: boolean): this {
method withoutRange (line 378) | withoutRange(low: K, high: K, includeHigh: boolean, returnThisIfUnchan...
method filter (line 387) | filter(callback: (k:K,v:V,counter:number) => boolean, returnThisIfUnch...
method mapValues (line 399) | mapValues<R>(callback: (v:V,k:K,counter:number) => R): BTree<K,R> {
method reduce (line 422) | reduce<R>(callback: (previous:R|undefined,currentPair:[K,V],counter:nu...
method entries (line 441) | entries(lowestKey?: K, reusedArray?: (K|V)[]): IterableIterator<[K,V]> {
method entriesReversed (line 495) | entriesReversed(highestKey?: K, reusedArray?: (K|V)[], skipHighest?: b...
method findPath (line 556) | private findPath(key?: K): { nodequeue: BNode<K,V>[][], nodeindex: num...
method keys (line 580) | keys(firstKey?: K): IterableIterator<K> {
method values (line 591) | values(firstKey?: K): IterableIterator<V> {
method maxNodeSize (line 604) | get maxNodeSize() {
method minKey (line 609) | minKey(): K | undefined { return this._root.minKey(); }
method maxKey (line 612) | maxKey(): K | undefined { return this._root.maxKey(); }
method clone (line 619) | clone(): this {
method greedyClone (line 631) | greedyClone(force?: boolean): this {
method toArray (line 638) | toArray(maxLength: number = 0x7FFFFFFF): [K,V][] {
method keysArray (line 646) | keysArray() {
method valuesArray (line 654) | valuesArray() {
method toString (line 662) | toString() {
method setIfNotPresent (line 669) | setIfNotPresent(key: K, value: V): boolean {
method nextHigherPair (line 679) | nextHigherPair(key: K|undefined, reusedArray?: [K,V]): [K,V]|undefined {
method nextHigherKey (line 690) | nextHigherKey(key: K|undefined): K|undefined {
method nextLowerPair (line 701) | nextLowerPair(key: K|undefined, reusedArray?: [K,V]): [K,V]|undefined {
method nextLowerKey (line 712) | nextLowerKey(key: K|undefined): K|undefined {
method getPairOrNextLower (line 724) | getPairOrNextLower(key: K, reusedArray?: [K,V]): [K,V]|undefined {
method getPairOrNextHigher (line 735) | getPairOrNextHigher(key: K, reusedArray?: [K,V]): [K,V]|undefined {
method changeIfPresent (line 742) | changeIfPresent(key: K, value: V): boolean {
method getRange (line 758) | getRange(low: K, high: K, includeHigh?: boolean, maxLength: number = 0...
method setPairs (line 776) | setPairs(pairs: [K,V][], overwrite?: boolean): number {
method forRange (line 803) | forRange<R=number>(low: K, high: K, includeHigh: boolean, onFound?: (k...
method editRange (line 837) | editRange<R=V>(low: K, high: K, includeHigh: boolean, onFound: (k:K,v:...
method editAll (line 859) | editAll<R=V>(onFound: (k:K,v:V,counter:number) => EditRangeResult<V,R>...
method deleteRange (line 871) | deleteRange(low: K, high: K, includeHigh: boolean): number {
method deleteKeys (line 876) | deleteKeys(keys: K[]): number {
method height (line 885) | get height(): number {
method freeze (line 901) | freeze() {
method unfreeze (line 911) | unfreeze() {
method isFrozen (line 922) | get isFrozen() {
method checkValid (line 931) | checkValid(checkOrdering = false) {
function asSet (line 944) | function asSet<K,V>(btree: BTree<K,V>): undefined extends V ? ISortedSet...
function iterator (line 955) | function iterator<T>(next: () => IteratorResult<T> = (() => ({ done:true...
class BNode (line 964) | class BNode<K,V> {
method isLeaf (line 973) | get isLeaf() { return (this as any).children === undefined; }
method constructor (line 975) | constructor(keys: K[] = [], values?: V[]) {
method size (line 981) | size(): number {
method maxKey (line 988) | maxKey() {
method indexOf (line 994) | indexOf(key: K, failXor: number, cmp: (a:K, b:K) => number): index {
method minKey (line 1065) | minKey(): K | undefined {
method minPair (line 1069) | minPair(reusedArray: [K,V]): [K,V] | undefined {
method maxPair (line 1077) | maxPair(reusedArray: [K,V]): [K,V] | undefined {
method clone (line 1086) | clone(): BNode<K,V> {
method greedyClone (line 1091) | greedyClone(force?: boolean): BNode<K,V> {
method get (line 1095) | get(key: K, defaultValue: V|undefined, tree: BTree<K,V>): V|undefined {
method getPairOrNextLower (line 1100) | getPairOrNextLower(key: K, compare: (a: K, b: K) => number, inclusive:...
method getPairOrNextHigher (line 1111) | getPairOrNextHigher(key: K, compare: (a: K, b: K) => number, inclusive...
method checkValid (line 1123) | checkValid(depth: number, tree: BTree<K,V>, baseIndex: number, checkOr...
method set (line 1146) | set(key: K, value: V, overwrite: boolean|undefined, tree: BTree<K,V>):...
method reifyValues (line 1176) | reifyValues() {
method insertInLeaf (line 1182) | insertInLeaf(i: index, key: K, value: V, tree: BTree<K,V>) {
method takeFromRight (line 1197) | takeFromRight(rhs: BNode<K,V>) {
method takeFromLeft (line 1212) | takeFromLeft(lhs: BNode<K,V>) {
method splitOffRightSide (line 1227) | splitOffRightSide(): BNode<K,V> {
method forRange (line 1237) | forRange<R>(low: K, high: K, includeHigh: boolean|undefined, editMode:...
method mergeSibling (line 1284) | mergeSibling(rhs: BNode<K,V>, _: number) {
class BNodeInternal (line 1297) | class BNodeInternal<K,V> extends BNode<K,V> {
method constructor (line 1308) | constructor(children: BNode<K,V>[], size: number, keys?: K[]) {
method clone (line 1319) | clone(): BNode<K,V> {
method size (line 1326) | size(): number {
method greedyClone (line 1330) | greedyClone(force?: boolean): BNode<K,V> {
method minKey (line 1339) | minKey() {
method minPair (line 1343) | minPair(reusedArray: [K,V]): [K,V] | undefined {
method maxPair (line 1347) | maxPair(reusedArray: [K,V]): [K,V] | undefined {
method get (line 1351) | get(key: K, defaultValue: V|undefined, tree: BTree<K,V>): V|undefined {
method getPairOrNextLower (line 1356) | getPairOrNextLower(key: K, compare: (a: K, b: K) => number, inclusive:...
method getPairOrNextHigher (line 1367) | getPairOrNextHigher(key: K, compare: (a: K, b: K) => number, inclusive...
method checkValid (line 1378) | checkValid(depth: number, tree: BTree<K,V>, baseIndex: number, checkOr...
method set (line 1419) | set(key: K, value: V, overwrite: boolean|undefined, tree: BTree<K,V>):...
method insert (line 1473) | insert(i: index, child: BNode<K,V>) {
method splitOffRightSide (line 1483) | splitOffRightSide() {
method splitOffLeftSide (line 1498) | splitOffLeftSide() {
method takeFromRight (line 1509) | takeFromRight(rhs: BNode<K,V>) {
method takeFromLeft (line 1522) | takeFromLeft(lhs: BNode<K,V>) {
method forRange (line 1541) | forRange<R>(low: K, high: K, includeHigh: boolean|undefined, editMode:...
method tryMerge (line 1596) | tryMerge(i: index, maxSize: number): boolean {
method mergeSibling (line 1617) | mergeSibling(rhs: BNode<K,V>, maxNodeSize: number) {
function sumChildSizes (line 1658) | function sumChildSizes<K,V>(children: BNode<K,V>[]): number {
function areOverlapping (line 1669) | function areOverlapping<K,V>(
function check (line 1684) | function check(fact: boolean, ...args: any[]) {
FILE: benchmarks.ts
class Timer (line 18) | class Timer {
method ms (line 20) | ms() { return ((perfNow() - this.start) * 100 | 0) / 100; }
method restart (line 21) | restart() { var ms = this.ms(); this.start += ms; return ms; }
function fillBTreeOfSize (line 637) | function fillBTreeOfSize(size: number, first = 0, spacing?: number, valu...
type TwoTreeBenchmark (line 644) | type TwoTreeBenchmark = (baseTitle: string, tree1: BTreeEx<number, numbe...
function testNonOverlappingRanges (line 646) | function testNonOverlappingRanges(
function testMaybeOverlappingRanges (line 653) | function testMaybeOverlappingRanges(
function test50PercentOverlappingRanges (line 670) | function test50PercentOverlappingRanges(
function testCompleteOverlap (line 684) | function testCompleteOverlap(
function testPercentOverlap (line 699) | function testPercentOverlap(
function testPartialMiddleOverlap (line 713) | function testPartialMiddleOverlap(
function testRandomOverlaps (line 728) | function testRandomOverlaps(
function testLargeSparseOverlap (line 759) | function testLargeSparseOverlap(
function intersectBySorting (line 792) | function intersectBySorting(
function subtractBySorting (line 819) | function subtractBySorting(
function perfNow (line 846) | function perfNow(): number {
function randInt (line 850) | function randInt(max: number) { return Math.random() * max | 0; }
function swap (line 852) | function swap(keys: any[], i: number, j: number) {
function makeArray (line 865) | function makeArray(size: number, randomOrder: boolean, lowest = 0, spaci...
function measure (line 878) | function measure<T=void>(
FILE: extended/bulkLoad.js
function bulkLoad (line 37) | function bulkLoad(keys, values, maxNodeSize, compare, loadFactor) {
function bulkLoadRoot (line 50) | function bulkLoadRoot(keys, values, maxNodeSize, compare, loadFactor) {
FILE: extended/bulkLoad.ts
function bulkLoad (line 16) | function bulkLoad<K, V>(
function bulkLoadRoot (line 34) | function bulkLoadRoot<K, V>(
FILE: extended/decompose.js
function decompose (line 19) | function decompose(left, right, combineFn, ignoreRight) {
function buildFromDecomposition (line 314) | function buildFromDecomposition(constructor, branchingFactor, decomposed...
function processSide (line 370) | function processSide(heights, nodes, start, end, step, context) {
function splitUpwardsAndInsert (line 456) | function splitUpwardsAndInsert(context, insertionDepth, subtree) {
function splitUpwardsAndInsertEntries (line 516) | function splitUpwardsAndInsertEntries(context, insertionDepth, entryCont...
function ensureNotShared (line 540) | function ensureNotShared(context, isSharedFrontierDepth, depthToInclusiv...
function updateSizeAndMax (line 562) | function updateSizeAndMax(context, unflushedSizes, isSharedFrontierDepth...
function updateFrontier (line 587) | function updateFrontier(context, depthLastValid) {
function findSplitCascadeEndDepth (line 608) | function findSplitCascadeEndDepth(context, insertionDepth, insertionCoun...
function insertNoCount (line 628) | function insertNoCount(parent, index, child) {
function getLeftmostIndex (line 633) | function getLeftmostIndex() {
function getRightmostIndex (line 636) | function getRightmostIndex(node) {
function getRightInsertionIndex (line 639) | function getRightInsertionIndex(node) {
function splitOffRightSide (line 642) | function splitOffRightSide(node) {
function splitOffLeftSide (line 645) | function splitOffLeftSide(node) {
function balanceLeavesRight (line 648) | function balanceLeavesRight(parent, underfilled, toTake) {
function balanceLeavesLeft (line 658) | function balanceLeavesLeft(parent, underfilled, toTake) {
function updateRightMax (line 666) | function updateRightMax(node, maxBelow) {
function mergeRightEntries (line 669) | function mergeRightEntries(leaf, entries) {
function mergeLeftEntries (line 673) | function mergeLeftEntries(leaf, entries) {
FILE: extended/decompose.ts
type DecomposeResult (line 11) | type DecomposeResult<K, V> = { heights: number[], nodes: BNode<K, V>[], ...
type DecomposePayload (line 16) | type DecomposePayload = { disqualified: boolean };
function decompose (line 31) | function decompose<K, V>(
function buildFromDecomposition (line 397) | function buildFromDecomposition<TBTree extends BTree<K, V>, K, V>(
function processSide (line 477) | function processSide<K, V>(
function splitUpwardsAndInsert (line 582) | function splitUpwardsAndInsert<K, V>(
function splitUpwardsAndInsertEntries (line 648) | function splitUpwardsAndInsertEntries<K, V>(
function ensureNotShared (line 680) | function ensureNotShared<K, V>(
function updateSizeAndMax (line 707) | function updateSizeAndMax<K, V>(
function updateFrontier (line 736) | function updateFrontier<K, V>(context: SideContext<K, V>, depthLastValid...
function findSplitCascadeEndDepth (line 757) | function findSplitCascadeEndDepth<K, V>(context: SideContext<K, V>, inse...
function insertNoCount (line 777) | function insertNoCount<K, V>(
type SideContext (line 786) | type SideContext<K, V> = {
function getLeftmostIndex (line 799) | function getLeftmostIndex<K, V>(): number {
function getRightmostIndex (line 803) | function getRightmostIndex<K, V>(node: BNodeInternal<K, V>): number {
function getRightInsertionIndex (line 807) | function getRightInsertionIndex<K, V>(node: BNodeInternal<K, V>): number {
function splitOffRightSide (line 811) | function splitOffRightSide<K, V>(node: BNodeInternal<K, V>): BNodeIntern...
function splitOffLeftSide (line 815) | function splitOffLeftSide<K, V>(node: BNodeInternal<K, V>): BNodeInterna...
function balanceLeavesRight (line 819) | function balanceLeavesRight<K,V>(parent: BNodeInternal<K,V>, underfilled...
function balanceLeavesLeft (line 830) | function balanceLeavesLeft<K,V>(parent: BNodeInternal<K,V>, underfilled:...
function updateRightMax (line 839) | function updateRightMax<K, V>(node: BNodeInternal<K, V>, maxBelow: K): v...
function mergeRightEntries (line 843) | function mergeRightEntries<K, V>(leaf: BNode<K, V>, entries: BNode<K, V>...
function mergeLeftEntries (line 848) | function mergeLeftEntries<K, V>(leaf: BNode<K, V>, entries: BNode<K, V>)...
FILE: extended/diffAgainst.js
function diffAgainst (line 19) | function diffAgainst(_treeA, _treeB, onlyA, onlyB, different) {
function finishCursorWalk (line 134) | function finishCursorWalk(cursor, cursorFinished, compareKeys, callback) {
function stepToEnd (line 148) | function stepToEnd(cursor, callback) {
function makeDiffCursor (line 162) | function makeDiffCursor(internal) {
function stepDiffCursor (line 176) | function stepDiffCursor(cursor, stepToNode) {
function compareDiffCursors (line 238) | function compareDiffCursors(cursorA, cursorB, compareKeys) {
FILE: extended/diffAgainst.ts
function diffAgainst (line 20) | function diffAgainst<K, V, R>(
function finishCursorWalk (line 140) | function finishCursorWalk<K, V, R>(
function stepToEnd (line 159) | function stepToEnd<K, V, R>(
function makeDiffCursor (line 177) | function makeDiffCursor<K, V>(
function stepDiffCursor (line 194) | function stepDiffCursor<K, V>(cursor: DiffCursor<K, V>, stepToNode?: boo...
function compareDiffCursors (line 254) | function compareDiffCursors<K, V>(
type DiffCursor (line 290) | type DiffCursor<K, V> = {
FILE: extended/forEachKeyInBoth.js
function forEachKeyInBoth (line 20) | function forEachKeyInBoth(treeA, treeB, callback) {
FILE: extended/forEachKeyInBoth.ts
function forEachKeyInBoth (line 20) | function forEachKeyInBoth<K, V, R = void>(
FILE: extended/forEachKeyNotIn.js
function forEachKeyNotIn (line 19) | function forEachKeyNotIn(includeTree, excludeTree, callback) {
FILE: extended/forEachKeyNotIn.ts
function forEachKeyNotIn (line 19) | function forEachKeyNotIn<K, V, R = void>(
FILE: extended/index.d.ts
class BTreeEx (line 9) | class BTreeEx<K = any, V = any> extends BTree<K, V> {
type BTreeEx (line 121) | interface BTreeEx<K = any, V = any> {
FILE: extended/index.js
function __ (line 13) | function __() { this.constructor = d; }
function BTreeEx (line 58) | function BTreeEx() {
FILE: extended/index.ts
class BTreeEx (line 18) | class BTreeEx<K = any, V = any> extends BTree<K, V> {
method bulkLoad (line 30) | static bulkLoad<K, V>(
method clone (line 45) | clone(): this {
method greedyClone (line 55) | greedyClone(force?: boolean): this {
method diffAgainst (line 77) | diffAgainst<R>(
method forEachKeyInBoth (line 99) | forEachKeyInBoth<R = void>(
method forEachKeyNotIn (line 119) | forEachKeyNotIn<R = void>(
method intersect (line 139) | intersect(other: BTreeEx<K, V>, combineFn: (key: K, leftValue: V, righ...
method union (line 155) | union(other: BTreeEx<K, V>, combineFn: (key: K, leftValue: V, rightVal...
method subtract (line 172) | subtract(other: BTreeEx<K, V>): BTreeEx<K, V> {
type BTreeEx (line 177) | interface BTreeEx<K = any, V = any> {
method bulkLoad (line 30) | static bulkLoad<K, V>(
method clone (line 45) | clone(): this {
method greedyClone (line 55) | greedyClone(force?: boolean): this {
method diffAgainst (line 77) | diffAgainst<R>(
method forEachKeyInBoth (line 99) | forEachKeyInBoth<R = void>(
method forEachKeyNotIn (line 119) | forEachKeyNotIn<R = void>(
method intersect (line 139) | intersect(other: BTreeEx<K, V>, combineFn: (key: K, leftValue: V, righ...
method union (line 155) | union(other: BTreeEx<K, V>, combineFn: (key: K, leftValue: V, rightVal...
method subtract (line 172) | subtract(other: BTreeEx<K, V>): BTreeEx<K, V> {
FILE: extended/intersect.js
function intersect (line 23) | function intersect(treeA, treeB, combineFn) {
FILE: extended/intersect.ts
function intersect (line 20) | function intersect<TBTree extends BTree<K, V>, K, V>(
FILE: extended/parallelWalk.js
function moveForwardOne (line 10) | function moveForwardOne(cur, other) {
function createCursor (line 28) | function createCursor(tree, makePayload, onEnterLeaf, onMoveInLeaf, onEx...
function getKey (line 57) | function getKey(c) {
function moveTo (line 67) | function moveTo(cur, other, targetKey, isInclusive, startedEqual) {
function noop (line 187) | function noop() { }
FILE: extended/parallelWalk.ts
type Cursor (line 8) | interface Cursor<K, V, TPayload> {
function moveForwardOne (line 28) | function moveForwardOne<K, V, TP>(
function createCursor (line 50) | function createCursor<K, V, TP>(
function getKey (line 79) | function getKey<K, V, TP>(c: Cursor<K, V, TP>): K {
function moveTo (line 89) | function moveTo<K, V, TP>(
function noop (line 221) | function noop(): void { }
FILE: extended/shared.js
function makeLeavesFrom (line 17) | function makeLeavesFrom(keys, values, maxNodeSize, loadFactor, onLeafCre...
function checkCanDoSetOperation (line 56) | function checkCanDoSetOperation(treeA, treeB, supportsDifferentBranching...
FILE: extended/shared.ts
type BTreeWithInternals (line 7) | type BTreeWithInternals<K, V, TBTree extends BTree<K, V> = BTree<K, V>> = {
function makeLeavesFrom (line 25) | function makeLeavesFrom<K, V>(
function checkCanDoSetOperation (line 74) | function checkCanDoSetOperation<K, V>(treeA: BTreeWithInternals<K, V>, t...
type BTreeConstructor (line 88) | type BTreeConstructor<TBTree extends BTree<K, V>, K, V> = new (entries?:...
FILE: extended/subtract.js
function subtract (line 19) | function subtract(targetTree, subtractTree) {
FILE: extended/subtract.ts
function subtract (line 19) | function subtract<TBTree extends BTree<K, V>, K, V>(
FILE: extended/union.js
function union (line 19) | function union(treeA, treeB, combineFn) {
FILE: extended/union.ts
function union (line 19) | function union<TBTree extends BTree<K, V>, K, V>(
FILE: interfaces.d.ts
type ISetSource (line 8) | interface ISetSource<K=any>
type IMapSource (line 19) | interface IMapSource<K=any, V=any> extends ISetSource<K>
type ISetSink (line 45) | interface ISetSink<K=any>
type IMapSink (line 57) | interface IMapSink<K=any, V=any>
type ISet (line 72) | interface ISet<K=any> extends ISetSource<K>, ISetSink<K> { }
type IMap (line 77) | interface IMap<K=any, V=any> extends IMapSource<K, V>, IMapSink<K, V> { }
type ISortedSetSource (line 81) | interface ISortedSetSource<K=any> extends ISetSource<K>
type ISortedMapSource (line 112) | interface ISortedMapSource<K=any, V=any> extends IMapSource<K, V>, ISort...
type ISortedSet (line 162) | interface ISortedSet<K=any> extends ISortedSetSource<K>, ISetSink<K> { }
type ISortedMap (line 166) | interface ISortedMap<K=any, V=any> extends IMap<K,V>, ISortedMapSource<K...
type ISetF (line 208) | interface ISetF<K=any> extends ISetSource<K> {
type IMapF (line 240) | interface IMapF<K=any, V=any> extends IMapSource<K, V>, ISetF<K> {
type ISortedSetF (line 274) | interface ISortedSetF<K=any> extends ISetF<K>, ISortedSetSource<K>
type ISortedMapF (line 280) | interface ISortedMapF<K=any,V=any> extends ISortedSetF<K>, IMapF<K,V>, I...
type ISortedMapConstructor (line 302) | interface ISortedMapConstructor<K,V> {
type ISortedMapFConstructor (line 305) | interface ISortedMapFConstructor<K,V> {
FILE: scripts/minify.js
function extendedTargets (line 8) | function extendedTargets() {
FILE: scripts/size-report.js
function discoverExtendedEntries (line 11) | function discoverExtendedEntries() {
function fileSize (line 34) | function fileSize(relativePath) {
function gzipSize (line 45) | function gzipSize(relativePath) {
function formatBytes (line 57) | function formatBytes(bytes) {
function pad (line 65) | function pad(str, length) {
FILE: sorted-array.d.ts
class SortedArray (line 3) | class SortedArray<K = any, V = any> implements IMap<K, V> {
FILE: sorted-array.js
function SortedArray (line 5) | function SortedArray(entries, compare) {
FILE: sorted-array.ts
class SortedArray (line 4) | class SortedArray<K=any, V=any> implements IMap<K,V>
method constructor (line 9) | public constructor(entries?: [K,V][], compare?: (a: K, b: K) => number) {
method size (line 17) | get size() { return this.a.length; }
method get (line 18) | get(key: K, defaultValue?: V): V | undefined {
method set (line 22) | set(key: K, value: V, overwrite?: boolean): boolean {
method has (line 30) | has(key: K): boolean {
method delete (line 33) | delete(key: K): boolean {
method clear (line 39) | clear() { this.a = []; }
method getArray (line 40) | getArray() { return this.a; }
method minKey (line 41) | minKey(): K | undefined { return this.a[0][0]; }
method maxKey (line 42) | maxKey(): K | undefined { return this.a[this.a.length-1][0]; }
method forEach (line 43) | forEach(callbackFn: (v:V, k:K, list:SortedArray<K,V>) => void) {
method entries (line 49) | entries(): IterableIterator<[K,V]> { return this.a.values(); }
method keys (line 50) | keys(): IterableIterator<K> { return this.a.map(pair => pair[0]).va...
method values (line 51) | values(): IterableIterator<V> { return this.a.map(pair => pair[1]).va...
method indexOf (line 53) | indexOf(key: K, failXor: number): number {
method [Symbol.iterator] (line 48) | [Symbol.iterator](): IterableIterator<[K,V]> { return this.a.values(); }
FILE: test/b+tree.test.ts
function testComparison (line 94) | function testComparison<T>(comparison: (a: T, b: T) => number, inOrder: ...
function buildTestTree (line 220) | function buildTestTree(entryCount: number, maxNodeSize: number) {
function expectSize (line 228) | function expectSize(tree: BTree<number, number>, size: number) {
function insert8 (line 325) | function insert8(maxNodeSize: number) {
function forExpector (line 334) | function forExpector(k:number, v:string, counter:number, i:number, first...
function testBTree (line 634) | function testBTree(maxNodeSize: number)
FILE: test/bulkLoad.test.ts
type Pair (line 7) | type Pair = [number, number];
function sequentialPairs (line 11) | function sequentialPairs(count: number, start = 0, step = 1): Pair[] {
function pairsFromKeys (line 21) | function pairsFromKeys(keys: number[]): Pair[] {
function toParallelArrays (line 25) | function toParallelArrays(pairs: Pair[]): { keys: number[]; values: numb...
function buildTreeFromPairs (line 36) | function buildTreeFromPairs(maxNodeSize: number, pairs: Pair[], loadFact...
function expectTreeMatches (line 43) | function expectTreeMatches(tree: BTree<number, number>, expected: Pair[]) {
function collectLeaves (line 49) | function collectLeaves(node: BNode<number, number>): BNode<number, numbe...
function assertInternalNodeFanout (line 59) | function assertInternalNodeFanout(node: BNode<number, number>, maxNodeSi...
FILE: test/diffAgainst.test.ts
constant FANOUTS (line 7) | const FANOUTS = [32, 10, 4] as const;
function runDiffAgainstSuite (line 15) | function runDiffAgainstSuite(maxNodeSize: number): void {
FILE: test/intersect.test.ts
type SharedCall (line 13) | type SharedCall = { key: number, leftValue: number, rightValue: number };
FILE: test/shared.d.ts
type TreeNodeStats (line 6) | type TreeNodeStats = {
type TreeEntries (line 12) | type TreeEntries = Array<[number, number]>;
type SetOperationFuzzSettings (line 13) | type SetOperationFuzzSettings = {
type FuzzCase (line 19) | type FuzzCase = {
type FuzzTreeSpec (line 36) | type FuzzTreeSpec = {
type PopulateFuzzTreesOptions (line 41) | type PopulateFuzzTreesOptions = {
FILE: test/shared.js
function countTreeNodeStats (line 12) | function countTreeNodeStats(tree) {
function logTreeNodeStats (line 52) | function logTreeNodeStats(prefix, stats) {
function randInt (line 60) | function randInt(max) {
function expectTreeEqualTo (line 64) | function expectTreeEqualTo(tree, list) {
function addToBoth (line 69) | function addToBoth(a, b, k, v) {
function makeArray (line 73) | function makeArray(size, randomOrder, spacing, rng) {
function swap (line 106) | function swap(keys, i, j) {
function buildEntriesFromMap (line 111) | function buildEntriesFromMap(entriesMap, compareFn) {
function populateFuzzTrees (line 118) | function populateFuzzTrees(specs, _a) {
function applyRemovalRunsToTree (line 158) | function applyRemovalRunsToTree(tree, entries, removalChance, branchingF...
function expectTreeMatchesEntries (line 184) | function expectTreeMatchesEntries(tree, entries) {
function validateFuzzSettings (line 193) | function validateFuzzSettings(settings) {
function forEachFuzzCase (line 203) | function forEachFuzzCase(settings, callback) {
FILE: test/shared.ts
type TreeNodeStats (line 10) | type TreeNodeStats = {
type TreeEntries (line 17) | type TreeEntries = Array<[number, number]>;
type SetOperationFuzzSettings (line 19) | type SetOperationFuzzSettings = {
type FuzzCase (line 26) | type FuzzCase = {
function countTreeNodeStats (line 36) | function countTreeNodeStats<K, V>(tree: BTree<K, V>): TreeNodeStats {
function logTreeNodeStats (line 85) | function logTreeNodeStats(prefix: string, stats: BTreeEx|TreeNodeStats):...
function randInt (line 94) | function randInt(max: number): number {
function expectTreeEqualTo (line 98) | function expectTreeEqualTo<K, V>(tree: BTree<K, V>, list: SortedArray<K,...
function addToBoth (line 103) | function addToBoth<K, V>(a: IMap<K, V>, b: IMap<K, V>, k: K, v: V): void {
function makeArray (line 107) | function makeArray(
function swap (line 146) | function swap(keys: any[], i: number, j: number) {
function buildEntriesFromMap (line 152) | function buildEntriesFromMap(
type FuzzTreeSpec (line 161) | type FuzzTreeSpec = {
type PopulateFuzzTreesOptions (line 167) | type PopulateFuzzTreesOptions = {
function populateFuzzTrees (line 175) | function populateFuzzTrees(
function applyRemovalRunsToTree (line 220) | function applyRemovalRunsToTree(
function expectTreeMatchesEntries (line 251) | function expectTreeMatchesEntries(tree: BTree<number, number>, entries: ...
function validateFuzzSettings (line 260) | function validateFuzzSettings(settings: SetOperationFuzzSettings): void {
function forEachFuzzCase (line 271) | function forEachFuzzCase(
FILE: test/subtract.test.ts
type NotInCall (line 14) | type NotInCall = { key: number, value: number };
FILE: test/union.test.ts
type UnionFn (line 16) | type UnionFn = (key: number, leftValue: number, rightValue: number) => n...
type UnionExpectationOptions (line 52) | type UnionExpectationOptions = {
Condensed preview — 59 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (628K chars).
[
{
"path": ".gitignore",
"chars": 879,
"preview": "# Generated files\n*.js.map\n*.bundle.js\n*.out.js\n*.min.js\n.testpack/\n\n# Misc\nbenchmarks.js\nbenchmarks.d.ts\ninterfaces.js\n"
},
{
"path": ".vscode/launch.json",
"chars": 1615,
"preview": "{\n // Use IntelliSense to learn about possible attributes.\n // Hover to view descriptions of existing attributes.\n"
},
{
"path": "LICENSE",
"chars": 1072,
"preview": "MIT License\n\nCopyright (c) 2018 David Piepgrass\n\nPermission is hereby granted, free of charge, to any person obtaining a"
},
{
"path": "b+tree.d.ts",
"chars": 25051,
"preview": "import { ISortedMap, ISortedMapF, ISortedSet } from './interfaces';\nexport { ISetSource, ISetSink, ISet, ISetF, ISortedS"
},
{
"path": "b+tree.js",
"chars": 72525,
"preview": "\"use strict\";\nvar __extends = (this && this.__extends) || (function () {\n var extendStatics = function (d, b) {\n "
},
{
"path": "b+tree.ts",
"chars": 71357,
"preview": "// B+ tree by David Piepgrass. License: MIT\nimport { ISortedMap, ISortedMapF, ISortedSet } from './interfaces';\n\nexport "
},
{
"path": "benchmarks.ts",
"chars": 30260,
"preview": "#!/usr/bin/env ts-node\nimport BTree from '.';\nimport BTreeEx from './extended';\nimport SortedArray from './sorted-array'"
},
{
"path": "extended/bulkLoad.d.ts",
"chars": 933,
"preview": "import BTree from '../b+tree';\n/**\n * Loads a B-Tree from a sorted list of entries in bulk. This is faster than insertin"
},
{
"path": "extended/bulkLoad.js",
"chars": 5082,
"preview": "\"use strict\";\nvar __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {\n if ("
},
{
"path": "extended/bulkLoad.ts",
"chars": 4039,
"preview": "import BTree, { BNode, BNodeInternal, check, fixMaxSize, sumChildSizes } from '../b+tree';\nimport { makeLeavesFrom as ma"
},
{
"path": "extended/decompose.d.ts",
"chars": 11,
"preview": "export {};\n"
},
{
"path": "extended/decompose.js",
"chars": 35148,
"preview": "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.buildFromDecomposition = exports.de"
},
{
"path": "extended/decompose.ts",
"chars": 35807,
"preview": "import BTree, { areOverlapping, BNode, BNodeInternal, check } from '../b+tree';\nimport { BTreeConstructor, makeLeavesFro"
},
{
"path": "extended/diffAgainst.d.ts",
"chars": 1384,
"preview": "import BTree from '../b+tree';\n/**\n * Computes the differences between `treeA` and `treeB`.\n * For efficiency, the diff "
},
{
"path": "extended/diffAgainst.js",
"chars": 13058,
"preview": "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nvar b_tree_1 = require(\"../b+tree\");\n/**\n *"
},
{
"path": "extended/diffAgainst.ts",
"chars": 13661,
"preview": "import BTree from '../b+tree';\nimport { BNode, BNodeInternal, check } from '../b+tree';\nimport { type BTreeWithInternals"
},
{
"path": "extended/forEachKeyInBoth.d.ts",
"chars": 1144,
"preview": "import BTree from '../b+tree';\n/**\n * Calls the supplied `callback` for each key/value pair shared by both trees, in sor"
},
{
"path": "extended/forEachKeyInBoth.js",
"chars": 3674,
"preview": "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nvar shared_1 = require(\"./shared\");\nvar par"
},
{
"path": "extended/forEachKeyInBoth.ts",
"chars": 3270,
"preview": "import BTree from '../b+tree';\nimport { type BTreeWithInternals, checkCanDoSetOperation } from './shared';\nimport { crea"
},
{
"path": "extended/forEachKeyNotIn.d.ts",
"chars": 1284,
"preview": "import BTree from '../b+tree';\n/**\n * Calls the supplied `callback` for each key/value pair that is in `includeTree` but"
},
{
"path": "extended/forEachKeyNotIn.js",
"chars": 4357,
"preview": "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nvar shared_1 = require(\"./shared\");\nvar par"
},
{
"path": "extended/forEachKeyNotIn.ts",
"chars": 3784,
"preview": "import BTree from '../b+tree';\nimport { type BTreeWithInternals, checkCanDoSetOperation } from './shared';\nimport { crea"
},
{
"path": "extended/index.d.ts",
"chars": 8531,
"preview": "import BTree from '../b+tree';\n/**\n * An extended version of the `BTree` class that includes additional functionality\n *"
},
{
"path": "extended/index.js",
"chars": 11172,
"preview": "\"use strict\";\nvar __extends = (this && this.__extends) || (function () {\n var extendStatics = function (d, b) {\n "
},
{
"path": "extended/index.ts",
"chars": 9999,
"preview": "import BTree, { defaultComparator } from '../b+tree';\nimport type { BTreeWithInternals } from './shared';\nimport diffAga"
},
{
"path": "extended/intersect.d.ts",
"chars": 981,
"preview": "import BTree from '../b+tree';\n/**\n * Returns a new tree containing only keys present in both input trees.\n * Neither tr"
},
{
"path": "extended/intersect.js",
"chars": 2142,
"preview": "\"use strict\";\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n return (mod && mod.__esModule"
},
{
"path": "extended/intersect.ts",
"chars": 2155,
"preview": "import BTree from '../b+tree';\nimport { checkCanDoSetOperation, type BTreeWithInternals, BTreeConstructor } from './shar"
},
{
"path": "extended/parallelWalk.d.ts",
"chars": 11,
"preview": "export {};\n"
},
{
"path": "extended/parallelWalk.js",
"chars": 6749,
"preview": "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.noop = exports.moveTo = exports.get"
},
{
"path": "extended/parallelWalk.ts",
"chars": 7799,
"preview": "import { BNode, BNodeInternal } from '../b+tree';\nimport type { BTreeWithInternals } from './shared';\n\n/**\n * A walkable"
},
{
"path": "extended/shared.d.ts",
"chars": 11,
"preview": "export {};\n"
},
{
"path": "extended/shared.js",
"chars": 2972,
"preview": "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.checkCanDoSetOperation = exports.br"
},
{
"path": "extended/shared.ts",
"chars": 3386,
"preview": "import BTree, { BNode } from '../b+tree';\n\n/**\n * BTree with access to internal properties.\n * @internal\n */\nexport type"
},
{
"path": "extended/subtract.d.ts",
"chars": 1102,
"preview": "import BTree from '../b+tree';\n/**\n * Returns a new tree containing only the keys that are present in `targetTree` but n"
},
{
"path": "extended/subtract.js",
"chars": 2161,
"preview": "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nvar shared_1 = require(\"./shared\");\nvar dec"
},
{
"path": "extended/subtract.ts",
"chars": 2294,
"preview": "import BTree from '../b+tree';\nimport { checkCanDoSetOperation, type BTreeWithInternals, BTreeConstructor } from './shar"
},
{
"path": "extended/union.d.ts",
"chars": 1143,
"preview": "import BTree from '../b+tree';\n/**\n * Efficiently unions two trees, reusing subtrees wherever possible without mutating "
},
{
"path": "extended/union.js",
"chars": 1962,
"preview": "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nvar shared_1 = require(\"./shared\");\nvar dec"
},
{
"path": "extended/union.ts",
"chars": 2149,
"preview": "import BTree from '../b+tree';\nimport { BTreeConstructor, type BTreeWithInternals, checkCanDoSetOperation } from './shar"
},
{
"path": "interfaces.d.ts",
"chars": 17557,
"preview": "\n/** Read-only set interface (subinterface of IMapSource<K,any>).\n * The word \"set\" usually means that each item in the"
},
{
"path": "package.json",
"chars": 3250,
"preview": "{\n \"name\": \"sorted-btree\",\n \"version\": \"2.1.1\",\n \"description\": \"A sorted list of key-value pairs in a fast, typed in"
},
{
"path": "readme.md",
"chars": 53707,
"preview": "B+ tree\n=======\n\nB+ trees are ordered collections of key-value pairs, sorted by key.\n\nThis is a fast B+ tree implementat"
},
{
"path": "scripts/minify.js",
"chars": 1219,
"preview": "const fs = require('fs');\nconst path = require('path');\nconst UglifyJS = require('uglify-js');\n\nconst rootDir = path.res"
},
{
"path": "scripts/size-report.js",
"chars": 3218,
"preview": "const fs = require('fs');\nconst path = require('path');\nconst zlib = require('zlib');\n//\n// This script prints out a siz"
},
{
"path": "sorted-array.d.ts",
"chars": 868,
"preview": "import { IMap } from './interfaces';\n/** A super-inefficient sorted list for testing purposes */\nexport default class So"
},
{
"path": "sorted-array.js",
"chars": 3013,
"preview": "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\n/** A super-inefficient sorted list for tes"
},
{
"path": "sorted-array.ts",
"chars": 2241,
"preview": "import {IMap} from './interfaces';\n\n/** A super-inefficient sorted list for testing purposes */\nexport default class Sor"
},
{
"path": "test/b+tree.test.ts",
"chars": 34084,
"preview": "import BTree, { IMap, defaultComparator, simpleComparator, areOverlapping } from '../b+tree';\nimport BTreeEx from '../ex"
},
{
"path": "test/bulkLoad.test.ts",
"chars": 9410,
"preview": "import BTree, { BNode, BNodeInternal } from '../b+tree';\nimport BTreeEx from '../extended';\nimport { bulkLoad } from '.."
},
{
"path": "test/diffAgainst.test.ts",
"chars": 8045,
"preview": "import BTree from '../b+tree';\nimport BTreeEx from '../extended';\nimport diffAgainst from '../extended/diffAgainst';\n\nva"
},
{
"path": "test/intersect.test.ts",
"chars": 11174,
"preview": "import BTreeEx from '../extended';\nimport intersect from '../extended/intersect';\nimport { comparatorErrorMsg } from '.."
},
{
"path": "test/setOperationFuzz.test.ts",
"chars": 5731,
"preview": "import BTreeEx from '../extended';\nimport MersenneTwister from 'mersenne-twister';\nimport {\n expectTreeMatchesEntries,\n"
},
{
"path": "test/shared.d.ts",
"chars": 2469,
"preview": "import BTree, { IMap } from '../b+tree';\nimport SortedArray from '../sorted-array';\nimport MersenneTwister from 'mersenn"
},
{
"path": "test/shared.js",
"chars": 9225,
"preview": "\"use strict\";\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n return (mod && mod.__esModule"
},
{
"path": "test/shared.ts",
"chars": 8514,
"preview": "import BTree, { BNode, BNodeInternal, IMap } from '../b+tree';\nimport SortedArray from '../sorted-array';\nimport Mersenn"
},
{
"path": "test/subtract.test.ts",
"chars": 11877,
"preview": "import BTreeEx from '../extended';\nimport forEachKeyNotIn from '../extended/forEachKeyNotIn';\nimport subtract from '../e"
},
{
"path": "test/union.test.ts",
"chars": 26626,
"preview": "import BTree from '../b+tree';\nimport BTreeEx from '../extended';\nimport union from '../extended/union';\nimport { branch"
},
{
"path": "tsconfig.json",
"chars": 1233,
"preview": "{ // TypeScript configuration file: provides options to the TypeScript \n // compiler (tsc) and makes VSCode recognize t"
}
]
About this extraction
This page contains the full source code of the qwertie/btree-typescript GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 59 files (595.3 KB), approximately 159.1k tokens, and a symbol index with 346 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.