Tarantool development patches archive
 help / color / mirror / Atom feed
* [Tarantool-patches] [PATCH 0/n] Datetime module implementation, stage #1
@ 2021-09-10 17:50 Timur Safin via Tarantool-patches
  2021-09-10 17:50 ` [Tarantool-patches] [PATCH 1/n] build, lua: built-in module datetime Timur Safin via Tarantool-patches
  2021-09-14 22:45 ` [Tarantool-patches] [PATCH 0/n] Datetime module implementation, stage #1 Vladislav Shpilevoy via Tarantool-patches
  0 siblings, 2 replies; 4+ messages in thread
From: Timur Safin via Tarantool-patches @ 2021-09-10 17:50 UTC (permalink / raw)
  To: v.shpilevoy; +Cc: tarantool-patches

After we have failed to get any consensus on prior version of 
datetime module, we have returned to the whiteboard and proceeded
"the procedure":

- PM creates initial design spec, we discuss, and only once 
  consensus achieved...;
- ... we implement it gradually, stage by stage.

Although, Mons' RFC for datetime module is not yet fully finished,
we have agreed that it's enough for stage #1 - 
https://hackmd.io/@Mons/S1Vfc_axK#%D0%AD%D1%82%D0%B0%D0%BF-1

The patch #1/n is an extraction of functionality needed for stage #1.

There is much fuller implementaton though, which you may find here
https://github.com/tarantool/tarantool/pull/6426
in addition to initial datetime support created by 1st patch, pull
request contain:

- Preliminary interval support via `date:add{}` and `date:sub{}` methods,
  more interval functionality to be added there soon;
- messagepack, json, and yaml serialization;
- box persistence and indices support;

Note, that these things ratified in RFC are neither yet presented in a
mentioned pull-request, nor in this patch:

- there is no yet %f support in format(). It will be done in the next 
  version of this patch;
- there is no Olson integration support necessary for timezone identifiers 
  parsing and tzindex support.

Original issue: 
- https://github.com/tarantool/tarantool/issues/5941
Full branch:
- https://github.com/tarantool/tarantool/tree/tsafin/gh-5941-datetime-take2-wip
Pull request for fuller branch:
- https://github.com/tarantool/tarantool/pull/6426


Timur Safin (1):
  build, lua: built-in module datetime

 .gitmodules                    |   3 +
 CMakeLists.txt                 |   8 +
 cmake/BuildCDT.cmake           |  10 +
 extra/exports                  |  37 ++
 src/CMakeLists.txt             |   5 +-
 src/lib/core/CMakeLists.txt    |   1 +
 src/lib/core/datetime.c        | 121 +++++++
 src/lib/core/datetime.h        |  94 +++++
 src/lua/datetime.lua           | 623 +++++++++++++++++++++++++++++++++
 src/lua/init.c                 |   4 +-
 src/lua/utils.c                |  19 +
 src/lua/utils.h                |  11 +
 test/app-tap/datetime.test.lua | 203 +++++++++++
 test/unit/CMakeLists.txt       |   2 +
 test/unit/datetime.c           | 261 ++++++++++++++
 test/unit/datetime.result      | 358 +++++++++++++++++++
 third_party/c-dt               |   1 +
 17 files changed, 1759 insertions(+), 2 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

-- 
2.29.2


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [Tarantool-patches] [PATCH 1/n] build, lua: built-in module datetime
  2021-09-10 17:50 [Tarantool-patches] [PATCH 0/n] Datetime module implementation, stage #1 Timur Safin via Tarantool-patches
@ 2021-09-10 17:50 ` Timur Safin via Tarantool-patches
  2021-09-14 21:53   ` Safin Timur via Tarantool-patches
  2021-09-14 22:45 ` [Tarantool-patches] [PATCH 0/n] Datetime module implementation, stage #1 Vladislav Shpilevoy via Tarantool-patches
  1 sibling, 1 reply; 4+ messages in thread
From: Timur Safin via Tarantool-patches @ 2021-09-10 17:50 UTC (permalink / raw)
  To: v.shpilevoy; +Cc: tarantool-patches

Introduce new builtin Tarantool module `datetime.lua` with
partial proxying to `c-dt` library for datetime parsing and
arithmetics.

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
  changes, which have provided 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.

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, but using
  prefix `tnt_dt_*` to avoid possible name clashes.

* `strftime` implemented as a simple ffi wrappers in the Tarantool
  kernel code.

  **TODO** to setablish %f format which we want for nanoseconds
  display we plan to slightly modify Olson strftime implementation
  and thus bundle it with kernel code. Not today, though.

* display datetime

  - introduced output routine for converting datetime
    to their default output (ISO-8601) format. This routine used
    by tostring() if given datatime object;

* 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()

Example,

```
local dt = datetime.new {
	nsec      = 123456789,
	usec      = 123456,
	msec      = 123,

	sec       = 19,
	min       = 29,
	hour      = 18,

	day       = 20,
	month     = 8,
	year      = 2021,

	timestamp = 1629476485.123,

	tzoffset  = 180,
}

