vibesql_types/temporal/
timestamp.rs1use std::{cmp::Ordering, fmt, str::FromStr};
4
5use super::{Date, Time};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct Timestamp {
18 pub date: Date,
19 pub time: Time,
20}
21
22impl Timestamp {
23 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 let trimmed = s.trim();
35
36 let timestamp_part = strip_timezone_suffix(trimmed);
38
39 if let Some(t_pos) = timestamp_part.find('T') {
41 let date_str = ×tamp_part[..t_pos];
42 let time_str = ×tamp_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 let parts: Vec<&str> = timestamp_part.split_whitespace().collect();
52
53 if parts.len() == 2 {
54 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 if let Ok(date) = Date::from_str(parts[0]) {
62 let midnight = Time::new(0, 0, 0, 0).unwrap();
64 return Ok(Timestamp::new(date, midnight));
65 }
66 }
67
68 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
79fn strip_timezone_suffix(s: &str) -> &str {
82 if s.ends_with('Z') || s.ends_with('z') {
84 return &s[..s.len() - 1];
85 }
86
87 if let Some(pos) = s.rfind(['+', '-']) {
90 if pos > 10 {
93 let potential_tz = &s[pos..];
95 if is_timezone_offset(potential_tz) {
96 return &s[..pos];
97 }
98 }
99 }
100
101 s
102}
103
104fn 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 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 if rest.len() == 4 {
125 return rest.chars().all(|c| c.is_ascii_digit());
126 }
127
128 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}