From 63f573ba26f8b4df3ed7580a8d1f3a53bb1226dc Mon Sep 17 00:00:00 2001 From: "He, Wanchen" Date: Wed, 11 Dec 2024 12:22:30 +0800 Subject: [PATCH 1/4] 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 9d516a75..f25f8774 100644 --- a/trantor/unittests/DateUnittest.cc +++ b/trantor/unittests/DateUnittest.cc @@ -112,6 +112,25 @@ TEST(Date, DatabaseStringTest) auto epoch = dbDateGMT.microSecondsSinceEpoch(); EXPECT_EQ(epoch, 0); } +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 26acf794..9e779291 100644 --- a/trantor/utils/Date.cc +++ b/trantor/utils/Date.cc @@ -355,6 +355,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 4684227d..ba2f98c0 100644 --- a/trantor/utils/Date.h +++ b/trantor/utils/Date.h @@ -293,6 +293,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 d66a8de58bcb6efcc32293a9501883a92fc9c869 Mon Sep 17 00:00:00 2001 From: "He, Wanchen" Date: Wed, 11 Dec 2024 13:27:00 +0800 Subject: [PATCH 2/4] 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 f25f8774..10957bb6 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) @@ -115,15 +116,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(), From cb7b7825e34530021b8e6800142f50b340a0b3af Mon Sep 17 00:00:00 2001 From: "He, Wanchen" Date: Tue, 9 Dec 2025 17:03:06 +0800 Subject: [PATCH 3/4] Better. --- trantor/utils/Date.cc | 7 +++++++ trantor/utils/Date.h | 12 ++---------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/trantor/utils/Date.cc b/trantor/utils/Date.cc index 9e779291..a710c75b 100644 --- a/trantor/utils/Date.cc +++ b/trantor/utils/Date.cc @@ -63,6 +63,13 @@ const Date Date::date() return Date(seconds * MICRO_SECONDS_PER_SEC + tv.tv_usec); #endif } + +int64_t Date::timezoneOffset() +{ + static int64_t offset = -Date(1970, 1, 1).secondsSinceEpoch(); + return offset; +} + const Date Date::after(double second) const { return Date(static_cast(microSecondsSinceEpoch_ + diff --git a/trantor/utils/Date.h b/trantor/utils/Date.h index ba2f98c0..c1cab6b3 100644 --- a/trantor/utils/Date.h +++ b/trantor/utils/Date.h @@ -72,13 +72,7 @@ class TRANTOR_EXPORT Date return Date::date(); } - static int64_t timezoneOffset() - { - static int64_t offset = -( - Date::fromDbStringLocal("1970-01-03 00:00:00").secondsSinceEpoch() - - 2LL * 3600LL * 24LL); - return offset; - } + static int64_t timezoneOffset(); /** * @brief Return a new Date instance that represents the time after some @@ -298,9 +292,7 @@ class TRANTOR_EXPORT Date * 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 + * - yyyy-mm-dd HH:MM[:SS[.ffffff]][ ][+-]08[[:]00] */ static Date parseDatetimeTz(const std::string &datetime); From 087c9219310e0aadeca5a90d52c995fbcf0fb94f Mon Sep 17 00:00:00 2001 From: "He, Wanchen" Date: Tue, 9 Dec 2025 18:07:03 +0800 Subject: [PATCH 4/4] Only consider tz when there is any. --- trantor/utils/Date.cc | 69 +++++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/trantor/utils/Date.cc b/trantor/utils/Date.cc index a710c75b..f00c97c4 100644 --- a/trantor/utils/Date.cc +++ b/trantor/utils/Date.cc @@ -362,6 +362,36 @@ Date Date::fromDbString(const std::string &datetime) static_cast(timezoneOffset())); } +int parseTzOffset(std::string &tz, int tzSign) +{ + if (tzSign == 0) + { + if (tz[0] == '-') + { + tz = tz.substr(1); + tzSign = -1; + } + else + { + tzSign = 1; + } + } + + if (tz == "Z") + { + return 0; + } + + 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; + return tzSign * (tzHour * 3600 + tzMin * 60); +} + Date Date::parseDatetimeTz(const std::string &datetime) { unsigned int year = {0}, month = {0}, day = {0}, hour = {0}, minute = {0}, @@ -405,6 +435,12 @@ Date Date::parseDatetimeTz(const std::string &datetime) v.push_back(v[1].substr(pos + 1)); v[1] = v[1].substr(0, pos); } + else if (!v[1].empty() && v[1].back() == 'Z') + { + v[1].pop_back(); + tzSign = 1; + v.emplace_back("Z"); + } } // parse time @@ -434,35 +470,18 @@ Date Date::parseDatetimeTz(const std::string &datetime) } } + trantor::Date dt(year, month, day, hour, minute, second, microSecond); + // 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); + tzOffset = parseTzOffset(v[2], tzSign); + return dt.after(timezoneOffset() - tzOffset); + } + else + { + return dt; } - - return trantor::Date(year, month, day, hour, minute, second, microSecond) - .after(timezoneOffset() - tzOffset); } std::string Date::toCustomFormattedStringLocal(const std::string &fmtStr,