local t = dt:totable()
--[[
	t.nsec
	t.sec
	t.min
	t.hour
	t.day
	t.month
	t.year

	t.isdst
	t.wday
	t.yday
--]]

dt:format()   -- 2021-08-21T14:53:34.032Z
dt:format('%Y-%m-%dT%H:%M:%S')   -- 2021-08-21T14:53:34

dt:set {
	nsec      = 123456789,
	usec      = 123456,
	msec      = 123,

	sec       = 19,
	min       = 29,
	hour      = 18,

	day       = 20,
	month     = 8,
	year      = 2021,

	tzoffset  = 180,

}
dt:set {
	timestamp = 1629476485.124,

	tzoffset  = 180,
}

```

Coverage is

	builtin/datetime.lua 238  43     84.70%

Part of #5941

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

`datetime` module has been introduced, which allows to create
and modify timestamp objects.

Please refer to https://hackmd.io/@Mons/S1Vfc_axK#Datetime-in-Tarantool
for more detailed description of module API.

Part of #5941
---
 .gitmodules                    |   3 +
 CMakeLists.txt                 |   8 +
 cmake/BuildCDT.cmake           |  10 +
 extra/exports                  |  37 ++
 src/CMakeLists.txt             |   5 +-
 src/lib/core/CMakeLists.txt    |   1 +
 src/lib/core/datetime.c        | 121 +++++++
 src/lib/core/datetime.h        |  94 +++++
 src/lua/datetime.lua           | 623 +++++++++++++++++++++++++++++++++
 src/lua/init.c                 |   4 +-
 src/lua/utils.c                |  19 +
 src/lua/utils.h                |  11 +
 test/app-tap/datetime.test.lua | 203 +++++++++++
 test/unit/CMakeLists.txt       |   2 +
 test/unit/datetime.c           | 261 ++++++++++++++
 test/unit/datetime.result      | 358 +++++++++++++++++++
 third_party/c-dt               |   1 +
 17 files changed, 1759 insertions(+), 2 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..1416a6b3f 100644
--- a/extra/exports
+++ b/extra/exports
@@ -148,6 +148,10 @@ csv_feed
 csv_iterator_create
 csv_next
 csv_setopt
+datetime_now
+datetime_strftime
+datetime_to_string
+datetime_unpack
 decimal_from_string
 decimal_unpack
 error_ref
@@ -447,6 +451,39 @@ title_set_status
 title_update
 tnt_default_cert_dir_paths
 tnt_default_cert_file_paths
+tnt_dt_add_months
+tnt_dt_add_quarters
+tnt_dt_add_years
+tnt_dt_days_in_month
+tnt_dt_days_in_quarter
+tnt_dt_days_in_year
+tnt_dt_dom
+tnt_dt_dow
+tnt_dt_doy
+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_leap_year
+tnt_dt_month
+tnt_dt_parse_iso_date
+tnt_dt_parse_iso_time
+tnt_dt_parse_iso_time_basic
+tnt_dt_parse_iso_time_extended
+tnt_dt_parse_iso_zone
+tnt_dt_parse_iso_zone_basic
+tnt_dt_parse_iso_zone_extended
+tnt_dt_parse_iso_zone_lenient
+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_dt_weeks_in_year
+tnt_dt_year
 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..4f2b59bd8
--- /dev/null
+++ b/src/lib/core/datetime.c
@@ -0,0 +1,121 @@
+/*
+ * 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->epoch;
+	dt_to_struct_tm(local_dt(secs), &tm);
+
+	int seconds_of_day = (int64_t)date->epoch % 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->epoch = 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->tzoffset = tm.tm_gmtoff / 60;
+}
+
+size_t
+datetime_strftime(char *buf, uint32_t len, const char *fmt,
+		  const struct datetime *date)
+{
+	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(char *buf, int len, const struct datetime *date)
+{
+	int offset = date->tzoffset;
+	/* 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->epoch + 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:%02d",
+		year, month, day, hour, minute, second);
+	if (nanosec != 0) {
+		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..7fa2c14a6
--- /dev/null
+++ b/src/lib/core/datetime.h
@@ -0,0 +1,94 @@
+#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 epoch;
+	/** Nanoseconds, if any. */
+	int32_t nsec;
+	/** Offset in minutes from UTC. */
+	int16_t tzoffset;
+	/** Olson timezone id */
+	int16_t tzindex;
+};
+
+/**
+ * Required size of datetime_to_string string buffer
+ */
+#define DT_TO_STRING_BUFSIZE   48
+
+/*
+ * Compare arguments of a datetime type
+ * @param lhs left datetime argument
+ * @param rhs right datetime argument
+ * @retval < 0 if lhs less than rhs
+ * @retval = 0 if lhs and rhs equal
+ * @retval > 0 if lhs greater than rhs
+ */
+int
+datetime_compare(const struct datetime *lhs, const struct datetime *rhs);
+
+/**
+ * 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(char *buf, int len, const struct datetime *date);
+
+/**
+ * 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(char *buf, uint32_t len, const char *fmt,
+		  const struct datetime *date);
+
+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..f63386716
--- /dev/null
+++ b/src/lua/datetime.lua
@@ -0,0 +1,623 @@
+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.
+]]
+
+ffi.cdef [[
+
+/* dt_core.h definitions */
+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);
+void   tnt_dt_to_ymd       (dt_t dt, int *y, int *m, int *d);
+void   tnt_dt_to_yqd       (dt_t dt, int *y, int *q, int *d);
+void   tnt_dt_to_ywd       (dt_t dt, int *y, int *w, int *d);
+
+int    tnt_dt_rdn          (dt_t dt);
+dt_dow_t tnt_dt_dow        (dt_t dt);
+
+/* dt_util.h */
+bool    tnt_dt_leap_year       (int y);
+int     tnt_dt_days_in_year    (int y);
+int     tnt_dt_days_in_quarter (int y, int q);
+int     tnt_dt_days_in_month   (int y, int m);
+int     tnt_dt_weeks_in_year   (int y);
+
+/* dt_accessor.h */
+
+int     tnt_dt_year         (dt_t dt);
+int     tnt_dt_month        (dt_t dt);
+int     tnt_dt_doy          (dt_t dt);
+int     tnt_dt_dom          (dt_t dt);
+
+/* dt_arithmetic.h definitions */
+
+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 */
+
+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 */
+
+int    datetime_to_string(char *buf, int len, const struct datetime * date);
+size_t datetime_strftime(char *buf, uint32_t len, const char *fmt,
+                        const struct datetime *date);
+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_table(o, message)
+    if type(o) ~= 'table' then
+        return error(("%s: expected table %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
+
+-- range may be of a form of pair {begin, end} or
+-- tuple {begin, end, negative}
+-- negative is a special value (so far) used for days only
+local function check_range(v, range, txt)
+    local len = #range
+    assert(len == 2 or len == 3)
+
+    local left, right, neg = unpack(range)
+    if neg == v or (v >= left and v <= right) then
+        return
+    end
+
+    if neg == nil then
+        error(('value %d of %s is out of allowed range [%d, %d]'):
+              format(v, txt, left, right), 2)
+    else
+        error(('value %d of %s is out of allowed range [%d, %d..%d]'):
+              format(v, txt, neg, left, right), 2)
+    end
+end
+
+local function nyi(msg)
+    local text = 'Not yet implemented'
+    if msg ~= nil then
+        text = ("%s : '%s'"):format(text, msg)
+    end
+    error(text, 3)
+end
+
+-- offset if __seconds__ of 1970-01-01 from 0000-01-01
+local SECS_EPOCH_OFFSET = (DT_EPOCH_1970_OFFSET * SECS_PER_DAY)
+
+-- convert from epoch related time to Rata Die related
+local function local_rd(secs)
+    return math_floor((secs + SECS_EPOCH_OFFSET) / SECS_PER_DAY)
+end
+
+-- convert UTC seconds to local seconds, adjusting by timezone
+local function local_secs(obj)
+    return obj.epoch + obj.tzoffset * 60
+end
+
+local function utc_secs(epoch, tzoffset)
+    return epoch - tzoffset * 60
+end
+
+-- get epoch seconds, shift to the local timezone
+-- adjust from 1970-related to 0000-related time
+-- then return dt in those coordinates (number of days
+-- since Rata Die date)
+local function local_dt(obj)
+    return builtin.tnt_dt_from_rdn(local_rd(local_secs(obj)))
+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.epoch - rhs.epoch
+    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 { epoch = self.epoch, nsec = self.nsec,
+             tzoffset = self.tzoffset, tzindex = 0 }
+end
+
+local parse_zone
+
+local function datetime_new_raw(epoch, nsec, tzoffset, tzindex)
+    local dt_obj = ffi.new(datetime_t)
+    dt_obj.epoch = epoch
+    dt_obj.nsec = nsec
+    dt_obj.tzoffset = tzoffset or 0
+    dt_obj.tzindex = tzindex or 0
+    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
+
+local function get_timezone(offset)
+    if type(offset) == 'number' then
+        return offset
+    elseif type(offset) == 'string' then
+        return parse_zone(offset)
+    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 hms = false
+    local dt = DT_EPOCH_1970_OFFSET -- default is 1970-01-01
+
+    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, -1}, '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 s = obj.sec
+    if s ~= nil then
+        check_range(s, {0, 60}, 'sec')
+        hms = true
+    end
+    local nsec, usec, msec = obj.nsec, obj.usec, obj.msec
+    -- if there are separate nsec, usec, or msec provided then
+    -- timestamp should be integer
+    local int_ts = nsec ~= nil or usec ~= nil or msec ~= nil
+
+    local ts = obj.timestamp
+    local fraction
+    if ts ~= nil then
+        s, fraction = math_modf(ts)
+        if not int_ts then
+            nsec = fraction * 1e9
+        end
+        hms = true
+    end
+
+    local offset = obj.tzoffset
+    if offset ~= nil then
+        offset = get_timezone(offset)
+        check_range(offset, {-720, 720}, offset)
+    end
+
+    if obj.tz ~= nil then
+        nyi('tz')
+    end
+
+    -- .year, .month, .day
+    if ymd then
+        y = y or 1970
+        M = M or 1
+        if d ~= nil and d < 0 then
+            d = builtin.tnt_dt_days_in_month(y, M)
+        end
+        dt = builtin.tnt_dt_from_ymd(y, M, d)
+    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 or 0)
+end
+
+local sz = 48
+local buff = ffi.new('char[?]', sz)
+
+--[[
+    Convert to text datetime values
+
+    - datetime will use ISO-8601 format:
+        1970-01-01T00:00Z
+        2021-08-18T16:57:08.981725+03:00
+]]
+local function datetime_tostring(self)
+    check_date(self, 'datetime.tostring()')
+
+    local len = builtin.datetime_to_string(buff, sz, self)
+    assert(len < sz)
+    return ffi.string(buff)
+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)
+    if len == 0 then
+        error(('invalid time-zone format %s'):format(str), 3)
+    end
+    return 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)
+    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
+
+--[[
+    Return table in os.date('*t') format, but with timezone
+    and nanoseconds
+]]
+local function datetime_totable(self)
+    local secs = local_secs(self) -- hour:minute should be in local timezone
+    local dt = local_dt(self)
+
+    return {
+        year = builtin.tnt_dt_year(dt),
+        month = builtin.tnt_dt_month(dt),
+        yday = builtin.tnt_dt_doy(dt),
+        day = builtin.tnt_dt_dom(dt),
+        wday = (ffi.cast('int32_t', builtin.tnt_dt_dow(dt)) + 1) % 7,
+        hour = math_floor((secs / 3600) % 24),
+        min = math_floor((secs / 60) % 60),
+        sec = secs % 60,
+        isdst = false, -- FIXME - after we introduced timezone/DST support
+        nsec = self.nsec,
+        tzoffset = self.tzoffset,
+        -- tz = "MSK", -- FIXME - after we introduced timezone support
+    }
+end
+
+local function datetime_update_dt(self, dt, new_offset)
+    local epoch = local_secs(self)
+    local secs_day = epoch % SECS_PER_DAY
+    epoch = (builtin.tnt_dt_rdn(dt) - DT_EPOCH_1970_OFFSET) * SECS_PER_DAY + secs_day
+    self.epoch = utc_secs(epoch, new_offset)
+end
+
+local function datetime_ymd_update(self, y, M, d, new_offset)
+    if d < 0 then
+        d = builtin.tnt_dt_days_in_month(y, M)
+    end
+    if d > 28 then
+        local day_in_month = builtin.tnt_dt_days_in_month(y, M)
+        if d > day_in_month then
+            error(('invalid number of days %d in month %d for %d'):
+                  format(d, M, y), 3)
+        end
+    end
+    local dt = builtin.tnt_dt_from_ymd(y or 0, M or 1, d or 1)
+    datetime_update_dt(self, dt, new_offset)
+end
+
+local function datetime_hms_update(self, h, m, s, new_offset)
+    local epoch = local_secs(self)
+    local secs_day = epoch - (epoch % SECS_PER_DAY)
+    self.epoch = utc_secs(secs_day + h * 3600 + m * 60 + s, new_offset)
+end
+
+local function bool2int(b)
+    return b and 1 or 0
+end
+
+local function datetime_set(self, obj)
+    check_table(obj, "datetime.set()")
+
+    local ymd = false
+    local hms = false
+
+    local dt = local_dt(self)
+    local y0 = ffi.new('int[1]')
+    local M0 = ffi.new('int[1]')
+    local d0 = ffi.new('int[1]')
+    builtin.tnt_dt_to_ymd(dt, y0, M0, d0)
+    y0, M0, d0 = y0[0], M0[0], d0[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, -1}, 'day')
+        ymd = true
+    end
+
+    local lsecs = local_secs(self)
+    local h0 = math_floor(lsecs / (24 * 60)) % 24
+    local m0 = math_floor(lsecs / 60) % 60
+    local sec0 = lsecs % 60
+
+    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 sec = obj.sec
+    if sec ~= nil then
+        check_range(sec, {0, 60}, 'sec')
+        hms = true
+    end
+
+    local nsec, usec, msec = obj.nsec, obj.usec, obj.msec
+    local count_usec = bool2int(nsec ~= nil) + bool2int(usec ~= nil) +
+                       bool2int(msec ~= nil)
+    if count_usec > 1 then
+        error('only one of nsec, usec or msecs may defined simultaneously', 2)
+    end
+    if usec ~= nil then
+        nsec = usec * 1e3
+    elseif msec ~= nil then
+        nsec = msec * 1e6
+    end
+
+    local ts = obj.timestamp
+    if ts ~= nil then
+        local sec_int, fraction
+        sec_int, fraction = math_modf(ts)
+        -- if there is one of nsec, usec, msec provided
+        -- then ignore fraction in timestamp
+        -- otherwise - use nsec, usec, or msec
+        if count_usec == 0 then
+            nsec = fraction * 1e9
+        end
+
+        self.secs = sec_int
+        self.nsec = nsec
+
+        return self
+    end
+
+    local offset0 = self.tzoffset
+    local offset = obj.tzoffset
+    if offset ~= nil then
+        offset = get_timezone(offset)
+        check_range(offset, {-720, 720}, 'tzoffset')
+        -- self.tzoffset = offset
+    end
+
+    if obj.tz ~= nil then
+        nyi('tz')
+    end
+
+    -- .year, .month, .day
+    if ymd then
+        datetime_ymd_update(self, y or y0, M or M0, d or d0, offset or offset0)
+    end
+
+    -- .hour, .minute, .second
+    if hms then
+        datetime_hms_update(self, h or h0, m or m0, sec or sec0, offset or offset0)
+    end
+
+    self.epoch, self.nsec = normalize_nsec(self.epoch, self.nsec)
+
+    if offset ~= nil then
+        self.tzoffset = offset
+    end
+
+    return self
+end
+
+local strfmt_sz = 128
+local strfmt_buff = ffi.new('char[?]', strfmt_sz)
+
+local function datetime_strftime(self, fmt)
+    check_date(self, "datetime.strftime()")
+    builtin.datetime_strftime(strfmt_buff, strfmt_sz, fmt, self)
+    return ffi.string(strfmt_buff)
+end
+
+local function datetime_format(self, fmt)
+    if fmt ~= nil then
+        return datetime_strftime(self, fmt)
+    else
+        return datetime_tostring(self)
+    end
+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.epoch end,
+        timestamp = function(self) return self.epoch + self.nsec / 1e9 end,
+
+        nsec = function(self) return self.nsec end,
+        usec = function(self) return self.nsec / 1e3 end,
+        msec = function(self) return self.nsec / 1e6 end,
+
+        dt = function(self) return local_dt(self) end,
+        year = function(self) return builtin.tnt_dt_year(local_dt(self)) end,
+        month = function(self) return builtin.tnt_dt_month(local_dt(self)) end,
+
+        yday = function(self) return builtin.tnt_dt_doy(local_dt(self)) end,
+        wday = function(self)
+            return ffi.cast('int32_t', builtin.tnt_dt_dow(local_dt(self)))
+        end,
+        day = function(self)
+            return builtin.tnt_dt_dom(local_dt(self))
+        end,
+        hour = function(self) return math_floor((local_secs(self) / 3600) % 24) end,
+        minute = function(self) return math_floor((local_secs(self) / 60) % 60) end,
+        second = function(self) return self.epoch % 60 end,
+
+        format = datetime_format,
+        totable = datetime_totable,
+        set = datetime_set,
+    }
+})
+
+return setmetatable(
+    {
+        new         = datetime_new,
+
+        now         = local_now,
+
+        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..b25656a1a 100644
--- a/src/lua/utils.c
+++ b/src/lua/utils.c
@@ -48,6 +48,8 @@ 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;
+
 
 void *
 luaL_pushcdata(struct lua_State *L, uint32_t ctypeid)
@@ -120,6 +122,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 +733,17 @@ 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 epoch;"
+			  "int32_t nsec;"
+			  "int16_t tzoffset;"
+			  "int16_t tzindex;"
+			  "};");
+	assert(rc == 0);
+	(void) rc;
+	CTID_DATETIME = luaL_ctypeid(L, "struct datetime");
+	assert(CTID_DATETIME != 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..9c08947cc 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,7 @@ 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;
 
 struct tt_uuid *
 luaL_pushuuid(struct lua_State *L);
@@ -78,6 +80,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..4740c800d
--- /dev/null
+++ b/test/app-tap/datetime.test.lua
@@ -0,0 +1,203 @@
+#!/usr/bin/env tarantool
+
+local tap = require('tap')
+local test = tap.test("errno")
+local date = require('datetime')
+
+test:plan(7)
+
+local function assert_raises(test, error_msg, func, ...)
+    local ok, err = pcall(func, ...)
+    local err_tail = err:gsub("^.+:%d+: ", "")
+    return test:ok(not ok and err_tail == error_msg,
+                   ('"%s" received, "%s" expected'):format(err_tail, error_msg))
+end
+
+test:test("Default date creation", function(test)
+    test:plan(9)
+    -- check empty arguments
+    local T1 = date.new()
+    test:is(T1.epoch, 0, "T.epoch ==0")
+    test:is(T1.nsec, 0, "T.nsec == 0")
+    test:is(T1.tzoffset, 0, "T.tzoffset == 0")
+    test:is(tostring(T1), "1970-01-01T00:00:00Z", "tostring(T1)")
+    -- check empty table
+    local T2 = date.new{}
+    test:is(T2.epoch, 0, "T.epoch ==0")
+    test:is(T2.nsec, 0, "T.nsec == 0")
+    test:is(T2.tzoffset, 0, "T.tzoffset == 0")
+    test:is(tostring(T2), "1970-01-01T00:00:00Z", "tostring(T2)")
+    -- check their equivalence
+    test:is(T1, T2, "T1 == T2")
+end)
+
+test:test("Datetime string formatting", function(test)
+    test:plan(6)
+    local t = date()
+    test:is(t.epoch, 0, ('t.epoch == %d'):format(tonumber(t.epoch)))
+    test:is(t.nsec, 0, ('t.nsec == %d'):format(t.nsec))
+    test:is(t.tzoffset, 0, ('t.tzoffset == %d'):format(t.tzoffset))
+    test:is(t:format('%d/%m/%Y'), '01/01/1970', '%s: format #1')
+    test:is(t:format('%A %d. %B %Y'), 'Thursday 01. January 1970', 'format #2')
+    test:is(t:format('%FT%T%z'), '1970-01-01T00:00:00+0000', 'format #3')
+end)
+
+test:test("totable{}", function(test)
+    test:plan(9)
+    local exp = {sec = 0, min = 0, wday = 5, day = 1,
+                 nsec = 0, isdst = false, yday = 1,
+                 tzoffset = 0, month = 1, year = 1970, hour = 0}
+    local T = date.new()
+    local TT = T:totable()
+    test:is_deeply(TT, exp, "date:totable()")
+
+    local D = os.date('*t')
+    TT = date.new(D):totable()
+    local keys = {
+        'sec', 'min', 'wday', 'day', 'yday', 'month', 'year', 'hour'
+    }
+    for _, key in pairs(keys) do
+        test:is(TT[key], D[key], ("[%s]: %s == %s"):format(key, TT[key], D[key]))
+    end
+end)
+
+test:test("Time :set{} operations", function(test)
+    test:plan(8)
+
+    local T = date.new{ year = 2021, month = 8, day = 31,
+                  hour = 0, min = 31, sec = 11, tzoffset = '+0300'}
+    test:is(tostring(T), '2021-08-31T00:31:11+03:00', 'initial')
+    test:is(tostring(T:set{ year = 2020 }), '2020-08-31T00:31:11+03:00', '2020 year')
+    test:is(tostring(T:set{ month = 11, day = 30 }), '2020-11-30T00:31:11+03:00', 'month = 11, day = 30')
+    test:is(tostring(T:set{ day = 9 }), '2020-11-09T00:31:11+03:00', 'day 9')
+    test:is(tostring(T:set{ hour = 6 }),  '2020-11-09T06:31:11+03:00', 'hour 6')
+    test:is(tostring(T:set{ min = 12, sec = 23 }), '2020-11-09T04:12:23+03:00', 'min 12, sec 23')
+    test:is(tostring(T:set{ tzoffset = -8*60 }), '2020-11-08T17:12:23-08:00', 'offset -0800' )
+    test:is(tostring(T:set{ tzoffset = '+0800' }), '2020-11-09T09:12:23+08:00', 'offset +0800' )
+end)
+
+local function range_check_error(name, value, range)
+    return ('value %s of %s is out of allowed range [%d, %d]'):
+              format(value, name, range[1], range[2])
+end
+
+local function range_check_3_error(v)
+    return ('value %d of %s is out of allowed range [%d, %d..%d]'):
+            format(v, 'day', -1, 1, 31)
+end
+
+test:test("Time invalid :set{} operations", function(test)
+    test:plan(17)
+
+    local T = date.new{}
+
+    assert_raises(test, range_check_error('year', 10000, {1, 9999}),
+                  function() T:set{ year = 10000} end)
+    assert_raises(test, range_check_error('year', -10, {1, 9999}),
+                  function() T:set{ year = -10} end)
+
+    assert_raises(test, range_check_error('month', 20, {1, 12}),
+                  function() T:set{ month = 20} end)
+    assert_raises(test, range_check_error('month', 0, {1, 12}),
+                  function() T:set{ month = 0} end)
+    assert_raises(test, range_check_error('month', -20, {1, 12}),
+                  function() T:set{ month = -20} end)
+
+    assert_raises(test,  range_check_3_error(40),
+                  function() T:set{ day = 40} end)
+    assert_raises(test,  range_check_3_error(0),
+                  function() T:set{ day = 0} end)
+    assert_raises(test,  range_check_3_error(-10),
+                  function() T:set{ day = -10} end)
+
+    assert_raises(test,  range_check_error('hour', 31, {0, 23}),
+                  function() T:set{ hour = 31} end)
+    assert_raises(test,  range_check_error('hour', -1, {0, 23}),
+                  function() T:set{ hour = -1} end)
+
+    assert_raises(test,  range_check_error('min', 60, {0, 59}),
+                  function() T:set{ min = 60} end)
+    assert_raises(test,  range_check_error('min', -1, {0, 59}),
+                  function() T:set{ min = -1} end)
+
+    assert_raises(test,  range_check_error('sec', 61, {0, 60}),
+                  function() T:set{ sec = 61} end)
+    assert_raises(test,  range_check_error('sec', -1, {0, 60}),
+                  function() T:set{ sec = -1} end)
+
+    local only1 = 'only one of nsec, usec or msecs may defined simultaneously'
+    assert_raises(test, only1, function()
+                    T:set{ nsec = 123456, usec = 123}
+                  end)
+    assert_raises(test, only1, function()
+                    T:set{ nsec = 123456, msec = 123}
+                  end)
+    assert_raises(test, only1, function()
+                    T:set{ nsec = 123456, usec = 1234, msec = 123}
+                  end)
+end)
+
+local function invalid_tz_fmt_error(val)
+    return ('invalid time-zone format %s'):format(val)
+end
+
+test:test("Time invalid tzoffset in :set{} operations", function(test)
+    test:plan(10)
+
+    local T = date.new{}
+    local bad_strings = {
+        'bogus',
+        '0100',
+        '+-0100',
+        '+25:00',
+        '+99:00',
+        '-99:00',
+    }
+    for _, val in ipairs(bad_strings) do
+        assert_raises(test, invalid_tz_fmt_error(val),
+                      function() T:set{ tzoffset = val } end)
+    end
+
+    local bad_numbers = {
+        800,
+        -800,
+        10000,
+        -10000,
+    }
+    for _, val in ipairs(bad_numbers) do
+        assert_raises(test, range_check_error('tzoffset', val, {-720, 720}),
+                      function() T:set{ tzoffset = val } end)
+    end
+end)
+
+
+test:test("Time :set{day = -1} operations", function(test)
+    test:plan(14)
+    local tests = {
+        {{ year = 2000, month = 3, day = -1}, '2000-03-31T00:00:00Z'},
+        {{ year = 2000, month = 2, day = -1}, '2000-02-29T00:00:00Z'},
+        {{ year = 2001, month = 2, day = -1}, '2001-02-28T00:00:00Z'},
+        {{ year = 1900, month = 2, day = -1}, '1900-02-28T00:00:00Z'},
+        {{ year = 1904, month = 2, day = -1}, '1904-02-29T00:00:00Z'},
+    }
+    local T
+    for _, row in ipairs(tests) do
+        local args, str = unpack(row)
+        T = date.new(args)
+        test:is(tostring(T), str, ('checking -1 with %s'):format(str))
+    end
+    assert_raises(test, range_check_3_error(0), function() T = date.new{day = 0} end)
+    assert_raises(test, range_check_3_error(-2), function() T = date.new{day = -2} end)
+    assert_raises(test, range_check_3_error(-10), function() T = date.new{day = -10} end)
+
+    T = date.new{ year = 1904, month = 2, day = -1 }
+    test:is(tostring(T), '1904-02-29T00:00:00Z', 'base before :set{}')
+    test:is(tostring(T:set{month = 3, day = 2}), '1904-03-02T00:00:00Z', '2 March')
+    test:is(tostring(T:set{day = -1}), '1904-03-31T00:00:00Z', '31 March')
+
+    assert_raises(test, range_check_3_error(0), function() T:set{day = 0} end)
+    assert_raises(test, range_check_3_error(-2), function() T:set{day = -2} end)
+    assert_raises(test, range_check_3_error(-10), function() T:set{day = -10} end)
+end)
+
+os.exit(test:check() and 0 or 1)
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index fc9b8abd2..5662e89f2 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -58,6 +58,8 @@ add_executable(random.test random.c core_test_utils.c)
 target_link_libraries(random.test core unit)
 add_executable(xmalloc.test xmalloc.c core_test_utils.c)
 target_link_libraries(xmalloc.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)
diff --git a/test/unit/datetime.c b/test/unit/datetime.c
new file mode 100644
index 000000000..f289e66dd
--- /dev/null
+++ b/test/unit/datetime.c
@@ -0,0 +1,261 @@
+#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->epoch / 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->epoch % 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, 0};
+		/* 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,
+			0
+		};
+		char buf[48];
+		datetime_to_string(buf, sizeof(buf), &date);
+		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..cbb3fc27c
--- /dev/null
+++ b/third_party/c-dt
@@ -0,0 +1 @@
+Subproject commit cbb3fc27c104aa7703b01a4108ce7871e1a28a1c
-- 
2.29.2


^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [Tarantool-patches] [PATCH 1/n] build, lua: built-in module datetime
  2021-09-10 17:50 ` [Tarantool-patches] [PATCH 1/n] build, lua: built-in module datetime Timur Safin via Tarantool-patches
