diff --git a/trantor/unittests/DateUnittest.cc b/trantor/unittests/DateUnittest.cc index 9d516a75..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) @@ -112,6 +113,28 @@ 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::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 : strs) + { + 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..f00c97c4 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_ + @@ -355,6 +362,128 @@ 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}, + 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); + } + else if (!v[1].empty() && v[1].back() == 'Z') + { + v[1].pop_back(); + tzSign = 1; + v.emplace_back("Z"); + } + } + + // 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]); + } + } + + trantor::Date dt(year, month, day, hour, minute, second, microSecond); + + // timezone + if (v.size() >= 3) + { + tzOffset = parseTzOffset(v[2], tzSign); + return dt.after(timezoneOffset() - tzOffset); + } + else + { + return dt; + } +} + 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..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 @@ -293,6 +287,15 @@ 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[[:]00] + */ + static Date parseDatetimeTz(const std::string &datetime); + /* clang-format off */ /** * @brief Generate a UTC time string.