395 lines
9.3 KiB
JavaScript
395 lines
9.3 KiB
JavaScript
// Copyright 2009 The Closure Library Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS-IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
/**
|
|
* @fileoverview A RangeSet is a structure that manages a list of ranges.
|
|
* Numeric ranges may be added and removed from the RangeSet, and the set may
|
|
* be queried for the presence or absence of individual values or ranges of
|
|
* values.
|
|
*
|
|
* This may be used, for example, to track the availability of sparse elements
|
|
* in an array without iterating over the entire array.
|
|
*
|
|
* @author brenneman@google.com (Shawn Brenneman)
|
|
*/
|
|
|
|
goog.provide('goog.math.RangeSet');
|
|
|
|
goog.require('goog.array');
|
|
goog.require('goog.iter.Iterator');
|
|
goog.require('goog.iter.StopIteration');
|
|
goog.require('goog.math.Range');
|
|
|
|
|
|
|
|
/**
|
|
* Constructs a new RangeSet, which can store numeric ranges.
|
|
*
|
|
* Ranges are treated as half-closed: that is, they are exclusive of their end
|
|
* value [start, end).
|
|
*
|
|
* New ranges added to the set which overlap the values in one or more existing
|
|
* ranges will be merged.
|
|
*
|
|
* @constructor
|
|
*/
|
|
goog.math.RangeSet = function() {
|
|
/**
|
|
* A sorted list of ranges that represent the values in the set.
|
|
* @type {!Array.<!goog.math.Range>}
|
|
* @private
|
|
*/
|
|
this.ranges_ = [];
|
|
};
|
|
|
|
|
|
if (goog.DEBUG) {
|
|
/**
|
|
* @return {string} A debug string in the form [[1, 5], [8, 9], [15, 30]].
|
|
* @override
|
|
*/
|
|
goog.math.RangeSet.prototype.toString = function() {
|
|
return '[' + this.ranges_.join(', ') + ']';
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* Compares two sets for equality.
|
|
*
|
|
* @param {goog.math.RangeSet} a A range set.
|
|
* @param {goog.math.RangeSet} b A range set.
|
|
* @return {boolean} Whether both sets contain the same values.
|
|
*/
|
|
goog.math.RangeSet.equals = function(a, b) {
|
|
// Fast check for object equality. Also succeeds if a and b are both null.
|
|
return a == b || !!(a && b && goog.array.equals(a.ranges_, b.ranges_,
|
|
goog.math.Range.equals));
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {!goog.math.RangeSet} A new RangeSet containing the same values as
|
|
* this one.
|
|
*/
|
|
goog.math.RangeSet.prototype.clone = function() {
|
|
var set = new goog.math.RangeSet();
|
|
|
|
for (var i = this.ranges_.length; i--;) {
|
|
set.ranges_[i] = this.ranges_[i].clone();
|
|
}
|
|
|
|
return set;
|
|
};
|
|
|
|
|
|
/**
|
|
* Adds a range to the set. If the new range overlaps existing values, those
|
|
* ranges will be merged.
|
|
*
|
|
* @param {goog.math.Range} a The range to add.
|
|
*/
|
|
goog.math.RangeSet.prototype.add = function(a) {
|
|
if (a.end <= a.start) {
|
|
// Empty ranges are ignored.
|
|
return;
|
|
}
|
|
|
|
a = a.clone();
|
|
|
|
// Find the insertion point.
|
|
for (var i = 0, b; b = this.ranges_[i]; i++) {
|
|
if (a.start <= b.end) {
|
|
a.start = Math.min(a.start, b.start);
|
|
break;
|
|
}
|
|
}
|
|
|
|
var insertionPoint = i;
|
|
|
|
for (; b = this.ranges_[i]; i++) {
|
|
if (a.end < b.start) {
|
|
break;
|
|
}
|
|
a.end = Math.max(a.end, b.end);
|
|
}
|
|
|
|
this.ranges_.splice(insertionPoint, i - insertionPoint, a);
|
|
};
|
|
|
|
|
|
/**
|
|
* Removes a range of values from the set.
|
|
*
|
|
* @param {goog.math.Range} a The range to remove.
|
|
*/
|
|
goog.math.RangeSet.prototype.remove = function(a) {
|
|
if (a.end <= a.start) {
|
|
// Empty ranges are ignored.
|
|
return;
|
|
}
|
|
|
|
// Find the insertion point.
|
|
for (var i = 0, b; b = this.ranges_[i]; i++) {
|
|
if (a.start < b.end) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!b || a.end < b.start) {
|
|
// The range being removed doesn't overlap any existing range. Exit early.
|
|
return;
|
|
}
|
|
|
|
var insertionPoint = i;
|
|
|
|
if (a.start > b.start) {
|
|
// There is an overlap with the nearest range. Modify it accordingly.
|
|
insertionPoint++;
|
|
|
|
if (a.end < b.end) {
|
|
goog.array.insertAt(this.ranges_,
|
|
new goog.math.Range(a.end, b.end),
|
|
insertionPoint);
|
|
}
|
|
b.end = a.start;
|
|
}
|
|
|
|
for (i = insertionPoint; b = this.ranges_[i]; i++) {
|
|
b.start = Math.max(a.end, b.start);
|
|
if (a.end < b.end) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.ranges_.splice(insertionPoint, i - insertionPoint);
|
|
};
|
|
|
|
|
|
/**
|
|
* Determines whether a given range is in the set. Only succeeds if the entire
|
|
* range is available.
|
|
*
|
|
* @param {goog.math.Range} a The query range.
|
|
* @return {boolean} Whether the entire requested range is set.
|
|
*/
|
|
goog.math.RangeSet.prototype.contains = function(a) {
|
|
if (a.end <= a.start) {
|
|
return false;
|
|
}
|
|
|
|
for (var i = 0, b; b = this.ranges_[i]; i++) {
|
|
if (a.start < b.end) {
|
|
if (a.end >= b.start) {
|
|
return goog.math.Range.contains(b, a);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
|
|
/**
|
|
* Determines whether a given value is set in the RangeSet.
|
|
*
|
|
* @param {number} value The value to test.
|
|
* @return {boolean} Whether the given value is in the set.
|
|
*/
|
|
goog.math.RangeSet.prototype.containsValue = function(value) {
|
|
for (var i = 0, b; b = this.ranges_[i]; i++) {
|
|
if (value < b.end) {
|
|
if (value >= b.start) {
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns the union of this RangeSet with another.
|
|
*
|
|
* @param {goog.math.RangeSet} set Another RangeSet.
|
|
* @return {!goog.math.RangeSet} A new RangeSet containing all values from
|
|
* either set.
|
|
*/
|
|
goog.math.RangeSet.prototype.union = function(set) {
|
|
// TODO(brenneman): A linear-time merge would be preferable if it is ever a
|
|
// bottleneck.
|
|
set = set.clone();
|
|
|
|
for (var i = 0, a; a = this.ranges_[i]; i++) {
|
|
set.add(a);
|
|
}
|
|
|
|
return set;
|
|
};
|
|
|
|
|
|
/**
|
|
* Subtracts the ranges of another set from this one, returning the result
|
|
* as a new RangeSet.
|
|
*
|
|
* @param {!goog.math.RangeSet} set The RangeSet to subtract.
|
|
* @return {!goog.math.RangeSet} A new RangeSet containing all values in this
|
|
* set minus the values of the input set.
|
|
*/
|
|
goog.math.RangeSet.prototype.difference = function(set) {
|
|
var ret = this.clone();
|
|
|
|
for (var i = 0, a; a = set.ranges_[i]; i++) {
|
|
ret.remove(a);
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
|
|
/**
|
|
* Intersects this RangeSet with another.
|
|
*
|
|
* @param {goog.math.RangeSet} set The RangeSet to intersect with.
|
|
* @return {!goog.math.RangeSet} A new RangeSet containing all values set in
|
|
* both this and the input set.
|
|
*/
|
|
goog.math.RangeSet.prototype.intersection = function(set) {
|
|
if (this.isEmpty() || set.isEmpty()) {
|
|
return new goog.math.RangeSet();
|
|
}
|
|
|
|
return this.difference(set.inverse(this.getBounds()));
|
|
};
|
|
|
|
|
|
/**
|
|
* Creates a subset of this set over the input range.
|
|
*
|
|
* @param {goog.math.Range} range The range to copy into the slice.
|
|
* @return {!goog.math.RangeSet} A new RangeSet with a copy of the values in the
|
|
* input range.
|
|
*/
|
|
goog.math.RangeSet.prototype.slice = function(range) {
|
|
var set = new goog.math.RangeSet();
|
|
if (range.start >= range.end) {
|
|
return set;
|
|
}
|
|
|
|
for (var i = 0, b; b = this.ranges_[i]; i++) {
|
|
if (b.end <= range.start) {
|
|
continue;
|
|
}
|
|
if (b.start > range.end) {
|
|
break;
|
|
}
|
|
|
|
set.add(new goog.math.Range(Math.max(range.start, b.start),
|
|
Math.min(range.end, b.end)));
|
|
}
|
|
|
|
return set;
|
|
};
|
|
|
|
|
|
/**
|
|
* Creates an inverted slice of this set over the input range.
|
|
*
|
|
* @param {goog.math.Range} range The range to copy into the slice.
|
|
* @return {!goog.math.RangeSet} A new RangeSet containing inverted values from
|
|
* the original over the input range.
|
|
*/
|
|
goog.math.RangeSet.prototype.inverse = function(range) {
|
|
var set = new goog.math.RangeSet();
|
|
|
|
set.add(range);
|
|
for (var i = 0, b; b = this.ranges_[i]; i++) {
|
|
if (range.start >= b.end) {
|
|
continue;
|
|
}
|
|
if (range.end < b.start) {
|
|
break;
|
|
}
|
|
|
|
set.remove(b);
|
|
}
|
|
|
|
return set;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {number} The sum of the lengths of ranges covered in the set.
|
|
*/
|
|
goog.math.RangeSet.prototype.coveredLength = function() {
|
|
return /** @type {number} */ (goog.array.reduce(
|
|
this.ranges_,
|
|
function(res, range) {
|
|
return res + range.end - range.start;
|
|
}, 0));
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {goog.math.Range} The total range this set covers, ignoring any
|
|
* gaps between ranges.
|
|
*/
|
|
goog.math.RangeSet.prototype.getBounds = function() {
|
|
if (this.ranges_.length) {
|
|
return new goog.math.Range(this.ranges_[0].start,
|
|
goog.array.peek(this.ranges_).end);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {boolean} Whether any ranges are currently in the set.
|
|
*/
|
|
goog.math.RangeSet.prototype.isEmpty = function() {
|
|
return this.ranges_.length == 0;
|
|
};
|
|
|
|
|
|
/**
|
|
* Removes all values in the set.
|
|
*/
|
|
goog.math.RangeSet.prototype.clear = function() {
|
|
this.ranges_.length = 0;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns an iterator that iterates over the ranges in the RangeSet.
|
|
*
|
|
* @param {boolean=} opt_keys Ignored for RangeSets.
|
|
* @return {!goog.iter.Iterator} An iterator over the values in the set.
|
|
*/
|
|
goog.math.RangeSet.prototype.__iterator__ = function(opt_keys) {
|
|
var i = 0;
|
|
var list = this.ranges_;
|
|
|
|
var iterator = new goog.iter.Iterator();
|
|
iterator.next = function() {
|
|
if (i >= list.length) {
|
|
throw goog.iter.StopIteration;
|
|
}
|
|
return list[i++].clone();
|
|
};
|
|
|
|
return iterator;
|
|
};
|