如何在JavaScript中将用户输入的时间解析为Date对象?

79

我正在为用户设计一个表单小部件,用于将一天中的时间输入到文本输入框中(用于日历应用程序)。使用 JavaScript(我们使用 jQuery FWIW),我想找到将用户输入的文本解析为 JavaScript Date() 对象的最佳方法,以便可以轻松地执行比较和其他操作。

我尝试了 parse() 方法,但对我的需求来说有点太挑剔了。我希望它能够成功解析以下示例输入时间(以及其他逻辑上类似的时间格式)为相同的 Date() 对象:

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

我想使用正则表达式来拆分输入并提取要用于创建我的 Date() 对象的信息。有什么最好的方法吗?

24个回答

78

以下是针对您提供的输入快速解决方案:

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

它也适用于其他一些变体(即使使用a.m.,例如,它仍然可以工作)。显然这很粗糙,但它也非常轻量级(比如使用一个完整的库要便宜得多)。

警告:该代码不适用于12:00 AM等情况。


8
在处理这个问题时,我发现它不能正确解析时间“12 pm”的变体,因为它会将数字小时数加上12。为了修复这个问题,我更改了d.setHours行的代码,更改后如下:d.setHours(parseInt(time[1]) + ((parseInt(time[1]) < 12 && time[3]) ? 12 : 0)); - Joe Lencioni
3
我注意到parseInt无法解析形如“:30”或“:00”的字符串,因此我将正则表达式改为在不包含冒号的情况下捕获分钟数。 - Joe Lencioni
8
在调用ParseInt时,需要使用10进制基数,因为在有前导0的情况下,JS会假定8进制基数,导致小时数被解释为0,如果它大于8且有前导0(因为08不是有效的8进制数字)。此外,将“p?”更改为“[pP]?”将使其在AM / PM为大写字母时也能起作用。总的来说,除非您非常确定这种方法适用于您,否则应使用库。请记住,时间对我们所有人都不友善。 - Benji York
5
使用"[pP]"的另一种选择是在文字结尾添加"i"。这样可以进行大小写不敏感的匹配。 - Chris Miller
3
对于通过谷歌搜索到这篇文章的人,我有一个建议:不要使用它。尽管看起来它可以工作,但在午夜12点左右就会出现错误。评论/编辑无法解决这个问题。Nathan的解决方案更为完整。 - braks
显示剩余3条评论

55
所有提供的示例都无法在凌晨12:00到12:59之间工作。如果正则表达式不匹配时间,它们还会抛出错误。以下代码可以解决这个问题: ```js function parseTime(str) { const match = /^(\d{1,2}):(\d{2})(am|pm)$/i.exec(str); if (!match) { return null; } let [, hours, minutes, meridian] = match; hours = parseInt(hours); if (hours === 12 && meridian.toLowerCase() === 'am') { hours -= 12; } else if (hours !== 12 && meridian.toLowerCase() === 'pm') { hours += 12; } return { hours, minutes: parseInt(minutes) }; } ```

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]) );
}
这将适用于任何包含时间的字符串。因此,"abcde12:00pmdef" 将被解析并返回 12 pm。如果希望它仅在字符串中仅包含时间时返回时间,则可以使用以下正则表达式,只需将 "time[4]" 替换为 "time[6]"。
/^(\d+)(:(\d\d))?\s*((a|(p))m?)?$/i

32
不要费力自己做,只需使用datejs

