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