Skip to main content

calendar_types/
duration.rs

1//! RFC 5545 duration types.
2
3use thiserror::Error;
4
5use crate::{
6    primitive::Sign,
7    time::{FractionalSecond, InvalidFractionalSecondError},
8};
9
10/// An unsigned length of time (RFC 8984 §1.4.6).
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub enum Duration {
13    Nominal(NominalDuration),
14    Exact(ExactDuration),
15}
16
17/// An error arising from an invalid [`Duration`] value.
18#[derive(Debug, Clone, Copy, Error, PartialEq, Eq)]
19pub enum InvalidDurationError {
20    /// The fractional second component is invalid.
21    #[error("invalid fractional second: {0}")]
22    FractionalSecond(#[from] InvalidFractionalSecondError),
23}
24
25/// A [`Duration`] which may be positive or negative (RFC 8984 §1.4.7).
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
27pub struct SignedDuration {
28    /// The sign of this duration.
29    pub sign: Sign,
30    /// The unsigned duration value.
31    pub duration: Duration,
32}
33
34impl From<Duration> for SignedDuration {
35    fn from(value: Duration) -> Self {
36        Self {
37            sign: Default::default(),
38            duration: value,
39        }
40    }
41}
42
43/// A [`Duration`] measured in terms of weeks, days, hours, minutes, seconds, and fractional
44/// seconds.
45#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
46pub struct NominalDuration {
47    /// The number of weeks.
48    pub weeks: u32,
49    /// The number of days.
50    pub days: u32,
51    /// The optional sub-day time component.
52    pub exact: Option<ExactDuration>,
53}
54
55/// A [`Duration`] measured only in terms of hours, minutes, seconds, and fractional seconds.
56#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
57pub struct ExactDuration {
58    /// The number of hours.
59    pub hours: u32,
60    /// The number of minutes.
61    pub minutes: u32,
62    /// The number of whole seconds.
63    pub seconds: u32,
64    /// The optional fractional second component.
65    pub frac: Option<FractionalSecond>,
66}
67
68impl std::fmt::Display for ExactDuration {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        if self.hours > 0 {
71            write!(f, "{}H", self.hours)?;
72        }
73        if self.minutes > 0 || (self.hours > 0 && (self.seconds > 0 || self.frac.is_some())) {
74            write!(f, "{}M", self.minutes)?;
75        }
76        if self.seconds > 0 || self.frac.is_some() {
77            write!(f, "{}", self.seconds)?;
78            if let Some(frac) = self.frac {
79                let nanos = frac.get().get();
80                let mut s = format!("{nanos:09}");
81                let trimmed = s.trim_end_matches('0');
82                s.truncate(trimmed.len());
83                write!(f, ".{s}")?;
84            }
85            write!(f, "S")?;
86        }
87        // Handle the zero case: if nothing was written, write "0S"
88        if self.hours == 0 && self.minutes == 0 && self.seconds == 0 && self.frac.is_none() {
89            write!(f, "0S")?;
90        }
91        Ok(())
92    }
93}
94
95impl std::fmt::Display for NominalDuration {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        if self.weeks > 0 {
98            write!(f, "{}W", self.weeks)?;
99        }
100        if self.days > 0 {
101            write!(f, "{}D", self.days)?;
102        }
103        if let Some(exact) = &self.exact {
104            write!(f, "T{exact}")?;
105        }
106        // Handle the zero case
107        if self.weeks == 0 && self.days == 0 && self.exact.is_none() {
108            write!(f, "0D")?;
109        }
110        Ok(())
111    }
112}
113
114impl std::fmt::Display for Duration {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        write!(f, "P")?;
117        match self {
118            Duration::Nominal(n) => write!(f, "{n}"),
119            Duration::Exact(e) => write!(f, "T{e}"),
120        }
121    }
122}
123
124impl std::fmt::Display for SignedDuration {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        match self.sign {
127            Sign::Neg => write!(f, "-{}", self.duration),
128            Sign::Pos => write!(f, "{}", self.duration),
129        }
130    }
131}