DZone Snippets is a public source code repository. Easily build up your personal collection of code snippets, categorize them with tags / keywords, and share them with the world

Snippets has posted 5883 posts at DZone. View Full User Profile

JavaScript DateBoxControl

09.12.2005
| 21940 views |
  • submit to reddit
        In input forms on web pages you often have to validate dates and users are always entering something off the wall.  Simon Incutio came up with a script to parse the dates.  I added some stuff and turned it into a control of sorts.  It is sort of US centric with date entering but it does accept iso style entries.

// Date Box Control
// 
// based on:
//  'Magic' date parsing, by Simon Willison (6th October 2003)
//   http://simon.incutio.com/archive/2003/10/06/betterDateInput
// 
// Notes
// To create a date box control, call the SetupDateBoxControl function.
// It should be passed an input element with type of text.
// This will first create a div after the input box.
// Then it will associate the div with the input element.
// The div will use the css classes: DateBoxControlMsg, DateBoxControlErrorMsg.
// Then the div is associated with the input element.
// Then the contents are validated so the div will get populated initially.
// The onchange event of the input element will invoke the validation function.
// The validation function populates the div.
// If a successfully parsed date, the div gets a nicely formatted date.
// If an unsuccessfully parse date, the div gets an error message.
//
// History
// 02/03/2005 - WSR : modified for use as datebox control
// 09/09/2005 - WSR : datebox is not required anymore (blank input is valid)
//                  : added style class to datebox itself

// hooks functionality up to given textbox
function SetupDateBoxControl( ctlDateBox )
   {

   // if a valid object was given
   if (ctlDateBox)
      {

      // add div after control for messages
      var divMessage = document.createElement('div');
      divMessage.className = 'DateBoxControlMsg';

      // if there is a next sibling
      if (ctlDateBox.nextSibling)
         {

         // insert before next sibling
		   ctlDateBox.parentNode.insertBefore( divMessage, ctlDateBox.nextSibling );

         }
      // if there is not a next sibling
      else
         {

         // append child to parent
         ctlDateBox.parentNode.appendChild( divMessage );         

         }

      // link message div to textbox for easy script access
      ctlDateBox.message = divMessage;

      // validate current contents
      DateBoxControl_Validate( ctlDateBox );

      // hook up event handlers
      ctlDateBox.onchange = function () { DateBoxControl_Validate(this); };
      
      }

   }

// add indexOf function to Array type
// finds the index of the first occurence of item in the array, or -1 if not found
Array.prototype.indexOf = function(item) {
    for (var i = 0; i < this.length; i++) {
        if (this[i] == item) {
            return i;
        }
    }
    return -1;
};

// add filter function to Array type
// returns an array of items judged true by the passed in test function
Array.prototype.filter = function(test) {
    var matches = [];
    for (var i = 0; i < this.length; i++) {
        if (test(this[i])) {
            matches[matches.length] = this[i];
        }
    }
    return matches;
};

// add right function to String type
// returns the rightmost x characters
String.prototype.right = function( intLength ) {
   if (intLength >= this.length)
      return this;
   else
      return this.substr( this.length - intLength, intLength );
};

// add trim function to String type
// trims leading and trailing whitespace
String.prototype.trim = function() { return this.replace(/^\s+|\s+$/, ''); };

// arrays for month and weekday names
var monthNames = "January February March April May June July August September October November December".split(" ");
var weekdayNames = "Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" ");

/* Takes a string, returns the index of the month matching that string, throws
   an error if 0 or more than 1 matches
*/
function parseMonth(month) {
    var matches = monthNames.filter(function(item) { 
        return new RegExp("^" + month, "i").test(item);
    });
    if (matches.length == 0) {
        throw new Error("Invalid month string");
    }
    if (matches.length < 1) {
        throw new Error("Ambiguous month");
    }
    return monthNames.indexOf(matches[0]);
}

