vibesql_types/temporal/
timestamp.rs

1//! SQL TIMESTAMP type implementation
2
3use std::{cmp::Ordering, fmt, str::FromStr};
4
5use super::{Date, Time};
6
7/// SQL TIMESTAMP type - represents a date and time
8///
9/// Supports multiple formats:
10/// - ISO 8601: '2024-01-01T14:30:00' or '2024-01-01T14:30:00.123456'
11/// - Space-separated: '2024-01-01 14:30:00' or '2024-01-01 14:30:00.123456'
12/// - With timezone: '2024-01-01T14:30:00Z' or '2024-01-01T14:30:00+05:00'
13/// - Date only: '2024-01-01' (assumes midnight)
14///
15/// Stored as Date and Time components for correct comparison
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct Timestamp {
18    pub date: Date,
19    pub time: Time,
20}
21
22impl Timestamp {
23    /// Create a new Timestamp
24    pub fn new(date: Date, time: Time) -> Self {
25        Timestamp { date, time }
26    }
27}
28
29impl FromStr for Timestamp {
30    type Err = String;
31
32    fn from_str(s: &str) -> Result<Self, Self::Err> {
33        // Trim whitespace
34        let trimmed = s.trim();
35
36        // Strip timezone suffix if present (Z, +HH:MM, -HH:MM)
37        let timestamp_part = strip_timezone_suffix(trimmed);
38
39        // Try parsing with 'T' separator (ISO 8601)
40        if let Some(t_pos) = timestamp_part.find('T') {
41            let date_str = &timestamp_part[..t_pos];
42            let time_str = &timestamp_part[t_pos + 1..];
43
44            let date = Date::from_str(date_str)?;
45            let time = Time::from_str(time_str)?;
46
47            return Ok(Timestamp::new(date, time));
48        }
49
50        // Try parsing with space separator
51        let parts: Vec<&str> = timestamp_part.split_whitespace().collect();
52
53        if parts.len() == 2 {
54            // Standard format: YYYY-MM-DD HH:MM:SS[.ffffff]
55            let date = Date::from_str(parts[0])?;
56            let time = Time::from_str(parts[1])?;
57
58            return Ok(Timestamp::new(date, time));
59        } else if parts.len() == 1 {
60            // Could be date-only format
61            if let Ok(date) = Date::from_str(parts[0]) {
62                // Date only - use midnight time
63                let midnight = Time::new(0, 0, 0, 0).unwrap();
64                return Ok(Timestamp::new(date, midnight));
65            }
66        }
67
68        // If all attempts failed, return helpful error message
69        Err(format!(
70            "Invalid timestamp format: '{}'. Supported formats: \
71            ISO 8601 (2025-11-10T08:24:34), \
72            space-separated (2025-11-10 08:24:34), \
73            or date only (2025-11-10)",
74            s
75        ))
76    }
77}
78
79/// Strip timezone suffix from timestamp string
80/// Handles: Z, +HH:MM, -HH:MM, +HHMM, -HHMM
81fn strip_timezone_suffix(s: &str) -> &str {
82    // Check for 'Z' suffix (UTC)
83    if s.ends_with('Z') || s.ends_with('z') {
84        return &s[..s.len() - 1];
85    }
86
87    // Check for +/- timezone offset
88    // Look for last occurrence of + or - (could be in date part, so we look from the right)
89    if let Some(pos) = s.rfind(['+', '-']) {
90        // Make sure this is actually a timezone indicator, not part of the date
91        // Timezone offsets appear after the time, so position should be > 10 (YYYY-MM-DD)
92        if pos > 10 {
93            // Check if what follows looks like a timezone offset
94            let potential_tz = &s[pos..];
95            if is_timezone_offset(potential_tz) {
96                return &s[..pos];
97            }
98        }
99    }
100
101    s
102}
103
104/// Check if a string looks like a timezone offset (+HH:MM, -HH:MM, +HHMM, -HHMM)
105fn is_timezone_offset(s: &str) -> bool {
106    if s.len() < 3 {
107        return false;
108    }
109
110    let sign = s.chars().next().unwrap();
111    if sign != '+' && sign != '-' {
112        return false;
113    }
114
115    let rest = &s[1..];
116
117    // Check for +HH:MM or -HH:MM format (6 chars: +HH:MM)
118    if rest.len() == 5 && rest.chars().nth(2) == Some(':') {
119        return rest[..2].chars().all(|c| c.is_ascii_digit())
120            && rest[3..].chars().all(|c| c.is_ascii_digit());
121    }
122
123    // Check for +HHMM or -HHMM format (4 chars: +HHMM)
124    if rest.len() == 4 {
125        return rest.chars().all(|c| c.is_ascii_digit());
126    }
127
128    // Check for +HH format (2 chars: +HH)
129    if rest.len() == 2 {
130        return rest.chars().all(|c| c.is_ascii_digit());
131    }
132
133    false
134}
135
136impl fmt::Display for Timestamp {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        write!(f, "{} {}", self.date, self.time)
139    }
140}
141
142impl PartialOrd for Timestamp {
143    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
144        Some(self.cmp(other))
145    }
146}
147
148impl Ord for Timestamp {
149    fn cmp(&self, other: &Self) -> Ordering {
150        self.date.cmp(&other.date).then_with(|| self.time.cmp(&other.time))
151    }
152}