Chapter 1

Numbers in Rust

👋 Anyone can read and edit this exercise. Sign up to save your progress.

This chapter was supposed to start with an integer joke. Turns out, it's pointless.

You already know how numbers work, so let's start with a quick tour of the types and then spend the rest of the chapter on what Rust does differently.

let byte: u8 = 255;           // a single byte, holds 0 to 255
let b: u32 = 42;              // unsigned, so it can't go negative
let a: i32 = -42;             // signed 32-bit, the everyday default
let big: i64 = 9_000_000_000; // 64-bit, for numbers too large for i32
let i: usize = 0;             // the type for sizes and indices
let price: f64 = 19.99;       // floating point (f32 is the smaller one)

That covers most of what you'll see in the wild. Now on to the interesting bits!

No silent overflows

If you push a number past its type's maximum, most languages won't tell you. For example, C wraps around, Java wraps around, and Python quietly grows the integer to fit. Rust won't do that. In a debug build it stops and panics instead of handing back a wrong answer.

let hp: u8 = 200;
let bonus: u8 = 100;

// The next line panics in debug mode
// because we attempt to add with overflow
let total = hp + bonus; 

If that bug had been in a C program, the total would be 44 instead of 300. The player would have no idea why their health bar suddenly dropped from 200 to 44 after picking up a bonus item.

Rust catches it at the source instead and tells you. It's best to be explicit about how you want to handle overflow, and Rust gives you three options:

(Release builds wrap by default for speed. Reach for these methods to get the same behavior in both debug and release builds.)

No implicit conversions

Rust never mixes numeric types for you. u32 + i32 won't compile, and you can't multiply a u32 by an f64 either. You convert on purpose, either with an as cast that truncates, or with .into() and .try_into() when you want a checked conversion.

let count: u32 = 42;
let price: f64 = 19.99;

// We spell out the cast here. No implicit conversions. 
let total = price * count as f64; 

Text into numbers

Parsing a string can fail, because the input might not be a number at all. So parse hands back a Result.

let n: u32 = "123".parse().unwrap_or(0);

We haven't talked about Result yet, but the idea is that Rust forces you to deal with the possibility of failure instead of ignoring it. For now, .unwrap_or(0) is fine. There's a dedicated chapter on Result later.

Health that doesn't wrap around

A u8 holds 0 to 255. Add past 255 and a debug build panics, while a release build wraps to a tiny number, so a fully-buffed character could read 3 HP instead of a capped 255. Neither is what you want from a health bar.

saturating_add is the fix. It does the addition but clamps at the type's maximum instead of overflowing, so stacking buffs tops out at 255 rather than wrapping around.

Useful from the standard library

Exercise 1 of 3
Open in Web Editor

