Chapter 1

Numbers in Rust

πŸ‘‹ Anyone can read and edit this exercise. Sign up to save your progress.

Rust's numeric types are not special, so a quick overview is enough before focusing 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)

Those are the types you'll see most often.

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 silently increases the capacity of its integers. Rust won't do that. Instead (in a debug build) it stops and panics instead of handing back a wrong answer.

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

// Panics in debug mode because this would overflow
let total = hp + bonus; 

If that had been in a C program, the total would now be 44 instead of 300. Imagine you play Diablo and pick up a bonus item and your health bar suddenly drops from 200 to 44. That would be weird.

Rust catches the overflow where it happens instead. When a result does not fit, you must choose the behavior:

Which variant you choose depends on your business logic. For example, if you expect the health bar to never exceed 255, saturating_add is the right choice.

Release builds wrap by default for speed.

Pro tip: the above methods are a great way to align the behavior of debug and release builds; I like to be explicit about overflow handling rather than relying on the default behavior of 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;

// No implicit conversions! 
// We have to spell out the cast here. 
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);

parse returns a Result. We will talk about Result later, but the main point is that it turns a potential parsing problem into an explicit value that we can handle instead of ignoring it by accident.

Knowing that you can call .unwrap_or(fallback_value_if_the_parsing_failed) on a Result is enough for this exercise.

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 be converted first. The as keyword is probably the simplest way to do the conversion.

    The function takes base damage as a u32 and an f64 bonus percentage, then returns the final damage as a u32. A bonus of 50.0 means adding half of the base damage again, whether it came from a critical hit, equipment, or some other modifier. Keep the calculation in f64 so the fractional percentage is not lost, then convert the final damage back to u32.

    Converting the final value back to u32 truncates the fractional part toward zero. That matches games which use whole HP, so 8.085 damage becomes 8, not 9. The final test checks that we truncate rather than round.

    Useful from the standard library

    • as is the cast operator. 1.7_f64 as u32 is 1, not 2, because the cast truncates.
    • f64::round rounds to the nearest integer instead. This exercise asks for truncation, so compare the two behaviors before choosing.
    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 turns text into the type you ask for. It returns a Result because not every input can, in fact, become the correct type you asked for.

      Returning 0 on failure is a bad idea in real code because it makes valid input "0" indistinguishable from garbage. In Rust, Option and Result preserve the distinction between valid zero and invalid input. For scenarios, where the absence of a value is an error, Result is the right choice. In our case, we have a problem if we try to parse a string that isn't a number. What should be the fallback value? For this simple case, we will use 0 as the fallback.

      Note that u32 can't be negative, so "-5".parse::<u32>() will fail and we should also return 0.

      Useful from the standard library

      • str::parse turns a string into a type you choose. 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).
        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've just acquainted with 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. If you try to add two numbers and the result doesn't fit into the type, Rust will panic in debug builds and wrap in release builds. When overflow is possible, pick the behavior you want: saturating_add (clamp), checked_add (returning an Option), or wrapping_add (wrap around is okay).
        • Rust never mixes numeric types for you. The types must align, and if they don't, you must convert them explicitly. For quick conversions, 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. (For example, we used truncation for the health damage example.)
        • If you first need to convert from a different type into a number type, there are helper functions like str::parse(), which turns your text into a value of a requested type. It returns a Result, and .unwrap_or(...) supplies a fallback when parsing fails.
        Next chapter 2Strings, &str, and chars