如何在.NET中进行ISO8601日期时间的正式正确解析?

8

有许多关于在.NET和C#中解析ISO8601日期/时间的SO问题和答案。然而,似乎没有任何“权威”的答案,即一个能够正确解析ISO8601中所有可能格式变体并禁止非ISO8601变体的格式正确的ISO8601解析器。

到目前为止,这个SO答案是最接近的匹配...

如何从ISO 8601格式创建.NET DateTime


3
需要注意的两个特殊情况:24:00以及使用逗号作为时间小数分隔符... - Jon Skeet
2
根据标准本身,句号是边缘情况,逗号是“正常”情况。在互联网相关案例中使用的大多数配置文件都规定使用句号,因此很多程序员更经常看到它。一个非常恶心的边缘情况是,在2004年修订之前,“150102”是“2015-01-02”和“15:01:02”的有效短格式,因此,如果需要向后兼容2000版本,则无法在没有带外信息的情况下“正确解析ISO8601中的所有可能格式变体”。 - Jon Hanna
1
@JonHanna:是的,我意识到了所有这些——我关于使用逗号分隔符的观点是,大多数程序员甚至不会考虑它。(坦白地说,我认为允许它是一个错误,但这是另一回事。) - Jon Skeet
1
@JonHanna:问题在于它是一种机器可读格式,而“.”通常用作数字的机器可读十进制分隔符,所以我不知道为什么他们决定在时间中使用逗号... - Jon Skeet
1
@JonSkeet,有很多可以使用的。不过我发现更常用的配置文件仍然很有用。但是,对于更常用的配置文件,一个特殊情况是,如果有人使用接受ISO 8601的解析器,那么你将允许像逗号这样的东西,而实际上应该禁止它。 - Jon Hanna
显示剩余5条评论
1个回答

30
好的。让我们从您对ISO 8601的限制开始:
  1. 您想要一个DateTime,因此不需要所有只产生时间、持续时间、年份或年份和月份的格式。
  2. 您想要一个DateTime,因此时区信息将变为“未指定”、“UTC”或“本地”,无法回到相同的时区。
  3. 您想要一个DateTime,因此失去了超过100纳秒的精度。
这使我们需要支持不到一半的ISO 8601格式,并解决了模棱两可的情况,因为它在日期和时间之间有歧义。
让我们从那些可以使用DateTime.ParseExact处理的格式开始:
DateTime.ParseExact(dateString, new string[]
  {
  "yyyy-MM-ddK", "yyyyMMddK", "yy-MM-ddK", "yyMMddK",
  "yyyy-MM-ddTHH:mm:ss.fffffffK", "yyyyMMddTHH:mm:ss.fffffffK", "yy-MM-ddTHH:mm:ss.fffffffK", "yyMMddTHH:mm:ss.fffffffK",
  "yyyy-MM-ddTHH:mm:ss,fffffffK", "yyyyMMddTHH:mm:ss,fffffffK", "yy-MM-ddTHH:mm:ss,fffffffK", "yyMMddTHH:mm:ss,fffffffK",
  "yyyy-MM-ddTHH:mm:ss.ffffffK", "yyyyMMddTHH:mm:ss.ffffffK", "yy-MM-ddTHH:mm:ss.ffffffK", "yyMMddTHH:mm:ss.ffffffK",
  "yyyy-MM-ddTHH:mm:ss,ffffffK", "yyyyMMddTHH:mm:ss,ffffffK", "yy-MM-ddTHH:mm:ss,ffffffK", "yyMMddTHH:mm:ss,ffffffK",
  "yyyy-MM-ddTHH:mm:ss.fffffK", "yyyyMMddTHH:mm:ss.fffffK", "yy-MM-ddTHH:mm:ss.fffffK", "yyMMddTHH:mm:ss.fffffK",
  "yyyy-MM-ddTHH:mm:ss,fffffK", "yyyyMMddTHH:mm:ss,fffffK", "yy-MM-ddTHH:mm:ss,fffffK", "yyMMddTHH:mm:ss,fffffK",
  "yyyy-MM-ddTHH:mm:ss.ffffK", "yyyyMMddTHH:mm:ss.ffffK", "yy-MM-ddTHH:mm:ss.ffffK", "yyMMddTHH:mm:ss.ffffK",
  "yyyy-MM-ddTHH:mm:ss,ffffK", "yyyyMMddTHH:mm:ss,ffffK", "yy-MM-ddTHH:mm:ss,ffffK", "yyMMddTHH:mm:ss,ffffK",
  "yyyy-MM-ddTHH:mm:ss.ffK", "yyyyMMddTHH:mm:ss.ffK", "yy-MM-ddTHH:mm:ss.ffK", "yyMMddTHH:mm:ss.ffK",
  "yyyy-MM-ddTHH:mm:ss,ffK", "yyyyMMddTHH:mm:ss,ffK", "yy-MM-ddTHH:mm:ss,ffK", "yyMMddTHH:mm:ss,ffK",
  "yyyy-MM-ddTHH:mm:ss.fK", "yyyyMMddTHH:mm:ss.fK", "yy-MM-ddTHH:mm:ss.fK", "yyMMddTHH:mm:ss.fK",
  "yyyy-MM-ddTHH:mm:ss,fK", "yyyyMMddTHH:mm:ss,fK", "yy-MM-ddTHH:mm:ss,fK", "yyMMddTHH:mm:ss,fK",
  "yyyy-MM-ddTHH:mm:ssK", "yyyyMMddTHH:mm:ssK", "yy-MM-ddTHH:mm:ssK", "yyMMddTHH:mm:ss‎K",
  "yyyy-MM-ddTHHmmss.fffffffK", "yyyyMMddTHHmmss.fffffffK", "yy-MM-ddTHHmmss.fffffffK", "yyMMddTHHmmss.fffffffK",
  "yyyy-MM-ddTHHmmss,fffffffK", "yyyyMMddTHHmmss,fffffffK", "yy-MM-ddTHHmmss,fffffffK", "yyMMddTHHmmss,fffffffK",
  "yyyy-MM-ddTHHmmss.ffffffK", "yyyyMMddTHHmmss.ffffffK", "yy-MM-ddTHHmmss.ffffffK", "yyMMddTHHmmss.ffffffK",
  "yyyy-MM-ddTHHmmss,ffffffK", "yyyyMMddTHHmmss,ffffffK", "yy-MM-ddTHHmmss,ffffffK", "yyMMddTHHmmss,ffffffK",
  "yyyy-MM-ddTHHmmss.fffffK", "yyyyMMddTHHmmss.fffffK", "yy-MM-ddTHHmmss.fffffK", "yyMMddTHHmmss.fffffK",
  "yyyy-MM-ddTHHmmss,fffffK", "yyyyMMddTHHmmss,fffffK", "yy-MM-ddTHHmmss,fffffK", "yyMMddTHHmmss,fffffK",
  "yyyy-MM-ddTHHmmss.ffffK", "yyyyMMddTHHmmss.ffffK", "yy-MM-ddTHHmmss.ffffK", "yyMMddTHHmmss.ffffK",
  "yyyy-MM-ddTHHmmss,ffffK", "yyyyMMddTHHmmss,ffffK", "yy-MM-ddTHHmmss,ffffK", "yyMMddTHHmmss,ffffK",
  "yyyy-MM-ddTHHmmss.ffK", "yyyyMMddTHHmmss.ffK", "yy-MM-ddTHHmmss.ffK", "yyMMddTHHmmss.ffK",
  "yyyy-MM-ddTHHmmss,ffK", "yyyyMMddTHHmmss,ffK", "yy-MM-ddTHHmmss,ffK", "yyMMddTHHmmss,ffK",
  "yyyy-MM-ddTHHmmss.fK", "yyyyMMddTHHmmss.fK", "yy-MM-ddTHHmmss.fK", "yyMMddTHHmmss.fK",
  "yyyy-MM-ddTHHmmss,fK", "yyyyMMddTHHmmss,fK", "yy-MM-ddTHHmmss,fK", "yyMMddTHHmmss,fK",
  "yyyy-MM-ddTHHmmssK", "yyyyMMddTHHmmssK", "yy-MM-ddTHHmmssK", "yyMMddTHHmmss‎K",
  "yyyy-MM-ddTHH:mmK", "yyyyMMddTHH:mmK", "yy-MM-ddTHH:mmK", "yyMMddTHH:mmK",
  "yyyy-MM-ddTHHK", "yyyyMMddTHHK", "yy-MM-ddTHHK", "yyMMddTHHK"
  },
CultureInfo.CurrentCulture, DateTimeStyles.AllowLeadingWhite | DateTimeStyles.AllowTrailingWhite | DateTimeStyles.AdjustToUniversal)
)

