{"id":104313,"date":"2020-09-29T06:59:05","date_gmt":"2020-09-29T13:59:05","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=104313"},"modified":"2020-09-29T06:59:05","modified_gmt":"2020-09-29T13:59:05","slug":"20200929-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20200929-05\/?p=104313","title":{"rendered":"How did we end up parsing Savvyday 29 Oatmeal 94 as Saturday 29 October 1994?"},"content":{"rendered":"<p>Some time ago, we learned that <a href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20200304-00\/?p=103527\"> the <code>Internet\u00adTime\u00adTo\u00adSystem\u00adTime<\/code> function manages to parse &#8220;Savvyday 29 Oatmeal 94&#8221; as &#8220;Saturday 29 October 1994&#8221;<\/a>. How did that happen? Is it <a href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20200304-00\/?p=103527#comment-136342\"> finding the date with the shortest English Levenshtein distance<\/a>?<\/p>\n<p>Nothing that fancy.<\/p>\n<p><b>Warning<\/b>: This article discusses implementation details, which are not contractual. The algorithm is subject to change in the future. The only thing that <code>Internet\u00adTime\u00adTo\u00adSystem\u00adTime<\/code> formally guarantees is that it can parse properly-formatted HTTP timestamps.<\/p>\n<p>The parsing is very simple. The official format for HTTP date strings is<\/p>\n<ol>\n<li>DayOfWeek, Day Month Year Hour Minute Second GMT<\/li>\n<\/ol>\n<p>In practice, not everybody follows the rules, so the parser accepts these three formats:<\/p>\n<ol>\n<li>DayOfWeek Day Month Year Hour Minute Second TZ<\/li>\n<li>DayOfWeek Month Day Hour Minute Second TZ Year<\/li>\n<li>DayOfWeek Month Day Hour Minute Second Year TZ<\/li>\n<\/ol>\n<p>After discarding non-alphanumerics, the parser takes each word in the input string and converts it to a number somehow. If it consists of digits, then it&#8217;s parsed to a number in the usual way. If it consists of alphabetics, then it&#8217;s parsed to a number by trying to match it against the list of valid tokens:<\/p>\n<table class=\"cp3\" style=\"border-collapse: collapse;\" border=\"1\" cellspacing=\"0\" cellpadding=\"3\">\n<tbody>\n<tr>\n<th>DayOfWeek<\/th>\n<th colspan=\"2\">Month<\/th>\n<th>TZ<\/th>\n<\/tr>\n<tr>\n<td>Sun = 0<\/td>\n<td>Jan = 1<\/td>\n<td>Jul = 7<\/td>\n<td>GMT<\/td>\n<\/tr>\n<tr>\n<td>Mon = 1<\/td>\n<td>Feb = 2<\/td>\n<td>Aug = 8<\/td>\n<td>UTC<\/td>\n<\/tr>\n<tr>\n<td>Tue = 2<\/td>\n<td>Mar = 3<\/td>\n<td>Sep = 9<\/td>\n<td>&nbsp;<\/td>\n<\/tr>\n<tr>\n<td>Wed = 3<\/td>\n<td>Apr = 4<\/td>\n<td>Oct = 10<\/td>\n<td>&nbsp;<\/td>\n<\/tr>\n<tr>\n<td>Thu = 4<\/td>\n<td>May = 5<\/td>\n<td>Nov = 11<\/td>\n<td>&nbsp;<\/td>\n<\/tr>\n<tr>\n<td>Fri = 5<\/td>\n<td>Jun = 6<\/td>\n<td>Dec = 12<\/td>\n<td>&nbsp;<\/td>\n<\/tr>\n<tr>\n<td>Sat = 7<\/td>\n<td>&nbsp;<\/td>\n<td>&nbsp;<\/td>\n<td>&nbsp;<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>If no match is found, then we look for an entry which shares the most initial characters with the word being parsed. If there is a unique such entry, then the parsed value is as given in the table. If there is no such entry, or the longest match is not unique, then parsing fails.\u00b9<\/p>\n<p>Since there only one time zone permitted in HTTP time\/date strings, all we have to remember is &#8220;Yup, it&#8217;s a time zone. There&#8217;s a time zone marker here.&#8221;<\/p>\n<p>For example, the string &#8220;Savvyday&#8221; is not in the above table, but it does share the following prefixes:<\/p>\n<table class=\"cp3\" style=\"border-collapse: collapse;\" border=\"1\" cellspacing=\"0\" cellpadding=\"3\">\n<tbody>\n<tr>\n<th>Length 1<\/th>\n<th>Length 2<\/th>\n<\/tr>\n<tr>\n<td>S(un)<\/td>\n<td>Sa(t)<\/td>\n<\/tr>\n<tr>\n<td>S(ep)<\/td>\n<td>&nbsp;<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The longest match is length 2, and there&#8217;s only one such match, so the word &#8220;Savvyday&#8221; is parsed as if it were &#8220;Sat&#8221;.<\/p>\n<p>Similarly, &#8220;Oatmeal&#8221; has only one match: Oct (length 1).<\/p>\n<p>After everything is parsed into a number, we decide which of the three formats we are looking at.<\/p>\n<p>If the second word was parsed from digits, then we are in case 1. If the seventh word was parsed from letters, then we are in case 2. Otherwise, we are in case 3.<\/p>\n<p>Once we&#8217;ve decided what case we&#8217;re in, we know where the year is. If the caller provided a two-digit year, upgrade it to a four-digit year.<\/p>\n<p>Finally, we copy the fields into the output structure. If a field is missing, it is taken from the current date and time.<\/p>\n<p>That&#8217;s it. Nothing fancy. The algorithm is optimized for the case where the string follows the correct format. If you pass something that&#8217;s not in the correct format, it does what it can. Sometimes it even comes up with something vaguely sensible!<\/p>\n<p>Usually not.\u00b2<\/p>\n<p>\u00b9 If you think about it, this can be done very quickly by a simple decision tree:<\/p>\n<table class=\"cp3\" style=\"border-collapse: collapse; text-align: center;\" border=\"1\" cellspacing=\"0\" cellpadding=\"3\">\n<tbody>\n<tr>\n<th colspan=\"3\">Character<\/th>\n<th rowspan=\"2\">Result<\/th>\n<\/tr>\n<tr>\n<th>1<\/th>\n<th>2<\/th>\n<th>3<\/th>\n<\/tr>\n<tr>\n<td rowspan=\"2\">A<\/td>\n<td>P<\/td>\n<td>&nbsp;<\/td>\n<td>Apr<\/td>\n<\/tr>\n<tr>\n<td>U<\/td>\n<td>&nbsp;<\/td>\n<td>Aug<\/td>\n<\/tr>\n<tr>\n<td>D<\/td>\n<td>&nbsp;<\/td>\n<td>&nbsp;<\/td>\n<td>Dec<\/td>\n<\/tr>\n<tr>\n<td rowspan=\"2\">F<\/td>\n<td>E<\/td>\n<td>&nbsp;<\/td>\n<td>Feb<\/td>\n<\/tr>\n<tr>\n<td>R<\/td>\n<td>&nbsp;<\/td>\n<td>Fri<\/td>\n<\/tr>\n<tr>\n<td>G<\/td>\n<td>&nbsp;<\/td>\n<td>&nbsp;<\/td>\n<td>GMT<\/td>\n<\/tr>\n<tr>\n<td rowspan=\"3\">J<\/td>\n<td>A<\/td>\n<td>&nbsp;<\/td>\n<td>Jan<\/td>\n<\/tr>\n<tr>\n<td rowspan=\"2\">U<\/td>\n<td>L<\/td>\n<td>Jul<\/td>\n<\/tr>\n<tr>\n<td>N<\/td>\n<td>Jun<\/td>\n<\/tr>\n<tr>\n<td rowspan=\"3\">M<\/td>\n<td rowspan=\"2\">A<\/td>\n<td>R<\/td>\n<td>Mar<\/td>\n<\/tr>\n<tr>\n<td>Y<\/td>\n<td>May<\/td>\n<\/tr>\n<tr>\n<td>O<\/td>\n<td>&nbsp;<\/td>\n<td>Mon<\/td>\n<\/tr>\n<tr>\n<td>N<\/td>\n<td>&nbsp;<\/td>\n<td>&nbsp;<\/td>\n<td>Nov<\/td>\n<\/tr>\n<tr>\n<td>O<\/td>\n<td>&nbsp;<\/td>\n<td>&nbsp;<\/td>\n<td>Oct<\/td>\n<\/tr>\n<tr>\n<td rowspan=\"3\">S<\/td>\n<td>A<\/td>\n<td>&nbsp;<\/td>\n<td>Sat<\/td>\n<\/tr>\n<tr>\n<td>E<\/td>\n<td>&nbsp;<\/td>\n<td>Sep<\/td>\n<\/tr>\n<tr>\n<td>U<\/td>\n<td>&nbsp;<\/td>\n<td>Sun<\/td>\n<\/tr>\n<tr>\n<td rowspan=\"2\">T<\/td>\n<td>H<\/td>\n<td>&nbsp;<\/td>\n<td>Thu<\/td>\n<\/tr>\n<tr>\n<td>U<\/td>\n<td>&nbsp;<\/td>\n<td>Tue<\/td>\n<\/tr>\n<tr>\n<td>U<\/td>\n<td>&nbsp;<\/td>\n<td>&nbsp;<\/td>\n<td>UTC<\/td>\n<\/tr>\n<tr>\n<td>W<\/td>\n<td>&nbsp;<\/td>\n<td>&nbsp;<\/td>\n<td>Wed<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>\u00b2 For example, <tt><a href=\"https:\/\/www.youtube.com\/watch?v=kfVsfOSbJY0\">Friday Friday<\/a> Friday Friday Friday Friday Friday Friday<\/tt> parses to &#8220;day 5 of month 5 year 5, hour 5 minute 5, and 5 seconds&#8221; or &#8220;May 5, 2005 at 05:05:05 GMT&#8221;.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Unsophisticated pattern matching.<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[2],"class_list":["post-104313","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-history"],"acf":[],"blog_post_summary":"<p>Unsophisticated pattern matching.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/104313","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=104313"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/104313\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=104313"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=104313"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=104313"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}