proof_of_sql_parser/posql_time/
timezone.rs

1use super::PoSQLTimestampError;
2use alloc::{string::ToString, sync::Arc};
3use core::fmt;
4use serde::{Deserialize, Serialize};
5
6/// Captures a timezone from a timestamp query
7#[derive(Debug, Clone, Copy, Hash, Serialize, Deserialize, PartialEq, Eq)]
8pub struct PoSQLTimeZone {
9    offset: i32,
10}
11
12impl PoSQLTimeZone {
13    /// Create a timezone from a count of seconds
14    #[must_use]
15    pub const fn new(offset: i32) -> Self {
16        PoSQLTimeZone { offset }
17    }
18    #[must_use]
19    /// The UTC timezone
20    pub const fn utc() -> Self {
21        PoSQLTimeZone::new(0)
22    }
23    /// Get the underlying offset in seconds
24    #[must_use]
25    pub const fn offset(self) -> i32 {
26        self.offset
27    }
28}
29
30impl TryFrom<&Option<Arc<str>>> for PoSQLTimeZone {
31    type Error = PoSQLTimestampError;
32
33    fn try_from(value: &Option<Arc<str>>) -> Result<Self, Self::Error> {
34        match value {
35            Some(tz_str) => {
36                let tz = Arc::as_ref(tz_str).to_uppercase();
37                match tz.as_str() {
38                    "Z" | "UTC" | "00:00" | "+00:00" | "0:00" | "+0:00" => Ok(PoSQLTimeZone::utc()),
39                    tz if tz.chars().count() == 6
40                        && (tz.starts_with('+') || tz.starts_with('-')) =>
41                    {
42                        let sign = if tz.starts_with('-') { -1 } else { 1 };
43                        let hours = tz[1..3]
44                            .parse::<i32>()
45                            .map_err(|_| PoSQLTimestampError::InvalidTimezoneOffset)?;
46                        let minutes = tz[4..6]
47                            .parse::<i32>()
48                            .map_err(|_| PoSQLTimestampError::InvalidTimezoneOffset)?;
49                        let total_seconds = sign * ((hours * 3600) + (minutes * 60));
50                        Ok(PoSQLTimeZone::new(total_seconds))
51                    }
52                    _ => Err(PoSQLTimestampError::InvalidTimezone {
53                        timezone: tz.to_string(),
54                    }),
55                }
56            }
57            None => Ok(PoSQLTimeZone::utc()),
58        }
59    }
60}
61
62impl fmt::Display for PoSQLTimeZone {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        let seconds = self.offset();
65        let hours = seconds / 3600;
66        let minutes = (seconds.abs() % 3600) / 60;
67        if seconds < 0 {
68            write!(f, "-{:02}:{:02}", hours.abs(), minutes)
69        } else {
70            write!(f, "+{hours:02}:{minutes:02}")
71        }
72    }
73}
74
75#[cfg(test)]
76mod timezone_parsing_tests {
77    use crate::posql_time::timezone;
78    use alloc::format;
79
80    #[test]
81    fn test_display_fixed_offset_positive() {
82        let timezone = timezone::PoSQLTimeZone::new(4500); // +01:15
83        assert_eq!(format!("{timezone}"), "+01:15");
84    }
85
86    #[test]
87    fn test_display_fixed_offset_negative() {
88        let timezone = timezone::PoSQLTimeZone::new(-3780); // -01:03
89        assert_eq!(format!("{timezone}"), "-01:03");
90    }
91
92    #[test]
93    fn test_display_utc() {
94        let timezone = timezone::PoSQLTimeZone::utc();
95        assert_eq!(format!("{timezone}"), "+00:00");
96    }
97}
98
99#[cfg(test)]
100mod timezone_offset_tests {
101    use crate::posql_time::{timestamp::PoSQLTimestamp, timezone};
102
103    #[test]
104    fn test_utc_timezone() {
105        let input = "2023-06-26T12:34:56Z";
106        let expected_timezone = timezone::PoSQLTimeZone::utc();
107        let result = PoSQLTimestamp::try_from(input).unwrap();
108        assert_eq!(result.timezone(), expected_timezone);
109    }
110
111    #[test]
112    fn test_positive_offset_timezone() {
113        let input = "2023-06-26T12:34:56+03:30";
114        let expected_timezone = timezone::PoSQLTimeZone::new(12600); // 3 hours and 30 minutes in seconds
115        let result = PoSQLTimestamp::try_from(input).unwrap();
116        assert_eq!(result.timezone(), expected_timezone);
117    }
118
119    #[test]
120    fn test_negative_offset_timezone() {
121        let input = "2023-06-26T12:34:56-04:00";
122        let expected_timezone = timezone::PoSQLTimeZone::new(-14400); // -4 hours in seconds
123        let result = PoSQLTimestamp::try_from(input).unwrap();
124        assert_eq!(result.timezone(), expected_timezone);
125    }
126
127    #[test]
128    fn test_zero_offset_timezone() {
129        let input = "2023-06-26T12:34:56+00:00";
130        let expected_timezone = timezone::PoSQLTimeZone::utc(); // Zero offset defaults to UTC
131        let result = PoSQLTimestamp::try_from(input).unwrap();
132        assert_eq!(result.timezone(), expected_timezone);
133    }
134}