├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── doc └── API.md ├── index.js ├── package.json ├── test ├── index_test.js ├── mocha.opts └── tree_test.js └── tree.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [Makefile] 4 | indent_style = tab 5 | 6 | [*.js] 7 | indent_style = space 8 | indent_size = 2 9 | 10 | [*.json] 11 | indent_style = tab 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /*.tgz 3 | /tmp/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /*.tgz 2 | /.travis.yml 3 | /tmp/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "node" 5 | - "0.10" 6 | - "0.11" 7 | - "0.12" 8 | - "4" 9 | - "5" 10 | - "6" 11 | - "7" 12 | 13 | notifications: 14 | email: ["andri@dot.ee"] 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.7.2 (Feb 22, 2017) 2 | - Fixes `RangeTree` given a range that ended farther than ranges beginning after it. 3 | This occurring was dependent on how the binary tree laid itself out. 4 | Thanks, [Ross Allen](https://github.com/ssorallen), for reporting this! 5 | 6 | ## 1.7.1 (Jan 15, 2017) 7 | - Fixes searching `RangeTree` with an empty range in it. Empty ranges are now ignored. 8 | 9 | ## 1.7.0 (Aug 5, 2016) 10 | - Throws `RangeError` if the given bounds are not valid (not of the following: `[]`, `()`, `[)`, `(]`). 11 | Thanks, [Nikhil Benesch](https://github.com/benesch), for the hint! 12 | 13 | ## 1.6.0 (Aug 5, 2016) 14 | - Adds [`Range.compareBeginToEnd`][] 15 | - Adds support for calling `RangeTree.prototype.search` with range to find ranges that intersect. 16 | 17 | [`Range.compareBeginToEnd`]: https://github.com/moll/js-strange/blob/master/doc/API.md#Range.compareBeginToEnd 18 | 19 | ## 1.5.0 (Jul 12, 2016) 20 | - Adds [`Range.prototype.valueOf`][] to get a more primitive representation of a range. 21 | Useful with [Egal.js][egal] or other libraries that compare value objects by their `valueOf` output. 22 | 23 | [`Range.prototype.valueOf`]: https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.valueOf 24 | [egal]: https://github.com/moll/js-egal 25 | 26 | ## 1.4.0 (Jul 8, 2016) 27 | - Makes `Range.prototype` a valid empty `Range`. 28 | Allows you to use it as an empty range: 29 | 30 | ```javascript 31 | var EMPTY_RANGE = Range.prototype 32 | EMPTY_RANGE.isEmpty() // => true 33 | EMPTY_RANGE.contains(new Range(0, 1)) // => false 34 | ``` 35 | 36 | ## 1.3.0 (Jul 17, 2015) 37 | - Adds [`Range.compareBeginToBegin`][]. 38 | - Adds [`Range.compareEndToEnd`][]. 39 | - Adds [`Range.union`][]. 40 | - Adds [`Range.prototype.compareBegin`][]. 41 | - Adds [`Range.prototype.compareEnd`][]. 42 | 43 | - Sets numeric unbounded endpoints to `-Infinity`/`Infinity` if you pass 44 | `Number` to [`Range.parse`][] as the parse function. 45 | 46 | ```javascript 47 | Range.parse("[15,]", Number) // => new Range(15, Infinity) 48 | Range.parse("(,3.14]", Number) // => new Range(-Infinity, 3.14, "(]") 49 | ``` 50 | 51 | - Fixes [`Range.prototype.intersects`][] to handle exclusive unbounded ranges 52 | properly. 53 | 54 | - Adds [`RangeTree`] for creating an interval tree (augmented binary search 55 | tree) for searching ranges that intersect with a given value. 56 | 57 | [`Range.compareBeginToBegin`]: https://github.com/moll/js-strange/blob/master/doc/API.md#Range.compareBeginToBegin 58 | [`Range.compareEndToEnd`]: https://github.com/moll/js-strange/blob/master/doc/API.md#Range.compareEndToEnd 59 | [`Range.union`]: https://github.com/moll/js-strange/blob/master/doc/API.md#Range.union 60 | [`Range.prototype.compareBegin`]: https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.compareBegin 61 | [`Range.prototype.compareEnd`]: https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.compareEnd 62 | [`RangeTree`]: https://github.com/moll/js-strange/blob/master/doc/API.md#RangeTree 63 | 64 | ## 1.2.0 (Jul 4, 2015) 65 | - Adds [`Range.prototype.isBounded`][]. 66 | - Adds [`Range.prototype.isUnbounded`][]. 67 | - Adds [`Range.prototype.isFinite`][]. 68 | - Adds [`Range.prototype.isInfinite`][]. 69 | 70 | [`Range.prototype.isBounded`]: https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.isBounded 71 | [`Range.prototype.isUnbounded`]: https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.isUnbounded 72 | [`Range.prototype.isFinite`]: https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.isFinite 73 | [`Range.prototype.isInfinite`]: https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.isInfinite 74 | 75 | ## 1.1.0 (Jun 29, 2015) 76 | - Adds [`Range.prototype.isEmpty`][]. 77 | - Adds [`Range.prototype.intersects`][]. 78 | - Adds [`Range.prototype.contains`][]. 79 | 80 | [`Range.prototype.isEmpty`]: https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.isEmpty 81 | [`Range.prototype.intersects`]: https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.intersects 82 | [`Range.prototype.contains`]: https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.contains 83 | 84 | ## 1.0.0 (May 11, 2015) 85 | - Adds support for setting range bounds with `new Range(1, 3, "[)")` to set 86 | a left-closed, right-open range. 87 | - Adds [`Range.prototype.toString`][] to stringify a range. 88 | - Adds [`Range.prototype.toJSON`][] as an alias to `Range.prototype.toString`. 89 | - Adds [`Range.parse`][] to parse the stringified range. 90 | 91 | [`Range.prototype.toString`]: https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.toString 92 | [`Range.prototype.toJSON`]: https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.toJSON 93 | [`Range.parse`]: https://github.com/moll/js-strange/blob/master/doc/API.md#Range.parse 94 | 95 | ## 0.1.337 (Oct 9, 2013) 96 | - First release. Its future is an infinite exclusive range. 97 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | stRange.js 2 | Copyright (C) 2013– Andri Möll 3 | 4 | This program is free software: you can redistribute it and/or modify it under 5 | the terms of the GNU Affero General Public License as published by the Free 6 | Software Foundation, either version 3 of the License, or any later version. 7 | 8 | Additional permission under the GNU Affero GPL version 3 section 7: 9 | If you modify this Program, or any covered work, by linking or 10 | combining it with other code, such other code is not for that reason 11 | alone subject to any of the requirements of the GNU Affero GPL version 3. 12 | 13 | In summary: 14 | - You can use this program for no cost. 15 | - You can use this program for both personal and commercial reasons. 16 | - You do not have to share your own program's code which uses this program. 17 | - You have to share modifications (e.g bug-fixes) you've made to this program. 18 | 19 | For the full copy of the GNU Affero General Public License see: 20 | http://www.gnu.org/licenses. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NODE_OPTS = 2 | TEST_OPTS = 3 | 4 | # NOTE: Sorry, mocumentation is not yet published. 5 | MOCUMENT = ~/Documents/Mocumentation/bin/mocument 6 | MOCUMENT_OPTS = --type yui --title stRange.js 7 | GITHUB_URL = https://github.com/moll/js-strange 8 | 9 | love: 10 | @echo "Feel like makin' love." 11 | 12 | test: 13 | @node $(NODE_OPTS) ./node_modules/.bin/_mocha -R dot $(TEST_OPTS) 14 | 15 | spec: 16 | @node $(NODE_OPTS) ./node_modules/.bin/_mocha -R spec $(TEST_OPTS) 17 | 18 | autotest: 19 | @node $(NODE_OPTS) ./node_modules/.bin/_mocha -R dot --watch $(TEST_OPTS) 20 | 21 | autospec: 22 | @node $(NODE_OPTS) ./node_modules/.bin/_mocha -R spec --watch $(TEST_OPTS) 23 | 24 | pack: 25 | @file=$$(npm pack); echo "$$file"; tar tf "$$file" 26 | 27 | publish: 28 | npm publish 29 | 30 | tag: 31 | git tag "v$$(node -e 'console.log(require("./package").version)')" 32 | 33 | doc: doc.json 34 | @mkdir -p doc 35 | @$(MOCUMENT) $(MOCUMENT_OPTS) tmp/doc/data.json > doc/API.md 36 | 37 | toc: doc.json 38 | @$(MOCUMENT) $(MOCUMENT_OPTS) \ 39 | --template toc \ 40 | --var api_url=$(GITHUB_URL)/blob/master/doc/API.md \ 41 | tmp/doc/data.json > tmp/TOC.md 42 | 43 | @echo '/^API$$/,/^License$$/{/^API$$/{r tmp/TOC.md\na\\\n\\\n\\\n\n};/^License/!d;}' |\ 44 | sed -i "" -f /dev/stdin README.md 45 | 46 | doc.json: 47 | @mkdir -p tmp 48 | @yuidoc --exclude test,node_modules --parse-only --outdir tmp/doc . 49 | 50 | clean: 51 | rm -f *.tgz tmp 52 | npm prune --production 53 | 54 | .PHONY: love 55 | .PHONY: test spec autotest autospec 56 | .PHONY: pack publish tag 57 | .PHONY: doc toc doc.json 58 | .PHONY: clean 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | stRange.js 2 | ========== 3 | [![NPM version][npm-badge]](https://www.npmjs.com/package/strange) 4 | [![Build status][travis-badge]](https://travis-ci.org/moll/js-strange) 5 | 6 | stRange.js is a **range object** for JavaScript. Use it to have a single value 7 | type with two endpoints and their boundaries. Also implements an interval tree 8 | for quick lookups. Stringifies itself in the style of `[begin,end)` and allows 9 | you to parse a string back. Also useful with PostgreSQL. 10 | 11 | [npm-badge]: https://img.shields.io/npm/v/strange.svg 12 | [travis-badge]: https://travis-ci.org/moll/js-strange.png?branch=master 13 | 14 | 15 | Installing 16 | ---------- 17 | stRange.js follows [semantic versioning](http://semver.org/), so feel free to 18 | depend on its major version with something like `>= 1.0.0 < 2` (a.k.a `^1.0.0`). 19 | 20 | ### Installing on Node.js 21 | ``` 22 | npm install strange 23 | ``` 24 | 25 | ### Installing for the browser 26 | stRange.js doesn't yet have a build ready for the browser, but you might be able 27 | to use [Browserify][browserify] to have it run there till then. 28 | 29 | [browserify]: https://github.com/substack/node-browserify 30 | 31 | 32 | Using 33 | ----- 34 | Create a Range object by passing in a beginning and end: 35 | ```javascript 36 | var Range = require("strange") 37 | var range = new Range(1, 5) 38 | ``` 39 | 40 | Check if something is a range and use it: 41 | ```javascript 42 | var Range = require("strange") 43 | if (range instanceof Range) console.log(range.begin, range.end) 44 | ``` 45 | 46 | ### Bounds 47 | You can set a range's bounds by passing the bounds as a two-character string of 48 | parentheses as the 3rd argument: 49 | ```javascript 50 | new Range(1, 3, "[)") 51 | ``` 52 | 53 | Bounds signify whether the range includes or excludes that particular endpoint. 54 | The range above therefore includes numbers `>= 1 < 3`. 55 | 56 | Pair | Meaning 57 | -----|-------- 58 | `()` | open 59 | `[]` | closed 60 | `[)` | left-closed, right-open 61 | `(]` | left-open, right-closed 62 | 63 | 64 | ### Parsing 65 | To parse a range stringified by `Range.prototype.toString`, pass it to 66 | `Range.parse`: 67 | 68 | ```javascript 69 | Range.parse("[a,z)") // => new Range("a", "z", "[)") 70 | ``` 71 | 72 | To have stRange.js also parse the endpoints, pass a function to `Range.parse`: 73 | ```javascript 74 | Range.parse("[42,69]", Number) // => new Range(42, 69) 75 | ``` 76 | 77 | ### Using with PostgreSQL 78 | The string format used by stRange.js matches [PostgreSQL's range type 79 | format](http://www.postgresql.org/docs/9.4/static/rangetypes.html). You can 80 | therefore use stRange.js to parse and stringify ranges for your database. 81 | 82 | 83 | API 84 | --- 85 | For extended documentation on all functions, please see the 86 | [stRange.js API Documentation][api]. 87 | 88 | [api]: https://github.com/moll/js-strange/blob/master/doc/API.md 89 | 90 | ### [Range](https://github.com/moll/js-strange/blob/master/doc/API.md#Range) 91 | - [begin](https://github.com/moll/js-strange/blob/master/doc/API.md#range.begin) 92 | - [bounds](https://github.com/moll/js-strange/blob/master/doc/API.md#range.bounds) 93 | - [end](https://github.com/moll/js-strange/blob/master/doc/API.md#range.end) 94 | - [.prototype.compareBegin](https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.compareBegin)(begin) 95 | - [.prototype.compareEnd](https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.compareEnd)(end) 96 | - [.prototype.contains](https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.contains)(value) 97 | - [.prototype.intersects](https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.intersects)(other) 98 | - [.prototype.isBounded](https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.isBounded)() 99 | - [.prototype.isEmpty](https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.isEmpty)() 100 | - [.prototype.isFinite](https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.isFinite)() 101 | - [.prototype.isInfinite](https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.isInfinite)() 102 | - [.prototype.isUnbounded](https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.isUnbounded)() 103 | - [.prototype.toJSON](https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.toJSON)() 104 | - [.prototype.toString](https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.toString)() 105 | - [.prototype.valueOf](https://github.com/moll/js-strange/blob/master/doc/API.md#Range.prototype.valueOf)() 106 | - [.compareBeginToBegin](https://github.com/moll/js-strange/blob/master/doc/API.md#Range.compareBeginToBegin)(a, b) 107 | - [.compareBeginToEnd](https://github.com/moll/js-strange/blob/master/doc/API.md#Range.compareBeginToEnd)(a, b) 108 | - [.compareEndToEnd](https://github.com/moll/js-strange/blob/master/doc/API.md#Range.compareEndToEnd)(a, b) 109 | - [.parse](https://github.com/moll/js-strange/blob/master/doc/API.md#Range.parse)(range, [parseEndpoint]) 110 | - [.union](https://github.com/moll/js-strange/blob/master/doc/API.md#Range.union)(union, a, b) 111 | 112 | ### [RangeTree](https://github.com/moll/js-strange/blob/master/doc/API.md#RangeTree) 113 | - [.prototype.search](https://github.com/moll/js-strange/blob/master/doc/API.md#RangeTree.prototype.search)(valueOrRange) 114 | - [.from](https://github.com/moll/js-strange/blob/master/doc/API.md#RangeTree.from)(ranges) 115 | 116 | 117 | License 118 | ------- 119 | stRange.js is released under a *Lesser GNU Affero General Public License*, which in summary means: 120 | 121 | - You **can** use this program for **no cost**. 122 | - You **can** use this program for **both personal and commercial reasons**. 123 | - You **do not have to share your own program's code** which uses this program. 124 | - You **have to share modifications** (e.g bug-fixes) you've made to this program. 125 | 126 | For more convoluted language, see the `LICENSE` file. 127 | 128 | 129 | About 130 | ----- 131 | **[Andri Möll](http://themoll.com)** typed this and the code. 132 | [Monday Calendar](https://mondayapp.com) supported the engineering work. 133 | 134 | If you find stRange.js needs improving, please don't hesitate to type to me now at [andri@dot.ee](mailto:andri@dot.ee) or [create an issue online](https://github.com/moll/js-strange/issues). 135 | -------------------------------------------------------------------------------- /doc/API.md: -------------------------------------------------------------------------------- 1 | stRange.js API Documentation 2 | ============================ 3 | ### [Range](#Range) 4 | - [begin](#range.begin) 5 | - [bounds](#range.bounds) 6 | - [end](#range.end) 7 | - [.prototype.compareBegin](#Range.prototype.compareBegin)(begin) 8 | - [.prototype.compareEnd](#Range.prototype.compareEnd)(end) 9 | - [.prototype.contains](#Range.prototype.contains)(value) 10 | - [.prototype.intersects](#Range.prototype.intersects)(other) 11 | - [.prototype.isBounded](#Range.prototype.isBounded)() 12 | - [.prototype.isEmpty](#Range.prototype.isEmpty)() 13 | - [.prototype.isFinite](#Range.prototype.isFinite)() 14 | - [.prototype.isInfinite](#Range.prototype.isInfinite)() 15 | - [.prototype.isUnbounded](#Range.prototype.isUnbounded)() 16 | - [.prototype.toJSON](#Range.prototype.toJSON)() 17 | - [.prototype.toString](#Range.prototype.toString)() 18 | - [.prototype.valueOf](#Range.prototype.valueOf)() 19 | - [.compareBeginToBegin](#Range.compareBeginToBegin)(a, b) 20 | - [.compareBeginToEnd](#Range.compareBeginToEnd)(a, b) 21 | - [.compareEndToEnd](#Range.compareEndToEnd)(a, b) 22 | - [.parse](#Range.parse)(range, [parseEndpoint]) 23 | - [.union](#Range.union)(union, a, b) 24 | 25 | ### [RangeTree](#RangeTree) 26 | - [.prototype.search](#RangeTree.prototype.search)(valueOrRange) 27 | - [.from](#RangeTree.from)(ranges) 28 | 29 | 30 | Range(begin, end, [bounds]) 31 | --------------------------- 32 | Create a new range object with the given begin and end endpoints. 33 | You can also pass a two character string for bounds. Defaults to` "[]"` for 34 | an all inclusive range. 35 | 36 | You can use any value for endpoints. `Null` is considered infinity for 37 | values that don't have a special infinity type like `Number` has `Infinity`. 38 | 39 | An empty range is one where either of the endpoints is `undefined` (like `new 40 | Range`) or a range with two equivalent, but exculsive endpoints 41 | (`new Range(5, 5, "[)")`). 42 | 43 | **Import**: 44 | ```javascript 45 | var Range = require("strange") 46 | ``` 47 | 48 | **Examples**: 49 | ```javascript 50 | new Range(10, 20) // => {begin: 10, end: 20, bounds: "[]"} 51 | new Range(new Date(2000, 5, 18), new Date(2000, 5, 22)) 52 | ``` 53 | 54 | ### range.begin 55 | Range's beginning, or left endpoint. 56 | 57 | ### range.bounds 58 | Range's bounds. 59 | 60 | Bounds signify whether the range includes or excludes that particular 61 | endpoint. 62 | 63 | Pair | Meaning 64 | -----|-------- 65 | `()` | open 66 | `[]` | closed 67 | `[)` | left-closed, right-open 68 | `(]` | left-open, right-closed 69 | 70 | **Examples**: 71 | ```javascript 72 | new Range(1, 5).bounds // => "[]" 73 | new Range(1, 5, "[)").bounds // => "[)" 74 | ``` 75 | 76 | ### range.end 77 | Range's end, or right endpoint. 78 | 79 | ### Range.prototype.compareBegin(begin) 80 | Compares this range's beginning with the given value. 81 | Returns `-1` if this range begins before the given value, `0` if they're 82 | equal and `1` if this range begins after the given value. 83 | 84 | `null` is considered to signify negative infinity for non-numeric range 85 | endpoints. 86 | 87 | **Examples**: 88 | ```javascript 89 | new Range(0, 10).compareBegin(5) // => -1 90 | new Range(0, 10).compareBegin(0) // => 0 91 | new Range(5, 10).compareBegin(0) // => 1 92 | new Range(5, 10).compareBegin(null) // => 1 93 | ``` 94 | 95 | ### Range.prototype.compareEnd(end) 96 | Compares this range's end with the given value. 97 | Returns `-1` if this range ends before the given value, `0` if they're 98 | equal and `1` if this range ends after the given value. 99 | 100 | `null` is considered to signify positive infinity for non-numeric range 101 | endpoints. 102 | 103 | **Examples**: 104 | ```javascript 105 | new Range(0, 10).compareEnd(5) // => -1 106 | new Range(0, 10).compareEnd(10) // => 0 107 | new Range(0, 5).compareEnd(10) // => 1 108 | new Range(0, 5).compareEnd(null) // => -1 109 | ``` 110 | 111 | ### Range.prototype.contains(value) 112 | Check if a given value is contained within this range. 113 | Returns `true` or `false`. 114 | 115 | **Examples**: 116 | ```javascript 117 | new Range(0, 10).contains(5) // => true 118 | new Range(0, 10).contains(10) // => true 119 | new Range(0, 10, "[)").contains(10) // => false 120 | ``` 121 | 122 | ### Range.prototype.intersects(other) 123 | Check if this range intersects with another. 124 | Returns `true` or `false`. 125 | 126 | Ranges that have common points intersect. Ranges that are consecutive and 127 | with *inclusive* endpoints are also intersecting. An empty range will never 128 | intersect. 129 | 130 | **Examples**: 131 | ```javascript 132 | new Range(0, 10).intersects(new Range(5, 7)) // => true 133 | new Range(0, 10).intersects(new Range(10, 20)) // => true 134 | new Range(0, 10, "[)").intersects(new Range(10, 20)) // => false 135 | new Range(0, 10).intersects(new Range(20, 30)) // => false 136 | ``` 137 | 138 | ### Range.prototype.isBounded() 139 | Check whether the range is bounded. 140 | A bounded range is one where neither endpoint is `null` or `Infinity`. An 141 | empty range is considered bounded. 142 | 143 | **Examples**: 144 | ```javascript 145 | new Range().isBounded() // => true 146 | new Range(5, 5).isBounded() // => true 147 | new Range(null, new Date(2000, 5, 18).isBounded() // => false 148 | new Range(0, Infinity).isBounded() // => false 149 | new Range(-Infinity, Infinity).isBounded() // => false 150 | ``` 151 | 152 | ### Range.prototype.isEmpty() 153 | Check whether the range is empty. 154 | An empty range is one where either of the endpoints is `undefined` (like `new 155 | Range`) or a range with two equivalent, but exculsive endpoints 156 | (`new Range(5, 5, "[)")`). 157 | 158 | Equivalence is checked by using the `<` operators, so value objects will be 159 | coerced into something comparable by JavaScript. That usually means calling 160 | the object's `valueOf` function. 161 | 162 | **Examples**: 163 | ```javascript 164 | new Range().isEmpty() // => true 165 | new Range(5, 5, "[)").isEmpty() // => true 166 | new Range(1, 10).isEmpty() // => false 167 | ``` 168 | 169 | ### Range.prototype.isFinite() 170 | Alias of [`isBounded`](#Range.prototype.isBounded). 171 | 172 | ### Range.prototype.isInfinite() 173 | Alias of [`isUnbounded`](#Range.prototype.isUnbounded). 174 | 175 | ### Range.prototype.isUnbounded() 176 | Check whether the range is unbounded. 177 | An unbounded range is one where either endpoint is `null` or `Infinity`. An 178 | empty range is not considered unbounded. 179 | 180 | **Examples**: 181 | ```javascript 182 | new Range().isUnbounded() // => false 183 | new Range(5, 5).isUnbounded() // => false 184 | new Range(null, new Date(2000, 5, 18).isUnbounded() // => true 185 | new Range(0, Infinity).isUnbounded() // => true 186 | new Range(-Infinity, Infinity).isUnbounded() // => true 187 | ``` 188 | 189 | ### Range.prototype.toJSON() 190 | Alias of [`toString`](#Range.prototype.toString). 191 | Stringifies the range when passing it to `JSON.stringify`. 192 | This way you don't need to manually call `toString` when stringifying. 193 | 194 | **Examples**: 195 | ```javascript 196 | JSON.stringify(new Range(1, 10)) // "\"[1,10]\"" 197 | ``` 198 | 199 | ### Range.prototype.toString() 200 | Stringifies a range in `[a,b]` format. 201 | 202 | This happens to match the string format used by [PostgreSQL's range type 203 | format](http://www.postgresql.org/docs/9.4/static/rangetypes.html). You can 204 | therefore use stRange.js to parse and stringify ranges for your database. 205 | 206 | **Examples**: 207 | ```javascript 208 | new Range(1, 5).toString() // => "[1,5]" 209 | new Range(1, 10, "[)").toString() // => "[1,10)" 210 | ``` 211 | 212 | ### Range.prototype.valueOf() 213 | Returns an array of the endpoints and bounds. 214 | 215 | Useful with [Egal.js](https://github.com/moll/js-egal) or other libraries 216 | that compare value objects by their `valueOf` output. 217 | 218 | **Examples**: 219 | ```javascript 220 | new Range(1, 10, "[)").valueOf() // => [1, 10, "[)"] 221 | ``` 222 | 223 | ### Range.compareBeginToBegin(a, b) 224 | Compares two range's beginnings. 225 | Returns `-1` if `a` begins before `b` begins, `0` if they're equal and `1` 226 | if `a` begins after `b`. 227 | 228 | **Examples**: 229 | ```javascript 230 | Range.compareBeginToBegin(new Range(0, 10), new Range(5, 15)) // => -1 231 | Range.compareBeginToBegin(new Range(0, 10), new Range(0, 15)) // => 0 232 | Range.compareBeginToBegin(new Range(0, 10), new Range(0, 15, "()")) // => 1 233 | ``` 234 | 235 | ### Range.compareBeginToEnd(a, b) 236 | Compares the first range's beginning to the second's end. 237 | Returns `<0` if `a` begins before `b` ends, `0` if one starts where the other 238 | ends and `>1` if `a` begins after `b` ends. 239 | 240 | **Examples**: 241 | ```javascript 242 | Range.compareBeginToEnd(new Range(0, 10), new Range(0, 5)) // => -1 243 | Range.compareBeginToEnd(new Range(0, 10), new Range(-10, 0)) // => 0 244 | Range.compareBeginToEnd(new Range(0, 10), new Range(-10, -5)) // => 1 245 | ``` 246 | 247 | ### Range.compareEndToEnd(a, b) 248 | Compares two range's endings. 249 | Returns `-1` if `a` ends before `b` ends, `0` if they're equal and `1` 250 | if `a` ends after `b`. 251 | 252 | **Examples**: 253 | ```javascript 254 | Range.compareEndToEnd(new Range(0, 10), new Range(5, 15)) // => -1 255 | Range.compareEndToEnd(new Range(0, 10), new Range(5, 10)) // => 0 256 | Range.compareEndToEnd(new Range(0, 10), new Range(5, 10, "()")) // => 1 257 | ``` 258 | 259 | ### Range.parse(range, [parseEndpoint]) 260 | Parses a string stringified by 261 | [`Range.prototype.toString`](#Range.prototype.toString). 262 | 263 | To have it also parse the endpoints to something other than a string, pass 264 | a function as the second argument. 265 | 266 | If you pass `Number` as the _parse_ function and the endpoints are 267 | unbounded, they'll be set to `Infinity` for easier computation. 268 | 269 | **Examples**: 270 | ```javascript 271 | Range.parse("[a,z)") // => new Range("a", "z", "[)") 272 | Range.parse("[42,69]", Number) // => new Range(42, 69) 273 | Range.parse("[15,]", Number) // => new Range(15, Infinity) 274 | Range.parse("(,3.14]", Number) // => new Range(-Infinity, 3.14, "(]") 275 | ``` 276 | 277 | ### Range.union(union, a, b) 278 | Merges two ranges and returns a range that encompasses both of them. 279 | The ranges don't have to be intersecting. 280 | 281 | **Examples**: 282 | ```javascript 283 | Range.union(new Range(0, 5), new Range(5, 10)) // => new Range(0, 10) 284 | Range.union(new Range(0, 10), new Range(5, 15)) // => new Range(0, 15) 285 | 286 | var a = new Range(-5, 0, "()") 287 | var b = new Range(5, 10) 288 | Range.union(a, b) // => new Range(-5, 10, "(]") 289 | ``` 290 | 291 | 292 | RangeTree(ranges, left, right) 293 | ------------------------------ 294 | Create an interval tree node. 295 | 296 | For creating a binary search tree out of an array of ranges, you might want 297 | to use [`RangeTree.from`](#RangeTree.from). 298 | 299 | **Import**: 300 | ```javascript 301 | var RangeTree = require("strange/tree") 302 | ``` 303 | 304 | **Examples**: 305 | ```javascript 306 | var left = new RangeTree([new Range(-5, 0)]) 307 | var right = new RangeTree([new Range(5, 10)]) 308 | var root = new RangeTree([new Range(0, 5), new Range(0, 10)], left, right] 309 | root.search(7) // => [new Range(0, 10), new Range(5, 10)] 310 | ``` 311 | 312 | ### RangeTree.prototype.search(valueOrRange) 313 | Search for ranges that include the given value or, given a range, intersect 314 | with it. 315 | Returns an array of matches or an empty one if no range contained or 316 | intersected with the given value. 317 | 318 | **Examples**: 319 | ```javascript 320 | var tree = RangeTree.from([new Range(40, 50)]) 321 | tree.search(42) // => [new Range(40, 50)] 322 | tree.search(13) // => [] 323 | tree.search(new Range(30, 42)) // => [new Range(40, 50)] 324 | ``` 325 | 326 | ### RangeTree.from(ranges) 327 | Create an interval tree (implemented as an augmented binary search tree) 328 | from an array of ranges. 329 | Returns a [`RangeTree`](#RangeTree) you can search on. 330 | 331 | If you need to relate the found ranges to other data, add some properties 332 | directly to every range _or_ use JavaScript's `Map` or `WeakMap` to relate 333 | extra data to those range instances. 334 | 335 | **Examples**: 336 | ```javascript 337 | var ranges = [new Range(0, 10), new Range(20, 30), new Range(40, 50)] 338 | RangeTree.from(ranges).search(42) // => [new Range(40, 50)] 339 | ``` 340 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var INVALID_BOUNDS_ERR = "Invalid range bounds: " 2 | module.exports = Range 3 | 4 | /** 5 | * Create a new range object with the given begin and end endpoints. 6 | * You can also pass a two character string for bounds. Defaults to` "[]"` for 7 | * an all inclusive range. 8 | * 9 | * You can use any value for endpoints. `Null` is considered infinity for 10 | * values that don't have a special infinity type like `Number` has `Infinity`. 11 | * 12 | * An empty range is one where either of the endpoints is `undefined` (like `new 13 | * Range`) or a range with two equivalent, but exculsive endpoints 14 | * (`new Range(5, 5, "[)")`). 15 | * 16 | * **Import**: 17 | * ```javascript 18 | * var Range = require("strange") 19 | * ``` 20 | * 21 | * @example 22 | * new Range(10, 20) // => {begin: 10, end: 20, bounds: "[]"} 23 | * new Range(new Date(2000, 5, 18), new Date(2000, 5, 22)) 24 | * 25 | * @class Range 26 | * @constructor 27 | * @param {Object} begin 28 | * @param {Object} end 29 | * @param {String} [bounds="[]"] 30 | */ 31 | function Range(begin, end, bounds) { 32 | if (!(this instanceof Range)) return new Range(begin, end, bounds) 33 | 34 | /** 35 | * Range's beginning, or left endpoint. 36 | * 37 | * @property {Object} begin 38 | */ 39 | this.begin = begin 40 | 41 | /** 42 | * Range's end, or right endpoint. 43 | * 44 | * @property {Object} end 45 | */ 46 | this.end = end 47 | 48 | /** 49 | * Range's bounds. 50 | * 51 | * Bounds signify whether the range includes or excludes that particular 52 | * endpoint. 53 | * 54 | * Pair | Meaning 55 | * -----|-------- 56 | * `()` | open 57 | * `[]` | closed 58 | * `[)` | left-closed, right-open 59 | * `(]` | left-open, right-closed 60 | * 61 | * @example 62 | * new Range(1, 5).bounds // => "[]" 63 | * new Range(1, 5, "[)").bounds // => "[)" 64 | * 65 | * @property {String} bounds 66 | */ 67 | this.bounds = bounds = bounds === undefined ? "[]" : bounds 68 | if (!isValidBounds(bounds)) throw new RangeError(INVALID_BOUNDS_ERR + bounds) 69 | } 70 | 71 | Range.prototype.begin = undefined 72 | Range.prototype.end = undefined 73 | Range.prototype.bounds = "[]" 74 | 75 | /** 76 | * Compares this range's beginning with the given value. 77 | * Returns `-1` if this range begins before the given value, `0` if they're 78 | * equal and `1` if this range begins after the given value. 79 | * 80 | * `null` is considered to signify negative infinity for non-numeric range 81 | * endpoints. 82 | * 83 | * @example 84 | * new Range(0, 10).compareBegin(5) // => -1 85 | * new Range(0, 10).compareBegin(0) // => 0 86 | * new Range(5, 10).compareBegin(0) // => 1 87 | * new Range(5, 10).compareBegin(null) // => 1 88 | * 89 | * @method compareBegin 90 | * @param {Object} begin 91 | */ 92 | Range.prototype.compareBegin = function(begin) { 93 | var a = this.begin === null ? -Infinity : this.begin 94 | var b = begin === null ? -Infinity : begin 95 | return compare(a, b) || (this.bounds[0] == "[" ? 0 : 1) 96 | } 97 | 98 | /** 99 | * Compares this range's end with the given value. 100 | * Returns `-1` if this range ends before the given value, `0` if they're 101 | * equal and `1` if this range ends after the given value. 102 | * 103 | * `null` is considered to signify positive infinity for non-numeric range 104 | * endpoints. 105 | * 106 | * @example 107 | * new Range(0, 10).compareEnd(5) // => -1 108 | * new Range(0, 10).compareEnd(10) // => 0 109 | * new Range(0, 5).compareEnd(10) // => 1 110 | * new Range(0, 5).compareEnd(null) // => -1 111 | * 112 | * @method compareEnd 113 | * @param {Object} end 114 | */ 115 | Range.prototype.compareEnd = function(end) { 116 | var a = this.end === null ? Infinity : this.end 117 | var b = end === null ? Infinity : end 118 | return compare(a, b) || (this.bounds[1] == "]" ? 0 : -1) 119 | } 120 | 121 | /** 122 | * Check whether the range is empty. 123 | * An empty range is one where either of the endpoints is `undefined` (like `new 124 | * Range`) or a range with two equivalent, but exculsive endpoints 125 | * (`new Range(5, 5, "[)")`). 126 | * 127 | * Equivalence is checked by using the `<` operators, so value objects will be 128 | * coerced into something comparable by JavaScript. That usually means calling 129 | * the object's `valueOf` function. 130 | * 131 | * @example 132 | * new Range().isEmpty() // => true 133 | * new Range(5, 5, "[)").isEmpty() // => true 134 | * new Range(1, 10).isEmpty() // => false 135 | * 136 | * @method isEmpty 137 | */ 138 | Range.prototype.isEmpty = function() { 139 | var a = this.begin === null ? -Infinity : this.begin 140 | var b = this.end === null ? Infinity : this.end 141 | if (a === undefined || b === undefined) return true 142 | return this.bounds != "[]" && compare(a, b) === 0 143 | } 144 | 145 | /** 146 | * Check whether the range is bounded. 147 | * A bounded range is one where neither endpoint is `null` or `Infinity`. An 148 | * empty range is considered bounded. 149 | * 150 | * @example 151 | * new Range().isBounded() // => true 152 | * new Range(5, 5).isBounded() // => true 153 | * new Range(null, new Date(2000, 5, 18).isBounded() // => false 154 | * new Range(0, Infinity).isBounded() // => false 155 | * new Range(-Infinity, Infinity).isBounded() // => false 156 | * 157 | * @method isBounded 158 | */ 159 | Range.prototype.isBounded = function() { 160 | if (this.begin === undefined || this.end === undefined) return true 161 | return !(isInfinity(this.begin) || isInfinity(this.end)) 162 | } 163 | 164 | /** 165 | * @method isFinite 166 | * @alias isBounded 167 | */ 168 | Range.prototype.isFinite = Range.prototype.isBounded 169 | 170 | /** 171 | * Check whether the range is unbounded. 172 | * An unbounded range is one where either endpoint is `null` or `Infinity`. An 173 | * empty range is not considered unbounded. 174 | * 175 | * @example 176 | * new Range().isUnbounded() // => false 177 | * new Range(5, 5).isUnbounded() // => false 178 | * new Range(null, new Date(2000, 5, 18).isUnbounded() // => true 179 | * new Range(0, Infinity).isUnbounded() // => true 180 | * new Range(-Infinity, Infinity).isUnbounded() // => true 181 | * 182 | * @method isUnbounded 183 | */ 184 | Range.prototype.isUnbounded = function() { 185 | return !this.isBounded() 186 | } 187 | 188 | /** 189 | * @method isInfinite 190 | * @alias isUnbounded 191 | */ 192 | Range.prototype.isInfinite = Range.prototype.isUnbounded 193 | 194 | /** 195 | * Check if a given value is contained within this range. 196 | * Returns `true` or `false`. 197 | * 198 | * @example 199 | * new Range(0, 10).contains(5) // => true 200 | * new Range(0, 10).contains(10) // => true 201 | * new Range(0, 10, "[)").contains(10) // => false 202 | * 203 | * @method contains 204 | * @param {Object} value 205 | */ 206 | Range.prototype.contains = function(value) { 207 | var a = this.begin 208 | var b = this.end 209 | 210 | return ( 211 | (b === null || (this.bounds[1] === "]" ? value <= b : value < b)) && 212 | (a === null || (this.bounds[0] === "[" ? a <= value : a < value)) 213 | ) 214 | } 215 | 216 | /** 217 | * Check if this range intersects with another. 218 | * Returns `true` or `false`. 219 | * 220 | * Ranges that have common points intersect. Ranges that are consecutive and 221 | * with *inclusive* endpoints are also intersecting. An empty range will never 222 | * intersect. 223 | * 224 | * @example 225 | * new Range(0, 10).intersects(new Range(5, 7)) // => true 226 | * new Range(0, 10).intersects(new Range(10, 20)) // => true 227 | * new Range(0, 10, "[)").intersects(new Range(10, 20)) // => false 228 | * new Range(0, 10).intersects(new Range(20, 30)) // => false 229 | * 230 | * @method intersects 231 | * @param {Object} other 232 | */ 233 | Range.prototype.intersects = function(other) { 234 | if (this.isEmpty()) return false 235 | if (other.isEmpty()) return false 236 | 237 | return ( 238 | Range.compareBeginToEnd(this, other) <= 0 && 239 | Range.compareBeginToEnd(other, this) <= 0 240 | ) 241 | } 242 | 243 | /** 244 | * Returns an array of the endpoints and bounds. 245 | * 246 | * Useful with [Egal.js](https://github.com/moll/js-egal) or other libraries 247 | * that compare value objects by their `valueOf` output. 248 | * 249 | * @example 250 | * new Range(1, 10, "[)").valueOf() // => [1, 10, "[)"] 251 | * 252 | * @method valueOf 253 | */ 254 | Range.prototype.valueOf = function() { 255 | return [this.begin, this.end, this.bounds] 256 | } 257 | 258 | /** 259 | * Stringifies a range in `[a,b]` format. 260 | * 261 | * This happens to match the string format used by [PostgreSQL's range type 262 | * format](http://www.postgresql.org/docs/9.4/static/rangetypes.html). You can 263 | * therefore use stRange.js to parse and stringify ranges for your database. 264 | * 265 | * @example 266 | * new Range(1, 5).toString() // => "[1,5]" 267 | * new Range(1, 10, "[)").toString() // => "[1,10)" 268 | * 269 | * @method toString 270 | */ 271 | Range.prototype.toString = function() { 272 | // FIXME: How to serialize an empty range with undefined endpoints? 273 | var a = stringify(this.begin) 274 | var b = stringify(this.end) 275 | return this.bounds[0] + a + "," + b + this.bounds[1] 276 | } 277 | 278 | /** 279 | * Stringifies the range when passing it to `JSON.stringify`. 280 | * This way you don't need to manually call `toString` when stringifying. 281 | * 282 | * @example 283 | * JSON.stringify(new Range(1, 10)) // "\"[1,10]\"" 284 | * 285 | * @method toJSON 286 | * @alias toString 287 | */ 288 | Range.prototype.toJSON = Range.prototype.toString 289 | Range.prototype.inspect = Range.prototype.toString 290 | 291 | /** 292 | * Compares two range's beginnings. 293 | * Returns `-1` if `a` begins before `b` begins, `0` if they're equal and `1` 294 | * if `a` begins after `b`. 295 | * 296 | * @example 297 | * Range.compareBeginToBegin(new Range(0, 10), new Range(5, 15)) // => -1 298 | * Range.compareBeginToBegin(new Range(0, 10), new Range(0, 15)) // => 0 299 | * Range.compareBeginToBegin(new Range(0, 10), new Range(0, 15, "()")) // => 1 300 | * 301 | * @static 302 | * @method compareBeginToBegin 303 | * @param {Object} a 304 | * @param {Object} b 305 | */ 306 | Range.compareBeginToBegin = function(a, b) { 307 | var aBegin = a.begin === null ? -Infinity : a.begin 308 | var bBegin = b.begin === null ? -Infinity : b.begin 309 | if (a.bounds[0] === b.bounds[0]) return compare(aBegin, bBegin) 310 | else return compare(aBegin, bBegin) || (b.bounds[0] === "(" ? -1 : 1) 311 | } 312 | 313 | /** 314 | * Compares the first range's beginning to the second's end. 315 | * Returns `<0` if `a` begins before `b` ends, `0` if one starts where the other 316 | * ends and `>1` if `a` begins after `b` ends. 317 | * 318 | * @example 319 | * Range.compareBeginToEnd(new Range(0, 10), new Range(0, 5)) // => -1 320 | * Range.compareBeginToEnd(new Range(0, 10), new Range(-10, 0)) // => 0 321 | * Range.compareBeginToEnd(new Range(0, 10), new Range(-10, -5)) // => 1 322 | * 323 | * @static 324 | * @method compareBeginToEnd 325 | * @param {Object} a 326 | * @param {Object} b 327 | */ 328 | Range.compareBeginToEnd = function(a, b) { 329 | var aBegin = a.begin === null ? -Infinity : a.begin 330 | var bEnd = b.end === null ? Infinity : b.end 331 | if (a.bounds[0] === "[" && b.bounds[1] === "]") return compare(aBegin, bEnd) 332 | else return compare(aBegin, bEnd) || 1 333 | } 334 | 335 | /** 336 | * Compares two range's endings. 337 | * Returns `-1` if `a` ends before `b` ends, `0` if they're equal and `1` 338 | * if `a` ends after `b`. 339 | * 340 | * @example 341 | * Range.compareEndToEnd(new Range(0, 10), new Range(5, 15)) // => -1 342 | * Range.compareEndToEnd(new Range(0, 10), new Range(5, 10)) // => 0 343 | * Range.compareEndToEnd(new Range(0, 10), new Range(5, 10, "()")) // => 1 344 | * 345 | * @static 346 | * @method compareEndToEnd 347 | * @param {Object} a 348 | * @param {Object} b 349 | */ 350 | Range.compareEndToEnd = function(a, b) { 351 | var aEnd = a.end === null ? Infinity : a.end 352 | var bEnd = b.end === null ? Infinity : b.end 353 | if (a.bounds[1] === b.bounds[1]) return compare(aEnd, bEnd) 354 | else return compare(aEnd, bEnd) || (a.bounds[1] === ")" ? -1 : 1) 355 | } 356 | 357 | /** 358 | * Parses a string stringified by 359 | * [`Range.prototype.toString`](#Range.prototype.toString). 360 | * 361 | * To have it also parse the endpoints to something other than a string, pass 362 | * a function as the second argument. 363 | * 364 | * If you pass `Number` as the _parse_ function and the endpoints are 365 | * unbounded, they'll be set to `Infinity` for easier computation. 366 | * 367 | * @example 368 | * Range.parse("[a,z)") // => new Range("a", "z", "[)") 369 | * Range.parse("[42,69]", Number) // => new Range(42, 69) 370 | * Range.parse("[15,]", Number) // => new Range(15, Infinity) 371 | * Range.parse("(,3.14]", Number) // => new Range(-Infinity, 3.14, "(]") 372 | * 373 | * @static 374 | * @method parse 375 | * @param {String} range 376 | * @param {Function} [parseEndpoint] 377 | */ 378 | Range.parse = function(range, parse) { 379 | var endpoints = range.slice(1, -1).split(",", 2) 380 | var begin = endpoints[0] ? parse ? parse(endpoints[0]) : endpoints[0] : null 381 | var end = endpoints[1] ? parse ? parse(endpoints[1]) : endpoints[1] : null 382 | if (parse === Number && begin === null) begin = -Infinity 383 | if (parse === Number && end === null) end = Infinity 384 | return new Range(begin, end, range[0] + range[range.length - 1]) 385 | } 386 | 387 | /** 388 | * Merges two ranges and returns a range that encompasses both of them. 389 | * The ranges don't have to be intersecting. 390 | * 391 | * @example 392 | * Range.union(new Range(0, 5), new Range(5, 10)) // => new Range(0, 10) 393 | * Range.union(new Range(0, 10), new Range(5, 15)) // => new Range(0, 15) 394 | * 395 | * var a = new Range(-5, 0, "()") 396 | * var b = new Range(5, 10) 397 | * Range.union(a, b) // => new Range(-5, 10, "(]") 398 | * 399 | * @static 400 | * @method union 401 | * @param {String} union 402 | * @param {Range} a 403 | * @param {Range} b 404 | */ 405 | Range.union = function(a, b) { 406 | var aIsEmpty = a.isEmpty() 407 | var bIsEmpty = b.isEmpty() 408 | if (aIsEmpty && bIsEmpty) return new Range 409 | else if (aIsEmpty) return b 410 | else if (bIsEmpty) return a 411 | 412 | var begin = Range.compareBeginToBegin(a, b) <= 0 ? a : b 413 | var end = Range.compareEndToEnd(a, b) <= 0 ? b : a 414 | return new Range(begin.begin, end.end, begin.bounds[0] + end.bounds[1]) 415 | } 416 | 417 | function isInfinity(value) { 418 | return value === null || value === Infinity || value === -Infinity 419 | } 420 | 421 | function isValidBounds(bounds) { 422 | switch (bounds) { 423 | case "()": 424 | case "[]": 425 | case "[)": 426 | case "(]": return true 427 | default: return false 428 | } 429 | } 430 | 431 | // The less-than operator ensures coercion with valueOf. 432 | function compare(a, b) { return a < b ? -1 : b < a ? 1 : 0 } 433 | function stringify(value) { return isInfinity(value) ? "" : String(value) } 434 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "strange", 3 | "version": "1.7.2", 4 | "description": "Range aka interval object. Supports exclusive and infinite ranges. Comes with an interval tree (augmented binary search tree).", 5 | "keywords": [ 6 | "range", 7 | "ranges", 8 | "interval", 9 | "interval-tree", 10 | "bst", 11 | "integer", 12 | "integers", 13 | "sort", 14 | "bounds", 15 | "math" 16 | ], 17 | "homepage": "https://github.com/moll/js-strange", 18 | "bugs": "https://github.com/moll/js-strange/issues", 19 | 20 | "author": { 21 | "name": "Andri Möll", 22 | "email": "andri@dot.ee", 23 | "url": "http://themoll.com" 24 | }, 25 | 26 | "repository": { 27 | "type": "git", 28 | "url": "git://github.com/moll/js-strange.git" 29 | }, 30 | 31 | "licenses": [{ 32 | "type": "LAGPL", 33 | "url": "https://github.com/moll/js-strange/blob/master/LICENSE" 34 | }], 35 | 36 | "main": "index.js", 37 | "scripts": {"test": "make test"}, 38 | 39 | "devDependencies": { 40 | "mocha": ">= 2.0.0 < 3", 41 | "must": ">= 0.13.0 < 0.14" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/index_test.js: -------------------------------------------------------------------------------- 1 | var Range = require("..") 2 | 3 | describe("Range", function() { 4 | describe("new", function() { 5 | it("must return an instance of Range", function() { 6 | new Range().must.be.an.instanceof(Range) 7 | }) 8 | 9 | it("must set begin and end with default bounds", function() { 10 | var range = new Range(42, 69) 11 | range.begin.must.equal(42) 12 | range.end.must.equal(69) 13 | range.bounds.must.equal("[]") 14 | }) 15 | 16 | 17 | it("must set begin and end to undefined if not given", function() { 18 | var range = new Range 19 | range.must.have.property("begin", undefined) 20 | range.must.have.property("end", undefined) 21 | range.bounds.must.equal("[]") 22 | }) 23 | 24 | ;["[]", "()", "[)", "(]"].forEach(function(bounds) { 25 | it("must set bounds given " + bounds, function() { 26 | var range = new Range(42, 69, bounds) 27 | range.begin.must.equal(42) 28 | range.end.must.equal(69) 29 | range.bounds.must.equal(bounds) 30 | }) 31 | }) 32 | 33 | it("must throw RangeError given invalid bounds", function() { 34 | var err 35 | try { new Range(42, 69, ")(") } catch (ex) { err = ex } 36 | err.must.be.an.error(RangeError, /bounds/i) 37 | }) 38 | }) 39 | 40 | describe("when called as a function", function() { 41 | it("must return an instance of Range", function() { 42 | Range().must.be.an.instanceof(Range) 43 | }) 44 | 45 | it("must set begin and end with given bounds", function() { 46 | var range = Range(42, 69, "()") 47 | range.begin.must.equal(42) 48 | range.end.must.equal(69) 49 | range.bounds.must.equal("()") 50 | }) 51 | }) 52 | 53 | describe(".prototype", function() { 54 | it("must be a valid range", function() { 55 | Range.prototype.isEmpty().must.be.true() 56 | Range.prototype.contains(new Range(0, 1)).must.be.false() 57 | }) 58 | }) 59 | 60 | describe(".prototype.isEmpty", function() { 61 | it("must return true given a range with undefined endpoints", function() { 62 | new Range(undefined, undefined).isEmpty().must.be.true() 63 | new Range(null, undefined).isEmpty().must.be.true() 64 | new Range(undefined, null).isEmpty().must.be.true() 65 | }) 66 | 67 | it("must return false given an unbounded range", function() { 68 | new Range(null, null).isEmpty().must.be.false() 69 | }) 70 | 71 | it("must return false given an unbounded exclusive range", function() { 72 | new Range(null, null, "()").isEmpty().must.be.false() 73 | }) 74 | 75 | it("must return true if exclusive with equivalent endpoints", function() { 76 | new Range(1, 1, "()").isEmpty().must.be.true() 77 | new Range(1, 1, "[)").isEmpty().must.be.true() 78 | new Range(1, 1, "(]").isEmpty().must.be.true() 79 | }) 80 | 81 | it("must return false if inclusive with equivalent endpoints", function() { 82 | new Range(1, 1, "[]").isEmpty().must.be.false() 83 | }) 84 | 85 | it("must return false if exclusive with non-equivalent endpoints", 86 | function() { 87 | new Range(1, 2, "()").isEmpty().must.be.false() 88 | }) 89 | }) 90 | 91 | describe(".prototype.isBounded", function() { 92 | it("must return true given a range with undefined endpoints", function() { 93 | new Range(undefined, undefined).isBounded().must.be.true() 94 | }) 95 | 96 | it("must return true given a range with undefined endpoints", function() { 97 | new Range(null, undefined).isBounded().must.be.true() 98 | new Range(undefined, null).isBounded().must.be.true() 99 | }) 100 | 101 | it("must return false given an unbounded range", function() { 102 | new Range(null, null).isBounded().must.be.false() 103 | }) 104 | 105 | it("must return false given a left unbounded range", function() { 106 | new Range(null, 1).isBounded().must.be.false() 107 | }) 108 | 109 | it("must return false given a right unbounded range", function() { 110 | new Range(1, null).isBounded().must.be.false() 111 | }) 112 | 113 | it("must return false given an unbounded range with Infinity", function() { 114 | new Range(-Infinity, Infinity).isBounded().must.be.false() 115 | }) 116 | 117 | it("must return false given a left unbounded range with Infinity", 118 | function() { 119 | new Range(-Infinity, 1).isBounded().must.be.false() 120 | }) 121 | 122 | it("must return false given a right unbounded range with Infinity", 123 | function() { 124 | new Range(1, Infinity).isBounded().must.be.false() 125 | }) 126 | 127 | it("must return true given a bounded range", function() { 128 | new Range(1, 1).isBounded().must.be.true() 129 | }) 130 | }) 131 | 132 | describe(".prototype.isUnbounded", function() { 133 | it("must return false given a range with undefined endpoints", function() { 134 | new Range(undefined, undefined).isUnbounded().must.be.false() 135 | }) 136 | 137 | it("must return false given a range with undefined endpoints", function() { 138 | new Range(null, undefined).isUnbounded().must.be.false() 139 | new Range(undefined, null).isUnbounded().must.be.false() 140 | }) 141 | 142 | it("must return true given an unbounded range", function() { 143 | new Range(null, null).isUnbounded().must.be.true() 144 | }) 145 | 146 | it("must return true given a left unbounded range", function() { 147 | new Range(null, 1).isUnbounded().must.be.true() 148 | }) 149 | 150 | it("must return true given a right unbounded range", function() { 151 | new Range(1, null).isUnbounded().must.be.true() 152 | }) 153 | 154 | it("must return true given an unbounded range with Infinity", function() { 155 | new Range(-Infinity, Infinity).isUnbounded().must.be.true() 156 | }) 157 | 158 | it("must return true given a left unbounded range with Infinity", 159 | function() { 160 | new Range(-Infinity, 1).isUnbounded().must.be.true() 161 | }) 162 | 163 | it("must return true given a right unbounded range with Infinity", 164 | function() { 165 | new Range(1, Infinity).isUnbounded().must.be.true() 166 | }) 167 | 168 | it("must return false given a bounded range", function() { 169 | new Range(1, 1).isUnbounded().must.be.false() 170 | }) 171 | }) 172 | 173 | describe(".prototype.isFinite", function() { 174 | it("must be an alias to Range.prototype.isBounded", function() { 175 | Range.prototype.isFinite.must.equal(Range.prototype.isBounded) 176 | }) 177 | }) 178 | 179 | describe(".prototype.isInfinite", function() { 180 | it("must be an alias to Range.prototype.isInfinite", function() { 181 | Range.prototype.isInfinite.must.equal(Range.prototype.isUnbounded) 182 | }) 183 | }) 184 | 185 | describe(".prototype.compareBegin", function() { 186 | describe("with inclusive bound", function() { 187 | it("must return 0 if given endpoint equal", function() { 188 | new Range(0, 10, "[)").compareBegin(0).must.equal(0) 189 | }) 190 | 191 | it("must return 0 if unbounded and equal", function() { 192 | new Range(null, 10, "[)").compareBegin(null).must.equal(0) 193 | }) 194 | 195 | it("must return -1 if given endpoint greater than", function() { 196 | new Range(0, 10, "[)").compareBegin(11).must.equal(-1) 197 | }) 198 | 199 | it("must return -1 if unbounded", function() { 200 | new Range(null, 10, "[)").compareBegin(11).must.equal(-1) 201 | }) 202 | 203 | it("must return 1 if given endpoint less than", function() { 204 | new Range(0, 10, "[)").compareBegin(-1).must.equal(1) 205 | }) 206 | 207 | it("must return 1 given null", function() { 208 | new Range(0, 10, "[)").compareBegin(null).must.equal(1) 209 | }) 210 | }) 211 | 212 | describe("with exclusive bound", function() { 213 | it("must return -1 if given endpoint greater than", function() { 214 | new Range(0, 10, "(]").compareBegin(1).must.equal(-1) 215 | }) 216 | 217 | it("must return -1 if unbounded", function() { 218 | new Range(null, 10, "(]").compareBegin(11).must.equal(-1) 219 | }) 220 | 221 | it("must return 1 if given endpoint equal", function() { 222 | new Range(0, 10, "(]").compareBegin(0).must.equal(1) 223 | }) 224 | 225 | it("must return 1 if unbounded and equal", function() { 226 | new Range(null, 10, "(]").compareBegin(null).must.equal(1) 227 | }) 228 | 229 | it("must return 1 if given endpoint less than", function() { 230 | new Range(0, 10, "(]").compareBegin(-1).must.equal(1) 231 | }) 232 | 233 | it("must return 1 given null", function() { 234 | new Range(0, 10, "(]").compareBegin(null).must.equal(1) 235 | }) 236 | }) 237 | }) 238 | 239 | describe(".prototype.compareEnd", function() { 240 | describe("with inclusive bound", function() { 241 | it("must return 0 if given endpoint equal", function() { 242 | new Range(0, 10, "(]").compareEnd(10).must.equal(0) 243 | }) 244 | 245 | it("must return 0 if unbounded and equal", function() { 246 | new Range(0, null, "(]").compareEnd(null).must.equal(0) 247 | }) 248 | 249 | it("must return -1 if given endpoint greater than", function() { 250 | new Range(0, 10, "(]").compareEnd(11).must.equal(-1) 251 | }) 252 | 253 | it("must return -1 if given null", function() { 254 | new Range(0, 10, "(]").compareEnd(null).must.equal(-1) 255 | }) 256 | 257 | it("must return 1 if given endpoint less than", function() { 258 | new Range(0, 10, "(]").compareEnd(9).must.equal(1) 259 | }) 260 | 261 | it("must return 1 if unbounded", function() { 262 | new Range(0, null, "(]").compareEnd(-1).must.equal(1) 263 | }) 264 | }) 265 | 266 | describe("with exclusive bound", function() { 267 | it("must return 1 if given endpoint equal", function() { 268 | new Range(0, 10, "[)").compareEnd(0).must.equal(1) 269 | }) 270 | 271 | it("must return -1 if given endpoint greater than", function() { 272 | new Range(0, 10, "[)").compareEnd(11).must.equal(-1) 273 | }) 274 | 275 | it("must return -1 if unbounded and equal", function() { 276 | new Range(10, null, "[)").compareEnd(null).must.equal(-1) 277 | }) 278 | 279 | it("must return -1 if given null", function() { 280 | new Range(0, 10, "(]").compareEnd(null).must.equal(-1) 281 | }) 282 | 283 | it("must return 1 if given endpoint less than", function() { 284 | new Range(0, 10, "[)").compareEnd(-1).must.equal(1) 285 | }) 286 | 287 | it("must return 1 if unbounded", function() { 288 | new Range(0, null, "[)").compareEnd(-1).must.equal(1) 289 | }) 290 | }) 291 | }) 292 | 293 | describe(".prototype.contains", function() { 294 | it("must return true when contained", function() { 295 | new Range(10, 20).contains(15).must.be.true() 296 | }) 297 | 298 | it("must return false when intersecting, but of zero size", function() { 299 | new Range(5, 5, "[)").contains(5).must.be.false() 300 | }) 301 | 302 | it("must return true when on inclusive boundary", function() { 303 | new Range(0, 10, "(]").contains(10).must.be.true() 304 | new Range(0, 10, "[)").contains(0).must.be.true() 305 | }) 306 | 307 | it("must return false when on exclusive boundary", function() { 308 | new Range(0, 10, "[)").contains(10).must.be.false() 309 | new Range(0, 10, "(]").contains(0).must.be.false() 310 | }) 311 | 312 | it("must return false when empty", function() { 313 | new Range().contains(5).must.be.false() 314 | }) 315 | 316 | it("must return true if one endpoint unbounded", function() { 317 | new Range(0, null).contains(10).must.be.true() 318 | new Range(null, 0).contains(-10).must.be.true() 319 | }) 320 | 321 | it("must return true if one endpoint unbounded and on inclusive boundary", 322 | function() { 323 | new Range(0, null).contains(0).must.be.true() 324 | new Range(null, 0).contains(0).must.be.true() 325 | }) 326 | 327 | it("must return false if one endpoint unbounded and on exclusive boundary", 328 | function() { 329 | new Range(0, null, "(]").contains(0).must.be.false() 330 | new Range(null, 0, "[)").contains(0).must.be.false() 331 | }) 332 | 333 | it("must return false if one endpoint undefined", function() { 334 | new Range(0, undefined).contains(0).must.be.false() 335 | new Range(undefined, 0).contains(0).must.be.false() 336 | }) 337 | }) 338 | 339 | describe(".prototype.intersects", function() { 340 | function intersects(a, b) { 341 | var result = a.intersects(b) 342 | b.intersects(a).must.equal(result) 343 | return result 344 | } 345 | 346 | function testWithBounds(bounds) { 347 | it("must return true when equal", function() { 348 | var a = new Range(10, 20, bounds) 349 | var b = new Range(10, 20, bounds) 350 | intersects(a, b).must.be.true() 351 | }) 352 | 353 | it("must return true when intersecting", function() { 354 | var a = new Range(10, 20, bounds) 355 | var b = new Range(5, 15, bounds) 356 | intersects(a, b).must.be.true() 357 | }) 358 | 359 | it("must return false when intersecting, but one empty", function() { 360 | var a = new Range(0, 10, bounds) 361 | var b = new Range(undefined, undefined, bounds) 362 | intersects(a, b).must.be.false() 363 | }) 364 | 365 | it("must return false when intersecting, but one of zero size", 366 | function() { 367 | var a = new Range(0, 10, bounds) 368 | var b = new Range(5, 5, "[)") 369 | intersects(a, b).must.be.false() 370 | }) 371 | 372 | it("must return false when not intersecting", function() { 373 | var a = new Range(0, 10, bounds) 374 | var b = new Range(30, 40, bounds) 375 | intersects(a, b).must.be.false() 376 | }) 377 | 378 | it("must return true when intersecting and one unbounded", function() { 379 | var a = new Range(0, 10, bounds) 380 | var b = new Range(5, null, bounds) 381 | intersects(a, b).must.be.true() 382 | }) 383 | 384 | it("must return true when one encloses the other", function() { 385 | var a = new Range(0, 10, bounds) 386 | var b = new Range(3, 6, bounds) 387 | intersects(a, b).must.be.true() 388 | }) 389 | 390 | it("must return true when one encloses the other and is unbounded", 391 | function() { 392 | var a = new Range(0, 10, bounds) 393 | var b = new Range(null, null, bounds) 394 | intersects(a, b).must.be.true() 395 | }) 396 | } 397 | 398 | describe("with inclusive bounds", testWithBounds.bind(null, "[]")) 399 | describe("with exclusive bounds", testWithBounds.bind(null, "()")) 400 | 401 | it("must return true when one encloses the other, but one exclusive", 402 | function() { 403 | var a = new Range(0, 10) 404 | var b = new Range(3, 6, "()") 405 | intersects(a, b).must.be.true() 406 | }) 407 | 408 | it("must return true when consecutive", function() { 409 | var a = new Range(0, 10) 410 | var b = new Range(10, 20) 411 | intersects(a, b).must.be.true() 412 | }) 413 | 414 | it("must return true when one at the boundary of other", function() { 415 | var a = new Range(0, 10) 416 | var b = new Range(10, 10) 417 | intersects(a, b).must.be.true() 418 | }) 419 | 420 | it("must return false when consecutive, but end exclusive", function() { 421 | var a = new Range(0, 10, "[)") 422 | var b = new Range(10, 20) 423 | intersects(a, b).must.be.false() 424 | }) 425 | 426 | it("must return false when consecutive, but begin exclusive", function() { 427 | var a = new Range(0, 10) 428 | var b = new Range(10, 20, "(]") 429 | intersects(a, b).must.be.false() 430 | }) 431 | 432 | it("must return false when consecutive, but both exclusive", 433 | function() { 434 | var a = new Range(0, 10, "[)") 435 | var b = new Range(10, 20, "(]") 436 | intersects(a, b).must.be.false() 437 | }) 438 | }) 439 | 440 | describe(".prototype.valueOf", function() { 441 | it("must return an array", function() { 442 | new Range(42, 69, "()").valueOf().must.eql([42, 69, "()"]) 443 | }) 444 | }) 445 | 446 | describe(".prototype.toString", function() { 447 | it("must stringify range with given bounds", function() { 448 | new Range(42, 69, "()").toString().must.equal("(42,69)") 449 | new Range(42, 69, "[]").toString().must.equal("[42,69]") 450 | new Range(42, 69, "[)").toString().must.equal("[42,69)") 451 | new Range(42, 69, "(]").toString().must.equal("(42,69]") 452 | }) 453 | 454 | it("must stringify begin", function() { 455 | // Having valueOf too ensures the value is stringied before string 456 | // concatenation. 457 | function Value(value) { this.value = value } 458 | Value.prototype.valueOf = function() { return null } 459 | Value.prototype.toString = function() { return this.value } 460 | 461 | var a = new Value(42) 462 | var b = new Value(69) 463 | new Range(a, b).toString().must.equal("[42,69]") 464 | }) 465 | 466 | it("must stringify null endpoint as empty", function() { 467 | new Range(42, null).toString().must.equal("[42,]") 468 | new Range(null, 42).toString().must.equal("[,42]") 469 | new Range(null, null).toString().must.equal("[,]") 470 | }) 471 | 472 | it("must stringify Infinity as empty", function() { 473 | new Range(42, Infinity).toString().must.equal("[42,]") 474 | new Range(-Infinity, 42).toString().must.equal("[,42]") 475 | new Range(-Infinity, Infinity).toString().must.equal("[,]") 476 | }) 477 | }) 478 | 479 | describe(".prototype.toJSON", function() { 480 | it("must be an alias to toString", function() { 481 | Range.prototype.toJSON.must.equal(Range.prototype.toString) 482 | }) 483 | }) 484 | 485 | describe(".prototype.inspect", function() { 486 | it("must be an alias to toString", function() { 487 | Range.prototype.inspect.must.equal(Range.prototype.toString) 488 | }) 489 | }) 490 | 491 | describe(".compareBeginToBegin", function() { 492 | function compare(a, b) { 493 | var result = Range.compareBeginToBegin(a, b) 494 | Range.compareBeginToBegin(b, a).must.eql(result * -1) 495 | return result 496 | } 497 | 498 | function testWithBounds(bounds) { 499 | it("must return 0 if equal", function() { 500 | var a = new Range(0, 10, bounds) 501 | var b = new Range(0, 5, bounds) 502 | compare(a, b).must.equal(0) 503 | }) 504 | 505 | it("must return -1 if less", function() { 506 | var a = new Range(-1, 10, bounds) 507 | var b = new Range(0, 10, bounds) 508 | compare(a, b).must.equal(-1) 509 | }) 510 | 511 | it("must return 0 if equal and unbounded", function() { 512 | var a = new Range(null, 10, bounds) 513 | var b = new Range(null, 5, bounds) 514 | compare(a, b).must.equal(0) 515 | }) 516 | 517 | it("must return -1 if one unbounded", function() { 518 | var a = new Range(null, 10, bounds) 519 | var b = new Range(0, 10, bounds) 520 | compare(a, b).must.equal(-1) 521 | }) 522 | } 523 | 524 | describe("with inclusive bounds", testWithBounds.bind(null, "[]")) 525 | describe("with exclusive bounds", testWithBounds.bind(null, "()")) 526 | 527 | describe("with different bounds", function() { 528 | it("must return -1 if equal", function() { 529 | compare(new Range(0, 10, "[]"), new Range(0, 10, "()")).must.equal(-1) 530 | }) 531 | 532 | it("must return -1 if less", function() { 533 | compare(new Range(-1, 10, "[]"), new Range(0, 10, "()")).must.equal(-1) 534 | compare(new Range(-1, 10, "()"), new Range(0, 10, "[]")).must.equal(-1) 535 | }) 536 | 537 | it("must return -1 if equal and unbounded", function() { 538 | var a = new Range(null, 10, "[]") 539 | var b = new Range(null, 10, "(]") 540 | compare(a, b).must.equal(-1) 541 | }) 542 | }) 543 | }) 544 | 545 | describe(".compareBeginToEnd", function() { 546 | var compare = Range.compareBeginToEnd 547 | 548 | describe("with inclusive bounds", function() { 549 | it("must return 0 if equal", function() { 550 | var a = new Range(0, 10, "[]") 551 | var b = new Range(-5, 0, "[]") 552 | compare(a, b).must.equal(0) 553 | }) 554 | 555 | it("must return -1 if less", function() { 556 | var a = new Range(-1, 10, "[]") 557 | var b = new Range(-5, 0, "[]") 558 | compare(a, b).must.equal(-1) 559 | }) 560 | 561 | it("must return -1 if unbounded", function() { 562 | var a = new Range(null, 10, "[]") 563 | var b = new Range(20, null, "[]") 564 | compare(a, b).must.equal(-1) 565 | }) 566 | 567 | it("must return -1 if begin unbounded", function() { 568 | var a = new Range(null, 10, "[]") 569 | var b = new Range(0, 5, "[]") 570 | compare(a, b).must.equal(-1) 571 | }) 572 | 573 | it("must return -1 if end unbounded", function() { 574 | var a = new Range(0, 10, "[]") 575 | var b = new Range(0, null, "[]") 576 | compare(a, b).must.equal(-1) 577 | }) 578 | }) 579 | 580 | describe("with exclusive bounds", function() { 581 | it("must return 1 if equal", function() { 582 | var a = new Range(0, 10, "()") 583 | var b = new Range(-5, 0, "()") 584 | compare(a, b).must.equal(1) 585 | }) 586 | 587 | it("must return -1 if less", function() { 588 | var a = new Range(-1, 10, "()") 589 | var b = new Range(-5, 0, "()") 590 | compare(a, b).must.equal(-1) 591 | }) 592 | 593 | it("must return -1 if unbounded", function() { 594 | var a = new Range(null, 10, "()") 595 | var b = new Range(20, null, "()") 596 | compare(a, b).must.equal(-1) 597 | }) 598 | 599 | it("must return -1 if begin unbounded", function() { 600 | var a = new Range(null, 10, "()") 601 | var b = new Range(0, 5, "()") 602 | compare(a, b).must.equal(-1) 603 | }) 604 | 605 | it("must return -1 if end unbounded", function() { 606 | var a = new Range(0, 10, "()") 607 | var b = new Range(0, null, "()") 608 | compare(a, b).must.equal(-1) 609 | }) 610 | }) 611 | 612 | describe("with different bounds", function() { 613 | it("must return 1 if begin inclusive and end exclusive", function() { 614 | compare(new Range(0, 10, "[]"), new Range(-10, 0, "()")).must.equal(1) 615 | }) 616 | 617 | it("must return 1 if begin exclusive and end inclusive", function() { 618 | compare(new Range(0, 10, "()"), new Range(-10, 0, "[]")).must.equal(1) 619 | }) 620 | 621 | it("must return -1 if less", function() { 622 | compare(new Range(-1, 10, "[]"), new Range(-10, 0, "()")).must.equal(-1) 623 | compare(new Range(-1, 10, "()"), new Range(-10, 0, "[]")).must.equal(-1) 624 | }) 625 | }) 626 | }) 627 | 628 | describe(".compareEndToEnd", function() { 629 | function compare(a, b) { 630 | var result = Range.compareEndToEnd(a, b) 631 | Range.compareEndToEnd(b, a).must.eql(result * -1) 632 | return result 633 | } 634 | 635 | function testWithBounds(bounds) { 636 | it("must return 0 if equal", function() { 637 | var a = new Range(0, 10, bounds) 638 | var b = new Range(5, 10, bounds) 639 | compare(a, b).must.equal(0) 640 | }) 641 | 642 | it("must return 0 if equal and unbounded", function() { 643 | var a = new Range(0, null, bounds) 644 | var b = new Range(5, null, bounds) 645 | compare(a, b).must.equal(0) 646 | }) 647 | 648 | it("must return -1 if less", function() { 649 | var a = new Range(0, 9, bounds) 650 | var b = new Range(0, 10, bounds) 651 | compare(a, b).must.equal(-1) 652 | }) 653 | 654 | it("must return -1 if one unbounded", function() { 655 | var a = new Range(0, 10, bounds) 656 | var b = new Range(0, null, bounds) 657 | compare(a, b).must.equal(-1) 658 | }) 659 | } 660 | 661 | describe("with inclusive bounds", testWithBounds.bind(null, "[]")) 662 | describe("with exclusive bounds", testWithBounds.bind(null, "()")) 663 | 664 | describe("with different bounds", function() { 665 | it("must return -1 if equal", function() { 666 | compare(new Range(0, 10, "()"), new Range(0, 10, "(]")).must.equal(-1) 667 | }) 668 | 669 | it("must return -1 if less", function() { 670 | compare(new Range(0, 9, "(]"), new Range(0, 10, "()")).must.equal(-1) 671 | compare(new Range(0, 9, "()"), new Range(0, 10, "(]")).must.equal(-1) 672 | }) 673 | 674 | it("must return -1 if equal and unbounded", function() { 675 | var a = new Range(0, null, "[)") 676 | var b = new Range(0, null, "[]") 677 | compare(a, b).must.equal(-1) 678 | }) 679 | }) 680 | }) 681 | 682 | describe(".parse", function() { 683 | it("must parse string with bounds", function() { 684 | Range.parse("[a,z]").must.eql(new Range("a", "z", "[]")) 685 | Range.parse("(a,z)").must.eql(new Range("a", "z", "()")) 686 | Range.parse("[a,z)").must.eql(new Range("a", "z", "[)")) 687 | Range.parse("(a,z]").must.eql(new Range("a", "z", "(]")) 688 | }) 689 | 690 | it("must parse endpoint with given function", function() { 691 | Range.parse("[42,69]", Number).must.eql(new Range(42, 69)) 692 | }) 693 | 694 | it("must parse string with infinite bounds", function() { 695 | Range.parse("[a,]").must.eql(new Range("a", null)) 696 | Range.parse("[,z]").must.eql(new Range(null, "z")) 697 | }) 698 | 699 | it("must parse string with infinite bounds given parse function", 700 | function() { 701 | var toUpperCase = Function.call.bind(String.prototype.toUpperCase) 702 | Range.parse("[a,]", toUpperCase).must.eql(new Range("A", null)) 703 | Range.parse("[,b]", toUpperCase).must.eql(new Range(null, "B")) 704 | Range.parse("(,)", toUpperCase).must.eql(new Range(null, null, "()")) 705 | }) 706 | 707 | it("must parse string with infinite bounds given Number", function() { 708 | Range.parse("[42,]", Number).must.eql(new Range(42, Infinity)) 709 | Range.parse("[,69]", Number).must.eql(new Range(-Infinity, 69)) 710 | Range.parse("(,)", Number).must.eql(new Range(-Infinity, Infinity, "()")) 711 | }) 712 | }) 713 | 714 | describe(".union", function() { 715 | function union(a, b) { 716 | var result = Range.union(a, b) 717 | Range.union(b, a).must.eql(result) 718 | return result 719 | } 720 | 721 | it("must return a union given one empty range", function() { 722 | var a = new Range(0, 5) 723 | var b = new Range(10, 10, "[)") 724 | union(a, b).must.eql(new Range(0, 5)) 725 | }) 726 | 727 | it("must return a union given two empty ranges", function() { 728 | var a = new Range(5, 5, "[)") 729 | var b = new Range(10, 10, "[)") 730 | union(a, b).must.eql(new Range) 731 | }) 732 | 733 | it("must return a union given same range twice", function() { 734 | var a = new Range(0, 10, "[)") 735 | union(a, a).must.eql(a) 736 | }) 737 | 738 | describe("with inclusive bounds", function() { 739 | it("must return a union given two intersecting ranges", function() { 740 | var a = new Range(0, 11, "[]") 741 | var b = new Range(9, 20, "[]") 742 | union(a, b).must.eql(new Range(0, 20, "[]")) 743 | }) 744 | 745 | it("must return a union given two consecutive ranges", function() { 746 | var a = new Range(0, 10, "[]") 747 | var b = new Range(10, 20, "[]") 748 | union(a, b).must.eql(new Range(0, 20, "[]")) 749 | }) 750 | 751 | it("must return a union given two non-consecutive ranges", function() { 752 | var a = new Range(0, 5, "[]") 753 | var b = new Range(15, 20, "[]") 754 | union(a, b).must.eql(new Range(0, 20, "[]")) 755 | }) 756 | }) 757 | 758 | describe("with exclusive bounds", function() { 759 | it("must return a union given two intersecting ranges", function() { 760 | var a = new Range(0, 11, "()") 761 | var b = new Range(9, 20, "()") 762 | union(a, b).must.eql(new Range(0, 20, "()")) 763 | }) 764 | 765 | it("must return a union given two close, but non-consecutive ranges", 766 | function() { 767 | var a = new Range(0, 10, "()") 768 | var b = new Range(10, 20, "()") 769 | union(a, b).must.eql(new Range(0, 20, "()")) 770 | }) 771 | 772 | it("must return a union given two non-consecutive ranges", function() { 773 | var a = new Range(0, 5, "()") 774 | var b = new Range(15, 20, "()") 775 | union(a, b).must.eql(new Range(0, 20, "()")) 776 | }) 777 | }) 778 | 779 | describe("with different bounds", function() { 780 | it("must return a union given two consecutive ranges", function() { 781 | var a = new Range(0, 10, "(]") 782 | var b = new Range(10, 20, "[)") 783 | union(a, b).must.eql(new Range(0, 20, "()")) 784 | }) 785 | 786 | it("must return a union given two non-consecutive ranges", function() { 787 | var a = new Range(0, 5, "[)") 788 | var b = new Range(15, 20, "[)") 789 | union(a, b).must.eql(new Range(0, 20, "[)")) 790 | }) 791 | }) 792 | }) 793 | }) 794 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | --require must 3 | -------------------------------------------------------------------------------- /test/tree_test.js: -------------------------------------------------------------------------------- 1 | var Range = require("..") 2 | var RangeTree = require("../tree") 3 | 4 | describe("RangeTree", function() { 5 | describe(".prototype.search", function() { 6 | describe("given value", function() { 7 | it("must return an empty array if empty", function() { 8 | var tree = RangeTree.from([]) 9 | tree.search(42).must.be.empty() 10 | }) 11 | 12 | it("must return an empty array if not found within 1", function() { 13 | var tree = RangeTree.from([new Range(0, 10)]) 14 | tree.search(42).must.be.empty() 15 | }) 16 | 17 | it("must return an empty array if with exclusive left boundary", 18 | function() { 19 | var tree = RangeTree.from([new Range(0, 10, "(]")]) 20 | tree.search(0).must.be.empty() 21 | }) 22 | 23 | it("must return an empty array if with exclusive right boundary", 24 | function() { 25 | var tree = RangeTree.from([new Range(0, 10, "[)")]) 26 | tree.search(10).must.be.empty() 27 | }) 28 | 29 | it("must return an empty array if not found within 3", function() { 30 | var ranges = [new Range(0, 10), new Range(15, 25), new Range(30, 40)] 31 | var tree = RangeTree.from(ranges) 32 | tree.search(42).must.be.empty() 33 | }) 34 | 35 | it("must return range if within 1", function() { 36 | var tree = RangeTree.from([new Range(0, 10)]) 37 | tree.search(5).must.eql([new Range(0, 10)]) 38 | }) 39 | 40 | it("must return range if within 2", function() { 41 | var tree = RangeTree.from([new Range(0, 10), new Range(20, 30)]) 42 | tree.search(5).must.eql([new Range(0, 10)]) 43 | tree.search(25).must.eql([new Range(20, 30)]) 44 | }) 45 | 46 | it("must return range if within 3", function() { 47 | var ranges = [new Range(0, 10), new Range(20, 30), new Range(40, 50)] 48 | var tree = RangeTree.from(ranges) 49 | tree.search(5).must.eql([new Range(0, 10)]) 50 | tree.search(25).must.eql([new Range(20, 30)]) 51 | tree.search(45).must.eql([new Range(40, 50)]) 52 | }) 53 | 54 | it("must return range if multiple with equal begin", function() { 55 | var tree = RangeTree.from([ 56 | new Range(0, 20), 57 | new Range(10, 25), 58 | new Range(15, 35), 59 | new Range(15, 40), 60 | ]) 61 | 62 | tree.search(37).must.eql([new Range(15, 40)]) 63 | }) 64 | 65 | it("must return multiple ranges if within 3", function() { 66 | var ranges = [new Range(0, 20), new Range(15, 35), new Range(30, 40)] 67 | var tree = RangeTree.from(ranges) 68 | tree.search(35).must.eql([new Range(15, 35), new Range(30, 40)]) 69 | }) 70 | 71 | it("must return multiple ranges with equal begin", function() { 72 | // Use plenty of ranges to ensure a naive dividing tree construction 73 | // puts the duplicate ranges further apart. 74 | var tree = RangeTree.from([ 75 | new Range(0, 20), 76 | new Range(10, 25), 77 | new Range(15, 35), 78 | new Range(15, 40), 79 | new Range(35, 40), 80 | new Range(35, 50) 81 | ]) 82 | 83 | tree.search(30).must.eql([new Range(15, 35), new Range(15, 40)]) 84 | }) 85 | 86 | it("must return multiple ranges if on two sides", function() { 87 | var tree = RangeTree.from([ 88 | new Range(0, 15), 89 | new Range(5, 5), 90 | new Range(10, 25) 91 | ]) 92 | 93 | tree.search(13).must.eql([new Range(0, 15), new Range(10, 25)]) 94 | }) 95 | 96 | it("must return multiple ranges if all equal", function() { 97 | var tree = RangeTree.from([new Range(5, 15), new Range(5, 15)]) 98 | tree.search(10).must.eql([new Range(5, 15), new Range(5, 15)]) 99 | }) 100 | 101 | it("must return multiple ranges if one equal but exclusive", function() { 102 | var tree = RangeTree.from([ 103 | new Range(5, 15), 104 | new Range(5, 10, "[)"), 105 | new Range(10, 15, "(]"), 106 | new Range(10, 20) 107 | ]) 108 | 109 | tree.search(10).must.eql([new Range(5, 15), new Range(10, 20)]) 110 | }) 111 | 112 | it("must return multiple ranges ordered by ascending end", function() { 113 | var tree = RangeTree.from([ 114 | new Range(0, 10), 115 | new Range(0, 15), 116 | new Range(0, 5) 117 | ]) 118 | 119 | tree.search(5).must.eql([ 120 | new Range(0, 5), 121 | new Range(0, 10), 122 | new Range(0, 15) 123 | ]) 124 | }) 125 | 126 | // This was a bug noticed on Jan 15, 2017 where an empty range caused 127 | // none to be matched. 128 | it("must return range if one empty", function() { 129 | var tree = RangeTree.from([new Range, new Range(0, 10)]) 130 | tree.search(5).must.eql([new Range(0, 10)]) 131 | }) 132 | }) 133 | 134 | describe("given range", function() { 135 | it("must return an empty array if empty", function() { 136 | var tree = RangeTree.from([]) 137 | tree.search(new Range(42, 69)).must.be.empty() 138 | }) 139 | 140 | it("must return an empty array if not found within 1", function() { 141 | var tree = RangeTree.from([new Range(0, 10)]) 142 | tree.search(new Range(42, 69)).must.be.empty() 143 | }) 144 | 145 | it("must return an empty array if with exclusive left boundary", 146 | function() { 147 | var tree = RangeTree.from([new Range(0, 10, "(]")]) 148 | tree.search(new Range(-10, 0)).must.be.empty() 149 | }) 150 | 151 | it("must return an empty array if with exclusive right boundary", 152 | function() { 153 | var tree = RangeTree.from([new Range(0, 10, "[)")]) 154 | tree.search(new Range(10, 20)).must.be.empty() 155 | }) 156 | 157 | it("must return an empty array if not found within 3", function() { 158 | var ranges = [new Range(0, 10), new Range(15, 25), new Range(30, 40)] 159 | var tree = RangeTree.from(ranges) 160 | tree.search(new Range(42, 69)).must.be.empty() 161 | }) 162 | 163 | it("must return range if within 1", function() { 164 | var tree = RangeTree.from([new Range(0, 10)]) 165 | tree.search(new Range(2, 7)).must.eql([new Range(0, 10)]) 166 | }) 167 | 168 | it("must return range if within 2", function() { 169 | var tree = RangeTree.from([new Range(0, 10), new Range(20, 30)]) 170 | tree.search(new Range(2, 7)).must.eql([new Range(0, 10)]) 171 | tree.search(new Range(22, 27)).must.eql([new Range(20, 30)]) 172 | }) 173 | 174 | it("must return range if within 3", function() { 175 | var ranges = [new Range(0, 10), new Range(20, 30), new Range(40, 50)] 176 | var tree = RangeTree.from(ranges) 177 | tree.search(new Range(2, 7)).must.eql([new Range(0, 10)]) 178 | tree.search(new Range(22, 27)).must.eql([new Range(20, 30)]) 179 | tree.search(new Range(42, 47)).must.eql([new Range(40, 50)]) 180 | }) 181 | 182 | it("must return range if multiple with equal begin", function() { 183 | var tree = RangeTree.from([ 184 | new Range(0, 20), 185 | new Range(10, 25), 186 | new Range(15, 35), 187 | new Range(15, 40), 188 | ]) 189 | 190 | tree.search(new Range(37, 39)).must.eql([new Range(15, 40)]) 191 | }) 192 | 193 | it("must return multiple ranges if within 3", function() { 194 | var ranges = [new Range(0, 20), new Range(15, 35), new Range(30, 40)] 195 | var tree = RangeTree.from(ranges) 196 | 197 | tree.search(new Range(32, 37)).must.eql([ 198 | new Range(15, 35), 199 | new Range(30, 40) 200 | ]) 201 | }) 202 | 203 | it("must return multiple ranges with equal begin", function() { 204 | // Use plenty of ranges to ensure a naive dividing tree construction 205 | // puts the duplicate ranges further apart. 206 | var tree = RangeTree.from([ 207 | new Range(0, 20), 208 | new Range(10, 25), 209 | new Range(15, 35), 210 | new Range(15, 40), 211 | new Range(35, 40), 212 | new Range(35, 50) 213 | ]) 214 | 215 | tree.search(new Range(30, 32)).must.eql([ 216 | new Range(15, 35), 217 | new Range(15, 40) 218 | ]) 219 | }) 220 | 221 | it("must return multiple ranges if on two sides", function() { 222 | var tree = RangeTree.from([ 223 | new Range(0, 15), 224 | new Range(5, 5), 225 | new Range(10, 25) 226 | ]) 227 | 228 | tree.search(new Range(6, 13)).must.eql([ 229 | new Range(0, 15), 230 | new Range(10, 25) 231 | ]) 232 | }) 233 | 234 | it("must return multiple ranges if all equal", function() { 235 | var tree = RangeTree.from([new Range(5, 15), new Range(5, 15)]) 236 | 237 | tree.search(new Range(7, 13)).must.eql([ 238 | new Range(5, 15), 239 | new Range(5, 15) 240 | ]) 241 | }) 242 | 243 | it("must return multiple ranges if one equal but exclusive", function() { 244 | var tree = RangeTree.from([ 245 | new Range(5, 15), 246 | new Range(5, 10, "[)"), 247 | new Range(10, 15, "(]"), 248 | new Range(10, 20) 249 | ]) 250 | 251 | tree.search(new Range(10, 10)).must.eql([ 252 | new Range(5, 15), 253 | new Range(10, 20) 254 | ]) 255 | }) 256 | 257 | it("must return multiple ranges ordered by ascending end", function() { 258 | var tree = RangeTree.from([ 259 | new Range(0, 10), 260 | new Range(0, 15), 261 | new Range(0, 5) 262 | ]) 263 | 264 | tree.search(new Range(3, 7)).must.eql([ 265 | new Range(0, 5), 266 | new Range(0, 10), 267 | new Range(0, 15) 268 | ]) 269 | }) 270 | 271 | // This was a bug #2 (https://github.com/moll/js-strange/issues/2) where 272 | // a node's total range end was not set properly because it considered 273 | // the range(s) to the right of it to always end farther. 274 | it("must return range farther than later ranges", function() { 275 | var tree = RangeTree.from([ 276 | new Range(-5, 5), 277 | new Range(0, 15), 278 | new Range(5, 10) 279 | ]) 280 | 281 | tree.search(new Range(12, 13)).must.eql([new Range(0, 15)]) 282 | }) 283 | 284 | // The original bug report for the bug above had long timestamps that 285 | // I normalized below. Keeping them here as more insurance. 286 | it("must return range farther than later ranges in original bug report", 287 | function() { 288 | var tree = RangeTree.from([ 289 | new Range(10, 20, "[)"), 290 | new Range(20, 30, "[)"), 291 | new Range(30, 40, "[)"), 292 | new Range(40, 50, "[)"), 293 | new Range(50, 60, "[)"), 294 | new Range(80, 100, "[)"), 295 | new Range(85, 86, "[)"), 296 | ]) 297 | 298 | var matches = tree.search(new Range(90, 95, "[)")) 299 | matches.must.eql([new Range(80, 100, "[)")]) 300 | }) 301 | 302 | it("must not return ranges if exclusive", function() { 303 | var tree = RangeTree.from([ 304 | new Range(0, 10, "[)"), 305 | new Range(20, 30, "(]") 306 | ]) 307 | 308 | tree.search(new Range(10, 20)).must.eql([]) 309 | }) 310 | 311 | it("must not return ranges if exclusive and side by side", function() { 312 | var tree = RangeTree.from([new Range(0, 10, "[)")]) 313 | tree.search(new Range(10, 15, "[)")).must.eql([]) 314 | }) 315 | }) 316 | }) 317 | }) 318 | -------------------------------------------------------------------------------- /tree.js: -------------------------------------------------------------------------------- 1 | var Range = require("./") 2 | var union = Range.union 3 | var compareBeginToBegin = Range.compareBeginToBegin 4 | var compareBeginToEnd = Range.compareBeginToEnd 5 | var compareEndToEnd = Range.compareEndToEnd 6 | var concat = Array.prototype.concat.bind(Array.prototype) 7 | var EMPTY_ARR = Array.prototype 8 | module.exports = RangeTree 9 | 10 | /** 11 | * Create an interval tree node. 12 | * 13 | * For creating a binary search tree out of an array of ranges, you might want 14 | * to use [`RangeTree.from`](#RangeTree.from). 15 | * 16 | * **Import**: 17 | * ```javascript 18 | * var RangeTree = require("strange/tree") 19 | * ``` 20 | * 21 | * @example 22 | * var left = new RangeTree([new Range(-5, 0)]) 23 | * var right = new RangeTree([new Range(5, 10)]) 24 | * var root = new RangeTree([new Range(0, 5), new Range(0, 10)], left, right] 25 | * root.search(7) // => [new Range(0, 10), new Range(5, 10)] 26 | * 27 | * @class RangeTree 28 | * @constructor 29 | * @param {Object|Object[]} ranges 30 | * @param {RangeTree} left 31 | * @param {RangeTree} right 32 | */ 33 | function RangeTree(keys, left, right) { 34 | // Store the longest range first. 35 | if (Array.isArray(keys)) this.keys = keys.slice().sort(reverseCompareEndToEnd) 36 | else this.keys = [keys] 37 | 38 | this.left = left || null 39 | this.right = right || null 40 | 41 | // Remember, the topmost key has the longest range. 42 | var a = this.left ? this.left.range : this.keys[0] 43 | var b = this.right ? union(this.keys[0], this.right.range) : this.keys[0] 44 | this.range = union(a, b) 45 | } 46 | 47 | /** 48 | * Create an interval tree (implemented as an augmented binary search tree) 49 | * from an array of ranges. 50 | * Returns a [`RangeTree`](#RangeTree) you can search on. 51 | * 52 | * If you need to relate the found ranges to other data, add some properties 53 | * directly to every range _or_ use JavaScript's `Map` or `WeakMap` to relate 54 | * extra data to those range instances. 55 | * 56 | * @example 57 | * var ranges = [new Range(0, 10), new Range(20, 30), new Range(40, 50)] 58 | * RangeTree.from(ranges).search(42) // => [new Range(40, 50)] 59 | * 60 | * @static 61 | * @method from 62 | * @param {Range[]} ranges 63 | */ 64 | RangeTree.from = function(ranges) { 65 | ranges = ranges.filter(isNotEmpty) 66 | ranges = ranges.sort(compareBeginToBegin) 67 | ranges = ranges.map(arrayify) 68 | ranges = ranges.reduce(dedupe, []) 69 | return this.new(ranges) 70 | } 71 | 72 | RangeTree.new = function(ranges) { 73 | switch (ranges.length) { 74 | case 0: return new this(new Range) 75 | case 1: return new this(ranges[0]) 76 | case 2: return new this(ranges[0], null, new this(ranges[1])) 77 | 78 | default: 79 | var middle = Math.floor(ranges.length / 2) 80 | var left = this.new(ranges.slice(0, middle)) 81 | var right = this.new(ranges.slice(middle + 1)) 82 | return new this(ranges[middle], left, right) 83 | } 84 | } 85 | 86 | /** 87 | * Search for ranges that include the given value or, given a range, intersect 88 | * with it. 89 | * Returns an array of matches or an empty one if no range contained or 90 | * intersected with the given value. 91 | * 92 | * @example 93 | * var tree = RangeTree.from([new Range(40, 50)]) 94 | * tree.search(42) // => [new Range(40, 50)] 95 | * tree.search(13) // => [] 96 | * tree.search(new Range(30, 42)) // => [new Range(40, 50)] 97 | * 98 | * @method search 99 | * @param {Object} valueOrRange 100 | */ 101 | RangeTree.prototype.search = function(value) { 102 | if (value instanceof Range) return this.searchByRange(value) 103 | else return this.searchByValue(value) 104 | } 105 | 106 | RangeTree.prototype.searchByValue = function(value) { 107 | if (!this.range.contains(value)) return EMPTY_ARR 108 | 109 | var ownPosition = this.keys[0].compareBegin(value) 110 | 111 | return concat( 112 | this.left ? this.left.searchByValue(value) : EMPTY_ARR, 113 | ownPosition <= 0 ? this.searchOwnByValue(value) : EMPTY_ARR, 114 | this.right && ownPosition < 0 ? this.right.searchByValue(value) : EMPTY_ARR 115 | ) 116 | } 117 | 118 | RangeTree.prototype.searchByRange = function(range) { 119 | if (!this.range.intersects(range)) return EMPTY_ARR 120 | 121 | var ownPosition = compareBeginToEnd(this.keys[0], range) 122 | 123 | return concat( 124 | this.left ? this.left.searchByRange(range) : EMPTY_ARR, 125 | ownPosition <= 0 ? this.searchOwnByRange(range) : EMPTY_ARR, 126 | this.right && ownPosition < 0 ? this.right.searchByRange(range) : EMPTY_ARR 127 | ) 128 | } 129 | 130 | // Sort ranges in ascending order for beauty. O:) 131 | RangeTree.prototype.searchOwnByValue = function(value) { 132 | return take(this.keys, function(r) { return r.contains(value) }).reverse() 133 | } 134 | 135 | RangeTree.prototype.searchOwnByRange = function(range) { 136 | return take(this.keys, function(r) { return r.intersects(range) }).reverse() 137 | } 138 | 139 | function dedupe(ranges, range) { 140 | var last = ranges[ranges.length - 1] 141 | 142 | if (last != null && compareBeginToBegin(last[0], range[0]) === 0) 143 | last.push(range[0]) 144 | else 145 | ranges.push(range) 146 | 147 | return ranges 148 | } 149 | 150 | function take(arr, fn) { 151 | var values = [] 152 | for (var i = 0; i < arr.length && fn(arr[i], i); ++i) values.push(arr[i]) 153 | return values 154 | } 155 | 156 | function arrayify(obj) { return [obj] } 157 | function isNotEmpty(range) { return !range.isEmpty() } 158 | function reverseCompareEndToEnd(a, b) { return compareEndToEnd(a, b) * -1 } 159 | --------------------------------------------------------------------------------