// 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: standard_form_text, standard_shaddow.
// 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
var finalYearOffset = 0;

function SetupDateBoxControl( ctlDateBox, yearOffset, createDiv )
{

if ( yearOffset ) {
		finalYearOffset = yearOffset;
}

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

  if ( true == createDiv || !document.getElementById('date_instructions_div') ){
  // add div after control for messages

      var divMessage = document.createElement('div');
      //divMessage.className = 'standard_form_text';

	  // 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 );

	     }
  } else {
  	var divMessage = document.getElementById('date_instructions_div');
  }

  if ( 'object' == typeof( createDiv ) ){
	 	var divMessage = createDiv;
  }

  // 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.");
}
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 Date.");
}
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.');

// 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());
  throw new Error('Invalid day value.');

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);
        if( yyyy > d.getFullYear() ){
        	yyyy -= 100;
        }

        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);
        if( yyyy > d.getFullYear() ){
        	yyyy -= 100;
        }

        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);

    }
},

//mm-dd-yyyy (ISO 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 (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 = 'standard_form_text';
     //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 = 'standard_form_text';
     //ctlDateBox.className = 'standard_form_text';

     }

  }
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 = 'standard_form_text';
  //ctlDateBox.className = 'DateBoxControl';

  }

}