mirror of
https://github.com/gabime/spdlog.git
synced 2026-04-10 11:34:29 +08:00
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.
POSIX 2024 requires the format 1 and 2 to take precedence over format 3.
In tests/test_timezone.cpp, we set TZ to "EST5EDT" or "IST-2IDT".
According to POSIX, "EST5EDT" should be interpreted as
- timezone "EST", which is five hours behind UTC
- corresponding DST timezone is "EDT", which is one hour ahead of
standard time
- it's implementation-defined when changing to and from DST occurs
The interpretion is similar for TZ="IST-2IDT". Obviously we're hitting
implementation-defined behavior here, which is inconsistent across
platforms, e.g., musl considers DST is always active if both DST start
and end rules are omitted, thus test_timezone.cpp would fail.
Let's also provide DST rules when setting TZ variables to avoid
depending on implementation-defined behavior.
Fixes: b656d1ceec ("Windows utc_minutes_offset(): Fix historical DST accuracy and improve offset calculation speed (~2.5x) (#3508)")
Signed-off-by: Yao Zi <me@ziyao.cc>
194 lines
5.6 KiB
C++
194 lines
5.6 KiB
C++
#ifndef SPDLOG_NO_TZ_OFFSET
|
|
|
|
#include "includes.h"
|
|
#include <ctime>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
|
|
// 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
|