moment-range
moment-range copied to clipboard
range.subtract to accept an Array of ranges
Would be nice if subtract method could receive an Array of ranges instead of a single range object.
const coreRange = moment.range(new Date(2017, 2, 3), new Date(2012, 2, 31))
const exceptionDates = [
moment.range(new Date(2017, 2, 4), new Date(2012, 2, 5)),
moment.range(new Date(2017, 2, 11), new Date(2012, 2, 12)),
moment.range(new Date(2017, 2, 18), new Date(2012, 2, 19)),
moment.range(new Date(2017, 2, 25), new Date(2012, 2, 26))
]
const includedDates = coreRange.subtract(exceptionDates)
// return array of range dates ignoring the weekend dates on this case.
For anyone looking for a function to do this with the current API, here's a quick, unpolished solution:
(UPDATED to allow subtracting entire range (subtract a from a))
function subtractRanges(longRanges, shortRanges)
{
// Always return an array
if(shortRanges.length === 0)
return longRanges.hasOwnProperty("length")
? longRanges
: [longRanges];
// Result is empty range
if(longRanges.length === 0)
return [];
if(!longRanges.hasOwnProperty("length"))
longRanges = [longRanges];
for(let long in longRanges)
{
for(let short in shortRanges)
{
longRanges[long] = longRanges[long].subtract(shortRanges[short])
if(longRanges[long].length === 0)
{
// Subtracted an entire range, remove it from list
longRanges.splice(long, 1);
shortRanges.splice(0, short);
return this.subtractRanges(longRanges, shortRanges);
}
else if(longRanges[long].length === 1)
{
// No subtraction made, but .subtract always returns arrays
longRanges[long] = longRanges[long][0];
}
else
{
// Successfully subtracted a subrange, flatten and recurse again
const flat = [].concat(...longRanges);
shortRanges.splice(0, short);
return this.subtractRanges(flat, shortRanges);
}
}
}
return longRanges;
}
Usage:
let day = moment("00:00:00", "HH:mm:ss").range("day");
let ranges = [moment("10:00:00", "HH:mm:ss").range("hour"), moment("16:00:00", "HH:mm:ss").range("hour")];
subtractRanges(day, ranges);
/*
[ { [Number: 36000000]
start: moment("2017-04-26T00:00:00.000"),
end: moment("2017-04-26T10:00:00.000") },
{ [Number: 18000001]
start: moment("2017-04-26T10:59:59.999"),
end: moment("2017-04-26T16:00:00.000") },
{ [Number: 25200000]
start: moment("2017-04-26T16:59:59.999"),
end: moment("2017-04-26T23:59:59.999") } ]
*/
Great post, but can you help me with removing a spread (...) operator? I want to use it on backend but my framework doesn't like spread operator. Here is fiddle how I rewrited it https://jsfiddle.net/FLhpq/4/ It works on web, but backend returns [TypeError: Cannot read property 'valueOf' of undefined], using latest moment.js and moment-range.
@l2ysho I don't see the relevance with the fiddle, but the spread operator is used only for the array flattening. You can replace that line with
const flat = [].concat.apply([], longRanges);
@mx781 Thx man, it helps me a lot. (ES6 newbie)
+1, Brilliant example above, worth a PR.
what is the expected return value when the subtracted ranges aren't consecutive? e.g.:
const coreRange = moment.range('2017-01-01', '2017-12-31');
const exceptionDates = [
moment.range('2017-04-10', '2017-04-15'),
moment.range('2017-04-05', '2017-04-08'),
moment.range('2017-02-01', '2017-05-01'),
moment.range('2017-04-13', '2017-05-01'),
];
coreRange.subtract(exceptionDates); // ??
That should likewise return an array of ranges.
@mx781 yes—specifically which ranges though?
very helpfull function subtractRanges!! thank you @mx781!
@gf3 Sorry for the delayed reply - I see your point now. In my mind, overlapping ranges should be subtracted from the core range as if they were all one, intermittent range. That is, if you look at it as a timeline, you "stack" all the exception dates based on their time, and the result of the subtraction are the segments which don't have an exception date on top of them.
A visual schematic is probably clearer:
I---J
E---F G----H
C-------D K-----L
A--------------------------------B
If A-B is the core range (longRanges), and the rest are exception dates (shortRanges), then the result should be an array [A-C, F-G, L-B].
Testing it out with your example, the function above does follow that logic:
subtractRanges(coreRange, exceptionDates);
[ { [Number: 2678400000]
start: moment("2017-01-01T00:00:00.000"),
end: moment("2017-02-01T00:00:00.000") },
{ [Number: 21085200000]
start: moment("2017-05-01T00:00:00.000"),
end: moment("2017-12-31T00:00:00.000") } ]
However, am not sure if it would work with all edge cases (e.g, if your core Range is two intervals and your exception dates span the "hole" in between).
Using @mx781 code while waiting for official - potential - implementation. It seems sorting rangesToSubtract is important in the example code above. Wanted to post that here in case anyone else discovers this. Depending on order of rangesToSubtract the function could return a range that should have been subtracted.
See example here: https://runkit.com/wubzz/5a8580d282a2050012c3282e
Here is my ES6 implementation (this requires flatten from lodash) -- usage is the same as @mx781 :
function subtractRanges (source, others) {
if (!Array.isArray(source)) {
source = [source]
}
return flatten(source.map(s => {
let remaining = [s]
flatten(others).forEach(o => {
remaining = flatten(remaining.map(r => r.subtract(o)))
})
return remaining
}))
}
This also works regardless of the range sorting. https://runkit.com/rocketraman/5aa0c92ca8fce800128d84bb
@rocketraman great! It could be written even shorter with reduce:
function subtractRanges(source, others) {
if (!Array.isArray(source)) {
source = [source];
}
return flatten(source.map(s => {
return flatten(others).reduce((remaining, o) => {
return flatten(remaining.map(r => r.subtract(o)));
}, [s]);
}));
}
This is great! I would love to see something like this make it into the library. I posted a similar question here which I'm trying to work through using this Subtract solution.
I'm running into a small issue, though. If we refer to the graphical representation above, I would expect to get back [A-C, F-G, L-B] but instead I'm only getting [F-G, L-B]
My data looks like this:
primary: range( May 1 -> July 30 )
secondary: [
range( May 14 -> May 17 )
range( May 17 -> May 18 )
range( May 18 -> May 28 )
range( May 28 -> June 6 )
range( June 6 -> June 15 )
range( June 18 -> June 21 )
]
expecting:
[
range( May 1 -> May 13 )
range( June 16 -> June 17 )
range( June 22 -> July 30 )
]
result:
[
range( June 16 -> June 17 )
range( June 22 -> July 30 )
]
I'll keep playing with it, thanks
@reustle I answered you over on StackOverflow...
@gf3 Could @dbettini 's implementation be added to the library, please?
Besides allowing subtracting multiple ranges, another important advantage is that it accepts multiple ranges as input too.
@fancydev18 @dbettini i'm not opposed to adding it—i just want to be sure we've covered all the cases. with the current implementation i believe there are some cases that don't make sense
tangentially related, i think it would also be helpful to add some of these graphical representations of ranges and actions to the documentation
i just want to be sure we've covered all the cases. with the current implementation i believe there are some cases that don't make sense
Do you have something specific in mind? We've implemented it in a production app and so far so good, it seems to work great. We need to subtract a set of intervals from a set of intervals.
Btw, it would be great to have the same ability for "add" as well (add a set of intervals to another set), without the requirement to be adjacent anymore.