Search code examples
javascriptjsondate

How could I get the the Nth Monday, Tuesday, etc. of every month between two dates using JavaScript


I have a program where a user can set an event, with an start date, end date and the period of repetition the event shall repeat, weekly, monthly by date, monthly by weekday and yearly. After the user creates the event, it gets saved in the database and the event is displayed in a calendar on the main page of my program.

So far I've been able to devise the algorithms for repeating dates weekly, monthly by dates and yearly, but not monthly by weekday. By "monthly by weekday" I'm referring to an event that repeats every same weekday once a month, in the period the start date and end date occupies.

For example, an event that repeats the first Monday of every month between March 1st and November 1st, March 1st is the first Monday of March, so I would like to generate a date that is the first Monday of April, which is April 5th, and so on on every month between March and November.

The snippet below is my function for repeating dates monthly by dates, this is for events that repeat every 15th of any month between the start date and end date.

function repeatEventMonthly(jsonArray, num){

//First I get the start date and end date as strings on a JSON, I separate
//them by the "/" and then use the individual parts to construct two Date Objects

var split = jsonArray[num].Fecha.split("/");
var split2 = jsonArray[num].fechaFin.split("/");

var dd = split[1];
var mm = split[0];
var yy = split[2];

var dd2 = split2[1];
var mm2 = split2[0];
var yy2 = split2[2];

var starDate = new Date();
var endDate = new Date();

starDate.setFullYear(yy);
starDate.setMonth(mm-1);
starDate.setDate(dd);
endDate.setFullYear(yy2);
endDate.setMonth(mm2-1);
endDate.setDate(dd2);

//the variable top means how many days are between the startDate and endDate.
//I use a function that calculates this number

var top = getDaysInDates(starDate, endDate);

if (jsonArray[num].tipoRepeticion == "2") {

 //the attribute "tipoRepeticion" in my JSON object lets me know what type of
 //repetition the event must be set, 2 means Monthly by Weekday and 3 is Monthly by Date

}else if(jsonArray[num].tipoRepeticion == "3"){

    //If the event has repetition type 3, then inside this for loop I generate a new date
    //which I create with the value of the startDate and then I add the index number in the for
    //loop cycle, meaning that if the startDate is March 3, the in index 1 is March 4, Index 2
    //is March 5 and so on, then with an If statement I check that if the date on the
    //generated date object is the same as the date of the startDate, I push the date to  
    //another JSON array that I use to display the dates on the calendar.

    for (let index = 0; index < top; index++) {
        let sd = new Date(starDate);
        sd.setDate(sd.getDate()+index);
        if (sd.getDate() == starDate.getDate()) {
            let str = ((sd.getMonth()+1) + "/" + sd.getDate() + "/" + sd.getFullYear()).toString();
            eventMen.push({

                // the function "construcDates" helps me create a valid String for my program
                // to display at the user.

                date: constructDates(str, 0, jsonArray[num].tipoRepeticion),
                title: jsonArray[num].titulo,
                descripcion: jsonArray[num].descripcion,
                tipo: jsonArray[num].tipo,
                tipoRepeticion : jsonArray[num].tipoRepeticion
            });
        }
    }
}

So this function effectively generates events one month apart but on the same date, meaning that if my event starts March 3rd, and ends on December 3rd, in my calendar it displays the same event on April 3rd, May 3rd, June 3rd, July 3rd... all the way to November 3rd. This method may be more complicated that I needs to be, but I'm still learning JavaScript so this is what I came up with.

However, despite having solved the logic for other type of repeating dates, (weekly, monthly by dates and yearly) repeating dates monthly by weekday is turning up to be pretty difficult, since there isn't the same number of days in every month, I can't just sum up 30 or 31 days to each month and hope the date lands on the same "first Monday of a month" for example. I would like to ask if anyone has any suggestion or solution as to how I could calculate this conundrum, also worth pointing out that the example of "first Monday of any month" is not the only possible type of repeating date by month, also it could the second Tuesday of every month or the last Friday as well.


Solution

  • To get the nth instance of a day in the month, create a date for the first of the month, move to the first instance of the required day, then add (n-1) weeks worth of days, e.g.

    /* Get nth instance of a particular weekday in a month
     *
     * @param {number|string} nth - instance of day, 1 to 4
     * @param {number|string} day - day of week, Sun 0, Mon 1, etc.
     * @param {Date} month - any date in month to get day of
     * @returns {Date} that is nth instance of day in month
    */
    function getNthDayInMonth(nth, day, month) {
      // Create new date for 1st of month
      let d = new Date(month.getFullYear(), month.getMonth());
      // Move to first instance of day in month and 
      // add (n - 1) weeks
      d.setDate(1 + (7 - d.getDay() + day)%7 + (nth - 1)*7);
      return d;
    }
    
    // Formater
    let f = (d) => new Intl.DateTimeFormat('en-GB', {
      weekday:'short',
      day:'2-digit',
      month:'short',
      year: 'numeric'
    }).format(d);
    
    // Examples
    [[1,1,new Date(2021,11),'1st Mon in Dec 2021'],
     [3,3,new Date(2022, 0),'3rd Wed in Jan 2022'],
     [4,1,new Date(2022, 1),'4th Mon in Feb 2022'],
     [4,2,new Date(2022, 1),'4th Tue in Feb 2022'],
     [4,3,new Date(2022, 1),'4th Wed in Feb 2022'],
     [1,3,new Date(2022, 5),'1st Wed in Jun 2022'],
     [5,5,new Date(2022,11),'5th Fri in Dec 2022']
    ].forEach(a => {
      let [n,d,m,note] = a;
      console.log(`${note}: ${f(getNthDayInMonth(n,d,m))}`);
    });

    There are always at least 4 instances of each day in a month, and up to 5 instances of some days (e.g. there are 5 Saturdays, Sundays and Mondays in Jan 2022). The nth parameter might be limited to 4, or if 5th instances are allowed, limit it to 5 then check if the end month is the same as the start month. If not, throw an error or just return undefined.

    To get all the nth days of months between two dates, loop over the months to get the nth instances and remove any dates that are before the start or after the end.