From 43e10ed3494998b5f07af93b83307f27a9fb755a Mon Sep 17 00:00:00 2001 From: Timur Safin <tsafin@tarantool.org> Date: Wed, 30 Jun 2021 18:06:52 +0300 Subject: [PATCH] build, lua: built-in module datetime Introduce a new builtin Tarantool module `datetime.lua` for timestamp and interval types support. New third_party module - c-dt ----------------------------- * Integrated chansen/c-dt parser as 3rd party module to the Tarantool cmake build process; * We use tarantool/c-dt instead of original chansen/c-dt to have an easier cmake build integration, as we have added some changes, which provide cmake support, and allow to rename symbols if necessary (this symbol renaming is similar to that we see with xxhash or icu). New built-in module `datetime` ------------------------------ * created a new Tarantool built-in module `datetime`, which uses `struct datetime` data structure for keeping timestamp values; * Lua module uses a number of `dt_*` functions from `c-dt` library, but they were renamed to `tnt_dt_*` at the moment of exporting from executable - to avoid possible name clashes with external libraries. * At the moment we libc `strftime` for formatting of datetime values according to flags passed, i.e. `date:format('%FT%T%z')` will return something like '1970-01-01T00:00:00+0000', but `date:format('%A %d, %B %Y')` will return 'Thursday 01, January 1970' * if there is no format provided then we use default `tnt_datetime_to_string()` function, which converts datetime to their default ISO-8601 output format, i.e. `tostring(date)` will return string like "1970-01-01T00:00:00Z" * There are a number of simplified interfaces - totable() for exporting table with attributes names as provided by `os.date('*t')` - set() method provides unified interface to set values using the set of attributes as defined above in totable() Example, ``` local dt = datetime.new { nsec = 123456789, sec = 19, min = 29, hour = 18, day = 20, month = 8, year = 2021, tzoffset = 180 } local t = dt:totable() --[[ { sec = 19, min = 29, wday = 6, day = 20, nsec = 123456789, isdst = false, yday = 232, tzoffset = 180, month = 8, year = 2021, hour = 18 } --]] dt:format() -- 2021-08-21T14:53:34.032Z dt:format('%Y-%m-%dT%H:%M:%S') -- 2021-08-21T14:53:34 dt:set { usec = 123456, sec = 19, min = 29, hour = 18, day = 20, month = 8, year = 2021, tzoffset = 180, } dt:set { timestamp = 1629476485.124, tzoffset = 180, } ``` Coverage is File Hits Missed Coverage ----------------------------------------- builtin/datetime.lua 299 23 92.86% ----------------------------------------- Total 299 23 92.86% Part of #5941 @TarantoolBot document Title: Introduced a new `datetime` module for timestamp and interval support Create `datetime` module for timestamp and interval types support. It allows to create date and timestamp values using either object interface, or via parsing of string values conforming to iso-8601 standard. One may manipulate (modify, subtract or add) timestamp and interval values. Please refer to https://hackmd.io/@Mons/S1Vfc_axK#Datetime-in-Tarantool for a more detailed description of module API. --- .gitmodules | 3 + CMakeLists.txt | 8 + cmake/BuildCDT.cmake | 10 + extra/exports | 13 + src/CMakeLists.txt | 5 +- src/lib/core/CMakeLists.txt | 1 + src/lib/core/datetime.c | 121 ++++++ src/lib/core/datetime.h | 81 ++++ src/lua/datetime.lua | 676 +++++++++++++++++++++++++++++++++ src/lua/init.c | 4 +- src/lua/utils.c | 12 + src/lua/utils.h | 2 + test/app-tap/datetime.test.lua | 608 +++++++++++++++++++++++++++++ test/unit/CMakeLists.txt | 2 + test/unit/datetime.c | 261 +++++++++++++ test/unit/datetime.result | 375 ++++++++++++++++++ third_party/c-dt | 1 + 17 files changed, 2181 insertions(+), 2 deletions(-) create mode 100644 cmake/BuildCDT.cmake create mode 100644 src/lib/core/datetime.c create mode 100644 src/lib/core/datetime.h create mode 100644 src/lua/datetime.lua create mode 100755 test/app-tap/datetime.test.lua create mode 100644 test/unit/datetime.c create mode 100644 test/unit/datetime.result create mode 160000 third_party/c-dt diff --git a/.gitmodules b/.gitmodules index f2f91ee721..aa3fbae4e9 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 e25b81eac1..8037c30a7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -571,6 +571,14 @@ endif() # zstd # +# +# Christian Hansen c-dt +# + +include(BuildCDT) +libccdt_build() +add_dependencies(build_bundled_libs cdt) + # # Third-Party misc # diff --git a/cmake/BuildCDT.cmake b/cmake/BuildCDT.cmake new file mode 100644 index 0000000000..80b26c64a1 --- /dev/null +++ b/cmake/BuildCDT.cmake @@ -0,0 +1,10 @@ +macro(libccdt_build) + set(LIBCDT_INCLUDE_DIRS ${PROJECT_SOURCE_DIR}/third_party/c-dt/) + set(LIBCDT_LIBRARIES cdt) + + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/third_party/c-dt/build/) + add_subdirectory(${PROJECT_SOURCE_DIR}/third_party/c-dt + ${CMAKE_CURRENT_BINARY_DIR}/third_party/c-dt/build/) + set_target_properties(cdt PROPERTIES COMPILE_FLAGS "-DDT_NAMESPACE=tnt_") + add_definitions("-DDT_NAMESPACE=tnt_") +endmacro() diff --git a/extra/exports b/extra/exports index 7fe90ce3f7..354ae0d4f0 100644 --- a/extra/exports +++ b/extra/exports @@ -415,8 +415,21 @@ title_set_interpretor_name title_set_script_name title_set_status title_update +tnt_datetime_now +tnt_datetime_strftime +tnt_datetime_to_string tnt_default_cert_dir_paths tnt_default_cert_file_paths +tnt_dt_days_in_month +tnt_dt_dom +tnt_dt_dow +tnt_dt_doy +tnt_dt_from_rdn +tnt_dt_from_ymd +tnt_dt_month +tnt_dt_parse_iso_zone_lenient +tnt_dt_to_ymd +tnt_dt_year tnt_iconv tnt_iconv_close tnt_iconv_open diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 59b949ab9a..08629f5a11 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) @@ -194,7 +196,8 @@ target_link_libraries(server core coll http_parser bit uri uuid swim swim_udp # Rule of thumb: if exporting a symbol from a static library, list the # library here. set (reexport_libraries server core misc bitset csv swim swim_udp swim_ev - shutdown ${LUAJIT_LIBRARIES} ${MSGPUCK_LIBRARIES} ${ICU_LIBRARIES} ${CURL_LIBRARIES} ${XXHASH_LIBRARIES}) + shutdown ${LUAJIT_LIBRARIES} ${MSGPUCK_LIBRARIES} ${ICU_LIBRARIES} + ${CURL_LIBRARIES} ${XXHASH_LIBRARIES} ${LIBCDT_LIBRARIES}) set (common_libraries ${reexport_libraries} diff --git a/src/lib/core/CMakeLists.txt b/src/lib/core/CMakeLists.txt index 2cd4d0b4f3..8bc776b82f 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 0000000000..7cf3cf8da3 --- /dev/null +++ b/src/lib/core/datetime.c @@ -0,0 +1,121 @@ +/* + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright 2021, Tarantool AUTHORS, please see AUTHORS file. + */ + +#include <assert.h> +#include <limits.h> +#include <string.h> +#include <time.h> + +#include "c-dt/dt.h" +#include "trivia/util.h" +#include "datetime.h" + +/** + * Given the seconds from Epoch (1970-01-01) we calculate date + * since Rata Die (0001-01-01). + * DT_EPOCH_1970_OFFSET is the distance in days from Rata Die to Epoch. + */ +static int +local_dt(int64_t secs) +{ + return dt_from_rdn((int)(secs / SECS_PER_DAY) + DT_EPOCH_1970_OFFSET); +} + +static int64_t +local_secs(const struct datetime *date) +{ + return (int64_t)date->epoch + date->tzoffset * 60; +} + +static void +datetime_to_tm(const struct datetime *date, struct tm *tm) +{ + memset(tm, 0, sizeof(*tm)); + int64_t local_epoch = local_secs(date); + dt_to_struct_tm(local_dt(local_epoch), tm); + + tm->tm_gmtoff = date->tzoffset * 60; + + int seconds_of_day = local_epoch % SECS_PER_DAY; + tm->tm_hour = (seconds_of_day / 3600) % 24; + tm->tm_min = (seconds_of_day / 60) % 60; + tm->tm_sec = seconds_of_day % 60; +} + +size_t +tnt_datetime_strftime(const struct datetime *date, char *buf, size_t len, + const char *fmt) +{ + struct tm tm; + datetime_to_tm(date, &tm); + return strftime(buf, len, fmt, &tm); +} + +void +tnt_datetime_now(struct datetime *now) +{ + struct timeval tv; + gettimeofday(&tv, NULL); + now->epoch = tv.tv_sec; + now->nsec = tv.tv_usec * 1000; + + struct tm tm; + localtime_r(&tv.tv_sec, &tm); + now->tzoffset = tm.tm_gmtoff / 60; +} + +/** + * NB! buf may be NULL, and we should handle it gracefully, returning + * calculated length of output string + */ +size_t +tnt_datetime_to_string(const struct datetime *date, char *buf, ssize_t len) +{ + int offset = date->tzoffset; + int64_t rd_seconds = (int64_t)date->epoch + offset * 60 + + SECS_EPOCH_1970_OFFSET; + int64_t rd_number = rd_seconds / SECS_PER_DAY; + assert(rd_number <= INT_MAX); + assert(rd_number >= INT_MIN); + dt_t dt = dt_from_rdn((int)rd_number); + + int year, month, day, second, nanosec, sign; + dt_to_ymd(dt, &year, &month, &day); + + int hour = (rd_seconds / 3600) % 24; + int minute = (rd_seconds / 60) % 60; + second = rd_seconds % 60; + nanosec = date->nsec; + + size_t sz = 0; + SNPRINT(sz, snprintf, buf, len, "%04d-%02d-%02dT%02d:%02d:%02d", + year, month, day, hour, minute, second); + if (nanosec != 0) { + if (nanosec % 1000000 == 0) { + SNPRINT(sz, snprintf, buf, len, ".%03d", + nanosec / 1000000); + + } else if (nanosec % 1000 == 0) { + SNPRINT(sz, snprintf, buf, len, ".%06d", + nanosec / 1000); + } else { + SNPRINT(sz, snprintf, buf, len, ".%09d", nanosec); + } + } + if (offset == 0) { + SNPRINT(sz, snprintf, buf, len, "Z"); + } else { + if (offset < 0) { + sign = '-'; + offset = -offset; + } else { + sign = '+'; + } + SNPRINT(sz, snprintf, buf, len, "%c%02d%02d", sign, + offset / 60, offset % 60); + } + return sz; +} diff --git a/src/lib/core/datetime.h b/src/lib/core/datetime.h new file mode 100644 index 0000000000..be82ea694d --- /dev/null +++ b/src/lib/core/datetime.h @@ -0,0 +1,81 @@ +#pragma once +/* + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright 2021, Tarantool AUTHORS, please see AUTHORS file. + */ + +#include <stdint.h> +#include <sys/types.h> + +#if defined(__cplusplus) +extern "C" { +#endif /* defined(__cplusplus) */ + +/** + * We count dates since so called "Rata Die" date + * January 1, 0001, Monday (as Day 1). + * But datetime structure keeps seconds since + * Unix "Epoch" date: + * Unix, January 1, 1970, Thursday + * + * The difference between Epoch (1970-01-01) + * and Rata Die (0001-01-01) is 719163 days. + */ + +#ifndef SECS_PER_DAY +#define SECS_PER_DAY 86400 +#define DT_EPOCH_1970_OFFSET 719163 +#endif + +#define SECS_EPOCH_1970_OFFSET ((int64_t)DT_EPOCH_1970_OFFSET * SECS_PER_DAY) + +/** Required size of tnt_datetime_to_string string buffer */ +#define DT_TO_STRING_BUFSIZE 48 + +/** + * datetime structure keeps number of seconds and + * nanoseconds since Unix Epoch. + * Time is normalized by UTC, so time-zone offset + * is informative only. + */ +struct datetime { + /** Seconds since Epoch. */ + double epoch; + /** Nanoseconds, if any. */ + int32_t nsec; + /** Offset in minutes from UTC. */ + int16_t tzoffset; + /** Olson timezone id */ + int16_t tzindex; +}; + +/** + * Convert datetime to string using default format + * @param date source datetime value + * @param buf output character buffer + * @param len size of output buffer + * @retval length of a resultant text + */ +size_t +tnt_datetime_to_string(const struct datetime *date, char *buf, ssize_t len); + +/** + * Convert datetime to string using default format provided + * Wrapper around standard strftime() function + * @param date source datetime value + * @param buf output buffer + * @param len size of output buffer + * @param fmt format + * @retval length of a resultant text + */ +size_t +tnt_datetime_strftime(const struct datetime *date, char *buf, size_t len, + const char *fmt); + +void +tnt_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 0000000000..bb24e5d22c --- /dev/null +++ b/src/lua/datetime.lua @@ -0,0 +1,676 @@ +local ffi = require('ffi') +local buffer = require('buffer') + +--[[ + `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). + + `c-dt` uses 32-bit integer number to store `dt` values, so range of + suported dates is limited to dates from -5879610-06-22 (INT32_MIN) to + +5879611-07-11 (INT32_MAX). + + For better compactness of our typical data in MessagePack stream we shift + root of our time to the Unix Epoch date (1970-01-01), thus our 0 is + actually dt = 719163. + + So here is a simple formula how convert our epoch-based seconds to dt values + dt = (secs / 86400) + 719163 + Where 719163 is an offset of Unix Epoch (1970-01-01) since Rata Die + (0001-01-01) in dates. +]] + +ffi.cdef[[ + +/* dt_core.h definitions */ +typedef int dt_t; + +typedef enum { + DT_MON = 1, + DT_MONDAY = 1, + DT_TUE = 2, + DT_TUESDAY = 2, + DT_WED = 3, + DT_WEDNESDAY = 3, + DT_THU = 4, + DT_THURSDAY = 4, + DT_FRI = 5, + DT_FRIDAY = 5, + DT_SAT = 6, + DT_SATURDAY = 6, + DT_SUN = 7, + DT_SUNDAY = 7, +} dt_dow_t; + +dt_t tnt_dt_from_rdn (int n); +dt_t tnt_dt_from_ymd (int y, int m, int d); +void tnt_dt_to_ymd (dt_t dt, int *y, int *m, int *d); + +dt_dow_t tnt_dt_dow (dt_t dt); + +/* dt_util.h */ +int tnt_dt_days_in_month (int y, int m); + +/* dt_accessor.h */ +int tnt_dt_year (dt_t dt); +int tnt_dt_month (dt_t dt); +int tnt_dt_doy (dt_t dt); +int tnt_dt_dom (dt_t dt); + +/* dt_parse_iso.h definitions */ +size_t tnt_dt_parse_iso_zone_lenient(const char *str, size_t len, int *offset); + +/* Tarantool functions - datetime.c */ +size_t tnt_datetime_to_string(const struct datetime * date, char *buf, + ssize_t len); +size_t tnt_datetime_strftime(const struct datetime *date, char *buf, + uint32_t len, const char *fmt); +void tnt_datetime_now(struct datetime *now); + +]] + +local builtin = ffi.C +local math_modf = math.modf +local math_floor = math.floor + +-- Unix, January 1, 1970, Thursday +local DAYS_EPOCH_OFFSET = 719163 +local SECS_PER_DAY = 86400 +local SECS_EPOCH_OFFSET = DAYS_EPOCH_OFFSET * SECS_PER_DAY +local TOSTRING_BUFSIZE = 48 + +-- minimum supported date - -5879610-06-22 +local MIN_DATE_YEAR = -5879610 +local MIN_DATE_MONTH = 6 +local MIN_DATE_DAY = 22 +-- maximum supported date - 5879611-07-11 +local MAX_DATE_YEAR = 5879611 +local MAX_DATE_MONTH = 7 +local MAX_DATE_DAY = 11 + +local date_tostr_stash = + buffer.ffi_stash_new(string.format('char[%s]', TOSTRING_BUFSIZE)) +local date_tostr_stash_take = date_tostr_stash.take +local date_tostr_stash_put = date_tostr_stash.put + +local datetime_t = ffi.typeof('struct datetime') + +local function is_datetime(o) + return ffi.istype(datetime_t, o) +end + +local function check_date(o, message) + if not is_datetime(o) then + return error(("%s: expected datetime, but received %s"): + format(message, type(o)), 2) + end +end + +local function check_table(o, message) + if type(o) ~= 'table' then + return error(("%s: expected table, but received %s"): + format(message, type(o)), 2) + end +end + +local function check_str(s, message) + if type(s) ~= 'string' then + return error(("%s: expected string, but received %s"): + format(message, type(s)), 2) + end +end + +-- range may be of a form of pair {from, to} or +-- tuple {fom, to, -1 in extra} +-- -1 is a special value (so far) used for days only +local function check_range(v, from, to, txt, extra) + if type(v) ~= 'number' then + error(('numeric value expected, but received %s'): + format(type(v)), 3) + end + if extra == v or (v >= from and v <= to) then + return + end + if extra == nil then + error(('value %d of %s is out of allowed range [%d, %d]'): + format(v, txt, from, to), 3) + else + error(('value %d of %s is out of allowed range [%d, %d..%d]'): + format(v, txt, extra, from, to), 3) + end +end + +local function check_ymd_range(y, M, d) + -- Fast path. Max/min year is rather theoretical. Nobody is going to + -- actually use them. + if y > MIN_DATE_YEAR and y < MAX_DATE_YEAR then + return + end + -- Slow path. + if y < MIN_DATE_YEAR then + goto min_err + elseif y > MAX_DATE_YEAR then + goto max_err + elseif y == MIN_DATE_YEAR then + if M < MIN_DATE_MONTH then + goto min_err + elseif M == MIN_DATE_MONTH and d < MIN_DATE_DAY then + goto min_err + end + return + -- y == MAX_DATE_YEAR + elseif M > MAX_DATE_MONTH then + goto max_err + elseif M == MAX_DATE_MONTH and d > MAX_DATE_DAY then + goto max_err + else + return + end +::min_err:: + error(('date %d-%02d-%02d is less than minimum allowed %d-%02d-%02d'): + format(y, M, d, MIN_DATE_YEAR, MIN_DATE_MONTH, MIN_DATE_DAY)) +::max_err:: + error(('date %d-%02d-%02d is greater than maximum allowed %d-%02d-%02d'): + format(y, M, d, MAX_DATE_YEAR, MAX_DATE_MONTH, MAX_DATE_DAY)) +end + +local function nyi(msg) + error(("Not yet implemented : '%s'"):format(msg), 3) +end + +-- convert from epoch related time to Rata Die related +local function local_rd(secs) + return math_floor((secs + SECS_EPOCH_OFFSET) / SECS_PER_DAY) +end + +-- convert UTC seconds to local seconds, adjusting by timezone +local function local_secs(obj) + return obj.epoch + obj.tzoffset * 60 +end + +local function utc_secs(epoch, tzoffset) + return epoch - tzoffset * 60 +end + +-- get epoch seconds, shift to the local timezone +-- adjust from 1970-related to 0000-related time +-- then return dt in those coordinates (number of days +-- since Rata Die date) +local function local_dt(obj) + return builtin.tnt_dt_from_rdn(local_rd(local_secs(obj))) +end + +local function datetime_cmp(lhs, rhs) + if not is_datetime(lhs) or not is_datetime(rhs) then + return nil + end + local sdiff = lhs.epoch - rhs.epoch + return sdiff ~= 0 and sdiff or (lhs.nsec - rhs.nsec) +end + +local function datetime_eq(lhs, rhs) + local rc = datetime_cmp(lhs, rhs) + return rc == 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 + +--[[ + parse_tzoffset accepts time-zone strings in both basic + and extended iso-8601 formats. + + Basic Extended + Z N/A + +hh N/A + -hh N/A + +hhmm +hh:mm + -hhmm -hh:mm + + Returns timezone offset in minutes if string was accepted + by parser, otherwise raise an error. +]] +local function parse_tzoffset(str) + local offset = ffi.new('int[1]') + local len = builtin.tnt_dt_parse_iso_zone_lenient(str, #str, offset) + if len ~= #str then + error(('invalid time-zone format %s'):format(str), 3) + end + return offset[0] +end + +local function datetime_new_raw(epoch, nsec, tzoffset) + local dt_obj = ffi.new(datetime_t) + dt_obj.epoch = epoch + dt_obj.nsec = nsec + dt_obj.tzoffset = tzoffset + dt_obj.tzindex = 0 + return dt_obj +end + +local function datetime_new_dt(dt, secs, nanosecs, offset) + local epoch = (dt - DAYS_EPOCH_OFFSET) * SECS_PER_DAY + return datetime_new_raw(epoch + secs - offset * 60, nanosecs, offset) +end + +local function get_timezone(offset, msg) + if type(offset) == 'number' then + return offset + elseif type(offset) == 'string' then + return parse_tzoffset(offset) + else + error(('%s: string or number expected, but received %s'): + format(msg, offset), 3) + end +end + +local function bool2int(b) + return b and 1 or 0 +end + +-- create datetime given attribute values from obj +local function datetime_new(obj) + if obj == nil then + return datetime_new_raw(0, 0, 0) + end + check_table(obj, 'datetime.new()') + + local ymd = false + local hms = false + local dt = DAYS_EPOCH_OFFSET + + local y = obj.year + if y ~= nil then + check_range(y, MIN_DATE_YEAR, MAX_DATE_YEAR, 'year') + ymd = true + end + local M = obj.month + if M ~= nil then + check_range(M, 1, 12, 'month') + ymd = true + end + local d = obj.day + if d ~= nil then + check_range(d, 1, 31, 'day', -1) + ymd = true + end + local h = obj.hour + if h ~= nil then + check_range(h, 0, 23, 'hour') + hms = true + end + local m = obj.min + if m ~= nil then + check_range(m, 0, 59, 'min') + hms = true + end + local s = obj.sec + if s ~= nil then + check_range(s, 0, 60, 'sec') + hms = true + end + + local nsec, usec, msec = obj.nsec, obj.usec, obj.msec + local count_usec = bool2int(nsec ~= nil) + bool2int(usec ~= nil) + + bool2int(msec ~= nil) + if count_usec > 0 then + if count_usec > 1 then + error('only one of nsec, usec or msecs may be defined '.. + 'simultaneously', 2) + end + if usec ~= nil then + check_range(usec, 0, 1e6, 'usec') + nsec = usec * 1e3 + elseif msec ~= nil then + check_range(msec, 0, 1e3, 'msec') + nsec = msec * 1e6 + else + check_range(nsec, 0, 1e9, 'nsec') + end + else + nsec = 0 + end + local ts = obj.timestamp + if ts ~= nil then + if ymd then + error('timestamp is not allowed if year/month/day provided', 2) + end + if hms then + error('timestamp is not allowed if hour/min/sec provided', 2) + end + local fraction + s, fraction = math_modf(ts) + -- if there are separate nsec, usec, or msec provided then + -- timestamp should be integer + if count_usec == 0 then + nsec = fraction * 1e9 + elseif fraction ~= 0 then + error('only integer values allowed in timestamp '.. + 'if nsec, usec, or msecs provided', 2) + end + hms = true + end + + local offset = obj.tzoffset + if offset ~= nil then + offset = get_timezone(offset, 'tzoffset') + -- at the moment the range of known timezones is UTC-12:00..UTC+14:00 + -- https://en.wikipedia.org/wiki/List_of_UTC_time_offsets + check_range(offset, -720, 840, 'tzoffset') + end + + if obj.tz ~= nil then + nyi('tz') + end + + -- .year, .month, .day + if ymd then + y = y or 1970 + M = M or 1 + d = d or 1 + if d < 0 then + d = builtin.tnt_dt_days_in_month(y, M) + elseif d > 28 then + local day_in_month = builtin.tnt_dt_days_in_month(y, M) + if d > day_in_month then + error(('invalid number of days %d in month %d for %d'): + format(d, M, y), 3) + end + end + check_ymd_range(y, M, d) + dt = builtin.tnt_dt_from_ymd(y, M, d) + end + + -- .hour, .minute, .second + local secs = 0 + if hms then + secs = (h or 0) * 3600 + (m or 0) * 60 + (s or 0) + end + + return datetime_new_dt(dt, secs, nsec, offset or 0) +end + +--[[ + Convert to text datetime values + + - datetime will use ISO-8601 format: + 1970-01-01T00:00Z + 2021-08-18T16:57:08.981725+03:00 +]] +local function datetime_tostring(self) + local buff = date_tostr_stash_take() + local len = builtin.tnt_datetime_to_string(self, buff, TOSTRING_BUFSIZE) + assert(len < TOSTRING_BUFSIZE) + local s = ffi.string(buff) + date_tostr_stash_put(buff) + return s +end + +--[[ + Create datetime object representing current time using microseconds + platform timer and local timezone information. +]] +local function datetime_now() + local d = datetime_new_raw(0, 0, 0) + builtin.tnt_datetime_now(d) + return d +end + +--[[ + dt_dow() returns days of week in range: 1=Monday .. 7=Sunday + convert it to os.date() wday which is in range: 1=Sunday .. 7=Saturday +]] +local function dow_to_wday(dow) + return tonumber(dow) % 7 + 1 +end + +--[[ + Return table in os.date('*t') format, but with timezone + and nanoseconds +]] +local function datetime_totable(self) + check_date(self, 'datetime.totable()') + local secs = local_secs(self) -- hour:minute should be in local timezone + local dt = local_dt(self) + + return { + year = builtin.tnt_dt_year(dt), + month = builtin.tnt_dt_month(dt), + yday = builtin.tnt_dt_doy(dt), + day = builtin.tnt_dt_dom(dt), + wday = dow_to_wday(builtin.tnt_dt_dow(dt)), + hour = math_floor((secs / 3600) % 24), + min = math_floor((secs / 60) % 60), + sec = secs % 60, + isdst = false, + nsec = self.nsec, + tzoffset = self.tzoffset, + } +end + +local function datetime_update_dt(self, dt, new_offset) + local epoch = local_secs(self) + local secs_day = epoch % SECS_PER_DAY + epoch = (dt - DAYS_EPOCH_OFFSET) * SECS_PER_DAY + secs_day + self.epoch = utc_secs(epoch, new_offset) +end + +local function datetime_ymd_update(self, y, M, d, new_offset) + if d < 0 then + d = builtin.tnt_dt_days_in_month(y, M) + elseif d > 28 then + local day_in_month = builtin.tnt_dt_days_in_month(y, M) + if d > day_in_month then + error(('invalid number of days %d in month %d for %d'): + format(d, M, y), 3) + end + end + local dt = builtin.tnt_dt_from_ymd(y, M, d) + datetime_update_dt(self, dt, new_offset) +end + +local function datetime_hms_update(self, h, m, s, new_offset) + local epoch = local_secs(self) + local secs_day = epoch - (epoch % SECS_PER_DAY) + self.epoch = utc_secs(secs_day + h * 3600 + m * 60 + s, new_offset) +end + +local function datetime_set(self, obj) + check_date(self, 'datetime.set()') + check_table(obj, "datetime.set()") + + local ymd = false + local hms = false + + local dt = local_dt(self) + local y0 = ffi.new('int[1]') + local M0 = ffi.new('int[1]') + local d0 = ffi.new('int[1]') + builtin.tnt_dt_to_ymd(dt, y0, M0, d0) + y0, M0, d0 = y0[0], M0[0], d0[0] + + local y = obj.year + if y ~= nil then + check_range(y, MIN_DATE_YEAR, MAX_DATE_YEAR, 'year') + ymd = true + end + local M = obj.month + if M ~= nil then + check_range(M, 1, 12, 'month') + ymd = true + end + local d = obj.day + if d ~= nil then + check_range(d, 1, 31, 'day', -1) + ymd = true + end + + local lsecs = local_secs(self) + local h0 = math_floor(lsecs / (24 * 60)) % 24 + local m0 = math_floor(lsecs / 60) % 60 + local sec0 = lsecs % 60 + + local h = obj.hour + if h ~= nil then + check_range(h, 0, 23, 'hour') + hms = true + end + local m = obj.min + if m ~= nil then + check_range(m, 0, 59, 'min') + hms = true + end + local sec = obj.sec + if sec ~= nil then + check_range(sec, 0, 60, 'sec') + hms = true + end + + local nsec, usec, msec = obj.nsec, obj.usec, obj.msec + local count_usec = bool2int(nsec ~= nil) + bool2int(usec ~= nil) + + bool2int(msec ~= nil) + if count_usec > 0 then + if count_usec > 1 then + error('only one of nsec, usec or msecs may be defined '.. + 'simultaneously', 2) + end + if usec ~= nil then + check_range(usec, 0, 1e6, 'usec') + self.nsec = usec * 1e3 + elseif msec ~= nil then + check_range(msec, 0, 1e3, 'msec') + self.nsec = msec * 1e6 + elseif nsec ~= nil then + check_range(nsec, 0, 1e9, 'nsec') + self.nsec = nsec + end + end + + local ts = obj.timestamp + if ts ~= nil then + if ymd then + error('timestamp is not allowed if year/month/day provided', 2) + end + if hms then + error('timestamp is not allowed if hour/min/sec provided', 2) + end + local sec_int, fraction + sec_int, fraction = math_modf(ts) + -- if there is one of nsec, usec, msec provided + -- then ignore fraction in timestamp + -- otherwise - use nsec, usec, or msec + if count_usec == 0 then + nsec = fraction * 1e9 + else + error('only integer values allowed in timestamp '.. + 'if nsec, usec, or msecs provided', 2) + end + + self.epoch = sec_int + self.nsec = nsec + + return self + end + + local offset = obj.tzoffset + if offset ~= nil then + offset = get_timezone(offset, 'tzoffset') + check_range(offset, -720, 840, 'tzoffset') + end + offset = offset or self.tzoffset + + if obj.tz ~= nil then + nyi('tz') + end + + -- .year, .month, .day + if ymd then + y = y or y0 + M = M or M0 + d = d or d0 + check_ymd_range(y, M, d) + datetime_ymd_update(self, y, M, d, offset) + end + + -- .hour, .minute, .second + if hms then + datetime_hms_update(self, h or h0, m or m0, sec or sec0, offset) + end + + self.tzoffset = offset + + return self +end + +local function datetime_strftime(self, fmt) + local strfmt_sz = 128 + local buff = ffi.new('char[?]', strfmt_sz) + check_str(fmt, "datetime.strftime()") + builtin.tnt_datetime_strftime(self, buff, strfmt_sz, fmt) + return ffi.string(buff) +end + +local function datetime_format(self, fmt) + check_date(self, 'datetime.format()') + if fmt ~= nil then + return datetime_strftime(self, fmt) + else + return datetime_tostring(self) + end +end + +local datetime_index_fields = { + timestamp = function(self) return self.epoch + self.nsec / 1e9 end, + + year = function(self) return builtin.tnt_dt_year(local_dt(self)) end, + yday = function(self) return builtin.tnt_dt_doy(local_dt(self)) end, + month = function(self) return builtin.tnt_dt_month(local_dt(self)) end, + day = function(self) + return builtin.tnt_dt_dom(local_dt(self)) + end, + wday = function(self) + return dow_to_wday(builtin.tnt_dt_dow(local_dt(self))) + end, + hour = function(self) return math_floor((local_secs(self) / 3600) % 24) end, + min = function(self) return math_floor((local_secs(self) / 60) % 60) end, + sec = function(self) return self.epoch % 60 end, + usec = function(self) return self.nsec / 1e3 end, + msec = function(self) return self.nsec / 1e6 end, + isdst = function(_) return false end, +} + +local datetime_index_functions = { + format = datetime_format, + totable = datetime_totable, + set = datetime_set, +} + +local function datetime_index(self, key) + local handler_field = datetime_index_fields[key] + if handler_field ~= nil then + return handler_field(self) + end + return datetime_index_functions[key] +end + +ffi.metatype(datetime_t, { + __tostring = datetime_tostring, + __eq = datetime_eq, + __lt = datetime_lt, + __le = datetime_le, + __index = datetime_index, +}) + +return setmetatable({ + new = datetime_new, + now = datetime_now, + is_datetime = is_datetime, +}, {}) diff --git a/src/lua/init.c b/src/lua/init.c index f9738025d5..127e935d70 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 c71cd48574..979716758c 100644 --- a/src/lua/utils.c +++ b/src/lua/utils.c @@ -48,6 +48,7 @@ static uint32_t CTID_STRUCT_IBUF_PTR; uint32_t CTID_CHAR_PTR; uint32_t CTID_CONST_CHAR_PTR; uint32_t CTID_UUID; +uint32_t CTID_DATETIME = 0; void * luaL_pushcdata(struct lua_State *L, uint32_t ctypeid) @@ -725,6 +726,17 @@ tarantool_lua_utils_init(struct lua_State *L) CTID_UUID = luaL_ctypeid(L, "struct tt_uuid"); assert(CTID_UUID != 0); + rc = luaL_cdef(L, "struct datetime {" + "double epoch;" + "int32_t nsec;" + "int16_t tzoffset;" + "int16_t tzindex;" + "};"); + assert(rc == 0); + (void) rc; + CTID_DATETIME = luaL_ctypeid(L, "struct datetime"); + assert(CTID_DATETIME != 0); + lua_pushcfunction(L, luaT_newthread_wrapper); luaT_newthread_ref = luaL_ref(L, LUA_REGISTRYINDEX); return 0; diff --git a/src/lua/utils.h b/src/lua/utils.h index 45070b778d..03dabaa50b 100644 --- a/src/lua/utils.h +++ b/src/lua/utils.h @@ -59,6 +59,7 @@ struct lua_State; struct ibuf; typedef struct ibuf box_ibuf_t; struct tt_uuid; +struct datetime; /** * Single global lua_State shared by core and modules. @@ -71,6 +72,7 @@ extern struct lua_State *tarantool_L; extern uint32_t CTID_CHAR_PTR; extern uint32_t CTID_CONST_CHAR_PTR; extern uint32_t CTID_UUID; +extern uint32_t CTID_DATETIME; struct tt_uuid * luaL_pushuuid(struct lua_State *L); diff --git a/test/app-tap/datetime.test.lua b/test/app-tap/datetime.test.lua new file mode 100755 index 0000000000..162628c0e4 --- /dev/null +++ b/test/app-tap/datetime.test.lua @@ -0,0 +1,608 @@ +#!/usr/bin/env tarantool + +local tap = require('tap') +local test = tap.test('errno') +local date = require('datetime') +local ffi = require('ffi') + +test:plan(12) + +-- minimum supported date - -5879610-06-22 +local MIN_DATE_YEAR = -5879610 +local MIN_DATE_MONTH = 6 +local MIN_DATE_DAY = 22 +-- maximum supported date - 5879611-07-11 +local MAX_DATE_YEAR = 5879611 +local MAX_DATE_MONTH = 7 +local MAX_DATE_DAY = 11 + +local incompat_types = 'incompatible types for comparison' +local only_integer_ts = 'only integer values allowed in timestamp'.. + ' if nsec, usec, or msecs provided' +local only_one_of = 'only one of nsec, usec or msecs may be defined'.. + ' simultaneously' +local timestamp_and_ymd = 'timestamp is not allowed if year/month/day provided' +local timestamp_and_hms = 'timestamp is not allowed if hour/min/sec provided' +local str_or_num_exp = 'tzoffset: string or number expected, but received' +local numeric_exp = 'numeric value expected, but received ' + +-- various error message generators +local function exp_datetime(name, value) + return ("%s: expected datetime, but received %s"):format(name, type(value)) +end + +local function nyi_error(msg) + return ("Not yet implemented : '%s'"):format(msg) +end + +local function table_expected(msg, value) + return ("%s: expected table, but received %s"): + format(msg, type(value)) +end + +local function expected_str(msg, value) + return ("%s: expected string, but received %s"):format(msg, type(value)) +end + +local function invalid_days_in_mon(d, M, y) + return ('invalid number of days %d in month %d for %d'):format(d, M, y) +end + +local function range_check_error(name, value, range) + return ('value %s of %s is out of allowed range [%d, %d]'): + format(value, name, range[1], range[2]) +end + +local function range_check_3_error(name, value, range) + return ('value %d of %s is out of allowed range [%d, %d..%d]'): + format(value, name, range[1], range[2], range[3]) +end + +local function less_than_min(y, M, d) + return ('date %d-%02d-%02d is less than minimum allowed %d-%02d-%02d'): + format(y, M, d, MIN_DATE_YEAR, MIN_DATE_MONTH, MIN_DATE_DAY) +end + +local function greater_than_max(y, M, d) + return ('date %d-%02d-%02d is greater than maximum allowed %d-%02d-%02d'): + format(y, M, d, MAX_DATE_YEAR, MAX_DATE_MONTH, MAX_DATE_DAY) +end + +local function invalid_tz_fmt_error(val) + return ('invalid time-zone format %s'):format(val) +end + +-- utility functions to gracefully handle pcall errors +local function assert_raises(test, error_msg, func, ...) + local ok, err = pcall(func, ...) + local err_tail = err:gsub("^.+:%d+: ", "") + return test:is(not ok and err_tail, error_msg, + ('"%s" received, "%s" expected'):format(err_tail, error_msg)) +end + +local function assert_raises_like(test, error_msg, func, ...) + local ok, err = pcall(func, ...) + local err_tail = err:gsub("^.+:%d+: ", "") + return test:like(not ok and err_tail, error_msg, + ('"%s" received, "%s" expected'):format(err_tail, error_msg)) +end + +test:test("Datetime API checks", function(test) + test:plan(12) + local ts = date.new() + local local_format = ts.format + local local_totable = ts.totable + local local_set = ts.set + + test:is(local_format(ts), "1970-01-01T00:00:00Z", "correct :format") + local table_expected = { + sec = 0, min = 0, wday = 5, day = 1, nsec = 0, + isdst = false, yday = 1, tzoffset = 0, month = 1, + year = 1970, hour = 0 + } + test:is_deeply(local_totable(ts), table_expected, "correct :totable") + local date_expected = date.new() + date_expected.epoch = 1 + test:is(local_set(ts, {sec = 1}), date_expected, "correct :set") + + -- check results of wrong arguments passed + assert_raises(test, exp_datetime("datetime.format()", 123), + function() return local_format(123) end) + assert_raises(test, exp_datetime("datetime.format()", "1970-01-01"), + function() return local_format("1970-01-01") end) + assert_raises(test, exp_datetime("datetime.format()"), + function() return local_format() end) + + assert_raises(test, exp_datetime("datetime.totable()", 123), + function() return local_totable(123) end) + assert_raises(test, exp_datetime("datetime.totable()", "1970-01-01"), + function() return local_totable("1970-01-01") end) + assert_raises(test, exp_datetime("datetime.totable()"), + function() return local_totable() end) + + assert_raises(test, exp_datetime("datetime.set()", 123), + function() return local_set(123) end) + assert_raises(test, exp_datetime("datetime.set()", "1970-01-01"), + function() return local_set("1970-01-01") end) + assert_raises(test, exp_datetime("datetime.set()"), + function() return local_set() end) +end) + +test:test("Default date creation and comparison", function(test) + test:plan(37) + -- check empty arguments + local ts1 = date.new() + test:is(ts1.epoch, 0, "ts.epoch ==0") + test:is(ts1.nsec, 0, "ts.nsec == 0") + test:is(ts1.tzoffset, 0, "ts.tzoffset == 0") + test:is(tostring(ts1), "1970-01-01T00:00:00Z", "tostring(ts1)") + -- check empty table + local ts2 = date.new{} + test:is(ts2.epoch, 0, "ts.epoch ==0") + test:is(ts2.nsec, 0, "ts.nsec == 0") + test:is(ts2.tzoffset, 0, "ts.tzoffset == 0") + test:is(tostring(ts2), "1970-01-01T00:00:00Z", "tostring(ts2)") + -- check their equivalence + test:is(ts1, ts2, "ts1 == ts2") + test:is(ts1 ~= ts2, false, "not ts1 != ts2") + + test:isnt(ts1, nil, "ts1 != {}") + test:isnt(ts1, {}, "ts1 != {}") + test:isnt(ts1, "1970-01-01T00:00:00Z", "ts1 != '1970-01-01T00:00:00Z'") + test:isnt(ts1, 19700101, "ts1 != 19700101") + + test:isnt(nil, ts1, "{} ~= ts1") + test:isnt({}, ts1 ,"{} ~= ts1") + test:isnt("1970-01-01T00:00:00Z", ts1, "'1970-01-01T00:00' ~= ts1") + test:isnt(19700101, ts1, "ts1 ~= ts1") + + test:is(ts1 < ts2, false, "not ts1 < ts2") + test:is(ts1 > ts2, false, "not ts1 < ts2") + test:is(ts1 <= ts2, true, "not ts1 < ts2") + test:is(ts1 >= ts2, true, "not ts1 < ts2") + + -- check is_datetime + test:is(date.is_datetime(ts1), true, "ts1 is datetime") + test:is(date.is_datetime(ts2), true, "ts2 is datetime") + test:is(date.is_datetime({}), false, "bogus is not datetime") + + -- check comparison errors -- ==, ~= not raise errors, other + -- comparison operators should raise + assert_raises(test, incompat_types, function() return ts1 < nil end) + assert_raises(test, incompat_types, function() return ts1 < 123 end) + assert_raises(test, incompat_types, function() return ts1 < '1970-01-01' end) + assert_raises(test, incompat_types, function() return ts1 <= nil end) + assert_raises(test, incompat_types, function() return ts1 <= 123 end) + assert_raises(test, incompat_types, function() return ts1 <= '1970-01-01' end) + assert_raises(test, incompat_types, function() return ts1 > nil end) + assert_raises(test, incompat_types, function() return ts1 > 123 end) + assert_raises(test, incompat_types, function() return ts1 > '1970-01-01' end) + assert_raises(test, incompat_types, function() return ts1 >= nil end) + assert_raises(test, incompat_types, function() return ts1 >= 123 end) + assert_raises(test, incompat_types, function() return ts1 >= '1970-01-01' end) +end) + +test:test("Simple date creation by attributes", function(test) + test:plan(12) + local ts + local obj = {} + local attribs = { + { 'year', 2000, '2000-01-01T00:00:00Z' }, + { 'month', 11, '2000-11-01T00:00:00Z' }, + { 'day', 30, '2000-11-30T00:00:00Z' }, + { 'hour', 6, '2000-11-30T06:00:00Z' }, + { 'min', 12, '2000-11-30T06:12:00Z' }, + { 'sec', 23, '2000-11-30T06:12:23Z' }, + { 'tzoffset', -8*60, '2000-11-30T06:12:23-0800' }, + { 'tzoffset', '+0800', '2000-11-30T06:12:23+0800' }, + } + for _, row in pairs(attribs) do + local key, value, str = unpack(row) + obj[key] = value + ts = date.new(obj) + test:is(tostring(ts), str, ('{%s = %s}, expected %s'): + format(key, value, str)) + end + test:is(tostring(date.new{timestamp = 1630359071.125}), + '2021-08-30T21:31:11.125Z', '{timestamp}') + test:is(tostring(date.new{timestamp = 1630359071, msec = 123}), + '2021-08-30T21:31:11.123Z', '{timestamp.msec}') + test:is(tostring(date.new{timestamp = 1630359071, usec = 123}), + '2021-08-30T21:31:11.000123Z', '{timestamp.usec}') + test:is(tostring(date.new{timestamp = 1630359071, nsec = 123}), + '2021-08-30T21:31:11.000000123Z', '{timestamp.nsec}') +end) + +test:test("Simple date creation by attributes - check failed", function(test) + test:plan(84) + + local boundary_checks = { + {'year', {MIN_DATE_YEAR, MAX_DATE_YEAR}}, + {'month', {1, 12}}, + {'day', {1, 31, -1}}, + {'hour', {0, 23}}, + {'min', {0, 59}}, + {'sec', {0, 60}}, + {'usec', {0, 1e6}}, + {'msec', {0, 1e3}}, + {'nsec', {0, 1e9}}, + {'tzoffset', {-720, 840}, str_or_num_exp}, + } + local ts = date.new() + + for _, row in pairs(boundary_checks) do + local attr_name, bounds, expected_msg = unpack(row) + local left, right, extra = unpack(bounds) + + if extra == nil then + assert_raises(test, + range_check_error(attr_name, left - 1, + {left, right}), + function() date.new{ [attr_name] = left - 1} end) + assert_raises(test, + range_check_error(attr_name, right + 1, + {left, right}), + function() date.new{ [attr_name] = right + 1} end) + assert_raises(test, + range_check_error(attr_name, left - 50, + {left, right}), + function() date.new{ [attr_name] = left - 50} end) + assert_raises(test, + range_check_error(attr_name, right + 50, + {left, right}), + function() date.new{ [attr_name] = right + 50} end) + else -- special case for {day = -1} + assert_raises(test, + range_check_3_error(attr_name, left - 1, + {extra, left, right}), + function() date.new{ [attr_name] = left - 1} end) + assert_raises(test, + range_check_3_error(attr_name, right + 1, + {extra, left, right}), + function() date.new{ [attr_name] = right + 1} end) + assert_raises(test, + range_check_3_error(attr_name, left - 50, + {extra, left, right}), + function() date.new{ [attr_name] = left - 50} end) + assert_raises(test, + range_check_3_error(attr_name, right + 50, + {extra, left, right}), + function() date.new{ [attr_name] = right + 50} end) + end + -- tzoffset uses different message to others + expected_msg = expected_msg or numeric_exp + assert_raises_like(test, expected_msg, + function() date.new{[attr_name] = {}} end) + assert_raises_like(test, expected_msg, + function() date.new{[attr_name] = ts} end) + end + + local specific_errors = { + {only_one_of, { nsec = 123456, usec = 123}}, + {only_one_of, { nsec = 123456, msec = 123}}, + {only_one_of, { usec = 123, msec = 123}}, + {only_one_of, { nsec = 123456, usec = 123, msec = 123}}, + {only_integer_ts, { timestamp = 12345.125, usec = 123}}, + {only_integer_ts, { timestamp = 12345.125, msec = 123}}, + {only_integer_ts, { timestamp = 12345.125, nsec = 123}}, + {timestamp_and_ymd, {timestamp = 1630359071.125, month = 9 }}, + {timestamp_and_ymd, {timestamp = 1630359071.125, month = 9 }}, + {timestamp_and_ymd, {timestamp = 1630359071.125, day = 29 }}, + {timestamp_and_hms, {timestamp = 1630359071.125, hour = 20 }}, + {timestamp_and_hms, {timestamp = 1630359071.125, min = 10 }}, + {timestamp_and_hms, {timestamp = 1630359071.125, sec = 29 }}, + {nyi_error('tz'), {tz = 400}}, + {table_expected('datetime.new()', '2001-01-01'), '2001-01-01'}, + {table_expected('datetime.new()', 20010101), 20010101}, + {range_check_3_error('day', 32, {-1, 1, 31}), + {year = 2021, month = 6, day = 32}}, + {invalid_days_in_mon(31, 6, 2021), { year = 2021, month = 6, day = 31}}, + {less_than_min(-5879610, 6, 21), + {year = -5879610, month = 6, day = 21}}, + {less_than_min(-5879610, 1, 1), + {year = -5879610, month = 1, day = 1}}, + {range_check_error('year', -16009610, {MIN_DATE_YEAR, MAX_DATE_YEAR}), + {year = -16009610, month = 12, day = 31}}, + {range_check_error('year', 16009610, {MIN_DATE_YEAR, MAX_DATE_YEAR}), + {year = 16009610, month = 1, day = 1}}, + {greater_than_max(5879611, 9, 1), + {year = 5879611, month = 9, day = 1}}, + {greater_than_max(5879611, 7, 12), + {year = 5879611, month = 7, day = 12}}, + } + for _, row in pairs(specific_errors) do + local err_msg, attribs = unpack(row) + print(require'json'.encode(attribs)) + assert_raises(test, err_msg, function() date.new(attribs) end) + end +end) + +test:test("Formatting limits", function(test) + test:plan(6) + local ts = date.new() + local len = ffi.C.tnt_datetime_to_string(ts, nil, 0) + test:is(len, 20, 'tostring() with NULL') + local buff = ffi.new('char[?]', len + 1) + len = ffi.C.tnt_datetime_to_string(ts, buff, len + 1) + test:is(len, 20, 'tostring() with non-NULL') + test:is(ffi.string(buff), '1970-01-01T00:00:00Z', 'Epoch string') + + local fmt = '%d/%m/%Y' + len = ffi.C.tnt_datetime_strftime(ts, nil, 0, fmt) + test:is(len, 0, 'format(fmt) with NULL') + local strfmt_sz = 128 + buff = ffi.new('char[?]', strfmt_sz) + len = ffi.C.tnt_datetime_strftime(ts, buff, strfmt_sz, fmt) + test:is(len, 10, 'format(fmt) with non-NULL') + test:is(ffi.string(buff), '01/01/1970', 'Epoch string (fmt)') +end) + +test:test("Datetime string formatting", function(test) + test:plan(8) + local t = date.new() + test:is(t.epoch, 0, ('t.epoch == %d'):format(tonumber(t.epoch))) + test:is(t.nsec, 0, ('t.nsec == %d'):format(t.nsec)) + test:is(t.tzoffset, 0, ('t.tzoffset == %d'):format(t.tzoffset)) + test:is(t:format('%d/%m/%Y'), '01/01/1970', '%s: format #1') + test:is(t:format('%A %d. %B %Y'), 'Thursday 01. January 1970', 'format #2') + test:is(t:format('%FT%T'), '1970-01-01T00:00:00', 'format #3') + test:is(t:format(), '1970-01-01T00:00:00Z', 'format #6') + assert_raises(test, expected_str('datetime.strftime()', 1234), + function() t:format(1234) end) +end) + +test:test("__index functions()", function(test) + test:plan(15) + -- 2000-01-29T03:30:12Z' + local ts = date.new{sec = 12, min = 30, hour = 3, + tzoffset = 0, day = 29, month = 1, year = 2000, + nsec = 123000000} + + test:is(ts.year, 2000, 'ts.year') + test:is(ts.yday, 29, 'ts.yday') + test:is(ts.month, 1, 'ts.month') + test:is(ts.day, 29, 'ts.day') + test:is(ts.wday, 7, 'ts.wday') + test:is(ts.min, 30, 'ts.min') + test:is(ts.hour, 3, 'ts.hour') + test:is(ts.min, 30, 'ts.min') + test:is(ts.sec, 12, 'ts.sec') + test:is(ts.isdst, false, "ts.isdst") + test:is(ts.tzoffset, 0, "ts.tzoffset") + test:is(ts.timestamp, 949116612.123, "ts.timestamp") + + test:is(ts.nsec, 123000000, 'ts.nsec') + test:is(ts.usec, 123000, 'ts.usec') + test:is(ts.msec, 123, 'ts.msec') +end) + +test:test("totable{}", function(test) + test:plan(78) + local exp = {sec = 0, min = 0, wday = 5, day = 1, + nsec = 0, isdst = false, yday = 1, + tzoffset = 0, month = 1, year = 1970, hour = 0} + local ts = date.new() + local totable = ts:totable() + test:is_deeply(totable, exp, 'date:totable()') + + local osdate = os.date('*t') + totable = date.new(osdate):totable() + local keys = { + 'sec', 'min', 'wday', 'day', 'yday', 'month', 'year', 'hour' + } + for _, key in pairs(keys) do + test:is(totable[key], osdate[key], + ('[%s]: %s == %s'):format(key, totable[key], osdate[key])) + end + for tst_d = 21,28 do + -- check wday wrapping for the whole week + osdate = os.date('*t', os.time{year = 2021, month = 9, day = tst_d}) + totable = date.new(osdate):totable() + for _, key in pairs(keys) do + test:is(totable[key], osdate[key], + ('[%s]: %s == %s'):format(key, totable[key], osdate[key])) + end + end + -- date.now() and os.date('*t') could span day boundary in between their + -- invocations. If midnight suddenly happened - simply call them both again + ts = date.now() osdate = os.date('*t') + if ts.day ~= osdate.day then + ts = date.now() osdate = os.date('*t') + end + for _, key in pairs({'wday', 'day', 'yday', 'month', 'year'}) do + test:is(ts[key], osdate[key], + ('[%s]: %s == %s'):format(key, ts[key], osdate[key])) + end +end) + +test:test("Time :set{} operations", function(test) + test:plan(12) + + local ts = date.new{ year = 2021, month = 8, day = 31, + hour = 0, min = 31, sec = 11, tzoffset = '+0300'} + test:is(tostring(ts), '2021-08-31T00:31:11+0300', 'initial') + test:is(tostring(ts:set{ year = 2020 }), '2020-08-31T00:31:11+0300', + '2020 year') + test:is(tostring(ts:set{ month = 11, day = 30 }), + '2020-11-30T00:31:11+0300', 'month = 11, day = 30') + test:is(tostring(ts:set{ day = 9 }), '2020-11-09T00:31:11+0300', + 'day 9') + test:is(tostring(ts:set{ hour = 6 }), '2020-11-09T06:31:11+0300', + 'hour 6') + test:is(tostring(ts:set{ min = 12, sec = 23 }), '2020-11-09T04:12:23+0300', + 'min 12, sec 23') + test:is(tostring(ts:set{ tzoffset = -8*60 }), '2020-11-08T17:12:23-0800', + 'offset -0800' ) + test:is(tostring(ts:set{ tzoffset = '+0800' }), '2020-11-09T09:12:23+0800', + 'offset +0800' ) + test:is(tostring(ts:set{ timestamp = 1630359071.125 }), + '2021-08-31T05:31:11.125+0800', 'timestamp 1630359071.125' ) + test:is(tostring(ts:set{ msec = 123}), '2021-08-31T05:31:11.123+0800', + 'msec = 123') + test:is(tostring(ts:set{ usec = 123}), '2021-08-31T05:31:11.000123+0800', + 'usec = 123') + test:is(tostring(ts:set{ nsec = 123}), '2021-08-31T05:31:11.000000123+0800', + 'nsec = 123') +end) + +test:test("Time invalid :set{} operations", function(test) + test:plan(84) + + local boundary_checks = { + {'year', {MIN_DATE_YEAR, MAX_DATE_YEAR}}, + {'month', {1, 12}}, + {'day', {1, 31, -1}}, + {'hour', {0, 23}}, + {'min', {0, 59}}, + {'sec', {0, 60}}, + {'usec', {0, 1e6}}, + {'msec', {0, 1e3}}, + {'nsec', {0, 1e9}}, + {'tzoffset', {-720, 840}, str_or_num_exp}, + } + local ts = date.new() + + for _, row in pairs(boundary_checks) do + local attr_name, bounds, expected_msg = unpack(row) + local left, right, extra = unpack(bounds) + + if extra == nil then + assert_raises(test, + range_check_error(attr_name, left - 1, + {left, right}), + function() ts:set{ [attr_name] = left - 1} end) + assert_raises(test, + range_check_error(attr_name, right + 1, + {left, right}), + function() ts:set{ [attr_name] = right + 1} end) + assert_raises(test, + range_check_error(attr_name, left - 50, + {left, right}), + function() ts:set{ [attr_name] = left - 50} end) + assert_raises(test, + range_check_error(attr_name, right + 50, + {left, right}), + function() ts:set{ [attr_name] = right + 50} end) + else -- special case for {day = -1} + assert_raises(test, + range_check_3_error(attr_name, left - 1, + {extra, left, right}), + function() ts:set{ [attr_name] = left - 1} end) + assert_raises(test, + range_check_3_error(attr_name, right + 1, + {extra, left, right}), + function() ts:set{ [attr_name] = right + 1} end) + assert_raises(test, + range_check_3_error(attr_name, left - 50, + {extra, left, right}), + function() ts:set{ [attr_name] = left - 50} end) + assert_raises(test, + range_check_3_error(attr_name, right + 50, + {extra, left, right}), + function() ts:set{ [attr_name] = right + 50} end) + end + -- tzoffset uses different message to others + expected_msg = expected_msg or numeric_exp + assert_raises_like(test, expected_msg, + function() ts:set{[attr_name] = {}} end) + assert_raises_like(test, expected_msg, + function() ts:set{[attr_name] = ts} end) + end + + ts:set{year = 2021} + local specific_errors = { + {only_one_of, { nsec = 123456, usec = 123}}, + {only_one_of, { nsec = 123456, msec = 123}}, + {only_one_of, { usec = 123, msec = 123}}, + {only_one_of, { nsec = 123456, usec = 123, msec = 123}}, + {only_integer_ts, { timestamp = 12345.125, usec = 123}}, + {only_integer_ts, { timestamp = 12345.125, msec = 123}}, + {only_integer_ts, { timestamp = 12345.125, nsec = 123}}, + {timestamp_and_ymd, {timestamp = 1630359071.125, month = 9 }}, + {timestamp_and_ymd, {timestamp = 1630359071.125, month = 9 }}, + {timestamp_and_ymd, {timestamp = 1630359071.125, day = 29 }}, + {timestamp_and_hms, {timestamp = 1630359071.125, hour = 20 }}, + {timestamp_and_hms, {timestamp = 1630359071.125, min = 10 }}, + {timestamp_and_hms, {timestamp = 1630359071.125, sec = 29 }}, + {nyi_error('tz'), {tz = 400}}, + {table_expected('datetime.set()', '2001-01-01'), '2001-01-01'}, + {table_expected('datetime.set()', 20010101), 20010101}, + {range_check_3_error('day', 32, {-1, 1, 31}), + {year = 2021, month = 6, day = 32}}, + {invalid_days_in_mon(31, 6, 2021), { month = 6, day = 31}}, + {less_than_min(-5879610, 6, 21), + {year = -5879610, month = 6, day = 21}}, + {less_than_min(-5879610, 1, 1), + {year = -5879610, month = 1, day = 1}}, + {range_check_error('year', -16009610, {MIN_DATE_YEAR, MAX_DATE_YEAR}), + {year = -16009610, month = 12, day = 31}}, + {range_check_error('year', 16009610, {MIN_DATE_YEAR, MAX_DATE_YEAR}), + {year = 16009610, month = 1, day = 1}}, + {greater_than_max(5879611, 9, 1), + {year = 5879611, month = 9, day = 1}}, + {greater_than_max(5879611, 7, 12), + {year = 5879611, month = 7, day = 12}}, + } + for _, row in pairs(specific_errors) do + local err_msg, attribs = unpack(row) + assert_raises(test, err_msg, function() ts:set(attribs) end) + end +end) + +test:test("Time invalid tzoffset in :set{} operations", function(test) + test:plan(14) + + local ts = date.new{} + local bad_strings = { + '+03:00 what?', + '-0000 ', + '+0000 ', + 'bogus', + '0100', + '+-0100', + '+25:00', + '+9900', + '-99:00', + } + for _, val in ipairs(bad_strings) do + assert_raises(test, invalid_tz_fmt_error(val), + function() ts:set{ tzoffset = val } end) + end + + local bad_numbers = { + 880, + -800, + 10000, + -10000, + } + for _, val in ipairs(bad_numbers) do + assert_raises(test, range_check_error('tzoffset', val, {-720, 840}), + function() ts:set{ tzoffset = val } end) + end + assert_raises(test, nyi_error('tz'), function() ts:set{tz = 400} end) +end) + +test:test("Time :set{day = -1} operations", function(test) + test:plan(8) + local tests = { + {{ year = 2000, month = 3, day = -1}, '2000-03-31T00:00:00Z'}, + {{ year = 2000, month = 2, day = -1}, '2000-02-29T00:00:00Z'}, + {{ year = 2001, month = 2, day = -1}, '2001-02-28T00:00:00Z'}, + {{ year = 1900, month = 2, day = -1}, '1900-02-28T00:00:00Z'}, + {{ year = 1904, month = 2, day = -1}, '1904-02-29T00:00:00Z'}, + } + local ts + for _, row in ipairs(tests) do + local args, str = unpack(row) + ts = date.new(args) + test:is(tostring(ts), str, ('checking -1 with %s'):format(str)) + end + + ts = date.new{ year = 1904, month = 2, day = -1 } + test:is(tostring(ts), '1904-02-29T00:00:00Z', 'base before :set{}') + test:is(tostring(ts:set{month = 3, day = 2}), '1904-03-02T00:00:00Z', + '2 March') + test:is(tostring(ts:set{day = -1}), '1904-03-31T00:00:00Z', '31 March') +end) + +os.exit(test:check() and 0 or 1) diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index e828f2b89c..3f6b0b9cb9 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -58,6 +58,8 @@ add_executable(random.test random.c core_test_utils.c) target_link_libraries(random.test core unit) add_executable(xmalloc.test xmalloc.c core_test_utils.c) target_link_libraries(xmalloc.test unit) +add_executable(datetime.test datetime.c) +target_link_libraries(datetime.test cdt core unit) add_executable(bps_tree.test bps_tree.cc) target_link_libraries(bps_tree.test small misc) diff --git a/test/unit/datetime.c b/test/unit/datetime.c new file mode 100644 index 0000000000..2d53fe3011 --- /dev/null +++ b/test/unit/datetime.c @@ -0,0 +1,261 @@ +#include "dt.h" +#include <assert.h> +#include <stdint.h> +#include <string.h> +#include <time.h> + +#include "datetime.h" +#include "trivia/util.h" +#include "unit.h" + +static const char sample[] = "2012-12-24T15:30Z"; + +#define S(s) {s, sizeof(s) - 1} +struct { + const char *str; + size_t len; +} tests[] = { + S("2012-12-24 15:30Z"), + S("2012-12-24 15:30z"), + S("2012-12-24 15:30"), + S("2012-12-24 16:30+01:00"), + S("2012-12-24 16:30+0100"), + S("2012-12-24 16:30+01"), + S("2012-12-24 14:30-01:00"), + S("2012-12-24 14:30-0100"), + S("2012-12-24 14:30-01"), + S("2012-12-24 15:30:00Z"), + S("2012-12-24 15:30:00z"), + S("2012-12-24 15:30:00"), + S("2012-12-24 16:30:00+01:00"), + S("2012-12-24 16:30:00+0100"), + S("2012-12-24 14:30:00-01:00"), + S("2012-12-24 14:30:00-0100"), + S("2012-12-24 15:30:00.123456Z"), + S("2012-12-24 15:30:00.123456z"), + S("2012-12-24 15:30:00.123456"), + S("2012-12-24 16:30:00.123456+01:00"), + S("2012-12-24 16:30:00.123456+01"), + S("2012-12-24 14:30:00.123456-01:00"), + S("2012-12-24 14:30:00.123456-01"), + S("2012-12-24t15:30Z"), + S("2012-12-24t15:30z"), + S("2012-12-24t15:30"), + S("2012-12-24t16:30+01:00"), + S("2012-12-24t16:30+0100"), + S("2012-12-24t14:30-01:00"), + S("2012-12-24t14:30-0100"), + S("2012-12-24t15:30:00Z"), + S("2012-12-24t15:30:00z"), + S("2012-12-24t15:30:00"), + S("2012-12-24t16:30:00+01:00"), + S("2012-12-24t16:30:00+0100"), + S("2012-12-24t14:30:00-01:00"), + S("2012-12-24t14:30:00-0100"), + S("2012-12-24t15:30:00.123456Z"), + S("2012-12-24t15:30:00.123456z"), + S("2012-12-24t16:30:00.123456+01:00"), + S("2012-12-24t14:30:00.123456-01:00"), + S("2012-12-24 16:30 +01:00"), + S("2012-12-24 14:30 -01:00"), + S("2012-12-24 15:30 UTC"), + S("2012-12-24 16:30 UTC+1"), + S("2012-12-24 16:30 UTC+01"), + S("2012-12-24 16:30 UTC+0100"), + S("2012-12-24 16:30 UTC+01:00"), + S("2012-12-24 14:30 UTC-1"), + S("2012-12-24 14:30 UTC-01"), + S("2012-12-24 14:30 UTC-01:00"), + S("2012-12-24 14:30 UTC-0100"), + S("2012-12-24 15:30 GMT"), + S("2012-12-24 16:30 GMT+1"), + S("2012-12-24 16:30 GMT+01"), + S("2012-12-24 16:30 GMT+0100"), + S("2012-12-24 16:30 GMT+01:00"), + S("2012-12-24 14:30 GMT-1"), + S("2012-12-24 14:30 GMT-01"), + S("2012-12-24 14:30 GMT-01:00"), + S("2012-12-24 14:30 GMT-0100"), + S("2012-12-24 14:30 -01:00"), + S("2012-12-24 16:30:00 +01:00"), + S("2012-12-24 14:30:00 -01:00"), + S("2012-12-24 16:30:00.123456 +01:00"), + S("2012-12-24 14:30:00.123456 -01:00"), + S("2012-12-24 15:30:00.123456 -00:00"), + S("20121224T1630+01:00"), + S("2012-12-24T1630+01:00"), + S("20121224T16:30+01"), + S("20121224T16:30 +01"), +}; +#undef S + +static int +parse_datetime(const char *str, size_t len, int64_t *secs_p, + int32_t *nanosecs_p, int32_t *offset_p) +{ + size_t n; + dt_t dt; + char c; + int sec_of_day = 0, nanosecond = 0, offset = 0; + + n = dt_parse_iso_date(str, len, &dt); + if (!n) + return 1; + if (n == len) + goto exit; + + c = str[n++]; + if (!(c == 'T' || c == 't' || c == ' ')) + return 1; + + str += n; + len -= n; + + n = dt_parse_iso_time(str, len, &sec_of_day, &nanosecond); + if (!n) + return 1; + if (n == len) + goto exit; + + if (str[n] == ' ') + n++; + + str += n; + len -= n; + + n = dt_parse_iso_zone_lenient(str, len, &offset); + if (!n || n != len) + return 1; + +exit: + *secs_p = ((int64_t)dt_rdn(dt) - DT_EPOCH_1970_OFFSET) * SECS_PER_DAY + + sec_of_day - offset * 60; + *nanosecs_p = nanosecond; + *offset_p = offset; + + return 0; +} + +static int +local_rd(const struct datetime *dt) +{ + return (int)((int64_t)dt->epoch / SECS_PER_DAY) + DT_EPOCH_1970_OFFSET; +} + +static int +local_dt(const struct datetime *dt) +{ + return dt_from_rdn(local_rd(dt)); +} + + +static void +datetime_to_tm(struct datetime *dt, struct tm *tm) +{ + memset(tm, 0, sizeof(*tm)); + dt_to_struct_tm(local_dt(dt), tm); + + int seconds_of_day = (int64_t)dt->epoch % 86400; + tm->tm_hour = (seconds_of_day / 3600) % 24; + tm->tm_min = (seconds_of_day / 60) % 60; + tm->tm_sec = seconds_of_day % 60; +} + +static void +datetime_test(void) +{ + size_t index; + int64_t secs_expected; + int32_t nanosecs; + int32_t offset; + + plan(355); + parse_datetime(sample, sizeof(sample) - 1, &secs_expected, &nanosecs, + &offset); + + for (index = 0; index < lengthof(tests); index++) { + int64_t secs; + int rc = parse_datetime(tests[index].str, tests[index].len, + &secs, &nanosecs, &offset); + is(rc, 0, "correct parse_datetime return value for '%s'", + tests[index].str); + is(secs, secs_expected, + "correct parse_datetime output " + "seconds for '%s", + tests[index].str); + + /* + * check that stringized literal produces the same date + * time fields + */ + static char buff[40]; + struct datetime dt = { secs, nanosecs, offset, 0 }; + /* datetime_to_tm returns time in GMT zone */ + struct tm tm = { .tm_sec = 0 }; + datetime_to_tm(&dt, &tm); + size_t len = strftime(buff, sizeof(buff), "%F %T", &tm); + ok(len > 0, "strftime"); + int64_t parsed_secs; + int32_t parsed_nsecs, parsed_ofs; + rc = parse_datetime(buff, len, &parsed_secs, &parsed_nsecs, + &parsed_ofs); + is(rc, 0, "correct parse_datetime return value for '%s'", buff); + is(secs, parsed_secs, "reversible seconds via strftime for '%s", + buff); + } + check_plan(); +} + +static void +tostring_datetime_test(void) +{ + static struct { + const char *string; + int64_t secs; + uint32_t nsec; + uint32_t offset; + } tests[] = { + {"1970-01-01T02:00:00+0200", 0, 0, 120}, + {"1970-01-01T01:30:00+0130", 0, 0, 90}, + {"1970-01-01T01:00:00+0100", 0, 0, 60}, + {"1970-01-01T00:01:00+0001", 0, 0, 1}, + {"1970-01-01T00:00:00Z", 0, 0, 0}, + {"1969-12-31T23:59:00-0001", 0, 0, -1}, + {"1969-12-31T23:00:00-0100", 0, 0, -60}, + {"1969-12-31T22:30:00-0130", 0, 0, -90}, + {"1969-12-31T22:00:00-0200", 0, 0, -120}, + {"1970-01-01T00:00:00.123456789Z", 0, 123456789, 0}, + {"1970-01-01T00:00:00.123456Z", 0, 123456000, 0}, + {"1970-01-01T00:00:00.123Z", 0, 123000000, 0}, + {"1973-11-29T21:33:09Z", 123456789, 0, 0}, + {"2013-10-28T17:51:56Z", 1382982716, 0, 0}, + {"9999-12-31T23:59:59Z", 253402300799, 0, 0}, + }; + size_t index; + + plan(15); + for (index = 0; index < lengthof(tests); index++) { + struct datetime date = { + tests[index].secs, + tests[index].nsec, + tests[index].offset, + 0 + }; + char buf[48]; + tnt_datetime_to_string(&date, buf, sizeof(buf)); + is(strcmp(buf, tests[index].string), 0, + "string '%s' expected, received '%s'", + tests[index].string, buf); + } + check_plan(); +} + +int +main(void) +{ + plan(2); + datetime_test(); + tostring_datetime_test(); + + return check_plan(); +} diff --git a/test/unit/datetime.result b/test/unit/datetime.result new file mode 100644 index 0000000000..19d07b25e1 --- /dev/null +++ b/test/unit/datetime.result @@ -0,0 +1,375 @@ +1..2 + 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 + 1..15 + ok 1 - string '1970-01-01T02:00:00+0200' expected, received '1970-01-01T02:00:00+0200' + ok 2 - string '1970-01-01T01:30:00+0130' expected, received '1970-01-01T01:30:00+0130' + ok 3 - string '1970-01-01T01:00:00+0100' expected, received '1970-01-01T01:00:00+0100' + ok 4 - string '1970-01-01T00:01:00+0001' expected, received '1970-01-01T00:01:00+0001' + ok 5 - string '1970-01-01T00:00:00Z' expected, received '1970-01-01T00:00:00Z' + ok 6 - string '1969-12-31T23:59:00-0001' expected, received '1969-12-31T23:59:00-0001' + ok 7 - string '1969-12-31T23:00:00-0100' expected, received '1969-12-31T23:00:00-0100' + ok 8 - string '1969-12-31T22:30:00-0130' expected, received '1969-12-31T22:30:00-0130' + ok 9 - string '1969-12-31T22:00:00-0200' expected, received '1969-12-31T22:00:00-0200' + 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 diff --git a/third_party/c-dt b/third_party/c-dt new file mode 160000 index 0000000000..cbb3fc27c1 --- /dev/null +++ b/third_party/c-dt @@ -0,0 +1 @@ +Subproject commit cbb3fc27c104aa7703b01a4108ce7871e1a28a1c -- GitLab