1use core::ops::Range;
14
15use alloc::{borrow::Cow, string::String};
16
17#[derive(Clone, Debug, Default, PartialEq, Eq)]
19pub struct IcalUtcOffset<'a>(pub Cow<'a, str>);
20
21impl IcalUtcOffset<'_> {
22 pub fn seconds(&self) -> Option<i32> {
27 let text = self.0.as_ref();
28
29 let (sign, digits) = match text.as_bytes().first()? {
30 b'+' => (1, &text[1..]),
31 b'-' => (-1, &text[1..]),
32 _ => (1, text),
33 };
34
35 if !matches!(digits.len(), 4 | 6) || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
36 return None;
37 }
38
39 let part = |range: Range<usize>| digits[range].parse::<i32>().ok();
40
41 let hours = part(0..2)?;
42 let minutes = part(2..4)?;
43 let seconds = if digits.len() == 6 { part(4..6)? } else { 0 };
44
45 Some(sign * (hours * 3600 + minutes * 60 + seconds))
46 }
47}
48
49impl<'a> From<&'a str> for IcalUtcOffset<'a> {
50 fn from(value: &'a str) -> Self {
51 Self(Cow::Borrowed(value))
52 }
53}
54
55impl From<String> for IcalUtcOffset<'_> {
56 fn from(value: String) -> Self {
57 Self(Cow::Owned(value))
58 }
59}
60
61impl<'a> From<Cow<'a, str>> for IcalUtcOffset<'a> {
62 fn from(value: Cow<'a, str>) -> Self {
63 Self(value)
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use crate::value::utc_offset::IcalUtcOffset;
70
71 #[test]
72 fn reads_every_offset_spelling() {
73 assert_eq!(IcalUtcOffset::from("-0500").seconds(), Some(-18_000));
74 assert_eq!(IcalUtcOffset::from("+0100").seconds(), Some(3_600));
75 assert_eq!(IcalUtcOffset::from("+053045").seconds(), Some(19_845));
76 assert_eq!(IcalUtcOffset::from("0000").seconds(), Some(0));
77 }
78
79 #[test]
80 fn refuses_what_is_not_an_offset() {
81 assert_eq!(IcalUtcOffset::from("").seconds(), None);
82 assert_eq!(IcalUtcOffset::from("+5").seconds(), None);
83 assert_eq!(IcalUtcOffset::from("+05:00").seconds(), None);
84 assert_eq!(IcalUtcOffset::from("+0h00").seconds(), None);
85 }
86}