将 ISO 8601 日期时间字符串转换为 **Date** 对象时,如何将日期时间重新定位到当前时区?

Posted

技术标签:

【中文标题】将 ISO 8601 日期时间字符串转换为 **Date** 对象时,如何将日期时间重新定位到当前时区?【英文标题】:How to reposition datetime into the current timezone when converting an ISO 8601 datetime string to a **Date** object? 【发布时间】:2020-04-16 02:15:21 【问题描述】:

假设我们在 2020 年 1 月 1 日午夜在伦敦,并进入一个应用程序,该应用程序将日期时间存储为这样的 ISO-8601 字符串。2020-01-01T00:00:00-00:00

稍后,我在洛杉矶,想在需要 javascript 日期对象的图表上查看此日期。

获取本地化的日期对象很容易。

const iso8601Date = '2020-01-01T00:00:00+00:00';
const theDate = new Date(iso8601Date);

console.log(typeOf(theDate)); // date
console.log(theDate);        // Tue Dec 31 2019 16:00:00 GMT-0800 (PST)

但是,有时我们想“忽略”时区偏移并分析数据,就好像它发生在当前时区一样。

这是我正在寻找但不知道如何完成的结果。

const iso8601Date = '2020-01-01T00:00:00+00:00';
const theRepositionedDate = someMagic(iso8601Date);

console.log(typeOf(theRepositionedDate)); // date
console.log(theRepositionedDate);         // Wed Jan 01 2020 00:00:00 GMT-0800 (PST)

如何重新定位日期并返回一个日期对象?

/* Helper function

Returns the object type
https://***.com/a/28475133/25197
    typeOf(); //undefined
    typeOf(null); //null
    typeOf(NaN); //number
    typeOf(5); //number
    typeOf(); //object
    typeOf([]); //array
    typeOf(''); //string
    typeOf(function () ); //function
    typeOf(/a/) //regexp
    typeOf(new Date()) //date
*/

function typeOf(obj) 
  return .toString
    .call(obj)
    .split(' ')[1]
    .slice(0, -1)
    .toLowerCase();

【问题讨论】:

【参考方案1】:

这实际上是 Why does Date.parse give incorrect results? 的复制品,但乍一看可能并不明显。

解析时间戳的第一条规则是“不要使用内置解析器”,即使对于 ECMA-262 支持的 2 或 3 种格式也是如此。

要可靠地解析时间戳,您必须知道格式。内置解析器会尝试解决它,因此它们之间存在差异,很可能会产生意想不到的结果。碰巧 '2020-01-01T00:00:00+00:00' 可能是唯一实际可靠解析的受支持格式。但它与严格的 ISO 8601 确实略有不同,并且不同的浏览器在应用 ECMAScript 解析规则的严格程度上也有所不同,因此很容易出错。

您可以通过修剪偏移信息将其转换为“本地”时间戳,即“2020-01-01T00:00:00”,但 Safari 至少会出错并将其视为 UTC。 ECMAScrip 本身与 ISO 8601 不一致,因为它将 ISO 8601 的仅日期形式视为 UTC(即当 ISO 8601 将其视为本地时,将“2020-01-01”视为 UTC)。

所以只需编写自己的解析器或使用库,有很多可供选择。如果你只是在寻找解析和格式化,有一些小于 2k 的缩小(还有examples on SO)。

如果您只想支持直接的 ISO 8601 等格式,例如,编写自己的代码并没有那么困难,例如

// Parse ISO 8601 timestamps in YYYY-MM-DDTHH:mm:ss±HH:mm format
// Optional "T" date time separator and
// Optional ":" offset hour minute separator
function parseIso(s, local) 
  let offset = (s.match(/[+-]\d\d:?\d\d$/) || [])[0];
  let b = s.split(/\D/g);
  // By default create a "local" date
  let d = new Date(
    b[0],
    b[1]-1,
    b[2] || 1,
    b[3] || 0,
    b[4] || 0,
    b[5] || 0
  );
  // Use offset if present and not told to ignore it
  if (offset && !local)
    let sign = /^\+/.test(offset)? 1 : -1;
    let [h, m] = offset.match(/\d\d/g);
    d.setMinutes(d.getMinutes() - sign * (h*60 + m*1) - d.getTimezoneOffset());
  
  return d;


// Samples
['2020-01-01T00:00:00+00:00', // UTC, ISO 8601 standard
 '2020-01-01 00:00:00+05:30', // IST, missing T
 '2020-01-01T00:00:00-0400',  // US EST, missing T and :
 '2020-01-01 00:00:00',       // No timezone, local always
 '2020-01-01'                 // Date-only as local (differs from ECMA-262)
].forEach(s => 
  console.log(s);
  console.log('Using offset\n' + parseIso(s).toString());
  console.log('Ignoring offset\n' + parseIso(s, true).toString());
);

【讨论】:

嘿@RobG。我整天为此工作,但没有成功。我非常感谢您的回复。它运行良好。 ? 再次感谢您。我今天又玩了一些,希望尽可能加快速度。如果您对结果感兴趣,请在codereview.*** 上发布。【参考方案2】:

