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 #[allow(clippy::cast_possible_truncation)]
151 let scale = 10u32.pow(9 - frac.len() as u32);
152 let mut t = Self::from_hms_nano(hour, min, sec, n * scale)?;
153 if let Some(off) = offset {
154 t = t.with_offset(off);
155 }
156 Some(t)
157 };
158 };
159 padded.parse().ok()?
160 } else {
161 0
162 };
163
164 let mut t = Self::from_hms_nano(hour, min, sec, nano)?;
165 if let Some(off) = offset {
166 t = t.with_offset(off);
167 }
168 Some(t)
169 }
170
171 #[must_use]
173 pub fn now() -> Self {
174 let ts = super::Timestamp::now();
175 ts.to_time()
176 }
177
178 #[must_use]
183 pub fn add_duration(self, dur: &super::Duration) -> Self {
184 #[allow(clippy::cast_possible_wrap)]
186 let total = self.nanos as i64 + dur.nanos();
187 #[allow(clippy::cast_possible_wrap)]
189 let wrapped = total.rem_euclid(NANOS_PER_DAY as i64) as u64;
190 Self {
191 nanos: wrapped,
192 offset: self.offset,
193 }
194 }
195
196 #[must_use]
202 pub fn truncate(&self, unit: &str) -> Option<Self> {
203 let truncated_nanos = match unit {
204 "hour" => (self.nanos / NANOS_PER_HOUR) * NANOS_PER_HOUR,
205 "minute" => (self.nanos / NANOS_PER_MINUTE) * NANOS_PER_MINUTE,
206 "second" => (self.nanos / NANOS_PER_SECOND) * NANOS_PER_SECOND,
207 _ => return None,
208 };
209 Some(Self {
210 nanos: truncated_nanos,
211 offset: self.offset,
212 })
213 }
214
215 fn utc_nanos(&self) -> u64 {
217 match self.offset {
218 Some(off) => {
219 #[allow(clippy::cast_possible_wrap)]
221 let adjusted = self.nanos as i64 - off as i64 * NANOS_PER_SECOND as i64;
222 #[allow(clippy::cast_possible_wrap)]
224 let result = adjusted.rem_euclid(NANOS_PER_DAY as i64) as u64;
225 result
226 }
227 None => self.nanos,
228 }
229 }
230}
231
232impl Default for Time {
233 fn default() -> Self {
234 Self {
235 nanos: 0,
236 offset: None,
237 }
238 }
239}
240
241impl Ord for Time {
242 fn cmp(&self, other: &Self) -> Ordering {
243 match (self.offset, other.offset) {
247 (Some(_), Some(_)) => self.utc_nanos().cmp(&other.utc_nanos()),
248 _ => self.nanos.cmp(&other.nanos),
249 }
250 }
251}
252
253impl PartialOrd for Time {
254 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
255 Some(self.cmp(other))
256 }
257}
258
259fn parse_offset_suffix(s: &str) -> (&str, Option<i32>) {
262 if let Some(rest) = s.strip_suffix('Z') {
263 return (rest, Some(0));
264 }
265 if s.len() >= 6 {
267 let sign_pos = s.len() - 6;
268 let candidate = &s[sign_pos..];
269 if (candidate.starts_with('+') || candidate.starts_with('-'))
270 && candidate.as_bytes()[3] == b':'
271 {
272 let sign: i32 = if candidate.starts_with('+') { 1 } else { -1 };
273 if let (Ok(h), Ok(m)) = (
274 candidate[1..3].parse::<i32>(),
275 candidate[4..6].parse::<i32>(),
276 ) {
277 return (&s[..sign_pos], Some(sign * (h * 3600 + m * 60)));
278 }
279 }
280 }
281 (s, None)
282}
283
284impl fmt::Debug for Time {
285 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286 write!(f, "Time({})", self)
287 }
288}
289
290impl fmt::Display for Time {
291 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292 let h = self.hour();
293 let m = self.minute();
294 let s = self.second();
295 let ns = self.nanosecond();
296
297 if ns > 0 {
298 let frac = format!("{:09}", ns);
300 let trimmed = frac.trim_end_matches('0');
301 write!(f, "{h:02}:{m:02}:{s:02}.{trimmed}")?;
302 } else {
303 write!(f, "{h:02}:{m:02}:{s:02}")?;
304 }
305
306 match self.offset {
307 Some(0) => write!(f, "Z"),
308 Some(off) => {
309 let sign = if off >= 0 { '+' } else { '-' };
310 let abs = off.unsigned_abs();
311 let oh = abs / 3600;
312 let om = (abs % 3600) / 60;
313 write!(f, "{sign}{oh:02}:{om:02}")
314 }
315 None => Ok(()),
316 }
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn test_basic() {
326 let t = Time::from_hms(14, 30, 45).unwrap();
327 assert_eq!(t.hour(), 14);
328 assert_eq!(t.minute(), 30);
329 assert_eq!(t.second(), 45);
330 assert_eq!(t.nanosecond(), 0);
331 }
332
333 #[test]
334 fn test_with_nanos() {
335 let t = Time::from_hms_nano(0, 0, 0, 123_456_789).unwrap();
336 assert_eq!(t.nanosecond(), 123_456_789);
337 assert_eq!(t.to_string(), "00:00:00.123456789");
338 }
339
340 #[test]
341 fn test_validation() {
342 assert!(Time::from_hms(24, 0, 0).is_none());
343 assert!(Time::from_hms(0, 60, 0).is_none());
344 assert!(Time::from_hms(0, 0, 60).is_none());
345 assert!(Time::from_hms_nano(0, 0, 0, 1_000_000_000).is_none());
346 }
347
348 #[test]
349 fn test_parse_basic() {
350 let t = Time::parse("14:30:00").unwrap();
351 assert_eq!(t.hour(), 14);
352 assert_eq!(t.minute(), 30);
353 assert_eq!(t.second(), 0);
354 assert!(t.offset_seconds().is_none());
355 }
356
357 #[test]
358 fn test_parse_with_offset() {
359 let t = Time::parse("14:30:00+02:00").unwrap();
360 assert_eq!(t.hour(), 14);
361 assert_eq!(t.offset_seconds(), Some(7200));
362
363 let t = Time::parse("14:30:00Z").unwrap();
364 assert_eq!(t.offset_seconds(), Some(0));
365
366 let t = Time::parse("14:30:00-05:30").unwrap();
367 assert_eq!(t.offset_seconds(), Some(-19800));
368 }
369
370 #[test]
371 fn test_parse_fractional() {
372 let t = Time::parse("14:30:00.5").unwrap();
373 assert_eq!(t.nanosecond(), 500_000_000);
374
375 let t = Time::parse("14:30:00.123").unwrap();
376 assert_eq!(t.nanosecond(), 123_000_000);
377 }
378
379 #[test]
380 fn test_display() {
381 assert_eq!(Time::from_hms(9, 5, 3).unwrap().to_string(), "09:05:03");
382 assert_eq!(
383 Time::from_hms(14, 30, 0)
384 .unwrap()
385 .with_offset(0)
386 .to_string(),
387 "14:30:00Z"
388 );
389 assert_eq!(
390 Time::from_hms(14, 30, 0)
391 .unwrap()
392 .with_offset(5 * 3600 + 30 * 60)
393 .to_string(),
394 "14:30:00+05:30"
395 );
396 }
397
398 #[test]
399 fn test_ordering() {
400 let t1 = Time::from_hms(10, 0, 0).unwrap();
401 let t2 = Time::from_hms(14, 0, 0).unwrap();
402 assert!(t1 < t2);
403 }
404
405 #[test]
406 fn test_ordering_with_offsets() {
407 let t1 = Time::from_hms(14, 0, 0).unwrap().with_offset(7200);
411 let t2 = Time::from_hms(13, 0, 0).unwrap().with_offset(0);
412 assert!(t1 < t2);
413 }
414
415 #[test]
416 fn test_truncate() {
417 let t = Time::from_hms_nano(14, 30, 45, 123_456_789).unwrap();
418
419 let hour = t.truncate("hour").unwrap();
420 assert_eq!(hour.hour(), 14);
421 assert_eq!(hour.minute(), 0);
422 assert_eq!(hour.second(), 0);
423 assert_eq!(hour.nanosecond(), 0);
424
425 let minute = t.truncate("minute").unwrap();
426 assert_eq!(minute.hour(), 14);
427 assert_eq!(minute.minute(), 30);
428 assert_eq!(minute.second(), 0);
429
430 let second = t.truncate("second").unwrap();
431 assert_eq!(second.hour(), 14);
432 assert_eq!(second.minute(), 30);
433 assert_eq!(second.second(), 45);
434 assert_eq!(second.nanosecond(), 0);
435
436 assert!(t.truncate("day").is_none());
437 }
438
439 #[test]
440 fn test_truncate_preserves_offset() {
441 let t = Time::from_hms_nano(14, 30, 45, 0)
442 .unwrap()
443 .with_offset(19800); let truncated = t.truncate("hour").unwrap();
445 assert_eq!(truncated.offset_seconds(), Some(19800));
446 assert_eq!(truncated.hour(), 14);
447 assert_eq!(truncated.minute(), 0);
448 }
449
450 #[test]
451 fn test_default() {
452 let t = Time::default();
453 assert_eq!(t.hour(), 0);
454 assert_eq!(t.minute(), 0);
455 assert_eq!(t.second(), 0);
456 }
457
458 #[test]
459 fn test_add_duration_wraps_at_midnight() {
460 use crate::types::Duration;
461 let t = Time::from_hms(23, 0, 0).unwrap();
463 let dur = Duration::from_nanos(2 * 3_600_000_000_000); let result = t.add_duration(&dur);
465 assert_eq!(result.hour(), 1);
466 assert_eq!(result.minute(), 0);
467 }
468
469 #[test]
470 fn test_add_duration_within_day() {
471 use crate::types::Duration;
472 let t = Time::from_hms(10, 30, 0).unwrap();
473 let dur = Duration::from_nanos(90 * 60 * 1_000_000_000); let result = t.add_duration(&dur);
475 assert_eq!(result.hour(), 12);
476 assert_eq!(result.minute(), 0);
477 }
478}