Skip to content

Commit 20f51b5

Browse files
authored
feat(timezone): add US Eastern and Central ↔ GMT conversions with DST and tests
1 parent f38806d commit 20f51b5

File tree

4 files changed

+279
-1
lines changed

4 files changed

+279
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ All notable changes to this project will be documented in this file.
2020
- Unified `now_realtime_us()` precision across Windows and Unix by combining realtime anchors with monotonic clocks.
2121
- Added MQL5 counterparts for recent workday boundary and ISO parsing helpers.
2222
- Introduced templated NTP client pools, offline/fake testing paths, background runner helpers, and a singleton NTP time service with convenience wrappers.
23+
- Added US Eastern Time (ET/NY) to GMT (UTC) conversion helpers with DST rules.
24+
- Added US Central Time (CT) to GMT (UTC) conversion helpers for America/Chicago.
2325

2426
## [v1.0.4] - 2025-09-20
2527
- fix ODR violations in headers

include/time_shield/time_zone_conversions.hpp

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#define _TIME_SHIELD_TIME_ZONE_CONVERSIONS_HPP_INCLUDED
55

66
/// \file time_zone_conversions.hpp
7-
/// \brief Helpers for converting CET/EET timestamps to GMT.
7+
/// \brief Helpers for converting CET/EET/ET timestamps to GMT (UTC).
88
/// \ingroup time_zone_conversions
99

1010
#include "date_time_struct.hpp"
@@ -95,6 +95,109 @@ namespace time_shield {
9595
return cet_to_gmt(eet - SEC_PER_HOUR);
9696
}
9797

98+
/// \brief Check if local US Eastern time uses DST.
99+
/// \param dt Local time in ET.
100+
/// \return True if DST applies for the provided local timestamp.
101+
inline bool is_us_eastern_dst_local(const DateTimeStruct& dt) {
102+
const int SWITCH_HOUR = 2;
103+
int start_day = 0;
104+
int end_day = 0;
105+
int start_month = 0;
106+
int end_month = 0;
107+
108+
if(dt.year >= 2007) {
109+
start_month = MAR;
110+
end_month = NOV;
111+
int first_sunday_march = 1 + (DAYS_PER_WEEK - day_of_week_date(dt.year, MAR, 1)) % DAYS_PER_WEEK;
112+
start_day = first_sunday_march + 7;
113+
end_day = 1 + (DAYS_PER_WEEK - day_of_week_date(dt.year, NOV, 1)) % DAYS_PER_WEEK;
114+
} else {
115+
start_month = APR;
116+
end_month = OCT;
117+
start_day = 1 + (DAYS_PER_WEEK - day_of_week_date(dt.year, APR, 1)) % DAYS_PER_WEEK;
118+
end_day = last_sunday_month_day(dt.year, OCT);
119+
}
120+
121+
if(dt.mon > start_month && dt.mon < end_month) {
122+
return true;
123+
}
124+
if(dt.mon < start_month || dt.mon > end_month) {
125+
return false;
126+
}
127+
if(dt.mon == start_month) {
128+
if(dt.day > start_day) {
129+
return true;
130+
}
131+
if(dt.day < start_day) {
132+
return false;
133+
}
134+
return dt.hour >= SWITCH_HOUR;
135+
}
136+
if(dt.mon == end_month) {
137+
if(dt.day < end_day) {
138+
return true;
139+
}
140+
if(dt.day > end_day) {
141+
return false;
142+
}
143+
return dt.hour < SWITCH_HOUR;
144+
}
145+
return false;
146+
}
147+
148+
/// \brief Convert US Eastern Time (New York, EST/EDT) to GMT (UTC).
149+
/// \param et Timestamp in seconds in ET.
150+
/// \return Timestamp in seconds in GMT (UTC).
151+
///
152+
/// GMT in this library uses UTC. DST rules are guaranteed for 1987+;
153+
/// earlier years use 1987-2006 rules as a best-effort approximation.
154+
inline ts_t et_to_gmt(ts_t et) {
155+
DateTimeStruct dt = to_date_time(et);
156+
bool is_dst = is_us_eastern_dst_local(dt);
157+
return et + SEC_PER_HOUR * (is_dst ? 4 : 5);
158+
}
159+
160+
/// \brief Convert GMT (UTC) to US Eastern Time (New York, EST/EDT).
161+
/// \param gmt Timestamp in seconds in GMT (UTC).
162+
/// \return Timestamp in seconds in ET.
163+
///
164+
/// GMT in this library uses UTC. DST rules are guaranteed for 1987+;
165+
/// earlier years use 1987-2006 rules as a best-effort approximation.
166+
inline ts_t gmt_to_et(ts_t gmt) {
167+
ts_t et_standard = gmt - SEC_PER_HOUR * 5;
168+
DateTimeStruct dt_local = to_date_time(et_standard);
169+
bool is_dst = is_us_eastern_dst_local(dt_local);
170+
return gmt - SEC_PER_HOUR * (is_dst ? 4 : 5);
171+
}
172+
173+
/// \brief Convert New York Time to GMT (UTC).
174+
/// \param ny Timestamp in seconds in ET.
175+
/// \return Timestamp in seconds in GMT (UTC).
176+
inline ts_t ny_to_gmt(ts_t ny) {
177+
return et_to_gmt(ny);
178+
}
179+
180+
/// \brief Convert GMT (UTC) to New York Time.
181+
/// \param gmt Timestamp in seconds in GMT (UTC).
182+
/// \return Timestamp in seconds in ET.
183+
inline ts_t gmt_to_ny(ts_t gmt) {
184+
return gmt_to_et(gmt);
185+
}
186+
187+
/// \brief Convert US Central Time (America/Chicago, CST/CDT) to GMT (UTC).
188+
/// \param ct Timestamp in seconds in CT.
189+
/// \return Timestamp in seconds in GMT (UTC).
190+
inline ts_t ct_to_gmt(ts_t ct) {
191+
return et_to_gmt(ct + SEC_PER_HOUR);
192+
}
193+
194+
/// \brief Convert GMT (UTC) to US Central Time (America/Chicago, CST/CDT).
195+
/// \param gmt Timestamp in seconds in GMT (UTC).
196+
/// \return Timestamp in seconds in CT.
197+
inline ts_t gmt_to_ct(ts_t gmt) {
198+
return gmt_to_et(gmt) - SEC_PER_HOUR;
199+
}
200+
98201
/// \brief Convert Greenwich Mean Time to Central European Time.
99202
/// \param gmt Timestamp in seconds in GMT.
100203
/// \return Timestamp in seconds in CET.

