Floats

#314 Aug 2026

314. Duration::mul_f64 — timeout * 1.5 Doesn't Compile, So You Round-Tripped Through Floats

Duration * 2 compiles, Duration * 1.5 doesn’t — and the usual workaround tears the duration apart into an f64 just to build it back up again.

Duration implements Mul<u32>, so doubling a timeout is easy. But backoff factors are rarely whole numbers — the classic multiplier is 1.5 or 2.0-with-jitter, and that’s where the type checker stops you:

1
2
3
4
5
6
7
use std::time::Duration;

let delay = Duration::from_millis(200);

let doubled = delay * 2;        // fine: Mul<u32>
// let next = delay * 1.5;      // error: cannot multiply
//                              // `Duration` by `{float}`

The workaround everyone reaches for is the float round-trip:

1
let next = Duration::from_secs_f64(delay.as_secs_f64() * 1.5);

It works, but it’s noisy, and it funnels a perfectly good Duration through a lossy f64 in both directions.

Multiply the duration directly

mul_f64 (and div_f64) have been on Duration since Rust 1.38 — they do the scaling in one call and keep nanosecond precision:

1
2
3
4
5
6
use std::time::Duration;

let delay = Duration::from_millis(200);

assert_eq!(delay.mul_f64(1.5), Duration::from_millis(300));
assert_eq!(delay.div_f64(2.0), Duration::from_millis(100));

Which makes an exponential backoff loop read exactly like the algorithm:

1
2
3
4
5
6
let mut delay = Duration::from_millis(100);
for _ in 0..4 {
    delay = delay.mul_f64(1.5).min(Duration::from_secs(2));
}
// 100ms → 150ms → 225ms → 337.5ms → 506.25ms
assert_eq!(delay, Duration::new(0, 506_250_000));

One caveat carried over from float land: mul_f64 panics if the factor produces a negative, non-finite, or overflowing result — the same failure mode as from_secs_f64 in bite 311. If your factor comes from untrusted math (jitter that can dip negative), clamp it first or go through Duration::try_from_secs_f64.

That closes out the week’s Duration tour: 311 built one from a float safely, 312 subtracted without panicking, 313 divided two of them — and today’s bite scales one without leaving the type.

#313 Aug 2026

313. div_duration_f64 — Your Progress Bar Sat at 0% Because as_secs() Truncated First

Dividing two Durations with as_secs() is integer division — 1.5s of 60s truncates to 0, and your progress bar never moves.

The obvious way to compute “how far along are we” divides seconds by seconds:

1
2
3
4
5
6
7
8
use std::time::Duration;

let elapsed = Duration::from_millis(1500);
let total = Duration::from_secs(60);

// 1 / 60 == 0 — integer division truncates
let frac = elapsed.as_secs() / total.as_secs();
assert_eq!(frac, 0);

as_secs() returns u64, so the sub-second part is gone before the division even runs, and the quotient truncates to zero on top of that. Switching to as_millis() just moves the truncation point; switching both sides to as_secs_f64() works but says nothing about intent.

Divide the durations directly

div_duration_f64 (stable since 1.80) divides one Duration by another and gives back the ratio as a float, sub-second precision included:

1
2
3
4
5
6
7
use std::time::Duration;

let elapsed = Duration::from_millis(1500);
let total = Duration::from_secs(60);

let frac = elapsed.div_duration_f64(total);
assert_eq!(frac, 0.025);

It reads in the same direction as the math: elapsed.div_duration_f64(total) is elapsed ÷ total. The same shape works for any “how many times does this fit” question — like a benchmark speedup:

1
2
3
4
let old = Duration::from_millis(750);
let new = Duration::from_millis(250);

assert_eq!(old.div_duration_f64(new), 3.0);

Two things worth knowing: there’s a div_duration_f32 twin if you’re feeding a graphics API, and dividing by Duration::ZERO follows float semantics — you get inf, not a panic. That’s one less edge case than the integer route, where total.as_secs() being zero would have crashed the division outright.

