From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from [87.239.111.99] (localhost [127.0.0.1]) by dev.tarantool.org (Postfix) with ESMTP id 386FE6EC55; Fri, 10 Sep 2021 20:51:55 +0300 (MSK) DKIM-Filter: OpenDKIM Filter v2.11.0 dev.tarantool.org 386FE6EC55 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=tarantool.org; s=dev; t=1631296315; bh=0vYPfqQr9VdSAwleCDObRRYJ+MXCFJoDK/sREuC0SW8=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:Cc: From; b=gdwaNsu58bW0NiTCoZFOIvYy7swWkEzRoeqHR2+yFe7HSjkjCsoEQceNKjefmD59E U9NdzS8OiLv4JxJgMyOI9c9ZFrCf7sJkz6FLMTD7PhZQHL1aRiR/zcoqCR3VubElym qwO+AO6xwa1aCzE6GkNMPp0EpK16+aO4FA4UHg6k= Received: from smtp44.i.mail.ru (smtp44.i.mail.ru [94.100.177.104]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by dev.tarantool.org (Postfix) with ESMTPS id B340B6EC55 for ; Fri, 10 Sep 2021 20:51:23 +0300 (MSK) DKIM-Filter: OpenDKIM Filter v2.11.0 dev.tarantool.org B340B6EC55 Received: by smtp44.i.mail.ru with esmtpa (envelope-from ) id 1mOkgM-0005dm-6B; Fri, 10 Sep 2021 20:51:23 +0300 To: v.shpilevoy@tarantool.org Date: Fri, 10 Sep 2021 20:50:52 +0300 Message-Id: <5b50cb62e4b786ecb420ee2de5ee4d8f8c59b5d5.1631291287.git.tsafin@tarantool.org> X-Mailer: git-send-email 2.29.2 In-Reply-To: References: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-4EC0790: 10 X-7564579A: 646B95376F6C166E X-77F55803: 4F1203BC0FB41BD91AE02D33A9C88A2F250B8A21226859B08342E9AB5F4ECC3300894C459B0CD1B96EBDC95F8BD6F80AC07D34CA8EBCAEF982CD8ED32F61933FB6BD4AB1FA1FD8A3 X-7FA49CB5: FF5795518A3D127A4AD6D5ED66289B5278DA827A17800CE7CE4525FFB91B9BBCEA1F7E6F0F101C67BD4B6F7A4D31EC0BCC500DACC3FED6E28638F802B75D45FF8AA50765F7900637911538129B0A8D078638F802B75D45FF36EB9D2243A4F8B5A6FCA7DBDB1FC311F39EFFDF887939037866D6147AF826D8B4CFF8813C19A15F5EAAA64FD05DD4E9117882F4460429724CE54428C33FAD305F5C1EE8F4F765FCAA867293B0326636D2E47CDBA5A96583BD4B6F7A4D31EC0BC014FD901B82EE079FA2833FD35BB23D27C277FBC8AE2E8BAA867293B0326636D2E47CDBA5A96583BA9C0B312567BB2376E601842F6C81A19E625A9149C048EE7B96B19DC40933211B780A39BCC1DD35D8FC6C240DEA7642DBF02ECDB25306B2B78CF848AE20165D0A6AB1C7CE11FEE3B355ED1E20F5346A6136E347CC761E07C4224003CC836476EA7A3FFF5B025636E2021AF6380DFAD1A18204E546F3947CB11811A4A51E3B096D1867E19FE1407959CC434672EE6371089D37D7C0E48F6C8AA50765F7900637BC468E7E89D8C5D6EFF80C71ABB335746BA297DBC24807EABDAD6C7F3747799A X-B7AD71C0: AC4F5C86D027EB782CDD5689AFBDA7A213B5FB47DCBC3458834459D11680B505B82F3CAAD27F3EFA79526DF2A4893F43 X-C1DE0DAB: C20DE7B7AB408E4181F030C43753B8186998911F362727C4C7A0BC55FA0FE5FC9817C961AFFEF7F07148CCB869AB1346438338FD78E7C854B1881A6453793CE9C32612AADDFBE061C61BE10805914D3804EBA3D8E7E5B87ABF8C51168CD8EBDBC493A577044FAF45DC48ACC2A39D04F89CDFB48F4795C241BDAD6C7F3747799A X-C8649E89: 4E36BF7865823D7055A7F0CF078B5EC49A30900B95165D3494FB0335DF05DC3AB5AAF48BE4FBA558E4D53B1607A086FF4D902AA0822CA0E7A183F34870CD69211D7E09C32AA3244CE091D04C4D20E95808DA6FB8875B50C8795D98D676DD64D0FACE5A9C96DEB163 X-D57D3AED: 3ZO7eAau8CL7WIMRKs4sN3D3tLDjz0dLbV79QFUyzQ2Ujvy7cMT6pYYqY16iZVKkSc3dCLJ7zSJH7+u4VD18S7Vl4ZUrpaVfd2+vE6kuoey4m4VkSEu530nj6fImhcD4MUrOEAnl0W826KZ9Q+tr5ycPtXkTV4k65bRjmOUUP8cvGozZ33TWg5HZplvhhXbhDGzqmQDTd6OAevLeAnq3Ra9uf7zvY2zzsIhlcp/Y7m53TZgf2aB4JOg4gkr2biojFfmM42hctyTVE6IPHF3nGw== X-Mailru-Sender: 6CA451E36783D721CBEA96CEA26D325D6EBDC95F8BD6F80AC07D34CA8EBCAEF9B7CBEF92542CD7C82F97C478340294DCC77752E0C033A69E0F0C7111264B8915FF1320A92A5534336C18EFA0BB12DBB0 X-Mras: Ok Subject: [Tarantool-patches] [PATCH 1/n] build, lua: built-in module datetime X-BeenThere: tarantool-patches@dev.tarantool.org X-Mailman-Version: 2.1.34 Precedence: list List-Id: Tarantool development patches List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , From: Timur Safin via Tarantool-patches Reply-To: Timur Safin Cc: tarantool-patches@dev.tarantool.org Errors-To: tarantool-patches-bounces@dev.tarantool.org Sender: "Tarantool-patches" Introduce new builtin Tarantool module `datetime.lua` with partial proxying to `c-dt` library for datetime parsing and arithmetics. New third_party module - c-dt ----------------------------- * Integrated chansen/c-dt parser as 3rd party module to the Tarantool cmake build process; * Points to tarantool/c-dt instead of original chansen/c-dt to have easier build integration, because there are additional changes, which have provided cmake support and have established symbols renaming facilities (similar to those we see in xxhash or icu). * took special care that generated build artifacts not override in-source files, but use different build/ directory. New built-in module `datetime` ------------------------------ * created a new Tarantool built-in module `datetime`; * register new cdef types for this module; * reexported some `dt_*` functions from `c-dt` library, but using prefix `tnt_dt_*` to avoid possible name clashes. * `strftime` implemented as a simple ffi wrappers in the Tarantool kernel code. **TODO** to setablish %f format which we want for nanoseconds display we plan to slightly modify Olson strftime implementation and thus bundle it with kernel code. Not today, though. * display datetime - introduced output routine for converting datetime to their default output (ISO-8601) format. This routine used by tostring() if given datatime object; * simplified interfaces - totable() export table values similar to os.date('*t') - set() provide unified interface to set values using the same set of attributes as in totable() Example, ``` local dt = datetime.new { nsec = 123456789, usec = 123456, msec = 123, sec = 19, min = 29, hour = 18, day = 20, month = 8, year = 2021, timestamp = 1629476485.123, tzoffset = 180, } local t = dt:totable() --[[ t.nsec t.sec t.min t.hour t.day t.month t.year t.isdst t.wday t.yday --]] dt:format() -- 2021-08-21T14:53:34.032Z dt:format('%Y-%m-%dT%H:%M:%S') -- 2021-08-21T14:53:34 dt:set { nsec = 123456789, usec = 123456, msec = 123, sec = 19, min = 29, hour = 18, day = 20, month = 8, year = 2021, tzoffset = 180, } dt:set { timestamp = 1629476485.124, tzoffset = 180, } ``` Coverage is builtin/datetime.lua 238 43 84.70% Part of #5941 @TarantoolBot document Title: Introduced new built-in `datetime` module `datetime` module has been introduced, which allows to create and modify timestamp objects. Please refer to https://hackmd.io/@Mons/S1Vfc_axK#Datetime-in-Tarantool for more detailed description of module API. Part of #5941 --- .gitmodules | 3 + CMakeLists.txt | 8 + cmake/BuildCDT.cmake | 10 + extra/exports | 37 ++ src/CMakeLists.txt | 5 +- src/lib/core/CMakeLists.txt | 1 + src/lib/core/datetime.c | 121 +++++++ src/lib/core/datetime.h | 94 +++++ src/lua/datetime.lua | 623 +++++++++++++++++++++++++++++++++ src/lua/init.c | 4 +- src/lua/utils.c | 19 + src/lua/utils.h | 11 + test/app-tap/datetime.test.lua | 203 +++++++++++ test/unit/CMakeLists.txt | 2 + test/unit/datetime.c | 261 ++++++++++++++ test/unit/datetime.result | 358 +++++++++++++++++++ third_party/c-dt | 1 + 17 files changed, 1759 insertions(+), 2 deletions(-) create mode 100644 cmake/BuildCDT.cmake create mode 100644 src/lib/core/datetime.c create mode 100644 src/lib/core/datetime.h create mode 100644 src/lua/datetime.lua create mode 100755 test/app-tap/datetime.test.lua create mode 100644 test/unit/datetime.c create mode 100644 test/unit/datetime.result create mode 160000 third_party/c-dt diff --git a/.gitmodules b/.gitmodules index f2f91ee72..aa3fbae4e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -43,3 +43,6 @@ [submodule "third_party/xxHash"] path = third_party/xxHash url = https://github.com/tarantool/xxHash +[submodule "third_party/c-dt"] + path = third_party/c-dt + url = https://github.com/tarantool/c-dt.git diff --git a/CMakeLists.txt b/CMakeLists.txt index e25b81eac..8037c30a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -571,6 +571,14 @@ endif() # zstd # +# +# Christian Hansen c-dt +# + +include(BuildCDT) +libccdt_build() +add_dependencies(build_bundled_libs cdt) + # # Third-Party misc # diff --git a/cmake/BuildCDT.cmake b/cmake/BuildCDT.cmake new file mode 100644 index 000000000..80b26c64a --- /dev/null +++ b/cmake/BuildCDT.cmake @@ -0,0 +1,10 @@ +macro(libccdt_build) + set(LIBCDT_INCLUDE_DIRS ${PROJECT_SOURCE_DIR}/third_party/c-dt/) + set(LIBCDT_LIBRARIES cdt) + + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/third_party/c-dt/build/) + add_subdirectory(${PROJECT_SOURCE_DIR}/third_party/c-dt + ${CMAKE_CURRENT_BINARY_DIR}/third_party/c-dt/build/) + set_target_properties(cdt PROPERTIES COMPILE_FLAGS "-DDT_NAMESPACE=tnt_") + add_definitions("-DDT_NAMESPACE=tnt_") +endmacro() diff --git a/extra/exports b/extra/exports index 9eaba1282..1416a6b3f 100644 --- a/extra/exports +++ b/extra/exports @@ -148,6 +148,10 @@ csv_feed csv_iterator_create csv_next csv_setopt +datetime_now +datetime_strftime +datetime_to_string +datetime_unpack decimal_from_string decimal_unpack error_ref @@ -447,6 +451,39 @@ title_set_status title_update tnt_default_cert_dir_paths tnt_default_cert_file_paths +tnt_dt_add_months +tnt_dt_add_quarters +tnt_dt_add_years +tnt_dt_days_in_month +tnt_dt_days_in_quarter +tnt_dt_days_in_year +tnt_dt_dom +tnt_dt_dow +tnt_dt_doy +tnt_dt_from_rdn +tnt_dt_from_struct_tm +tnt_dt_from_yd +tnt_dt_from_ymd +tnt_dt_from_yqd +tnt_dt_from_ywd +tnt_dt_leap_year +tnt_dt_month +tnt_dt_parse_iso_date +tnt_dt_parse_iso_time +tnt_dt_parse_iso_time_basic +tnt_dt_parse_iso_time_extended +tnt_dt_parse_iso_zone +tnt_dt_parse_iso_zone_basic +tnt_dt_parse_iso_zone_extended +tnt_dt_parse_iso_zone_lenient +tnt_dt_rdn +tnt_dt_to_struct_tm +tnt_dt_to_yd +tnt_dt_to_ymd +tnt_dt_to_yqd +tnt_dt_to_ywd +tnt_dt_weeks_in_year +tnt_dt_year tnt_iconv tnt_iconv_close tnt_iconv_open diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index adb03b3f4..4473ff1da 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -51,6 +51,8 @@ lua_source(lua_sources ../third_party/luafun/fun.lua) lua_source(lua_sources lua/httpc.lua) lua_source(lua_sources lua/iconv.lua) lua_source(lua_sources lua/swim.lua) +lua_source(lua_sources lua/datetime.lua) + # LuaJIT jit.* library lua_source(lua_sources ${LUAJIT_SOURCE_ROOT}/src/jit/bc.lua) lua_source(lua_sources ${LUAJIT_SOURCE_ROOT}/src/jit/bcsave.lua) @@ -193,7 +195,8 @@ target_link_libraries(server core coll http_parser bit uri uuid swim swim_udp # Rule of thumb: if exporting a symbol from a static library, list the # library here. set (reexport_libraries server core misc bitset csv swim swim_udp swim_ev - shutdown ${LUAJIT_LIBRARIES} ${MSGPUCK_LIBRARIES} ${ICU_LIBRARIES} ${CURL_LIBRARIES} ${XXHASH_LIBRARIES}) + shutdown ${LUAJIT_LIBRARIES} ${MSGPUCK_LIBRARIES} ${ICU_LIBRARIES} + ${CURL_LIBRARIES} ${XXHASH_LIBRARIES} ${LIBCDT_LIBRARIES}) set (common_libraries ${reexport_libraries} diff --git a/src/lib/core/CMakeLists.txt b/src/lib/core/CMakeLists.txt index 2cd4d0b4f..8bc776b82 100644 --- a/src/lib/core/CMakeLists.txt +++ b/src/lib/core/CMakeLists.txt @@ -30,6 +30,7 @@ set(core_sources decimal.c mp_decimal.c cord_buf.c + datetime.c ) if (TARGET_OS_NETBSD) diff --git a/src/lib/core/datetime.c b/src/lib/core/datetime.c new file mode 100644 index 000000000..4f2b59bd8 --- /dev/null +++ b/src/lib/core/datetime.c @@ -0,0 +1,121 @@ +/* + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright 2021, Tarantool AUTHORS, please see AUTHORS file. + */ + +#include +#include +#include +#include + +#include "trivia/util.h" +#include "datetime.h" + +/* + * Given the seconds from Epoch (1970-01-01) we calculate date + * since Rata Die (0001-01-01). + * DT_EPOCH_1970_OFFSET is the distance in days from Rata Die to Epoch. + */ +static int +local_dt(int64_t secs) +{ + return dt_from_rdn((int)(secs / SECS_PER_DAY) + DT_EPOCH_1970_OFFSET); +} + +static struct tm * +datetime_to_tm(const struct datetime *date) +{ + static struct tm tm; + + memset(&tm, 0, sizeof(tm)); + int64_t secs = date->epoch; + dt_to_struct_tm(local_dt(secs), &tm); + + int seconds_of_day = (int64_t)date->epoch % SECS_PER_DAY; + tm.tm_hour = (seconds_of_day / 3600) % 24; + tm.tm_min = (seconds_of_day / 60) % 60; + tm.tm_sec = seconds_of_day % 60; + + return &tm; +} + +void +datetime_now(struct datetime *now) +{ + struct timeval tv; + gettimeofday(&tv, NULL); + now->epoch = tv.tv_sec; + now->nsec = tv.tv_usec * 1000; + + time_t now_seconds; + time(&now_seconds); + struct tm tm; + localtime_r(&now_seconds, &tm); + now->tzoffset = tm.tm_gmtoff / 60; +} + +size_t +datetime_strftime(char *buf, uint32_t len, const char *fmt, + const struct datetime *date) +{ + struct tm *p_tm = datetime_to_tm(date); + return strftime(buf, len, fmt, p_tm); +} + + +/* NB! buf may be NULL, and we should handle it gracefully, returning + * calculated length of output string + */ +int +datetime_to_string(char *buf, int len, const struct datetime *date) +{ + int offset = date->tzoffset; + /* for negative offsets around Epoch date we could get + * negative secs value, which should be attributed to + * 1969-12-31, not 1970-01-01, thus we first shift + * epoch to Rata Die then divide by seconds per day, + * not in reverse + */ + int64_t rd_seconds = (int64_t)date->epoch + offset * 60 + + SECS_EPOCH_1970_OFFSET; + int rd_number = rd_seconds / SECS_PER_DAY; + assert(rd_number <= INT_MAX); + assert(rd_number >= INT_MIN); + dt_t dt = dt_from_rdn(rd_number); + + int year, month, day, second, nanosec, sign; + dt_to_ymd(dt, &year, &month, &day); + + int hour = (rd_seconds / 3600) % 24; + int minute = (rd_seconds / 60) % 60; + second = rd_seconds % 60; + nanosec = date->nsec; + + int sz = 0; + SNPRINT(sz, snprintf, buf, len, "%04d-%02d-%02dT%02d:%02d:%02d", + year, month, day, hour, minute, second); + if (nanosec != 0) { + if ((nanosec % 1000000) == 0) + SNPRINT(sz, snprintf, buf, len, ".%03d", + nanosec / 1000000); + else if ((nanosec % 1000) == 0) + SNPRINT(sz, snprintf, buf, len, ".%06d", + nanosec / 1000); + else + SNPRINT(sz, snprintf, buf, len, ".%09d", nanosec); + } + if (offset == 0) { + SNPRINT(sz, snprintf, buf, len, "Z"); + } else { + if (offset < 0) { + sign = '-'; + offset = -offset; + } else { + sign = '+'; + } + SNPRINT(sz, snprintf, buf, len, "%c%02d:%02d", sign, + offset / 60, offset % 60); + } + return sz; +} diff --git a/src/lib/core/datetime.h b/src/lib/core/datetime.h new file mode 100644 index 000000000..7fa2c14a6 --- /dev/null +++ b/src/lib/core/datetime.h @@ -0,0 +1,94 @@ +#pragma once +/* + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright 2021, Tarantool AUTHORS, please see AUTHORS file. + */ + +#include +#include +#include "c-dt/dt.h" + +#if defined(__cplusplus) +extern "C" +{ +#endif /* defined(__cplusplus) */ + +/** + * We count dates since so called "Rata Die" date + * January 1, 0001, Monday (as Day 1). + * But datetime structure keeps seconds since + * Unix "Epoch" date: + * Unix, January 1, 1970, Thursday + * + * The difference between Epoch (1970-01-01) + * and Rata Die (0001-01-01) is 719163 days. + */ + +#ifndef SECS_PER_DAY +#define SECS_PER_DAY 86400 +#define DT_EPOCH_1970_OFFSET 719163 +#endif + +#define SECS_EPOCH_1970_OFFSET \ + ((int64_t)DT_EPOCH_1970_OFFSET * SECS_PER_DAY) +/** + * datetime structure keeps number of seconds since + * Unix Epoch. + * Time is normalized by UTC, so time-zone offset + * is informative only. + */ +struct datetime { + /** Seconds since Epoch. */ + double epoch; + /** Nanoseconds, if any. */ + int32_t nsec; + /** Offset in minutes from UTC. */ + int16_t tzoffset; + /** Olson timezone id */ + int16_t tzindex; +}; + +/** + * Required size of datetime_to_string string buffer + */ +#define DT_TO_STRING_BUFSIZE 48 + +/* + * Compare arguments of a datetime type + * @param lhs left datetime argument + * @param rhs right datetime argument + * @retval < 0 if lhs less than rhs + * @retval = 0 if lhs and rhs equal + * @retval > 0 if lhs greater than rhs + */ +int +datetime_compare(const struct datetime *lhs, const struct datetime *rhs); + +/** + * Convert datetime to string using default format + * @param date source datetime value + * @param buf output character buffer + * @param len size ofoutput buffer + */ +int +datetime_to_string(char *buf, int len, const struct datetime *date); + +/** + * Convert datetime to string using default format provided + * Wrapper around standard strftime() function + * @param date source datetime value + * @param fmt format + * @param buf output buffer + * @param len size of output buffer + */ +size_t +datetime_strftime(char *buf, uint32_t len, const char *fmt, + const struct datetime *date); + +void +datetime_now(struct datetime *now); + +#if defined(__cplusplus) +} /* extern "C" */ +#endif /* defined(__cplusplus) */ diff --git a/src/lua/datetime.lua b/src/lua/datetime.lua new file mode 100644 index 000000000..f63386716 --- /dev/null +++ b/src/lua/datetime.lua @@ -0,0 +1,623 @@ +local ffi = require('ffi') + +--[[ + `c-dt` library functions handles properly both positive and negative `dt` + values, where `dt` is a number of dates since Rata Die date (0001-01-01). + + For better compactness of our typical data in MessagePack stream we shift + root of our time to the Unix Epoch date (1970-01-01), thus our 0 is + actually dt = 719163. + + So here is a simple formula how convert our epoch-based seconds to dt values + dt = (secs / 86400) + 719163 + Where 719163 is an offset of Unix Epoch (1970-01-01) since Rata Die + (0001-01-01) in dates. +]] + +ffi.cdef [[ + +/* dt_core.h definitions */ +typedef int dt_t; + +typedef enum { + DT_MON = 1, + DT_MONDAY = 1, + DT_TUE = 2, + DT_TUESDAY = 2, + DT_WED = 3, + DT_WEDNESDAY = 3, + DT_THU = 4, + DT_THURSDAY = 4, + DT_FRI = 5, + DT_FRIDAY = 5, + DT_SAT = 6, + DT_SATURDAY = 6, + DT_SUN = 7, + DT_SUNDAY = 7, +} dt_dow_t; + +dt_t tnt_dt_from_rdn (int n); +dt_t tnt_dt_from_ymd (int y, int m, int d); +void tnt_dt_to_ymd (dt_t dt, int *y, int *m, int *d); +void tnt_dt_to_yqd (dt_t dt, int *y, int *q, int *d); +void tnt_dt_to_ywd (dt_t dt, int *y, int *w, int *d); + +int tnt_dt_rdn (dt_t dt); +dt_dow_t tnt_dt_dow (dt_t dt); + +/* dt_util.h */ +bool tnt_dt_leap_year (int y); +int tnt_dt_days_in_year (int y); +int tnt_dt_days_in_quarter (int y, int q); +int tnt_dt_days_in_month (int y, int m); +int tnt_dt_weeks_in_year (int y); + +/* dt_accessor.h */ + +int tnt_dt_year (dt_t dt); +int tnt_dt_month (dt_t dt); +int tnt_dt_doy (dt_t dt); +int tnt_dt_dom (dt_t dt); + +/* dt_arithmetic.h definitions */ + +typedef enum { + DT_EXCESS, + DT_LIMIT, + DT_SNAP +} dt_adjust_t; + +dt_t tnt_dt_add_years (dt_t dt, int delta, dt_adjust_t adjust); +dt_t tnt_dt_add_quarters (dt_t dt, int delta, dt_adjust_t adjust); +dt_t tnt_dt_add_months (dt_t dt, int delta, dt_adjust_t adjust); + +/* dt_parse_iso.h definitions */ + +size_t tnt_dt_parse_iso_date (const char *str, size_t len, dt_t *dt); +size_t tnt_dt_parse_iso_time (const char *str, size_t len, int *sod, int *nsec); +size_t tnt_dt_parse_iso_zone_lenient(const char *str, size_t len, int *offset); + +/* Tarantool functions - datetime.c */ + +int datetime_to_string(char *buf, int len, const struct datetime * date); +size_t datetime_strftime(char *buf, uint32_t len, const char *fmt, + const struct datetime *date); +void datetime_now(struct datetime *now); + +]] + +local builtin = ffi.C +local math_modf = math.modf +local math_floor = math.floor + +local SECS_PER_DAY = 86400 +local NANOS_PER_SEC = 1000000000 + +-- c-dt/dt_config.h + +-- Unix, January 1, 1970, Thursday +local DT_EPOCH_1970_OFFSET = 719163 + + +local datetime_t = ffi.typeof('struct datetime') + +local function is_datetime(o) + return ffi.istype(datetime_t, o) +end + +local function check_date(o, message) + if not is_datetime(o) then + return error(("%s: expected datetime, but received %s"): + format(message, o), 2) + end +end + +local function check_table(o, message) + if type(o) ~= 'table' then + return error(("%s: expected table %s"):format(message, o), 2) + end +end + +local function check_str(s, message) + if not type(s) == 'string' then + return error(("%s: expected string, but received %s"): + format(message, s), 2) + end +end + +-- range may be of a form of pair {begin, end} or +-- tuple {begin, end, negative} +-- negative is a special value (so far) used for days only +local function check_range(v, range, txt) + local len = #range + assert(len == 2 or len == 3) + + local left, right, neg = unpack(range) + if neg == v or (v >= left and v <= right) then + return + end + + if neg == nil then + error(('value %d of %s is out of allowed range [%d, %d]'): + format(v, txt, left, right), 2) + else + error(('value %d of %s is out of allowed range [%d, %d..%d]'): + format(v, txt, neg, left, right), 2) + end +end + +local function nyi(msg) + local text = 'Not yet implemented' + if msg ~= nil then + text = ("%s : '%s'"):format(text, msg) + end + error(text, 3) +end + +-- offset if __seconds__ of 1970-01-01 from 0000-01-01 +local SECS_EPOCH_OFFSET = (DT_EPOCH_1970_OFFSET * SECS_PER_DAY) + +-- convert from epoch related time to Rata Die related +local function local_rd(secs) + return math_floor((secs + SECS_EPOCH_OFFSET) / SECS_PER_DAY) +end + +-- convert UTC seconds to local seconds, adjusting by timezone +local function local_secs(obj) + return obj.epoch + obj.tzoffset * 60 +end + +local function utc_secs(epoch, tzoffset) + return epoch - tzoffset * 60 +end + +-- get epoch seconds, shift to the local timezone +-- adjust from 1970-related to 0000-related time +-- then return dt in those coordinates (number of days +-- since Rata Die date) +local function local_dt(obj) + return builtin.tnt_dt_from_rdn(local_rd(local_secs(obj))) +end + +local function normalize_nsec(secs, nsec) + if nsec < 0 then + secs = secs - 1 + nsec = nsec + NANOS_PER_SEC + elseif nsec >= NANOS_PER_SEC then + secs = secs + 1 + nsec = nsec - NANOS_PER_SEC + end + return secs, nsec +end + +local function datetime_cmp(lhs, rhs) + if not is_datetime(lhs) or not is_datetime(rhs) then + return nil + end + local sdiff = lhs.epoch - rhs.epoch + return sdiff ~= 0 and sdiff or (lhs.nsec - rhs.nsec) +end + +local function datetime_eq(lhs, rhs) + local rc = datetime_cmp(lhs, rhs) + return rc ~= nil and rc == 0 +end + +local function datetime_lt(lhs, rhs) + local rc = datetime_cmp(lhs, rhs) + return rc == nil and error('incompatible types for comparison', 2) or + rc < 0 +end + +local function datetime_le(lhs, rhs) + local rc = datetime_cmp(lhs, rhs) + return rc == nil and error('incompatible types for comparison', 2) or + rc <= 0 +end + +local function datetime_serialize(self) + return { epoch = self.epoch, nsec = self.nsec, + tzoffset = self.tzoffset, tzindex = 0 } +end + +local parse_zone + +local function datetime_new_raw(epoch, nsec, tzoffset, tzindex) + local dt_obj = ffi.new(datetime_t) + dt_obj.epoch = epoch + dt_obj.nsec = nsec + dt_obj.tzoffset = tzoffset or 0 + dt_obj.tzindex = tzindex or 0 + return dt_obj +end + +local function datetime_new_dt(dt, secs, fraction, offset) + local epochV = dt ~= nil and (builtin.tnt_dt_rdn(dt) - DT_EPOCH_1970_OFFSET) * + SECS_PER_DAY or 0 + local secsV = secs or 0 + local fracV = fraction or 0 + local ofsV = offset or 0 + return datetime_new_raw(epochV + secsV - ofsV * 60, fracV, ofsV) +end + +local function get_timezone(offset) + if type(offset) == 'number' then + return offset + elseif type(offset) == 'string' then + return parse_zone(offset) + end +end + +-- create datetime given attribute values from obj +local function datetime_new(obj) + if obj == nil or type(obj) ~= 'table' then + return datetime_new_raw(0, 0, 0) + end + + local ymd = false + local hms = false + local dt = DT_EPOCH_1970_OFFSET -- default is 1970-01-01 + + local y = obj.year + if y ~= nil then + check_range(y, {1, 9999}, 'year') + ymd = true + end + local M = obj.month + if M ~= nil then + check_range(M, {1, 12}, 'month') + ymd = true + end + local d = obj.day + if d ~= nil then + check_range(d, {1, 31, -1}, 'day') + ymd = true + end + local h = obj.hour + if h ~= nil then + check_range(h, {0, 23}, 'hour') + hms = true + end + local m = obj.min + if m ~= nil then + check_range(m, {0, 59}, 'min') + hms = true + end + local s = obj.sec + if s ~= nil then + check_range(s, {0, 60}, 'sec') + hms = true + end + local nsec, usec, msec = obj.nsec, obj.usec, obj.msec + -- if there are separate nsec, usec, or msec provided then + -- timestamp should be integer + local int_ts = nsec ~= nil or usec ~= nil or msec ~= nil + + local ts = obj.timestamp + local fraction + if ts ~= nil then + s, fraction = math_modf(ts) + if not int_ts then + nsec = fraction * 1e9 + end + hms = true + end + + local offset = obj.tzoffset + if offset ~= nil then + offset = get_timezone(offset) + check_range(offset, {-720, 720}, offset) + end + + if obj.tz ~= nil then + nyi('tz') + end + + -- .year, .month, .day + if ymd then + y = y or 1970 + M = M or 1 + if d ~= nil and d < 0 then + d = builtin.tnt_dt_days_in_month(y, M) + end + dt = builtin.tnt_dt_from_ymd(y, M, d) + end + + -- .hour, .minute, .second + local secs = 0 + if hms then + secs = (h or 0) * 3600 + (m or 0) * 60 + (s or 0) + end + + return datetime_new_dt(dt, secs, nsec, offset or 0) +end + +local sz = 48 +local buff = ffi.new('char[?]', sz) + +--[[ + Convert to text datetime values + + - datetime will use ISO-8601 format: + 1970-01-01T00:00Z + 2021-08-18T16:57:08.981725+03:00 +]] +local function datetime_tostring(self) + check_date(self, 'datetime.tostring()') + + local len = builtin.datetime_to_string(buff, sz, self) + assert(len < sz) + return ffi.string(buff) +end + +--[[ + Basic Extended + Z N/A + +hh N/A + -hh N/A + +hhmm +hh:mm + -hhmm -hh:mm + + Returns pair of constructed datetime object, and length of string + which has been accepted by parser. +]] +parse_zone = function(str) + check_str("datetime.parse_zone()") + local offset = ffi.new('int[1]') + local len = builtin.tnt_dt_parse_iso_zone_lenient(str, #str, offset) + if len == 0 then + error(('invalid time-zone format %s'):format(str), 3) + end + return offset[0] +end + +--[[ + Dispatch function to create datetime from string or table. + Creates default timeobject (pointing to Epoch date) if + called without arguments. +]] +local function datetime_from(o) + if o == nil or type(o) == 'table' then + return datetime_new(o) + end +end + +--[[ + Create datetime object representing current time using microseconds + platform timer and local timezone information. +]] +local function local_now() + local d = datetime_new_raw(0, 0, 0) + builtin.datetime_now(d) + return d +end + +--[[ + Return table in os.date('*t') format, but with timezone + and nanoseconds +]] +local function datetime_totable(self) + local secs = local_secs(self) -- hour:minute should be in local timezone + local dt = local_dt(self) + + return { + year = builtin.tnt_dt_year(dt), + month = builtin.tnt_dt_month(dt), + yday = builtin.tnt_dt_doy(dt), + day = builtin.tnt_dt_dom(dt), + wday = (ffi.cast('int32_t', builtin.tnt_dt_dow(dt)) + 1) % 7, + hour = math_floor((secs / 3600) % 24), + min = math_floor((secs / 60) % 60), + sec = secs % 60, + isdst = false, -- FIXME - after we introduced timezone/DST support + nsec = self.nsec, + tzoffset = self.tzoffset, + -- tz = "MSK", -- FIXME - after we introduced timezone support + } +end + +local function datetime_update_dt(self, dt, new_offset) + local epoch = local_secs(self) + local secs_day = epoch % SECS_PER_DAY + epoch = (builtin.tnt_dt_rdn(dt) - DT_EPOCH_1970_OFFSET) * SECS_PER_DAY + secs_day + self.epoch = utc_secs(epoch, new_offset) +end + +local function datetime_ymd_update(self, y, M, d, new_offset) + if d < 0 then + d = builtin.tnt_dt_days_in_month(y, M) + end + if d > 28 then + local day_in_month = builtin.tnt_dt_days_in_month(y, M) + if d > day_in_month then + error(('invalid number of days %d in month %d for %d'): + format(d, M, y), 3) + end + end + local dt = builtin.tnt_dt_from_ymd(y or 0, M or 1, d or 1) + datetime_update_dt(self, dt, new_offset) +end + +local function datetime_hms_update(self, h, m, s, new_offset) + local epoch = local_secs(self) + local secs_day = epoch - (epoch % SECS_PER_DAY) + self.epoch = utc_secs(secs_day + h * 3600 + m * 60 + s, new_offset) +end + +local function bool2int(b) + return b and 1 or 0 +end + +local function datetime_set(self, obj) + check_table(obj, "datetime.set()") + + local ymd = false + local hms = false + + local dt = local_dt(self) + local y0 = ffi.new('int[1]') + local M0 = ffi.new('int[1]') + local d0 = ffi.new('int[1]') + builtin.tnt_dt_to_ymd(dt, y0, M0, d0) + y0, M0, d0 = y0[0], M0[0], d0[0] + + local y = obj.year + if y ~= nil then + check_range(y, {1, 9999}, 'year') + ymd = true + end + local M = obj.month + if M ~= nil then + check_range(M, {1, 12}, 'month') + ymd = true + end + local d = obj.day + if d ~= nil then + check_range(d, {1, 31, -1}, 'day') + ymd = true + end + + local lsecs = local_secs(self) + local h0 = math_floor(lsecs / (24 * 60)) % 24 + local m0 = math_floor(lsecs / 60) % 60 + local sec0 = lsecs % 60 + + local h = obj.hour + if h ~= nil then + check_range(h, {0, 23}, 'hour') + hms = true + end + local m = obj.min + if m ~= nil then + check_range(m, {0, 59}, 'min') + hms = true + end + local sec = obj.sec + if sec ~= nil then + check_range(sec, {0, 60}, 'sec') + hms = true + end + + local nsec, usec, msec = obj.nsec, obj.usec, obj.msec + local count_usec = bool2int(nsec ~= nil) + bool2int(usec ~= nil) + + bool2int(msec ~= nil) + if count_usec > 1 then + error('only one of nsec, usec or msecs may defined simultaneously', 2) + end + if usec ~= nil then + nsec = usec * 1e3 + elseif msec ~= nil then + nsec = msec * 1e6 + end + + local ts = obj.timestamp + if ts ~= nil then + local sec_int, fraction + sec_int, fraction = math_modf(ts) + -- if there is one of nsec, usec, msec provided + -- then ignore fraction in timestamp + -- otherwise - use nsec, usec, or msec + if count_usec == 0 then + nsec = fraction * 1e9 + end + + self.secs = sec_int + self.nsec = nsec + + return self + end + + local offset0 = self.tzoffset + local offset = obj.tzoffset + if offset ~= nil then + offset = get_timezone(offset) + check_range(offset, {-720, 720}, 'tzoffset') + -- self.tzoffset = offset + end + + if obj.tz ~= nil then + nyi('tz') + end + + -- .year, .month, .day + if ymd then + datetime_ymd_update(self, y or y0, M or M0, d or d0, offset or offset0) + end + + -- .hour, .minute, .second + if hms then + datetime_hms_update(self, h or h0, m or m0, sec or sec0, offset or offset0) + end + + self.epoch, self.nsec = normalize_nsec(self.epoch, self.nsec) + + if offset ~= nil then + self.tzoffset = offset + end + + return self +end + +local strfmt_sz = 128 +local strfmt_buff = ffi.new('char[?]', strfmt_sz) + +local function datetime_strftime(self, fmt) + check_date(self, "datetime.strftime()") + builtin.datetime_strftime(strfmt_buff, strfmt_sz, fmt, self) + return ffi.string(strfmt_buff) +end + +local function datetime_format(self, fmt) + if fmt ~= nil then + return datetime_strftime(self, fmt) + else + return datetime_tostring(self) + end +end + +ffi.metatype(datetime_t, { + __tostring = datetime_tostring, + __serialize = datetime_serialize, + __eq = datetime_eq, + __lt = datetime_lt, + __le = datetime_le, + __index = { + epoch = function(self) return self.epoch end, + timestamp = function(self) return self.epoch + self.nsec / 1e9 end, + + nsec = function(self) return self.nsec end, + usec = function(self) return self.nsec / 1e3 end, + msec = function(self) return self.nsec / 1e6 end, + + dt = function(self) return local_dt(self) end, + year = function(self) return builtin.tnt_dt_year(local_dt(self)) end, + month = function(self) return builtin.tnt_dt_month(local_dt(self)) end, + + yday = function(self) return builtin.tnt_dt_doy(local_dt(self)) end, + wday = function(self) + return ffi.cast('int32_t', builtin.tnt_dt_dow(local_dt(self))) + end, + day = function(self) + return builtin.tnt_dt_dom(local_dt(self)) + end, + hour = function(self) return math_floor((local_secs(self) / 3600) % 24) end, + minute = function(self) return math_floor((local_secs(self) / 60) % 60) end, + second = function(self) return self.epoch % 60 end, + + format = datetime_format, + totable = datetime_totable, + set = datetime_set, + } +}) + +return setmetatable( + { + new = datetime_new, + + now = local_now, + + is_datetime = is_datetime, + }, { + __call = function(self, ...) return datetime_from(...) end + } +) diff --git a/src/lua/init.c b/src/lua/init.c index f9738025d..127e935d7 100644 --- a/src/lua/init.c +++ b/src/lua/init.c @@ -129,7 +129,8 @@ extern char strict_lua[], parse_lua[], process_lua[], humanize_lua[], - memprof_lua[] + memprof_lua[], + datetime_lua[] ; static const char *lua_modules[] = { @@ -184,6 +185,7 @@ static const char *lua_modules[] = { "memprof.process", process_lua, "memprof.humanize", humanize_lua, "memprof", memprof_lua, + "datetime", datetime_lua, NULL }; diff --git a/src/lua/utils.c b/src/lua/utils.c index c71cd4857..b25656a1a 100644 --- a/src/lua/utils.c +++ b/src/lua/utils.c @@ -48,6 +48,8 @@ static uint32_t CTID_STRUCT_IBUF_PTR; uint32_t CTID_CHAR_PTR; uint32_t CTID_CONST_CHAR_PTR; uint32_t CTID_UUID; +uint32_t CTID_DATETIME = 0; + void * luaL_pushcdata(struct lua_State *L, uint32_t ctypeid) @@ -120,6 +122,12 @@ luaL_pushuuidstr(struct lua_State *L, const struct tt_uuid *uuid) lua_pushlstring(L, str, UUID_STR_LEN); } +struct datetime * +luaL_pushdatetime(struct lua_State *L) +{ + return luaL_pushcdata(L, CTID_DATETIME); +} + int luaL_iscdata(struct lua_State *L, int idx) { @@ -725,6 +733,17 @@ tarantool_lua_utils_init(struct lua_State *L) CTID_UUID = luaL_ctypeid(L, "struct tt_uuid"); assert(CTID_UUID != 0); + rc = luaL_cdef(L, "struct datetime {" + "double epoch;" + "int32_t nsec;" + "int16_t tzoffset;" + "int16_t tzindex;" + "};"); + assert(rc == 0); + (void) rc; + CTID_DATETIME = luaL_ctypeid(L, "struct datetime"); + assert(CTID_DATETIME != 0); + lua_pushcfunction(L, luaT_newthread_wrapper); luaT_newthread_ref = luaL_ref(L, LUA_REGISTRYINDEX); return 0; diff --git a/src/lua/utils.h b/src/lua/utils.h index 45070b778..9c08947cc 100644 --- a/src/lua/utils.h +++ b/src/lua/utils.h @@ -59,6 +59,7 @@ struct lua_State; struct ibuf; typedef struct ibuf box_ibuf_t; struct tt_uuid; +struct datetime; /** * Single global lua_State shared by core and modules. @@ -71,6 +72,7 @@ extern struct lua_State *tarantool_L; extern uint32_t CTID_CHAR_PTR; extern uint32_t CTID_CONST_CHAR_PTR; extern uint32_t CTID_UUID; +extern uint32_t CTID_DATETIME; struct tt_uuid * luaL_pushuuid(struct lua_State *L); @@ -78,6 +80,15 @@ luaL_pushuuid(struct lua_State *L); void luaL_pushuuidstr(struct lua_State *L, const struct tt_uuid *uuid); +/** + * @brief Push cdata of a datetime type onto the stack. + * @param L Lua State + * @sa luaL_pushcdata + * @return memory associated with this datetime data + */ +struct datetime * +luaL_pushdatetime(struct lua_State *L); + /** \cond public */ /** diff --git a/test/app-tap/datetime.test.lua b/test/app-tap/datetime.test.lua new file mode 100755 index 000000000..4740c800d --- /dev/null +++ b/test/app-tap/datetime.test.lua @@ -0,0 +1,203 @@ +#!/usr/bin/env tarantool + +local tap = require('tap') +local test = tap.test("errno") +local date = require('datetime') + +test:plan(7) + +local function assert_raises(test, error_msg, func, ...) + local ok, err = pcall(func, ...) + local err_tail = err:gsub("^.+:%d+: ", "") + return test:ok(not ok and err_tail == error_msg, + ('"%s" received, "%s" expected'):format(err_tail, error_msg)) +end + +test:test("Default date creation", function(test) + test:plan(9) + -- check empty arguments + local T1 = date.new() + test:is(T1.epoch, 0, "T.epoch ==0") + test:is(T1.nsec, 0, "T.nsec == 0") + test:is(T1.tzoffset, 0, "T.tzoffset == 0") + test:is(tostring(T1), "1970-01-01T00:00:00Z", "tostring(T1)") + -- check empty table + local T2 = date.new{} + test:is(T2.epoch, 0, "T.epoch ==0") + test:is(T2.nsec, 0, "T.nsec == 0") + test:is(T2.tzoffset, 0, "T.tzoffset == 0") + test:is(tostring(T2), "1970-01-01T00:00:00Z", "tostring(T2)") + -- check their equivalence + test:is(T1, T2, "T1 == T2") +end) + +test:test("Datetime string formatting", function(test) + test:plan(6) + local t = date() + test:is(t.epoch, 0, ('t.epoch == %d'):format(tonumber(t.epoch))) + test:is(t.nsec, 0, ('t.nsec == %d'):format(t.nsec)) + test:is(t.tzoffset, 0, ('t.tzoffset == %d'):format(t.tzoffset)) + test:is(t:format('%d/%m/%Y'), '01/01/1970', '%s: format #1') + test:is(t:format('%A %d. %B %Y'), 'Thursday 01. January 1970', 'format #2') + test:is(t:format('%FT%T%z'), '1970-01-01T00:00:00+0000', 'format #3') +end) + +test:test("totable{}", function(test) + test:plan(9) + local exp = {sec = 0, min = 0, wday = 5, day = 1, + nsec = 0, isdst = false, yday = 1, + tzoffset = 0, month = 1, year = 1970, hour = 0} + local T = date.new() + local TT = T:totable() + test:is_deeply(TT, exp, "date:totable()") + + local D = os.date('*t') + TT = date.new(D):totable() + local keys = { + 'sec', 'min', 'wday', 'day', 'yday', 'month', 'year', 'hour' + } + for _, key in pairs(keys) do + test:is(TT[key], D[key], ("[%s]: %s == %s"):format(key, TT[key], D[key])) + end +end) + +test:test("Time :set{} operations", function(test) + test:plan(8) + + local T = date.new{ year = 2021, month = 8, day = 31, + hour = 0, min = 31, sec = 11, tzoffset = '+0300'} + test:is(tostring(T), '2021-08-31T00:31:11+03:00', 'initial') + test:is(tostring(T:set{ year = 2020 }), '2020-08-31T00:31:11+03:00', '2020 year') + test:is(tostring(T:set{ month = 11, day = 30 }), '2020-11-30T00:31:11+03:00', 'month = 11, day = 30') + test:is(tostring(T:set{ day = 9 }), '2020-11-09T00:31:11+03:00', 'day 9') + test:is(tostring(T:set{ hour = 6 }), '2020-11-09T06:31:11+03:00', 'hour 6') + test:is(tostring(T:set{ min = 12, sec = 23 }), '2020-11-09T04:12:23+03:00', 'min 12, sec 23') + test:is(tostring(T:set{ tzoffset = -8*60 }), '2020-11-08T17:12:23-08:00', 'offset -0800' ) + test:is(tostring(T:set{ tzoffset = '+0800' }), '2020-11-09T09:12:23+08:00', 'offset +0800' ) +end) + +local function range_check_error(name, value, range) + return ('value %s of %s is out of allowed range [%d, %d]'): + format(value, name, range[1], range[2]) +end + +local function range_check_3_error(v) + return ('value %d of %s is out of allowed range [%d, %d..%d]'): + format(v, 'day', -1, 1, 31) +end + +test:test("Time invalid :set{} operations", function(test) + test:plan(17) + + local T = date.new{} + + assert_raises(test, range_check_error('year', 10000, {1, 9999}), + function() T:set{ year = 10000} end) + assert_raises(test, range_check_error('year', -10, {1, 9999}), + function() T:set{ year = -10} end) + + assert_raises(test, range_check_error('month', 20, {1, 12}), + function() T:set{ month = 20} end) + assert_raises(test, range_check_error('month', 0, {1, 12}), + function() T:set{ month = 0} end) + assert_raises(test, range_check_error('month', -20, {1, 12}), + function() T:set{ month = -20} end) + + assert_raises(test, range_check_3_error(40), + function() T:set{ day = 40} end) + assert_raises(test, range_check_3_error(0), + function() T:set{ day = 0} end) + assert_raises(test, range_check_3_error(-10), + function() T:set{ day = -10} end) + + assert_raises(test, range_check_error('hour', 31, {0, 23}), + function() T:set{ hour = 31} end) + assert_raises(test, range_check_error('hour', -1, {0, 23}), + function() T:set{ hour = -1} end) + + assert_raises(test, range_check_error('min', 60, {0, 59}), + function() T:set{ min = 60} end) + assert_raises(test, range_check_error('min', -1, {0, 59}), + function() T:set{ min = -1} end) + + assert_raises(test, range_check_error('sec', 61, {0, 60}), + function() T:set{ sec = 61} end) + assert_raises(test, range_check_error('sec', -1, {0, 60}), + function() T:set{ sec = -1} end) + + local only1 = 'only one of nsec, usec or msecs may defined simultaneously' + assert_raises(test, only1, function() + T:set{ nsec = 123456, usec = 123} + end) + assert_raises(test, only1, function() + T:set{ nsec = 123456, msec = 123} + end) + assert_raises(test, only1, function() + T:set{ nsec = 123456, usec = 1234, msec = 123} + end) +end) + +local function invalid_tz_fmt_error(val) + return ('invalid time-zone format %s'):format(val) +end + +test:test("Time invalid tzoffset in :set{} operations", function(test) + test:plan(10) + + local T = date.new{} + local bad_strings = { + 'bogus', + '0100', + '+-0100', + '+25:00', + '+99:00', + '-99:00', + } + for _, val in ipairs(bad_strings) do + assert_raises(test, invalid_tz_fmt_error(val), + function() T:set{ tzoffset = val } end) + end + + local bad_numbers = { + 800, + -800, + 10000, + -10000, + } + for _, val in ipairs(bad_numbers) do + assert_raises(test, range_check_error('tzoffset', val, {-720, 720}), + function() T:set{ tzoffset = val } end) + end +end) + + +test:test("Time :set{day = -1} operations", function(test) + test:plan(14) + local tests = { + {{ year = 2000, month = 3, day = -1}, '2000-03-31T00:00:00Z'}, + {{ year = 2000, month = 2, day = -1}, '2000-02-29T00:00:00Z'}, + {{ year = 2001, month = 2, day = -1}, '2001-02-28T00:00:00Z'}, + {{ year = 1900, month = 2, day = -1}, '1900-02-28T00:00:00Z'}, + {{ year = 1904, month = 2, day = -1}, '1904-02-29T00:00:00Z'}, + } + local T + for _, row in ipairs(tests) do + local args, str = unpack(row) + T = date.new(args) + test:is(tostring(T), str, ('checking -1 with %s'):format(str)) + end + assert_raises(test, range_check_3_error(0), function() T = date.new{day = 0} end) + assert_raises(test, range_check_3_error(-2), function() T = date.new{day = -2} end) + assert_raises(test, range_check_3_error(-10), function() T = date.new{day = -10} end) + + T = date.new{ year = 1904, month = 2, day = -1 } + test:is(tostring(T), '1904-02-29T00:00:00Z', 'base before :set{}') + test:is(tostring(T:set{month = 3, day = 2}), '1904-03-02T00:00:00Z', '2 March') + test:is(tostring(T:set{day = -1}), '1904-03-31T00:00:00Z', '31 March') + + assert_raises(test, range_check_3_error(0), function() T:set{day = 0} end) + assert_raises(test, range_check_3_error(-2), function() T:set{day = -2} end) + assert_raises(test, range_check_3_error(-10), function() T:set{day = -10} end) +end) + +os.exit(test:check() and 0 or 1) diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index fc9b8abd2..5662e89f2 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -58,6 +58,8 @@ add_executable(random.test random.c core_test_utils.c) target_link_libraries(random.test core unit) add_executable(xmalloc.test xmalloc.c core_test_utils.c) target_link_libraries(xmalloc.test core unit) +add_executable(datetime.test datetime.c) +target_link_libraries(datetime.test cdt core unit) add_executable(bps_tree.test bps_tree.cc) target_link_libraries(bps_tree.test small misc) diff --git a/test/unit/datetime.c b/test/unit/datetime.c new file mode 100644 index 000000000..f289e66dd --- /dev/null +++ b/test/unit/datetime.c @@ -0,0 +1,261 @@ +#include "dt.h" +#include +#include +#include +#include + +#include "unit.h" +#include "datetime.h" +#include "trivia/util.h" + +static const char sample[] = "2012-12-24T15:30Z"; + +#define S(s) {s, sizeof(s) - 1} +struct { + const char *str; + size_t len; +} tests[] = { + S("2012-12-24 15:30Z"), + S("2012-12-24 15:30z"), + S("2012-12-24 15:30"), + S("2012-12-24 16:30+01:00"), + S("2012-12-24 16:30+0100"), + S("2012-12-24 16:30+01"), + S("2012-12-24 14:30-01:00"), + S("2012-12-24 14:30-0100"), + S("2012-12-24 14:30-01"), + S("2012-12-24 15:30:00Z"), + S("2012-12-24 15:30:00z"), + S("2012-12-24 15:30:00"), + S("2012-12-24 16:30:00+01:00"), + S("2012-12-24 16:30:00+0100"), + S("2012-12-24 14:30:00-01:00"), + S("2012-12-24 14:30:00-0100"), + S("2012-12-24 15:30:00.123456Z"), + S("2012-12-24 15:30:00.123456z"), + S("2012-12-24 15:30:00.123456"), + S("2012-12-24 16:30:00.123456+01:00"), + S("2012-12-24 16:30:00.123456+01"), + S("2012-12-24 14:30:00.123456-01:00"), + S("2012-12-24 14:30:00.123456-01"), + S("2012-12-24t15:30Z"), + S("2012-12-24t15:30z"), + S("2012-12-24t15:30"), + S("2012-12-24t16:30+01:00"), + S("2012-12-24t16:30+0100"), + S("2012-12-24t14:30-01:00"), + S("2012-12-24t14:30-0100"), + S("2012-12-24t15:30:00Z"), + S("2012-12-24t15:30:00z"), + S("2012-12-24t15:30:00"), + S("2012-12-24t16:30:00+01:00"), + S("2012-12-24t16:30:00+0100"), + S("2012-12-24t14:30:00-01:00"), + S("2012-12-24t14:30:00-0100"), + S("2012-12-24t15:30:00.123456Z"), + S("2012-12-24t15:30:00.123456z"), + S("2012-12-24t16:30:00.123456+01:00"), + S("2012-12-24t14:30:00.123456-01:00"), + S("2012-12-24 16:30 +01:00"), + S("2012-12-24 14:30 -01:00"), + S("2012-12-24 15:30 UTC"), + S("2012-12-24 16:30 UTC+1"), + S("2012-12-24 16:30 UTC+01"), + S("2012-12-24 16:30 UTC+0100"), + S("2012-12-24 16:30 UTC+01:00"), + S("2012-12-24 14:30 UTC-1"), + S("2012-12-24 14:30 UTC-01"), + S("2012-12-24 14:30 UTC-01:00"), + S("2012-12-24 14:30 UTC-0100"), + S("2012-12-24 15:30 GMT"), + S("2012-12-24 16:30 GMT+1"), + S("2012-12-24 16:30 GMT+01"), + S("2012-12-24 16:30 GMT+0100"), + S("2012-12-24 16:30 GMT+01:00"), + S("2012-12-24 14:30 GMT-1"), + S("2012-12-24 14:30 GMT-01"), + S("2012-12-24 14:30 GMT-01:00"), + S("2012-12-24 14:30 GMT-0100"), + S("2012-12-24 14:30 -01:00"), + S("2012-12-24 16:30:00 +01:00"), + S("2012-12-24 14:30:00 -01:00"), + S("2012-12-24 16:30:00.123456 +01:00"), + S("2012-12-24 14:30:00.123456 -01:00"), + S("2012-12-24 15:30:00.123456 -00:00"), + S("20121224T1630+01:00"), + S("2012-12-24T1630+01:00"), + S("20121224T16:30+01"), + S("20121224T16:30 +01"), +}; +#undef S + +static int +parse_datetime(const char *str, size_t len, int64_t *secs_p, + int32_t *nanosecs_p, int32_t *offset_p) +{ + size_t n; + dt_t dt; + char c; + int sec_of_day = 0, nanosecond = 0, offset = 0; + + n = dt_parse_iso_date(str, len, &dt); + if (!n) + return 1; + if (n == len) + goto exit; + + c = str[n++]; + if (!(c == 'T' || c == 't' || c == ' ')) + return 1; + + str += n; + len -= n; + + n = dt_parse_iso_time(str, len, &sec_of_day, &nanosecond); + if (!n) + return 1; + if (n == len) + goto exit; + + if (str[n] == ' ') + n++; + + str += n; + len -= n; + + n = dt_parse_iso_zone_lenient(str, len, &offset); + if (!n || n != len) + return 1; + +exit: + *secs_p = ((int64_t)dt_rdn(dt) - DT_EPOCH_1970_OFFSET) * SECS_PER_DAY + + sec_of_day - offset * 60; + *nanosecs_p = nanosecond; + *offset_p = offset; + + return 0; +} + +static int +local_rd(const struct datetime *dt) +{ + return (int)((int64_t)dt->epoch / SECS_PER_DAY) + DT_EPOCH_1970_OFFSET; +} + +static int +local_dt(const struct datetime *dt) +{ + return dt_from_rdn(local_rd(dt)); +} + +struct tm * +datetime_to_tm(struct datetime *dt) +{ + static struct tm tm; + + memset(&tm, 0, sizeof(tm)); + dt_to_struct_tm(local_dt(dt), &tm); + + int seconds_of_day = (int64_t)dt->epoch % 86400; + tm.tm_hour = (seconds_of_day / 3600) % 24; + tm.tm_min = (seconds_of_day / 60) % 60; + tm.tm_sec = seconds_of_day % 60; + + return &tm; +} + +static void datetime_test(void) +{ + size_t index; + int64_t secs_expected; + int32_t nanosecs; + int32_t offset; + + plan(355); + parse_datetime(sample, sizeof(sample) - 1, + &secs_expected, &nanosecs, &offset); + + for (index = 0; index < lengthof(tests); index++) { + int64_t secs; + int rc = parse_datetime(tests[index].str, tests[index].len, + &secs, &nanosecs, &offset); + is(rc, 0, "correct parse_datetime return value for '%s'", + tests[index].str); + is(secs, secs_expected, "correct parse_datetime output " + "seconds for '%s", + tests[index].str); + + /* + * check that stringized literal produces the same date + * time fields + */ + static char buff[40]; + struct datetime dt = {secs, nanosecs, offset, 0}; + /* datetime_to_tm returns time in GMT zone */ + struct tm *p_tm = datetime_to_tm(&dt); + size_t len = strftime(buff, sizeof(buff), "%F %T", p_tm); + ok(len > 0, "strftime"); + int64_t parsed_secs; + int32_t parsed_nsecs, parsed_ofs; + rc = parse_datetime(buff, len, &parsed_secs, &parsed_nsecs, &parsed_ofs); + is(rc, 0, "correct parse_datetime return value for '%s'", buff); + is(secs, parsed_secs, + "reversible seconds via strftime for '%s", buff); + } + check_plan(); +} + + +static void +tostring_datetime_test(void) +{ + static struct { + const char *string; + int64_t secs; + uint32_t nsec; + uint32_t offset; + } tests[] = { + {"1970-01-01T02:00+02:00", 0, 0, 120}, + {"1970-01-01T01:30+01:30", 0, 0, 90}, + {"1970-01-01T01:00+01:00", 0, 0, 60}, + {"1970-01-01T00:01+00:01", 0, 0, 1}, + {"1970-01-01T00:00Z", 0, 0, 0}, + {"1969-12-31T23:59-00:01", 0, 0, -1}, + {"1969-12-31T23:00-01:00", 0, 0, -60}, + {"1969-12-31T22:30-01:30", 0, 0, -90}, + {"1969-12-31T22:00-02:00", 0, 0, -120}, + {"1970-01-01T00:00:00.123456789Z", 0, 123456789, 0}, + {"1970-01-01T00:00:00.123456Z", 0, 123456000, 0}, + {"1970-01-01T00:00:00.123Z", 0, 123000000, 0}, + {"1973-11-29T21:33:09Z", 123456789, 0, 0}, + {"2013-10-28T17:51:56Z", 1382982716, 0, 0}, + {"9999-12-31T23:59:59Z", 253402300799, 0, 0}, + }; + size_t index; + + plan(15); + for (index = 0; index < lengthof(tests); index++) { + struct datetime date = { + tests[index].secs, + tests[index].nsec, + tests[index].offset, + 0 + }; + char buf[48]; + datetime_to_string(buf, sizeof(buf), &date); + is(strcmp(buf, tests[index].string), 0, + "string '%s' expected, received '%s'", + tests[index].string, buf); + } + check_plan(); +} + +int +main(void) +{ + plan(2); + datetime_test(); + tostring_datetime_test(); + + return check_plan(); +} diff --git a/test/unit/datetime.result b/test/unit/datetime.result new file mode 100644 index 000000000..33997d9df --- /dev/null +++ b/test/unit/datetime.result @@ -0,0 +1,358 @@ +1..1 + 1..355 + ok 1 - correct parse_datetime return value for '2012-12-24 15:30Z' + ok 2 - correct parse_datetime output seconds for '2012-12-24 15:30Z + ok 3 - strftime + ok 4 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 5 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 6 - correct parse_datetime return value for '2012-12-24 15:30z' + ok 7 - correct parse_datetime output seconds for '2012-12-24 15:30z + ok 8 - strftime + ok 9 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 10 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 11 - correct parse_datetime return value for '2012-12-24 15:30' + ok 12 - correct parse_datetime output seconds for '2012-12-24 15:30 + ok 13 - strftime + ok 14 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 15 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 16 - correct parse_datetime return value for '2012-12-24 16:30+01:00' + ok 17 - correct parse_datetime output seconds for '2012-12-24 16:30+01:00 + ok 18 - strftime + ok 19 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 20 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 21 - correct parse_datetime return value for '2012-12-24 16:30+0100' + ok 22 - correct parse_datetime output seconds for '2012-12-24 16:30+0100 + ok 23 - strftime + ok 24 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 25 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 26 - correct parse_datetime return value for '2012-12-24 16:30+01' + ok 27 - correct parse_datetime output seconds for '2012-12-24 16:30+01 + ok 28 - strftime + ok 29 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 30 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 31 - correct parse_datetime return value for '2012-12-24 14:30-01:00' + ok 32 - correct parse_datetime output seconds for '2012-12-24 14:30-01:00 + ok 33 - strftime + ok 34 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 35 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 36 - correct parse_datetime return value for '2012-12-24 14:30-0100' + ok 37 - correct parse_datetime output seconds for '2012-12-24 14:30-0100 + ok 38 - strftime + ok 39 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 40 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 41 - correct parse_datetime return value for '2012-12-24 14:30-01' + ok 42 - correct parse_datetime output seconds for '2012-12-24 14:30-01 + ok 43 - strftime + ok 44 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 45 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 46 - correct parse_datetime return value for '2012-12-24 15:30:00Z' + ok 47 - correct parse_datetime output seconds for '2012-12-24 15:30:00Z + ok 48 - strftime + ok 49 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 50 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 51 - correct parse_datetime return value for '2012-12-24 15:30:00z' + ok 52 - correct parse_datetime output seconds for '2012-12-24 15:30:00z + ok 53 - strftime + ok 54 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 55 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 56 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 57 - correct parse_datetime output seconds for '2012-12-24 15:30:00 + ok 58 - strftime + ok 59 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 60 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 61 - correct parse_datetime return value for '2012-12-24 16:30:00+01:00' + ok 62 - correct parse_datetime output seconds for '2012-12-24 16:30:00+01:00 + ok 63 - strftime + ok 64 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 65 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 66 - correct parse_datetime return value for '2012-12-24 16:30:00+0100' + ok 67 - correct parse_datetime output seconds for '2012-12-24 16:30:00+0100 + ok 68 - strftime + ok 69 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 70 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 71 - correct parse_datetime return value for '2012-12-24 14:30:00-01:00' + ok 72 - correct parse_datetime output seconds for '2012-12-24 14:30:00-01:00 + ok 73 - strftime + ok 74 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 75 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 76 - correct parse_datetime return value for '2012-12-24 14:30:00-0100' + ok 77 - correct parse_datetime output seconds for '2012-12-24 14:30:00-0100 + ok 78 - strftime + ok 79 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 80 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 81 - correct parse_datetime return value for '2012-12-24 15:30:00.123456Z' + ok 82 - correct parse_datetime output seconds for '2012-12-24 15:30:00.123456Z + ok 83 - strftime + ok 84 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 85 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 86 - correct parse_datetime return value for '2012-12-24 15:30:00.123456z' + ok 87 - correct parse_datetime output seconds for '2012-12-24 15:30:00.123456z + ok 88 - strftime + ok 89 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 90 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 91 - correct parse_datetime return value for '2012-12-24 15:30:00.123456' + ok 92 - correct parse_datetime output seconds for '2012-12-24 15:30:00.123456 + ok 93 - strftime + ok 94 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 95 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 96 - correct parse_datetime return value for '2012-12-24 16:30:00.123456+01:00' + ok 97 - correct parse_datetime output seconds for '2012-12-24 16:30:00.123456+01:00 + ok 98 - strftime + ok 99 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 100 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 101 - correct parse_datetime return value for '2012-12-24 16:30:00.123456+01' + ok 102 - correct parse_datetime output seconds for '2012-12-24 16:30:00.123456+01 + ok 103 - strftime + ok 104 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 105 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 106 - correct parse_datetime return value for '2012-12-24 14:30:00.123456-01:00' + ok 107 - correct parse_datetime output seconds for '2012-12-24 14:30:00.123456-01:00 + ok 108 - strftime + ok 109 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 110 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 111 - correct parse_datetime return value for '2012-12-24 14:30:00.123456-01' + ok 112 - correct parse_datetime output seconds for '2012-12-24 14:30:00.123456-01 + ok 113 - strftime + ok 114 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 115 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 116 - correct parse_datetime return value for '2012-12-24t15:30Z' + ok 117 - correct parse_datetime output seconds for '2012-12-24t15:30Z + ok 118 - strftime + ok 119 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 120 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 121 - correct parse_datetime return value for '2012-12-24t15:30z' + ok 122 - correct parse_datetime output seconds for '2012-12-24t15:30z + ok 123 - strftime + ok 124 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 125 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 126 - correct parse_datetime return value for '2012-12-24t15:30' + ok 127 - correct parse_datetime output seconds for '2012-12-24t15:30 + ok 128 - strftime + ok 129 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 130 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 131 - correct parse_datetime return value for '2012-12-24t16:30+01:00' + ok 132 - correct parse_datetime output seconds for '2012-12-24t16:30+01:00 + ok 133 - strftime + ok 134 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 135 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 136 - correct parse_datetime return value for '2012-12-24t16:30+0100' + ok 137 - correct parse_datetime output seconds for '2012-12-24t16:30+0100 + ok 138 - strftime + ok 139 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 140 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 141 - correct parse_datetime return value for '2012-12-24t14:30-01:00' + ok 142 - correct parse_datetime output seconds for '2012-12-24t14:30-01:00 + ok 143 - strftime + ok 144 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 145 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 146 - correct parse_datetime return value for '2012-12-24t14:30-0100' + ok 147 - correct parse_datetime output seconds for '2012-12-24t14:30-0100 + ok 148 - strftime + ok 149 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 150 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 151 - correct parse_datetime return value for '2012-12-24t15:30:00Z' + ok 152 - correct parse_datetime output seconds for '2012-12-24t15:30:00Z + ok 153 - strftime + ok 154 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 155 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 156 - correct parse_datetime return value for '2012-12-24t15:30:00z' + ok 157 - correct parse_datetime output seconds for '2012-12-24t15:30:00z + ok 158 - strftime + ok 159 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 160 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 161 - correct parse_datetime return value for '2012-12-24t15:30:00' + ok 162 - correct parse_datetime output seconds for '2012-12-24t15:30:00 + ok 163 - strftime + ok 164 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 165 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 166 - correct parse_datetime return value for '2012-12-24t16:30:00+01:00' + ok 167 - correct parse_datetime output seconds for '2012-12-24t16:30:00+01:00 + ok 168 - strftime + ok 169 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 170 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 171 - correct parse_datetime return value for '2012-12-24t16:30:00+0100' + ok 172 - correct parse_datetime output seconds for '2012-12-24t16:30:00+0100 + ok 173 - strftime + ok 174 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 175 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 176 - correct parse_datetime return value for '2012-12-24t14:30:00-01:00' + ok 177 - correct parse_datetime output seconds for '2012-12-24t14:30:00-01:00 + ok 178 - strftime + ok 179 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 180 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 181 - correct parse_datetime return value for '2012-12-24t14:30:00-0100' + ok 182 - correct parse_datetime output seconds for '2012-12-24t14:30:00-0100 + ok 183 - strftime + ok 184 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 185 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 186 - correct parse_datetime return value for '2012-12-24t15:30:00.123456Z' + ok 187 - correct parse_datetime output seconds for '2012-12-24t15:30:00.123456Z + ok 188 - strftime + ok 189 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 190 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 191 - correct parse_datetime return value for '2012-12-24t15:30:00.123456z' + ok 192 - correct parse_datetime output seconds for '2012-12-24t15:30:00.123456z + ok 193 - strftime + ok 194 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 195 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 196 - correct parse_datetime return value for '2012-12-24t16:30:00.123456+01:00' + ok 197 - correct parse_datetime output seconds for '2012-12-24t16:30:00.123456+01:00 + ok 198 - strftime + ok 199 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 200 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 201 - correct parse_datetime return value for '2012-12-24t14:30:00.123456-01:00' + ok 202 - correct parse_datetime output seconds for '2012-12-24t14:30:00.123456-01:00 + ok 203 - strftime + ok 204 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 205 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 206 - correct parse_datetime return value for '2012-12-24 16:30 +01:00' + ok 207 - correct parse_datetime output seconds for '2012-12-24 16:30 +01:00 + ok 208 - strftime + ok 209 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 210 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 211 - correct parse_datetime return value for '2012-12-24 14:30 -01:00' + ok 212 - correct parse_datetime output seconds for '2012-12-24 14:30 -01:00 + ok 213 - strftime + ok 214 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 215 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 216 - correct parse_datetime return value for '2012-12-24 15:30 UTC' + ok 217 - correct parse_datetime output seconds for '2012-12-24 15:30 UTC + ok 218 - strftime + ok 219 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 220 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 221 - correct parse_datetime return value for '2012-12-24 16:30 UTC+1' + ok 222 - correct parse_datetime output seconds for '2012-12-24 16:30 UTC+1 + ok 223 - strftime + ok 224 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 225 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 226 - correct parse_datetime return value for '2012-12-24 16:30 UTC+01' + ok 227 - correct parse_datetime output seconds for '2012-12-24 16:30 UTC+01 + ok 228 - strftime + ok 229 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 230 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 231 - correct parse_datetime return value for '2012-12-24 16:30 UTC+0100' + ok 232 - correct parse_datetime output seconds for '2012-12-24 16:30 UTC+0100 + ok 233 - strftime + ok 234 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 235 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 236 - correct parse_datetime return value for '2012-12-24 16:30 UTC+01:00' + ok 237 - correct parse_datetime output seconds for '2012-12-24 16:30 UTC+01:00 + ok 238 - strftime + ok 239 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 240 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 241 - correct parse_datetime return value for '2012-12-24 14:30 UTC-1' + ok 242 - correct parse_datetime output seconds for '2012-12-24 14:30 UTC-1 + ok 243 - strftime + ok 244 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 245 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 246 - correct parse_datetime return value for '2012-12-24 14:30 UTC-01' + ok 247 - correct parse_datetime output seconds for '2012-12-24 14:30 UTC-01 + ok 248 - strftime + ok 249 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 250 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 251 - correct parse_datetime return value for '2012-12-24 14:30 UTC-01:00' + ok 252 - correct parse_datetime output seconds for '2012-12-24 14:30 UTC-01:00 + ok 253 - strftime + ok 254 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 255 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 256 - correct parse_datetime return value for '2012-12-24 14:30 UTC-0100' + ok 257 - correct parse_datetime output seconds for '2012-12-24 14:30 UTC-0100 + ok 258 - strftime + ok 259 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 260 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 261 - correct parse_datetime return value for '2012-12-24 15:30 GMT' + ok 262 - correct parse_datetime output seconds for '2012-12-24 15:30 GMT + ok 263 - strftime + ok 264 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 265 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 266 - correct parse_datetime return value for '2012-12-24 16:30 GMT+1' + ok 267 - correct parse_datetime output seconds for '2012-12-24 16:30 GMT+1 + ok 268 - strftime + ok 269 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 270 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 271 - correct parse_datetime return value for '2012-12-24 16:30 GMT+01' + ok 272 - correct parse_datetime output seconds for '2012-12-24 16:30 GMT+01 + ok 273 - strftime + ok 274 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 275 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 276 - correct parse_datetime return value for '2012-12-24 16:30 GMT+0100' + ok 277 - correct parse_datetime output seconds for '2012-12-24 16:30 GMT+0100 + ok 278 - strftime + ok 279 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 280 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 281 - correct parse_datetime return value for '2012-12-24 16:30 GMT+01:00' + ok 282 - correct parse_datetime output seconds for '2012-12-24 16:30 GMT+01:00 + ok 283 - strftime + ok 284 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 285 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 286 - correct parse_datetime return value for '2012-12-24 14:30 GMT-1' + ok 287 - correct parse_datetime output seconds for '2012-12-24 14:30 GMT-1 + ok 288 - strftime + ok 289 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 290 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 291 - correct parse_datetime return value for '2012-12-24 14:30 GMT-01' + ok 292 - correct parse_datetime output seconds for '2012-12-24 14:30 GMT-01 + ok 293 - strftime + ok 294 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 295 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 296 - correct parse_datetime return value for '2012-12-24 14:30 GMT-01:00' + ok 297 - correct parse_datetime output seconds for '2012-12-24 14:30 GMT-01:00 + ok 298 - strftime + ok 299 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 300 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 301 - correct parse_datetime return value for '2012-12-24 14:30 GMT-0100' + ok 302 - correct parse_datetime output seconds for '2012-12-24 14:30 GMT-0100 + ok 303 - strftime + ok 304 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 305 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 306 - correct parse_datetime return value for '2012-12-24 14:30 -01:00' + ok 307 - correct parse_datetime output seconds for '2012-12-24 14:30 -01:00 + ok 308 - strftime + ok 309 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 310 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 311 - correct parse_datetime return value for '2012-12-24 16:30:00 +01:00' + ok 312 - correct parse_datetime output seconds for '2012-12-24 16:30:00 +01:00 + ok 313 - strftime + ok 314 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 315 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 316 - correct parse_datetime return value for '2012-12-24 14:30:00 -01:00' + ok 317 - correct parse_datetime output seconds for '2012-12-24 14:30:00 -01:00 + ok 318 - strftime + ok 319 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 320 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 321 - correct parse_datetime return value for '2012-12-24 16:30:00.123456 +01:00' + ok 322 - correct parse_datetime output seconds for '2012-12-24 16:30:00.123456 +01:00 + ok 323 - strftime + ok 324 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 325 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 326 - correct parse_datetime return value for '2012-12-24 14:30:00.123456 -01:00' + ok 327 - correct parse_datetime output seconds for '2012-12-24 14:30:00.123456 -01:00 + ok 328 - strftime + ok 329 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 330 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 331 - correct parse_datetime return value for '2012-12-24 15:30:00.123456 -00:00' + ok 332 - correct parse_datetime output seconds for '2012-12-24 15:30:00.123456 -00:00 + ok 333 - strftime + ok 334 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 335 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 336 - correct parse_datetime return value for '20121224T1630+01:00' + ok 337 - correct parse_datetime output seconds for '20121224T1630+01:00 + ok 338 - strftime + ok 339 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 340 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 341 - correct parse_datetime return value for '2012-12-24T1630+01:00' + ok 342 - correct parse_datetime output seconds for '2012-12-24T1630+01:00 + ok 343 - strftime + ok 344 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 345 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 346 - correct parse_datetime return value for '20121224T16:30+01' + ok 347 - correct parse_datetime output seconds for '20121224T16:30+01 + ok 348 - strftime + ok 349 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 350 - reversible seconds via strftime for '2012-12-24 15:30:00 + ok 351 - correct parse_datetime return value for '20121224T16:30 +01' + ok 352 - correct parse_datetime output seconds for '20121224T16:30 +01 + ok 353 - strftime + ok 354 - correct parse_datetime return value for '2012-12-24 15:30:00' + ok 355 - reversible seconds via strftime for '2012-12-24 15:30:00 +ok 1 - subtests diff --git a/third_party/c-dt b/third_party/c-dt new file mode 160000 index 000000000..cbb3fc27c --- /dev/null +++ b/third_party/c-dt @@ -0,0 +1 @@ +Subproject commit cbb3fc27c104aa7703b01a4108ce7871e1a28a1c -- 2.29.2