vibesql_types/temporal/
time.rs1use std::{cmp::Ordering, fmt, str::FromStr};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub struct Time {
11 pub hour: u8, pub minute: u8, pub second: u8, pub nanosecond: u32, }
16
17impl Time {
18 pub fn new(hour: u8, minute: u8, second: u8, nanosecond: u32) -> Result<Self, String> {
20 if hour > 23 {
21 return Err(format!("Invalid hour: {}", hour));
22 }
23 if minute > 59 {
24 return Err(format!("Invalid minute: {}", minute));
25 }
26 if second > 59 {
27 return Err(format!("Invalid second: {}", second));
28 }
29 if nanosecond > 999_999_999 {
30 return Err(format!("Invalid nanosecond: {}", nanosecond));
31 }
32 Ok(Time { hour, minute, second, nanosecond })
33 }
34}
35
36impl FromStr for Time {
37 type Err = String;
38
39 fn from_str(s: &str) -> Result<Self, Self::Err> {
40 let (time_part, frac_part) = if let Some(dot_pos) = s.find('.') {
42 (&s[..dot_pos], Some(&s[dot_pos + 1..]))
43 } else {
44 (s, None)
45 };
46
47 let parts: Vec<&str> = time_part.split(':').collect();
48 if parts.len() != 3 {
49 return Err(format!("Invalid time format: '{}' (expected HH:MM:SS)", s));
50 }
51
52 let hour = parts[0].parse::<u8>().map_err(|_| format!("Invalid hour: '{}'", parts[0]))?;
53 let minute =
54 parts[1].parse::<u8>().map_err(|_| format!("Invalid minute: '{}'", parts[1]))?;
55 let second =
56 parts[2].parse::<u8>().map_err(|_| format!("Invalid second: '{}'", parts[2]))?;
57
58 let nanosecond = if let Some(frac) = frac_part {
60 let padded = format!("{:0<9}", frac);
62 let truncated = &padded[..9.min(padded.len())];
63 truncated
64 .parse::<u32>()
65 .map_err(|_| format!("Invalid fractional seconds: '{}'", frac))?
66 } else {
67 0
68 };
69
70 Time::new(hour, minute, second, nanosecond)
71 }
72}
73
74impl fmt::Display for Time {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 if self.nanosecond == 0 {
77 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)
78 } else {
79 let frac = format!("{:09}", self.nanosecond);
86 let trimmed_len = frac.trim_end_matches('0').len().max(3);
87 write!(
88 f,
89 "{:02}:{:02}:{:02}.{}",
90 self.hour,
91 self.minute,
92 self.second,
93 &frac[..trimmed_len]
94 )
95 }
96 }
97}
98
99impl PartialOrd for Time {
100 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
101 Some(self.cmp(other))
102 }
103}
104
105impl Ord for Time {
106 fn cmp(&self, other: &Self) -> Ordering {
107 self.hour
108 .cmp(&other.hour)
109 .then_with(|| self.minute.cmp(&other.minute))
110 .then_with(|| self.second.cmp(&other.second))
111 .then_with(|| self.nanosecond.cmp(&other.nanosecond))
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 fn time_with_ns(nanosecond: u32) -> Time {
120 Time::new(13, 15, 44, nanosecond).unwrap()
121 }
122
123 #[test]
124 fn display_no_fraction_when_nanosecond_zero() {
125 assert_eq!(time_with_ns(0).to_string(), "13:15:44");
126 }
127
128 #[test]
129 fn display_pads_fraction_to_minimum_three_digits() {
130 assert_eq!(time_with_ns(500_000_000).to_string(), "13:15:44.500");
132 assert_eq!(time_with_ns(120_000_000).to_string(), "13:15:44.120");
133 assert_eq!(time_with_ns(1_000_000).to_string(), "13:15:44.001");
134 }
135
136 #[test]
137 fn display_keeps_three_digit_fractions_unchanged() {
138 assert_eq!(time_with_ns(123_000_000).to_string(), "13:15:44.123");
139 }
140
141 #[test]
142 fn display_preserves_sub_millisecond_digits() {
143 assert_eq!(time_with_ns(123_456_000).to_string(), "13:15:44.123456");
145 assert_eq!(time_with_ns(123_456_789).to_string(), "13:15:44.123456789");
146 }
147
148 #[test]
149 fn display_parse_round_trip_preserves_nanoseconds() {
150 for ns in [
151 0u32,
152 500_000_000,
153 123_000_000,
154 120_000_000,
155 123_456_000,
156 1_000_000,
157 123_456_789,
158 999_999_999,
159 1, ] {
161 let t = time_with_ns(ns);
162 let parsed = Time::from_str(&t.to_string()).unwrap();
163 assert_eq!(parsed, t, "round-trip failed for ns={} (rendered '{}')", ns, t);
164 }
165 }
166}