/* Same as parseMonth but for days of the week */
function parseWeekday(weekday) {
    var matches = weekdayNames.filter(function(item) {
        return new RegExp("^" + weekday, "i").test(item);
    });
    if (matches.length == 0) {
        throw new Error("Invalid day string");
    }
    if (matches.length < 1) {
        throw new Error("Ambiguous weekday");
    }
    return weekdayNames.indexOf(matches[0]);
}

function DateInRange( yyyy, mm, dd )
   {

   // if month out of range
   if ( mm < 0 || mm > 11 )
      throw new Error('Invalid month value.  Valid months values are 1 to 12');

   // get last day in month
   var d = (11 == mm) ? new Date(yyyy + 1, 0, 0) : new Date(yyyy, mm + 1, 0);

   // if date out of range
   if ( dd < 1 || dd > d.getDate() )
      throw new Error('Invalid date value.  Valid date values for ' + monthNames[mm] + ' are 1 to ' + d.getDate().toString());

   return true;

   }

/* Array of objects, each has 're', a regular expression and 'handler', a 
   function for creating a date from something that matches the regular 
   expression. Handlers may throw errors if string is unparseable. 
*/
var dateParsePatterns = [
    // Today
    {   re: /^today/i,
        handler: function() { 
            return new Date();
        } 
    },
    // Tomorrow
    {   re: /^tomorrow/i,
        handler: function() {
            var d = new Date(); 
            d.setDate(d.getDate() + 1); 
            return d;
        }
    },
    // Yesterday
    {   re: /^yesterday/i,
        handler: function() {
            var d = new Date();
            d.setDate(d.getDate() - 1);
            return d;
        }
    },
    // mmddyyyy (American style)
    {   re: /(\d{2})(\d{2})(\d{4})/,
        handler: function(bits) {

            var yyyy = parseInt(bits[3], 10);
            var dd = parseInt(bits[2], 10);
            var mm = parseInt(bits[1], 10) - 1;

            if ( DateInRange( yyyy, mm, dd ) )
               return new Date(yyyy, mm, dd);

        }
    },
    // mmddyy (American style) short year
    {   re: /(\d{2})(\d{2})(\d{2})/,
        handler: function(bits) {

            var d = new Date();
            var yyyy = d.getFullYear() - (d.getFullYear() % 100) + parseInt(bits[3], 10);
            var dd = parseInt(bits[2], 10);
            var mm = parseInt(bits[1], 10) - 1;

            if ( DateInRange(yyyy, mm, dd) )
               return new Date(yyyy, mm, dd);

        }
    },
    // 4th
    {   re: /^(\d{1,2})(st|nd|rd|th)?$/i, 
        handler: function(bits) {

            var d = new Date();
            var yyyy = d.getFullYear();
            var dd = parseInt(bits[1], 10);
            var mm = d.getMonth();

            if ( DateInRange( yyyy, mm, dd ) )
               return new Date(yyyy, mm, dd);

        }
    },
    // 4th Jan
    {   re: /^(\d{1,2})(?:st|nd|rd|th)? (\w+)$/i, 
        handler: function(bits) {

            var d = new Date();
            var yyyy = d.getFullYear();
            var dd = parseInt(bits[1], 10);
            var mm = parseMonth(bits[2]);

            if ( DateInRange( yyyy, mm, dd ) )
               return new Date(yyyy, mm, dd);

        }
    },
    // 4th Jan 2003
    {   re: /^(\d{1,2})(?:st|nd|rd|th)? (\w+),? (\d{4})$/i,
        handler: function(bits) {

            var yyyy = parseInt(bits[3], 10);
            var dd = parseInt(bits[1], 10);
            var mm = parseMonth(bits[2]);

            if ( DateInRange( yyyy, mm, dd ) )
               return new Date(yyyy, mm, dd);

        }
    },
    // Jan 4th
    {   re: /^(\w+) (\d{1,2})(?:st|nd|rd|th)?$/i, 
        handler: function(bits) {

            var d = new Date();
            var yyyy = d.getFullYear(); 
            var dd = parseInt(bits[2], 10);
            var mm = parseMonth(bits[1]);

            if ( DateInRange( yyyy, mm, dd ) )
               return new Date(yyyy, mm, dd);

        }
    },
    // Jan 4th 2003
    {   re: /^(\w+) (\d{1,2})(?:st|nd|rd|th)?,? (\d{4})$/i,
        handler: function(bits) {

            var yyyy = parseInt(bits[3], 10); 
            var dd = parseInt(bits[2], 10);
            var mm = parseMonth(bits[1]);

            if ( DateInRange( yyyy, mm, dd ) )
               return new Date(yyyy, mm, dd);

        }
    },
    // next Tuesday - this is suspect due to weird meaning of "next"
    {   re: /^next (\w+)$/i,
        handler: function(bits) {

            var d = new Date();
            var day = d.getDay();
            var newDay = parseWeekday(bits[1]);
            var addDays = newDay - day;
            if (newDay <= day) {
                addDays += 7;
            }
            d.setDate(d.getDate() + addDays);
            return d;

        }
    },
    // last Tuesday
    {   re: /^last (\w+)$/i,
        handler: function(bits) {

            var d = new Date();
            var wd = d.getDay();
            var nwd = parseWeekday(bits[1]);
         
            // determine the number of days to subtract to get last weekday
            // calculates 0 if weekdays are the same so we have to change this to 7
            var addDays = (wd == nwd) ? -7 : (-1 * (wd + 7 - nwd)) % 7;
            
            // adjust date and return
            d.setDate(d.getDate() + addDays);
            return d;

        }
    },
    // Tuesday
    {   re: /^(\w+)$/i,
        handler: function(bits) {

            var d = new Date();
            var wd = d.getDay();
            var nwd = parseWeekday(bits[1]);
         
            // if same weekday, return date         
            if (nwd == wd)
               return d;

            // if new weekday is before current weekday
            if (nwd < wd )
               {
 
               // calculate last weekday
               d.setDate(d.getDate() + ((wd == nwd) ? -7 : (-1 * (wd + 7 - nwd)) % 7));

               }
            // if new weekday is after current weekday
            else
               {

               // calculate next weekday
               d.setDate(d.getDate() + (nwd - wd));

               }
               
            return d;

        }
    },
    // mm/dd/yyyy (American style)
    {   re: /(\d{1,2})\/(\d{1,2})\/(\d{4})/,
        handler: function(bits) {

            var yyyy = parseInt(bits[3], 10);
            var dd = parseInt(bits[2], 10);
            var mm = parseInt(bits[1], 10) - 1;

            if ( DateInRange( yyyy, mm, dd ) )
               return new Date(yyyy, mm, dd);

        }
    },
    // mm/dd/yy (American style) short year
    {   re: /(\d{1,2})\/(\d{1,2})\/(\d{1,2})/,
        handler: function(bits) {

            var d = new Date();
            var yyyy = d.getFullYear() - (d.getFullYear() % 100) + parseInt(bits[3], 10);
            var dd = parseInt(bits[2], 10);
            var mm = parseInt(bits[1], 10) - 1;

            if ( DateInRange(yyyy, mm, dd) )
               return new Date(yyyy, mm, dd);

        }
    },
    // mm/dd (American style) omitted year
    {   re: /(\d{1,2})\/(\d{1,2})/,
        handler: function(bits) {

            var d = new Date();
            var yyyy = d.getFullYear();
            var dd = parseInt(bits[2], 10);
            var mm = parseInt(bits[1], 10) - 1;

            if ( DateInRange(yyyy, mm, dd) )
               return new Date(yyyy, mm, dd);

        }
    },
    // yyyy-mm-dd (ISO style)
    {   re: /(\d{4})-(\d{1,2})-(\d{1,2})/,
        handler: function(bits) {

            var yyyy = parseInt(bits[1], 10);
            var dd = parseInt(bits[3], 10);
            var mm = parseInt(bits[2], 10) - 1;

            if ( DateInRange( yyyy, mm, dd ) )
               return new Date(yyyy, mm, dd);

        }
    },
    // yy-mm-dd (ISO style) short year
    {   re: /(\d{1,2})-(\d{1,2})-(\d{1,2})/,
        handler: function(bits) {

            var d = new Date();
            var yyyy = d.getFullYear() - (d.getFullYear() % 100) + parseInt(bits[1], 10);
            var dd = parseInt(bits[3], 10);
            var mm = parseInt(bits[2], 10) - 1;

            if ( DateInRange( yyyy, mm, dd ) )
               return new Date(yyyy, mm, dd);

        }
    },
    // mm-dd (ISO style) omitted year
    {   re: /(\d{1,2})-(\d{1,2})/,
        handler: function(bits) {

            var d = new Date();
            var yyyy = d.getFullYear();
            var dd = parseInt(bits[2], 10);
            var mm = parseInt(bits[1], 10) - 1;

            if ( DateInRange( yyyy, mm, dd ) )
               return new Date(yyyy, mm, dd);

        }
    }
];

