UNPKG

stem-core

Version:

Frontend and core-library framework

540 lines (474 loc) 14.4 kB
import {mapIterator} from "../base/Utils"; // Data structure to keep a set of objects/values sorted by a given criteria // Can support inserting a value (with an optional key), getting a range // Can be used for instance to keep a rolling view on a table while scrolling // TODO: should probably also support keeping track of a range with dispatchers? class BinarySearchTreeNode { constructor(key, parent=null) { this.weight = Math.random(); this.parent = parent; this.left = this.right = null; this.key = key; } leftWeight() { return (this.left && this.left.weight) || -1; } rightWeight() { return (this.right && this.right.weight) || -1; } min() { return (this.left && this.left.min()) || this; } max() { return (this.right && this.right.max()) || this; } replaceParentRef(newNode) { if (!this.parent) { return; } if (this.parent.left === this) { this.parent.left = newNode; } if (this.parent.right === this) { this.parent.right = newNode; } } rotateLeft() { const a = this, b = this.right; const c = b.left; a.replaceParentRef(b); b.left = a; b.parent = a.parent; a.parent = b; a.right = c; if (c) { c.parent = a; } return b; } rotateRight() { const a = this.left, b = this; const c = a.right; b.replaceParentRef(a); a.right = b; a.parent = b.parent; b.parent = a; b.left = c; if (c) { c.parent = b; } return a; } balance(nodeToReturn) { let newRoot = this; if (this.rightWeight() > this.weight) { newRoot = this.rotateLeft(); } else if (this.leftWeight() > this.weight) { newRoot = this.rotateRight(); } newRoot.update(); return [nodeToReturn, newRoot]; } add(value, key, comparator) { const comp = comparator(key, this.key); let addedNode, newRoot; if (comp < 0) { if (this.left == null) { this.left = new this.constructor(value, key, this); addedNode = this.left; } else { [addedNode, newRoot] = this.left.add(value, key, comparator); this.left = newRoot; } } else if (comp > 0) { if (this.right == null) { this.right = new this.constructor(value, key, this); addedNode = this.right; } else { [addedNode, newRoot] = this.right.add(value, key, comparator); this.right = newRoot; } } return this.balance(addedNode); } delete(value, key, comparator) { const comp = comparator(key, this.key); let removedNode, newRoot; if (comp === 0) { if (!this.left && !this.right) { return [this, undefined]; } if (this.leftWeight() > this.rightWeight()) { return this.rotateRight().delete(value, key, comparator); } else { return this.rotateLeft().delete(value, key, comparator); } } if (comp === -1) { [removedNode, newRoot] = this.left.delete(value, key, comparator); this.left = newRoot; } else if (comp === 1) { [removedNode, newRoot] = this.right.delete(value, key, comparator); this.right = newRoot; } return this.balance(removedNode); } next() { if (this.right) { let node = this.right; while (node.left) { node = node.left; } return node; } let node = this; while (node.parent && node.parent.right === node) { node = node.parent; } return node.parent; } update() { // This method is called whenever a change that occurs in the tree influences this node. // This is the only method from this class that should be overwritten. } } class SortedSetNode extends BinarySearchTreeNode { constructor(value, key, parent=null) { super(key, parent); this.value = value; this.size = 1; } leftSize() { return (this.left && this.left.size) || 0; } rightSize() { return (this.right && this.right.size) || 0; } recalcSize() { this.size = this.leftSize() + this.rightSize() + 1; } update() { this.left && this.left.recalcSize(); this.right && this.right.recalcSize(); this.recalcSize(); } getIndex(value, key, comparator) { const comp = comparator(key, this.key); if (comp === 0) { return this.leftSize(); } if (comp === -1) { return this.left.getIndex(value, key, comparator); } return this.leftSize() + 1 + this.right.getIndex(value, key, comparator); } get(index) { const leftSize = this.leftSize(); if (leftSize === index) { return this.value; } if (leftSize > index) { return this.left.get(index); } return this.right.get(index - leftSize - 1); } toJSON() { let json = { value: this.value, key: this.key, weight: this.weight, size: this.size }; if (this.left) { json.left = this.left.toJSON(); } if (this.right) { json.right = this.right.toJSON(); } return json; } } export class SortedSet { constructor(values=[], options={}) { if (!Array.isArray(values)) { options = values; values = []; } this.comparator = options.cmp || options.comparator || this.constructor.defaultComparator; this.init(values); } init(values) { // Map to store the link between an object and their node in the tree. this.nodeMap = new Map(); this.rootNode = null; for (let value of values) { let key; if (Array.isArray(value)) { key = value[1]; value = value[0]; } else { key = value; } this.add(value, key); } } getNodeByIndex(index) { return this.nodeMap.get(this.get(index)); } getNodeByValue(value) { return this.nodeMap.get(value); } setComparator(cmp) { this.comparator = cmp; this.init(this.toArray()); } static defaultComparator(a, b) { if (a == b) { return 0; } return (a < b) ? -1 : 1; } add(value, key=value) { if (this.has(value)) { return null; } if (!this.rootNode) { this.rootNode = new SortedSetNode(value, key); this.nodeMap.set(value, this.rootNode); return this.rootNode; } const [node, newRoot] = this.rootNode.add(value, key, this.comparator); this.nodeMap.set(value, node); this.rootNode = newRoot; return node; } has(value) { return this.nodeMap.has(value); } size() { if (!this.rootNode) { return 0; } return this.rootNode.size; } // Remove the passed value from the SortedSet delete(value, key=value) { if (!this.has(value)) { return null; } const [node, newRoot] = this.rootNode.delete(value, key, this.comparator); this.rootNode = newRoot; this.nodeMap.delete(value); return node; } clear() { this.init([]); } getIndex(value, key=value) { if (!this.has(value)) { return -1; } return this.rootNode.getIndex(value, key, this.comparator); } // Return the index-th value in order by priority get(index) { if (this.size() < index) { return null; } return this.rootNode.get(index); } min() { if (!this.size()) { return null; } return this.rootNode.min().value; } max() { if (!this.size()) { return null; } return this.rootNode.max().value; } // Return iterator over [key, value] * entries(startIndex=0, endIndex=this.size()) { if (startIndex < endIndex) { let node = this.getNodeByIndex(startIndex); let position = startIndex; while (node && position < endIndex) { yield [node.key, node.value]; node = node.next(); position += 1; } } } values(startIndex=0, endIndex=this.size()) { return mapIterator(this.entries(startIndex, endIndex), it => it[1]); } toArray(startIndex=0, endIndex=this.size()) { return Array.from(this.values(startIndex, endIndex)); } toJSON() { if (!this.rootNode) { return {}; } return this.rootNode.toJSON(); } toString() { return this.toArray().toString(); } } SortedSet.prototype.remove = SortedSet.prototype.delete; SortedSet.prototype[Symbol.iterator] = SortedSet.prototype.values; /* import {suffixWithOrdinal} from "../base/Utils"; export class SortedSetUnitTests { static tests = [ "emptySet", "add10Delete1", "add1Delete1", "add10Clear", "indexQuery", "valueQuery" ]; static int() { return parseInt(Math.random() * 10000000); } static checkSanity(set) { const dfs = (node) => { if (!node) { return true; } if ((node.left && set.comparator(node.key, node.left.key) === -1) || (node.right && set.comparator(node.key, node.right.key) === 1)) { console.error("BST property not maintained for node", node); return false; } if (node.weight < node.leftWeight() || node.weight < node.rightWeight()) { console.error("Heap property not maintained for node", node); return false; } if (node.size !== node.leftSize() + node.rightSize() + 1) { console.error("Size not maintained for node", node); return false; } return (!node.left || dfs(node.left)) && (!node.right || dfs(node.right)); }; return dfs(set.rootNode); } static emptySet() { let set = new SortedSet(); return this.checkSanity(set); } static add10Delete1() { let set = new SortedSet(); let ok = true; for (let i = 0; i < 100; i += 1) { let x; for (let j = 0; j < 10; j += 1) { if (j === 4) { x = this.int(); set.add(x); } else { set.add(this.int()); } ok = ok && this.checkSanity(set); } set.delete(x); ok = ok && this.checkSanity(set); } return ok; } static add1Delete1() { let set = new SortedSet(); let ok = true; for (let i = 0; i < 100; i += 1) { let x = this.int(); set.add(x); ok = ok && this.checkSanity(set); set.delete(x); ok = ok && this.checkSanity(set); } return ok; } static add10Clear() { let set = new SortedSet(); let ok = true; for (let i = 0; i < 100; i += 1) { for (let j = 0; j < 10; j += 1) { set.add(this.int()); ok = ok && this.checkSanity(set); } set.clear(); ok = ok && this.checkSanity(set); } return ok; } static indexQuery() { let set = new SortedSet(); let ok = true; let n = 500; let a = []; for (let i = 0; i < n; i += 1) { a.push(this.int()); set.add(a[a.length - 1]); } a.sort(); for (let i = 0; i < 100; i += 1) { let index = parseInt(Math.random() * n); if (set.get(index) !== a[index]) { console.error("Wrong index query", index, set); ok = false; break; } } return ok; } static valueQuery() { let set = new SortedSet(); let ok = true; let n = 500; let a = []; for (let i = 0; i < n; i += 1) { a.push(this.int()); set.add(a[a.length - 1]); } a.sort(); for (let i = 0; i < 100; i += 1) { let index = parseInt(Math.random() * n); if (set.getIndex(a[index]) !== index) { console.error("Wrong value query", index, set); ok = false; break; } } return ok; } static runAllTests(numRuns=100) { let ok = true; for (let i = 1; i <= numRuns; i += 1) { console.log("Running tests " + suffixWithOrdinal(i) + " time out of " + numRuns); for (let test of this.tests) { ok = ok && this[test](); } } if (ok) { console.log("Successfully ran all tests " + numRuns + " times."); } } } export class SortedSetProfiler { static NUM_OPERATIONS = 100000; static runProfiler(steps=this.NUM_OPERATIONS) { let s = new SortedSet(); let startTime = performance.now(); let existing = 0; for (let j = 0; j < 10; j += 1) { startTime = performance.now(); for (let i = 0; i < steps; i += 1) { s.add(Math.random()); } console.log("Added", steps, "values to set already containing", existing, "values in", performance.now() - startTime, "ms"); existing += steps; } } } */