tests/time_zone_conversion_test.cpp

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@
1111
int main() {
1212
using namespace time_shield;
1313

14+
const int SWITCH_HOUR = 2;
15+
16+
auto first_sunday_month_day = [](int year, int month) {
17+
return 1 + (DAYS_PER_WEEK - day_of_week_date(year, month, 1)) % DAYS_PER_WEEK;
18+
};
19+
20+
auto second_sunday_month_day = [&](int year, int month) {
21+
return first_sunday_month_day(year, month) + 7;
22+
};
23+
1424
ts_t cet_winter = to_timestamp(2023, 1, 1, 12, 0, 0);
1525
ts_t gmt_winter = cet_to_gmt(cet_winter);
1626
assert(gmt_winter == to_timestamp(2023, 1, 1, 11, 0, 0));
@@ -35,6 +45,52 @@ int main() {
3545
ts_t gmt_eet_summer = eet_to_gmt(eet_summer);
3646
assert(gmt_eet_summer == to_timestamp(2023, 7, 1, 9, 0, 0));
3747

48+
int spring_day_2024 = second_sunday_month_day(2024, MAR);
49+
ts_t et_before_spring = to_timestamp(2024, int(MAR), spring_day_2024, 1, 59, 59);
50+
ts_t gmt_before_spring = et_to_gmt(et_before_spring);
51+
assert(gmt_before_spring == to_timestamp(2024, int(MAR), spring_day_2024, 6, 59, 59));
52+
53+
ts_t gmt_spring_switch = to_timestamp(2024, int(MAR), spring_day_2024, 7, 0, 0);
54+
ts_t et_after_spring = gmt_to_et(gmt_spring_switch);
55+
assert(et_after_spring == to_timestamp(2024, int(MAR), spring_day_2024, 3, 0, 0));
56+
57+
int fall_day_2024 = first_sunday_month_day(2024, NOV);
58+
ts_t gmt_fall_before = to_timestamp(2024, int(NOV), fall_day_2024, 6, 59, 59);
59+
ts_t et_fall_before = gmt_to_et(gmt_fall_before);
60+
assert(gmt_fall_before - et_fall_before == SEC_PER_HOUR * 4);
61+
62+
ts_t gmt_fall_after = to_timestamp(2024, int(NOV), fall_day_2024, 7, 0, 0);
63+
ts_t et_fall_after = gmt_to_et(gmt_fall_after);
64+
assert(gmt_fall_after - et_fall_after == SEC_PER_HOUR * 5);
65+
66+
int spring_day_2006 = first_sunday_month_day(2006, APR);
67+
ts_t et_before_2006 = to_timestamp(2006, int(APR), spring_day_2006, 1, 59, 59);
68+
ts_t gmt_before_2006 = et_to_gmt(et_before_2006);
69+
assert(gmt_before_2006 == to_timestamp(2006, int(APR), spring_day_2006, 6, 59, 59));
70+
71+
ts_t et_after_2006 = to_timestamp(2006, int(APR), spring_day_2006, SWITCH_HOUR, 0, 0);
72+
ts_t gmt_after_2006 = et_to_gmt(et_after_2006);
73+
assert(gmt_after_2006 == to_timestamp(2006, int(APR), spring_day_2006, 6, 0, 0));
74+
75+
int fall_day_2006 = last_sunday_month_day(2006, OCT);
76+
ts_t et_fall_2006_before = to_timestamp(2006, int(OCT), fall_day_2006, 1, 59, 59);
77+
ts_t gmt_fall_2006_before = et_to_gmt(et_fall_2006_before);
78+
assert(gmt_fall_2006_before == to_timestamp(2006, int(OCT), fall_day_2006, 5, 59, 59));
79+
80+
ts_t et_fall_2006_after = to_timestamp(2006, int(OCT), fall_day_2006, SWITCH_HOUR, 0, 0);
81+
ts_t gmt_fall_2006_after = et_to_gmt(et_fall_2006_after);
82+
assert(gmt_fall_2006_after == to_timestamp(2006, int(OCT), fall_day_2006, 7, 0, 0));
83+
84+
ts_t et_round_trip_winter = to_timestamp(2024, 1, 15, 12, 0, 0);
85+
ts_t et_round_trip_summer = to_timestamp(2024, 7, 15, 12, 0, 0);
86+
assert(et_round_trip_winter == gmt_to_et(et_to_gmt(et_round_trip_winter)));
87+
assert(et_round_trip_summer == gmt_to_et(et_to_gmt(et_round_trip_summer)));
88+
89+
ts_t gmt_round_trip_winter = to_timestamp(2024, 1, 15, 17, 0, 0);
90+
ts_t gmt_round_trip_summer = to_timestamp(2024, 7, 15, 16, 0, 0);
91+
assert(gmt_round_trip_winter == et_to_gmt(gmt_to_et(gmt_round_trip_winter)));
92+
assert(gmt_round_trip_summer == et_to_gmt(gmt_to_et(gmt_round_trip_summer)));
93+
3894
for(int year : {2021, 2022, 2023, 2024}) {
3995
int day = last_sunday_month_day(year, OCT);
4096

@@ -77,6 +133,30 @@ int main() {
77133
(void)gmt_eet_winter;
78134
(void)eet_summer;
79135
(void)gmt_eet_summer;
136+
(void)spring_day_2024;
137+
(void)et_before_spring;
138+
(void)gmt_before_spring;
139+
(void)gmt_spring_switch;
140+
(void)et_after_spring;
141+
(void)fall_day_2024;
142+
(void)gmt_fall_before;
143+
(void)et_fall_before;
144+
(void)gmt_fall_after;
145+
(void)et_fall_after;
146+
(void)spring_day_2006;
147+
(void)et_before_2006;
148+
(void)gmt_before_2006;
149+
(void)et_after_2006;
150+
(void)gmt_after_2006;
151+
(void)fall_day_2006;
152+
(void)et_fall_2006_before;
153+
(void)gmt_fall_2006_before;
154+
(void)et_fall_2006_after;
155+
(void)gmt_fall_2006_after;
156+
(void)et_round_trip_winter;
157+
(void)et_round_trip_summer;
158+
(void)gmt_round_trip_winter;
159+
(void)gmt_round_trip_summer;
80160

81161
return 0;
82162
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#include <time_shield/time_zone_conversions.hpp>
2+
#include <time_shield/time_conversions.hpp>
3+
#include <cassert>
4+
5+
/// \brief Tests US ET/CT conversions to UTC including DST transitions.
6+
int main() {
7+
using namespace time_shield;
8+
9+
auto first_sunday_month_day = [](int year, int month) {
10+
return 1 + (DAYS_PER_WEEK - day_of_week_date(year, month, 1)) % DAYS_PER_WEEK;
11+
};
12+
13+
auto second_sunday_month_day = [&](int year, int month) {
14+
return first_sunday_month_day(year, month) + 7;
15+
};
16+
17+
ts_t ct_standard = to_timestamp(2024, 1, 15, 12, 0, 0);
18+
ts_t gmt_ct_standard = ct_to_gmt(ct_standard);
19+
assert(gmt_ct_standard == to_timestamp(2024, 1, 15, 18, 0, 0));
20+
21+
ts_t et_standard = to_timestamp(2024, 1, 15, 12, 0, 0);
22+
ts_t gmt_et_standard = et_to_gmt(et_standard);
23+
assert(gmt_et_standard == to_timestamp(2024, 1, 15, 17, 0, 0));
24+
25+
ts_t ct_summer = to_timestamp(2024, 7, 15, 12, 0, 0);
26+
ts_t gmt_ct_summer = ct_to_gmt(ct_summer);
27+
assert(gmt_ct_summer == to_timestamp(2024, 7, 15, 17, 0, 0));
28+
29+
ts_t et_summer = to_timestamp(2024, 7, 15, 12, 0, 0);
30+
ts_t gmt_et_summer = et_to_gmt(et_summer);
31+
assert(gmt_et_summer == to_timestamp(2024, 7, 15, 16, 0, 0));
32+
33+
ts_t ct_wrap_check = to_timestamp(2024, 6, 10, 9, 0, 0);
34+
assert(ct_to_gmt(ct_wrap_check) == et_to_gmt(ct_wrap_check + SEC_PER_HOUR));
35+
36+
ts_t gmt_wrap_check = to_timestamp(2024, 6, 10, 15, 0, 0);
37+
assert(gmt_to_ct(gmt_wrap_check) == gmt_to_et(gmt_wrap_check) - SEC_PER_HOUR);
38+
39+
int spring_day_2024 = second_sunday_month_day(2024, MAR);
40+
ts_t et_before_spring = to_timestamp(2024, int(MAR), spring_day_2024, 1, 59, 59);
41+
ts_t gmt_et_before_spring = et_to_gmt(et_before_spring);
42+
assert(gmt_et_before_spring == to_timestamp(2024, int(MAR), spring_day_2024, 6, 59, 59));
43+
44+
ts_t et_after_spring = to_timestamp(2024, int(MAR), spring_day_2024, 3, 0, 0);
45+
ts_t gmt_et_after_spring = et_to_gmt(et_after_spring);
46+
assert(gmt_et_after_spring == to_timestamp(2024, int(MAR), spring_day_2024, 7, 0, 0));
47+
48+
ts_t ct_before_spring = to_timestamp(2024, int(MAR), spring_day_2024, 1, 59, 59);
49+
ts_t gmt_before_spring = ct_to_gmt(ct_before_spring);
50+
assert(gmt_before_spring == to_timestamp(2024, int(MAR), spring_day_2024, 6, 59, 59));
51+
52+
ts_t ct_after_spring = to_timestamp(2024, int(MAR), spring_day_2024, 3, 0, 0);
53+
ts_t gmt_after_spring = ct_to_gmt(ct_after_spring);
54+
assert(gmt_after_spring == to_timestamp(2024, int(MAR), spring_day_2024, 8, 0, 0));
55+
56+
int fall_day_2024 = first_sunday_month_day(2024, NOV);
57+
ts_t et_repeat_hour = to_timestamp(2024, int(NOV), fall_day_2024, 1, 30, 0);
58+
ts_t gmt_et_repeat_hour = et_to_gmt(et_repeat_hour);
59+
assert(gmt_et_repeat_hour == to_timestamp(2024, int(NOV), fall_day_2024, 5, 30, 0));
60+
61+
ts_t ct_repeat_hour = to_timestamp(2024, int(NOV), fall_day_2024, 1, 30, 0);
62+
ts_t gmt_repeat_hour = ct_to_gmt(ct_repeat_hour);
63+
assert(gmt_repeat_hour == to_timestamp(2024, int(NOV), fall_day_2024, 7, 30, 0));
64+
65+
ts_t ct_after_fall = to_timestamp(2024, int(NOV), fall_day_2024, 2, 0, 0);
66+
ts_t gmt_after_fall = ct_to_gmt(ct_after_fall);
67+
assert(gmt_after_fall == to_timestamp(2024, int(NOV), fall_day_2024, 8, 0, 0));
68+
69+
(void)gmt_ct_standard;
70+
(void)gmt_et_standard;
71+
(void)gmt_ct_summer;
72+
(void)gmt_et_summer;
73+
(void)ct_wrap_check;
74+
(void)gmt_wrap_check;
75+
(void)spring_day_2024;
76+
(void)et_before_spring;
77+
(void)gmt_et_before_spring;
78+
(void)et_after_spring;
79+
(void)gmt_et_after_spring;
80+
(void)ct_before_spring;
81+
(void)gmt_before_spring;
82+
(void)ct_after_spring;
83+
(void)gmt_after_spring;
84+
(void)fall_day_2024;
85+
(void)et_repeat_hour;
86+
(void)gmt_et_repeat_hour;
87+
(void)ct_repeat_hour;
88+
(void)gmt_repeat_hour;
89+
(void)ct_after_fall;
90+
(void)gmt_after_fall;
91+
92+
return 0;
93+
}

0 commit comments

Comments
 (0)