livesplit_core/timing/
time_span.rs

1use crate::{
2    platform::{prelude::*, Duration},
3    util::ascii_char::AsciiChar,
4};
5use core::{
6    num::ParseIntError,
7    ops::{Add, AddAssign, Neg, Sub, SubAssign},
8    str::FromStr,
9};
10use snafu::{ensure, OptionExt, ResultExt};
11
12/// A `TimeSpan` represents a certain span of time.
13#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
14pub struct TimeSpan(Duration);
15
16impl TimeSpan {
17    /// Creates a new `TimeSpan` of zero length.
18    pub const fn zero() -> Self {
19        Self(Duration::ZERO)
20    }
21
22    /// Creates a new `TimeSpan` from a given amount of seconds.
23    pub fn from_seconds(seconds: f64) -> Self {
24        Self(Duration::seconds_f64(seconds))
25    }
26
27    /// Creates a new `TimeSpan` from a given amount of milliseconds.
28    pub fn from_milliseconds(milliseconds: f64) -> Self {
29        Self(Duration::seconds_f64(0.001 * milliseconds))
30    }
31
32    /// Creates a new `TimeSpan` from a given amount of days.
33    pub fn from_days(days: f64) -> Self {
34        Self(Duration::seconds_f64(days * (24.0 * 60.0 * 60.0)))
35    }
36
37    /// Converts the `TimeSpan` to a `Duration` from the `time` crate.
38    pub const fn to_duration(&self) -> Duration {
39        self.0
40    }
41
42    /// Returns the underlying raw seconds and the nanoseconds past the last
43    /// full second that make up the `TimeSpan`. This is the most lossless
44    /// representation of a `TimeSpan`.
45    pub const fn to_seconds_and_subsec_nanoseconds(&self) -> (i64, i32) {
46        (self.0.whole_seconds(), self.0.subsec_nanoseconds())
47    }
48
49    /// Returns the total amount of seconds (including decimals) this `TimeSpan`
50    /// represents.
51    pub fn total_seconds(&self) -> f64 {
52        self.0.as_seconds_f64()
53    }
54
55    /// Returns the total amount of milliseconds (including decimals) this
56    /// `TimeSpan` represents.
57    pub fn total_milliseconds(&self) -> f64 {
58        1_000.0 * self.total_seconds()
59    }
60
61    /// Parses an optional `TimeSpan` from a given textual representation of the
62    /// `TimeSpan`. If the given text consists entirely of whitespace or is
63    /// empty, `None` is returned.
64    pub fn parse_opt(text: &str) -> Result<Option<TimeSpan>, ParseError> {
65        if text.trim().is_empty() {
66            Ok(None)
67        } else {
68            Ok(Some(text.parse()?))
69        }
70    }
71}
72
73/// The Error type for a `TimeSpan` that couldn't be parsed.
74#[derive(Debug, snafu::Snafu)]
75#[snafu(context(suffix(false)))]
76pub enum ParseError {
77    /// An empty string is not a valid `TimeSpan`.
78    Empty,
79    /// The time is too large to be represented.
80    Overflow,
81    /// The fractional part contains characters that are not digits.
82    FractionDigits,
83    /// Couldn't parse the fractional part.
84    Fraction {
85        /// The underlying error.
86        source: ParseIntError,
87    },
88    /// Couldn't parse the seconds, minutes, or hours.
89    Time {
90        /// The underlying error.
91        source: ParseIntError,
92    },
93}
94
95impl FromStr for TimeSpan {
96    type Err = ParseError;
97
98    fn from_str(mut text: &str) -> Result<Self, ParseError> {
99        // It's faster to use `strip_prefix` with char literals if it's an ASCII
100        // char, otherwise prefer using string literals.
101        #[allow(clippy::single_char_pattern)]
102        let negate =
103            if let Some(remainder) = text.strip_prefix('-').or_else(|| text.strip_prefix("−")) {
104                text = remainder;
105                true
106            } else {
107                false
108            };
109
110        let (seconds_text, nanos) =
111            if let Some((seconds, mut nanos)) = AsciiChar::DOT.split_once(text) {
112                if nanos.len() > 9 {
113                    nanos = nanos.get(..9).context(FractionDigits)?;
114                }
115                (
116                    seconds,
117                    nanos.parse::<u32>().context(Fraction)? * 10_u32.pow(9 - nanos.len() as u32),
118                )
119            } else {
120                (text, 0u32)
121            };
122
123        ensure!(!seconds_text.is_empty(), Empty);
124
125        let mut seconds = 0u64;
126
127        for split in AsciiChar::COLON.split_iter(seconds_text) {
128            seconds = seconds
129                .checked_mul(60)
130                .context(Overflow)?
131                .checked_add(split.parse::<u64>().context(Time)?)
132                .context(Overflow)?;
133        }
134
135        let (mut seconds, mut nanos) = (
136            i64::try_from(seconds).ok().context(Overflow)?,
137            i32::try_from(nanos).ok().context(Overflow)?,
138        );
139
140        if negate {
141            seconds = -seconds;
142            nanos = -nanos;
143        }
144
145        Ok(Duration::new(seconds, nanos).into())
146    }
147}
148
149impl Default for TimeSpan {
150    fn default() -> Self {
151        TimeSpan(Duration::ZERO)
152    }
153}
154
155impl From<Duration> for TimeSpan {
156    fn from(duration: Duration) -> Self {
157        TimeSpan(duration)
158    }
159}
160
161impl From<TimeSpan> for Duration {
162    fn from(time_span: TimeSpan) -> Self {
163        time_span.0
164    }
165}
166
167impl Add for TimeSpan {
168    type Output = TimeSpan;
169    fn add(self, rhs: TimeSpan) -> TimeSpan {
170        TimeSpan(self.0 + rhs.0)
171    }
172}
173
174impl Sub for TimeSpan {
175    type Output = TimeSpan;
176    fn sub(self, rhs: TimeSpan) -> TimeSpan {
177        TimeSpan(self.0 - rhs.0)
178    }
179}
180
181impl AddAssign for TimeSpan {
182    fn add_assign(&mut self, rhs: TimeSpan) {
183        *self = *self + rhs;
184    }
185}
186
187impl SubAssign for TimeSpan {
188    fn sub_assign(&mut self, rhs: TimeSpan) {
189        *self = *self - rhs;
190    }
191}
192
193impl Neg for TimeSpan {
194    type Output = TimeSpan;
195    fn neg(self) -> TimeSpan {
196        TimeSpan(-self.0)
197    }
198}
199
200use core::fmt;
201use serde::de::{self, Deserialize, Deserializer, Visitor};
202
203impl<'de> Deserialize<'de> for TimeSpan {
204    fn deserialize<D>(deserializer: D) -> Result<TimeSpan, D::Error>
205    where
206        D: Deserializer<'de>,
207    {
208        deserializer.deserialize_str(TimeSpanVisitor)
209    }
210}
211
212struct TimeSpanVisitor;
213
214impl Visitor<'_> for TimeSpanVisitor {
215    type Value = TimeSpan;
216
217    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
218        formatter.write_str("a string containing a time")
219    }
220
221    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
222    where
223        E: de::Error,
224    {
225        v.parse()
226            .map_err(|_| E::custom(format!("Not a valid time string: {v:?}")))
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn parsing() {
236        TimeSpan::from_str("-12:37:30.12").unwrap();
237        TimeSpan::from_str("-37:30.12").unwrap();
238        TimeSpan::from_str("-30.12").unwrap();
239        TimeSpan::from_str("-10:30").unwrap();
240        TimeSpan::from_str("-30").unwrap();
241        TimeSpan::from_str("-100").unwrap();
242        TimeSpan::from_str("--30").unwrap_err();
243        TimeSpan::from_str("-").unwrap_err();
244        TimeSpan::from_str("").unwrap_err();
245        TimeSpan::from_str("-10:-30").unwrap_err();
246        TimeSpan::from_str("10:-30").unwrap_err();
247        TimeSpan::from_str("10.5:30.5").unwrap_err();
248        TimeSpan::from_str("NaN").unwrap_err();
249        TimeSpan::from_str("Inf").unwrap_err();
250        assert!(matches!(
251            TimeSpan::from_str("1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1"),
252            Err(ParseError::Overflow),
253        ));
254        assert_eq!(
255            TimeSpan::from_str("10.123456789")
256                .unwrap()
257                .to_seconds_and_subsec_nanoseconds(),
258            (10, 123456789)
259        );
260        assert_eq!(
261            TimeSpan::from_str("10.0987654321987654321")
262                .unwrap()
263                .to_seconds_and_subsec_nanoseconds(),
264            (10, 98765432)
265        );
266    }
267}