From 02aa8f514d5a8cae32af08925d6b9fccde1ea1ca Mon Sep 17 00:00:00 2001 From: Timur Safin <tsafin@tarantool.org> Date: Wed, 6 Oct 2021 19:38:21 +0300 Subject: [PATCH] datetime, lua: strptime-like parse format To parse date/time strings using format string we use `strptime()` implementation from FreeBSD, which is modified to use our `struct datetime` data structure. List of supported format has been extended to include `%f` which is flag used whenever you need to process nanoseconds part of datetime value. ``` tarantool> T = date.parse('Thu Jan 1 03:00:00 1970', {format = '%c'}) tarantool> T - 1970-01-01T03:00:00Z tarantool> T = date.parse('12/31/2020', {format = '%m/%d/%y'}) tarantool> T - 2020-12-31T00:00:00Z tarantool> T = date.parse('1970-01-01T03:00:00.125000000+0300', {format = '%FT%T.%f%z'}) tarantool> T - 1970-01-01T03:00:00.125+0300 ``` Part of #6731 NO_DOC=internal NO_CHANGELOG=internal --- extra/exports | 1 + src/lib/core/datetime.c | 50 +++ src/lib/core/datetime.h | 11 + src/lib/tzcode/CMakeLists.txt | 2 +- src/lib/tzcode/strftime.c | 57 +-- src/lib/tzcode/strptime.c | 720 +++++++++++++++++++++++++++++++++ src/lib/tzcode/timelocal.c | 91 +++++ src/lib/tzcode/timelocal.h | 65 +++ src/lib/tzcode/tzcode.h | 10 + src/lua/datetime.lua | 17 +- src/lua/tnt_datetime.c | 6 + test/app-tap/datetime.test.lua | 222 ++++++---- 12 files changed, 1109 insertions(+), 143 deletions(-) create mode 100644 src/lib/tzcode/strptime.c create mode 100644 src/lib/tzcode/timelocal.c create mode 100644 src/lib/tzcode/timelocal.h diff --git a/extra/exports b/extra/exports index aba536f81b..be61fb7c1a 100644 --- a/extra/exports +++ b/extra/exports @@ -418,6 +418,7 @@ title_update tnt_datetime_now tnt_datetime_parse_full tnt_datetime_strftime +tnt_datetime_strptime tnt_datetime_to_string tnt_datetime_unpack tnt_default_cert_dir_paths diff --git a/src/lib/core/datetime.c b/src/lib/core/datetime.c index ccf20f9d8a..9df22a690d 100644 --- a/src/lib/core/datetime.c +++ b/src/lib/core/datetime.c @@ -68,6 +68,56 @@ datetime_strftime(const struct datetime *date, char *buf, size_t len, return tnt_strftime(buf, len, fmt, &tm); } +/** + * Create datetime structure using given tnt_tm fieldsÑŽ + */ +static void +tm_to_datetime(struct tnt_tm *tm, struct datetime *date) +{ + assert(tm != NULL); + assert(date != NULL); + int year = tm->tm_year; + int mon = tm->tm_mon; + int mday = tm->tm_mday; + int yday = tm->tm_yday; + int wday = tm->tm_wday; + dt_t dt = 0; + + if ((year | mon | mday) == 0) { + if (yday != 0) { + dt = yday - 1 + DT_EPOCH_1970_OFFSET; + } else if (wday != 0) { + /* 1970-01-01 was Thursday */ + dt = ((wday - 4) % 7) + DT_EPOCH_1970_OFFSET; + } + } else { + assert(mday >= 0 && mday < 32); + assert(mon >= 0 && mon <= 11); + dt = dt_from_ymd(year + 1900, mon + 1, mday); + } + int64_t local_secs = + (int64_t)dt * SECS_PER_DAY - SECS_EPOCH_1970_OFFSET; + local_secs += tm->tm_hour * 3600 + tm->tm_min * 60 + tm->tm_sec; + date->epoch = local_secs - tm->tm_gmtoff; + date->nsec = tm->tm_nsec; + date->tzindex = 0; + date->tzoffset = tm->tm_gmtoff / 60; +} + +size_t +datetime_strptime(struct datetime *date, const char *buf, const char *fmt) +{ + assert(date != NULL); + assert(fmt != NULL); + assert(buf != NULL); + struct tnt_tm t = { .tm_epoch = 0 }; + char *ret = tnt_strptime(buf, fmt, &t); + if (ret == NULL) + return 0; + tm_to_datetime(&t, date); + return ret - buf; +} + void datetime_now(struct datetime *now) { diff --git a/src/lib/core/datetime.h b/src/lib/core/datetime.h index cf18278343..cad0c7f272 100644 --- a/src/lib/core/datetime.h +++ b/src/lib/core/datetime.h @@ -130,6 +130,17 @@ datetime_to_tm(const struct datetime *date, struct tnt_tm *tm); size_t datetime_parse_full(struct datetime *date, const char *str, size_t len, int32_t offset); +/** + * Parse buffer given format, and construct datetime value + * @param date output datetime value + * @param buf input text buffer (0-terminated) + * @param fmt format to use for parsing + * @retval Upon successful completion returns length of accepted + * prefix substring, 0 otherwise. + * @sa strptime() + */ +size_t +datetime_strptime(struct datetime *date, const char *buf, const char *fmt); #if defined(__cplusplus) } /* extern "C" */ diff --git a/src/lib/tzcode/CMakeLists.txt b/src/lib/tzcode/CMakeLists.txt index 979ec3d67c..2e6b00402d 100644 --- a/src/lib/tzcode/CMakeLists.txt +++ b/src/lib/tzcode/CMakeLists.txt @@ -1,2 +1,2 @@ -add_library(tzcode STATIC strftime.c) +add_library(tzcode STATIC strftime.c strptime.c timelocal.c) target_link_libraries(tzcode) diff --git a/src/lib/tzcode/strftime.c b/src/lib/tzcode/strftime.c index d07b9d0949..eb8bf28c35 100644 --- a/src/lib/tzcode/strftime.c +++ b/src/lib/tzcode/strftime.c @@ -36,68 +36,13 @@ #include "private.h" #include "trivia/util.h" #include "tzcode.h" +#include "timelocal.h" #include <assert.h> #include <locale.h> #include <stdbool.h> #include <stdio.h> -struct lc_time_T { - const char *mon[MONTHSPERYEAR]; - const char *month[MONTHSPERYEAR]; - const char *wday[DAYSPERWEEK]; - const char *weekday[DAYSPERWEEK]; - const char *X_fmt; - const char *x_fmt; - const char *c_fmt; - const char *am; - const char *pm; - const char *date_fmt; -}; - -#define Locale (&C_time_locale) - -static const struct lc_time_T C_time_locale = { - {"Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}, - {"January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December"}, - {"Sun", "Mon", "Tue", "Wed", - "Thu", "Fri", "Sat"}, - {"Sunday", "Monday", "Tuesday", "Wednesday", - "Thursday", "Friday", "Saturday"}, - - /* X_fmt */ - "%H:%M:%S", - - /* - ** x_fmt - ** C99 and later require this format. - ** Using just numbers (as here) makes Quakers happier; - ** it's also compatible with SVR4. - */ - "%m/%d/%y", - - /* - ** c_fmt - ** C99 and later require this format. - ** Previously this code used "%D %X", but we now conform to C99. - ** Note that - ** "%a %b %d %H:%M:%S %Y" - ** is used by Solaris 2.3. - */ - "%a %b %e %T %Y", - - /* am */ - "AM", - - /* pm */ - "PM", - - /* date_fmt */ - "%a %b %e %H:%M:%S %Z %Y" -}; - static int pow10[] = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000 }; diff --git a/src/lib/tzcode/strptime.c b/src/lib/tzcode/strptime.c new file mode 100644 index 0000000000..37656d225f --- /dev/null +++ b/src/lib/tzcode/strptime.c @@ -0,0 +1,720 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause-FreeBSD + * + * Copyright (c) 2014 Gary Mills + * Copyright 2011, Nexenta Systems, Inc. All rights reserved. + * Copyright (c) 1994 Powerdog Industries. All rights reserved. + * + * Copyright (c) 2011 The FreeBSD Foundation + * All rights reserved. + * Portions of this software were developed by David Chisnall + * under sponsorship from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY POWERDOG INDUSTRIES ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE POWERDOG INDUSTRIES BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation + * are those of the authors and should not be interpreted as representing + * official policies, either expressed or implied, of Powerdog Industries. + */ + +#include <sys/cdefs.h> + +#ifndef lint +#ifndef NOID +static char copyright[] __attribute__((unused)) = +"@(#) Copyright (c) 1994 Powerdog Industries. All rights reserved."; +static char sccsid[] __attribute__((unused)) = +"@(#)strptime.c 0.1 (Powerdog) 94/03/27"; +#endif /* !defined NOID */ +#endif /* not lint */ + +#include <assert.h> +#include <ctype.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +#include "private.h" +#include "timelocal.h" +#include "tzcode.h" + +enum flags { + FLAG_NONE = 1 << 0, + FLAG_YEAR = 1 << 1, + FLAG_MONTH = 1 << 2, + FLAG_YDAY = 1 << 3, + FLAG_MDAY = 1 << 4, + FLAG_WDAY = 1 << 5, + FLAG_EPOCH = 1 << 6, + FLAG_NSEC = 1 << 7, +}; + +/* + * Calculate the week day of the first day of a year. Valid for + * the Gregorian calendar, which began Sept 14, 1752 in the UK + * and its colonies. Ref: + * http://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week + */ + +static int +first_wday_of(int year) +{ + return ((2 * (3 - (year / 100) % 4)) + (year % 100) + + ((year % 100) / 4) + (isleap(year) ? 6 : 0) + 1) % 7; +} + +char * +tnt_strptime(const char *__restrict buf, const char *__restrict fmt, + struct tnt_tm *__restrict tm) +{ + char c; + int day_offset = -1, wday_offset; + int week_offset; + int i, len; + int Ealternative, Oalternative; + enum flags flags = FLAG_NONE; + int century = -1; + int year = -1; + const char *ptr = fmt; + + static int start_of_month[2][13] = { + { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 }, + { 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 } + }; + + while (*ptr != 0) { + c = *ptr++; + + if (c != '%') { + if (isspace((u_char)c)) + while (*buf != 0 && isspace((u_char)*buf)) + buf++; + else if (c != *buf++) + return NULL; + continue; + } + + Ealternative = 0; + Oalternative = 0; + label: + c = *ptr++; + switch (c) { + case '%': + if (*buf++ != '%') + return NULL; + break; + + case '+': + buf = tnt_strptime(buf, Locale->date_fmt, tm); + if (buf == NULL) + return NULL; + flags |= FLAG_WDAY | FLAG_MONTH | FLAG_MDAY | FLAG_YEAR; + break; + + case 'C': + if (!is_digit((u_char)*buf)) + 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--; + } + + century = i; + flags |= FLAG_YEAR; + + break; + + case 'c': + buf = tnt_strptime(buf, Locale->c_fmt, tm); + if (buf == NULL) + return NULL; + flags |= FLAG_WDAY | FLAG_MONTH | FLAG_MDAY | FLAG_YEAR; + break; + + case 'D': + buf = tnt_strptime(buf, "%m/%d/%y", tm); + if (buf == NULL) + return NULL; + flags |= FLAG_MONTH | FLAG_MDAY | FLAG_YEAR; + break; + + case 'E': + if (Ealternative || Oalternative) + break; + Ealternative++; + goto label; + + case 'O': + if (Ealternative || Oalternative) + break; + Oalternative++; + goto label; + + case 'v': + buf = tnt_strptime(buf, "%e-%b-%Y", tm); + if (buf == NULL) + return NULL; + flags |= FLAG_MONTH | FLAG_MDAY | FLAG_YEAR; + break; + + case 'F': + buf = tnt_strptime(buf, "%Y-%m-%d", tm); + if (buf == NULL) + return NULL; + flags |= FLAG_MONTH | FLAG_MDAY | FLAG_YEAR; + break; + + case 'R': + buf = tnt_strptime(buf, "%H:%M", tm); + if (buf == NULL) + return NULL; + break; + + case 'r': + buf = tnt_strptime(buf, Locale->ampm_fmt, tm); + if (buf == NULL) + return NULL; + break; + + case 'T': + buf = tnt_strptime(buf, "%H:%M:%S", tm); + if (buf == NULL) + return NULL; + break; + + case 'X': + buf = tnt_strptime(buf, Locale->X_fmt, tm); + if (buf == NULL) + return NULL; + break; + + case 'x': + buf = tnt_strptime(buf, Locale->x_fmt, tm); + if (buf == NULL) + return NULL; + flags |= FLAG_MONTH | FLAG_MDAY | FLAG_YEAR; + break; + + case 'j': + 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--; + } + if (i < 1 || i > 366) + return NULL; + + tm->tm_yday = i - 1; + flags |= FLAG_YDAY; + + break; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + for (; *ptr != 0 && is_digit((u_char)*ptr); ptr++) + ; + + c = *ptr++; + assert(c == 'f'); + /* fallthru */ + case 'f': + if (!is_digit((u_char)*buf)) + return NULL; + + len = 9; + for (i = 0; len && *buf != 0 && is_digit((u_char)*buf); + buf++) { + i *= 10; + i += *buf - '0'; + len--; + } + while (len) { + i *= 10; + len--; + } + tm->tm_nsec = i; + flags |= FLAG_NSEC; + + break; + + case 'M': + case 'S': + if (*buf == 0 || isspace((u_char)*buf)) + break; + + 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--; + } + + if (c == 'M') { + if (i > 59) + return NULL; + tm->tm_min = i; + } else { + if (i > 60) + return NULL; + tm->tm_sec = i; + } + + break; + + case 'H': + case 'I': + case 'k': + case 'l': + /* + * %k and %l specifiers are documented as being + * blank-padded. However, there is no harm in + * allowing zero-padding. + * + * XXX %k and %l specifiers may gobble one too many + * digits if used incorrectly. + */ + + len = 2; + if ((c == 'k' || c == 'l') && isblank((u_char)*buf)) { + buf++; + len = 1; + } + + 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--; + } + if (c == 'H' || c == 'k') { + if (i > 23) + return NULL; + } else if (i == 0 || i > 12) + return NULL; + + tm->tm_hour = i; + + break; + + case 'p': + /* + * XXX This is bogus if parsed before hour-related + * specifiers. + */ + if (tm->tm_hour > 12) + return NULL; + + len = strlen(Locale->am); + if (strncasecmp(buf, Locale->am, len) == 0) { + if (tm->tm_hour == 12) + tm->tm_hour = 0; + buf += len; + break; + } + + len = strlen(Locale->pm); + if (strncasecmp(buf, Locale->pm, len) == 0) { + if (tm->tm_hour != 12) + tm->tm_hour += 12; + buf += len; + break; + } + + return NULL; + + case 'A': + case 'a': + for (i = 0; i < DAYSPERWEEK; i++) { + len = strlen(Locale->weekday[i]); + if (strncasecmp(buf, Locale->weekday[i], len) == + 0) + break; + len = strlen(Locale->wday[i]); + if (strncasecmp(buf, Locale->wday[i], len) == 0) + break; + } + if (i == DAYSPERWEEK) + return NULL; + + buf += len; + tm->tm_wday = i; + flags |= FLAG_WDAY; + break; + + case 'U': + case 'W': + /* + * XXX This is bogus, as we can not assume any valid + * information present in the tm structure at this + * point to calculate a real value, so just check the + * range for now. + */ + 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--; + } + if (i > 53) + return NULL; + + if (c == 'U') + day_offset = TM_SUNDAY; + else + day_offset = TM_MONDAY; + + week_offset = i; + + break; + + case 'u': + case 'w': + if (!is_digit((u_char)*buf)) + return NULL; + + i = *buf++ - '0'; + if (i < 0 || i > 7 || (c == 'u' && i < 1) || + (c == 'w' && i > 6)) + return NULL; + + tm->tm_wday = i % 7; + flags |= FLAG_WDAY; + + break; + + case 'e': + /* + * With %e format, our strftime(3) adds a blank space + * before single digits. + */ + if (*buf != 0 && isspace((u_char)*buf)) + buf++; + /* FALLTHROUGH */ + case 'd': + /* + * The %e specifier was once explicitly documented as + * not being zero-padded but was later changed to + * equivalent to %d. There is no harm in allowing + * such padding. + * + * XXX The %e specifier may gobble one too many + * digits if used incorrectly. + */ + 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--; + } + if (i == 0 || i > 31) + return NULL; + + tm->tm_mday = i; + flags |= FLAG_MDAY; + + break; + + case 'B': + case 'b': + case 'h': + for (i = 0; i < MONTHSPERYEAR; i++) { + if (Oalternative) { + if (c == 'B') { + len = strlen( + Locale->alt_month[i]); + if (strncasecmp( + buf, + Locale->alt_month + [i], + len) == 0) + break; + } + } else { + len = strlen(Locale->month[i]); + if (strncasecmp(buf, Locale->month[i], + len) == 0) + break; + } + } + /* + * Try the abbreviated month name if the full name + * wasn't found and Oalternative was not requested. + */ + if (i == MONTHSPERYEAR && !Oalternative) { + for (i = 0; i < MONTHSPERYEAR; i++) { + len = strlen(Locale->mon[i]); + if (strncasecmp(buf, Locale->mon[i], + len) == 0) + break; + } + } + if (i == MONTHSPERYEAR) + return NULL; + + tm->tm_mon = i; + buf += len; + flags |= FLAG_MONTH; + + break; + + case 'm': + 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--; + } + if (i < 1 || i > 12) + return NULL; + + tm->tm_mon = i - 1; + flags |= FLAG_MONTH; + + break; + + case 's': { + char *cp; + long n; + + n = strtol(buf, &cp, 10); + if (n == 0) { + return NULL; + } + buf = cp; + tm->tm_epoch = n; + flags |= FLAG_EPOCH; + } break; + + case 'G': /* ISO 8601 year (four digits) */ + case 'g': /* ISO 8601 year (two digits) */ + case 'Y': + case 'y': + if (*buf == 0 || isspace((u_char)*buf)) + break; + + if (!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--; + } + if (c == 'Y' || c == 'G') + century = i / 100; + year = i % 100; + + flags |= FLAG_YEAR; + + break; + + case 'Z': { + const char *cp; + char *zonestr; + + for (cp = buf; *cp && isupper((u_char)*cp); ++cp) + /* empty */; + if (cp - buf) { + zonestr = alloca(cp - buf + 1); + strncpy(zonestr, buf, cp - buf); + zonestr[cp - buf] = '\0'; + tzset(); + if (0 == strcmp(zonestr, "GMT") || + 0 == strcmp(zonestr, "UTC")) { + tm->tm_gmtoff = 0; + } else if (0 == strcmp(zonestr, tzname[0])) { + tm->tm_isdst = 0; + } else if (0 == strcmp(zonestr, tzname[1])) { + tm->tm_isdst = 1; + } else { + return NULL; + } + buf += cp - buf; + } + } break; + + case 'z': { + int sign = 1; + + if (*buf != '+') { + if (*buf == '-') + sign = -1; + else + return NULL; + } + + buf++; + i = 0; + for (len = 4; len > 0; len--) { + if (is_digit((u_char)*buf)) { + i *= 10; + i += *buf - '0'; + buf++; + } else if (len == 2) { + i *= 100; + break; + } else + return NULL; + } + + if (i > 1400 || (sign == -1 && i > 1200) || + (i % 100) >= 60) + return NULL; + tm->tm_gmtoff = + sign * ((i / 100) * 3600 + i % 100 * 60); + } break; + + case 'n': + case 't': + while (isspace((u_char)*buf)) + buf++; + break; + + default: + return NULL; + } + } + + if (century != -1 || year != -1) { + if (year == -1) + year = 0; + if (century == -1) { + if (year < 69) + year += 100; + } else + year += century * 100 - TM_YEAR_BASE; + tm->tm_year = year; + } + + if (!(flags & FLAG_YDAY) && (flags & FLAG_YEAR)) { + if ((flags & (FLAG_MONTH | FLAG_MDAY)) == + (FLAG_MONTH | FLAG_MDAY)) { + tm->tm_yday = start_of_month[isleap(tm->tm_year + + TM_YEAR_BASE)] + [tm->tm_mon] + + (tm->tm_mday - 1); + flags |= FLAG_YDAY; + } else if (day_offset != -1) { + int tmpwday, tmpyday, fwo; + + fwo = first_wday_of(tm->tm_year + TM_YEAR_BASE); + /* No incomplete week (week 0). */ + if (week_offset == 0 && fwo == day_offset) + return NULL; + + /* Set the date to the first Sunday (or Monday) + * of the specified week of the year. + */ + tmpwday = + (flags & FLAG_WDAY) ? tm->tm_wday : day_offset; + tmpyday = (7 - fwo + day_offset) % 7 + + (week_offset - 1) * 7 + + (tmpwday - day_offset + 7) % 7; + /* Impossible yday for incomplete week (week 0). */ + if (tmpyday < 0) { + if (flags & FLAG_WDAY) + return NULL; + tmpyday = 0; + } + tm->tm_yday = tmpyday; + flags |= FLAG_YDAY; + } + } + + if ((flags & (FLAG_YEAR | FLAG_YDAY)) == (FLAG_YEAR | FLAG_YDAY)) { + if (!(flags & FLAG_MONTH)) { + i = 0; + while (tm->tm_yday >= + start_of_month[isleap(tm->tm_year + + TM_YEAR_BASE)][i]) + i++; + if (i > 12) { + i = 1; + tm->tm_yday -= start_of_month[isleap( + tm->tm_year + TM_YEAR_BASE)][12]; + tm->tm_year++; + } + tm->tm_mon = i - 1; + flags |= FLAG_MONTH; + } + if (!(flags & FLAG_MDAY)) { + tm->tm_mday = tm->tm_yday - + start_of_month[isleap(tm->tm_year + + TM_YEAR_BASE)] + [tm->tm_mon] + + 1; + flags |= FLAG_MDAY; + } + if (!(flags & FLAG_WDAY)) { + i = 0; + wday_offset = first_wday_of(tm->tm_year); + while (i++ <= tm->tm_yday) { + if (wday_offset++ >= 6) + wday_offset = 0; + } + tm->tm_wday = wday_offset; + flags |= FLAG_WDAY; + } + } + + return (char *)buf; +} diff --git a/src/lib/tzcode/timelocal.c b/src/lib/tzcode/timelocal.c new file mode 100644 index 0000000000..32f145ac8a --- /dev/null +++ b/src/lib/tzcode/timelocal.c @@ -0,0 +1,91 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause-FreeBSD + * + * Copyright (c) 2001 Alexey Zelkin <phantom@FreeBSD.org> + * Copyright (c) 1997 FreeBSD Inc. + * All rights reserved. + * + * Copyright (c) 2011 The FreeBSD Foundation + * All rights reserved. + * Portions of this software were developed by David Chisnall + * under sponsorship from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <stdlib.h> +#include <sys/cdefs.h> + +#include "timelocal.h" + +const struct lc_time_T C_time_locale = { + { "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }, + { "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" }, + { "Sun", "Mon", "Tue", "Wed", + "Thu", "Fri", "Sat" }, + { "Sunday", "Monday", "Tuesday", "Wednesday", + "Thursday", "Friday", "Saturday" }, + + /* X_fmt */ + "%H:%M:%S", + + /* + * x_fmt + * Since the C language standard calls for + * "date, using locale's date format," anything goes. + * Using just numbers (as here) makes Quakers happier; + * it's also compatible with SVR4. + */ + "%m/%d/%y", + + /* + * c_fmt + */ + "%a %b %e %H:%M:%S %Y", + + /* am */ + "AM", + + /* pm */ + "PM", + + /* date_fmt */ + "%a %b %e %H:%M:%S %Z %Y", + + /* alt_month + * Standalone months forms for %OB + */ + { "January", "February", "March", "April", "May", "June", "July", + "August", "September", "October", "November", "December" }, + + /* md_order + * Month / day order in dates + */ + "md", + + /* ampm_fmt + * To determine 12-hour clock format time (empty, if N/A) + */ + "%I:%M:%S %p" +}; diff --git a/src/lib/tzcode/timelocal.h b/src/lib/tzcode/timelocal.h new file mode 100644 index 0000000000..ab7bf252c6 --- /dev/null +++ b/src/lib/tzcode/timelocal.h @@ -0,0 +1,65 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause-FreeBSD + * + * Copyright (c) 1997-2002 FreeBSD Project. + * All rights reserved. + * + * Copyright (c) 2011 The FreeBSD Foundation + * All rights reserved. + * Portions of this software were developed by David Chisnall + * under sponsorship from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * $FreeBSD$ + */ + +#ifndef _TIMELOCAL_H_ +#define _TIMELOCAL_H_ +#include "private.h" +#include <locale.h> + +/** + * Private header file for the strftime and strptime localization + * stuff. + */ +struct lc_time_T { + const char *mon[MONTHSPERYEAR]; + const char *month[MONTHSPERYEAR]; + const char *wday[DAYSPERWEEK]; + const char *weekday[DAYSPERWEEK]; + const char *X_fmt; + const char *x_fmt; + const char *c_fmt; + const char *am; + const char *pm; + const char *date_fmt; + const char *alt_month[MONTHSPERYEAR]; + const char *md_order; + const char *ampm_fmt; +}; + +#define Locale (&C_time_locale) + +extern const struct lc_time_T C_time_locale; + +#endif /* !_TIMELOCAL_H_ */ diff --git a/src/lib/tzcode/tzcode.h b/src/lib/tzcode/tzcode.h index 75531800e8..1b3add2397 100644 --- a/src/lib/tzcode/tzcode.h +++ b/src/lib/tzcode/tzcode.h @@ -56,6 +56,16 @@ size_t tnt_strftime(char *s, size_t maxsize, const char *format, const struct tnt_tm *tm); +/** + * tnt_strptime is a Tarantool version of POSIX strptime() + * which has been extended with %f (fractions of second) + * flag support. + * @sa strptime() + */ +char * +tnt_strptime(const char *__restrict buf, const char *__restrict fmt, + struct tnt_tm *__restrict tm); + #if defined(__cplusplus) } /* extern "C" */ #endif /* defined(__cplusplus) */ diff --git a/src/lua/datetime.lua b/src/lua/datetime.lua index b032d3284b..8bb21e440a 100644 --- a/src/lua/datetime.lua +++ b/src/lua/datetime.lua @@ -76,6 +76,8 @@ size_t tnt_datetime_strftime(const struct datetime *date, char *buf, uint32_t len, const char *fmt); size_t tnt_datetime_parse_full(struct datetime *date, const char *str, size_t len, int32_t offset); +size_t tnt_datetime_strptime(struct datetime *date, const char *buf, + const char *fmt); void tnt_datetime_now(struct datetime *now); ]] @@ -923,6 +925,19 @@ local function datetime_parse_full(str, tzoffset) return date, len end +--[[ + Parse datetime string given `strptime` like format. + Returns constructed datetime object and length of accepted string. +]] +local function datetime_parse_format(str, fmt) + local date = ffi.new(datetime_t) + local len = builtin.tnt_datetime_strptime(date, str, fmt) + if len == 0 then + error(("could not parse '%s' using '%s' format"):format(str, fmt)) + end + return date, len +end + local function datetime_parse_from(str, obj) check_str(str, 'datetime.parse()') local fmt = '' @@ -946,7 +961,7 @@ local function datetime_parse_from(str, obj) if not fmt or fmt == '' or fmt == 'iso8601' or fmt == 'rfc3339' then return datetime_parse_full(str, offset or 0) else - error(("unknown format '%s'"):format(fmt), 2) + return datetime_parse_format(str, fmt) end end diff --git a/src/lua/tnt_datetime.c b/src/lua/tnt_datetime.c index 71573a5129..49fa1b1303 100644 --- a/src/lua/tnt_datetime.c +++ b/src/lua/tnt_datetime.c @@ -14,6 +14,12 @@ tnt_datetime_strftime(const struct datetime *date, char *buf, size_t len, return datetime_strftime(date, buf, len, fmt); } +size_t +tnt_datetime_strptime(struct datetime *date, const char *buf, const char *fmt) +{ + return datetime_strptime(date, buf, fmt); +} + void tnt_datetime_now(struct datetime *now) { diff --git a/test/app-tap/datetime.test.lua b/test/app-tap/datetime.test.lua index 974468545f..ee40a3081c 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(29) +test:plan(31) -- minimum supported date - -5879610-06-22 local MIN_DATE_YEAR = -5879610 @@ -492,16 +492,16 @@ test:test("Multiple tests for parser (with nanoseconds)", function(test) end end) -local function couldnt_parse(txt) - return ("could not parse '%s'"):format(txt) -end - local function create_date_string(date) local year, month, day = date.year or 1970, date.month or 1, date.day or 1 local hour, min, sec = date.hour or 0, date.min or 0, date.sec or 0 return ('%04d-%02d-%02dT%02d:%02d:%02dZ'):format(year, month, day, hour, min, sec) end +local function couldnt_parse(txt) + return ("could not parse '%s'"):format(txt) +end + test:test("Check parsing of dates with invalid attributes", function(test) test:plan(32) @@ -590,97 +590,113 @@ test:test("Datetime formatting of huge dates", function(test) check_variant_formats(test, base, variants) end) +local strftime_formats = { + { '%A', 1, 'Thursday' }, + { '%a', 1, 'Thu' }, + { '%B', 1, 'January' }, + { '%b', 1, 'Jan' }, + { '%h', 1, 'Jan' }, + { '%C', 0, '19' }, + { '%c', 1, 'Thu Jan 1 03:00:00 1970' }, + { '%D', 1, '01/01/70' }, + { '%m/%d/%y', 1, '01/01/70' }, + { '%d', 1, '01' }, + { '%Ec', 1, 'Thu Jan 1 03:00:00 1970' }, + { '%EC', 0, '19' }, + { '%Ex', 1, '01/01/70' }, + { '%EX', 1, '03:00:00' }, + { '%Ey', 1, '70' }, + { '%EY', 1, '1970' }, + { '%Od', 1, '01' }, + { '%oe', 0, 'oe' }, + { '%OH', 1, '03' }, + { '%OI', 1, '03' }, + { '%Om', 1, '01' }, + { '%OM', 1, '00' }, + { '%OS', 1, '00' }, + { '%Ou', 1, '4' }, + { '%OU', 1, '00' }, + { '%OV', 0, '01' }, + { '%Ow', 1, '4' }, + { '%OW', 1, '00' }, + { '%Oy', 1, '70' }, + { '%e', 1, ' 1' }, + { '%F', 1, '1970-01-01' }, + { '%Y-%m-%d', 1, '1970-01-01' }, + { '%H', 1, '03' }, + { '%I', 1, '03' }, + { '%j', 1, '001' }, + { '%k', 1, ' 3' }, + { '%l', 1, ' 3' }, + { '%M', 1, '00' }, + { '%m', 1, '01' }, + { '%n', 1, '\n' }, + { '%p', 1, 'AM' }, + { '%R', 1, '03:00' }, + { '%H:%M', 1, '03:00' }, + { '%r', 1, '03:00:00 AM' }, + { '%I:%M:%S %p', 1, '03:00:00 AM' }, + { '%S', 1, '00' }, + { '%s', 1, '10800' }, + { '%f', 1, '125' }, + { '%3f', 0, '125' }, + { '%6f', 0, '125000' }, + { '%6d', 0, '6d' }, + { '%3D', 0, '3D' }, + { '%T', 1, '03:00:00' }, + { '%H:%M:%S', 1, '03:00:00' }, + { '%t', 1, '\t' }, + { '%U', 1, '00' }, + { '%u', 1, '4' }, + { '%V', 0, '01' }, + { '%G', 1, '1970' }, + { '%g', 1, '70' }, + { '%v', 1, ' 1-Jan-1970' }, + { '%e-%b-%Y', 1, ' 1-Jan-1970' }, + { '%W', 1, '00' }, + { '%w', 1, '4' }, + { '%X', 1, '03:00:00' }, + { '%x', 1, '01/01/70' }, + { '%y', 1, '70' }, + { '%Y', 1, '1970' }, + { '%z', 1, '+0300' }, + { '%%', 1, '%' }, + { '%Y-%m-%dT%H:%M:%S.%9f%z', 1, '1970-01-01T03:00:00.125000000+0300' }, + { '%Y-%m-%dT%H:%M:%S.%f%z', 1, '1970-01-01T03:00:00.125+0300' }, + { '%Y-%m-%dT%H:%M:%S.%f', 1, '1970-01-01T03:00:00.125' }, + { '%FT%T.%f', 1, '1970-01-01T03:00:00.125' }, + { '%FT%T.%f%z', 1, '1970-01-01T03:00:00.125+0300' }, + { '%FT%T.%9f%z', 1, '1970-01-01T03:00:00.125000000+0300' }, +} + test:test("Datetime string formatting detailed", function(test) test:plan(77) local T = date.new{ timestamp = 0.125 } T:set{ tzoffset = 180 } test:is(tostring(T), '1970-01-01T03:00:00.125+0300', 'tostring()') - local formats = { - { '%A', 'Thursday' }, - { '%a', 'Thu' }, - { '%B', 'January' }, - { '%b', 'Jan' }, - { '%h', 'Jan' }, - { '%C', '19' }, - { '%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' }, - { '%EC', '19' }, - { '%Ex', '01/01/70' }, - { '%EX', '03:00:00' }, - { '%Ey', '70' }, - { '%EY', '1970' }, - { '%Od', '01' }, - { '%oe', 'oe' }, - { '%OH', '03' }, - { '%OI', '03' }, - { '%Om', '01' }, - { '%OM', '00' }, - { '%OS', '00' }, - { '%Ou', '4' }, - { '%OU', '00' }, - { '%OV', '01' }, - { '%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' }, - { '%3f', '125' }, - { '%6f', '125000' }, - { '%6d', '6d' }, - { '%3D', '3D' }, - { '%T', '03:00:00' }, - { '%H:%M:%S', '03:00:00' }, - { '%t', '\t' }, - { '%U', '00' }, - { '%u', '4' }, - { '%V', '01' }, - { '%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' }, - } - for _, row in pairs(formats) do - local fmt, value = unpack(row) + for _, row in pairs(strftime_formats) do + local fmt, _, value = unpack(row) test:is(T:format(fmt), value, ('format %s, expected %s'):format(fmt, value)) end end) +test:test("Datetime string parsing by format (detailed)", function(test) + test:plan(68) + local T = date.new{ timestamp = 0.125 } + T:set{ tzoffset = 180 } + test:is(tostring(T), '1970-01-01T03:00:00.125+0300', 'tostring()') + + for _, row in pairs(strftime_formats) do + local fmt, check, value = unpack(row) + if check > 0 then + local res = date.parse(value, {format = fmt}) + test:is(res ~= nil, true, ('parse of %s'):format(fmt)) + end + end +end) + test:test("__index functions()", function(test) test:plan(15) -- 2000-01-29T03:30:12Z' @@ -1374,6 +1390,42 @@ test:test("Parse tiny date into seconds and other parts", function(test) test:is(tiny.timestamp, 30.528, "timestamp") end) +test:test("Parse strptime format", function(test) + test:plan(17) + local formats = { + {'Thu Jan 1 03:00:00 1970', '%c', '1970-01-01T03:00:00Z'}, + {'01/01/70', '%D', '1970-01-01T00:00:00Z'}, + {'01/01/70', '%m/%d/%y', '1970-01-01T00:00:00Z'}, + {'Thu Jan 1 03:00:00 1970', '%Ec', '1970-01-01T03:00:00Z'}, + {'1970-01-01', '%F', '1970-01-01T00:00:00Z'}, + {'1970-01-01', '%Y-%m-%d', '1970-01-01T00:00:00Z'}, + {' 1-Jan-1970', '%v', '1970-01-01T00:00:00Z'}, + {' 1-Jan-1970', '%e-%b-%Y', '1970-01-01T00:00:00Z'}, + {'01/01/70', '%x', '1970-01-01T00:00:00Z'}, + {'1970-01-01T0300+0300', '%Y-%m-%dT%H%M%z', + '1970-01-01T03:00:00+0300'}, + {'1970-01-01T03:00:00+0300', '%Y-%m-%dT%H:%M:%S%z', + '1970-01-01T03:00:00+0300'}, + {'1970-01-01T03:00:00.125000000+0300', '%Y-%m-%dT%H:%M:%S.%f%z', + '1970-01-01T03:00:00.125+0300'}, + {'1970-01-01T03:00:00.125+0300', '%Y-%m-%dT%H:%M:%S.%f%z', + '1970-01-01T03:00:00.125+0300'}, + {'1970-01-01T03:00:00.125', '%Y-%m-%dT%H:%M:%S.%f', + '1970-01-01T03:00:00.125Z'}, + {'1970-01-01T03:00:00.125', '%FT%T.%f', + '1970-01-01T03:00:00.125Z'}, + {'1970-01-01T03:00:00.125+0300', '%FT%T.%f%z', + '1970-01-01T03:00:00.125+0300'}, + {'1970-01-01T03:00:00.125000000+0300', '%FT%T.%f%z', + '1970-01-01T03:00:00.125+0300'}, + } + for _, row in pairs(formats) do + local str, fmt, exp = unpack(row) + local dt = date.parse(str, {format = fmt}) + test:is(tostring(dt), exp, ('parse %s via %s'):format(str, fmt)) + end +end) + test:test("totable{}", function(test) test:plan(78) local exp = {sec = 0, min = 0, wday = 5, day = 1, -- GitLab