@ 2021-09-14 21:53   ` Safin Timur via Tarantool-patches
  0 siblings, 0 replies; 4+ messages in thread
From: Safin Timur via Tarantool-patches @ 2021-09-14 21:53 UTC (permalink / raw)
  To: v.shpilevoy; +Cc: tarantool-patches



On 10.09.2021 20:50, Timur Safin wrote:
...
> +    local ts = obj.timestamp
> +    if ts ~= nil then
> +        local sec_int, fraction
> +        sec_int, fraction = math_modf(ts)
> +        -- if there is one of nsec, usec, msec provided
> +        -- then ignore fraction in timestamp
> +        -- otherwise - use nsec, usec, or msec
> +        if count_usec == 0 then
> +            nsec = fraction * 1e9
> +        end
> +
> +        self.secs = sec_int
> +        self.nsec = nsec
> +
> +        return self
> +    end
> +

Discovered the harder way (via hitting runtime exception) that I've not 
renamed .secs access here to .epoch field access.

-------------------------------------------------------------
diff --git a/src/lua/datetime.lua b/src/lua/datetime.lua
index 4c6471bed..540a5a940 100644
--- a/src/lua/datetime.lua
+++ b/src/lua/datetime.lua
@@ -664,7 +664,7 @@ local function datetime_set(self, obj)
              nsec = fraction * 1e9
          end

-        self.secs = sec_int
+        self.epoch = sec_int
          self.nsec = nsec

          return self
-------------------------------------------------------------

This is an indication that I've not tested date:set{timestamp = N} 
access thouroughly, and more tests cases needed. Will update 1st commit 
to branch tsafin/gh-5941-datetime-take2-wip soon with the corrected code 
and more tests.

Timur


^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [Tarantool-patches] [PATCH 0/n] Datetime module implementation, stage #1
  2021-09-10 17:50 [Tarantool-patches] [PATCH 0/n] Datetime module implementation, stage #1 Timur Safin via Tarantool-patches
  2021-09-10 17:50 ` [Tarantool-patches] [PATCH 1/n] build, lua: built-in module datetime Timur Safin via Tarantool-patches
