How to parse a time into a Date object from user input in JavaScript?

263

Solution 1

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

function parseTime( t ) {
   var d = new Date();
   var time = t.match( /(\d+)(?::(\d\d))?\s*(p?)/ );
   d.setHours( parseInt( time[1]) + (time[3] ? 12 : 0) );
   d.setMinutes( parseInt( time[2]) || 0 );
   return d;
}

var tests = [
  '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', '1a', '12', '12a', '12p', '12am', '12pm', '2400am', '2400pm', '2400', 
  '1000', '100', '123', '2459', '2359', '2359am', '1100', '123p',
  '1234', '1', '9', '99', '999', '9999', '99999', '0000', '0011', '-1', 'mioaw' ];

for ( var i = 0; i < tests.length; i++ ) {
  console.log( tests[i].padStart( 9, ' ' ) + " = " + parseTime(tests[i]) );
}

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

Warning: The code doe not work with 12:00 AM, etc.

Solution 2

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


var tests = [
  '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', '1a', '12', '12a', '12p', '12am', '12pm', '2400am', '2400pm', '2400', 
  '1000', '100', '123', '2459', '2359', '2359am', '1100', '123p',
  '1234', '1', '9', '99', '999', '9999', '99999', '0000', '0011', '-1', 'mioaw' ];

for ( var i = 0; i < tests.length; i++ ) {
  console.log( tests[i].padStart( 9, ' ' ) + " = " + parseTime(tests[i]) );
}

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

Solution 3

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

Solution 4

Most of the regex solutions here throw errors when the string can't be parsed, and not many of them account for strings like 1330 or 130pm. Even though these formats weren't specified by the OP, I find them critical for parsing dates input by humans.

All of this got me to thinking that using a regular expression might not be the best approach for this.

My solution is a function that not only parses the time, but also allows you to specify an output format and a step (interval) at which to round minutes to. At about 70 lines, it's still lightweight and parses all of the aforementioned formats as well as ones without colons.

function parseTime(time, format, step) {
	
	var hour, minute, stepMinute,
		defaultFormat = 'g:ia',
		pm = time.match(/p/i) !== null,
		num = time.replace(/[^0-9]/g, '');
	
	// Parse for hour and minute
	switch(num.length) {
		case 4:
			hour = parseInt(num[0] + num[1], 10);
			minute = parseInt(num[2] + num[3], 10);
			break;
		case 3:
			hour = parseInt(num[0], 10);
			minute = parseInt(num[1] + num[2], 10);
			break;
		case 2:
		case 1:
			hour = parseInt(num[0] + (num[1] || ''), 10);
			minute = 0;
			break;
		default:
			return '';
	}
	
	// Make sure hour is in 24 hour format
	if( pm === true && hour > 0 && hour < 12 ) hour += 12;
	
	// Force pm for hours between 13:00 and 23:00
	if( hour >= 13 && hour <= 23 ) pm = true;
	
	// Handle step
	if( step ) {
		// Step to the nearest hour requires 60, not 0
		if( step === 0 ) step = 60;
		// Round to nearest step
		stepMinute = (Math.round(minute / step) * step) % 60;
		// Do we need to round the hour up?
		if( stepMinute === 0 && minute >= 30 ) {
			hour++;
			// Do we need to switch am/pm?
			if( hour === 12 || hour === 24 ) pm = !pm;
		}
		minute = stepMinute;
	}
	
	// Keep within range
	if( hour <= 0 || hour >= 24 ) hour = 0;
	if( minute < 0 || minute > 59 ) minute = 0;

	// Format output
	return (format || defaultFormat)
		// 12 hour without leading 0
        .replace(/g/g, hour === 0 ? '12' : 'g')
		.replace(/g/g, hour > 12 ? hour - 12 : hour)
		// 24 hour without leading 0
		.replace(/G/g, hour)
		// 12 hour with leading 0
		.replace(/h/g, hour.toString().length > 1 ? (hour > 12 ? hour - 12 : hour) : '0' + (hour > 12 ? hour - 12 : hour))
		// 24 hour with leading 0
		.replace(/H/g, hour.toString().length > 1 ? hour : '0' + hour)
		// minutes with leading zero
		.replace(/i/g, minute.toString().length > 1 ? minute : '0' + minute)
		// simulate seconds
		.replace(/s/g, '00')
		// lowercase am/pm
		.replace(/a/g, pm ? 'pm' : 'am')
		// lowercase am/pm
		.replace(/A/g, pm ? 'PM' : 'AM');
}

