Variables, Types and Functions · about 40 min

Floats, Booleans, Characters and Casts

Why this matters

Measurements, money estimates, physics and graphics need fractional numbers; every decision a program makes comes down to a true-or-false value; and text is built from characters. These three scalar types each have a surprise that catches beginners: floats are not exact, && may skip its right side, and a Rust char is not a byte. Learn the surprises now and they will not cost you an afternoon later.

Floating-point numbers

Rust has two float types: f64 (8 bytes, about 15-16 significant decimal digits, the default) and f32 (4 bytes, about 7 digits, used where memory matters, as in graphics). Both follow the IEEE 754 standard that every modern CPU implements, so they behave the same as floats in C, Python or JavaScript.

fn main() {
    let x = 7.0 / 2.0;
    let y: f32 = 1.0 / 3.0;
    let z = 1.0 / 3.0;
    println!("{x} {y} {z}");
    println!("{}", 2.0_f64.sqrt());
    println!("{} {} {}", 2.7_f64.floor(), 2.5_f64.round(), (-2.7_f64).abs());
    println!("{:.3}", std::f64::consts::PI);
}
3.5 0.33333334 0.3333333333333333
1.4142135623730951
2 3 2.7
3.142

Float literals need a dot (7.0, or 7. or the suffix 7f64); 7 alone is an integer, and Rust never mixes the two automatically. Math functions are methods: x.sqrt(), x.powi(3), x.sin(), x.max(y).

What is inside an f64

An f64 is scientific notation in base 2: a sign bit, an 11-bit exponent and a 52-bit fraction (the "mantissa"):

 1 bit    11 bits         52 bits
+------+-----------+----------------------------+
| sign | exponent  |  fraction                  |    value = (-1)^sign * 1.fraction * 2^(exponent-1023)
+------+-----------+----------------------------+

With 52 bits of fraction, only numbers that are sums of powers of two (with a limited span) are exact. 0.5, 0.25 and 0.375 are exact. 0.1 is 0.000110011001100... in binary, repeating forever, so it is rounded:

fn main() {
    println!("{}", 0.1 + 0.2);
    println!("{}", 0.1 + 0.2 == 0.3);
    println!("{:.20}", 0.1);
    let close = (0.1_f64 + 0.2 - 0.3).abs() < 1e-9;
    println!("{close}");
}
0.30000000000000004
false
0.10000000000000000555
true

Rules that follow from this:

Special values

IEEE 754 has values for results that are not ordinary numbers:

fn main() {
    let inf = 1.0 / 0.0;
    let nan = 0.0_f64 / 0.0;
    println!("{inf} {} {nan}", -inf);
    println!("{}", nan == nan);
    println!("{}", nan.is_nan());
}
inf -inf NaN
false
true

Float division by zero does not panic (unlike integers); it gives infinity. NaN ("not a number") results from undefined operations and is not equal to anything, not even itself. Use is_nan() to test for it. This is also why floats cannot be sorted with the plain sort method or used as keys in some collections, which you will meet in module 9.

Booleans

bool has two values, true and false, and occupies one byte. Comparisons produce bools:

operatormeaning
== !=equal, not equal
< <= > >=ordering
&&and
||or
!not
fn main() {
    let age = 34;
    let member = true;
    let discount = age >= 65 || member;
    let adult_non_member = age >= 18 && !member;
    println!("{discount} {adult_non_member}");
    println!("{}", true as i32 + true as i32);
}
true false
2

&& and || short-circuit: if the left side already decides the answer, the right side is not evaluated at all. In b != 0 && a / b > 2, the division never runs when b is zero. Order your conditions so the cheap or protective test comes first.

Rust has no "truthiness": if 1 { ... } is a type error. Conditions must be bool, so write if count != 0.

Characters

A char is a single Unicode scalar value: any character from any script, plus symbols and emoji. It is always 4 bytes, because it holds the character's code point, a number up to 0x10FFFF.

fn main() {
    let letter = 'R';
    let accented = 'é';
    let heart = '♥';
    println!("{letter} {accented} {heart}");
    println!("{} {} {}", letter as u32, accented as u32, heart as u32);
    println!("{}", std::mem::size_of::<char>());
    println!("{} {}", letter.is_alphabetic(), '7'.to_digit(10).unwrap());
    println!("{}", 'a'.to_ascii_uppercase());
}
R é ♥
82 233 9829
4
true 7
A

Note the single quotes: 'R' is a char, "R" is a string (text of any length). This matters: in a string, Rust stores characters in UTF-8, where each takes 1 to 4 bytes, so a string is not an array of chars. Module 6 explains this properly. For now: one character, single quotes, 4 bytes, holds a Unicode code point.

Casting with as

as converts between primitive types. It always succeeds, which means it sometimes changes the value:

fn main() {
    println!("{}", 3.99_f64 as i32);
    println!("{}", -3.99_f64 as i32);
    println!("{}", 1e20_f64 as i32);
    println!("{}", 300_i32 as u8);
    println!("{}", -1_i32 as u32);
    println!("{}", 65_u8 as char);
    println!("{}", 'A' as u8);
    println!("{}", 7 as f64 / 2.0);
}
3
-3
2147483647
44
4294967295
A
65
3.5

Use as when you know the value fits or when truncation is what you want. When it might not fit, the checked conversions u8::try_from(x) and friends (module 8) are the safe choice.

Common mistakes

Mixing integers and floats:

fn main() {
    let c = 37.0;
    let f = c * 9 / 5 + 32;
    println!("{f}");
}
error[E0277]: cannot multiply `{float}` by `{integer}`
 --> main.rs:3:15
  |
