UNPKG

intervals-fn

Version:

Manipulate intervals in a functional way

363 lines (362 loc) 14.3 kB
"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]))); };