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]
99 pub fn truncate(&self, unit: &str) -> Option<Self> {
100 let local_micros = self.utc_micros + self.offset_seconds as i64 * 1_000_000;
102 let local_ts = Timestamp::from_micros(local_micros);
103 let truncated_local = local_ts.truncate(unit)?;
104 let utc_micros = truncated_local.as_micros() - self.offset_seconds as i64 * 1_000_000;
105 Some(Self {
106 utc_micros,
107 offset_seconds: self.offset_seconds,
108 })
109 }
110
111 #[must_use]
120 pub fn parse(s: &str) -> Option<Self> {
121 let pos = s.find('T').or_else(|| s.find('t'))?;
122 let date = Date::parse(&s[..pos])?;
123 let time = Time::parse(&s[pos + 1..])?;
124 time.offset_seconds()?;
126 Self::from_date_time(date, time)
127 }
128}
129
130impl PartialEq for ZonedDatetime {
131 fn eq(&self, other: &Self) -> bool {
132 self.utc_micros == other.utc_micros
133 }
134}
135
136impl Eq for ZonedDatetime {}
137
138impl Hash for ZonedDatetime {
139 fn hash<H: Hasher>(&self, state: &mut H) {
140 self.utc_micros.hash(state);
142 }
143}
144
145impl Ord for ZonedDatetime {
146 fn cmp(&self, other: &Self) -> Ordering {
147 self.utc_micros.cmp(&other.utc_micros)
148 }
149}
150
151impl PartialOrd for ZonedDatetime {
152 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
153 Some(self.cmp(other))
154 }
155}
156
157impl fmt::Debug for ZonedDatetime {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 write!(f, "ZonedDatetime({})", self)
160 }
161}
162
163impl fmt::Display for ZonedDatetime {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 let local_micros = self.utc_micros + self.offset_seconds as i64 * 1_000_000;
166 let ts = Timestamp::from_micros(local_micros);
167 let date = ts.to_date();
168 let time = ts.to_time();
169
170 let (year, month, day) = (date.year(), date.month(), date.day());
171 let (h, m, s) = (time.hour(), time.minute(), time.second());
172 let micro_frac = local_micros.rem_euclid(1_000_000) as u64;
173
174 if micro_frac > 0 {
175 let frac = format!("{micro_frac:06}");
177 let trimmed = frac.trim_end_matches('0');
178 write!(
179 f,
180 "{year:04}-{month:02}-{day:02}T{h:02}:{m:02}:{s:02}.{trimmed}"
181 )?;
182 } else {
183 write!(f, "{year:04}-{month:02}-{day:02}T{h:02}:{m:02}:{s:02}")?;
184 }
185
186 match self.offset_seconds {
187 0 => write!(f, "Z"),
188 off => {
189 let sign = if off >= 0 { '+' } else { '-' };
190 let abs = off.unsigned_abs();
191 let oh = abs / 3600;
192 let om = (abs % 3600) / 60;
193 write!(f, "{sign}{oh:02}:{om:02}")
194 }
195 }
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_parse_utc() {
205 let zdt = ZonedDatetime::parse("2024-06-15T10:30:00Z").unwrap();
206 assert_eq!(zdt.offset_seconds(), 0);
207 assert_eq!(zdt.to_string(), "2024-06-15T10:30:00Z");
208 }
209
210 #[test]
211 fn test_parse_positive_offset() {
212 let zdt = ZonedDatetime::parse("2024-06-15T10:30:00+05:30").unwrap();
213 assert_eq!(zdt.offset_seconds(), 19800);
214 assert_eq!(zdt.to_string(), "2024-06-15T10:30:00+05:30");
215 }
216
217 #[test]
218 fn test_parse_negative_offset() {
219 let zdt = ZonedDatetime::parse("2024-06-15T10:30:00-04:00").unwrap();
220 assert_eq!(zdt.offset_seconds(), -14400);
221 assert_eq!(zdt.to_string(), "2024-06-15T10:30:00-04:00");
222 }
223
224 #[test]
225 fn test_equality_same_instant() {
226 let z1 = ZonedDatetime::parse("2024-06-15T15:30:00+05:30").unwrap();
228 let z2 = ZonedDatetime::parse("2024-06-15T10:00:00Z").unwrap();
229 assert_eq!(z1, z2);
230 }
231
232 #[test]
233 fn test_no_offset_fails() {
234 assert!(ZonedDatetime::parse("2024-06-15T10:30:00").is_none());
235 }
236
237 #[test]
238 fn test_local_date_time() {
239 let zdt = ZonedDatetime::parse("2024-06-15T23:30:00+05:30").unwrap();
240 let local_date = zdt.to_local_date();
241 assert_eq!(local_date.year(), 2024);
242 assert_eq!(local_date.month(), 6);
243 assert_eq!(local_date.day(), 15);
244
245 let local_time = zdt.to_local_time();
246 assert_eq!(local_time.hour(), 23);
247 assert_eq!(local_time.minute(), 30);
248 assert_eq!(local_time.offset_seconds(), Some(19800));
249 }
250
251 #[test]
252 fn test_from_timestamp_offset() {
253 let ts = Timestamp::from_secs(1_718_444_400); let zdt = ZonedDatetime::from_timestamp_offset(ts, 3600); assert_eq!(zdt.as_timestamp(), ts);
256 assert_eq!(zdt.offset_seconds(), 3600);
257 assert_eq!(zdt.to_string(), "2024-06-15T10:40:00+01:00");
258 }
259
260 #[test]
261 fn test_ordering() {
262 let earlier = ZonedDatetime::parse("2024-06-15T10:00:00Z").unwrap();
263 let later = ZonedDatetime::parse("2024-06-15T12:00:00Z").unwrap();
264 assert!(earlier < later);
265 }
266
267 #[test]
268 fn test_truncate() {
269 let zdt = ZonedDatetime::parse("2024-06-15T14:30:45+02:00").unwrap();
271
272 let day = zdt.truncate("day").unwrap();
273 assert_eq!(day.offset_seconds(), 7200);
274 assert_eq!(day.to_string(), "2024-06-15T00:00:00+02:00");
276
277 let hour = zdt.truncate("hour").unwrap();
278 assert_eq!(hour.to_string(), "2024-06-15T14:00:00+02:00");
279
280 let minute = zdt.truncate("minute").unwrap();
281 assert_eq!(minute.to_string(), "2024-06-15T14:30:00+02:00");
282 }
283
284 #[test]
285 fn test_with_fractional_seconds() {
286 let zdt = ZonedDatetime::parse("2024-06-15T10:30:00.123Z").unwrap();
287 assert_eq!(zdt.to_string(), "2024-06-15T10:30:00.123Z");
288 }
289}