views:

3423

answers:

12

I am working on a form widget for users to enter a time of day into a text input (for a calendar application). Using JavaScript (we are using jQuery FWIW), I want to find the best way to parse the text that the user enters into a JavaScript Date() object so I can easily perform comparisons and other things on it.

I tried the parse() method and it is a little too picky for my needs. I would expect it to be able to successfully parse the following example input times (in addition to other logically similar time formats) as the same Date() object:

  • 1:00 pm
  • 1:00 p.m.
  • 1:00 p
  • 1:00pm
  • 1:00p.m.
  • 1:00p
  • 1 pm
  • 1 p.m.
  • 1 p
  • 1pm
  • 1p.m.
  • 1p
  • 13:00
  • 13

I am thinking that I might use regular expressions to split up the input and extract the information I want to use to create my Date() object. What is the best way to do this?

+15  A: 

Don't bother doing it yourself, just use datejs.

Jim
25KB just to do dates?!?! I mean, nice library no doubt, and if I had to have psycho date handling functionality, it would be the one. But 25KB is larger than all of the core of jQuery!!!
Jason Bunting
Very nice, thanks for the link. I didn't know about datejs before. Although it seems like it is a bit on the larger side (about 26 KB) for what I need.
Joe Lencioni
Given the range of input you want to accept, I would go for datejs as well. It seems to handle most of them, apart from the one which is just a number, which it takes as the day of the month.
insin
Yeah, I might just go ahead and use datejs. I can get around the single number input being regarded as a month by prepending '1/1/2000 ' to the string when I parse the time.
Joe Lencioni
How big is it minified and gzipped? Probably pretty small.
Andrew Hedges
I just had the same need, used Datejs. It was painless and effective.
Corey Trager
A: 

Why not use validation to narrow down what a user can put in and simplify the list to only include formats that can be parsed (or parsed after some tweaking).

I don't think it's asking too much to require a user to put a time in a supported format.

dd:dd A(m)/P(m)

dd A(m)/P(m)

dd

Wayne
You are right, it really is not asking too much.It is, however, a bit of a hurdle for the user and I want to make this particular form as easy to use as is reasonable. Ideally, the input will be flexible enough to interpret what they typed in and reformat it to a standard format.
Joe Lencioni
+23  A: 

A quick solution which works on the input that you've specified:

var times = ['1:00 pm','1:00 p.m.','1:00 p','1:00pm',
  '1:00p.m.','1:00p','1 pm','1 p.m.','1 p','1pm','1p.m.', '1p','13:00','13'];

for ( var i = 0; i < times.length; i++ ) {
  var d = new Date();
  var time = times[i].match(/(\d+)(?::(\d\d))?\s*(p?)/);
  d.setHours( parseInt(time[1]) + (time[3] ? 12 : 0) );
  d.setMinutes( parseInt(time[2]) || 0 );
  console.log( d );
}

