lively.lang
Version:
JavaScript utils providing useful abstractions for working with collections, functions, objects.
253 lines (230 loc) • 8.88 kB
JavaScript
import { range } from "./array.js";
/*global System, global*/
// show-in-doc
// Intervals are arrays whose first two elements are numbers and the
// first element should be less or equal the second element, see
// [`interval.isInterval`](). This abstraction is useful when working with text
// ranges in rich text, for example.
var GLOBAL = typeof System !== "undefined" ? System.global :
(typeof window !== 'undefined' ? window : global);
function isInterval(object) {
// Example:
// interval.isInterval([1,12]) // => true
// interval.isInterval([1,12, {property: 23}]) // => true
// interval.isInterval([1]) // => false
// interval.isInterval([12, 1]) // => false
return Array.isArray(object)
&& object.length >= 2
&& object[0] <= object[1];
}
function sort(intervals) {
// Sorts intervals according to rules defined in [`interval.compare`]().
return intervals.sort(compare);
}
function compare(a, b) {
// How [`interval.sort`]() compares.
// We assume that `a[0] <= a[1] and b[0] <= b[1]` according to `isInterval`
// ```
// -3: a < b and non-overlapping, e.g [1,2] and [3,4]
// -2: a < b and intervals border at each other, e.g [1,3] and [3,4]
// -1: a < b and overlapping, e.g, [1,3] and [2,4] or [1,3] and [1,4]
// 0: a = b, e.g. [1,2] and [1,2]
// 1: a > b and overlapping, e.g. [2,4] and [1,3]
// 2: a > b and share border, e.g [1,4] and [0,1]
// 3: a > b and non-overlapping, e.g [2,4] and [0,1]
// ```
if (a[0] < b[0]) { // -3 || -2 || -1
if (a[1] < b[0]) return -3;
if (a[1] === b[0]) return -2;
return -1;
}
if (a[0] === b[0]) { // -1 || 0 || 1
if (a[1] === b[1]) return 0;
return a[1] < b[1] ? -1 : 1;
}
// we know a[0] > b[0], 1 || 2 || 3
return -1 * compare(b, a);
}
function coalesce(interval1, interval2, optMergeCallback) {
// Turns two interval into one iff compare(interval1, interval2) ∈ [-2,
// -1,0,1, 2] (see [`inerval.compare`]()).
// Otherwise returns null. Optionally uses merge function.
// Examples:
// interval.coalesce([1,4], [5,7]) // => null
// interval.coalesce([1,2], [1,2]) // => [1,2]
// interval.coalesce([1,4], [3,6]) // => [1,6]
// interval.coalesce([3,6], [4,5]) // => [3,6]
var cmpResult = this.compare(interval1, interval2);
switch (cmpResult) {
case -3:
case 3: return null;
case 0:
optMergeCallback && optMergeCallback(interval1, interval2, interval1);
return interval1;
case 2:
case 1: var temp = interval1; interval1 = interval2; interval2 = temp; // swap
case -2:
case -1:
var coalesced = [interval1[0], Math.max(interval1[1], interval2[1])];
optMergeCallback && optMergeCallback(interval1, interval2, coalesced);
return coalesced;
default: throw new Error("Interval compare failed");
}
}
function coalesceOverlapping(intervals, mergeFunc) {
// Like `coalesce` but accepts an array of intervals.
// Example:
// interval.coalesceOverlapping([[9,10], [1,8], [3, 7], [15, 20], [14, 21]])
// // => [[1,8],[9,10],[14,21]]
var condensed = [], len = intervals.length;
while (len > 0) {
var ival = intervals.shift(); len--;
for (var i = 0; i < len; i++) {
var otherInterval = intervals[i],
coalesced = coalesce(ival, otherInterval, mergeFunc);
if (coalesced) {
ival = coalesced;
intervals.splice(i, 1);
len--; i--;
}
}
condensed.push(ival);
}
return this.sort(condensed);
}
function mergeOverlapping(intervalsA, intervalsB, mergeFunc) {
var result = [];
while (intervalsA.length > 0) {
var intervalA = intervalsA.shift();
var toMerge = intervalsB.map(function(intervalB) {
var cmp = compare(intervalA, intervalB);
return cmp === -1 || cmp === 0 || cmp === 1;
});
result.push(mergeFunc(intervalA, toMerge[0]))
result.push(intervalA);
}
return result;
}
function intervalsInRangeDo(start, end, intervals, iterator, mergeFunc, context) {
// Merges and iterates through sorted intervals. Will "fill up"
// intervals. This is currently used for computing text chunks in
// lively.morphic.TextCore.
// Example:
// interval.intervalsInRangeDo(
// 2, 10, [[0, 1], [5,8], [2,4]],
// function(i, isNew) { i.push(isNew); return i; })
// // => [[2,4,false],[4,5,true],[5,8,false],[8,10,true]]
context = context || GLOBAL;
// need to be sorted for the algorithm below
intervals = this.sort(intervals);
var free = [], nextInterval, collected = [];
// merged intervals are already sorted, simply "negate" the interval array;
while ((nextInterval = intervals.shift())) {
if (nextInterval[1] < start) continue;
if (nextInterval[0] < start) {
nextInterval = Array.prototype.slice.call(nextInterval);
nextInterval[0] = start;
};
var nextStart = end < nextInterval[0] ? end : nextInterval[0];
if (start < nextStart) {
collected.push(iterator.call(context, [start, nextStart], true));
};
if (end < nextInterval[1]) {
nextInterval = Array.prototype.slice.call(nextInterval);
nextInterval[1] = end;
}
// special case, the newly constructed interval has length 0,
// happens when intervals contains doubles at the start
if (nextInterval[0] === nextInterval[1]) {
var prevInterval;
if (mergeFunc && (prevInterval = collected.slice(-1)[0])) {
// arguments: a, b, merged, like in the callback of #merge
mergeFunc.call(context, prevInterval, nextInterval, prevInterval);
}
} else {
collected.push(iterator.call(context, nextInterval, false));
}
start = nextInterval[1];
if (start >= end) break;
}
if (start < end) collected.push(iterator.call(context, [start, end], true));
return collected;
}
function intervalsInbetween(start, end, intervals) {
// Computes "free" intervals between the intervals given in range start - end
// currently used for computing text chunks in lively.morphic.TextCore
// Example:
// interval.intervalsInbetween(0, 10,[[1,4], [5,8]])
// // => [[0,1],[4,5],[8,10]]
return intervalsInRangeDo(start, end,
coalesceOverlapping(Array.prototype.slice.call(intervals)),
(interval, isNew) => isNew ? interval : null)
.filter(Boolean);
}
function mapToMatchingIndexes(intervals, intervalsToFind) {
// Returns an array of indexes of the items in intervals that match
// items in `intervalsToFind`.
// Note: We expect intervals and intervals to be sorted according to [`interval.compare`]()!
// This is the optimized version of:
// ```
// return intervalsToFind.collect(function findOne(toFind) {
// var startIdx, endIdx;
// var start = intervals.detect(function(ea, i) {
// startIdx = i; return ea[0] === toFind[0]; });
// if (start === undefined) return [];
// var end = intervals.detect(function(ea, i) {
// endIdx = i; return ea[1] === toFind[1]; });
// if (end === undefined) return [];
// return Array.range(startIdx, endIdx);
// });
// ```
var startIntervalIndex = 0, endIntervalIndex, currentInterval;
return intervalsToFind.map(function(toFind) {
while ((currentInterval = intervals[startIntervalIndex])) {
if (currentInterval[0] < toFind[0]) { startIntervalIndex++; continue };
break;
}
if (currentInterval && currentInterval[0] === toFind[0]) {
endIntervalIndex = startIntervalIndex;
while ((currentInterval = intervals[endIntervalIndex])) {
if (currentInterval[1] < toFind[1]) { endIntervalIndex++; continue };
break;
}
if (currentInterval && currentInterval[1] === toFind[1]) {
return range(startIntervalIndex, endIntervalIndex);
}
}
return [];
});
}
function benchmark() {
// ignore-in-doc
// Used for developing the code above. If you change the code, please
// make sure that you don't worsen the performance!
// See also lively.lang.tests.ExtensionTests.IntervallTest
// import { timeToRunN } from "./function.js";
function benchmarkFunc(func, args, n) {
return `${func.name} ${timeToRunN(() => func.apply(null, args, 100000), n)}ms`
}
return [
"Friday, 20. July 2012:",
"coalesceOverlapping: 0.0003ms",
"intervalsInbetween: 0.002ms",
"mapToMatchingIndexes: 0.02ms",
'vs.\n' + new Date() + ":",
benchmarkFunc(coalesceOverlapping, [[[9,10], [1,8], [3, 7], [15, 20], [14, 21]]], 100000),
benchmarkFunc(intervalsInbetween, [0, 10, [[8, 10], [0, 2], [3, 5]]], 100000),
benchmarkFunc(mapToMatchingIndexes, [range(0, 1000).map(n => [n, n+1]), [[4,8], [500,504], [900,1004]]], 1000)
].join('\n');
}
export {
isInterval,
sort,
compare,
coalesce,
coalesceOverlapping,
mergeOverlapping,
intervalsInRangeDo,
intervalsInbetween,
mapToMatchingIndexes,
}