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}