希望能够确定与当地时间相匹配的带有时区的日期,但这很快就会变得棘手。

如果您不想支持ISO 8601:2000及之前允许的两位数字年份,但被ISO 8601:2004禁止,则请删除上面所有带有“yy”而不是“yyyy”的字符串:

DateTime.ParseExact(dateString, new string[]
  {
  "yyyy-MM-ddK", "yyyyMMddK",
  "yyyy-MM-ddTHH:mm:ss.fffffffK", "yyyyMMddTHH:mm:ss.fffffffK",
  "yyyy-MM-ddTHH:mm:ss,fffffffK", "yyyyMMddTHH:mm:ss,fffffffK",
  "yyyy-MM-ddTHH:mm:ss.ffffffK", "yyyyMMddTHH:mm:ss.ffffffK",
  "yyyy-MM-ddTHH:mm:ss,ffffffK", "yyyyMMddTHH:mm:ss,ffffffK",
  "yyyy-MM-ddTHH:mm:ss.fffffK", "yyyyMMddTHH:mm:ss.fffffK",
  "yyyy-MM-ddTHH:mm:ss,fffffK", "yyyyMMddTHH:mm:ss,fffffK",
  "yyyy-MM-ddTHH:mm:ss.ffffK", "yyyyMMddTHH:mm:ss.ffffK",
  "yyyy-MM-ddTHH:mm:ss,ffffK", "yyyyMMddTHH:mm:ss,ffffK",
  "yyyy-MM-ddTHH:mm:ss.ffK", "yyyyMMddTHH:mm:ss.ffK",
  "yyyy-MM-ddTHH:mm:ss,ffK", "yyyyMMddTHH:mm:ss,ffK",
  "yyyy-MM-ddTHH:mm:ss.fK", "yyyyMMddTHH:mm:ss.fK",
  "yyyy-MM-ddTHH:mm:ss,fK", "yyyyMMddTHH:mm:ss,fK",
  "yyyy-MM-ddTHH:mm:ssK", "yyyyMMddTHH:mm:ssK",
  "yyyy-MM-ddTHHmmss.fffffffK", "yyyyMMddTHHmmss.fffffffK",
  "yyyy-MM-ddTHHmmss,fffffffK", "yyyyMMddTHHmmss,fffffffK",
  "yyyy-MM-ddTHHmmss.ffffffK", "yyyyMMddTHHmmss.ffffffK",
  "yyyy-MM-ddTHHmmss,ffffffK", "yyyyMMddTHHmmss,ffffffK",
  "yyyy-MM-ddTHHmmss.fffffK", "yyyyMMddTHHmmss.fffffK",
  "yyyy-MM-ddTHHmmss,fffffK", "yyyyMMddTHHmmss,fffffK",
  "yyyy-MM-ddTHHmmss.ffffK", "yyyyMMddTHHmmss.ffffK",
  "yyyy-MM-ddTHHmmss,ffffK", "yyyyMMddTHHmmss,ffffK",
  "yyyy-MM-ddTHHmmss.ffK", "yyyyMMddTHHmmss.ffK",
  "yyyy-MM-ddTHHmmss,ffK", "yyyyMMddTHHmmss,ffK",
  "yyyy-MM-ddTHHmmss.fK", "yyyyMMddTHHmmss.fK",
  "yyyy-MM-ddTHHmmss,fK", "yyyyMMddTHHmmss,fK",
  "yyyy-MM-ddTHHmmssK", "yyyyMMddTHHmmssK",
  "yyyy-MM-ddTHH:mmK", "yyyyMMddTHH:mmK",
  "yyyy-MM-ddTHHK", "yyyyMMddTHHK"
  },
CultureInfo.CurrentCulture, DateTimeStyles.AllowLeadingWhite | DateTimeStyles.AllowTrailingWhite)
)

