UNPKG

intervals-fn

Version:

Manipulate intervals in a functional way

361 lines (360 loc) 14.1 kB
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; }; import { any, aperture, applySpec, concat, converge, dissoc, drop, dropWhile, either, groupWith, head, isEmpty, isNil, map, nthArg, pipe, prop, reject, sortBy, unfold, unnest, } from 'ramda'; const dissocMany = (...props) => { return pipe.apply(null, props.map(p => dissoc(p))); // Workaround for TS issue #4130 }; // tslint:disable:prefer-object-spread export const convertFTtoSE = (r) => dissocMany('from', 'to')(Object.assign({}, r, { start: r.from, end: r.to })); export const convertARtoSE = ([start, end]) => ({ start, end }); export const convertSEtoFT = (r) => dissocMany('start', 'end')(Object.assign({}, r, { from: r.start, to: r.end })); export const 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. */ export const 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 reject(isNil, 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 */ export const 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 */ export const isOverlapping = (intervalsA, intervalsB) => { if ([intervalsA, intervalsB].some(isEmpty)) { return false; } const intsA = intervalsA[0]; const newInters2 = dropWhile(beforeOrAdjTo(intsA), intervalsB); if (isEmpty(newInters2)) { return false; } const intsB = newInters2[0]; return isOverlappingSimple(intsA, intsB) ? true : isOverlapping(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 */ export const 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 */ export const 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 */ export const 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 */ export const 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 */ export const 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` */ export const 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 */ export const isEqual = (a, b) => { return a.start === b.start && a.end === b.end; }; const propFromNthArg = (n, propName) => pipe(nthArg(n), prop(propName)); const maxEnd = (ranges) => ranges.reduce((a, b) => (a.end > b.end ? a : b)); const simplifyPipe = pipe(groupWith(either(isOverlappingSimple, isMeeting)), map(converge(applySpec({ start: propFromNthArg(0, 'start'), end: propFromNthArg(1, 'end') }), [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 */ export const simplify = (intervals) => simplifyPipe([...intervals]); const sortByStart = sortBy(prop('start')); const unifyPipe = pipe(concat, sortByStart, 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` */ export const unify = (intervalsA, intervalsB) => unifyPipe([...intervalsA], [...intervalsB]); const intersectUnfolderSeed = (i1, i2) => { const new1 = i1[0].end > i2[0].end ? i1 : drop(1, i1); const new2 = i2[0].end > i1[0].end ? i2 : drop(1, i2); return [new1, new2]; }; const intersectUnfolder = ([inters1, inters2]) => { if (any(isEmpty)([inters1, inters2])) { return false; } const newInters1 = dropWhile(beforeOrAdjTo(inters2[0]), inters1); if (isEmpty(newInters1)) { return false; } const inter1 = newInters1[0]; const newInters2 = dropWhile(beforeOrAdjTo(inter1), inters2); if (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` */ export const intersect = (intervalsA, intervalsB) => { return 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 => 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. */ export const merge = (mergeFn, intervals) => { return unfold(mergeUnfolder(mergeFn), intervals); }; const subtractInter = (mask, base) => { return 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` */ export const substract = (base, mask) => { const intersection = intersect(mask, base); return unnest(base.map(b => subtractInter(intersection.filter(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. */ export const split = (splits, intervals) => { if (splits.length < 1 || intervals.length < 1) { return intervals; } return unnest(intervals.map(int => splits.reduce((acc, i) => { const lastInt = acc.pop(); return [...acc, ...splitIntervalWithIndex(lastInt, i)]; }, [int]))); };