// parses date string input
function parseDateString( strDateInput )
   {
   
   // cycle through date parse patterns
   for (var i = 0; i < dateParsePatterns.length; i++)
      {

      // get regular expression for this pattern
      var re = dateParsePatterns[i].re;

      // get handler function for this pattern
      var handler = dateParsePatterns[i].handler;

      // parse input using regular expression
      var bits = re.exec(strDateInput);

      // if there was a match
      if (bits)
         {

         alert( re );

         // return the result of the handler function (which constitutes bits into a date)
         return handler(bits);

         }

      }

   // if no pattern matched - throw exception
   throw new Error("Invalid date string");

   }

// validates the input from datebox as a date
function DateBoxControl_Validate( ctlDateBox )
   {

   ctlDateBox.value = ctlDateBox.value.trim();

   if ( ctlDateBox.value.length > 0 )
      {

      try
         {

         // parse input to get date  (error is raised if it can't be parsed)
         var dtValue = parseDateString(ctlDateBox.value.trim());

         // assign date in mm/dd/yyyy format to textbox
         ctlDateBox.value = ('0' + (dtValue.getMonth() + 1).toString()).right(2) + '/' + ('0' + dtValue.getDate().toString()).right(2) + '/' + dtValue.getFullYear().toString();

         // add more formal date to message div associated with textbox
         if (!ctlDateBox.message.firstChild)
            ctlDateBox.message.appendChild(document.createTextNode(dtValue.toDateString()));
         else
            ctlDateBox.message.firstChild.nodeValue = dtValue.toDateString();

         // swith class name back to default so styling is changed
         ctlDateBox.message.className = 'DateBoxControlMsg';
         ctlDateBox.className = 'DateBoxControl';

         }
      catch (e)
         {

         // use error message from exception
         var strMessage = e.message;

         // give a nicer message to built-in javascript exception message
         if (strMessage.indexOf('is null or not an object') < -1)
            strMessage = 'Invalid date string';

         // add error message to message div associated with textbox
         if (!ctlDateBox.message.firstChild)
            ctlDateBox.message.appendChild(document.createTextNode(strMessage));
         else
            ctlDateBox.message.firstChild.nodeValue = strMessage;

         // switch class name to error so styling is changed
         ctlDateBox.message.className = 'DateBoxControlErrorMsg';
         ctlDateBox.className = 'DateBoxControlError';
         
         }

      }
   else
      {

      // clear message div associated with textbox
      if (!ctlDateBox.message.firstChild)
         ctlDateBox.message.appendChild(document.createTextNode(''));
      else
         ctlDateBox.message.firstChild.nodeValue = '';

      // swith class name back to default so styling is changed
      ctlDateBox.message.className = 'DateBoxControlMsg';
      ctlDateBox.className = 'DateBoxControl';

      }

   }