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.
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:
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.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.
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;
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.
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 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
asis the cast operator.1.7_f64 as u32is1, not2, because the cast truncates.f64::roundrounds to the nearest integer instead. This exercise asks for truncation, so compare the two behaviors before choosing.
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 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::parseturns a string into a type you choose. 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).
/// 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'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 anOption), orwrapping_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
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. (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 aResult, and.unwrap_or(...)supplies a fallback when parsing fails.