From 653b62f240843aadad664529e3eadfc77ab21e03 Mon Sep 17 00:00:00 2001 From: "He, Wanchen" Date: Wed, 11 Dec 2024 12:22:30 +0800 Subject: [PATCH 1/2] Support parsing datetime with timezone. --- trantor/unittests/DateUnittest.cc | 19 ++++++ trantor/utils/Date.cc | 103 ++++++++++++++++++++++++++++++ trantor/utils/Date.h | 11 ++++ 3 files changed, 133 insertions(+) diff --git a/trantor/unittests/DateUnittest.cc b/trantor/unittests/DateUnittest.cc index 4583bc98..c8a38709 100644 --- a/trantor/unittests/DateUnittest.cc +++ b/trantor/unittests/DateUnittest.cc @@ -107,6 +107,25 @@ TEST(Date, DatabaseStringTest) us = (dbDate.microSecondsSinceEpoch() % 1000000); EXPECT_EQ(us, 3); } +TEST(Date, TimezoneTest) +{ + std::string str0 = "2024-01-01 04:00:00.123"; + std::string str1 = "2024-01-01 12:00:00.123 +08:00"; + std::string str2 = "2024-01-01 11:00:00.123+0700"; + std::string str3 = "2024-01-01 10:00:00.123 0600"; + std::string str4 = "2024-01-01 03:00:00.123 -01:00"; + std::string str5 = "2024-01-01 02:00:00.123-02:00"; + std::string str6 = "2024-01-01 01:00:00.123 -0300"; + + auto date = trantor::Date::fromDbString(str0); + for (auto &s : {str1, str2, str3, str4, str5, str6}) + { + auto dateTz = trantor::Date::parseDatetimeTz(s); + EXPECT_EQ(date.microSecondsSinceEpoch(), + dateTz.microSecondsSinceEpoch()); + } +} + int main(int argc, char **argv) { testing::InitGoogleTest(&argc, argv); diff --git a/trantor/utils/Date.cc b/trantor/utils/Date.cc index 0da38731..a7e9cf46 100644 --- a/trantor/utils/Date.cc +++ b/trantor/utils/Date.cc @@ -323,6 +323,109 @@ Date Date::fromDbString(const std::string &datetime) static_cast(timezoneOffset())); } +Date Date::parseDatetimeTz(const std::string &datetime) +{ + unsigned int year = {0}, month = {0}, day = {0}, hour = {0}, minute = {0}, + second = {0}, microSecond = {0}; + int tzSign{0}, tzOffset{0}; + std::vector v = splitString(datetime, " "); + if (v.empty()) + { + throw std::invalid_argument("Invalid date string: " + datetime); + } + + // parse date + const std::vector date = splitString(v[0], "-"); + if (date.size() != 3) + { + throw std::invalid_argument("Invalid date string: " + datetime); + } + year = std::stol(date[0]); + month = std::stol(date[1]); + day = std::stol(date[2]); + + // only have date part + if (v.size() <= 1) + { + return trantor::Date{year, month, day}; + } + + // check timezone without space seperated + if (v.size() == 2) + { + auto pos = v[1].find('+'); + if (pos != std::string::npos) + { + tzSign = 1; + v.push_back(v[1].substr(pos + 1)); + v[1] = v[1].substr(0, pos); + } + else if ((pos = v[1].find('-')) != std::string::npos) + { + tzSign = -1; + v.push_back(v[1].substr(pos + 1)); + v[1] = v[1].substr(0, pos); + } + } + + // parse time + std::vector timeParts = splitString(v[1], ":"); + if (timeParts.size() < 2 || timeParts.size() > 3) + { + throw std::invalid_argument("Invalid time string: " + datetime); + } + hour = std::stol(timeParts[0]); + minute = std::stol(timeParts[1]); + if (timeParts.size() == 3) + { + auto secParts = splitString(timeParts[2], "."); + second = std::stol(secParts[0]); + // micro seconds + if (secParts.size() > 1) + { + if (secParts[1].length() > 6) + { + secParts[1].resize(6); + } + else if (secParts[1].length() < 6) + { + secParts[1].append(6 - secParts[1].length(), '0'); + } + microSecond = std::stol(secParts[1]); + } + } + + // timezone + if (v.size() >= 3) + { + std::string &tz = v[2]; + if (tzSign == 0) + { + if (tz[0] == '-') + { + tz = tz.substr(1); + tzSign = -1; + } + else + { + tzSign = 1; + } + } + + auto tzParts = splitString(tz, ":"); + if (tzParts.size() == 1 && tz.size() == 4) + { + tzParts = {tz.substr(0, 2), tz.substr(2)}; // 0800 + } + int tzHour = std::stoi(tzParts[0]); + int tzMin = tzParts.size() > 1 ? std::stoi(tzParts[1]) : 0; + tzOffset = tzSign * (tzHour * 3600 + tzMin * 60); + } + + return trantor::Date(year, month, day, hour, minute, second, microSecond) + .after(timezoneOffset() - tzOffset); +} + std::string Date::toCustomFormattedStringLocal(const std::string &fmtStr, bool showMicroseconds) const { diff --git a/trantor/utils/Date.h b/trantor/utils/Date.h index 4b1c8e60..a90a9e45 100644 --- a/trantor/utils/Date.h +++ b/trantor/utils/Date.h @@ -295,6 +295,17 @@ class TRANTOR_EXPORT Date */ static Date fromDbString(const std::string &datetime); + /** + * @brief Parse a datetime string + * Could be following format: + * - yyyy-mm-dd + * - yyyy-mm-dd HH:MM[:SS[.ffffff]] + * - yyyy-mm-dd HH:MM[:SS[.ffffff]][ ][+-]08 + * - yyyy-mm-dd HH:MM[:SS[.ffffff]][ ][+-]08:00 + * - yyyy-mm-dd HH:MM[:SS[.ffffff]][ ][+-]0800 + */ + static Date parseDatetimeTz(const std::string &datetime); + /* clang-format off */ /** * @brief Generate a UTC time string. From cfb49e05d6fa8b48e670be3d1760548c5dce4da1 Mon Sep 17 00:00:00 2001 From: "He, Wanchen" Date: Wed, 11 Dec 2024 13:27:00 +0800 Subject: [PATCH 2/2] more testcases. --- trantor/unittests/DateUnittest.cc | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/trantor/unittests/DateUnittest.cc b/trantor/unittests/DateUnittest.cc index c8a38709..a1c7c17f 100644 --- a/trantor/unittests/DateUnittest.cc +++ b/trantor/unittests/DateUnittest.cc @@ -1,6 +1,7 @@ #include #include #include +#include #include using namespace trantor; TEST(Date, constructorTest) @@ -110,15 +111,18 @@ TEST(Date, DatabaseStringTest) TEST(Date, TimezoneTest) { std::string str0 = "2024-01-01 04:00:00.123"; - std::string str1 = "2024-01-01 12:00:00.123 +08:00"; - std::string str2 = "2024-01-01 11:00:00.123+0700"; - std::string str3 = "2024-01-01 10:00:00.123 0600"; - std::string str4 = "2024-01-01 03:00:00.123 -01:00"; - std::string str5 = "2024-01-01 02:00:00.123-02:00"; - std::string str6 = "2024-01-01 01:00:00.123 -0300"; + std::vector strs{"2024-01-01 12:00:00.123 +08:00", + "2024-01-01 11:00:00.123+0700", + "2024-01-01 10:00:00.123 0600", + "2024-01-01 03:00:00.123 -01:00", + "2024-01-01 02:00:00.123-02:00", + "2024-01-01 01:00:00.123 -0300", + "2024-01-01 12:00:00.123 08", + "2024-01-01 02:00:00.123-02", + "2024-01-01 14:00:00.123+10"}; auto date = trantor::Date::fromDbString(str0); - for (auto &s : {str1, str2, str3, str4, str5, str6}) + for (auto &s : strs) { auto dateTz = trantor::Date::parseDatetimeTz(s); EXPECT_EQ(date.microSecondsSinceEpoch(),