diff --git a/extra/exports b/extra/exports
index aba536f81b5077e6bb28ee2e79deedacd380f75b..be61fb7c1aef5653cd19bac91784c89e0e2dfb4a 100644
--- a/extra/exports
+++ b/extra/exports
@@ -418,6 +418,7 @@ title_update
 tnt_datetime_now
 tnt_datetime_parse_full
 tnt_datetime_strftime
+tnt_datetime_strptime
 tnt_datetime_to_string
 tnt_datetime_unpack
 tnt_default_cert_dir_paths
diff --git a/src/lib/core/datetime.c b/src/lib/core/datetime.c
index ccf20f9d8aa923abd9277c607849b682b8d61de1..9df22a690d1624a61f60e1241e3438f593e2451c 100644
--- a/src/lib/core/datetime.c
+++ b/src/lib/core/datetime.c
@@ -68,6 +68,56 @@ datetime_strftime(const struct datetime *date, char *buf, size_t len,
 	return tnt_strftime(buf, len, fmt, &tm);
 }
 
+/**
+ * Create datetime structure using given tnt_tm fieldsÑŽ
+ */
+static void
+tm_to_datetime(struct tnt_tm *tm, struct datetime *date)
+{
+	assert(tm != NULL);
+	assert(date != NULL);
+	int year = tm->tm_year;
+	int mon = tm->tm_mon;
+	int mday = tm->tm_mday;
+	int yday = tm->tm_yday;
+	int wday = tm->tm_wday;
+	dt_t dt = 0;
+
+	if ((year | mon | mday) == 0) {
+		if (yday != 0) {
+			dt = yday - 1 + DT_EPOCH_1970_OFFSET;
+		} else if (wday != 0) {
+			/* 1970-01-01 was Thursday */
+			dt = ((wday - 4) % 7) + DT_EPOCH_1970_OFFSET;
+		}
+	} else {
+		assert(mday >= 0 && mday < 32);
+		assert(mon >= 0 && mon <= 11);
+		dt = dt_from_ymd(year + 1900, mon + 1, mday);
+	}
+	int64_t local_secs =
+		(int64_t)dt * SECS_PER_DAY - SECS_EPOCH_1970_OFFSET;
+	local_secs += tm->tm_hour * 3600 + tm->tm_min * 60 + tm->tm_sec;
+	date->epoch = local_secs - tm->tm_gmtoff;
+	date->nsec = tm->tm_nsec;
+	date->tzindex = 0;
+	date->tzoffset = tm->tm_gmtoff / 60;
+}
+
+size_t
+datetime_strptime(struct datetime *date, const char *buf, const char *fmt)
+{
+	assert(date != NULL);
+	assert(fmt != NULL);
+	assert(buf != NULL);
+	struct tnt_tm t = { .tm_epoch = 0 };
+	char *ret = tnt_strptime(buf, fmt, &t);
+	if (ret == NULL)
+		return 0;
+	tm_to_datetime(&t, date);
+	return ret - buf;
+}
+
 void
 datetime_now(struct datetime *now)
 {
diff --git a/src/lib/core/datetime.h b/src/lib/core/datetime.h
index cf182783432527417b36609964c3964bcfb93052..cad0c7f272fcbe21399bea147f12fb2f0333b588 100644
--- a/src/lib/core/datetime.h
+++ b/src/lib/core/datetime.h
@@ -130,6 +130,17 @@ datetime_to_tm(const struct datetime *date, struct tnt_tm *tm);
 size_t
 datetime_parse_full(struct datetime *date, const char *str, size_t len,
 		    int32_t offset);
+/**
+ * Parse buffer given format, and construct datetime value
+ * @param date output datetime value
+ * @param buf input text buffer (0-terminated)
+ * @param fmt format to use for parsing
+ * @retval Upon successful completion returns length of accepted
+ *         prefix substring, 0 otherwise.
+ * @sa strptime()
+ */
+size_t
+datetime_strptime(struct datetime *date, const char *buf, const char *fmt);
 
 #if defined(__cplusplus)
 } /* extern "C" */
diff --git a/src/lib/tzcode/CMakeLists.txt b/src/lib/tzcode/CMakeLists.txt
index 979ec3d67c68bd8d4668b341d327618e60c014a1..2e6b00402d620217a07a30e14a54f0fdca5198a1 100644
--- a/src/lib/tzcode/CMakeLists.txt
+++ b/src/lib/tzcode/CMakeLists.txt
@@ -1,2 +1,2 @@
-add_library(tzcode STATIC strftime.c)
+add_library(tzcode STATIC strftime.c strptime.c timelocal.c)
 target_link_libraries(tzcode)
diff --git a/src/lib/tzcode/strftime.c b/src/lib/tzcode/strftime.c
index d07b9d09491a09eaffd948c2d53681665cc75fa5..eb8bf28c35baf12c77c04e63b78210e10f5f3301 100644
--- a/src/lib/tzcode/strftime.c
+++ b/src/lib/tzcode/strftime.c
@@ -36,68 +36,13 @@
 #include "private.h"
 #include "trivia/util.h"
 #include "tzcode.h"
+#include "timelocal.h"
 
 #include <assert.h>
 #include <locale.h>
 #include <stdbool.h>
 #include <stdio.h>
 