这仍然留下了一个问题,即形如2009-W53-7的日期表示方式对应于2010年1月3日:
DateTime ParseISO8601(string dateString, bool allowTwoYear = false)
{
  var match = new Regex(@"\b(\d{4})(-W(\d{2})-|W(\d{2}))(\d)(T\S+)?\b").Match(dateString);
  if(match.Success)
  {
    int year = int.Parse(match.Groups[1].Value);
    int week = int.Parse(match.Groups[3].Value + match.Groups[4].Value);
    int day = int.Parse(match.Groups[5].Value);
    if(year < 1 || year > 9999 || week < 1 || week > 53 || day < 1 || day > 7)
      throw new FormatException();
    var firstJan = new DateTime(year, 1, 1);
    var firstWeek = firstJan.DayOfWeek >= DayOfWeek.Friday
      ? firstJan.AddDays(firstJan.DayOfWeek - DayOfWeek.Monday - 1)
      : firstJan.AddDays(DayOfWeek.Monday - firstJan.DayOfWeek);
    DateTime fromWeekAndDay = firstWeek.AddDays((week - 1) * 7 + day - 1);
    if(week > 51 && fromWeekAndDay > ParseISO8601(fromWeekAndDay.Year + "-W01-1"))
      throw new FormatException();
    if(match.Groups[6].Success)
    {
      // We're just going to let the handling for the other formats deal with any time portion:
      dateString = fromWeekAndDay.ToString("yyyy-MM-dd") + match.Groups[6].Value;
    }
    else
      return fromWeekAndDay;
  }
  var formats = allowTwoYear
    ? new []
    {
      "yyyy-MM-ddK", "yyyyMMddK", "yy-MM-ddK", "yyMMddK",
      "yyyy-MM-ddTHH:mm:ss.fffffffK", "yyyyMMddTHH:mm:ss.fffffffK", "yy-MM-ddTHH:mm:ss.fffffffK", "yyMMddTHH:mm:ss.fffffffK",
      "yyyy-MM-ddTHH:mm:ss,fffffffK", "yyyyMMddTHH:mm:ss,fffffffK", "yy-MM-ddTHH:mm:ss,fffffffK", "yyMMddTHH:mm:ss,fffffffK",
      "yyyy-MM-ddTHH:mm:ss.ffffffK", "yyyyMMddTHH:mm:ss.ffffffK", "yy-MM-ddTHH:mm:ss.ffffffK", "yyMMddTHH:mm:ss.ffffffK",
      "yyyy-MM-ddTHH:mm:ss,ffffffK", "yyyyMMddTHH:mm:ss,ffffffK", "yy-MM-ddTHH:mm:ss,ffffffK", "yyMMddTHH:mm:ss,ffffffK",
      "yyyy-MM-ddTHH:mm:ss.fffffK", "yyyyMMddTHH:mm:ss.fffffK", "yy-MM-ddTHH:mm:ss.fffffK", "yyMMddTHH:mm:ss.fffffK",
      "yyyy-MM-ddTHH:mm:ss,fffffK", "yyyyMMddTHH:mm:ss,fffffK", "yy-MM-ddTHH:mm:ss,fffffK", "yyMMddTHH:mm:ss,fffffK",
      "yyyy-MM-ddTHH:mm:ss.ffffK", "yyyyMMddTHH:mm:ss.ffffK", "yy-MM-ddTHH:mm:ss.ffffK", "yyMMddTHH:mm:ss.ffffK",
      "yyyy-MM-ddTHH:mm:ss,ffffK", "yyyyMMddTHH:mm:ss,ffffK", "yy-MM-ddTHH:mm:ss,ffffK", "yyMMddTHH:mm:ss,ffffK",
      "yyyy-MM-ddTHH:mm:ss.ffK", "yyyyMMddTHH:mm:ss.ffK", "yy-MM-ddTHH:mm:ss.ffK", "yyMMddTHH:mm:ss.ffK",
      "yyyy-MM-ddTHH:mm:ss,ffK", "yyyyMMddTHH:mm:ss,ffK", "yy-MM-ddTHH:mm:ss,ffK", "yyMMddTHH:mm:ss,ffK",
      "yyyy-MM-ddTHH:mm:ss.fK", "yyyyMMddTHH:mm:ss.fK", "yy-MM-ddTHH:mm:ss.fK", "yyMMddTHH:mm:ss.fK",
      "yyyy-MM-ddTHH:mm:ss,fK", "yyyyMMddTHH:mm:ss,fK", "yy-MM-ddTHH:mm:ss,fK", "yyMMddTHH:mm:ss,fK",
      "yyyy-MM-ddTHH:mm:ssK", "yyyyMMddTHH:mm:ssK", "yy-MM-ddTHH:mm:ssK", "yyMMddTHH:mm:ss‎K",
      "yyyy-MM-ddTHHmmss.fffffffK", "yyyyMMddTHHmmss.fffffffK", "yy-MM-ddTHHmmss.fffffffK", "yyMMddTHHmmss.fffffffK",
      "yyyy-MM-ddTHHmmss,fffffffK", "yyyyMMddTHHmmss,fffffffK", "yy-MM-ddTHHmmss,fffffffK", "yyMMddTHHmmss,fffffffK",
      "yyyy-MM-ddTHHmmss.ffffffK", "yyyyMMddTHHmmss.ffffffK", "yy-MM-ddTHHmmss.ffffffK", "yyMMddTHHmmss.ffffffK",
      "yyyy-MM-ddTHHmmss,ffffffK", "yyyyMMddTHHmmss,ffffffK", "yy-MM-ddTHHmmss,ffffffK", "yyMMddTHHmmss,ffffffK",
      "yyyy-MM-ddTHHmmss.fffffK", "yyyyMMddTHHmmss.fffffK", "yy-MM-ddTHHmmss.fffffK", "yyMMddTHHmmss.fffffK",
      "yyyy-MM-ddTHHmmss,fffffK", "yyyyMMddTHHmmss,fffffK", "yy-MM-ddTHHmmss,fffffK", "yyMMddTHHmmss,fffffK",
      "yyyy-MM-ddTHHmmss.ffffK", "yyyyMMddTHHmmss.ffffK", "yy-MM-ddTHHmmss.ffffK", "yyMMddTHHmmss.ffffK",
      "yyyy-MM-ddTHHmmss,ffffK", "yyyyMMddTHHmmss,ffffK", "yy-MM-ddTHHmmss,ffffK", "yyMMddTHHmmss,ffffK",
      "yyyy-MM-ddTHHmmss.ffK", "yyyyMMddTHHmmss.ffK", "yy-MM-ddTHHmmss.ffK", "yyMMddTHHmmss.ffK",
      "yyyy-MM-ddTHHmmss,ffK", "yyyyMMddTHHmmss,ffK", "yy-MM-ddTHHmmss,ffK", "yyMMddTHHmmss,ffK",
      "yyyy-MM-ddTHHmmss.fK", "yyyyMMddTHHmmss.fK", "yy-MM-ddTHHmmss.fK", "yyMMddTHHmmss.fK",
      "yyyy-MM-ddTHHmmss,fK", "yyyyMMddTHHmmss,fK", "yy-MM-ddTHHmmss,fK", "yyMMddTHHmmss,fK",
      "yyyy-MM-ddTHHmmssK", "yyyyMMddTHHmmssK", "yy-MM-ddTHHmmssK", "yyMMddTHHmmss‎K",
      "yyyy-MM-ddTHH:mmK", "yyyyMMddTHH:mmK", "yy-MM-ddTHH:mmK", "yyMMddTHH:mmK",
      "yyyy-MM-ddTHHK", "yyyyMMddTHHK", "yy-MM-ddTHHK", "yyMMddTHHK"
    }
    : new []
    {
      "yyyy-MM-ddK", "yyyyMMddK",
      "yyyy-MM-ddTHH:mm:ss.fffffffK", "yyyyMMddTHH:mm:ss.fffffffK",
      "yyyy-MM-ddTHH:mm:ss,fffffffK", "yyyyMMddTHH:mm:ss,fffffffK",
      "yyyy-MM-ddTHH:mm:ss.ffffffK", "yyyyMMddTHH:mm:ss.ffffffK",
      "yyyy-MM-ddTHH:mm:ss,ffffffK", "yyyyMMddTHH:mm:ss,ffffffK",
      "yyyy-MM-ddTHH:mm:ss.fffffK", "yyyyMMddTHH:mm:ss.fffffK",
      "yyyy-MM-ddTHH:mm:ss,fffffK", "yyyyMMddTHH:mm:ss,fffffK",
      "yyyy-MM-ddTHH:mm:ss.ffffK", "yyyyMMddTHH:mm:ss.ffffK",
      "yyyy-MM-ddTHH:mm:ss,ffffK", "yyyyMMddTHH:mm:ss,ffffK",
      "yyyy-MM-ddTHH:mm:ss.ffK", "yyyyMMddTHH:mm:ss.ffK",
      "yyyy-MM-ddTHH:mm:ss,ffK", "yyyyMMddTHH:mm:ss,ffK",
      "yyyy-MM-ddTHH:mm:ss.fK", "yyyyMMddTHH:mm:ss.fK",
      "yyyy-MM-ddTHH:mm:ss,fK", "yyyyMMddTHH:mm:ss,fK",
      "yyyy-MM-ddTHH:mm:ssK", "yyyyMMddTHH:mm:ssK",
      "yyyy-MM-ddTHHmmss.fffffffK", "yyyyMMddTHHmmss.fffffffK",
      "yyyy-MM-ddTHHmmss,fffffffK", "yyyyMMddTHHmmss,fffffffK",
      "yyyy-MM-ddTHHmmss.ffffffK", "yyyyMMddTHHmmss.ffffffK",
      "yyyy-MM-ddTHHmmss,ffffffK", "yyyyMMddTHHmmss,ffffffK",
      "yyyy-MM-ddTHHmmss.fffffK", "yyyyMMddTHHmmss.fffffK",
      "yyyy-MM-ddTHHmmss,fffffK", "yyyyMMddTHHmmss,fffffK",
      "yyyy-MM-ddTHHmmss.ffffK", "yyyyMMddTHHmmss.ffffK",
      "yyyy-MM-ddTHHmmss,ffffK", "yyyyMMddTHHmmss,ffffK",
      "yyyy-MM-ddTHHmmss.ffK", "yyyyMMddTHHmmss.ffK",
      "yyyy-MM-ddTHHmmss,ffK", "yyyyMMddTHHmmss,ffK",
      "yyyy-MM-ddTHHmmss.fK", "yyyyMMddTHHmmss.fK",
      "yyyy-MM-ddTHHmmss,fK", "yyyyMMddTHHmmss,fK",
      "yyyy-MM-ddTHHmmssK", "yyyyMMddTHHmmssK",
      "yyyy-MM-ddTHH:mmK", "yyyyMMddTHH:mmK",
      "yyyy-MM-ddTHHK", "yyyyMMddTHHK"
    };
  return DateTime.ParseExact(dateString, formats, CultureInfo.InvariantCulture, DateTimeStyles.AllowLeadingWhite | DateTimeStyles.AllowTrailingWhite);
}

