UNPKG

strange

Version:

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

434 lines (405 loc) 13.1 kB
var INVALID_BOUNDS_ERR = "Invalid range bounds: " module.exports = Range /** * Create a new range object with the given begin and end endpoints. * You can also pass a two character string for bounds. Defaults to` "[]"` for * an all inclusive range. * * You can use any value for endpoints. `Null` is considered infinity for * values that don't have a special infinity type like `Number` has `Infinity`. * * An empty range is one where either of the endpoints is `undefined` (like `new * Range`) or a range with two equivalent, but exculsive endpoints * (`new Range(5, 5, "[)")`). * * **Import**: * ```javascript * var Range = require("strange") * ``` * * @example * new Range(10, 20) // => {begin: 10, end: 20, bounds: "[]"} * new Range(new Date(2000, 5, 18), new Date(2000, 5, 22)) * * @class Range * @constructor * @param {Object} begin * @param {Object} end * @param {String} [bounds="[]"] */ function Range(begin, end, bounds) { if (!(this instanceof Range)) return new Range(begin, end, bounds) /** * Range's beginning, or left endpoint. * * @property {Object} begin */ this.begin = begin /** * Range's end, or right endpoint. * * @property {Object} end */ this.end = end /** * Range's bounds. * * Bounds signify whether the range includes or excludes that particular * endpoint. * * Pair | Meaning * -----|-------- * `()` | open * `[]` | closed * `[)` | left-closed, right-open * `(]` | left-open, right-closed * * @example * new Range(1, 5).bounds // => "[]" * new Range(1, 5, "[)").bounds // => "[)" * * @property {String} bounds */ this.bounds = bounds = bounds === undefined ? "[]" : bounds if (!isValidBounds(bounds)) throw new RangeError(INVALID_BOUNDS_ERR + bounds) } Range.prototype.begin = undefined Range.prototype.end = undefined Range.prototype.bounds = "[]" /** * Compares this range's beginning with the given value. * Returns `-1` if this range begins before the given value, `0` if they're * equal and `1` if this range begins after the given value. * * `null` is considered to signify negative infinity for non-numeric range * endpoints. * * @example * new Range(0, 10).compareBegin(5) // => -1 * new Range(0, 10).compareBegin(0) // => 0 * new Range(5, 10).compareBegin(0) // => 1 * new Range(5, 10).compareBegin(null) // => 1 * * @method compareBegin * @param {Object} begin */ Range.prototype.compareBegin = function(begin) { var a = this.begin === null ? -Infinity : this.begin var b = begin === null ? -Infinity : begin return compare(a, b) || (this.bounds[0] == "[" ? 0 : 1) } /** * Compares this range's end with the given value. * Returns `-1` if this range ends before the given value, `0` if they're * equal and `1` if this range ends after the given value. * * `null` is considered to signify positive infinity for non-numeric range * endpoints. * * @example * new Range(0, 10).compareEnd(5) // => -1 * new Range(0, 10).compareEnd(10) // => 0 * new Range(0, 5).compareEnd(10) // => 1 * new Range(0, 5).compareEnd(null) // => -1 * * @method compareEnd * @param {Object} end */ Range.prototype.compareEnd = function(end) { var a = this.end === null ? Infinity : this.end var b = end === null ? Infinity : end return compare(a, b) || (this.bounds[1] == "]" ? 0 : -1) } /** * Check whether the range is empty. * An empty range is one where either of the endpoints is `undefined` (like `new * Range`) or a range with two equivalent, but exculsive endpoints * (`new Range(5, 5, "[)")`). * * Equivalence is checked by using the `<` operators, so value objects will be * coerced into something comparable by JavaScript. That usually means calling * the object's `valueOf` function. * * @example * new Range().isEmpty() // => true * new Range(5, 5, "[)").isEmpty() // => true * new Range(1, 10).isEmpty() // => false * * @method isEmpty */ Range.prototype.isEmpty = function() { var a = this.begin === null ? -Infinity : this.begin var b = this.end === null ? Infinity : this.end if (a === undefined || b === undefined) return true return this.bounds != "[]" && compare(a, b) === 0 } /** * Check whether the range is bounded. * A bounded range is one where neither endpoint is `null` or `Infinity`. An * empty range is considered bounded. * * @example * new Range().isBounded() // => true * new Range(5, 5).isBounded() // => true * new Range(null, new Date(2000, 5, 18).isBounded() // => false * new Range(0, Infinity).isBounded() // => false * new Range(-Infinity, Infinity).isBounded() // => false * * @method isBounded */ Range.prototype.isBounded = function() { if (this.begin === undefined || this.end === undefined) return true return !(isInfinity(this.begin) || isInfinity(this.end)) } /** * @method isFinite * @alias isBounded */ Range.prototype.isFinite = Range.prototype.isBounded /** * Check whether the range is unbounded. * An unbounded range is one where either endpoint is `null` or `Infinity`. An * empty range is not considered unbounded. * * @example * new Range().isUnbounded() // => false * new Range(5, 5).isUnbounded() // => false * new Range(null, new Date(2000, 5, 18).isUnbounded() // => true * new Range(0, Infinity).isUnbounded() // => true * new Range(-Infinity, Infinity).isUnbounded() // => true * * @method isUnbounded */ Range.prototype.isUnbounded = function() { return !this.isBounded() } /** * @method isInfinite * @alias isUnbounded */ Range.prototype.isInfinite = Range.prototype.isUnbounded /** * Check if a given value is contained within this range. * Returns `true` or `false`. * * @example * new Range(0, 10).contains(5) // => true * new Range(0, 10).contains(10) // => true * new Range(0, 10, "[)").contains(10) // => false * * @method contains * @param {Object} value */ Range.prototype.contains = function(value) { var a = this.begin var b = this.end return ( (b === null || (this.bounds[1] === "]" ? value <= b : value < b)) && (a === null || (this.bounds[0] === "[" ? a <= value : a < value)) ) } /** * Check if this range intersects with another. * Returns `true` or `false`. * * Ranges that have common points intersect. Ranges that are consecutive and * with *inclusive* endpoints are also intersecting. An empty range will never * intersect. * * @example * new Range(0, 10).intersects(new Range(5, 7)) // => true * new Range(0, 10).intersects(new Range(10, 20)) // => true * new Range(0, 10, "[)").intersects(new Range(10, 20)) // => false * new Range(0, 10).intersects(new Range(20, 30)) // => false * * @method intersects * @param {Object} other */ Range.prototype.intersects = function(other) { if (this.isEmpty()) return false if (other.isEmpty()) return false return ( Range.compareBeginToEnd(this, other) <= 0 && Range.compareBeginToEnd(other, this) <= 0 ) } /** * Returns an array of the endpoints and bounds. * * Useful with [Egal.js](https://github.com/moll/js-egal) or other libraries * that compare value objects by their `valueOf` output. * * @example * new Range(1, 10, "[)").valueOf() // => [1, 10, "[)"] * * @method valueOf */ Range.prototype.valueOf = function() { return [this.begin, this.end, this.bounds] } /** * Stringifies a range in `[a,b]` format. * * This happens to match the string format used by [PostgreSQL's range type * format](http://www.postgresql.org/docs/9.4/static/rangetypes.html). You can * therefore use stRange.js to parse and stringify ranges for your database. * * @example * new Range(1, 5).toString() // => "[1,5]" * new Range(1, 10, "[)").toString() // => "[1,10)" * * @method toString */ Range.prototype.toString = function() { // FIXME: How to serialize an empty range with undefined endpoints? var a = stringify(this.begin) var b = stringify(this.end) return this.bounds[0] + a + "," + b + this.bounds[1] } /** * Stringifies the range when passing it to `JSON.stringify`. * This way you don't need to manually call `toString` when stringifying. * * @example * JSON.stringify(new Range(1, 10)) // "\"[1,10]\"" * * @method toJSON * @alias toString */ Range.prototype.toJSON = Range.prototype.toString Range.prototype.inspect = Range.prototype.toString /** * Compares two range's beginnings. * Returns `-1` if `a` begins before `b` begins, `0` if they're equal and `1` * if `a` begins after `b`. * * @example * Range.compareBeginToBegin(new Range(0, 10), new Range(5, 15)) // => -1 * Range.compareBeginToBegin(new Range(0, 10), new Range(0, 15)) // => 0 * Range.compareBeginToBegin(new Range(0, 10), new Range(0, 15, "()")) // => 1 * * @static * @method compareBeginToBegin * @param {Object} a * @param {Object} b */ Range.compareBeginToBegin = function(a, b) { var aBegin = a.begin === null ? -Infinity : a.begin var bBegin = b.begin === null ? -Infinity : b.begin if (a.bounds[0] === b.bounds[0]) return compare(aBegin, bBegin) else return compare(aBegin, bBegin) || (b.bounds[0] === "(" ? -1 : 1) } /** * Compares the first range's beginning to the second's end. * Returns `<0` if `a` begins before `b` ends, `0` if one starts where the other * ends and `>1` if `a` begins after `b` ends. * * @example * Range.compareBeginToEnd(new Range(0, 10), new Range(0, 5)) // => -1 * Range.compareBeginToEnd(new Range(0, 10), new Range(-10, 0)) // => 0 * Range.compareBeginToEnd(new Range(0, 10), new Range(-10, -5)) // => 1 * * @static * @method compareBeginToEnd * @param {Object} a * @param {Object} b */ Range.compareBeginToEnd = function(a, b) { var aBegin = a.begin === null ? -Infinity : a.begin var bEnd = b.end === null ? Infinity : b.end if (a.bounds[0] === "[" && b.bounds[1] === "]") return compare(aBegin, bEnd) else return compare(aBegin, bEnd) || 1 } /** * Compares two range's endings. * Returns `-1` if `a` ends before `b` ends, `0` if they're equal and `1` * if `a` ends after `b`. * * @example * Range.compareEndToEnd(new Range(0, 10), new Range(5, 15)) // => -1 * Range.compareEndToEnd(new Range(0, 10), new Range(5, 10)) // => 0 * Range.compareEndToEnd(new Range(0, 10), new Range(5, 10, "()")) // => 1 * * @static * @method compareEndToEnd * @param {Object} a * @param {Object} b */ Range.compareEndToEnd = function(a, b) { var aEnd = a.end === null ? Infinity : a.end var bEnd = b.end === null ? Infinity : b.end if (a.bounds[1] === b.bounds[1]) return compare(aEnd, bEnd) else return compare(aEnd, bEnd) || (a.bounds[1] === ")" ? -1 : 1) } /** * Parses a string stringified by * [`Range.prototype.toString`](#Range.prototype.toString). * * To have it also parse the endpoints to something other than a string, pass * a function as the second argument. * * If you pass `Number` as the _parse_ function and the endpoints are * unbounded, they'll be set to `Infinity` for easier computation. * * @example * Range.parse("[a,z)") // => new Range("a", "z", "[)") * Range.parse("[42,69]", Number) // => new Range(42, 69) * Range.parse("[15,]", Number) // => new Range(15, Infinity) * Range.parse("(,3.14]", Number) // => new Range(-Infinity, 3.14, "(]") * * @static * @method parse * @param {String} range * @param {Function} [parseEndpoint] */ Range.parse = function(range, parse) { var endpoints = range.slice(1, -1).split(",", 2) var begin = endpoints[0] ? parse ? parse(endpoints[0]) : endpoints[0] : null var end = endpoints[1] ? parse ? parse(endpoints[1]) : endpoints[1] : null if (parse === Number && begin === null) begin = -Infinity if (parse === Number && end === null) end = Infinity return new Range(begin, end, range[0] + range[range.length - 1]) } /** * Merges two ranges and returns a range that encompasses both of them. * The ranges don't have to be intersecting. * * @example * Range.union(new Range(0, 5), new Range(5, 10)) // => new Range(0, 10) * Range.union(new Range(0, 10), new Range(5, 15)) // => new Range(0, 15) * * var a = new Range(-5, 0, "()") * var b = new Range(5, 10) * Range.union(a, b) // => new Range(-5, 10, "(]") * * @static * @method union * @param {String} union * @param {Range} a * @param {Range} b */ Range.union = function(a, b) { var aIsEmpty = a.isEmpty() var bIsEmpty = b.isEmpty() if (aIsEmpty && bIsEmpty) return new Range else if (aIsEmpty) return b else if (bIsEmpty) return a var begin = Range.compareBeginToBegin(a, b) <= 0 ? a : b var end = Range.compareEndToEnd(a, b) <= 0 ? b : a return new Range(begin.begin, end.end, begin.bounds[0] + end.bounds[1]) } function isInfinity(value) { return value === null || value === Infinity || value === -Infinity } function isValidBounds(bounds) { switch (bounds) { case "()": case "[]": case "[)": case "(]": return true default: return false } } // The less-than operator ensures coercion with valueOf. function compare(a, b) { return a < b ? -1 : b < a ? 1 : 0 } function stringify(value) { return isInfinity(value) ? "" : String(value) }