livesplit_core/timing/
time_span.rs1use 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#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
14pub struct TimeSpan(Duration);
15
16impl TimeSpan {
17 pub const fn zero() -> Self {
19 Self(Duration::ZERO)
20 }
21
22 pub fn from_seconds(seconds: f64) -> Self {
24 Self(Duration::seconds_f64(seconds))
25 }
26
27 pub fn from_milliseconds(milliseconds: f64) -> Self {
29 Self(Duration::seconds_f64(0.001 * milliseconds))
30 }
31
32 pub fn from_days(days: f64) -> Self {
34 Self(Duration::seconds_f64(days * (24.0 * 60.0 * 60.0)))
35 }
36
37 pub const fn to_duration(&self) -> Duration {
39 self.0
40 }
41
42 pub const fn to_seconds_and_subsec_nanoseconds(&self) -> (i64, i32) {
46 (self.0.whole_seconds(), self.0.subsec_nanoseconds())
47 }
48
49 pub fn total_seconds(&self) -> f64 {
52 self.0.as_seconds_f64()
53 }
54
55 pub fn total_milliseconds(&self) -> f64 {
58 1_000.0 * self.total_seconds()
59 }
60
61 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#[derive(Debug, snafu::Snafu)]
75#[snafu(context(suffix(false)))]
76pub enum ParseError {
77 Empty,
79 Overflow,
81 FractionDigits,
83 Fraction {
85 source: ParseIntError,
87 },
88 Time {
90 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 #[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}