1+ #ifndef SPDLOG_NO_TZ_OFFSET
2+
3+ #include " includes.h"
4+ #include < ctime>
5+ #include < cstdlib>
6+ #include < cstring>
7+
8+ // Helper to construct a simple std::tm from components
9+ std::tm make_tm (int year, int month, int day, int hour, int minute) {
10+ std::tm t;
11+ std::memset (&t, 0 , sizeof (t));
12+ t.tm_year = year - 1900 ;
13+ t.tm_mon = month - 1 ;
14+ t.tm_mday = day;
15+ t.tm_hour = hour;
16+ t.tm_min = minute;
17+ t.tm_sec = 0 ;
18+ t.tm_isdst = -1 ;
19+ std::mktime (&t);
20+ return t;
21+ }
22+
23+ // Cross-platform RAII Helper to safely set/restore process timezone
24+ class ScopedTZ {
25+ std::string original_tz_;
26+ bool has_original_ = false ;
27+
28+ public:
29+ explicit ScopedTZ (const std::string& tz_name) {
30+ // save current TZ
31+ #ifdef _WIN32
32+ char * buf = nullptr ;
33+ size_t len = 0 ;
34+ if (_dupenv_s (&buf, &len, " TZ" ) == 0 && buf != nullptr ) {
35+ original_tz_ = std::string (buf);
36+ has_original_ = true ;
37+ free (buf);
38+ }
39+ #else
40+ const char * tz = std::getenv (" TZ" );
41+ if (tz) {
42+ original_tz_ = tz;
43+ has_original_ = true ;
44+ }
45+ #endif
46+
47+ // set new TZ
48+ #ifdef _WIN32
49+ _putenv_s (" TZ" , tz_name.c_str ());
50+ _tzset ();
51+ #else
52+ setenv (" TZ" , tz_name.c_str (), 1 );
53+ tzset ();
54+ #endif
55+ }
56+
57+ ~ScopedTZ () {
58+ // restore original TZ
59+ #ifdef _WIN32
60+ if (has_original_) {
61+ _putenv_s (" TZ" , original_tz_.c_str ());
62+ } else {
63+ _putenv_s (" TZ" , " " );
64+ }
65+ _tzset ();
66+ #else
67+ if (has_original_) {
68+ setenv (" TZ" , original_tz_.c_str (), 1 );
69+ } else {
70+ unsetenv (" TZ" );
71+ }
72+ tzset ();
73+ #endif
74+ }
75+ };
76+
77+ using spdlog::details::os::utc_minutes_offset;
78+
79+ TEST_CASE (" UTC Offset - Western Hemisphere (USA - Standard Time)" , " [timezone][west]" ) {
80+ // EST5EDT: Eastern Standard Time (UTC-5)
81+ ScopedTZ tz (" EST5EDT" );
82+
83+ // Jan 15th (Winter)
84+ auto tm = make_tm (2023 , 1 , 15 , 12 , 0 );
85+ REQUIRE (utc_minutes_offset (tm) == -300 );
86+ }
87+
88+ TEST_CASE (" UTC Offset - Eastern Hemisphere (Europe/Israel - Standard Time)" , " [timezone][east]" ) {
89+ // IST-2IDT: Israel Standard Time (UTC+2)
90+ ScopedTZ tz (" IST-2IDT" );
91+
92+ // Jan 15th (Winter)
93+ auto tm = make_tm (2023 , 1 , 15 , 12 , 0 );
94+ REQUIRE (utc_minutes_offset (tm) == 120 );
95+ }
96+
97+ TEST_CASE (" UTC Offset - Zero Offset (UTC/GMT)" , " [timezone][utc]" ) {
98+ ScopedTZ tz (" GMT0" );
99+
100+ // Check Winter
101+ auto tm_winter = make_tm (2023 , 1 , 15 , 12 , 0 );
102+ REQUIRE (utc_minutes_offset (tm_winter) == 0 );
103+
104+ // Check Summer (GMT never shifts, so this should also be 0)
105+ auto tm_summer = make_tm (2023 , 7 , 15 , 12 , 0 );
106+ REQUIRE (utc_minutes_offset (tm_summer) == 0 );
107+ }
108+
109+ TEST_CASE (" UTC Offset - Non-Integer Hour Offsets (India)" , " [timezone][partial]" ) {
110+ // IST-5:30: India Standard Time (UTC+5:30)
111+ ScopedTZ tz (" IST-5:30" );
112+
113+ auto tm = make_tm (2023 , 1 , 15 , 12 , 0 );
114+ REQUIRE (utc_minutes_offset (tm) == 330 );
115+ }
116+
117+ TEST_CASE (" UTC Offset - Edge Case: Negative Offset Crossing Midnight" , " [timezone][edge]" ) {
118+ ScopedTZ tz (" EST5EDT" );
119+ // Late night Dec 31st, 2023
120+ auto tm = make_tm (2023 , 12 , 31 , 23 , 59 );
121+ REQUIRE (utc_minutes_offset (tm) == -300 );
122+ }
123+
124+ TEST_CASE (" UTC Offset - Edge Case: Leap Year" , " [timezone][edge]" ) {
125+ ScopedTZ tz (" EST5EDT" );
126+ // Feb 29, 2024 (Leap Day) - Winter
127+ auto tm = make_tm (2024 , 2 , 29 , 12 , 0 );
128+ REQUIRE (utc_minutes_offset (tm) == -300 );
129+ }
130+
131+ TEST_CASE (" UTC Offset - Edge Case: Invalid Date (Pre-Epoch)" , " [timezone][edge]" ) {
132+ #ifdef _WIN32
133+ // Windows mktime returns -1 for dates before 1970.
134+ // We expect the function to safely return 0 (fallback).
135+ auto tm = make_tm (1960 , 1 , 1 , 12 , 0 );
136+ REQUIRE (utc_minutes_offset (tm) == 0 );
137+ #else
138+ // Unix mktime handles pre-1970 dates correctly.
139+ // We expect the actual historical offset (EST was UTC-5 in 1960).
140+ ScopedTZ tz (" EST5EDT" );
141+ auto tm = make_tm (1960 , 1 , 1 , 12 , 0 );
142+ REQUIRE (utc_minutes_offset (tm) == -300 );
143+ #endif
144+ }
145+
146+ #endif // !SPDLOG_NO_TZ_OFFSET
0 commit comments