diff --git a/src/box/sql/mem.c b/src/box/sql/mem.c
index 35e52fe6867ab4df4ad8e2ba40c5fc8d6df5b618..2d369d704fa9219ad49c6e3c3abb3f786aa862d5 100644
--- a/src/box/sql/mem.c
+++ b/src/box/sql/mem.c
@@ -864,7 +864,7 @@ str_to_datetime(struct Mem *mem)
 {
 	assert(mem->type == MEM_TYPE_STR);
 	struct datetime dt;
-	if (datetime_parse_full(&dt, mem->z, mem->n, 0) <= 0)
+	if (datetime_parse_full(&dt, mem->z, mem->n, NULL, 0) <= 0)
 		return -1;
 	mem_set_datetime(mem, &dt);
 	return 0;
diff --git a/src/lib/core/datetime.c b/src/lib/core/datetime.c
index 07f9988a1f219759f66fd9b734a7f0ecbfcf4767..62fd5da0fa1b7e00d41bc4614eb62f7c9d92358a 100644
--- a/src/lib/core/datetime.c
+++ b/src/lib/core/datetime.c
@@ -80,10 +80,7 @@ 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 bool
+bool
 tm_to_datetime(struct tnt_tm *tm, struct datetime *date)
 {
 	assert(tm != NULL);
@@ -224,9 +221,42 @@ datetime_to_string(const struct datetime *date, char *buf, ssize_t len)
 	return sz;
 }
 
+static inline int64_t
+dt_epoch(dt_t dt)
+{
+	return ((int64_t)dt_rdn(dt) - DT_EPOCH_1970_OFFSET) * SECS_PER_DAY;
+}
+
+/** Common timezone suffix parser */
+static inline ssize_t
+parse_tz_suffix(const char *str, size_t len, time_t base,
+		int16_t *tzindex, int32_t *offset)
+{
+	/* 1st attempt: decode as MSK */
+	const struct date_time_zone *zone;
+	long gmtoff = 0;
+	ssize_t l = timezone_epoch_lookup(str, len, base, &zone, &gmtoff);
+	if (l < 0)
+		return l;
+	if (l > 0) {
+		assert(zone != NULL);
+		*offset = gmtoff / 60;
+		*tzindex = timezone_index(zone);
+		assert(l <= (ssize_t)len);
+		return l;
+	}
+
+	/* 2nd attempt: decode as +03:00 */
+	*tzindex = 0;
+	l = dt_parse_iso_zone_lenient(str, len, offset);
+	assert(l <= (ssize_t)len);
+
+	return l;
+}
+
 ssize_t
 datetime_parse_full(struct datetime *date, const char *str, size_t len,
-		    int32_t offset)
+		    const char *tzsuffix, int32_t offset)
 {
 	size_t n;
 	dt_t dt;
@@ -260,6 +290,19 @@ datetime_parse_full(struct datetime *date, const char *str, size_t len,
 	if (len <= 0)
 		goto exit;
 
+	/* now we have parsed enough of date literal, and we are
+	 * ready to consume timezone suffix, if overridden
+	 */
+	time_t base = dt_epoch(dt) + sec_of_day - offset * 60;
+	ssize_t l;
+	if (tzsuffix != NULL) {
+		l = parse_tz_suffix(tzsuffix, strlen(tzsuffix), base,
+				    &tzindex, &offset);
+		if (l < 0)
+			return l;
+		goto exit;
+	}
+
 	if (*str == ' ') {
 		str++;
 		len--;
@@ -267,30 +310,13 @@ datetime_parse_full(struct datetime *date, const char *str, size_t len,
 	if (len <= 0)
 		goto exit;
 
-	/* 1st attempt: decode as MSK */
-	const struct date_time_zone *zone;
-	ssize_t l = timezone_lookup(str, len, &zone);
+	l = parse_tz_suffix(str, len, base, &tzindex, &offset);
 	if (l < 0)
 		return l;
-	if (l > 0) {
-		assert(zone != NULL);
-		assert(((TZ_AMBIGUOUS | TZ_NYI) & timezone_flags(zone)) == 0);
-		offset = timezone_offset(zone);
-		tzindex = timezone_index(zone);
-		str += l;
-		len -= l;
-		if (len <= 0)
-			goto exit;
-	}
-
-	/* 2nd attempt: decode as +03:00 */
-	n = dt_parse_iso_zone_lenient(str, len, &offset);
-	str += n;
+	str += l;
 
 exit:
-	date->epoch =
-		((int64_t)dt_rdn(dt) - DT_EPOCH_1970_OFFSET) * SECS_PER_DAY +
-		sec_of_day - offset * 60;
+	date->epoch = dt_epoch(dt) + sec_of_day - offset * 60;
 	date->nsec = nanosecond;
 	date->tzoffset = offset;
 	date->tzindex = tzindex;
@@ -299,19 +325,15 @@ datetime_parse_full(struct datetime *date, const char *str, size_t len,
 }
 
 ssize_t
-datetime_parse_tz(const char *str, size_t len, int16_t *tzoffset,
+datetime_parse_tz(const char *str, size_t len, time_t base, int16_t *tzoffset,
 		  int16_t *tzindex)
 {
-	const struct date_time_zone *zone;
-	ssize_t l = timezone_lookup(str, len, &zone);
+	int32_t offset = 0;
+	ssize_t l = parse_tz_suffix(str, len, base, tzindex, &offset);
 	if (l <= 0)
 		return l;
-
-	assert(l <= (ssize_t)len);
-	assert(zone != NULL);
-	assert(((TZ_AMBIGUOUS | TZ_NYI) & timezone_flags(zone)) == 0);
-	*tzoffset = timezone_offset(zone);
-	*tzindex = timezone_index(zone);
+	assert(offset <= INT16_MAX);
+	*tzoffset = offset;
 	return l;
 }
 
diff --git a/src/lib/core/datetime.h b/src/lib/core/datetime.h
index 9b7a8a087d010c879f1fbf28b090a20643e12064..ca5dbee1b44b92d56671814fdb1269e719ca94d2 100644
--- a/src/lib/core/datetime.h
+++ b/src/lib/core/datetime.h
@@ -33,7 +33,7 @@ struct tnt_tm;
 #endif
 
 /** Required size of datetime_to_string string buffer */
-#define DT_TO_STRING_BUFSIZE 48
+#define DT_TO_STRING_BUFSIZE 64
 
 /**
  * Required buffer size to store the string representation of any possible
@@ -158,15 +158,22 @@ datetime_ev_now(struct datetime *now);
 void
 datetime_to_tm(const struct datetime *date, struct tnt_tm *tm);
 
+/**
+ * Create @sa datetime structure using given @sa tnt_tm fields.
+ */
+bool
+tm_to_datetime(struct tnt_tm *tm, struct datetime *date);
+
 /**
  * Parse datetime text in ISO-8601 given format, and construct output
  * datetime value
  * @param date output datetime value
  * @param str input text in relaxed ISO-8601 format (0-terminated)
  * @param len length of str buffer
- * @param offset timezone offset, if you need to interpret
- *        date/time value as local. Pass 0 if you need to interpret it as UTC,
- *        or apply offset from string.
+ * @param tzsuffix timezone override, if you need to interpret
+ *        date/time value as local. Pass NULL if you need to interpret it
+ *        as UTC, or you provide meaningful suffix in the literal to be
+ *        parsed.
  * @retval Upon successful completion returns length of accepted
  *         prefix substring. It's ok if there is some unaccepted trailer.
  *         Returns 0 only if text is not recognizable as date/time string.
@@ -175,7 +182,7 @@ datetime_to_tm(const struct datetime *date, struct tnt_tm *tm);
  */
 ssize_t
 datetime_parse_full(struct datetime *date, const char *str, size_t len,
-		    int32_t offset);
+		    const char *tzsuffix, int32_t offset);
 
 /** Parse timezone suffix
  * @param str input text in relaxed ISO-8601 format (0-terminated)
@@ -188,7 +195,7 @@ datetime_parse_full(struct datetime *date, const char *str, size_t len,
  * @sa datetime_parse_full()
  */
 ssize_t
-datetime_parse_tz(const char *str, size_t len, int16_t *tzoffset,
+datetime_parse_tz(const char *str, size_t len, time_t base, int16_t *tzoffset,
 		  int16_t *tzindex);
 
 /**
diff --git a/src/lib/tzcode/localtime.c b/src/lib/tzcode/localtime.c
index 7f66f449b4d8c16fa5bdb486c910435a637f18cb..7e654d812ea5affe2d9526973fb40f0c18ed92b1 100644
--- a/src/lib/tzcode/localtime.c
+++ b/src/lib/tzcode/localtime.c
@@ -1507,7 +1507,7 @@ localsub(struct state const *sp, time_t const *timep, int_fast32_t setname,
 #if NETBSD_INSPIRED
 
 struct tnt_tm *
-localtime_rz(struct state *sp, time_t const *timep, struct tnt_tm *tmp)
+tnt_localtime_rz(struct state *sp, time_t const *timep, struct tnt_tm *tmp)
 {
 	return localsub(sp, timep, 0, tmp);
 }
diff --git a/src/lib/tzcode/strptime.c b/src/lib/tzcode/strptime.c
index ae9ec6a7f4d2d68e95e448bd7eef8379b534560c..a8ac38231f12e054bd81ba5117d37ef729f8588d 100644
--- a/src/lib/tzcode/strptime.c
+++ b/src/lib/tzcode/strptime.c
@@ -551,13 +551,10 @@ tnt_strptime(const char *__restrict buf, const char *__restrict fmt,
 				zonestr[cp - buf] = '\0';
 
 				const struct date_time_zone *zone;
-				size_t n = timezone_lookup(zonestr, cp - buf,
-							   &zone);
+				size_t n = timezone_tm_lookup(zonestr, cp - buf,
+							      &zone, tm);
 				if (n <= 0)
 					return NULL;
-				tm->tm_gmtoff = timezone_offset(zone);
-				tm->tm_tzindex = timezone_index(zone);
-				tm->tm_isdst = false;
 
 				buf += cp - buf;
 			}
diff --git a/src/lib/tzcode/timezone.c b/src/lib/tzcode/timezone.c
index 94b73b63faf40928c075ed8402b4a2289619c233..c4cd0a5a2dc8e90e580cce4f45076ad608a8e801 100644
--- a/src/lib/tzcode/timezone.c
+++ b/src/lib/tzcode/timezone.c
@@ -8,8 +8,59 @@
 #include <stddef.h>
 #include "datetime.h"
 #include "timezone.h"
+#include "tzcode.h"
 #include "trivia/util.h"
 
+/**
+ * Array of zone descriptors, placed in their natural id order, used for
+ * translation from zone id to unique zone name and their attributes.
+ */
+static struct date_time_zone zones_raw[] = {
+#define ZONE_ABBREV(id, offset, name, flags) {name, id, flags, offset},
+#define ZONE_UNIQUE(id, name) {name, id, TZ_OLSON, 0},
+#define ZONE_ALIAS(id, alias, name) {alias, id, TZ_OLSON|TZ_ALIAS, 0},
+#include "timezones.h"
+#undef ZONE_ALIAS
+#undef ZONE_UNIQUE
+#undef ZONE_ABBREV
+};
+
+#define ZONES_MAX 1024
+static_assert(ZONES_MAX >= lengthof(zones_raw), "please increase ZONES_MAX");
+
+/**
+ * Sorted array of zone descriptors, whether it's abbreviation, full
+ * zone name or [backward-compatible] link name.
+ */
+static struct date_time_zone zones_sorted[lengthof(zones_raw)];
+static struct date_time_zone zones_unsorted[ZONES_MAX];
+
+static int
+compare_zones(const void *a, const void *b)
+{
+	return strcasecmp(((const struct date_time_zone *) a)->name,
+			  ((const struct date_time_zone*) b)->name);
+}
+
+static void __attribute__((constructor))
+sort_array(void)
+{
+	size_t i;
+	/* 1st save zones in id order for stringization */
+	for (i = 0; i < lengthof(zones_raw); i++) {
+		size_t id = zones_raw[i].id;
+		assert(id < lengthof(zones_unsorted));
+		/* save to unsorted array if it's not an alias */
+		if ((zones_raw[i].flags & TZ_ALIAS) == 0)
+			zones_unsorted[id] = zones_raw[i];
+	}
+	/* 2nd copy raw to be sorted and prepare them for bsearch */
+	assert(sizeof(zones_sorted) == sizeof(zones_raw));
+	memcpy(zones_sorted, zones_raw, sizeof(zones_raw));
+	qsort(zones_sorted, lengthof(zones_sorted), sizeof(zones_sorted[0]),
+	      compare_zones);
+}
+
 int16_t
 timezone_offset(const struct date_time_zone *zone)
 {
@@ -28,52 +79,11 @@ timezone_flags(const struct date_time_zone *zone)
 	return zone->flags;
 }
 
-static const char * zone_names[] = {
-#define ZONE_ABBREV(id, offset, name, flags) [id] = name,
-#define ZONE_UNIQUE(id, name) [id] = name,
-#define ZONE_ALIAS(id, alias, name)
-#include "timezones.h"
-#undef ZONE_ALIAS
-#undef ZONE_UNIQUE
-#undef ZONE_ABBREV
-};
-
 const char*
 timezone_name(int64_t index)
 {
-	assert((size_t)index < lengthof(zone_names));
-	return zone_names[index];
-}
-
-static struct date_time_zone zone_abbrevs[] = {
-#define ZONE_ABBREV(id, offset, name, flags) { name, id, flags, offset },
-#define ZONE_UNIQUE(id, name) { name, id, TZ_OLSON, 0 },
-#define ZONE_ALIAS(id, alias, name) { alias, id, TZ_OLSON|TZ_ALIAS, 0 },
-#include "timezones.h"
-#undef ZONE_ALIAS
-#undef ZONE_UNIQUE
-#undef ZONE_ABBREV
-};
-
-static int
-compare_abbrevs(const void *a, const void *b)
-{
-	return strcasecmp(((const struct date_time_zone *) a)->name,
-			  ((const struct date_time_zone*) b)->name);
-}
-
-static inline struct date_time_zone *
-sort_abbrevs_singleton(void)
-{
-	static struct date_time_zone *sorted = NULL;
-	if (sorted != NULL)
-		return sorted;
-
-	qsort(zone_abbrevs, lengthof(zone_abbrevs),
-	      sizeof(struct date_time_zone), compare_abbrevs);
-	sorted = zone_abbrevs;
-
-	return sorted;
+	assert((size_t)index < lengthof(zones_unsorted));
+	return zones_unsorted[index].name;
 }
 
 /**
@@ -99,18 +109,18 @@ char_span_alpha(const char *src, size_t len)
 	return n;
 }
 
-ssize_t
-timezone_lookup(const char *str, size_t len, const struct date_time_zone **zone)
+static inline ssize_t
+timezone_raw_lookup(const char *str, size_t len,
+		    const struct date_time_zone **zone)
 {
 	len = char_span_alpha(str, len);
 	if (len == 0)
 		return 0;
 
-	struct date_time_zone *sorted = sort_abbrevs_singleton();
 	struct date_time_zone wrap = {.name = str};
 	struct date_time_zone *found = (struct date_time_zone *)
-		bsearch(&wrap, sorted, lengthof(zone_abbrevs),
-			sizeof(struct date_time_zone), compare_abbrevs);
+		bsearch(&wrap, zones_sorted, lengthof(zones_sorted),
+			sizeof(zones_sorted[0]), compare_zones);
 
 	if (found != NULL) {
 		/* lua assumes that single bit is set, not both */
@@ -123,3 +133,69 @@ timezone_lookup(const char *str, size_t len, const struct date_time_zone **zone)
 	}
 	return -1;
 }
+
+ssize_t
+timezone_tm_lookup(const char *str, size_t len,
+		   const struct date_time_zone **zone,
+		   struct tnt_tm *tm)
+{
+	ssize_t rc = timezone_raw_lookup(str, len, zone);
+	if (rc <= 0)
+		return rc;
+
+	const struct date_time_zone *found = *zone;
+	if ((found->flags & TZ_OLSON) == 0) {
+		tm->tm_gmtoff = found->offset * 60;
+		tm->tm_tzindex = found->id;
+		tm->tm_isdst = false;
+		return rc;
+	}
+	timezone_t tz = tzalloc(str); // FIXME - cache loaded
+	if (tz == NULL)
+		return 0;
+	struct datetime date = {.epoch = 0};
+	if (tm_to_datetime(tm, &date) == false)
+		goto exit_0;
+	time_t epoch = (int64_t)date.epoch;
+	struct tnt_tm * result = tnt_localtime_rz(tz, &epoch, tm);
+	if (result == NULL)
+		goto exit_0;
+	tzfree(tz);
+	return rc;
+exit_0:
+	if (tz != NULL)
+		tzfree(tz);
+	return 0;
+}
+
+ssize_t
+timezone_epoch_lookup(const char *str, size_t len, time_t base,
+		      const struct date_time_zone **zone,
+		      long *gmtoff)
+{
+	ssize_t rc = timezone_raw_lookup(str, len, zone);
+	if (rc <= 0)
+		return rc;
+
+	const struct date_time_zone *found = *zone;
+	if ((found->flags & TZ_OLSON) == 0) {
+		assert((TZ_ERROR_MASK & found->flags) == 0);
+		*gmtoff = found->offset * 60;
+		return rc;
+	}
+	timezone_t tz = NULL;
+	tz = tzalloc(str); // FIXME - cache loaded
+	if (tz == NULL)
+		return 0;
+	struct tnt_tm tm = {.tm_epoch = 0};
+	struct tnt_tm * result = tnt_localtime_rz(tz, &base, &tm);
+	if (result == NULL)
+		goto exit_0;
+	*gmtoff = result->tm_gmtoff;
+	tzfree(tz);
+	return rc;
+exit_0:
+	if (tz != NULL)
+		tzfree(tz);
+	return 0;
+}
diff --git a/src/lib/tzcode/timezone.h b/src/lib/tzcode/timezone.h
index b2b984e3523efa871d209f7a206c94ae65dc6df3..b8eaf8e3d9557d1429847b11eba1f536449e0327 100644
--- a/src/lib/tzcode/timezone.h
+++ b/src/lib/tzcode/timezone.h
@@ -11,6 +11,8 @@
 extern "C" {
 #endif
 
+struct tnt_tm;
+
 enum {
 	TZ_UTC = 0x01,
 	TZ_RFC = 0x02,
@@ -19,6 +21,8 @@ enum {
 	TZ_NYI = 0x10,
 	TZ_OLSON = 0x20,
 	TZ_ALIAS = 0x40,
+
+	TZ_ERROR_MASK = TZ_AMBIGUOUS | TZ_NYI,
 };
 
 /**
@@ -40,14 +44,29 @@ struct date_time_zone {
  * with attributes of this symbol
  * @param[in] s input string to parse
  * @param[in] len length of input string
- * @param[out] zone return zone structure, if found
+ * @param[in] base use base date for timezone parameters resolution
+ * @param[out] zone return found zone structure
+ * @param[out] gmtoff return gmtoff for the found zone
  * @retval positive value - length of accepted string,
  *         negative value - string looks legit, but is unknown or
  *         unsupported at the moment and should generate exception,
  *         0 - means string is bogus, and should be ignored.
  */
 ssize_t
-timezone_lookup(const char *s, size_t len, const struct date_time_zone **zone);
+timezone_epoch_lookup(const char *s, size_t len, time_t base,
+		      const struct date_time_zone **zone,
+		      long *gmtoff);
+
+/**
+ * Parse timezone string like @sa timezone_epoch_lookup, but use
+ * @struct tnt_tm for passing base date and returning resolved
+ * tm_gmtoff or tm_isdst.
+ */
+ssize_t
+timezone_tm_lookup(const char *str, size_t len,
+		   const struct date_time_zone **zone,
+		   struct tnt_tm *tm);
+
 /** Return offset in minutes for given zone */
 int16_t
 timezone_offset(const struct date_time_zone *zone);
diff --git a/src/lib/tzcode/tzcode.h b/src/lib/tzcode/tzcode.h
index 8e752c6325414cc7b6b87a337604ab31f156ee9d..6a9c3146d405de48a41909eed567b8b3d7b6d92b 100644
--- a/src/lib/tzcode/tzcode.h
+++ b/src/lib/tzcode/tzcode.h
@@ -73,6 +73,9 @@ timezone_t tzalloc(const char *);
 /** Free loaded timezone definition */
 void tzfree(timezone_t);
 
+struct tnt_tm *
+tnt_localtime_rz(struct state *sp, time_t const *timep, struct tnt_tm *tmp);
+
 #if defined(__cplusplus)
 } /* extern "C" */
 #endif /* defined(__cplusplus) */
diff --git a/src/lua/datetime.lua b/src/lua/datetime.lua
index ae10b56cf6a5a8957aeb0b63b05092d15cd51a82..f5c53932de39c5107b90442526490c96be59f89b 100644
--- a/src/lua/datetime.lua
+++ b/src/lua/datetime.lua
@@ -68,9 +68,10 @@ size_t tnt_datetime_to_string(const struct datetime * date, char *buf,
 size_t tnt_datetime_strftime(const struct datetime *date, char *buf,
                              uint32_t len, const char *fmt);
 ssize_t tnt_datetime_parse_full(struct datetime *date, const char *str,
-                                size_t len, int32_t offset);
-ssize_t tnt_datetime_parse_tz(const char *str, size_t len, int16_t *tzoffset,
-                              int16_t *tzindex);
+                                size_t len, const char *tzsuffix,
+                                int32_t offset);
+ssize_t tnt_datetime_parse_tz(const char *str, size_t len, time_t base,
+                              int16_t *tzoffset, int16_t *tzindex);
 size_t tnt_datetime_strptime(struct datetime *date, const char *buf,
                              const char *fmt);
 void   tnt_datetime_now(struct datetime *now);
@@ -109,7 +110,7 @@ local math_floor = math.floor
 local DAYS_EPOCH_OFFSET = 719163
 local SECS_PER_DAY      = 86400
 local SECS_EPOCH_OFFSET = DAYS_EPOCH_OFFSET * SECS_PER_DAY
-local TOSTRING_BUFSIZE  = 48
+local TOSTRING_BUFSIZE  = 64
 local IVAL_TOSTRING_BUFSIZE = 96
 local STRFTIME_BUFSIZE  = 128
 
@@ -441,16 +442,20 @@ local function parse_tzoffset(str)
     return offset[0]
 end
 
+local function epoch_from_dt(dt)
+    return (dt - DAYS_EPOCH_OFFSET) * SECS_PER_DAY
+end
+
 --[[
     Parse timezone name similar way as datetime_parse_full parse
     full literal.
 ]]
-local function parse_tzname(tzname)
+local function parse_tzname(base_epoch, tzname)
     check_str(tzname, 'parse_tzname()')
     local ptzindex = date_int16_stash_take()
     local ptzoffset = date_int16_stash_take()
-    local len = builtin.tnt_datetime_parse_tz(tzname, #tzname, ptzoffset,
-                                              ptzindex)
+    local len = builtin.tnt_datetime_parse_tz(tzname, #tzname, base_epoch,
+                                              ptzoffset, ptzindex)
     if len > 0 then
         local tzoffset, tzindex = ptzoffset[0], ptzindex[0]
         date_int16_stash_put(ptzoffset)
@@ -486,8 +491,7 @@ local function datetime_new_dt(dt, secs, nanosecs, offset, tzindex)
     nanosecs = nanosecs or 0
     offset = offset or 0
     tzindex = tzindex or 0
-    local epoch = (dt - DAYS_EPOCH_OFFSET) * SECS_PER_DAY
-    return datetime_new_raw(epoch + secs - offset * 60, nanosecs,
+    return datetime_new_raw(epoch_from_dt(dt) + secs - offset * 60, nanosecs,
                             offset, tzindex)
 end
 
@@ -593,12 +597,6 @@ local function datetime_new(obj)
         check_range(offset, -720, 840, 'tzoffset')
     end
 
-    local tzindex = 0
-    local tzname = obj.tz
-    if tzname ~= nil then
-        offset, tzindex = parse_tzname(tzname)
-    end
-
     -- .year, .month, .day
     if ymd then
         y = y or 1970
@@ -616,6 +614,12 @@ local function datetime_new(obj)
         dt = dt_from_ymd_checked(y, M, d)
     end
 
+    local tzindex = 0
+    local tzname = obj.tz
+    if tzname ~= nil then
+        offset, tzindex = parse_tzname(epoch_from_dt(dt), tzname)
+    end
+
     -- .hour, .minute, .second
     local secs = 0
     if hms then
@@ -841,12 +845,11 @@ end
        date [T] time [ ] time_zone
     Returns constructed datetime object and length of accepted string.
 ]]
-local function datetime_parse_full(str, tzoffset, tzindex)
+local function datetime_parse_full(str, tzname, offset)
     check_str(str, 'datetime.parse()')
     local date = ffi.new(datetime_t)
-    local len = builtin.tnt_datetime_parse_full(date, str, #str, tzoffset)
+    local len = builtin.tnt_datetime_parse_full(date, str, #str, tzname, offset)
     if len > 0 then
-        date.tzindex = tzindex and tzindex or date.tzindex
         return date, tonumber(len)
     elseif len == -builtin.TZ_NYI then
         error(("could not parse '%s' - nyi timezone"):format(str))
@@ -873,26 +876,30 @@ end
 local function datetime_parse_from(str, obj)
     check_str(str, 'datetime.parse()')
     local fmt = ''
-    local offset
-    local tzindex
+    local tzoffset
+    local tzname
 
     if obj ~= nil then
         check_table(obj, 'datetime.parse()')
         fmt = obj.format
-        offset = obj.tzoffset
+        tzoffset = obj.tzoffset
+        tzname = obj.tz
     end
     check_str_or_nil(fmt, 'datetime.parse()')
 
-    if offset ~= nil then
-        offset = get_timezone(offset, 'tzoffset')
+    local offset = 0
+    if tzoffset ~= nil then
+        offset = get_timezone(tzoffset, 'tzoffset')
         check_range(offset, -720, 840, 'tzoffset')
     end
-    if obj and obj.tz ~= nil then
-        offset, tzindex = parse_tzname(obj.tz)
+
+    if tzname ~= nil then
+        check_str(tzname, 'datetime.parse()')
     end
 
     if not fmt or fmt == '' or fmt == 'iso8601' or fmt == 'rfc3339' then
-        return datetime_parse_full(str, offset or 0, tzindex)
+        -- Effect of .tz overrides .tzoffset
+        return datetime_parse_full(str, tzname, offset)
     else
         return datetime_parse_format(str, fmt)
     end
@@ -1057,9 +1064,6 @@ local function datetime_set(self, obj)
     offset = offset or self.tzoffset
 
     local tzname = obj.tz
-    if tzname ~= nil then
-        offset, self.tzindex = parse_tzname(tzname)
-    end
 
     local ts = obj.timestamp
     if ts ~= nil then
@@ -1081,6 +1085,9 @@ local function datetime_set(self, obj)
                   'if nsec, usec, or msecs provided', 2)
         end
 
+        if tzname ~= nil then
+            offset, self.tzindex = parse_tzname(sec_int, tzname)
+        end
         self.epoch = utc_secs(sec_int, offset)
         self.nsec = nsec
         self.tzoffset = offset
@@ -1099,6 +1106,10 @@ local function datetime_set(self, obj)
         datetime_ymd_update(self, y, M, d)
     end
 
+    if tzname ~= nil then
+        offset, self.tzindex = parse_tzname(self.epoch, tzname)
+    end
+
     -- .hour, .minute, .second
     if hms then
         datetime_hms_update(self, h or h0, m or m0, sec or sec0)
diff --git a/src/lua/tnt_datetime.c b/src/lua/tnt_datetime.c
index 2b0e32702907febb8d10f11a17006a758f5b1480..f243d8e3c5ee62764523e39e4f64741c78017085 100644
--- a/src/lua/tnt_datetime.c
+++ b/src/lua/tnt_datetime.c
@@ -35,16 +35,16 @@ tnt_datetime_to_string(const struct datetime *date, char *buf, ssize_t len)
 
 ssize_t
 tnt_datetime_parse_full(struct datetime *date, const char *str, size_t len,
-			int32_t offset)
+			const char *tzsuffix, int32_t offset)
 {
-	return datetime_parse_full(date, str, len, offset);
+	return datetime_parse_full(date, str, len, tzsuffix, offset);
 }
 
 ssize_t
-tnt_datetime_parse_tz(const char *str, size_t len, int16_t *tzoffset,
-		      int16_t *tzindex)
+tnt_datetime_parse_tz(const char *str, size_t len, time_t base_date,
+		      int16_t *tzoffset, int16_t *tzindex)
 {
-	return datetime_parse_tz(str, len, tzoffset, tzindex);
+	return datetime_parse_tz(str, len, base_date, tzoffset, tzindex);
 }
 
 struct datetime *
diff --git a/test/app-tap/datetime.test.lua b/test/app-tap/datetime.test.lua
index 48e4e8f7dd3b56dc8c909b929c7d6f9c4e5f301f..08988cf84c126adafc84f8683bf0e3bf7ef7540b 100755
--- a/test/app-tap/datetime.test.lua
+++ b/test/app-tap/datetime.test.lua
@@ -552,7 +552,7 @@ test:test("Check parsing of dates with invalid attributes", function(test)
 end)
 
 test:test("Parsing of timezone abbrevs", function(test)
-    test:plan(195)
+    test:plan(220)
     local zone_abbrevs = {
         -- military
         A =   1*60, B =   2*60, C =   3*60,
@@ -574,6 +574,13 @@ test:test("Parsing of timezone abbrevs", function(test)
         AMDT = 5 * 60,  BDST = 1 * 60,  IRKT = 8 * 60,
         KST = 9 * 60,   PDT = -7 * 60,  WET = 0 * 60,
         HOVDST = 8 * 60, CHODST = 9 * 60,
+
+        -- Olson
+        ['Europe/Moscow'] = 180,
+        ['Africa/Abidjan'] = 0,
+        ['America/Argentina/Buenos_Aires'] = -180,
+        ['Asia/Krasnoyarsk'] = 420,
+        ['Pacific/Fiji'] = 720,
     }
     local exp_pattern = '^2020%-02%-10T00:00'
     local base_date = '2020-02-10T0000 '
@@ -590,7 +597,7 @@ test:test("Parsing of timezone abbrevs", function(test)
 end)
 
 test:test("Parsing of timezone names (tzindex)", function(test)
-    test:plan(273)
+    test:plan(308)
     local zone_abbrevs = {
         -- military
         A =  1, B =  2, C =  3,
@@ -613,6 +620,13 @@ test:test("Parsing of timezone names (tzindex)", function(test)
         AMDT = 336,  BDST = 344, IRKT = 409,
         KST = 226,   PDT = 264,  WET = 314,
         HOVDST = 664, CHODST = 656,
+
+        -- Olson
+        ['Europe/Moscow'] = 947,
+        ['Africa/Abidjan'] = 672,
+        ['America/Argentina/Buenos_Aires'] = 694,
+        ['Asia/Krasnoyarsk'] = 861,
+        ['Pacific/Fiji'] = 984,
     }
     local exp_pattern = '^2020%-02%-10T00:00'
     local base_date = '2020-02-10T0000 '
@@ -620,6 +634,7 @@ test:test("Parsing of timezone names (tzindex)", function(test)
     for zone, index in pairs(zone_abbrevs) do
         local date_text = base_date .. zone
         local date, len = date.parse(date_text)
+        print(zone, index)
         test:isnt(date, nil, 'parse ' .. zone)
         test:is(date.tzindex, index, 'expected tzindex')
         test:is(date.tz, zone, 'expected timezone name')
@@ -636,27 +651,18 @@ local function error_ambiguous(s)
     return ("could not parse '%s' - ambiguous timezone"):format(s)
 end
 
-local function error_nyi(s)
-    return ("could not parse '%s' - nyi timezone"):format(s)
-end
-
 local function error_generic(s)
     return ("could not parse '%s'"):format(s)
 end
 
 test:test("Parsing of timezone names (errors)", function(test)
-    test:plan(13)
+    test:plan(9)
     local zones_arratic = {
         -- ambiguous
         AT = error_ambiguous, BT = error_ambiguous,
         ACT = error_ambiguous, BST = error_ambiguous,
         GST = error_ambiguous, WAT = error_ambiguous,
         AZOST = error_ambiguous,
-        -- not yet implemented
-        ['Africa/Abidjan'] = error_nyi,
-        ['America/Argentina/Buenos_Aires'] = error_nyi,
-        ['Asia/Krasnoyarsk'] = error_nyi,
-        ['Pacific/Fiji'] = error_nyi,
         -- generic errors
         ['XXX'] = error_generic,
         ['A-_'] = error_generic,
diff --git a/test/fuzz/datetime_parse_full_fuzzer.c b/test/fuzz/datetime_parse_full_fuzzer.c
index 0c6bbd7006e6a7f0e55894c9d93930d136109db3..de4d7a61f7c818e6de1b15afc5b176531ac5c468 100644
--- a/test/fuzz/datetime_parse_full_fuzzer.c
+++ b/test/fuzz/datetime_parse_full_fuzzer.c
@@ -15,7 +15,7 @@ LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
 	memcpy(buf, data, size);
 	buf[size] = '\0';
 	struct datetime date_expected;
-	datetime_parse_full(&date_expected, buf, size, 0);
+	datetime_parse_full(&date_expected, buf, size, NULL, 0);
 	free(buf);
 
 	return 0;
diff --git a/test/unit/datetime.c b/test/unit/datetime.c
index 684f8ef71b518e61fd8d51703a763b6ee9e7c4a7..1dd7c59ac95fe6ef36448ed87d24943076ad5922 100644
--- a/test/unit/datetime.c
+++ b/test/unit/datetime.c
@@ -106,12 +106,13 @@ datetime_test(void)
 	struct datetime date_expected;
 
 	plan(497);
-	datetime_parse_full(&date_expected, sample, sizeof(sample) - 1, 0);
+	datetime_parse_full(&date_expected, sample, sizeof(sample) - 1,
+			    NULL, 0);
 
 	for (index = 0; index < lengthof(tests); index++) {
 		struct datetime date;
 		size_t len = datetime_parse_full(&date, tests[index].str,
-						 tests[index].len, 0);
+						 tests[index].len, NULL, 0);
 		is(len > 0, true, "correct parse_datetime return value "
 		   "for '%s'", tests[index].str);
 		is(date.epoch, date_expected.epoch,
@@ -133,7 +134,7 @@ datetime_test(void)
 		is(date.epoch, date_strp.epoch,
 		   "reversible seconds via datetime_strptime for '%s'", buff);
 		struct datetime date_parsed;
-		len = datetime_parse_full(&date_parsed, buff, len, 0);
+		len = datetime_parse_full(&date_parsed, buff, len, NULL, 0);
 		is(len > 0, true, "correct datetime_parse_full return value "
 		   "for '%s'", buff);
 		is(date.epoch, date_parsed.epoch,