39
仅为了处理日期就需要25KB吗?!我是说,毫无疑问这是一个很好的库,如果我需要精确的日期处理功能,那它肯定是个好选择。但是25KB的大小比jQuery核心的总和还要大!!! - Jason Bunting
4
考虑到您想要接受的输入范围,我也会选择使用 datejs。它似乎可以处理大多数情况,除了仅为数字的情况,它会将其作为月份中的日期。 - Jonny Buchanan
1
非常赞!关于大小 - 实际上每个语言环境是25KB。但至少它支持多语言环境!不要编写自己的程序,使用可用的内容(或编写更好的库并共享)。此外,虽然JS中的空格被删除,但对我来说它看起来并没有被压缩,因此您可能可以在那里节省一些字节。如果这对您很重要的话。 - johndodo
3
我是一名人类,不知道“12 wed 2020”是什么意思。有多种解释方法。如果我猜测DateJS在做什么,我会认为它把“12”解释为月份中的几号,然后寻找下一个落在12日的星期三,这个日期是在十二月份。唯一让我惊讶的是它放弃了“2020”的部分而没有将其解释为晚上8点20分。 - Jim
1
这实际上是一个糟糕的答案,因为它没有展示如何使用库来执行所需的任务。该库很棒,但答案很差。 - Heretic Monkey
显示剩余4条评论

16
这里大部分正则表达式的解决方案在无法解析字符串时会抛出错误,而且很少有这些方案能够处理像 1330 或者 130pm 这样的字符串。尽管这些格式没有被 OP 指定,但我认为它们对于解析人类输入的日期非常重要。
所有这些让我想到使用正则表达式可能不是最好的方法。
我的解决方案是一个函数,它不仅可以解析时间,还允许您指定输出格式和将分钟舍入到的步骤(间隔)。它只有约 70 行代码,仍然轻巧,并且可以解析前面提到的所有格式以及不带冒号的格式。
演示:http://jsfiddle.net/HwwzS/1/ 代码:https://gist.github.com/claviska/4744736

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


1
我必须标记这个。当尺寸很重要时,这是唯一真正有用的答案。大多数情况下,我使用momentjs,但与此解决方案相比,它非常庞大。 - Peter Wone
3
我需要那个,所以就用了一个简单粗暴的方法解决了:http://codepen.io/anon/pen/EjrVqq 也许有更好的解决办法,但我还无法确定。 - Nomenator

12

这里是对Joe版本的改进。欢迎进一步编辑。

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

更新:

  • 添加了radix参数到parseInt()调用中(以便jslint不会抱怨)。
  • 使正则表达式不区分大小写,因此"2:23 PM"可以像"2:23 pm"一样工作。

3

以下是针对使用支持24小时制的用户的解决方案:

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

请注意,以上时间格式均为两位小时数加两位分钟数。

function parseTime(text) {
  var time = text.match(/(\d?\d):?(\d?\d?)/);
 var h = parseInt(time[1], 10);
 var m = parseInt(time[2], 10) || 0;
 
 if (h > 24) {
        // try a different format
  time = text.match(/(\d)(\d?\d?)/);
  h = parseInt(time[1], 10);
  m = parseInt(time[2], 10) || 0;
 } 
 
  var d = new Date();
  d.setHours(h);
  d.setMinutes(m);
  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]) );
}


3

我在实施John Resig的解决方案时遇到了几个问题。这里是我根据他的答案修改后一直在使用的函数:

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

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


这个程序存在与我在上面得到最高票答案中指出的相同的漏洞。 - Benji York

3

其他答案的编译表

首先,我无法相信没有内置功能或者强大的第三方库可以处理这个问题。实际上,这是Web开发,我可以相信。

尝试使用所有这些不同算法测试所有边缘情况让我的头都晕了,因此我将所有答案和测试编译成了一个方便的表格。

代码(以及生成的表格)太大了,无法内联,所以我做了一个JSFiddle:

http://jsfiddle.net/jLv16ydb/4/show

// heres some filler code of the functions I included in the test,
// because StackOverfleaux wont let me have a jsfiddle link without code
Functions = [
    JohnResig,
    Qwertie,
    PatrickMcElhaney,
    Brad,
    NathanVillaescusa,
    DaveJarvis,
    AndrewCetinic,
    StefanHaberl,
    PieterDeZwart,
    JoeLencioni,
    Claviska,
    RobG,
    DateJS,
    MomentJS
];
// I didn't include `date-fns`, because it seems to have even more
// limited parsing than MomentJS or DateJS

