Introduction

Programming usually involves problem-solving using basic building blocks. As such, an effective programmer should be fluent with the basics and know how to combine them to solve more complex problems.

To gain proficiency and to develop problem-solving skills, there's no better way than practicing. I believe the most effective way to practice is to work in a real project, ideally under a mentor who can tell good code from bad code, and who can show how they solve problems.

However, sometimes working on a real project is not possible e.g., not being hired due to the lack of experience, or the high barrier to entry with complex open-source projects. The next best approach is to solve increasingly complex challenges from books and tutorials. This definitely helps with learning the basics, building problem-solving skills up to a level, and gaining confidence as a programmer.

I have been trying to pick up the Rust programming language. This book documents my attempt at gaining Rust proficiency by following the challenges in Exercises for Programmers.12

1 Although many people suggests books such as Programming Rust after The Book, I find doing challenges less boring.

2 As you can see here, this is also me practicing writing in English.

Chapter 1: Input/Output

Hello, World!

Challenge

Ask the user for their name and greet them.

Solution

This is a variation of the classic "Hello, World" program. Instead of just printing out a fixed string, we need to read a string use it in our greeting:

What is your name?
Huynh
Hello, Huynh!
use std::io;

fn main() -> io::Result<()>{
    println!("What is your name?");

    let mut name = String::new();
    io::stdin().read_line(&mut name)?;
    let name = name.trim();

    println!("Hello, {name}!");

    Ok(())
}

An interesting Rust feature used here is variable shadowing, where the second variable name shadows the first variable of the same name. Also, note that the compiler guarantees that the second variable is not a dangling reference.

Variation 1

In this variation, no variable is allowed. Therefore, we cannot no longer use Stdin::read_line(), which writes to an existing buffer. Instead, we can use Stdin::lines(), which returns an iterator over input lines.

    println!(
        "Hello, {}!",
        io::stdin() // Get a handle to stdin
            .lines() // iterate over input lines
            .next() // get the first one
            .expect("No line entered")? // unwrap it
            .trim() // trim whitespaces
    );

Doing multiple things in one statement like this maybe a bad practice, but we did solve the challenge.

Variation 2

Now, we want different greetings for different people. The first thing we need to add is a list of greetings to choose from. Here is a short list:1

const GREETINGS: &[&str] = &[
    "Hi",
    "Hi there",
    "Hey",
    "Hello",
    "Hello, nice to meet you",
    "I've heard so much about you",
];

As there are no further requirements, we can choose among several equally acceptable approaches, for example:

  • Choose a random greeting every time.
  • Choose the greeting based on the initials.
  • Choose the greeting based on a hash of the name.

Let's go with the first approach. To make this easier, we shall depend on the rand crate to generate random numbers:

# File: Cargo.toml
[dependencies]
rand = "0.8.5"
// File: hello_v2.rs
use rand::{seq::SliceRandom, thread_rng};

And here is the code to print randomized greetings:

    let mut rng = thread_rng();
    println!("{}, {}!", GREETINGS.choose(&mut rng).unwrap(), name);
1

As a non-native English speaker, I'm always amazed by the variety of ways to say "Hello" in English. For example, this blog documents "107 interesting and different ways to say hi in English".

Counting the Number of Characters

Challenge

Create a program that reads a string, then echoes back the string together with its length.

Background

We already know how to read a string from user input from the previous challenge. The remaining question is how to calculate its length. Intuitively, the length of a string is the number of characters that it has. But what does it mean by a character?

The lengths of ASCII strings

For ASCII strings, each character is represented by a 1-byte code point. The length of a string is the same as the number of bytes used to store the string.

The lengths of UTF-8 strings

For Unicode strings, the answer is more complex. Consider the emoji 'šŸ¤¦šŸ¼ā€ā™‚ļø'. It is one graphical unit represented by 17 bytes:1

Scalar namePrinted scalarCode units
U+1F926 Face Palm🤦4
U+1F3FC Emoji Modifier Fitzpatrick Type-3šŸ¼4
U+200D Zero Width Joinernot printable3
U+2642 Male Sign♂3
U+FE0F Variation Selector-16ā—Œļø3
Total: 17

Here we are presented with several terms:

  • UTF-8 code unit: 8 bits/1 byte
  • Unicode code point: A position in the Unicode character set. In the UTF-8 encoding, each code point is represented by one to four code units.
  • Unicode scalar value: A Unicode code point except high-surrogate and low-surrogate code points. In other words, the set of scalar values is a subset of code points.
  • Extended grapheme cluster: The whole emoji is an extended grapheme cluster.

