hexfloat2/
lib.rs

1//! `hexfloat2` supports hexadecimal f32/64 syntax.
2//!
3//! IEEE754 defines a syntax for "hexadecimal-significand character sequences"
4//! that lets you write a precise representation of a floating point number.
5//!
6//! For example:
7//! - `0x1.0p0` is just 1.0
8//! - `0x8.8p1` is 8.5 * 2^1, or 17.
9//! - `0x3.0p-12` is 3 * 2^-12, or 0.000732421875 in decimal.
10//!
11//! Unlike decimal representations, "hexfloat" representations don't
12//! involve any rounding, so a format-then-parse round trip will always
13//! return exactly the same original value.
14//!
15//! A formatted hexfloat will always appear in its "canonical" format,
16//! copying the exact bit representation as closely as possible. For example,
17//! the value 2^-20 will always be rendered as `0x1.0p-19`.
18//!
19//! The parser attempts to handle "non-canonical" representations. For example,
20//! these values will all be parsed as 2^-20:
21//! - `0x1.0p-20`
22//! - `0x2.0p-21`
23//! - `0x0.0008p-7`
24//!
25//! Some inputs won't parse: values with too
26//! many hex digits (e.g. `0x10000000000000000p20`) will fail to parse
27//! because the parser is only willing to parse up to 16 hex digits.
28//! Values that are outside the range that can be represented in the
29//! underlying type (f32 or f64) will also fail to parse.
30//!
31//! Values with excessive precision will have the trailing bits dropped.
32//! For example, `0x1.0000000000001p0` will be truncated to `1.0` when
33//! parsed into a `HexFloat<f32>` (but would fit in an f64).
34//!
35//! "Subnormal" values can be successfully formatted and parsed;
36//! `0x0.000002p-127` can be parsed as an f32; anything smaller will
37//! be truncated to 0.
38//!
39//! # Examples
40//! ```
41//! use hexfloat2::HexFloat32;
42//!
43//! const EXPECTED: f32 = 1.0 / 1048576.0;
44//!
45//! let x = "0x1.0p-20";
46//! let fx: HexFloat32 = x.parse().unwrap();
47//! assert_eq!(*fx, EXPECTED);
48//!
49//! let y = "0x2.0p-21";
50//! let fy: HexFloat32 = y.parse().unwrap();
51//! assert_eq!(*fy, EXPECTED);
52//!
53//! let z = "0x0.0008p-7";
54//! let fz: HexFloat32 = z.parse().unwrap();
55//! assert_eq!(*fz, EXPECTED);
56//!
57//! # #[cfg(feature = "alloc")]
58//! let sz = format!("{fz}");
59//! # #[cfg(feature = "alloc")]
60//! assert_eq!(sz, "0x1.0p-20");
61//! ```
62//!
63//! This crate provides newtype wrappers `HexFloat<T>`, aka `HexFloat32` or
64//! `HexFloat64`, that implement `Display` and `FromStr`.
65//!
66//! If you don't want to deal with the wrapper structs, you can also call
67//! `hexfloat::parse::<T>()` or `hexfloat::format::<T>()` instead.
68//!
69
70#![allow(unknown_lints)]
71#![deny(unsafe_code)]
72#![warn(missing_docs)]
73#![warn(clippy::cast_lossless)]
74#![warn(clippy::cast_possible_truncation)]
75#![warn(clippy::cast_possible_wrap)]
76#![warn(clippy::cast_sign_loss)]
77#![cfg_attr(not(feature = "std"), no_std)]
78
79#[cfg(feature = "alloc")]
80extern crate alloc;
81
82use core::fmt::Display;
83use core::ops::{Deref, DerefMut};
84
85mod float;
86#[cfg(feature = "alloc")]
87mod format;
88mod parser;
89
90pub use parser::ParseError;
91
92use crate::float::FloatBits;
93
94/// A wrapper type for floating point values that uses "hexfloat" syntax.
95///
96/// When parsed or formatted, the resulting value will be in precise
97/// hexadecimal format.
98///
99/// There are type aliases available:
100/// - `HexFloat32` is equivalent to `HexFloat<f32>`
101/// - `HexFloat64` is equivalent to `HexFloat<f64>`
102///
103/// # Examples
104/// ```
105/// use hexfloat2::HexFloat32;
106///
107/// const EXPECTED: f32 = 1.0 / 1048576.0;
108///
109/// let x = "0x1.0p-20";
110/// let fx: HexFloat32 = x.parse().unwrap();
111/// assert_eq!(*fx, EXPECTED);
112///
113/// let y = "0x2.0p-21";
114/// let fy: HexFloat32 = y.parse().unwrap();
115/// assert_eq!(*fy, EXPECTED);
116///
117/// let z = "0x0.0008p-7";
118/// let fz: HexFloat32 = z.parse().unwrap();
119/// assert_eq!(*fz, EXPECTED);
120///
121/// # #[cfg(feature = "alloc")]
122/// let sz = format!("{fz}");
123/// # #[cfg(feature = "alloc")]
124/// assert_eq!(sz, "0x1.0p-20");
125/// ```
126///
127#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, Hash)]
128pub struct HexFloat<T>(pub T);
129
130/// An alias for [`HexFloat<f32>`].
131pub type HexFloat32 = HexFloat<f32>;
132/// An alias for [`HexFloat<f64>`].
133pub type HexFloat64 = HexFloat<f64>;
134
135impl<T> AsRef<T> for HexFloat<T> {
136    fn as_ref(&self) -> &T {
137        &self.0
138    }
139}
140
141impl<T> Deref for HexFloat<T> {
142    type Target = T;
143
144    fn deref(&self) -> &Self::Target {
145        &self.0
146    }
147}
148
149impl<T> DerefMut for HexFloat<T> {
150    fn deref_mut(&mut self) -> &mut Self::Target {
151        &mut self.0
152    }
153}
154
155impl<T> HexFloat<T> {
156    /// Construct a `HexFloat` from an `f32` or `f64`.
157    pub const fn new(value: T) -> Self {
158        Self(value)
159    }
160}
161
162impl<T> From<T> for HexFloat<T> {
163    fn from(value: T) -> Self {
164        HexFloat(value)
165    }
166}
167
168impl From<HexFloat<f32>> for f32 {
169    fn from(hexfloat: HexFloat<f32>) -> Self {
170        hexfloat.0
171    }
172}
173
174impl From<HexFloat<f64>> for f64 {
175    fn from(hexfloat: HexFloat<f64>) -> Self {
176        hexfloat.0
177    }
178}
179
180/// A marker trait for supported floating point types.
181pub trait SupportedFloat: FloatBits + Display {}
182impl SupportedFloat for f32 {}
183impl SupportedFloat for f64 {}
184
185/// Parse a hexfloat string.
186///
187/// # Examples
188/// ```
189/// let value: f32 = hexfloat2::parse("0x1.8p0").unwrap();
190/// assert_eq!(value, 1.5);
191/// ```
192pub fn parse<F>(value: &str) -> Result<F, ParseError>
193where
194    F: SupportedFloat,
195{
196    value.parse::<HexFloat<F>>().map(|hf| hf.0)
197}
198
199/// Format a floating point value using hexfloat syntax.
200///
201/// # Examples
202/// ```
203/// assert_eq!(hexfloat2::format(100.0f32), "0x1.9p6");
204/// ```
205#[cfg(feature = "alloc")]
206pub fn format<F: SupportedFloat>(value: F) -> alloc::string::String {
207    #[allow(unused_imports)]
208    use alloc::string::ToString;
209
210    HexFloat(value).to_string()
211}