如何在中对ISO8601日期时间进行形式正确的解析.Net

本文关键字:Net ISO8601 时间 日期 | 更新日期: 2023-09-27 18:01:08

中有许多关于ISO8601日期/时间解析的SO问答。NET和C#。然而,似乎在任何地方都没有一个"明确"的答案,即一个提供正式正确的ISO8601解析器的答案,该解析器将正确解析ISO8601中所有可能的格式变体,并且不允许非ISO8601变体。

这个SO的答案是迄今为止最接近的匹配。。。

如何创建。NET DateTime来自ISO 8601格式

如何在中对ISO8601日期时间进行形式正确的解析.Net

好的。让我们从您自己对ISO8601的限制开始:

  1. 您需要一个DateTime,因此不需要所有只产生一天中的时间、持续时间、年份或月份和年份的格式
  2. 您需要DateTime,因此时区信息将变为"Unspecified"、"UTC"或"Local",而无法往返返回同一时区
  3. 您想要一个DateTime,因此会丢失超过100ns的精度

这使得我们只需要支持不到一半的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)
)

这仍然给我们留下了2010年1月3日2009-W53-7形式的日期问题:

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日期时间是无用的,因为它接受2010年1月3日的2009-W53-7,这是正确的ISO 8601处理,但ISO 8601的web日期时间配置文件不允许。