最后,您需要决定如果收到精度大于100ns的日期和时间应该怎么做:

public static DateTime ParseISO8601(string dateString, MidpointRounding rounding = MidpointRounding.ToEven, bool allowTwoYear = false)
{
  var match = new Regex(@"\b(\d{4})(-W(\d{2})-|W(\d{2}))(\d)(T\S+)?\b").Match(dateString);
  if(match.Success)
  {
    int year = int.Parse(match.Groups[1].Value);
    int week = int.Parse(match.Groups[3].Value + match.Groups[4].Value);
    int day = int.Parse(match.Groups[5].Value);
    if(year < 1 || year > 9999 || week < 1 || week > 53 || day < 1 || day > 7)
      throw new FormatException();
    var firstJan = new DateTime(year, 1, 1);
    var firstWeek = firstJan.DayOfWeek >= DayOfWeek.Friday
      ? firstJan.AddDays(firstJan.DayOfWeek - DayOfWeek.Monday - 1)
      : firstJan.AddDays(DayOfWeek.Monday - firstJan.DayOfWeek);
    DateTime fromWeekAndDay = firstWeek.AddDays((week - 1) * 7 + day - 1);
    if(week > 51 && fromWeekAndDay > ParseISO8601(fromWeekAndDay.Year + "-W01-1"))
      throw new FormatException();
    if(match.Groups[6].Success)
    {
      // We're just going to let the handling for the other formats deal with any time portion:
      dateString = fromWeekAndDay.ToString("yyyy-MM-dd") + match.Groups[6].Value;
    }
    else
      return fromWeekAndDay;
  }
  var excessiveFractions = new Regex(@"(\d(\.|,‎)\d{8,})");
  if(excessiveFractions.IsMatch(dateString))
    dateString = excessiveFractions.Replace(
      dateString,
      m => decimal.Round(decimal.Parse(m.Value.Substring(0, Math.Max(m.Value.Length, 10)).Replace(',', '.')), 7, rounding).ToString()
       );
  var formats = allowTwoYear
    ? new []
    {
      "yyyy-MM-ddK", "yyyyMMddK", "yy-MM-ddK", "yyMMddK",
      "yyyy-MM-ddTHH:mm:ss.fffffffK", "yyyyMMddTHH:mm:ss.fffffffK", "yy-MM-ddTHH:mm:ss.fffffffK", "yyMMddTHH:mm:ss.fffffffK",
      "yyyy-MM-ddTHH:mm:ss,fffffffK", "yyyyMMddTHH:mm:ss,fffffffK", "yy-MM-ddTHH:mm:ss,fffffffK", "yyMMddTHH:mm:ss,fffffffK",
      "yyyy-MM-ddTHH:mm:ss.ffffffK", "yyyyMMddTHH:mm:ss.ffffffK", "yy-MM-ddTHH:mm:ss.ffffffK", "yyMMddTHH:mm:ss.ffffffK",
      "yyyy-MM-ddTHH:mm:ss,ffffffK", "yyyyMMddTHH:mm:ss,ffffffK", "yy-MM-ddTHH:mm:ss,ffffffK", "yyMMddTHH:mm:ss,ffffffK",
      "yyyy-MM-ddTHH:mm:ss.fffffK", "yyyyMMddTHH:mm:ss.fffffK", "yy-MM-ddTHH:mm:ss.fffffK", "yyMMddTHH:mm:ss.fffffK",
      "yyyy-MM-ddTHH:mm:ss,fffffK", "yyyyMMddTHH:mm:ss,fffffK", "yy-MM-ddTHH:mm:ss,fffffK", "yyMMddTHH:mm:ss,fffffK",
      "yyyy-MM-ddTHH:mm:ss.ffffK", "yyyyMMddTHH:mm:ss.ffffK", "yy-MM-ddTHH:mm:ss.ffffK", "yyMMddTHH:mm:ss.ffffK",
      "yyyy-MM-ddTHH:mm:ss,ffffK", "yyyyMMddTHH:mm:ss,ffffK", "yy-MM-ddTHH:mm:ss,ffffK", "yyMMddTHH:mm:ss,ffffK",
      "yyyy-MM-ddTHH:mm:ss.ffK", "yyyyMMddTHH:mm:ss.ffK", "yy-MM-ddTHH:mm:ss.ffK", "yyMMddTHH:mm:ss.ffK",
      "yyyy-MM-ddTHH:mm:ss,ffK", "yyyyMMddTHH:mm:ss,ffK", "yy-MM-ddTHH:mm:ss,ffK", "yyMMddTHH:mm:ss,ffK",
      "yyyy-MM-ddTHH:mm:ss.fK", "yyyyMMddTHH:mm:ss.fK", "yy-MM-ddTHH:mm:ss.fK", "yyMMddTHH:mm:ss.fK",
      "yyyy-MM-ddTHH:mm:ss,fK", "yyyyMMddTHH:mm:ss,fK", "yy-MM-ddTHH:mm:ss,fK", "yyMMddTHH:mm:ss,fK",
      "yyyy-MM-ddTHH:mm:ssK", "yyyyMMddTHH:mm:ssK", "yy-MM-ddTHH:mm:ssK", "yyMMddTHH:mm:ss‎K",
      "yyyy-MM-ddTHHmmss.fffffffK", "yyyyMMddTHHmmss.fffffffK", "yy-MM-ddTHHmmss.fffffffK", "yyMMddTHHmmss.fffffffK",
      "yyyy-MM-ddTHHmmss,fffffffK", "yyyyMMddTHHmmss,fffffffK", "yy-MM-ddTHHmmss,fffffffK", "yyMMddTHHmmss,fffffffK",
      "yyyy-MM-ddTHHmmss.ffffffK", "yyyyMMddTHHmmss.ffffffK", "yy-MM-ddTHHmmss.ffffffK", "yyMMddTHHmmss.ffffffK",
      "yyyy-MM-ddTHHmmss,ffffffK", "yyyyMMddTHHmmss,ffffffK", "yy-MM-ddTHHmmss,ffffffK", "yyMMddTHHmmss,ffffffK",
      "yyyy-MM-ddTHHmmss.fffffK", "yyyyMMddTHHmmss.fffffK", "yy-MM-ddTHHmmss.fffffK", "yyMMddTHHmmss.fffffK",
      "yyyy-MM-ddTHHmmss,fffffK", "yyyyMMddTHHmmss,fffffK", "yy-MM-ddTHHmmss,fffffK", "yyMMddTHHmmss,fffffK",
      "yyyy-MM-ddTHHmmss.ffffK", "yyyyMMddTHHmmss.ffffK", "yy-MM-ddTHHmmss.ffffK", "yyMMddTHHmmss.ffffK",
      "yyyy-MM-ddTHHmmss,ffffK", "yyyyMMddTHHmmss,ffffK", "yy-MM-ddTHHmmss,ffffK", "yyMMddTHHmmss,ffffK",
      "yyyy-MM-ddTHHmmss.ffK", "yyyyMMddTHHmmss.ffK", "yy-MM-ddTHHmmss.ffK", "yyMMddTHHmmss.ffK",
      "yyyy-MM-ddTHHmmss,ffK", "yyyyMMddTHHmmss,ffK", "yy-MM-ddTHHmmss,ffK", "yyMMddTHHmmss,ffK",
      "yyyy-MM-ddTHHmmss.fK", "yyyyMMddTHHmmss.fK", "yy-MM-ddTHHmmss.fK", "yyMMddTHHmmss.fK",
      "yyyy-MM-ddTHHmmss,fK", "yyyyMMddTHHmmss,fK", "yy-MM-ddTHHmmss,fK", "yyMMddTHHmmss,fK",
      "yyyy-MM-ddTHHmmssK", "yyyyMMddTHHmmssK", "yy-MM-ddTHHmmssK", "yyMMddTHHmmss‎K",
      "yyyy-MM-ddTHH:mmK", "yyyyMMddTHH:mmK", "yy-MM-ddTHH:mmK", "yyMMddTHH:mmK",
      "yyyy-MM-ddTHHK", "yyyyMMddTHHK", "yy-MM-ddTHHK", "yyMMddTHHK"
    }
    : new []
    {
      "yyyy-MM-ddK", "yyyyMMddK",
      "yyyy-MM-ddTHH:mm:ss.fffffffK", "yyyyMMddTHH:mm:ss.fffffffK",
      "yyyy-MM-ddTHH:mm:ss,fffffffK", "yyyyMMddTHH:mm:ss,fffffffK",
      "yyyy-MM-ddTHH:mm:ss.ffffffK", "yyyyMMddTHH:mm:ss.ffffffK",
      "yyyy-MM-ddTHH:mm:ss,ffffffK", "yyyyMMddTHH:mm:ss,ffffffK",
      "yyyy-MM-ddTHH:mm:ss.fffffK", "yyyyMMddTHH:mm:ss.fffffK",
      "yyyy-MM-ddTHH:mm:ss,fffffK", "yyyyMMddTHH:mm:ss,fffffK",
      "yyyy-MM-ddTHH:mm:ss.ffffK", "yyyyMMddTHH:mm:ss.ffffK",
      "yyyy-MM-ddTHH:mm:ss,ffffK", "yyyyMMddTHH:mm:ss,ffffK",
      "yyyy-MM-ddTHH:mm:ss.ffK", "yyyyMMddTHH:mm:ss.ffK",
      "yyyy-MM-ddTHH:mm:ss,ffK", "yyyyMMddTHH:mm:ss,ffK",
      "yyyy-MM-ddTHH:mm:ss.fK", "yyyyMMddTHH:mm:ss.fK",
      "yyyy-MM-ddTHH:mm:ss,fK", "yyyyMMddTHH:mm:ss,fK",
      "yyyy-MM-ddTHH:mm:ssK", "yyyyMMddTHH:mm:ssK",
      "yyyy-MM-ddTHHmmss.fffffffK", "yyyyMMddTHHmmss.fffffffK",
      "yyyy-MM-ddTHHmmss,fffffffK", "yyyyMMddTHHmmss,fffffffK",
      "yyyy-MM-ddTHHmmss.ffffffK", "yyyyMMddTHHmmss.ffffffK",
      "yyyy-MM-ddTHHmmss,ffffffK", "yyyyMMddTHHmmss,ffffffK",
      "yyyy-MM-ddTHHmmss.fffffK", "yyyyMMddTHHmmss.fffffK",
      "yyyy-MM-ddTHHmmss,fffffK", "yyyyMMddTHHmmss,fffffK",
      "yyyy-MM-ddTHHmmss.ffffK", "yyyyMMddTHHmmss.ffffK",
      "yyyy-MM-ddTHHmmss,ffffK", "yyyyMMddTHHmmss,ffffK",
      "yyyy-MM-ddTHHmmss.ffK", "yyyyMMddTHHmmss.ffK",
      "yyyy-MM-ddTHHmmss,ffK", "yyyyMMddTHHmmss,ffK",
      "yyyy-MM-ddTHHmmss.fK", "yyyyMMddTHHmmss.fK",
      "yyyy-MM-ddTHHmmss,fK", "yyyyMMddTHHmmss,fK",
      "yyyy-MM-ddTHHmmssK", "yyyyMMddTHHmmssK",
      "yyyy-MM-ddTHH:mmK", "yyyyMMddTHH:mmK",
      "yyyy-MM-ddTHHK", "yyyyMMddTHHK"
    };
  return DateTime.ParseExact(dateString, formats, CultureInfo.InvariantCulture, DateTimeStyles.AllowLeadingWhite | DateTimeStyles.AllowTrailingWhite);
}

