grafeo_common/types/
zoned_datetime.rs1use serde::{Deserialize, Serialize};
8use std::cmp::Ordering;
9use std::fmt;
10use std::hash::{Hash, Hasher};
11
12use super::{Date, Time, Timestamp};
13
14#[derive(Clone, Copy, Serialize, Deserialize)]
31pub struct ZonedDatetime {
32 utc_micros: i64,
34 offset_seconds: i32,
36}
37
38impl ZonedDatetime {
39 #[inline]
41 #[must_use]
42 pub const fn from_timestamp_offset(ts: Timestamp, offset_seconds: i32) -> Self {
43 Self {
44 utc_micros: ts.as_micros(),
45 offset_seconds,
46 }
47 }
48
49 #[must_use]
53 pub fn from_date_time(date: Date, time: Time) -> Option<Self> {
54 let offset = time.offset_seconds()?;
55 let ts = Timestamp::from_date_time(date, time);
56 Some(Self {
57 utc_micros: ts.as_micros(),
58 offset_seconds: offset,
59 })
60 }
61
62 #[inline]
64 #[must_use]
65 pub const fn as_timestamp(&self) -> Timestamp {
66 Timestamp::from_micros(self.utc_micros)
67 }
68
69 #[inline]
71 #[must_use]
72 pub const fn offset_seconds(&self) -> i32 {
73 self.offset_seconds
74 }
75
76 #[must_use]
78 pub fn to_local_date(&self) -> Date {
79 let local_micros = self.utc_micros + self.offset_seconds as i64 * 1_000_000;
80 Timestamp::from_micros(local_micros).to_date()
81 }
82
83 #[must_use]
86 pub fn to_local_time(&self) -> Time {
87 let local_micros = self.utc_micros + self.offset_seconds as i64 * 1_000_000;
88 Timestamp::from_micros(local_micros)
89 .to_time()
90 .with_offset(self.offset_seconds)
91 }
92
93 #[must_use]
102 pub fn parse(s: &str) -> Option<Self> {
103 let pos = s.find('T').or_else(|| s.find('t'))?;
104 let date = Date::parse(&s[..pos])?;
105 let time = Time::parse(&s[pos + 1..])?;
106 time.offset_seconds()?;
108 Self::from_date_time(date, time)
109 }
110}
111
112impl PartialEq for ZonedDatetime {
113 fn eq(&self, other: &Self) -> bool {
114 self.utc_micros == other.utc_micros
115 }
116}
117
118impl Eq for ZonedDatetime {}
119
120impl Hash for ZonedDatetime {
121 fn hash<H: Hasher>(&self, state: &mut H) {
122 self.utc_micros.hash(state);
124 }
125}
126
127impl Ord for ZonedDatetime {
128 fn cmp(&self, other: &Self) -> Ordering {
129 self.utc_micros.cmp(&other.utc_micros)
130 }
131}
132
133impl PartialOrd for ZonedDatetime {
134 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
135 Some(self.cmp(other))
136 }
137}
138
139impl fmt::Debug for ZonedDatetime {
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141 write!(f, "ZonedDatetime({})", self)
142 }
143}
144
145impl fmt::Display for ZonedDatetime {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 let local_micros = self.utc_micros + self.offset_seconds as i64 * 1_000_000;
148 let ts = Timestamp::from_micros(local_micros);
149 let date = ts.to_date();
150 let time = ts.to_time();
151
152 let (year, month, day) = (date.year(), date.month(), date.day());
153 let (h, m, s) = (time.hour(), time.minute(), time.second());
154 let micro_frac = local_micros.rem_euclid(1_000_000) as u64;
155
156 if micro_frac > 0 {
157 let frac = format!("{micro_frac:06}");
159 let trimmed = frac.trim_end_matches('0');
160 write!(
161 f,
162 "{year:04}-{month:02}-{day:02}T{h:02}:{m:02}:{s:02}.{trimmed}"
163 )?;
164 } else {
165 write!(f, "{year:04}-{month:02}-{day:02}T{h:02}:{m:02}:{s:02}")?;
166 }
167
168 match self.offset_seconds {
169 0 => write!(f, "Z"),
170 off => {
171 let sign = if off >= 0 { '+' } else { '-' };
172 let abs = off.unsigned_abs();
173 let oh = abs / 3600;
174 let om = (abs % 3600) / 60;
175 write!(f, "{sign}{oh:02}:{om:02}")
176 }
177 }
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_parse_utc() {
187 let zdt = ZonedDatetime::parse("2024-06-15T10:30:00Z").unwrap();
188 assert_eq!(zdt.offset_seconds(), 0);
189 assert_eq!(zdt.to_string(), "2024-06-15T10:30:00Z");
190 }
191
192 #[test]
193 fn test_parse_positive_offset() {
194 let zdt = ZonedDatetime::parse("2024-06-15T10:30:00+05:30").unwrap();
195 assert_eq!(zdt.offset_seconds(), 19800);
196 assert_eq!(zdt.to_string(), "2024-06-15T10:30:00+05:30");
197 }
198
199 #[test]
200 fn test_parse_negative_offset() {
201 let zdt = ZonedDatetime::parse("2024-06-15T10:30:00-04:00").unwrap();
202 assert_eq!(zdt.offset_seconds(), -14400);
203 assert_eq!(zdt.to_string(), "2024-06-15T10:30:00-04:00");
204 }
205
206 #[test]
207 fn test_equality_same_instant() {
208 let z1 = ZonedDatetime::parse("2024-06-15T15:30:00+05:30").unwrap();
210 let z2 = ZonedDatetime::parse("2024-06-15T10:00:00Z").unwrap();
211 assert_eq!(z1, z2);
212 }
213
214 #[test]
215 fn test_no_offset_fails() {
216 assert!(ZonedDatetime::parse("2024-06-15T10:30:00").is_none());
217 }
218
219 #[test]
220 fn test_local_date_time() {
221 let zdt = ZonedDatetime::parse("2024-06-15T23:30:00+05:30").unwrap();
222 let local_date = zdt.to_local_date();
223 assert_eq!(local_date.year(), 2024);
224 assert_eq!(local_date.month(), 6);
225 assert_eq!(local_date.day(), 15);
226
227 let local_time = zdt.to_local_time();
228 assert_eq!(local_time.hour(), 23);
229 assert_eq!(local_time.minute(), 30);
230 assert_eq!(local_time.offset_seconds(), Some(19800));
231 }
232
233 #[test]
234 fn test_from_timestamp_offset() {
235 let ts = Timestamp::from_secs(1_718_444_400); let zdt = ZonedDatetime::from_timestamp_offset(ts, 3600); assert_eq!(zdt.as_timestamp(), ts);
238 assert_eq!(zdt.offset_seconds(), 3600);
239 assert_eq!(zdt.to_string(), "2024-06-15T10:40:00+01:00");
240 }
241
242 #[test]
243 fn test_ordering() {
244 let earlier = ZonedDatetime::parse("2024-06-15T10:00:00Z").unwrap();
245 let later = ZonedDatetime::parse("2024-06-15T12:00:00Z").unwrap();
246 assert!(earlier < later);
247 }
248
249 #[test]
250 fn test_with_fractional_seconds() {
251 let zdt = ZonedDatetime::parse("2024-06-15T10:30:00.123Z").unwrap();
252 assert_eq!(zdt.to_string(), "2024-06-15T10:30:00.123Z");
253 }
254}