'Javascript Date and DST, add months modify hours

I know it's a famous question and i know to modify date, but i can't find anything regarding to this problem.

I have an ISO 8061 date and i want to add/substract month without modifying the hours and days:

I have the following input:

var add1Month = "2018-10-01T23:59:59.000Z";
var add10Month = "2018-12-01T00:00:00.000Z";

I want to add 1 month to add1Month and 10 to add10Month to have the following output:

var resAdd1Month  = "2018-11-01T23:59:59.000Z";
var resAdd10Month = "2019-10-01T00:00:00.000Z";

Note that days and hours aren't modified and musn't been modified.

Usually i use the couple setMonth/getMonth or moment to modify it but i can't have the good result with it :

// With date
var add1Month = new Date("2018-10-01T23:59:59.000Z");
add1Month.setMonth(add1Month.getMonth() + 1);
console.log(add1Month.toJSON()); // 2018-11-02T00:59:59.000Z


var add10Month = new Date("2018-12-01T00:00:00.000Z")
add10Month.setMonth(add10Month.getMonth() + 10);
console.log(add10Month.toJSON()); // 2019-09-30T23:00:00.000Z



// With moment
var add1Month = new Date(moment("2018-10-01T23:59:59.000Z").add(1, 'M').format()).toJSON();
console.log(add1Month); // 2018-11-02T00:59:59.000Z

var add10Month = new Date(moment("2018-12-01T00:00:00.000Z").add(10, 'M').format()).toJSON()
console.log(add10Month); // 2019-09-30T23:00:00.000Z

There's an easy way to add month to a date without modifying date or hours in case of DST cross ?

The only solution i've found now it's to add it by hand :

function addMonth(isoDate, month){

  var splitDate = isoDate.substr(0,10).split("-").map(function(e){return parseInt(e)});

   // modify month
   splitDate[1] = splitDate[1]+month;

   // modify year if needed
   splitDate[0] = splitDate[0] + Math.floor((splitDate[1] || -12) / 12);

   // remove extra month aleady added in year and take account negative for month substraction
   splitDate[1] = Math.abs((12 + splitDate[1]) % 12) || 12;

   // reformat with 2 digit month and days
   splitDate[1] = ("00"+splitDate[1]).substr(-2);
   splitDate[2] = ("00"+splitDate[2]).substr(-2);


   // manage end day of month 31/30 => 2018-07-31 - 1 month => 2018-06-31 => 2018-06-30 (remove day until valid date found)
    while(new Date(splitDate[0]+"-"+splitDate[1]+"-"+splitDate[2]+isoDate.substr(10)).toJSON() !=
splitDate[0]+"-"+splitDate[1]+"-"+splitDate[2]+isoDate.substr(10)
   )
   {
     splitDate[2] = parseInt(splitDate[2]) - 1;
     splitDate[2] = ("00"+(splitDate[2])).substr(-2);
   }


   return splitDate[0]+"-"+splitDate[1]+"-"+splitDate[2]+isoDate.substr(10);

}

addMonth("2018-10-01T23:59:59.000Z", 1); // "2018-11-01T23:59:59.000Z"
addMonth("2018-12-01T00:00:00.000Z", 10); // "2019-10-01T00:00:00.000Z"
addMonth("2018-06-01T00:00:00.000Z", -11); // "2017-05-01T00:00:00.000Z"
 addMonth("2018-03-31T23:59:59.000Z", -1); // "2018-02-28T23:59:59.000Z"

Thanks in advance !



Solution 1:[1]

That "by hand" can be beautified a bit:

 function addMonths(date, add) {
  const [day, time] = date.split("T");
  let [years, months, days] = day.split("-");

  months = +months + add;

  years = +years + Math.floor(months / 12);
  months = months % 12;



  return [years, months.padStart(2, 0), days].join("-") + "T" + time;
}

Solution 2:[2]

Came across the same issue and worked around it with a rather simple way - assuming the shift is always gonna be small compared to a day's length, I round up the milliseconds difference to a day's length and re-apply the difference :

// Return new date with added days without DST shift (relative to UTC)
function add_days(date, count) {
  return new Date(date.getTime() + count*86400000);
}

// Return new date with added months without DST shift (relative to UTC)
function add_months(date, count) {
  let date2 = new Date(date);
  date2.setMonth(date.getMonth() + count);
  let numDays = Math.round((date2-date) / 86400000);
  return add_days(date, numDays);
}

// Test results
for (let i=1; i<=12; i++) {
  let d = new Date(new Date().getFullYear()+"-"+i+"-01Z");
  console.log("Add 1m to", d.toISOString());
  
  let d2 = new Date(d);
  d2.setMonth(d.getMonth()+1);
  console.log("     js :", d2.toISOString());

  let d3 = add_months(d,1);
  console.log("     fn :", d3.toISOString());

  if (d2.toISOString() != d3.toISOString())
    console.log("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^");
}

If your country/locale does not use DST, all results should be the same.

In my case I am experiencing the following differences :

Add 1m to 2022-03-01T00:00:00.000Z
     js : 2022-03-31T23:00:00.000Z
     fn : 2022-04-01T00:00:00.000Z
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Add 1m to 2022-10-01T00:00:00.000Z
     js : 2022-11-01T01:00:00.000Z
     fn : 2022-11-01T00:00:00.000Z
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 PAPLabs