1use super::{PoSQLTimeUnit, PoSQLTimeZone, PoSQLTimestampError};
2use alloc::{format, string::ToString};
3use chrono::{offset::LocalResult, DateTime, TimeZone, Utc};
4use core::hash::Hash;
5use serde::{Deserialize, Serialize};
67/// Represents a fully parsed timestamp with detailed time unit and timezone information
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
9pub struct PoSQLTimestamp {
10/// The datetime representation in UTC.
11timestamp: DateTime<Utc>,
1213/// The precision of the datetime value, e.g., seconds, milliseconds.
14timeunit: PoSQLTimeUnit,
1516/// The timezone of the datetime, either UTC or a fixed offset from UTC.
17timezone: PoSQLTimeZone,
18}
1920impl PoSQLTimestamp {
21/// Returns the combined date and time with time zone.
22#[must_use]
23pub fn timestamp(&self) -> DateTime<Utc> {
24self.timestamp
25 }
2627/// Returns the [`PoSQLTimeUnit`] for this timestamp
28#[must_use]
29pub fn timeunit(&self) -> PoSQLTimeUnit {
30self.timeunit
31 }
3233/// Returns the [`PoSQLTimeZone`] for this timestamp
34#[must_use]
35pub fn timezone(&self) -> PoSQLTimeZone {
36self.timezone
37 }
3839/// Attempts to parse a timestamp string into an [`PoSQLTimestamp`] structure.
40 /// This function supports two primary formats:
41 ///
42 /// 1. **RFC 3339 Parsing**:
43 /// - Parses the timestamp along with its timezone.
44 /// - If parsing succeeds, it extracts the timezone offset using `dt.offset().local_minus_utc()`
45 /// and then uses this to construct the appropriate `PoSQLTimeZone`.
46 ///
47 /// 2. **Timezone Parsing and Conversion**:
48 /// - The `from_offset` method is used to determine whether the timezone should be represented
49 /// as `Utc` or `FixedOffset`. This function simplifies the decision based on the offset value.
50 ///
51 /// # Errors
52 /// This function returns a `PoSQLTimestampError` in the following cases:
53 ///
54 /// - **Parsing Error**: Returns `PoSQLTimestampError::ParsingError` if the input string does not conform
55 /// to the RFC 3339 format or if the timestamp cannot be parsed due to invalid formatting.
56 /// This error includes the original parsing error message for further details.
57 ///
58 /// # Examples
59 /// ```
60 /// use chrono::{DateTime, Utc};
61 /// use proof_of_sql_parser::posql_time::{PoSQLTimestamp, PoSQLTimeZone};
62 ///
63 /// // Parsing an RFC 3339 timestamp without a timezone:
64 /// let timestamp_str = "2009-01-03T18:15:05Z";
65 /// let intermediate_timestamp = PoSQLTimestamp::try_from(timestamp_str).unwrap();
66 /// assert_eq!(intermediate_timestamp.timezone(), PoSQLTimeZone::utc());
67 ///
68 /// // Parsing an RFC 3339 timestamp with a positive timezone offset:
69 /// let timestamp_str_with_tz = "2009-01-03T18:15:05+03:00";
70 /// let intermediate_timestamp = PoSQLTimestamp::try_from(timestamp_str_with_tz).unwrap();
71 /// assert_eq!(intermediate_timestamp.timezone(), PoSQLTimeZone::new(10800)); // 3 hours in seconds
72 /// ```
73pub fn try_from(timestamp_str: &str) -> Result<Self, PoSQLTimestampError> {
74let dt = DateTime::parse_from_rfc3339(timestamp_str).map_err(|e| {
75 PoSQLTimestampError::ParsingError {
76 error: e.to_string(),
77 }
78 })?;
7980let offset_seconds = dt.offset().local_minus_utc();
81let timezone = PoSQLTimeZone::new(offset_seconds);
82let nanoseconds = dt.timestamp_subsec_nanos();
83let 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 };
9293Ok(PoSQLTimestamp {
94 timestamp: dt.with_timezone(&Utc),
95 timeunit,
96 timezone,
97 })
98 }
99100/// Attempts to parse a timestamp string into an `PoSQLTimestamp` structure.
101 /// This function supports two primary formats:
102 ///
103 /// **Unix Epoch Time Parsing**:
104 /// - Since Unix epoch timestamps don't inherently carry timezone information,
105 /// any Unix time parsed directly from an integer is assumed to be in UTC.
106 ///
107 /// # Errors
108 /// This function returns a `PoSQLTimestampError` in the following cases:
109 ///
110 /// - **Ambiguous Time**: Returns `PoSQLTimestampError::Ambiguous` if the provided epoch time
111 /// corresponds to a time that is ambiguous (e.g., during a daylight saving time change where
112 /// the local time could correspond to two different UTC times).
113 ///
114 /// - **Non-Existent Local Time**: Returns `PoSQLTimestampError::LocalTimeDoesNotExist` if the
115 /// provided epoch time corresponds to a time that does not exist in the local time zone (e.g.,
116 /// during a daylight saving time change where a certain local time is skipped).
117 ///
118 /// # Examples
119 /// ```
120 /// use chrono::{DateTime, Utc};
121 /// use proof_of_sql_parser::posql_time::{PoSQLTimestamp, PoSQLTimeZone};
122 ///
123 /// // Parsing a Unix epoch timestamp (assumed to be seconds and UTC):
124 /// let unix_time = 1231006505;
125 /// let intermediate_timestamp = PoSQLTimestamp::to_timestamp(unix_time).unwrap();
126 /// assert_eq!(intermediate_timestamp.timezone(), PoSQLTimeZone::utc());
127 /// ```
128pub fn to_timestamp(epoch: i64) -> Result<Self, PoSQLTimestampError> {
129match 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:
136format!("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}
142143#[cfg(test)]
144mod tests {
145use super::*;
146147#[test]
148fn test_unix_epoch_time_timezone() {
149let unix_time = 1_231_006_505; // Unix time as integer
150let expected_timezone = PoSQLTimeZone::utc(); // Unix time should always be UTC
151let result = PoSQLTimestamp::to_timestamp(unix_time).unwrap();
152assert_eq!(result.timezone, expected_timezone);
153 }
154155#[test]
156fn test_unix_epoch_timestamp_parsing() {
157let unix_time = 1_231_006_505; // Example Unix timestamp (seconds since epoch)
158let expected_datetime = Utc.timestamp_opt(unix_time, 0).unwrap();
159let expected_unit = PoSQLTimeUnit::Second; // Assuming basic second precision for Unix timestamp
160let input = unix_time; // Simulate input as integer since Unix times are often transmitted as strings
161let result = PoSQLTimestamp::to_timestamp(input).unwrap();
162163assert_eq!(result.timestamp, expected_datetime);
164assert_eq!(result.timeunit, expected_unit);
165 }
166167#[test]
168fn test_basic_rfc3339_timestamp() {
169let input = "2023-06-26T12:34:56Z";
170let expected = Utc.with_ymd_and_hms(2023, 6, 26, 12, 34, 56).unwrap();
171let result = PoSQLTimestamp::try_from(input).unwrap();
172assert_eq!(result.timestamp, expected);
173 }
174175#[test]
176fn test_rfc3339_timestamp_with_positive_offset() {
177let input = "2023-06-26T08:00:00+04:30";
178let expected = Utc.with_ymd_and_hms(2023, 6, 26, 3, 30, 0).unwrap(); // Adjusted to UTC
179let result = PoSQLTimestamp::try_from(input).unwrap();
180assert_eq!(result.timestamp, expected);
181 }
182183#[test]
184fn test_rfc3339_timestamp_with_negative_offset() {
185let input = "2023-06-26T20:00:00-05:00";
186let expected = Utc.with_ymd_and_hms(2023, 6, 27, 1, 0, 0).unwrap(); // Adjusted to UTC
187let result = PoSQLTimestamp::try_from(input).unwrap();
188assert_eq!(result.timestamp, expected);
189 }
190191#[test]
192fn test_rfc3339_timestamp_with_utc_designator() {
193let input = "2023-06-26T12:34:56Z";
194let expected = Utc.with_ymd_and_hms(2023, 6, 26, 12, 34, 56).unwrap();
195let result = PoSQLTimestamp::try_from(input).unwrap();
196assert_eq!(result.timestamp, expected);
197 }
198199#[test]
200fn test_invalid_rfc3339_timestamp() {
201let input = "not-a-timestamp";
202assert_eq!(
203 PoSQLTimestamp::try_from(input),
204Err(PoSQLTimestampError::ParsingError {
205 error: "input contains invalid characters".into()
206 })
207 );
208 }
209210#[test]
211fn test_timestamp_with_seconds() {
212let input = "2023-06-26T12:34:56Z";
213let expected_time = Utc.with_ymd_and_hms(2023, 6, 26, 12, 34, 56).unwrap();
214let expected_unit = PoSQLTimeUnit::Second;
215let result = PoSQLTimestamp::try_from(input).unwrap();
216assert_eq!(result.timestamp, expected_time);
217assert_eq!(result.timeunit, expected_unit);
218 }
219220#[test]
221fn test_general_parsing_error() {
222// This test assumes that there's a catch-all parsing error case that isn't covered by the more specific errors.
223let malformed_input = "2009-01-03T::00Z"; // Intentionally malformed timestamp
224let result = PoSQLTimestamp::try_from(malformed_input);
225assert!(matches!(
226 result,
227Err(PoSQLTimestampError::ParsingError { .. })
228 ));
229 }
230231#[test]
232fn test_basic_date_time_support() {
233let inputs = ["2009-01-03T18:15:05Z", "2009-01-03T18:15:05+02:00"];
234for input in inputs {
235assert!(
236 DateTime::parse_from_rfc3339(input).is_ok(),
237"Should parse correctly: {input}"
238);
239 }
240 }
241242#[test]
243fn test_leap_seconds() {
244let input = "1998-12-31T23:59:60Z"; // fyi the 59:-->60<-- is the leap second
245assert!(PoSQLTimestamp::try_from(input).is_ok());
246 }
247248#[test]
249fn test_leap_seconds_ranges() {
250// Timestamp just before the leap second
251let before_leap_second = "1998-12-31T23:59:59Z";
252// Timestamp during the leap second
253let leap_second = "1998-12-31T23:59:60Z";
254// Timestamp just after the leap second
255let after_leap_second = "1999-01-01T00:00:00Z";
256257// Parse timestamps
258let before_leap_dt = PoSQLTimestamp::try_from(before_leap_second).unwrap();
259let leap_second_dt = PoSQLTimestamp::try_from(leap_second).unwrap();
260let after_leap_dt = PoSQLTimestamp::try_from(after_leap_second).unwrap();
261262// Ensure that "23:59:60Z" - 1 second is considered equivalent to "23:59:59Z"
263assert_eq!(
264 before_leap_dt.timestamp,
265 leap_second_dt.timestamp - chrono::Duration::seconds(1)
266 );
267268// Ensure that "23:59:60Z" + 1 second is "1999-01-01T00:00:00Z"
269assert_eq!(
270 after_leap_dt.timestamp,
271 leap_second_dt.timestamp + chrono::Duration::seconds(1)
272 );
273 }
274275#[test]
276fn test_rejecting_incorrect_formats() {
277let incorrect_formats = [
278"2009-January-03",
279"25:61:61",
280"20090103",
281"181505",
282"18:15:05",
283 ];
284for input in incorrect_formats {
285assert!(
286 DateTime::parse_from_rfc3339(input).is_err(),
287"Should reject incorrect format: {input}"
288);
289 }
290 }
291}