proof_of_sql_parser/posql_time/
timestamp.rs1use super::{PoSQLTimeUnit, PoSQLTimeZone, PoSQLTimestampError};
2use alloc::{format, string::ToString};
3use chrono::{offset::LocalResult, DateTime, TimeZone, Utc};
4use core::hash::Hash;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
9pub struct PoSQLTimestamp {
10    timestamp: DateTime<Utc>,
12
13    timeunit: PoSQLTimeUnit,
15
16    timezone: PoSQLTimeZone,
18}
19
20impl PoSQLTimestamp {
21    #[must_use]
23    pub fn timestamp(&self) -> DateTime<Utc> {
24        self.timestamp
25    }
26
27    #[must_use]
29    pub fn timeunit(&self) -> PoSQLTimeUnit {
30        self.timeunit
31    }
32
33    #[must_use]
35    pub fn timezone(&self) -> PoSQLTimeZone {
36        self.timezone
37    }
38
39    pub fn try_from(timestamp_str: &str) -> Result<Self, PoSQLTimestampError> {
74        let dt = DateTime::parse_from_rfc3339(timestamp_str).map_err(|e| {
75            PoSQLTimestampError::ParsingError {
76                error: e.to_string(),
77            }
78        })?;
79
80        let offset_seconds = dt.offset().local_minus_utc();
81        let timezone = PoSQLTimeZone::new(offset_seconds);
82        let nanoseconds = dt.timestamp_subsec_nanos();
83        let timeunit = if nanoseconds % 1_000 != 0 {
84            PoSQLTimeUnit::Nanosecond
85        } else if nanoseconds % 1_000_000 != 0 {
86            PoSQLTimeUnit::Microsecond
87        } else if nanoseconds % 1_000_000_000 != 0 {
88            PoSQLTimeUnit::Millisecond
89        } else {
90            PoSQLTimeUnit::Second
91        };
92
93        Ok(PoSQLTimestamp {
94            timestamp: dt.with_timezone(&Utc),
95            timeunit,
96            timezone,
97        })
98    }
99
100    pub fn to_timestamp(epoch: i64) -> Result<Self, PoSQLTimestampError> {
129        match Utc.timestamp_opt(epoch, 0) {
130            LocalResult::Single(timestamp) => Ok(PoSQLTimestamp {
131                timestamp,
132                timeunit: PoSQLTimeUnit::Second,
133                timezone: PoSQLTimeZone::utc(),
134            }),
135            LocalResult::Ambiguous(earliest, latest) => Err(PoSQLTimestampError::Ambiguous{ error:
136                format!("The local time is ambiguous because there is a fold in the local time: earliest: {earliest} latest: {latest} "),
137        }),
138            LocalResult::None => Err(PoSQLTimestampError::LocalTimeDoesNotExist),
139        }
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_unix_epoch_time_timezone() {
149        let unix_time = 1_231_006_505; let expected_timezone = PoSQLTimeZone::utc(); let result = PoSQLTimestamp::to_timestamp(unix_time).unwrap();
152        assert_eq!(result.timezone, expected_timezone);
153    }
154
155    #[test]
156    fn test_unix_epoch_timestamp_parsing() {
157        let unix_time = 1_231_006_505; let expected_datetime = Utc.timestamp_opt(unix_time, 0).unwrap();
159        let expected_unit = PoSQLTimeUnit::Second; let input = unix_time; let result = PoSQLTimestamp::to_timestamp(input).unwrap();
162
163        assert_eq!(result.timestamp, expected_datetime);
164        assert_eq!(result.timeunit, expected_unit);
165    }
166
167    #[test]
168    fn test_basic_rfc3339_timestamp() {
169        let input = "2023-06-26T12:34:56Z";
170        let expected = Utc.with_ymd_and_hms(2023, 6, 26, 12, 34, 56).unwrap();
171        let result = PoSQLTimestamp::try_from(input).unwrap();
172        assert_eq!(result.timestamp, expected);
173    }
174
175    #[test]
176    fn test_rfc3339_timestamp_with_positive_offset() {
177        let input = "2023-06-26T08:00:00+04:30";
178        let expected = Utc.with_ymd_and_hms(2023, 6, 26, 3, 30, 0).unwrap(); let result = PoSQLTimestamp::try_from(input).unwrap();
180        assert_eq!(result.timestamp, expected);
181    }
182
183    #[test]
184    fn test_rfc3339_timestamp_with_negative_offset() {
185        let input = "2023-06-26T20:00:00-05:00";
186        let expected = Utc.with_ymd_and_hms(2023, 6, 27, 1, 0, 0).unwrap(); let result = PoSQLTimestamp::try_from(input).unwrap();
188        assert_eq!(result.timestamp, expected);
189    }
190
191    #[test]
192    fn test_rfc3339_timestamp_with_utc_designator() {
193        let input = "2023-06-26T12:34:56Z";
194        let expected = Utc.with_ymd_and_hms(2023, 6, 26, 12, 34, 56).unwrap();
195        let result = PoSQLTimestamp::try_from(input).unwrap();
196        assert_eq!(result.timestamp, expected);
197    }
198
199    #[test]
200    fn test_invalid_rfc3339_timestamp() {
201        let input = "not-a-timestamp";
202        assert_eq!(
203            PoSQLTimestamp::try_from(input),
204            Err(PoSQLTimestampError::ParsingError {
205                error: "input contains invalid characters".into()
206            })
207        );
208    }
209
210    #[test]
211    fn test_timestamp_with_seconds() {
212        let input = "2023-06-26T12:34:56Z";
213        let expected_time = Utc.with_ymd_and_hms(2023, 6, 26, 12, 34, 56).unwrap();
214        let expected_unit = PoSQLTimeUnit::Second;
215        let result = PoSQLTimestamp::try_from(input).unwrap();
216        assert_eq!(result.timestamp, expected_time);
217        assert_eq!(result.timeunit, expected_unit);
218    }
219
220    #[test]
221    fn test_general_parsing_error() {
222        let malformed_input = "2009-01-03T::00Z"; let result = PoSQLTimestamp::try_from(malformed_input);
225        assert!(matches!(
226            result,
227            Err(PoSQLTimestampError::ParsingError { .. })
228        ));
229    }
230
231    #[test]
232    fn test_basic_date_time_support() {
233        let inputs = ["2009-01-03T18:15:05Z", "2009-01-03T18:15:05+02:00"];
234        for input in inputs {
235            assert!(
236                DateTime::parse_from_rfc3339(input).is_ok(),
237                "Should parse correctly: {input}"
238            );
239        }
240    }
241
242    #[test]
243    fn test_leap_seconds() {
244        let input = "1998-12-31T23:59:60Z"; assert!(PoSQLTimestamp::try_from(input).is_ok());
246    }
247
248    #[test]
249    fn test_leap_seconds_ranges() {
250        let before_leap_second = "1998-12-31T23:59:59Z";
252        let leap_second = "1998-12-31T23:59:60Z";
254        let after_leap_second = "1999-01-01T00:00:00Z";
256
257        let before_leap_dt = PoSQLTimestamp::try_from(before_leap_second).unwrap();
259        let leap_second_dt = PoSQLTimestamp::try_from(leap_second).unwrap();
260        let after_leap_dt = PoSQLTimestamp::try_from(after_leap_second).unwrap();
261
262        assert_eq!(
264            before_leap_dt.timestamp,
265            leap_second_dt.timestamp - chrono::Duration::seconds(1)
266        );
267
268        assert_eq!(
270            after_leap_dt.timestamp,
271            leap_second_dt.timestamp + chrono::Duration::seconds(1)
272        );
273    }
274
275    #[test]
276    fn test_rejecting_incorrect_formats() {
277        let incorrect_formats = [
278            "2009-January-03",
279            "25:61:61",
280            "20090103",
281            "181505",
282            "18:15:05",
283        ];
284        for input in incorrect_formats {
285            assert!(
286                DateTime::parse_from_rfc3339(input).is_err(),
287                "Should reject incorrect format: {input}"
288            );
289        }
290    }
291}