请随意分叉我的小工具,并添加更多算法和测试用例。
我没有添加任何结果与“期望”输出之间的比较,因为有些情况下,“期望”输出可能存在争议(例如,12 应该被解释为 12:00am 还是 12:00pm?)。您需要仔细查看表格,看哪个算法对您来说最有意义。
注意:颜色并不一定表示输出质量或“期望性”,它们只表示输出类型:
- 红色 = js 错误抛出 - 黄色 = "falsy" 值(undefined,null,NaN,"","invalid date") - 绿色 = js Date() 对象 - 浅绿色 = 其他所有值
如果输出是一个 Date() 对象,则会将其转换为24小时 HH:mm 格式以便比较。

有趣...我建议大家特别关注图书馆对mioaw999aaa12:34aaa2400等输入的响应,因为这些边缘情况是争议的主要焦点,尽管在像1pm这样不太极端的情况下也存在一些分歧。 - Qwertie

2
这里有另一种方法,可以覆盖原始答案、任何合理数量的数字、猫的数据输入和逻辑谬误。算法如下:
  1. 确定子午线是否为post meridiem
  2. 将输入数字转换为整数值。
  3. 时间在0到24之间:小时是整点,没有分钟(12小时是下午)。
  4. 时间在100到2359之间:小时除以100是整点,分钟模100余数。
  5. 从2400开始的时间:小时是午夜,余数是分钟。
  6. 当小时超过12时,减去12并强制post meridiem为true。
  7. 当分钟超过59时,强制为59。
将小时、分钟和post meridiem转换为Date对象是读者的练习(许多其他答案展示了如何做到这一点)。

"use strict";

String.prototype.toTime = function () {
  var time = this;
  var post_meridiem = false;
  var ante_meridiem = false;
  var hours = 0;
  var minutes = 0;

  if( time != null ) {
    post_meridiem = time.match( /p/i ) !== null;
    ante_meridiem = time.match( /a/i ) !== null;

    // Preserve 2400h time by changing leading zeros to 24.
    time = time.replace( /^00/, '24' );

    // Strip the string down to digits and convert to a number.
    time = parseInt( time.replace( /\D/g, '' ) );
  }
  else {
    time = 0;
  }

  if( time > 0 && time < 24 ) {
    // 1 through 23 become hours, no minutes.
    hours = time;
  }
  else if( time >= 100 && time <= 2359 ) {
    // 100 through 2359 become hours and two-digit minutes.
    hours = ~~(time / 100);
    minutes = time % 100;
  }
  else if( time >= 2400 ) {
    // After 2400, it's midnight again.
    minutes = (time % 100);
    post_meridiem = false;
  }

  if( hours == 12 && ante_meridiem === false ) {
    post_meridiem = true;
  }

  if( hours > 12 ) {
    post_meridiem = true;
    hours -= 12;
  }

  if( minutes > 59 ) {
    minutes = 59;
  }

  var result =
    (""+hours).padStart( 2, "0" ) + ":" + (""+minutes).padStart( 2, "0" ) +
    (post_meridiem ? "PM" : "AM");

  return result;
};

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, ' ' ) + " = " + tests[i].toTime() );
}

使用jQuery时,新定义的String原型的用法如下:
  <input type="text" class="time" />

  $(".time").change( function() {
    var $this = $(this);
    $(this).val( time.toTime() );
  });

2