Like bite 311 and bite 312, the theme is the same: Duration already has a method for the math you’re about to do by hand — and the hand-rolled version is where the bugs live.

#311 Aug 2026

311. Duration::try_from_secs_f64 — Your Backoff Math Went Negative, and from_secs_f64 Panicked

Duration::from_secs_f64 panics on negative, NaN, or overflowing input — exactly the values float math produces when a jittered backoff dips below zero.

Computing a delay in float land is convenient: multiply a base by a factor, subtract some jitter, done. But the conversion back to Duration is a trap:

1
2
3
4
5
6
7
8
9
use std::time::Duration;

let base: f64 = 0.2;
let jitter: f64 = 0.3;
let secs = base - jitter; // -0.1

// panics: can not convert float seconds
// to Duration: value is negative
let delay = Duration::from_secs_f64(secs);

The panic conditions are anything that isn’t representable: negative values, NaN, and anything too large for Duration. All three are one arithmetic slip away — a subtraction that dips below zero, a 0.0 / 0.0 hiding in a rate calculation, an overflowing powi on the retry counter.

The fallible version hands you a Result

Duration::try_from_secs_f64 (stable since 1.66) does the same conversion but returns Err instead of panicking:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::time::Duration;

let ok = Duration::try_from_secs_f64(1.5);
assert_eq!(ok.unwrap(), Duration::from_millis(1500));

assert!(Duration::try_from_secs_f64(-0.1).is_err());
assert!(Duration::try_from_secs_f64(f64::NAN).is_err());
assert!(
    Duration::try_from_secs_f64(f64::INFINITY).is_err()
);

For backoff code, the natural move is to clamp the failure to something sane instead of crashing the retry loop:

1
2
3
4
let secs = -0.1_f64; // jitter took us negative
let delay = Duration::try_from_secs_f64(secs)
    .unwrap_or(Duration::ZERO);
assert_eq!(delay, Duration::ZERO);

There’s a try_from_secs_f32 twin for f32 inputs.

Like classify in bite 309, the point is that the float you’re holding can be quietly unrepresentable — and the moment of conversion is where that surfaces. try_from_secs_f64 lets it surface as a value you can handle, not a panic in the middle of your retry loop.

#310 Aug 2026

310. f64::max — The Fold That Quietly Swallows NaN

f64::NAN.max(3.0) is 3.0. A running max over data with a NaN in it returns a perfectly clean-looking number — and the is_nan check downstream swears nothing went wrong.

f64::max and f64::min follow IEEE 754’s maxNum/minNum semantics: if one operand is NaN, they return the other one. So NaN doesn’t poison the result the way it does with + or * — it silently drops out:

1
2
3
4
5
6
7
8
assert_eq!(f64::NAN.max(3.0), 3.0);
assert_eq!(3.0_f64.max(f64::NAN), 3.0);

let v = [2.5, f64::NAN, 3.1];
let max = v.iter().copied().fold(f64::MIN, f64::max);

assert_eq!(max, 3.1);   // NaN vanished
assert!(!max.is_nan()); // your check never fires

Sometimes that’s exactly what you want — treat NaN as “no data” and take the max of what’s left. But if a NaN in the input means the computation upstream went wrong, checking the result tells you nothing: the fold already laundered it.

The stable way to make NaN surface is total_cmp (the same total ordering from bite 101). In IEEE total order, positive NaN sorts above +inf, so a NaN in the data becomes the maximum instead of disappearing:

1
2
3
4
5
let strict = v.iter().copied()
    .max_by(|a, b| a.total_cmp(b))
    .unwrap();

assert!(strict.is_nan()); // NaN surfaces

(There are NaN-propagating f64::maximum / f64::minimum methods in the works, but they’re still unstable — total_cmp is the fix on stable.)