Results

    Compiler / runtime output
    
                
    Stuck? Show a hint No spoilers, just a nudge
    1. A plain current + gain would panic (debug) or wrap (release) once the sum passes 255.
    2. Look for saturating_add on the integer types: it clamps at the maximum instead of overflowing.
    Reveal the full solution Spoiler: the complete answer
    /// Adds `gain` health to `current`, capping at `u8::MAX` (255) instead of
    /// overflowing.
    ///
    /// `saturating_add` performs the addition but clamps the result at the type's
    /// maximum, so a big stack of buffs tops out at 255 rather than wrapping around
    /// to a small number (which is what a plain `+` would do in a release build).
    fn add_health(current: u8, gain: u8) -> u8 {
        current.saturating_add(gain)
    }
    
    #[test]
    fn test_add_health() {
        assert_eq!(add_health(100, 50), 150);
        assert_eq!(add_health(200, 100), 255);
        assert_eq!(add_health(255, 1), 255);
        assert_eq!(add_health(0, 0), 0);
    }
    

    `damage_with_bonus`

    Rust never converts between numeric types implicitly. If you want to multiply a u32 by an f64, one of them has to change shape first, and you have to say so. The as keyword does the conversion.

    The function takes a base damage as u32 and a bonus as an f64 percentage (so 50.0 means "+50%"; think a critical hit, an equipment buff, or any other damage modifier), and returns the final damage as a u32. You'll need to:

    1. Turn the percentage into a multiplier (divide by 100.0).
    2. Multiply it by the base, but only after casting the base to f64, because Rust won't mix the two for you.
    3. Add the bonus to the base.
    4. Cast back to u32 to return.

    That last cast truncates the fractional part toward zero, which is exactly what we want here: most games quantise to whole HP, so 8.085 damage becomes 8, not 9. The final test pins this down.

    Useful from the standard library

    • as is the cast operator. 1.7_f64 as u32 is 1 (truncation), not 2. That same truncation is what drops fractional HP in this exercise.
    • f64::round exists too, and rounds to the nearest integer. It's the right tool when you want nearest-integer rounding, but this exercise asks for truncation, so you won't need it here.
    Exercise 2 of 3
    Open in Web Editor

    Results

      Compiler / runtime output
      
                  
      Stuck? Show a hint No spoilers, just a nudge
      1. The maths is base + base * (bonus_percent / 100.0).
      2. base is u32 and bonus_percent is f64, so you can't multiply them directly. Cast base with as f64 first.
      3. To go back to u32 for the return value, use a plain as u32 cast. That truncates the fractional part (drops any fractional HP), which is what the 15.5% test pins down.
      Reveal the full solution Spoiler: the complete answer
      /// Applies a damage bonus to a base damage value.
      ///
      /// The bonus is a percentage *added on top* of the base, so
      /// `50.0` means "+50%": a base of `100` becomes `150`. Think of
      /// it as a critical-hit bonus, an equipment buff, or any other
      /// damage modifier expressed as a percentage. Fractional HP is
      /// dropped (truncated toward zero), which is what most games do;
      /// half-HP doesn't exist.
      fn damage_with_bonus(base: u32, bonus_percent: f64) -> u32 {
          let multiplier = 1.0 + bonus_percent / 100.0;
          (base as f64 * multiplier) as u32
      }
      
      #[test]
      fn test_damage_with_bonus() {
          // No bonus: damage is unchanged.
          assert_eq!(damage_with_bonus(100, 0.0), 100);
          // Classic +50% bonus (e.g. a critical hit).
          assert_eq!(damage_with_bonus(100, 50.0), 150);
          // +100% = double damage.
          assert_eq!(damage_with_bonus(80, 100.0), 160);
          // Fractional HP truncates toward zero:
          // 7 + 7 * 0.155 = 8.085 → 8, not 9.
          assert_eq!(damage_with_bonus(7, 15.5), 8);
      }
      

      Parsing strings into numbers

      str::parse is the universal "turn this text into a value of some type" method. It returns a Result, because the input might not be parseable. We don't have Result yet (it gets its own chapter later), so we'll collapse failure to 0 for now.

      Returning 0 on failure is a bad idea in real code: it silently merges "the input was the number zero" with "the input was garbage". Rust has a much better tool for this in Option and Result, each of which gets its own chapter. For now, parse().unwrap_or(0) is the shortest way to satisfy the tests.

      Note that u32 can't be negative, so "-5".parse::<u32>() will fail and we should also return 0. If you reach for i32 first, the test for "-5" may surprise you.

      Useful from the standard library

      • str::parse turns a string into any type that implements FromStr. Returns a Result because the input might not be valid.
      • Result::unwrap_or hands back the value on Ok, or the fallback you give it on Err. Useful for the "just give me a number" path here.
      Exercise 3 of 3
      Open in Web Editor

      Results

        Compiler / runtime output
        
                    
        Stuck? Show a hint No spoilers, just a nudge
        1. &str has a .parse() method that can produce many number types. You'll need a type annotation so it knows which one.
        2. .parse::<u32>() returns a Result<u32, _>. The exercise asks for 0 on failure, so reach for .unwrap_or(0).
        3. (Forward reference: in the Result chapter you'll learn why returning Result directly is the better signature.)
        Reveal the full solution Spoiler: the complete answer
        /// Parses a string into a positive integer.
        /// Returns the number if valid, 0 if invalid.
        fn parse_positive_integer(input: &str) -> u32 {
            input.parse().unwrap_or(0)
        }
        
        #[test]
        fn test_parse_positive_integer() {
            assert_eq!(parse_positive_integer("123"), 123);
            assert_eq!(parse_positive_integer("0"), 0);
            assert_eq!(parse_positive_integer("invalid"), 0);
            assert_eq!(parse_positive_integer("-5"), 0);
        }
        

        Wrapping up numbers

        You met Rust's stance on numbers: overflow is caught rather than ignored, type conversions are explicit, and parsing returns a Result you have to deal with.

        What we learned

        • Overflow isn't silent. A plain + past a type's maximum panics in debug and wraps in release. When overflow is possible, pick the behavior you want: saturating_add (clamp), checked_add (Option), or wrapping_add (opt-in wraparound).
        • Rust never mixes numeric types for you. Use as for a truncating cast, or .into() / .try_into() when you want a checked conversion.
        • as u32 on a float truncates toward zero (1.7 as u32 is 1); f64::round rounds to the nearest integer. We used truncation for the damage bonus because games drop fractional HP.
        • str::parse() is the universal text-to-value method. It returns a Result; pair it with .unwrap_or(...) until you've met Result properly in its own chapter.
        Next chapter 2Strings, &str, and chars