1use alloc::{borrow::Cow, format, string::String};
16
17#[derive(Clone, Debug, Default, PartialEq, Eq)]
19pub struct IcalDuration<'a>(pub Cow<'a, str>);
20
21impl IcalDuration<'_> {
22 pub fn seconds(&self) -> Option<i64> {
29 let span = self.0.as_ref();
30
31 let (sign, span) = match span.strip_prefix('-') {
32 Some(span) => (-1, span),
33 None => (1, span.strip_prefix('+').unwrap_or(span)),
34 };
35
36 let mut total: i64 = 0;
37 let mut amount = String::new();
38
39 for character in span.strip_prefix('P')?.chars() {
40 if character.is_ascii_digit() {
41 amount.push(character);
42 continue;
43 }
44
45 if character == 'T' {
48 continue;
49 }
50
51 let unit = match character {
52 'W' => 604_800,
53 'D' => 86_400,
54 'H' => 3_600,
55 'M' => 60,
56 'S' => 1,
57 _ => return None,
58 };
59
60 total += amount.parse::<i64>().ok()? * unit;
61 amount.clear();
62 }
63
64 Some(sign * total)
65 }
66
67 pub fn from_seconds(seconds: i64) -> IcalDuration<'static> {
74 let sign = match seconds < 0 {
75 true => "-",
76 false => "",
77 };
78
79 let seconds = seconds.unsigned_abs();
80 let (days, rest) = (seconds / 86_400, seconds % 86_400);
81 let (hours, rest) = (rest / 3_600, rest % 3_600);
82 let (minutes, seconds) = (rest / 60, rest % 60);
83
84 let mut duration = String::from(sign);
85 duration.push('P');
86
87 if days > 0 {
88 duration.push_str(&format!("{days}D"));
89 }
90
91 if hours == 0 && minutes == 0 && seconds == 0 {
92 if days == 0 {
95 duration.push_str("T0S");
96 }
97
98 return IcalDuration(Cow::Owned(duration));
99 }
100
101 duration.push('T');
102
103 for (amount, unit) in [(hours, 'H'), (minutes, 'M'), (seconds, 'S')] {
104 if amount > 0 {
105 duration.push_str(&format!("{amount}{unit}"));
106 }
107 }
108
109 IcalDuration(Cow::Owned(duration))
110 }
111}
112
113impl<'a> From<&'a str> for IcalDuration<'a> {
114 fn from(value: &'a str) -> Self {
115 Self(Cow::Borrowed(value))
116 }
117}
118
119impl From<String> for IcalDuration<'_> {
120 fn from(value: String) -> Self {
121 Self(Cow::Owned(value))
122 }
123}
124
125impl<'a> From<Cow<'a, str>> for IcalDuration<'a> {
126 fn from(value: Cow<'a, str>) -> Self {
127 Self(value)
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use crate::value::duration::IcalDuration;
134
135 #[test]
136 fn reads_every_unit_the_grammar_admits() {
137 assert_eq!(IcalDuration::from("P1W").seconds(), Some(604_800));
138 assert_eq!(
139 IcalDuration::from("P15DT5H0M20S").seconds(),
140 Some(1_314_020)
141 );
142 assert_eq!(IcalDuration::from("-P1D").seconds(), Some(-86_400));
143 assert_eq!(IcalDuration::from("PT0S").seconds(), Some(0));
144 }
145
146 #[test]
147 fn refuses_what_is_not_a_duration() {
148 assert_eq!(IcalDuration::from("").seconds(), None);
149 assert_eq!(IcalDuration::from("1D").seconds(), None);
150 assert_eq!(IcalDuration::from("P1Y").seconds(), None);
151 }
152
153 #[test]
154 fn a_duration_written_from_seconds_reads_back_as_those_seconds() {
155 for seconds in [0, 1, 59, 60, 3_600, 86_400, 1_314_020, -86_400, -90] {
156 let written = IcalDuration::from_seconds(seconds);
157
158 assert_eq!(written.seconds(), Some(seconds), "{}", written.0);
159 }
160 }
161
162 #[test]
163 fn a_week_comes_back_spelled_in_days() {
164 assert_eq!(&*IcalDuration::from_seconds(604_800).0, "P7D");
165 }
166}