Therefore, when it comes to a UTF-8 string, we have multiple lengths measurements. For the string "šŸ¤¦šŸ¼ā€ā™‚ļø":

  • The number of bytes used to encode it: 17,
  • The number of Unicode scalar values/code points: 5,
  • The number of extended grapheme clusters: 1.

Solution

To count the number of code units and scalar values, we can use the built-in functions str::len() and str::chars(). For the number of extended grapheme clusters, we shall use the crate unic_segment instead of implementing our own segmentation algorithm.

    let num_bytes = input.len();
    let num_scalars = input.chars().count();
    let num_egc = unic_segment::Graphemes::new(&input).count();

Example output:

Enter a string:
šŸ¤¦šŸ¼ā€ā™‚ļøšŸ¤¦šŸ¼
Your string 'šŸ¤¦šŸ¼ā€ā™‚ļøšŸ¤¦šŸ¼' has:
        25 byte(s)
        7 scalar value(s)
        2 extended grapheme cluster(s).

1 More at It's Not Wrong that "šŸ¤¦šŸ¼ā€ā™‚ļø".length == 7, Henri Sivonen.

Printing Quotes

Challenge

Prompt for a quote and an author's name. Display the author's name and then the quote inside quotation marks.

Solution

The main purpose of this exercise is to create strings containing character escapes. Knowing character escapes, the solution is simple:

use std::io;

fn main() -> io::Result<()> {
    println!("What is the quote?");
    let mut inputs = io::stdin().lines();
    let quote = inputs.next().expect("No quote provided")?;

    println!("Who said it?");
    let author = inputs.next().expect("No author provided")?;

    println!("{} says, \"{}\"", author, quote);

    Ok(())
}

Sample output:

What is the quote?
51% of quotes on the Internet are fake.
Who said it?
Me
Me says, "51% of quotes on the Internet are fake."

Of course, there are other escape sequences, for instance:

#![allow(unused)]
fn main() {
println!("\"\nI\u{2764}\u{FE0F}\u{1F1FB}\u{1F1F3}\r\"")
}

This code snippet prints

"
Iā¤ļøšŸ‡»šŸ‡³
"

Mad Lib

Challenge

Generate random stories from tuples of (noun, verb, adjective, adverb).

Solution

To generate stories, we can start with a template and fill in the blanks. For instance, when provided with the template

template = "Do you {verb} your {adjective} {noun} {adverb}? That's hilarious!";

and the values noun="dog", verb="walk", adjective="blue", adverb="quickly", the resulting story is

Do you walk your blue dog quickly? That's hilarious!

The missing pieces are the lists of words and a method for selecting words from them. Here are my lists of not-so-funny-but-safe words:

const NOUNS: &[&str] = &["dog", "cat", "elephant", "goldfish", "python"];
const VERBS: &[&str] = &["walk", "touch", "kiss", "run", "feed"];
const ADJECTIVES: &[&str] = &["blue", "hot", "cool", "soft", "hard", "dry", "wet"];
const ADVERBS: &[&str] = &["quickly", "lazily", "happily", "hungrily"];

Below is a function for drawing a random word from a list. Notice the parameter 'a indicating the lifetime of the borrowed return value.

fn random_choice<'a>(choices: &[&'a str], rng: &mut ThreadRng) -> &'a str {
    choices.choose(rng).unwrap()
}

With all the elements in place, we can wire up the final solution:

fn main() {
    let mut rng = rand::thread_rng();
    let noun = random_choice(NOUNS, &mut rng);
    let verb = random_choice(VERBS, &mut rng);
    let adjective = random_choice(ADJECTIVES, &mut rng);
    let adverb = random_choice(ADVERBS, &mut rng);

    println!(
        "Do you {} your {} {} {}? That's hilarious!",
        verb, adjective, noun, adverb
    );
}

Some sample outputs:

Do you touch your wet elephant quickly? That's hilarious!
Do you feed your cool cat hungrily? That's hilarious!
Do you walk your soft goldfish quickly? That's hilarious!

With the presented word lists, how many distinct stories are there? According to the rule of product, the answer is

$$ |nouns| \times |verbs| \times |adjectives| \times |adverbs| = 5 \times 5 \times 7 \times 4
= 700 $$

where |S| denotes the number of elements of in a set S.

Simple Math

Challenge

Read two numbers from stdin and compute their sum, difference, product, and quotient.

Solution

In previous challenges, we have learned how to read strings from stdin. Here we make a function for that:

