├── package.json ├── LICENCE ├── README.md ├── test.js └── IntervalTree.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "SHIN Suzuki ", 3 | "name": "interval-tree", 4 | "description": "interval tree in javascript", 5 | "version": "0.1.0", 6 | "keywords": ["tree", "interval", "range", "algorithm"], 7 | "repository": { 8 | "type": "git", 9 | "url": "github:shinout/interval-tree.git" 10 | }, 11 | "main": "IntervalTree.js", 12 | "scripts": { 13 | "test": "test.js" 14 | }, 15 | "engines": { 16 | "node": ">=0.4" 17 | }, 18 | 19 | "dependencies": { 20 | "sortedlist" : ">=0.1.0" 21 | }, 22 | 23 | "devDependencies": {} 24 | } 25 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012 SHIN Suzuki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | interval-tree 2 | ========== 3 | interval tree in JavaScript 4 | 5 | **this repository is no more maintenanced.** 6 | **use [interval-tree2](https://github.com/shinout/interval-tree2)** 7 | 8 | 9 | ### Installation ### 10 | git clone git://github.com/shinout/interval-tree.git 11 | 12 | OR 13 | 14 | npm install interval-tree 15 | 16 | ### Usage ### 17 | var IntervalTree = require('interval-tree'); 18 | 19 | // add interval data 20 | 21 | var itree = new IntervalTree(300); // 300 : the center of the tree 22 | 23 | itree.add([22, 56, 'foo']); 24 | itree.add([44, 199, 'bar']); 25 | 26 | // search 1: get overlapped regions from one point 27 | var results = itree.search(103); 28 | 29 | results.forEach(function(result) { 30 | console.log(result.data); // overlapped range data 31 | console.log(result.id); // id of the overlapped range 32 | }); 33 | 34 | // search 2: get overlapped regions from a range 35 | var results2 = itree.search(103, 400); 36 | 37 | results2.forEach(function(result) { 38 | console.log(result.data); // overlapped range data 39 | console.log(result.id); // id of the overlapped range 40 | console.log(result.rate1); // overlapped rate to the given range 41 | console.log(result.rate2); // overlapped rate to the range of this result 42 | }); 43 | 44 | 45 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var IntervalTree = require('./IntervalTree'); 2 | function test() { 3 | function createRandomInterval(unit, id) { 4 | var p1 = Math.floor(Math.random() * unit); 5 | var p2 = Math.floor(Math.random() * unit); 6 | if (p1 === p2) { 7 | if (p1 > 0 ) { 8 | p1--; 9 | } 10 | else { 11 | p2++; 12 | } 13 | } 14 | return [Math.min(p1,p2), Math.max(p1,p2), id]; 15 | } 16 | 17 | var iTree = new IntervalTree(500); 18 | 19 | 20 | var intervals = (function() { 21 | var ret = []; 22 | for (var i=0; i<100; i++) ret.push(createRandomInterval(1000, i)); 23 | return ret; 24 | })(); 25 | 26 | intervals.forEach(function(val) { 27 | iTree.add(val, val[2]); 28 | }); 29 | 30 | 31 | 32 | 33 | /** 34 | * point search 35 | **/ 36 | var results = iTree.search(500); 37 | var resultIds = results.map(function(val) { 38 | return Number(val.id); 39 | }); 40 | 41 | 42 | intervals.forEach(function(v, k) { 43 | if (resultIds.indexOf(k) >= 0) { 44 | if (v[0] <= 500 && v[1] >= 500) { 45 | console.log('true positive', v); 46 | } 47 | else { 48 | throw new Error('false positive'); 49 | } 50 | } 51 | else { 52 | if (v[0] <= 500 && v[1] >= 500) { 53 | throw new Error('false negative'); 54 | } 55 | else { 56 | console.log('true negative', v); 57 | } 58 | } 59 | }); 60 | 61 | /** 62 | * range search 63 | **/ 64 | results = iTree.search(500, 700); 65 | 66 | /** 67 | * range search evaluation 68 | **/ 69 | results.forEach(function(result) { 70 | if (result.rate1 == 0 && result.rate2 == 0) { 71 | throw new Error('false positive'); 72 | } 73 | }); 74 | // TODO : detect false negative 75 | } 76 | 77 | 78 | if (process.argv[1] == __filename) { 79 | test(); 80 | } 81 | -------------------------------------------------------------------------------- /IntervalTree.js: -------------------------------------------------------------------------------- 1 | var SortedList = require('sortedlist'); 2 | 3 | /** 4 | * IntervalTree 5 | * 6 | * @param (object) data: 7 | * @param (number) center: 8 | * @param (object) options: 9 | * center: 10 | * 11 | **/ 12 | function IntervalTree(center, options) { 13 | options || (options = {}); 14 | 15 | this.startKey = options.startKey || 0; // start key 16 | this.endKey = options.endKey || 1; // end key 17 | this.intervalHash = {}; // id => interval object 18 | this.pointTree = new SortedList({ // b-tree of start, end points 19 | compare: function(a, b) { 20 | if (a == null) return -1; 21 | if (b == null) return 1; 22 | var c = a[0]- b[0]; 23 | return (c > 0) ? 1 : (c == 0) ? 0 : -1; 24 | } 25 | }); 26 | 27 | this._autoIncrement = 0; 28 | 29 | // index of the root node 30 | if (!center || typeof center != 'number') { 31 | throw new Error('you must specify center index as the 2nd argument.'); 32 | } 33 | 34 | this.root = new Node(center, this); 35 | } 36 | 37 | 38 | /** 39 | * publid methods 40 | **/ 41 | 42 | 43 | /** 44 | * add new range 45 | **/ 46 | IntervalTree.prototype.add = function(data, id) { 47 | if (this.intervalHash[id]) { 48 | throw new Error('id ' + id + ' is already registered.'); 49 | } 50 | 51 | if (id == undefined) { 52 | while (this.intervalHash[this._autoIncrement]) { 53 | this._autoIncrement++; 54 | } 55 | id = this._autoIncrement; 56 | } 57 | 58 | var itvl = new Interval(data, id, this.startKey, this.endKey); 59 | this.pointTree.insert([itvl.start, id]); 60 | this.pointTree.insert([itvl.end, id]); 61 | this.intervalHash[id] = itvl; 62 | this._autoIncrement++; 63 | _insert.call(this, this.root, itvl); 64 | }; 65 | 66 | 67 | /** 68 | * search 69 | * 70 | * @param (integer) val: 71 | * @return (array) 72 | **/ 73 | IntervalTree.prototype.search = function(val1, val2) { 74 | var ret = []; 75 | if (typeof val1 != 'number') { 76 | throw new Error(val1 + ': invalid input'); 77 | } 78 | 79 | if (val2 == undefined) { 80 | _pointSearch.call(this, this.root, val1, ret); 81 | } 82 | else if (typeof val2 == 'number') { 83 | _rangeSearch.call(this, val1, val2, ret); 84 | } 85 | else { 86 | throw new Error(val1 + ',' + val2 + ': invalid input'); 87 | } 88 | return ret; 89 | }; 90 | 91 | 92 | /** 93 | * remove: 94 | **/ 95 | IntervalTree.prototype.remove = function(interval_id) { 96 | }; 97 | 98 | 99 | 100 | /** 101 | * private methods 102 | **/ 103 | 104 | 105 | /** 106 | * _insert 107 | **/ 108 | function _insert(node, itvl) { 109 | if (itvl.end < node.idx) { 110 | if (!node.left) { 111 | node.left = new Node((itvl.start + itvl.end) / 2, this); 112 | } 113 | return _insert.call(this, node.left, itvl); 114 | } 115 | 116 | if (node.idx < itvl.start) { 117 | if (!node.right) { 118 | node.right = new Node((itvl.start + itvl.end) / 2, this); 119 | } 120 | return _insert.call(this, node.right, itvl); 121 | } 122 | return node.insert(itvl); 123 | } 124 | 125 | 126 | /** 127 | * _pointSearch 128 | * @param (Node) node 129 | * @param (integer) idx 130 | * @param (Array) arr 131 | **/ 132 | function _pointSearch(node, idx, arr) { 133 | if (!node) return; 134 | 135 | if (idx < node.idx) { 136 | node.starts.every(function(itvl) { 137 | var bool = (itvl.start <= idx); 138 | if (bool) arr.push(itvl.result()); 139 | return bool; 140 | }); 141 | return _pointSearch.call(this, node.left, idx, arr); 142 | } 143 | 144 | else if (idx > node.idx) { 145 | node.ends.every(function(itvl) { 146 | var bool = (itvl.end >= idx); 147 | if (bool) arr.push(itvl.result()); 148 | return bool; 149 | }); 150 | return _pointSearch.call(this, node.right, idx, arr); 151 | } 152 | // exact equal 153 | else { 154 | node.starts.map(function(itvl) { arr.push(itvl.result()) }); 155 | } 156 | } 157 | 158 | 159 | 160 | /** 161 | * _rangeSearch 162 | * @param (integer) start 163 | * @param (integer) end 164 | * @param (Array) arr 165 | **/ 166 | function _rangeSearch(start, end, arr) { 167 | if (end - start <= 0) { 168 | throw new Error('end must be greater than start. start: ' + start + ', end: ' + end); 169 | } 170 | var resultHash = {}; 171 | 172 | var wholeWraps = []; 173 | _pointSearch.call(this, this.root, (start + end) >> 1, wholeWraps, true); 174 | 175 | wholeWraps.forEach(function(result) { 176 | resultHash[result.id] = true; 177 | }); 178 | 179 | 180 | var idx1 = this.pointTree.bsearch([start, null]); 181 | var pointTreeArray = this.pointTree; 182 | while (idx1 >= 0 && pointTreeArray[idx1][0] == start) { 183 | idx1--; 184 | } 185 | 186 | var idx2 = this.pointTree.bsearch([end, null]); 187 | if (idx2 >= 0) 188 | { 189 | var len = pointTreeArray.length -1; 190 | while (idx2 <= len && pointTreeArray[idx2][0] <= end) { 191 | idx2++; 192 | } 193 | 194 | pointTreeArray.slice(idx1 + 1, idx2).forEach(function(point) { 195 | var id = point[1]; 196 | resultHash[id] = true; 197 | }, this); 198 | 199 | Object.keys(resultHash).forEach(function(id) { 200 | var itvl = this.intervalHash[id]; 201 | arr.push(itvl.result(start, end)); 202 | }, this); 203 | } 204 | } 205 | 206 | 207 | 208 | /** 209 | * subclasses 210 | * 211 | **/ 212 | 213 | 214 | /** 215 | * Node : prototype of each node in a interval tree 216 | * 217 | **/ 218 | function Node(idx) { 219 | this.idx = idx; 220 | this.starts = new SortedList({ 221 | compare: function(a, b) { 222 | if (a == null) return -1; 223 | if (b == null) return 1; 224 | var c = a.start - b.start; 225 | return (c > 0) ? 1 : (c == 0) ? 0 : -1; 226 | } 227 | }); 228 | 229 | this.ends = new SortedList({ 230 | compare: function(a, b) { 231 | if (a == null) return -1; 232 | if (b == null) return 1; 233 | var c = a.end - b.end; 234 | return (c < 0) ? 1 : (c == 0) ? 0 : -1; 235 | } 236 | }); 237 | }; 238 | 239 | /** 240 | * insert an Interval object to this node 241 | **/ 242 | Node.prototype.insert = function(interval) { 243 | this.starts.insert(interval); 244 | this.ends.insert(interval); 245 | }; 246 | 247 | 248 | 249 | /** 250 | * Interval : prototype of interval info 251 | **/ 252 | function Interval(data, id, s, e) { 253 | this.id = id; 254 | this.start = data[s]; 255 | this.end = data[e]; 256 | this.data = data; 257 | 258 | if (typeof this.start != 'number' || typeof this.end != 'number') { 259 | throw new Error('start, end must be number. start: ' + this.start + ', end: ' + this.end); 260 | } 261 | 262 | if ( this.start >= this.end) { 263 | throw new Error('start must be smaller than end. start: ' + this.start + ', end: ' + this.end); 264 | } 265 | } 266 | 267 | /** 268 | * get result object 269 | **/ 270 | Interval.prototype.result = function(start, end) { 271 | var ret = { 272 | id : this.id, 273 | data : this.data 274 | }; 275 | if (typeof start == 'number' && typeof end == 'number') { 276 | /** 277 | * calc overlapping rate 278 | **/ 279 | var left = Math.max(this.start, start); 280 | var right = Math.min(this.end, end); 281 | var lapLn = right - left; 282 | ret.rate1 = lapLn / (end - start); 283 | ret.rate2 = lapLn / (this.end - this.start); 284 | } 285 | return ret; 286 | }; 287 | 288 | module.exports = IntervalTree; 289 | --------------------------------------------------------------------------------