const createDate = (isoDate) => 
  isoDate = new Date(isoDate)

  return new Date(Date.UTC(
    isoDate.getUTCFullYear(),
    isoDate.getUTCMonth(),
    isoDate.getUTCDate(),
    isoDate.getUTCMinutes(),
    isoDate.getUTCSeconds(),
    isoDate.getUTCMilliseconds()
  ));


const iso8601Date = '2020-01-01T00:00:00+00:00';
const theRepositionedDate = createDate(iso8601Date);

console.log(theRepositionedDate instanceof Date); // true
console.log(theRepositionedDate);

【讨论】:

谢谢...但这将返回一个string。我需要一个date 可以使用Date.prototype.getUTCDate()等UTC方法获取你想要的值。 感谢@aagamezl 的帮助。我已经尝试了很多东西,包括***上其他问题的一堆东西,但我无法得到我正在寻找的结果。带有工作代码的答案会非常有帮助。 这里的问题是它假设输入偏移量总是为零(UTC)。 OP表示数据可能来自伦敦。英国在冬季使用 +00:00,但在夏季使用 +01:00。 @aagamezl 请不要因为第一次尝试而气馁。继续提供帮助!【参考方案3】:

但是,有时我们想“忽略”时区偏移并分析数据,就好像它发生在当前时区一样。

好的,那就忽略它。

const iso8601Date = '2020-01-01T00:00:00+00:00';
const theDate = new Date(iso8601Date.substring(0, 19));

之所以有效,是因为您正在从 2020-01-01T00:00:00 创建一个 Date 对象 - 一个没有偏移的 ISO 8601 日期时间。

ECMAScript section 20.3.1.15 - Date Time String Format 说:

当不存在时区偏移时,仅日期形式被解释为 UTC 时间,日期时间形式被解释为本地时间

【讨论】:

是的,但 Safari 仍然会出错并将 2020-01-01T00:00:00 视为 UTC。可惜Date Time String Format: default time zone difference from ES5 not web-compatible。 :-( @RobG - Arrrggghhh... 你是对的。 Mac 上的 Safari 最新版 (13) 没有正确实现这一点。 (Screenshot) 仅供参考 - 我使用他们的反馈助手工具向 Apple 报告了该错误。 (这里没有提供链接到这里的公共网址,抱歉)【参考方案4】:

基于@RobG 的回答,我可以通过使用单个正则表达式来加快这个速度。在这里发布以供后代使用。

const isoToDate = (iso8601, ignoreTimezone = false) => 
  // Differences from default `new Date()` are...
  // - Returns a local datetime for all without-timezone inputs, including date-only strings.
  // - ignoreTimezone processes datetimes-with-timezones as if they are without-timezones.
  // - Accurate across all mobile browsers.  https://***.com/a/61242262/25197

  const dateTimeParts = iso8601.match(
    /(\d4)-(\d2)-(\d2)(?:[T ](\d2):(\d2):(\d2)(?:\.(\d0,7))?(?:([+-])(\d2):(\d2))?)?/,
  );

  // Create a "localized" Date by always specifying a time. If you create a date without specifying
  // time a date set at midnight in UTC Zulu is returned.  https://www.diigo.com/0hc3eb
  const date = new Date(
    dateTimeParts[1], // year
    dateTimeParts[2] - 1, // month (0-indexed)
    dateTimeParts[3] || 1, // day
    dateTimeParts[4] || 0, // hours
    dateTimeParts[5] || 0, // minutes
    dateTimeParts[6] || 0, // seconds
    dateTimeParts[7] || 0, // milliseconds
  );

  const sign = dateTimeParts[8];
  if (sign && ignoreTimezone === false) 
    const direction = sign === '+' ? 1 : -1;
    const hoursOffset = dateTimeParts[9] || 0;
    const minutesOffset = dateTimeParts[10] || 0;
    const offset = direction * (hoursOffset * 60 + minutesOffset * 1);
    date.setMinutes(date.getMinutes() - offset - date.getTimezoneOffset());
  

  return date;
;

主要区别在于单个正则表达式一次返回所有匹配的组。

Here's a regex101 及其匹配/分组的一些示例。

它的速度大约是 @RobG 令人敬畏的公认答案的两倍,比 moment.jsdate-fns 包快 4-6 倍。 ?

【讨论】:

以上是关于将 ISO 8601 日期时间字符串转换为 **Date** 对象时,如何将日期时间重新定位到当前时区?的主要内容,如果未能解决你的问题,请参考以下文章

Python - 将日期的字符串表示形式转换为 ISO 8601

将日期对象转换为ISO 8601格式的字符串

Swift:将 ISO8601 转换为日期

一种将当前日期时间转换为 ISO 8601 格式的优雅方法 [重复]

将 ISO8601 字符串隐式转换为 Debezium 的 TIMESTAMPTZ (postgresql)

如何配置 web api 的 json 序列化器将日期时间转换为 ISO 8601 格式?