Skip to content

Commit 9abf7cf

Browse files
authored
Don't use Floats to Parse Times (#578)
We want to be able to perfectly roundtrip a time, and since we already switched to using only integers when formatting a time, we are able to store them in a splits file in full precision, down to the nanosecond. This PR allows us to parse them again without any loss of precision. In the PRs for formatting a time we gained a bunch of speed by switching to the integers. I haven't done any benchmarks here, but improving the parsing speed isn't the point of this PR anyway.
1 parent 026c66d commit 9abf7cf

8 files changed

Lines changed: 117 additions & 61 deletions

File tree

src/timing/formatter/segment_time.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,5 @@ impl Display for Inner {
106106
fn test() {
107107
let time = "4:20.69".parse::<TimeSpan>().unwrap();
108108
let formatted = SegmentTime::new().format(time).to_string();
109-
// FIXME: The parser should use integer parsing as well, and then this
110-
// doesn't need to specialize bad floating point behavior anymore.
111-
assert!(
112-
// Modern processors
113-
formatted == "4:20.69" ||
114-
// Old x86 processors are apparently less precise
115-
formatted == "4:20.68"
116-
);
109+
assert_eq!(formatted, "4:20.69");
117110
}

src/timing/time_span.rs

Lines changed: 78 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
use crate::platform::{prelude::*, Duration};
1+
use crate::{
2+
platform::{prelude::*, Duration},
3+
util::ascii_char::AsciiChar,
4+
};
25
use core::{
3-
num::{ParseFloatError, ParseIntError},
6+
num::ParseIntError,
47
ops::{Add, AddAssign, Neg, Sub, SubAssign},
58
str::FromStr,
69
};
@@ -73,15 +76,17 @@ impl TimeSpan {
7376
pub enum ParseError {
7477
/// An empty string is not a valid Time Span.
7578
Empty,
76-
/// The seconds need to be a finite number.
77-
Finite,
78-
/// Couldn't parse the seconds.
79-
Seconds {
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 {
8085
/// The underlying error.
81-
source: ParseFloatError,
86+
source: ParseIntError,
8287
},
83-
/// Couldn't parse the minutes or hours.
84-
MinutesOrHours {
88+
/// Couldn't parse the seconds, minutes, or hours.
89+
Time {
8590
/// The underlying error.
8691
source: ParseIntError,
8792
},
@@ -94,27 +99,50 @@ impl FromStr for TimeSpan {
9499
// It's faster to use `strip_prefix` with char literals if it's an ASCII
95100
// char, otherwise prefer using string literals.
96101
#[allow(clippy::single_char_pattern)]
97-
let factor =
102+
let negate =
98103
if let Some(remainder) = text.strip_prefix('-').or_else(|| text.strip_prefix("−")) {
99104
text = remainder;
100-
-1.0
105+
true
101106
} else {
102-
1.0
107+
false
103108
};
104109

105-
let mut pieces = text.split(':');
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+
};
106122

107-
let last = pieces.next_back().context(Empty)?;
108-
let seconds = last.parse::<f64>().context(Seconds)?;
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+
}
109134

110-
ensure!(seconds.is_finite() && seconds >= 0.0, Finite);
135+
let (mut seconds, mut nanos) = (
136+
i64::try_from(seconds).ok().context(Overflow)?,
137+
i32::try_from(nanos).ok().context(Overflow)?,
138+
);
111139

112-
let mut minutes = 0.0;
113-
for split in pieces {
114-
minutes = minutes * 60.0 + split.parse::<u32>().context(MinutesOrHours)? as f64;
140+
if negate {
141+
seconds = -seconds;
142+
nanos = -nanos;
115143
}
116144

117-
Ok(TimeSpan::from_seconds(factor * (seconds + minutes * 60.0)))
145+
Ok(Duration::new(seconds, nanos).into())
118146
}
119147
}
120148

@@ -205,19 +233,35 @@ mod tests {
205233

206234
#[test]
207235
fn parsing() {
208-
"-12:37:30.12".parse::<TimeSpan>().unwrap();
209-
"-37:30.12".parse::<TimeSpan>().unwrap();
210-
"-30.12".parse::<TimeSpan>().unwrap();
211-
"-10:30".parse::<TimeSpan>().unwrap();
212-
"-30".parse::<TimeSpan>().unwrap();
213-
"-100".parse::<TimeSpan>().unwrap();
214-
"--30".parse::<TimeSpan>().unwrap_err();
215-
"-".parse::<TimeSpan>().unwrap_err();
216-
"".parse::<TimeSpan>().unwrap_err();
217-
"-10:-30".parse::<TimeSpan>().unwrap_err();
218-
"10:-30".parse::<TimeSpan>().unwrap_err();
219-
"10.5:30.5".parse::<TimeSpan>().unwrap_err();
220-
"NaN".parse::<TimeSpan>().unwrap_err();
221-
"Inf".parse::<TimeSpan>().unwrap_err();
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+
);
222266
}
223267
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use core::iter;
2+
13
#[derive(Copy, Clone, PartialEq, Eq)]
24
#[repr(transparent)]
35
pub struct AsciiChar(u8);
@@ -14,6 +16,8 @@ impl AsciiChar {
1416
pub const SINGLE_QUOTE: Self = Self::new(b'\'');
1517
pub const DOUBLE_QUOTE: Self = Self::new(b'"');
1618
pub const EQUALITY_SIGN: Self = Self::new(b'=');
19+
pub const COLON: Self = Self::new(b':');
20+
pub const DOT: Self = Self::new(b'.');
1721

1822
pub const fn new(c: u8) -> Self {
1923
if c > 127 {
@@ -41,4 +45,21 @@ impl AsciiChar {
4145
// to be valid UTF-8.
4246
unsafe { Some((text.get_unchecked(..pos), text.get_unchecked(pos + 1..))) }
4347
}
48+
49+
pub fn split_iter(self, text: &str) -> impl Iterator<Item = &str> {
50+
let mut slot = Some(text);
51+
iter::from_fn(move || {
52+
let rem = slot?;
53+
Some(match self.split_once(rem) {
54+
Some((before, after)) => {
55+
slot = Some(after);
56+
before
57+
}
58+
None => {
59+
slot = None;
60+
rem
61+
}
62+
})
63+
})
64+
}
4465
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::{ascii_char::AsciiChar, trim_end};
1+
use super::ascii_char::AsciiChar;
22

33
#[repr(transparent)]
44
pub struct AsciiSet([bool; 256]);
@@ -78,7 +78,7 @@ impl AsciiSet {
7878
// position is found by the find_not method. Also since we only
7979
// skipped ASCII characters, splitting the string at the position
8080
// will not result in a string that is not valid UTF-8.
81-
trim_end(unsafe { text.get_unchecked(pos..) })
81+
self.trim_end(unsafe { text.get_unchecked(pos..) })
8282
} else {
8383
""
8484
}

src/util/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Various utilities used in this crate.
22
3+
pub(crate) mod ascii_char;
4+
pub(crate) mod ascii_set;
35
pub(crate) mod byte_parsing;
46
mod clear_vec;
57
pub(crate) mod not_nan;

src/util/xml/mod.rs

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ use core::{
55
iter, str,
66
};
77

8-
use self::ascii_char::AsciiChar;
9-
10-
mod ascii_char;
11-
mod ascii_set;
128
pub mod helper;
139
mod reader;
1410
mod writer;
@@ -18,6 +14,8 @@ pub use self::{
1814
writer::{AttributeWriter, DisplayValue, Value, Writer, NO_ATTRIBUTES},
1915
};
2016

17+
use super::{ascii_char::AsciiChar, ascii_set::AsciiSet};
18+
2119
#[derive(Copy, Clone)]
2220
pub struct Tag<'a>(&'a str);
2321

@@ -64,7 +62,7 @@ impl<'a> Attributes<'a> {
6462
iter::from_fn(move || {
6563
rem = trim_start(rem);
6664
let (key, space_maybe, after) =
67-
ascii_set::AsciiSet::EQUALITY_OR_WHITE_SPACE.split_three_way(rem)?;
65+
AsciiSet::EQUALITY_OR_WHITE_SPACE.split_three_way(rem)?;
6866
rem = after;
6967
if space_maybe.get() != b'=' {
7068
rem = trim_start(rem);
@@ -191,19 +189,15 @@ impl<'a> Text<'a> {
191189
}
192190

193191
fn split_whitespace(rem: &str) -> Option<(&str, &str)> {
194-
ascii_set::AsciiSet::WHITE_SPACE.split(rem)
192+
AsciiSet::WHITE_SPACE.split(rem)
195193
}
196194

197195
fn trim(rem: &str) -> &str {
198-
ascii_set::AsciiSet::WHITE_SPACE.trim(rem)
196+
AsciiSet::WHITE_SPACE.trim(rem)
199197
}
200198

201199
fn trim_start(rem: &str) -> &str {
202-
ascii_set::AsciiSet::WHITE_SPACE.trim_start(rem)
203-
}
204-
205-
fn trim_end(rem: &str) -> &str {
206-
ascii_set::AsciiSet::WHITE_SPACE.trim_end(rem)
200+
AsciiSet::WHITE_SPACE.trim_start(rem)
207201
}
208202

209203
fn parse_hexadecimal(bytes: &str) -> char {

src/util/xml/reader.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use super::{ascii_char::AsciiChar, trim, Tag, TagName, Text};
1+
use crate::util::ascii_char::AsciiChar;
2+
3+
use super::{trim, Tag, TagName, Text};
24

35
enum TagState<'a> {
46
Closed,

src/util/xml/writer.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use core::fmt::{self, Write};
22

3-
use super::{ascii_char::AsciiChar, ascii_set, Text};
3+
use crate::util::{ascii_char::AsciiChar, ascii_set::AsciiSet};
4+
5+
use super::Text;
46

57
pub struct Writer<T> {
68
sink: T,
@@ -173,9 +175,7 @@ pub trait Value {
173175

174176
impl Value for &str {
175177
fn write_escaped<T: fmt::Write>(mut self, sink: &mut T) -> fmt::Result {
176-
while let Some((before, char, rem)) =
177-
ascii_set::AsciiSet::CHARACTERS_TO_ESCAPE.split_three_way(self)
178-
{
178+
while let Some((before, char, rem)) = AsciiSet::CHARACTERS_TO_ESCAPE.split_three_way(self) {
179179
self = rem;
180180
sink.write_str(before)?;
181181
sink.write_str(match char {

0 commit comments

Comments
 (0)