forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
2.9 KiB
1//! IANA timezone helpers backed by jiff's bundled tzdb, so results do not depend on the host.
2
3use jiff::{Timestamp, tz::TimeZone};
4
5use crate::error::{Error, Result};
6
7/// Resolves an IANA name, falling back to UTC for anything unknown.
8fn zone_or_utc(tz: &str) -> TimeZone {
9 TimeZone::get(tz).unwrap_or(TimeZone::UTC)
10}
11
12/// Accepts only names known to the bundled tzdb, such as "Europe/Istanbul" or "UTC".
13pub fn validate_timezone(name: &str) -> Result<()> {
14 TimeZone::get(name)
15 .map(|_| ())
16 .map_err(|_| Error::Invalid(format!("timezone {name:?}")))
17}
18
19/// Every known IANA name, sorted and without duplicates, for the timezone `<select>`.
20pub fn all_timezones() -> Vec<String> {
21 let mut names: Vec<String> = jiff::tz::db()
22 .available()
23 .map(|name| name.to_string())
24 .collect();
25 names.sort();
26 names.dedup();
27 names
28}
29
30/// Absolute time for display, e.g. "2026-10-15 14:05 +03". Unknown zones fall back to UTC.
31pub fn format_in_tz(unix: i64, tz: &str) -> String {
32 let Ok(ts) = Timestamp::from_second(unix) else {
33 return "invalid time".to_string();
34 };
35 let formatted = ts
36 .to_zoned(zone_or_utc(tz))
37 .strftime("%Y-%m-%d %H:%M %:z")
38 .to_string();
39 // Whole-hour offsets print as "+03" rather than "+03:00".
40 match formatted.strip_suffix(":00") {
41 Some(short) => short.to_string(),
42 None => formatted,
43 }
44}
45
46/// Local clock time with its offset, e.g. "14:05 (UTC+03:00)". Unknown zones fall back to UTC.
47pub fn local_time_now(tz: &str, now_unix: i64) -> String {
48 let Ok(ts) = Timestamp::from_second(now_unix) else {
49 return "invalid time".to_string();
50 };
51 ts.to_zoned(zone_or_utc(tz))
52 .strftime("%H:%M (UTC%:z)")
53 .to_string()
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 #[test]
61 fn validates_known_and_unknown_names() {
62 assert!(validate_timezone("Europe/Istanbul").is_ok());
63 assert!(validate_timezone("UTC").is_ok());
64 assert!(validate_timezone("Mars/Base").is_err());
65 assert!(validate_timezone("").is_err());
66 }
67
68 #[test]
69 fn formats_absolute_time_in_zone() {
70 // 2026-10-15 11:05:00 UTC is 14:05 in Istanbul (UTC+03, no DST).
71 let unix = 1_792_062_300;
72 assert_eq!(
73 format_in_tz(unix, "Europe/Istanbul"),
74 "2026-10-15 14:05 +03"
75 );
76 assert_eq!(format_in_tz(unix, "Mars/Base"), "2026-10-15 11:05 +00");
77 }
78
79 #[test]
80 fn local_time_shows_offset() {
81 let unix = 1_792_062_300;
82 assert_eq!(local_time_now("Europe/Istanbul", unix), "14:05 (UTC+03:00)");
83 assert_eq!(local_time_now("UTC", unix), "11:05 (UTC+00:00)");
84 }
85
86 #[test]
87 fn lists_timezones_sorted() {
88 let zones = all_timezones();
89 assert!(zones.iter().any(|z| z == "UTC"));
90 assert!(zones.iter().any(|z| z == "America/New_York"));
91 assert!(zones.windows(2).all(|w| w[0] < w[1]));
92 }
93}