Like the subnormals in bite 309, this is a case where the value you’re inspecting passes every check while the data behind it lies. Decide explicitly: NaN-ignoring (f64::max) or NaN-surfacing (max_by(total_cmp)) — just don’t let the default decide for you.

#309 Aug 2026

309. classify — x > 0.0 Passed, Then 1.0 / x Returned inf

Between zero and the smallest “real” float lives a twilight zone of subnormals — values that pass your > 0.0 guard but blow up the moment you divide by them.

f64::MIN_POSITIVE (~2.2e-308) is the smallest normal float. Below it, floats go subnormal: the exponent is maxed out, so the mantissa gives up leading bits and precision degrades. They’re not zero, but they don’t behave like the floats you know:

1
2
3
4
5
6
7
8
let x = 1e-310_f64; // fell out of some computation

// your guard passes...
assert!(x > 0.0);
assert!(x.is_finite());

// ...but the reciprocal overflows
assert!((1.0 / x).is_infinite());

is_normal is the one-line fix — it’s true only for regular floats, and false for zero, subnormals, infinities, and NaN:

1
2
3
assert!(!x.is_normal());
assert!(x.is_subnormal());
assert!(1.0_f64.is_normal());

When you need to know which weird case you got, classify turns the whole is_nan / is_infinite / == 0.0 ladder into one exhaustive match:

1
2
3
4
5
6
7
8
use std::num::FpCategory;

match x.classify() {
    FpCategory::Normal => { /* safe to invert */ }
    FpCategory::Zero | FpCategory::Subnormal => { /* underflow */ }
    FpCategory::Infinite | FpCategory::Nan => { /* bad input */ }
}
assert_eq!(x.classify(), FpCategory::Subnormal);

The match is exhaustive, so unlike the boolean ladder, the compiler makes sure you handled every category — including the one you forgot exists.

If a division’s denominator came from data instead of a literal, denom.is_normal() is the guard you meant when you wrote denom != 0.0.

#308 Aug 2026

308. sin_cos — One Angle, Both Halves, No Swap Bug

Every rotation, polar conversion, and circle you draw needs sin and cos of the same angle — and every codebase has one spot where x got the sin and y got the cos.

Two separate calls means two chances to put the results in the wrong slot, and the compiler can’t help — both are f64. sin_cos returns the pair as a tuple, so the destructure names them once and the math below reads like math:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let theta = 30.0_f64.to_radians(); // bite 307
let r = 2.0;

// two calls, two chances to swap them
let x = r * theta.cos();
let y = r * theta.sin();

// one call, one destructure
let (sin, cos) = theta.sin_cos();
let (x, y) = (r * cos, r * sin);
assert!((y - 1.0).abs() < 1e-15); // r·sin 30° = 1

One thing to burn in: the tuple is (sin, cos) — sin first, matching the method name, even though (x, y) order would put cos first. Destructure as let (s, c) = … and the names keep you honest.

The classic customer is a 2D rotation, where the same sin and cos appear four times:

1
2
3
4
5
6
7
fn rotate((x, y): (f64, f64), theta: f64) -> (f64, f64) {
    let (s, c) = theta.sin_cos();
    (x * c - y * s, x * s + y * c)
}

let (x, y) = rotate((1.0, 0.0), 90.0_f64.to_radians());
assert!(x.abs() < 1e-15 && (y - 1.0).abs() < 1e-15);

Without sin_cos that function either calls sin and cos twice each, or you write the two temporaries by hand — which is exactly where the swap sneaks in.

Don’t reach for it as a speed hack: std is free to implement it as the two calls you’d have written, and often does. The win is intent — one angle in, both halves out, and bite 300’s atan2 will happily turn the pair back into the angle:

1
2
let (s, c) = 1.0_f64.sin_cos();
assert!((s.atan2(c) - 1.0).abs() < 1e-15);

If a sin and a cos of the same angle sit within three lines of each other, they want to be one sin_cos.

#307 Aug 2026

307. to_degrees / to_radians — Stop Hand-Typing 180.0 / PI

