Ever wondered what the next representable floating point number after 1.0 is? Since Rust 1.86, f64::next_up and f64::next_down let you step through the number line one float at a time.
The problem
Floating point numbers aren’t evenly spaced — the gap between representable values grows as the magnitude increases. Before next_up / next_down, figuring out the next neighbor required bit-level manipulation of the IEEE 754 representation:
1
2
3
4
5
6
7
8
9
10
| fn main() {
// The hard way (before 1.86): manually decode bits
let x: f64 = 1.0;
let bits = x.to_bits();
let next_bits = bits + 1;
let next = f64::from_bits(next_bits);
assert!(next > x);
assert_eq!(next, 1.0000000000000002);
}
|
Error-prone, unreadable, and doesn’t handle edge cases like negative numbers, zero, or special values.
The clean way
next_up returns the smallest f64 greater than self. next_down returns the largest f64 less than self:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| fn main() {
let x: f64 = 1.0;
let up = x.next_up();
let down = x.next_down();
assert!(up > x);
assert!(down < x);
assert_eq!(up, 1.0000000000000002);
assert_eq!(down, 0.9999999999999998);
// There's no float between x and its neighbors
assert_eq!(up.next_down(), x);
assert_eq!(down.next_up(), x);
}
|
They handle all the edge cases you’d rather not think about — negative numbers, subnormals, and infinity:
1
2
3
4
5
6
7
8
9
10
11
12
| fn main() {
// Works across zero
assert_eq!(0.0_f64.next_up(), 5e-324); // smallest positive f64
assert_eq!(0.0_f64.next_down(), -5e-324); // largest negative f64
// Infinity is the boundary
assert_eq!(f64::MAX.next_up(), f64::INFINITY);
assert_eq!(f64::INFINITY.next_up(), f64::INFINITY);
// NaN stays NaN
assert!(f64::NAN.next_up().is_nan());
}
|
Practical use: precision-aware comparisons
The gap between adjacent floats is called an ULP (unit in the last place). next_up lets you build tolerance-aware comparisons without guessing at epsilon values:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| fn almost_equal(a: f64, b: f64, max_ulps: u32) -> bool {
if a == b { return true; }
let mut current = a;
for _ in 0..max_ulps {
current = if a < b { current.next_up() } else { current.next_down() };
if current == b { return true; }
}
false
}
fn main() {
let a = 0.1 + 0.2;
let b = 0.3;
// They're not equal...
assert_ne!(a, b);
// ...but they're within 1 ULP of each other
assert!(almost_equal(a, b, 1));
}
|
Also available on f32 with the same API. These methods are const fn, so you can use them in const contexts too.