Feed Sign in with OpenID OpenID

Simon Willison’s Weblog

A better way of entering dates

The CreativityGoblin dropped in on me today, and as a result I’ve been tackling the challenge of entering dates in to a web application. In the past, I’ve used DHTML calendar widgets for this purpose (my favourite is Mishoo’s highly configurable, standards compliant JS Calendar) but while widgets like this have a great deal of “wow” factor I’m not convinced that they are the best entry mechanism when it comes to raw user speed. Today’s experiment was partially inspired by PHP’s strtotime function, which accepts a string in a wide variety of formats and converts it in to a time.

I wanted the same thing but in Javascript, as interfaces like this are best carried out on the client without needing a round trip to the server to check any entered data. Here’s a demo of what I came up with (javascript code here). It accepts a number of different input formats and converts them to a standardised mm/dd/yyyy (American style dates because it’s for use in an American piece of software) when you move the focus away from the box. Importantly, the date is shown in two places: in the input box itself and in “Mon Oct 06 2003” format as text below the date entry field. This second display serves two purposes. Firstly, by displaying the date in an unambiguous format mistakes are easier to spot (especially important considering the American date format used in the main input). Secondly, it provides a useful place to display error messages should the script fail to parse some input.

The Javascript itself was quite fun to write, and uses a number of interesting idioms I’ve picked up over the past year. It adds a ’filter’ method to Javascript’s Array class to better support functional programming (in fact I use it to match partially entered month and weekday names) and an ’indexOf’ method identical to the one provided by the String class. The majority of the work is done by a data structure called dateParsePatterns, which defines a set of pairs of regular expressions and handler functions. The regular expressions match a n input style and extract any useful information; the handler functions then create a Date object from the extracted information and return it to the caller.

Finally, the code uses an error handling technique I picked up on Ward’s Wiki called the SamuraiPrinciple, which states you should either complete your contract and return a valid result, or throw an exception. This is used by the handlers and the main date parsing function itself, with thrown exceptions only caught by the magicDate function attached directly to the onblur event of the input box.

There are still quite a few improvements that could be made to the code: more input styles (easily added by extending the main data structure), better planned functions and maybe a clean up to move more of the code out of the root Javascript namespace. For the moment though it serves my purpose just fine.

Update: It wasn’t the Creativity Goblin after all—it was the goblin of someone elses half remembered idea. It turns out my friend Andy wrote something very similar to this back in July. Thinking back, I can remember seeing it as well. Despite not being original it’s still a very useful piece of code.

This is A better way of entering dates by Simon Willison, posted on 6th October 2003.

View blog reactions

Next: Infinite Python Data Structures

Previous: Interesting jobs at the BBC