3 |     let f = c * 9 / 5 + 32;
  |               ^ no implementation for `{float} * {integer}`
  |
  = help: the trait `Mul<{integer}>` is not implemented for `{float}`
  = help: the following other types implement trait `Mul<Rhs>`:
            `&f128` implements `Mul<f128>`
            `&f128` implements `Mul`
            `&f16` implements `Mul<f16>`
            `&f16` implements `Mul`
            `&f32` implements `Mul<f32>`
            `&f32` implements `Mul`
            `&f64` implements `Mul<f64>`
            `&f64` implements `Mul`
          and 57 others

Write 9.0, 5.0, 32.0, or convert integer variables with as f64.

Double quotes for a char: let c: char = "a"; is E0308, expected char, found &str.

Comparing floats with == gives false for values that "should" be equal. Use a tolerance.

Assuming as checks: big as u8 silently drops high bits. If that is not what you want, it is a bug the compiler cannot see.

Recap

Going further: print f64::EPSILON, f64::MAX and 0.1_f32 as f64, and explain the last one. Search for "What Every Computer Scientist Should Know About Floating-Point Arithmetic" for the deep version.

Exercises

Exercise 1 · quiz

Why not 0.3?

println!("{}", 0.1 + 0.2 == 0.3); prints false. Why?

Answer
  • 0.1 and 0.2 cannot be represented exactly in binary, so their sum is a tiny bit off from the stored value of 0.3

Binary fractions can only represent sums of powers of two (1/2, 1/4, 1/8 ...) exactly. 0.1 is an infinitely repeating binary fraction, like 1/3 in decimal, so it is rounded to the nearest representable f64. The rounding errors of 0.1 and 0.2 add up to something slightly different from the rounding of 0.3. Every language with IEEE 754 floats behaves this way. Compare floats with a tolerance: (a - b).abs() < 1e-9.

Exercise 2 · write code

Temperature table

Convert the Celsius temperatures -40, 0, 37 and 100 to Fahrenheit (F = C * 9 / 5 + 32) and print one line per temperature with exactly one decimal on both sides:

-40.0 C = -40.0 F
0.0 C = 32.0 F
37.0 C = 98.6 F
100.0 C = 212.0 F

Do the arithmetic in f64.

File: main.rs

Starting point

fn main() {
    let c = -40.0;
    let f = c * 9 / 5 + 32;
    println!("{c} C = {f} F");
}

Example

expected output

-40.0 C = -40.0 F
0.0 C = 32.0 F
37.0 C = 98.6 F
100.0 C = 212.0 F
Hint 1

The starter does not compile: c * 9 multiplies a float by an integer. Write 9.0.

Hint 2

{c:.1} prints with exactly one decimal. Shadowing c and f for each line is fine (functions come in a later lesson).

Reference solution
fn main() {
    let c = -40.0;
    let f = c * 9.0 / 5.0 + 32.0;
    println!("{c:.1} C = {f:.1} F");
    let c = 0.0;
    let f = c * 9.0 / 5.0 + 32.0;
    println!("{c:.1} C = {f:.1} F");
    let c = 37.0;
    let f = c * 9.0 / 5.0 + 32.0;
    println!("{c:.1} C = {f:.1} F");
    let c = 100.0;
    let f = c * 9.0 / 5.0 + 32.0;
    println!("{c:.1} C = {f:.1} F");
}

Exercise 3 · write code

Characters are numbers

Print the Unicode code point of the characters A, z and é, and the character with code 66, exactly like this:

A = 65
z = 122
é = 233
66 = B

Use as u32 to turn a char into its number, and char::from_u32(66).unwrap() (or 66u8 as char) for the reverse.

File: main.rs

Starting point

fn main() {
    let a = 'A';
}

Example

expected output

A = 65
z = 122
é = 233
66 = B
Hint 1

'A' as u32 is 65. Characters use single quotes; strings use double quotes.

Reference solution
fn main() {
    let a = 'A';
    let z = 'z';
    let e = 'é';
    println!("{a} = {}", a as u32);
    println!("{z} = {}", z as u32);
    println!("{e} = {}", e as u32);
    println!("66 = {}", char::from_u32(66).unwrap());
}

Exercise 4 · write code

Leap years with boolean logic

A year is a leap year if it is divisible by 4, except that years divisible by 100 are not, except that years divisible by 400 are. Using only arithmetic, comparison and the logical operators &&, ||, ! (no if), print whether 1900, 2000, 2024 and 2023 are leap years:

1900: false
2000: true
2024: true
2023: false

File: main.rs

Starting point

fn main() {
    let year = 1900;
    let leap = year % 4 == 0;
    println!("{year}: {leap}");
}

Example

expected output

1900: false
2000: true
2024: true
2023: false
Hint 1

Divisible by 4 and not by 100 is year % 4 == 0 && year % 100 != 0.

Hint 2

Combine that with || year % 400 == 0, with parentheses to make the grouping clear.

Reference solution
fn main() {
    let year = 1900;
    let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
    println!("{year}: {leap}");
    let year = 2000;
    let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
    println!("{year}: {leap}");
    let year = 2024;
    let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
    println!("{year}: {leap}");
    let year = 2023;
    let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
    println!("{year}: {leap}");
}

Exercise 5 · quiz

What does as do?

What does println!("{} {} {}", 2.9_f64 as i32, -2.9_f64 as i32, 300_i32 as u8); print?

Answer
  • 2 -2 44

Float to integer with as truncates toward zero (2.9 becomes 2, -2.9 becomes -2) and saturates at the type's limits. Integer to a smaller integer keeps only the low bits: 300 is 1_0010_1100 in binary, and the low 8 bits 0010_1100 are 44. as never fails and never panics, which is exactly why it can surprise you; for conversions that might not fit, prefer u8::try_from(x), which reports the problem (module 8).