[Tarantool-patches] [PATCH v6 1/5] build, lua: built-in module datetime

Timur Safin tsafin at tarantool.org
Thu Aug 19 05:56:29 MSK 2021


New third_party module - c-dt
-----------------------------

* Integrated chansen/c-dt parser as 3rd party module to the
  Tarantool cmake build process;
* Points to tarantool/c-dt instead of original chansen/c-dt to
  have easier build integration, because there are additional
  commits which have integrated cmake support and have established
  symbols renaming facilities (similar to those we see in xxhash
  or icu).
* took special care that generated build artifacts not override
  in-source files, but use different build/ directory.

* added datetime parsing unit tests:
  - for literals - with and without trailing timezones;
  - we check that strftime is reversible and produce consistent
    results after roundtrip from/to strings;

New built-in module `datetime`
------------------------------

* created a new Tarantool built-in module `datetime`;
* register new cdef types for this module;
* reexported some `dt_*` functions from `c-dt` library.
  We have to rename externally public symbols to avoid
  name clashes possible usage of this same party module but
  from inside of dynamic libraries. We prefix c-dt symbols
  in the Tarantool build with `tnt_` prefix;

* `strftime` implemented as a simple ffi wrappers in C
  kernel code:

We used to use heavy ffi-related Lua code, but it appears
to be quite fragile, if we run this code on different OS
targets. To make this Lua code less platfrom dependent we
have moved all platform specifics to the C level.
Specifically we want to avoid dependence on `struct tm {}`
layout and GLIBC-specific `strftime` behaviour (which
differs to BSD/Mac LIBC behaviour).

* display datetime

  - introduced output routine for converting datetime
    to their default output format.

  - use this routine for tostring() in datetime.lua

- verify that we parse expected, and fail with unexpected:
  - for that, in the datetime.lua, we have extended api of `parse_date()`,
    `parse_time()`, and `parse_time_zone()` so they return
    not only parsed object, but also a length of parsed substring;
  - which allows us to parse even _partially_ valid strings like
    "20121224 Foo bar".

* simplified interfaces
  - totable() export table values similar to os.date('*t')
  - set() provide unified interface to set values using
    the same set of attributes as in totable()

Part of #5941

@TarantoolBot document
Title: Introduced new built-in `datetime` module

`datetime` module has been introduced, which allows to parse
ISO-8601 literals representing timestamps of various formats,
and then manipulate with date objects.

Please refer to https://github.com/tarantool/tarantool/discussions/6244
for more detailed description of module API.

Part of #5941
---
 .gitmodules                    |   3 +
 CMakeLists.txt                 |   8 +
 cmake/BuildCDT.cmake           |  10 +
 extra/exports                  |  32 ++
 src/CMakeLists.txt             |   5 +-
 src/lib/core/CMakeLists.txt    |   1 +
 src/lib/core/datetime.c        | 125 ++++++++
 src/lib/core/datetime.h        |  86 +++++
 src/lua/datetime.lua           | 567 +++++++++++++++++++++++++++++++++
 src/lua/init.c                 |   4 +-
 src/lua/utils.c                |  27 ++
 src/lua/utils.h                |  12 +
 test/app-tap/datetime.test.lua | 213 +++++++++++++
 test/unit/CMakeLists.txt       |   3 +-
 test/unit/datetime.c           | 260 +++++++++++++++
 test/unit/datetime.result      | 358 +++++++++++++++++++++
 third_party/c-dt               |   1 +
 17 files changed, 1712 insertions(+), 3 deletions(-)
 create mode 100644 cmake/BuildCDT.cmake
 create mode 100644 src/lib/core/datetime.c
 create mode 100644 src/lib/core/datetime.h
 create mode 100644 src/lua/datetime.lua
 create mode 100755 test/app-tap/datetime.test.lua
 create mode 100644 test/unit/datetime.c
 create mode 100644 test/unit/datetime.result
 create mode 160000 third_party/c-dt

diff --git a/.gitmodules b/.gitmodules
index f2f91ee72..aa3fbae4e 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -43,3 +43,6 @@
 [submodule "third_party/xxHash"]
 	path = third_party/xxHash
 	url = https://github.com/tarantool/xxHash
+[submodule "third_party/c-dt"]
+	path = third_party/c-dt
+	url = https://github.com/tarantool/c-dt.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e25b81eac..8037c30a7 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -571,6 +571,14 @@ endif()
 # zstd
 #
 
+#
+# Christian Hansen c-dt
+#
+
+include(BuildCDT)
+libccdt_build()
+add_dependencies(build_bundled_libs cdt)
+
 #
 # Third-Party misc
 #
diff --git a/cmake/BuildCDT.cmake b/cmake/BuildCDT.cmake
new file mode 100644
index 000000000..80b26c64a
--- /dev/null
+++ b/cmake/BuildCDT.cmake
@@ -0,0 +1,10 @@
+macro(libccdt_build)
+    set(LIBCDT_INCLUDE_DIRS ${PROJECT_SOURCE_DIR}/third_party/c-dt/)
+    set(LIBCDT_LIBRARIES cdt)
+
+    file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/third_party/c-dt/build/)
+    add_subdirectory(${PROJECT_SOURCE_DIR}/third_party/c-dt
+                     ${CMAKE_CURRENT_BINARY_DIR}/third_party/c-dt/build/)
+    set_target_properties(cdt PROPERTIES COMPILE_FLAGS "-DDT_NAMESPACE=tnt_")
+    add_definitions("-DDT_NAMESPACE=tnt_")
+endmacro()
diff --git a/extra/exports b/extra/exports
index 9eaba1282..0e7392d61 100644
--- a/extra/exports
+++ b/extra/exports
@@ -148,8 +148,16 @@ csv_feed
 csv_iterator_create
 csv_next
 csv_setopt
+datetime_now
+datetime_strftime
+datetime_to_string
+datetime_unpack
 decimal_from_string
 decimal_unpack
+dt_year
+dt_month
+dt_doy
+dt_dom
 error_ref
 error_set_prev
 error_unref
@@ -447,6 +455,30 @@ title_set_status
 title_update
 tnt_default_cert_dir_paths
 tnt_default_cert_file_paths
+tnt_dt_add_years
+tnt_dt_add_quarters
+tnt_dt_add_months
+tnt_dt_dow
+tnt_dt_from_rdn
+tnt_dt_from_struct_tm
+tnt_dt_from_yd
+tnt_dt_from_ymd
+tnt_dt_from_yqd
+tnt_dt_from_ywd
+tnt_dt_parse_iso_date
+tnt_dt_parse_iso_time_basic
+tnt_dt_parse_iso_time_extended
+tnt_dt_parse_iso_time
+tnt_dt_parse_iso_zone_basic
+tnt_dt_parse_iso_zone_extended
+tnt_dt_parse_iso_zone_lenient
+tnt_dt_parse_iso_zone
+tnt_dt_rdn
+tnt_dt_to_struct_tm
+tnt_dt_to_yd
+tnt_dt_to_ymd
+tnt_dt_to_yqd
+tnt_dt_to_ywd
 tnt_iconv
 tnt_iconv_close
 tnt_iconv_open
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index adb03b3f4..4473ff1da 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -51,6 +51,8 @@ lua_source(lua_sources ../third_party/luafun/fun.lua)
 lua_source(lua_sources lua/httpc.lua)
 lua_source(lua_sources lua/iconv.lua)
 lua_source(lua_sources lua/swim.lua)
+lua_source(lua_sources lua/datetime.lua)
+
 # LuaJIT jit.* library
 lua_source(lua_sources ${LUAJIT_SOURCE_ROOT}/src/jit/bc.lua)
 lua_source(lua_sources ${LUAJIT_SOURCE_ROOT}/src/jit/bcsave.lua)