70 comments

  1. Would it be possible to attach a license to the script ?

    And random notes:

    • 2003-10-06 is said to be invalid; I don't think so :)
    • 16/6/1979 is guessed to be "Sun Apr 06 1980"

    But it's a great idea.

    Frederic Peters - 6th October 2003 21:09 - #

  2. Brilliant! It works great! I need to take some time to really see what you've done but from the looks of things this is a great alternative to entering dates. Especially for web applications.

    Josh - 6th October 2003 21:10 - #

  3. Very nice. I also typed an ISO date (yyyy-mm-dd) reflexively.

    Michael Z. - 6th October 2003 21:29 - #

  4. Frederic that's not a bug, it's a feature :) It uses American style dates so 16/6/1979 means "the 6th day of the 16th month" - although to be honest it should probably throw an error rather than roll the months on a year and 4 months. 2003-10-06 isn't supported but it could be very easily by adding a new item to the dateParsePatterns array. In fact, the code would look something like this:

    1. {
    2. re: /(\d{4})-(\d{2})-(\d{2})/,
    3. handler: function(bits) {
    4. var d = new Date();
    5. d.setYear(bits[1]);
    6. d.setDate(bits[3]);
    7. d.setMonth(parseInt(bits[2]) - 1);
    8. return d;
    9. }
    10. },

    (OK, I admit it, it sucks not having a pre tag)

    Simon Willison - 6th October 2003 21:36 - #

  5. I had no idea ISO was so popular. I've added it to the suported formats in the demo and fixed a bug whereby '08' was being interpreted as Octal.

    Simon Willison - 6th October 2003 21:47 - #

  6. Nice! I love the dateParsePatterns piece -- that's a beautiful way of handling it.

    Adrian Holovaty - 6th October 2003 21:53 - #

  7. Suggestion: It'd be cool if it detected whether \d{2}/\d{2}/\d\{4} was a mm/dd/yyyy or dd/mm/yyyy and handled it appropriately. You could only do that when the dd was unambiguously not a month (i.e., greater than 12), but it could be helpful.

    Adrian Holovaty - 6th October 2003 21:59 - #

  8. What a fantastic bit of javascript, I too have generally used a calendar type control in the past but this has really opened my eyes to this alternative.

    One thing you might want to look at is leap years as different formats of entering the date yield different resulting dates (including the day of the week), '29 feb 2004' is converted to '03/01/2004', however '02/29/2004' seems to work correctly

    Chris Alcock - 6th October 2003 22:13 - #

  9. First of all, I must say it's nice piece of code! Though I have one doubt. How do you convey user that they can enter date in any format?? IMHO, today's internet user is used to see instruction next to date field to see how he/she should be entering it. So now instead of putting instruction that enter date in (MM/DD/YYYY) format, will you will put instruction saying 'You can enter date in whatever format you like', is it? Will this make things more confusing for user??? I don't know! Just my 2 cents. JD http://jdk.phpkid.org

    JD - 6th October 2003 22:32 - #

  10. Damn,

    I just read that newlines are not converted to breaks! Any particular reason for this??

    JD

    JD - 6th October 2003 22:34 - #

  11. JD - yup, it's because I'm an evil XHTML facist. Some day I plan to revamp the comment system to be slightly more forgiving.

    As for telling the user, this interface is designed for private systems that a group of people will be using on a regular basis. As such, makign an interface obvious to a first time user becomes less important than allowing an experienced user to enter data quickly and efficiently.

    Simon Willison - 6th October 2003 23:08 - #

  12. Simon, This is an excellent little script you've put together. Very impressive.

    Scott Johnson - 6th October 2003 23:16 - #

  13. Very impressive simon. One very small thing: When you type in 'next s' or 'next t' it does not return a day or error message. I realize that it's because there two days that start with 's' and 't'. Maybe an ambigious match error message could be printed?

    rick - 7th October 2003 00:35 - #

  14. Good point rick - I've changed it so the error message for those cases comes up as "Ambiguous month" or "Ambiguous weekday". Thanks.

    Simon Willison - 7th October 2003 00:53 - #

  15. Why does it clear the input field if there is an error? Surely it would be friendlier to leave the input unchanged, so corrections can be made.

    Eric Scheid - 7th October 2003 07:14 - #

  16. Eric: that's a really good point. I've altered it to leave the incorrect text in place but change the background colour of the text box pink as an extra visual alert that the data is incorrect. I've also turned off the "select all text in this box when I focus on it" effect when the box contains invalid data.

    Simon Willison - 7th October 2003 07:42 - #

  17. works great. :) could it be extended to parse things like "first tuesday in august next year"?

    dvd - 7th October 2003 14:23 - #

  18. Thanks for bringing this up, Simon. I'd completely forgotten about doing that. Anyway, you're as much the Creativity Goblin as I am - just been having a play with my original.

    dvd - the deed is done!

    For anyone who cares, I've got a demo of the results (script is here), but when I tried it originally it had the wonderful side effect of killing various browsers, so it's not my fault if your browser dies!

    Andrew - 7th October 2003 20:38 - #

  19. This is pretty cool, but in tying my BD 5/8/1973 -> May 8, 1973 5-8-1973 -> Invalid hmmm.... but I'm still gonna use it on my page! Thx much!

    manmoth - 10th October 2003 16:49 - #

  20. also, once focus leaves the input field, clicking back in the field selects the entire text - it is impossible to click the cursor in a partical position or select a substring of the entry, other than using the arrow keys/end/home. Of course, I've only tried this in Opera, so it may be browser-specific behavior...

    manmoth - 10th October 2003 16:53 - #

  21. That's intentional; it means that if you want to retype the whole date (as opposed to modifying what's in there already) you can just start typeing rather than having to select and delete everything in the box. You can still select individual parts of the date using the cursor keys. You can disable that by removing the onfocus() event from the input box.

    Simon Willison - 10th October 2003 17:22 - #

  22. Working well, though it's a bit unsatisfying when using it for multiple date fields on a single form, as each input element must have a unique id, requiring duplicate entries in the style section. Perhaps you could manipulate the style directly via the javascript so you can easily accomodate this in the code. (My first implementation of this script had six date fields, so it gets a bit cumbersome). Overall I like it and the javascript was well written, making it easy to modify so it displays yyyy-mm-dd instead of mm/dd/yyyy as I needed for DB2 insert statements.

    manmoth - 10th October 2003 22:15 - #

  23. About the Javascript; "12th october 2002" works but "12th of october 2002" doesn't.
    Wouldn't it just be a matter of datestring = datestring.replace(' of ', ' ') ?

    Peter Bengtsson - 16th October 2003 14:50 - #

  24. just get a look to this : www.totallysmartit.com Personnaly, I think Lea makes one of the best JS-calander i've seen! Bah ... this is just my opinion.. :)

    Tom Z - 17th October 2003 15:03 - #

  25. Peter; that could be supported by modifying the regular expression used for identifying '12th October 2003' strings. Here's the updated regexp:

    
    // 4th Jan 2003
        {   re: /^(\d{1,2})(?:st|nd|rd|th)? (?:of )?(\w+),? (\d{4})$/i,
            handler: function(bits) {
                var d = new Date();
                d.setDate(parseInt(bits[1], 10));
                d.setMonth(parseMonth(bits[2]));
                d.setYear(bits[3]);
                return d;
            }
        },
    

    Simon Willison - 17th October 2003 15:10 - #

  26. Found a bug.

    In the handler for "// 4th Jan 2003" Suppose you have the following
    var d = new Date(); // d=2003-11-29
    d.setYear(2003);
    d.setDate(31); // d now = 2003-11-31, which doesn't exist!!
    d.setDate(12); // d now = 2003-12-01, when you'd expect 2003-12-31

    When the day 31 is applied to a November date, some other things change because as the date object is being modified it changes.
    The solution is to do this instead
    var d = new Date(2003, 11, 31);

    This solved my problem. Initial value of the input was 31 December 2003 but as soon as magicDate had done its work, it changed to 1 December 2003.

    Peter Bengtsson - 29th November 2003 17:20 - #

  27. I entered a date and pressed enter and nothing happened, other than the date box being blanked. What is supposed to happen? Is there supposed to be a page with the results?

    Stuey - 3rd December 2003 22:22 - #

  28. You need to blur away from the text field to see the effect, rather than hitting enter. I'll make this more obvious on the page - it also means that the submit method of a form should first ensure all dates have been converted before submitting.

    Simon Willison - 3rd December 2003 22:53 - #

  29. I wrote something similar for a client last year. I wanted to be able to handle people typing US date format where the month was unambiguously a month (as I'm in the UK, we wanted the rest of the world's format as default ;o)

    I found this worked nicely:

    // JavaScript's Date.parse() requires MDY, with a separator of / or -
    // Let's see if we can move things around to conform with this.
    var re = /(\d+)[\.\-\/\s]+(\d+)[\.\-\/\s]+(\d+)/;
    
    if (theValue.match(re))
    {
    	var part1 = RegExp.$1;
    	var part2 = RegExp.$2;
    	var part3 = RegExp.$3;
    
    	if (part1 > 2000 &&
    	    part2 <= 12 &&
    	    part2 >= 1 &&
    	    part3 <= 31 &&
    	    part3 >= 1)
    	{
    		// Good grief, it's ISO-8601 format!
    		tmpDate = new Date(part2 + '/' + part3 + '/' + part1);
    	} else
    	{
    		if (part3 < 100) part3 = parseInt(part3, 10) + 2000;
    
    		if (part2 > 12)
    		{
    			// Found a definite MDY date.
    			tmpDate = new Date(part1 + '/' + part2 + '/' + part3);
    		} else if(part1 > 12)
    		{
    			// Found a definite DMY date.
    			tmpDate = new Date(part2 + '/' + part1 + '/' + part3);
    		} else
    		{
    			// This date matches the regular date format, but we don't
    			// know if it's DMY or MDY (or something more perverse).
    			// We're gonna assume it's DMY, being British 'n' all.
    			tmpDate = new Date(part2 + '/' + part1 + '/' + part3);
    		} // if (part2 > 12) ...
    	} // if (part1 > 2000 ...)
    
    	if (debugging) alert("Extracted date as " + tmpDate.toString());
    } else
    {
    	// We can't parse that date, so we'll set it to NaN.
    	tmpDate = NaN;
    } // if (theValue.match(re))
    

    I postprocessed to handle the NaN value, but you don't need to see that. And please excuse the anal commenting... ;o)

    Owen Blacker - 23rd January 2004 12:30 - #

  30. jnhfjhfjh

    jhfjhf - 20th February 2004 01:17 - #

  31. Try entering the last day of any month with 31 days (e.g. 1/31/2004). It converts the date to day 1 of that month (e.g. 1/1/2004).

    Kevin Guske - 14th April 2004 18:40 - #

  32. The 31st day works under Mozilla, but not under Internet Exploder.

    Kevin Guske - 14th April 2004 19:07 - #

  33. OK. I was able to get it to work under IE by using SetFullYear in place of setYear and by using setMonth BEFORE setDate. For example: // mm/dd/yyyy (American style) { re: /(\d{1,2})\/(\d{1,2})\/(\d{4})/, handler: function(bits) { var d = new Date(); d.setFullYear(bits[3]); d.setMonth(parseInt(bits[1], 10) - 1); // Because months indexed from 0 d.setDate(parseInt(bits[2], 10)); return d; } },

    Kevin Guske - 14th April 2004 19:21 - #

  34. Sorry! I'm not a very prolific poster...

    // mm/dd/yyyy (American style)
    { re: /(\d{1,2})\/(\d{1,2})\/(\d{4})/,
    handler: function(bits) {
    var d = new Date();
    d.setFullYear(bits[3]);
    d.setMonth(parseInt(bits[1], 10) - 1); // Because months indexed from 0
    d.setDate(parseInt(bits[2], 10));
    return d;
    }
    },

    Kevin Guske - 14th April 2004 19:28 - #

  35. I would love to see an option to either work in US format (mm/dd/yyyy) or in International format (dd/mm/yyyy). Has anyone worked this out?

    nizam - 10th June 2004 04:24 - #

  36. There is an extra comma at the very end, when the dateParsePatterns array is being created. It increases the length of the array by one...yet it does not add another blank item to the array (IE6). This causes an exception to be raised if an expression is not matched.

    aliya - 18th June 2004 21:53 - #

  37. Excellent work! But I just ran it on IE 5.0 (Windows 200) and got some javascript errors though. Has anyone else tested it on IE 5.0?

    Joe - 22nd June 2004 22:21 - #

  38. This is a fabulous idea! I'll be using this in a a project I'm working on right now. While playing with the demo, I found myself typing things like "a week from next tuesday." Is that a reasonable thing to expect the script to parse? I don't know. Something to think about...

    Thanks for your contribution!

    --johnt

    John Tangney - 8th September 2004 17:04 - #

  39. I know it might screw around with impartial parsing, but I always feel an onnchange event is better than an onblur, as people mightn't necessarily blur before submitting.

    Perhaps to stop parsing for every letter typed a timeout could be imposed. So if a letter isn't typed within 0.5 seconds -- or some other period -- it attempts to parse the field.

    Cameron Adams - 22nd September 2004 02:12 - #

  40. Thanks for the script. I implemented the last tueday functionality and added some other stuff like short date entries. See the full script below. Simon, feel free to use it however you like.

    // Date Box Control
    // 
    // based on:
    //  'Magic' date parsing, by Simon Willison (6th October 2003)
    //   http://simon.incutio.com/archive/2003/10/06/better DateInput 
    // 
    // 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
    
    // 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;
            }
        },
        // 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
                var addDays = (-1 * (wd + 7 - nwd)) % 7;
    
                // above calculate 0 if weekdays are the same so we have to change this to 7
                if (0 == addDays)
                   addDays = -7;
                
                // adjust date and return
                d.setDate(d.getDate() + addDays);
                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)
             {
    
             // 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 )
       {
    
       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.createText Node(dtValue.toDateString())); 
          else
             ctlDateBox.message.firstChild.nodeValue = dtValue.toDateString();
    
          // swith class name back to default so styling is changed
          ctlDateBox.message.className = 'DateBoxControlMsg';
    
          }
       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.createText Node(strMessage)); 
          else
             ctlDateBox.message.firstChild.nodeValue = strMessage;
    
          // switch class name to error so styling is changed
          ctlDateBox.message.className = 'DateBoxControlErrorMsg';
          
          }
    
       }
    

    Will Rickards - 4th February 2005 15:39 - #

  41. This I liked at first... except... I hate feeling like a whinging ninny... If you enter a pseudo date... and hit return nothing happens. You kind of have to click outside the date area. I tried it, it didn't work and then launched FireFox to see if it was a Safari thing. Most things are. I can't type "wed" or "wednesday" and have it assume I mean this wednesday. How dumb is that? As I'm entering the date, I have no idea what TODAY'S day or date is (I have no context). I also don't like the way errors are handled. I entered next week to see if it raised a branching interface to clarify what I meant. By that I mean it's cute and needs to handle more situations. I also don't like the fact tha the dates are american ... ooh I hate that... I also don't like the fact that if you enter "next tues" that it actually shows THIS tuesday.... or is that an americanism too? And now... having reached whinging ninny nirvana flow... I can't actually conceptually move from one date to the next... for example, if I find out that 18th April 2005 is a Monday, which it is, and want the following monday, I have to do some mental arithmetic and add 7 onto 18. I can say "the following monday"... This is a different form of input than a calendar widget that would be suited for some date input needs, like entering a birthday, but not for planning a party. I also think that date handling really needs more attention in collections of dates, or ranges, or repetitions than in simply choosing a date.

    Tom Smith - 18th April 2005 13:30 - #

  42. Those are some excellent points - in particular the enter thing (I've been hit by that myself), but everything else makes sense as well. If I ever revisit this script in the future I'll be sure to take them in to account.

    Simon Willison - 18th April 2005 14:47 - #

  43. What do you mean "if I ever revisit the script"???? WHAT?!!! You mean that's it... That's all... Blimey... Come on Simon, how am I gonna get my whinging ninny rocks off if you don't continue making this better? (I lied about hating feeling like one)... It's a great idea... that just needs a lifetimes work to be fantastic (humane date entry... it's so cool... Raskin would be proud) ... so come on... get on with it... for my sake if nothing else... tom

    Tom Smith - 19th April 2005 09:20 - #

  44. I came across a bug recently. Some examples include 'Mar 31' translating to 3/1/2005, and 'Jan 31' translating to 1/1/2005. This problem may be resolved by ensuring that the setMonth() function is always called before the setDate() function for all handlers. For example // 4th Jan { re: /^(\d{1,2})(?:st|nd|rd|th)? (\w+)$/i, handler: function(bits) { var d = new Date(); d.setDate(parseInt(bits[1], 10)); d.setMonth(parseMonth(bits[2])); return d; } }, should be changed to // 4th Jan { re: /^(\d{1,2})(?:st|nd|rd|th)? (\w+)$/i, handler: function(bits) { var d = new Date(); d.setMonth(parseMonth(bits[2])); //Note that this function is called first d.setDate(parseInt(bits[1], 10)); return d; } }, This is an excellent date validation module. Great work!

    Darryl Havlicek - 25th April 2005 17:23 - #

  45. Neat idea - damn sweet code :) Implementing the gnu specs in js would be a worthy goal?

    paul vudmaska - 27th April 2005 02:21 - #

  46. This is awesome. I'll definitely use this on my next project. One more idea though, should it be possible for the user to only enter the month and date? Accept "2/24" or "2 24" would make my users very happy :)

    ctran - 30th April 2005 18:51 - #

  47. this is awesome and I really hope you're able to get this working smoothly... however I'm at a loss... I enter "Today" in the text box, click outside the text box, and then get presented w/ 7/31/05. That's pretty cool! Underneath the text box is the toString version, "Fri Jul 29 2005" which looks much more readable than 7/31/05, however when I put "Fri Jul 29 2005" into the text box and click outside I get, "Invalid date string!" Would be nice to have the Fri Jul 29 2005 toString output be recognizable as a valid date! otherwise great work, and I look forward to seeing future updates !

    Nick Johnson - 29th July 2005 22:52 - #

  48. Re Tom Smith's comment 41 - the problems of context and having to hit enter would actually all be solved if this calendar control were integrated with "The Coolest DHTML Calendar".

    It could work as:

    User clicks in or tabs into input box

    Calendar appears (no button needed)

    User begins typing something like "Wed"

    On each key press the dateParsePattern() could fire, and if it returns something valid it could feed that to "The Coolest DHTML Calendar". If the user leaves the input box at any point, the most recent date displayed in the control is entered into the field.

    No need to press enter, onblur() becomes a sensible event to the user, user gets context including answers like "What the heck is today anyway?" and "I'm thinking the 8th but is that a Wednesday?", etc, etc.

    If this were open source it would be a nice derivative open source project to assemble from the 2... .

    Chris Moschini - 8th August 2005 15:59 - #

  49. American displaydates are unlogical and flawd. The format YYYYMMD is preffered for various reasons; i don't even have to state why they are better, its obvious.

    Barry Staes - 17th September 2005 00:02 - #

  50. Has anyone else noticed that 31st dates are being changed to 1st dates? ie, try 10/31/2005 and you get 10/01/2005 .. Maybe I'm just missing something?

    Seth Thornberry - 19th September 2005 22:07 - #

  51. Hello, I recently encountered a Date() problem.

    var d = new Date();
    d.setFullYear(1965);
    d.setMonth(1);
    d.setDate(23);
    document.write(d);

    this is what I get:
    "Tue Mar 23 11:44:06 UTC+0800 1965"

    it seem that setMonth() works fine for for all other months except february, namely the numbers 1, 13, 25 etc.

    Any idea?

    SongPerk Sem

    songperk sem - 30th September 2005 04:52 - #

  52. I threw together some code to handle changing the date using up/down arrows. Tought it might come in handy:
    function handleKeyDown(input,e) {
    var key = (window.Event) ? e.which : e.keyCode;

    if ("" == input.value)
    {
    var d = new Date();
    input.value = (d.getMonth() + 1) + '/' + d.getDate() + '/' + d.getFullYear();
    }
    else switch(key)
    {
    case 38: // up
    var d = parseDateString(input.value);
    d.setDate(d.getDate() + 1);
    input.value = (d.getMonth() + 1) + '/' + d.getDate() + '/' + d.getFullYear();
    break;
    case 40: // down
    var d = parseDateString(input.value);
    d.setDate(d.getDate() - 1);
    input.value = (d.getMonth() + 1) + '/' + d.getDate() + '/' + d.getFullYear();
    break;
    }
    }

    jlampa - 6th October 2005 22:51 - #

  53. ow do I modify the return from American to yyyy-mm-dd (ISO style)?

    bigD - 3rd November 2005 18:38 - #

  54. The script is perfect. Now if it only did time instead of dates. Is there some code out there that can do times also. Like for an in and out text box for a online timecard

    hullio - 4th November 2005 18:28 - #

  55. 29 f 2004 does not work

    Graham Butcher - 21st December 2005 16:37 - #

  56. I have just completed developing "the next generation of better way of entering dates" which includes the infamous JSCalendar DHTML popup calendar.

    This combines all the various comments on this post including many of the fixes and patches, and feature requests people have mentioned above.

    • Hitting enter to update the date
    • Using JSCalendar in tandem
    • Configuration option to have iso or us date format with a change of a simple string in the dateparse.js file
      • Now supports both the mm/dd/yyyy and yyyy-mm-dd
    • Rollover on 2/29/2006 to 3/1/2006
    • Having "of" within a date such as 2rd of January or 2nd of jan
    • Fully implemented: Last Tuesday
    • And some new touches I wanted..

    Come check it out the full demo at http://datebox.inimit.com.

    All suggestions are welcome! Please email them to nshb@inimit.com

    Nathaniel Brown - 11th January 2006 16:35 - #

  57. I've used your program and Nathaniel Brown's adapted one, however, it seems that trying to select 31st Jan (now it is the month of Feb only 28 days) does not work correctly :(

    I'm currently trailing through the code to try and find the fix ;) I'm doing this as I find this calendar GREAT!

    Thanks Dominic

    Dominic O'Sullivan - 1st February 2006 18:02 - #

  58. Finally found it ;o)

    All that is needed is to reorder the calls in getDateObj to setYear, setMonth and setDate and voila it works correctly:

    • function getDateObj(yyyy, mm, dd) {
    • var obj = new Date();
    • obj.setYear(yyyy);
    • obj.setMonth(mm);
    • obj.setDate(dd);
    • return obj;
    • }

    There are also several other places in var parseDatePatterns that have this order incorrect too ;o)

    N.B. It makes sense when you think about it in hindsight, we were trying to change the date first so we were trying to change it to 31st Feb!

    Hope you can update your download file ;o)

    Dominic O'Sullivan - 1st February 2006 18:25 - #

  59. Has anybody found a way for this control to either add support for international date formats (dd/MM/YY etc), or to only accept international date formats?

    Robert Clancy - 28th February 2006 00:22 - #

  60. Robert,

    Glad you asked, try Date input and calendar popup.

    I've applied Dominic O'Sullivan's fix as well as modified last and added this, next, first, second, third, fourth and fifth day to the parser. You can now enter a date in a different order (mdy or dmy) depending on a global format string.

    Tanny O'Haley - 3rd March 2006 23:06 - #

  61. Hi there, Good piece of work. I did extend it a little bit, like accepting more format. In my case I was pretty sure the year starts with 20 instead of 19. Hope it helps ... // mm-dd-yy or m-d-yy (American style) { re: /(\d{1,2})-(\d{1,2})-(\d{2})/, handler: function(bits) { var d = new Date(); d.setYear("20"+bits[3]); d.setDate(parseInt(bits[2], 10)); d.setMonth(parseInt(bits[1], 10) - 1); // Because months indexed from 0 return d; } },

    Raaj - 8th March 2006 23:11 - #

  62. OK, there seems to be a further issue with what I posted in that Feb cannot be selected when the current date is 29th, 30th or 31st. I fixed this by setting the date to the 1st of the month at the start of the function:

    • function getDateObj(yyyy, mm, dd) {
    • var obj = new Date();
    • obj.setDate(1);
    • obj.setYear(yyyy);
    • obj.setMonth(mm);
    • obj.setDate(dd);
    • return obj;
    • }

    Dominic O'Sullivan - 30th March 2006 10:36 - #

  63. Simon et al:

    Is there any objection to me (or anyone else) submitting code that uses these techniques and techniques based on these ideas to the dojo toolkit project (www.dojotoolkit.org)?

    Rick

    Rick Morrison - 27th April 2006 19:19 - #

  64. Rick: no problem at all; go right ahead.

    Simon Willison - 28th April 2006 12:51 - #

  65. Hey Rick, Feel free to use the http://datebox.inimit.com version, which I have also released as a Ruby on Rails plugin/engine. It has been verified to work with the scriptaculous and prototype javascript libraries. -NB

    Nathaniel Brown - 13th May 2006 09:11 - #

  66. How about adding a Friday Week and such. Here in australia we use that term frequently in mean that for eg. Friday week means go to the next friday coming and then its the friday after that.

    Shaun Williams - 29th May 2006 12:59 - #

  67. Why are you messing with the array prototype? I tried using your dateparse in my project and it causes bugs in my code. I use the javascript "for in" statement and modifying the Array object causes this to fail. I changed your

    Array.prototype.indexOf = function(item) {

    to

    arrayIndexOf = function(arr, item) {

    And my problems when away. Is the former option really that much better?

    Michael - 14th June 2006 06:25 - #

  68. Your code suffers from a bug in the way the date object is created. Annoyingly, this bug only manifests itself on certain days of the month. Anyway, the solution is to update your getDateObj() function as follows:

    function getDateObj(yyyy, mm, dd)
    {
      var obj = new Date();
      // set year, then month, then date or the date object you get back won't be what you expect.
      obj.setYear(yyyy);
      obj.setMonth(mm);
      obj.setDate(dd);
      return obj;
    }

    Jeff Howden - 10th July 2006 23:47 - #

  69. I modified your date parsing javascript file for Australia. I added date ranges to be parsed (with a start and end date) for different timing resolutions. e.g. you can type "tomorrow morning" which will resolve to an time range between 5am and midday tomorrow.

    Vijay Santhanam - 24th August 2006 06:14 - #

  70. Jeff Howden - thanks for "Your code suffers from a bug in the way the date object is created. Annoyingly, this bug only manifests itself on certain days of the month. Anyway, the solution is to update your getDateObj() function as follows"

    Pozycjonowanie - 29th October 2006 12:30 - #

Comments are closed.

Previously hosted at http://simon.incutio.com/archive/2003/10/06/betterDateInput

A django site