-struct lc_time_T {
-	const char *mon[MONTHSPERYEAR];
-	const char *month[MONTHSPERYEAR];
-	const char *wday[DAYSPERWEEK];
-	const char *weekday[DAYSPERWEEK];
-	const char *X_fmt;
-	const char *x_fmt;
-	const char *c_fmt;
-	const char *am;
-	const char *pm;
-	const char *date_fmt;
-};
-
-#define Locale (&C_time_locale)
-
-static const struct lc_time_T C_time_locale = {
-	{"Jan", "Feb", "Mar", "Apr", "May", "Jun",
-	 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"},
-	{"January", "February", "March", "April", "May", "June",
-	 "July", "August", "September", "October", "November", "December"},
-	{"Sun", "Mon", "Tue", "Wed",
-	 "Thu", "Fri", "Sat"},
-	{"Sunday", "Monday", "Tuesday", "Wednesday",
-	 "Thursday", "Friday", "Saturday"},
-
-	/* X_fmt */
-	"%H:%M:%S",
-
-	/*
-	 ** x_fmt
-	 ** C99 and later require this format.
-	 ** Using just numbers (as here) makes Quakers happier;
-	 ** it's also compatible with SVR4.
-	 */
-	"%m/%d/%y",
-
-	/*
-	 ** c_fmt
-	 ** C99 and later require this format.
-	 ** Previously this code used "%D %X", but we now conform to C99.
-	 ** Note that
-	 ** "%a %b %d %H:%M:%S %Y"
-	 ** is used by Solaris 2.3.
-	 */
-	"%a %b %e %T %Y",
-
-	/* am */
-	"AM",
-
-	/* pm */
-	"PM",
-
-	/* date_fmt */
-	"%a %b %e %H:%M:%S %Z %Y"
-};
-
 static int pow10[] = { 1,      10,	100,	  1000,	     10000,
 		       100000, 1000000, 10000000, 100000000, 1000000000 };
 
diff --git a/src/lib/tzcode/strptime.c b/src/lib/tzcode/strptime.c
new file mode 100644
index 0000000000000000000000000000000000000000..37656d225f04d858d2e551fe0d078f4293a4341d
--- /dev/null
+++ b/src/lib/tzcode/strptime.c
@@ -0,0 +1,720 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * Copyright (c) 2014 Gary Mills
+ * Copyright 2011, Nexenta Systems, Inc.  All rights reserved.
+ * Copyright (c) 1994 Powerdog Industries.  All rights reserved.
+ *
+ * Copyright (c) 2011 The FreeBSD Foundation
+ * All rights reserved.
+ * Portions of this software were developed by David Chisnall
+ * under sponsorship from the FreeBSD Foundation.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer
+ *    in the documentation and/or other materials provided with the
+ *    distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY POWERDOG INDUSTRIES ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE POWERDOG INDUSTRIES BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * The views and conclusions contained in the software and documentation
+ * are those of the authors and should not be interpreted as representing
+ * official policies, either expressed or implied, of Powerdog Industries.
+ */
+
+#include <sys/cdefs.h>
+
+#ifndef lint
+#ifndef NOID
+static char copyright[] __attribute__((unused)) =
+"@(#) Copyright (c) 1994 Powerdog Industries.  All rights reserved.";
+static char sccsid[] __attribute__((unused)) =
+"@(#)strptime.c	0.1 (Powerdog) 94/03/27";
+#endif /* !defined NOID */
+#endif /* not lint */
+
+#include <assert.h>
+#include <ctype.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "private.h"
+#include "timelocal.h"
+#include "tzcode.h"
+
+enum flags {
+	FLAG_NONE = 1 << 0,
+	FLAG_YEAR = 1 << 1,
+	FLAG_MONTH = 1 << 2,
+	FLAG_YDAY = 1 << 3,
+	FLAG_MDAY = 1 << 4,
+	FLAG_WDAY = 1 << 5,
+	FLAG_EPOCH = 1 << 6,
+	FLAG_NSEC = 1 << 7,
+};
+
+/*
+ * Calculate the week day of the first day of a year. Valid for
+ * the Gregorian calendar, which began Sept 14, 1752 in the UK
+ * and its colonies. Ref:
+ * http://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week
+ */
+
+static int
+first_wday_of(int year)
+{
+	return ((2 * (3 - (year / 100) % 4)) + (year % 100) +
+		((year % 100) / 4) + (isleap(year) ? 6 : 0) + 1) % 7;
+}
+
+char *
+tnt_strptime(const char *__restrict buf, const char *__restrict fmt,
+	     struct tnt_tm *__restrict tm)
+{
+	char c;
+	int day_offset = -1, wday_offset;
+	int week_offset;
+	int i, len;
+	int Ealternative, Oalternative;
+	enum flags flags = FLAG_NONE;
+	int century = -1;
+	int year = -1;
+	const char *ptr = fmt;
+
+	static int start_of_month[2][13] = {
+		{ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 },
+		{ 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 }
+	};
+
+	while (*ptr != 0) {
+		c = *ptr++;
+
+		if (c != '%') {
+			if (isspace((u_char)c))
+				while (*buf != 0 && isspace((u_char)*buf))
+					buf++;
+			else if (c != *buf++)
+				return NULL;
+			continue;
+		}
+
+		Ealternative = 0;
+		Oalternative = 0;
+	label:
+		c = *ptr++;
+		switch (c) {
+		case '%':
+			if (*buf++ != '%')
+				return NULL;
+			break;
+
+		case '+':
+			buf = tnt_strptime(buf, Locale->date_fmt, tm);
+			if (buf == NULL)
+				return NULL;
+			flags |= FLAG_WDAY | FLAG_MONTH | FLAG_MDAY | FLAG_YEAR;
+			break;
+
+		case 'C':
+			if (!is_digit((u_char)*buf))
+				return NULL;
+
+			/* XXX This will break for 3-digit centuries. */
+			len = 2;
+			for (i = 0; len && *buf != 0 && is_digit((u_char)*buf);
+			     buf++) {
+				i *= 10;
+				i += *buf - '0';
+				len--;
+			}
+
+			century = i;
+			flags |= FLAG_YEAR;
+
+			break;
+
+		case 'c':
+			buf = tnt_strptime(buf, Locale->c_fmt, tm);
+			if (buf == NULL)
+				return NULL;
+			flags |= FLAG_WDAY | FLAG_MONTH | FLAG_MDAY | FLAG_YEAR;
+			break;
+
+		case 'D':
+			buf = tnt_strptime(buf, "%m/%d/%y", tm);
+			if (buf == NULL)
+				return NULL;
+			flags |= FLAG_MONTH | FLAG_MDAY | FLAG_YEAR;
+			break;
+
+		case 'E':
+			if (Ealternative || Oalternative)
+				break;
+			Ealternative++;
+			goto label;
+
+		case 'O':
+			if (Ealternative || Oalternative)
+				break;
+			Oalternative++;
+			goto label;
+
+		case 'v':
+			buf = tnt_strptime(buf, "%e-%b-%Y", tm);
+			if (buf == NULL)
+				return NULL;
+			flags |= FLAG_MONTH | FLAG_MDAY | FLAG_YEAR;
+			break;
+
+		case 'F':
+			buf = tnt_strptime(buf, "%Y-%m-%d", tm);
+			if (buf == NULL)
+				return NULL;
+			flags |= FLAG_MONTH | FLAG_MDAY | FLAG_YEAR;
+			break;
+
+		case 'R':
+			buf = tnt_strptime(buf, "%H:%M", tm);
+			if (buf == NULL)
+				return NULL;
+			break;
+
+		case 'r':
+			buf = tnt_strptime(buf, Locale->ampm_fmt, tm);
+			if (buf == NULL)
+				return NULL;
+			break;
+
+		case 'T':
+			buf = tnt_strptime(buf, "%H:%M:%S", tm);
+			if (buf == NULL)
+				return NULL;
+			break;
+
+		case 'X':
+			buf = tnt_strptime(buf, Locale->X_fmt, tm);
+			if (buf == NULL)
+				return NULL;
+			break;
+
+		case 'x':
+			buf = tnt_strptime(buf, Locale->x_fmt, tm);
+			if (buf == NULL)
+				return NULL;
+			flags |= FLAG_MONTH | FLAG_MDAY | FLAG_YEAR;
+			break;
+
+		case 'j':
+			if (!is_digit((u_char)*buf))
+				return NULL;
+
+			len = 3;
+			for (i = 0; len && *buf != 0 && is_digit((u_char)*buf);
+			     buf++) {
+				i *= 10;
+				i += *buf - '0';
+				len--;
+			}
+			if (i < 1 || i > 366)
+				return NULL;
+
+			tm->tm_yday = i - 1;
+			flags |= FLAG_YDAY;
+
+			break;
+
+		case '0':
+		case '1':
+		case '2':
+		case '3':
+		case '4':
+		case '5':
+		case '6':
+		case '7':
+		case '8':
+		case '9':
+			for (; *ptr != 0 && is_digit((u_char)*ptr); ptr++)
+				;
+
+			c = *ptr++;
+			assert(c == 'f');
+			/* fallthru */
+		case 'f':
+			if (!is_digit((u_char)*buf))
+				return NULL;
+
+			len = 9;
+			for (i = 0; len && *buf != 0 && is_digit((u_char)*buf);
+			     buf++) {
+				i *= 10;
+				i += *buf - '0';
+				len--;
+			}
+			while (len) {
+				i *= 10;
+				len--;
+			}
+			tm->tm_nsec = i;
+			flags |= FLAG_NSEC;
+
+			break;
+
+		case 'M':
+		case 'S':
+			if (*buf == 0 || isspace((u_char)*buf))
+				break;
+
+			if (!is_digit((u_char)*buf))
+				return NULL;
+
+			len = 2;
+			for (i = 0; len && *buf != 0 && is_digit((u_char)*buf);
+			     buf++) {
+				i *= 10;
+				i += *buf - '0';
+				len--;
+			}
+
+			if (c == 'M') {
+				if (i > 59)
+					return NULL;
+				tm->tm_min = i;
+			} else {
+				if (i > 60)
+					return NULL;
+				tm->tm_sec = i;
+			}
+
+			break;
+
+		case 'H':
+		case 'I':
+		case 'k':
+		case 'l':
+			/*
+			 * %k and %l specifiers are documented as being
+			 * blank-padded.  However, there is no harm in
+			 * allowing zero-padding.
+			 *
+			 * XXX %k and %l specifiers may gobble one too many
+			 * digits if used incorrectly.
+			 */
+
+			len = 2;
+			if ((c == 'k' || c == 'l') && isblank((u_char)*buf)) {
+				buf++;
+				len = 1;
+			}
+
+			if (!is_digit((u_char)*buf))
+				return NULL;
+
+			for (i = 0; len && *buf != 0 && is_digit((u_char)*buf);
+			     buf++) {
+				i *= 10;
+				i += *buf - '0';
+				len--;
+			}
+			if (c == 'H' || c == 'k') {
+				if (i > 23)
+					return NULL;
+			} else if (i == 0 || i > 12)
+				return NULL;
+
+			tm->tm_hour = i;
+
+			break;
+
+		case 'p':
+			/*
+			 * XXX This is bogus if parsed before hour-related
+			 * specifiers.
+			 */
+			if (tm->tm_hour > 12)
+				return NULL;
+
+			len = strlen(Locale->am);
+			if (strncasecmp(buf, Locale->am, len) == 0) {
+				if (tm->tm_hour == 12)
+					tm->tm_hour = 0;
+				buf += len;
+				break;
+			}
+
+			len = strlen(Locale->pm);
+			if (strncasecmp(buf, Locale->pm, len) == 0) {
+				if (tm->tm_hour != 12)
+					tm->tm_hour += 12;
+				buf += len;
+				break;
+			}
+
+			return NULL;
+
+		case 'A':
+		case 'a':
+			for (i = 0; i < DAYSPERWEEK; i++) {
+				len = strlen(Locale->weekday[i]);
+				if (strncasecmp(buf, Locale->weekday[i], len) ==
+				    0)
+					break;
+				len = strlen(Locale->wday[i]);
+				if (strncasecmp(buf, Locale->wday[i], len) == 0)
+					break;
+			}
+			if (i == DAYSPERWEEK)
+				return NULL;
+
+			buf += len;
+			tm->tm_wday = i;
+			flags |= FLAG_WDAY;
+			break;
+
+		case 'U':
+		case 'W':
+			/*
+			 * XXX This is bogus, as we can not assume any valid
+			 * information present in the tm structure at this
+			 * point to calculate a real value, so just check the
+			 * range for now.
+			 */
+			if (!is_digit((u_char)*buf))
+				return NULL;
+
+			len = 2;
+			for (i = 0; len && *buf != 0 && is_digit((u_char)*buf);
+			     buf++) {
+				i *= 10;
+				i += *buf - '0';
+				len--;
+			}
+			if (i > 53)
+				return NULL;
+
+			if (c == 'U')
+				day_offset = TM_SUNDAY;
+			else
+				day_offset = TM_MONDAY;
+
+			week_offset = i;
+
+			break;
+
+		case 'u':
+		case 'w':
+			if (!is_digit((u_char)*buf))
+				return NULL;
+
+			i = *buf++ - '0';
+			if (i < 0 || i > 7 || (c == 'u' && i < 1) ||
+			    (c == 'w' && i > 6))
+				return NULL;
+
+			tm->tm_wday = i % 7;
+			flags |= FLAG_WDAY;
+
+			break;
+
+		case 'e':
+			/*
+			 * With %e format, our strftime(3) adds a blank space
+			 * before single digits.
+			 */
+			if (*buf != 0 && isspace((u_char)*buf))
+				buf++;
+			/* FALLTHROUGH */
+		case 'd':
+			/*
+			 * The %e specifier was once explicitly documented as
+			 * not being zero-padded but was later changed to
+			 * equivalent to %d.  There is no harm in allowing
+			 * such padding.
+			 *
+			 * XXX The %e specifier may gobble one too many
+			 * digits if used incorrectly.
+			 */
+			if (!is_digit((u_char)*buf))
+				return NULL;
+
+			len = 2;
+			for (i = 0; len && *buf != 0 && is_digit((u_char)*buf);
+			     buf++) {
+				i *= 10;
+				i += *buf - '0';
+				len--;
+			}
+			if (i == 0 || i > 31)
+				return NULL;
+
+			tm->tm_mday = i;
+			flags |= FLAG_MDAY;
+
+			break;
+
+		case 'B':
+		case 'b':
+		case 'h':
+			for (i = 0; i < MONTHSPERYEAR; i++) {
+				if (Oalternative) {
+					if (c == 'B') {
+						len = strlen(
+							Locale->alt_month[i]);
+						if (strncasecmp(
+							    buf,
+							    Locale->alt_month
+								    [i],
+							    len) == 0)
+							break;
+					}
+				} else {
+					len = strlen(Locale->month[i]);
+					if (strncasecmp(buf, Locale->month[i],
+							len) == 0)
+						break;
+				}
+			}
+			/*
+			 * Try the abbreviated month name if the full name
+			 * wasn't found and Oalternative was not requested.
+			 */
+			if (i == MONTHSPERYEAR && !Oalternative) {
+				for (i = 0; i < MONTHSPERYEAR; i++) {
+					len = strlen(Locale->mon[i]);
+					if (strncasecmp(buf, Locale->mon[i],
+							len) == 0)
+						break;
+				}
+			}
+			if (i == MONTHSPERYEAR)
+				return NULL;
+
+			tm->tm_mon = i;
+			buf += len;
+			flags |= FLAG_MONTH;
+
+			break;
+
+		case 'm':
+			if (!is_digit((u_char)*buf))
+				return NULL;
+
+			len = 2;
+			for (i = 0; len && *buf != 0 && is_digit((u_char)*buf);
+			     buf++) {
+				i *= 10;
+				i += *buf - '0';
+				len--;
+			}
+			if (i < 1 || i > 12)
+				return NULL;
+
+			tm->tm_mon = i - 1;
+			flags |= FLAG_MONTH;
+
+			break;
+
+		case 's': {
+			char *cp;
+			long n;
+
+			n = strtol(buf, &cp, 10);
+			if (n == 0) {
+				return NULL;
+			}
+			buf = cp;
+			tm->tm_epoch = n;
+			flags |= FLAG_EPOCH;
+		} break;
+
+		case 'G': /* ISO 8601 year (four digits) */
+		case 'g': /* ISO 8601 year (two digits) */
+		case 'Y':
+		case 'y':
+			if (*buf == 0 || isspace((u_char)*buf))
+				break;
+
+			if (!is_digit((u_char)*buf))
+				return NULL;
+
+			len = (c == 'Y' || c == 'G') ? 4 : 2;
+			for (i = 0; len && *buf != 0 && is_digit((u_char)*buf);
+			     buf++) {
+				i *= 10;
+				i += *buf - '0';
+				len--;
+			}
+			if (c == 'Y' || c == 'G')
+				century = i / 100;
+			year = i % 100;
+
+			flags |= FLAG_YEAR;
+
+			break;
+
+		case 'Z': {
+			const char *cp;
+			char *zonestr;
+
+			for (cp = buf; *cp && isupper((u_char)*cp); ++cp)
+				/* empty */;
+			if (cp - buf) {
+				zonestr = alloca(cp - buf + 1);
+				strncpy(zonestr, buf, cp - buf);
+				zonestr[cp - buf] = '\0';
+				tzset();
+				if (0 == strcmp(zonestr, "GMT") ||
+				    0 == strcmp(zonestr, "UTC")) {
+					tm->tm_gmtoff = 0;
+				} else if (0 == strcmp(zonestr, tzname[0])) {
+					tm->tm_isdst = 0;
+				} else if (0 == strcmp(zonestr, tzname[1])) {
+					tm->tm_isdst = 1;
+				} else {
+					return NULL;
+				}
+				buf += cp - buf;
+			}
+		} break;
+
+		case 'z': {
+			int sign = 1;
+
+			if (*buf != '+') {
+				if (*buf == '-')
+					sign = -1;
+				else
+					return NULL;
+			}
+
+			buf++;
+			i = 0;
+			for (len = 4; len > 0; len--) {
+				if (is_digit((u_char)*buf)) {
+					i *= 10;
+					i += *buf - '0';
+					buf++;
+				} else if (len == 2) {
+					i *= 100;
+					break;
+				} else
+					return NULL;
+			}
+
+			if (i > 1400 || (sign == -1 && i > 1200) ||
+			    (i % 100) >= 60)
+				return NULL;
+			tm->tm_gmtoff =
+				sign * ((i / 100) * 3600 + i % 100 * 60);
+		} break;
+
+		case 'n':
+		case 't':
+			while (isspace((u_char)*buf))
+				buf++;
+			break;
+
+		default:
+			return NULL;
+		}
+	}
+
+	if (century != -1 || year != -1) {
+		if (year == -1)
+			year = 0;
+		if (century == -1) {
+			if (year < 69)
+				year += 100;
+		} else
+			year += century * 100 - TM_YEAR_BASE;
+		tm->tm_year = year;
+	}
+
+	if (!(flags & FLAG_YDAY) && (flags & FLAG_YEAR)) {
+		if ((flags & (FLAG_MONTH | FLAG_MDAY)) ==
+		    (FLAG_MONTH | FLAG_MDAY)) {
+			tm->tm_yday = start_of_month[isleap(tm->tm_year +
+							    TM_YEAR_BASE)]
+						    [tm->tm_mon] +
+				(tm->tm_mday - 1);
+			flags |= FLAG_YDAY;
+		} else if (day_offset != -1) {
+			int tmpwday, tmpyday, fwo;
+
+			fwo = first_wday_of(tm->tm_year + TM_YEAR_BASE);
+			/* No incomplete week (week 0). */
+			if (week_offset == 0 && fwo == day_offset)
+				return NULL;
+
+			/* Set the date to the first Sunday (or Monday)
+			 * of the specified week of the year.
+			 */
+			tmpwday =
+				(flags & FLAG_WDAY) ? tm->tm_wday : day_offset;
+			tmpyday = (7 - fwo + day_offset) % 7 +
+				(week_offset - 1) * 7 +
+				(tmpwday - day_offset + 7) % 7;
+			/* Impossible yday for incomplete week (week 0). */
+			if (tmpyday < 0) {
+				if (flags & FLAG_WDAY)
+					return NULL;
+				tmpyday = 0;
+			}
+			tm->tm_yday = tmpyday;
+			flags |= FLAG_YDAY;
+		}
+	}
+
+	if ((flags & (FLAG_YEAR | FLAG_YDAY)) == (FLAG_YEAR | FLAG_YDAY)) {
+		if (!(flags & FLAG_MONTH)) {
+			i = 0;
+			while (tm->tm_yday >=
+			       start_of_month[isleap(tm->tm_year +
+						     TM_YEAR_BASE)][i])
+				i++;
+			if (i > 12) {
+				i = 1;
+				tm->tm_yday -= start_of_month[isleap(
+					tm->tm_year + TM_YEAR_BASE)][12];
+				tm->tm_year++;
+			}
+			tm->tm_mon = i - 1;
+			flags |= FLAG_MONTH;
+		}
+		if (!(flags & FLAG_MDAY)) {
+			tm->tm_mday = tm->tm_yday -
+				start_of_month[isleap(tm->tm_year +
+						      TM_YEAR_BASE)]
+					      [tm->tm_mon] +
+				1;
+			flags |= FLAG_MDAY;
+		}
+		if (!(flags & FLAG_WDAY)) {
+			i = 0;
+			wday_offset = first_wday_of(tm->tm_year);
+			while (i++ <= tm->tm_yday) {
+				if (wday_offset++ >= 6)
+					wday_offset = 0;
+			}
+			tm->tm_wday = wday_offset;
+			flags |= FLAG_WDAY;
+		}
+	}
+
+	return (char *)buf;
+}
diff --git a/src/lib/tzcode/timelocal.c b/src/lib/tzcode/timelocal.c
new file mode 100644
index 0000000000000000000000000000000000000000..32f145ac8a0c8f5f7054f139fdbe7c7832a0bf52
--- /dev/null
+++ b/src/lib/tzcode/timelocal.c
@@ -0,0 +1,91 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * Copyright (c) 2001 Alexey Zelkin <phantom@FreeBSD.org>
+ * Copyright (c) 1997 FreeBSD Inc.
+ * All rights reserved.
+ *
+ * Copyright (c) 2011 The FreeBSD Foundation
+ * All rights reserved.
+ * Portions of this software were developed by David Chisnall
+ * under sponsorship from the FreeBSD Foundation.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdlib.h>
+#include <sys/cdefs.h>
+
+#include "timelocal.h"
+
+const struct lc_time_T C_time_locale = {
+	{ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+	  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" },
+	{ "January", "February", "March", "April", "May", "June",
+	  "July", "August", "September", "October", "November", "December" },
+	{ "Sun", "Mon", "Tue", "Wed",
+	  "Thu", "Fri", "Sat" },
+	{ "Sunday", "Monday", "Tuesday", "Wednesday",
+	  "Thursday", "Friday", "Saturday" },
+
+	/* X_fmt */
+	"%H:%M:%S",
+
+	/*
+	 * x_fmt
+	 * Since the C language standard calls for
+	 * "date, using locale's date format," anything goes.
+	 * Using just numbers (as here) makes Quakers happier;
+	 * it's also compatible with SVR4.
+	 */
+	"%m/%d/%y",
+
+	/*
+	 * c_fmt
+	 */
+	"%a %b %e %H:%M:%S %Y",
+
+	/* am */
+	"AM",
+
+	/* pm */
+	"PM",
+
+	/* date_fmt */
+	"%a %b %e %H:%M:%S %Z %Y",
+
+	/* alt_month
+	 * Standalone months forms for %OB
+	 */
+	{ "January", "February", "March", "April", "May", "June", "July",
+	  "August", "September", "October", "November", "December" },
+
+	/* md_order
+	 * Month / day order in dates
+	 */
+	"md",
+
+	/* ampm_fmt
+	 * To determine 12-hour clock format time (empty, if N/A)
+	 */
+	"%I:%M:%S %p"
+};
diff --git a/src/lib/tzcode/timelocal.h b/src/lib/tzcode/timelocal.h
new file mode 100644
index 0000000000000000000000000000000000000000..ab7bf252c6675b6cbb37527affc77b6f7b2b40d6
--- /dev/null
+++ b/src/lib/tzcode/timelocal.h
@@ -0,0 +1,65 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * Copyright (c) 1997-2002 FreeBSD Project.
+ * All rights reserved.
+ *
+ * Copyright (c) 2011 The FreeBSD Foundation
+ * All rights reserved.
+ * Portions of this software were developed by David Chisnall
+ * under sponsorship from the FreeBSD Foundation.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * $FreeBSD$
+ */
+
+#ifndef _TIMELOCAL_H_
+#define _TIMELOCAL_H_
+#include "private.h"
+#include <locale.h>
+
+/**
+ * Private header file for the strftime and strptime localization
+ * stuff.
+ */
+struct lc_time_T {
+	const char *mon[MONTHSPERYEAR];
+	const char *month[MONTHSPERYEAR];
+	const char *wday[DAYSPERWEEK];
+	const char *weekday[DAYSPERWEEK];
+	const char *X_fmt;
+	const char *x_fmt;
+	const char *c_fmt;
+	const char *am;
+	const char *pm;
+	const char *date_fmt;
+	const char *alt_month[MONTHSPERYEAR];
+	const char *md_order;
+	const char *ampm_fmt;
+};
+
+#define Locale (&C_time_locale)
+
+extern const struct lc_time_T C_time_locale;
+
+#endif /* !_TIMELOCAL_H_ */
diff --git a/src/lib/tzcode/tzcode.h b/src/lib/tzcode/tzcode.h
index 75531800e88d1ca68359a24862781152a1d27bf9..1b3add2397407f9c357fca2fc3ee4f3a7c6a8cea 100644
--- a/src/lib/tzcode/tzcode.h
+++ b/src/lib/tzcode/tzcode.h
@@ -56,6 +56,16 @@ size_t
 tnt_strftime(char *s, size_t maxsize, const char *format,
 	     const struct tnt_tm *tm);
 
+/**
+ * tnt_strptime is a Tarantool version of POSIX strptime()
+ * which has been extended with %f (fractions of second)
+ * flag support.
+ * @sa strptime()
+ */
+char *
+tnt_strptime(const char *__restrict buf, const char *__restrict fmt,
+	     struct tnt_tm *__restrict tm);
+
 #if defined(__cplusplus)
 } /* extern "C" */
 #endif /* defined(__cplusplus) */
diff --git a/src/lua/datetime.lua b/src/lua/datetime.lua
index b032d3284b29055badf50fb7ea67a0c93246063b..8bb21e440a5dfd46487c2b05cd6523d3cbc530e5 100644
--- a/src/lua/datetime.lua
+++ b/src/lua/datetime.lua
@@ -76,6 +76,8 @@ size_t tnt_datetime_strftime(const struct datetime *date, char *buf,
                              uint32_t len, const char *fmt);
 size_t tnt_datetime_parse_full(struct datetime *date, const char *str,
                                size_t len, int32_t offset);
+size_t tnt_datetime_strptime(struct datetime *date, const char *buf,
+                             const char *fmt);
 void   tnt_datetime_now(struct datetime *now);
 
 ]]
