#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct SyntaxError;
pub fn parse_hex_float(
chars: impl Iterator<Item = char>,
decimal_sep: char,
) -> Result<(f64, usize), SyntaxError> {
const F64_EXP_BIAS: i32 = 1023;
let mut chars = chars.peekable();
let mut consumed = 0;
let negative = match chars.peek().copied() {
Some('+' | '-') => {
consumed += 1;
chars.next() == Some('-')
}
_ => false,
};
let sign = if negative { -1.0 } else { 1.0 };
match (chars.next(), chars.next()) {
(Some('0'), Some('x' | 'X')) => consumed += 2,
_ => return Err(SyntaxError),
}
let mut seen_digits = false;
while chars.next_if_eq(&'0').is_some() {
consumed += 1;
seen_digits = true;
}
let mut mantissa: u64 = 0;
let mut shift = 64;
let mut add_digit = |digit: u32| {
debug_assert!(digit < 16);
seen_digits = true;
if shift > 0 {
shift -= 4;
mantissa |= u64::from(digit) << shift;
}
};
let consumed_before_decimal = consumed;
while let Some(d) = chars.peek().and_then(|c| c.to_digit(16)) {
consumed += 1;
add_digit(d);
chars.next();
}
let decimal_point_pos: i32 = (consumed - consumed_before_decimal)
.try_into()
.unwrap_or(i32::MAX);
if chars.next_if_eq(&decimal_sep).is_some() {
consumed += 1;
while let Some(d) = chars.peek().and_then(|c| c.to_digit(16)) {
consumed += 1;
add_digit(d);
chars.next();
}
}
if !seen_digits {
return Err(SyntaxError);
}
let mut explicit_exp: i32 = 0;
if matches!(chars.peek(), Some('p' | 'P')) {
chars.next();
consumed += 1;
let negative = match chars.peek() {
Some('+' | '-') => {
consumed += 1;
chars.next() == Some('-')
}
_ => false,
};
let before = consumed;
while let Some(d) = chars.peek().and_then(|c| c.to_digit(10)) {
explicit_exp = explicit_exp.saturating_mul(10).saturating_add(d as i32);
consumed += 1;
chars.next();
}
if consumed == before {
return Err(SyntaxError);
}
if negative {
explicit_exp = -explicit_exp;
}
}
if mantissa == 0 {
return Ok((0.0f64.copysign(sign), consumed));
}
let zeros = mantissa.leading_zeros() as i32;
mantissa <<= zeros;
let trim = 64 - 53;
let halfway = 1 << (trim - 1);
let rounding_bits = mantissa & ((1 << trim) - 1);
mantissa ^= rounding_bits; if rounding_bits > halfway || (rounding_bits == halfway && (mantissa & (1 << trim)) != 0) {
mantissa = if let Some(m) = mantissa.checked_add(1 << trim) {
m
} else {
explicit_exp = explicit_exp.saturating_add(1);
1
};
}
let exponent = decimal_point_pos
.saturating_mul(4)
.saturating_add(explicit_exp)
.saturating_sub(1 + zeros);
if exponent > 1023 {
return Ok((f64::INFINITY.copysign(sign), consumed));
}
let remaining_exp = (1022 + exponent).min(0);
let exponent = exponent.max(-1022);
debug_assert!((-1022..=1023).contains(&exponent));
debug_assert!(remaining_exp <= 0);
let biased_exp: u64 = (exponent + F64_EXP_BIAS).try_into().unwrap();
let bits = (u64::from(negative) << 63) | (biased_exp << 52) | ((mantissa << 1) >> (64 - 52));
let mut f = f64::from_bits(bits);
if remaining_exp < 0 {
f *= 2.0f64.powi(remaining_exp);
}
Ok((f, consumed))
}
#[cfg(test)]
mod tests {
use super::{parse_hex_float, SyntaxError};
fn parse(input: &str) -> f64 {
let res = parse_hex_float(input.chars(), '.')
.unwrap_or_else(|_| panic!("Failed to parse {}", input));
assert_eq!(res.1, input.len());
res.0
}
#[test]
fn test_parse_hex_float_rounding() {
let nextbefore = |f: f64| f64::from_bits(f.to_bits() - 1);
assert_eq!(parse("0x1.FFFFFFFFFFFFFp0"), nextbefore(2.0));
assert_eq!(parse("0x1.FFFFFFFFFFFFF7p0"), nextbefore(2.0));
assert_eq!(parse("0x1.FFFFFFFFFFFFF8p0"), 2.0); assert_eq!(parse("0x1.FFFFFFFFFFFFF8p-4"), 0.125); assert_eq!(parse("0x1.FFFFFFFFFFFFF9p0"), 2.0);
assert_eq!(parse("0x1.FFFFFFFFFFFFFFp0"), 2.0);
assert_eq!(parse("-0x1.FFFFFFFFFFFFFp0"), nextbefore(-2.0));
assert_eq!(parse("-0x1.FFFFFFFFFFFFF7p0"), nextbefore(-2.0));
assert_eq!(parse("-0x1.FFFFFFFFFFFFF8p0"), -2.0); assert_eq!(parse("-0x1.FFFFFFFFFFFFF8p-4"), -0.125); assert_eq!(parse("-0x1.FFFFFFFFFFFFF9p0"), -2.0);
assert_eq!(parse("-0x1.FFFFFFFFFFFFFFp0"), -2.0);
let nb2 = nextbefore(2.0);
assert_eq!(parse("0x1.FFFFFFFFFFFFEp0"), nextbefore(nb2));
assert_eq!(parse("0x1.FFFFFFFFFFFFE7p0"), nextbefore(nb2));
assert_eq!(parse("0x1.FFFFFFFFFFFFE8p0"), nextbefore(nb2)); assert_eq!(parse("0x1.FFFFFFFFFFFFE9p0"), nb2);
assert_eq!(parse("0x1.FFFFFFFFFFFFEFp0"), nb2);
let nb2 = nextbefore(-2.0);
assert_eq!(parse("-0x1.FFFFFFFFFFFFEp0"), nextbefore(nb2));
assert_eq!(parse("-0x1.FFFFFFFFFFFFE7p0"), nextbefore(nb2));
assert_eq!(parse("-0x1.FFFFFFFFFFFFE8p0"), nextbefore(nb2)); assert_eq!(parse("-0x1.FFFFFFFFFFFFE9p0"), nb2);
assert_eq!(parse("-0x1.FFFFFFFFFFFFEFp0"), nb2);
}
#[test]
fn test_parse_hex_float_valid() {
assert_eq!(parse("0x0"), 0.0);
assert_eq!(parse("0X0"), 0.0);
assert_eq!(parse("0X000"), 0.0);
assert_eq!(parse("0X0000.8"), 0.5);
assert_eq!(parse("0x1"), 1.0);
assert_eq!(parse("0x1p0"), 1.0);
assert_eq!(parse("0x1P0"), 1.0);
assert_eq!(parse("0x1.8p1"), 3.0);
assert_eq!(parse("0x2p2"), 8.0);
assert_eq!(parse("0x1.8"), 1.5);
assert_eq!(parse("0x1.2p3"), 9.0);
assert_eq!(parse("0x10p-1"), 8.0);
assert_eq!(parse("0x1.p1"), 2.0);
assert_eq!(parse("0x.8p0"), 0.5);
assert_eq!(parse("0x.1p4"), 1.0);
assert_eq!(parse("0x2"), 2.0);
assert_eq!(parse("0x2P1"), 4.0);
assert_eq!(parse("0x2.4"), 2.25);
assert_eq!(parse("0x2.4p2"), 9.0);
assert_eq!(parse("0x3p-2"), 0.75);
assert_eq!(parse("0x4p-3"), 0.5);
assert_eq!(parse("0x5"), 5.0);
assert_eq!(parse("0x5p1"), 10.0);
assert_eq!(parse("0x5.1p0"), 5.0625);
assert_eq!(parse("0x5.1p1"), 10.125);
assert_eq!(parse("0x8"), 8.0);
assert_eq!(parse("0x8p0"), 8.0);
assert_eq!(parse("0x8.8"), 8.5);
assert_eq!(parse("0x9"), 9.0);
assert_eq!(parse("0x9p-1"), 4.5);
assert_eq!(parse("0xA"), 10.0);
assert_eq!(parse("0xAp1"), 20.0);
assert_eq!(parse("0xB"), 11.0);
assert_eq!(parse("0xBp-1"), 5.5);
assert_eq!(parse("0xC"), 12.0);
assert_eq!(parse("0xCp2"), 48.0);
assert_eq!(parse("0xF"), 15.0);
assert_eq!(parse("0xFp-2"), 3.75);
assert_eq!(parse("0x10"), 16.0);
assert_eq!(parse("0x10p-4"), 1.0);
assert_eq!(parse("0x1A"), 26.0);
assert_eq!(parse("0x1Ap3"), 208.0);
assert_eq!(parse("0x1F"), 31.0);
assert_eq!(parse("0x1Fp1"), 62.0);
assert_eq!(parse("0x20"), 32.0);
assert_eq!(parse("0x20p-5"), 1.0);
}
#[test]
fn test_parse_hex_float_length() {
let parse_len = |input: &str| {
let res = parse_hex_float(input.chars(), '.')
.unwrap_or_else(|_| panic!("Failed to parse {}", input));
res.1
};
assert_eq!(parse_len("0x0ZZZ"), 3);
assert_eq!(parse_len("0x1.p1ZZZZ"), 6);
assert_eq!(parse_len("0x1234ZZZZZZZ"), 6);
}
#[test]
fn test_parse_hex_float_errors() {
assert_eq!(parse_hex_float("".chars(), '.'), Err(SyntaxError));
assert_eq!(parse_hex_float("0xZ".chars(), '.'), Err(SyntaxError));
assert_eq!(parse_hex_float("1A3P1.1p2".chars(), '.'), Err(SyntaxError));
assert_eq!(parse_hex_float("1A3G.1p2".chars(), '.'), Err(SyntaxError));
}
#[test]
fn test_parse_hex_float_signed_zero() {
let z1 = parse_hex_float("0x0p0".chars(), '.').unwrap().0;
let z2 = parse_hex_float("-0x0p0".chars(), '.').unwrap().0;
assert_eq!(z1, 0.0);
assert_eq!(z2, 0.0);
assert!(z1.is_sign_positive());
assert!(!z2.is_sign_positive());
}
#[test]
fn test_parse_hex_floats_saturating_exp() {
assert_eq!(parse("0x1p999999"), f64::INFINITY);
assert_eq!(parse("-0x1p999999"), -f64::INFINITY);
assert_eq!(parse("0x1p36893488147419103232"), f64::INFINITY);
assert_eq!(parse("-0x1p36893488147419103232"), -f64::INFINITY);
assert_eq!(parse(&format!("0x1{:0<260}", "")), f64::INFINITY);
assert_eq!(parse(&format!("-0x1{:0<260}", "")), -f64::INFINITY);
assert_eq!(parse(&format!("0x1{:0<512}p-2000", "")), 2.0f64.powi(48));
assert_eq!(
parse(&format!("-0x1{:0<512}p-2000", "")),
-(2.0f64.powi(48))
);
}
#[test]
fn test_parse_hex_float_denormals() {
assert_eq!(parse("0x1p-1022"), f64::MIN_POSITIVE);
assert_eq!(parse("0x1p-1023"), f64::MIN_POSITIVE / 2.0);
assert_eq!(parse("0x1p-1024"), f64::MIN_POSITIVE / 4.0);
assert_eq!(parse("0x.8p-1024"), f64::MIN_POSITIVE / 8.0);
assert_eq!(parse("0x.4p-1024"), f64::MIN_POSITIVE / 16.0);
assert_eq!(parse("0x.4p-9999"), 0.0);
}
}