proof_of_sql/base/posql_time/
timezone.rs1use super::PoSQLTimestampError;
2use alloc::{string::ToString, sync::Arc};
3use core::fmt;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, Hash, Serialize, Deserialize, PartialEq, Eq)]
8pub struct PoSQLTimeZone {
9 offset: i32,
10}
11
12impl PoSQLTimeZone {
13 #[must_use]
15 pub const fn new(offset: i32) -> Self {
16 PoSQLTimeZone { offset }
17 }
18 #[must_use]
19 pub const fn utc() -> Self {
21 PoSQLTimeZone::new(0)
22 }
23 #[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 super::*;
78 use alloc::format;
79
80 #[test]
81 fn test_display_fixed_offset_positive() {
82 let timezone = PoSQLTimeZone::new(4500); assert_eq!(format!("{timezone}"), "+01:15");
84 }
85
86 #[test]
87 fn test_display_fixed_offset_negative() {
88 let timezone = PoSQLTimeZone::new(-3780); assert_eq!(format!("{timezone}"), "-01:03");
90 }
91
92 #[test]
93 fn test_display_utc() {
94 let timezone = PoSQLTimeZone::utc();
95 assert_eq!(format!("{timezone}"), "+00:00");
96 }
97
98 #[test]
99 fn we_can_parse_time_zone() {
100 let timezone_as_str: Option<Arc<str>> = Some("-01:03".into());
101 let timezone = PoSQLTimeZone::try_from(&timezone_as_str).unwrap();
102 let expected_time_zone = PoSQLTimeZone::new(-3780);
103 assert_eq!(timezone, expected_time_zone);
104 }
105
106 #[test]
107 fn we_can_parse_none_time_zone() {
108 let timezone = PoSQLTimeZone::try_from(&None).unwrap();
109 let expected_time_zone = PoSQLTimeZone::utc();
110 assert_eq!(timezone, expected_time_zone);
111 }
112
113 #[test]
114 fn we_cannot_parse_invalid_time_zone() {
115 let timezone_as_str: Option<Arc<str>> = Some("111111111".into());
116 let timezone_err = PoSQLTimeZone::try_from(&timezone_as_str).unwrap_err();
117 assert!(matches!(
118 timezone_err,
119 PoSQLTimestampError::InvalidTimezone { timezone: _ }
120 ));
121 }
122}