@ 2021-09-14 22:45 ` Vladislav Shpilevoy via Tarantool-patches
  1 sibling, 0 replies; 4+ messages in thread
From: Vladislav Shpilevoy via Tarantool-patches @ 2021-09-14 22:45 UTC (permalink / raw)
  To: Timur Safin; +Cc: tarantool-patches

I am not sure how can I review this here, since this patchset contains
only the first commit from the PR. So I am going to leave comments in
the PR. Although I suspect it will complicate the review due to how
quickly PRs turn into a pile of garbage when there are many comments.

^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2021-09-14 22:45 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-09-10 17:50 [Tarantool-patches] [PATCH 0/n] Datetime module implementation, stage #1 Timur Safin via Tarantool-patches
2021-09-10 17:50 ` [Tarantool-patches] [PATCH 1/n] build, lua: built-in module datetime Timur Safin via Tarantool-patches
2021-09-14 21:53   ` Safin Timur via Tarantool-patches
2021-09-14 22:45 ` [Tarantool-patches] [PATCH 0/n] Datetime module implementation, stage #1 Vladislav Shpilevoy via Tarantool-patches

Tarantool development patches archive

This inbox may be cloned and mirrored by anyone:

	git clone --mirror https://lists.tarantool.org/tarantool-patches/0 tarantool-patches/git/0.git

	# If you have public-inbox 1.1+ installed, you may
	# initialize and index your mirror using the following commands:
	public-inbox-init -V2 tarantool-patches tarantool-patches/ https://lists.tarantool.org/tarantool-patches \
		tarantool-patches@dev.tarantool.org.
	public-inbox-index tarantool-patches

Example config snippet for mirrors.


AGPL code for this site: git clone https://public-inbox.org/public-inbox.git