Hexadecimal float support for Rust 1.43 or later. (For earlier versions, try 0.1.0
)
use hexf::hexf64;
assert_eq!(hexf64!("0x1.999999999999ap-4"), 0.1f64);
The literal is explicitly typed,
and should match to the pattern SIGN "0x" INTEGRAL "." FRACTIONAL "p" EXPSIGN EXPDIGITS
, where:
-
All Latin letters are matched case-insensitively;
-
SIGN
andEXPSIGN
are either+
,-
or empty; -
INTEGRAL
andFRACTIONAL
are one or more hexadecimal digits, optionally separated by or ending with exactly one underscore (_
) (but cannot begin with it); -
At least one of
INTEGRAL
orFRACTIONAL
should be present (1.0
or.0
or1.
is allowed,1
is not); -
EXPDIGITS
is decimal digits, optionally separated by or beginning or ending with exactly one underscore (_
).
It is a compile-time error to put an invalid literal.
// hexf32! failed: invalid hexadecimal float literal
let invalid = hexf32!("42");
It is also a compile-time error to put a literal that would be not exactly representable in the target type.
// hexf32! failed: cannot exactly represent float in target type
let inexact = hexf32!("0x1.99999bp-4");
// hexf32! failed: cannot exactly represent float in target type
let inexact_subnormal = hexf32!("0x1.8p-149");
// hexf64! failed: cannot exactly represent float in target type
let overflow = hexf64!("0x1.0p1024");
// hexf64! failed: cannot exactly represent float in target type
let underflow = hexf64!("0x1.0p-1075");
The crate (and also a standalone hexf-parse
crate) provides
parse_hexf32
and parse_hexf64
functions,
which allows parsing failures (reported via a ParseHexfError
type).
These functions will allow for interleaved underscores only if the second parameter is true;
this is added for the consistency, because Rust allows for underscores in numeric literals,
but not in the standard library ("3_4".parse::<i32>()
is an error).
This crate heavily relies on the fact that the recent enough Rust compiler can correctly print and read a floating point number. So the actual implementation of this crate is, well, done by printing the parsed hexadecimal float back to the correct decimal digits, which is picked up by the compiler to produce an exact bit pattern.
Wait, then what's the point of hexadecimal floats? The answer is that they are "invented" by ISO C99 to avoid implementation pitfalls. Ideally it should be possible to enumerate enough fractional digits to get the correctly rounded bit pattern, but many implementations didn't (quite understandably, because it is actually quite hard). So the Standard has made a compromise: in the conforming implementation decimal floats should parse to very close to, but not exactly, the correctly rounded number:
The significand part is interpreted as a (decimal or hexadecimal) rational number; the digit sequence in the exponent part is interpreted as a decimal integer. [...] For decimal floating constants, and also for hexadecimal floating constants when FLT_RADIX is not a power of 2, the result is either the nearest representable value, or the larger or smaller representable value immediately adjacent to the nearest representable value, chosen in an implementation-defined manner. [...]
—ISO C99, Section 6.4.4.2 Floating constants, Paragraph 3 (emphases mine)
Indeed, it is relatively easier to parse decimal floats in that accuracy. Hexadecimal floats are born out of this legacy, but Rust doesn't have to! Hexadecimal floats can be still useful for manually writing float bits down, or for converting from other languages, however. This crate exists for those rarer use cases.
See rust-lang/rust#1433 for the more context.