1use chrono::{DateTime, LocalResult, NaiveDateTime, Offset, TimeZone as _, Utc};
2use chrono_tz::Tz;
3
4#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
6pub enum TimeZoneError {
7 #[error("unknown timezone: {0}")]
9 Unknown(String),
10 #[error("ambiguous time")]
12 Ambiguous,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct TimeZone {
18 tz: Tz,
19 name: String,
20}
21
22impl TimeZone {
23 pub fn new(name: &str) -> Result<Self, TimeZoneError> {
25 find_timezone(name)
26 }
27
28 pub fn find(name: &str) -> Result<Self, TimeZoneError> {
30 find_timezone(name)
31 }
32
33 #[must_use]
35 pub fn name(&self) -> &str {
36 &self.name
37 }
38
39 #[must_use]
41 pub fn now(&self) -> DateTime<Tz> {
42 let now = crate::testing::frozen_now().unwrap_or_else(Utc::now);
43 self.convert(now)
44 }
45
46 #[must_use]
48 pub fn utc_offset(&self) -> i32 {
49 self.now().offset().fix().local_minus_utc()
50 }
51
52 #[must_use]
54 pub fn convert(&self, time: DateTime<Utc>) -> DateTime<Tz> {
55 time.with_timezone(&self.tz)
56 }
57
58 pub fn parse(&self, s: &str, fmt: &str) -> Result<DateTime<Tz>, TimeZoneError> {
60 let naive = NaiveDateTime::parse_from_str(s, fmt)
61 .map_err(|_| TimeZoneError::Unknown(s.to_owned()))?;
62
63 match self.tz.from_local_datetime(&naive) {
64 LocalResult::Single(datetime) => Ok(datetime),
65 LocalResult::Ambiguous(_, _) | LocalResult::None => Err(TimeZoneError::Ambiguous),
66 }
67 }
68}
69
70pub fn find_timezone(name: &str) -> Result<TimeZone, TimeZoneError> {
72 let canonical = match name {
73 "Eastern Time (US & Canada)" => "America/New_York",
74 "Central Time (US & Canada)" => "America/Chicago",
75 "Mountain Time (US & Canada)" => "America/Denver",
76 "Pacific Time (US & Canada)" => "America/Los_Angeles",
77 "UTC" => "Etc/UTC",
78 "London" => "Europe/London",
79 "Tokyo" => "Asia/Tokyo",
80 "Beijing" => "Asia/Shanghai",
81 other => other,
82 };
83
84 let tz = canonical
85 .parse::<Tz>()
86 .map_err(|_| TimeZoneError::Unknown(name.to_owned()))?;
87
88 Ok(TimeZone {
89 tz,
90 name: name.to_owned(),
91 })
92}
93
94#[must_use]
96pub fn all_timezones() -> Vec<&'static str> {
97 chrono_tz::TZ_VARIANTS.iter().map(|tz| tz.name()).collect()
98}
99
100#[cfg(test)]
101mod tests {
102 use super::{TimeZone, TimeZoneError, all_timezones, find_timezone};
103 use crate::testing::{TESTING_TIME_LOCK, freeze_time};
104 use chrono::{TimeZone as _, Timelike, Utc};
105 use chrono_tz::Tz;
106
107 #[test]
108 fn time_zone_accepts_iana_names() {
109 let zone = TimeZone::new("America/New_York").unwrap();
110
111 assert_eq!(zone.name(), "America/New_York");
112 assert_eq!(
113 zone.convert(Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap())
114 .timezone(),
115 Tz::America__New_York
116 );
117 }
118
119 #[test]
120 fn time_zone_finds_rails_aliases() {
121 let zone = find_timezone("Eastern Time (US & Canada)").unwrap();
122 let converted = zone.convert(Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap());
123
124 assert_eq!(zone.name(), "Eastern Time (US & Canada)");
125 assert_eq!(converted.timezone(), Tz::America__New_York);
126 }
127
128 #[test]
129 fn time_zone_supports_multiple_aliases() {
130 let samples = [
131 ("Central Time (US & Canada)", Tz::America__Chicago),
132 ("Mountain Time (US & Canada)", Tz::America__Denver),
133 ("Pacific Time (US & Canada)", Tz::America__Los_Angeles),
134 ("London", Tz::Europe__London),
135 ("Tokyo", Tz::Asia__Tokyo),
136 ("Beijing", Tz::Asia__Shanghai),
137 ("UTC", Tz::Etc__UTC),
138 ];
139
140 for (name, expected) in samples {
141 let zone = find_timezone(name).unwrap();
142 let converted = zone.convert(Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap());
143 assert_eq!(converted.timezone(), expected, "alias {name}");
144 }
145 }
146
147 #[test]
148 fn time_zone_convert_translates_utc_to_local_time() {
149 let zone = TimeZone::find("America/Los_Angeles").unwrap();
150 let utc = Utc.with_ymd_and_hms(2024, 1, 15, 12, 0, 0).unwrap();
151
152 let converted = zone.convert(utc);
153
154 assert_eq!(converted.hour(), 4);
155 assert_eq!(converted.timezone(), Tz::America__Los_Angeles);
156 }
157
158 #[test]
159 fn time_zone_now_uses_current_zone_and_respects_freeze_time() {
160 let _lock = TESTING_TIME_LOCK.lock().unwrap();
161 let frozen = Utc.with_ymd_and_hms(2024, 3, 1, 15, 30, 0).unwrap();
162 let _guard = freeze_time(frozen);
163 let zone = TimeZone::find("Tokyo").unwrap();
164
165 let now = zone.now();
166
167 assert_eq!(now.timezone(), Tz::Asia__Tokyo);
168 assert_eq!(now.hour(), 0);
169 }
170
171 #[test]
172 fn time_zone_utc_offset_returns_seconds() {
173 let _lock = TESTING_TIME_LOCK.lock().unwrap();
174 let frozen = Utc.with_ymd_and_hms(2024, 1, 15, 12, 0, 0).unwrap();
175 let _guard = freeze_time(frozen);
176 let zone = TimeZone::find("America/New_York").unwrap();
177
178 assert_eq!(zone.utc_offset(), -5 * 60 * 60);
179 }
180
181 #[test]
182 fn time_zone_parse_reads_local_time() {
183 let zone = TimeZone::find("America/New_York").unwrap();
184
185 let parsed = zone
186 .parse("2024-01-15 08:45:30", "%Y-%m-%d %H:%M:%S")
187 .unwrap();
188
189 assert_eq!(parsed.timezone(), Tz::America__New_York);
190 assert_eq!(parsed.hour(), 8);
191 assert_eq!(parsed.minute(), 45);
192 assert_eq!(parsed.second(), 30);
193 }
194
195 #[test]
196 fn time_zone_parse_rejects_ambiguous_time() {
197 let zone = TimeZone::find("America/New_York").unwrap();
198
199 let error = zone
200 .parse("2024-11-03 01:30:00", "%Y-%m-%d %H:%M:%S")
201 .unwrap_err();
202
203 assert_eq!(error, TimeZoneError::Ambiguous);
204 }
205
206 #[test]
207 fn time_zone_unknown_name_returns_error() {
208 let error = TimeZone::find("Mars/Base").unwrap_err();
209
210 assert_eq!(error, TimeZoneError::Unknown(String::from("Mars/Base")));
211 }
212
213 #[test]
214 fn time_zone_parse_invalid_input_returns_error() {
215 let zone = TimeZone::find("UTC").unwrap();
216 let error = zone.parse("not-a-time", "%Y-%m-%d %H:%M:%S").unwrap_err();
217
218 assert_eq!(error, TimeZoneError::Unknown(String::from("not-a-time")));
219 }
220
221 #[test]
222 fn all_timezones_contains_common_iana_names() {
223 let zones = all_timezones();
224
225 assert!(zones.contains(&"America/New_York"));
226 assert!(zones.contains(&"Europe/London"));
227 assert!(zones.contains(&"Asia/Tokyo"));
228 }
229
230 #[test]
231 fn time_zone_major_aliases_resolve_to_expected_iana_regions() {
232 let samples = [
233 ("London", "Europe/London"),
234 ("Tokyo", "Asia/Tokyo"),
235 ("Beijing", "Asia/Shanghai"),
236 ("UTC", "Etc/UTC"),
237 ];
238
239 for (name, expected) in samples {
240 let zone = TimeZone::find(name).unwrap();
241 let converted = zone.convert(Utc.with_ymd_and_hms(2024, 1, 15, 12, 0, 0).unwrap());
242
243 assert_eq!(zone.name(), name);
244 assert_eq!(converted.timezone().name(), expected, "lookup {name}");
245 }
246 }
247
248 #[test]
249 fn time_zone_utc_offset_changes_with_dst_for_named_zones() {
250 let _lock = TESTING_TIME_LOCK.lock().unwrap();
251 let eastern = TimeZone::find("Eastern Time (US & Canada)").unwrap();
252 let london = TimeZone::find("London").unwrap();
253
254 {
255 let _guard = freeze_time(Utc.with_ymd_and_hms(2024, 1, 15, 12, 0, 0).unwrap());
256 assert_eq!(eastern.utc_offset(), -5 * 60 * 60);
257 assert_eq!(london.utc_offset(), 0);
258 }
259
260 {
261 let _guard = freeze_time(Utc.with_ymd_and_hms(2024, 7, 15, 12, 0, 0).unwrap());
262 assert_eq!(eastern.utc_offset(), -4 * 60 * 60);
263 assert_eq!(london.utc_offset(), 60 * 60);
264 }
265 }
266
267 #[test]
268 fn time_zone_now_reflects_dst_shift_across_date_boundaries() {
269 let _lock = TESTING_TIME_LOCK.lock().unwrap();
270 let zone = TimeZone::find("America/Los_Angeles").unwrap();
271
272 {
273 let _guard = freeze_time(Utc.with_ymd_and_hms(2024, 1, 1, 4, 30, 0).unwrap());
274 assert_eq!(
275 zone.now().format("%Y-%m-%d %H:%M %:z").to_string(),
276 "2023-12-31 20:30 -08:00"
277 );
278 }
279
280 {
281 let _guard = freeze_time(Utc.with_ymd_and_hms(2024, 7, 1, 4, 30, 0).unwrap());
282 assert_eq!(
283 zone.now().format("%Y-%m-%d %H:%M %:z").to_string(),
284 "2024-06-30 21:30 -07:00"
285 );
286 }
287 }
288
289 #[test]
290 fn time_zone_now_enforces_spring_dst_rules() {
291 let _lock = TESTING_TIME_LOCK.lock().unwrap();
292 let zone = TimeZone::find("Eastern Time (US & Canada)").unwrap();
293 let _guard = freeze_time(Utc.with_ymd_and_hms(2024, 3, 10, 7, 0, 0).unwrap());
294
295 assert_eq!(
296 zone.now().format("%Y-%m-%d %H:%M:%S %:z").to_string(),
297 "2024-03-10 03:00:00 -04:00"
298 );
299 }
300
301 #[test]
302 fn time_zone_now_distinguishes_repeated_fall_hour_by_offset() {
303 let _lock = TESTING_TIME_LOCK.lock().unwrap();
304 let zone = TimeZone::find("Eastern Time (US & Canada)").unwrap();
305
306 {
307 let _guard = freeze_time(Utc.with_ymd_and_hms(2024, 11, 3, 5, 30, 0).unwrap());
308 assert_eq!(
309 zone.now().format("%Y-%m-%d %H:%M:%S %:z").to_string(),
310 "2024-11-03 01:30:00 -04:00"
311 );
312 }
313
314 {
315 let _guard = freeze_time(Utc.with_ymd_and_hms(2024, 11, 3, 6, 30, 0).unwrap());
316 assert_eq!(
317 zone.now().format("%Y-%m-%d %H:%M:%S %:z").to_string(),
318 "2024-11-03 01:30:00 -05:00"
319 );
320 }
321 }
322
323 #[test]
324 fn time_zone_alias_parse_matches_iana_in_winter_and_summer() {
325 let alias = TimeZone::find("Eastern Time (US & Canada)").unwrap();
326 let iana = TimeZone::find("America/New_York").unwrap();
327
328 for input in ["2024-01-15 08:45:30", "2024-07-15 08:45:30"] {
329 assert_eq!(
330 alias
331 .parse(input, "%Y-%m-%d %H:%M:%S")
332 .unwrap()
333 .with_timezone(&Utc),
334 iana.parse(input, "%Y-%m-%d %H:%M:%S")
335 .unwrap()
336 .with_timezone(&Utc),
337 "input {input}"
338 );
339 }
340 }
341
342 #[test]
343 fn time_zone_convert_preserves_fractional_seconds_across_dst_offsets() {
344 let zone = TimeZone::find("Eastern Time (US & Canada)").unwrap();
345 let winter = Utc
346 .with_ymd_and_hms(2000, 1, 1, 0, 0, 0)
347 .unwrap()
348 .with_nanosecond(123_456_000)
349 .unwrap();
350 let summer = Utc
351 .with_ymd_and_hms(2000, 7, 1, 0, 0, 0)
352 .unwrap()
353 .with_nanosecond(123_456_000)
354 .unwrap();
355
356 let converted_winter = zone.convert(winter);
357 let converted_summer = zone.convert(summer);
358
359 assert_eq!(converted_winter.nanosecond(), 123_456_000);
360 assert_eq!(
361 converted_winter.format("%Y-%m-%d %H:%M:%S %:z").to_string(),
362 "1999-12-31 19:00:00 -05:00"
363 );
364 assert_eq!(converted_summer.nanosecond(), 123_456_000);
365 assert_eq!(
366 converted_summer.format("%Y-%m-%d %H:%M:%S %:z").to_string(),
367 "2000-06-30 20:00:00 -04:00"
368 );
369 }
370
371 #[test]
372 fn time_zone_parse_rejects_nonexistent_spring_forward_time() {
373 let zone = TimeZone::find("America/New_York").unwrap();
374
375 let error = zone
376 .parse("2024-03-10 02:30:00", "%Y-%m-%d %H:%M:%S")
377 .unwrap_err();
378
379 assert_eq!(error, TimeZoneError::Ambiguous);
380 }
381
382 #[test]
383 fn time_zone_parse_formats_offsets_around_dst_boundaries() {
384 let zone = TimeZone::find("America/New_York").unwrap();
385
386 let before_spring = zone
387 .parse("2024-03-10 01:59:59", "%Y-%m-%d %H:%M:%S")
388 .unwrap();
389 let after_spring = zone
390 .parse("2024-03-10 03:00:00", "%Y-%m-%d %H:%M:%S")
391 .unwrap();
392 let after_fall = zone
393 .parse("2024-11-03 02:30:00", "%Y-%m-%d %H:%M:%S")
394 .unwrap();
395
396 assert_eq!(
397 before_spring.format("%Y-%m-%d %H:%M:%S %:z").to_string(),
398 "2024-03-10 01:59:59 -05:00"
399 );
400 assert_eq!(
401 after_spring.format("%Y-%m-%d %H:%M:%S %:z").to_string(),
402 "2024-03-10 03:00:00 -04:00"
403 );
404 assert_eq!(
405 after_fall.format("%Y-%m-%d %H:%M:%S %:z").to_string(),
406 "2024-11-03 02:30:00 -05:00"
407 );
408 }
409
410 #[test]
411 fn time_zone_convert_handles_half_and_quarter_hour_offsets() {
412 let utc = Utc.with_ymd_and_hms(2024, 1, 1, 20, 30, 0).unwrap();
413 let adelaide = TimeZone::find("Australia/Adelaide").unwrap();
414 let kathmandu = TimeZone::find("Asia/Kathmandu").unwrap();
415
416 assert_eq!(
417 adelaide
418 .convert(utc)
419 .format("%Y-%m-%d %H:%M %:z")
420 .to_string(),
421 "2024-01-02 07:00 +10:30"
422 );
423 assert_eq!(
424 kathmandu
425 .convert(utc)
426 .format("%Y-%m-%d %H:%M %:z")
427 .to_string(),
428 "2024-01-02 02:15 +05:45"
429 );
430 }
431
432 #[test]
433 fn time_zone_new_accepts_aliases() {
434 let zone = TimeZone::new("Pacific Time (US & Canada)").unwrap();
435
436 assert_eq!(zone.name(), "Pacific Time (US & Canada)");
437 }
438
439 #[test]
440 fn find_timezone_preserves_display_name_for_iana_identifiers() {
441 let zone = find_timezone("Asia/Tokyo").unwrap();
442
443 assert_eq!(zone.name(), "Asia/Tokyo");
444 }
445
446 #[test]
447 fn time_zone_convert_tokyo_crosses_date_boundary() {
448 let zone = TimeZone::find("Asia/Tokyo").unwrap();
449 let utc = Utc.with_ymd_and_hms(2024, 1, 1, 18, 30, 0).unwrap();
450
451 assert_eq!(
452 zone.convert(utc).format("%Y-%m-%d %H:%M %:z").to_string(),
453 "2024-01-02 03:30 +09:00"
454 );
455 }
456
457 #[test]
458 fn time_zone_convert_utc_is_identity() {
459 let zone = TimeZone::find("UTC").unwrap();
460 let utc = Utc.with_ymd_and_hms(2024, 1, 1, 18, 30, 0).unwrap();
461
462 assert_eq!(
463 zone.convert(utc).format("%Y-%m-%d %H:%M %:z").to_string(),
464 "2024-01-01 18:30 +00:00"
465 );
466 }
467
468 #[test]
469 fn time_zone_now_in_utc_matches_frozen_timestamp() {
470 let _lock = TESTING_TIME_LOCK.lock().unwrap();
471 let frozen = Utc.with_ymd_and_hms(2024, 5, 1, 12, 34, 56).unwrap();
472 let _guard = freeze_time(frozen);
473 let zone = TimeZone::find("UTC").unwrap();
474
475 assert_eq!(
476 zone.now().format("%Y-%m-%d %H:%M:%S %:z").to_string(),
477 "2024-05-01 12:34:56 +00:00"
478 );
479 }
480
481 #[test]
482 fn time_zone_utc_offset_for_utc_is_zero() {
483 let _lock = TESTING_TIME_LOCK.lock().unwrap();
484 let frozen = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
485 let _guard = freeze_time(frozen);
486 let zone = TimeZone::find("UTC").unwrap();
487
488 assert_eq!(zone.utc_offset(), 0);
489 }
490
491 #[test]
492 fn time_zone_utc_offset_for_tokyo_is_nine_hours() {
493 let _lock = TESTING_TIME_LOCK.lock().unwrap();
494 let frozen = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
495 let _guard = freeze_time(frozen);
496 let zone = TimeZone::find("Asia/Tokyo").unwrap();
497
498 assert_eq!(zone.utc_offset(), 9 * 60 * 60);
499 }
500
501 #[test]
502 fn time_zone_utc_offset_for_india_is_half_hour() {
503 let _lock = TESTING_TIME_LOCK.lock().unwrap();
504 let frozen = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
505 let _guard = freeze_time(frozen);
506 let zone = TimeZone::find("Asia/Kolkata").unwrap();
507
508 assert_eq!(zone.utc_offset(), 19_800);
509 }
510
511 #[test]
512 fn time_zone_utc_offset_for_kathmandu_is_quarter_hour() {
513 let _lock = TESTING_TIME_LOCK.lock().unwrap();
514 let frozen = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
515 let _guard = freeze_time(frozen);
516 let zone = TimeZone::find("Asia/Kathmandu").unwrap();
517
518 assert_eq!(zone.utc_offset(), 20_700);
519 }
520
521 #[test]
522 fn time_zone_parse_in_utc_yields_exact_timestamp() {
523 let zone = TimeZone::find("UTC").unwrap();
524 let parsed = zone
525 .parse("2024-01-15 08:45:30", "%Y-%m-%d %H:%M:%S")
526 .unwrap();
527
528 assert_eq!(
529 parsed.with_timezone(&Utc),
530 Utc.with_ymd_and_hms(2024, 1, 15, 8, 45, 30).unwrap()
531 );
532 }
533
534 #[test]
535 fn time_zone_parse_in_tokyo_maps_to_expected_utc_instant() {
536 let zone = TimeZone::find("Asia/Tokyo").unwrap();
537 let parsed = zone
538 .parse("2024-01-15 08:45:30", "%Y-%m-%d %H:%M:%S")
539 .unwrap();
540
541 assert_eq!(
542 parsed.with_timezone(&Utc),
543 Utc.with_ymd_and_hms(2024, 1, 14, 23, 45, 30).unwrap()
544 );
545 }
546
547 #[test]
548 fn time_zone_parse_in_london_has_winter_offset() {
549 let zone = TimeZone::find("Europe/London").unwrap();
550 let parsed = zone
551 .parse("2024-01-15 08:45:30", "%Y-%m-%d %H:%M:%S")
552 .unwrap();
553
554 assert_eq!(
555 parsed.format("%Y-%m-%d %H:%M:%S %:z").to_string(),
556 "2024-01-15 08:45:30 +00:00"
557 );
558 }
559
560 #[test]
561 fn time_zone_parse_in_london_has_summer_offset() {
562 let zone = TimeZone::find("Europe/London").unwrap();
563 let parsed = zone
564 .parse("2024-07-15 08:45:30", "%Y-%m-%d %H:%M:%S")
565 .unwrap();
566
567 assert_eq!(
568 parsed.format("%Y-%m-%d %H:%M:%S %:z").to_string(),
569 "2024-07-15 08:45:30 +01:00"
570 );
571 }
572
573 #[test]
574 fn time_zone_parse_in_london_rejects_ambiguous_fall_time() {
575 let zone = TimeZone::find("Europe/London").unwrap();
576 let error = zone
577 .parse("2024-10-27 01:30:00", "%Y-%m-%d %H:%M:%S")
578 .unwrap_err();
579
580 assert_eq!(error, TimeZoneError::Ambiguous);
581 }
582
583 #[test]
584 fn time_zone_parse_in_london_rejects_nonexistent_spring_time() {
585 let zone = TimeZone::find("Europe/London").unwrap();
586 let error = zone
587 .parse("2024-03-31 01:30:00", "%Y-%m-%d %H:%M:%S")
588 .unwrap_err();
589
590 assert_eq!(error, TimeZoneError::Ambiguous);
591 }
592
593 #[test]
594 fn all_timezones_contains_half_and_quarter_hour_regions() {
595 let zones = all_timezones();
596
597 assert!(zones.contains(&"Asia/Kolkata"));
598 assert!(zones.contains(&"Asia/Kathmandu"));
599 assert!(zones.contains(&"Australia/Adelaide"));
600 }
601
602 #[test]
603 fn all_timezones_contains_major_us_regions() {
604 let zones = all_timezones();
605
606 assert!(zones.contains(&"America/Chicago"));
607 assert!(zones.contains(&"America/Los_Angeles"));
608 }
609
610 #[test]
611 fn time_zone_unknown_alias_reports_original_input() {
612 let error = find_timezone("Eastern").unwrap_err();
613
614 assert_eq!(error, TimeZoneError::Unknown(String::from("Eastern")));
615 }
616
617 #[test]
618 fn time_zone_alias_and_iana_lookup_match_same_instant() {
619 let utc = Utc.with_ymd_and_hms(2024, 2, 1, 12, 0, 0).unwrap();
620 let alias = TimeZone::find("Eastern Time (US & Canada)").unwrap();
621 let iana = TimeZone::find("America/New_York").unwrap();
622
623 assert_eq!(
624 alias.convert(utc).with_timezone(&Utc),
625 iana.convert(utc).with_timezone(&Utc)
626 );
627 assert_eq!(
628 alias.convert(utc).format("%Y-%m-%d %H:%M %:z").to_string(),
629 "2024-02-01 07:00 -05:00"
630 );
631 }
632
633 #[test]
634 fn time_zone_convert_kolkata_crosses_midnight() {
635 let zone = TimeZone::find("Asia/Kolkata").unwrap();
636 let utc = Utc.with_ymd_and_hms(2024, 1, 1, 20, 30, 0).unwrap();
637
638 assert_eq!(
639 zone.convert(utc).format("%Y-%m-%d %H:%M %:z").to_string(),
640 "2024-01-02 02:00 +05:30"
641 );
642 }
643
644 #[test]
645 fn time_zone_convert_kathmandu_crosses_midnight() {
646 let zone = TimeZone::find("Asia/Kathmandu").unwrap();
647 let utc = Utc.with_ymd_and_hms(2024, 1, 1, 20, 30, 0).unwrap();
648
649 assert_eq!(
650 zone.convert(utc).format("%Y-%m-%d %H:%M %:z").to_string(),
651 "2024-01-02 02:15 +05:45"
652 );
653 }
654
655 #[test]
656 fn time_zone_now_in_half_hour_zone_reflects_offset() {
657 let _lock = TESTING_TIME_LOCK.lock().unwrap();
658 let frozen = Utc.with_ymd_and_hms(2024, 1, 1, 20, 30, 0).unwrap();
659 let _guard = freeze_time(frozen);
660 let zone = TimeZone::find("Asia/Kolkata").unwrap();
661
662 assert_eq!(
663 zone.now().format("%Y-%m-%d %H:%M %:z").to_string(),
664 "2024-01-02 02:00 +05:30"
665 );
666 }
667
668 #[test]
669 fn time_zone_alias_name_is_preserved() {
670 let zone = TimeZone::find("London").unwrap();
671
672 assert_eq!(zone.name(), "London");
673 }
674
675 #[test]
676 fn time_zone_parse_supports_alternative_format_strings() {
677 let zone = TimeZone::find("UTC").unwrap();
678 let parsed = zone.parse("15/01/2024 08:45", "%d/%m/%Y %H:%M").unwrap();
679
680 assert_eq!(
681 parsed.format("%Y-%m-%d %H:%M %:z").to_string(),
682 "2024-01-15 08:45 +00:00"
683 );
684 }
685
686 #[test]
687 fn time_zone_parse_los_angeles_offsets_differ_by_season() {
688 let zone = TimeZone::find("America/Los_Angeles").unwrap();
689 let winter = zone
690 .parse("2024-01-15 08:45:30", "%Y-%m-%d %H:%M:%S")
691 .unwrap();
692 let summer = zone
693 .parse("2024-07-15 08:45:30", "%Y-%m-%d %H:%M:%S")
694 .unwrap();
695
696 assert_eq!(
697 winter.format("%Y-%m-%d %H:%M:%S %:z").to_string(),
698 "2024-01-15 08:45:30 -08:00"
699 );
700 assert_eq!(
701 summer.format("%Y-%m-%d %H:%M:%S %:z").to_string(),
702 "2024-07-15 08:45:30 -07:00"
703 );
704 }
705}