sin wants radians, your users want degrees, and somewhere in your codebase a hand-typed * 180.0 / PI is waiting to be fat-fingered. The conversion has been a method on floats since Rust 1.0.

Every trig function in std speaks radians. Every protractor, compass heading, and UI slider speaks degrees. So this line gets written over and over:

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

let heading: f64 = 90.0;

// hand-rolled
let rad = heading * PI / 180.0;

// stdlib
let rad = heading.to_radians();
assert!((rad - PI / 2.0).abs() < 1e-15);

Same in the other direction — bite 300’s atan2 hands you radians, and to_degrees turns them back into something a human can read:

1
2
let angle = 1.0_f64.atan2(1.0); // 45° as radians
assert!((angle.to_degrees() - 45.0).abs() < 1e-12);

The readability win is obvious: no magic constant to mistype (57.2958, 0.0174533, and their truncated cousins show up in real codebases), and the intent is in the method name instead of a comment.

There’s a correctness win too. to_degrees multiplies by one precomputed constant — 180.0 / PI rounded once at full precision. The hand-rolled version does two operations, and the intermediate x * 180.0 can overflow even when the final result is representable:

1
2
3
4
let big = f64::MAX / 100.0;

assert!((big * 180.0 / PI).is_infinite()); // x * 180 blew up
assert!(big.to_degrees().is_finite());     // one multiply, no overflow

The everyday use is keeping unit confusion out of trig calls — convert at the boundary, and everything inside stays in radians:

1
2
let slope = 30.0_f64.to_radians().sin();
assert!((slope - 0.5).abs() < 1e-15);

If a PI / 180.0 appears anywhere outside a constants module, it wants to be a to_radians.

#306 Aug 2026

306. powi — Integer Powers Without the powf Detour

x * x * x doesn’t scale, and powf(3.0) routes an integer exponent through the full floating-point pow machinery. powi is the method built for exactly this case.

Three ways to cube a float:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let x: f64 = 1.05;

// the hand-rolled chain
let cubed = x * x * x;

// powf: full transcendental pow
let cubed = x.powf(3.0);

// powi: integer exponent, fast path
let cubed = x.powi(3);

The chain stops being readable past the third power and can’t handle a runtime exponent at all. powf works, but it’s the general routine — built to handle any real exponent, when all you have is a small integer.

powi takes an i32 and computes the power by repeated multiplication. Negative exponents give you the reciprocal, no 1.0 / dance:

1
assert_eq!(2.0_f64.powi(-3), 0.125); // 1 / 2³

The quieter win is negative bases. As bite 302 showed with cbrt, powf on a negative base is one fractional exponent away from NaN. With powi the exponent is an integer by construction, so the sign question always has an answer:

1
2
assert_eq!((-3.0_f64).powi(2), 9.0);
assert_eq!((-3.0_f64).powi(3), -27.0);

Compound growth is the everyday use case — the exponent is a year count, not a real number:

1
2
3
4
5
6
let balance: f64 = 1_000.0;
let rate: f64 = 0.05;
let years: i32 = 10;

let future = balance * (1.0 + rate).powi(years);
assert!((future - 1_628.89).abs() < 0.01);

If the exponent in your powf call ends in .0, it wants to be a powi.

#305 Aug 2026

305. to_bits — Hash and Dedup Floats Without a Wrapper Crate

HashSet<f64> doesn’t compile — floats aren’t Hash or Eq. f64::to_bits turns each float into its exact u64 bit pattern, which is both.

Try to dedup a list of readings and the compiler stops you at the door:

1
2
3
4
5
use std::collections::HashSet;

// error[E0277]: the trait bound `f64: Eq`
// is not satisfied
// let seen: HashSet<f64> = HashSet::new();

f64 can’t be Eq because NaN != NaN, and it can’t be Hash because hashing requires consistent equality. The usual escape hatch is a wrapper crate like ordered-float — but for keying and dedup, std already has what you need:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use std::collections::HashSet;

