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!
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:
a.saturating_add(b) clamps at the maximum, so 255 stays 255.a.checked_add(b) returns None on overflow, so you can handle it yourself.a.wrapping_add(b) opts back into wraparound, for the times you actually want it.(Release builds wrap by default for speed. Reach for these methods to get the same behavior in both debug and release builds.)
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;
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.
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
u8::saturating_addreturns the sum, or the type's maximum if the true sum wouldn't fit.
current + gain would panic (debug) or wrap (release) once the sum passes 255.saturating_add on the integer types: it clamps at the maximum instead of overflowing.
/// 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);
}
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:
100.0).f64, because Rust won't mix the two for you.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
asis the cast operator.1.7_f64 as u32is1(truncation), not2. That same truncation is what drops fractional HP in this exercise.f64::roundexists 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.
base + base * (bonus_percent / 100.0).base is u32 and bonus_percent is f64, so you can't multiply them directly.
Cast base with as f64 first.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.
/// 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);
}
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::parseturns a string into any type that implementsFromStr. Returns aResultbecause the input might not be valid.Result::unwrap_orhands back the value onOk, or the fallback you give it onErr. Useful for the "just give me a number" path here.
&str has a .parse() method that can produce many number types.
You'll need a type annotation so it knows which one..parse::<u32>() returns a Result<u32, _>.
The exercise asks for 0 on failure, so reach for .unwrap_or(0).Result chapter you'll learn why returning Result directly is the better signature.)
/// 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);
}
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), orwrapping_add(opt-in wraparound).- Rust never mixes numeric types for you. Use
asfor a truncating cast, or.into()/.try_into()when you want a checked conversion.as u32on a float truncates toward zero (1.7 as u32is1);f64::roundrounds 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 aResult; pair it with.unwrap_or(...)until you've metResultproperly in its own chapter.