moment-range icon indicating copy to clipboard operation
moment-range copied to clipboard

range.subtract to accept an Array of ranges

Open gReis89 opened this issue 8 years ago • 18 comments

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.

gReis89 avatar Mar 03 '17 11:03 gReis89

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") } ]
*/

mx781 avatar Apr 26 '17 15:04 mx781

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 avatar May 19 '17 14:05 l2ysho

@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 avatar May 19 '17 14:05 mx781

@mx781 Thx man, it helps me a lot. (ES6 newbie)

l2ysho avatar May 19 '17 14:05 l2ysho

+1, Brilliant example above, worth a PR.

wubzz avatar Jun 28 '17 13:06 wubzz

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); // ??

gf3 avatar Dec 18 '17 21:12 gf3

That should likewise return an array of ranges.

mx781 avatar Jan 02 '18 10:01 mx781

@mx781 yes—specifically which ranges though?

gf3 avatar Jan 03 '18 16:01 gf3

very helpfull function subtractRanges!! thank you @mx781!

AlexanderKositsyn avatar Jan 18 '18 19:01 AlexanderKositsyn

@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).

mx781 avatar Jan 19 '18 10:01 mx781

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

wubzz avatar Feb 15 '18 12:02 wubzz

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 avatar Mar 08 '18 05:03 rocketraman

@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]);
  }));
}

dbettini avatar May 07 '18 00:05 dbettini

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 avatar Jun 08 '18 08:06 reustle

@reustle I answered you over on StackOverflow...

rocketraman avatar Jun 08 '18 16:06 rocketraman

@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 avatar Oct 28 '19 14:10 fancydev18

@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

gf3 avatar Oct 29 '19 17:10 gf3

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.

fancydev18 avatar Oct 29 '19 19:10 fancydev18