UNPKG

strange

Version:

Range aka interval object. Supports exclusive and infinite ranges. Comes with an interval tree (augmented binary search tree).

159 lines (138 loc) 4.88 kB
var Range = require("./") var union = Range.union var compareBeginToBegin = Range.compareBeginToBegin var compareBeginToEnd = Range.compareBeginToEnd var compareEndToEnd = Range.compareEndToEnd var concat = Array.prototype.concat.bind(Array.prototype) var EMPTY_ARR = Array.prototype module.exports = RangeTree /** * Create an interval tree node. * * For creating a binary search tree out of an array of ranges, you might want * to use [`RangeTree.from`](#RangeTree.from). * * **Import**: * ```javascript * var RangeTree = require("strange/tree") * ``` * * @example * var left = new RangeTree([new Range(-5, 0)]) * var right = new RangeTree([new Range(5, 10)]) * var root = new RangeTree([new Range(0, 5), new Range(0, 10)], left, right] * root.search(7) // => [new Range(0, 10), new Range(5, 10)] * * @class RangeTree * @constructor * @param {Object|Object[]} ranges * @param {RangeTree} left * @param {RangeTree} right */ function RangeTree(keys, left, right) { // Store the longest range first. if (Array.isArray(keys)) this.keys = keys.slice().sort(reverseCompareEndToEnd) else this.keys = [keys] this.left = left || null this.right = right || null // Remember, the topmost key has the longest range. var a = this.left ? this.left.range : this.keys[0] var b = this.right ? union(this.keys[0], this.right.range) : this.keys[0] this.range = union(a, b) } /** * Create an interval tree (implemented as an augmented binary search tree) * from an array of ranges. * Returns a [`RangeTree`](#RangeTree) you can search on. * * If you need to relate the found ranges to other data, add some properties * directly to every range _or_ use JavaScript's `Map` or `WeakMap` to relate * extra data to those range instances. * * @example * var ranges = [new Range(0, 10), new Range(20, 30), new Range(40, 50)] * RangeTree.from(ranges).search(42) // => [new Range(40, 50)] * * @static * @method from * @param {Range[]} ranges */ RangeTree.from = function(ranges) { ranges = ranges.filter(isNotEmpty) ranges = ranges.sort(compareBeginToBegin) ranges = ranges.map(arrayify) ranges = ranges.reduce(dedupe, []) return this.new(ranges) } RangeTree.new = function(ranges) { switch (ranges.length) { case 0: return new this(new Range) case 1: return new this(ranges[0]) case 2: return new this(ranges[0], null, new this(ranges[1])) default: var middle = Math.floor(ranges.length / 2) var left = this.new(ranges.slice(0, middle)) var right = this.new(ranges.slice(middle + 1)) return new this(ranges[middle], left, right) } } /** * Search for ranges that include the given value or, given a range, intersect * with it. * Returns an array of matches or an empty one if no range contained or * intersected with the given value. * * @example * var tree = RangeTree.from([new Range(40, 50)]) * tree.search(42) // => [new Range(40, 50)] * tree.search(13) // => [] * tree.search(new Range(30, 42)) // => [new Range(40, 50)] * * @method search * @param {Object} valueOrRange */ RangeTree.prototype.search = function(value) { if (value instanceof Range) return this.searchByRange(value) else return this.searchByValue(value) } RangeTree.prototype.searchByValue = function(value) { if (!this.range.contains(value)) return EMPTY_ARR var ownPosition = this.keys[0].compareBegin(value) return concat( this.left ? this.left.searchByValue(value) : EMPTY_ARR, ownPosition <= 0 ? this.searchOwnByValue(value) : EMPTY_ARR, this.right && ownPosition < 0 ? this.right.searchByValue(value) : EMPTY_ARR ) } RangeTree.prototype.searchByRange = function(range) { if (!this.range.intersects(range)) return EMPTY_ARR var ownPosition = compareBeginToEnd(this.keys[0], range) return concat( this.left ? this.left.searchByRange(range) : EMPTY_ARR, ownPosition <= 0 ? this.searchOwnByRange(range) : EMPTY_ARR, this.right && ownPosition < 0 ? this.right.searchByRange(range) : EMPTY_ARR ) } // Sort ranges in ascending order for beauty. O:) RangeTree.prototype.searchOwnByValue = function(value) { return take(this.keys, function(r) { return r.contains(value) }).reverse() } RangeTree.prototype.searchOwnByRange = function(range) { return take(this.keys, function(r) { return r.intersects(range) }).reverse() } function dedupe(ranges, range) { var last = ranges[ranges.length - 1] if (last != null && compareBeginToBegin(last[0], range[0]) === 0) last.push(range[0]) else ranges.push(range) return ranges } function take(arr, fn) { var values = [] for (var i = 0; i < arr.length && fn(arr[i], i); ++i) values.push(arr[i]) return values } function arrayify(obj) { return [obj] } function isNotEmpty(range) { return !range.isEmpty() } function reverseCompareEndToEnd(a, b) { return compareEndToEnd(a, b) * -1 }