Tarantool development patches archive
 help / color / mirror / Atom feed
* [Tarantool-patches] [PATCH v4 0/7] Initial datetime support
@ 2021-08-12 22:33 Timur Safin via Tarantool-patches
  2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 1/7] build: add Christian Hansen c-dt to the build Timur Safin via Tarantool-patches
                   ` (6 more replies)
  0 siblings, 7 replies; 9+ messages in thread
From: Timur Safin via Tarantool-patches @ 2021-08-12 22:33 UTC (permalink / raw)
  To: imun, v.shpilevoy; +Cc: tarantool-patches

* Version #4 changes:

A lot.

  - Got rid of closures usage here and there. Benchmarks revealed there is 
    20x speed difference of hash with closure handlers to the case when we 
    check keys using sequence of simple ifs.
    And despite the fact that it looks ugler, we have updated all places which
    used to use closures - so much performance impact does not worth it;

  - c-dt library now is accompanied with corresponding unit-tests;

  - due to a new (old) way of exporting public symbols, which is now used by 
    Tarantool build, we have modified c-dt sources to use approach similar 
    to that used by xxhash (or ICU) - "namespaces" for public symbols.
    If there is DT_NAMESPACE macro (e.g. DT_NAMESPACE=tnt_) defined at the 
    moment of compilation we rename all relevant public symbols to 
    use that "namespace" as prefix
 
	#define dt_from_rdn DT_NAME(DT_NAMESPACE, dt_from_rdn)

    C sources continue to use original name (now as a macro), but in Lua ffi
    we have to use renamed symbol - `tnt_dt_from_rdn`.

  - We have separated code which implement MessagePack related funcionality
    from generic (i.e. stringization) support for datetime types.
    Stringization is in `src/core/datetime.c`, but MessagePack support is in 
    `src/core/mp_datetime.c`.

  - `ctime` and `asctime` now use reentrant versions (i.e. `ctime_r` instead of 
    `ctime`, and `asctime_r` instead of `asctime`). This allows us to avoid 
    nasty problem with GC where it was possible to corrupt returned 
    static buffer regardless the fact that it was immediately passed
    to `ffi.string`. 
    `test/app-tap/gh-5632-6050-6259-gc-buf-reuse.test.lua` updated to check our 
    new api `datetime.ctime`, and `datetime.asctime`. 

  - Introduced MessagePack unit-test which check C implementation (mp 
    encode/decode, mp_snprint, mp_fprint) 

  - Some special efforts have been taken to make sure we deal correctly 
    with datetime stamps which were before Unix Epoch (1970-01-01), i.e. 
    for negative stamps or timezones against epoch.

  - And last, but not the least: in addition to usual getters like 
    `timestamp`, `nanoseconds`, `epoch`, we have introduced function 
    modifiers `to_utc()` or `to_tz(offset)` which allow to manipulate with 
    datetime timezone `.offset` field. Actually it does not do much now 
    (only reattributing object with different offset, but not changed 
    actual timestamp, which is UTC normalized). But different timestamp 
    modifies textual representation, which simplify debugging.

* Version #3 changes:

  - renamed `struct datetime_t` to `struct datetime`, and `struct 
    datetime_interval_t` to `struct datetime_interval`;
  - significantly reworked arguments checks in module api entries;
  - fixed datetime comparisons;
  - changed hints calculation to take into account fractional part;
  - provided more comments here and there;

NB! There are MacOSX problems due to GLIBC specific code used (as Vlad
    has already pointed out) - so additional patch, making it more 
    cross-compatible coming here soon...

* Version #2 changes:

  - fixed problem with overloaded '-' and '+' operations for datetime 
    arguments;
  - fixed messagepack serialization problems;
  - heavily documented MessagePack serialization schema in the code;
  - introduced working implementation of datetime hints for storage engines;
  - made interval related names be more consistent, renamed durations and period 
    to intervals, i.e. t_datetime_duration to datetime_interval_t, 
    duration_* to interval_*, period to interval;
  - properly implemented all reasonable cases of datetime+interval 
    arithmetic;
  - moved all initialization code to utils.c;
  - renamed core/mp_datetime.c to core/datetime.c because it makes more
    sense now;

* Version #1 - initial RFC

In brief
--------

This patchset implements datetime lua support in box, with serialization 
to messagepack, yaml, json and lua mode. Also it contains storage 
engines' indices implementation for datetime type introduced.

* Current implementation is heavily influenced by Sci-Lua lua-time module
  https://github.com/stepelu/lua-time 
  e.g. you could find very similar approach for handling of operations
  with year or month long intervals (which should be handled differently than
  usual intervals of seconds, or days).

* But internally we actually use Christian Hanson' c-dt module
  https://github.com/chansen/c-dt 
  (though it has been modified slightly for cleaner integration
  into our cmake-based build process).


Datetime Module API
-------------------

Small draft of documentation for our `datetime` module API has been
extracted to the discussion topic there - 
https://github.com/tarantool/tarantool/discussions/6244#discussioncomment-1043988
We try to update documentation there once any relevant change introdcued 
to the implementaton.

Messagepack serialization schema
--------------------------------

In short it looks like:
- now we introduce new MP_EXT extension type #4;
- we may save 1 required and 2 optional fields for datetime field;

In all gory details it's explained in MessagePack serialization schema depicted here:
https://github.com/tarantool/tarantool/discussions/6244#discussioncomment-1043990


https://github.com/tarantool/tarantool/issues/5941
https://github.com/tarantool/tarantool/issues/5946

https://github.com/tarantool/tarantool/tree/tsafin/gh-5941-datetime-v4

Timur Safin (7):
  build: add Christian Hansen c-dt to the build
  lua: built-in module datetime
  lua, datetime: display datetime
  box, datetime: messagepack support for datetime
  box, datetime: datetime comparison for indices
  lua, datetime: time intervals support
  datetime: changelog for datetime module

 .gitmodules                                   |   3 +
 CMakeLists.txt                                |   8 +
 .../gh-5941-datetime-type-support.md          |   4 +
 cmake/BuildCDT.cmake                          |  10 +
 extra/exports                                 |  34 +
 src/CMakeLists.txt                            |   5 +-
 src/box/field_def.c                           |  53 +-
 src/box/field_def.h                           |   4 +
 src/box/lua/serialize_lua.c                   |   7 +-
 src/box/memtx_space.c                         |   3 +-
 src/box/msgpack.c                             |   7 +-
 src/box/tuple_compare.cc                      |  77 ++
 src/box/vinyl.c                               |   3 +-
 src/lib/core/CMakeLists.txt                   |   5 +-
 src/lib/core/datetime.c                       | 176 ++++
 src/lib/core/datetime.h                       | 115 +++
 src/lib/core/mp_datetime.c                    | 189 ++++
 src/lib/core/mp_datetime.h                    |  89 ++
 src/lib/core/mp_extension_types.h             |   1 +
 src/lib/mpstream/mpstream.c                   |  11 +
 src/lib/mpstream/mpstream.h                   |   4 +
 src/lua/datetime.lua                          | 880 ++++++++++++++++++
 src/lua/init.c                                |   4 +-
 src/lua/msgpack.c                             |  12 +
 src/lua/msgpackffi.lua                        |  18 +
 src/lua/serializer.c                          |   4 +
 src/lua/serializer.h                          |   2 +
 src/lua/utils.c                               |  28 +-
 src/lua/utils.h                               |  12 +
 test/app-tap/datetime.test.lua                | 370 ++++++++
 .../gh-5632-6050-6259-gc-buf-reuse.test.lua   |  74 +-
 test/engine/datetime.result                   |  77 ++
 test/engine/datetime.test.lua                 |  35 +
 test/unit/CMakeLists.txt                      |   3 +-
 test/unit/datetime.c                          | 379 ++++++++
 test/unit/datetime.result                     | 358 +++++++
 third_party/c-dt                              |   1 +
 third_party/lua-cjson/lua_cjson.c             |   8 +
 third_party/lua-yaml/lyaml.cc                 |   6 +-
 39 files changed, 3051 insertions(+), 28 deletions(-)
 create mode 100644 changelogs/unreleased/gh-5941-datetime-type-support.md
 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/lib/core/mp_datetime.c
 create mode 100644 src/lib/core/mp_datetime.h
 create mode 100644 src/lua/datetime.lua
 create mode 100755 test/app-tap/datetime.test.lua
 create mode 100644 test/engine/datetime.result
 create mode 100644 test/engine/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] 9+ messages in thread

* [Tarantool-patches] [PATCH v4 1/7] build: add Christian Hansen c-dt to the build
  2021-08-12 22:33 [Tarantool-patches] [PATCH v4 0/7] Initial datetime support Timur Safin via Tarantool-patches
@ 2021-08-12 22:33 ` Timur Safin via Tarantool-patches
  2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 2/7] lua: built-in module datetime Timur Safin via Tarantool-patches
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 9+ messages in thread
From: Timur Safin via Tarantool-patches @ 2021-08-12 22:33 UTC (permalink / raw)
  To: imun, v.shpilevoy; +Cc: tarantool-patches

* 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).
  We have to be able to rename externally public symbols to avoid
  name clashes with 3rd party modules. We prefix c-dt symbols
  in the Tarantool build with `tnt_` prefix;
* 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;
* also we check that strftime is reversible and produce consistent
  results after roundtrip from/to strings;
* discovered the harder way that on *BSD/MacOSX `strftime()` format
  `%z` outputs local time-zone if passed `tm_gmtoff` is 0.
  This behaviour is different to that we observed on Linux, thus we
  might have different execution results. Made test to not use `%z`
  and only operate with normalized date time formats `%F` and `%T`

Part of #5941
---
 .gitmodules               |   3 +
 CMakeLists.txt            |   8 +
 cmake/BuildCDT.cmake      |   8 +
 src/CMakeLists.txt        |   3 +-
 test/unit/CMakeLists.txt  |   3 +-
 test/unit/datetime.c      | 221 +++++++++++++++++++++++
 test/unit/datetime.result | 358 ++++++++++++++++++++++++++++++++++++++
 third_party/c-dt          |   1 +
 8 files changed, 603 insertions(+), 2 deletions(-)
 create mode 100644 cmake/BuildCDT.cmake
 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..53c86f2a5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -571,6 +571,14 @@ endif()
 # zstd
 #
 
