Search This Blog

Friday, May 10, 2013

JavaScript dataAdd function

One of the few gifts to programming contributed by the BASIC language is the dateAdd function.
I have seen versions of it ported to Oracle PL/SQL, SQL Server TSQL, PHP and other languages.
Javascript is an obvious candidate for this.
Like many languages, Javascript has a date object and you can easily add a required number of days to it, (this is how date additions are handled in Oracle SQL).
However what you want is to be able to easily and reliably add different periods - weeks, months, years a date add function specifying the interval.
There are various examples - here, and here and even here, the problem with these is that they don't add months correctly.
If you add 1 month to the 31st of January - e.g.
var d = new Date ('31/01/2007');
d.dateAdd('m', 1);
The code suggested is:

 case "m":
      dTemp.setMonth(dTemp.getMonth() + iNum);
      break;


This is logical but as January has 31 days and February has (usually) 28, it appears the setMonth function increments the date by the length of the current month.
I had to implement an invoicing function, and accounts people tend to be very particular about their dates.
My suggestion for a month add algorithm is:

case "m": {        // month
    var dParts = { 'year' : this.getFullYear
                 , 'month': this.getMonth()
                 , 'day' : this.getDate() };
    dParts.month =+ p_Number;
    if (dParts.month > 11) {
   dParts.month =- 11;
   dParts.year++;
        this.setYear(dParts.year);
    }
    this.setMonth(dParts.month);
    break;
}

