compat: MUSL implementation of strptime()

On Windows there's no strptime(). A similar functionality is available
from C++11's `std::get_time` in `<iomanip>`, but there are subtle
differences, including actual behavioral differences when parsing
invalid dates such as "January 41st" (which is invalid everywhere, and
could become "January 4th", eating one char less), or Feb 29th, 2018
(which does not exist).

Let's just import the MIT-licensed implementation from the MUSL C
library because it's reasonably self-contained and appears to work the
same as the glibc one.
diff --git a/compat/strptime.c b/compat/strptime.c
new file mode 100644
index 0000000..3177b92
--- /dev/null
+++ b/compat/strptime.c
@@ -0,0 +1,213 @@
+/*
+ * This comes from the musl C library which has been licensed under the permissive MIT license.
+ *
+ * Downloaded from https://git.musl-libc.org/cgit/musl/plain/src/time/strptime.c,
+ * commit 98e688a9da5e7b2925dda17a2d6820dddf1fb287.
+ * */
+
+#include <stdlib.h>
+#include <langinfo.h>
+#include <time.h>
+#include <ctype.h>
+#include <stddef.h>
+#include <string.h>
+#include <strings.h>
+
+char *strptime(const char *restrict s, const char *restrict f, struct tm *restrict tm)
+{
+	int i, w, neg, adj, min, range, *dest, dummy;
+	const char *ex;
+	size_t len;
+	int want_century = 0, century = 0, relyear = 0;
+	while (*f) {
+		if (*f != '%') {
+			if (isspace(*f)) for (; *s && isspace(*s); s++);
+			else if (*s != *f) return 0;
+			else s++;
+			f++;
+			continue;
+		}
+		f++;
+		if (*f == '+') f++;
+		if (isdigit(*f)) {
+			char *new_f;
+			w=strtoul(f, &new_f, 10);
+			f = new_f;
+		} else {
+			w=-1;
+		}
+		adj=0;
+		switch (*f++) {
+		case 'a': case 'A':
+			dest = &tm->tm_wday;
+			min = ABDAY_1;
+			range = 7;
+			goto symbolic_range;
+		case 'b': case 'B': case 'h':
+			dest = &tm->tm_mon;
+			min = ABMON_1;
+			range = 12;
+			goto symbolic_range;
+		case 'c':
+			s = strptime(s, nl_langinfo(D_T_FMT), tm);
+			if (!s) return 0;
+			break;
+		case 'C':
+			dest = &century;
+			if (w<0) w=2;
+			want_century |= 2;
+			goto numeric_digits;
+		case 'd': case 'e':
+			dest = &tm->tm_mday;
+			min = 1;
+			range = 31;
+			goto numeric_range;
+		case 'D':
+			s = strptime(s, "%m/%d/%y", tm);
+			if (!s) return 0;
+			break;
+		case 'H':
+			dest = &tm->tm_hour;
+			min = 0;
+			range = 24;
+			goto numeric_range;
+		case 'I':
+			dest = &tm->tm_hour;
+			min = 1;
+			range = 12;
+			goto numeric_range;
+		case 'j':
+			dest = &tm->tm_yday;
+			min = 1;
+			range = 366;
+			adj = 1;
+			goto numeric_range;
+		case 'm':
+			dest = &tm->tm_mon;
+			min = 1;
+			range = 12;
+			adj = 1;
+			goto numeric_range;
+		case 'M':
+			dest = &tm->tm_min;
+			min = 0;
+			range = 60;
+			goto numeric_range;
+		case 'n': case 't':
+			for (; *s && isspace(*s); s++);
+			break;
+		case 'p':
+			ex = nl_langinfo(AM_STR);
+			len = strlen(ex);
+			if (!strncasecmp(s, ex, len)) {
+				tm->tm_hour %= 12;
+				s += len;
+				break;
+			}
+			ex = nl_langinfo(PM_STR);
+			len = strlen(ex);
+			if (!strncasecmp(s, ex, len)) {
+				tm->tm_hour %= 12;
+				tm->tm_hour += 12;
+				s += len;
+				break;
+			}
+			return 0;
+		case 'r':
+			s = strptime(s, nl_langinfo(T_FMT_AMPM), tm);
+			if (!s) return 0;
+			break;
+		case 'R':
+			s = strptime(s, "%H:%M", tm);
+			if (!s) return 0;
+			break;
+		case 'S':
+			dest = &tm->tm_sec;
+			min = 0;
+			range = 61;
+			goto numeric_range;
+		case 'T':
+			s = strptime(s, "%H:%M:%S", tm);
+			if (!s) return 0;
+			break;
+		case 'U':
+		case 'W':
+			/* Throw away result, for now. (FIXME?) */
+			dest = &dummy;
+			min = 0;
+			range = 54;
+			goto numeric_range;
+		case 'w':
+			dest = &tm->tm_wday;
+			min = 0;
+			range = 7;
+			goto numeric_range;
+		case 'x':
+			s = strptime(s, nl_langinfo(D_FMT), tm);
+			if (!s) return 0;
+			break;
+		case 'X':
+			s = strptime(s, nl_langinfo(T_FMT), tm);
+			if (!s) return 0;
+			break;
+		case 'y':
+			dest = &relyear;
+			w = 2;
+			want_century |= 1;
+			goto numeric_digits;
+		case 'Y':
+			dest = &tm->tm_year;
+			if (w<0) w=4;
+			adj = 1900;
+			want_century = 0;
+			goto numeric_digits;
+		case '%':
+			if (*s++ != '%') return 0;
+			break;
+		default:
+			return 0;
+		numeric_range:
+			if (!isdigit(*s)) return 0;
+			*dest = 0;
+			for (i=1; i<=min+range && isdigit(*s); i*=10)
+				*dest = *dest * 10 + *s++ - '0';
+			if (*dest - min >= (unsigned)range) return 0;
+			*dest -= adj;
+			switch((char *)dest - (char *)tm) {
+			case offsetof(struct tm, tm_yday):
+				;
+			}
+			goto update;
+		numeric_digits:
+			neg = 0;
+			if (*s == '+') s++;
+			else if (*s == '-') neg=1, s++;
+			if (!isdigit(*s)) return 0;
+			for (*dest=i=0; i<w && isdigit(*s); i++)
+				*dest = *dest * 10 + *s++ - '0';
+			if (neg) *dest = -*dest;
+			*dest -= adj;
+			goto update;
+		symbolic_range:
+			for (i=2*range-1; i>=0; i--) {
+				ex = nl_langinfo(min+i);
+				len = strlen(ex);
+				if (strncasecmp(s, ex, len)) continue;
+				s += len;
+				*dest = i % range;
+				break;
+			}
+			if (i<0) return 0;
+			goto update;
+		update:
+			//FIXME
+			;
+		}
+	}
+	if (want_century) {
+		tm->tm_year = relyear;
+		if (want_century & 2) tm->tm_year += century * 100 - 1900;
+		else if (tm->tm_year <= 68) tm->tm_year += 100;
+	}
+	return (char *)s;
+}