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                        if hours > 12 || minutes >= 60 {
50                            return Err(PoSQLTimestampError::InvalidTimezoneOffset);
51                        }
52                        let total_seconds = sign * ((hours * 3600) + (minutes * 60));
53                        Ok(PoSQLTimeZone::new(total_seconds))
54                    }
55                    _ => Err(PoSQLTimestampError::InvalidTimezone {
56                        timezone: tz.to_string(),
57                    }),
58                }
59            }
60            None => Ok(PoSQLTimeZone::utc()),
61        }
62    }
63}
64
65impl fmt::Display for PoSQLTimeZone {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        let seconds = self.offset();
68        let hours = seconds / 3600;
69        let minutes = (seconds.abs() % 3600) / 60;
70        if seconds < 0 {
71            write!(f, "-{:02}:{:02}", hours.abs(), minutes)
72        } else {
73            write!(f, "+{hours:02}:{minutes:02}")
74        }
75    }
76}
77
78#[cfg(test)]
79mod timezone_arc_str_parsing {
80
81    use super::*;
82    use crate::posql_time::{timezone, PoSQLTimestampError::InvalidTimezoneOffset};
83    use alloc::format;
84
85    #[test]
86    fn test_parsing_from_arc_str_fixed_offset() {
87        let ss = "00:00";
88        let timezone_arc: Arc<str> = Arc::from(ss);
89        let timezone = timezone::PoSQLTimeZone::try_from(&Some(timezone_arc)).unwrap(); // +01:15
90        assert_eq!(format!("{timezone}"), "+00:00");
91    }
92
93    #[test]
94    fn test_parsing_from_arc_str_fixed_offset_positive() {
95        let input_timezone = "+01:15";
96        let timezone_arc: Arc<str> = Arc::from(input_timezone);
97        let timezone = timezone::PoSQLTimeZone::try_from(&Some(timezone_arc)).unwrap(); // +01:15
98        assert_eq!(format!("{timezone}"), "+01:15");
99    }
100
101    #[test]
102    fn test_parsing_from_arc_str_fixed_offset_negative() {
103        let input_timezone = "-01:03";
104        let timezone_arc: Arc<str> = Arc::from(input_timezone);
105        let timezone = timezone::PoSQLTimeZone::try_from(&Some(timezone_arc)).unwrap(); // +01:15
106        assert_eq!(format!("{timezone}"), "-01:03");
107    }
108
109    #[test]
110    fn check_for_invalid_timezone_hour_offset() {
111        let input_timezone = "-0A:03";
112        let timezone_arc: Arc<str> = Arc::from(input_timezone);
113        let offset_error = timezone::PoSQLTimeZone::try_from(&Some(timezone_arc)); // should be invalid time offset error
114        assert_eq!(offset_error, Err(InvalidTimezoneOffset));
115
116        let input_timezone = "-13:03";
117        let timezone_arc: Arc<str> = Arc::from(input_timezone);
118        let offset_error = timezone::PoSQLTimeZone::try_from(&Some(timezone_arc)); // should be invalid time offset error
119        assert_eq!(offset_error, Err(InvalidTimezoneOffset));
120
121        let input_timezone = "-11:60";
122        let timezone_arc: Arc<str> = Arc::from(input_timezone);
123        let offset_error = timezone::PoSQLTimeZone::try_from(&Some(timezone_arc)); // should be invalid time offset error
124        assert_eq!(offset_error, Err(InvalidTimezoneOffset));
125    }
126
127    #[test]
128    fn check_for_invalid_timezone_minute_offset() {
129        let input_timezone = "-00:B3";
130        let timezone_arc: Arc<str> = Arc::from(input_timezone);
131        let offset_error = timezone::PoSQLTimeZone::try_from(&Some(timezone_arc)); // should be invalid time offset error
132        assert_eq!(offset_error, Err(InvalidTimezoneOffset));
133        let input_timezone = "-00:83";
134        let timezone_arc: Arc<str> = Arc::from(input_timezone);
135        let offset_error = timezone::PoSQLTimeZone::try_from(&Some(timezone_arc)); // should be invalid time offset error
136        assert_eq!(offset_error, Err(InvalidTimezoneOffset));
137    }
138
139    #[test]
140    fn test_invalid_timezone() {
141        let expected = PoSQLTimestampError::InvalidTimezone {
142            timezone: "WRONG".to_string(),
143        };
144        let timezone_input = "WRONG";
145        let timezone_arc: Arc<str> = Arc::from(timezone_input);
146        let timezone_err = timezone::PoSQLTimeZone::try_from(&Some(timezone_arc)); // +01:15
147        assert_eq!(expected, timezone_err.err().unwrap());
148    }
149
150    #[test]
151    fn test_when_none() {
152        let timezone = timezone::PoSQLTimeZone::try_from(&None).unwrap(); // +01:15
153        assert_eq!(format!("{timezone}"), "+00:00");
154    }
155}
156
157#[cfg(test)]
158mod timezone_parsing_tests {
159    use crate::posql_time::timezone;
160    use alloc::format;
161
162    #[test]
163    fn test_display_fixed_offset_positive() {
164        let timezone = timezone::PoSQLTimeZone::new(4500); // +01:15
165        assert_eq!(format!("{timezone}"), "+01:15");
166    }
167
168    #[test]
169    fn test_display_fixed_offset_negative() {
170        let timezone = timezone::PoSQLTimeZone::new(-3780); // -01:03
171        assert_eq!(format!("{timezone}"), "-01:03");
172    }
173
174    #[test]
175    fn test_display_utc() {
176        let timezone = timezone::PoSQLTimeZone::utc();
177        assert_eq!(format!("{timezone}"), "+00:00");
178    }
179}
180
181#[cfg(test)]
182mod timezone_offset_tests {
183    use crate::posql_time::{timestamp::PoSQLTimestamp, timezone};
184
185    #[test]
186    fn test_utc_timezone() {
187        let input = "2023-06-26T12:34:56Z";
188        let expected_timezone = timezone::PoSQLTimeZone::utc();
189        let result = PoSQLTimestamp::try_from(input).unwrap();
190        assert_eq!(result.timezone(), expected_timezone);
191    }
192
193    #[test]
194    fn test_positive_offset_timezone() {
195        let input = "2023-06-26T12:34:56+03:30";
196        let expected_timezone = timezone::PoSQLTimeZone::new(12600); // 3 hours and 30 minutes in seconds
197        let result = PoSQLTimestamp::try_from(input).unwrap();
198        assert_eq!(result.timezone(), expected_timezone);
199    }
200
201    #[test]
202    fn test_negative_offset_timezone() {
203        let input = "2023-06-26T12:34:56-04:00";
204        let expected_timezone = timezone::PoSQLTimeZone::new(-14400); // -4 hours in seconds
205        let result = PoSQLTimestamp::try_from(input).unwrap();
206        assert_eq!(result.timezone(), expected_timezone);
207    }
208
209    #[test]
210    fn test_zero_offset_timezone() {
211        let input = "2023-06-26T12:34:56+00:00";
212        let expected_timezone = timezone::PoSQLTimeZone::utc(); // Zero offset defaults to UTC
213        let result = PoSQLTimestamp::try_from(input).unwrap();
214        assert_eq!(result.timezone(), expected_timezone);
215    }
216}