The logic handles wrapping around a year by extracting the month and year (also the day which I didn't think I needed).

This is better, but still not correct. When adding one month to 31st of January, we will get the 3rd of March. This is because the new day is 31st of February, which is then wraps over into March.
We need to know how many days are in the next month and adjust accordingly.



        case "m": {        // month
            var dParts = {'year' : this.getFullYear()
                          , 'month': this.getMonth()
                          , 'day' : this.getDate() };
            dParts.month = dParts.month + p_Number;
            if (dParts.month > 11) {
             dParts.month = dParts.month - 11;
             dParts.year++;
             this.setYear(dParts.year);
            }
            newMonthLength = daysInMonth(dParts.month, dParts.year);
            if ( dParts.day > newMonthLength) { this.setDate(newMonthLength); }
            this.setMonth(dParts.month);
            break;
                    }



I wrote 2 functions - one as an extension to the date class and a standalone function that just takes the month and year.


function daysInMonth_Extension() {
var year = this.getFullYear();
var month = this.getMonth();
if (month == 1) {
//february
if ( year % 500 == 0) { return(29); }
else if (year % 100 == 0) { return(28); }
else if (year % 4 == 0) { return(29); }
else { return(28) };
} else if (month == 3 || month == 5 || month == 8 || month == 10) { return(30); }
else {return(31); } 
}  /* daysInMonth_Extension */
Date.prototype.daysInMonth = daysInMonth_Extension;

function daysInMonth(month, year) {
if (month == 1) {
//february
if ( year % 500 == 0) { return(29); }
else if (year % 100 == 0) { return(28); }
else if (year % 4 == 0) { return(29); }
else { return(28) };
} else if (month == 3 || month == 5 || month == 8 || month == 10) { return(30); }
else {return(31); } 
}  /* daysInMonth */


This allows for February and September, April, June and November, remember month numbers start at 0. I should/will refactor this, but by now I was getting over it.


Incorporating code from the first link above you get the following - which I will put up on gitHub:






function daysInMonth_Extension() {
var year = this.getFullYear();
var month = this.getMonth();
if (month == 1) {
//february
if ( year % 500 == 0) { return(29); }
else if (year % 100 == 0) { return(28); }
else if (year % 4 == 0) { return(29); }
else { return(28) };
} else if (month == 3 || month == 5 || month == 8 || month == 10) { return(30); }
else {return(31); } 
}  /* daysInMonth_Extension */
Date.prototype.daysInMonth = daysInMonth_Extension;

function daysInMonth(month, year) {
if (month == 1) {
//february
if ( year % 500 == 0) { return(29); }
else if (year % 100 == 0) { return(28); }
else if (year % 4 == 0) { return(29); }
else { return(28) };
} else if (month == 3 || month == 5 || month == 8 || month == 10) { return(30); }
else {return(31); } 
}  /* daysInMonth */

function myDateAdd_Extension(p_Interval, p_Number){
    var thing = new String();
   
    //in the spirt of VB we'll make this function non-case sensitive
    //and convert the charcters for the coder.
    p_Interval = p_Interval.toLowerCase();
    
    if(isNaN(p_Number)){
    
        //Only accepts numbers 
        //throws an error so that the coder can see why he effed up    
        throw "The second parameter must be a number. \n You passed: " + p_Number;
        return false;
    }
    p_Number = new Number(p_Number);
    switch(p_Interval.toLowerCase()){
        case "yyyy": {// year
            this.setFullYear(this.getFullYear() + p_Number);
            break;
        }
        case "q": {        // quarter
            this.setMonth(this.getMonth() + (p_Number*3));
            break;
        }
        case "m": {        // month
            var dParts = { 'year' : this.getFullYear(), 'month': this.getMonth(), 'day' : this.getDate() };
            dParts.month = dParts.month + p_Number;
            if (dParts.month > 11) {
            dParts.month = dParts.month - 11;
            dParts.year++;
            this.setYear(dParts.year);
            }
            newMonthLength = daysInMonth(dParts.month, dParts.year);
            if ( dParts.day > newMonthLength) { dParts.day = newMonthLength; this.setDate(newMonthLength); }
            this.setMonth(dParts.month);
            break;
                    }
        case "y":        // day of year
        case "d":        // day
        case "w": {        // weekday
            this.setDate(this.getDate() + p_Number);
            break;
        }
        case "ww": {    // week of year
            this.setDate(this.getDate() + (p_Number*7));
            break;
        }
        case "h": {        // hour
            this.setHours(this.getHours() + p_Number);
            break;
        }
        case "n": {        // minute
            this.setMinutes(this.getMinutes() + p_Number);
            break;
        }
        case "s": {        // second
            this.setSeconds(this.getSeconds() + p_Number);
            break;
        }
        case "ms": {        // second
            this.setMilliseconds(this.getMilliseconds() + p_Number);
            break;
        }
        default: {
        
            //throws an error so that the coder can see why he effed up and
            //a list of elegible letters.
            throw    "The first parameter must be a string from this list: \n" +
                    "yyyy, q, m, y, d, w, ww, h, n, s, or ms. You passed: " + p_Interval;
            return false;
        }
    }
    return this;
}
Date.prototype.dateAdd = myDateAdd_Extension;

Note - I have tested this with days, years and months, for example:


            var dToday = new Date();    
            alert(dToday.dateAdd("m", 2));


However I haven't tested it with hours, minutes, seconds and milliseconds.

Subsequent Testing.

This may be better, but it still doesn't handle the rollover correctly.
If you add 14 months to a date, or addings day rolls over a month or year.

One easy change, change the if condition in month add code to a while statement:

        case "m": {        // month
            var dParts = { 'year' : this.getFullYear(), 'month': this.getMonth(), 'day' : this.getDate() };
            dParts.month = dParts.month + p_Number;
            while (dParts.month > 11) {
             dParts.month = dParts.month - 11;
             dParts.year++;
             this.setYear(dParts.year);
            }
            newMonthLength = daysInMonth(dParts.month, dParts.year);
            if ( dParts.day > newMonthLength) { dParts.day = newMonthLength; this.setDate(newMonthLength); }
            this.setMonth(dParts.month);


Adding days has the same problem, so I refactored the code adding addDays and addMonths functions:


        case "m": {        // month
            var dParts = { 'year' : this.getFullYear(), 'month': this.getMonth(), 'day' : this.getDate() };
            this.addMonths(dParts, p_Number);
        }
        case "d" : {
            var dParts = { 'year' : this.getFullYear(), 'month': this.getMonth(), 'day' : this.getDate() };
            this.addDays(dParts, p_Number);
        }




function myDateAdd_addMonths_Extension(dParts, p_Number){
            dParts.month = dParts.month + p_Number;
            while (dParts.month > 11) {
             dParts.month = dParts.month - 11;
             dParts.year++;
             this.setYear(dParts.year);
            }
            newMonthLength = daysInMonth(dParts.month, dParts.year);
            if ( dParts.day > newMonthLength) { dParts.day = newMonthLength; this.setDate(newMonthLength); }
            this.setMonth(dParts.month);
}


function myDateAdd_addDays_Extension(dParts, p_Number){
            dateParts.day += p_Number;
            monthLength = daysInMonth(dParts.month, dParts.year);
    while ($dateParts->d > $monthLength) {
dParts.month++;
dateParts.day -= monthLength;
if (dateParts.month > 11) {
             dParts.month = dParts.month - 11;
             dParts.year++;
         } }
               monthLength = daysInMonth(dParts.month, dParts.year);
    }

}


Date.prototype.addMonthsmyDateAdd_addMonths_Extension;
Date.prototype.addDaysmyDateAdd_addDays_Extension;


This shows how complicated date calculations are, more testing is need to verify this code.

No comments:

Post a Comment