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 fn utc_nanos(&self) -> u64 {
192 match self.offset {
193 Some(off) => {
194 let adjusted = self.nanos as i64 - off as i64 * NANOS_PER_SECOND as i64;
195 adjusted.rem_euclid(NANOS_PER_DAY as i64) as u64
196 }
197 None => self.nanos,
198 }
199 }
200}
201
202impl Default for Time {
203 fn default() -> Self {
204 Self {
205 nanos: 0,
206 offset: None,
207 }
208 }
209}
210
211impl Ord for Time {
212 fn cmp(&self, other: &Self) -> Ordering {
213 match (self.offset, other.offset) {
217 (Some(_), Some(_)) => self.utc_nanos().cmp(&other.utc_nanos()),
218 _ => self.nanos.cmp(&other.nanos),
219 }
220 }
221}
222
223impl PartialOrd for Time {
224 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
225 Some(self.cmp(other))
226 }
227}
228
229fn parse_offset_suffix(s: &str) -> (&str, Option<i32>) {
232 if let Some(rest) = s.strip_suffix('Z') {
233 return (rest, Some(0));
234 }
235 if s.len() >= 6 {
237 let sign_pos = s.len() - 6;
238 let candidate = &s[sign_pos..];
239 if (candidate.starts_with('+') || candidate.starts_with('-'))
240 && candidate.as_bytes()[3] == b':'
241 {
242 let sign: i32 = if candidate.starts_with('+') { 1 } else { -1 };
243 if let (Ok(h), Ok(m)) = (
244 candidate[1..3].parse::<i32>(),
245 candidate[4..6].parse::<i32>(),
246 ) {
247 return (&s[..sign_pos], Some(sign * (h * 3600 + m * 60)));
248 }
249 }
250 }
251 (s, None)
252}
253
254impl fmt::Debug for Time {
255 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256 write!(f, "Time({})", self)
257 }
258}
259
260impl fmt::Display for Time {
261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262 let h = self.hour();
263 let m = self.minute();
264 let s = self.second();
265 let ns = self.nanosecond();
266
267 if ns > 0 {
268 let frac = format!("{:09}", ns);
270 let trimmed = frac.trim_end_matches('0');
271 write!(f, "{h:02}:{m:02}:{s:02}.{trimmed}")?;
272 } else {
273 write!(f, "{h:02}:{m:02}:{s:02}")?;
274 }
275
276 match self.offset {
277 Some(0) => write!(f, "Z"),
278 Some(off) => {
279 let sign = if off >= 0 { '+' } else { '-' };
280 let abs = off.unsigned_abs();
281 let oh = abs / 3600;
282 let om = (abs % 3600) / 60;
283 write!(f, "{sign}{oh:02}:{om:02}")
284 }
285 None => Ok(()),
286 }
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn test_basic() {
296 let t = Time::from_hms(14, 30, 45).unwrap();
297 assert_eq!(t.hour(), 14);
298 assert_eq!(t.minute(), 30);
299 assert_eq!(t.second(), 45);
300 assert_eq!(t.nanosecond(), 0);
301 }
302
303 #[test]
304 fn test_with_nanos() {
305 let t = Time::from_hms_nano(0, 0, 0, 123_456_789).unwrap();
306 assert_eq!(t.nanosecond(), 123_456_789);
307 assert_eq!(t.to_string(), "00:00:00.123456789");
308 }
309
310 #[test]
311 fn test_validation() {
312 assert!(Time::from_hms(24, 0, 0).is_none());
313 assert!(Time::from_hms(0, 60, 0).is_none());
314 assert!(Time::from_hms(0, 0, 60).is_none());
315 assert!(Time::from_hms_nano(0, 0, 0, 1_000_000_000).is_none());
316 }
317
318 #[test]
319 fn test_parse_basic() {
320 let t = Time::parse("14:30:00").unwrap();
321 assert_eq!(t.hour(), 14);
322 assert_eq!(t.minute(), 30);
323 assert_eq!(t.second(), 0);
324 assert!(t.offset_seconds().is_none());
325 }
326
327 #[test]
328 fn test_parse_with_offset() {
329 let t = Time::parse("14:30:00+02:00").unwrap();
330 assert_eq!(t.hour(), 14);
331 assert_eq!(t.offset_seconds(), Some(7200));
332
333 let t = Time::parse("14:30:00Z").unwrap();
334 assert_eq!(t.offset_seconds(), Some(0));
335
336 let t = Time::parse("14:30:00-05:30").unwrap();
337 assert_eq!(t.offset_seconds(), Some(-19800));
338 }
339
340 #[test]
341 fn test_parse_fractional() {
342 let t = Time::parse("14:30:00.5").unwrap();
343 assert_eq!(t.nanosecond(), 500_000_000);
344
345 let t = Time::parse("14:30:00.123").unwrap();
346 assert_eq!(t.nanosecond(), 123_000_000);
347 }
348
349 #[test]
350 fn test_display() {
351 assert_eq!(Time::from_hms(9, 5, 3).unwrap().to_string(), "09:05:03");
352 assert_eq!(
353 Time::from_hms(14, 30, 0)
354 .unwrap()
355 .with_offset(0)
356 .to_string(),
357 "14:30:00Z"
358 );
359 assert_eq!(
360 Time::from_hms(14, 30, 0)
361 .unwrap()
362 .with_offset(5 * 3600 + 30 * 60)
363 .to_string(),
364 "14:30:00+05:30"
365 );
366 }
367
368 #[test]
369 fn test_ordering() {
370 let t1 = Time::from_hms(10, 0, 0).unwrap();
371 let t2 = Time::from_hms(14, 0, 0).unwrap();
372 assert!(t1 < t2);
373 }
374
375 #[test]
376 fn test_ordering_with_offsets() {
377 let t1 = Time::from_hms(14, 0, 0).unwrap().with_offset(7200);
381 let t2 = Time::from_hms(13, 0, 0).unwrap().with_offset(0);
382 assert!(t1 < t2);
383 }
384
385 #[test]
386 fn test_default() {
387 let t = Time::default();
388 assert_eq!(t.hour(), 0);
389 assert_eq!(t.minute(), 0);
390 assert_eq!(t.second(), 0);
391 }
392}