@@ -193,7 +195,8 @@ target_link_libraries(server core coll http_parser bit uri uuid swim swim_udp
 # Rule of thumb: if exporting a symbol from a static library, list the
 # library here.
 set (reexport_libraries server core misc bitset csv swim swim_udp swim_ev
-     shutdown ${LUAJIT_LIBRARIES} ${MSGPUCK_LIBRARIES} ${ICU_LIBRARIES} ${CURL_LIBRARIES} ${XXHASH_LIBRARIES})
+     shutdown ${LUAJIT_LIBRARIES} ${MSGPUCK_LIBRARIES} ${ICU_LIBRARIES}
+     ${CURL_LIBRARIES} ${XXHASH_LIBRARIES} ${LIBCDT_LIBRARIES})
 
 set (common_libraries
     ${reexport_libraries}
diff --git a/src/lib/core/CMakeLists.txt b/src/lib/core/CMakeLists.txt
index 2cd4d0b4f..8bc776b82 100644
--- a/src/lib/core/CMakeLists.txt
+++ b/src/lib/core/CMakeLists.txt
@@ -30,6 +30,7 @@ set(core_sources
     decimal.c
     mp_decimal.c
     cord_buf.c
+    datetime.c
 )
 
 if (TARGET_OS_NETBSD)
diff --git a/src/lib/core/datetime.c b/src/lib/core/datetime.c
new file mode 100644
index 000000000..7125090e6
--- /dev/null
+++ b/src/lib/core/datetime.c
@@ -0,0 +1,125 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright 2021, Tarantool AUTHORS, please see AUTHORS file.
+ */
+
+#include <assert.h>
+#include <limits.h>
+#include <string.h>
+#include <time.h>
+
+#include "trivia/util.h"
+#include "datetime.h"
+
+/*
+ * Given the seconds from Epoch (1970-01-01) we calculate date
+ * since Rata Die (0001-01-01).
+ * DT_EPOCH_1970_OFFSET is the distance in days from Rata Die to Epoch.
+ */
+static int
+local_dt(int64_t secs)
+{
+	return dt_from_rdn((int)(secs / SECS_PER_DAY) + DT_EPOCH_1970_OFFSET);
+}
+
+static struct tm *
+datetime_to_tm(const struct datetime *date)
+{
+	static struct tm tm;
+
+	memset(&tm, 0, sizeof(tm));
+	int64_t secs = date->secs;
+	dt_to_struct_tm(local_dt(secs), &tm);
+
+	int seconds_of_day = (int64_t)date->secs % SECS_PER_DAY;
+	tm.tm_hour = (seconds_of_day / 3600) % 24;
+	tm.tm_min = (seconds_of_day / 60) % 60;
+	tm.tm_sec = seconds_of_day % 60;
+
+	return &tm;
+}
+
+void
+datetime_now(struct datetime *now)
+{
+	struct timeval tv;
+	gettimeofday(&tv, NULL);
+	now->secs = tv.tv_sec;
+	now->nsec = tv.tv_usec * 1000;
+
+	time_t now_seconds;
+	time(&now_seconds);
+	struct tm tm;
+	localtime_r(&now_seconds, &tm);
+	now->offset = tm.tm_gmtoff / 60;
+}
+
+size_t
+datetime_strftime(const struct datetime *date, const char *fmt, char *buf,
+		  uint32_t len)
+{
+	struct tm *p_tm = datetime_to_tm(date);
+	return strftime(buf, len, fmt, p_tm);
+}
+
+
+/* NB! buf may be NULL, and we should handle it gracefully, returning
+ * calculated length of output string
+ */
+int
+datetime_to_string(const struct datetime *date, char *buf, int len)
+{
+	int offset = date->offset;
+	/* for negative offsets around Epoch date we could get
+	 * negative secs value, which should be attributed to
+	 * 1969-12-31, not 1970-01-01, thus we first shift
+	 * epoch to Rata Die then divide by seconds per day,
+	 * not in reverse
+	 */
+	int64_t rd_seconds = (int64_t)date->secs + offset * 60 +
+			     SECS_EPOCH_1970_OFFSET;
+	int rd_number = rd_seconds / SECS_PER_DAY;
+	assert(rd_number <= INT_MAX);
+	assert(rd_number >= INT_MIN);
+	dt_t dt = dt_from_rdn(rd_number);
+
+	int year, month, day, second, nanosec, sign;
+	dt_to_ymd(dt, &year, &month, &day);
+
+	int hour = (rd_seconds / 3600) % 24;
+	int minute = (rd_seconds / 60) % 60;
+	second = rd_seconds % 60;
+	nanosec = date->nsec;
+
+	int sz = 0;
+	SNPRINT(sz, snprintf, buf, len, "%04d-%02d-%02dT%02d:%02d",
+		year, month, day, hour, minute);
+	if (second || nanosec) {
+		SNPRINT(sz, snprintf, buf, len, ":%02d", second);
+		if (nanosec) {
+			if ((nanosec % 1000000) == 0)
+				SNPRINT(sz, snprintf, buf, len, ".%03d",
+					nanosec / 1000000);
+			else if ((nanosec % 1000) == 0)
+				SNPRINT(sz, snprintf, buf, len, ".%06d",
+					nanosec / 1000);
+			else
+				SNPRINT(sz, snprintf, buf, len, ".%09d", nanosec);
+		}
+	}
+	if (offset == 0) {
+		SNPRINT(sz, snprintf, buf, len, "Z");
+	} else {
+		if (offset < 0) {
+			sign = '-';
+			offset = -offset;
+		} else {
+			sign = '+';
+		}
+		SNPRINT(sz, snprintf, buf, len, "%c%02d:%02d", sign,
+			offset / 60, offset % 60);
+	}
+	return sz;
+}
+
diff --git a/src/lib/core/datetime.h b/src/lib/core/datetime.h
new file mode 100644
index 000000000..71feefded
--- /dev/null
+++ b/src/lib/core/datetime.h
@@ -0,0 +1,86 @@
+#pragma once
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright 2021, Tarantool AUTHORS, please see AUTHORS file.
+ */
+
+#include <stdint.h>
+#include <stdbool.h>
+#include "c-dt/dt.h"
+
+#if defined(__cplusplus)
+extern "C"
+{
+#endif /* defined(__cplusplus) */
+
+/**
+ * We count dates since so called "Rata Die" date
+ * January 1, 0001, Monday (as Day 1).
+ * But datetime structure keeps seconds since
+ * Unix "Epoch" date:
+ * Unix, January 1, 1970, Thursday
+ *
+ * The difference between Epoch (1970-01-01)
+ * and Rata Die (0001-01-01) is 719163 days.
+ */
+
+#ifndef SECS_PER_DAY
+#define SECS_PER_DAY          86400
+#define DT_EPOCH_1970_OFFSET  719163
+#endif
+
+#define SECS_EPOCH_1970_OFFSET 	\
+	((int64_t)DT_EPOCH_1970_OFFSET * SECS_PER_DAY)
+/**
+ * datetime structure keeps number of seconds since
+ * Unix Epoch.
+ * Time is normalized by UTC, so time-zone offset
+ * is informative only.
+ */
+struct datetime {
+	/** Seconds since Epoch. */
+	double secs;
+	/** Nanoseconds, if any. */
+	uint32_t nsec;
+	/** Offset in minutes from UTC. */
+	int32_t offset;
+};
+
+/**
+ * Date/time interval structure
+ */
+struct datetime_interval {
+	/** Relative seconds delta. */
+	double secs;
+	/** Nanoseconds delta, if any. */
+	uint32_t nsec;
+};
+
+/**
+ * Convert datetime to string using default format
+ * @param date source datetime value
+ * @param buf output character buffer
+ * @param len size ofoutput buffer
+ */
+int
+datetime_to_string(const struct datetime *date, char *buf, int len);
+
+/**
+ * Convert datetime to string using default format provided
+ * Wrapper around standard strftime() function
+ * @param date source datetime value
+ * @param fmt format
+ * @param buf output buffer
+ * @param len size of output buffer
+ */
+size_t
+datetime_strftime(const struct datetime *date, const char *fmt, char *buf,
+		  uint32_t len);
+
+void
+datetime_now(struct datetime *now);
+
+#if defined(__cplusplus)
+} /* extern "C" */
+#endif /* defined(__cplusplus) */
diff --git a/src/lua/datetime.lua b/src/lua/datetime.lua
new file mode 100644
index 000000000..88aad0744
--- /dev/null
+++ b/src/lua/datetime.lua
@@ -0,0 +1,567 @@
+local ffi = require('ffi')
+
+--[[
+    `c-dt` library functions handles properly both positive and negative `dt`
+    values, where `dt` is a number of dates since Rata Die date (0001-01-01).
+
+    For better compactness of our typical data in MessagePack stream we shift
+    root of our time to the Unix Epoch date (1970-01-01), thus our 0 is
+    actually dt = 719163.
+
+    So here is a simple formula how convert our epoch-based seconds to dt values
+        dt = (secs / 86400) + 719163
+    Where 719163 is an offset of Unix Epoch (1970-01-01) since Rata Die
+    (0001-01-01) in dates.
+]]
+
+-- dt_core.h definitions
+ffi.cdef [[
+
+typedef int dt_t;
+
+typedef enum {
+    DT_MON       = 1,
+    DT_MONDAY    = 1,
+    DT_TUE       = 2,
+    DT_TUESDAY   = 2,
+    DT_WED       = 3,
+    DT_WEDNESDAY = 3,
+    DT_THU       = 4,
+    DT_THURSDAY  = 4,
+    DT_FRI       = 5,
+    DT_FRIDAY    = 5,
+    DT_SAT       = 6,
+    DT_SATURDAY  = 6,
+    DT_SUN       = 7,
+    DT_SUNDAY    = 7,
+} dt_dow_t;
+
+dt_t   tnt_dt_from_rdn     (int n);
+dt_t   tnt_dt_from_ymd     (int y, int m, int d);
+int    tnt_dt_rdn          (dt_t dt);
+
+dt_dow_t tnt_dt_dow        (dt_t dt);
+
+]]
+
+-- dt_accessor.h
+ffi.cdef [[
+
+int     dt_year         (dt_t dt);
+int     dt_month        (dt_t dt);
+
+int     dt_doy          (dt_t dt);
+int     dt_dom          (dt_t dt);
+
+]]
+
+-- dt_arithmetic.h definitions
+ffi.cdef [[
+
+typedef enum {
+    DT_EXCESS,
+    DT_LIMIT,
+    DT_SNAP
+} dt_adjust_t;
+
+dt_t   tnt_dt_add_years    (dt_t dt, int delta, dt_adjust_t adjust);
+dt_t   tnt_dt_add_quarters (dt_t dt, int delta, dt_adjust_t adjust);
+dt_t   tnt_dt_add_months   (dt_t dt, int delta, dt_adjust_t adjust);
+
+]]
+
+-- dt_parse_iso.h definitions
+ffi.cdef [[
+
+size_t tnt_dt_parse_iso_date (const char *str, size_t len, dt_t *dt);
+size_t tnt_dt_parse_iso_time (const char *str, size_t len, int *sod, int *nsec);
+size_t tnt_dt_parse_iso_zone_lenient(const char *str, size_t len, int *offset);
+
+]]
+
+-- Tarantool functions - datetime.c
+ffi.cdef [[
+
+int    datetime_to_string(const struct datetime * date, char *buf, int len);
+size_t datetime_strftime(const struct datetime *date, const char *fmt, char *buf,
+                         uint32_t len);
+void   datetime_now(struct datetime *now);
+
+]]
+
+local builtin = ffi.C
+local math_modf = math.modf
+local math_floor = math.floor
+
+local SECS_PER_DAY     = 86400
+local NANOS_PER_SEC    = 1000000000
+
+-- c-dt/dt_config.h
+
+-- Unix, January 1, 1970, Thursday
+local DT_EPOCH_1970_OFFSET = 719163
+
+
+local datetime_t = ffi.typeof('struct datetime')
+
+local function is_datetime(o)
+    return ffi.istype(datetime_t, o)
+end
+
+local function check_date(o, message)
+    if not is_datetime(o) then
+        return error(("%s: expected datetime, but received %s"):
+                     format(message, o), 2)
+    end
+end
+
+local function check_str(s, message)
+    if not type(s) == 'string' then
+        return error(("%s: expected string, but received %s"):
+                     format(message, s), 2)
+    end
+end
+
+local function check_range(v, range, txt)
+    assert(#range == 2)
+    if v < range[1] or v > range[2] then
+        error(('value %d of %s is out of allowed range [%d, %d]'):
+              format(v, txt, range[1], range[2]), 4)
+    end
+end
+
+local SECS_EPOCH_OFFSET = (DT_EPOCH_1970_OFFSET * SECS_PER_DAY)
+
+local function local_rd(secs)
+    return math_floor((secs + SECS_EPOCH_OFFSET) / SECS_PER_DAY)
+end
+
+local function local_dt(secs)
+    return builtin.tnt_dt_from_rdn(local_rd(secs))
+end
+
+local function normalize_nsec(secs, nsec)
+    if nsec < 0 then
+        secs = secs - 1
+        nsec = nsec + NANOS_PER_SEC
+    elseif nsec >= NANOS_PER_SEC then
+        secs = secs + 1
+        nsec = nsec - NANOS_PER_SEC
+    end
+    return secs, nsec
+end
+
+local function datetime_cmp(lhs, rhs)
+    if not is_datetime(lhs) or not is_datetime(rhs) then
+        return nil
+    end
+    local sdiff = lhs.secs - rhs.secs
+    return sdiff ~= 0 and sdiff or (lhs.nsec - rhs.nsec)
+end
+
+local function datetime_eq(lhs, rhs)
+    local rc = datetime_cmp(lhs, rhs)
+    return rc ~= nil and rc == 0
+end
+
+local function datetime_lt(lhs, rhs)
+    local rc = datetime_cmp(lhs, rhs)
+    return rc == nil and error('incompatible types for comparison', 2) or
+           rc < 0
+end
+
+local function datetime_le(lhs, rhs)
+    local rc = datetime_cmp(lhs, rhs)
+    return rc == nil and error('incompatible types for comparison', 2) or
+           rc <= 0
+end
+
+local function datetime_serialize(self)
+    return { secs = self.secs, nsec = self.nsec, offset = self.offset }
+end
+
+local parse_zone
+
+local function datetime_new_raw(secs, nsec, offset)
+    local dt_obj = ffi.new(datetime_t)
+    dt_obj.secs = secs
+    dt_obj.nsec = nsec
+    dt_obj.offset = offset
+    return dt_obj
+end
+
+local function datetime_new_dt(dt, secs, fraction, offset)
+    local epochV = dt ~= nil and (builtin.tnt_dt_rdn(dt) - DT_EPOCH_1970_OFFSET) *
+                   SECS_PER_DAY or 0
+    local secsV = secs or 0
+    local fracV = fraction or 0
+    local ofsV = offset or 0
+    return datetime_new_raw(epochV + secsV - ofsV * 60, fracV, ofsV)
+end
+
+-- create datetime given attribute values from obj
+-- { secs = N, nsec = M, offset = O}
+local function datetime_new_obj(obj, ...)
+    if obj == nil or type(obj) == 'table' then
+        return ffi.new(datetime_t, obj)
+    else
+        return datetime_new_raw(obj, ...)
+    end
+end
+
+-- create datetime given attribute values from obj
+local function datetime_new(obj)
+    if obj == nil or type(obj) ~= 'table' then
+        return datetime_new_raw(0, 0, 0)
+    end
+    local ymd = false
+
+    local nsec = 0
+    local hms = false
+
+    local dt = 0
+
+    local y = obj.year
+    if y ~= nil then
+        check_range(y, {1, 9999}, 'year')
+        ymd = true
+    end
+    local M = obj.month
+    if M ~= nil then
+        check_range(M, {1, 12}, 'month')
+        ymd = true
+    end
+    local d = obj.day
+    if d ~= nil then
+        check_range(d, {1, 31}, 'day')
+        ymd = true
+    end
+    local h = obj.hour
+    if h ~= nil then
+        check_range(h, {0, 23}, 'hour')
+        hms = true
+    end
+    local m = obj.min
+    if m ~= nil then
+        check_range(m, {0, 59}, 'min')
+        hms = true
+    end
+    local ts = obj.sec
+    local s = 0
+    if ts ~= nil then
+        check_range(ts, {0, 60}, 'sec')
+        s, nsec = math_modf(ts)
+        nsec = nsec * 1e9 -- convert fraction to nanoseconds
+        hms = true
+    end
+    local offset = obj.tz
+    if offset ~= nil then
+        if type(offset) == 'number' then
+            -- tz offset in minutes
+            check_range(offset, {0, 720}, offset)
+        elseif type(offset) == 'string' then
+            local zone = parse_zone(offset)
+            if zone == nil then
+                error(('invalid time-zone format %s'):format(offset), 2)
+            else
+                offset = zone.offset
+            end
+        end
+    end
+
+    -- .year, .month, .day
+    if ymd then
+        dt = builtin.tnt_dt_from_ymd(y or 0, M or 0, d or 0)
+    end
+
+    -- .hour, .minute, .second
+    local secs = 0
+    if hms then
+        secs = (h or 0) * 3600 + (m or 0) * 60 + (s or 0)
+    end
+
+    return datetime_new_dt(dt, secs, nsec, offset)
+end
+
+--[[
+    Convert to text datetime values
+
+    - datetime will use ISO-8601 forat:
+        1970-01-01T00:00Z
+        2021-08-18T16:57:08.981725+03:00
+]]
+local function datetime_tostring(o)
+    if ffi.typeof(o) == datetime_t then
+        local sz = 48
+        local buff = ffi.new('char[?]', sz)
+        local len = builtin.datetime_to_string(o, buff, sz)
+        assert(len < sz)
+        return ffi.string(buff)
+    end
+end
+
+--[[
+    Parse partial ISO-8601 date string
+
+    Accepetd formats are:
+
+    Basic      Extended
+    20121224   2012-12-24   Calendar date   (ISO 8601)
+    2012359    2012-359     Ordinal date    (ISO 8601)
+    2012W521   2012-W52-1   Week date       (ISO 8601)
+    2012Q485   2012-Q4-85   Quarter date
+
+    Returns pair of constructed datetime object, and length of string
+    which has been accepted by parser.
+]]
+
+local function parse_date(str)
+    check_str("datetime.parse_date()")
+    local dt = ffi.new('dt_t[1]')
+    local len = builtin.tnt_dt_parse_iso_date(str, #str, dt)
+    return len > 0 and datetime_new_dt(dt[0]) or nil, tonumber(len)
+end
+
+--[[
+    Basic    Extended
+    Z        N/A
+    +hh      N/A
+    -hh      N/A
+    +hhmm    +hh:mm
+    -hhmm    -hh:mm
+
+    Returns pair of constructed datetime object, and length of string
+    which has been accepted by parser.
+]]
+parse_zone = function(str)
+    check_str("datetime.parse_zone()")
+    local offset = ffi.new('int[1]')
+    local len = builtin.tnt_dt_parse_iso_zone_lenient(str, #str, offset)
+    return len > 0 and offset[0], tonumber(len)
+end
+
+--[[
+    aggregated parse functions
+    assumes to deal with date T time time_zone
+    at once
+
+    date [T] time [ ] time_zone
+
+    Returns constructed datetime object.
+]]
+local function parse(str)
+    check_str("datetime.parse()")
+    local dt = ffi.new('dt_t[1]')
+    local len = #str
+    local n = builtin.tnt_dt_parse_iso_date(str, len, dt)
+    local dt_ = dt[0]
+    if n == 0 or len == n then
+        return datetime_new_dt(dt_)
+    end
+
+    str = str:sub(tonumber(n) + 1)
+
+    local ch = str:sub(1, 1)
+    if ch:match('[Tt ]') == nil then
+        return datetime_new_dt(dt_)
+    end
+
+    str = str:sub(2)
+    len = #str
+
+    local sp = ffi.new('int[1]')
+    local fp = ffi.new('int[1]')
+    local n = builtin.tnt_dt_parse_iso_time(str, len, sp, fp)
+    if n == 0 then
+        return datetime_new_dt(dt_)
+    end
+    local sp_ = sp[0]
+    local fp_ = fp[0]
+    if len == n then
+        return datetime_new_dt(dt_, sp_, fp_)
+    end
+
+    str = str:sub(tonumber(n) + 1)
+
+    if str:sub(1, 1) == ' ' then
+        str = str:sub(2)
+    end
+
+    len = #str
+
+    local offset = ffi.new('int[1]')
+    n = builtin.tnt_dt_parse_iso_zone_lenient(str, len, offset)
+    if n == 0 then
+        return datetime_new_dt(dt_, sp_, fp_)
+    end
+    return datetime_new_dt(dt_, sp_, fp_, offset[0])
+end
+
+--[[
+    Dispatch function to create datetime from string or table.
+    Creates default timeobject (pointing to Epoch date) if
+    called without arguments.
+]]
+local function datetime_from(o)
+    if o == nil or type(o) == 'table' then
+        return datetime_new(o)
+    elseif type(o) == 'string' then
+        return parse(o)
+    end
+end
+
+--[[
+    Create datetime object representing current time using microseconds
+    platform timer and local timezone information.
+]]
+local function local_now()
+    local d = datetime_new_raw(0, 0, 0)
+    builtin.datetime_now(d)
+    return d
+end
+
+-- addition or subtraction from date/time of a given interval
+-- described via table direction should be +1 or -1
+local function datetime_increment(self, o, direction)
+    assert(direction == -1 or direction == 1)
+    local title = direction > 0 and "datetime.add" or "datetime.sub"
+    check_date(self, title)
+    if type(o) ~= 'table' then
+        error(('%s - object expected'):format(title), 2)
+    end
+
+    local secs, nsec = self.secs, self.nsec
+    local offset = self.offset
+
+    -- operations with intervals should be done using human dates
+    -- not UTC dates, thus we normalize to UTC
+    local dt = local_dt(secs)
+
+    local ym_updated = false
+    local years, months, weeks = o.years, o.months, o.weeks
+
+    if years ~= nil then
+        check_range(years, {0, 9999}, 'years')
+        dt = builtin.tnt_dt_add_years(dt, direction * years, builtin.DT_LIMIT)
+        ym_updated = true
+    end
+    if months ~= nil then
+        check_range(months, {0, 12}, 'months')
+        dt = builtin.tnt_dt_add_months(dt, direction * months, builtin.DT_LIMIT)
+        ym_updated = true
+    end
+    if ym_updated then
+        secs = (builtin.tnt_dt_rdn(dt) - DT_EPOCH_1970_OFFSET) * SECS_PER_DAY +
+                secs % SECS_PER_DAY
+    end
+
+    if weeks ~= nil then
+        check_range(weeks, {0, 52}, 'weeks')
+        secs = secs + direction * 7 * weeks * SECS_PER_DAY
+    end
+
+    local days, hours, minutes, seconds = o.days, o.hours, o.minutes, o.seconds
+    if days ~= nil then
+        check_range(days, {0, 31}, 'days')
+        secs = secs + direction * days * SECS_PER_DAY
+    end
+    if hours ~= nil then
+        check_range(hours, {0, 23}, 'hours')
+        secs = secs + direction * 60 * 60 * hours
+    end
+    if minutes ~= nil then
+        check_range(minutes, {0, 59}, 'minutes')
+        secs = secs + direction * 60 * minutes
+    end
+    if seconds ~= nil then
+        check_range(seconds, {0, 60}, 'seconds')
+        local s, frac = math.modf(seconds)
+        secs = secs + direction * s
+        nsec = nsec + direction * frac * 1e9
+    end
+
+    secs, nsec = normalize_nsec(secs, nsec)
+
+    return datetime_new_raw(secs, nsec, offset)
+end
+
+--[[
+    Return table in os.date('*t') format, but with timezone
+    and nanoseconds
+]]
+local function datetime_totable(self)
+    local secs = self.secs
+    local dt = local_dt(secs)
+    local year = builtin.dt_year(dt)
+    local month = builtin.dt_month(dt)
+    local yday = builtin.dt_doy(dt)
+    local wday = ffi.cast('int32_t', builtin.tnt_dt_dow(dt))
+    local day_of_month = builtin.dt_dom(dt)
+    local hour = math_floor((secs / 3600) % 24)
+    local minute = math_floor((secs / 60) % 60)
+    local second = secs % 60
+
+    return {
+        sec = second,
+        min = minute,
+        day = day_of_month,
+        isdst = false,
+        wday = wday,
+        yday = yday,
+        year = year,
+        month = month,
+        hour = hour,
+        nsec = self.nsec,
+        tz = self.offset,
+    }
+end
+
+local function datetime_set(self, obj)
+    -- FIXME
+    return datetime_new(obj)
+end
+
+local function strftime(fmt, o)
+    check_date(o, "datetime.strftime()")
+    local sz = 128
+    local buff = ffi.new('char[?]', sz)
+    builtin.datetime_strftime(o, fmt, buff, sz)
+    return ffi.string(buff)
+end
+
+ffi.metatype(datetime_t, {
+    __tostring = datetime_tostring,
+    __serialize = datetime_serialize,
+    __eq = datetime_eq,
+    __lt = datetime_lt,
+    __le = datetime_le,
+    __index = {
+        epoch = function(self) return self.secs end,
+        timestamp = function(self) return self.secs + self.nsec / 1e9 end,
+        nanoseconds = function(self) return self.secs * 1e9 + self.nsec end,
+        microseconds = function(self) return self.secs * 1e6 + self.nsec / 1e3 end,
+        milliseconds = function(self) return self.secs * 1e3 + self.nsec / 1e6 end,
+        seconds = function(self) return self.secs + self.nsec / 1e9 end,
+        add = function(self, obj) return datetime_increment(self, obj, 1) end,
+        sub = function(self, obj) return datetime_increment(self, obj, -1) end,
+        totable = datetime_totable,
+        set = datetime_set,
+    }
+})
+
+return setmetatable(
+    {
+        new         = datetime_new,
+        new_raw     = datetime_new_obj,
+
+        parse       = parse,
+        parse_date  = parse_date,
+
+        now         = local_now,
+        strftime    = strftime,
+
+        is_datetime = is_datetime,
+    }, {
+        __call = function(self, ...) return datetime_from(...) end
+    }
+)
diff --git a/src/lua/init.c b/src/lua/init.c
index f9738025d..127e935d7 100644
--- a/src/lua/init.c
+++ b/src/lua/init.c
@@ -129,7 +129,8 @@ extern char strict_lua[],
 	parse_lua[],
 	process_lua[],
 	humanize_lua[],
-	memprof_lua[]
+	memprof_lua[],
+	datetime_lua[]
 ;
 
 static const char *lua_modules[] = {
@@ -184,6 +185,7 @@ static const char *lua_modules[] = {
 	"memprof.process", process_lua,
 	"memprof.humanize", humanize_lua,
 	"memprof", memprof_lua,
+	"datetime", datetime_lua,
 	NULL
 };
 
diff --git a/src/lua/utils.c b/src/lua/utils.c
index c71cd4857..2c89326f3 100644
--- a/src/lua/utils.c
+++ b/src/lua/utils.c
@@ -48,6 +48,9 @@ static uint32_t CTID_STRUCT_IBUF_PTR;
 uint32_t CTID_CHAR_PTR;
 uint32_t CTID_CONST_CHAR_PTR;
 uint32_t CTID_UUID;
+uint32_t CTID_DATETIME = 0;
+uint32_t CTID_INTERVAL = 0;
+
 
 void *
 luaL_pushcdata(struct lua_State *L, uint32_t ctypeid)
@@ -120,6 +123,12 @@ luaL_pushuuidstr(struct lua_State *L, const struct tt_uuid *uuid)
 	lua_pushlstring(L, str, UUID_STR_LEN);
 }
 
+struct datetime *
+luaL_pushdatetime(struct lua_State *L)
+{
+	return luaL_pushcdata(L, CTID_DATETIME);
+}
+
 int
 luaL_iscdata(struct lua_State *L, int idx)
 {
@@ -725,6 +734,24 @@ tarantool_lua_utils_init(struct lua_State *L)
 	CTID_UUID = luaL_ctypeid(L, "struct tt_uuid");
 	assert(CTID_UUID != 0);
 
+	rc = luaL_cdef(L, "struct datetime {"
+			  "double secs;"
+			  "int32_t nsec;"
+			  "int32_t offset;"
+			  "};");
+	assert(rc == 0);
+	(void) rc;
+	CTID_DATETIME = luaL_ctypeid(L, "struct datetime");
+	assert(CTID_DATETIME != 0);
+	rc = luaL_cdef(L, "struct datetime_interval {"
+			  "double secs;"
+			  "int32_t nsec;"
+			  "};");
+	assert(rc == 0);
+	(void) rc;
+	CTID_INTERVAL = luaL_ctypeid(L, "struct datetime_interval");
+	assert(CTID_INTERVAL != 0);
+
 	lua_pushcfunction(L, luaT_newthread_wrapper);
 	luaT_newthread_ref = luaL_ref(L, LUA_REGISTRYINDEX);
 	return 0;
diff --git a/src/lua/utils.h b/src/lua/utils.h
index 45070b778..73495b607 100644
--- a/src/lua/utils.h
+++ b/src/lua/utils.h
@@ -59,6 +59,7 @@ struct lua_State;
 struct ibuf;
 typedef struct ibuf box_ibuf_t;
 struct tt_uuid;
+struct datetime;
 
 /**
  * Single global lua_State shared by core and modules.
@@ -71,6 +72,8 @@ extern struct lua_State *tarantool_L;
 extern uint32_t CTID_CHAR_PTR;
 extern uint32_t CTID_CONST_CHAR_PTR;
 extern uint32_t CTID_UUID;
+extern uint32_t CTID_DATETIME;
+extern uint32_t CTID_INTERVAL;
 
 struct tt_uuid *
 luaL_pushuuid(struct lua_State *L);
@@ -78,6 +81,15 @@ luaL_pushuuid(struct lua_State *L);
 void
 luaL_pushuuidstr(struct lua_State *L, const struct tt_uuid *uuid);
 
+/**
+ * @brief Push cdata of a datetime type onto the stack.
+ * @param L Lua State
+ * @sa luaL_pushcdata
+ * @return memory associated with this datetime data
+ */
+struct datetime *
+luaL_pushdatetime(struct lua_State *L);
+
 /** \cond public */
 
 /**
diff --git a/test/app-tap/datetime.test.lua b/test/app-tap/datetime.test.lua
new file mode 100755
index 000000000..5c39feee8
--- /dev/null
+++ b/test/app-tap/datetime.test.lua
@@ -0,0 +1,213 @@
+#!/usr/bin/env tarantool
+
+local tap = require('tap')
+local test = tap.test("errno")
+local date = require('datetime')
+local ffi = require('ffi')
+
+test:plan(7)
+
+test:test("Simple tests for parser", function(test)
+    test:plan(2)
+    test:ok(date("1970-01-01T01:00:00Z") ==
+            date {year=1970, month=1, day=1, hour=1, minute=0, second=0})
+    test:ok(date("1970-01-01T02:00:00+02:00") ==
+            date {year=1970, month=1, day=1, hour=2, minute=0, second=0, tz=120})
+end)
+
+test:test("Multiple tests for parser (with nanoseconds)", function(test)
+    test:plan(193)
+    -- borrowed from p5-time-moments/t/180_from_string.t
+    local tests =
+    {
+        {'1970-01-01T00:00Z',                  0,         0,    0, 1},
+        {'1970-01-01T02:00+02:00',             0,         0,  120, 1},
+        {'1970-01-01T01:30+01:30',             0,         0,   90, 1},
+        {'1970-01-01T01:00+01:00',             0,         0,   60, 1},
+        {'1970-01-01T00:01+00:01',             0,         0,    1, 1},
+        {'1970-01-01T00:00Z',                  0,         0,    0, 1},
+        {'1969-12-31T23:59-00:01',             0,         0,   -1, 1},
+        {'1969-12-31T23:00-01:00',             0,         0,  -60, 1},
+        {'1969-12-31T22:30-01:30',             0,         0,  -90, 1},
+        {'1969-12-31T22:00-02:00',             0,         0, -120, 1},
+        {'1970-01-01T00:00:00.123456789Z',     0, 123456789,    0, 1},
+        {'1970-01-01T00:00:00.12345678Z',      0, 123456780,    0, 0},
+        {'1970-01-01T00:00:00.1234567Z',       0, 123456700,    0, 0},
+        {'1970-01-01T00:00:00.123456Z',        0, 123456000,    0, 1},
+        {'1970-01-01T00:00:00.12345Z',         0, 123450000,    0, 0},
+        {'1970-01-01T00:00:00.1234Z',          0, 123400000,    0, 0},
+        {'1970-01-01T00:00:00.123Z',           0, 123000000,    0, 1},
+        {'1970-01-01T00:00:00.12Z',            0, 120000000,    0, 0},
+        {'1970-01-01T00:00:00.1Z',             0, 100000000,    0, 0},
+        {'1970-01-01T00:00:00.01Z',            0,  10000000,    0, 0},
+        {'1970-01-01T00:00:00.001Z',           0,   1000000,    0, 1},
+        {'1970-01-01T00:00:00.0001Z',          0,    100000,    0, 0},
+        {'1970-01-01T00:00:00.00001Z',         0,     10000,    0, 0},
+        {'1970-01-01T00:00:00.000001Z',        0,      1000,    0, 1},
+        {'1970-01-01T00:00:00.0000001Z',       0,       100,    0, 0},
+        {'1970-01-01T00:00:00.00000001Z',      0,        10,    0, 0},
+        {'1970-01-01T00:00:00.000000001Z',     0,         1,    0, 1},
+        {'1970-01-01T00:00:00.000000009Z',     0,         9,    0, 1},
+        {'1970-01-01T00:00:00.00000009Z',      0,        90,    0, 0},
+        {'1970-01-01T00:00:00.0000009Z',       0,       900,    0, 0},
+        {'1970-01-01T00:00:00.000009Z',        0,      9000,    0, 1},
+        {'1970-01-01T00:00:00.00009Z',         0,     90000,    0, 0},
+        {'1970-01-01T00:00:00.0009Z',          0,    900000,    0, 0},
+        {'1970-01-01T00:00:00.009Z',           0,   9000000,    0, 1},
+        {'1970-01-01T00:00:00.09Z',            0,  90000000,    0, 0},
+        {'1970-01-01T00:00:00.9Z',             0, 900000000,    0, 0},
+        {'1970-01-01T00:00:00.99Z',            0, 990000000,    0, 0},
+        {'1970-01-01T00:00:00.999Z',           0, 999000000,    0, 1},
+        {'1970-01-01T00:00:00.9999Z',          0, 999900000,    0, 0},
+        {'1970-01-01T00:00:00.99999Z',         0, 999990000,    0, 0},
+        {'1970-01-01T00:00:00.999999Z',        0, 999999000,    0, 1},
+        {'1970-01-01T00:00:00.9999999Z',       0, 999999900,    0, 0},
+        {'1970-01-01T00:00:00.99999999Z',      0, 999999990,    0, 0},
+        {'1970-01-01T00:00:00.999999999Z',     0, 999999999,    0, 1},
+        {'1970-01-01T00:00:00.0Z',             0,         0,    0, 0},
+        {'1970-01-01T00:00:00.00Z',            0,         0,    0, 0},
+        {'1970-01-01T00:00:00.000Z',           0,         0,    0, 0},
+        {'1970-01-01T00:00:00.0000Z',          0,         0,    0, 0},
+        {'1970-01-01T00:00:00.00000Z',         0,         0,    0, 0},
+        {'1970-01-01T00:00:00.000000Z',        0,         0,    0, 0},
+        {'1970-01-01T00:00:00.0000000Z',       0,         0,    0, 0},
+        {'1970-01-01T00:00:00.00000000Z',      0,         0,    0, 0},
+        {'1970-01-01T00:00:00.000000000Z',     0,         0,    0, 0},
+        {'1973-11-29T21:33:09Z',       123456789,         0,    0, 1},
+        {'2013-10-28T17:51:56Z',      1382982716,         0,    0, 1},
+        {'9999-12-31T23:59:59Z',    253402300799,         0,    0, 1},
+    }
+    for _, value in ipairs(tests) do
+        local str, epoch, nsec, offset, check
+        str, epoch, nsec, offset, check = unpack(value)
+        local dt = date(str)
+        test:ok(dt.secs == epoch, ('%s: dt.secs == %d'):format(str, epoch))
+        test:ok(dt.nsec == nsec, ('%s: dt.nsec == %d'):format(str, nsec))
+        test:ok(dt.offset == offset, ('%s: dt.offset == %d'):format(str, offset))
+        if check > 0 then
+            test:ok(str == tostring(dt), ('%s == tostring(%s)'):
+                    format(str, tostring(dt)))
+        end
+    end
+end)
+
+ffi.cdef [[
+    void tzset(void);
+]]
+
+test:test("Datetime string formatting", function(test)
+    test:plan(5)
+    local str = "1970-01-01"
+    local t = date(str)
+    test:ok(t.secs == 0, ('%s: t.secs == %d'):format(str, tonumber(t.secs)))
+    test:ok(t.nsec == 0, ('%s: t.nsec == %d'):format(str, t.nsec))
+    test:ok(t.offset == 0, ('%s: t.offset == %d'):format(str, t.offset))
+    test:ok(date.strftime('%d/%m/%Y', t) == '01/01/1970', ('%s: strftime #1'):format(str))
+    test:ok(date.strftime('%A %d. %B %Y', t) == 'Thursday 01. January 1970', ('%s: strftime #2'):format(str))
+end)
+
+test:test("Parse iso date - valid strings", function(test)
+    test:plan(32)
+    local good = {
+        {2012, 12, 24, "20121224",                   8 },
+        {2012, 12, 24, "20121224  Foo bar",          8 },
+        {2012, 12, 24, "2012-12-24",                10 },
+        {2012, 12, 24, "2012-12-24 23:59:59",       10 },
+        {2012, 12, 24, "2012-12-24T00:00:00+00:00", 10 },
+        {2012, 12, 24, "2012359",                    7 },
+        {2012, 12, 24, "2012359T235959+0130",        7 },
+        {2012, 12, 24, "2012-359",                   8 },
+        {2012, 12, 24, "2012W521",                   8 },
+        {2012, 12, 24, "2012-W52-1",                10 },
+        {2012, 12, 24, "2012Q485",                   8 },
+        {2012, 12, 24, "2012-Q4-85",                10 },
+        {   1,  1,  1, "0001-Q1-01",                10 },
+        {   1,  1,  1, "0001-W01-1",                10 },
+        {   1,  1,  1, "0001-01-01",                10 },
+        {   1,  1,  1, "0001-001",                   8 },
+    }
+
+    for _, value in ipairs(good) do
+        local year, month, day, str, date_part_len
+        year, month, day, str, date_part_len = unpack(value)
+        local expected_date = date{year = year, month = month, day = day}
+        local date_part, len
+        date_part, len = date.parse_date(str)
+        test:ok(len == date_part_len, ('%s: length check %d'):format(str, len))
+        test:ok(expected_date == date_part, ('%s: expected date'):format(str))
+    end
+end)
+
+test:test("Parse iso date - invalid strings", function(test)
+    test:plan(62)
+    local bad = {
+        "20121232"   , -- Invalid day of month
+        "2012-12-310", -- Invalid day of month
+        "2012-13-24" , -- Invalid month
+        "2012367"    , -- Invalid day of year
+        "2012-000"   , -- Invalid day of year
+        "2012W533"   , -- Invalid week of year
+        "2012-W52-8" , -- Invalid day of week
+        "2012Q495"   , -- Invalid day of quarter
+        "2012-Q5-85" , -- Invalid quarter
+        "20123670"   , -- Trailing digit
+        "201212320"  , -- Trailing digit
+        "2012-12"    , -- Reduced accuracy
+        "2012-Q4"    , -- Reduced accuracy
+        "2012-Q42"   , -- Invalid
+        "2012-Q1-1"  , -- Invalid day of quarter
+        "2012Q--420" , -- Invalid
+        "2012-Q-420" , -- Invalid
+        "2012Q11"    , -- Incomplete
+        "2012Q1234"  , -- Trailing digit
+        "2012W12"    , -- Incomplete
+        "2012W1234"  , -- Trailing digit
+        "2012W-123"  , -- Invalid
+        "2012-W12"   , -- Incomplete
+        "2012-W12-12", -- Trailing digit
+        "2012U1234"  , -- Invalid
+        "2012-1234"  , -- Invalid
+        "2012-X1234" , -- Invalid
+        "0000-Q1-01" , -- Year less than 0001
+        "0000-W01-1" , -- Year less than 0001
+        "0000-01-01" , -- Year less than 0001
+        "0000-001"   , -- Year less than 0001
+    }
+
+    for _, str in ipairs(bad) do
+        local date_part, len
+        date_part, len = date.parse_date(str)
+        test:ok(len == 0, ('%s: length check %d'):format(str, len))
+        test:ok(date_part == nil, ('%s: empty date check %s'):format(str, date_part))
+    end
+end)
+
+test:test("Parse tiny date into seconds and other parts", function(test)
+    test:plan(7)
+    local str = '19700101 00:00:30.528'
+    local tiny = date(str)
+    test:ok(tiny.secs == 30, ("secs of '%s'"):format(str))
+    test:ok(tiny.nsec == 528000000, ("nsec of '%s'"):format(str))
+    test:ok(tiny:nanoseconds() == 30528000000, "nanoseconds")
+    test:ok(tiny:microseconds() == 30528000, "microseconds")
+    test:ok(tiny:milliseconds() == 30528, "milliseconds")
+    test:ok(tiny:seconds() == 30.528, "seconds")
+    test:ok(tiny:timestamp() == 30.528, "timestamp")
+end)
+
+test:test("Time interval operations", function(test)
+    test:plan(2)
+
+    -- check arithmetic with leap dates
+    local T = date('1972-02-29')
+    test:ok(tostring(T:add{years = 1, months = 2}) == '1973-05-01T00:00Z',
+            ('T:add{years=1,months=2}(%s)'):format(T))
+
+    -- check average, not leap dates
+    T = date('1970-01-08')
+    test:ok(tostring(T:add{years = 1, months = 2}) == '1971-03-08T00:00Z',
+            ('T:add{years=1,months=2}(%s)'):format(T))
+
+end)
+
+os.exit(test:check() and 0 or 1)
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index 5bb7cd6e7..8194a133f 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -56,7 +56,8 @@ add_executable(uuid.test uuid.c core_test_utils.c)
 target_link_libraries(uuid.test uuid unit)
 add_executable(random.test random.c core_test_utils.c)
 target_link_libraries(random.test core unit)
-
+add_executable(datetime.test datetime.c)
+target_link_libraries(datetime.test cdt core unit)
 add_executable(bps_tree.test bps_tree.cc)
 target_link_libraries(bps_tree.test small misc)
 add_executable(bps_tree_iterator.test bps_tree_iterator.cc)
diff --git a/test/unit/datetime.c b/test/unit/datetime.c
new file mode 100644
index 000000000..931636172
--- /dev/null
+++ b/test/unit/datetime.c
@@ -0,0 +1,260 @@
+#include "dt.h"
+#include <assert.h>
+#include <stdint.h>
+#include <string.h>
+#include <time.h>
+
+#include "unit.h"
+#include "datetime.h"
+#include "trivia/util.h"
+
+static const char sample[] = "2012-12-24T15:30Z";
+
+#define S(s) {s, sizeof(s) - 1}
+struct {
+	const char *str;
+	size_t len;
+} tests[] = {
+	S("2012-12-24 15:30Z"),
+	S("2012-12-24 15:30z"),
+	S("2012-12-24 15:30"),
+	S("2012-12-24 16:30+01:00"),
+	S("2012-12-24 16:30+0100"),
+	S("2012-12-24 16:30+01"),
+	S("2012-12-24 14:30-01:00"),
+	S("2012-12-24 14:30-0100"),
+	S("2012-12-24 14:30-01"),
+	S("2012-12-24 15:30:00Z"),
+	S("2012-12-24 15:30:00z"),
+	S("2012-12-24 15:30:00"),
+	S("2012-12-24 16:30:00+01:00"),
+	S("2012-12-24 16:30:00+0100"),
+	S("2012-12-24 14:30:00-01:00"),
+	S("2012-12-24 14:30:00-0100"),
+	S("2012-12-24 15:30:00.123456Z"),
+	S("2012-12-24 15:30:00.123456z"),
+	S("2012-12-24 15:30:00.123456"),
+	S("2012-12-24 16:30:00.123456+01:00"),
+	S("2012-12-24 16:30:00.123456+01"),
+	S("2012-12-24 14:30:00.123456-01:00"),
+	S("2012-12-24 14:30:00.123456-01"),
+	S("2012-12-24t15:30Z"),
+	S("2012-12-24t15:30z"),
+	S("2012-12-24t15:30"),
+	S("2012-12-24t16:30+01:00"),
+	S("2012-12-24t16:30+0100"),
+	S("2012-12-24t14:30-01:00"),
+	S("2012-12-24t14:30-0100"),
+	S("2012-12-24t15:30:00Z"),
+	S("2012-12-24t15:30:00z"),
+	S("2012-12-24t15:30:00"),
+	S("2012-12-24t16:30:00+01:00"),
+	S("2012-12-24t16:30:00+0100"),
+	S("2012-12-24t14:30:00-01:00"),
+	S("2012-12-24t14:30:00-0100"),
+	S("2012-12-24t15:30:00.123456Z"),
+	S("2012-12-24t15:30:00.123456z"),
+	S("2012-12-24t16:30:00.123456+01:00"),
+	S("2012-12-24t14:30:00.123456-01:00"),
+	S("2012-12-24 16:30 +01:00"),
+	S("2012-12-24 14:30 -01:00"),
+	S("2012-12-24 15:30 UTC"),
+	S("2012-12-24 16:30 UTC+1"),
+	S("2012-12-24 16:30 UTC+01"),
+	S("2012-12-24 16:30 UTC+0100"),
+	S("2012-12-24 16:30 UTC+01:00"),
+	S("2012-12-24 14:30 UTC-1"),
+	S("2012-12-24 14:30 UTC-01"),
+	S("2012-12-24 14:30 UTC-01:00"),
+	S("2012-12-24 14:30 UTC-0100"),
+	S("2012-12-24 15:30 GMT"),
+	S("2012-12-24 16:30 GMT+1"),
+	S("2012-12-24 16:30 GMT+01"),
+	S("2012-12-24 16:30 GMT+0100"),
+	S("2012-12-24 16:30 GMT+01:00"),
+	S("2012-12-24 14:30 GMT-1"),
+	S("2012-12-24 14:30 GMT-01"),
+	S("2012-12-24 14:30 GMT-01:00"),
+	S("2012-12-24 14:30 GMT-0100"),
+	S("2012-12-24 14:30 -01:00"),
+	S("2012-12-24 16:30:00 +01:00"),
+	S("2012-12-24 14:30:00 -01:00"),
+	S("2012-12-24 16:30:00.123456 +01:00"),
+	S("2012-12-24 14:30:00.123456 -01:00"),
+	S("2012-12-24 15:30:00.123456 -00:00"),
+	S("20121224T1630+01:00"),
+	S("2012-12-24T1630+01:00"),
+	S("20121224T16:30+01"),
+	S("20121224T16:30 +01"),
+};
+#undef S
+
+static int
+parse_datetime(const char *str, size_t len, int64_t *secs_p,
+	       int32_t *nanosecs_p, int32_t *offset_p)
+{
+	size_t n;
+	dt_t dt;
+	char c;
+	int sec_of_day = 0, nanosecond = 0, offset = 0;
+
+	n = dt_parse_iso_date(str, len, &dt);
+	if (!n)
+		return 1;
+	if (n == len)
+		goto exit;
+
+	c = str[n++];
+	if (!(c == 'T' || c == 't' || c == ' '))
+		return 1;
+
+	str += n;
+	len -= n;
+
+	n = dt_parse_iso_time(str, len, &sec_of_day, &nanosecond);
+	if (!n)
+		return 1;
+	if (n == len)
+		goto exit;
+
+	if (str[n] == ' ')
+		n++;
+
+	str += n;
+	len -= n;
+
+	n = dt_parse_iso_zone_lenient(str, len, &offset);
+	if (!n || n != len)
+		return 1;
+
+exit:
+	*secs_p = ((int64_t)dt_rdn(dt) - DT_EPOCH_1970_OFFSET) * SECS_PER_DAY +
+		  sec_of_day - offset * 60;
+	*nanosecs_p = nanosecond;
+	*offset_p = offset;
+
+	return 0;
+}
+
+static int
+local_rd(const struct datetime *dt)
+{
+	return (int)((int64_t)dt->secs / SECS_PER_DAY) + DT_EPOCH_1970_OFFSET;
+}
+
+static int
+local_dt(const struct datetime *dt)
+{
+	return dt_from_rdn(local_rd(dt));
+}
+
+struct tm *
+datetime_to_tm(struct datetime *dt)
+{
+	static struct tm tm;
+
+	memset(&tm, 0, sizeof(tm));
+	dt_to_struct_tm(local_dt(dt), &tm);
+
+	int seconds_of_day = (int64_t)dt->secs % 86400;
+	tm.tm_hour = (seconds_of_day / 3600) % 24;
+	tm.tm_min = (seconds_of_day / 60) % 60;
+	tm.tm_sec = seconds_of_day % 60;
+
+	return &tm;
+}
+
+static void datetime_test(void)
+{
+	size_t index;
+	int64_t secs_expected;
+	int32_t nanosecs;
+	int32_t offset;
+
+	plan(355);
+	parse_datetime(sample, sizeof(sample) - 1,
+		       &secs_expected, &nanosecs, &offset);
+
+	for (index = 0; index < lengthof(tests); index++) {
+		int64_t secs;
+		int rc = parse_datetime(tests[index].str, tests[index].len,
+					&secs, &nanosecs, &offset);
+		is(rc, 0, "correct parse_datetime return value for '%s'",
+		   tests[index].str);
+		is(secs, secs_expected, "correct parse_datetime output "
+					"seconds for '%s",
+		   tests[index].str);
+
+		/*
+		 * check that stringized literal produces the same date
+		 * time fields
+		 */
+		static char buff[40];
+		struct datetime dt = {secs, nanosecs, offset};
+		/* datetime_to_tm returns time in GMT zone */
+		struct tm *p_tm = datetime_to_tm(&dt);
+		size_t len = strftime(buff, sizeof(buff), "%F %T", p_tm);
+		ok(len > 0, "strftime");
+		int64_t parsed_secs;
+		int32_t parsed_nsecs, parsed_ofs;
+		rc = parse_datetime(buff, len, &parsed_secs, &parsed_nsecs, &parsed_ofs);
+		is(rc, 0, "correct parse_datetime return value for '%s'", buff);
+		is(secs, parsed_secs,
+		   "reversible seconds via strftime for '%s", buff);
+	}
+	check_plan();
+}
+
+
+static void
+tostring_datetime_test(void)
+{
+	static struct {
+		const char *string;
+		int64_t     secs;
+		uint32_t    nsec;
+		uint32_t    offset;
+	} tests[] = {
+		{"1970-01-01T02:00+02:00",          0,         0,  120},
+		{"1970-01-01T01:30+01:30",          0,         0,   90},
+		{"1970-01-01T01:00+01:00",          0,         0,   60},
+		{"1970-01-01T00:01+00:01",          0,         0,    1},
+		{"1970-01-01T00:00Z",               0,         0,    0},
+		{"1969-12-31T23:59-00:01",          0,         0,   -1},
+		{"1969-12-31T23:00-01:00",          0,         0,  -60},
+		{"1969-12-31T22:30-01:30",          0,         0,  -90},
+		{"1969-12-31T22:00-02:00",          0,         0, -120},
+		{"1970-01-01T00:00:00.123456789Z",  0, 123456789,    0},
+		{"1970-01-01T00:00:00.123456Z",     0, 123456000,    0},
+		{"1970-01-01T00:00:00.123Z",        0, 123000000,    0},
+		{"1973-11-29T21:33:09Z",    123456789,         0,    0},
+		{"2013-10-28T17:51:56Z",   1382982716,         0,    0},
+		{"9999-12-31T23:59:59Z", 253402300799,         0,    0},
+	};
+	size_t index;
+
+	plan(15);
+	for (index = 0; index < lengthof(tests); index++) {
+		struct datetime date = {
+			tests[index].secs,
+			tests[index].nsec,
+			tests[index].offset
+		};
+		char buf[48];
+		datetime_to_string(&date, buf, sizeof(buf));
+		is(strcmp(buf, tests[index].string), 0,
+		   "string '%s' expected, received '%s'",
+		   tests[index].string, buf);
+	}
+	check_plan();
+}
+
+int
+main(void)
+{
+	plan(2);
+	datetime_test();
+	tostring_datetime_test();
+
+	return check_plan();
+}
diff --git a/test/unit/datetime.result b/test/unit/datetime.result
new file mode 100644
index 000000000..33997d9df
--- /dev/null
+++ b/test/unit/datetime.result
@@ -0,0 +1,358 @@
+1..1
+    1..355
+    ok 1 - correct parse_datetime return value for '2012-12-24 15:30Z'
+    ok 2 - correct parse_datetime output seconds for '2012-12-24 15:30Z
+    ok 3 - strftime
+    ok 4 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 5 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 6 - correct parse_datetime return value for '2012-12-24 15:30z'
+    ok 7 - correct parse_datetime output seconds for '2012-12-24 15:30z
+    ok 8 - strftime
+    ok 9 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 10 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 11 - correct parse_datetime return value for '2012-12-24 15:30'
+    ok 12 - correct parse_datetime output seconds for '2012-12-24 15:30
+    ok 13 - strftime
+    ok 14 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 15 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 16 - correct parse_datetime return value for '2012-12-24 16:30+01:00'
+    ok 17 - correct parse_datetime output seconds for '2012-12-24 16:30+01:00
+    ok 18 - strftime
+    ok 19 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 20 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 21 - correct parse_datetime return value for '2012-12-24 16:30+0100'
+    ok 22 - correct parse_datetime output seconds for '2012-12-24 16:30+0100
+    ok 23 - strftime
+    ok 24 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 25 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 26 - correct parse_datetime return value for '2012-12-24 16:30+01'
+    ok 27 - correct parse_datetime output seconds for '2012-12-24 16:30+01
+    ok 28 - strftime
+    ok 29 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 30 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 31 - correct parse_datetime return value for '2012-12-24 14:30-01:00'
+    ok 32 - correct parse_datetime output seconds for '2012-12-24 14:30-01:00
+    ok 33 - strftime
+    ok 34 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 35 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 36 - correct parse_datetime return value for '2012-12-24 14:30-0100'
+    ok 37 - correct parse_datetime output seconds for '2012-12-24 14:30-0100
+    ok 38 - strftime
+    ok 39 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 40 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 41 - correct parse_datetime return value for '2012-12-24 14:30-01'
+    ok 42 - correct parse_datetime output seconds for '2012-12-24 14:30-01
+    ok 43 - strftime
+    ok 44 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 45 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 46 - correct parse_datetime return value for '2012-12-24 15:30:00Z'
+    ok 47 - correct parse_datetime output seconds for '2012-12-24 15:30:00Z
+    ok 48 - strftime
+    ok 49 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 50 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 51 - correct parse_datetime return value for '2012-12-24 15:30:00z'
+    ok 52 - correct parse_datetime output seconds for '2012-12-24 15:30:00z
+    ok 53 - strftime
+    ok 54 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 55 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 56 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 57 - correct parse_datetime output seconds for '2012-12-24 15:30:00
+    ok 58 - strftime
+    ok 59 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 60 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 61 - correct parse_datetime return value for '2012-12-24 16:30:00+01:00'
+    ok 62 - correct parse_datetime output seconds for '2012-12-24 16:30:00+01:00
+    ok 63 - strftime
+    ok 64 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 65 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 66 - correct parse_datetime return value for '2012-12-24 16:30:00+0100'
+    ok 67 - correct parse_datetime output seconds for '2012-12-24 16:30:00+0100
+    ok 68 - strftime
+    ok 69 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 70 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 71 - correct parse_datetime return value for '2012-12-24 14:30:00-01:00'
+    ok 72 - correct parse_datetime output seconds for '2012-12-24 14:30:00-01:00
+    ok 73 - strftime
+    ok 74 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 75 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 76 - correct parse_datetime return value for '2012-12-24 14:30:00-0100'
+    ok 77 - correct parse_datetime output seconds for '2012-12-24 14:30:00-0100
+    ok 78 - strftime
+    ok 79 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 80 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 81 - correct parse_datetime return value for '2012-12-24 15:30:00.123456Z'
+    ok 82 - correct parse_datetime output seconds for '2012-12-24 15:30:00.123456Z
+    ok 83 - strftime
+    ok 84 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 85 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 86 - correct parse_datetime return value for '2012-12-24 15:30:00.123456z'
+    ok 87 - correct parse_datetime output seconds for '2012-12-24 15:30:00.123456z
+    ok 88 - strftime
+    ok 89 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 90 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 91 - correct parse_datetime return value for '2012-12-24 15:30:00.123456'
+    ok 92 - correct parse_datetime output seconds for '2012-12-24 15:30:00.123456
+    ok 93 - strftime
+    ok 94 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 95 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 96 - correct parse_datetime return value for '2012-12-24 16:30:00.123456+01:00'
+    ok 97 - correct parse_datetime output seconds for '2012-12-24 16:30:00.123456+01:00
+    ok 98 - strftime
+    ok 99 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 100 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 101 - correct parse_datetime return value for '2012-12-24 16:30:00.123456+01'
+    ok 102 - correct parse_datetime output seconds for '2012-12-24 16:30:00.123456+01
+    ok 103 - strftime
+    ok 104 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 105 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 106 - correct parse_datetime return value for '2012-12-24 14:30:00.123456-01:00'
+    ok 107 - correct parse_datetime output seconds for '2012-12-24 14:30:00.123456-01:00
+    ok 108 - strftime
+    ok 109 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 110 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 111 - correct parse_datetime return value for '2012-12-24 14:30:00.123456-01'
+    ok 112 - correct parse_datetime output seconds for '2012-12-24 14:30:00.123456-01
+    ok 113 - strftime
+    ok 114 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 115 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 116 - correct parse_datetime return value for '2012-12-24t15:30Z'
+    ok 117 - correct parse_datetime output seconds for '2012-12-24t15:30Z
+    ok 118 - strftime
+    ok 119 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 120 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 121 - correct parse_datetime return value for '2012-12-24t15:30z'
+    ok 122 - correct parse_datetime output seconds for '2012-12-24t15:30z
+    ok 123 - strftime
+    ok 124 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 125 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 126 - correct parse_datetime return value for '2012-12-24t15:30'
+    ok 127 - correct parse_datetime output seconds for '2012-12-24t15:30
+    ok 128 - strftime
+    ok 129 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 130 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 131 - correct parse_datetime return value for '2012-12-24t16:30+01:00'
+    ok 132 - correct parse_datetime output seconds for '2012-12-24t16:30+01:00
+    ok 133 - strftime
+    ok 134 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 135 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 136 - correct parse_datetime return value for '2012-12-24t16:30+0100'
+    ok 137 - correct parse_datetime output seconds for '2012-12-24t16:30+0100
+    ok 138 - strftime
+    ok 139 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 140 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 141 - correct parse_datetime return value for '2012-12-24t14:30-01:00'
+    ok 142 - correct parse_datetime output seconds for '2012-12-24t14:30-01:00
+    ok 143 - strftime
+    ok 144 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 145 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 146 - correct parse_datetime return value for '2012-12-24t14:30-0100'
+    ok 147 - correct parse_datetime output seconds for '2012-12-24t14:30-0100
+    ok 148 - strftime
+    ok 149 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 150 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 151 - correct parse_datetime return value for '2012-12-24t15:30:00Z'
+    ok 152 - correct parse_datetime output seconds for '2012-12-24t15:30:00Z
+    ok 153 - strftime
+    ok 154 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 155 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 156 - correct parse_datetime return value for '2012-12-24t15:30:00z'
+    ok 157 - correct parse_datetime output seconds for '2012-12-24t15:30:00z
+    ok 158 - strftime
+    ok 159 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 160 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 161 - correct parse_datetime return value for '2012-12-24t15:30:00'
+    ok 162 - correct parse_datetime output seconds for '2012-12-24t15:30:00
+    ok 163 - strftime
+    ok 164 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 165 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 166 - correct parse_datetime return value for '2012-12-24t16:30:00+01:00'
+    ok 167 - correct parse_datetime output seconds for '2012-12-24t16:30:00+01:00
+    ok 168 - strftime
+    ok 169 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 170 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 171 - correct parse_datetime return value for '2012-12-24t16:30:00+0100'
+    ok 172 - correct parse_datetime output seconds for '2012-12-24t16:30:00+0100
+    ok 173 - strftime
+    ok 174 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 175 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 176 - correct parse_datetime return value for '2012-12-24t14:30:00-01:00'
+    ok 177 - correct parse_datetime output seconds for '2012-12-24t14:30:00-01:00
+    ok 178 - strftime
+    ok 179 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 180 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 181 - correct parse_datetime return value for '2012-12-24t14:30:00-0100'
+    ok 182 - correct parse_datetime output seconds for '2012-12-24t14:30:00-0100
+    ok 183 - strftime
+    ok 184 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 185 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 186 - correct parse_datetime return value for '2012-12-24t15:30:00.123456Z'
+    ok 187 - correct parse_datetime output seconds for '2012-12-24t15:30:00.123456Z
+    ok 188 - strftime
+    ok 189 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 190 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 191 - correct parse_datetime return value for '2012-12-24t15:30:00.123456z'
+    ok 192 - correct parse_datetime output seconds for '2012-12-24t15:30:00.123456z
+    ok 193 - strftime
+    ok 194 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 195 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 196 - correct parse_datetime return value for '2012-12-24t16:30:00.123456+01:00'
+    ok 197 - correct parse_datetime output seconds for '2012-12-24t16:30:00.123456+01:00
+    ok 198 - strftime
+    ok 199 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 200 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 201 - correct parse_datetime return value for '2012-12-24t14:30:00.123456-01:00'
+    ok 202 - correct parse_datetime output seconds for '2012-12-24t14:30:00.123456-01:00
+    ok 203 - strftime
+    ok 204 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 205 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 206 - correct parse_datetime return value for '2012-12-24 16:30 +01:00'
+    ok 207 - correct parse_datetime output seconds for '2012-12-24 16:30 +01:00
+    ok 208 - strftime
+    ok 209 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 210 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 211 - correct parse_datetime return value for '2012-12-24 14:30 -01:00'
+    ok 212 - correct parse_datetime output seconds for '2012-12-24 14:30 -01:00
+    ok 213 - strftime
+    ok 214 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 215 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 216 - correct parse_datetime return value for '2012-12-24 15:30 UTC'
+    ok 217 - correct parse_datetime output seconds for '2012-12-24 15:30 UTC
+    ok 218 - strftime
+    ok 219 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 220 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 221 - correct parse_datetime return value for '2012-12-24 16:30 UTC+1'
+    ok 222 - correct parse_datetime output seconds for '2012-12-24 16:30 UTC+1
+    ok 223 - strftime
+    ok 224 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 225 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 226 - correct parse_datetime return value for '2012-12-24 16:30 UTC+01'
+    ok 227 - correct parse_datetime output seconds for '2012-12-24 16:30 UTC+01
+    ok 228 - strftime
+    ok 229 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 230 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 231 - correct parse_datetime return value for '2012-12-24 16:30 UTC+0100'
+    ok 232 - correct parse_datetime output seconds for '2012-12-24 16:30 UTC+0100
+    ok 233 - strftime
+    ok 234 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 235 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 236 - correct parse_datetime return value for '2012-12-24 16:30 UTC+01:00'
+    ok 237 - correct parse_datetime output seconds for '2012-12-24 16:30 UTC+01:00
+    ok 238 - strftime
+    ok 239 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 240 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 241 - correct parse_datetime return value for '2012-12-24 14:30 UTC-1'
+    ok 242 - correct parse_datetime output seconds for '2012-12-24 14:30 UTC-1
+    ok 243 - strftime
+    ok 244 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 245 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 246 - correct parse_datetime return value for '2012-12-24 14:30 UTC-01'
+    ok 247 - correct parse_datetime output seconds for '2012-12-24 14:30 UTC-01
+    ok 248 - strftime
+    ok 249 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 250 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 251 - correct parse_datetime return value for '2012-12-24 14:30 UTC-01:00'
+    ok 252 - correct parse_datetime output seconds for '2012-12-24 14:30 UTC-01:00
+    ok 253 - strftime
+    ok 254 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 255 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 256 - correct parse_datetime return value for '2012-12-24 14:30 UTC-0100'
+    ok 257 - correct parse_datetime output seconds for '2012-12-24 14:30 UTC-0100
+    ok 258 - strftime
+    ok 259 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 260 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 261 - correct parse_datetime return value for '2012-12-24 15:30 GMT'
+    ok 262 - correct parse_datetime output seconds for '2012-12-24 15:30 GMT
+    ok 263 - strftime
+    ok 264 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 265 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 266 - correct parse_datetime return value for '2012-12-24 16:30 GMT+1'
+    ok 267 - correct parse_datetime output seconds for '2012-12-24 16:30 GMT+1
+    ok 268 - strftime
+    ok 269 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 270 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 271 - correct parse_datetime return value for '2012-12-24 16:30 GMT+01'
+    ok 272 - correct parse_datetime output seconds for '2012-12-24 16:30 GMT+01
+    ok 273 - strftime
+    ok 274 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 275 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 276 - correct parse_datetime return value for '2012-12-24 16:30 GMT+0100'
+    ok 277 - correct parse_datetime output seconds for '2012-12-24 16:30 GMT+0100
+    ok 278 - strftime
+    ok 279 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 280 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 281 - correct parse_datetime return value for '2012-12-24 16:30 GMT+01:00'
+    ok 282 - correct parse_datetime output seconds for '2012-12-24 16:30 GMT+01:00
+    ok 283 - strftime
+    ok 284 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 285 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 286 - correct parse_datetime return value for '2012-12-24 14:30 GMT-1'
+    ok 287 - correct parse_datetime output seconds for '2012-12-24 14:30 GMT-1
+    ok 288 - strftime
+    ok 289 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 290 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 291 - correct parse_datetime return value for '2012-12-24 14:30 GMT-01'
+    ok 292 - correct parse_datetime output seconds for '2012-12-24 14:30 GMT-01
+    ok 293 - strftime
+    ok 294 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 295 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 296 - correct parse_datetime return value for '2012-12-24 14:30 GMT-01:00'
+    ok 297 - correct parse_datetime output seconds for '2012-12-24 14:30 GMT-01:00
+    ok 298 - strftime
+    ok 299 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 300 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 301 - correct parse_datetime return value for '2012-12-24 14:30 GMT-0100'
+    ok 302 - correct parse_datetime output seconds for '2012-12-24 14:30 GMT-0100
+    ok 303 - strftime
+    ok 304 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 305 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 306 - correct parse_datetime return value for '2012-12-24 14:30 -01:00'
+    ok 307 - correct parse_datetime output seconds for '2012-12-24 14:30 -01:00
+    ok 308 - strftime
+    ok 309 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 310 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 311 - correct parse_datetime return value for '2012-12-24 16:30:00 +01:00'
+    ok 312 - correct parse_datetime output seconds for '2012-12-24 16:30:00 +01:00
+    ok 313 - strftime
+    ok 314 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 315 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 316 - correct parse_datetime return value for '2012-12-24 14:30:00 -01:00'
+    ok 317 - correct parse_datetime output seconds for '2012-12-24 14:30:00 -01:00
+    ok 318 - strftime
+    ok 319 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 320 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 321 - correct parse_datetime return value for '2012-12-24 16:30:00.123456 +01:00'
+    ok 322 - correct parse_datetime output seconds for '2012-12-24 16:30:00.123456 +01:00
+    ok 323 - strftime
+    ok 324 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 325 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 326 - correct parse_datetime return value for '2012-12-24 14:30:00.123456 -01:00'
+    ok 327 - correct parse_datetime output seconds for '2012-12-24 14:30:00.123456 -01:00
+    ok 328 - strftime
+    ok 329 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 330 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 331 - correct parse_datetime return value for '2012-12-24 15:30:00.123456 -00:00'
+    ok 332 - correct parse_datetime output seconds for '2012-12-24 15:30:00.123456 -00:00
+    ok 333 - strftime
+    ok 334 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 335 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 336 - correct parse_datetime return value for '20121224T1630+01:00'
+    ok 337 - correct parse_datetime output seconds for '20121224T1630+01:00
+    ok 338 - strftime
+    ok 339 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 340 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 341 - correct parse_datetime return value for '2012-12-24T1630+01:00'
+    ok 342 - correct parse_datetime output seconds for '2012-12-24T1630+01:00
+    ok 343 - strftime
+    ok 344 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 345 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 346 - correct parse_datetime return value for '20121224T16:30+01'
+    ok 347 - correct parse_datetime output seconds for '20121224T16:30+01
+    ok 348 - strftime
+    ok 349 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 350 - reversible seconds via strftime for '2012-12-24 15:30:00
+    ok 351 - correct parse_datetime return value for '20121224T16:30 +01'
+    ok 352 - correct parse_datetime output seconds for '20121224T16:30 +01
+    ok 353 - strftime
+    ok 354 - correct parse_datetime return value for '2012-12-24 15:30:00'
+    ok 355 - reversible seconds via strftime for '2012-12-24 15:30:00
+ok 1 - subtests
diff --git a/third_party/c-dt b/third_party/c-dt
new file mode 160000
index 000000000..3cbbbc7f0
--- /dev/null
+++ b/third_party/c-dt
@@ -0,0 +1 @@
+Subproject commit 3cbbbc7f032cfa67a8df9f81101403249825d7f3
-- 
2.29.2



More information about the Tarantool-patches mailing list