intervals-fn
Version:
Manipulate intervals in a functional way
363 lines (362 loc) • 14.3 kB
JavaScript
"use strict";
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) if (e.indexOf(p[i]) < 0)
t[p[i]] = s[p[i]];
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
const ramda_1 = require("ramda");
const dissocMany = (...props) => {
return ramda_1.pipe.apply(null, props.map(p => ramda_1.dissoc(p))); // Workaround for TS issue #4130
};
// tslint:disable:prefer-object-spread
exports.convertFTtoSE = (r) => dissocMany('from', 'to')(Object.assign({}, r, { start: r.from, end: r.to }));
exports.convertARtoSE = ([start, end]) => ({ start, end });
exports.convertSEtoFT = (r) => dissocMany('start', 'end')(Object.assign({}, r, { from: r.start, to: r.end }));
exports.convertSEtoAR = (r) => [r.start, r.end];
/**
* Complement of `intervals` bounded to `boundaries`. Convert space between two consecutive intervals into interval.
* Keeps extra object properties on `boundaries`.
* intervals array has to be sorted.
* Doesn't mutate input. Output keeps input's structure.
*
* boundaries | interval(s) | result
* --- | --- | ---
* { start: 0, end: 10} | [{ start: 3, end: 7 }] | [{ start: 0, end: 3 }, { start: 7, end: 10 }]
* { start: 0, end: 10} | [{ start: 2, end: 4 }, { start: 7, end: 8 }] | [{ start: 0, end: 2 }, { start: 4, end: 7 }, { start: 8, end: 10 }]
*
* @param boundaries arg1: interval defining boundaries for the complement computation.
* @param intervals arg2: array of intervals that complement the result.
* @returns array of intervals.
*/
exports.complement = (boundaries, intervals) => {
const _a = boundaries, { start, end } = _a, rest = __rest(_a, ["start", "end"]); // See TypeScript/pull/13288 TypeScript/issues/10727
const prepRanges = [
{ start: -Infinity, end: start },
...intervals,
{ start: end, end: Infinity },
];
return ramda_1.reject(ramda_1.isNil, ramda_1.aperture(2, prepRanges).map(([r1, r2]) => (r1.end >= r2.start ? null : Object.assign({ start: r1.end, end: r2.start }, rest))));
};
/**
* Test if `intervalA` overlaps with `intervalB`.
*
* intervalA | intervalB | result
* --- | --- | ---
* { start: 0, end: 10} | { start: 3, end: 7 } | true
* { start: 0, end: 5} | { start: 5, end: 7 } | false
*
* @param intervalA arg1: interval
* @param intervalB arg2: interval
* @returns true if overlaps
*/
exports.isOverlappingSimple = (a, b) => {
return b.start < a.end && b.end > a.start;
};
const isOverlappingNum = (a, b) => {
return a.start < b && b < a.end;
};
const beforeOrAdjTo = (afterInt) => (beforeInt) => beforeInt.end <= afterInt.start;
/**
* Test if `intervalA` overlaps with `intervalB`.
*
* Accept array of intervals.
* Intervals arrays have to be sorted.
*
* intervalA | intervalB | result
* --- | --- | ---
* { start: 0, end: 10} | { start: 3, end: 7 } | true
* { start: 0, end: 5} | { start: 5, end: 7 } | false
* { start: 5, end: 10} | [{ start: 0, end: 4 }, { start: 7, end: 8 }] | true
*
* @param intervalA arg1: interval or array of intervals
* @param intervalB arg2: interval or array of intervals
* @returns true if overlaps
*/
exports.isOverlapping = (intervalsA, intervalsB) => {
if ([intervalsA, intervalsB].some(ramda_1.isEmpty)) {
return false;
}
const intsA = intervalsA[0];
const newInters2 = ramda_1.dropWhile(beforeOrAdjTo(intsA), intervalsB);
if (ramda_1.isEmpty(newInters2)) {
return false;
}
const intsB = newInters2[0];
return exports.isOverlappingSimple(intsA, intsB) ? true : exports.isOverlapping(ramda_1.drop(1, intervalsA), newInters2);
};
/**
* Test if `intervalA` is adjacent to (meets) `intervalB`.
*
* intervalA | intervalB | result
* --- | --- | ---
* { start: 0, end: 10} | { start: 3, end: 7 } | false
* { start: 0, end: 5} | { start: 5, end: 7 } | true
*
* @param intervalA arg1: interval
* @param intervalB arg2: interval
* @returns true if adjacent
*/
exports.isMeeting = (a, b) => {
return a.start === b.end || a.end === b.start;
};
/**
* Test if `intervalA` is before or adjacent `intervalB`.
*
* intervalA | intervalB | result
* --- | --- | ---
* { start: 0, end: 2} | { start: 3, end: 7 } | true
* { start: 0, end: 5} | { start: 3, end: 7 } | false
*
* @param intervalA arg1: interval
* @param intervalB arg2: interval
* @returns true if before
*/
exports.isBefore = (a, b) => {
return a.end <= b.start;
};
/**
* Test if `intervalA` is after or adjacent `intervalB`.
*
* intervalA | intervalB | result
* --- | --- | ---
* { start: 5, end: 10} | { start: 3, end: 4 } | true
* { start: 5, end: 10} | { start: 3, end: 6 } | false
*
* @param intervalA arg1: interval
* @param intervalB arg2: interval
* @returns true if after
*/
exports.isAfter = (a, b) => {
return a.start >= b.end;
};
/**
* Test if `intervalA` and `intervalB` share the same starting point.
*
* intervalA | intervalB | result
* --- | --- | ---
* { start: 5, end: 10} | { start: 5, end: 4 } | true
* { start: 5, end: 10} | { start: 0, end: 10 } | false
*
* @param intervalA arg1: interval
* @param intervalB arg2: interval
* @returns true if same starting point
*/
exports.isStarting = (a, b) => {
return a.start === b.start;
};
/**
* Test if `intervalA` and `intervalB` share the same ending point.
*
* intervalA | intervalB | result
* --- | --- | ---
* { start: 5, end: 10} | { start: 0, end: 10 } | true
* { start: 5, end: 10} | { start: 5, end: 7 } | false
*
* @param intervalA arg1: interval
* @param intervalB arg2: interval
* @returns true if same ending point
*/
exports.isEnding = (a, b) => {
return a.end === b.end;
};
/**
* Test if `intervalA` occurs in `intervalB`. `intervalsB` act as boundaries. Can share starting and/or ending point.
*
* intervalA | intervalB | result
* --- | --- | ---
* { start: 2, end: 6} | { start: 0, end: 10 } | true
* { start: 5, end: 10} | { start: 0, end: 10 } | true
* { start: 5, end: 10} | { start: 0, end: 9 } | false
*
* @param intervalA arg1: interval
* @param intervalB arg2: interval
* @returns true if `intervalA` occurs in `intervalB`
*/
exports.isDuring = (a, b) => {
return a.start >= b.start && a.end <= b.end;
};
/**
* Test if `intervalA` is equivalent to `intervalB`.
*
* intervalA | intervalB | result
* --- | --- | ---
* { start: 5, end: 10} | { start: 5, end: 10 } | true
* { start: 5, end: 10} | { start: 0, end: 10 } | false
*
* @param intervalA arg1: interval
* @param intervalB arg2: interval
* @returns true if equivalent
*/
exports.isEqual = (a, b) => {
return a.start === b.start && a.end === b.end;
};
const propFromNthArg = (n, propName) => ramda_1.pipe(ramda_1.nthArg(n), ramda_1.prop(propName));
const maxEnd = (ranges) => ranges.reduce((a, b) => (a.end > b.end ? a : b));
const simplifyPipe = ramda_1.pipe(ramda_1.groupWith(ramda_1.either(exports.isOverlappingSimple, exports.isMeeting)), ramda_1.map(ramda_1.converge(ramda_1.applySpec({ start: propFromNthArg(0, 'start'), end: propFromNthArg(1, 'end') }), [ramda_1.head, maxEnd])));
/**
* Simplification of `intervals`. Unify touching or overlapping intervals.
*
* Intervals array has to be sorted.
*
* Doesn't mutate input. Output keeps input's structure.
*
* | intervals A | result |
* | ----------- | ------ |
* | [{ start: 3, end: 9 }, { start: 9, end: 13 }, { start: 11, end: 14 }] | [{ start: 3, end: 14 }] |
*
* @param intervalA
*/
exports.simplify = (intervals) => simplifyPipe([...intervals]);
const sortByStart = ramda_1.sortBy(ramda_1.prop('start'));
const unifyPipe = ramda_1.pipe(ramda_1.concat, sortByStart, exports.simplify);
/**
* Union of `intervals`.
*
* Accept array of intervals. Doesn't mutate input. Output keeps input's structure.
* Intervals arrays have to be sorted.
*
* interval(s) A | interval(s) B | result
* --- | --- | ---
* [{ start: 0, end: 4}] | [{ start: 3, end: 7 }, { start: 9, end: 11 }] | [{ start: 0, end: 7 }, { start: 9, end: 11 }]
*
* @param intervalA arg1: array of intervals
* @param intervalB arg2: array of intervals
* @returns union of `arg1` and `arg2`
*/
exports.unify = (intervalsA, intervalsB) => unifyPipe([...intervalsA], [...intervalsB]);
const intersectUnfolderSeed = (i1, i2) => {
const new1 = i1[0].end > i2[0].end ? i1 : ramda_1.drop(1, i1);
const new2 = i2[0].end > i1[0].end ? i2 : ramda_1.drop(1, i2);
return [new1, new2];
};
const intersectUnfolder = ([inters1, inters2]) => {
if (ramda_1.any(ramda_1.isEmpty)([inters1, inters2])) {
return false;
}
const newInters1 = ramda_1.dropWhile(beforeOrAdjTo(inters2[0]), inters1);
if (ramda_1.isEmpty(newInters1)) {
return false;
}
const inter1 = newInters1[0];
const newInters2 = ramda_1.dropWhile(beforeOrAdjTo(inter1), inters2);
if (ramda_1.isEmpty(newInters2)) {
return false;
}
const inter2 = newInters2[0];
const minMaxInter = Object.assign({}, inter2, { end: Math.min(inter1.end, inter2.end), start: Math.max(inter1.start, inter2.start) });
const resultInter = beforeOrAdjTo(minMaxInter)(minMaxInter) ? null : minMaxInter;
const seed = intersectUnfolderSeed(newInters1, newInters2);
return [resultInter, seed];
};
/**
* Intersection of `intervals`. Does not simplify result. Keeps extra object properties on `intervalB`.
*
* `interalA` and `interalB` can have different structure.
* Accept array of intervals. Doesn't mutate input. Output keeps `intervalB` structure.
* Intervals arrays have to be sorted.
*
* interval(s) A | interval(s) B | result
* --- | --- | ---
* { from: 0, to: 4 } | { start: 3, end: 7, foo: 'bar' } | [{ start: 3, end: 4, foo: 'bar' }]
* { start: 0, end: 10 } | [{ start: 2, end: 5}, { start: 5, end: 8}] | [{ start: 2, end: 5 }, { start: 5, end: 8 }]
* [{ start: 0, end: 4 }, { start: 8, end: 11 }] | [{ start: 2, end: 9 }, { start: 10, end: 13 }] | [{ start: 2, end: 4 }, { start: 8, end: 9 }, { start: 10, end: 11 }]
*
* @param intervalA arg1: array of intervals
* @param intervalB arg2: array of intervals
* @returns intersection of `arg1` and `arg2`
*/
exports.intersect = (intervalsA, intervalsB) => {
return ramda_1.unfold(intersectUnfolder, [intervalsA, intervalsB]).filter(i => i != null);
};
const minStart = (ranges) => ranges.reduce((a, b) => (a.start < b.start ? a : b));
const mergeUnfolder = (mergeFn) => (ints) => {
if (!ints.length) {
return false;
}
const start = minStart(ints).start;
const withoutStart = ints
.filter(a => a.end > start)
.map(a => (a.start === start ? Object.assign({}, a, { start: a.end }) : a));
const end = minStart(withoutStart).start;
const toMerge = ints.filter(a => exports.isDuring({ start, end }, a));
const next = Object.assign({}, mergeFn(toMerge), { start, end });
return [
next,
ints.filter(a => a.end > end).map(a => (a.start <= end ? Object.assign({}, a, { start: end }) : a)),
];
};
/**
* Merge extra properties of all intervals inside `intervals`, when overlapping, with provided function `mergeFn`.
* Can also be used to generate an array of intervals without overlaps
*
* Doesn't mutate input. Output keeps input's structure.
* Interval array has to be sorted.
*
* parameter | value
* --- | ---
* mergeFn | `(a, b) => {...a, data: a.data + b.data }`
* intervals | `[{ start: 0, end: 10, data: 5 }, { start: 4, end: 7, data: 100 }]`
* result | `[{ start: 0, end: 4, data: 5 }, { start: 4, end: 7, data: 105 }, { start: 7, end: 10, data: 5 }]`
* @param mergeFn arg1: function to merge extra properties of overlapping intervals
* @param intervals arg2: intervals with extra properties.
*/
exports.merge = (mergeFn, intervals) => {
return ramda_1.unfold(mergeUnfolder(mergeFn), intervals);
};
const subtractInter = (mask, base) => {
return exports.complement(base, mask);
};
/**
* Subtact `base` with `mask`.
* Keeps extra object properties on `base`.
*
* Accept array of intervals. Doesn't mutate input. Output keeps input's structure.
* Intervals arrays have to be sorted.
*
* interval(s) base | interval(s) mask | result
* --- | --- | ---
* [{ start: 0, end: 4 }] | [{ start: 3, end: 7 }] | [{ start: 0, end: 3 }]
* [{ start: 0, end: 4 }, { start: 8, end: 11 }] | [{ start: 2, end: 9 }, { start: 10, end: 13 }] | [{ start: 0, end: 2 }, { start: 9, end: 10 }]
*
* @param intervalA arg1: array of intervals
* @param intervalB arg2: array of intervals
* @returns intersection of `arg1` and `arg2`
*/
exports.substract = (base, mask) => {
const intersection = exports.intersect(mask, base);
return ramda_1.unnest(base.map(b => subtractInter(intersection.filter(exports.isOverlappingSimple.bind(null, b)), b)));
};
const splitIntervalWithIndex = (int, index) => {
if (!isOverlappingNum(int, index)) {
return [int];
}
return [Object.assign({}, int, { start: int.start, end: index }), Object.assign({}, int, { start: index, end: int.end })];
};
/**
* Split `intervals` with `splitIndexes`.
* Keeps extra object properties on `intervals`.
* Doesn't mutate input. Output keeps input's structure.
*
* splitIndexes | interval(s) | result
* --- | --- | ---
* [2, 4] | { start: 0, end: 6, foo: 'bar' } | [{ start: 0, end: 2, foo: 'bar' }, { start: 2, end: 4, foo: 'bar' } { start: 4, end: 6, foo: 'bar' }]
* [5] | [{ start: 0, end: 7 }, { start: 3, end: 8 }] | [{ start: 0, end: 5 }, { start: 5, end: 7 }, { start: 3, end: 5 }, { start: 5, end: 8 }]
*
* @param splitIndexes arg1: defines indexes where intervals are splitted.
* @param intervals arg2: intervals to be splitted.
* @returns array of intervals.
*/
exports.split = (splits, intervals) => {
if (splits.length < 1 || intervals.length < 1) {
return intervals;
}
return ramda_1.unnest(intervals.map(int => splits.reduce((acc, i) => {
const lastInt = acc.pop();
return [...acc, ...splitIntervalWithIndex(lastInt, i)];
}, [int])));
};