现在,虽然像 2015-03-29T12:53:20.238748294819293021383+01:00 这样的字符串会失去一些精度,但我们仍然可以尽可能地解析它。

现在我们需要将 24:00:00 作为有效时间捕获,然后捕获有效时间,如 2015-06-30T23:59:60(尽管我没有针对从未发生的 2014-06-30T23:59:60 进行检查,因为那将需要一个不断更新的闰秒数据库):

public static DateTime ParseISO8601(string dateString, MidpointRounding rounding = MidpointRounding.ToEven, bool allowTwoYear = false, bool leapSecondMeansNextDay = false)
{
  var match = new Regex(@"\b(\d{4})(-W(\d{2})-|W(\d{2}))(\d)(T\S+)?\b").Match(dateString);
  if(match.Success)
  {
    int year = int.Parse(match.Groups[1].Value);
    int week = int.Parse(match.Groups[3].Value + match.Groups[4].Value);
    int day = int.Parse(match.Groups[5].Value);
    if(year < 1 || year > 9999 || week < 1 || week > 53 || day < 1 || day > 7)
      throw new FormatException();
    var firstJan = new DateTime(year, 1, 1);
    var firstWeek = firstJan.DayOfWeek >= DayOfWeek.Friday
      ? firstJan.AddDays(firstJan.DayOfWeek - DayOfWeek.Monday - 1)
      : firstJan.AddDays(DayOfWeek.Monday - firstJan.DayOfWeek);
    DateTime fromWeekAndDay = firstWeek.AddDays((week - 1) * 7 + day - 1);
    if(week > 51 && fromWeekAndDay > ParseISO8601(fromWeekAndDay.Year + "-W01-1"))
      throw new FormatException();
    if(match.Groups[6].Success)
    {
      // We're just going to let the handling for the other formats deal with any time fraction:
      dateString = fromWeekAndDay.ToString("yyyy-MM-dd") + match.Groups[6].Value;
    }
    return fromWeekAndDay;
  }
  var excessiveFractions = new Regex(@"(\d(\.|,‎)\d{8,})");
  if(excessiveFractions.IsMatch(dateString))
    dateString = excessiveFractions.Replace(
      dateString,
      m => decimal.Round(decimal.Parse(m.Value.Substring(0, Math.Max(m.Value.Length, 10))), 7, rounding).ToString()
       );
  if(dateString.Contains("T24"))
  {
    var yesterday = ParseISO8601(dateString.Replace("T24", "T00"), rounding, allowTwoYear);
    if(yesterday.TimeOfDay != TimeSpan.Zero)
      throw new FormatException();
    return yesterday.AddDays(1);
  }
  var leapSecond = new Regex("T23:?59:?60");
  if(leapSecond.IsMatch(dateString))
  {
    var secondBefore = ParseISO8601(leapSecond.Replace(dateString, "T23:59:59"));
    if(secondBefore.TimeOfDay != new TimeSpan(23, 59, 59)) // can't have fractions past second 60
      throw new FormatException();
    // Can only be on --12-31 or --06-30
    if((secondBefore.Month == 12 && secondBefore.Day == 31) || (secondBefore.Month == 6 && secondBefore.Day == 30))
      // since DateTime can't handle leap seconds, we need a policy as to which side of it to be on.
      return leapSecondMeansNextDay ? secondBefore.AddSeconds(1) : secondBefore;
    throw new FormatException();
  }
  var formats = allowTwoYear
    ? new []
    {
      "yyyy-MM-ddK", "yyyyMMddK", "yy-MM-ddK", "yyMMddK",
      "yyyy-MM-ddTHH:mm:ss.fffffffK", "yyyyMMddTHH:mm:ss.fffffffK", "yy-MM-ddTHH:mm:ss.fffffffK", "yyMMddTHH:mm:ss.fffffffK",
      "yyyy-MM-ddTHH:mm:ss,fffffffK", "yyyyMMddTHH:mm:ss,fffffffK", "yy-MM-ddTHH:mm:ss,fffffffK", "yyMMddTHH:mm:ss,fffffffK",
      "yyyy-MM-ddTHH:mm:ss.ffffffK", "yyyyMMddTHH:mm:ss.ffffffK", "yy-MM-ddTHH:mm:ss.ffffffK", "yyMMddTHH:mm:ss.ffffffK",
      "yyyy-MM-ddTHH:mm:ss,ffffffK", "yyyyMMddTHH:mm:ss,ffffffK", "yy-MM-ddTHH:mm:ss,ffffffK", "yyMMddTHH:mm:ss,ffffffK",
      "yyyy-MM-ddTHH:mm:ss.fffffK", "yyyyMMddTHH:mm:ss.fffffK", "yy-MM-ddTHH:mm:ss.fffffK", "yyMMddTHH:mm:ss.fffffK",
      "yyyy-MM-ddTHH:mm:ss,fffffK", "yyyyMMddTHH:mm:ss,fffffK", "yy-MM-ddTHH:mm:ss,fffffK", "yyMMddTHH:mm:ss,fffffK",
      "yyyy-MM-ddTHH:mm:ss.ffffK", "yyyyMMddTHH:mm:ss.ffffK", "yy-MM-ddTHH:mm:ss.ffffK", "yyMMddTHH:mm:ss.ffffK",
      "yyyy-MM-ddTHH:mm:ss,ffffK", "yyyyMMddTHH:mm:ss,ffffK", "yy-MM-ddTHH:mm:ss,ffffK", "yyMMddTHH:mm:ss,ffffK",
      "yyyy-MM-ddTHH:mm:ss.ffK", "yyyyMMddTHH:mm:ss.ffK", "yy-MM-ddTHH:mm:ss.ffK", "yyMMddTHH:mm:ss.ffK",
      "yyyy-MM-ddTHH:mm:ss,ffK", "yyyyMMddTHH:mm:ss,ffK", "yy-MM-ddTHH:mm:ss,ffK", "yyMMddTHH:mm:ss,ffK",
      "yyyy-MM-ddTHH:mm:ss.fK", "yyyyMMddTHH:mm:ss.fK", "yy-MM-ddTHH:mm:ss.fK", "yyMMddTHH:mm:ss.fK",
      "yyyy-MM-ddTHH:mm:ss,fK", "yyyyMMddTHH:mm:ss,fK", "yy-MM-ddTHH:mm:ss,fK", "yyMMddTHH:mm:ss,fK",
      "yyyy-MM-ddTHH:mm:ssK", "yyyyMMddTHH:mm:ssK", "yy-MM-ddTHH:mm:ssK", "yyMMddTHH:mm:ss‎K",
      "yyyy-MM-ddTHHmmss.fffffffK", "yyyyMMddTHHmmss.fffffffK", "yy-MM-ddTHHmmss.fffffffK", "yyMMddTHHmmss.fffffffK",
      "yyyy-MM-ddTHHmmss,fffffffK", "yyyyMMddTHHmmss,fffffffK", "yy-MM-ddTHHmmss,fffffffK", "yyMMddTHHmmss,fffffffK",
      "yyyy-MM-ddTHHmmss.ffffffK", "yyyyMMddTHHmmss.ffffffK", "yy-MM-ddTHHmmss.ffffffK", "yyMMddTHHmmss.ffffffK",
      "yyyy-MM-ddTHHmmss,ffffffK", "yyyyMMddTHHmmss,ffffffK", "yy-MM-ddTHHmmss,ffffffK", "yyMMddTHHmmss,ffffffK",
      "yyyy-MM-ddTHHmmss.fffffK", "yyyyMMddTHHmmss.fffffK", "yy-MM-ddTHHmmss.fffffK", "yyMMddTHHmmss.fffffK",
      "yyyy-MM-ddTHHmmss,fffffK", "yyyyMMddTHHmmss,fffffK", "yy-MM-ddTHHmmss,fffffK", "yyMMddTHHmmss,fffffK",
      "yyyy-MM-ddTHHmmss.ffffK", "yyyyMMddTHHmmss.ffffK", "yy-MM-ddTHHmmss.ffffK", "yyMMddTHHmmss.ffffK",
      "yyyy-MM-ddTHHmmss,ffffK", "yyyyMMddTHHmmss,ffffK", "yy-MM-ddTHHmmss,ffffK", "yyMMddTHHmmss,ffffK",
      "yyyy-MM-ddTHHmmss.ffK", "yyyyMMddTHHmmss.ffK", "yy-MM-ddTHHmmss.ffK", "yyMMddTHHmmss.ffK",
      "yyyy-MM-ddTHHmmss,ffK", "yyyyMMddTHHmmss,ffK", "yy-MM-ddTHHmmss,ffK", "yyMMddTHHmmss,ffK",
      "yyyy-MM-ddTHHmmss.fK", "yyyyMMddTHHmmss.fK", "yy-MM-ddTHHmmss.fK", "yyMMddTHHmmss.fK",
      "yyyy-MM-ddTHHmmss,fK", "yyyyMMddTHHmmss,fK", "yy-MM-ddTHHmmss,fK", "yyMMddTHHmmss,fK",
      "yyyy-MM-ddTHHmmssK", "yyyyMMddTHHmmssK", "yy-MM-ddTHHmmssK", "yyMMddTHHmmss‎K",
      "yyyy-MM-ddTHH:mmK", "yyyyMMddTHH:mmK", "yy-MM-ddTHH:mmK", "yyMMddTHH:mmK",
      "yyyy-MM-ddTHHK", "yyyyMMddTHHK", "yy-MM-ddTHHK", "yyMMddTHHK"
    }
    : new []
    {
      "yyyy-MM-ddK", "yyyyMMddK",
      "yyyy-MM-ddTHH:mm:ss.fffffffK", "yyyyMMddTHH:mm:ss.fffffffK",
      "yyyy-MM-ddTHH:mm:ss,fffffffK", "yyyyMMddTHH:mm:ss,fffffffK",
      "yyyy-MM-ddTHH:mm:ss.ffffffK", "yyyyMMddTHH:mm:ss.ffffffK",
      "yyyy-MM-ddTHH:mm:ss,ffffffK", "yyyyMMddTHH:mm:ss,ffffffK",
      "yyyy-MM-ddTHH:mm:ss.fffffK", "yyyyMMddTHH:mm:ss.fffffK",
      "yyyy-MM-ddTHH:mm:ss,fffffK", "yyyyMMddTHH:mm:ss,fffffK",
      "yyyy-MM-ddTHH:mm:ss.ffffK", "yyyyMMddTHH:mm:ss.ffffK",
      "yyyy-MM-ddTHH:mm:ss,ffffK", "yyyyMMddTHH:mm:ss,ffffK",
      "yyyy-MM-ddTHH:mm:ss.ffK", "yyyyMMddTHH:mm:ss.ffK",
      "yyyy-MM-ddTHH:mm:ss,ffK", "yyyyMMddTHH:mm:ss,ffK",
      "yyyy-MM-ddTHH:mm:ss.fK", "yyyyMMddTHH:mm:ss.fK",
      "yyyy-MM-ddTHH:mm:ss,fK", "yyyyMMddTHH:mm:ss,fK",
      "yyyy-MM-ddTHH:mm:ssK", "yyyyMMddTHH:mm:ssK",
      "yyyy-MM-ddTHHmmss.fffffffK", "yyyyMMddTHHmmss.fffffffK",
      "yyyy-MM-ddTHHmmss,fffffffK", "yyyyMMddTHHmmss,fffffffK",
      "yyyy-MM-ddTHHmmss.ffffffK", "yyyyMMddTHHmmss.ffffffK",
      "yyyy-MM-ddTHHmmss,ffffffK", "yyyyMMddTHHmmss,ffffffK",
      "yyyy-MM-ddTHHmmss.fffffK", "yyyyMMddTHHmmss.fffffK",
      "yyyy-MM-ddTHHmmss,fffffK", "yyyyMMddTHHmmss,fffffK",
      "yyyy-MM-ddTHHmmss.ffffK", "yyyyMMddTHHmmss.ffffK",
      "yyyy-MM-ddTHHmmss,ffffK", "yyyyMMddTHHmmss,ffffK",
      "yyyy-MM-ddTHHmmss.ffK", "yyyyMMddTHHmmss.ffK",
      "yyyy-MM-ddTHHmmss,ffK", "yyyyMMddTHHmmss,ffK",
      "yyyy-MM-ddTHHmmss.fK", "yyyyMMddTHHmmss.fK",
      "yyyy-MM-ddTHHmmss,fK", "yyyyMMddTHHmmss,fK",
      "yyyy-MM-ddTHHmmssK", "yyyyMMddTHHmmssK",
      "yyyy-MM-ddTHH:mmK", "yyyyMMddTHH:mmK",
      "yyyy-MM-ddTHHK", "yyyyMMddTHHK"
    };
  return DateTime.ParseExact(dateString, formats, CultureInfo.InvariantCulture, DateTimeStyles.AllowLeadingWhite | DateTimeStyles.AllowTrailingWhite);
}

