Math

#082 Apr 2026

82. isqrt — Integer Square Root Without Floating Point

(n as f64).sqrt() as u64 is the classic hack — and it silently gives the wrong answer for large values. Rust 1.84 stabilized isqrt on every integer type: exact, float-free, no precision traps.

The floating-point trap

Converting to f64, calling .sqrt(), and casting back is the go-to pattern. It looks fine. It isn’t.

1
2
3
4
let n: u64 = 10_000_000_000_000_000_000;
let bad = (n as f64).sqrt() as u64;
// bad == 3_162_277_660, but floor(sqrt(n)) is 3_162_277_660 — or is it?
// For many large u64 values, the f64 round-trip is off by 1.

f64 only has 53 bits of mantissa, so for u64 values above 2^53 the conversion loses precision before you even take the square root.

The fix: isqrt

1
2
3
4
let n: u64 = 10_000_000_000_000_000_000;
let root = n.isqrt();
assert_eq!(root * root <= n, true);
assert_eq!((root + 1).checked_mul(root + 1).map_or(true, |sq| sq > n), true);

It’s defined on every integer type — u8, u16, u32, u64, u128, usize, and their signed counterparts — and always returns the exact floor of the square root. No casts, no rounding, no surprises.

Signed integers too

1
2
3
4
5
6
let x: i32 = 42;
assert_eq!(x.isqrt(), 6); // 6*6 = 36, 7*7 = 49

// Negative values would panic, so check first:
let maybe_neg: i32 = -4;
assert_eq!(maybe_neg.checked_isqrt(), None);

Use checked_isqrt on signed types when the input might be negative — it returns Option<T> instead of panicking.

When you’d reach for it

Perfect-square checks, tight loops over divisors, hash table sizing, geometry on integer grids — anywhere you were reaching for f64::sqrt purely to round down, reach for isqrt instead. It’s faster, exact, and one character shorter.

#078 Apr 2026

78. div_ceil — Divide and Round Up Without the Overflow Bug

Need to split items into fixed-size pages or chunks? The classic (n + size - 1) / size trick silently overflows. div_ceil does it correctly.

The classic footgun

Paging, chunking, allocating — any time you divide and need to round up, this pattern shows up:

1
2
3
fn pages_needed(total: u64, per_page: u64) -> u64 {
    (total + per_page - 1) / per_page // ⚠️ overflows when total is large
}

It works until total + per_page - 1 wraps around. With u64::MAX items and a page size of 10, you get a wrong answer instead of a panic or correct result.

The fix: div_ceil

Stabilized in Rust 1.73, div_ceil handles the rounding without intermediate overflow:

1
2
3
fn pages_needed(total: u64, per_page: u64) -> u64 {
    total.div_ceil(per_page)
}

One method call, no overflow risk, intent crystal clear.

Real-world examples

Allocating pixel rows for a tiled renderer:

1
2
3
4
let image_height: u32 = 1080;
let tile_size: u32 = 64;
let tile_rows = image_height.div_ceil(tile_size);
assert_eq!(tile_rows, 17); // 16 full tiles + 1 partial

Splitting work across threads:

1
2
3
4
let items: usize = 1000;
let threads: usize = 6;
let chunk_size = items.div_ceil(threads);
assert_eq!(chunk_size, 167); // each thread handles at most 167 items

It works on all unsigned integers

div_ceil is available on u8, u16, u32, u64, u128, and usize. Signed integers also have it (since Rust 1.73), but watch out — it rounds toward positive infinity, which for negative dividends means rounding away from zero.

1
2
let signed: i32 = -7;
assert_eq!(signed.div_ceil(2), -3); // rounds toward +∞, not toward 0

Next time you reach for (a + b - 1) / b, stop — div_ceil already exists and it won’t betray you at the boundaries.

#068 Apr 2026

68. f64::next_up — Walk the Floating Point Number Line

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.

#057 Apr 2026

57. New Math Constants — GOLDEN_RATIO and EULER_GAMMA in std

Tired of defining your own golden ratio or Euler-Mascheroni constant? As of Rust 1.94, std ships them out of the box — no more copy-pasting magic numbers.

Before: Roll Your Own

If you needed the golden ratio or the Euler-Mascheroni constant before Rust 1.94, you had to define them yourself:

1
2
const PHI: f64 = 1.618033988749895;
const EULER_GAMMA: f64 = 0.5772156649015329;

This works, but it’s error-prone. One wrong digit and your calculations drift. And every project that needs these ends up with its own slightly-different copy.

After: Just Use std

Rust 1.94 added GOLDEN_RATIO and EULER_GAMMA to the standard consts modules for both f32 and f64:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use std::f64::consts::{GOLDEN_RATIO, EULER_GAMMA};

fn main() {
    // Golden ratio: (1 + √5) / 2
    let phi = GOLDEN_RATIO;
    assert!((phi * phi - phi - 1.0).abs() < 1e-10);

    // Euler-Mascheroni constant
    let gamma = EULER_GAMMA;
    assert!((gamma - 0.5772156649015329).abs() < 1e-10);

    println!("φ = {phi}");
    println!("γ = {gamma}");
}

These sit right alongside the constants you already know — PI, TAU, E, SQRT_2, and friends.

Where You’d Actually Use Them

Golden ratio shows up in algorithm design (Fibonacci heaps, golden-section search), generative art, and UI layout proportions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::f64::consts::GOLDEN_RATIO;

fn golden_section_dimensions(width: f64) -> (f64, f64) {
    let height = width / GOLDEN_RATIO;
    (width, height)
}

fn main() {
    let (w, h) = golden_section_dimensions(800.0);
    assert!((w / h - GOLDEN_RATIO).abs() < 1e-10);
    println!("Width: {w}, Height: {h:.2}");
}

Euler-Mascheroni constant appears in number theory, harmonic series approximations, and probability:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use std::f64::consts::EULER_GAMMA;

/// Approximate the N-th harmonic number using the
/// asymptotic expansion: H_n ≈ ln(n) + γ + 1/(2n)
fn harmonic_approx(n: u64) -> f64 {
    let nf = n as f64;
    nf.ln() + EULER_GAMMA + 1.0 / (2.0 * nf)
}

fn main() {
    // Exact H_10 = 1 + 1/2 + 1/3 + ... + 1/10
    let exact: f64 = (1..=10).map(|i| 1.0 / i as f64).sum();
    let approx = harmonic_approx(10);

    println!("Exact H_10:  {exact:.6}");
    println!("Approx H_10: {approx:.6}");
    assert!((exact - approx).abs() < 0.01);
}

The Full Lineup

With these additions, std::f64::consts now includes: PI, TAU, E, SQRT_2, SQRT_3, LN_2, LN_10, LOG2_E, LOG2_10, LOG10_2, LOG10_E, FRAC_1_PI, FRAC_1_SQRT_2, FRAC_1_SQRT_2PI, FRAC_2_PI, FRAC_2_SQRT_PI, FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6, FRAC_PI_8, GOLDEN_RATIO, and EULER_GAMMA. That’s a pretty complete toolkit for numerical work — all with full f64 precision, available at compile time.