+#
+# Chritian Hanson 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..343fb1b99
--- /dev/null
+++ b/cmake/BuildCDT.cmake
@@ -0,0 +1,8 @@
+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/)
+endmacro()
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index adb03b3f4..97b0cb326 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -193,7 +193,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/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index 5bb7cd6e7..31b183a8f 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 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..5d83c2f53
--- /dev/null
+++ b/test/unit/datetime.c
@@ -0,0 +1,221 @@
+#include "dt.h"
+#include <assert.h>
+#include <stdint.h>
+#include <string.h>
+#include <time.h>
+
+#include "unit.h"
+
+static const char sample[] = "2012-12-24T15:30Z";
+
+#define S(s) {s, sizeof(s) - 1}
+struct {
+	const char * sz;
+	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
+
+#define DIM(a) (sizeof(a) / sizeof(a[0]))
+
+/* p5-time-moment/src/moment_parse.c: parse_string_lenient() */
+static int
+parse_datetime(const char *str, size_t len, int64_t *sp, int32_t *np,
+	       int32_t *op)
+{
+	size_t n;
+	dt_t dt;
+	char c;
+	int sod = 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, &sod, &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:
+	*sp = ((int64_t)dt_rdn(dt) - 719163) * 86400 + sod - offset * 60;
+	*np = nanosecond;
+	*op = offset;
+
+	return 0;
+}
+
+/* avoid introducing external datetime.h dependency -
+   just copy paste it for today
+*/
+#define SECS_PER_DAY      86400
+#define DT_EPOCH_1970_OFFSET 719163
+
+struct datetime {
+	int64_t secs;
+	int32_t nsec;
+	int32_t offset;
+};
+
+static int
+local_rd(const struct datetime *dt)
+{
+	return (int)(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 = 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 ofs;
+
+	plan(355);
+	parse_datetime(sample, sizeof(sample) - 1,
+		       &secs_expected, &nanosecs, &ofs);
+
+	for (index = 0; index < DIM(tests); index++) {
+		int64_t secs;
+		int rc = parse_datetime(tests[index].sz, tests[index].len,
+						&secs, &nanosecs, &ofs);
+		is(rc, 0, "correct parse_datetime return value for '%s'",
+		   tests[index].sz);
+		is(secs, secs_expected, "correct parse_datetime output "
+		   "seconds for '%s", tests[index].sz);
+
+		/* check that stringized literal produces the same date */
+		/* time fields */
+		static char buff[40];
+		struct datetime dt = {secs, nanosecs, ofs};
+		/* 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");
+		rc = parse_datetime(buff, len, &dt.secs, &dt.nsec, &dt.offset);
+		is(rc, 0, "correct parse_datetime return value for '%s'", buff);
+		is(secs, dt.secs,
+		   "reversible seconds via strftime for '%s", buff);
+	}
+}
+
+int
+main(void)
+{
+	plan(1);
+	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..5b1398ca8
--- /dev/null
+++ b/third_party/c-dt
@@ -0,0 +1 @@
+Subproject commit 5b1398ca8f53513985670a2e832b5f6e253addf7
-- 
2.29.2


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

* [Tarantool-patches] [PATCH v4 2/7] lua: built-in module datetime
  2021-08-12 22:33 [Tarantool-patches] [PATCH v4 0/7] Initial datetime support Timur Safin via Tarantool-patches
  2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 1/7] build: add Christian Hansen c-dt to the build Timur Safin via Tarantool-patches
@ 2021-08-12 22:33 ` Timur Safin via Tarantool-patches
  2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 3/7] lua, datetime: display datetime Timur Safin via Tarantool-patches
                   ` (4 subsequent siblings)
  6 siblings, 0 replies; 9+ messages in thread
From: Timur Safin via Tarantool-patches @ 2021-08-12 22:33 UTC (permalink / raw)
  To: imun, v.shpilevoy; +Cc: tarantool-patches

* created a new Tarantool built-in module `datetime`;
* register cdef types for this module;
* export some `dt_*` functions from `c-dt` library;

* implemented `ctime`, `asctime` and `strftime` as
  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).

* introduced simple datetime tests

Created app-tap test for new builtin module `datetime.lua`,
where we specifically check:

- cases of simple datetime string formatting using:
  - asctime (gmt time);
  - ctime (local TZ time);
  - strftime (using given format).

- 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".

- check calculated attributes to date object, e.g.:
  - timestamp, seconds, microseconds, minute, or hours
  - to_utc(), and to_tz() allow to switch timezone of a
    datetime object. It's not changing much - only timezone
    but that impacts textual representation of a date.

Part of #5941
---
 cmake/BuildCDT.cmake                          |   2 +
 extra/exports                                 |  26 +
 src/CMakeLists.txt                            |   2 +
 src/lib/core/CMakeLists.txt                   |   1 +
 src/lib/core/datetime.c                       |  96 ++++
 src/lib/core/datetime.h                       |  95 ++++
 src/lua/datetime.lua                          | 500 ++++++++++++++++++
 src/lua/init.c                                |   4 +-
 src/lua/utils.c                               |  27 +
 src/lua/utils.h                               |  12 +
 test/app-tap/datetime.test.lua                | 206 ++++++++
 .../gh-5632-6050-6259-gc-buf-reuse.test.lua   |  74 ++-
 12 files changed, 1043 insertions(+), 2 deletions(-)
 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

diff --git a/cmake/BuildCDT.cmake b/cmake/BuildCDT.cmake
index 343fb1b99..80b26c64a 100644
--- a/cmake/BuildCDT.cmake
+++ b/cmake/BuildCDT.cmake
@@ -5,4 +5,6 @@ macro(libccdt_build)
     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..80eb92abd 100644
--- a/extra/exports
+++ b/extra/exports
@@ -148,8 +148,34 @@ csv_feed
 csv_iterator_create
 csv_next
 csv_setopt
+datetime_asctime
+datetime_ctime
+datetime_now
+datetime_strftime
+decimal_unpack
 decimal_from_string
 decimal_unpack
+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
 error_ref
 error_set_prev
 error_unref
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 97b0cb326..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)
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..ab26c3445
--- /dev/null
+++ b/src/lib/core/datetime.c
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2021, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <string.h>
+#include <time.h>
+
+#include "trivia/util.h"
+#include "datetime.h"
+
+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 = 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;
+}
+
+char *
+datetime_asctime(const struct datetime *date, char *buf)
+{
+	struct tm *p_tm = datetime_to_tm(date);
+	return asctime_r(p_tm, buf);
+}
+
+char *
+datetime_ctime(const struct datetime *date, char *buf)
+{
+	time_t time = date->secs;
+	return ctime_r(&time, buf);
+}
+
+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);
+}
diff --git a/src/lib/core/datetime.h b/src/lib/core/datetime.h
new file mode 100644
index 000000000..4ec459860
--- /dev/null
+++ b/src/lib/core/datetime.h
@@ -0,0 +1,95 @@
+#pragma once
+/*
+ * Copyright 2021, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include "c-dt/dt.h"
+
+#if defined(__cplusplus)
+extern "C"
+{
+#endif /* defined(__cplusplus) */
+
+#ifndef SECS_PER_DAY
+#define SECS_PER_DAY          86400
+#define DT_EPOCH_1970_OFFSET  719163
+#endif
+
+/**
+ * Full datetime structure representing moments
+ * since Unix Epoch (1970-01-01).
+ * Time is kept normalized to UTC, time-zone offset
+ * is informative only.
+ */
+struct datetime {
+	/** seconds since epoch */
+	int64_t secs;
+	/** nanoseconds if any */
+	int32_t nsec;
+	/** offset in minutes from UTC */
+	int32_t offset;
+};
+
+/**
+ * Date/time interval structure
+ */
+struct datetime_interval {
+	/** relative seconds delta */
+	int64_t secs;
+	/** nanoseconds delta */
+	int32_t nsec;
+};
+
+/**
+ * Convert datetime to string using default asctime format
+ * "Sun Sep 16 01:03:52 1973\n\0"
+ * Wrapper around reenterable asctime_r() version of POSIX function
+ * @param date source datetime value
+ * @sa datetime_ctime
+ */
+char *
+datetime_asctime(const struct datetime *date, char *buf);
+
+char *
+datetime_ctime(const struct datetime *date, char *buf);
+
+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..7ef5a8d56
--- /dev/null
+++ b/src/lua/datetime.lua
@@ -0,0 +1,500 @@
+local ffi = require('ffi')
+
+ffi.cdef [[
+
+    /*
+    `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.
+
+    */
+    typedef int dt_t;
+
+    // dt_core.h
+    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_parse_iso.h
+    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);
+
+    // datetime.c
+    int
+    datetime_to_string(const struct datetime * date, char *buf, uint32_t len);
+
+    char *
+    datetime_asctime(const struct datetime *date, char *buf);
+
+    char *
+    datetime_ctime(const struct datetime *date, char *buf);
+
+    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 SECS_PER_DAY     = 86400
+
+-- c-dt/dt_config.h
+
+-- Unix, January 1, 1970, Thursday
+local DT_EPOCH_1970_OFFSET = 719163LL
+
+
+local datetime_t = ffi.typeof('struct datetime')
+local interval_t = ffi.typeof('struct datetime_interval')
+
+local function is_interval(o)
+    return type(o) == 'cdata' and ffi.istype(interval_t, o)
+end
+
+local function is_datetime(o)
+    return type(o) == 'cdata' and ffi.istype(datetime_t, o)
+end
+
+local function is_date_interval(o)
+    return type(o) == 'cdata' and
+           (ffi.istype(interval_t, o) or ffi.istype(datetime_t, o))
+end
+
+local function interval_new()
+    local interval = ffi.new(interval_t)
+    return interval
+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 function datetime_cmp(lhs, rhs)
+    if not is_date_interval(lhs) or
+       not is_date_interval(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 function interval_serialize(self)
+    return { secs = self.secs, nsec = self.nsec }
+end
+
+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, frac, offset)
+    local epochV = dt ~= nil and (builtin.tnt_dt_rdn(dt) - DT_EPOCH_1970_OFFSET) *
+                   SECS_PER_DAY or 0
+    local secsV = secs ~= nil and secs or 0
+    local fracV = frac ~= nil and frac or 0
+    local ofsV = offset ~= nil and offset or 0
+    return datetime_new_raw(epochV + secsV - ofsV * 60, fracV, ofsV)
+end
+
+-- create datetime given attribute values from obj
+-- in the "easy mode", providing builder with
+-- .secs, .nsec, .offset
+local function datetime_new_obj(obj, ...)
+    if obj == nil or type(obj) ~= 'table' then
+        return datetime_new_raw(obj, ...)
+    end
+    local secs = 0
+    local nsec = 0
+    local offset = 0
+
+    for key, value in pairs(obj) do
+        if key == 'secs' then
+            secs = value
+        elseif key == 'nsec' then
+            nsec = value
+        elseif key == 'offset' then
+            offset = value
+        else
+            error(('unknown attribute %s'):format(key), 2)
+        end
+    end
+
+    return datetime_new_raw(secs, nsec, offset)
+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 y = 0
+    local M = 0
+    local d = 0
+    local ymd = false
+
+    local h = 0
+    local m = 0
+    local s = 0
+    local frac = 0
+    local hms = false
+    local offset = 0
+
+    local dt = 0
+
+    for key, value in pairs(obj) do
+        if key == 'year' then
+            check_range(value, {1, 9999}, key)
+            y = value
+            ymd = true
+        elseif key == 'month' then
+            check_range(value, {1, 12}, key)
+            M = value
+            ymd = true
+        elseif key == 'day' then
+            check_range(value, {1, 31}, key)
+            d = value
+            ymd = true
+        elseif key == 'hour' then
+            check_range(value, {0, 23}, key)
+            h = value
+            hms = true
+        elseif key == 'min' or key == 'minute' then
+            check_range(value, {0, 59}, key)
+            m = value
+            hms = true
+        elseif key == 'sec' or key == 'second' then
+            check_range(value, {0, 60}, key)
+            s, frac = math_modf(value)
+            frac = frac * 1e9 -- convert fraction to nanoseconds
+            hms = true
+        elseif key == 'tz' then
+        -- tz offset in minutes
+            check_range(value, {0, 720}, key)
+            offset = value
+        elseif key == 'isdst' or key == 'wday' or key =='yday' then -- luacheck: ignore 542
+            -- ignore unused os.date attributes
+        else
+            error(('unknown attribute %s'):format(key), 2)
+        end
+    end
+
+    -- .year, .month, .day
+    if ymd then
+        dt = builtin.tnt_dt_from_ymd(y, M, d)
+    end
+
+    -- .hour, .minute, .second
+    local secs = 0
+    if hms then
+        secs = h * 3600 + m * 60 + s
+    end
+
+    return datetime_new_dt(dt, secs, frac, offset)
+end
+
+--[[
+    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
+]]
+
+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
+    T12                 N/A
+    T1230               T12:30
+    T123045             T12:30:45
+    T123045.123456789   T12:30:45.123456789
+    T123045,123456789   T12:30:45,123456789
+
+    The time designator [T] may be omitted.
+]]
+local function parse_time(str)
+    check_str("datetime.parse_time()")
+    local sp = ffi.new('int[1]')
+    local fp = ffi.new('int[1]')
+    local len = builtin.tnt_dt_parse_iso_time(str, #str, sp, fp)
+    return len > 0 and datetime_new_dt(nil, sp[0], fp[0]) or nil,
+           tonumber(len)
+end
+
+--[[
+    Basic    Extended
+    Z        N/A
+    +hh      N/A
+    -hh      N/A
+    +hhmm    +hh:mm
+    -hhmm    -hh:mm
+]]
+local function parse_zone(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 datetime_new_dt(nil, nil, nil, offset[0]) or nil,
+           tonumber(len)
+end
+
+
+--[[
+    aggregated parse functions
+    assumes to deal with date T time time_zone
+    at once
+
+    date [T] time [ ] time_zone
+]]
+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
+
+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
+
+local function local_now()
+    local d = datetime_new_raw(0, 0, 0)
+    builtin.datetime_now(d)
+    return d
+end
+
+-- Change the time-zone to the provided target_offset
+-- Time `.secs`/`.nsec` are always UTC normalized, we need only to
+-- reattribute object with different `.offset`
+local function datetime_to_tz(self, tgt_ofs)
+    if self.offset == tgt_ofs then
+        return self
+    end
+    if type(tgt_ofs) == 'string' then
+        local obj = parse_zone(tgt_ofs)
+        if obj == nil then
+            error(('%s: invalid time-zone format %s'):format(self, tgt_ofs), 2)
+        else
+            tgt_ofs = obj.offset
+        end
+    end
+    return datetime_new_raw(self.secs, self.nsec, tgt_ofs)
+end
+
+local function datetime_index(self, key)
+    if key == 'epoch' or key == 'unixtime' then
+        return self.secs
+    elseif key == 'ts' or key == 'timestamp' then
+        return tonumber(self.secs) + self.nsec / 1e9
+    elseif key == 'ns' or key == 'nanoseconds' then
+        return self.secs * 1e9 + self.nsec
+    elseif key == 'us' or key == 'microseconds' then
+        return self.secs * 1e6 + self.nsec / 1e3
+    elseif key == 'ms' or key == 'milliseconds' then
+        return self.secs * 1e3 + self.nsec / 1e6
+    elseif key == 's' or key == 'seconds' then
+        return tonumber(self.secs) + self.nsec / 1e9
+    elseif key == 'm' or key == 'min' or key == 'minutes' then
+        return (tonumber(self.secs) + self.nsec / 1e9) / 60
+    elseif key == 'hr' or key == 'hours' then
+        return (tonumber(self.secs) + self.nsec / 1e9) / (60 * 60)
+    elseif key == 'd' or key == 'days' then
+        return (tonumber(self.secs) + self.nsec / 1e9) / (24 * 60 * 60)
+    elseif key == 'to_utc' then
+        return function(self)
+            return datetime_to_tz(self, 0)
+        end
+    elseif key == 'to_tz' then
+        return function(self, offset)
+            return datetime_to_tz(self, offset)
+        end
+    else
+        error(('unknown attribute %s'):format(key), 2)
+    end
+end
+
+local function datetime_newindex(self, key, value)
+    if key == 'epoch' or key == 'unixtime' then
+        self.secs = value
+        self.nsec, self.offset = 0, 0
+    elseif key == 'ts' or key == 'timestamp' then
+        local secs, frac = math_modf(value)
+        self.secs = secs
+        self.nsec = frac * 1e9
+        self.offset = 0
+    else
+        error(('assigning to unknown attribute %s'):format(key), 2)
+    end
+end
+
+-- sizeof("Wed Jun 30 21:49:08 1993\n")
+local buf_len = 26
+
+local function asctime(o)
+    check_date(o, "datetime:asctime()")
+    local buf = ffi.new('char[?]', buf_len)
+    return ffi.string(builtin.datetime_asctime(o, buf))
+end
+
+local function ctime(o)
+    check_date(o, "datetime:ctime()")
+    local buf = ffi.new('char[?]', buf_len)
+    return ffi.string(builtin.datetime_ctime(o, buf))
+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
+
+local datetime_mt = {
+    __serialize = datetime_serialize,
+    __eq = datetime_eq,
+    __lt = datetime_lt,
+    __le = datetime_le,
+    __index = datetime_index,
+    __newindex = datetime_newindex,
+}
+
+local interval_mt = {
+    __serialize = interval_serialize,
+    __eq = datetime_eq,
+    __lt = datetime_lt,
+    __le = datetime_le,
+    __index = datetime_index,
+}
+
+ffi.metatype(interval_t, interval_mt)
+ffi.metatype(datetime_t, datetime_mt)
+
+return setmetatable(
+    {
+        new         = datetime_new,
+        new_raw     = datetime_new_obj,
+        interval    = interval_new,
+
+        parse       = parse,
+        parse_date  = parse_date,
+        parse_time  = parse_time,
+        parse_zone  = parse_zone,
+
+        now         = local_now,
+        strftime    = strftime,
+        asctime     = asctime,
+        ctime       = ctime,
+
+        is_datetime = is_datetime,
+        is_interval = is_interval,
+    }, {
+        __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..9130b60b5 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 {"
+			  "int64_t 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 {"
+			  "int64_t 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..464d4bd49
--- /dev/null
+++ b/test/app-tap/datetime.test.lua
@@ -0,0 +1,206 @@
+#!/usr/bin/env tarantool
+
+local tap = require('tap')
+local test = tap.test("errno")
+local date = require('datetime')
+local ffi = require('ffi')
+
+
+test:plan(6)
+
+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(168)
+    -- borrowed from p5-time-moments/t/180_from_string.t
+    local tests =
+    {
+        { '1970-01-01T00:00:00Z',                       0,           0,    0 },
+        { '1970-01-01T02:00:00+02:00',                  0,           0,  120 },
+        { '1970-01-01T01:30:00+01:30',                  0,           0,   90 },
+        { '1970-01-01T01:00:00+01:00',                  0,           0,   60 },
+        { '1970-01-01T00:01:00+00:01',                  0,           0,    1 },
+        { '1970-01-01T00:00:00+00:00',                  0,           0,    0 },
+        { '1969-12-31T23:59:00-00:01',                  0,           0,   -1 },
+        { '1969-12-31T23:00:00-01:00',                  0,           0,  -60 },
+        { '1969-12-31T22:30:00-01:30',                  0,           0,  -90 },
+        { '1969-12-31T22:00:00-02:00',                  0,           0, -120 },
+        { '1970-01-01T00:00:00.123456789Z',             0,   123456789,    0 },
+        { '1970-01-01T00:00:00.12345678Z',              0,   123456780,    0 },
+        { '1970-01-01T00:00:00.1234567Z',               0,   123456700,    0 },
+        { '1970-01-01T00:00:00.123456Z',                0,   123456000,    0 },
+        { '1970-01-01T00:00:00.12345Z',                 0,   123450000,    0 },
+        { '1970-01-01T00:00:00.1234Z',                  0,   123400000,    0 },
+        { '1970-01-01T00:00:00.123Z',                   0,   123000000,    0 },
+        { '1970-01-01T00:00:00.12Z',                    0,   120000000,    0 },
+        { '1970-01-01T00:00:00.1Z',                     0,   100000000,    0 },
+        { '1970-01-01T00:00:00.01Z',                    0,    10000000,    0 },
+        { '1970-01-01T00:00:00.001Z',                   0,     1000000,    0 },
+        { '1970-01-01T00:00:00.0001Z',                  0,      100000,    0 },
+        { '1970-01-01T00:00:00.00001Z',                 0,       10000,    0 },
+        { '1970-01-01T00:00:00.000001Z',                0,        1000,    0 },
+        { '1970-01-01T00:00:00.0000001Z',               0,         100,    0 },
+        { '1970-01-01T00:00:00.00000001Z',              0,          10,    0 },
+        { '1970-01-01T00:00:00.000000001Z',             0,           1,    0 },
+        { '1970-01-01T00:00:00.000000009Z',             0,           9,    0 },
+        { '1970-01-01T00:00:00.00000009Z',              0,          90,    0 },
+        { '1970-01-01T00:00:00.0000009Z',               0,         900,    0 },
+        { '1970-01-01T00:00:00.000009Z',                0,        9000,    0 },
+        { '1970-01-01T00:00:00.00009Z',                 0,       90000,    0 },
+        { '1970-01-01T00:00:00.0009Z',                  0,      900000,    0 },
+        { '1970-01-01T00:00:00.009Z',                   0,     9000000,    0 },
+        { '1970-01-01T00:00:00.09Z',                    0,    90000000,    0 },
+        { '1970-01-01T00:00:00.9Z',                     0,   900000000,    0 },
+        { '1970-01-01T00:00:00.99Z',                    0,   990000000,    0 },
+        { '1970-01-01T00:00:00.999Z',                   0,   999000000,    0 },
+        { '1970-01-01T00:00:00.9999Z',                  0,   999900000,    0 },
+        { '1970-01-01T00:00:00.99999Z',                 0,   999990000,    0 },
+        { '1970-01-01T00:00:00.999999Z',                0,   999999000,    0 },
+        { '1970-01-01T00:00:00.9999999Z',               0,   999999900,    0 },
+        { '1970-01-01T00:00:00.99999999Z',              0,   999999990,    0 },
+        { '1970-01-01T00:00:00.999999999Z',             0,   999999999,    0 },
+        { '1970-01-01T00:00:00.0Z',                     0,           0,    0 },
+        { '1970-01-01T00:00:00.00Z',                    0,           0,    0 },
+        { '1970-01-01T00:00:00.000Z',                   0,           0,    0 },
+        { '1970-01-01T00:00:00.0000Z',                  0,           0,    0 },
+        { '1970-01-01T00:00:00.00000Z',                 0,           0,    0 },
+        { '1970-01-01T00:00:00.000000Z',                0,           0,    0 },
+        { '1970-01-01T00:00:00.0000000Z',               0,           0,    0 },
+        { '1970-01-01T00:00:00.00000000Z',              0,           0,    0 },
+        { '1970-01-01T00:00:00.000000000Z',             0,           0,    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 },
+    }
+    for _, value in ipairs(tests) do
+        local str, epoch, nsec, offset
+        str, epoch, nsec, offset = 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))
+    end
+end)
+
+ffi.cdef [[
+    void tzset(void);
+]]
+
+test:test("Datetime string formatting", function(test)
+    test:plan(7)
+    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.asctime(t) == 'Thu Jan  1 00:00:00 1970\n', ('%s: asctime'):format(str))
+    -- ctime() is local timezone dependent. To make sure that
+    -- test is deterministic we enforce timezone via TZ environment
+    -- manipulations and calling tzset()
+
+    -- redefine timezone to be always GMT-2
+    os.setenv('TZ', 'GMT-2')
+    ffi.C.tzset()
+    test:ok(date.ctime(t) == 'Thu Jan  1 02:00:00 1970\n', ('%s: ctime with timezone'):format(str))
+    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(9)
+    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")
+    test:ok(tiny.minutes == 0.5088, "minutes")
+    test:ok(tiny.hours == 0.00848, "hours")
+end)
+
+os.exit(test:check() and 0 or 1)
diff --git a/test/app-tap/gh-5632-6050-6259-gc-buf-reuse.test.lua b/test/app-tap/gh-5632-6050-6259-gc-buf-reuse.test.lua
index 84c2944e5..7eb05f9e8 100755
--- a/test/app-tap/gh-5632-6050-6259-gc-buf-reuse.test.lua
+++ b/test/app-tap/gh-5632-6050-6259-gc-buf-reuse.test.lua
@@ -14,6 +14,7 @@ local uri = require('uri')
 local json = require('json')
 local msgpackffi = require('msgpackffi')
 local decimal = require('decimal')
+local datetime = require('datetime')
 
 local function test_uuid(test)
     test:plan(1)
@@ -256,15 +257,86 @@ local function test_decimal(test)
     test:ok(is_success, 'decimal str in gc')
 end
 
+local function test_datetime_asctime(test)
+    test:plan(1)
+
+    local gc_count = 100
+    local iter_count = 1000
+    local is_success = true
+
+    local T1 = datetime('1970-01-01')
+    local T2 = datetime('2000-01-01')
+
+    local function datetime_asctime()
+        local str1 = datetime.asctime(T1)
+        local str2 = datetime.asctime(T2)
+        local str3 = datetime.asctime(T1)
+        local str4 = datetime.asctime(T2)
+        if str1 ~= str3 or str2 ~= str4 then
+            is_success = false
+        end
+    end
+
+    local function create_gc()
+        for _ = 1, gc_count do
+            ffi.gc(ffi.new('char[1]'), function() datetime_asctime() end)
+        end
+    end
+
+    for _ = 1, iter_count do
+        create_gc()
+        datetime_asctime()
+    end
+
+    test:ok(is_success, 'info datetime in gc')
+end
+
+local function test_datetime_ctime(test)
+    test:plan(1)
+
+    local gc_count = 100
+    local iter_count = 1000
+    local is_success = true
+
+    local T1 = datetime('1970-01-01')
+    local T2 = datetime('2000-01-01')
+
+    local function datetime_ctime()
+        local str1 = datetime.ctime(T1)
+        local str2 = datetime.ctime(T2)
+        local str3 = datetime.ctime(T1)
+        local str4 = datetime.ctime(T2)
+        if str1 ~= str3 or str2 ~= str4 then
+            is_success = false
+        end
+    end
+
+    local function create_gc()
+        for _ = 1, gc_count do
+            ffi.gc(ffi.new('char[1]'), function() datetime_ctime() end)
+        end
+    end
+
+    for _ = 1, iter_count do
+        create_gc()
+        datetime_ctime()
+    end
+
+    test:ok(is_success, 'info datetime in gc')
+end
+
+
 box.cfg{}
 
 local test = tap.test('gh-5632-6050-6259-gc-buf-reuse')
-test:plan(6)
+test:plan(8)
 test:test('uuid in __gc', test_uuid)
 test:test('uri in __gc', test_uri)
 test:test('msgpackffi in __gc', test_msgpackffi)
 test:test('json in __gc', test_json)
 test:test('info uuid in __gc', test_info_uuid)
 test:test('decimal str in __gc', test_decimal)
+test:test('datetime.asctime in __gc', test_datetime_asctime)
+test:test('datetime.ctime in __gc', test_datetime_ctime)
 
 os.exit(test:check() and 0 or 1)
-- 
2.29.2


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

* [Tarantool-patches] [PATCH v4 3/7] lua, datetime: display datetime
  2021-08-12 22:33 [Tarantool-patches] [PATCH v4 0/7] Initial datetime support Timur Safin via Tarantool-patches
  2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 1/7] build: add Christian Hansen c-dt to the build Timur Safin via Tarantool-patches
  2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 2/7] lua: built-in module datetime Timur Safin via Tarantool-patches
@ 2021-08-12 22:33 ` Timur Safin via Tarantool-patches
  2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 4/7] box, datetime: messagepack support for datetime Timur Safin via Tarantool-patches
                   ` (3 subsequent siblings)
  6 siblings, 0 replies; 9+ messages in thread
From: Timur Safin via Tarantool-patches @ 2021-08-12 22:33 UTC (permalink / raw)
  To: imun, v.shpilevoy; +Cc: tarantool-patches

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

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

Part of #5941
---
 extra/exports                  |   1 +
 src/lib/core/datetime.c        |  71 ++++++++++++++++++
 src/lua/datetime.lua           |  35 +++++++++
 test/app-tap/datetime.test.lua | 131 ++++++++++++++++++---------------
 test/unit/CMakeLists.txt       |   2 +-
 test/unit/datetime.c           |  61 +++++++++++----
 6 files changed, 227 insertions(+), 74 deletions(-)

diff --git a/extra/exports b/extra/exports
index 80eb92abd..2437e175c 100644
--- a/extra/exports
+++ b/extra/exports
@@ -152,6 +152,7 @@ datetime_asctime
 datetime_ctime
 datetime_now
 datetime_strftime
+datetime_to_string
 decimal_unpack
 decimal_from_string
 decimal_unpack
diff --git a/src/lib/core/datetime.c b/src/lib/core/datetime.c
index ab26c3445..e0300417a 100644
--- a/src/lib/core/datetime.c
+++ b/src/lib/core/datetime.c
@@ -29,6 +29,8 @@
  * SUCH DAMAGE.
  */
 
+#include <assert.h>
+#include <limits.h>
 #include <string.h>
 #include <time.h>
 
@@ -94,3 +96,72 @@ datetime_strftime(const struct datetime *date, const char *fmt, char *buf,
 	struct tm *p_tm = datetime_to_tm(date);
 	return strftime(buf, len, fmt, p_tm);
 }
+
+#define SECS_EPOCH_1970_OFFSET ((int64_t)DT_EPOCH_1970_OFFSET * SECS_PER_DAY)
+
+/* 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, uint32_t len)
+{
+#define ADVANCE(sz)		\
+	if (buf != NULL) { 	\
+		buf += sz; 	\
+		len -= sz; 	\
+	}			\
+	ret += sz;
+
+	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 secs = date->secs + offset * 60 + SECS_EPOCH_1970_OFFSET;
+	assert((secs / SECS_PER_DAY) <= INT_MAX);
+	dt_t dt = dt_from_rdn(secs / SECS_PER_DAY);
+
+	int year, month, day, sec, ns, sign;
+	dt_to_ymd(dt, &year, &month, &day);
+
+	int hour = (secs / 3600) % 24,
+	    minute = (secs / 60) % 60;
+	sec = secs % 60;
+	ns = date->nsec;
+
+	int ret = 0;
+	uint32_t sz = snprintf(buf, len, "%04d-%02d-%02dT%02d:%02d",
+			       year, month, day, hour, minute);
+	ADVANCE(sz);
+	if (sec || ns) {
+		sz = snprintf(buf, len, ":%02d", sec);
+		ADVANCE(sz);
+		if (ns) {
+			if ((ns % 1000000) == 0)
+				sz = snprintf(buf, len, ".%03d", ns / 1000000);
+			else if ((ns % 1000) == 0)
+				sz = snprintf(buf, len, ".%06d", ns / 1000);
+			else
+				sz = snprintf(buf, len, ".%09d", ns);
+			ADVANCE(sz);
+		}
+	}
+	if (offset == 0) {
+		sz = snprintf(buf, len, "Z");
+		ADVANCE(sz);
+	}
+	else {
+		if (offset < 0)
+			sign = '-', offset = -offset;
+		else
+			sign = '+';
+
+		sz = snprintf(buf, len, "%c%02d:%02d", sign, offset / 60, offset % 60);
+		ADVANCE(sz);
+	}
+	return ret;
+}
+#undef ADVANCE
+
diff --git a/src/lua/datetime.lua b/src/lua/datetime.lua
index 7ef5a8d56..00d50a7f1 100644
--- a/src/lua/datetime.lua
+++ b/src/lua/datetime.lua
@@ -249,6 +249,37 @@ local function datetime_new(obj)
     return datetime_new_dt(dt, secs, frac, offset)
 end
 
+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)
+    elseif ffi.typeof(o) == interval_t then
+        local ts = o.timestamp
+        local sign = '+'
+
+        if ts < 0 then
+            ts = -ts
+            sign = '-'
+        end
+
+        if ts < 60 then
+            return ('%s%s secs'):format(sign, ts)
+        elseif ts < 60 * 60 then
+            return ('%+d minutes, %s seconds'):format(o.minutes, ts % 60)
+        elseif ts < 24 * 60 * 60 then
+            return ('%+d hours, %d minutes, %s seconds'):format(
+                    o.hours, o.minutes % 60, ts % 60)
+        else
+            return ('%+d days, %d hours, %d minutes, %s seconds'):format(
+                    o.days, o.hours % 24, o.minutes % 60, ts % 60)
+        end
+    end
+end
+
+
 --[[
     Basic      Extended
     20121224   2012-12-24   Calendar date   (ISO 8601)
@@ -457,6 +488,7 @@ local function strftime(fmt, o)
 end
 
 local datetime_mt = {
+    __tostring = datetime_tostring,
     __serialize = datetime_serialize,
     __eq = datetime_eq,
     __lt = datetime_lt,
@@ -466,6 +498,7 @@ local datetime_mt = {
 }
 
 local interval_mt = {
+    __tostring = datetime_tostring,
     __serialize = interval_serialize,
     __eq = datetime_eq,
     __lt = datetime_lt,
@@ -487,6 +520,8 @@ return setmetatable(
         parse_time  = parse_time,
         parse_zone  = parse_zone,
 
+        tostring    = datetime_tostring,
+
         now         = local_now,
         strftime    = strftime,
         asctime     = asctime,
diff --git a/test/app-tap/datetime.test.lua b/test/app-tap/datetime.test.lua
index 464d4bd49..244ec2575 100755
--- a/test/app-tap/datetime.test.lua
+++ b/test/app-tap/datetime.test.lua
@@ -6,7 +6,7 @@ local date = require('datetime')
 local ffi = require('ffi')
 
 
-test:plan(6)
+test:plan(7)
 
 test:test("Simple tests for parser", function(test)
     test:plan(2)
@@ -17,74 +17,78 @@ test:test("Simple tests for parser", function(test)
 end)
 
 test:test("Multiple tests for parser (with nanoseconds)", function(test)
-    test:plan(168)
+    test:plan(193)
     -- borrowed from p5-time-moments/t/180_from_string.t
     local tests =
     {
-        { '1970-01-01T00:00:00Z',                       0,           0,    0 },
-        { '1970-01-01T02:00:00+02:00',                  0,           0,  120 },
-        { '1970-01-01T01:30:00+01:30',                  0,           0,   90 },
-        { '1970-01-01T01:00:00+01:00',                  0,           0,   60 },
-        { '1970-01-01T00:01:00+00:01',                  0,           0,    1 },
-        { '1970-01-01T00:00:00+00:00',                  0,           0,    0 },
-        { '1969-12-31T23:59:00-00:01',                  0,           0,   -1 },
-        { '1969-12-31T23:00:00-01:00',                  0,           0,  -60 },
-        { '1969-12-31T22:30:00-01:30',                  0,           0,  -90 },
-        { '1969-12-31T22:00:00-02:00',                  0,           0, -120 },
-        { '1970-01-01T00:00:00.123456789Z',             0,   123456789,    0 },
-        { '1970-01-01T00:00:00.12345678Z',              0,   123456780,    0 },
-        { '1970-01-01T00:00:00.1234567Z',               0,   123456700,    0 },
-        { '1970-01-01T00:00:00.123456Z',                0,   123456000,    0 },
-        { '1970-01-01T00:00:00.12345Z',                 0,   123450000,    0 },
-        { '1970-01-01T00:00:00.1234Z',                  0,   123400000,    0 },
-        { '1970-01-01T00:00:00.123Z',                   0,   123000000,    0 },
-        { '1970-01-01T00:00:00.12Z',                    0,   120000000,    0 },
-        { '1970-01-01T00:00:00.1Z',                     0,   100000000,    0 },
-        { '1970-01-01T00:00:00.01Z',                    0,    10000000,    0 },
-        { '1970-01-01T00:00:00.001Z',                   0,     1000000,    0 },
-        { '1970-01-01T00:00:00.0001Z',                  0,      100000,    0 },
-        { '1970-01-01T00:00:00.00001Z',                 0,       10000,    0 },
-        { '1970-01-01T00:00:00.000001Z',                0,        1000,    0 },
-        { '1970-01-01T00:00:00.0000001Z',               0,         100,    0 },
-        { '1970-01-01T00:00:00.00000001Z',              0,          10,    0 },
-        { '1970-01-01T00:00:00.000000001Z',             0,           1,    0 },
-        { '1970-01-01T00:00:00.000000009Z',             0,           9,    0 },
-        { '1970-01-01T00:00:00.00000009Z',              0,          90,    0 },
-        { '1970-01-01T00:00:00.0000009Z',               0,         900,    0 },
-        { '1970-01-01T00:00:00.000009Z',                0,        9000,    0 },
-        { '1970-01-01T00:00:00.00009Z',                 0,       90000,    0 },
-        { '1970-01-01T00:00:00.0009Z',                  0,      900000,    0 },
-        { '1970-01-01T00:00:00.009Z',                   0,     9000000,    0 },
-        { '1970-01-01T00:00:00.09Z',                    0,    90000000,    0 },
-        { '1970-01-01T00:00:00.9Z',                     0,   900000000,    0 },
-        { '1970-01-01T00:00:00.99Z',                    0,   990000000,    0 },
-        { '1970-01-01T00:00:00.999Z',                   0,   999000000,    0 },
-        { '1970-01-01T00:00:00.9999Z',                  0,   999900000,    0 },
-        { '1970-01-01T00:00:00.99999Z',                 0,   999990000,    0 },
-        { '1970-01-01T00:00:00.999999Z',                0,   999999000,    0 },
-        { '1970-01-01T00:00:00.9999999Z',               0,   999999900,    0 },
-        { '1970-01-01T00:00:00.99999999Z',              0,   999999990,    0 },
-        { '1970-01-01T00:00:00.999999999Z',             0,   999999999,    0 },
-        { '1970-01-01T00:00:00.0Z',                     0,           0,    0 },
-        { '1970-01-01T00:00:00.00Z',                    0,           0,    0 },
-        { '1970-01-01T00:00:00.000Z',                   0,           0,    0 },
-        { '1970-01-01T00:00:00.0000Z',                  0,           0,    0 },
-        { '1970-01-01T00:00:00.00000Z',                 0,           0,    0 },
-        { '1970-01-01T00:00:00.000000Z',                0,           0,    0 },
-        { '1970-01-01T00:00:00.0000000Z',               0,           0,    0 },
-        { '1970-01-01T00:00:00.00000000Z',              0,           0,    0 },
-        { '1970-01-01T00:00:00.000000000Z',             0,           0,    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 },
+        {'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
-        str, epoch, nsec, offset = unpack(value)
+        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)
 
@@ -203,4 +207,11 @@ test:test("Parse tiny date into seconds and other parts", function(test)
     test:ok(tiny.hours == 0.00848, "hours")
 end)
 
+test:test("Stringization of date", function(test)
+    test:plan(1)
+    local str = '19700101Z'
+    local dt = date(str)
+    test:ok(tostring(dt) == '1970-01-01T00:00Z', ('tostring(%s)'):format(str))
+end)
+
 os.exit(test:check() and 0 or 1)
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index 31b183a8f..8194a133f 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -57,7 +57,7 @@ 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 unit)
+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
index 5d83c2f53..f718b7de0 100644
--- a/test/unit/datetime.c
+++ b/test/unit/datetime.c
@@ -5,6 +5,7 @@
 #include <time.h>
 
 #include "unit.h"
+#include "datetime.h"
 
 static const char sample[] = "2012-12-24T15:30Z";
 
@@ -136,18 +137,6 @@ exit:
 	return 0;
 }
 
-/* avoid introducing external datetime.h dependency -
-   just copy paste it for today
-*/
-#define SECS_PER_DAY      86400
-#define DT_EPOCH_1970_OFFSET 719163
-
-struct datetime {
-	int64_t secs;
-	int32_t nsec;
-	int32_t offset;
-};
-
 static int
 local_rd(const struct datetime *dt)
 {
@@ -209,13 +198,59 @@ static void datetime_test(void)
 		is(secs, dt.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 < DIM(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(1);
+	plan(2);
 	datetime_test();
+	tostring_datetime_test();
 
 	return check_plan();
 }
-- 
2.29.2


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

* [Tarantool-patches] [PATCH v4 4/7] box, datetime: messagepack support for datetime
  2021-08-12 22:33 [Tarantool-patches] [PATCH v4 0/7] Initial datetime support Timur Safin via Tarantool-patches
                   ` (2 preceding siblings ...)
  2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 3/7] lua, datetime: display datetime Timur Safin via Tarantool-patches
@ 2021-08-12 22:33 ` Timur Safin via Tarantool-patches
  2021-08-12 23:03   ` Safin Timur via Tarantool-patches
  2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 5/7] box, datetime: datetime comparison for indices Timur Safin via Tarantool-patches
                   ` (2 subsequent siblings)
  6 siblings, 1 reply; 9+ messages in thread
From: Timur Safin via Tarantool-patches @ 2021-08-12 22:33 UTC (permalink / raw)
  To: imun, v.shpilevoy; +Cc: tarantool-patches

Serialize datetime_t as newly introduced MP_EXT type.
It saves 1 required integer field and upto 2 optional
unsigned fields in very compact fashion.
- secs is required field;
- but nsec, offset are both optional;

* json, yaml serialization formats, lua output mode
  supported;
* exported symbols for datetime messagepack size calculations
  so they are available for usage on Lua side.

Part of #5941
Part of #5946
---
 extra/exports                     |   2 +
 src/box/field_def.c               |  35 +++---
 src/box/field_def.h               |   1 +
 src/box/lua/serialize_lua.c       |   7 +-
 src/box/msgpack.c                 |   7 +-
 src/box/tuple_compare.cc          |  20 ++++
 src/lib/core/CMakeLists.txt       |   4 +-
 src/lib/core/datetime.c           |   9 ++
 src/lib/core/datetime.h           |  20 ++++
 src/lib/core/mp_datetime.c        | 189 ++++++++++++++++++++++++++++++
 src/lib/core/mp_datetime.h        |  89 ++++++++++++++
 src/lib/core/mp_extension_types.h |   1 +
 src/lib/mpstream/mpstream.c       |  11 ++
 src/lib/mpstream/mpstream.h       |   4 +
 src/lua/msgpack.c                 |  12 ++
 src/lua/msgpackffi.lua            |  18 +++
 src/lua/serializer.c              |   4 +
 src/lua/serializer.h              |   2 +
 src/lua/utils.c                   |   1 -
 test/unit/datetime.c              | 125 +++++++++++++++++++-
 third_party/lua-cjson/lua_cjson.c |   8 ++
 third_party/lua-yaml/lyaml.cc     |   6 +-
 22 files changed, 554 insertions(+), 21 deletions(-)
 create mode 100644 src/lib/core/mp_datetime.c
 create mode 100644 src/lib/core/mp_datetime.h

diff --git a/extra/exports b/extra/exports
index 2437e175c..345ffbf25 100644
--- a/extra/exports
+++ b/extra/exports
@@ -397,6 +397,7 @@ mp_decode_uint
 mp_encode_array
 mp_encode_bin
 mp_encode_bool
+mp_encode_datetime
 mp_encode_decimal
 mp_encode_double
 mp_encode_float
@@ -413,6 +414,7 @@ mp_next
 mp_next_slowpath
 mp_parser_hint
 mp_sizeof_array
+mp_sizeof_datetime
 mp_sizeof_decimal
 mp_sizeof_str
 mp_sizeof_uuid
diff --git a/src/box/field_def.c b/src/box/field_def.c
index 51acb8025..2682a42ee 100644
--- a/src/box/field_def.c
+++ b/src/box/field_def.c
@@ -72,6 +72,7 @@ const uint32_t field_mp_type[] = {
 	/* [FIELD_TYPE_UUID]     =  */ 0, /* only MP_UUID is supported */
 	/* [FIELD_TYPE_ARRAY]    =  */ 1U << MP_ARRAY,
 	/* [FIELD_TYPE_MAP]      =  */ (1U << MP_MAP),
+	/* [FIELD_TYPE_DATETIME] =  */ 0, /* only MP_DATETIME is supported */
 };
 
 const uint32_t field_ext_type[] = {
@@ -83,11 +84,13 @@ const uint32_t field_ext_type[] = {
 	/* [FIELD_TYPE_INTEGER]   = */ 0,
 	/* [FIELD_TYPE_BOOLEAN]   = */ 0,
 	/* [FIELD_TYPE_VARBINARY] = */ 0,
-	/* [FIELD_TYPE_SCALAR]    = */ (1U << MP_DECIMAL) | (1U << MP_UUID),
+	/* [FIELD_TYPE_SCALAR]    = */ (1U << MP_DECIMAL) | (1U << MP_UUID) |
+		(1U << MP_DATETIME),
 	/* [FIELD_TYPE_DECIMAL]   = */ 1U << MP_DECIMAL,
 	/* [FIELD_TYPE_UUID]      = */ 1U << MP_UUID,
 	/* [FIELD_TYPE_ARRAY]     = */ 0,
 	/* [FIELD_TYPE_MAP]       = */ 0,
+	/* [FIELD_TYPE_DATETIME]  = */ 1U << MP_DATETIME,
 };
 
 const char *field_type_strs[] = {
@@ -104,6 +107,7 @@ const char *field_type_strs[] = {
 	/* [FIELD_TYPE_UUID]     = */ "uuid",
 	/* [FIELD_TYPE_ARRAY]    = */ "array",
 	/* [FIELD_TYPE_MAP]      = */ "map",
+	/* [FIELD_TYPE_DATETIME] = */ "datetime",
 };
 
 const char *on_conflict_action_strs[] = {
@@ -128,20 +132,21 @@ field_type_by_name_wrapper(const char *str, uint32_t len)
  * values can be stored in the j type.
  */
 static const bool field_type_compatibility[] = {
-	   /*   ANY   UNSIGNED  STRING   NUMBER  DOUBLE  INTEGER  BOOLEAN VARBINARY SCALAR  DECIMAL   UUID    ARRAY    MAP  */
-/*   ANY    */ true,   false,   false,   false,   false,   false,   false,   false,  false,  false,  false,   false,   false,
-/* UNSIGNED */ true,   true,    false,   true,    false,   true,    false,   false,  true,   false,  false,   false,   false,
-/*  STRING  */ true,   false,   true,    false,   false,   false,   false,   false,  true,   false,  false,   false,   false,
-/*  NUMBER  */ true,   false,   false,   true,    false,   false,   false,   false,  true,   false,  false,   false,   false,
-/*  DOUBLE  */ true,   false,   false,   true,    true,    false,   false,   false,  true,   false,  false,   false,   false,
-/*  INTEGER */ true,   false,   false,   true,    false,   true,    false,   false,  true,   false,  false,   false,   false,
-/*  BOOLEAN */ true,   false,   false,   false,   false,   false,   true,    false,  true,   false,  false,   false,   false,
-/* VARBINARY*/ true,   false,   false,   false,   false,   false,   false,   true,   true,   false,  false,   false,   false,
-/*  SCALAR  */ true,   false,   false,   false,   false,   false,   false,   false,  true,   false,  false,   false,   false,
-/*  DECIMAL */ true,   false,   false,   true,    false,   false,   false,   false,  true,   true,   false,   false,   false,
-/*   UUID   */ true,   false,   false,   false,   false,   false,   false,   false,  false,  false,  true,    false,   false,
-/*   ARRAY  */ true,   false,   false,   false,   false,   false,   false,   false,  false,  false,  false,   true,    false,
-/*    MAP   */ true,   false,   false,   false,   false,   false,   false,   false,  false,  false,  false,   false,   true,
+	   /*   ANY   UNSIGNED  STRING   NUMBER  DOUBLE  INTEGER  BOOLEAN VARBINARY SCALAR  DECIMAL   UUID    ARRAY    MAP     DATETIME */
+/*   ANY    */ true,   false,   false,   false,   false,   false,   false,   false,  false,  false,  false,   false,   false,   false,
+/* UNSIGNED */ true,   true,    false,   true,    false,   true,    false,   false,  true,   false,  false,   false,   false,   false,
+/*  STRING  */ true,   false,   true,    false,   false,   false,   false,   false,  true,   false,  false,   false,   false,   false,
+/*  NUMBER  */ true,   false,   false,   true,    false,   false,   false,   false,  true,   false,  false,   false,   false,   false,
+/*  DOUBLE  */ true,   false,   false,   true,    true,    false,   false,   false,  true,   false,  false,   false,   false,   false,
+/*  INTEGER */ true,   false,   false,   true,    false,   true,    false,   false,  true,   false,  false,   false,   false,   false,
+/*  BOOLEAN */ true,   false,   false,   false,   false,   false,   true,    false,  true,   false,  false,   false,   false,   false,
+/* VARBINARY*/ true,   false,   false,   false,   false,   false,   false,   true,   true,   false,  false,   false,   false,   false,
+/*  SCALAR  */ true,   false,   false,   false,   false,   false,   false,   false,  true,   false,  false,   false,   false,   false,
+/*  DECIMAL */ true,   false,   false,   true,    false,   false,   false,   false,  true,   true,   false,   false,   false,   false,
+/*   UUID   */ true,   false,   false,   false,   false,   false,   false,   false,  false,  false,  true,    false,   false,   false,
+/*   ARRAY  */ true,   false,   false,   false,   false,   false,   false,   false,  false,  false,  false,   true,    false,   false,
+/*    MAP   */ true,   false,   false,   false,   false,   false,   false,   false,  false,  false,  false,   false,   true,    false,
+/* DATETIME */ true,   false,   false,   false,   false,   false,   false,   false,  true,   false,  false,   false,   false,   true,
 };
 
 bool
diff --git a/src/box/field_def.h b/src/box/field_def.h
index c5cfe5e86..120b2a93d 100644
--- a/src/box/field_def.h
+++ b/src/box/field_def.h
@@ -63,6 +63,7 @@ enum field_type {
 	FIELD_TYPE_UUID,
 	FIELD_TYPE_ARRAY,
 	FIELD_TYPE_MAP,
+	FIELD_TYPE_DATETIME,
 	field_type_MAX
 };
 
diff --git a/src/box/lua/serialize_lua.c b/src/box/lua/serialize_lua.c
index 1f791980f..51855011b 100644
--- a/src/box/lua/serialize_lua.c
+++ b/src/box/lua/serialize_lua.c
@@ -768,7 +768,7 @@ static int
 dump_node(struct lua_dumper *d, struct node *nd, int indent)
 {
 	struct luaL_field *field = &nd->field;
-	char buf[FPCONV_G_FMT_BUFSIZE];
+	char buf[FPCONV_G_FMT_BUFSIZE + 8];
 	int ltype = lua_type(d->L, -1);
 	const char *str = NULL;
 	size_t len = 0;
@@ -861,6 +861,11 @@ dump_node(struct lua_dumper *d, struct node *nd, int indent)
 			str = tt_uuid_str(field->uuidval);
 			len = UUID_STR_LEN;
 			break;
+		case MP_DATETIME:
+			nd->mask |= NODE_QUOTE;
+			str = buf;
+			len = datetime_to_string(field->dateval, buf, sizeof buf);
+			break;
 		default:
 			d->err = EINVAL;
 			snprintf(d->err_msg, sizeof(d->err_msg),
diff --git a/src/box/msgpack.c b/src/box/msgpack.c
index 1723dea4c..12f4fd95a 100644
--- a/src/box/msgpack.c
+++ b/src/box/msgpack.c
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020, Tarantool AUTHORS, please see AUTHORS file.
+ * Copyright 2020-2021, Tarantool AUTHORS, please see AUTHORS file.
  *
  * Redistribution and use in source and binary forms, with or
  * without modification, are permitted provided that the following
@@ -35,6 +35,7 @@
 #include "mp_decimal.h"
 #include "uuid/mp_uuid.h"
 #include "mp_error.h"
+#include "mp_datetime.h"
 
 static int
 msgpack_fprint_ext(FILE *file, const char **data, int depth)
@@ -47,6 +48,8 @@ msgpack_fprint_ext(FILE *file, const char **data, int depth)
 		return mp_fprint_decimal(file, data, len);
 	case MP_UUID:
 		return mp_fprint_uuid(file, data, len);
+	case MP_DATETIME:
+		return mp_fprint_datetime(file, data, len);
 	case MP_ERROR:
 		return mp_fprint_error(file, data, depth);
 	default:
@@ -65,6 +68,8 @@ msgpack_snprint_ext(char *buf, int size, const char **data, int depth)
 		return mp_snprint_decimal(buf, size, data, len);
 	case MP_UUID:
 		return mp_snprint_uuid(buf, size, data, len);
+	case MP_DATETIME:
+		return mp_snprint_datetime(buf, size, data, len);
 	case MP_ERROR:
 		return mp_snprint_error(buf, size, data, depth);
 	default:
diff --git a/src/box/tuple_compare.cc b/src/box/tuple_compare.cc
index 43cd29ce9..9a69f2a72 100644
--- a/src/box/tuple_compare.cc
+++ b/src/box/tuple_compare.cc
@@ -36,6 +36,7 @@
 #include "lib/core/decimal.h"
 #include "lib/core/mp_decimal.h"
 #include "uuid/mp_uuid.h"
+#include "lib/core/mp_datetime.h"
 #include "lib/core/mp_extension_types.h"
 
 /* {{{ tuple_compare */
@@ -76,6 +77,7 @@ enum mp_class {
 	MP_CLASS_STR,
 	MP_CLASS_BIN,
 	MP_CLASS_UUID,
+	MP_CLASS_DATETIME,
 	MP_CLASS_ARRAY,
 	MP_CLASS_MAP,
 	mp_class_max,
@@ -99,6 +101,8 @@ static enum mp_class mp_ext_classes[] = {
 	/* .MP_UNKNOWN_EXTENSION = */ mp_class_max, /* unsupported */
 	/* .MP_DECIMAL		 = */ MP_CLASS_NUMBER,
 	/* .MP_UUID		 = */ MP_CLASS_UUID,
+	/* .MP_ERROR		 = */ mp_class_max,
+	/* .MP_DATETIME		 = */ MP_CLASS_DATETIME,
 };
 
 static enum mp_class
@@ -390,6 +394,19 @@ mp_compare_uuid(const char *field_a, const char *field_b)
 	return memcmp(field_a + 2, field_b + 2, UUID_PACKED_LEN);
 }
 
+static int
+mp_compare_datetime(const char *lhs, const char *rhs)
+{
+	struct datetime lhs_dt, rhs_dt;
+	struct datetime *ret;
+	ret = mp_decode_datetime(&lhs, &lhs_dt);
+	assert(ret != NULL);
+	ret = mp_decode_datetime(&rhs, &rhs_dt);
+	assert(ret != NULL);
+	(void)ret;
+	return datetime_compare(&lhs_dt, &rhs_dt);
+}
+
 typedef int (*mp_compare_f)(const char *, const char *);
 static mp_compare_f mp_class_comparators[] = {
 	/* .MP_CLASS_NIL    = */ NULL,
@@ -398,6 +415,7 @@ static mp_compare_f mp_class_comparators[] = {
 	/* .MP_CLASS_STR    = */ mp_compare_str,
 	/* .MP_CLASS_BIN    = */ mp_compare_bin,
 	/* .MP_CLASS_UUID   = */ mp_compare_uuid,
+	/* .MP_CLASS_DATETIME=*/ mp_compare_datetime,
 	/* .MP_CLASS_ARRAY  = */ NULL,
 	/* .MP_CLASS_MAP    = */ NULL,
 };
@@ -478,6 +496,8 @@ tuple_compare_field(const char *field_a, const char *field_b,
 		return mp_compare_decimal(field_a, field_b);
 	case FIELD_TYPE_UUID:
 		return mp_compare_uuid(field_a, field_b);
+	case FIELD_TYPE_DATETIME:
+		return mp_compare_datetime(field_a, field_b);
 	default:
 		unreachable();
 		return 0;
diff --git a/src/lib/core/CMakeLists.txt b/src/lib/core/CMakeLists.txt
index 8bc776b82..61fc6548f 100644
--- a/src/lib/core/CMakeLists.txt
+++ b/src/lib/core/CMakeLists.txt
@@ -31,6 +31,7 @@ set(core_sources
     mp_decimal.c
     cord_buf.c
     datetime.c
+    mp_datetime.c
 )
 
 if (TARGET_OS_NETBSD)
@@ -44,7 +45,8 @@ add_library(core STATIC ${core_sources})
 
 target_link_libraries(core salad small uri decNumber bit ${LIBEV_LIBRARIES}
                       ${LIBEIO_LIBRARIES} ${LIBCORO_LIBRARIES}
-                      ${MSGPUCK_LIBRARIES} ${ICU_LIBRARIES})
+                      ${MSGPUCK_LIBRARIES} ${ICU_LIBRARIES}
+                      ${LIBCDT_LIBRARIES})
 
 if (ENABLE_BACKTRACE AND NOT TARGET_OS_DARWIN)
     target_link_libraries(core gcc_s ${UNWIND_LIBRARIES})
diff --git a/src/lib/core/datetime.c b/src/lib/core/datetime.c
index e0300417a..7b65e28db 100644
--- a/src/lib/core/datetime.c
+++ b/src/lib/core/datetime.c
@@ -165,3 +165,12 @@ datetime_to_string(const struct datetime *date, char *buf, uint32_t len)
 }
 #undef ADVANCE
 
+int
+datetime_compare(const struct datetime *lhs, const struct datetime *rhs)
+{
+	int result = COMPARE_RESULT(lhs->secs, rhs->secs);
+	if (result != 0)
+		return result;
+
+	return COMPARE_RESULT(lhs->nsec, rhs->nsec);
+}
diff --git a/src/lib/core/datetime.h b/src/lib/core/datetime.h
index 4ec459860..a3c799d7a 100644
--- a/src/lib/core/datetime.h
+++ b/src/lib/core/datetime.h
@@ -70,6 +70,26 @@ struct datetime_interval {
 	int32_t nsec;
 };
 
+/**
+ * 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(const struct datetime *date, char *buf, uint32_t len);
+
 /**
  * Convert datetime to string using default asctime format
  * "Sun Sep 16 01:03:52 1973\n\0"
diff --git a/src/lib/core/mp_datetime.c b/src/lib/core/mp_datetime.c
new file mode 100644
index 000000000..d0a3e562c
--- /dev/null
+++ b/src/lib/core/mp_datetime.c
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2021, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include "mp_datetime.h"
+#include "msgpuck.h"
+#include "mp_extension_types.h"
+
+/*
+  Datetime MessagePack serialization schema is MP_EXT (0xC7 for 1 byte length)
+  extension, which creates container of 1 to 3 integers.
+
+  +----+---+-----------+====~~~~~~~====+-----~~~~~~~~-------+....~~~~~~~....+
+  |0xC7| 4 |len (uint8)| seconds (int) | nanoseconds (uint) | offset (uint) |
+  +----+---+-----------+====~~~~~~~====+-----~~~~~~~~-------+....~~~~~~~....+
+
+  MessagePack extension MP_EXT (0xC7), after 1-byte length, contains:
+
+  - signed integer seconds part (required). Depending on the value of
+    seconds it may be from 1 to 8 bytes positive or negative integer number;
+
+  - [optional] fraction time in nanoseconds as unsigned integer.
+    If this value is 0 then it's not saved (unless there is offset field,
+    as below);
+
+  - [optional] timzeone offset in minutes as unsigned integer.
+    If this field is 0 then it's not saved.
+ */
+
+static inline uint32_t
+mp_sizeof_Xint(int64_t n)
+{
+	return n < 0 ? mp_sizeof_int(n) : mp_sizeof_uint(n);
+}
+
+static inline char *
+mp_encode_Xint(char *data, int64_t v)
+{
+	return v < 0 ? mp_encode_int(data, v) : mp_encode_uint(data, v);
+}
+
+static inline int64_t
+mp_decode_Xint(const char **data)
+{
+	switch (mp_typeof(**data)) {
+	case MP_UINT:
+		return (int64_t)mp_decode_uint(data);
+	case MP_INT:
+		return mp_decode_int(data);
+	default:
+		mp_unreachable();
+	}
+	return 0;
+}
+
+static inline uint32_t
+mp_sizeof_datetime_raw(const struct datetime *date)
+{
+	uint32_t sz = mp_sizeof_Xint(date->secs);
+
+	// even if nanosecs == 0 we need to output anything
+	// if we have non-null tz offset
+	if (date->nsec != 0 || date->offset != 0)
+		sz += mp_sizeof_Xint(date->nsec);
+	if (date->offset)
+		sz += mp_sizeof_Xint(date->offset);
+	return sz;
+}
+
+uint32_t
+mp_sizeof_datetime(const struct datetime *date)
+{
+	return mp_sizeof_ext(mp_sizeof_datetime_raw(date));
+}
+
+struct datetime *
+datetime_unpack(const char **data, uint32_t len, struct datetime *date)
+{
+	const char * svp = *data;
+
+	memset(date, 0, sizeof(*date));
+
+	date->secs = mp_decode_Xint(data);
+
+	len -= *data - svp;
+	if (len <= 0)
+		return date;
+
+	svp = *data;
+	date->nsec = mp_decode_Xint(data);
+	len -= *data - svp;
+
+	if (len <= 0)
+		return date;
+
+	date->offset = mp_decode_Xint(data);
+
+	return date;
+}
+
+struct datetime *
+mp_decode_datetime(const char **data, struct datetime *date)
+{
+	if (mp_typeof(**data) != MP_EXT)
+		return NULL;
+
+	int8_t type;
+	uint32_t len = mp_decode_extl(data, &type);
+
+	if (type != MP_DATETIME || len == 0) {
+		return NULL;
+	}
+	return datetime_unpack(data, len, date);
+}
+
+char *
+datetime_pack(char *data, const struct datetime *date)
+{
+	data = mp_encode_Xint(data, date->secs);
+	if (date->nsec != 0 || date->offset != 0)
+		data = mp_encode_Xint(data, date->nsec);
+	if (date->offset)
+		data = mp_encode_Xint(data, date->offset);
+
+	return data;
+}
+
+char *
+mp_encode_datetime(char *data, const struct datetime *date)
+{
+	uint32_t len = mp_sizeof_datetime_raw(date);
+
+	data = mp_encode_extl(data, MP_DATETIME, len);
+
+	return datetime_pack(data, date);
+}
+
+int
+mp_snprint_datetime(char *buf, int size, const char **data, uint32_t len)
+{
+	struct datetime date = {0};
+
+	if (datetime_unpack(data, len, &date) == NULL)
+		return -1;
+
+	return datetime_to_string(&date, buf, size);
+}
+
+int
+mp_fprint_datetime(FILE *file, const char **data, uint32_t len)
+{
+	struct datetime date;
+
+	if (datetime_unpack(data, len, &date) == NULL)
+		return -1;
+
+	char buf[48];
+	datetime_to_string(&date, buf, sizeof buf);
+
+	return fprintf(file, "%s", buf);
+}
+
diff --git a/src/lib/core/mp_datetime.h b/src/lib/core/mp_datetime.h
new file mode 100644
index 000000000..9a4d2720c
--- /dev/null
+++ b/src/lib/core/mp_datetime.h
@@ -0,0 +1,89 @@
+#pragma once
+/*
+ * Copyright 2021, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+#include "datetime.h"
+
+#if defined(__cplusplus)
+extern "C"
+{
+#endif /* defined(__cplusplus) */
+
+/**
+ * Unpack datetime data from MessagePack buffer.
+ * @sa datetime_pack
+ */
+struct datetime *
+datetime_unpack(const char **data, uint32_t len, struct datetime *date);
+
+/**
+ * Pack datetime data to MessagePack buffer.
+ * @sa datetime_unpack
+ */
+char *
+datetime_pack(char *data, const struct datetime *date);
+
+/**
+ * Calculate size of MessagePack buffer for datetime data.
+ */
+uint32_t
+mp_sizeof_datetime(const struct datetime *date);
+
+/**
+ * Decode data from MessagePack buffer to datetime structure.
+ */
+struct datetime *
+mp_decode_datetime(const char **data, struct datetime *date);
+
+/**
+ * Encode datetime structure to the MessagePack buffer.
+ */
+char *
+mp_encode_datetime(char *data, const struct datetime *date);
+
+/**
+ * Print datetime's string representation into a given buffer.
+ * @sa mp_snprint_decimal
+ */
+int
+mp_snprint_datetime(char *buf, int size, const char **data, uint32_t len);
+
+/**
+ * Print datetime's string representation into a stream.
+ * @sa mp_fprint_decimal
+ */
+int
+mp_fprint_datetime(FILE *file, const char **data, uint32_t len);
+
+#if defined(__cplusplus)
+} /* extern "C" */
+#endif /* defined(__cplusplus) */
diff --git a/src/lib/core/mp_extension_types.h b/src/lib/core/mp_extension_types.h
index e3ff9f5d0..3b7eaee7c 100644
--- a/src/lib/core/mp_extension_types.h
+++ b/src/lib/core/mp_extension_types.h
@@ -44,6 +44,7 @@ enum mp_extension_type {
     MP_DECIMAL = 1,
     MP_UUID = 2,
     MP_ERROR = 3,
+    MP_DATETIME = 4,
     mp_extension_type_MAX,
 };
 
diff --git a/src/lib/mpstream/mpstream.c b/src/lib/mpstream/mpstream.c
index 70ca29889..d3e1de965 100644
--- a/src/lib/mpstream/mpstream.c
+++ b/src/lib/mpstream/mpstream.c
@@ -35,6 +35,7 @@
 #include "msgpuck.h"
 #include "mp_decimal.h"
 #include "uuid/mp_uuid.h"
+#include "mp_datetime.h"
 
 void
 mpstream_reserve_slow(struct mpstream *stream, size_t size)
@@ -208,6 +209,16 @@ mpstream_encode_uuid(struct mpstream *stream, const struct tt_uuid *uuid)
 	mpstream_advance(stream, pos - data);
 }
 
+void
+mpstream_encode_datetime(struct mpstream *stream, const struct datetime *val)
+{
+	char *data = mpstream_reserve(stream, mp_sizeof_datetime(val));
+	if (data == NULL)
+		return;
+	char *pos = mp_encode_datetime(data, val);
+	mpstream_advance(stream, pos - data);
+}
+
 void
 mpstream_memcpy(struct mpstream *stream, const void *src, uint32_t n)
 {
diff --git a/src/lib/mpstream/mpstream.h b/src/lib/mpstream/mpstream.h
index a60add143..94831160f 100644
--- a/src/lib/mpstream/mpstream.h
+++ b/src/lib/mpstream/mpstream.h
@@ -39,6 +39,7 @@ extern "C" {
 #endif /* defined(__cplusplus) */
 
 struct tt_uuid;
+struct datetime;
 
 /**
 * Ask the allocator to reserve at least size bytes. It can reserve
@@ -145,6 +146,9 @@ mpstream_encode_decimal(struct mpstream *stream, const decimal_t *val);
 void
 mpstream_encode_uuid(struct mpstream *stream, const struct tt_uuid *uuid);
 
+void
+mpstream_encode_datetime(struct mpstream *stream, const struct datetime *dt);
+
 /** Copies n bytes from memory area src to stream. */
 void
 mpstream_memcpy(struct mpstream *stream, const void *src, uint32_t n);
diff --git a/src/lua/msgpack.c b/src/lua/msgpack.c
index b6ecf2b1e..bca53f6b7 100644
--- a/src/lua/msgpack.c
+++ b/src/lua/msgpack.c
@@ -46,6 +46,7 @@
 #include "lib/core/decimal.h" /* decimal_unpack() */
 #include "lib/uuid/mp_uuid.h" /* mp_decode_uuid() */
 #include "lib/core/mp_extension_types.h"
+#include "lib/core/mp_datetime.h"
 
 #include "cord_buf.h"
 #include <fiber.h>
@@ -200,6 +201,9 @@ restart: /* used by MP_EXT of unidentified subtype */
 			break;
 		case MP_ERROR:
 			return luamp_encode_extension(L, top, stream);
+		case MP_DATETIME:
+			mpstream_encode_datetime(stream, field->dateval);
+			break;
 		default:
 			/* Run trigger if type can't be encoded */
 			type = luamp_encode_extension(L, top, stream);
@@ -333,6 +337,14 @@ luamp_decode(struct lua_State *L, struct luaL_serializer *cfg,
 				goto ext_decode_err;
 			return;
 		}
+		case MP_DATETIME:
+		{
+			struct datetime *date = luaL_pushdatetime(L);
+			date = datetime_unpack(data, len, date);
+			if (date == NULL)
+				goto ext_decode_err;
+			return;
+		}
 		default:
 			/* reset data to the extension header */
 			*data = svp;
diff --git a/src/lua/msgpackffi.lua b/src/lua/msgpackffi.lua
index 1d54f11b8..fb5e7d644 100644
--- a/src/lua/msgpackffi.lua
+++ b/src/lua/msgpackffi.lua
@@ -26,6 +26,10 @@ char *
 mp_encode_uuid(char *data, const struct tt_uuid *uuid);
 uint32_t
 mp_sizeof_uuid();
+uint32_t
+mp_sizeof_datetime(const struct t_datetime_tz *date);
+char *
+mp_encode_datetime(char *data, const struct t_datetime_tz *date);
 float
 mp_decode_float(const char **data);
 double
@@ -36,6 +40,8 @@ decimal_t *
 decimal_unpack(const char **data, uint32_t len, decimal_t *dec);
 struct tt_uuid *
 uuid_unpack(const char **data, uint32_t len, struct tt_uuid *uuid);
+struct datetime *
+datetime_unpack(const char **data, uint32_t len, struct datetime *date);
 ]])
 
 local strict_alignment = (jit.arch == 'arm')
@@ -142,6 +148,11 @@ local function encode_uuid(buf, uuid)
     builtin.mp_encode_uuid(p, uuid)
 end
 
+local function encode_datetime(buf, date)
+    local p = buf:alloc(builtin.mp_sizeof_datetime(date))
+    builtin.mp_encode_datetime(p, date)
+end
+
 local function encode_int(buf, num)
     if num >= 0 then
         if num <= 0x7f then
@@ -320,6 +331,7 @@ on_encode(ffi.typeof('float'), encode_float)
 on_encode(ffi.typeof('double'), encode_double)
 on_encode(ffi.typeof('decimal_t'), encode_decimal)
 on_encode(ffi.typeof('struct tt_uuid'), encode_uuid)
+on_encode(ffi.typeof('struct datetime'), encode_datetime)
 
 --------------------------------------------------------------------------------
 -- Decoder
@@ -513,6 +525,12 @@ local ext_decoder = {
         builtin.uuid_unpack(data, len, uuid)
         return uuid
     end,
+    -- MP_DATETIME
+    [4] = function(data, len)
+        local dt = ffi.new("struct datetime")
+        builtin.datetime_unpack(data, len, dt)
+        return dt
+    end,
 }
 
 local function decode_ext(data)
diff --git a/src/lua/serializer.c b/src/lua/serializer.c
index 8db6746a3..24f4a5ff9 100644
--- a/src/lua/serializer.c
+++ b/src/lua/serializer.c
@@ -41,6 +41,7 @@
 #include "lib/core/mp_extension_types.h"
 #include "lua/error.h"
 
+#include "datetime.h"
 #include "trivia/util.h"
 #include "diag.h"
 #include "serializer_opts.h"
@@ -544,6 +545,9 @@ luaL_tofield(struct lua_State *L, struct luaL_serializer *cfg,
 				   opts != NULL &&
 				   opts->error_marshaling_enabled) {
 				field->ext_type = MP_ERROR;
+			} else if (cd->ctypeid == CTID_DATETIME) {
+				field->ext_type = MP_DATETIME;
+				field->dateval = (struct datetime *)cdata;
 			} else {
 				field->ext_type = MP_UNKNOWN_EXTENSION;
 			}
diff --git a/src/lua/serializer.h b/src/lua/serializer.h
index 0a0501a74..e7a240e0a 100644
--- a/src/lua/serializer.h
+++ b/src/lua/serializer.h
@@ -52,6 +52,7 @@ extern "C" {
 #include <lauxlib.h>
 
 #include "trigger.h"
+#include "lib/core/datetime.h"
 #include "lib/core/decimal.h" /* decimal_t */
 #include "lib/core/mp_extension_types.h"
 #include "lua/error.h"
@@ -223,6 +224,7 @@ struct luaL_field {
 		uint32_t size;
 		decimal_t *decval;
 		struct tt_uuid *uuidval;
+		struct datetime *dateval;
 	};
 	enum mp_type type;
 	/* subtypes of MP_EXT */
diff --git a/src/lua/utils.c b/src/lua/utils.c
index 9130b60b5..e1d796041 100644
--- a/src/lua/utils.c
+++ b/src/lua/utils.c
@@ -254,7 +254,6 @@ luaL_setcdatagc(struct lua_State *L, int idx)
 	lua_pop(L, 1);
 }
 
-
 /**
  * A helper to register a single type metatable.
  */
diff --git a/test/unit/datetime.c b/test/unit/datetime.c
index f718b7de0..fcaebfe2d 100644
--- a/test/unit/datetime.c
+++ b/test/unit/datetime.c
@@ -6,6 +6,9 @@
 
 #include "unit.h"
 #include "datetime.h"
+#include "mp_datetime.h"
+#include "msgpuck.h"
+#include "mp_extension_types.h"
 
 static const char sample[] = "2012-12-24T15:30Z";
 
@@ -245,12 +248,132 @@ tostring_datetime_test(void)
 	check_plan();
 }
 
+static void
+mp_datetime_test()
+{
+	static struct {
+		int64_t     secs;
+		uint32_t    nsec;
+		uint32_t    offset;
+		uint32_t    len;
+	} tests[] = {
+		{/* '1970-01-01T02:00+02:00' */          0,         0,  120, 6},
+		{/* '1970-01-01T01:30+01:30' */          0,         0,   90, 6},
+		{/* '1970-01-01T01:00+01:00' */          0,         0,   60, 6},
+		{/* '1970-01-01T00:01+00:01' */          0,         0,    1, 6},
+		{/* '1970-01-01T00:00Z' */               0,         0,    0, 3},
+		{/* '1969-12-31T23:59-00:01' */          0,         0,   -1, 6},
+		{/* '1969-12-31T23:00-01:00' */          0,         0,  -60, 6},
+		{/* '1969-12-31T22:30-01:30' */          0,         0,  -90, 6},
+		{/* '1969-12-31T22:00-02:00' */          0,         0, -120, 6},
+		{/* '1970-01-01T00:00:00.123456789Z' */  0, 123456789,    0, 9},
+		{/* '1970-01-01T00:00:00.123456Z' */     0, 123456000,    0, 9},
+		{/* '1970-01-01T00:00:00.123Z' */        0, 123000000,    0, 9},
+		{/* '1973-11-29T21:33:09Z' */    123456789,         0,    0, 8},
+		{/* '2013-10-28T17:51:56Z' */   1382982716,         0,    0, 8},
+		{/* '9999-12-31T23:59:59Z' */ 253402300799,         0,    0, 12},
+		{/* '9999-12-31T23:59:59.123456789Z' */ 253402300799, 123456789, 0, 17},
+		{/* '9999-12-31T23:59:59.123456789-02:00' */ 253402300799, 123456789, -120, 18},
+	};
+	size_t index;
+
+	plan(85);
+	for (index = 0; index < DIM(tests); index++) {
+		struct datetime date = {
+			tests[index].secs,
+			tests[index].nsec,
+			tests[index].offset
+		};
+		char buf[24], *data = buf;
+		const char *data1 = buf;
+		struct datetime ret;
+
+		char *end = mp_encode_datetime(data, &date);
+		uint32_t len = mp_sizeof_datetime(&date);
+		is(len, tests[index].len, "len %u, expected len %u",
+		   len, tests[index].len);
+		is(end - data, len,
+		   "mp_sizeof_datetime(%d) == encoded length %ld",
+		   len, end - data);
+
+		struct datetime *rc = mp_decode_datetime(&data1, &ret);
+		is(rc, &ret, "mp_decode_datetime() return code");
+		is(data1, end, "mp_sizeof_uuid() == decoded length");
+		is(datetime_compare(&date, &ret), 0, "datetime_compare(&date, &ret)");
+	}
+	check_plan();
+}
+
+
+static int
+mp_fprint_ext_test(FILE *file, const char **data, int depth)
+{
+	(void)depth;
+	int8_t type;
+	uint32_t len = mp_decode_extl(data, &type);
+	if (type != MP_DATETIME)
+		return fprintf(file, "undefined");
+	return mp_fprint_datetime(file, data, len);
+}
+
+static int
+mp_snprint_ext_test(char *buf, int size, const char **data, int depth)
+{
+        (void)depth;
+        int8_t type;
+        uint32_t len = mp_decode_extl(data, &type);
+        if (type != MP_DATETIME)
+                return snprintf(buf, size, "undefined");
+        return mp_snprint_datetime(buf, size, data, len);
+}
+
+static void
+mp_print_test(void)
+{
+	plan(5);
+	header();
+
+	mp_snprint_ext = mp_snprint_ext_test;
+	mp_fprint_ext = mp_fprint_ext_test;
+
+	char sample[64];
+	char buffer[64];
+	char str[64];
+	struct datetime date = {0, 0, 0}; // 1970-01-01T00:00Z
+
+	mp_encode_datetime(buffer, &date);
+	int sz = datetime_to_string(&date, str, sizeof str);
+	int rc = mp_snprint(NULL, 0, buffer);
+	is(rc, sz, "correct mp_snprint size %u with empty buffer", rc);
+	rc = mp_snprint(str, sizeof(str), buffer);
+	is(rc, sz, "correct mp_snprint size %u", rc);
+	datetime_to_string(&date, sample, sizeof sample);
+	is(strcmp(str, sample), 0, "correct mp_snprint result");
+
+	FILE *f = tmpfile();
+	rc = mp_fprint(f, buffer);
+	is(rc, sz, "correct mp_fprint size %u", sz);
+	rewind(f);
+	rc = fread(str, 1, sizeof(str), f);
+	str[rc] = 0;
+	is(strcmp(str, sample), 0, "correct mp_fprint result %u", rc);
+	fclose(f);
+
+	mp_snprint_ext = mp_snprint_ext_default;
+	mp_fprint_ext = mp_fprint_ext_default;
+
+	footer();
+	check_plan();
+}
+
 int
 main(void)
 {
-	plan(2);
+	plan(4);
 	datetime_test();
 	tostring_datetime_test();
+	mp_datetime_test();
+	mp_print_test();
 
 	return check_plan();
 }
diff --git a/third_party/lua-cjson/lua_cjson.c b/third_party/lua-cjson/lua_cjson.c
index 7a326075a..924e0419a 100644
--- a/third_party/lua-cjson/lua_cjson.c
+++ b/third_party/lua-cjson/lua_cjson.c
@@ -52,6 +52,7 @@
 #include "mp_extension_types.h" /* MP_DECIMAL, MP_UUID */
 #include "tt_static.h"
 #include "uuid/tt_uuid.h" /* tt_uuid_to_string(), UUID_STR_LEN */
+#include "core/datetime.h"
 #include "cord_buf.h"
 
 typedef enum {
@@ -426,6 +427,13 @@ static void json_append_data(lua_State *l, struct luaL_serializer *cfg,
         case MP_UUID:
             return json_append_string(cfg, json, tt_uuid_str(field.uuidval),
                                       UUID_STR_LEN);
+
+        case MP_DATETIME:
+        {
+            char buf[128];
+            size_t sz = datetime_to_string(field.dateval, buf, sizeof buf);
+            return json_append_string(cfg, json, buf, sz);
+        }
         default:
             assert(false);
         }
diff --git a/third_party/lua-yaml/lyaml.cc b/third_party/lua-yaml/lyaml.cc
index 2b67dcc6a..034104737 100644
--- a/third_party/lua-yaml/lyaml.cc
+++ b/third_party/lua-yaml/lyaml.cc
@@ -617,7 +617,7 @@ static int dump_node(struct lua_yaml_dumper *dumper)
    yaml_event_t ev;
    yaml_scalar_style_t style = YAML_PLAIN_SCALAR_STYLE;
    int is_binary = 0;
-   char buf[FPCONV_G_FMT_BUFSIZE];
+   char buf[FPCONV_G_FMT_BUFSIZE + 8];
    struct luaL_field field;
    bool unused;
    (void) unused;
@@ -707,6 +707,10 @@ static int dump_node(struct lua_yaml_dumper *dumper)
          str = tt_uuid_str(field.uuidval);
          len = UUID_STR_LEN;
          break;
+      case MP_DATETIME:
+         len = datetime_to_string(field.dateval, buf, sizeof buf);
+         str = buf;
+         break;
       default:
          assert(0); /* checked by luaL_checkfield() */
       }
-- 
2.29.2


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

* [Tarantool-patches] [PATCH v4 5/7] box, datetime: datetime comparison for indices
  2021-08-12 22:33 [Tarantool-patches] [PATCH v4 0/7] Initial datetime support Timur Safin via Tarantool-patches
                   ` (3 preceding siblings ...)
  2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 4/7] box, datetime: messagepack support for datetime Timur Safin via Tarantool-patches
@ 2021-08-12 22:33 ` Timur Safin via Tarantool-patches
  2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 6/7] lua, datetime: time intervals support Timur Safin via Tarantool-patches
  2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 7/7] datetime: changelog for datetime module Timur Safin via Tarantool-patches
  6 siblings, 0 replies; 9+ messages in thread
From: Timur Safin via Tarantool-patches @ 2021-08-12 22:33 UTC (permalink / raw)
  To: imun, v.shpilevoy; +Cc: tarantool-patches

* storage hints implemented for datetime_t values;
* proper comparison for indices of datetime type.

Part of #5941
Part of #5946
---
 src/box/field_def.c           | 18 ++++++++
 src/box/field_def.h           |  3 ++
 src/box/memtx_space.c         |  3 +-
 src/box/tuple_compare.cc      | 57 ++++++++++++++++++++++++++
 src/box/vinyl.c               |  3 +-
 test/engine/datetime.result   | 77 +++++++++++++++++++++++++++++++++++
 test/engine/datetime.test.lua | 35 ++++++++++++++++
 7 files changed, 192 insertions(+), 4 deletions(-)
 create mode 100644 test/engine/datetime.result
 create mode 100644 test/engine/datetime.test.lua

diff --git a/src/box/field_def.c b/src/box/field_def.c
index 2682a42ee..97033d0bb 100644
--- a/src/box/field_def.c
+++ b/src/box/field_def.c
@@ -194,3 +194,21 @@ field_type_by_name(const char *name, size_t len)
 		return FIELD_TYPE_ANY;
 	return field_type_MAX;
 }
+
+const bool field_type_index_allowed[] =
+    {
+	/* [FIELD_TYPE_ANY]      = */ false,
+	/* [FIELD_TYPE_UNSIGNED] = */ true,
+	/* [FIELD_TYPE_STRING]   = */ true,
+	/* [FIELD_TYPE_NUMBER]   = */ true,
+	/* [FIELD_TYPE_DOUBLE]   = */ true,
+	/* [FIELD_TYPE_INTEGER]  = */ true,
+	/* [FIELD_TYPE_BOOLEAN]  = */ true,
+	/* [FIELD_TYPE_VARBINARY]= */ true,
+	/* [FIELD_TYPE_SCALAR]   = */ true,
+	/* [FIELD_TYPE_DECIMAL]  = */ true,
+	/* [FIELD_TYPE_UUID]     = */ true,
+	/* [FIELD_TYPE_ARRAY]    = */ false,
+	/* [FIELD_TYPE_MAP]      = */ false,
+	/* [FIELD_TYPE_DATETIME] = */ true,
+};
diff --git a/src/box/field_def.h b/src/box/field_def.h
index 120b2a93d..bd02418df 100644
--- a/src/box/field_def.h
+++ b/src/box/field_def.h
@@ -120,6 +120,9 @@ extern const uint32_t field_ext_type[];
 extern const struct opt_def field_def_reg[];
 extern const struct field_def field_def_default;
 
+/** helper table for checking allowed indices for types */
+extern const bool field_type_index_allowed[];
+
 /**
  * @brief Field definition
  * Contains information about of one tuple field.
diff --git a/src/box/memtx_space.c b/src/box/memtx_space.c
index b71318d24..1ab16122e 100644
--- a/src/box/memtx_space.c
+++ b/src/box/memtx_space.c
@@ -748,8 +748,7 @@ memtx_space_check_index_def(struct space *space, struct index_def *index_def)
 	/* Check that there are no ANY, ARRAY, MAP parts */
 	for (uint32_t i = 0; i < key_def->part_count; i++) {
 		struct key_part *part = &key_def->parts[i];
-		if (part->type <= FIELD_TYPE_ANY ||
-		    part->type >= FIELD_TYPE_ARRAY) {
+		if (!field_type_index_allowed[part->type]) {
 			diag_set(ClientError, ER_MODIFY_INDEX,
 				 index_def->name, space_name(space),
 				 tt_sprintf("field type '%s' is not supported",
diff --git a/src/box/tuple_compare.cc b/src/box/tuple_compare.cc
index 9a69f2a72..110017853 100644
--- a/src/box/tuple_compare.cc
+++ b/src/box/tuple_compare.cc
@@ -538,6 +538,8 @@ tuple_compare_field_with_type(const char *field_a, enum mp_type a_type,
 						   field_b, b_type);
 	case FIELD_TYPE_UUID:
 		return mp_compare_uuid(field_a, field_b);
+	case FIELD_TYPE_DATETIME:
+		return mp_compare_datetime(field_a, field_b);
 	default:
 		unreachable();
 		return 0;
@@ -1538,6 +1540,21 @@ func_index_compare_with_key(struct tuple *tuple, hint_t tuple_hint,
 #define HINT_VALUE_DOUBLE_MAX	(exp2(HINT_VALUE_BITS - 1) - 1)
 #define HINT_VALUE_DOUBLE_MIN	(-exp2(HINT_VALUE_BITS - 1))
 
+/**
+ * We need to squeeze 64 bits of seconds and 32 bits of nanoseconds
+ * into 60 bits of hint value. The idea is to represent wide enough
+ * years range, and leave the rest of bits occupied from nanoseconds part:
+ * - 36 bits is enough for time range of [208BC..4147]
+ * - for nanoseconds there is left 24 bits, which are MSB part of
+ *   32-bit value
+ */
+#define HINT_VALUE_SECS_BITS	36
+#define HINT_VALUE_NSEC_BITS	(HINT_VALUE_BITS - HINT_VALUE_SECS_BITS)
+#define HINT_VALUE_SECS_MAX	((1LL << HINT_VALUE_SECS_BITS) - 1)
+#define HINT_VALUE_SECS_MIN	(-(1LL << HINT_VALUE_SECS_BITS))
+#define HINT_VALUE_NSEC_SHIFT	(sizeof(int) * CHAR_BIT - HINT_VALUE_NSEC_BITS)
+#define HINT_VALUE_NSEC_MAX	((1ULL << HINT_VALUE_NSEC_BITS) - 1)
+
 /*
  * HINT_CLASS_BITS should be big enough to store any mp_class value.
  * Note, ((1 << HINT_CLASS_BITS) - 1) is reserved for HINT_NONE.
@@ -1630,6 +1647,25 @@ hint_uuid_raw(const char *data)
 	return hint_create(MP_CLASS_UUID, val);
 }
 
+static inline hint_t
+hint_datetime(struct datetime *date)
+{
+	/*
+	 * Use at most HINT_VALUE_SECS_BITS from datetime
+	 * seconds field as a hint value, and at MSB part
+	 * of HINT_VALUE_NSEC_BITS from nanoseconds.
+	 */
+	int64_t secs = date->secs;
+	int32_t nsec = date->nsec;
+	uint64_t val = secs <= HINT_VALUE_SECS_MIN ? 0 :
+			secs - HINT_VALUE_SECS_MIN;
+	if (val >= HINT_VALUE_SECS_MAX)
+		val = HINT_VALUE_SECS_MAX;
+	val <<= HINT_VALUE_NSEC_BITS;
+	val |= (nsec >> HINT_VALUE_NSEC_SHIFT) & HINT_VALUE_NSEC_MAX;
+	return hint_create(MP_CLASS_DATETIME, val);
+}
+
 static inline uint64_t
 hint_str_raw(const char *s, uint32_t len)
 {
@@ -1761,6 +1797,17 @@ field_hint_uuid(const char *field)
 	return hint_uuid_raw(data);
 }
 
+static inline hint_t
+field_hint_datetime(const char *field)
+{
+	assert(mp_typeof(*field) == MP_EXT);
+	int8_t ext_type;
+	uint32_t len = mp_decode_extl(&field, &ext_type);
+	assert(ext_type == MP_DATETIME);
+	struct datetime date;
+	return hint_datetime(datetime_unpack(&field, len, &date));
+}
+
 static inline hint_t
 field_hint_string(const char *field, struct coll *coll)
 {
@@ -1812,6 +1859,11 @@ field_hint_scalar(const char *field, struct coll *coll)
 		}
 		case MP_UUID:
 			return hint_uuid_raw(field);
+		case MP_DATETIME:
+		{
+			struct datetime date;
+			return hint_datetime(datetime_unpack(&field, len, &date));
+		}
 		default:
 			unreachable();
 		}
@@ -1849,6 +1901,8 @@ field_hint(const char *field, struct coll *coll)
 		return field_hint_decimal(field);
 	case FIELD_TYPE_UUID:
 		return field_hint_uuid(field);
+	case FIELD_TYPE_DATETIME:
+		return field_hint_datetime(field);
 	default:
 		unreachable();
 	}
@@ -1963,6 +2017,9 @@ key_def_set_hint_func(struct key_def *def)
 	case FIELD_TYPE_UUID:
 		key_def_set_hint_func<FIELD_TYPE_UUID>(def);
 		break;
+	case FIELD_TYPE_DATETIME:
+		key_def_set_hint_func<FIELD_TYPE_DATETIME>(def);
+		break;
 	default:
 		/* Invalid key definition. */
 		def->key_hint = NULL;
diff --git a/src/box/vinyl.c b/src/box/vinyl.c
index c80b2d99b..360d1fa70 100644
--- a/src/box/vinyl.c
+++ b/src/box/vinyl.c
@@ -662,8 +662,7 @@ vinyl_space_check_index_def(struct space *space, struct index_def *index_def)
 	/* Check that there are no ANY, ARRAY, MAP parts */
 	for (uint32_t i = 0; i < key_def->part_count; i++) {
 		struct key_part *part = &key_def->parts[i];
-		if (part->type <= FIELD_TYPE_ANY ||
-		    part->type >= FIELD_TYPE_ARRAY) {
+		if (!field_type_index_allowed[part->type]) {
 			diag_set(ClientError, ER_MODIFY_INDEX,
 				 index_def->name, space_name(space),
 				 tt_sprintf("field type '%s' is not supported",
diff --git a/test/engine/datetime.result b/test/engine/datetime.result
new file mode 100644
index 000000000..848a0aaec
--- /dev/null
+++ b/test/engine/datetime.result
@@ -0,0 +1,77 @@
+-- test-run result file version 2
+env = require('test_run')
+ | ---
+ | ...
+test_run = env.new()
+ | ---
+ | ...
+engine = test_run:get_cfg('engine')
+ | ---
+ | ...
+
+date = require('datetime')
+ | ---
+ | ...
+
+_ = box.schema.space.create('T', {engine = engine})
+ | ---
+ | ...
+_ = box.space.T:create_index('pk', {parts={1,'datetime'}})
+ | ---
+ | ...
+
+box.space.T:insert{date('1970-01-01')}\
+box.space.T:insert{date('1970-01-02')}\
+box.space.T:insert{date('1970-01-03')}\
+box.space.T:insert{date('2000-01-01')}
+ | ---
+ | ...
+
+o = box.space.T:select{}
+ | ---
+ | ...
+assert(tostring(o[1][1]) == '1970-01-01T00:00Z')
+ | ---
+ | - true
+ | ...
+assert(tostring(o[2][1]) == '1970-01-02T00:00Z')
+ | ---
+ | - true
+ | ...
+assert(tostring(o[3][1]) == '1970-01-03T00:00Z')
+ | ---
+ | - true
+ | ...
+assert(tostring(o[4][1]) == '2000-01-01T00:00Z')
+ | ---
+ | - true
+ | ...
+
+for i = 1,16 do\
+    box.space.T:insert{date.now()}\
+end
+ | ---
+ | ...
+
+a = box.space.T:select{}
+ | ---
+ | ...
+err = {}
+ | ---
+ | ...
+for i = 1, #a - 1 do\
+    if a[i][1] >= a[i+1][1] then\
+        table.insert(err, {a[i][1], a[i+1][1]})\
+        break\
+    end\
+end
+ | ---
+ | ...
+
+err
+ | ---
+ | - []
+ | ...
+box.space.T:drop()
+ | ---
+ | ...
diff --git a/test/engine/datetime.test.lua b/test/engine/datetime.test.lua
new file mode 100644
index 000000000..3685e4d4b
--- /dev/null
+++ b/test/engine/datetime.test.lua
@@ -0,0 +1,35 @@
+env = require('test_run')
+test_run = env.new()
+engine = test_run:get_cfg('engine')
+
+date = require('datetime')
+
+_ = box.schema.space.create('T', {engine = engine})
+_ = box.space.T:create_index('pk', {parts={1,'datetime'}})
+
+box.space.T:insert{date('1970-01-01')}\
+box.space.T:insert{date('1970-01-02')}\
+box.space.T:insert{date('1970-01-03')}\
+box.space.T:insert{date('2000-01-01')}
+
+o = box.space.T:select{}
+assert(tostring(o[1][1]) == '1970-01-01T00:00Z')
+assert(tostring(o[2][1]) == '1970-01-02T00:00Z')
+assert(tostring(o[3][1]) == '1970-01-03T00:00Z')
+assert(tostring(o[4][1]) == '2000-01-01T00:00Z')
+
+for i = 1,16 do\
+    box.space.T:insert{date.now()}\
+end
+
+a = box.space.T:select{}
+err = {}
+for i = 1, #a - 1 do\
+    if a[i][1] >= a[i+1][1] then\
+        table.insert(err, {a[i][1], a[i+1][1]})\
+        break\
+    end\
+end
+
+err
+box.space.T:drop()
-- 
2.29.2


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

* [Tarantool-patches] [PATCH v4 6/7] lua, datetime: time intervals support
  2021-08-12 22:33 [Tarantool-patches] [PATCH v4 0/7] Initial datetime support Timur Safin via Tarantool-patches
                   ` (4 preceding siblings ...)
  2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 5/7] box, datetime: datetime comparison for indices Timur Safin via Tarantool-patches
@ 2021-08-12 22:33 ` Timur Safin via Tarantool-patches
  2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 7/7] datetime: changelog for datetime module Timur Safin via Tarantool-patches
  6 siblings, 0 replies; 9+ messages in thread
From: Timur Safin via Tarantool-patches @ 2021-08-12 22:33 UTC (permalink / raw)
  To: imun, v.shpilevoy; +Cc: tarantool-patches

* created few entry points (months(N), years(N), days(N), etc.)
  for easier datetime arithmetic;
* additions/subtractions of years/months use `dt_add_years()`
  and `dt_add_months()` from 3rd party c-dt library;
* also there are `:add{}` and `:sub{}` methods in datetime
  object to add or substract more complex intervals;
* introduced `is_datetime()` and `is_interval()` helpers for checking
  of validity of passed arguments;
* human-readable stringization implemented for interval objects.

Note, that additions/subtractions completed for all _reasonable_
combinations of values of date and interval types;

	Time + Interval	=> Time
	Interval + Time => Time
	Time - Time 	=> Interval
	Time - Interval => Time
	Interval + Interval => Interval
	Interval - Interval => Interval

Part of #5941
---
 extra/exports                  |   5 +
 src/lua/datetime.lua           | 349 ++++++++++++++++++++++++++++++++-
 test/app-tap/datetime.test.lua | 159 ++++++++++++++-
 3 files changed, 508 insertions(+), 5 deletions(-)

diff --git a/extra/exports b/extra/exports
index 345ffbf25..b92623dc4 100644
--- a/extra/exports
+++ b/extra/exports
@@ -151,11 +151,16 @@ csv_setopt
 datetime_asctime
 datetime_ctime
 datetime_now
+datetime_pack
 datetime_strftime
 datetime_to_string
+datetime_unpack
 decimal_unpack
 decimal_from_string
 decimal_unpack
+tnt_dt_add_years
+tnt_dt_add_quarters
+tnt_dt_add_months
 tnt_dt_dow
 tnt_dt_from_rdn
 tnt_dt_from_struct_tm
diff --git a/src/lua/datetime.lua b/src/lua/datetime.lua
index 00d50a7f1..8206f175a 100644
--- a/src/lua/datetime.lua
+++ b/src/lua/datetime.lua
@@ -24,6 +24,17 @@ ffi.cdef [[
 
     int      tnt_dt_rdn          (dt_t dt);
 
+    // dt_arithmetic.h
+    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
     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);
@@ -50,8 +61,10 @@ ffi.cdef [[
 
 local builtin = ffi.C
 local math_modf = math.modf
+local math_floor = math.floor
 
 local SECS_PER_DAY     = 86400
+local NANOS_PER_SEC    = 1000000000LL
 
 -- c-dt/dt_config.h
 
@@ -62,8 +75,23 @@ local DT_EPOCH_1970_OFFSET = 719163LL
 local datetime_t = ffi.typeof('struct datetime')
 local interval_t = ffi.typeof('struct datetime_interval')
 
+ffi.cdef [[
+    struct interval_months {
+        int m;
+    };
+
+    struct interval_years {
+        int y;
+    };
+]]
+local interval_months_t = ffi.typeof('struct interval_months')
+local interval_years_t = ffi.typeof('struct interval_years')
+
 local function is_interval(o)
-    return type(o) == 'cdata' and ffi.istype(interval_t, o)
+    return type(o) == 'cdata' and
+           (ffi.istype(interval_t, o) or
+            ffi.istype(interval_months_t, o) or
+            ffi.istype(interval_years_t, o))
 end
 
 local function is_datetime(o)
@@ -72,7 +100,10 @@ end
 
 local function is_date_interval(o)
     return type(o) == 'cdata' and
-           (ffi.istype(interval_t, o) or ffi.istype(datetime_t, o))
+           (ffi.istype(datetime_t, o) or
+            ffi.istype(interval_t, o) or
+            ffi.istype(interval_months_t, o) or
+            ffi.istype(interval_years_t, o))
 end
 
 local function interval_new()
@@ -80,6 +111,13 @@ local function interval_new()
     return interval
 end
 
+local function check_number(n, message)
+    if type(n) ~= 'number' then
+        return error(("%s: expected number, but received %s"):
+                     format(message, n), 2)
+    end
+end
+
 local function check_date(o, message)
     if not is_datetime(o) then
         return error(("%s: expected datetime, but received %s"):
@@ -87,6 +125,20 @@ local function check_date(o, message)
     end
 end
 
+local function check_date_interval(o, message)
+    if not is_datetime(o) and not is_interval(o) then
+        return error(("%s: expected datetime or interval, but received %s"):
+                     format(message, o), 2)
+    end
+end
+
+local function check_interval(o, message)
+    if not is_interval(o) then
+        return error(("%s: expected interval, 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"):
@@ -102,6 +154,77 @@ local function check_range(v, range, txt)
     end
 end
 
+local function interval_years_new(y)
+    check_number(y, "years(number)")
+    local o = ffi.new(interval_years_t)
+    o.y = y
+    return o
+end
+
+local function interval_months_new(m)
+    check_number(m, "months(number)")
+    local o = ffi.new(interval_months_t)
+    o.m = m
+    return o
+end
+
+local function interval_weeks_new(w)
+    check_number(w, "weeks(number)")
+    local o = ffi.new(interval_t)
+    o.secs = w * SECS_PER_DAY * 7
+    return o
+end
+
+local function interval_days_new(d)
+    check_number(d, "days(number)")
+    local o = ffi.new(interval_t)
+    o.secs = d * SECS_PER_DAY
+    return o
+end
+
+local function interval_hours_new(h)
+    check_number(h, "hours(number)")
+    local o = ffi.new(interval_t)
+    o.secs = h * 60 * 60
+    return o
+end
+
+local function interval_minutes_new(m)
+    check_number(m, "minutes(number)")
+    local o = ffi.new(interval_t)
+    o.secs = m * 60
+    return o
+end
+
+local function interval_seconds_new(s)
+    check_number(s, "seconds(number)")
+    local o = ffi.new(interval_t)
+    o.nsec = s % 1 * 1e9
+    o.secs = s - (s % 1)
+    return o
+end
+
+local SECS_EPOCH_OFFSET = (DT_EPOCH_1970_OFFSET * SECS_PER_DAY)
+
+local function local_rd(secs)
+    return math_floor(tonumber((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_date_interval(lhs) or
        not is_date_interval(rhs) then
@@ -256,6 +379,10 @@ local function datetime_tostring(o)
         local len = builtin.datetime_to_string(o, buff, sz)
         assert(len < sz)
         return ffi.string(buff)
+    elseif ffi.typeof(o) == interval_years_t then
+        return ('%+d years'):format(o.y)
+    elseif ffi.typeof(o) == interval_months_t then
+        return ('%+d months'):format(o.m)
     elseif ffi.typeof(o) == interval_t then
         local ts = o.timestamp
         local sign = '+'
@@ -279,6 +406,126 @@ local function datetime_tostring(o)
     end
 end
 
+local function date_first(lhs, rhs)
+    if is_datetime(lhs) then
+        return lhs, rhs
+    else
+        return rhs, lhs
+    end
+end
+
+local function error_incompatible(name)
+    error(("datetime:%s() - incompatible type of arguments"):
+          format(name), 3)
+end
+
+--[[
+Matrix of subtraction operands eligibility and their result type
+
+|                 |  datetime | interval | interval_months | interval_years |
++-----------------+-----------+----------+-----------------+----------------+
+| datetime        |  interval | datetime | datetime        | datetime       |
+| interval        |           | interval |                 |                |
+| interval_months |           |          | interval_months |                |
+| interval_years  |           |          |                 | interval_years |
+]]
+local function datetime_sub(lhs, rhs)
+    check_date_interval(lhs, "operator -")
+    local d, s = lhs, rhs
+    local left_t = ffi.typeof(d)
+    local right_t = ffi.typeof(s)
+    local o
+
+    if left_t == datetime_t then
+        -- 1. left is date, right is date or generic interval
+        if right_t == datetime_t or right_t == interval_t then
+            o = right_t == datetime_t and interval_new() or datetime_new()
+            o.secs, o.nsec = normalize_nsec(lhs.secs - rhs.secs,
+                                            lhs.nsec - rhs.nsec)
+            return o
+        -- 2. left is date, right is interval in months
+        elseif right_t == interval_months_t then
+            local dt = local_dt(lhs.secs)
+            dt = builtin.tnt_dt_add_months(dt, -rhs.m, builtin.DT_LIMIT)
+            return datetime_new_dt(dt, lhs.secs % SECS_PER_DAY,
+                                   lhs.nsec, lhs.offset)
+
+        -- 3. left is date, right is interval in years
+        elseif right_t == interval_years_t then
+            local dt = local_dt(lhs.secs)
+            dt = builtin.tnt_dt_add_years(dt, -rhs.y, builtin.DT_LIMIT)
+            return datetime_new_dt(dt, lhs.secs % SECS_PER_DAY,
+                                   lhs.nsec, lhs.offset)
+        else
+            error_incompatible("operator -")
+        end
+    -- 4. both left and right are generic intervals
+    elseif left_t == interval_t and right_t == interval_t then
+        o = interval_new()
+        o.secs, o.nsec = normalize_nsec(lhs.secs - rhs.secs,
+                                        lhs.nsec - rhs.nsec)
+        return o
+    -- 5. both left and right are intervals in months
+    elseif left_t == interval_months_t and right_t == interval_months_t then
+        return interval_months_new(lhs.m - rhs.m)
+    -- 5. both left and right are intervals in years
+    elseif left_t == interval_years_t and right_t == interval_years_t then
+        return interval_years_new(lhs.y - rhs.y)
+    else
+        error_incompatible("operator -")
+    end
+end
+
+--[[
+Matrix of addition operands eligibility and their result type
+
+|                 |  datetime | interval | interval_months | interval_years |
++-----------------+-----------+----------+-----------------+----------------+
+| datetime        |  datetime | datetime | datetime        | datetime       |
+| interval        |  datetime | interval |                 |                |
+| interval_months |  datetime |          | interval_months |                |
+| interval_years  |  datetime |          |                 | interval_years |
+]]
+local function datetime_add(lhs, rhs)
+    local d, s = date_first(lhs, rhs)
+
+    check_date_interval(d, "operator +")
+    check_interval(s, "operator +")
+    local left_t = ffi.typeof(d)
+    local right_t = ffi.typeof(s)
+    local o
+
+    -- 1. left is date, right is date or interval
+    if left_t == datetime_t and right_t == interval_t then
+        o = datetime_new()
+        o.secs, o.nsec = normalize_nsec(d.secs + s.secs, d.nsec + s.nsec)
+        return o
+    -- 2. left is date, right is interval in months
+    elseif left_t == datetime_t and right_t == interval_months_t then
+        local dt = builtin.tnt_dt_add_months(local_dt(d.secs), s.m, builtin.DT_LIMIT)
+        local secs = d.secs % SECS_PER_DAY
+        return datetime_new_dt(dt, secs, d.nsec, d.offset or 0)
+
+    -- 3. left is date, right is interval in years
+    elseif left_t == datetime_t and right_t == interval_years_t then
+        local dt = builtin.tnt_dt_add_years(local_dt(d.secs), s.y, builtin.DT_LIMIT)
+        local secs = d.secs % SECS_PER_DAY
+        return datetime_new_dt(dt, secs, d.nsec, d.offset or 0)
+    -- 4. both left and right are generic intervals
+    elseif left_t == interval_t and right_t == interval_t then
+        o = interval_new()
+        o.secs, o.nsec = normalize_nsec(d.secs + s.secs, d.nsec + s.nsec)
+        return o
+    -- 5. both left and right are intervals in months
+    elseif left_t == interval_months_t and right_t == interval_months_t then
+        return interval_months_new(d.m + s.m)
+    -- 6. both left and right are intervals in years
+    elseif left_t == interval_years_t and right_t == interval_years_t then
+        return interval_years_new(d.y + s.y)
+    else
+        error_incompatible("operator +")
+    end
+end
 
 --[[
     Basic      Extended
@@ -400,6 +647,75 @@ local function local_now()
     return d
 end
 
+-- addition or subtraction from date/time of a given interval
+-- described via table direction should be +1 or -1
+local function interval_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 ym_updated = false
+    local dhms_updated = false
+
+    local secs, nsec
+    secs, nsec = self.secs, self.nsec
+    -- operations with intervals should be done using human dates
+    -- not UTC dates, thus we normalize to UTC
+    local dt = local_dt(secs)
+
+    for key, value in pairs(o) do
+        if key == 'years' then
+            check_range(value, {0, 9999}, key)
+            dt = builtin.tnt_dt_add_years(dt, direction * value, builtin.DT_LIMIT)
+            ym_updated = true
+        elseif key == 'months' then
+            check_range(value, {0, 12}, key)
+            dt = builtin.tnt_dt_add_months(dt, direction * value, builtin.DT_LIMIT)
+            ym_updated = true
+        elseif key == 'weeks' then
+            check_range(value, {0, 52}, key)
+            secs = secs + direction * 7 * value * SECS_PER_DAY
+            dhms_updated = true
+        elseif key == 'days' then
+            check_range(value, {0, 31}, key)
+            secs = secs + direction * value * SECS_PER_DAY
+            dhms_updated = true
+        elseif key == 'hours' then
+            check_range(value, {0, 23}, key)
+            secs = secs + direction * 60 * 60 * value
+            dhms_updated = true
+        elseif key == 'minutes' then
+            check_range(value, {0, 59}, key)
+            secs = secs + direction * 60 * value
+        elseif key == 'seconds' then
+            check_range(value, {0, 60}, key)
+            local s, frac = math.modf(value)
+            secs = secs + direction * s
+            nsec = nsec + direction * frac * 1e9
+            dhms_updated = true
+        end
+    end
+
+    secs, nsec = normalize_nsec(secs, nsec)
+
+    -- .days, .hours, .minutes, .seconds
+    if dhms_updated then
+        self.secs = secs
+        self.nsec = nsec
+    end
+
+    -- .years, .months updated
+    if ym_updated then
+        self.secs = (builtin.tnt_dt_rdn(dt) - DT_EPOCH_1970_OFFSET) * SECS_PER_DAY +
+                    secs % SECS_PER_DAY
+    end
+
+    return self
+end
+
 -- Change the time-zone to the provided target_offset
 -- Time `.secs`/`.nsec` are always UTC normalized, we need only to
 -- reattribute object with different `.offset`
@@ -437,6 +753,14 @@ local function datetime_index(self, key)
         return (tonumber(self.secs) + self.nsec / 1e9) / (60 * 60)
     elseif key == 'd' or key == 'days' then
         return (tonumber(self.secs) + self.nsec / 1e9) / (24 * 60 * 60)
+    elseif key == 'add' then
+        return function(self, obj)
+            return interval_increment(self, obj, 1)
+        end
+    elseif key == 'sub' then
+        return function(self, obj)
+            return interval_increment(self, obj, -1)
+        end
     elseif key == 'to_utc' then
         return function(self)
             return datetime_to_tz(self, 0)
@@ -493,6 +817,8 @@ local datetime_mt = {
     __eq = datetime_eq,
     __lt = datetime_lt,
     __le = datetime_le,
+    __sub = datetime_sub,
+    __add = datetime_add,
     __index = datetime_index,
     __newindex = datetime_newindex,
 }
@@ -503,16 +829,35 @@ local interval_mt = {
     __eq = datetime_eq,
     __lt = datetime_lt,
     __le = datetime_le,
+    __sub = datetime_sub,
+    __add = datetime_add,
+    __index = datetime_index,
+}
+
+local interval_tiny_mt = {
+    __tostring = datetime_tostring,
+    __serialize = interval_serialize,
+    __sub = datetime_sub,
+    __add = datetime_add,
     __index = datetime_index,
 }
 
 ffi.metatype(interval_t, interval_mt)
 ffi.metatype(datetime_t, datetime_mt)
+ffi.metatype(interval_years_t, interval_tiny_mt)
+ffi.metatype(interval_months_t, interval_tiny_mt)
 
 return setmetatable(
     {
         new         = datetime_new,
         new_raw     = datetime_new_obj,
+        years       = interval_years_new,
+        months      = interval_months_new,
+        weeks       = interval_weeks_new,
+        days        = interval_days_new,
+        hours       = interval_hours_new,
+        minutes     = interval_minutes_new,
+        seconds     = interval_seconds_new,
         interval    = interval_new,
 
         parse       = parse,
diff --git a/test/app-tap/datetime.test.lua b/test/app-tap/datetime.test.lua
index 244ec2575..a2d7fac57 100755
--- a/test/app-tap/datetime.test.lua
+++ b/test/app-tap/datetime.test.lua
@@ -6,7 +6,7 @@ local date = require('datetime')
 local ffi = require('ffi')
 
 
-test:plan(7)
+test:plan(10)
 
 test:test("Simple tests for parser", function(test)
     test:plan(2)
@@ -207,11 +207,164 @@ test:test("Parse tiny date into seconds and other parts", function(test)
     test:ok(tiny.hours == 0.00848, "hours")
 end)
 
-test:test("Stringization of date", function(test)
-    test:plan(1)
+test:test("Stringization of dates and intervals", function(test)
+    test:plan(13)
     local str = '19700101Z'
     local dt = date(str)
     test:ok(tostring(dt) == '1970-01-01T00:00Z', ('tostring(%s)'):format(str))
+    test:ok(tostring(date.seconds(12)) == '+12 secs', '+12 seconds')
+    test:ok(tostring(date.seconds(-12)) == '-12 secs', '-12 seconds')
+    test:ok(tostring(date.minutes(12)) == '+12 minutes, 0 seconds', '+12 minutes')
+    test:ok(tostring(date.minutes(-12)) == '-12 minutes, 0 seconds', '-12 minutes')
+    test:ok(tostring(date.hours(12)) == '+12 hours, 0 minutes, 0 seconds',
+            '+12 hours')
+    test:ok(tostring(date.hours(-12)) == '-12 hours, 0 minutes, 0 seconds',
+            '-12 hours')
+    test:ok(tostring(date.days(12)) == '+12 days, 0 hours, 0 minutes, 0 seconds',
+            '+12 days')
+    test:ok(tostring(date.days(-12)) == '-12 days, 0 hours, 0 minutes, 0 seconds',
+            '-12 days')
+    test:ok(tostring(date.months(5)) == '+5 months', '+5 months')
+    test:ok(tostring(date.months(-5)) == '-5 months', '-5 months')
+    test:ok(tostring(date.years(4)) == '+4 years', '+4 years')
+    test:ok(tostring(date.years(-4)) == '-4 years', '-4 years')
+end)
+
+test:test("Time interval operations", function(test)
+    test:plan(12)
+
+    -- check arithmetic with leap dates
+    local T = date('1972-02-29')
+    local M = date.months(2)
+    local Y = date.years(1)
+    test:ok(tostring(T + M) == '1972-04-29T00:00Z', ('T(%s) + M(%s'):format(T, M))
+    test:ok(tostring(T + Y) == '1973-03-01T00:00Z', ('T(%s) + Y(%s'):format(T, Y))
+    test:ok(tostring(T + M + Y) == '1973-04-30T00:00Z',
+            ('T(%s) + M(%s) + Y(%s'):format(T, M, Y))
+    test:ok(tostring(T + Y + M) == '1973-05-01T00:00Z',
+            ('T(%s) + M(%s) + Y(%s'):format(T, M, Y))
+    test:ok(tostring(T:add{years = 1, months = 2}) == '1973-04-30T00:00Z',
+            ('T:add{years=1,months=2}(%s)'):format(T))
+
+    -- check average, not leap dates
+    T = date('1970-01-08')
+    test:ok(tostring(T + M) == '1970-03-08T00:00Z', ('T(%s) + M(%s'):format(T, M))
+    test:ok(tostring(T + Y) == '1971-01-08T00:00Z', ('T(%s) + Y(%s'):format(T, Y))
+    test:ok(tostring(T + M + Y) == '1971-03-08T00:00Z',
+            ('T(%s) + M(%s) + Y(%s'):format(T, M, Y))
+    test:ok(tostring(T + Y + M) == '1971-03-08T00:00Z',
+            ('T(%s) + Y(%s) + M(%s'):format(T, Y, M))
+    test:ok(tostring(T:add{years = 1, months = 2}) == '1971-03-08T00:00Z',
+            ('T:add{years=1,months=2}(%s)'):format(T))
+
+
+    -- subtraction of 2 dates
+    local T2 = date('19700103')
+    local T1 = date('1970-01-01')
+    test:ok(tostring(T2 - T1) == '+2 days, 0 hours, 0 minutes, 0 seconds',
+            ('T2(%s) - T1(%s'):format(T2, T1))
+    test:ok(tostring(T1 - T2) == '-2 days, 0 hours, 0 minutes, 0 seconds',
+            ('T2(%s) - T1(%s'):format(T2, T1))
+end)
+
+local function catchadd(A, B)
+    return pcall(function() return A + B end)
+end
+
+--[[
+Matrix of addition operands eligibility and their result type
+
+|                 |  datetime | interval | interval_months | interval_years |
++-----------------+-----------+----------+-----------------+----------------+
+| datetime        |  datetime | datetime | datetime        | datetime       |
+| interval        |  datetime | interval |                 |                |
+| interval_months |  datetime |          | interval_months |                |
+| interval_years  |  datetime |          |                 | interval_years |
+]]
+
+test:test("Matrix of allowed time and interval additions", function(test)
+    test:plan(20)
+
+    -- check arithmetic with leap dates
+    local T1970 = date('1970-01-01')
+    local T2000 = date('2000-01-01')
+    local I1 = date.days(1)
+    local M2 = date.months(2)
+    local M10 = date.months(10)
+    local Y1 = date.years(1)
+    local Y5 = date.years(5)
+
+    test:ok(catchadd(T1970, I1) == true, "status: T + I")
+    test:ok(catchadd(T1970, M2) == true, "status: T + M")
+    test:ok(catchadd(T1970, Y1) == true, "status: T + Y")
+    test:ok(catchadd(T1970, T2000) == false, "status: T + T")
+    test:ok(catchadd(I1, T1970) == true, "status: I + T")
+    test:ok(catchadd(M2, T1970) == true, "status: M + T")
+    test:ok(catchadd(Y1, T1970) == true, "status: Y + T")
+    test:ok(catchadd(I1, Y1) == false, "status: I + Y")
+    test:ok(catchadd(M2, Y1) == false, "status: M + Y")
+    test:ok(catchadd(I1, Y1) == false, "status: I + Y")
+    test:ok(catchadd(Y5, M10) == false, "status: Y + M")
+    test:ok(catchadd(Y5, I1) == false, "status: Y + I")
+    test:ok(catchadd(Y5, Y1) == true, "status: Y + Y")
+
+    test:ok(tostring(T1970 + I1) == "1970-01-02T00:00Z", "value: T + I")
+    test:ok(tostring(T1970 + M2) == "1970-03-01T00:00Z", "value: T + M")
+    test:ok(tostring(T1970 + Y1) == "1971-01-01T00:00Z", "value: T + Y")
+    test:ok(tostring(I1 + T1970) == "1970-01-02T00:00Z", "value: I + T")
+    test:ok(tostring(M2 + T1970) == "1970-03-01T00:00Z", "value: M + T")
+    test:ok(tostring(Y1 + T1970) == "1971-01-01T00:00Z", "value: Y + T")
+    test:ok(tostring(Y5 + Y1) == "+6 years", "Y + Y")
+
+end)
+
+local function catchsub_status(A, B)
+    return pcall(function() return A - B end)
+end
+
+--[[
+Matrix of subtraction operands eligibility and their result type
+
+|                 |  datetime | interval | interval_months | interval_years |
++-----------------+-----------+----------+-----------------+----------------+
+| datetime        |  interval | datetime | datetime        | datetime       |
+| interval        |           | interval |                 |                |
+| interval_months |           |          | interval_months |                |
+| interval_years  |           |          |                 | interval_years |
+]]
+test:test("Matrix of allowed time and interval subtractions", function(test)
+    test:plan(18)
+
+    -- check arithmetic with leap dates
+    local T1970 = date('1970-01-01')
+    local T2000 = date('2000-01-01')
+    local I1 = date.days(1)
+    local M2 = date.months(2)
+    local M10 = date.months(10)
+    local Y1 = date.years(1)
+    local Y5 = date.years(5)
+
+    test:ok(catchsub_status(T1970, I1) == true, "status: T - I")
+    test:ok(catchsub_status(T1970, M2) == true, "status: T - M")
+    test:ok(catchsub_status(T1970, Y1) == true, "status: T - Y")
+    test:ok(catchsub_status(T1970, T2000) == true, "status: T - T")
+    test:ok(catchsub_status(I1, T1970) == false, "status: I + T")
+    test:ok(catchsub_status(M2, T1970) == false, "status: M + T")
+    test:ok(catchsub_status(Y1, T1970) == false, "status: Y + T")
+    test:ok(catchsub_status(I1, Y1) == false, "status: I - Y")
+    test:ok(catchsub_status(M2, Y1) == false, "status: M - Y")
+    test:ok(catchsub_status(I1, Y1) == false, "status: I - Y")
+    test:ok(catchsub_status(Y5, M10) == false, "status: Y - M")
+    test:ok(catchsub_status(Y5, I1) == false, "status: Y - I")
+    test:ok(catchsub_status(Y5, Y1) == true, "status: Y - Y")
+
+    test:ok(tostring(T1970 - I1) == "1969-12-31T00:00Z", "value: T - I")
+    test:ok(tostring(T1970 - M2) == "1969-11-01T00:00Z", "value: T - M")
+    test:ok(tostring(T1970 - Y1) == "1969-01-01T00:00Z", "value: T - Y")
+    test:ok(tostring(T1970 - T2000) == "-10957 days, 0 hours, 0 minutes, 0 seconds",
+            "value: T - T")
+    test:ok(tostring(Y5 - Y1) == "+4 years", "value: Y - Y")
+
 end)
 
 os.exit(test:check() and 0 or 1)
-- 
2.29.2


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

* [Tarantool-patches] [PATCH v4 7/7] datetime: changelog for datetime module
  2021-08-12 22:33 [Tarantool-patches] [PATCH v4 0/7] Initial datetime support Timur Safin via Tarantool-patches
                   ` (5 preceding siblings ...)
  2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 6/7] lua, datetime: time intervals support Timur Safin via Tarantool-patches
@ 2021-08-12 22:33 ` Timur Safin via Tarantool-patches
  6 siblings, 0 replies; 9+ messages in thread
From: Timur Safin via Tarantool-patches @ 2021-08-12 22:33 UTC (permalink / raw)
  To: imun, v.shpilevoy; +Cc: tarantool-patches

Introduced new date/time/interval types support to lua and storage engines.

Closes #5941
Closes #5946
---
 changelogs/unreleased/gh-5941-datetime-type-support.md | 4 ++++
 1 file changed, 4 insertions(+)
 create mode 100644 changelogs/unreleased/gh-5941-datetime-type-support.md

diff --git a/changelogs/unreleased/gh-5941-datetime-type-support.md b/changelogs/unreleased/gh-5941-datetime-type-support.md
new file mode 100644
index 000000000..3c755008e
--- /dev/null
+++ b/changelogs/unreleased/gh-5941-datetime-type-support.md
@@ -0,0 +1,4 @@
+## feature/lua/datetime
+
+ * Introduce new builtin module for date/time/interval support - `datetime.lua`.
+   Support of new datetime type in storage engines (gh-5941, gh-5946).
-- 
2.29.2


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

* Re: [Tarantool-patches] [PATCH v4 4/7] box, datetime: messagepack support for datetime
  2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 4/7] box, datetime: messagepack support for datetime Timur Safin via Tarantool-patches
@ 2021-08-12 23:03   ` Safin Timur via Tarantool-patches
  0 siblings, 0 replies; 9+ messages in thread
From: Safin Timur via Tarantool-patches @ 2021-08-12 23:03 UTC (permalink / raw)
  To: imun, v.shpilevoy; +Cc: tarantool-patches



On 13.08.2021 1:33, Timur Safin wrote:
> Serialize datetime_t as newly introduced MP_EXT type.
> It saves 1 required integer field and upto 2 optional
> unsigned fields in very compact fashion.
> - secs is required field;
> - but nsec, offset are both optional;
> 
> * json, yaml serialization formats, lua output mode
>    supported;
> * exported symbols for datetime messagepack size calculations
>    so they are available for usage on Lua side.
> 
> Part of #5941
> Part of #5946
> ---
>   extra/exports                     |   2 +
>   src/box/field_def.c               |  35 +++---
>   src/box/field_def.h               |   1 +
>   src/box/lua/serialize_lua.c       |   7 +-
>   src/box/msgpack.c                 |   7 +-
>   src/box/tuple_compare.cc          |  20 ++++
>   src/lib/core/CMakeLists.txt       |   4 +-
>   src/lib/core/datetime.c           |   9 ++
>   src/lib/core/datetime.h           |  20 ++++
>   src/lib/core/mp_datetime.c        | 189 ++++++++++++++++++++++++++++++
>   src/lib/core/mp_datetime.h        |  89 ++++++++++++++
>   src/lib/core/mp_extension_types.h |   1 +
>   src/lib/mpstream/mpstream.c       |  11 ++
>   src/lib/mpstream/mpstream.h       |   4 +
>   src/lua/msgpack.c                 |  12 ++
>   src/lua/msgpackffi.lua            |  18 +++
>   src/lua/serializer.c              |   4 +
>   src/lua/serializer.h              |   2 +
>   src/lua/utils.c                   |   1 -

Discovered the harder way (massive number of failing CI builds) that 
I've forgotten to update result samples for test/unit/datetime.c test.

Now have updated this patch to include test/unit/datetime.result and 
have force pushed to the branch.

>   test/unit/datetime.c              | 125 +++++++++++++++++++-
>   third_party/lua-cjson/lua_cjson.c |   8 ++
>   third_party/lua-yaml/lyaml.cc     |   6 +-
>   22 files changed, 554 insertions(+), 21 deletions(-)
>   create mode 100644 src/lib/core/mp_datetime.c
>   create mode 100644 src/lib/core/mp_datetime.h
> 

==================================================================
diff --git a/test/unit/datetime.result b/test/unit/datetime.result
index 33997d9df..5f01c4344 100644
--- a/test/unit/datetime.result
+++ b/test/unit/datetime.result
@@ -1,4 +1,4 @@
-1..1
+1..4
      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
@@ -356,3 +356,116 @@
      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
+    1..15
+    ok 1 - string '1970-01-01T02:00+02:00' expected, received 
'1970-01-01T02:00+02:00'
+    ok 2 - string '1970-01-01T01:30+01:30' expected, received 
'1970-01-01T01:30+01:30'
+    ok 3 - string '1970-01-01T01:00+01:00' expected, received 
'1970-01-01T01:00+01:00'
+    ok 4 - string '1970-01-01T00:01+00:01' expected, received 
'1970-01-01T00:01+00:01'
+    ok 5 - string '1970-01-01T00:00Z' expected, received 
'1970-01-01T00:00Z'
+    ok 6 - string '1969-12-31T23:59-00:01' expected, received 
'1969-12-31T23:59-00:01'
+    ok 7 - string '1969-12-31T23:00-01:00' expected, received 
'1969-12-31T23:00-01:00'
+    ok 8 - string '1969-12-31T22:30-01:30' expected, received 
'1969-12-31T22:30-01:30'
+    ok 9 - string '1969-12-31T22:00-02:00' expected, received 
'1969-12-31T22:00-02:00'
+    ok 10 - string '1970-01-01T00:00:00.123456789Z' expected, received 
'1970-01-01T00:00:00.123456789Z'
+    ok 11 - string '1970-01-01T00:00:00.123456Z' expected, received 
'1970-01-01T00:00:00.123456Z'
+    ok 12 - string '1970-01-01T00:00:00.123Z' expected, received 
'1970-01-01T00:00:00.123Z'
+    ok 13 - string '1973-11-29T21:33:09Z' expected, received 
'1973-11-29T21:33:09Z'
+    ok 14 - string '2013-10-28T17:51:56Z' expected, received 
'2013-10-28T17:51:56Z'
+    ok 15 - string '9999-12-31T23:59:59Z' expected, received 
'9999-12-31T23:59:59Z'
+ok 2 - subtests
+    1..85
+    ok 1 - len 6, expected len 6
+    ok 2 - mp_sizeof_datetime(6) == encoded length 6
+    ok 3 - mp_decode_datetime() return code
+    ok 4 - mp_sizeof_uuid() == decoded length
+    ok 5 - datetime_compare(&date, &ret)
+    ok 6 - len 6, expected len 6
+    ok 7 - mp_sizeof_datetime(6) == encoded length 6
+    ok 8 - mp_decode_datetime() return code
+    ok 9 - mp_sizeof_uuid() == decoded length
+    ok 10 - datetime_compare(&date, &ret)
+    ok 11 - len 6, expected len 6
+    ok 12 - mp_sizeof_datetime(6) == encoded length 6
+    ok 13 - mp_decode_datetime() return code
+    ok 14 - mp_sizeof_uuid() == decoded length
+    ok 15 - datetime_compare(&date, &ret)
+    ok 16 - len 6, expected len 6
+    ok 17 - mp_sizeof_datetime(6) == encoded length 6
+    ok 18 - mp_decode_datetime() return code
+    ok 19 - mp_sizeof_uuid() == decoded length
+    ok 20 - datetime_compare(&date, &ret)
+    ok 21 - len 3, expected len 3
+    ok 22 - mp_sizeof_datetime(3) == encoded length 3
+    ok 23 - mp_decode_datetime() return code
+    ok 24 - mp_sizeof_uuid() == decoded length
+    ok 25 - datetime_compare(&date, &ret)
+    ok 26 - len 6, expected len 6
+    ok 27 - mp_sizeof_datetime(6) == encoded length 6
+    ok 28 - mp_decode_datetime() return code
+    ok 29 - mp_sizeof_uuid() == decoded length
+    ok 30 - datetime_compare(&date, &ret)
+    ok 31 - len 6, expected len 6
+    ok 32 - mp_sizeof_datetime(6) == encoded length 6
+    ok 33 - mp_decode_datetime() return code
+    ok 34 - mp_sizeof_uuid() == decoded length
+    ok 35 - datetime_compare(&date, &ret)
+    ok 36 - len 6, expected len 6
+    ok 37 - mp_sizeof_datetime(6) == encoded length 6
+    ok 38 - mp_decode_datetime() return code
+    ok 39 - mp_sizeof_uuid() == decoded length
+    ok 40 - datetime_compare(&date, &ret)
+    ok 41 - len 6, expected len 6
+    ok 42 - mp_sizeof_datetime(6) == encoded length 6
+    ok 43 - mp_decode_datetime() return code
+    ok 44 - mp_sizeof_uuid() == decoded length
+    ok 45 - datetime_compare(&date, &ret)
+    ok 46 - len 9, expected len 9
+    ok 47 - mp_sizeof_datetime(9) == encoded length 9
+    ok 48 - mp_decode_datetime() return code
+    ok 49 - mp_sizeof_uuid() == decoded length
+    ok 50 - datetime_compare(&date, &ret)
+    ok 51 - len 9, expected len 9
+    ok 52 - mp_sizeof_datetime(9) == encoded length 9
+    ok 53 - mp_decode_datetime() return code
+    ok 54 - mp_sizeof_uuid() == decoded length
+    ok 55 - datetime_compare(&date, &ret)
+    ok 56 - len 9, expected len 9
+    ok 57 - mp_sizeof_datetime(9) == encoded length 9
+    ok 58 - mp_decode_datetime() return code
+    ok 59 - mp_sizeof_uuid() == decoded length
+    ok 60 - datetime_compare(&date, &ret)
+    ok 61 - len 8, expected len 8
+    ok 62 - mp_sizeof_datetime(8) == encoded length 8
+    ok 63 - mp_decode_datetime() return code
+    ok 64 - mp_sizeof_uuid() == decoded length
+    ok 65 - datetime_compare(&date, &ret)
+    ok 66 - len 8, expected len 8
+    ok 67 - mp_sizeof_datetime(8) == encoded length 8
+    ok 68 - mp_decode_datetime() return code
+    ok 69 - mp_sizeof_uuid() == decoded length
+    ok 70 - datetime_compare(&date, &ret)
+    ok 71 - len 12, expected len 12
+    ok 72 - mp_sizeof_datetime(12) == encoded length 12
+    ok 73 - mp_decode_datetime() return code
+    ok 74 - mp_sizeof_uuid() == decoded length
+    ok 75 - datetime_compare(&date, &ret)
+    ok 76 - len 17, expected len 17
+    ok 77 - mp_sizeof_datetime(17) == encoded length 17
+    ok 78 - mp_decode_datetime() return code
+    ok 79 - mp_sizeof_uuid() == decoded length
+    ok 80 - datetime_compare(&date, &ret)
+    ok 81 - len 18, expected len 18
+    ok 82 - mp_sizeof_datetime(18) == encoded length 18
+    ok 83 - mp_decode_datetime() return code
+    ok 84 - mp_sizeof_uuid() == decoded length
+    ok 85 - datetime_compare(&date, &ret)
+ok 3 - subtests
+    1..5
+       *** mp_print_test ***
+    ok 1 - correct mp_snprint size 17 with empty buffer
+    ok 2 - correct mp_snprint size 17
+    ok 3 - correct mp_snprint result
+    ok 4 - correct mp_fprint size 17
+    ok 5 - correct mp_fprint result 17
+       *** mp_print_test: done ***
+ok 4 - subtests
==================================================================

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

end of thread, other threads:[~2021-08-12 23:03 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-08-12 22:33 [Tarantool-patches] [PATCH v4 0/7] Initial datetime support Timur Safin via Tarantool-patches
2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 1/7] build: add Christian Hansen c-dt to the build Timur Safin via Tarantool-patches
2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 2/7] lua: built-in module datetime Timur Safin via Tarantool-patches
2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 3/7] lua, datetime: display datetime Timur Safin via Tarantool-patches
2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 4/7] box, datetime: messagepack support for datetime Timur Safin via Tarantool-patches
2021-08-12 23:03   ` Safin Timur via Tarantool-patches
2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 5/7] box, datetime: datetime comparison for indices Timur Safin via Tarantool-patches
2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 6/7] lua, datetime: time intervals support Timur Safin via Tarantool-patches
2021-08-12 22:33 ` [Tarantool-patches] [PATCH v4 7/7] datetime: changelog for datetime module Timur Safin via Tarantool-patches

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox