UNPKG

pm4js

Version:

Process Mining for Javascript

1,253 lines (1,232 loc) 53.6 kB
// Implementation taken from: // https://github.com/pineapplemachine/interval-tree-type-js // Credit to these implementations for serving as a reference: // https://github.com/IvanPinezhaninov/IntervalTree // https://github.com/stanislavkozlovski/Red-Black-Tree const Red = true; const Black = false; const SameValueZero = (a, b) => a === b || (a !== a && b !== b); const IntervalComparator = (a, b) => b.high - a.high; class IntervalTree{ constructor(valuesEqual){ this.valuesEqual = valuesEqual || SameValueZero; this.root = null; if(typeof(this.valuesEqual) !== "function"){ throw new TypeError("Value equality argument must be a function."); } } // Helper to validate numeric inputs and throw helpful // errors when the inputs are invalid. static validate(point, description, nanok){ const value = point.valueOf(); if(typeof(value) !== "number"){ throw new TypeError(`${description} must be a number.`); } if(!nanok && value !== value){ throw new RangeError(`${description} must not be NaN.`); } return value; } // Returns true when the tree is empty and false otherwise. isEmpty(){ return !this.root; } // Get the total number of intervals in the tree. getIntervalCount(){ return this.root ? this.root.getIntervalCount() : 0; } // Insert a node into the tree associating a value with an interval. insert(low, high, value){ // Validate input interval low = IntervalTree.validate(low, "Low bound", false); high = IntervalTree.validate(high, "High bound", false); if(high < low) throw new RangeError( `Invalid interval [${low}, ${high}]. ` + "The high bound must be greater than or equal to the low bound." ); // Handle the case where the tree is currently empty if(!this.root){ this.root = new IntervalTreeNode( this.valuesEqual, low, high, null, Black ); this.root.addInterval(low, high, value); return this.root; } // Otherwise, search for the place where this interval should be added let node = this.root; while(true){ if(low < node.low){ if(node.left){ node = node.left; }else{ // Add the interval to a new left child of this node node.addLeftChild(this, low, high, value); break; } }else if(low > node.low){ if(node.right){ node = node.right; }else{ // Add the interval to a new right child of this node node.addRightChild(this, low, high, value); break; } }else{ // Add the interval to this node (same low boundary) node.addInterval(low, high, value); node.addIntervalUpdateLimits(); break; } } } // Remove the first matching interval from the tree. // Value equality checks use SameValueZero. remove(low, high, value){ // Immediately abort if the tree is empty if(!this.root) return null; // Validate interval bounds low = IntervalTree.validate(low, "Low bound", true); high = IntervalTree.validate(high, "High bound", true); // Exit immediately if the input interval isn't valid if(high !== high || low !== low || high < low) return null; // Get the node that should contain this interval const node = this.root.getNodeWithInterval(low, high, value); // Abort if the interval wasn't found anywhere in the tree if(!node) return null; // Try to remove the interval const removedInterval = node.removeInterval(low, high, value); // No matching interval? Abort with null return value if(!removedInterval) return null; // If there are no more instances in the node, then remove the node if(node.intervals.length === 0) node.remove(this); // Return the removed interval return removedInterval; } // Remove all matching intervals from the tree. removeAll(low, high, value){ // Immediately abort if the tree is empty if(!this.root) return null; // Validate interval bounds low = IntervalTree.validate(low, "Low bound", true); high = IntervalTree.validate(high, "High bound", true); // Exit immediately if the input interval isn't valid if(high !== high || low !== low || high < low) return null; // Get the node that should contain this interval const node = this.root.getNodeWithInterval(low, high, value); // Abort if the interval wasn't found anywhere in the tree if(!node) return null; // Try to remove all instances of the interval const removedIntervals = node.removeAllIntervals(low, high, value); // If there are no more instances in the node, then remove the node if(node.intervals.length === 0) node.remove(this); // Return the number of removed intervals return removedIntervals.length ? removedIntervals : null; } // Get whether a matching interval is contained in the tree. contains(low, high, value){ // Immediately abort if the tree is empty if(!this.root) return null; // Validate interval bounds low = IntervalTree.validate(low, "Low bound", true); high = IntervalTree.validate(high, "High bound", true); // Exit immediately if the input interval isn't valid if(high !== high || low !== low || high < low) return null; // Get the node that should contain this interval const node = this.root.getNodeWithInterval(low, high, value); // Count the number of matching intervals return node && node.contains(low, high, value); } // Get an array of matching intervals in the tree. getContained(low, high, value){ // Immediately abort if the tree is empty if(!this.root) return null; // Validate interval bounds low = IntervalTree.validate(low, "Low bound", true); high = IntervalTree.validate(high, "High bound", true); // Exit immediately if the input interval isn't valid if(high !== high || low !== low || high < low) return null; // Get the node that should contain this interval const node = this.root.getNodeWithInterval(low, high, value); // Handle the case where there was no matching node if(!node) return null; // Count the number of matching intervals const contained = node.getContainedIntervals(low, high, value); return contained.length ? contained : null; } // Enumerate all intervals that intersect a point *queryPoint(point){ if(!this.root) return; point = IntervalTree.validate(point, "Point", true); // Exit immediately if the point input was NaN if(point !== point) return; // Search the tree, starting with the root let stack = [this.root]; while(stack.length){ const node = stack.pop(); if(point >= node.low && point <= node.high){ for(let interval of node.intervals){ if(interval.high >= point) yield interval; else break; } } if(node.left && point <= node.left.maximumHigh && point >= node.left.minimumLow ){ stack.push(node.left); } if(node.right && point <= node.right.maximumHigh && point >= node.right.minimumLow ){ stack.push(node.right); } } } // Enumerate all intervals that end before or on a point *queryBeforePoint(point){ if(!this.root) return; point = IntervalTree.validate(point, "Point", true); // Exit immediately if the point input was NaN if(point !== point) return; // Search the tree, starting with the root let stack = [this.root]; while(stack.length){ const node = stack.pop(); if(point >= node.low){ for(let i = node.intervals.length - 1; i >= 0; i--){ const interval = node.intervals[i]; if(interval.high <= point) yield interval; else break; } } if(node.left && point >= node.left.minimumHigh){ stack.push(node.left); } if(node.right && point >= node.right.minimumHigh){ stack.push(node.right); } } } // Enumerate all intervals that begin after or on a point *queryAfterPoint(point){ if(!this.root) return; point = IntervalTree.validate(point, "Point", true); // Exit immediately if the point input was NaN if(point !== point) return; // Search the tree, starting with the root let stack = [this.root]; while(stack.length){ const node = stack.pop(); if(point <= node.low){ for(let interval of node.intervals) yield interval; } if(node.left && point <= node.left.maximumLow){ stack.push(node.left); } if(node.right && point <= node.right.maximumLow){ stack.push(node.right); } } } // Enumerate all intervals that do NOT intersect a point // Intervals with a boundary exactly equal to the point are included // in the output. *queryExcludePoint(point){ if(!this.root) return; point = IntervalTree.validate(point, "Point", true); // Exit immediately if the point input was NaN if(point !== point) return; // Search the tree, starting with the root let stack = [this.root]; while(stack.length){ const node = stack.pop(); if(point <= node.low){ for(let interval of node.intervals) yield interval; }else{ for(let i = node.intervals.length - 1; i >= 0; i--){ const interval = node.intervals[i]; if(interval.high <= point) yield interval; else break; } } if(node.left && ( point >= node.left.minimumHigh || point <= node.left.maximumLow )){ stack.push(node.left); } if(node.right && ( point >= node.right.minimumHigh || point <= node.right.maximumLow )){ stack.push(node.right); } } } // Enumerate all intervals that intersect another interval *queryInterval(low, high){ if(!this.root) return; // Validate interval bounds low = IntervalTree.validate(low, "Low bound", true); high = IntervalTree.validate(high, "High bound", true); // Exit immediately if the input interval isn't valid if(high !== high || low !== low || high < low) return; // Search the tree, starting with the root let stack = [this.root]; while(stack.length){ const node = stack.pop(); if(low <= node.high && high >= node.low){ for(let interval of node.intervals){ if(interval.high >= low) yield interval; else break; } } if(node.left && high >= node.left.minimumLow && low <= node.left.maximumHigh ){ stack.push(node.left); } if(node.right && high >= node.right.minimumLow && low <= node.right.maximumHigh ){ stack.push(node.right); } } } // Enumerate all intervals that are entirely contained within the input. *queryWithinInterval(low, high){ if(!this.root) return; low = IntervalTree.validate(low, "Low bound", true); high = IntervalTree.validate(high, "High bound", true); // Exit immediately if the input interval isn't valid if(high !== high || low !== low || high < low) return null; // Search the tree, starting with the root let stack = [this.root]; while(stack.length){ const node = stack.pop(); if(node.low >= low){ for(let i = node.intervals.length - 1; i >= 0; i--){ const interval = node.intervals[i]; if(interval.high <= high) yield interval; else break; } } if(node.left && node.left.maximumLow >= low && node.left.minimumHigh <= high ){ stack.push(node.left); } if(node.right && node.right.maximumLow >= low && node.right.minimumHigh <= high ){ stack.push(node.right); } } } // Enumerate all intervals that do NOT intersect another interval // Intervals with a low bound exactly equal to the high input bound, // or with a high bound exactly equal to the low input bound, are included // in the output. *queryExcludeInterval(low, high){ if(!this.root) return; low = IntervalTree.validate(low, "Low bound", true); high = IntervalTree.validate(high, "High bound", true); // Exit immediately if the input interval isn't valid if(high !== high || low !== low || high < low) return null; // Search the tree, starting with the root let stack = [this.root]; while(stack.length){ const node = stack.pop(); if(high <= node.low){ for(let interval of node.intervals) yield interval; }else{ for(let i = node.intervals.length - 1; i >= 0; i--){ const interval = node.intervals[i]; if(interval.high <= low) yield interval; else break; } } if(node.left && ( low >= node.left.minimumHigh || high <= node.left.maximumLow )){ stack.push(node.left); } if(node.right && ( low >= node.right.minimumHigh || high <= node.right.maximumLow )){ stack.push(node.right); } } } // Enumerate all nodes in the tree (in no particular order) *nodes(){ if(!this.root) return; let stack = [this.root]; while(stack.length){ const node = stack.pop(); if(node.left) stack.push(node.left); if(node.right) stack.push(node.right); yield node; } } // Enumerate all the nodes in the tree (in ascending order) *nodesAscending(){ let i = 0; let node = this.root && this.root.getLeftmostChild(); while(node){ yield node; node = node.getSuccessor(); } } // Enumerate all the nodes in the tree (in descending order) *nodesDescending(){ let node = this.root && this.root.getRightmostChild(); while(node){ yield node; node = node.getPredecessor(); } } // Enumerate all intervals in the tree (in no particular order) *intervals(){ for(let node of this.nodes()){ // Note: for...of is about 40% as performant as of node v10.7.0 // for(let interval of node.intervals) yield interval; for(let i = 0; i < node.intervals.length; i++){ yield node.intervals[i]; } } } // Enumerate all intervals in the tree (in ascending order) *ascending(){ for(let node of this.nodesAscending()){ for(let i = node.intervals.length - 1; i >= 0; i--){ yield node.intervals[i]; } } } // Enumerate all intervals in the tree (in descending order) *descending(){ for(let node of this.nodesDescending()){ // Note: for...of is about 40% as performant as of node v10.7.0 // for(let interval of node.intervals) yield interval; for(let i = 0; i < node.intervals.length; i++){ yield node.intervals[i]; } } } // Enumerate intervals (in no particular order) [Symbol.iterator](){ return this.intervals(); } } // An IntervalTree contains IntervalTreeNodes class IntervalTreeNode{ static getIntervalsArray(valuesEqual){ return new SortedArray(IntervalComparator, ( (a, b) => a.high === b.high && valuesEqual(a.value, b.value) )); } constructor(valuesEqual, low, high, parent, color){ this.valuesEqual = valuesEqual; this.intervals = IntervalTreeNode.getIntervalsArray(valuesEqual); // The color of the node, for balancing this.color = color; // The interval bounds for this node this.low = low; this.high = high; // The interval bounds for this entire subtree this.minimumLow = low; this.maximumLow = low; this.minimumHigh = high; this.maximumHigh = high; // The node's parent and children this.parent = parent; this.left = null; this.right = null; } // Add a new interval to the node. // Typically a boundary limit correcting method needs to be called after // this one, like insertUpdateLimits (for newly-added nodes) or // addIntervalUpdateLimits (when adding to existing nodes). addInterval(low, high, value){ const interval = new Interval(low, high, value); this.intervals.insert(interval); if(interval.high < this.minimumHigh) this.minimumHigh = interval.high; if(interval.high > this.maximumHigh) this.maximumHigh = interval.high; if(interval.high > this.high) this.high = interval.high; } // Remove an interval from the node. Returns the interval // if one was removed, or null if there was no matching interval. // The caller should check if this was the last interval and, // if it was, should then remove the node from the tree entirely. removeInterval(low, high, value){ const interval = new Interval(low, high, value); const index = this.intervals.indexOf(interval); if(index < 0) return null; const removedInterval = this.intervals.splice(index, 1)[0]; if(this.intervals.length && (index === 0 || index === this.intervals.length) ){ this.high = this.intervals[0].high; this.removeUpdateLimits(); } return removedInterval; } // Remove all matching intervals from the node. // Returns an array of the removed intervals. // The caller should check if there are no remaining intervals and, // if not, should then remove the node from the tree entirely. removeAllIntervals(low, high, value){ const interval = new Interval(low, high, value); const removedIntervals = this.intervals.removeAll(interval); // TODO: This should not have to be done in every case if(removedIntervals.length && this.intervals.length){ this.removeUpdateLimits(); } return removedIntervals; } // Get whether the node contains a matching interval // Assumes that the interval low bound is already known to match this node. contains(low, high, value){ const interval = new Interval(low, high, value); const index = this.intervals.indexOf(interval); return index < 0 ? null : this.intervals[index]; } // Get all matching intervals // Assumes that the interval low bound is already known to match this node. getContainedIntervals(low, high, value){ const interval = new Interval(low, high, value); return this.intervals.getEqualValues(interval); } // Get the parent's opposite child node. getSibling(){ if(!this.parent) return null; if(this === this.parent.left) return this.parent.right; else return this.parent.left; } // Get the leftmost node in the subtree // for which this node is the root. getLeftmostChild(){ let node = this; while(node.left) node = node.left; return node; } // Get the rightmost node in the subtree // for which this node is the root. getRightmostChild(){ let node = this; while(node.right) node = node.right; return node; } // Get the next node in the sort order. getSuccessor(){ if(this.right) return this.right.getLeftmostChild(); let node = this; while(node){ if(node.parent && node === node.parent.left) return node.parent; node = node.parent; } } // Get the next node in the sort order. getPredecessor(){ if(this.left) return this.left.getRightmostChild(); let node = this; while(node){ if(node.parent && node === node.parent.right) return node.parent; node = node.parent; } } // Remove the parent node's reference to this one as a child node. makeOrphan(){ if(!this.parent) return; if(this === this.parent.left) this.parent.left = null; else this.parent.right = null; } // Compute the height of this subtree. Requires a complete traversal. // A node with no children has a subtree height of 0. getHeight(){ let maxHeight = 0; const stack = [{node: this, height: 0}]; while(stack.length){ const next = stack.pop(); if(next.height > maxHeight){ maxHeight = next.height; } if(next.node.left){ stack.push({node: next.node.left, height: next.height + 1}); } if(next.node.right){ stack.push({node: next.node.right, height: next.height + 1}); } } return maxHeight; } // Get the number of intervals in the subtree. getIntervalCount(){ let intervalCount = 0; const stack = [this]; while(stack.length){ const node = stack.pop(); if(node.left) stack.push(node.left); if(node.right) stack.push(node.right); intervalCount += node.intervals.length; } return intervalCount; } // Add a child on the left side. addLeftChild(tree, low, high, value){ this.left = new IntervalTreeNode( this.valuesEqual, low, high, this, Red ); this.left.addInterval(low, high, value); this.left.insertUpdateLimits(); this.left.insertionFix(tree); return this.left; } // Add a child on the right side. addRightChild(tree, low, high, value){ this.right = new IntervalTreeNode( this.valuesEqual, low, high, this, Red ); this.right.addInterval(low, high, value); this.right.insertUpdateLimits(); this.right.insertionFix(tree); return this.right; } // Ensure that the tree retains valid red-black structure following // the insertion of a new node. insertionFix(tree, child){ let node = this; // While node and parent are both Red while(node.color === Red && node.parent.color === Red){ const parent = node.parent; const uncle = parent.getSibling(); if(uncle && uncle.color === Red){ // Parent's sibling is Red uncle.color = Black; parent.color = Black; parent.parent.color = Red; node = parent.parent; }else{ if((parent.left === node) !== (parent.parent.left === parent)){ node.rotate(tree); node.rotate(tree); }else{ parent.rotate(tree); node = parent; } } if(!node.parent){ break; } } if(!node.parent){ node.color = Black; } } // Get the node containing a given interval, if any exists. getNodeWithInterval(low, high, value){ let node = this; while(node){ if(low < node.low){ if(!node.left) return null; node = node.left; }else if(low > node.low){ if(!node.right) return null; node = node.right; }else{ return node; } } return null; } // Delete this node from the tree. // This method may swap information with another node (the successor) // and delete that node instead of this one. remove(tree){ let replaceWith; if(this.left && this.right){ const next = this.right.getLeftmostChild(); this.low = next.low; this.high = next.high; this.intervals = next.intervals; this.removeUpdateLimits(); next.handleRemoval(tree); }else{ this.handleRemoval(tree); } } // Delete a node with one child or no children from the tree. // This helper is called by the `remove` method. handleRemoval(tree){ // Get the one child node, if there is one. const child = this.left || this.right; // Handle the case where this node is the root if(!this.parent){ tree.root = child; if(child){ child.parent = null; child.color = Black; } // Delete a red node (which by implication has no children) }else if(this.color === Red){ this.makeOrphan(); // Delete a black node with a red child }else if(child && child.color === Red){ this.intervals = child.intervals; this.low = child.low; this.high = child.high; this.minimumLow = child.minimumLow; this.maximumLow = child.maximumLow; this.minimumHigh = child.minimumHigh; this.maximumHigh = child.maximumHigh; this.left = child.left; this.right = child.right; // Delete a black node with a black child // Note: This case should not actually be reachable? }else if(child){ this.swapWithChild(child); child.removalFix(tree); this.makeOrphan(); // Delete a black node with no children }else{ this.removalFix(tree); this.makeOrphan(); } // Update interval information if(this.parent){ this.parent.removeUpdateLimits(); // Note: A reference implementation used this behavior instead. // This change did not cause issues during extensive testing. // Still, if bugs occur, it may be because of this change. // this.parent.immediateUpdateLimits(); // if(this.parent.parent) this.parent.parent.removeUpdateLimits(); } } // Ensure that the tree retains valid red-black structure following // the removal of a node that may have disrupted the structure. removalFix(tree){ let node = this; while(node.color === Black && node.parent){ let sibling = node.getSibling(); if(sibling.color === Red){ sibling.rotate(tree); sibling = node.getSibling(); } if( (!sibling.left || sibling.left.color === Black) && (!sibling.right || sibling.right.color === Black) ){ sibling.color = Red; node = node.parent; }else{ if(sibling === sibling.parent.left && ( !sibling.left || sibling.left.color === Black )){ sibling = sibling.rotateLeft(tree); }else if(sibling === sibling.parent.right && ( !sibling.right || sibling.right.color === Black )){ sibling = sibling.rotateRight(tree); } sibling.rotate(tree); node = node.parent.getSibling(); } } node.color = Black; } // Rotate right if this is the left child of the parent, or rotate // left if this is the right child. rotate(tree){ if(this === this.parent.left){ this.parent.rotateRight(tree); }else{ this.parent.rotateLeft(tree); } } // Effectively switch places for this node and its right child. rotateLeft(tree){ const child = this.right; this.swapWithChild(tree, child); this.parent = child; this.right = child.left; if(child.left) child.left.parent = this; child.left = this; this.rotateCommon(child); return child; } // Effectively switch places for this node and its left child. rotateRight(tree){ const child = this.left; this.swapWithChild(tree, child); this.parent = child; this.left = child.right; if(child.right) child.right.parent = this; child.right = this; this.rotateCommon(child); return child; } // Helper used by rotateLeft and rotateRight operations. rotateCommon(child){ const swapColor = this.color; this.color = child.color; child.color = swapColor; this.immediateUpdateLimits(); if(child.minimumLow > this.minimumLow){ child.minimumLow = this.minimumLow; } if(child.maximumLow < this.maximumLow){ child.maximumLow = this.maximumLow; } if(child.minimumHigh > this.minimumHigh){ child.minimumHigh = this.minimumHigh; } if(child.maximumHigh < this.maximumHigh){ child.maximumHigh = this.maximumHigh; } } // Make a child node become the child of this node's parent, instead. // This is one part of a rotation operation. swapWithChild(tree, child){ if(child) child.parent = this.parent; if(!this.parent){ tree.root = child; }else if(this === this.parent.left){ this.parent.left = child; }else{ this.parent.right = child; } } // Update minimumLow and maximumHigh interval bounds after inserting this node. // Propagates updates up to parents when needed. insertUpdateLimits(){ let node = this; let changed = true; while(node.parent && changed){ changed = false; if(node.parent.minimumLow > this.minimumLow){ node.parent.minimumLow = this.minimumLow; changed = true; } if(node.parent.maximumLow < this.maximumLow){ node.parent.maximumLow = this.maximumLow; changed = true; } if(node.parent.minimumHigh > this.minimumHigh){ node.parent.minimumHigh = this.minimumHigh; changed = true; } if(node.parent.maximumHigh < this.maximumHigh){ node.parent.maximumHigh = this.maximumHigh; changed = true; } node = node.parent; } } // Update maximumHigh interval bounds after adding a new interval to this node. // Propagates updates up to parents when needed. addIntervalUpdateLimits(){ let node = this.parent; let changed = true; while(node && changed){ changed = false; if(node.minimumHigh > this.minimumHigh){ node.minimumHigh = this.minimumHigh; changed = true; } if(node.maximumHigh < this.maximumHigh){ node.maximumHigh = this.maximumHigh; changed = true; } node = node.parent; } } // Update minimumLow and maximumHigh interval bounds after replacing a removed // node with this node. Propagates updates up to parents when needed. removeUpdateLimits(){ let node = this; while(node){ // TODO: Under what conditions can this exit without continuing // to traverse the rest of the nodes to the root? node.immediateUpdateLimits(); node = node.parent; } } // Helper to determine minimumLow and maximumHigh interval bounds for the // entire subtree based on the information in this node and its // immediate children. immediateUpdateLimits(){ // Since nodes are in ascending order of length left-to-right, // computing the extreme low interval bounds is straightforward. this.minimumLow = this.left ? this.left.minimumLow : this.low; this.maximumLow = this.right ? this.right.maximumLow : this.low; // Maximum interval bounds are effectively: // max|min(high bound for this, for left child, for right child) this.minimumHigh = this.intervals[this.intervals.length - 1].high; this.maximumHigh = this.high; // Should always equal intervals[0].high if(this.left){ if(this.left.minimumHigh < this.minimumHigh){ this.minimumHigh = this.left.minimumHigh; } if(this.left.maximumHigh > this.maximumHigh){ this.maximumHigh = this.left.maximumHigh; } } if(this.right){ if(this.right.minimumHigh < this.minimumHigh){ this.minimumHigh = this.right.minimumHigh; } if(this.right.maximumHigh > this.maximumHigh){ this.maximumHigh = this.right.maximumHigh; } } } // Extremely useful stringification tools for debugging // Get a string representation of this subtree to log to a CLI // toDebugString(indent, label){ // indent = indent || ""; // label = label || "ROOT"; // const istr = i => `\x1b[90m[${i.low}, ${i.high}]:\x1b[39m ${i.value}` // let str = (indent + label + ":: " + // `\x1b[92m${this.low}\x1b[39m ` + // "\x1b[" + (this.color ? "91mRED" : "94mBLK") + "\x1b[39m : " + // `(${this.intervals.map(istr).join(", ")}) ` + // `\x1b[90mp.\x1b[92m${this.parent && this.parent.low}\x1b[39m ` + // `[${this.minimumLow},${this.maximumLow}..${this.minimumHigh},${this.maximumHigh}]` // ); // if(this.left) str += "\n" + this.left.toDebugString(indent + " ", "L"); // if(this.right) str += "\n" + this.right.toDebugString(indent + " ", "R"); // return str; // } // Log a subtree string representation to a chrome DevTools console // log(){ // for(let l of this.toDebugString().split("\n")){ // const p = require("ansicolor").parse(l); // console.log(...p.asChromeConsoleLogArguments); // } // } } // An IntervalTreeNode contains Intervals class Interval{ constructor(low, high, value){ this.low = low; this.high = high; this.value = value; } } // Comparator function used by SortedArray when none is passed explicitly const DefaultComparator = ((a, b) => ( a < b ? -1 : (a > b ? +1 : 0) )); // Array type with sorted insertion methods and optimized // implementations of some Array methods. // SortedArray does not stop you from pushing, shifting, // splicing, or assigning values at an index. // However, if these things are not done judiciously, then // the array will no longer be sorted and its methods will // no longer function correctly. class SortedArray extends Array{ // Construct a new SortedArray. Uses Array.sort to sort // the input collection, if any; the sort may be unstable. constructor(){ let values = null; let valuesEqual = null; let comparator = null; let reversedComparator = null; // new SortedArray(comparator) if(arguments.length === 1 && typeof(arguments[0]) === "function" ){ comparator = arguments[0]; // new SortedArray(comparator, valuesEqual) }else if(arguments.length === 2 && typeof(arguments[0]) === "function" && typeof(arguments[1]) === "function" ){ comparator = arguments[0]; valuesEqual = arguments[1]; // new SortedArray(values, comparator?, valuesEqual?) }else{ values = arguments[0]; comparator = arguments[1]; valuesEqual = arguments[2]; } if(comparator && typeof(comparator) !== "function"){ // Verify comparator input throw new TypeError("Comparator argument must be a function."); } if(valuesEqual && typeof(valuesEqual) !== "function"){ // Verify comparator input throw new TypeError("Value equality argument must be a function."); } // new SortedArray(length, cmp?, eq?) - needed by some inherited methods if(typeof(values) === "number"){ if(!Number.isInteger(values) || values < 0){ throw new RangeError("Invalid array length"); } super(values); // new SortedArray(SortedArray, cmp?, eq?) - same or unspecified comparator }else if(values instanceof SortedArray && ( !comparator || values.comparator === comparator )){ super(); super.push(...values); comparator = values.comparator; reversedComparator = values.reversedComparator; if(!valuesEqual) valuesEqual = values.valuesEqual; // new SortedArray(Array, cmp?, eq?) }else if(Array.isArray(values)){ super(); super.push(...values); super.sort(comparator || DefaultComparator); if(values instanceof SortedArray && !valuesEqual){ valuesEqual = values.valuesEqual; } // new SortedArray(iterable, cmp?, eq?) }else if(values && typeof(values[Symbol.iterator]) === "function"){ super(); for(let value of values) super.push(value); super.sort(comparator || DefaultComparator); // new SortedArray(object with length, cmp?, eq?) - e.g. `arguments` }else if(values && typeof(values) === "object" && Number.isFinite(values.length) ){ super(); for(let i = 0; i < values.length; i++) super.push(values[i]); super.sort(comparator || DefaultComparator); // new SortedArray() // new SortedArray(comparator) // new SortedArray(comparator, valuesEqual) }else if(!values){ super(); // new SortedArray(???) }else{ throw new TypeError( "Unhandled values input type. Expected an iterable." ); } this.valuesEqual = valuesEqual || SameValueZero; this.comparator = comparator || DefaultComparator; this.reversedComparator = reversedComparator; } // Construct a SortedArray with elements given as arguments. static of(...values){ return new SortedArray(values); } // Construct a SortedArray from assumed-sorted arguments. static ofSorted(...values){ const array = new SortedArray(); Array.prototype.push.apply(array, values); return array; } // Construct a SortedArray from the given inputs. static from(values, comparator, valuesEqual){ return new SortedArray(values, comparator, valuesEqual); } // Construct a SortedArray from assumed-sorted values. static fromSorted(values, comparator, valuesEqual){ const array = new SortedArray(null, comparator, valuesEqual); if(Array.isArray(values)){ Array.prototype.push.apply(array, values); }else{ for(let value of values) Array.prototype.push.call(array, value); } return array; } /// SortedArray methods // Insert a value into the list. insert(value){ const index = this.lastInsertionIndexOf(value); this.splice(index, 0, value); return this.length; } // Insert an iterable of assumed-sorted values into the list // This will typically be faster than calling `insert` in a loop. insertSorted(values){ // Optimized implementation for arrays and array-like objects if(values && typeof(values) === "object" && Number.isFinite(values.length) ){ // Exit immediately if the values array is empty if(values.length === 0){ return this.length; } // If the last element in the input precedes the first element // in the array, the input can be prepended in one go. const lastInsertionIndex = this.lastInsertionIndexOf( values[values.length - 1] ); if(lastInsertionIndex === 0){ this.unshift(...values); return this.length; } // If the first element would go in the same place in the array // as the last element, then it can be spliced in all at once. const firstInsertionIndex = this.lastInsertionIndexOf(values[0]); if(firstInsertionIndex === lastInsertionIndex){ this.splice(firstInsertionIndex, 0, ...values); return this.length; } // Array contents must be interlaced let insertIndex = 0; for(let valIndex = 0; valIndex < values.length; valIndex++){ const value = values[valIndex]; insertIndex = this.lastInsertionIndexOf(value, insertIndex); // If this element was at the end of the array, then every other // element of the input is too and they can be appended at once. if(insertIndex === this.length && valIndex < values.length - 1){ this.push(...values.slice(valIndex)); return this.length; }else{ this.splice(insertIndex++, 0, value); } } return this.length; // Generalized implementation for any iterable }else if(values && typeof(values[Symbol.iterator]) === "function"){ let insertIndex = 0; for(let value of values){ insertIndex = this.lastInsertionIndexOf(value, insertIndex); this.splice(insertIndex++, 0, value); } return this.length; // Produce an error if the input isn't an acceptable type. }else{ throw new TypeError("Expected an iterable list of values."); } } // Remove the first matching value. // Returns true if a matching element was found and removed, // or false if no matching element was found. remove(value){ const index = this.indexOf(value); if(index >= 0){ this.splice(index, 1); return true; }else{ return false; } } // Remove the last matching value. // Returns true if a matching element was found and removed, // or false if no matching element was found. removeLast(value){ const index = this.lastIndexOf(value); if(index >= 0){ this.splice(index, 1); return true; }else{ return false; } } // Remove all matching values. // Returns the removed elements as a new SortedArray. removeAll(value){ let index = this.firstInsertionIndexOf(value); const removed = new SortedArray(); removed.valuesEqual = this.valuesEqual; removed.comparator = this.comparator; removed.reversedComparator = this.reversedComparator; while(index < this.length && this.comparator(this[index], value) === 0 ){ if(this.valuesEqual(this[index], value)){ Array.prototype.push.call(removed, this[index]); this.splice(index, 1); }else{ index++; } } return removed; } // Get all equal values. // Returns the equivalent elements as a new SortedArray. getEqualValues(value){ let index = this.firstInsertionIndexOf(value); const equal = new SortedArray(); equal.valuesEqual = this.valuesEqual; equal.comparator = this.comparator; equal.reversedComparator = this.reversedComparator; while(index < this.length && this.comparator(this[index], value) === 0 ){ if(this.valuesEqual(this[index], value)){ Array.prototype.push.call(equal, this[index]); } index++; } return equal; } // Returns the index of the first equal element, // or the index that such an element should // be inserted at if there is no equal element. firstInsertionIndexOf(value, fromIndex, endIndex){ const from = (typeof(fromIndex) !== "number" || fromIndex !== fromIndex ? 0 : (fromIndex < 0 ? Math.max(0, this.length + fromIndex) : fromIndex) ); const end = (typeof(endIndex) !== "number" || endIndex !== endIndex ? this.length : (endIndex < 0 ? this.length + endIndex : Math.min(this.length, endIndex) ) ); let min = from - 1; let max = end; while(1 + min < max){ const mid = min + Math.floor((max - min) / 2); const cmp = this.comparator(value, this[mid]); if(cmp > 0) min = mid; else max = mid; } return max; } // Returns the index of the last equal element, // or the index that such an element should // be inserted at if there is no equal element. lastInsertionIndexOf(value, fromIndex, endIndex){ const from = (typeof(fromIndex) !== "number" || fromIndex !== fromIndex ? 0 : (fromIndex < 0 ? Math.max(0, this.length + fromIndex) : fromIndex) ); const end = (typeof(endIndex) !== "number" || endIndex !== endIndex ? this.length : (endIndex < 0 ? this.length + endIndex : Math.min(this.length, endIndex) ) ); let min = from - 1; let max = end; while(1 + min < max){ const mid = min + Math.floor((max - min) / 2); const cmp = this.comparator(value, this[mid]); if(cmp >= 0) min = mid; else max = mid; } return max; } // Returns the index of the first equal element, or -1 if // there is no equal element. indexOf(value, fromIndex){ let index = this.firstInsertionIndexOf(value, fromIndex); if(index >= 0 && index < this.length && this.valuesEqual(this[index], value) ){ return index; } while(++index < this.length && this.comparator(value, this[index]) === 0 ){ if(this.valuesEqual(this[index], value)) return index; } return -1; } // Returns the index of the last equal element, or -1 if // there is no equal element. lastIndexOf(value, fromIndex){ let index = this.lastInsertionIndexOf(value, 0, fromIndex); if(index >= 0 && index < this.length && this.valuesEqual(this[index], value) ){ return index; } while(--index >= 0 && this.comparator(value, this[index]) === 0 ){ if(this.valuesEqual(this[index], value)) return index; } return -1; } // Returns true when the value is contained within the // array, and false when not. includes(valu