8

There are many SO questions and answers regarding parsing of ISO8601 date/times in .NET and C#. However, there doesn't seem to be a 'definitive' answer anywhere, i.e. an answer that presents a formally correct ISO8601 parser that would correctly parse all of the possible format variants within ISO8601 and that would disallow non-ISO8601 variants.

This SO answer is the closest match so far...

How to create a .NET DateTime from ISO 8601 format

Community
  • 1
  • 1
redcalx
  • 8,177
  • 4
  • 56
  • 105
  • 3
    Two corner cases to think about: 24:00, and using comma as the decimal separator for times... – Jon Skeet Jul 06 '15 at 10:53
  • That question has bunch of seemingly good answer, how are they not definitive? – Andrey Jul 06 '15 at 11:01
  • 2
    @JonSkeet by the standard itself, period is the corner case, comma the "normal" case. Most profiles used in internet-related cases prescribe the period though, making it more often seen by a lot of programmers. A really nasty corner case is that prior to the 2004 revision, `150102` is a valid short form for both `2015-01-02` and `15:01:02`, so if backwards compatibility to the 2000 version is required, the ability to "correctly parse all of the possible format variants within ISO8601" cannot be done without out-of-band information. – Jon Hanna Jul 06 '15 at 11:02
  • @Andrey, by definitive I mean 'formally correct'. None of the answers provide a fully compliant ISO8601 parser (as far as I can tell), they either cover part of the standard, allow non-compliant formats, or both. – redcalx Jul 06 '15 at 11:07
  • 1
    @JonHanna: Yes, I realize all of that - my point about using the comma separator is that most programmers wouldn't even *consider* it. (Frankly I regard allowing it as a mistake, but that's a different matter.) – Jon Skeet Jul 06 '15 at 11:27
  • @JonSkeet I just feel lucky that we're allowed the period at all, and that the profiles I generally use, use it. Of course, were I from a different culture I might feel irritated by that by the same token. – Jon Hanna Jul 06 '15 at 12:57
  • 1
    @JonHanna: Well, the thing is it's meant to be a machine-readable format - and `.` is universally used as the machine-readable decimal separator for numbers, so I don't know why they decided to prefer commas for times... – Jon Skeet Jul 06 '15 at 13:02
  • 1
    @JonSkeet because it wasn't all that universal in 1975 when ISO 3307 was written (and I don't know what national or other standards influenced that) and indeed isn't universal in ISO-land where comma is often the preferred separator. – Jon Hanna Jul 06 '15 at 13:08
  • @JonHanna: That's fair enough. I have other beefs with 8601, mind you... – Jon Skeet Jul 06 '15 at 13:10
  • 1
    @JonSkeet there are plenty to have. I do find the more commonly used profiles to be useful though. Still, with the more commonly used profiles the corner case is that if someone were to use a parser that accepted all of ISO 8601 then you'd allow things like the comma when you should in fact ban it. – Jon Hanna Jul 06 '15 at 13:25

1 Answers1

30

Okay. Let's start with the limitations you are placing on ISO 8601 yourself:

  1. You want a DateTime, so all formats that only result in a time-of-day, duration, year or month-and-year are not needed.
  2. You want a DateTime, so timezone information will become "Unspecified", "UTC" or "Local" without the ability to round-trip back to the same timezone.
  3. You want a DateTime, and therefore lose precision beyond 100ns.

That leaves us with less than half of the ISO 8601 formats to support, and resolves the ambiguous case as it is ambiguous between a date-only and a time-only meaning.

Let's start with those that we could handle with 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)
)

It would be nice to identify those dates with time zones that matched the local time, but it gets fiddly fast.

If you don't want to support the two-digit years allowed by ISO 8601:2000 and earlier but prohibited in ISO 8601:2004 then remove all of the strings with "yy" rather than "yyyy" above:

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

This still leaves us with the problem of dates in the form 2009-W53-7 for the 3rd of January 2010:

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

Finally you have to decide what to do if you receive a date and time with a greater precision than 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);
}

Now while we lose precision with a string like 2015-03-29T12:53:20.238748294819293021383+01:00 we can still parse it as well as could be feasible.

Now we need to catch 24:00:00 as a valid time, and then catch valid times like 2015-06-30T23:59:60 (though I don't check against 2014-06-30T23:59:60 which never happened, as that would need a constantly updated database of leap-seconds):

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

All of this is a lot of work, though kinda fun. And we still aren't touching on maintaining timezone (not a tricky change to the above, with DateTimeOffset) time-only strings (should return a TimeSpan), durations (should also return a TimeSpan), periods or repeating periods.

Meanwhile, ISO 8601 is intended to be used to define one or more profiles which in turn define one or more subsets of the allowed formats, perhaps with other rules. Generally we want to program to one of these profiles rather than to ISO 8601 generally. For example, the above is useless for parsing web date-times because it accepts 2009-W53-7 for the 3rd of January 2010 which is correct ISO 8601 handling, but not allowed by the web-datetime profile of ISO 8601.

Jon Hanna
  • 110,372
  • 10
  • 146
  • 251
  • Well that was thorough! I think this serves to show that when ISO8601 is used in data interchange specs, what is really wanted is some subset that covers the commonly used forms, and a few less thought about outlier forms that ought be covered, such as maybe leap seconds. What generally isn't wanted is '2009-W53-7', or two char years, or any other 'weirdness'. In other words, when I see 'ISO8601' named in a specification, they probably didn't mean the full ISO8601, or if they did then the coders probably didn't implement it (because of patchy support in frameworks). – redcalx Jul 06 '15 at 14:13
  • Is there a 'cleaner' (than ISO8601) date-time specification? – redcalx Jul 06 '15 at 14:17
  • 2
    Yes, 8601 is intended to be used as a basis for defining *profiles* which give one or more subsets, perhaps along with other rules,for different purposes. 2009-W53-7 is very useful for some machine stamping processes, while 2010-03-01 would be the Web datetime for the same date. If someone says they need/give ISO 8601 they almost always mean a particular profile, though they may not know that (many assume the profiles used in their field are the only profiles). A definitive 8601-parser would be too liberal for almost all practical uses. – Jon Hanna Jul 06 '15 at 14:21
  • 2
    At http://www.hackcraft.net/web/datetime/ I mention several profiles used in the web. The one called W3C DTF is good for a lot of uses, and that article goes a bit into alternatives for when it won't suit. – Jon Hanna Jul 06 '15 at 14:24
  • Oh my, is this is not definitive answer I don't know which can be. – Andrey Jul 06 '15 at 15:39
  • @Andrey try coming up with an answer that allows pre-2004 two-digit years and also allows for time-only strings and you'll have the real definitive answer. You'll also have done the impossible, because if that's your parameters then `010203` can be equivalent to both `01-02-03` and `01:02:03`! – Jon Hanna Jul 06 '15 at 16:37
  • @JonHanna this is crazy. I think your answer is as good as it can get. – Andrey Jul 06 '15 at 17:34
  • @Andrey, yeah I just meant to indicate that, as the answer says itself, a completely comprehensive method would be neither possible nor ultimately of much use. – Jon Hanna Jul 06 '15 at 18:19
  • 1
    @JonHanna thanks for this post, it was very useful. FYI, you forgot to add all of the patterns with millisecond precision (.fffK and ,fffK) – tjsmith Oct 05 '16 at 16:33
  • @tjsmith looks like I did. Aw well, exercise for the reader :) – Jon Hanna Oct 05 '16 at 19:57