1use super::components::weekday;
4use super::js_date::JsDate;
5use std::collections::BTreeMap;
6
7pub type DstRule = fn(i64) -> JsDate;
9
10#[derive(Clone, Copy, Debug)]
12pub enum TimezoneSpec {
13 Fixed(i64),
15 Ambiguous {
17 offset_during_dst: i64,
18 offset_non_dst: i64,
19 dst_start: DstRule,
20 dst_end: DstRule,
21 },
22}
23
24impl PartialEq for TimezoneSpec {
25 fn eq(&self, other: &Self) -> bool {
26 use std::ptr::fn_addr_eq;
27 match (self, other) {
28 (Fixed(a), Fixed(b)) => a == b,
29 (
30 TimezoneSpec::Ambiguous {
31 offset_during_dst: a1,
32 offset_non_dst: a2,
33 dst_start: a3,
34 dst_end: a4,
35 },
36 TimezoneSpec::Ambiguous {
37 offset_during_dst: b1,
38 offset_non_dst: b2,
39 dst_start: b3,
40 dst_end: b4,
41 },
42 ) => a1 == b1 && a2 == b2 && fn_addr_eq(*a3, *b3) && fn_addr_eq(*a4, *b4),
43 _ => false,
44 }
45 }
46}
47
48pub type TimezoneAbbrMap = BTreeMap<String, TimezoneSpec>;
50
51#[derive(Clone, Debug, PartialEq, Eq)]
53pub enum TimezoneInput {
54 Abbr(String),
55 Minutes(i64),
56}
57
58use TimezoneSpec::Fixed;
59
60fn cet_dst_start(year: i64) -> JsDate {
61 get_last_weekday_of_month(year, 3, weekday::SUNDAY, 2)
62}
63fn cet_dst_end(year: i64) -> JsDate {
64 get_last_weekday_of_month(year, 10, weekday::SUNDAY, 3)
65}
66fn us_dst_start(year: i64) -> JsDate {
67 get_nth_weekday_of_month(year, 3, weekday::SUNDAY, 2, 2)
68}
69fn us_dst_end(year: i64) -> JsDate {
70 get_nth_weekday_of_month(year, 11, weekday::SUNDAY, 1, 2)
71}
72
73const fn us(during_dst: i64, non_dst: i64) -> TimezoneSpec {
74 TimezoneSpec::Ambiguous {
75 offset_during_dst: during_dst,
76 offset_non_dst: non_dst,
77 dst_start: us_dst_start,
78 dst_end: us_dst_end,
79 }
80}
81
82pub static TIMEZONE_ABBR_MAP: &[(&str, TimezoneSpec)] = &[
84 (
85 "CET",
86 TimezoneSpec::Ambiguous {
87 offset_during_dst: 120,
88 offset_non_dst: 60,
89 dst_start: cet_dst_start,
90 dst_end: cet_dst_end,
91 },
92 ),
93 ("CT", us(-300, -360)),
94 ("ET", us(-240, -300)),
95 ("MT", us(-360, -420)),
96 ("PT", us(-420, -480)),
97 ("ACDT", Fixed(630)),
98 ("ACST", Fixed(570)),
99 ("ADT", Fixed(-180)),
100 ("AEDT", Fixed(660)),
101 ("AEST", Fixed(600)),
102 ("AFT", Fixed(270)),
103 ("AKDT", Fixed(-480)),
104 ("AKST", Fixed(-540)),
105 ("ALMT", Fixed(360)),
106 ("AMST", Fixed(-180)),
107 ("AMT", Fixed(-240)),
108 ("ANAST", Fixed(720)),
109 ("ANAT", Fixed(720)),
110 ("AQTT", Fixed(300)),
111 ("ART", Fixed(-180)),
112 ("AST", Fixed(-240)),
113 ("AWDT", Fixed(540)),
114 ("AWST", Fixed(480)),
115 ("AZOST", Fixed(0)),
116 ("AZOT", Fixed(-60)),
117 ("AZST", Fixed(300)),
118 ("AZT", Fixed(240)),
119 ("BNT", Fixed(480)),
120 ("BOT", Fixed(-240)),
121 ("BRST", Fixed(-120)),
122 ("BRT", Fixed(-180)),
123 ("BST", Fixed(60)),
124 ("BTT", Fixed(360)),
125 ("CAST", Fixed(480)),
126 ("CAT", Fixed(120)),
127 ("CCT", Fixed(390)),
128 ("CDT", Fixed(-300)),
129 ("CEST", Fixed(120)),
130 ("timezoneOffsetDuringDst", Fixed(120)),
131 ("timezoneOffsetNonDst", Fixed(60)),
132 ("CHADT", Fixed(825)),
133 ("CHAST", Fixed(765)),
134 ("CKT", Fixed(-600)),
135 ("CLST", Fixed(-180)),
136 ("CLT", Fixed(-240)),
137 ("COT", Fixed(-300)),
138 ("CST", Fixed(-360)),
139 ("timezoneOffsetDuringDst", Fixed(-300)),
140 ("timezoneOffsetNonDst", Fixed(-360)),
141 ("CVT", Fixed(-60)),
142 ("CXT", Fixed(420)),
143 ("ChST", Fixed(600)),
144 ("DAVT", Fixed(420)),
145 ("EASST", Fixed(-300)),
146 ("EAST", Fixed(-360)),
147 ("EAT", Fixed(180)),
148 ("ECT", Fixed(-300)),
149 ("EDT", Fixed(-240)),
150 ("EEST", Fixed(180)),
151 ("EET", Fixed(120)),
152 ("EGST", Fixed(0)),
153 ("EGT", Fixed(-60)),
154 ("EST", Fixed(-300)),
155 ("timezoneOffsetDuringDst", Fixed(-240)),
156 ("timezoneOffsetNonDst", Fixed(-300)),
157 ("FJST", Fixed(780)),
158 ("FJT", Fixed(720)),
159 ("FKST", Fixed(-180)),
160 ("FKT", Fixed(-240)),
161 ("FNT", Fixed(-120)),
162 ("GALT", Fixed(-360)),
163 ("GAMT", Fixed(-540)),
164 ("GET", Fixed(240)),
165 ("GFT", Fixed(-180)),
166 ("GILT", Fixed(720)),
167 ("GMT", Fixed(0)),
168 ("GST", Fixed(240)),
169 ("GYT", Fixed(-240)),
170 ("HAA", Fixed(-180)),
171 ("HAC", Fixed(-300)),
172 ("HADT", Fixed(-540)),
173 ("HAE", Fixed(-240)),
174 ("HAP", Fixed(-420)),
175 ("HAR", Fixed(-360)),
176 ("HAST", Fixed(-600)),
177 ("HAT", Fixed(-90)),
178 ("HAY", Fixed(-480)),
179 ("HKT", Fixed(480)),
180 ("HLV", Fixed(-210)),
181 ("HNA", Fixed(-240)),
182 ("HNC", Fixed(-360)),
183 ("HNE", Fixed(-300)),
184 ("HNP", Fixed(-480)),
185 ("HNR", Fixed(-420)),
186 ("HNT", Fixed(-150)),
187 ("HNY", Fixed(-540)),
188 ("HOVT", Fixed(420)),
189 ("ICT", Fixed(420)),
190 ("IDT", Fixed(180)),
191 ("IOT", Fixed(360)),
192 ("IRDT", Fixed(270)),
193 ("IRKST", Fixed(540)),
194 ("IRKT", Fixed(540)),
195 ("IRST", Fixed(210)),
196 ("IST", Fixed(330)),
197 ("JST", Fixed(540)),
198 ("KGT", Fixed(360)),
199 ("KRAST", Fixed(480)),
200 ("KRAT", Fixed(480)),
201 ("KST", Fixed(540)),
202 ("KUYT", Fixed(240)),
203 ("LHDT", Fixed(660)),
204 ("LHST", Fixed(630)),
205 ("LINT", Fixed(840)),
206 ("MAGST", Fixed(720)),
207 ("MAGT", Fixed(720)),
208 ("MART", Fixed(-510)),
209 ("MAWT", Fixed(300)),
210 ("MDT", Fixed(-360)),
211 ("MESZ", Fixed(120)),
212 ("MEZ", Fixed(60)),
213 ("MHT", Fixed(720)),
214 ("MMT", Fixed(390)),
215 ("MSD", Fixed(240)),
216 ("MSK", Fixed(180)),
217 ("MST", Fixed(-420)),
218 ("timezoneOffsetDuringDst", Fixed(-360)),
219 ("timezoneOffsetNonDst", Fixed(-420)),
220 ("MUT", Fixed(240)),
221 ("MVT", Fixed(300)),
222 ("MYT", Fixed(480)),
223 ("NCT", Fixed(660)),
224 ("NDT", Fixed(-90)),
225 ("NFT", Fixed(690)),
226 ("NOVST", Fixed(420)),
227 ("NOVT", Fixed(360)),
228 ("NPT", Fixed(345)),
229 ("NST", Fixed(-150)),
230 ("NUT", Fixed(-660)),
231 ("NZDT", Fixed(780)),
232 ("NZST", Fixed(720)),
233 ("OMSST", Fixed(420)),
234 ("OMST", Fixed(420)),
235 ("PDT", Fixed(-420)),
236 ("PET", Fixed(-300)),
237 ("PETST", Fixed(720)),
238 ("PETT", Fixed(720)),
239 ("PGT", Fixed(600)),
240 ("PHOT", Fixed(780)),
241 ("PHT", Fixed(480)),
242 ("PKT", Fixed(300)),
243 ("PMDT", Fixed(-120)),
244 ("PMST", Fixed(-180)),
245 ("PONT", Fixed(660)),
246 ("PST", Fixed(-480)),
247 ("timezoneOffsetDuringDst", Fixed(-420)),
248 ("timezoneOffsetNonDst", Fixed(-480)),
249 ("PWT", Fixed(540)),
250 ("PYST", Fixed(-180)),
251 ("PYT", Fixed(-240)),
252 ("RET", Fixed(240)),
253 ("SAMT", Fixed(240)),
254 ("SAST", Fixed(120)),
255 ("SBT", Fixed(660)),
256 ("SCT", Fixed(240)),
257 ("SGT", Fixed(480)),
258 ("SRT", Fixed(-180)),
259 ("SST", Fixed(-660)),
260 ("TAHT", Fixed(-600)),
261 ("TFT", Fixed(300)),
262 ("TJT", Fixed(300)),
263 ("TKT", Fixed(780)),
264 ("TLT", Fixed(540)),
265 ("TMT", Fixed(300)),
266 ("TVT", Fixed(720)),
267 ("ULAT", Fixed(480)),
268 ("UTC", Fixed(0)),
269 ("UYST", Fixed(-120)),
270 ("UYT", Fixed(-180)),
271 ("UZT", Fixed(300)),
272 ("VET", Fixed(-210)),
273 ("VLAST", Fixed(660)),
274 ("VLAT", Fixed(660)),
275 ("VUT", Fixed(660)),
276 ("WAST", Fixed(120)),
277 ("WAT", Fixed(60)),
278 ("WEST", Fixed(60)),
279 ("WESZ", Fixed(60)),
280 ("WET", Fixed(0)),
281 ("WEZ", Fixed(0)),
282 ("WFT", Fixed(720)),
283 ("WGST", Fixed(-120)),
284 ("WGT", Fixed(-180)),
285 ("WIB", Fixed(420)),
286 ("WIT", Fixed(540)),
287 ("WITA", Fixed(480)),
288 ("WST", Fixed(780)),
289 ("WT", Fixed(0)),
290 ("YAKST", Fixed(600)),
291 ("YAKT", Fixed(600)),
292 ("YAPT", Fixed(600)),
293 ("YEKST", Fixed(360)),
294 ("YEKT", Fixed(360)),
295];
296
297pub fn lookup(abbr: &str) -> Option<TimezoneSpec> {
298 TIMEZONE_ABBR_MAP.iter().find(|(k, _)| *k == abbr).map(|(_, v)| *v)
299}
300
301pub fn get_nth_weekday_of_month(year: i64, month: i64, weekday: i64, n: i64, hour: i64) -> JsDate {
303 let mut day_of_month = 0;
304 let mut i = 0;
305 while i < n {
306 day_of_month += 1;
307 if JsDate::local(year, month - 1, day_of_month, 0, 0, 0, 0).get_day() == weekday {
308 i += 1;
309 }
310 }
311 JsDate::local(year, month - 1, day_of_month, hour, 0, 0, 0)
312}
313
314pub fn get_last_weekday_of_month(year: i64, month: i64, weekday: i64, hour: i64) -> JsDate {
316 let one_indexed_weekday = if weekday == 0 { 7 } else { weekday };
317 let mut date = JsDate::local(year, month - 1 + 1, 1, 12, 0, 0, 0);
318 let first_weekday_next_month = if date.get_day() == 0 { 7 } else { date.get_day() };
319 let day_diff = if first_weekday_next_month == one_indexed_weekday {
320 7
321 } else if first_weekday_next_month < one_indexed_weekday {
322 7 + first_weekday_next_month - one_indexed_weekday
323 } else {
324 first_weekday_next_month - one_indexed_weekday
325 };
326 date.set_date(date.get_date() - day_diff);
327 JsDate::local(year, month - 1, date.get_date(), hour, 0, 0, 0)
328}
329
330fn date_gt(a: JsDate, b: JsDate) -> bool {
332 matches!((a.get_time(), b.get_time()), (Some(a), Some(b)) if a > b)
333}
334
335pub fn to_timezone_offset(
337 input: Option<&TimezoneInput>,
338 date: Option<JsDate>,
339 overrides: &TimezoneAbbrMap,
340) -> Option<i64> {
341 let abbr = match input? {
342 TimezoneInput::Minutes(m) => return Some(*m),
343 TimezoneInput::Abbr(a) => a,
344 };
345 let spec = overrides.get(abbr).copied().or_else(|| lookup(abbr))?;
346 match spec {
347 Fixed(offset) => Some(offset),
348 TimezoneSpec::Ambiguous {
349 offset_during_dst,
350 offset_non_dst,
351 dst_start,
352 dst_end,
353 } => {
354 let date = date?;
355 let year = date.get_full_year();
356 if date_gt(date, dst_start(year)) && !date_gt(date, dst_end(year)) {
357 Some(offset_during_dst)
358 } else {
359 Some(offset_non_dst)
360 }
361 }
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368
369 #[test]
370 fn table_size_matches_upstream() {
371 assert_eq!(TIMEZONE_ABBR_MAP.len(), 203);
373 }
374
375 #[test]
376 fn fixed_and_ambiguous() {
377 let none = TimezoneAbbrMap::new();
378 assert_eq!(
379 to_timezone_offset(Some(&TimezoneInput::Abbr("CDT".into())), None, &none),
380 Some(-300)
381 );
382 assert_eq!(
383 to_timezone_offset(Some(&TimezoneInput::Abbr("JST".into())), None, &none),
384 Some(540)
385 );
386 assert_eq!(
387 to_timezone_offset(Some(&TimezoneInput::Abbr("XYZ".into())), None, &none),
388 None
389 );
390 assert_eq!(
391 to_timezone_offset(Some(&TimezoneInput::Minutes(-90)), None, &none),
392 Some(-90)
393 );
394 let summer = JsDate::parse_iso("2021-07-01T00:00:00.000Z");
395 let winter = JsDate::parse_iso("2021-01-15T00:00:00.000Z");
396 assert_eq!(
397 to_timezone_offset(Some(&TimezoneInput::Abbr("ET".into())), summer, &none),
398 Some(-240)
399 );
400 assert_eq!(
401 to_timezone_offset(Some(&TimezoneInput::Abbr("ET".into())), winter, &none),
402 Some(-300)
403 );
404 assert_eq!(
405 to_timezone_offset(Some(&TimezoneInput::Abbr("ET".into())), None, &none),
406 None
407 );
408 let mut overrides = TimezoneAbbrMap::new();
409 overrides.insert("JST".into(), Fixed(1));
410 assert_eq!(
411 to_timezone_offset(Some(&TimezoneInput::Abbr("JST".into())), None, &overrides),
412 Some(1)
413 );
414 }
415
416 #[test]
417 fn dst_boundaries() {
418 assert_eq!(us_dst_start(2021).to_iso_string().unwrap(), "2021-03-14T02:00:00.000Z");
420 assert_eq!(us_dst_end(2021).to_iso_string().unwrap(), "2021-11-07T02:00:00.000Z");
421 assert_eq!(cet_dst_start(2021).to_iso_string().unwrap(), "2021-03-28T02:00:00.000Z");
422 assert_eq!(cet_dst_end(2021).to_iso_string().unwrap(), "2021-10-31T03:00:00.000Z");
423 }
424}