Rodrigo Leite
Rodrigo Leite

Reputation: 596

Checking if date matches recurring date

I have a date variable containing a date, and an interval variable containing an array of numbers. Each number on the interval array represents a date, which is acquired by adding said number to the day count of the previous date. For example:

If date is equal to 2016-09-01 and interval is equal to [15, 15, 20], the resulting dates would be 2016-09-16, 2016-10-01, 2016-10-21, 2016-11-06 and so on.

I want to check if a given date matches that pattern. To do this, I tried using moment-recur, which does exactly what I want with the .every() function, but intervals with repeating numbers don't seem to work ([15, 15, 20] would be parsed as [15, 20] for example). How can I accomplish this, either with moment-recur or a different library?

Here's the desired output using moment-recur:

const now = moment();
const date = moment("2016-09-10", "YYYY-MM-DD");

console.log(date.recur().every([18, 18, 57]).days().matches(now));

Upvotes: 3

Views: 1473

Answers (3)

GregL
GregL

Reputation: 38131

You can do this without having to use moment-recur if you want to, using a similar approach to what @Mark_M described

The first step is to determine the number of days between the given date and the start date: moment(endDate).diff(startDate, 'days');.

Then, create an array of cumulative totals for the days since start date for each of the entries in the interval array:

function sumSoFar(nums) {
   return nums.reduce((sums, num) => {
       const prevSum = last(sums) || 0;
       return sums.concat(num + prevSum);
   }, []);
}

// ...

const sums = sumSoFar(interval);

Finally, the sum of the whole interval array is just the last entry in that list, and so we can find out which entry in the interval list it matches by taking the days difference modulo interval sum. If that is 0, or an entry in the sums array, then the date matches the interval. If not, then it doesn't.

Here is the complete code I came up with:

const startDate = moment('2016-09-01');
const interval = [15, 15, 20];
const last = (arr) => arr[arr.length - 1];
const sum = (nums) => nums.reduce((acc, num) => acc + num, 0);

function sumSoFar(nums) {
  return nums.reduce((sums, num) => {
    const prevSum = last(sums) || 0;
    return sums.concat(num + prevSum);
  }, []);
}

const validDates = [moment('2016-09-16'), moment('2016-10-01'), moment('2016-10-21'), moment('2016-11-05')];

function isValid(startDate, interval, date) {
  const days = moment(date).diff(startDate, 'days');
  const sums = sumSoFar(interval);
  const remainingDays = days % last(sums);
  return remainingDays === 0 || sums.indexOf(remainingDays) >= 0;
}

validDates.forEach(d => console.log(isValid(startDate, interval, d)));
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.1/moment.min.js"></script>

Upvotes: 1

RobG
RobG

Reputation: 147483

In a very kludgy way, what you're trying to do is generate new dates that accumulate according to your sequence, then see if at some point it matches a test date.

The following uses moment.js, but really doesn't need it. The moment functionality could be replaced with about 10 lines of code in a couple of separate functions.

/* @param {Date} sequenceStart - start of sequence
** @param {Array} sequence - sequence of intervals
** @param {Date} date - date for comparison
*/
function inSequence(sequenceStart, sequence, date) {
  // Copy start date so don't affect original
  var s = moment(sequenceStart);
  // Get test date in suitable format
  var d = moment(date).format('YYYYMMDD');
  var i = 0;

  do {
    // debug
    console.log(s.format('YYYYMMDD') + ' ' + d)

    // If dates match, return true
    if (s.format('YYYYMMDD') == d) {
      return true;
    }
    // If didn't match, add the next value in the sequence
    s.add(sequence[i++ % sequence.length], 'day');

  // Stop if go past test date
  } while (s.format('YYYYMMDD') <= d)

  // If didn't return true, return false
  return false;
}

var sequence = [15,15,20];
var start = new Date(2017,8,1); // 1 Sep 2017
var test  = new Date(2017,10,5) // 5 Nov 2017

console.log(inSequence(start, sequence, test));
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.1/moment.min.js"></script>

Upvotes: 1

Mark
Mark

Reputation: 92461

The accumulative nature of what you are trying to do is a little tricky.

There may be a nicer way, but I think this works pretty well so long as your interval list isn't too big.

The main insight is that if you are looking for an accumulative interval of say [2, 10, 11] then you will be looking for every 2, 12, 23, 25, 35, 46, etc. This amount to looking for three different dates at regular intervals of the sum of your accumulator -- in this case 23. So you could just use moment's every() with each of the three cases and a single interval.

For example if you have:

const now = moment();
const date = moment("2016-10-22", "YYYY-MM-DD");
const interval = [18, 18, 57]

// sum of the intervals -- i.e. the main interval (93)
const main_int = interval.reduce( (prev, curr) => prev + curr );

// an array of days for each small interval
const dates = interval.map(i  => moment(date.add(i ,'days')))

// moment mutates the original so this results in 
// [ moment("2016-11-09T00:00:00.000"),
//   moment("2016-11-27T00:00:00.000"),
//   moment("2017-01-23T00:00:00.000") ]

// Now see if any of these days matches the main interval

const match = dates.some(d => d.recur().every(main_int).days().matches(now))

Upvotes: 2

Related Questions