diff --git a/cmake/BuildCDT.cmake b/cmake/BuildCDT.cmake index 80b26c64a12ab0086387c35760046ea7f659c522..8c89e8a117d58e01ec6ec928ba313a94d52e0a2c 100644 --- a/cmake/BuildCDT.cmake +++ b/cmake/BuildCDT.cmake @@ -5,6 +5,9 @@ macro(libccdt_build) file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/third_party/c-dt/build/) add_subdirectory(${PROJECT_SOURCE_DIR}/third_party/c-dt ${CMAKE_CURRENT_BINARY_DIR}/third_party/c-dt/build/) - set_target_properties(cdt PROPERTIES COMPILE_FLAGS "-DDT_NAMESPACE=tnt_") + set_target_properties(cdt + PROPERTIES COMPILE_FLAGS + "-DDT_NAMESPACE=tnt_ -DDT_PARSE_ISO_YEAR0 -DDT_PARSE_ISO_TNT" + ) add_definitions("-DDT_NAMESPACE=tnt_") endmacro() diff --git a/extra/exports b/extra/exports index be61fb7c1aef5653cd19bac91784c89e0e2dfb4a..efbe17eef29ca2c76c9d85cdfdd2dec9dd05a008 100644 --- a/extra/exports +++ b/extra/exports @@ -430,6 +430,7 @@ tnt_dt_dow tnt_dt_doy tnt_dt_from_rdn tnt_dt_from_ymd +tnt_dt_from_ymd_checked tnt_dt_month tnt_dt_parse_iso_date tnt_dt_parse_iso_time diff --git a/src/lib/core/datetime.c b/src/lib/core/datetime.c index 9df22a690d1624a61f60e1241e3438f593e2451c..0360fe7fc0db1f01a087a3023c532c4308748bcc 100644 --- a/src/lib/core/datetime.c +++ b/src/lib/core/datetime.c @@ -7,8 +7,10 @@ #include <assert.h> #include <limits.h> #include <string.h> +#include <stdbool.h> #include <time.h> +#define DT_PARSE_ISO_TNT #include "c-dt/dt.h" #include "datetime.h" #include "trivia/util.h" @@ -71,7 +73,7 @@ datetime_strftime(const struct datetime *date, char *buf, size_t len, /** * Create datetime structure using given tnt_tm fieldsÑŽ */ -static void +static bool tm_to_datetime(struct tnt_tm *tm, struct datetime *date) { assert(tm != NULL); @@ -91,9 +93,12 @@ tm_to_datetime(struct tnt_tm *tm, struct datetime *date) dt = ((wday - 4) % 7) + DT_EPOCH_1970_OFFSET; } } else { - assert(mday >= 0 && mday < 32); + if (mday == 0) + mday = 1; + assert(mday >= 1 && mday <= 31); assert(mon >= 0 && mon <= 11); - dt = dt_from_ymd(year + 1900, mon + 1, mday); + if (dt_from_ymd_checked(year + 1900, mon + 1, mday, &dt) == false) + return false; } int64_t local_secs = (int64_t)dt * SECS_PER_DAY - SECS_EPOCH_1970_OFFSET; @@ -102,6 +107,7 @@ tm_to_datetime(struct tnt_tm *tm, struct datetime *date) date->nsec = tm->tm_nsec; date->tzindex = 0; date->tzoffset = tm->tm_gmtoff / 60; + return true; } size_t @@ -114,7 +120,8 @@ datetime_strptime(struct datetime *date, const char *buf, const char *fmt) char *ret = tnt_strptime(buf, fmt, &t); if (ret == NULL) return 0; - tm_to_datetime(&t, date); + if (tm_to_datetime(&t, date) == false) + return 0; return ret - buf; } @@ -198,30 +205,37 @@ datetime_parse_full(struct datetime *date, const char *str, size_t len, n = dt_parse_iso_date(str, len, &dt); if (n == 0) return 0; - if (n == len) + + str += n; + len -= n; + if (len <= 0) goto exit; - c = str[n++]; + c = *str++; if (c != 'T' && c != 't' && c != ' ') return 0; - - str += n; - len -= n; + len--; + if (len <= 0) + goto exit; n = dt_parse_iso_time(str, len, &sec_of_day, &nanosecond); if (n == 0) return 0; - if (n == len) - goto exit; - - if (str[n] == ' ') - n++; str += n; len -= n; + if (len <= 0) + goto exit; + + if (*str == ' ') { + str++; + len--; + } + if (len <= 0) + goto exit; n = dt_parse_iso_zone_lenient(str, len, &offset); - if (n == 0 || n != len) + if (n == 0) return 0; str += n; diff --git a/src/lib/tzcode/strptime.c b/src/lib/tzcode/strptime.c index 37656d225f04d858d2e551fe0d078f4293a4341d..88939cae332b98f7f177ed058dce8048d28fe899 100644 --- a/src/lib/tzcode/strptime.c +++ b/src/lib/tzcode/strptime.c @@ -83,6 +83,23 @@ first_wday_of(int year) ((year % 100) / 4) + (isleap(year) ? 6 : 0) + 1) % 7; } +#define NUM_(N, buf) \ + ({ size_t _len = N; \ + long val = 0; \ + long sign = +1; \ + if ('-' == *buf) { \ + buf++; \ + sign = -1; \ + } \ + for (; _len > 0 && *buf != 0 && is_digit((u_char)*buf); \ + buf++, _len--) \ + val = val * 10 + (*buf - '0'); \ + sign * val; \ + }) + +#define NUM2(buf) NUM_(2, buf) +#define NUM3(buf) NUM_(3, buf) + char * tnt_strptime(const char *__restrict buf, const char *__restrict fmt, struct tnt_tm *__restrict tm) @@ -136,13 +153,7 @@ tnt_strptime(const char *__restrict buf, const char *__restrict fmt, return NULL; /* XXX This will break for 3-digit centuries. */ - len = 2; - for (i = 0; len && *buf != 0 && is_digit((u_char)*buf); - buf++) { - i *= 10; - i += *buf - '0'; - len--; - } + i = NUM2(buf); century = i; flags |= FLAG_YEAR; @@ -224,13 +235,7 @@ tnt_strptime(const char *__restrict buf, const char *__restrict fmt, if (!is_digit((u_char)*buf)) return NULL; - len = 3; - for (i = 0; len && *buf != 0 && is_digit((u_char)*buf); - buf++) { - i *= 10; - i += *buf - '0'; - len--; - } + i = NUM3(buf); if (i < 1 || i > 366) return NULL; @@ -283,13 +288,7 @@ tnt_strptime(const char *__restrict buf, const char *__restrict fmt, if (!is_digit((u_char)*buf)) return NULL; - len = 2; - for (i = 0; len && *buf != 0 && is_digit((u_char)*buf); - buf++) { - i *= 10; - i += *buf - '0'; - len--; - } + i = NUM2(buf); if (c == 'M') { if (i > 59) @@ -325,12 +324,7 @@ tnt_strptime(const char *__restrict buf, const char *__restrict fmt, if (!is_digit((u_char)*buf)) return NULL; - for (i = 0; len && *buf != 0 && is_digit((u_char)*buf); - buf++) { - i *= 10; - i += *buf - '0'; - len--; - } + i = NUM_(len, buf); if (c == 'H' || c == 'k') { if (i > 23) return NULL; @@ -397,13 +391,7 @@ tnt_strptime(const char *__restrict buf, const char *__restrict fmt, if (!is_digit((u_char)*buf)) return NULL; - len = 2; - for (i = 0; len && *buf != 0 && is_digit((u_char)*buf); - buf++) { - i *= 10; - i += *buf - '0'; - len--; - } + i = NUM2(buf); if (i > 53) return NULL; @@ -452,13 +440,7 @@ tnt_strptime(const char *__restrict buf, const char *__restrict fmt, if (!is_digit((u_char)*buf)) return NULL; - len = 2; - for (i = 0; len && *buf != 0 && is_digit((u_char)*buf); - buf++) { - i *= 10; - i += *buf - '0'; - len--; - } + i = NUM2(buf); if (i == 0 || i > 31) return NULL; @@ -514,13 +496,7 @@ tnt_strptime(const char *__restrict buf, const char *__restrict fmt, if (!is_digit((u_char)*buf)) return NULL; - len = 2; - for (i = 0; len && *buf != 0 && is_digit((u_char)*buf); - buf++) { - i *= 10; - i += *buf - '0'; - len--; - } + i = NUM2(buf); if (i < 1 || i > 12) return NULL; @@ -549,16 +525,11 @@ tnt_strptime(const char *__restrict buf, const char *__restrict fmt, if (*buf == 0 || isspace((u_char)*buf)) break; - if (!is_digit((u_char)*buf)) + if (*buf != '-' && !is_digit((u_char)*buf)) return NULL; - len = (c == 'Y' || c == 'G') ? 4 : 2; - for (i = 0; len && *buf != 0 && is_digit((u_char)*buf); - buf++) { - i *= 10; - i += *buf - '0'; - len--; - } + len = (c == 'Y' || c == 'G') ? 7 : 2; + i = NUM_(len, buf); if (c == 'Y' || c == 'G') century = i / 100; year = i % 100; @@ -579,7 +550,8 @@ tnt_strptime(const char *__restrict buf, const char *__restrict fmt, zonestr[cp - buf] = '\0'; tzset(); if (0 == strcmp(zonestr, "GMT") || - 0 == strcmp(zonestr, "UTC")) { + 0 == strcmp(zonestr, "UTC") || + 0 == strcmp(zonestr, "Z")) { tm->tm_gmtoff = 0; } else if (0 == strcmp(zonestr, tzname[0])) { tm->tm_isdst = 0; @@ -593,6 +565,16 @@ tnt_strptime(const char *__restrict buf, const char *__restrict fmt, } break; case 'z': { + + /* Even for %z format specifier we better to accept + * Zulu timezone as default Tarantool shortcut for + * +00:00 offset. + */ + if (*buf == 'Z') { + buf++; + tm->tm_gmtoff = 0; + break; + } int sign = 1; if (*buf != '+') { diff --git a/src/lua/datetime.lua b/src/lua/datetime.lua index 8bb21e440a5dfd46487c2b05cd6523d3cbc530e5..7b494fe92bd666b103a7820aa2f9718f848501d3 100644 --- a/src/lua/datetime.lua +++ b/src/lua/datetime.lua @@ -922,7 +922,7 @@ local function datetime_parse_full(str, tzoffset) if len == 0 then error(("could not parse '%s'"):format(str)) end - return date, len + return date, tonumber(len) end --[[ @@ -935,7 +935,7 @@ local function datetime_parse_format(str, fmt) if len == 0 then error(("could not parse '%s' using '%s' format"):format(str, fmt)) end - return date, len + return date, tonumber(len) end local function datetime_parse_from(str, obj) diff --git a/test/app-tap/datetime.test.lua b/test/app-tap/datetime.test.lua index ee40a3081ca2831d08147db04b1f18c6591c52d4..6d60aa5dd13300fae4a77b1e418232a278609471 100755 --- a/test/app-tap/datetime.test.lua +++ b/test/app-tap/datetime.test.lua @@ -11,7 +11,7 @@ local ffi = require('ffi') --]] if jit.arch == 'arm64' then jit.off() end -test:plan(31) +test:plan(32) -- minimum supported date - -5879610-06-22 local MIN_DATE_YEAR = -5879610 @@ -96,17 +96,16 @@ local function invalid_date_fmt_error(str) return ('invalid date format %s'):format(str) 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+: ", "") + local err_tail = err and err:gsub("^.+:%d+: ", "") or '' 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+: ", "") + local err_tail = err and err:gsub("^.+:%d+: ", "") or '' return test:like(not ok and err_tail, error_msg, ('"%s" received, "%s" expected'):format(err_tail, error_msg)) end @@ -498,6 +497,28 @@ local function create_date_string(date) return ('%04d-%02d-%02dT%02d:%02d:%02dZ'):format(year, month, day, hour, min, sec) end +test:test("Check parsing of full supported years range", function(test) + test:plan(63) + local valid_years = { + -5879610, -5879000, -5800000, -2e6, -1e5, -1e4, -9999, -2000, -1000, + 0, 1, 1000, 1900, 1970, 2000, 9999, + 1e4, 1e6, 2e6, 5e6, 5879611 + } + local fmt = '%FT%T%z' + for _, y in ipairs(valid_years) do + local txt = ('%04d-06-22'):format(y) + local dt = date.parse(txt) + test:isnt(dt, nil, dt) + local out_txt = tostring(dt) + local out_dt = date.parse(out_txt) + test:is(dt, out_dt, ('default parse of %s (%s == %s)'): + format(out_txt, dt, out_dt)) + local fmt_dt = date.parse(out_txt, {format = fmt}) + test:is(dt, fmt_dt, ('parse via format %s (%s == %s)'): + format(fmt, dt, fmt_dt)) + end +end) + local function couldnt_parse(txt) return ("could not parse '%s'"):format(txt) end @@ -1307,7 +1328,7 @@ test:test("Matrix of allowed time and interval subtractions", function(test) end) test:test("Parse iso8601 date - valid strings", function(test) - test:plan(32) + test:plan(54) local good = { {2012, 12, 24, "20121224", 8 }, {2012, 12, 24, "20121224 Foo bar", 8 }, @@ -1325,6 +1346,17 @@ test:test("Parse iso8601 date - valid strings", function(test) { 1, 1, 1, "0001-W01-1", 10 }, { 1, 1, 1, "0001-01-01", 10 }, { 1, 1, 1, "0001-001", 8 }, + {9999, 12, 31, "9999-12-31", 10 }, + { 0, 1, 1, "0000-Q1-01", 10 }, + { 0, 1, 3, "0000-W01-1", 10 }, + { 0, 1, 1, "0000-01-01", 10 }, + { 0, 1, 1, "0000-001", 8 }, + {-200, 12, 31, "-200-12-31", 10 }, + {-1000,12, 31, "-1000-12-31", 11 }, + {-10000,12,31, "-10000-12-31", 12 }, + {-5879610,6,22,"-5879610-06-22", 14 }, + {10000, 1, 1, "10000-01-01", 11 }, + {5879611,7, 1, "5879611-07-01", 13 }, } for _, value in ipairs(good) do @@ -1339,7 +1371,7 @@ test:test("Parse iso8601 date - valid strings", function(test) end) test:test("Parse iso8601 date - invalid strings", function(test) - test:plan(31) + test:plan(29) local bad = { "20121232" , -- Invalid day of month "2012-12-310", -- Invalid day of month @@ -1368,10 +1400,8 @@ test:test("Parse iso8601 date - invalid strings", function(test) "2012U1234" , -- Invalid "2012-1234" , -- Invalid "2012-X1234" , -- Invalid - "0000-Q1-01" , -- Year less than 0001 - "0000-W01-1" , -- Year less than 0001 - "0000-01-01" , -- Year less than 0001 - "0000-001" , -- Year less than 0001 + "-5879611-01-01", -- Year less than 5879610-06-22 + "5879612-01-01", -- Year greater than 5879611-07-11 } for _, str in ipairs(bad) do diff --git a/test/unit/datetime.c b/test/unit/datetime.c index b183be67457af7636c941db1e43be74ff05b12a6..fb2919caaa56a61b1f0c7e9eaf1487b366864513 100644 --- a/test/unit/datetime.c +++ b/test/unit/datetime.c @@ -102,7 +102,7 @@ datetime_test(void) size_t index; struct datetime date_expected; - plan(355); + plan(497); datetime_parse_full(&date_expected, sample, sizeof(sample) - 1, 0); for (index = 0; index < lengthof(tests); index++) { @@ -121,15 +121,20 @@ datetime_test(void) * time fields */ static char buff[DT_TO_STRING_BUFSIZE]; - struct tnt_tm tm = { .tm_sec = 0 }; len = datetime_strftime(&date, buff, sizeof(buff), "%F %T%z"); ok(len > 0, "strftime"); + struct datetime date_strp; + len = datetime_strptime(&date_strp, buff, "%F %T%z"); + is(len > 0, true, "correct parse_strptime return value " + "for '%s'", buff); + is(date.epoch, date_strp.epoch, + "reversible seconds via datetime_strptime for '%s'", buff); struct datetime date_parsed; len = datetime_parse_full(&date_parsed, buff, len, 0); - is(len > 0, true, "correct parse_datetime return value " + is(len > 0, true, "correct datetime_parse_full return value " "for '%s'", buff); is(date.epoch, date_parsed.epoch, - "reversible seconds via strftime for '%s'", buff); + "reversible seconds via datetime_parse_full for '%s'", buff); } check_plan(); } @@ -158,10 +163,12 @@ tostring_datetime_test(void) {"1973-11-29T21:33:09Z", 123456789, 0, 0}, {"2013-10-28T17:51:56Z", 1382982716, 0, 0}, {"9999-12-31T23:59:59Z", 253402300799, 0, 0}, + {"10000-01-01T00:00:00Z", 253402300800, 0, 0}, + {"5879611-07-11T00:00:00Z", 185480451417600, 0, 0}, }; size_t index; - plan(15); + plan(17); for (index = 0; index < lengthof(tests); index++) { struct datetime date = { tests[index].secs, @@ -187,7 +194,7 @@ _dt_to_epoch(dt_t dt) static void parse_date_test(void) { - plan(59); + plan(154); static struct { int64_t epoch; @@ -210,6 +217,19 @@ parse_date_test(void) { -62135596800, "0001-W01-1", 10 }, { -62135596800, "0001-01-01", 10 }, { -62135596800, "0001-001", 8 }, + + /* Tarantool extra ranges */ + { -62167219200, "0000-01-01", 10 }, + { -62167046400, "0000-W01-1", 10 }, + { -62167219200, "0000-Q1-01", 10 }, + { -68447116800, "-200-12-31", 10 }, + { -377705203200, "-10000-12-31", 12 }, + { -185604722870400, "-5879610-06-22", 14 }, + { -185604706627200, "-5879610W521", 12 }, + { 253402214400, "9999-12-31", 10 }, + { 253402300800, "10000-01-01", 11 }, + { 185480451417600, "5879611-07-11", 13 }, + { 185480434915200, "5879611Q101", 11 }, }; size_t index; @@ -220,10 +240,10 @@ parse_date_test(void) int64_t expected_epoch = valid_tests[index].epoch; size_t len = tnt_dt_parse_iso_date(str, expected_len, &dt); int64_t epoch = _dt_to_epoch(dt); - is(len, expected_len, "string '%s' parse failed, len %lu", str, + is(len, expected_len, "string '%s' parse, len %lu", str, len); - is(epoch, expected_epoch, - "string '%s' parse failed, epoch %" PRId64, str, epoch); + is(epoch, expected_epoch, "string '%s' parse, epoch %" PRId64, + str, epoch); } static const char *const invalid_tests[] = { @@ -262,6 +282,98 @@ parse_date_test(void) is(len, 0, "expected failure of string '%s' parse, len %lu", str, len); } + + /* check strptime formats */ + const struct { + const char *fmt; + const char *text; + } format_tests[] = { + { "%A", "Thursday" }, + { "%a", "Thu" }, + { "%B", "January" }, + { "%b", "Jan" }, + { "%h", "Jan" }, + { "%c", "Thu Jan 1 03:00:00 1970" }, + { "%D", "01/01/70" }, + { "%m/%d/%y", "01/01/70" }, + { "%d", "01" }, + { "%Ec", "Thu Jan 1 03:00:00 1970" }, + { "%Ex", "01/01/70" }, + { "%EX", "03:00:00" }, + { "%Ey", "70" }, + { "%EY", "1970" }, + { "%Od", "01" }, + { "%OH", "03" }, + { "%OI", "03" }, + { "%Om", "01" }, + { "%OM", "00" }, + { "%OS", "00" }, + { "%Ou", "4" }, + { "%OU", "00" }, + { "%Ow", "4" }, + { "%OW", "00" }, + { "%Oy", "70" }, + { "%e", " 1" }, + { "%F", "1970-01-01" }, + { "%Y-%m-%d", "1970-01-01" }, + { "%H", "03" }, + { "%I", "03" }, + { "%j", "001" }, + { "%k", " 3" }, + { "%l", " 3" }, + { "%M", "00" }, + { "%m", "01" }, + { "%n", "\n" }, + { "%p", "AM" }, + { "%R", "03:00" }, + { "%H:%M", "03:00" }, + { "%r", "03:00:00 AM" }, + { "%I:%M:%S %p", "03:00:00 AM" }, + { "%S", "00" }, + { "%s", "10800" }, + { "%f", "125" }, + { "%T", "03:00:00" }, + { "%H:%M:%S", "03:00:00" }, + { "%t", "\t" }, + { "%U", "00" }, + { "%u", "4" }, + { "%G", "1970" }, + { "%g", "70" }, + { "%v", " 1-Jan-1970" }, + { "%e-%b-%Y", " 1-Jan-1970" }, + { "%W", "00" }, + { "%w", "4" }, + { "%X", "03:00:00" }, + { "%x", "01/01/70" }, + { "%y", "70" }, + { "%Y", "1970" }, + { "%z", "+0300" }, + { "%%", "%" }, + { "%Y-%m-%dT%H:%M:%S.%9f%z", "1970-01-01T03:00:00.125000000+0300" }, + { "%Y-%m-%dT%H:%M:%S.%f%z", "1970-01-01T03:00:00.125+0300" }, + { "%Y-%m-%dT%H:%M:%S.%f", "1970-01-01T03:00:00.125" }, + { "%FT%T.%f", "1970-01-01T03:00:00.125" }, + { "%FT%T.%f%z", "1970-01-01T03:00:00.125+0300" }, + { "%FT%T.%9f%z", "1970-01-01T03:00:00.125000000+0300" }, + { "%Y-%m-%d", "0000-01-01" }, + { "%Y-%m-%d", "0001-01-01" }, + { "%Y-%m-%d", "9999-01-01" }, + { "%Y-%m-%d", "10000-01-01" }, + { "%Y-%m-%d", "10000-01-01" }, + { "%Y-%m-%d", "5879611-07-11" }, + }; + + for (index = 0; index < lengthof(format_tests); index++) { + const char *fmt = format_tests[index].fmt; + const char *text = format_tests[index].text; + struct tnt_tm date = { .tm_epoch = 0}; + char *ptr = tnt_strptime(text, fmt, &date); + static char buff[DT_TO_STRING_BUFSIZE]; + tnt_strftime(buff, sizeof(buff), "%FT%T%z", &date); + isnt(ptr, NULL, "parse string '%s' using '%s' (result '%s')", + text, fmt, buff); + } + check_plan(); }