1use crate::utilities::FormatError;
11use std::fmt;
12use std::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign};
13use std::str::FromStr;
14use std::time::Duration;
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub struct TimeSpan {
19 ticks: i64,
20}
21
22impl TimeSpan {
23 pub const TICKS_PER_MILLISECOND: i64 = 10_000;
25 pub const TICKS_PER_SECOND: i64 = 10_000_000;
27 pub const TICKS_PER_MINUTE: i64 = 600_000_000;
29 pub const TICKS_PER_HOUR: i64 = 36_000_000_000;
31 pub const TICKS_PER_DAY: i64 = 864_000_000_000;
33
34 pub const ZERO: TimeSpan = TimeSpan { ticks: 0 };
36 pub const MAX_VALUE: TimeSpan = TimeSpan { ticks: i64::MAX };
38 pub const MIN_VALUE: TimeSpan = TimeSpan { ticks: i64::MIN };
40
41 #[inline]
43 pub const fn from_ticks(ticks: i64) -> Self {
44 Self { ticks }
45 }
46
47 pub fn from_seconds(value: f64) -> Self {
50 Self::interval(value, Self::TICKS_PER_SECOND as f64)
51 }
52
53 pub fn from_milliseconds(value: f64) -> Self {
56 Self::interval(value, Self::TICKS_PER_MILLISECOND as f64)
57 }
58
59 pub fn from_minutes(value: f64) -> Self {
61 Self::interval(value, Self::TICKS_PER_MINUTE as f64)
62 }
63
64 fn interval(value: f64, scale: f64) -> Self {
65 assert!(!value.is_nan(), "TimeSpan does not accept floating point Not-a-Number values.");
66 let ticks = value * scale;
67 assert!(
68 ticks <= i64::MAX as f64 && ticks >= i64::MIN as f64,
69 "TimeSpan overflowed because the duration is too long."
70 );
71 Self { ticks: ticks.round() as i64 }
72 }
73
74 #[inline]
76 pub const fn ticks(self) -> i64 {
77 self.ticks
78 }
79
80 #[inline]
82 pub fn total_seconds(self) -> f64 {
83 self.ticks as f64 / Self::TICKS_PER_SECOND as f64
84 }
85
86 #[inline]
88 pub fn total_milliseconds(self) -> f64 {
89 self.ticks as f64 / Self::TICKS_PER_MILLISECOND as f64
90 }
91
92 pub fn to_duration(self) -> Option<Duration> {
94 if self.ticks < 0 {
95 return None;
96 }
97 let ticks = self.ticks as u64;
98 Some(Duration::new(ticks / Self::TICKS_PER_SECOND as u64, ((ticks % Self::TICKS_PER_SECOND as u64) * 100) as u32))
99 }
100
101 pub fn parse(s: &str) -> Result<TimeSpan, FormatError> {
104 let invalid = || FormatError::from_string(format!("Invalid TimeSpan string: \"{s}\"."));
105 let text = s.trim();
106 let (negative, body) = match text.strip_prefix('-') {
107 Some(rest) => (true, rest),
108 None => (false, text),
109 };
110 if body.is_empty() {
111 return Err(invalid());
112 }
113 let number = |part: &str| -> Result<i64, FormatError> {
114 if part.is_empty() || part.len() > 9 || !part.bytes().all(|b| b.is_ascii_digit()) {
115 return Err(invalid());
116 }
117 part.parse::<i64>().map_err(|_| invalid())
118 };
119
120 let parts: Vec<&str> = body.split(':').collect();
121 let (mut days, hours, minutes, seconds_part): (i64, i64, i64, Option<&str>) = match parts.as_slice() {
122 [days] => (number(days)?, 0, 0, None),
123 [hours, minutes] => {
124 let (days, hours) = Self::split_days(hours, &number)?;
125 (days, hours, number(minutes)?, None)
126 }
127 [hours, minutes, seconds] => {
128 let (days, hours) = Self::split_days(hours, &number)?;
129 (days, hours, number(minutes)?, Some(*seconds))
130 }
131 [days, hours, minutes, seconds] => (number(days)?, number(hours)?, number(minutes)?, Some(*seconds)),
132 _ => return Err(invalid()),
133 };
134 let (seconds, fraction_ticks) = match seconds_part {
135 None => (0, 0),
136 Some(part) => match part.split_once('.') {
137 None => (number(part)?, 0),
138 Some((whole, fraction)) => {
139 if fraction.is_empty() || fraction.len() > 7 || !fraction.bytes().all(|b| b.is_ascii_digit()) {
140 return Err(invalid());
141 }
142 let mut ticks = fraction.parse::<i64>().map_err(|_| invalid())?;
143 for _ in fraction.len()..7 {
144 ticks *= 10;
145 }
146 (if whole.is_empty() { 0 } else { number(whole)? }, ticks)
148 }
149 },
150 };
151 if hours > 23 || minutes > 59 || seconds > 59 {
152 return Err(FormatError::from_string(format!(
153 "The TimeSpan string \"{s}\" could not be parsed because at least one of the numeric components is out of range."
154 )));
155 }
156 if days > 10_675_199 {
157 days = i64::MAX / Self::TICKS_PER_DAY + 1;
158 }
159 let ticks = days
160 .checked_mul(Self::TICKS_PER_DAY)
161 .and_then(|t| t.checked_add(hours * Self::TICKS_PER_HOUR))
162 .and_then(|t| t.checked_add(minutes * Self::TICKS_PER_MINUTE))
163 .and_then(|t| t.checked_add(seconds * Self::TICKS_PER_SECOND))
164 .and_then(|t| t.checked_add(fraction_ticks))
165 .ok_or_else(|| {
166 FormatError::from_string(format!(
167 "The TimeSpan string \"{s}\" could not be parsed because at least one of the numeric components is out of range."
168 ))
169 })?;
170 Ok(TimeSpan { ticks: if negative { -ticks } else { ticks } })
171 }
172
173 fn split_days(part: &str, number: &dyn Fn(&str) -> Result<i64, FormatError>) -> Result<(i64, i64), FormatError> {
174 match part.split_once('.') {
175 Some((days, hours)) => Ok((number(days)?, number(hours)?)),
176 None => Ok((0, number(part)?)),
177 }
178 }
179}
180
181impl TimeSpan {
183 pub fn from_hms(hours: i32, minutes: i32, seconds: i32) -> Self {
186 Self::from_dhms_milliseconds(0, hours, minutes, seconds, 0)
187 }
188
189 pub fn from_dhms(days: i32, hours: i32, minutes: i32, seconds: i32) -> Self {
192 Self::from_dhms_milliseconds(days, hours, minutes, seconds, 0)
193 }
194
195 pub fn from_dhms_milliseconds(days: i32, hours: i32, minutes: i32, seconds: i32, milliseconds: i32) -> Self {
199 let total_milliseconds = (days as i64 * 86_400 + hours as i64 * 3_600 + minutes as i64 * 60 + seconds as i64) * 1_000
200 + milliseconds as i64;
201 let ticks = total_milliseconds
202 .checked_mul(Self::TICKS_PER_MILLISECOND)
203 .expect("TimeSpan overflowed because the duration is too long.");
204 Self { ticks }
205 }
206
207 pub fn from_hours(value: f64) -> Self {
209 Self::interval(value, Self::TICKS_PER_HOUR as f64)
210 }
211
212 pub fn from_days(value: f64) -> Self {
214 Self::interval(value, Self::TICKS_PER_DAY as f64)
215 }
216
217 #[inline]
219 pub const fn days(self) -> i32 {
220 (self.ticks / Self::TICKS_PER_DAY) as i32
221 }
222
223 #[inline]
225 pub const fn hours(self) -> i32 {
226 (self.ticks / Self::TICKS_PER_HOUR % 24) as i32
227 }
228
229 #[inline]
231 pub const fn minutes(self) -> i32 {
232 (self.ticks / Self::TICKS_PER_MINUTE % 60) as i32
233 }
234
235 #[inline]
237 pub const fn seconds(self) -> i32 {
238 (self.ticks / Self::TICKS_PER_SECOND % 60) as i32
239 }
240
241 #[inline]
243 pub const fn milliseconds(self) -> i32 {
244 (self.ticks / Self::TICKS_PER_MILLISECOND % 1000) as i32
245 }
246
247 #[inline]
249 pub fn total_days(self) -> f64 {
250 self.ticks as f64 / Self::TICKS_PER_DAY as f64
251 }
252
253 #[inline]
255 pub fn total_hours(self) -> f64 {
256 self.ticks as f64 / Self::TICKS_PER_HOUR as f64
257 }
258
259 #[inline]
261 pub fn total_minutes(self) -> f64 {
262 self.ticks as f64 / Self::TICKS_PER_MINUTE as f64
263 }
264
265 pub fn duration(self) -> TimeSpan {
267 TimeSpan { ticks: self.ticks.checked_abs().expect("TimeSpan overflowed because the duration is too long.") }
268 }
269
270 #[track_caller]
277 pub fn to_string_format(self, format: &str) -> String {
278 match self.try_to_string_format(format) {
279 Ok(text) => text,
280 Err(error) => panic!("{error}"),
281 }
282 }
283
284 pub fn try_to_string_format(self, format: &str) -> Result<String, FormatError> {
287 use std::fmt::Write;
288
289 let invalid = || FormatError::new("Input string was not in a correct format.");
290 let negative = self.ticks < 0;
291 let ticks = self.ticks.unsigned_abs();
292 let days = ticks / Self::TICKS_PER_DAY as u64;
293 let hours = ticks / Self::TICKS_PER_HOUR as u64 % 24;
294 let minutes = ticks / Self::TICKS_PER_MINUTE as u64 % 60;
295 let seconds = ticks / Self::TICKS_PER_SECOND as u64 % 60;
296 let fraction = ticks % Self::TICKS_PER_SECOND as u64;
297
298 let mut out = String::new();
299 let mut chars = format.chars();
300 match (chars.next(), chars.next()) {
301 (None, _) | (Some('c' | 't' | 'T'), None) => return Ok(self.to_string()),
302 (Some('g'), None) => {
303 if negative {
304 out.push('-');
305 }
306 if days != 0 {
307 let _ = write!(out, "{days}:");
308 }
309 let _ = write!(out, "{hours}:{minutes:02}:{seconds:02}");
310 if fraction != 0 {
311 let digits = format!("{fraction:07}");
312 let _ = write!(out, ".{}", digits.trim_end_matches('0'));
313 }
314 return Ok(out);
315 }
316 (Some('G'), None) => {
317 if negative {
318 out.push('-');
319 }
320 let _ = write!(out, "{days}:{hours:02}:{minutes:02}:{seconds:02}.{fraction:07}");
321 return Ok(out);
322 }
323 (Some(_), None) => return Err(invalid()),
324 _ => {}
325 }
326
327 let bytes = format.as_bytes();
330 let mut index = 0;
331 let mut single = false;
332 while index < bytes.len() {
333 let byte = bytes[index];
334 let count = if single { 1 } else { bytes[index..].iter().take_while(|other| **other == byte).count() };
335 single = false;
336 match byte {
337 b'd' if count <= 8 => {
338 let _ = write!(out, "{days:0count$}");
339 }
340 b'h' if count <= 2 => {
341 let _ = write!(out, "{hours:0count$}");
342 }
343 b'm' if count <= 2 => {
344 let _ = write!(out, "{minutes:0count$}");
345 }
346 b's' if count <= 2 => {
347 let _ = write!(out, "{seconds:0count$}");
348 }
349 b'f' if count <= 7 => {
350 let value = fraction / 10_u64.pow((7 - count) as u32);
351 let _ = write!(out, "{value:0count$}");
352 }
353 b'F' if count <= 7 => {
354 let mut value = fraction / 10_u64.pow((7 - count) as u32);
355 let mut digits = count;
356 while digits > 0 && value % 10 == 0 {
357 value /= 10;
358 digits -= 1;
359 }
360 if digits > 0 {
361 let _ = write!(out, "{value:0digits$}");
362 }
363 }
364 b'\'' | b'"' => {
365 let mut rest = format[index + 1..].char_indices();
368 let end = loop {
369 match rest.next() {
370 Some((offset, c)) if c == byte as char => break index + 1 + offset,
371 Some((_, '\\')) => out.push(rest.next().ok_or_else(invalid)?.1),
372 Some((_, c)) => out.push(c),
373 None => return Err(invalid()),
374 }
375 };
376 index = end + 1;
377 continue;
378 }
379 b'%' => {
380 match bytes.get(index + 1) {
384 Some(next) if !matches!(*next, b'%' | b'\'' | b'"' | b'\\') => single = true,
385 _ => return Err(invalid()),
386 }
387 index += 1;
388 continue;
389 }
390 b'\\' => {
391 let escaped = format[index + 1..].chars().next().ok_or_else(invalid)?;
392 out.push(escaped);
393 index += 1 + escaped.len_utf8();
394 continue;
395 }
396 _ => return Err(invalid()),
397 }
398 index += count;
399 }
400 Ok(out)
401 }
402}
403
404impl From<Duration> for TimeSpan {
405 fn from(value: Duration) -> Self {
408 let ticks = value.as_nanos() / 100;
409 TimeSpan { ticks: i64::try_from(ticks).unwrap_or(i64::MAX) }
410 }
411}
412
413impl FromStr for TimeSpan {
414 type Err = FormatError;
415 fn from_str(s: &str) -> Result<Self, Self::Err> {
416 TimeSpan::parse(s)
417 }
418}
419
420impl fmt::Display for TimeSpan {
421 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423 let negative = self.ticks < 0;
424 let ticks = self.ticks.unsigned_abs();
425 let days = ticks / Self::TICKS_PER_DAY as u64;
426 let hours = ticks / Self::TICKS_PER_HOUR as u64 % 24;
427 let minutes = ticks / Self::TICKS_PER_MINUTE as u64 % 60;
428 let seconds = ticks / Self::TICKS_PER_SECOND as u64 % 60;
429 let fraction = ticks % Self::TICKS_PER_SECOND as u64;
430 if negative {
431 f.write_str("-")?;
432 }
433 if days != 0 {
434 write!(f, "{days}.")?;
435 }
436 write!(f, "{hours:02}:{minutes:02}:{seconds:02}")?;
437 if fraction != 0 {
438 write!(f, ".{fraction:07}")?;
439 }
440 Ok(())
441 }
442}
443
444impl Add for TimeSpan {
445 type Output = TimeSpan;
446 #[inline]
447 fn add(self, rhs: TimeSpan) -> TimeSpan {
448 TimeSpan { ticks: self.ticks.checked_add(rhs.ticks).expect("TimeSpan overflowed because the duration is too long.") }
449 }
450}
451
452impl Sub for TimeSpan {
453 type Output = TimeSpan;
454 #[inline]
455 fn sub(self, rhs: TimeSpan) -> TimeSpan {
456 TimeSpan { ticks: self.ticks.checked_sub(rhs.ticks).expect("TimeSpan overflowed because the duration is too long.") }
457 }
458}
459
460impl AddAssign for TimeSpan {
461 #[inline]
462 fn add_assign(&mut self, rhs: TimeSpan) {
463 *self = *self + rhs;
464 }
465}
466
467impl SubAssign for TimeSpan {
468 #[inline]
469 fn sub_assign(&mut self, rhs: TimeSpan) {
470 *self = *self - rhs;
471 }
472}
473
474impl Neg for TimeSpan {
475 type Output = TimeSpan;
476 #[inline]
477 fn neg(self) -> TimeSpan {
478 TimeSpan { ticks: self.ticks.checked_neg().expect("TimeSpan overflowed because the duration is too long.") }
479 }
480}
481
482impl Mul<f64> for TimeSpan {
483 type Output = TimeSpan;
484 fn mul(self, rhs: f64) -> TimeSpan {
486 TimeSpan::interval(self.ticks as f64, rhs)
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493
494 #[test]
495 fn from_seconds_rounds_to_ticks() {
496 assert_eq!(TimeSpan::from_seconds(1.0).ticks(), 10_000_000);
497 assert_eq!(TimeSpan::from_seconds(-0.5).ticks(), -5_000_000);
498 assert_eq!(TimeSpan::from_milliseconds(10.0).ticks(), 100_000);
499 assert_eq!(TimeSpan::from_seconds(0.5).total_seconds(), 0.5);
500 }
501
502 #[test]
503 fn parse_accepts_the_invariant_formats() {
504 assert_eq!(TimeSpan::parse("0:0:1").unwrap(), TimeSpan::from_seconds(1.0));
505 assert_eq!(TimeSpan::parse("00:00:00.3").unwrap(), TimeSpan::from_milliseconds(300.0));
506 assert_eq!(TimeSpan::parse("00:00:10.0153932").unwrap().ticks(), 100_153_932);
507 assert_eq!(TimeSpan::parse("1").unwrap().ticks(), TimeSpan::TICKS_PER_DAY);
508 assert_eq!(TimeSpan::parse("1:30").unwrap().ticks(), TimeSpan::TICKS_PER_HOUR + 30 * TimeSpan::TICKS_PER_MINUTE);
509 assert_eq!(TimeSpan::parse("1.02:03:04").unwrap().ticks(), TimeSpan::parse("1:2:3:4").unwrap().ticks());
510 assert_eq!(TimeSpan::parse(" -0:0:2 ").unwrap(), TimeSpan::from_seconds(-2.0));
511 assert_eq!(TimeSpan::parse("0:0:.075").unwrap().ticks(), 750_000);
513 }
514
515 #[test]
516 fn parse_rejects_malformed_input() {
517 for input in ["", "abc", "1:2:3:4:5", "0:60:0", "24:00", "0:0:1.", "0:0:1.12345678", "1,5"] {
518 assert!(TimeSpan::parse(input).is_err(), "{input}");
519 }
520 }
521
522 #[test]
523 fn display_round_trips() {
524 for input in ["00:00:01", "1.02:03:04", "-00:00:02.5000000", "00:00:10.0153932"] {
525 assert_eq!(TimeSpan::parse(input).unwrap().to_string(), input);
526 }
527 }
528
529 #[test]
532 fn custom_formats_match_net() {
533 let spans = [
534 TimeSpan::from_hms(0, 0, 0),
535 TimeSpan::from_hms(9, 4, 5),
536 TimeSpan::from_hms(15, 4, 5),
537 TimeSpan::from_dhms(1, 2, 3, 4),
538 -TimeSpan::from_hms(3, 4, 5),
539 TimeSpan::from_dhms_milliseconds(0, 23, 59, 59, 999),
540 ];
541 let rows: [(&str, Option<[&str; 6]>); 20] = [
542 ("%h", Some(["0", "9", "15", "2", "3", "23"])),
543 ("hh", Some(["00", "09", "15", "02", "03", "23"])),
544 ("h", None),
545 ("%m", Some(["0", "4", "4", "3", "4", "59"])),
546 ("mm", Some(["00", "04", "04", "03", "04", "59"])),
547 ("ss", Some(["00", "05", "05", "04", "05", "59"])),
548 ("%s", Some(["0", "5", "5", "4", "5", "59"])),
549 ("%'a'", None),
550 ("%%", None),
551 ("h\\:mm", Some(["0:00", "9:04", "15:04", "2:03", "3:04", "23:59"])),
552 ("hh':'mm", Some(["00:00", "09:04", "15:04", "02:03", "03:04", "23:59"])),
553 ("%d", Some(["0", "0", "0", "1", "0", "0"])),
554 ("hhh", None),
555 ("%hh", Some(["00", "99", "1515", "22", "33", "2323"])),
556 ("h mm", None),
557 ("\\h", Some(["h"; 6])),
558 ("'ab", None),
559 ("%\\h", None),
560 ("ff", Some(["00", "00", "00", "00", "00", "99"])),
561 ("%F", Some(["", "", "", "", "", "9"])),
562 ];
563 for (format, expected) in rows {
564 for (index, span) in spans.iter().enumerate() {
565 let actual = span.try_to_string_format(format).ok();
566 assert_eq!(expected.map(|texts| texts[index].to_string()), actual, "{format:?} of {span}");
567 }
568 }
569
570 assert_eq!("h'm 09", TimeSpan::from_hms(9, 4, 5).to_string_format("'h\\'m 'hh"));
572 assert!(TimeSpan::from_hms(9, 4, 5).try_to_string_format("'h\\'").is_err());
573 }
574
575 #[test]
576 fn converts_from_and_to_duration() {
577 assert_eq!(TimeSpan::from(Duration::from_millis(1500)), TimeSpan::from_seconds(1.5));
578 assert_eq!(TimeSpan::from_seconds(1.5).to_duration(), Some(Duration::from_millis(1500)));
579 assert_eq!(TimeSpan::from_seconds(-1.0).to_duration(), None);
580 }
581}