所有这些都需要大量的工作,但也有点有趣。我们还没有触及到维护时区(使用 DateTimeOffset 不是很麻烦),只处理时间字符串(应该返回一个 TimeSpan)、持续时间(也应该返回一个 TimeSpan)、周期或重复周期。
同时,ISO 8601 旨在用于定义一个或多个配置文件,这些配置文件又定义了允许格式的一个或多个子集,可能还有其他规则。通常,我们希望按照其中一个配置文件编程,而不是通常按照 ISO 8601 进行编程。例如,上述内容无法解析 Web 日期时间,因为它接受 2009-W53-7 作为2010年1月3日,这是正确的 ISO 8601 处理方式,但不被 ISO 8601 的 Web 日期时间配置文件所允许。

非常详细!我认为这表明,当ISO8601在数据交换规范中使用时,真正想要的是涵盖常用形式和一些少数不太被考虑但应该被涵盖的异常形式的子集,例如闰秒。通常不需要的是“2009-W53-7”,或者两个字符的年份,或任何其他“怪异”的形式。换句话说,当我看到规范中命名“ISO8601”时,他们可能并不意味着完整的ISO 8601,或者如果他们确实这样做了,那么编码人员可能没有实现它(因为框架中支持不充分)。 - redcalx
2
是的,8601旨在用作定义配置文件的基础,这些配置文件可以为不同目的提供一个或多个子集,以及其他规则。2009-W53-7对于某些机器冲压过程非常有用,而2010-03-01将是同一日期的Web日期时间。如果有人说他们需要/提供ISO 8601,他们几乎总是指特定的配置文件,尽管他们可能不知道(许多人认为他们所在领域使用的配置文件是唯一的)。一个明确的8601解析器对于几乎所有实际应用都过于自由。 - Jon Hanna
2
在http://www.hackcraft.net/web/datetime/中,我提到了网页中使用的几个配置文件。名为W3C DTF的配置文件适用于许多用途,并且该文章稍微介绍了一些替代方案,以便在不适用时使用。 - Jon Hanna
@Andrey 尝试提供一个答案,允许使用2004年之前的两位数字年份,并且还允许仅包含时间的字符串,那么你就会得到真正的明确答案。但如果这是你的参数,那么 010203 可以等同于 01-02-0301:02:03,这几乎是不可能的! - Jon Hanna
1
@JonHanna,感谢您的这篇文章,非常有用。顺便提一下,您忘记添加所有毫秒精度的模式(.fffK和, fffK)。 - tjsmith
显示剩余5条评论

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