fn read_line() -> Option<String> {
    let mut input = String::new();
    io::stdin().read_line(&mut input).ok()?;
    Some(input.trim().to_string())
}

The next step is to convert input strings into integers. Luckily that can be done with the built-in function str::parse().

    let num1: i32 = read_line()
        .expect("Cannot read line 1")
        .parse()
        .expect("Cannot parse the first line");

The second number can be parsed similarly.

The final step -- computing the two numbers' sum, difference, product, and quotients -- is straightforward with built-in operators:

    println!("{} + {} = {}", num1, num2, num1 + num2);
    println!("{} - {} = {}", num1, num2, num1 - num2);
    println!("{} * {} = {}", num1, num2, num1 * num2);
    println!("{} / {} = {}", num1, num2, num1 / num2);

Retirement Calculator

Challenge

Given the number of years that a user need to work, calculate their retirement year.

Solution

The main point of this challenge is to learn to work with date/time. In particular, we need to get the current year.

Unfortunately, the standard library does not provide such a function. We can use the chrono crate instead:

cargo add chrono

And now getting the current (UTC) year is simple:

#![allow(unused)]
fn main() {
use chrono::{prelude::Utc, Datelike};
let current_utc_year = Utc::now().year();
}

The crate also supports local time:

use chrono::{prelude::Datelike, Local};
let current_local_year = Local::now().year();

Area of a Rectangular Room

Read the length and width in feet of a rectangle room. Print the area of the room in both square feet and square meters.

This is a simple exercise to practice calculation.

The area in square feet is simply the product of the sizes in feet length_ft, width_ft. To calculate the area in square meters, we can first convert the width and length to meter then multiply them.

    let area_ft2 = length_ft * width_ft;

    let length_m = length_ft * FT_TO_M;
    let width_m = width_ft * FT_TO_M;
    let area_m2 = length_m * width_m;

where FT_TO_M is a constant conversion factor:

const FT_TO_M: f64 = 0.3048;

Pizza Party

Given (number of pizzas, number of slices per pizza, number of people), divide the pizza slices among the people

This is a simple program using remainder and quotient operations.

#![allow(unused)]
fn main() {
    let ntotal_slices = nslices_per_pizza * npizzas;
    let slices_per_person = ntotal_slices / npeople;
    let remainder = ntotal_slices % npeople;
}

The function below handles the pluralization of the word "piece":

#![allow(unused)]
fn main() {
fn print_pieces(num: u64) -> String {
    if num > 1 {
        format!("{} pieces", num)
    } else {
        format!("{} piece", num)
    }
}
}

Paint Calculator

Given the size of a room and the amount of paint needed to paint a square feet of ceiling, calculate the total amount of paint needed. You can only buy a whole number of gallons of paint.

In this exercise, we need to perform rounding up a number instead of rounding down. The calculation itself is easy:

#![allow(unused)]
fn main() {
    let area_ft2 = length_ft * width_ft;
    let required_paint: u64 = (area_ft2 / paint_per_ft2).ceil() as u64;
}

The remaining part is to print the result:

#![allow(unused)]
fn main() {
    println!(
        "You will need to purchase {} {} of paint to cover {} square {}.",
        required_paint,
        find_noun_form(required_paint, "gallons"),
        area_ft2,
        find_noun_form(area_ft2, "foot")
    );
}

And here are the functions to pluralize a word:

#![allow(unused)]
fn main() {
fn pluralize(word: &str) -> String {
    match word {
        "foot" => "feet".to_string(),
        _ => word.to_owned() + "s",
    }
}
}
#![allow(unused)]
fn main() {
trait HasOne {
    const ONE: Self;
}

impl HasOne for u64 {
    const ONE: u64 = 1;
}

impl HasOne for f64 {
    const ONE: f64 = 1.0;
}

fn find_noun_form<T>(count: T, word: &str) -> String
where
    T: HasOne + std::cmp::PartialOrd,
{
    if count > T::ONE {
        pluralize(word)
    } else {
        word.to_string()
    }
}
}

Notice how we use HasOne trait to write a generic find_noun_form function that accepts both u64 and f64 as the first parameter. In addition, we use associated constant feature to declare the ONE constant for different number types. In practice, we could use the num crate instead.

Self-Checkout

Create a simple self-checkout system. Prompt for the prices and quantities of several items. Calculate the subtotal, tax, and total using a tax rate of 5.5%.

