forked from moll/js-strange
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtree.js
158 lines (138 loc) · 4.76 KB
/
tree.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
var Range = require("./")
var union = Range.union
var compareBeginToBegin = Range.compareBeginToBegin
var compareBeginToEnd = Range.compareBeginToEnd
var compareEndToEnd = Range.compareEndToEnd
var concat = Array.prototype.concat.bind(Array.prototype)
var EMPTY_ARR = Array.prototype
module.exports = RangeTree
/**
* Create an interval tree node.
*
* For creating a binary search tree out of an array of ranges, you might want
* to use [`RangeTree.from`](#RangeTree.from).
*
* **Import**:
* ```javascript
* var RangeTree = (require)("strange/tree")
* ```
*
* @example
* var left = new RangeTree([new Range(-5, 0)])
* var right = new RangeTree([new Range(5, 10)])
* var root = new RangeTree([new Range(0, 5), new Range(0, 10)], left, right]
* root.search(7) // => [new Range(0, 10), new Range(5, 10)]
*
* @class RangeTree
* @constructor
* @param {Object|Object[]} ranges
* @param {RangeTree} left
* @param {RangeTree} right
*/
function RangeTree(keys, left, right) {
// Store the longest range first.
if (Array.isArray(keys)) this.keys = keys.slice().sort(reverseCompareEndToEnd)
else this.keys = [keys]
this.left = left || null
this.right = right || null
// Remember, the topmost key has the longest range.
var a = this.left ? this.left.range : this.keys[0]
var b = this.right ? union(this.keys[0], this.right.range) : this.keys[0]
this.range = union(a, b)
}
/**
* Create an interval tree (implemented as an augmented binary search tree)
* from an array of ranges.
* Returns a [`RangeTree`](#RangeTree) you can search on.
*
* If you need to relate the found ranges to other data, add some properties
* directly to every range _or_ use JavaScript's `Map` or `WeakMap` to relate
* extra data to those range instances.
*
* @example
* var ranges = [new Range(0, 10), new Range(20, 30), new Range(40, 50)]
* RangeTree.from(ranges).search(42) // => [new Range(40, 50)]
*
* @static
* @method from
* @param {Range[]} ranges
*/
RangeTree.from = function(ranges) {
ranges = ranges.filter(isNotEmpty)
ranges = ranges.sort(compareBeginToBegin)
ranges = ranges.map(arrayify)
ranges = ranges.reduce(dedupe, [])
return this.new(ranges)
}
RangeTree.new = function(ranges) {
switch (ranges.length) {
case 0: return new this(new Range)
case 1: return new this(ranges[0])
case 2: return new this(ranges[0], null, new this(ranges[1]))
default:
var middle = Math.floor(ranges.length / 2)
var left = this.new(ranges.slice(0, middle))
var right = this.new(ranges.slice(middle + 1))
return new this(ranges[middle], left, right)
}
}
/**
* Search for ranges that include the given value or, given a range, intersect
* with it.
* Returns an array of matches or an empty one if no range contained or
* intersected with the given value.
*
* @example
* var tree = RangeTree.from([new Range(40, 50)])
* tree.search(42) // => [new Range(40, 50)]
* tree.search(13) // => []
* tree.search(new Range(30, 42)) // => [new Range(40, 50)]
*
* @method search
* @param {Object} valueOrRange
*/
RangeTree.prototype.search = function(value) {
if (value instanceof Range) return this.searchByRange(value)
else return this.searchByValue(value)
}
RangeTree.prototype.searchByValue = function(value) {
if (!this.range.contains(value)) return EMPTY_ARR
var ownPosition = this.keys[0].compareBegin(value)
return concat(
this.left ? this.left.searchByValue(value) : EMPTY_ARR,
ownPosition <= 0 ? this.searchOwnByValue(value) : EMPTY_ARR,
this.right && ownPosition < 0 ? this.right.searchByValue(value) : EMPTY_ARR
)
}
RangeTree.prototype.searchByRange = function(range) {
if (!this.range.intersects(range)) return EMPTY_ARR
var ownPosition = compareBeginToEnd(this.keys[0], range)
return concat(
this.left ? this.left.searchByRange(range) : EMPTY_ARR,
ownPosition <= 0 ? this.searchOwnByRange(range) : EMPTY_ARR,
this.right && ownPosition < 0 ? this.right.searchByRange(range) : EMPTY_ARR
)
}
// Sort ranges in ascending order for beauty. O:)
RangeTree.prototype.searchOwnByValue = function(value) {
return take(this.keys, function(r) { return r.contains(value) }).reverse()
}
RangeTree.prototype.searchOwnByRange = function(range) {
return take(this.keys, function(r) { return r.intersects(range) }).reverse()
}
function dedupe(ranges, range) {
var last = ranges[ranges.length - 1]
if (last != null && compareBeginToBegin(last[0], range[0]) === 0)
last.push(range[0])
else
ranges.push(range)
return ranges
}
function take(arr, fn) {
var values = []
for (var i = 0; i < arr.length && fn(arr[i], i); ++i) values.push(arr[i])
return values
}
function arrayify(obj) { return [obj] }
function isNotEmpty(range) { return !range.isEmpty() }
function reverseCompareEndToEnd(a, b) { return compareEndToEnd(a, b) * -1 }