let readings: [f64; 4] =
    [0.1 + 0.2, 0.3, 0.15, 0.3];

let mut seen = HashSet::new();
let uniq: Vec<f64> = readings
    .into_iter()
    .filter(|x| seen.insert(x.to_bits()))
    .collect();

// 0.1 + 0.2 is not bit-equal to 0.3
assert_eq!(uniq.len(), 3);

to_bits is a transmute, not a cast: (1.0f64).to_bits() is 4607182418800017408, not 1. Every distinct float maps to a distinct u64, and f64::from_bits round-trips it losslessly:

1
2
let x: f64 = 0.1 + 0.2;
assert_eq!(f64::from_bits(x.to_bits()), x);

Two things bit-equality changes, both usually what you want for keys:

  • NaN becomes equal to itself (same payload, same bits), so a NaN key is stored once instead of leaking in forever.
  • 0.0 and -0.0 compare == as floats but have different bit patterns, so they count as two keys.

Note the first assert above: 0.1 + 0.2 stays in the set alongside 0.3 because they really are different floats. to_bits doesn’t paper over float imprecision — it makes it visible. If you want tolerance-based grouping, that’s a different tool; for exact identity — memoization keys, dedup, caching — the bit pattern is the honest answer.

#304 Aug 2026

304. trunc & fract — Split a Float Without the Cast Round-Trip

Need the whole and fractional parts of a float? The (x as i64) as f64 round-trip works — until the value doesn’t fit in an i64. trunc and fract split the float directly.

The cast detour everyone writes first:

1
2
3
4
5
6
let t = 7.75_f64;

let whole = (t as i64) as f64;
let frac = t - whole;
assert_eq!(whole, 7.0);
assert_eq!(frac, 0.75);

The direct version — no casts, no subtraction:

1
2
3
let t = 7.75_f64;
assert_eq!(t.trunc(), 7.0);
assert_eq!(t.fract(), 0.75);

Both parts keep the sign, and trunc() + fract() always reassembles the original — which also makes trunc the “toward zero” sibling in the rounding family from bite 303:

1
2
3
4
let t = -7.75_f64;
assert_eq!(t.trunc(), -7.0);
assert_eq!(t.fract(), -0.75);
assert_eq!(t.trunc() + t.fract(), t);

The real reason to drop the cast: as i64 silently saturates past ±2⁶³, so the round-trip hands back the wrong number without a peep. trunc has no range limit:

1
2
3
4
5
let big = 1e19_f64; // > i64::MAX

assert_eq!(big.trunc(), 1e19); // correct
assert_eq!((big as i64) as f64,
    9.223372036854776e18); // wrong, silently

The classic use case — splitting a quantity across two units:

1
2
3
4
5
let secs = 92.375_f64;
let mins = (secs / 60.0).trunc();
let rest = secs - mins * 60.0;
assert_eq!(mins, 1.0);
assert_eq!(rest, 32.375);

Note that fract on a negative number is negative — if you want “distance above the floor” instead (always in [0, 1)), that’s x - x.floor(), or x.rem_euclid(1.0).

#303 Aug 2026

303. round_ties_even — round() Pushes Every Half Up, and Your Totals Drift

round() sends every .5 away from zero — so summing rounded values drifts upward. round_ties_even rounds ties to the even neighbor and the bias cancels out.

Round a batch of exact midpoints with round() and watch the total inflate:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let vals = [0.5_f64, 1.5, 2.5, 3.5];
// true sum: 8.0

let away: f64 = vals.iter()
    .map(|v| v.round())
    .sum();
assert_eq!(away, 10.0); // 1 + 2 + 3 + 4

let even: f64 = vals.iter()
    .map(|v| v.round_ties_even())
    .sum();
assert_eq!(even, 8.0); // 0 + 2 + 2 + 4

round() uses “half away from zero”: every tie moves in the same direction, so the errors all point the same way and accumulate. round_ties_even — banker’s rounding — sends ties to whichever neighbor is even, so roughly half go down and half go up:

1
2
3
4
assert_eq!(0.5_f64.round_ties_even(), 0.0);
assert_eq!(1.5_f64.round_ties_even(), 2.0);
assert_eq!(2.5_f64.round_ties_even(), 2.0);
assert_eq!((-2.5_f64).round_ties_even(), -2.0);

Only exact ties behave differently — 2.4999 and 2.5001 round the same either way:

1
2
assert_eq!(2.4999_f64.round_ties_even(), 2.0);
assert_eq!(2.5001_f64.round_ties_even(), 3.0);

This is the rounding mode IEEE 754 uses by default for a reason: it’s statistically unbiased over many operations. It’s also what Python’s built-in round does — if a value “rounds wrong” when porting Python code to Rust, this is why. Stable since Rust 1.77.

#302 Aug 2026

302. cbrt — powf(1.0/3.0) Says the Cube Root of -27 Is NaN

Every negative number has a real cube root — -27 is just -3. But (-27.0).powf(1.0 / 3.0) returns NaN. cbrt is the method that actually knows what a cube root is.

The naive formula falls over the moment the input goes negative:

1
2
3
4
5
6
7
let x = -27.0_f64;

let naive = x.powf(1.0 / 3.0);
assert!(naive.is_nan());

let root = x.cbrt();
assert_eq!(root, -3.0);

Why the NaN? powf computes x^y roughly as exp(y * ln(x)), and ln of a negative number doesn’t exist. powf can’t know that 0.3333333333333333 was meant to be ⅓ — it’s just a float that isn’t 1/3, so there’s no “odd root of a negative” special case to fall into. Mathematically fine operation, wrong tool.

The exponent being off also costs accuracy on positive inputs. 1.0 / 3.0 isn’t exactly one third, so powf computes a slightly different power:

1
2
3
4
let approx = 729.0_f64.powf(1.0 / 3.0);
assert_ne!(approx, 9.0); // 8.999999999999998

assert_eq!(729.0_f64.cbrt(), 9.0);

cbrt also keeps the edge cases sane where the formula produces nonsense: (-0.0).cbrt() is -0.0, and f64::NEG_INFINITY.cbrt() is NEG_INFINITY — not NaN.

Same story as this morning’s ln_1p (bite 301): when std ships a dedicated method for the operation you’re approximating with a formula, the method wins on both correctness and precision. cbrt has been stable since Rust 1.0 — and there’s a sqrt next to it you were already using.

#301 Aug 2026

301. ln_1p / exp_m1 — Adding 1.0 Destroys Your Tiny Float Before ln Ever Runs

(1.0 + x).ln() looks innocent, but for tiny x the 1.0 + step rounds away most of x’s digits before ln even sees them. ln_1p and exp_m1 do the + 1 internally, where nothing is lost.

Say you’re turning a tiny growth rate into a log-return:

1
2
3
4
5
6
let r = 1e-12_f64;

// 1.0 + r rounds away most of r
let naive = (1.0 + r).ln();
// ~1.0000889e-12 — 4 digits, then noise
assert!((naive - r).abs() > 1e-17);

The problem is spacing. Around 1.0, consecutive f64 values are about 2.2e-16 apart, so 1.0 + 1e-12 snaps to the nearest representable value — and the snap error (~9e-17 here) is enormous compared to r itself. ln then faithfully computes the log of the wrong number. You get 1.0000889e-12: four correct digits, then noise.

ln_1p computes ln(1 + x) without ever materializing 1 + x:

1
2
3
let precise = r.ln_1p();
// true value is r - r²/2 ≈ r - 5e-25
assert!((precise - r).abs() < 1e-20);

Full precision — the true answer differs from r only in the 25th decimal place, and ln_1p nails it.

The same trap exists in the other direction. e^x - 1 for tiny x:

