egui_charts/model/
timezone.rs1use std::{borrow::Cow, fmt, str::FromStr};
4
5use chrono::{
6 DateTime, Duration, FixedOffset, LocalResult, NaiveDate, NaiveDateTime, Offset, TimeZone,
7 Timelike, Utc,
8};
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
17pub struct TimezoneId {
18 name: Cow<'static, str>,
19 zone: Zone,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23enum Zone {
24 Fixed(FixedOffset),
25 #[cfg(feature = "timezones")]
26 Named(chrono_tz::Tz),
27}
28
29impl TimezoneId {
30 pub fn utc() -> Self {
32 Self {
33 name: Cow::Borrowed("UTC"),
34 zone: Zone::Fixed(FixedOffset::east_opt(0).unwrap()),
35 }
36 }
37
38 pub fn named(name: &str) -> Option<Self> {
40 name.parse().ok()
41 }
42
43 pub fn fixed_offset(seconds: i32) -> Option<Self> {
46 let offset = FixedOffset::east_opt(seconds)?;
47 Some(Self {
48 name: Cow::Owned(offset.to_string()),
49 zone: Zone::Fixed(offset),
50 })
51 }
52
53 pub fn name(&self) -> &str {
55 &self.name
56 }
57
58 fn offset(&self, fixed: FixedOffset) -> TimezoneOffset {
59 TimezoneOffset {
60 zone: self.clone(),
61 fixed,
62 }
63 }
64}
65
66impl Default for TimezoneId {
67 fn default() -> Self {
68 Self::utc()
69 }
70}
71
72impl FromStr for TimezoneId {
73 type Err = super::ParseError;
74
75 fn from_str(name: &str) -> Result<Self, Self::Err> {
76 if name == "UTC" {
77 return Ok(Self::utc());
78 }
79 let bytes = name.as_bytes();
80 if matches!(bytes.first(), Some(b'+' | b'-')) {
81 let valid = matches!(bytes.len(), 6 | 9)
82 && bytes[3] == b':'
83 && (bytes.len() == 6 || bytes[6] == b':')
84 && bytes.iter().enumerate().skip(1).all(|(i, b)| {
85 if i == 3 || i == 6 {
86 *b == b':'
87 } else {
88 b.is_ascii_digit()
89 }
90 });
91 if valid {
92 let number = |i| i32::from(bytes[i] - b'0') * 10 + i32::from(bytes[i + 1] - b'0');
93 let (hours, minutes) = (number(1), number(4));
94 let seconds = if bytes.len() == 9 { number(7) } else { 0 };
95 if hours < 24 && minutes < 60 && seconds < 60 {
96 let offset = hours * 3600 + minutes * 60 + seconds;
97 return Self::fixed_offset(if bytes[0] == b'-' { -offset } else { offset })
98 .ok_or_else(|| super::ParseError::new("time zone", name, "a fixed offset reads +HH:MM or -HH:MM, optionally with :SS, below 24 hours"));
99 }
100 }
101 return Err(super::ParseError::new(
102 "time zone",
103 name,
104 "a fixed offset reads +HH:MM or -HH:MM, optionally with :SS, below 24 hours",
105 ));
106 }
107 #[cfg(feature = "timezones")]
108 if let Ok(zone) = name.parse::<chrono_tz::Tz>() {
109 return Ok(Self {
110 name: Cow::Borrowed(zone.name()),
111 zone: Zone::Named(zone),
112 });
113 }
114 Err(super::ParseError::new(
115 "time zone",
116 name,
117 if cfg!(feature = "timezones") {
118 "unknown IANA time zone"
119 } else {
120 "named time zones need the timezones feature"
121 },
122 ))
123 }
124}
125
126impl fmt::Display for TimezoneId {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 f.write_str(self.name())
129 }
130}
131
132impl Serialize for TimezoneId {
133 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
134 serializer.serialize_str(self.name())
135 }
136}
137
138impl<'de> Deserialize<'de> for TimezoneId {
139 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
140 String::deserialize(deserializer)?
141 .parse()
142 .map_err(serde::de::Error::custom)
143 }
144}
145
146#[derive(Clone, Debug, PartialEq, Eq)]
148pub struct TimezoneOffset {
149 zone: TimezoneId,
150 fixed: FixedOffset,
151}
152
153impl Offset for TimezoneOffset {
154 fn fix(&self) -> FixedOffset {
155 self.fixed
156 }
157}
158
159impl fmt::Display for TimezoneOffset {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 self.fixed.fmt(f)
162 }
163}
164
165impl TimeZone for TimezoneId {
166 type Offset = TimezoneOffset;
167
168 fn from_offset(offset: &Self::Offset) -> Self {
169 offset.zone.clone()
170 }
171
172 fn offset_from_local_date(&self, _local: &NaiveDate) -> LocalResult<Self::Offset> {
173 match self.zone {
174 Zone::Fixed(offset) => LocalResult::Single(self.offset(offset)),
175 #[cfg(feature = "timezones")]
176 Zone::Named(zone) => zone
177 .offset_from_local_date(_local)
178 .map(|offset| self.offset(offset.fix())),
179 }
180 }
181
182 fn offset_from_local_datetime(&self, _local: &NaiveDateTime) -> LocalResult<Self::Offset> {
183 match self.zone {
184 Zone::Fixed(offset) => LocalResult::Single(self.offset(offset)),
185 #[cfg(feature = "timezones")]
186 Zone::Named(zone) => zone
187 .offset_from_local_datetime(_local)
188 .map(|offset| self.offset(offset.fix())),
189 }
190 }
191
192 fn offset_from_utc_date(&self, _utc: &NaiveDate) -> Self::Offset {
193 match self.zone {
194 Zone::Fixed(offset) => self.offset(offset),
195 #[cfg(feature = "timezones")]
196 Zone::Named(zone) => self.offset(zone.offset_from_utc_date(_utc).fix()),
197 }
198 }
199
200 fn offset_from_utc_datetime(&self, _utc: &NaiveDateTime) -> Self::Offset {
201 match self.zone {
202 Zone::Fixed(offset) => self.offset(offset),
203 #[cfg(feature = "timezones")]
204 Zone::Named(zone) => self.offset(zone.offset_from_utc_datetime(_utc).fix()),
205 }
206 }
207}
208
209pub(crate) fn local_time<T: TimeZone>(zone: T, time: DateTime<Utc>) -> NaiveDateTime {
211 time.naive_utc()
212 .checked_add_offset(zone.offset_from_utc_datetime(&time.naive_utc()).fix())
213 .unwrap_or(time.naive_utc())
214}
215
216pub(crate) fn local_instant<T: TimeZone>(
218 zone: T,
219 mut local: NaiveDateTime,
220) -> Option<DateTime<Utc>> {
221 if let Some(time) = zone.from_local_datetime(&local).earliest() {
222 return Some(time.to_utc());
223 }
224 local = local.with_nanosecond(0)?;
225 for _ in 0..=24 * 60 * 60 {
226 local = local.checked_add_signed(Duration::seconds(1))?;
227 if let Some(time) = zone.from_local_datetime(&local).earliest() {
228 return Some(time.to_utc());
229 }
230 }
231 None
232}