Skip to main content

mathverse_core/
float.rs

1//! Floating-point utilities beyond the standard library surface:
2//! accurate `expm1`/`log1p`, exact summation, `isclose`, and the classic
3//! `frexp`/`ldexp`/`modf`/`nextafter` decomposition functions.
4//!
5//! `expm1`/`log1p` use hardware-accelerated `std` methods on standard builds
6//! and fall back to `libm` under `no_std` (when the `libm` feature is on).
7
8/// `e^x - 1`, accurate for small `x` (no catastrophic cancellation).
9///
10/// ```
11/// use mathverse_core::float::expm1;
12/// assert!((expm1(1e-10) - 1e-10).abs() < 1e-20);
13/// assert!((expm1(1.0) - (core::f64::consts::E - 1.0)).abs() < 1e-15);
14/// ```
15#[cfg(feature = "std")]
16#[inline]
17#[must_use]
18pub fn expm1(x: f64) -> f64 {
19    x.exp_m1()
20}
21
22/// `e^x - 1`, accurate for small `x` (libm fallback for `no_std`).
23#[cfg(all(not(feature = "std"), feature = "libm"))]
24#[inline]
25#[must_use]
26pub fn expm1(x: f64) -> f64 {
27    libm::expm1(x)
28}
29
30/// `ln(1 + x)`, accurate for small `x`.
31///
32/// ```
33/// use mathverse_core::float::log1p;
34/// assert!((log1p(1e-12) - 1e-12).abs() < 1e-24);
35/// assert!((log1p(core::f64::consts::E - 1.0) - 1.0).abs() < 1e-15);
36/// ```
37#[cfg(feature = "std")]
38#[inline]
39#[must_use]
40pub fn log1p(x: f64) -> f64 {
41    x.ln_1p()
42}
43
44/// `ln(1 + x)`, accurate for small `x` (libm fallback for `no_std`).
45#[cfg(all(not(feature = "std"), feature = "libm"))]
46#[inline]
47#[must_use]
48pub fn log1p(x: f64) -> f64 {
49    libm::log1p(x)
50}
51
52/// Exactly rounded sum of a slice (Neumaier compensated summation).
53///
54/// Returns the correctly-rounded sum even when naive summation would lose
55/// precision to cancellation.
56///
57/// ```
58/// use mathverse_core::float::fsum;
59/// let xs = [1e16, 1.0, -1e16];
60/// assert_eq!(fsum(&xs), 1.0); // naive sum gives 0.0
61/// ```
62#[must_use]
63pub fn fsum(xs: &[f64]) -> f64 {
64    let mut sum = 0.0;
65    let mut correction = 0.0;
66    for &x in xs {
67        let t = sum + x;
68        if sum.abs() >= x.abs() {
69            correction += (sum - t) + x;
70        } else {
71            correction += (x - t) + sum;
72        }
73        sum = t;
74    }
75    sum + correction
76}
77
78/// Approximate equality with relative and absolute tolerances.
79///
80/// `|a - b| <= abs_tol + rel_tol * max(|a|, |b|)`; NaNs never compare close.
81///
82/// ```
83/// use mathverse_core::float::isclose;
84/// assert!(isclose(0.1 + 0.2, 0.3, 1e-9, 1e-12));
85/// assert!(!isclose(1.0, 2.0, 1e-9, 1e-12));
86/// ```
87#[inline]
88#[must_use]
89#[allow(clippy::suboptimal_flops)] // mul_add is a std-only inherent; keep portable
90pub fn isclose(a: f64, b: f64, rel_tol: f64, abs_tol: f64) -> bool {
91    (a - b).abs() <= rel_tol * b.abs().max(a.abs()) + abs_tol
92}
93
94/// Split `x` into a mantissa `m` in `[0.5, 1)` (sign preserved) and an
95/// integer exponent `e` such that `x == m * 2^e`.
96///
97/// Zero, infinities and NaN return `(x, 0)`.
98///
99/// ```
100/// use mathverse_core::float::frexp;
101/// let (m, e) = frexp(6.0);
102/// assert_eq!(m, 0.75);
103/// assert_eq!(e, 3);
104/// ```
105#[must_use]
106pub fn frexp(x: f64) -> (f64, i32) {
107    if x == 0.0 || x.is_nan() || x.is_infinite() {
108        return (x, 0);
109    }
110    let bits = x.to_bits();
111    let exponent = ((bits >> 52) & 0x7FF) as i32;
112    if exponent == 0 {
113        // Subnormal: normalize by scaling up, then adjust the exponent.
114        let (m, e) = frexp(x * 4503599627370496.0); // 2^52
115        return (m, e - 52);
116    }
117    let mantissa = (bits & !(0x7FFu64 << 52)) | (1022u64 << 52);
118    (f64::from_bits(mantissa), exponent - 1022)
119}
120
121/// Construct `x * 2^e`, correctly handling subnormals and overflow.
122///
123/// ```
124/// use mathverse_core::float::{frexp, ldexp};
125/// let x = 123.456;
126/// let (m, e) = frexp(x);
127/// assert!((ldexp(m, e) - x).abs() < 1e-12);
128/// assert!(ldexp(1.0, 2000).is_infinite());
129/// ```
130#[must_use]
131pub fn ldexp(x: f64, e: i32) -> f64 {
132    if x == 0.0 || x.is_nan() || x.is_infinite() {
133        return x;
134    }
135    let mut bits = x.to_bits();
136    let exponent = ((bits >> 52) & 0x7FF) as i32;
137    if exponent == 0 {
138        // Subnormal input: normalize first, then recurse with adjusted exponent.
139        return ldexp(x * 4503599627370496.0, e - 52); // 2^52
140    }
141    let new_exp = exponent + e;
142    if new_exp <= 0 {
143        // Underflow to subnormal: multiply by 2^new_exp through repeated
144        // halving (at most 1074 iterations).
145        let mut r = x;
146        let mut k = e;
147        while k < 0 {
148            r *= 0.5;
149            k += 1;
150        }
151        return r;
152    }
153    if new_exp >= 0x7FF {
154        return if bits >> 63 == 1 { f64::NEG_INFINITY } else { f64::INFINITY };
155    }
156    bits = (bits & !(0x7FFu64 << 52)) | ((new_exp as u64) << 52);
157    f64::from_bits(bits)
158}
159
160/// Split `x` into fractional and integral parts, both with `x`'s sign.
161///
162/// Returns `(fractional, integral)`.
163///
164/// ```
165/// use mathverse_core::float::modf;
166/// let (frac, int) = modf(2.7);
167/// assert!((frac - 0.7).abs() < 1e-12);
168/// assert_eq!(int, 2.0);
169/// let (frac, int) = modf(-2.7);
170/// assert!((frac + 0.7).abs() < 1e-12);
171/// assert_eq!(int, -2.0);
172/// ```
173#[must_use]
174pub fn modf(x: f64) -> (f64, f64) {
175    if x.is_nan() || x.is_infinite() {
176        return (x, 0.0);
177    }
178    let integral = trunc_f64(x);
179    (x - integral, integral)
180}
181
182/// Truncate toward zero using bit surgery, so it works without `std`.
183///
184/// The low fraction bits are cleared; subnormals and values below 1 truncate
185/// to signed zero.
186#[inline]
187#[must_use]
188fn trunc_f64(x: f64) -> f64 {
189    if x == 0.0 || x.is_nan() || x.is_infinite() {
190        return x;
191    }
192    let bits = x.to_bits();
193    let exponent = ((bits >> 52) & 0x7FF) as i32;
194    if exponent == 0 {
195        return f64::from_bits(bits & (1u64 << 63)); // subnormal -> signed zero
196    }
197    let unbiased = exponent - 1023;
198    if unbiased >= 52 {
199        return x;
200    }
201    if unbiased < 0 {
202        return f64::from_bits(bits & (1u64 << 63)); // |x| < 1 -> signed zero
203    }
204    let keep = 52 - unbiased; // fraction bits live below bit `keep`
205    let mask = !((1u64 << keep) - 1);
206    f64::from_bits(bits & mask)
207}
208
209/// Next representable `f64` stepping from `x` toward `y`.
210///
211/// Returns `y` when `x == y`; `+0.0`/`-0.0` are handled per IEEE-754.
212///
213/// ```
214/// use mathverse_core::float::nextafter;
215/// assert!(nextafter(1.0, 2.0) > 1.0);
216/// assert!(nextafter(1.0, 0.0) < 1.0);
217/// assert_eq!(nextafter(0.0, 1.0), f64::from_bits(1));
218/// assert_eq!(nextafter(1.0, 1.0), 1.0);
219/// ```
220#[must_use]
221pub fn nextafter(x: f64, y: f64) -> f64 {
222    if x.is_nan() || y.is_nan() {
223        return f64::NAN;
224    }
225    if x == y {
226        return y;
227    }
228    if x == 0.0 {
229        let mut r = f64::from_bits(1);
230        if y < 0.0 {
231            r = -r;
232        }
233        return r;
234    }
235    let mut bits = x.to_bits();
236    if (x < y) == (x > 0.0) {
237        bits += 1;
238    } else {
239        bits -= 1;
240    }
241    f64::from_bits(bits)
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn expm1_log1p_small() {
250        assert!((expm1(1e-10) - 1e-10).abs() < 1e-20);
251        assert!((log1p(1e-10) - 1e-10).abs() < 1e-20);
252        assert!((expm1(0.0) - 0.0).abs() < 1e-20);
253        assert!((log1p(0.0) - 0.0).abs() < 1e-20);
254    }
255
256    #[test]
257    fn fsum_exactness() {
258        assert_eq!(fsum(&[1e16, 1.0, -1e16]), 1.0);
259        assert_eq!(fsum(&[0.1; 10]), 1.0);
260    }
261
262    #[test]
263    fn isclose_behavior() {
264        assert!(isclose(0.1 + 0.2, 0.3, 1e-9, 1e-12));
265        assert!(!isclose(1.0, 2.0, 1e-9, 1e-12));
266        assert!(!isclose(f64::NAN, f64::NAN, 1.0, 1.0));
267        // Subnormal numbers: use very small atol
268        assert!(isclose(1e-300, 1e-300, 1e-9, 1e-320));
269    }
270
271    #[test]
272    fn frexp_ldexp_roundtrip() {
273        for &x in &[0.5, 1.0, 2.0, 6.0, 123.456, 1e-300, -7.5] {
274            let (m, e) = frexp(x);
275            assert!(m.abs() >= 0.5 && m.abs() < 1.0 || x == 0.0);
276            assert!((ldexp(m, e) - x).abs() < x.abs() * 1e-12 || x == 0.0);
277        }
278        assert_eq!(frexp(0.0), (0.0, 0));
279        assert_eq!(frexp(0.75), (0.75, 0));
280        assert_eq!(frexp(6.0), (0.75, 3));
281        assert!(ldexp(1.0, 2000).is_infinite());
282        assert_eq!(ldexp(1.0, -2000), 0.0);
283    }
284
285    #[test]
286    fn modf_split() {
287        assert_eq!(modf(2.7).1, 2.0);
288        assert_eq!(modf(-2.7).1, -2.0);
289        assert_eq!(modf(0.0).1, 0.0);
290        assert_eq!(modf(f64::INFINITY).1, 0.0);
291    }
292
293    #[test]
294    fn nextafter_stepping() {
295        assert!(nextafter(1.0, 2.0) > 1.0);
296        assert!(nextafter(1.0, 0.0) < 1.0);
297        assert_eq!(nextafter(0.0, 1.0), f64::from_bits(1));
298        assert_eq!(nextafter(1.0, 1.0), 1.0);
299        assert!(nextafter(f64::MAX, f64::INFINITY).is_infinite());
300    }
301}