This is a simple exercise. The main challenge is to decide when to stop reading user input. Here we use match clause to check for invalid input and stop:

    loop {
        println!("Enter the price of item {}", count + 1);
        let price = match read_value::<f64>(&mut stdin()) {
            Ok(price) => price,
            _ => break,
        };

        println!("Enter the quantity of item {}", count + 1);
        let quantity = match read_value::<f64>(&mut stdin()) {
            Ok(q) => q,
            _ => break,
        };

        subtotal += price * quantity;
        count += 1;
    }

Currency Conversion

Convert from Euros to US dollars. Ensure that fractions of a cent are rounded up to the next penny.

The calculation is simple. To ensure that fractions are rounded up, we can use the function f64::ceil.

    let dollars = (euros * exchange_rate).ceil() / 100.0;

Simple Interest

Create a program calculating simple interest.

The formula for simple interest is:

$$ A = P(1 + rt) $$

where $$P$$ is the principal amount, $$r$$ is the annual rate of interest, and

$$t$$

is the number of years the amount is invested.

The program is a simple translation from the formula to code.

Compound Interest

Write a program to compute the value of an investment compounded over time.

The formula for compound interest is:

A = P (1 + r/n) ^ (nt)

where:

  • P is the principal amount,
  • r is the rate per period,
  • t is the number of years invested,
  • n is the number of times the interest is compounded per year,
  • A is the amount at the end of the investment.

The program is a simple translation from the formula to code.

Chapter 3: Making Decisions

Tax Calculator

If the state is "WI", then each order is taxed 5.5%. Otherwise, there is no tax. Write a program to calculate the total price, given the order amount and the state name.

This exercise can be solved with an if statement:

    if state == "WI" {
        const WI_TAX_RATE: f64 = 0.055;
        let tax = WI_TAX_RATE * amount;
        let total = tax + amount;
        println!("The subtotal is ${amount:.2}.");
        println!("The tax is ${tax:.2}.");
        println!("The total is ${total:.2}.");
    } else {
        println!("The total is ${amount:2}.");
    }

Password Validation

Write a simple program checking user inputs against a secret string.

The main logic checking user input is simple:

    if password == PASSWORD {
        println!("Welcome!");
    } else {
        println!("I don't know you.");
    }

It is a common practice to not show the user's password when reading it. To do so, we can use the crate rpassword:

    let password = rpassword::read_password().expect("Failed to read the password.");

Legal Driving Age

Check if the user is of legal driving age (16 or more).

use std::io::stdin;
use utils::read_value;

const LEGAL_DRIVING_AGE: i32 = 16;
fn main() {
    println!("What is your age?");
    let age: i32 = read_value(&mut stdin()).expect("Cannot read age");
    if age >= LEGAL_DRIVING_AGE {
        println!("You are old enough to legally drive");
    } else {
        println!("You are not old enough to legally drive");
    }
}

Blood Alcohol Calculator

Calculate the blood alcohol content (BAC) based on 5 inputs:

  • body weight,
  • gender,
  • number of drinks,
  • amount of alcohol by volume of the drink consumed, and
  • amount of time since the last drink.

Finally, decide if it's legal to drive by compare the BAC with a fixed threshold (0.08).

The first step in solving this exercise is to read the input. We already have code to read numbers, so the only new thing is to read the gender. We can read and store the gender as a string, but it's better to have a dedicated data type and early validation. Here we use an enum for gender and implement FromStr trait to parse it:

enum Gender {
    Male,
    Female,
}

struct ParseGenderError {}

impl FromStr for Gender {
    type Err = ParseGenderError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.trim().to_uppercase().as_str() {
            "M" | "MALE" => Ok(Gender::Male),
            "F" | "FEMALE" => Ok(Gender::Female),
            _ => Err(ParseGenderError {}),
        }
    }
}

The code above features the FromStr trait and matching on multiple alternatives using a vertical bar |.

After reading the input, it is straightforward to calculate the BAC.

    let alcohol_distribution_ratio = match gender {
        Gender::Male => 0.73,
        Gender::Female => 0.66,
    };

    let bac = alcohol_amount * 5.14 / (body_weight * alcohol_distribution_ratio) - 0.015 * hours;

Temperature Converter

Convert temperatures between Celsius and Fahrenheit.

$$C = (F āˆ’ 32) \times 5 / 9$$ $$F = (C \times 9 / 5) + 32$$

The tools so far are sufficient to solve this exercise. As something extra, we implement the Display trait for TemperatureScale enum to output it as strings:

enum TemperatureScale {
    Celsius,
    Farenheit,
}

impl Display for TemperatureScale {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TemperatureScale::Celsius => write!(f, "Celsius"),
            TemperatureScale::Farenheit => write!(f, "Farenheit"),
        }
    }
}