vast4_rs/
duration.rs

1/// Duration represents a span of time. The value format is `HH:MM:SS.mmm` or `HH:MM:SS`.
2#[derive(Default, PartialEq, Clone, Debug)]
3pub struct Duration(std::time::Duration);
4
5impl Duration {
6    const NANOS_PER_MILLI: u32 = 1_000_000;
7    const SECS_PER_HOURS: u64 = Self::SECS_PER_MINUTE * 60;
8    const SECS_PER_MINUTE: u64 = 60;
9
10    pub fn new(hours: u64, minutes: u64, secs: u64, milli_secs: u32) -> Self {
11        let secs = hours * Self::SECS_PER_HOURS + minutes * Self::SECS_PER_MINUTE + secs;
12        let nanos = milli_secs * Self::NANOS_PER_MILLI;
13        Self(std::time::Duration::new(secs, nanos))
14    }
15}
16
17impl From<std::time::Duration> for Duration {
18    fn from(value: std::time::Duration) -> Self {
19        Self(value)
20    }
21}
22
23impl From<Duration> for std::time::Duration {
24    fn from(value: Duration) -> Self {
25        value.0
26    }
27}
28
29impl std::fmt::Display for Duration {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        let secs = self.0.as_secs();
32        let millis = self.0.subsec_millis();
33
34        let hh = secs / Self::SECS_PER_HOURS;
35        let mm = (secs % Self::SECS_PER_HOURS) / Self::SECS_PER_MINUTE;
36        let ss = secs % Self::SECS_PER_MINUTE;
37
38        write!(f, "{hh:02}:{mm:02}:{ss:02}")?;
39        if millis > 0 {
40            write!(f, ".{millis:03}")?;
41        }
42        Ok(())
43    }
44}
45
46impl std::str::FromStr for Duration {
47    type Err = crate::VastParseError;
48
49    fn from_str(s: &str) -> Result<Self, Self::Err> {
50        macro_rules! error {
51            () => {
52                crate::VastParseError::new(format!("duration parse error: '{s}'"))
53            };
54        }
55        macro_rules! parse {
56            ($var:ident, $ty:ty) => {
57                $var.parse::<$ty>().map_err(|_| error!())
58            };
59        }
60
61        // allow for the attribute to be present as an empty string
62        let s = s.trim();
63        if s.is_empty() {
64            return Ok(Duration::new(0, 0, 0, 0));
65        }
66
67        let mut iter = s.split(':');
68        match (iter.next(), iter.next(), iter.next(), iter.next()) {
69            (Some(hh), None, None, None) => {
70                let hh = parse!(hh, u64)?;
71                Ok(Duration::new(hh, 0, 0, 0))
72            }
73            (Some(hh), Some(mm), None, None) => {
74                let hh = parse!(hh, u64)?;
75                let mm = parse!(mm, u64)?;
76                Ok(Duration::new(hh, mm, 0, 0))
77            }
78            (Some(hh), Some(mm), Some(ss_ms), None) => {
79                let hh = parse!(hh, u64)?;
80                let mm = parse!(mm, u64)?;
81                Ok(match ss_ms.split_once('.') {
82                    Some((ss, ms)) => Duration::new(hh, mm, parse!(ss, u64)?, parse!(ms, u32)?),
83                    None => Duration::new(hh, mm, parse!(ss_ms, u64)?, 0),
84                })
85            }
86            // fallthrough, if the duration cannot be parsed, then write an duration of 0
87            _ => Ok(Duration::new(0, 0, 0, 0)),
88        }
89    }
90}
91
92impl hard_xml::XmlWrite for Duration {
93    fn to_writer<W: std::io::Write>(
94        &self,
95        writer: &mut hard_xml::XmlWriter<W>,
96    ) -> hard_xml::XmlResult<()> {
97        if !self.0.is_zero() {
98            writer.write_element_start("Duration")?;
99            writer.write_element_end_open()?;
100            write!(writer.inner, "{self}")?;
101            writer.write_element_end_close("Duration")?;
102        }
103        Ok(())
104    }
105}
106
107impl<'a> hard_xml::XmlRead<'a> for Duration {
108    fn from_reader(reader: &mut hard_xml::XmlReader<'a>) -> hard_xml::XmlResult<Self> {
109        reader.read_till_element_start("Duration")?;
110        let text = reader.read_text("Duration")?;
111        <Duration as std::str::FromStr>::from_str(text.as_ref())
112            .map_err(|e| hard_xml::XmlError::FromStr(e.into()))
113    }
114}
115
116#[cfg(test)]
117#[test]
118fn test_duration_parse_and_format() {
119    use std::str::FromStr;
120
121    macro_rules! parse_and_format {
122        ($str:expr, $dur:expr) => {
123            let got = Duration::from_str($str).unwrap();
124            assert_eq!(got, $dur);
125            assert_eq!(format!("{got}"), $str);
126        };
127    }
128
129    parse_and_format!("11:11:11.111", Duration::new(11, 11, 11, 111));
130    parse_and_format!("12:34:56", Duration::new(12, 34, 56, 0));
131    parse_and_format!("00:00:00", Duration::from_str("").unwrap());
132    parse_and_format!("00:00:00", Duration::from_str("12:34:56:78").unwrap());
133}
134
135crate::declare_test!(
136    test_duration,
137    Duration,
138    r#"<Duration>11:11:11.111</Duration>"#,
139    Duration::new(11, 11, 11, 111)
140);
141
142// crate::declare_test!(test_duration_empty, Duration, "", Duration::new(11, 11, 11, 111));
143
144#[cfg(test)]
145#[derive(hard_xml::XmlWrite, hard_xml::XmlRead, PartialEq, Debug)]
146#[xml(tag = "Xml")]
147struct Xml {
148    #[xml(child = "Duration", default)]
149    dur: Duration,
150}
151
152crate::declare_test!(
153    test_duration_property,
154    Xml,
155    "<Xml><Duration>12:34:56.999</Duration></Xml>",
156    Xml {
157        dur: Duration::new(12, 34, 56, 999)
158    }
159);
160
161crate::declare_test!(
162    test_duration_property_empty,
163    Xml,
164    "<Xml></Xml>",
165    Xml {
166        dur: Default::default()
167    }
168);