It should work for a few other varieties as well (even if a.m. is used, it'll still work - for example). Obviously this is pretty crude but it's also pretty lightweight (much cheaper to use that than a full library, for example).

John Resig
well done. this is perfect for my needs. :)
Joe Lencioni
After working with this, I noticed that it doesn't properly parse variants of the time "12 pm" because it adds 12 to the hours number. To fix, I changed the d.setHours line to read: d.setHours( parseInt(time[1]) + ( ( parseInt(time[1]) < 12
Joe Lencioni
I also noticed that parseInt was choking on strings like ':30' or ':00' so I changed the regex to capture the minutes without the colon
Joe Lencioni
You'd better hope d doesn't fall on a day where a daylight savings change takes effect. This also assumes English conventions.
peller
Does this method parse 12:00am as 12:00pm?
Ryan
This method won't work with 12:00 AM correctly.
HeavyWave
The calls to ParseInt need a radix of 10 because JS assumes a radix of 8 when there is a leading 0, resulting in the hour being interpreted as 0 if it is greater than 8 and has a leading 0 (because 08 isn't a valid base 8 number).Also, changing "p?" to "[pP]?" will make it work when AM/PM are upper case.All in all, unless you're *really* sure this approach will work for you, you should use a library. Remember, time hates us all.
Benji York
+1  A: 

Just to suggest an alternative, you could use an Ajax call to send the input to a server-side PHP script, parse the sucker using strtotime and send back a timestamp.

Andrew Hedges
+2  A: 

I came across a couple of kinks in implementing John Resig's solution. Here is the modified function that I have been using based on his answer:

function parseTime(timeString)
{
  if (timeString == '') return null;
  var d = new Date();
  var time = timeString.match(/(\d+)(:(\d\d))?\s*(p?)/);
  d.setHours( parseInt(time[1]) + ( ( parseInt(time[1]) < 12 && time[4] ) ? 12 : 0) );
  d.setMinutes( parseInt(time[3]) || 0 );
  d.setSeconds(0, 0);
  return d;
} // parseTime()
Joe Lencioni
This has the same bugs I noted in the highest-voted answer above.
Benji York
+4  A: 

Here's an improvement on Joe's version. Feel free to edit it further.

parseTime(timeString)
{
  if (timeString == '') return null;
  var d = new Date();
  var time = timeString.match(/(\d+)(:(\d\d))?\s*(p?)/i);
  d.setHours( parseInt(time[1],10) + ( ( parseInt(time[1],10) < 12 && time[4] ) ? 12 : 0) );
  d.setMinutes( parseInt(time[3],10) || 0 );
  d.setSeconds(0, 0);
  return d;
}

Changes:

  • Added radix parameter to the parseInt() calls (so jslint won't complain).
  • Made the regex case-insenstive so "2:23 PM" works like "2:23 pm"
Patrick McElhaney
A: 

Hi, a bit change

/(\d+)(?::(\d\d))(?::(\d\d))?\s*([pP]?)/

// added test for p or P // added seconds

d.setHours( parseInt(time[1]) + (time[4] ? 12 : 0) ); // care with new indexes d.setMinutes( parseInt(time[2]) || 0 ); d.setSeconds( parseInt(time[3]) || 0 );

thanks

+2  A: 

All of the examples provided fail to work for times from 12:00 am to 12:59 am. They also throw an error if the regex does not match a time. The following handles this:

function parseTime(timeString) {    
    if (timeString == '') return null;

    var time = timeString.match(/(\d+)(:(\d\d))?\s*(p?)/i); 
    if (time == null) return null;

    var hours = parseInt(time[1],10);    
    if (hours == 12 && !time[4]) {
          hours = 0;
    }
    else {
        hours += (hours < 12 && time[4])? 12 : 0;
    }   
    var d = new Date();             
    d.setHours(hours);
    d.setMinutes(parseInt(time[3],10) || 0);
    d.setSeconds(0, 0);  
    return d;
}

This will work for strings which contain a time anywhere inside them. So "abcde12:00pmdef" would be parsed and return 12 pm. If the desired outcome is that it only returns a time when the string only contains a time in them the following regular expression can be used provided you replace "time[4]" with "time[6]".

/^(\d+)(:(\d\d))?\s*((a|(p))m?)?$/i
Nathan Villaescusa
Thanks, this works perfectly and solves the problems I was having with the earlier answers.
Tauren
+1  A: 

This is a more rugged approach that takes into account how users intend to use this type of input. For example, if a user entered "12", they would expect it to be 12pm (noon), and not 12am. The below function handles all of this. It is also available here: http://blog.de-zwart.net/2010-02/javascript-parse-time/

/**
 * Parse a string that looks like time and return a date object.
 * @return  Date object on success, false on error.
 */
String.prototype.parseTime = function() {
    // trim it and reverse it so that the minutes will always be greedy first:
    var value = this.trim().reverse();

    // We need to reverse the string to match the minutes in greedy first, then hours
    var timeParts = value.match(/(a|p)?\s*((\d{2})?:?)(\d{1,2})/i);

    // This didnt match something we know
    if (!timeParts) {
        return false;
    }

    // reverse it:
    timeParts = timeParts.reverse();

    // Reverse the internal parts:
    for( var i = 0; i < timeParts.length; i++ ) {
        timeParts[i] = timeParts[i] === undefined ? '' : timeParts[i].reverse();
    }

    // Parse out the sections:
    var minutes = parseInt(timeParts[1], 10) || 0;
    var hours = parseInt(timeParts[0], 10);
    var afternoon = timeParts[3].toLowerCase() == 'p' ? true : false;

    // If meridian not set, and hours is 12, then assume afternoon.
    afternoon = !timeParts[3] && hours == 12 ? true : afternoon;
    // Anytime the hours are greater than 12, they mean afternoon
    afternoon = hours > 12 ? true : afternoon;
    // Make hours be between 0 and 12:
    hours -= hours > 12 ? 12 : 0;
    // Add 12 if its PM but not noon
    hours += afternoon && hours != 12 ? 12 : 0;
    // Remove 12 for midnight:
    hours -= !afternoon && hours == 12 ? 12 : 0;

    // Check number sanity:
    if( minutes >= 60 || hours >= 24 ) {
        return false;
    }

    // Return a date object with these values set.
    var d = new Date();
    d.setHours(hours);
    d.setMinutes(minutes);
    return d;
}

This is a string prototype, so you can use it like so:

var str = '12am';
var date = str.parseTime();
Pieter de Zwart
+1  A: 

AnyTime.Converter can parse dates/times in many different formats:

http://www.ama3.com/anytime/

Andrew M. Andrews III
I should add: skip past the picker stuff at the top 3/4 of that page and look at the section on converting:http://www.ama3.com/anytime/#converting
Andrew M. Andrews III
A: 

An improvement to Patrick McElhaney's solution (his does not handle 12am correctly)

var d = new Date(); var time = timeString.match(/(\d+)(:(\d\d))?\s*([pP]?)/i); var h = parseInt(time[1], 10); if (time[4]) { if (h < 12) h += 12; } else if (h == 12) h = 0; d.setHours(h); d.setMinutes(parseInt(time[3], 10) || 0); d.setSeconds(0, 0);

Brad
A: 

Hi,

Here's a solution more for all of those who are using a 24h clock:

function parseTime(text) {
  var time = text.match(/(\d?\d):?(\d?\d?)/);
    var h = parseInt(time[1]);
    var m = parseInt(time[2]) || 0;

    if (h > 24) {
        // try a different format
        time = text.match(/(\d)(\d?\d?)/);
        h = parseInt(time[1]);
        m = parseInt(time[2]) || 0;
    } 

  var d = new Date();
  d.setHours(h);
  d.setMinutes(m);
  return d;     
}

Note, that this function also supports parsing of strings like

  • 0820 -> 08:20
  • 32 -> 03:02
  • 124 -> 12:04
Stefan Haberl