From 25bca54ba7c47dcd9ffb10bcbaf9ff4f7fb0b229 Mon Sep 17 00:00:00 2001 From: MeanSquaredError <35379301+MeanSquaredError@users.noreply.github.com> Date: Thu, 7 Sep 2023 07:23:44 +0300 Subject: [PATCH] Replace regex-based date/time parsing with manual parser (#520) * Replace regex-based date/time string parsing with manually written parsing code. * Add date/time parser tests. --- include/sqlpp11/detail/parse_date_time.h | 273 ++++++++++++++----- include/sqlpp11/mysql/char_result.h | 4 +- include/sqlpp11/postgresql/bind_result.h | 6 +- include/sqlpp11/sqlite3/bind_result.h | 39 +-- tests/core/usage/CMakeLists.txt | 3 +- tests/core/usage/DateTimeParser.cpp | 330 +++++++++++++++++++++++ 6 files changed, 544 insertions(+), 111 deletions(-) create mode 100644 tests/core/usage/DateTimeParser.cpp diff --git a/include/sqlpp11/detail/parse_date_time.h b/include/sqlpp11/detail/parse_date_time.h index d6e36170..60d47245 100644 --- a/include/sqlpp11/detail/parse_date_time.h +++ b/include/sqlpp11/detail/parse_date_time.h @@ -27,100 +27,237 @@ * POSSIBILITY OF SUCH DAMAGE. */ -#include +#include + +#include namespace sqlpp { namespace detail { - // Parse a date string formatted as YYYY-MM-DD - // - inline bool parse_string_date(::sqlpp::chrono::day_point& value, const char* date_string) + inline bool parse_unsigned(int& value, const char*& input, int length) { - static const std::regex rx{"(\\d{4})-(\\d{2})-(\\d{2})"}; - std::cmatch mr; - if (std::regex_match(date_string, mr, rx) == false) + value = 0; + auto new_input = input; + while (length--) { - return false; + auto ch = *new_input++; + if (std::isdigit(ch) == false) + { + return false; + } + value = value * 10 + ch - '0'; } - value = ::sqlpp::chrono::day_point{ - ::date::year{std::atoi(date_string + mr.position(1))} / // Year - std::atoi(date_string + mr.position(2)) / // Month - std::atoi(date_string + mr.position(3)) // Day of month - }; + input = new_input; return true; } - // Parse a date string formatted as YYYY-MM-DD HH:MM:SS.US TZ - // .US are optional fractional seconds, up to 6 digits in length - // TZ is an optional time zone offset formatted as +HH[:MM] or -HH[:MM] - // - inline bool parse_string_date_time(::sqlpp::chrono::microsecond_point& value, const char* date_time_string) + inline bool parse_character(const char*& input, char ch) { - static const std::regex rx{ - "(\\d{4})-(\\d{2})-(\\d{2}) " - "(\\d{2}):(\\d{2}):(\\d{2})(?:\\.(\\d{1,6}))?" - "(?:([+-])(\\d{2})(?::(\\d{2})(?::(\\d{2}))?)?)?" - }; - std::cmatch mr; - if (std::regex_match(date_time_string, mr, rx) == false) + if (*input != ch) { return false; } - value = - ::sqlpp::chrono::day_point{ - ::date::year{std::atoi(date_time_string + mr.position(1))} / // Year - std::atoi(date_time_string + mr.position(2)) / // Month - std::atoi(date_time_string + mr.position(3)) // Day of month - } + - std::chrono::hours{std::atoi(date_time_string + mr.position(4))} + // Hour - std::chrono::minutes{std::atoi(date_time_string + mr.position(5))} + // Minute - std::chrono::seconds{std::atoi(date_time_string + mr.position(6))} + // Second - ::std::chrono::microseconds{ // Second fraction - mr[7].matched ? std::stoi((mr[7].str() + "000000").substr(0, 6)) : 0 - }; - if (mr[8].matched) + ++input; + return true; + } + + inline bool parse_yyyy_mm_dd(sqlpp::chrono::day_point& dp, const char*& input) + { + auto new_input = input; + int year, month, day; + if ((parse_unsigned(year, new_input, 4) == false) || (parse_character(new_input, '-') == false) || + (parse_unsigned(month, new_input, 2) == false) || (parse_character(new_input, '-') == false) || + (parse_unsigned(day, new_input, 2) == false)) { - const auto tz_sign = (date_time_string[mr.position(8)] == '+') ? 1 : -1; - const auto tz_offset = - std::chrono::hours{std::atoi(date_time_string + mr.position(9))} + - std::chrono::minutes{mr[10].matched ? std::atoi(date_time_string + mr.position(10)) : 0} + - std::chrono::seconds{mr[11].matched ? std::atoi(date_time_string + mr.position(11)) : 0}; - value -= tz_sign * tz_offset; + return false; + } + dp = ::date::year{year} / month / day; + input = new_input; + return true; + } + + inline bool parse_hh_mm_ss(std::chrono::microseconds& us, const char*& input) + { + auto new_input = input; + int hour, minute, second; + if ((parse_unsigned(hour, new_input, 2) == false) || (parse_character(new_input, ':') == false) || + (parse_unsigned(minute, new_input, 2) == false) || (parse_character(new_input, ':') == false) || + (parse_unsigned(second, new_input, 2) == false)) + { + return false; + } + // Strings that have valid format but year, month and/or day values that fall outside of the + // correct ranges are still mapped to day_point values. For the exact rules of the mapping see + // https://en.cppreference.com/w/cpp/chrono/year_month_day/operator_days + us = std::chrono::hours{hour} + std::chrono::minutes{minute} + std::chrono::seconds{second}; + input = new_input; + return true; + } + + inline bool parse_ss_fraction(std::chrono::microseconds& us, const char*& input) + { + auto new_input = input; + if (parse_character(new_input, '.') == false) + { + return false; + } + int value = 0; + int len_max = 6; + int len_actual; + for (len_actual = 0; (len_actual < len_max) && std::isdigit(*new_input); ++len_actual, ++new_input) + { + value = value * 10 + *new_input - '0'; + } + if (len_actual == 0) + { + return false; + } + for (; len_actual < len_max; ++len_actual) + { + value *= 10; + } + us = std::chrono::microseconds{value}; + input = new_input; + return true; + } + + inline bool parse_tz(std::chrono::microseconds& offset, const char*& input) + { + auto new_input = input; + int tz_sign; + if (parse_character(new_input, '+')) + { + tz_sign = 1; + } + else if (parse_character(new_input, '-')) + { + tz_sign = -1; + } + else + { + return false; + } + int hour; + if (parse_unsigned(hour, new_input, 2) == false) + { + return false; + } + offset = tz_sign * std::chrono::hours{hour}; + input = new_input; + int minute; + if ((parse_character(new_input, ':') == false) || (parse_unsigned(minute, new_input, 2) == false)) + { + return true; + } + offset += tz_sign * std::chrono::minutes{minute}; + input = new_input; + int second; + if ((parse_character(new_input, ':') == false) || (parse_unsigned(second, new_input, 2) == false)) + { + return true; + } + offset += tz_sign * std::chrono::seconds{second}; + input = new_input; + return true; + } + + inline bool parse_hh_mm_ss_us_tz(std::chrono::microseconds& us, const char*& input) + { + if (parse_hh_mm_ss(us, input) == false) + { + return false; + } + std::chrono::microseconds fraction; + if (parse_ss_fraction(fraction, input)) + { + us += fraction; + } + std::chrono::microseconds tz_offset; + if (parse_tz(tz_offset, input)) + { + us -= tz_offset; } return true; } - // Parse a time string formatted as HH:MM:SS[.US][ TZ] - // .US is up to 6 digits in length - // TZ is an optional time zone offset formatted as +HH[:MM] or -HH[:MM] + // Parse timestamp formatted as YYYY-MM-DD HH:MM:SS.U+HH:MM:SS + // The microseconds and timezone offset are optional // - inline bool parse_string_time_of_day(::std::chrono::microseconds& value, const char* time_string) + inline bool parse_timestamp(sqlpp::chrono::microsecond_point& tp, const char* date_time_string) { - static const std::regex rx{ - "(\\d{2}):(\\d{2}):(\\d{2})(?:\\.(\\d{1,6}))?" - "(?:([+-])(\\d{2})(?::(\\d{2})(?::(\\d{2}))?)?)?" - }; - std::cmatch mr; - if (std::regex_match (time_string, mr, rx) == false) + sqlpp::chrono::day_point parsed_ymd; + std::chrono::microseconds parsed_tod; + if ((parse_yyyy_mm_dd(parsed_ymd, date_time_string) == false) || + (parse_character(date_time_string, ' ') == false) || + (parse_hh_mm_ss_us_tz(parsed_tod, date_time_string) == false)) { return false; } - value = - std::chrono::hours{std::atoi(time_string + mr.position(1))} + // Hour - std::chrono::minutes{std::atoi(time_string + mr.position(2))} + // Minute - std::chrono::seconds{std::atoi(time_string + mr.position(3))} + // Second - ::std::chrono::microseconds{ // Second fraction - mr[4].matched ? std::stoi((mr[4].str() + "000000").substr(0, 6)) : 0 - }; - if (mr[5].matched) + if (*date_time_string) { - const auto tz_sign = (time_string[mr.position(5)] == '+') ? 1 : -1; - const auto tz_offset = - std::chrono::hours{std::atoi(time_string + mr.position(6))} + - std::chrono::minutes{mr[7].matched ? std::atoi(time_string + mr.position(7)) : 0} + - std::chrono::seconds{mr[8].matched ? std::atoi(time_string + mr.position(8)) : 0}; - value -= tz_sign * tz_offset; + return false; + } + tp = parsed_ymd + parsed_tod; + return true; + } + + // Parse date string formatted as YYYY-MM-DD + // + inline bool parse_date(sqlpp::chrono::day_point& dp, const char* date_string) + { + if (parse_yyyy_mm_dd(dp, date_string) == false) + { + return false; + } + if (*date_string) + { + return false; + } + return true; + } + + // Parse time string formatted as YYYY-MM-DD HH:MM:SS.U+HH:MM:SS + // The time-of-day part is optional + // + inline bool parse_date_or_timestamp(sqlpp::chrono::microsecond_point& tp, const char* date_time_string) + { + sqlpp::chrono::day_point parsed_ymd; + if (parse_yyyy_mm_dd(parsed_ymd, date_time_string) == false) + { + return false; + } + if (*date_time_string == 0) + { + tp = parsed_ymd; + return true; + } + std::chrono::microseconds parsed_tod; + if ((parse_character(date_time_string, ' ') == false) || + (parse_hh_mm_ss_us_tz(parsed_tod, date_time_string) == false)) + { + return false; + } + if (*date_time_string == 0) + { + tp = parsed_ymd + parsed_tod; + return true; + } + return false; + } + + // Parse time of day string formatted as HH:MM:SS.U+HH:MM:SS + // The microseconds and timezone offset are optional + // + inline bool parse_time_of_day(std::chrono::microseconds& us, const char* time_string) + { + if (parse_hh_mm_ss_us_tz(us, time_string) == false) + { + return false; + } + if (*time_string) + { + return false; } return true; } diff --git a/include/sqlpp11/mysql/char_result.h b/include/sqlpp11/mysql/char_result.h index 951c1707..e05ac2fb 100644 --- a/include/sqlpp11/mysql/char_result.h +++ b/include/sqlpp11/mysql/char_result.h @@ -152,7 +152,7 @@ namespace sqlpp if (_handle->debug) std::cerr << "MySQL debug: date string: " << date_string << std::endl; - if (::sqlpp::detail::parse_string_date(*value, date_string) == false) + if (::sqlpp::detail::parse_date(*value, date_string) == false) { if (_handle->debug) std::cerr << "MySQL debug: invalid date result: " << date_string << std::endl; @@ -175,7 +175,7 @@ namespace sqlpp if (_handle->debug) std::cerr << "MySQL debug: date_time string: " << date_time_string << std::endl; - if (::sqlpp::detail::parse_string_date_time(*value, date_time_string) == false) + if (::sqlpp::detail::parse_timestamp(*value, date_time_string) == false) { if (_handle->debug) std::cerr << "MySQL debug: invalid date_time result: " << date_time_string << std::endl; diff --git a/include/sqlpp11/postgresql/bind_result.h b/include/sqlpp11/postgresql/bind_result.h index 220977ee..9dc7086e 100644 --- a/include/sqlpp11/postgresql/bind_result.h +++ b/include/sqlpp11/postgresql/bind_result.h @@ -239,7 +239,7 @@ namespace sqlpp { std::cerr << "PostgreSQL debug: date string: " << date_string << std::endl; } - if (::sqlpp::detail::parse_string_date(*value, date_string) == false) + if (::sqlpp::detail::parse_date(*value, date_string) == false) { if (_handle->debug()) { @@ -269,7 +269,7 @@ namespace sqlpp { std::cerr << "PostgreSQL debug: got date_time string: " << date_string << std::endl; } - if (::sqlpp::detail::parse_string_date_time(*value, date_string) == false) + if (::sqlpp::detail::parse_timestamp(*value, date_string) == false) { if (_handle->debug()) { @@ -301,7 +301,7 @@ namespace sqlpp std::cerr << "PostgreSQL debug: got time string: " << time_string << std::endl; } - if (::sqlpp::detail::parse_string_time_of_day(*value, time_string) == false) + if (::sqlpp::detail::parse_time_of_day(*value, time_string) == false) { if (_handle->debug()) { std::cerr << "PostgreSQL debug: got invalid time '" << time_string << "'" << std::endl; diff --git a/include/sqlpp11/sqlite3/bind_result.h b/include/sqlpp11/sqlite3/bind_result.h index c7bf98f2..5fcee824 100644 --- a/include/sqlpp11/sqlite3/bind_result.h +++ b/include/sqlpp11/sqlite3/bind_result.h @@ -34,7 +34,6 @@ #include #include -#include #ifdef _MSC_VER #include @@ -46,40 +45,6 @@ namespace sqlpp { namespace sqlite3 { - namespace detail - { - // Parse a date string formatted as YYYY-MM-DD[ HH:MM:SS[.US]] - // - inline bool parse_string_date_opt_time(::sqlpp::chrono::microsecond_point& value, const char* date_time_string) - { - static const std::regex rx{ - "(\\d{4})-(\\d{2})-(\\d{2})" - "(?: (\\d{2}):(\\d{2}):(\\d{2})(?:\\.(\\d{1,6}))?)?" - }; - std::cmatch mr; - if (std::regex_match(date_time_string, mr, rx) == false) - { - return false; - } - value = ::sqlpp::chrono::day_point{ - ::date::year{std::atoi(date_time_string + mr.position(1))} / // Year - std::atoi(date_time_string + mr.position(2)) / // Month - std::atoi(date_time_string + mr.position(3)) // Day of month - }; - if (mr[4].matched) - { - value += - std::chrono::hours{std::atoi(date_time_string + mr.position(4))} + // Hour - std::chrono::minutes{std::atoi(date_time_string + mr.position(5))} + // Minute - std::chrono::seconds{std::atoi(date_time_string + mr.position(6))} + // Second - ::std::chrono::microseconds{ // Second fraction - mr[7].matched ? std::stoi((mr[7].str() + "000000").substr(0, 6)) : 0 - }; - } - return true; - } - } // namespace detail - class SQLPP11_SQLITE3_EXPORT bind_result_t { std::shared_ptr _handle; @@ -208,7 +173,7 @@ namespace sqlpp reinterpret_cast(sqlite3_column_text(_handle->sqlite_statement, static_cast(index))); if (_handle->debug) std::cerr << "Sqlite3 debug: date string: " << date_string << std::endl; - if (::sqlpp::detail::parse_string_date(*value, date_string) == false) + if (::sqlpp::detail::parse_date(*value, date_string) == false) { if (_handle->debug) std::cerr << "Sqlite3 debug: invalid date result: " << date_string << std::endl; @@ -232,7 +197,7 @@ namespace sqlpp if (_handle->debug) std::cerr << "Sqlite3 debug: date_time string: " << date_time_string << std::endl; // We treat DATETIME fields as containing either date+time or just date. - if (detail::parse_string_date_opt_time(*value, date_time_string) == false) + if (::sqlpp::detail::parse_date_or_timestamp(*value, date_time_string) == false) { if (_handle->debug) std::cerr << "Sqlite3 debug: invalid date_time result: " << date_time_string << std::endl; diff --git a/tests/core/usage/CMakeLists.txt b/tests/core/usage/CMakeLists.txt index 8c85e83d..cd0c3fac 100644 --- a/tests/core/usage/CMakeLists.txt +++ b/tests/core/usage/CMakeLists.txt @@ -33,6 +33,7 @@ set(test_files BooleanExpression.cpp CustomQuery.cpp DateTime.cpp + DateTimeParser.cpp Interpret.cpp Insert.cpp Remove.cpp @@ -77,4 +78,4 @@ endforeach() # OUTPUT "${CMAKE_CURRENT_LIST_DIR}/Sample.h" # COMMAND "${PYTHON_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/scripts/ddl2cpp" "${CMAKE_CURRENT_LIST_DIR}/sample.sql" Sample test # DEPENDS "${CMAKE_CURRENT_LIST_DIR}/sample.sql" -# VERBATIM) \ No newline at end of file +# VERBATIM) diff --git a/tests/core/usage/DateTimeParser.cpp b/tests/core/usage/DateTimeParser.cpp new file mode 100644 index 00000000..8cd46c50 --- /dev/null +++ b/tests/core/usage/DateTimeParser.cpp @@ -0,0 +1,330 @@ +/* + * Copyright (c) 2023, Vesselin Atanasov + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * 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 COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +#include +#include + +namespace +{ + std::chrono::microseconds build_tod(int hour = 0, int minute = 0, int second = 0, int us = 0, bool tz_plus = true, int tz_hour = 0, int tz_minute = 0, int tz_second = 0) + { + std::chrono::microseconds result{0}; + // We add time components one by one to the resulting microsecond_point in order to avoid going through temporary time_point values + // with small bitsize which could cause in integer overflow. + result += std::chrono::hours{hour}; + result += std::chrono::minutes{minute}; + result += std::chrono::seconds{second}; + result += std::chrono::microseconds{us}; + std::chrono::microseconds tz_offset{std::chrono::hours{tz_hour} + std::chrono::minutes{tz_minute} + std::chrono::seconds{tz_second}}; + if (tz_plus) { + tz_offset = -tz_offset; + } + result += tz_offset; + return result; + } + + sqlpp::chrono::microsecond_point build_timestamp(int year, int month, int day, int hour = 0, int minute = 0, int second = 0, int us = 0, bool tz_plus = true, int tz_hour = 0, int tz_minute = 0, int tz_second = 0) + { + return + date::sys_days{date::year{year}/month/day} + + build_tod(hour, minute, second, us, tz_plus, tz_hour,tz_minute,tz_second); + } + + template + void require_equal(int line, const L& l, const R& r) + { + if (l != r) + { + std::cerr << line << ": "; + serialize(sqlpp::wrap_operand_t{l}, std::cerr); + std::cerr << " != "; + serialize(sqlpp::wrap_operand_t{r}, std::cerr); + std::cerr << std::endl; + throw std::runtime_error("Unexpected result"); + } + } + + void test_valid_dates() + { + using namespace sqlpp::chrono; + using namespace date; + + for (const auto& date_pair : std::vector>{ + // Minimum and maximum dates + {"0001-01-01", year{1}/1/1}, {"9999-12-31", year{9999}/12/31}, + // Month minimum and maximum days + {"1999-01-01", year{1999}/1/1}, {"1999-01-31", year{1999}/1/31}, + {"1999-02-01", year{1999}/2/1}, {"1999-02-28", year{1999}/2/28}, + {"1999-03-01", year{1999}/3/1}, {"1999-03-31", year{1999}/3/31}, + {"1999-04-01", year{1999}/4/1}, {"1999-04-30", year{1999}/4/30}, + {"1999-05-01", year{1999}/5/1}, {"1999-05-31", year{1999}/5/31}, + {"1999-06-01", year{1999}/6/1}, {"1999-06-30", year{1999}/6/30}, + {"1999-07-01", year{1999}/7/1}, {"1999-07-31", year{1999}/7/31}, + {"1999-08-01", year{1999}/8/1}, {"1999-08-31", year{1999}/8/31}, + {"1999-09-01", year{1999}/9/1}, {"1999-09-30", year{1999}/9/30}, + {"1999-10-01", year{1999}/10/1}, {"1999-10-31", year{1999}/10/31}, + {"1999-11-01", year{1999}/11/1}, {"1999-11-30", year{1999}/11/30}, + {"1999-12-01", year{1999}/12/1}, {"1999-12-31", year{1999}/12/31}, + // YYYY-02-29 + {"2396-02-29", year{2396}/2/29}, {"2400-02-29", year{2400}/2/29}, + // Valid format, but the year, month and/or day fall outside of the correct ranges + {"1980-00-02", year{1980}/0/2}, {"1980-13-02", year{1980}/13/2}, + {"1980-01-00", year{1980}/1/0}, {"1980-01-32", year{1980}/1/32}, + {"1981-02-29", year{1981}/2/29}, {"2100-02-29", year{2100}/2/29} + }) + { + day_point dp; + if (sqlpp::detail::parse_date(dp, date_pair.first) == false) + { + std::cerr << "Could not parse a valid date string: " << date_pair.first << std::endl; + throw std::runtime_error{"Parse error"}; + } + require_equal(__LINE__, dp, date_pair.second); + } + } + + void test_invalid_dates() + { + using namespace sqlpp::chrono; + + for (const auto& date_str : std::vector{ + // Invalid year + "", "1", "12", "123" , "1234", "A", + // Invalid month + "1980--02", "1980-1-02", "1980-123-02", "1980-W-02", + // Invalid day + "1980-01-", "1980-01-0", "1980-01-123", "1980-01-Q", + // Invalid separator + "1980 01 02", "1980- 01-02", "1980 -01-02", "1980-01 -02", "1980-01- 02", "1980-01T02" + // Trailing characters + "1980-01-02 ", "1980-01-02T", "1980-01-02 UTC", "1980-01-02EST", "1980-01-02+01" + }) + { + day_point dp; + if (sqlpp::detail::parse_date(dp, date_str)) + { + std::cerr << "Parsed successfully an invalid date string " << date_str << ", value "; + serialize(sqlpp::wrap_operand_t{dp}, std::cerr); + std::cerr << std::endl; + throw std::runtime_error{"Parse error"}; + } + } + } + + void test_valid_time_of_day() + { + using namespace std::chrono; + + for (const auto& tod_pair : std::vector>{ + // Minimum value + {"00:00:00", build_tod()}, + // Maximum hours + {"23:00:00", build_tod(23)}, + // Maximum minutes + {"00:59:00", build_tod(0, 59)}, + // Maximum seconds + {"00:00:59", build_tod(0, 0, 59)}, + // Second fractions + {"01:23:54.000001", build_tod(1, 23, 54, 1)}, + {"01:23:54.999999", build_tod(1, 23, 54, 999999)}, + // Timezone offsets + {"10:09:08+03", build_tod(10, 9, 8, 0, true, 3)}, + {"10:09:08-03", build_tod(10, 9, 8, 0, false, 3)}, + {"10:09:08+03:02", build_tod(10, 9, 8, 0, true, 3, 2)}, + {"10:09:08-03:02", build_tod(10, 9, 8, 0, false, 3, 2)}, + {"10:09:08+13:12:11", build_tod(10, 9, 8, 0, true, 13, 12, 11)}, + {"10:09:08-13:12:11", build_tod(10, 9, 8, 0, false, 13, 12, 11)}, + // Second fraction and timezone offset + {"10:09:08.1+03", build_tod(10, 9, 8, 100000, true, 3)}, + {"10:09:08.12-07:40", build_tod(10, 9, 8, 120000, false, 7, 40)}, + {"10:09:08.123+12:38:49", build_tod(10, 9, 8, 123000, true, 12, 38, 49)}, + // Valid format but invalid hour, minute or second range + {"25:00:10", build_tod(25, 0, 10)} + }) + { + microseconds us; + if (sqlpp::detail::parse_time_of_day(us, tod_pair.first) == false) + { + std::cerr << "Could not parse a valid time-of-day string: " << tod_pair.first << std::endl; + throw std::runtime_error{"Parse error"}; + } + require_equal(__LINE__, us, tod_pair.second); + } + } + + void test_invalid_time_of_day() + { + using namespace std::chrono; + + for (const auto& tod_str : std::vector{ + // Generic string + "A", "BC", "!()", + // Invalid hour + "-01:23:45", "AA:10:11", + // Invalid minute + "13::07", "13:A:07", "13:1:07", "13:-01:07" + // Invalid second + "04:07:", "04:07:A", "04:07:1", "04:07:-01" + // Invalid fraction + "01:02:03.", "01:02:03.A", "01:02:03.1234567", "01:02:03.1A2", + // Invalid timezone + "01:03:03!01", "01:03:03+A", "01:03:03+1", "01:03:03+1A", "01:03:03+456", + "01:03:03+12:", "01:03:03+12:1", "01:03:03+12:1A", "01:03:03+12:01:", + "01:03:03+12:01:1", "01:03:03+12:01:1A" + }) + { + microseconds us; + if (sqlpp::detail::parse_time_of_day(us, tod_str)) + { + std::cerr << "Parsed successfully an invalid time-of-day string " << tod_str << ", value "; + serialize(sqlpp::wrap_operand_t{us}, std::cerr); + std::cerr << std::endl; + throw std::runtime_error{"Parse error"}; + } + } + } + + void test_valid_timestamp() + { + using namespace sqlpp::chrono; + using namespace date; + using namespace std::chrono; + + for (const auto& timestamp_pair : std::vector>{ + // Minimum and maximum timestamps + {"0001-01-01 00:00:00", build_timestamp(1, 1, 1)}, {"9999-12-31 23:59:59.999999", build_timestamp(9999, 12, 31, 23, 59, 59, 999999)}, + // Timestamp with time zone + {"1234-03-25 23:17:08.479210+10:17:29", build_timestamp(1234, 3, 25, 23, 17, 8, 479210, true, 10, 17, 29)} + }) + { + microsecond_point tp; + if (sqlpp::detail::parse_timestamp(tp, timestamp_pair.first) == false) + { + std::cerr << "Could not parse a valid timestamp string: " << timestamp_pair.first << std::endl; + throw std::runtime_error{"Parse error"}; + } + require_equal(__LINE__, tp, timestamp_pair.second); + } + } + + void test_invalid_timestamp() + { + using namespace sqlpp::chrono; + + for (const auto& timestamp_str : std::vector{ + // Generic string + "", "B", ")-#\\", + // Invalid date + "197%-03-17 10:32:09", + // Invalid time of day + "2020-02-18 22:2:28" + // Invalid time zone + "1924-02-28 18:35:36+1" + // Leading space + " 2030-17-01 15:20:30", + // Trailing space + "2030-17-01 15:20:30 " + }) + { + microsecond_point tp; + if (sqlpp::detail::parse_timestamp(tp, timestamp_str)) + { + std::cerr << "Parsed successfully an invalid timestamp string " << timestamp_str << ", value "; + serialize(sqlpp::wrap_operand_t{tp}, std::cerr); + std::cerr << std::endl; + throw std::runtime_error{"Parse error"}; + } + } + } + + void test_valid_date_or_timestamp() + { + using namespace sqlpp::chrono; + using namespace date; + using namespace std::chrono; + + for (const auto& timestamp_pair : std::vector>{ + // Valid date + {"1998-02-03", build_timestamp(1998, 2, 3)}, + // Valid timestamp + {"2015-07-08 06:32:45.872+23:14:39", build_timestamp(2015,7,8,6,32,45,872000,true,23,14,39)} + }) + { + microsecond_point tp; + if (sqlpp::detail::parse_date_or_timestamp(tp, timestamp_pair.first) == false) + { + std::cerr << "Could not parse a valid date or timestamp string: " << timestamp_pair.first << std::endl; + throw std::runtime_error{"Parse error"}; + } + require_equal(__LINE__, tp, timestamp_pair.second); + } + } + + void test_invalid_date_or_timestamp() + { + using namespace sqlpp::chrono; + + for (const auto& timestamp_str : std::vector{ + // Generic string + "", "C", "/=", + // Invalid dates + "A123-01-02", "1980-E-04", "1981-09-", + // Invalid timestamps + "2023-12-31 1:02:03", "2024-03-04 05::06", + // Invalid time zone + "1930-03-18 17:30:31+01:", + // Leading space + " 1930-03-18 17:30:31+01", + // Trailing space + "1930-03-18 17:30:31+01 " + }) + { + microsecond_point tp; + if (sqlpp::detail::parse_date_or_timestamp(tp, timestamp_str)) + { + std::cerr << "Parsed successfully an invalid date or timestamp string " << timestamp_str << ", value "; + serialize(sqlpp::wrap_operand_t{tp}, std::cerr); + std::cerr << std::endl; + throw std::runtime_error{"Parse error"}; + } + } + } +} + +int DateTimeParser(int, char*[]) +{ + test_valid_dates(); + test_invalid_dates(); + test_valid_time_of_day(); + test_invalid_time_of_day(); + test_valid_timestamp(); + test_invalid_timestamp(); + test_valid_date_or_timestamp(); + test_invalid_date_or_timestamp(); + return 0; +}