From 4c81eef807c20bd60cb9123e590db02c0145451a Mon Sep 17 00:00:00 2001 From: Nick-Lafky Date: Mon, 12 Oct 2026 12:31:59 -0400 Subject: [PATCH] fix: convert to the nearest f64 Amp-Thread-ID: https://ampcode.com/threads/T-01a11c3a-df69-74b3-b72c-b2ce36b3b318 Co-authored-by: Amp --- CHANGELOG.md | 2 ++ src/float.rs | 29 +++++++++++++++++++++ src/lib.rs | 5 +--- tests/it/convert_f64.rs | 58 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44d89fb..874f081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - ReleaseDate +### Fixed +- `From for f64` now returns the nearest `f64`. Previously, the integral and fractional parts were rounded separately and then summed, which could be off by one ULP (e.g. `1.0353` became `1.0352999999999999`). ## [0.9.5] - 2026-02-03 ### Fixed diff --git a/src/float.rs b/src/float.rs index e833677..c8be826 100644 --- a/src/float.rs +++ b/src/float.rs @@ -7,6 +7,35 @@ use crate::{ ConvertError, FixedPoint, Precision, }; +pub(crate) fn to_f64(mantissa: i128, exponent: i32) -> f64 { + const MAX_EXACT_MANTISSA: u128 = 1 << f64::MANTISSA_DIGITS; + const MAX_EXACT_POW10: i32 = 22; + + if (-MAX_EXACT_POW10..=0).contains(&exponent) && mantissa.unsigned_abs() <= MAX_EXACT_MANTISSA { + return mantissa as f64 / 10u128.pow(exponent.unsigned_abs()) as f64; + } + + let mut mantissa_buf = itoa::Buffer::new(); + let mut exponent_buf = itoa::Buffer::new(); + let parts = [ + mantissa_buf.format(mantissa), + "e", + exponent_buf.format(exponent), + ]; + + let mut buf = [0u8; 52]; + let mut len = 0; + for part in parts { + buf[len..len + part.len()].copy_from_slice(part.as_bytes()); + len += part.len(); + } + + core::str::from_utf8(&buf[..len]) + .expect("ASCII only") + .parse() + .expect("a valid float literal") +} + macro_rules! impl_try_from_f64 { ($layout:tt) => { impl TryFrom for FixedPoint<$layout, P> { diff --git a/src/lib.rs b/src/lib.rs index 0d0a5dd..4e9acae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -712,10 +712,7 @@ macro_rules! impl_fixed_point { impl From> for f64 { fn from(value: FixedPoint<$layout, P>) -> Self { - let coef = FixedPoint::<$layout, P>::COEF; - let integral = (value.inner / coef) as f64; - let fractional = ((value.inner % coef) as f64) / (coef as f64); - integral + fractional + float::to_f64(value.inner.into(), -FixedPoint::<$layout, P>::PRECISION) } } diff --git a/tests/it/convert_f64.rs b/tests/it/convert_f64.rs index aadb5a4..af1fef1 100644 --- a/tests/it/convert_f64.rs +++ b/tests/it/convert_f64.rs @@ -28,6 +28,10 @@ fn to_f64() -> Result<()> { (fp!(-922337203.685477581), -922337203.6854776); (fp!(92233720.36854775), 92233720.36854775); (fp!(-92233720.36854775), -92233720.36854775); + (fp!(1.14), 1.14); + (fp!(1.0131), 1.0131); + (fp!(1.0353), 1.0353); + (fp!(-1.0353), -1.0353); }, fp128 { (fp!(0.0000000000025), 25e-13); @@ -37,11 +41,65 @@ fn to_f64() -> Result<()> { (fp!(-0.000000000000000025), -25e-18); (fp!(2.1234567890123457), 2.1234567890123457); (fp!(-2.1234567890123457), -2.1234567890123457); + (fp!(1.8511142654), 1.8511142654); + (fp!(36.908491254007), 36.908491254007); + (fp!(69.9978429397149072), 69.99784293971491); }, }; Ok(()) } +#[test] +#[cfg(feature = "i64")] +#[allow(clippy::float_cmp)] +fn to_f64_around_exact_mantissa() { + type FixedPoint = fixnum::FixedPoint; + + let exact = 1i64 << 53; + assert_eq!(f64::from(FixedPoint::from_bits(exact)), 900719925474.0992); + assert_eq!( + f64::from(FixedPoint::from_bits(exact + 1)), + 900719925474.0993 + ); + assert_eq!(f64::from(FixedPoint::from_bits(-exact)), -900719925474.0992); + assert_eq!( + f64::from(FixedPoint::from_bits(-exact - 1)), + -900719925474.0993 + ); +} + +#[test] +#[cfg(feature = "i128")] +#[allow(clippy::float_cmp)] +fn to_f64_beyond_exact_power_of_10() { + type FixedPoint = fixnum::FixedPoint; + + assert_eq!(f64::from(FixedPoint::from_bits(1)), 1e-23); + assert_eq!( + f64::from(FixedPoint::from_bits(-123456789)), + -1.23456789e-15 + ); +} + +#[test] +#[allow(clippy::float_cmp)] +fn to_f64_limits() { + use fixnum::ops::Bounded; + + #[cfg(feature = "i64")] + { + type FixedPoint = fixnum::FixedPoint; + assert_eq!(f64::from(FixedPoint::MIN), -9.223372036854776e18); + assert_eq!(f64::from(FixedPoint::MAX), 9.223372036854776e18); + } + #[cfg(feature = "i128")] + { + type FixedPoint = fixnum::FixedPoint; + assert_eq!(f64::from(FixedPoint::MIN), -1.7014118346046923e38); + assert_eq!(f64::from(FixedPoint::MAX), 1.7014118346046923e38); + } +} + #[test] fn from_f64() -> Result<()> { test_fixed_point! {