我对其他答案不满意,所以我又写了一个。

  • 识别秒和毫秒
  • 对于无效输入,如“13:00pm”或“11:65”,返回undefined
  • 如果提供了localDate参数,则返回本地时间;否则,在Unix纪元(1970年1月1日)上返回UTC时间。
  • 支持军事时间,例如1330(要禁用,请使正则表达式中的第一个“:”为必需)
  • 允许单独使用小时,使用24小时制(即“7”表示上午7点)
  • 允许将小时24视为小时0的同义词,但不允许小时25
  • 要求时间位于字符串开头(要禁用,请在正则表达式中删除^\s*
  • 具有测试代码,可检测输出是否不正确。

编辑:现在它是一个,包括一个timeToString格式化程序:npm i simplertime


/**
 * Parses a string into a Date. Supports several formats: "12", "1234",
 * "12:34", "12:34pm", "12:34 PM", "12:34:56 pm", and "12:34:56.789".
 * The time must be at the beginning of the string but can have leading spaces.
 * Anything is allowed after the time as long as the time itself appears to
 * be valid, e.g. "12:34*Z" is OK but "12345" is not.
 * @param {string} t Time string, e.g. "1435" or "2:35 PM" or "14:35:00.0"
 * @param {Date|undefined} localDate If this parameter is provided, setHours
 *        is called on it. Otherwise, setUTCHours is called on 1970/1/1.
 * @returns {Date|undefined} The parsed date, if parsing succeeded.
 */
function parseTime(t, localDate) {
  // ?: means non-capturing group and ?! is zero-width negative lookahead
  var time = t.match(/^\s*(\d\d?)(?::?(\d\d))?(?::(\d\d))?(?!\d)(\.\d+)?\s*(pm?|am?)?/i);
  if (time) {
    var hour = parseInt(time[1]), pm = (time[5] || ' ')[0].toUpperCase();
    var min = time[2] ? parseInt(time[2]) : 0;
    var sec = time[3] ? parseInt(time[3]) : 0;
    var ms = (time[4] ? parseFloat(time[4]) * 1000 : 0);
    if (pm !== ' ' && (hour == 0 || hour > 12) || hour > 24 || min >= 60 || sec >= 60)
      return undefined;
    if (pm === 'A' && hour === 12) hour = 0;
    if (pm === 'P' && hour !== 12) hour += 12;
    if (hour === 24) hour = 0;
    var date = new Date(localDate!==undefined ? localDate.valueOf() : 0);
    var set = (localDate!==undefined ? date.setHours : date.setUTCHours);
    set.call(date, hour, min, sec, ms);
    return date;
  }
  return undefined;
}

var testSuite = {
  '1300':  ['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', '1:00:00PM', '1300', '13'],
  '1100':  ['11:00am', '11:00 AM', '11:00', '11:00:00', '1100'],
  '1359':  ['1:59 PM', '13:59', '13:59:00', '1359', '1359:00', '0159pm'],
  '100':   ['1:00am', '1:00 am', '0100', '1', '1a', '1 am'],
  '0':     ['00:00', '24:00', '12:00am', '12am', '12:00:00 AM', '0000', '1200 AM'],
  '30':    ['0:30', '00:30', '24:30', '00:30:00', '12:30:00 am', '0030', '1230am'],
  '1435':  ["2:35 PM", "14:35:00.0", "1435"],
  '715.5': ["7:15:30", "7:15:30am"],
  '109':   ['109'], // Three-digit numbers work (I wasn't sure if they would)
  '':      ['12:60', '11:59:99', '-12:00', 'foo', '0660', '12345', '25:00'],
};

var passed = 0;
for (var key in testSuite) {
  let num = parseFloat(key), h = num / 100 | 0;
  let m = num % 100 | 0, s = (num % 1) * 60;
  let expected = Date.UTC(1970, 0, 1, h, m, s); // Month is zero-based
  let strings = testSuite[key];
  for (let i = 0; i < strings.length; i++) {
    var result = parseTime(strings[i]);
    if (result === undefined ? key !== '' : key === '' || expected !== result.valueOf()) {
      console.log(`Test failed at ${key}:"${strings[i]}" with result ${result ? result.toUTCString() : 'undefined'}`);
    } else {
      passed++;
    }
  }
}
console.log(passed + ' tests passed.');

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接