#ifndef SPDLOG_NO_TZ_OFFSET #include "includes.h" #include #include #include // Helper to construct a simple std::tm from components std::tm make_tm(int year, int month, int day, int hour, int minute) { std::tm t; std::memset(&t, 0, sizeof(t)); t.tm_year = year - 1900; t.tm_mon = month - 1; t.tm_mday = day; t.tm_hour = hour; t.tm_min = minute; t.tm_sec = 0; t.tm_isdst = -1; std::mktime(&t); return t; } // Cross-platform RAII Helper to safely set/restore process timezone class ScopedTZ { std::string original_tz_; bool has_original_ = false; public: explicit ScopedTZ(const std::string& tz_name) { // save current TZ #ifdef _WIN32 char* buf = nullptr; size_t len = 0; if (_dupenv_s(&buf, &len, "TZ") == 0 && buf != nullptr) { original_tz_ = std::string(buf); has_original_ = true; free(buf); } #else const char* tz = std::getenv("TZ"); if (tz) { original_tz_ = tz; has_original_ = true; } #endif // set new TZ #ifdef _WIN32 _putenv_s("TZ", tz_name.c_str()); _tzset(); #else setenv("TZ", tz_name.c_str(), 1); tzset(); #endif } ~ScopedTZ() { // restore original TZ #ifdef _WIN32 if (has_original_) { _putenv_s("TZ", original_tz_.c_str()); } else { _putenv_s("TZ", ""); } _tzset(); #else if (has_original_) { setenv("TZ", original_tz_.c_str(), 1); } else { unsetenv("TZ"); } tzset(); #endif } }; using spdlog::details::os::utc_minutes_offset; /* * POSIX 2024 defines three formats for the TZ environment variable, * * 1. Implementation defined format which always starts with a colon: * ":characters". * 2. A specifier which fully describes the timezone rule in format * "stdoffset[dst[offset][,start[/time],end[/time]]]". Note the * offset and start/end part could be omitted, in which case one hour * is implied, or it's considered implementation-defined when changing * to and from Daylight Saving Time occurs. * 3. Geographical or special timezone from an implementation-defined * timezone database. * * On POSIX-compilant systems, we prefer format 2, and explicitly specify the * DST rules to avoid implementation-defined behavior. * * See also IEEE 1003.1-2024 8.3 Other Environment Variables. */ #ifndef _WIN32 /* * Standard time is UTC-5 ("EST"), DST time is UTC-4 ("EDT"). DST is active * from 2:00 on the 2nd Sunday in March, to 2:00 on 1st Sunday in November. */ #define EST5EDT "EST5EDT,M3.2.0,M11.1.0" /* * Standard time is UTC+2 ("IST"), DST time is UTC+3 ("IDT"). DST is active * from 2:00 on following day of the 4th Thursday in March, to 2:00 on the * last Sunday in October. */ #define IST_MINUS2_IDT "IST-2IDT,M3.4.4/26,M10.5.0" #else /* * However, Windows doesn't follow the POSIX rules and only accept a TZ * environment variable in format * * tzn [+|-]hh[:mm[:ss] ][dzn] * * thus we couldn't specify the DST rules. Luckily, Windows C runtime library * assumes the United State's rules for implementing the calculation of DST, * which is fine for our test cases. * * See also https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/tzset?view=msvc-170 */ #define EST5EDT "EST5EDT" #define IST_MINUS2_IDT "IST-2IDT" #endif TEST_CASE("UTC Offset - Western Hemisphere (USA - Standard Time)", "[timezone][west]") { // EST5EDT: Eastern Standard Time (UTC-5) ScopedTZ tz(EST5EDT); // Jan 15th (Winter) auto tm = make_tm(2023, 1, 15, 12, 0); REQUIRE(utc_minutes_offset(tm) == -300); } TEST_CASE("UTC Offset - Eastern Hemisphere (Europe/Israel - Standard Time)", "[timezone][east]") { // IST-2IDT: Israel Standard Time (UTC+2) ScopedTZ tz(IST_MINUS2_IDT); // Jan 15th (Winter) auto tm = make_tm(2023, 1, 15, 12, 0); REQUIRE(utc_minutes_offset(tm) == 120); } TEST_CASE("UTC Offset - Zero Offset (UTC/GMT)", "[timezone][utc]") { ScopedTZ tz("GMT0"); // Check Winter auto tm_winter = make_tm(2023, 1, 15, 12, 0); REQUIRE(utc_minutes_offset(tm_winter) == 0); // Check Summer (GMT never shifts, so this should also be 0) auto tm_summer = make_tm(2023, 7, 15, 12, 0); REQUIRE(utc_minutes_offset(tm_summer) == 0); } TEST_CASE("UTC Offset - Non-Integer Hour Offsets (India)", "[timezone][partial]") { // IST-5:30: India Standard Time (UTC+5:30) ScopedTZ tz("IST-5:30"); auto tm = make_tm(2023, 1, 15, 12, 0); REQUIRE(utc_minutes_offset(tm) == 330); } TEST_CASE("UTC Offset - Edge Case: Negative Offset Crossing Midnight", "[timezone][edge]") { ScopedTZ tz(EST5EDT); // Late night Dec 31st, 2023 auto tm = make_tm(2023, 12, 31, 23, 59); REQUIRE(utc_minutes_offset(tm) == -300); } TEST_CASE("UTC Offset - Edge Case: Leap Year", "[timezone][edge]") { ScopedTZ tz(EST5EDT); // Feb 29, 2024 (Leap Day) - Winter auto tm = make_tm(2024, 2, 29, 12, 0); REQUIRE(utc_minutes_offset(tm) == -300); } TEST_CASE("UTC Offset - Edge Case: Invalid Date (Pre-Epoch)", "[timezone][edge]") { #ifdef _WIN32 // Windows mktime returns -1 for dates before 1970. // We expect the function to safely return 0 (fallback). auto tm = make_tm(1960, 1, 1, 12, 0); REQUIRE(utc_minutes_offset(tm) == 0); #else // Unix mktime handles pre-1970 dates correctly. // We expect the actual historical offset (EST was UTC-5 in 1960). ScopedTZ tz(EST5EDT); auto tm = make_tm(1960, 1, 1, 12, 0); REQUIRE(utc_minutes_offset(tm) == -300); #endif } #endif // !SPDLOG_NO_TZ_OFFSET