@@ -923,6 +925,19 @@ local function datetime_parse_full(str, tzoffset)
     return date, len
 end
 
+--[[
+    Parse datetime string given `strptime` like format.
+    Returns constructed datetime object and length of accepted string.
+]]
+local function datetime_parse_format(str, fmt)
+    local date = ffi.new(datetime_t)
+    local len = builtin.tnt_datetime_strptime(date, str, fmt)
+    if len == 0 then
+        error(("could not parse '%s' using '%s' format"):format(str, fmt))
+    end
+    return date, len
+end
+
 local function datetime_parse_from(str, obj)
     check_str(str, 'datetime.parse()')
     local fmt = ''
@@ -946,7 +961,7 @@ local function datetime_parse_from(str, obj)
     if not fmt or fmt == '' or fmt == 'iso8601' or fmt == 'rfc3339' then
         return datetime_parse_full(str, offset or 0)
     else
-        error(("unknown format '%s'"):format(fmt), 2)
+        return datetime_parse_format(str, fmt)
     end
 end
 
diff --git a/src/lua/tnt_datetime.c b/src/lua/tnt_datetime.c
index 71573a512923a5c1a840da883d51fb51dcc0d051..49fa1b1303966c9106343affa905d53fbdd29d25 100644
--- a/src/lua/tnt_datetime.c
+++ b/src/lua/tnt_datetime.c
@@ -14,6 +14,12 @@ tnt_datetime_strftime(const struct datetime *date, char *buf, size_t len,
 	return datetime_strftime(date, buf, len, fmt);
 }
 