var tests = [
  '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', '1a', '12', '12a', '12p', '12am', '12pm', '2400am', '2400pm', '2400', 
  '1000', '100', '123', '2459', '2359', '2359am', '1100', '123p',
  '1234', '1', '9', '99', '999', '9999', '99999', '0000', '0011', '-1', 'mioaw' ];

for ( var i = 0; i < tests.length; i++ ) {
  console.log( tests[i].padStart( 9, ' ' ) + " = " + parseTime(tests[i]) );
}

Solution 5

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

function 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;
}

var tests = [
  '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', '1a', '12', '12a', '12p', '12am', '12pm', '2400am', '2400pm', '2400', 
  '1000', '100', '123', '2459', '2359', '2359am', '1100', '123p',
  '1234', '1', '9', '99', '999', '9999', '99999', '0000', '0011', '-1', 'mioaw' ];

for ( var i = 0; i < tests.length; i++ ) {
  console.log( tests[i].padStart( 9, ' ' ) + " = " + parseTime(tests[i]) );
}

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"
Share:
263
MAAAAANI
Author by

MAAAAANI

Updated on July 05, 2022

Comments

  • MAAAAANI
    MAAAAANI almost 2 years

    I'm using node.js with socketstream..I'm getting following error while running my

    "appln.Cannot find module '/entry' [Break On This Error]

    throw new Error("Cannot find module '" + x + "'")

               NewNodeProject
     client
       -code
          -app
          -libs 
       -css
          -libs
     static
     templates
     views
       -app.html
     node_modules
       -express
       -socketstream
     server
       -rpc
          -user.js
       -app.js
    
    • paulbjensen
      paulbjensen over 11 years
      Hi Maaaaani, did you generate that project using 'socketstream new <project_name>'? It looks like you're missing an entry.js file at /client/code/app/entry.js
    • MAAAAANI
      MAAAAANI over 11 years
      @paulbjensen : no i didn't generate the project using 'socketstream new <project_name>'.ok i will try that.
    • MAAAAANI
      MAAAAANI over 11 years
      @paulbjensen:thanks.now my application is working fine..
  • Jason Bunting
    Jason Bunting over 15 years
    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!!!
  • serverpunk
    serverpunk over 15 years
    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.
  • Joe Lencioni
    Joe Lencioni over 15 years
    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
    Joe Lencioni over 15 years
    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
    Joe Lencioni over 15 years
    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 && time[3] ) ? 12 : 0) );
  • Joe Lencioni
    Joe Lencioni over 15 years
    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
  • peller
    peller over 14 years
    You'd better hope d doesn't fall on a day where a daylight savings change takes effect. This also assumes English conventions.
  • Andrew M. Andrews III
    Andrew M. Andrews III about 14 years
    I should add: skip past the picker stuff at the top 3/4 of that page and look at the section on converting: ama3.com/anytime/#converting
  • Benji York
    Benji York almost 14 years
    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
    Benji York almost 14 years
    This has the same bugs I noted in the highest-voted answer above.
  • Lucas Gabriel Sánchez
    Lucas Gabriel Sánchez almost 13 years
    An alternative to using "[pP]" would be to append "i" to the end of the literal. That would make the match case-insensitive.
  • johndodo
    johndodo about 12 years
    Huge +1! As for the size - actually it is 25KB per locale. But at least it supports locales! Instead of writing you own procedures, use what is available (or write a better library and share it). Also, while whitespace is stripped from JS, it doesn't look minified to me, so you might be able to save some bytes there. If it matters that much to you.
  • Jonas N
    Jonas N over 11 years
    Tried it at datejs.com (under "Mad Skillz…") "12 wed 2020" ==> "Monday, December 03, 2012 12:00:00 AM" -- wat? 2012??
  • Lomithrani
    Lomithrani over 11 years
    I'm a human and I don't know what "12 wed 2020" means. There are multiple ways to interpret that. If I were to guess what DateJS was doing, I would say that it interpreted "12" as the day of the month and looked for the next Wednesday that fell on the 12th, which is in December. The only thing that surprised me was that it threw away "2020" instead of interpreting it as 8:20pm.
  • Michael Trouw
    Michael Trouw about 11 years
    And now also available in function form, for the lazy (like me): var timeParser = function(stringTime) { var d = new Date(); var time = stringTime.match(/(\d+)(?::(\d\d))?\s*(p?)/); d.setHours( parseInt(time[1]) + (time[3] ? 12 : 0) ); d.setMinutes( parseInt(time[2]) || 0 ); //console.log( d ); return d; }
  • Peter Wone
    Peter Wone almost 10 years
    I had to mark this up. It's the only really useful answer when size matters. Most of the time I use momentjs but that's enormous compared to this solution.
  • Nomenator
    Nomenator almost 9 years
    I needed that, and so went ahead and fixed that with a dirty hack: codepen.io/anon/pen/EjrVqq there should be a better solution, but I couldn't put my finger on it yet.
  • braks
    braks over 8 years
    A word of advice to anyone who has found this through Google like I did. Don't use it. It seems to work, but it's wrong for times around 12am. The comments/edits don't solve this. Nathan's solution is more complete.
  • jwg
    jwg about 7 years
    @SebastianMach Even at Venezuelan speeds, 25KB only takes 1/8 of a second.
  • Sebastian Mach
    Sebastian Mach about 7 years
    @Jim: I am just talking potential features. Never say never, especially with ambitious product owners.
  • gruppler
    gruppler over 6 years
    This package doesn't support 24-hour time, which may or may not be a significant limitation.
  • Sgnl
    Sgnl over 6 years
    @gruppler thanks for that very important note. FWIW, there is a Pull Request with work done to support the 24-hour format. The PR was submitted in 2014. Anyway, here's a link to that Pull Request: github.com/zackdever/time/pull/7
  • Qwertie
    Qwertie almost 6 years
    This package doesn't support seconds or milliseconds. (Most of the answers here don't either, but I tend to expect more from a "package" than a code snippet in an SO answer.)
  • Dave Jarvis
    Dave Jarvis about 4 years
    Note that for time-tracking applications, 1100 would want to resolve as 11:00am. (I'd wager that most human activities that we enter as data take place during the day.) If you can recall the edge cases where my solution was lacking, a comment to my answer would be appreciated. Nice summary!
  • Dave Jarvis
    Dave Jarvis about 4 years
    As a minor nit, string = String(string); can be string = String(string.toLowerCase()) to avoid a redundant call. There are some other simplifications around the boolean logic that can be made..
  • Dave Jarvis
    Dave Jarvis about 4 years
    A few other points: the 99* tests give inconsistent results, 1000 could mean 10:00am, and -1 as 1:00pm is good. However, returning a Date object seems to go a bit beyond the use case. (It assumes that the current Date is also desired, which isn't necessarily true for any particular time being entered.)
  • V. Rubinetti
    V. Rubinetti about 4 years
    It was a long time ago that I wrote this answer, but note that I wrote this code for a Google Calendar extension, so I made it mimic that behavior, even if the behavior was questionable. For example, 1100 maps to 11pm in GCal. Re: 99*, I don't know. It could be debated what 999, 9999, etc should interpreted as, but my comments say my assumptions. GCal in that case doesn't even accept eg 52:95. Finally, re: Date. It provides some other nice get and format functions. Also some libraries need things in Date format, which was my case iirc.
  • Heretic Monkey
    Heretic Monkey about 3 years
    This is actually a bad answer, because it doesn't show how to use the library to perform the task in question. The library is great, but the answer sucks.
  • Ralph Yozzo
    Ralph Yozzo almost 3 years
    This one has issues with 12:30am etc. stackoverflow.com/users/264837/nathan-villaescusa answer is better.