#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.

#298 Aug 2026

298. copysign — Stamp One Number's Sign Onto Another, Sign Bit and All

Bite 297 showed signum reading the sign bit. copysign is the other half: it writes one — take this magnitude, give it that number’s sign, one call.

The branch

You have a fixed magnitude and a direction living in another variable — a step size, a force, a correction — and out comes the same conditional every time:

1
2
3
let delta: f64 = -3.2;

let step = if delta < 0.0 { -0.25 } else { 0.25 };

One call

1
2
3
4
5
let step = 0.25_f64.copysign(delta); // -0.25

assert_eq!(3.5_f64.copysign(-1.0), -3.5);
assert_eq!((-3.5_f64).copysign(1.0), 3.5);
assert_eq!(7.0_f64.copysign(2.0), 7.0);

The receiver contributes the magnitude, the argument contributes the sign. Yesterday’s “step toward the target” loop works for floats too — magnitude from you, direction from the gap:

1
2
3
4
5
6
7
let mut pos = 1.0_f64;
let target = 2.0;

while (target - pos).abs() >= 0.25 {
    pos += 0.25_f64.copysign(target - pos);
}
assert_eq!(pos, 2.0); // 0.25 divides 1.0 exactly

It really is the sign bit

Like f64::signum, copysign works on the raw sign bit — which is exactly what makes it more honest than the if:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// the branch calls -0.0 "positive"...
let x = -0.0_f64;
let s = if x < 0.0 { -1.0 } else { 1.0 };
assert_eq!(s, 1.0);

// ...copysign actually looks at the bit
assert!(1.0_f64.copysign(-0.0).is_sign_negative());

// and a NaN's magnitude stays NaN, sign still applied
assert!(f64::NAN.copysign(-1.0).is_sign_negative());

Anywhere you’re bolting a direction onto a magnitude — drag opposing velocity, rounding away from zero, mirroring a coordinate — copysign is the branch-free, -0.0-correct way to say it.

#297 Aug 2026

297. signum — The -1 / 0 / +1 You Keep Building With an If-Else Ladder

Every “which direction?” check ends up as the same three-branch ladder: greater, less, equal. signum is that ladder as one method call — with a float twist you need to know about.

The ladder

You want the direction of a difference — for a comparator, a game step, a sort key:

1
2
3
4
5
6
7
8
9
let delta: i32 = -7;

let dir = if delta > 0 {
    1
} else if delta < 0 {
    -1
} else {
    0
};

One call

1
2
3
4
5
let dir = delta.signum(); // -1

assert_eq!(42i32.signum(), 1);
assert_eq!(0i32.signum(), 0);
assert_eq!((-3i64).signum(), -1);

It shines whenever you need to move one step toward a target, whatever the distance:

1
2
3
4
5
6
7
let mut pos: i32 = 3;
let target = 7;

while pos != target {
    pos += (target - pos).signum(); // steps ±1, stops exactly
}
assert_eq!(pos, target);

No overshoot, no separate branches for “target is behind me” — the sign does the steering.

The float trap

f64::signum is not the same function. It reports the sign bit, so zero is never 0.0:

1
2
3
assert_eq!(0.0_f64.signum(), 1.0);    // not 0.0!
assert_eq!((-0.0_f64).signum(), -1.0);
assert!(f64::NAN.signum().is_nan());

If you want the integer-style three-way answer for floats, be explicit about zero:

1
2
3
let x = 0.0_f64;
let dir = if x == 0.0 { 0.0 } else { x.signum() };
assert_eq!(dir, 0.0);

Reach for signum on integers without a second thought; on floats, remember it answers “which sign bit?” — not “is this positive, negative, or zero?”.