1use serde::{Deserialize, Serialize};
4use std::cmp::Ordering;
5use std::fmt;
6
7const NANOS_PER_DAY: u64 = 86_400_000_000_000;
9const NANOS_PER_HOUR: u64 = 3_600_000_000_000;
10const NANOS_PER_MINUTE: u64 = 60_000_000_000;
11const NANOS_PER_SECOND: u64 = 1_000_000_000;
12
13#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
32pub struct Time {
33 nanos: u64,
35 offset: Option<i32>,
37}
38
39impl Time {
40 #[must_use]
42 pub fn from_hms(hour: u32, min: u32, sec: u32) -> Option<Self> {
43 Self::from_hms_nano(hour, min, sec, 0)
44 }
45
46 #[must_use]
48 pub fn from_hms_nano(hour: u32, min: u32, sec: u32, nano: u32) -> Option<Self> {
49 if hour >= 24 || min >= 60 || sec >= 60 || nano >= 1_000_000_000 {
50 return None;
51 }
52 let nanos = hour as u64 * NANOS_PER_HOUR
53 + min as u64 * NANOS_PER_MINUTE
54 + sec as u64 * NANOS_PER_SECOND
55 + nano as u64;
56 Some(Self {
57 nanos,
58 offset: None,
59 })
60 }
61
62 #[must_use]
64 pub fn from_nanos(nanos: u64) -> Option<Self> {
65 if nanos >= NANOS_PER_DAY {
66 return None;
67 }
68 Some(Self {
69 nanos,
70 offset: None,
71 })
72 }
73
74 #[must_use]
76 pub fn with_offset(self, offset_secs: i32) -> Self {
77 Self {
78 nanos: self.nanos,
79 offset: Some(offset_secs),
80 }
81 }
82
83 #[must_use]
85 pub fn hour(&self) -> u32 {
86 (self.nanos / NANOS_PER_HOUR) as u32
87 }
88
89 #[must_use]
91 pub fn minute(&self) -> u32 {
92 ((self.nanos % NANOS_PER_HOUR) / NANOS_PER_MINUTE) as u32
93 }
94
95 #[must_use]
97 pub fn second(&self) -> u32 {
98 ((self.nanos % NANOS_PER_MINUTE) / NANOS_PER_SECOND) as u32
99 }
100
101 #[must_use]
103 pub fn nanosecond(&self) -> u32 {
104 (self.nanos % NANOS_PER_SECOND) as u32
105 }
106
107 #[must_use]
109 pub fn as_nanos(&self) -> u64 {
110 self.nanos
111 }
112
113 #[must_use]
115 pub fn offset_seconds(&self) -> Option<i32> {
116 self.offset
117 }
118
119 #[must_use]
121 pub fn parse(s: &str) -> Option<Self> {
122 let (time_part, offset) = parse_offset_suffix(s);
124
125 let parts: Vec<&str> = time_part.splitn(2, '.').collect();
126 let hms: Vec<&str> = parts[0].splitn(3, ':').collect();
127 if hms.len() < 2 {
128 return None;
129 }
130
131 let hour: u32 = hms[0].parse().ok()?;
132 let min: u32 = hms[1].parse().ok()?;
133 let sec: u32 = if hms.len() == 3 {
134 hms[2].parse().ok()?
135 } else {
136 0
137 };
138
139 let nano: u32 = if parts.len() == 2 {
141 let frac = parts[1];
142 let padded = if frac.len() >= 9 {
144 &frac[..9]
145 } else {
146 return {
148 let n: u32 = frac.parse().ok()?;
149 let scale = 10u32.pow(9 - frac.len() as u32);
150 let mut t = Self::from_hms_nano(hour, min, sec, n * scale)?;
151 if let Some(off) = offset {
152 t = t.with_offset(off);
153 }
154 Some(t)
155 };
156 };
157 padded.parse().ok()?
158 } else {
159 0
160 };
161
162 let mut t = Self::from_hms_nano(hour, min, sec, nano)?;
163 if let Some(off) = offset {
164 t = t.with_offset(off);
165 }
166 Some(t)
167 }
168
169 #[must_use]
171 pub fn now() -> Self {
172 let ts = super::Timestamp::now();
173 ts.to_time()
174 }
175
176 #[must_use]
181 pub fn add_duration(self, dur: &super::Duration) -> Self {
182 let total = self.nanos as i64 + dur.nanos();
183 let wrapped = total.rem_euclid(NANOS_PER_DAY as i64) as u64;
184 Self {
185 nanos: wrapped,
186 offset: self.offset,
187 }
188 }
189
190 #[must_use]
196 pub fn truncate(&self, unit: &str) -> Option<Self> {
197 let truncated_nanos = match unit {
198 "hour" => (self.nanos / NANOS_PER_HOUR) * NANOS_PER_HOUR,
199 "minute" => (self.nanos / NANOS_PER_MINUTE) * NANOS_PER_MINUTE,
200 "second" => (self.nanos / NANOS_PER_SECOND) * NANOS_PER_SECOND,
201 _ => return None,
202 };
203 Some(Self {
204 nanos: truncated_nanos,
205 offset: self.offset,
206 })
207 }
208
209 fn utc_nanos(&self) -> u64 {
211 match self.offset {
212 Some(off) => {
213 let adjusted = self.nanos as i64 - off as i64 * NANOS_PER_SECOND as i64;
214 adjusted.rem_euclid(NANOS_PER_DAY as i64) as u64
215 }
216 None => self.nanos,
217 }
218 }
219}
220
221impl Default for Time {
222 fn default() -> Self {
223 Self {
224 nanos: 0,
225 offset: None,
226 }
227 }
228}
229
230impl Ord for Time {
231 fn cmp(&self, other: &Self) -> Ordering {
232 match (self.offset, other.offset) {
236 (Some(_), Some(_)) => self.utc_nanos().cmp(&other.utc_nanos()),
237 _ => self.nanos.cmp(&other.nanos),
238 }
239 }
240}
241
242impl PartialOrd for Time {
243 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
244 Some(self.cmp(other))
245 }
246}
247
248fn parse_offset_suffix(s: &str) -> (&str, Option<i32>) {
251 if let Some(rest) = s.strip_suffix('Z') {
252 return (rest, Some(0));
253 }
254 if s.len() >= 6 {
256 let sign_pos = s.len() - 6;
257 let candidate = &s[sign_pos..];
258 if (candidate.starts_with('+') || candidate.starts_with('-'))
259 && candidate.as_bytes()[3] == b':'
260 {
261 let sign: i32 = if candidate.starts_with('+') { 1 } else { -1 };
262 if let (Ok(h), Ok(m)) = (
263 candidate[1..3].parse::<i32>(),
264 candidate[4..6].parse::<i32>(),
265 ) {
266 return (&s[..sign_pos], Some(sign * (h * 3600 + m * 60)));
267 }
268 }
269 }
270 (s, None)
271}
272
273impl fmt::Debug for Time {
274 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275 write!(f, "Time({})", self)
276 }
277}
278
279impl fmt::Display for Time {
280 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281 let h = self.hour();
282 let m = self.minute();
283 let s = self.second();
284 let ns = self.nanosecond();
285
286 if ns > 0 {
287 let frac = format!("{:09}", ns);
289 let trimmed = frac.trim_end_matches('0');
290 write!(f, "{h:02}:{m:02}:{s:02}.{trimmed}")?;
291 } else {
292 write!(f, "{h:02}:{m:02}:{s:02}")?;
293 }
294
295 match self.offset {
296 Some(0) => write!(f, "Z"),
297 Some(off) => {
298 let sign = if off >= 0 { '+' } else { '-' };
299 let abs = off.unsigned_abs();
300 let oh = abs / 3600;
301 let om = (abs % 3600) / 60;
302 write!(f, "{sign}{oh:02}:{om:02}")
303 }
304 None => Ok(()),
305 }
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[test]
314 fn test_basic() {
315 let t = Time::from_hms(14, 30, 45).unwrap();
316 assert_eq!(t.hour(), 14);
317 assert_eq!(t.minute(), 30);
318 assert_eq!(t.second(), 45);
319 assert_eq!(t.nanosecond(), 0);
320 }
321
322 #[test]
323 fn test_with_nanos() {
324 let t = Time::from_hms_nano(0, 0, 0, 123_456_789).unwrap();
325 assert_eq!(t.nanosecond(), 123_456_789);
326 assert_eq!(t.to_string(), "00:00:00.123456789");
327 }
328
329 #[test]
330 fn test_validation() {
331 assert!(Time::from_hms(24, 0, 0).is_none());
332 assert!(Time::from_hms(0, 60, 0).is_none());
333 assert!(Time::from_hms(0, 0, 60).is_none());
334 assert!(Time::from_hms_nano(0, 0, 0, 1_000_000_000).is_none());
335 }
336
337 #[test]
338 fn test_parse_basic() {
339 let t = Time::parse("14:30:00").unwrap();
340 assert_eq!(t.hour(), 14);
341 assert_eq!(t.minute(), 30);
342 assert_eq!(t.second(), 0);
343 assert!(t.offset_seconds().is_none());
344 }
345
346 #[test]
347 fn test_parse_with_offset() {
348 let t = Time::parse("14:30:00+02:00").unwrap();
349 assert_eq!(t.hour(), 14);
350 assert_eq!(t.offset_seconds(), Some(7200));
351
352 let t = Time::parse("14:30:00Z").unwrap();
353 assert_eq!(t.offset_seconds(), Some(0));
354
355 let t = Time::parse("14:30:00-05:30").unwrap();
356 assert_eq!(t.offset_seconds(), Some(-19800));
357 }
358
359 #[test]
360 fn test_parse_fractional() {
361 let t = Time::parse("14:30:00.5").unwrap();
362 assert_eq!(t.nanosecond(), 500_000_000);
363
364 let t = Time::parse("14:30:00.123").unwrap();
365 assert_eq!(t.nanosecond(), 123_000_000);
366 }
367
368 #[test]
369 fn test_display() {
370 assert_eq!(Time::from_hms(9, 5, 3).unwrap().to_string(), "09:05:03");
371 assert_eq!(
372 Time::from_hms(14, 30, 0)
373 .unwrap()
374 .with_offset(0)
375 .to_string(),
376 "14:30:00Z"
377 );
378 assert_eq!(
379 Time::from_hms(14, 30, 0)
380 .unwrap()
381 .with_offset(5 * 3600 + 30 * 60)
382 .to_string(),
383 "14:30:00+05:30"
384 );
385 }
386
387 #[test]
388 fn test_ordering() {
389 let t1 = Time::from_hms(10, 0, 0).unwrap();
390 let t2 = Time::from_hms(14, 0, 0).unwrap();
391 assert!(t1 < t2);
392 }
393
394 #[test]
395 fn test_ordering_with_offsets() {
396 let t1 = Time::from_hms(14, 0, 0).unwrap().with_offset(7200);
400 let t2 = Time::from_hms(13, 0, 0).unwrap().with_offset(0);
401 assert!(t1 < t2);
402 }
403
404 #[test]
405 fn test_truncate() {
406 let t = Time::from_hms_nano(14, 30, 45, 123_456_789).unwrap();
407
408 let hour = t.truncate("hour").unwrap();
409 assert_eq!(hour.hour(), 14);
410 assert_eq!(hour.minute(), 0);
411 assert_eq!(hour.second(), 0);
412 assert_eq!(hour.nanosecond(), 0);
413
414 let minute = t.truncate("minute").unwrap();
415 assert_eq!(minute.hour(), 14);
416 assert_eq!(minute.minute(), 30);
417 assert_eq!(minute.second(), 0);
418
419 let second = t.truncate("second").unwrap();
420 assert_eq!(second.hour(), 14);
421 assert_eq!(second.minute(), 30);
422 assert_eq!(second.second(), 45);
423 assert_eq!(second.nanosecond(), 0);
424
425 assert!(t.truncate("day").is_none());
426 }
427
428 #[test]
429 fn test_truncate_preserves_offset() {
430 let t = Time::from_hms_nano(14, 30, 45, 0)
431 .unwrap()
432 .with_offset(19800); let truncated = t.truncate("hour").unwrap();
434 assert_eq!(truncated.offset_seconds(), Some(19800));
435 assert_eq!(truncated.hour(), 14);
436 assert_eq!(truncated.minute(), 0);
437 }
438
439 #[test]
440 fn test_default() {
441 let t = Time::default();
442 assert_eq!(t.hour(), 0);
443 assert_eq!(t.minute(), 0);
444 assert_eq!(t.second(), 0);
445 }
446}