1
2
3
4
5
6
7
// exp(r) ≈ 1.0000000000010000...
// subtracting 1.0 exposes the rounding
let naive = r.exp() - 1.0;
assert!((naive - r).abs() > 1e-17);

let precise = r.exp_m1(); // e^r - 1
assert!((precise - r).abs() < 1e-20);

And since they’re exact inverses, they round-trip cleanly:

1
2
let back = r.ln_1p().exp_m1();
assert!((back - r).abs() < 1e-26);

If x is comfortably large — say 0.1 and up — the naive forms are fine. But interest rates, probabilities, and per-step deltas live exactly in the tiny range where they aren’t. Both methods have been stable since Rust 1.0; they’re a rename away.

#300 Aug 2026

300. atan2 — The Angle Formula That Knows Its Quadrant

(dy / dx).atan() happily returns an angle — in the wrong quadrant half the time, and NaN at the origin. atan2 takes both components and gets every case right.

This morning’s bite 299 covered hypot, the robust way to get the length of a vector. Its companion problem is the angle — and the naive formula fails in a sneakier way.

atan alone can only return angles between −π/2 and π/2, because the division throws away the signs of the inputs:

1
2
3
4
5
let (dx, dy) = (-3.0_f64, 4.0);

// (-3, 4) is up-and-left: second quadrant
let naive = (dy / dx).atan();
assert!(naive < 0.0); // ...a negative angle?!

4.0 / -3.0 is the same number as -4.0 / 3.0, so atan can’t tell the second quadrant from the fourth. atan2 takes dy and dx separately, keeps both signs, and returns the full −π to π range:

1
2
3
4
5
6
let angle = dy.atan2(dx);
assert!((angle - 2.2142974355881810).abs() < 1e-12);

// naive answer is off by exactly π
let err = angle - naive;
assert!((err - std::f64::consts::PI).abs() < 1e-12);

It also survives the edge the division dies on — the origin:

1
2
assert!((0.0_f64 / 0.0).atan().is_nan());
assert_eq!(0.0_f64.atan2(0.0), 0.0);

Together with hypot, this is the complete cartesian-to-polar kit, and the round trip closes to within an ulp:

1
2
3
4
let r = dx.hypot(dy);
let theta = dy.atan2(dx);
assert!((r * theta.cos() - dx).abs() < 1e-12);
assert!((r * theta.sin() - dy).abs() < 1e-12);

Argument order trips everyone up once: it’s y.atan2(x) — the vertical component is self. If your angles keep coming out mirrored across the diagonal, you’ve swapped them.

#299 Aug 2026

299. hypot — The Distance Formula That Doesn't Overflow

(dx*dx + dy*dy).sqrt() looks harmless — until the squares overflow to infinity even though the answer would fit in an f64 just fine. hypot computes the same distance without ever squaring your inputs.

The textbook distance formula squares first, adds, then takes the root:

1
2
3
4
let dx = 3.0_f64;
let dy = 4.0_f64;
let dist = (dx * dx + dy * dy).sqrt();
assert_eq!(dist, 5.0);

Works great — until the components get big. f64::MAX is about 1.8e308, so squaring anything past ~1.3e154 blows straight through it:

1
2
3
4
5
6
let dx = 3.0e200_f64;
let dy = 4.0e200_f64;

// dx*dx is 9e400 — that's infinity in f64
let naive = (dx * dx + dy * dy).sqrt();
assert!(naive.is_infinite());

The final answer, 5.0e200, fits in an f64 with room to spare. It’s only the intermediate squares that overflow. hypot is built for exactly this — it computes sqrt(x² + y²) using a rescaling algorithm that never materializes the squares:

1
2
3
let dist = dx.hypot(dy);
assert!(dist.is_finite());
assert!((dist / 5.0e200 - 1.0).abs() < 1e-15);

(Note the relative comparison — hypot is accurate to within an ulp or so, not bit-exact.)

The same trick saves you at the other end of the scale: for tiny components the squares underflow to zero, and the naive formula returns 0.0 for a distance that plainly isn’t zero. hypot gets that case right too:

