Skip to main content

vibesql_types/temporal/
time.rs

1//! SQL TIME type implementation
2
3use std::{cmp::Ordering, fmt, str::FromStr};
4
5/// SQL TIME type - represents a time without date
6///
7/// Format: HH:MM:SS or HH:MM:SS.fff (e.g., '14:30:00' or '14:30:00.123')
8/// Stored as hour, minute, second, nanosecond components for correct comparison
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub struct Time {
11    pub hour: u8,        // 0-23
12    pub minute: u8,      // 0-59
13    pub second: u8,      // 0-59
14    pub nanosecond: u32, // 0-999999999
15}
16
17impl Time {
18    /// Create a new Time (validation is basic)
19    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        // Parse format: HH:MM:SS or HH:MM:SS.fff
41        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        // Parse fractional seconds if present
59        let nanosecond = if let Some(frac) = frac_part {
60            // Pad or truncate to 9 digits (nanoseconds)
61            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            // Display fractional seconds, trimming trailing zeros but never
80            // below 3 digits. SQLite's subsec rendering is always exactly 3
81            // digits (`HH:MM:SS.SSS`); padding to a minimum of 3 keeps
82            // `.5` → `.500` aligned with SQLite while preserving stored
83            // sub-millisecond precision (e.g. `.123456`) so Display → parse
84            // round-trips hold (issue #5332).
85            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        // SQLite renders subsec with exactly 3 digits: `.5` must become `.500`.
131        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        // No hard truncation: stored sub-ms precision must stay visible.
144        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, // renders as .000000001
160        ] {
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}