Good. Start with the restrictions you set on ISO 8601:
- You want to use DateTime, so all formats that result only in time of day, duration, year, or months and years are not needed.
- You want to use DateTime, so the time zone information will become "Unspecified", "UTC" or "Local" without the ability to go back to the same time zone.
- You want a DateTime and therefore lose accuracy beyond 100ns.
This leaves us with less than half of the ISO 8601 formats for support and allows for an ambiguous case, since it is ambiguous between the date value and the time value.
Let's start with the ones 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:ssK", "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", "yyMMddTHHmmssK", "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 these dates with time zones that coincided with local time, but it is becoming very fast.
If you do not want to support the double-digit years permitted by ISO 8601: 2000 and earlier, but prohibited by ISO 8601: 2004, then delete all lines with "yy" and not "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 a date problem in the form of 2009-W53-7 January 3rd, 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:ssK", "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", "yyMMddTHHmmssK", "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 must decide what to do if you get the date and time with more accuracy 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:ssK", "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", "yyMMddTHHmmssK", "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 that we are losing precision with a line like 2015-03-29T12:53:20.238748294819293021383+01:00 , we can still parse it, and it’s also possible.
Now we need to catch 24:00:00 as a valid time, and then catch valid times, such as 2015-06-30T23:59:60 (although I do not check for 2014-06-30T23:59:60 , which has never been as this will require a constantly updated jump database -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:ssK", "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", "yyMMddTHHmmssK", "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 this is a lot of work, albeit curious. And we still don’t touch maintaining the time zone (not the difficult change above, with DateTimeOffset ) of time strings (should return a TimeSpan ), durations (should also return a TimeSpan ), periods or repeating periods.
Meanwhile, ISO 8601 is intended to define one or more profiles, which, in turn, define one or more subsets of the allowed formats, possibly with different rules. Usually we want to program one of these profiles, not ISO 8601 as a whole. For example, the foregoing is useless for parsing web 2009-W53-7 , since it accepts 2009-W53-7 for January 3, 2010, which is the proper processing of ISO 8601 but not allowed by the web-datetime profile of ISO 8601.