+size_t
+tnt_datetime_strptime(struct datetime *date, const char *buf, const char *fmt)
+{
+	return datetime_strptime(date, buf, fmt);
+}
+
 void
 tnt_datetime_now(struct datetime *now)
 {
diff --git a/test/app-tap/datetime.test.lua b/test/app-tap/datetime.test.lua
index 974468545fbb2be10b399f57cc020dda0cca6278..ee40a3081ca2831d08147db04b1f18c6591c52d4 100755
--- a/test/app-tap/datetime.test.lua
+++ b/test/app-tap/datetime.test.lua
@@ -11,7 +11,7 @@ local ffi = require('ffi')
 --]]
 if jit.arch == 'arm64' then jit.off() end
 
-test:plan(29)
+test:plan(31)
 
 -- minimum supported date - -5879610-06-22
 local MIN_DATE_YEAR = -5879610
@@ -492,16 +492,16 @@ test:test("Multiple tests for parser (with nanoseconds)", function(test)
     end
 end)
 
-local function couldnt_parse(txt)
-    return ("could not parse '%s'"):format(txt)
-end
-
 local function create_date_string(date)
     local year, month, day = date.year or 1970, date.month or 1, date.day or 1
     local hour, min, sec = date.hour or 0, date.min or 0, date.sec or 0
     return ('%04d-%02d-%02dT%02d:%02d:%02dZ'):format(year, month, day, hour, min, sec)
 end
 
