use alloc::{borrow::Cow, format, string::String};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct IcalDuration<'a>(pub Cow<'a, str>);
impl IcalDuration<'_> {
pub fn seconds(&self) -> Option<i64> {
let span = self.0.as_ref();
let (sign, span) = match span.strip_prefix('-') {
Some(span) => (-1, span),
None => (1, span.strip_prefix('+').unwrap_or(span)),
};
let mut total: i64 = 0;
let mut amount = String::new();
for character in span.strip_prefix('P')?.chars() {
if character.is_ascii_digit() {
amount.push(character);
continue;
}
if character == 'T' {
continue;
}
let unit = match character {
'W' => 604_800,
'D' => 86_400,
'H' => 3_600,
'M' => 60,
'S' => 1,
_ => return None,
};
total += amount.parse::<i64>().ok()? * unit;
amount.clear();
}
Some(sign * total)
}
pub fn from_seconds(seconds: i64) -> IcalDuration<'static> {
let sign = match seconds < 0 {
true => "-",
false => "",
};
let seconds = seconds.unsigned_abs();
let (days, rest) = (seconds / 86_400, seconds % 86_400);
let (hours, rest) = (rest / 3_600, rest % 3_600);
let (minutes, seconds) = (rest / 60, rest % 60);
let mut duration = String::from(sign);
duration.push('P');
if days > 0 {
duration.push_str(&format!("{days}D"));
}
if hours == 0 && minutes == 0 && seconds == 0 {
if days == 0 {
duration.push_str("T0S");
}
return IcalDuration(Cow::Owned(duration));
}
duration.push('T');
for (amount, unit) in [(hours, 'H'), (minutes, 'M'), (seconds, 'S')] {
if amount > 0 {
duration.push_str(&format!("{amount}{unit}"));
}
}
IcalDuration(Cow::Owned(duration))
}
}
impl<'a> From<&'a str> for IcalDuration<'a> {
fn from(value: &'a str) -> Self {
Self(Cow::Borrowed(value))
}
}
impl From<String> for IcalDuration<'_> {
fn from(value: String) -> Self {
Self(Cow::Owned(value))
}
}
impl<'a> From<Cow<'a, str>> for IcalDuration<'a> {
fn from(value: Cow<'a, str>) -> Self {
Self(value)
}
}
#[cfg(test)]
mod tests {
use crate::value::duration::IcalDuration;
#[test]
fn reads_every_unit_the_grammar_admits() {
assert_eq!(IcalDuration::from("P1W").seconds(), Some(604_800));
assert_eq!(
IcalDuration::from("P15DT5H0M20S").seconds(),
Some(1_314_020)
);
assert_eq!(IcalDuration::from("-P1D").seconds(), Some(-86_400));
assert_eq!(IcalDuration::from("PT0S").seconds(), Some(0));
}
#[test]
fn refuses_what_is_not_a_duration() {
assert_eq!(IcalDuration::from("").seconds(), None);
assert_eq!(IcalDuration::from("1D").seconds(), None);
assert_eq!(IcalDuration::from("P1Y").seconds(), None);
}
#[test]
fn a_duration_written_from_seconds_reads_back_as_those_seconds() {
for seconds in [0, 1, 59, 60, 3_600, 86_400, 1_314_020, -86_400, -90] {
let written = IcalDuration::from_seconds(seconds);
assert_eq!(written.seconds(), Some(seconds), "{}", written.0);
}
}
#[test]
fn a_week_comes_back_spelled_in_days() {
assert_eq!(&*IcalDuration::from_seconds(604_800).0, "P7D");
}
}