1
2
3
let tiny = 1.0e-200_f64;
assert_eq!((tiny * tiny).sqrt(), 0.0); // underflow
assert!(tiny.hypot(tiny) > 0.0);

If your coordinates are sane screen pixels, the naive formula is fine. But the moment the magnitudes are user-supplied or physics-scaled, reach for hypot — same one-liner, no cliff at 1e154.

#101 Apr 2026

101. f64::total_cmp — Sort Floats Without the NaN Panic

Tried v.sort() on a Vec<f64> and hit the trait Ord is not implemented for f64? Then reached for .sort_by(|a, b| a.partial_cmp(b).unwrap()) and now a stray NaN is about to panic your service at 3am? f64::total_cmp is the one-liner that makes both problems disappear.

Why f64 doesn’t implement Ord

Floats form a partial order because NaN is not equal to anything — not even itself. So f64: PartialOrd but not Ord, which means sort() flat out refuses to compile:

1
2
let mut temps: Vec<f64> = vec![21.5, 18.0, 23.0, 19.5];
// temps.sort(); // ❌ the trait `Ord` is not implemented for `f64`

The classic workaround is partial_cmp().unwrap():

1
2
3
4
let mut temps: Vec<f64> = vec![21.5, 18.0, 23.0, 19.5];
temps.sort_by(|a, b| a.partial_cmp(b).unwrap());

assert_eq!(temps, [18.0, 19.5, 21.5, 23.0]);

Works — until a NaN sneaks in. Then partial_cmp returns None, the unwrap fires, and your sort becomes a panic.

Enter total_cmp

f64::total_cmp implements the IEEE 754 totalOrder predicate: a real total ordering on every f64 bit pattern, including all the NaNs. It returns Ordering directly — no Option, no panic:

1
2
3
4
let mut temps: Vec<f64> = vec![21.5, 18.0, 23.0, 19.5];
temps.sort_by(f64::total_cmp);

assert_eq!(temps, [18.0, 19.5, 21.5, 23.0]);

Same result for well-behaved input, but now NaN won’t take the process down:

1
2
3
4
5
6
7
8
9
let mut values: Vec<f64> = vec![3.0, f64::NAN, 1.0, f64::NEG_INFINITY, 2.0];
values.sort_by(f64::total_cmp);

// Finite values in order, -∞ at the front, NaN at the back.
assert_eq!(values[0], f64::NEG_INFINITY);
assert_eq!(values[1], 1.0);
assert_eq!(values[2], 2.0);
assert_eq!(values[3], 3.0);
assert!(values[4].is_nan());

min and max too

partial_cmp poisons more than just sort. Any time you reach for iter().max_by(|a, b| a.partial_cmp(b).unwrap()), you’ve written the same latent panic. total_cmp fits there too:

1
2
3
4
5
6
7
let readings = [3.2_f64, 1.4, 4.8, 2.1];

let peak = readings.iter().copied().max_by(f64::total_cmp).unwrap();
let low  = readings.iter().copied().min_by(f64::total_cmp).unwrap();

assert_eq!(peak, 4.8);
assert_eq!(low, 1.4);

Sorting structs by a float field

Because total_cmp takes two &f64s and returns Ordering, it slots straight into sort_by:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct Trade { symbol: &'static str, price: f64 }

let mut book = vec![
    Trade { symbol: "AAPL", price: 172.40 },
    Trade { symbol: "NVDA", price: 915.10 },
    Trade { symbol: "MSFT", price: 419.80 },
];

book.sort_by(|a, b| a.price.total_cmp(&b.price));

assert_eq!(book[0].symbol, "AAPL");
assert_eq!(book[2].symbol, "NVDA");

When to reach for it

Any time you’re about to type partial_cmp(...).unwrap() for a float, stop and use total_cmp instead. f32::total_cmp works the same way. Available since Rust 1.62 — the fix has been hiding in plain sight for years.