Right, let’s talk about numbers that lie to you. Not maliciously, mind you, but out of sheer, fundamental, mathematical necessity. We’re entering the world of floating-point numbers, and if you think 0.1 + 0.2 == 0.3, prepare to have your entire reality gently, but firmly, corrected.

We use them to represent a huge range of values, from the mass of a proton to the national debt, by allowing the decimal point to “float.” Rust gives you two main flavors: f32 (single-precision) and f64 (double-precision). Unless you’re working on a deeply embedded system where every byte counts, just use f64. It’s the default for a reason: it’s double the precision (about 15-16 decimal digits instead of 6-7 for f32) and on modern hardware, the performance difference is often negligible. The extra headache you save yourself from weird precision errors is worth it.

How They Work (And Why They’re Weird)

Don’t think of them as infinitely precise decimals. Think of them as scientific notation encoded in binary. A 64-bit f64 number uses 1 bit for the sign, 11 bits for the exponent (the power of 2), and 52 bits for the significand (the significant digits). This is brilliant for range and efficiency, but it means we’re representing base-10 numbers in a base-2 system. And just like you can’t perfectly represent 1/3 in base-10 (0.333…), you can’t perfectly represent 0.1 in base-2. It becomes an infinitely repeating binary sequence, which gets rounded to the nearest value that fits in 52 bits.

This is why the classic example happens. Let’s run it.

fn main() {
    let result = 0.1 + 0.2;
    println!("0.1 + 0.2 = {}", result); // Prints 0.30000000000000004
    println!("0.1 + 0.2 == 0.3? {}", result == 0.3); // Prints false
}

See? It’s not a bug in Rust; it’s a fundamental property of binary floating-point arithmetic. Every language that uses IEEE 754 standard floats has this “issue.” Welcome to the club.

The Perils of Comparison

This leads us to the first and most important rule: Never, ever use == to compare floating-point numbers. It’s a recipe for heartbreak. Because of these tiny rounding errors, two numbers that should be equal mathematically often won’t be exactly equal in their binary representation.

So what do you do? You check if they’re close enough. You define a tiny tolerance value (often called an epsilon) and see if the difference between the two numbers is smaller than that.

fn main() {
    let result = 0.1 + 0.2;
    let epsilon = 1e-10; // A very small tolerance

    // Check if the absolute difference is less than epsilon
    let are_equal = (result - 0.3).abs() < epsilon;
    println!("Are they close enough? {}", are_equal); // Prints true
}

The f32 and f64 types have a method for this called abs(), and they also provide a constant for the smallest difference between two numbers that can be represented: f64::EPSILON. For most practical purposes, using a slightly larger epsilon is safer.

Special Values and Mayhem

Floating-point numbers have entire concepts dedicated to things that aren’t numbers. This is where it gets fun.

  • NaN (Not a Number): This is what you get from mathematically invalid operations like taking the square root of a negative number or 0.0 / 0.0. A crucial detail: NaN is not equal to anything, including itself. You must use the is_nan() method to check for it.

  • Infinity: You can have positive and negative infinity, generated by things like 1.0 / 0.0 (which is f64::INFINITY) or -1.0 / 0.0 (f64::NEG_INFINITY). They exist to allow computations to continue instead of immediately crashing, which is sometimes useful and sometimes a silent error that propagates through your entire system.

  • Negative Zero: Yes, -0.0 is a thing. It exists due to the sign bit being separate. It usually compares equal to regular 0.0, but it can matter in certain mathematical contexts.

fn main() {
    let nan = 0.0_f64 / 0.0;
    println!("NaN == NaN? {}", nan == nan); // false. Read that again.
    println!("Is it NaN? {}", nan.is_nan()); // true

    let inf = 1.0 / 0.0;
    println!("What is infinity? {}", inf); // inf
    println!("Is it finite? {}", inf.is_finite()); // false

    let neg_zero = -0.0;
    println!("-0.0 == 0.0? {}", neg_zero == 0.0); // true, but...
    println!("Are they the same? {}", 1.0 / neg_zero == 1.0 / 0.0); // false! (-inf != inf)
}

When to Use What (And What to Avoid)

Use f64 for almost everything: game physics, scientific computing, most financial calculations (with care!), and general-purpose math.

Use f32 only when you have a good reason: graphics programming (GPUs are optimized for them), massive arrays where memory bandwidth is the bottleneck, or specific embedded targets.

And for the love of all that is holy, do not use floats for money. The precision errors will slowly siphon pennies out of your accounts until you’re audited. Use integers representing the smallest unit (e.g., cents) or a decimal library designed for base-10 arithmetic, like rust_decimal.

Floats are a powerful, necessary tool. They’re fast, standardized, and incredibly useful. But they demand respect. Understand their limitations, never trust them with ==, always check for NaN, and you’ll get along just fine. They’re not bad, they’re just… approximate. Like most of my weekend plans.