+local function couldnt_parse(txt)
+    return ("could not parse '%s'"):format(txt)
+end
+
 test:test("Check parsing of dates with invalid attributes", function(test)
     test:plan(32)
 
@@ -590,97 +590,113 @@ test:test("Datetime formatting of huge dates", function(test)
     check_variant_formats(test, base, variants)
 end)
 
+local strftime_formats = {
+    { '%A',                      1, 'Thursday' },
+    { '%a',                      1, 'Thu' },
+    { '%B',                      1, 'January' },
+    { '%b',                      1, 'Jan' },
+    { '%h',                      1, 'Jan' },
+    { '%C',                      0, '19' },
+    { '%c',                      1, 'Thu Jan  1 03:00:00 1970' },
+    { '%D',                      1, '01/01/70' },
+    { '%m/%d/%y',                1, '01/01/70' },
+    { '%d',                      1, '01' },
+    { '%Ec',                     1, 'Thu Jan  1 03:00:00 1970' },
+    { '%EC',                     0, '19' },
+    { '%Ex',                     1, '01/01/70' },
+    { '%EX',                     1, '03:00:00' },
+    { '%Ey',                     1, '70' },
+    { '%EY',                     1, '1970' },
+    { '%Od',                     1, '01' },
+    { '%oe',                     0, 'oe' },
+    { '%OH',                     1, '03' },
+    { '%OI',                     1, '03' },
+    { '%Om',                     1, '01' },
+    { '%OM',                     1, '00' },
+    { '%OS',                     1, '00' },
+    { '%Ou',                     1, '4' },
+    { '%OU',                     1, '00' },
+    { '%OV',                     0, '01' },
+    { '%Ow',                     1, '4' },
+    { '%OW',                     1, '00' },
+    { '%Oy',                     1, '70' },
+    { '%e',                      1, ' 1' },
+    { '%F',                      1, '1970-01-01' },
+    { '%Y-%m-%d',                1, '1970-01-01' },
+    { '%H',                      1, '03' },
+    { '%I',                      1, '03' },
+    { '%j',                      1, '001' },
+    { '%k',                      1, ' 3' },
+    { '%l',                      1, ' 3' },
+    { '%M',                      1, '00' },
+    { '%m',                      1, '01' },
+    { '%n',                      1, '\n' },
+    { '%p',                      1, 'AM' },
+    { '%R',                      1, '03:00' },
+    { '%H:%M',                   1, '03:00' },
+    { '%r',                      1, '03:00:00 AM' },
+    { '%I:%M:%S %p',             1, '03:00:00 AM' },
+    { '%S',                      1, '00' },
+    { '%s',                      1, '10800' },
+    { '%f',                      1, '125' },
+    { '%3f',                     0, '125' },
+    { '%6f',                     0, '125000' },
+    { '%6d',                     0, '6d' },
+    { '%3D',                     0, '3D' },
+    { '%T',                      1, '03:00:00' },
+    { '%H:%M:%S',                1, '03:00:00' },
+    { '%t',                      1, '\t' },
+    { '%U',                      1, '00' },
+    { '%u',                      1, '4' },
+    { '%V',                      0, '01' },
+    { '%G',                      1, '1970' },
+    { '%g',                      1, '70' },
+    { '%v',                      1, ' 1-Jan-1970' },
+    { '%e-%b-%Y',                1, ' 1-Jan-1970' },
+    { '%W',                      1, '00' },
+    { '%w',                      1, '4' },
+    { '%X',                      1, '03:00:00' },
+    { '%x',                      1, '01/01/70' },
+    { '%y',                      1, '70' },
+    { '%Y',                      1, '1970' },
+    { '%z',                      1, '+0300' },
+    { '%%',                      1, '%' },
+    { '%Y-%m-%dT%H:%M:%S.%9f%z', 1, '1970-01-01T03:00:00.125000000+0300' },
+    { '%Y-%m-%dT%H:%M:%S.%f%z',  1, '1970-01-01T03:00:00.125+0300' },
+    { '%Y-%m-%dT%H:%M:%S.%f',    1, '1970-01-01T03:00:00.125' },
+    { '%FT%T.%f',                1, '1970-01-01T03:00:00.125' },
+    { '%FT%T.%f%z',              1, '1970-01-01T03:00:00.125+0300' },
+    { '%FT%T.%9f%z',             1, '1970-01-01T03:00:00.125000000+0300' },
+}
+
 test:test("Datetime string formatting detailed", function(test)
     test:plan(77)
     local T = date.new{ timestamp = 0.125 }
     T:set{ tzoffset = 180 }
     test:is(tostring(T), '1970-01-01T03:00:00.125+0300', 'tostring()')
 
-    local formats = {
-        { '%A',                         'Thursday' },
-        { '%a',                         'Thu' },
-        { '%B',                         'January' },
-        { '%b',                         'Jan' },
-        { '%h',                         'Jan' },
-        { '%C',                         '19' },
-        { '%c',                         'Thu Jan  1 03:00:00 1970' },
-        { '%D',                         '01/01/70' },
-        { '%m/%d/%y',                   '01/01/70' },
-        { '%d',                         '01' },
-        { '%Ec',                        'Thu Jan  1 03:00:00 1970' },
-        { '%EC',                        '19' },
-        { '%Ex',                        '01/01/70' },
-        { '%EX',                        '03:00:00' },
-        { '%Ey',                        '70' },
-        { '%EY',                        '1970' },
-        { '%Od',                        '01' },
-        { '%oe',                        'oe' },
-        { '%OH',                        '03' },
-        { '%OI',                        '03' },
-        { '%Om',                        '01' },
-        { '%OM',                        '00' },
-        { '%OS',                        '00' },
-        { '%Ou',                        '4' },
-        { '%OU',                        '00' },
-        { '%OV',                        '01' },
-        { '%Ow',                        '4' },
-        { '%OW',                        '00' },
-        { '%Oy',                        '70' },
-        { '%e',                         ' 1' },
-        { '%F',                         '1970-01-01' },
-        { '%Y-%m-%d',                   '1970-01-01' },
-        { '%H',                         '03' },
-        { '%I',                         '03' },
-        { '%j',                         '001' },
-        { '%k',                         ' 3' },
-        { '%l',                         ' 3' },
-        { '%M',                         '00' },
-        { '%m',                         '01' },
-        { '%n',                         '\n' },
-        { '%p',                         'AM' },
-        { '%R',                         '03:00' },
-        { '%H:%M',                      '03:00' },
-        { '%r',                         '03:00:00 AM' },
-        { '%I:%M:%S %p',                '03:00:00 AM' },
-        { '%S',                         '00' },
-        { '%s',                         '10800' },
-        { '%f',                         '125' },
-        { '%3f',                        '125' },
-        { '%6f',                        '125000' },
-        { '%6d',                        '6d' },
-        { '%3D',                        '3D' },
-        { '%T',                         '03:00:00' },
-        { '%H:%M:%S',                   '03:00:00' },
-        { '%t',                         '\t' },
-        { '%U',                         '00' },
-        { '%u',                         '4' },
-        { '%V',                         '01' },
-        { '%G',                         '1970' },
-        { '%g',                         '70' },
-        { '%v',                         ' 1-Jan-1970' },
-        { '%e-%b-%Y',                   ' 1-Jan-1970' },
-        { '%W',                         '00' },
-        { '%w',                         '4' },
-        { '%X',                         '03:00:00' },
-        { '%x',                         '01/01/70' },
-        { '%y',                         '70' },
-        { '%Y',                         '1970' },
-        { '%z',                         '+0300' },
-        { '%%',                         '%' },
-        { '%Y-%m-%dT%H:%M:%S.%9f%z',    '1970-01-01T03:00:00.125000000+0300' },
-        { '%Y-%m-%dT%H:%M:%S.%f%z',     '1970-01-01T03:00:00.125+0300' },
-        { '%Y-%m-%dT%H:%M:%S.%f',       '1970-01-01T03:00:00.125' },
-        { '%FT%T.%f',                   '1970-01-01T03:00:00.125' },
-        { '%FT%T.%f%z',                 '1970-01-01T03:00:00.125+0300' },
-        { '%FT%T.%9f%z',                '1970-01-01T03:00:00.125000000+0300' },
-    }
-    for _, row in pairs(formats) do
-        local fmt, value = unpack(row)
+    for _, row in pairs(strftime_formats) do
+        local fmt, _, value = unpack(row)
         test:is(T:format(fmt), value,
                 ('format %s, expected %s'):format(fmt, value))
     end
 end)
 
+test:test("Datetime string parsing by format (detailed)", function(test)
+    test:plan(68)
+    local T = date.new{ timestamp = 0.125 }
+    T:set{ tzoffset = 180 }
+    test:is(tostring(T), '1970-01-01T03:00:00.125+0300', 'tostring()')
+
+    for _, row in pairs(strftime_formats) do
+        local fmt, check, value = unpack(row)
+        if check > 0 then
+            local res = date.parse(value, {format = fmt})
+            test:is(res ~= nil, true, ('parse of %s'):format(fmt))
+        end
+    end
+end)
+
 test:test("__index functions()", function(test)
     test:plan(15)
     -- 2000-01-29T03:30:12Z'
@@ -1374,6 +1390,42 @@ test:test("Parse tiny date into seconds and other parts", function(test)
     test:is(tiny.timestamp, 30.528, "timestamp")
 end)
 
+test:test("Parse strptime format", function(test)
+    test:plan(17)
+    local formats = {
+        {'Thu Jan  1 03:00:00 1970',    '%c',       '1970-01-01T03:00:00Z'},
+        {'01/01/70',                    '%D',       '1970-01-01T00:00:00Z'},
+        {'01/01/70',                    '%m/%d/%y', '1970-01-01T00:00:00Z'},
+        {'Thu Jan  1 03:00:00 1970',    '%Ec',      '1970-01-01T03:00:00Z'},
+        {'1970-01-01',                  '%F',       '1970-01-01T00:00:00Z'},
+        {'1970-01-01',                  '%Y-%m-%d', '1970-01-01T00:00:00Z'},
+        {' 1-Jan-1970',                 '%v',       '1970-01-01T00:00:00Z'},
+        {' 1-Jan-1970',                 '%e-%b-%Y', '1970-01-01T00:00:00Z'},
+        {'01/01/70',                    '%x',       '1970-01-01T00:00:00Z'},
+        {'1970-01-01T0300+0300',        '%Y-%m-%dT%H%M%z',
+            '1970-01-01T03:00:00+0300'},
+        {'1970-01-01T03:00:00+0300',    '%Y-%m-%dT%H:%M:%S%z',
+            '1970-01-01T03:00:00+0300'},
+        {'1970-01-01T03:00:00.125000000+0300',  '%Y-%m-%dT%H:%M:%S.%f%z',
+            '1970-01-01T03:00:00.125+0300'},
+        {'1970-01-01T03:00:00.125+0300',        '%Y-%m-%dT%H:%M:%S.%f%z',
+            '1970-01-01T03:00:00.125+0300'},
+        {'1970-01-01T03:00:00.125',             '%Y-%m-%dT%H:%M:%S.%f',
+            '1970-01-01T03:00:00.125Z'},
+        {'1970-01-01T03:00:00.125',             '%FT%T.%f',
+            '1970-01-01T03:00:00.125Z'},
+        {'1970-01-01T03:00:00.125+0300',        '%FT%T.%f%z',
+            '1970-01-01T03:00:00.125+0300'},
+        {'1970-01-01T03:00:00.125000000+0300',  '%FT%T.%f%z',
+            '1970-01-01T03:00:00.125+0300'},
+    }
+    for _, row in pairs(formats) do
+        local str, fmt, exp = unpack(row)
+        local dt = date.parse(str, {format = fmt})
+        test:is(tostring(dt), exp, ('parse %s via %s'):format(str, fmt))
+    end
+end)
+
 test:test("totable{}", function(test)
     test:plan(78)
     local exp = {sec = 0, min = 0, wday = 5, day = 1,