Okay. Let's start with the limitations you are placing on ISO 8601 yourself:
- You want a DateTime, so all formats that only result in a time-of-day, duration, year or month-and-year are not needed.
- You want a DateTime, so timezone information will become "Unspecified", "UTC" or "Local" without the ability to round-trip back to the same timezone.
- 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: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 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: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 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: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 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: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 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.