Parse a `±HH:MM` string into a [`CronTimezone`]. Accepts both `+00:00` and `-00:00` as UTC. # Errors - [`CronTimezoneError::IanaNotSupported`] — the string contains `/` or letters (other than a leading `+`/`-`), indicating an IANA name. - [`CronTimezoneError::Format`] — the string does not conform to `±HH:MM`. - [`CronTimezoneError::OutOfRange`] — the offset exceeds ±18:00.
(s: &str)
| 72 | /// - [`CronTimezoneError::Format`] — the string does not conform to `±HH:MM`. |
| 73 | /// - [`CronTimezoneError::OutOfRange`] — the offset exceeds ±18:00. |
| 74 | pub fn parse(s: &str) -> Result<Self, CronTimezoneError> { |
| 75 | // Detect IANA names: contain '/' or alphabetic characters after the |
| 76 | // optional leading sign. |
| 77 | let body = s.trim_start_matches(['+', '-']); |
| 78 | if body.contains('/') || body.chars().any(|c| c.is_alphabetic()) { |
| 79 | return Err(CronTimezoneError::IanaNotSupported(s.to_string())); |
| 80 | } |
| 81 | |
| 82 | // Must start with '+' or '-'. |
| 83 | let (sign, rest) = if let Some(r) = s.strip_prefix('+') { |
| 84 | (1i32, r) |
| 85 | } else if let Some(r) = s.strip_prefix('-') { |
| 86 | (-1i32, r) |
| 87 | } else { |
| 88 | return Err(CronTimezoneError::Format(s.to_string())); |
| 89 | }; |
| 90 | |
| 91 | // Parse HH:MM. Both parts must be pure ASCII digits (no embedded signs, |
| 92 | // spaces, or other characters that Rust's integer parser would accept). |
| 93 | let (hh_str, mm_str) = rest |
| 94 | .split_once(':') |
| 95 | .ok_or_else(|| CronTimezoneError::Format(s.to_string()))?; |
| 96 | |
| 97 | if !hh_str.chars().all(|c| c.is_ascii_digit()) |
| 98 | || !mm_str.chars().all(|c| c.is_ascii_digit()) |
| 99 | { |
| 100 | return Err(CronTimezoneError::Format(s.to_string())); |
| 101 | } |
| 102 | |
| 103 | let hours: i32 = hh_str |
| 104 | .parse() |
| 105 | .map_err(|_| CronTimezoneError::Format(s.to_string()))?; |
| 106 | let minutes: i32 = mm_str |
| 107 | .parse() |
| 108 | .map_err(|_| CronTimezoneError::Format(s.to_string()))?; |
| 109 | |
| 110 | // Validate field ranges. |
| 111 | if !(0..=23).contains(&hours) || !(0..=59).contains(&minutes) { |
| 112 | return Err(CronTimezoneError::Format(s.to_string())); |
| 113 | } |
| 114 | |
| 115 | let offset = sign * (hours * 3600 + minutes * 60); |
| 116 | |
| 117 | if offset.abs() > MAX_OFFSET_SECONDS { |
| 118 | return Err(CronTimezoneError::OutOfRange(s.to_string())); |
| 119 | } |
| 120 | |
| 121 | Ok(Self(offset)) |
| 122 | } |
| 123 | |
| 124 | /// Format the offset as a `±HH:MM` string. |
| 125 | pub fn as_string(&self) -> String { |