Practical Programming David Bouchet
Practical Work #6

Rust: Strings

Submission

Due Date

By Friday 5 April 2019 23:42

Directory Hierarchy

Create your git repository (replace john.smith by your own login).

                
                    $ git clone git@git.cri.epita.net:p/2022-s4-tp/tp06-john.smith
                
            

It must contain the following files and directories:

Do not submit any executable or binary files. You have to delete all the target directories.

The AUTHORS file must contain the following information.

AUTHORS
                
                    First Name
                    Family Name
                    Login
                    Email Address
                
            

The last character of your AUTHORS file must be a newline character.

For instance:

AUTHORS
                
                    $ cat AUTHORS 
                    John
                    Smith
                    john.smith
                    john.smith@epita.fr
                    $ # Command prompt ready for the next command...
                
            

Each time you connect to one of the school's computers, you have to set the default configuration of Rust's tools.

                
                    $ rustup default stable
                
            

Be careful, if you do not follow all the given instructions, no point will be given to your answers.

Introduction

The purpose of this practical is to familiarize yourself with Rust's basic string manipulation. That is, how to iterate over a string and access its characters. At the same time, you will learn how to deal with command-line arguments and how to use simple iterators.

First, you should know that in Rust, there are two different types used to encode strings: String and &str.

The String type is used to encode dynamic strings. The contents of a dynamic string can be modified at runtime. We can change, add or remove any characters.

The &str type is used to encode string slices. The contents of a string slice cannot be modified at runtime. We cannot change, add or remove any characters. This type is commonly used to encode string literals as well as immutable references of a String type.

Reversing Strings

Introduction

Let us start with a very simple exercise. We want to invert all the characters of a string. There are different ways to do so, but let us take advantage of this exercise to get the hang of iterator principles.

String Iterators

To iterate over a string, we can use a for loop with a string iterator. To get a string iterator, we can use the chars() method.

So, try the example below. You can execute it by using the Rust Playground. Just click on the 'RUN' link to open the Rust Playground.

RUN
                
                    fn main()
                    {
                        let s = "Hello, world!";

                        for c in s.chars()
                        {
                            dbg!(c);
                        }
                    }
                
            

Here is the output of the above program.

                
                    [src/main.rs:7] c = 'H'
                    [src/main.rs:7] c = 'e'
                    [src/main.rs:7] c = 'l'
                    [src/main.rs:7] c = 'l'
                    [src/main.rs:7] c = 'o'
                    [src/main.rs:7] c = ','
                    [src/main.rs:7] c = ' '
                    [src/main.rs:7] c = 'w'
                    [src/main.rs:7] c = 'o'
                    [src/main.rs:7] c = 'r'
                    [src/main.rs:7] c = 'l'
                    [src/main.rs:7] c = 'd'
                    [src/main.rs:7] c = '!'
                
            

Chaining Iterators

The chars() method returns an iterator. Also, some iterators can call the rev() method, which returns a new iterator. This new iterator reverses the direction of the previous iterator. In other words, the first item of the new iterator is the last item of the previous iterator (and vice versa).

A function that turns an iterator into another iterator is called iterator adapter.

Therefore, the rev() method is an iterator adapter. Try to apply this iterator to the one returned by the chars() method.

RUN
                
                    fn main()
                    {
                        let s = "Hello, world!";

                        for c in s.chars().rev()
                        {
                            dbg!(c);
                        }
                    }
                
            

The output is as follows:

                
                    [src/main.rs:7] c = '!'
                    [src/main.rs:7] c = 'd'
                    [src/main.rs:7] c = 'l'
                    [src/main.rs:7] c = 'r'
                    [src/main.rs:7] c = 'o'
                    [src/main.rs:7] c = 'w'
                    [src/main.rs:7] c = ' '
                    [src/main.rs:7] c = ','
                    [src/main.rs:7] c = 'o'
                    [src/main.rs:7] c = 'l'
                    [src/main.rs:7] c = 'l'
                    [src/main.rs:7] c = 'e'
                    [src/main.rs:7] c = 'H'
                
            

As you can see, the characters are printed in the opposite order.

Turning Iterators into Collections

So, now that we can iterate over the characters of a string in the opposite order, we can append each character to a new string, so that we obtain a reversed string.

RUN
                
                    fn main()
                    {
                        let s = "Hello, world!";

                        let mut r = String::new();

                        for c in s.chars().rev()
                        {
                            r.push(c);
                        }

                        dbg!(r);
                    }
                
            

Here is the output:

                
                    [src/bin/for.rs:12] r = "!dlrow ,olleH"
                
            

It works. We have reversed a string. But usually, in Rust we use another approach to change a string iterator into a string. We use the collect() method. This method is really powerful. It can turn any iterator into a specific collection (and a string can be seen as a collection of characters).

So in our case, we can use this method to convert the iterator returned by rev() into a string of characters without a for loop.

A function that gets an iterator and does not return an iterator is called iterator consumer.

Therefore, the collect() method is an iterator consumer.

Create a new project:

                
                    cargo new reverse --vcs none
                    cd reverse
                
            

Modify the default main.rs file with the contents below and complete the test_reverse() and reverse() functions. You have to use the collect() method (do not use a for loop).

Remember that the best practice is to write the test function (i.e. test_reverse()) before the function we want to test (i.e. reverse()).

main.rs
                
                    fn main()
                    {
                        let s = reverse("Hello, world!");
                        dbg!(s);
                    }

                    fn reverse(s: &str) -> String
                    {
                        // TODO
                    }

                    #[cfg(test)]
                    mod tests
                    {
                        use super::*;

                        #[test]
                        fn test_reverse()
                        {
                            // TODO
                        }
                    }
                
            

Test your function with cargo test and run it. The expected result is as follows.

                
                    [src/main.rs:4] s = "!dlrow ,olleH"
                
            

When you are done, clean your directory.

Scrabble Score

Introduction

In this section, you will write a program that prints the score of a word in a Scrabble-like game.

The rules are quite simple. Each letter has a score (i.e. a number of points).

Your program will take three arguments:

The --double and --triple options can be enabled at the same time. The score of the word is then multiplied by six.

Here are some examples of the expected result:

                
                    $ cargo -q run -- world
                    w: 4
                    o: 1
                    r: 1
                    l: 1
                    d: 2
                    Score = 9
                    $ cargo -q run -- -d world
                    w: 4
                    o: 1
                    r: 1
                    l: 1
                    d: 2
                    x2
                    Score = 18
                    $ cargo -q run -- --triple world
                    w: 4
                    o: 1
                    r: 1
                    l: 1
                    d: 2
                    x3
                    Score = 27
                    $ cargo -q run -- -dt world
                    w: 4
                    o: 1
                    r: 1
                    l: 1
                    d: 2
                    x2
                    x3
                    Score = 54
                
            

Create a new project:

                
                    cargo new scrabble --vcs none
                    cd scrabble
                
            

Command-Line Arguments

Getting Command-Line Arguments

The first thing to do is to get the command-line arguments passed to your program.

To do so, you can use the following iterator:

                
                   std::env::args()
                
            

This iterator allows you to iterate over the command-line arguments passed to your program. Replace the default main.rs file by the following one:

main.rs
                
                    fn main()
                    {
                        for arg in std::env::args()
                        {
                            dbg!(arg);
                        }
                    }
                
            

And test it with the following commands:

                
                    $ cargo -q run
                    [src/main.rs:5] arg = "target/debug/scrabble"
                    $ cargo -q run -- hello world -o --opt
                    [src/main.rs:5] arg = "target/debug/scrabble"
                    [src/main.rs:5] arg = "hello"
                    [src/main.rs:5] arg = "world"
                    [src/main.rs:5] arg = "-o"
                    [src/main.rs:5] arg = "--opt"
                
            

As you can see, the first item of the iterator is not really an argument: it is the path of the executable file.

The arg() iterator is defined in the env module. The env module is defined in the std crate. We have not looked at the module yet. So for now, just consider that it is a convenient way to organize your code. But the problem with this organization is that, sometimes, the full name of a function can be long. To solve this problem, Rust has the use keyword.

For instance, the following code is equivalent to the previous one.

                
                    use std::env;

                    fn main()
                    {
                        for arg in env::args()
                        {
                            dbg!(arg);
                        }
                    }
                
            

The use keyword in the above example brings the env name into scope.

Or, if you are sure that there are no other args() functions in your code, you can also bring the args name into scope.

                
                    use std::env::args;

                    fn main()
                    {
                        for arg in args()
                        {
                            dbg!(arg);
                        }
                    }
                
            

Parsing Command-Line Arguments

Now, we have to parse the command-line arguments. That is, we are going to store the word to score and the options in the following structure:

                
                    struct Arg
                    {
                        double: bool,
                        triple: bool,
                        word: String,
                    }
                
            

We have not looked at the structures yet. Rust's structures are much more powerful than C's, but for this first approach, we will use a structure in the same way as we would do in C.

This structure has three fields:

Replace the previous main.rs file by the following one:

main.rs
                
                    use std::env;
                    use std::process;
                    
                    #[derive(Debug)]
                    struct Arg
                    {
                        double: bool,
                        triple: bool,
                        word: String,
                    }
                    
                    fn main()
                    {
                        let arg = get_arg();
                        dbg!(arg);
                    }
                    
                    fn get_arg() -> Arg
                    {
                        let mut arg = Arg
                        {
                            double: false,
                            triple: false,
                            word: String::new(),
                        };

                        // TODO
                    }
                
            

Ignore the following instruction for the time being: it is used for display purposes only.

                
                    #[derive(Debug)]
                
            

In the get_arg() function, we first create an Arg structure. By default, we disable the double and triple option and initialize the word field to an empty string.

Complete the get_arg() function so that it changes the arg structure according to the arguments passed to your program. Then, return this structure. To simplify the error management, you will not have to handle all error cases. Just check that the word to score is not missing. If it is, you will print an error message on the standard error.

Follow these instructions:

Finally, test your program with the following commands. It should print exactly the same outputs.

                
                    $ cargo -q run
                    The word to score is missing.
                    $ cargo -q run -- --double -t
                    The word to score is missing.
                    $ cargo -q run -- hello
                    [src/main.rs:15] arg = Arg {
                        double: false,
                        triple: false,
                        word: "hello"
                    }
                    $ cargo -q run -- -d world
                    [src/main.rs:15] arg = Arg {
                        double: true,
                        triple: false,
                        word: "world"
                    }
                    $ cargo -q run -- -d rust --triple
                    [src/main.rs:15] arg = Arg {
                        double: true,
                        triple: true,
                        word: "rust"
                    }
                    $ cargo -q run -- -t rust
                    [src/main.rs:15] arg = Arg {
                        double: false,
                        triple: true,
                        word: "rust"
                    }
                    $ cargo -q run -- -dt rust
                    [src/main.rs:15] arg = Arg {
                        double: true,
                        triple: true,
                        word: "rust"
                    }
                    $ cargo -q run -- --invalid-option rust
                    [src/main.rs:15] arg = Arg {
                        double: false,
                        triple: false,
                        word: "rust"
                    }
                
            

The Main Function

Now, modify the main function so that it prints exactly the following outputs on the terminal. You are free to use your own method.

                
                    $ cargo run -q -- theory
                    t: 1
                    h: 4
                    e: 1
                    o: 1
                    r: 1
                    y: 4
                    Score = 12
                    $ cargo run -q -- --double theory
                    t: 1
                    h: 4
                    e: 1
                    o: 1
                    r: 1
                    y: 4
                    x2
                    Score = 24
                    $ cargo run -q -- -t theory
                    t: 1
                    h: 4
                    e: 1
                    o: 1
                    r: 1
                    y: 4
                    x3
                    Score = 36
                    $ cargo run -q -- -dt theory
                    t: 1
                    h: 4
                    e: 1
                    o: 1
                    r: 1
                    y: 4
                    x2
                    x3
                    Score = 72
                    $ cargo run -q -- abcdefghijklmnopqrstuvwxyz
                    a: 1
                    b: 3
                    c: 3
                    d: 2
                    e: 1
                    f: 4
                    g: 2
                    h: 4
                    i: 1
                    j: 8
                    k: 5
                    l: 1
                    m: 3
                    n: 1
                    o: 1
                    p: 3
                    q: 10
                    r: 1
                    s: 1
                    t: 1
                    u: 1
                    v: 4
                    w: 4
                    x: 8
                    y: 4
                    z: 10
                    Score = 87
                
            

Finally, save your code in another crate:

                
                    $ mkdir src/bin
                    $ cp src/main.rs src/bin/simple_cli.rs
                
            

Now, your package contains two crates:

From now on, do not modify the simple_cli crate. At the present time, these two crates are identical. But we are going to modify the main crate in the next section.

Since there are two crates in the package, we have to specify the crate name in the cargo run command.

For instance:

                
                    $ cargo run -q --bin scrabble -- -dt theory
                    t: 1
                    h: 4
                    e: 1
                    o: 1
                    r: 1
                    y: 4
                    x2
                    x3
                    Score = 72
                
            

In order to avoid typing such a long and cumbersome command, we are going to create the following alias:

                
                    $ alias scrabble='cargo run -q --bin scrabble --'
                
            

So, the previous command becomes:

                
                    $ scrabble -dt theory
                    t: 1
                    h: 4
                    e: 1
                    o: 1
                    r: 1
                    y: 4
                    x2
                    x3
                    Score = 72
                
            

We will use this alias in the next section.

Note that it is useless to create aliases for cargo check and cargo build. By default, these commands check and build all the crates of the package respectively.

Improvement

In this section, we are going to improve the management of the command-line arguments. The way it is handled in the previous section is far from perfect:

The problem is that parsing command-line arguments, managing errors and providing help require many lines of code. Our case is simple, but when you have quite a lot of options, it can take a lot of time to code that properly.

That is the reason why we are going to use an external library crate that will help us to manage command-line arguments.

This library can be found on crates.io. It is the StructOpt library.

To use this external crate with your own crate, you first have to specify the name of the package and its version in your Cargo.toml file as shown below (line 8).

Cargo.toml
                
                    [package]
                    name = "scrabble"
                    version = "0.1.0"
                    authors = ["John Smith<john.smith@debug-pro.com>"]
                    edition = "2018"
                    
                    [dependencies]
                    structopt = "0.2"
                
            

Next time you build your project, Cargo will load and compile the structop crate.

                
                    $ cargo build
                        Updating crates.io index
                      Downloaded syn v0.15.27
                       Compiling proc-macro2 v0.4.27
                       Compiling libc v0.2.49
                       Compiling unicode-xid v0.1.0
                       Compiling unicode-width v0.1.5
                       Compiling unicode-segmentation v1.2.1
                       Compiling bitflags v1.0.4
                       Compiling strsim v0.7.0
                       Compiling vec_map v0.8.1
                       Compiling ansi_term v0.11.0
                       Compiling textwrap v0.10.0
                       Compiling heck v0.3.1
                       Compiling quote v0.6.11
                       Compiling atty v0.2.11
                       Compiling syn v0.15.27
                       Compiling clap v2.32.0
                       Compiling structopt-derive v0.2.14
                       Compiling structopt v0.2.14
                       Compiling scrabble v0.1.0 (pw_06_rust_strings/scrabble)
                        Finished dev [unoptimized + debuginfo] target(s) in 12.15s
                
            

Replace the contents of your current src/main.rs file with the following one:

main.rs
                
                    use structopt::StructOpt;
                    
                    #[derive(Debug, StructOpt)]
                    #[structopt(name = "Scrabble Score",
                        about = "Find the score of a word for a Scrabble-like game.")]
                    struct Arg
                    {
                        /// Mutiplies the score by two.
                        #[structopt(short, long)]
                        double: bool,
                    
                        /// Mutiplies the score by three.
                        #[structopt(short, long)]
                        triple: bool,
                    
                        /// Word to score.
                        word: String,
                    }
                    
                    fn main()
                    {
                        let arg = Arg::from_args();
                        dbg!(arg);
                    }
                
            

When looking at this code, we can see that only attributes and comments have been added, but not any lines of code.

Attributes start with the '#' characters. Actually, attributes are used to call special macros named procedural macros. However, our purpose, here, is not to understand macros, which happen to be complicated, but rather to understand how we can use attributes, which should not be difficult.

Compile this code and test it with the following commands:

                
                    $ scrabble hello
                    [src/main.rs:23] arg = Arg {
                        double: false,
                        triple: false,
                        word: "hello"
                    }
                    $ scrabble -d world
                    [src/main.rs:23] arg = Arg {
                        double: true,
                        triple: false,
                        word: "world"
                    }
                    $ scrabble -d rust --triple
                    [src/main.rs:23] arg = Arg {
                        double: true,
                        triple: true,
                        word: "rust"
                    }
                    $ scrabble -t rust
                    [src/main.rs:23] arg = Arg {
                        double: false,
                        triple: true,
                        word: "rust"
                    }
                    $ scrabble -dt rust
                    [src/main.rs:23] arg = Arg {
                        double: true,
                        triple: true,
                        word: "rust"
                    }
                
            

When the options are valid, it works the same way as our previous get_arg() function.

But now, let us try with some invalid options.

                
                    $ scrabble 
                    error: The following required arguments were not provided:
                        <word>
                    
                    USAGE:
                        scrabble [FLAGS] <word>
                    
                    For more information try --help
                    $ scrabble -o hello
                    error: Found argument '-o' which wasn't expected, or isn't valid in this context
                    
                    USAGE:
                        scrabble [FLAGS] <word>
                    
                    For more information try --help
                
            

As we can see, StructOpt has generated error messages, which not only give information about the error but also about our program usage. It is also said that we can try the --help option. Strange, because we did not program any help option. So let us try.

                
                    $ scrabble --help
                    Scrabble Score 0.1.0
                    John Smith <john.smith@debug-pro.com>
                    Find the score of a word for a Scrabble-like game.
                    
                    USAGE:
                        scrabble [FLAGS] <word>
                    
                    FLAGS:
                        -d, --double     Multiplies the score by two
                        -h, --help       Prints help information
                        -t, --triple     Multiplies the score by three
                        -V, --version    Prints version information
                    
                    ARGS:
                        <word>    Word to score.
                
            

Great! StructOpt has also generated a help option for us.

Let us conclude that by using StructOpt, we can parse the command-line arguments in a better way than we have before (without any lines of code). Actually, we have seen only a few number of features that StructOpt provides.

Do not forget to clean your directory.

Color

Introduction

In this section, you will write a program that prints values for the color CSS property. For instance, these three forms are possible for the red color:

For further details, see Web colors.

Your program will take three arguments:

According to the name of a color, your program will always print the hexadecimal format. If the decimal option is enabled, it will also print the RGB format in decimal. If the percent option is enabled, it will also print the RGB format in percent. The decimal and percent options can be enabled at the same time.

Here are some examples of the expected result:

                
                    $ cargo run -q -- blue
                    blue: #0000FF
                    $ cargo run -q -- white --decimal
                    white: #FFFFFF; RGB(255, 255, 255)
                    $ cargo run -q -- silver -dp
                    silver: #C0C0C0; RGB(192, 192, 192); RGB(75%, 75%, 75%)
                
            

Create a new project:

                
                    cargo new color --vcs none
                    cd color
                
            

Provided Code

The main.rs File

main.rs
                
                    use structopt::StructOpt;
                    
                    #[derive(Debug, StructOpt)]
                    // TODO: Set the 'name' and 'about' values of the structopt attribute.
                    struct Arg
                    {
                        // TODO
                    }
                    
                    fn main()
                    {
                        let arg = Arg::from_args();
                        dbg!(arg);
                    }
                    
                    fn get_hex(name: &str) -> String
                    {
                        unimplemented!();
                    }
                    
                    fn get_dec(hex: &str) -> String
                    {
                        unimplemented!();
                    }
                    
                    fn get_per(hex: &str) -> String
                    {
                        unimplemented!();
                    }
                    
                    fn to_per(c: u8) -> u8
                    {
                        unimplemented!();
                    }
                    
                    #[cfg(test)]
                    mod tests
                    {
                        use super::*;
                    
                        #[test]
                        fn test_get_hex()
                        {
                            assert_eq!(get_hex("black"), "#000000");
                            assert_eq!(get_hex("navy"), "#000080");
                            assert_eq!(get_hex("green"), "#008000");
                            assert_eq!(get_hex("teal"), "#008080");
                            assert_eq!(get_hex("maroon"), "#800000");
                            assert_eq!(get_hex("purple"), "#800080");
                            assert_eq!(get_hex("olive"), "#808000");
                            assert_eq!(get_hex("silver"), "#C0C0C0");
                            assert_eq!(get_hex("gray"), "#808080");
                            assert_eq!(get_hex("blue"), "#0000FF");
                            assert_eq!(get_hex("lime"), "#00FF00");
                            assert_eq!(get_hex("aqua"), "#00FFFF");
                            assert_eq!(get_hex("red"), "#FF0000");
                            assert_eq!(get_hex("fuchsia"), "#FF00FF");
                            assert_eq!(get_hex("yellow"), "#FFFF00");
                            assert_eq!(get_hex("white"), "#FFFFFF");
                            assert_eq!(get_hex("no_color"), "");
                        }
                    
                        #[test]
                        fn test_get_dec()
                        {
                            assert_eq!(get_dec("#000000"), "RGB(0, 0, 0)");
                            assert_eq!(get_dec("#000080"), "RGB(0, 0, 128)");
                            assert_eq!(get_dec("#008000"), "RGB(0, 128, 0)");
                            assert_eq!(get_dec("#008080"), "RGB(0, 128, 128)");
                            assert_eq!(get_dec("#800000"), "RGB(128, 0, 0)");
                            assert_eq!(get_dec("#800080"), "RGB(128, 0, 128)");
                            assert_eq!(get_dec("#808000"), "RGB(128, 128, 0)");
                            assert_eq!(get_dec("#C0C0C0"), "RGB(192, 192, 192)");
                            assert_eq!(get_dec("#808080"), "RGB(128, 128, 128)");
                            assert_eq!(get_dec("#0000FF"), "RGB(0, 0, 255)");
                            assert_eq!(get_dec("#00FF00"), "RGB(0, 255, 0)");
                            assert_eq!(get_dec("#00FFFF"), "RGB(0, 255, 255)");
                            assert_eq!(get_dec("#FF0000"), "RGB(255, 0, 0)");
                            assert_eq!(get_dec("#FF00FF"), "RGB(255, 0, 255)");
                            assert_eq!(get_dec("#FFFF00"), "RGB(255, 255, 0)");
                            assert_eq!(get_dec("#FFFFFF"), "RGB(255, 255, 255)");
                            assert_eq!(get_dec("#6495ED"), "RGB(100, 149, 237)");
                        }
                    
                        #[test]
                        fn test_get_per()
                        {
                            assert_eq!(get_per("#000000"), "RGB(0%, 0%, 0%)");
                            assert_eq!(get_per("#000080"), "RGB(0%, 0%, 50%)");
                            assert_eq!(get_per("#008000"), "RGB(0%, 50%, 0%)");
                            assert_eq!(get_per("#008080"), "RGB(0%, 50%, 50%)");
                            assert_eq!(get_per("#800000"), "RGB(50%, 0%, 0%)");
                            assert_eq!(get_per("#800080"), "RGB(50%, 0%, 50%)");
                            assert_eq!(get_per("#808000"), "RGB(50%, 50%, 0%)");
                            assert_eq!(get_per("#C0C0C0"), "RGB(75%, 75%, 75%)");
                            assert_eq!(get_per("#808080"), "RGB(50%, 50%, 50%)");
                            assert_eq!(get_per("#0000FF"), "RGB(0%, 0%, 100%)");
                            assert_eq!(get_per("#00FF00"), "RGB(0%, 100%, 0%)");
                            assert_eq!(get_per("#00FFFF"), "RGB(0%, 100%, 100%)");
                            assert_eq!(get_per("#FF0000"), "RGB(100%, 0%, 0%)");
                            assert_eq!(get_per("#FF00FF"), "RGB(100%, 0%, 100%)");
                            assert_eq!(get_per("#FFFF00"), "RGB(100%, 100%, 0%)");
                            assert_eq!(get_per("#FFFFFF"), "RGB(100%, 100%, 100%)");
                            assert_eq!(get_per("#6495ED"), "RGB(39%, 58%, 92%)");
                        }
                    
                        #[test]
                        fn test_to_per()
                        {
                            assert_eq!(to_per(0), 0);
                            assert_eq!(to_per(50), 19);
                            assert_eq!(to_per(64), 25);
                            assert_eq!(to_per(128), 50);
                            assert_eq!(to_per(192), 75);
                            assert_eq!(to_per(255), 100);
                        }
                    }
                
            

The colors.csv File

The whole file can be downloaded here: colors.csv

Here is an extract:

colors.csv
                
                    aliceblue;#F0F8FF
                    antiquewhite;#FAEBD7
                    aqua;#00FFFF
                    aquamarine;#7FFFD4
                    azure;#F0FFFF
                    beige;#F5F5DC
                    bisque;#FFE4C4
                    black;#000000
                    blanchedalmond;#FFEBCD

                    # ... snip ...

                    thistle;#D8BFD8
                    tomato;#FF6347
                    turquoise;#40E0D0
                    violet;#EE82EE
                    wheat;#F5DEB3
                    white;#FFFFFF
                    whitesmoke;#F5F5F5
                    yellow;#FFFF00
                    yellowgreen;#9ACD32
                
            

Parsing Command-Line Arguments

The first step is to parse the command-line arguments. You will use the StructOpt library. So, do not forget to update the dependencies part of your Cargo.toml file.

Now, replace the default main file by the provided file and define an Arg structure. Your program will compile with some warnings. Just ignore them.

Here is the expected result:

                
                    $ cargo run -q -- -h
                    Color 0.1.0
                    John Smith <john.smith@debug-pro.com>
                    Print the RGB values of a color from its name.

                    USAGE:
                        color [FLAGS] <name>

                    FLAGS:
                        -d, --decimal    Prints RGB values in decimal
                        -h, --help       Prints help information
                        -p, --percent    Prints RGB values in percent
                        -V, --version    Prints version information

                    ARGS:
                        <name>    Name of the color
                    $ cargo run -q -- blue
                    [src/main.rs:23] arg = Arg {
                        decimal: false,
                        percent: false,
                        name: "blue"
                    }
                    $ cargo run -q -- blue --decimal
                    [src/main.rs:23] arg = Arg {
                        decimal: true,
                        percent: false,
                        name: "blue"
                    }
                    $ cargo run -q -- red -p
                    [src/main.rs:23] arg = Arg {
                        decimal: false,
                        percent: true,
                        name: "red"
                    }
                    $ cargo run -q -- black -dp
                    [src/main.rs:23] arg = Arg {
                        decimal: true,
                        percent: true,
                        name: "black"
                    }
                
            

Implementation

The get_hex() function

The function signature is as follows:

                
                    fn get_hex(name: &str) -> String
                
            

The color names and their associated hexadecimal values are stored in a file. This file should be located on the project directory (i.e. color/). It can be downloaded on a previous section. Each line contains the name of a color followed by its hexadecimal representation (they are separated by a semicolon). We assume that this file exists and its contents are always valid.

To get the contents of the file, you can use the following instruction.

                
                    let contents = std::fs::read_to_string("colors.csv").unwrap();
                
            

The read_to_string() function returns the contents of the file in a string of characters (String).

Actually, it does not return exactly a string of characters, but a special type named Result, which contains this string. This special type is used to handle errors. Since we have not looked at error management yet, we use the unwrap() method, which extracts the string from the special type. If any error occurs, the unwrap() method panics. In a further lesson, we will see how to manage errors in a better way.

To sum up:

Now, you have to extract the right hexadecimal color from that variable. There are different ways to do that. What you can do is to scan the contents of the variable line by line.

Here are some tips:

Have a look at the following examples. They may help you.

RUN
                
                    fn main()
                    {
                        let s1 = "Hello, world!\nGood bye!\nI'll be back.";
                        dbg!(s1);

                        for line in s1.lines()
                        {
                            dbg!(line);
                        }

                        let s2 = "david.bouchet";
                        let mut words = s2.split('.');

                        dbg!(s2);
                        dbg!(words.next().unwrap());
                        dbg!(words.next().unwrap());
                    }
                
            

Here is the output of the above program.

                
                    [src/main.rs:4] s1 = "Hello, world!\nGood bye!\nI\'ll be back."
                    [src/main.rs:8] line = "Hello, world!"
                    [src/main.rs:8] line = "Good bye!"
                    [src/main.rs:8] line = "I\'ll be back."
                    [src/main.rs:14] s2 = "david.bouchet"
                    [src/main.rs:15] words.next().unwrap() = "david"
                    [src/main.rs:16] words.next().unwrap() = "bouchet"
                
            

A test is provided: test_get_hex(). Take a good look at it to get a clear understanding of what get_hex() is supposed to return. Then, you can test your function with the following command.

                
                    cargo test get_hex
                
            

The get_dec() function

The function signature is as follows:

                
                    fn get_dec(hex: &str) -> String
                
            

Tips: for this function, you should use string slices, the u8::from_str_radix() function and the format!() macro.

Have a look at the following examples. They may help you.

RUN
                
                    fn main()
                    {
                        let hex = "#AB12FE";
                    
                        dbg!(hex);
                        dbg!(&hex[1..3]);
                        dbg!(&hex[3..5]);
                        dbg!(&hex[5..7]);
                    
                        let r = u8::from_str_radix(&hex[3..5], 16).unwrap();
                        dbg!(r);
                    
                        let s = format!("RGB({}, {}, {})", 10, 11, 12);
                        dbg!(s);
                    }
                
            

Here is the output of the above program.

                
                    [src/main.rs:5] hex = "#AB12FE"
                    [src/main.rs:6] &hex[1..3] = "AB"
                    [src/main.rs:7] &hex[3..5] = "12"
                    [src/main.rs:8] &hex[5..7] = "FE"
                    [src/main.rs:11] r = 18
                    [src/main.rs:14] s = "RGB(10, 11, 12)"
                
            

A test is provided: test_get_dec(). Take a good look at it to get a clear understanding of what get_dec() is supposed to return. Then, you can test your function with the following command.

                
                    cargo test get_dec
                
            

The to_per() function

The function signature is as follows:

                
                    fn to_per(c: u8) -> u8
                
            

Here are some tips:

A test is provided: test_to_per(). Take a good look at it to get a clear understanding of what to_per() is supposed to return. Then, you can test your function with the following command.

                
                    cargo test to_per
                
            

The get_per() function

The function signature is as follows:

                
                    fn get_per(hex: &str) -> String
                
            

A test is provided: test_get_per(). Take a good look at it to get a clear understanding of what get_per() is supposed to return. Then, you can test your function with the following command.

                
                    cargo test get_per
                
            

The main() function

Finally, implement the main() function.

Here are some tips:

Here are some examples of the expected result:

                
                    $ alias color='cargo run -q --'
                    $ color aquamarine
                    aquamarine: #7FFFD4
                    $ color blue -d
                    blue: #0000FF; RGB(0, 0, 255)
                    $ color firebrick --percent
                    firebrick: #B22222; RGB(69%, 13%, 13%)
                    $ color hotpink --decimal --percent
                    hotpink: #FF69B4; RGB(255, 105, 180); RGB(100%, 41%, 70%)
                    $ color hotpink -dp
                    hotpink: #FF69B4; RGB(255, 105, 180); RGB(100%, 41%, 70%)
                    $ color abcde
                    The 'abcde' color is undefined.
                
            

When you are done, clean your directory.

Hangman Game

Introduction

In this section, you will write a hangman game. This version has two players. The rules are quite simple:

Here is an example:

                
                    $ echo -e "David\nrust" > player_1
                    $ echo -e "Rolland\nlanguage" > player_2
                    $ ls -F
                    Cargo.lock  Cargo.toml  player_1  player_2  player_test  src/  target/
                    $ cargo run -q
                    ******************************************** David plays
                    8: --------
                    Enter a letter:
                    a
                    8: -A---A--
                    Enter a letter:
                    e
                    8: -A---A-E
                    Enter a letter:
                    i
                    7: -A---A-E
                    Enter a letter:
                    o
                    6: -A---A-E
                    Enter a letter:
                    u
                    6: -A--UA-E
                    Enter a letter:
                    b
                    5: -A--UA-E
                    Enter a letter:
                    c
                    4: -A--UA-E
                    Enter a letter:
                    d
                    3: -A--UA-E
                    Enter a letter:
                    f
                    2: -A--UA-E
                    Enter a letter:
                    g
                    2: -A-GUAGE
                    Enter a letter:
                    h
                    1: -A-GUAGE
                    Enter a letter:
                    k
                    ******************************************** Rolland plays
                    8: ----
                    Enter a letter:
                    a
                    7: ----
                    Enter a letter:
                    e
                    6: ----
                    Enter a letter:
                    i
                    5: ----
                    Enter a letter:
                    o
                    4: ----
                    Enter a letter:
                    u
                    4: -U--
                    Enter a letter:
                    r
                    4: RU--
                    Enter a letter:
                    b
                    3: RU--
                    Enter a letter:
                    t
                    3: RU-T
                    Enter a letter:
                    s
                    3: RUST
                    ******************************************** Result
                    David: 26.863 seconds
                    Rolland: 18.175 seconds
                    The winner is Rolland
                
            

Provided Files

The main.rs file

main.rs
                
                    use std::time::Instant;
                    use std::time::Duration;
                    
                    const INPUT_MAX: u8 = 8;
                    
                    #[derive(Debug, PartialEq)]
                    struct Player
                    {
                        name: String,
                        word: String,
                        found: bool,
                        time: Duration,
                    }
                    
                    fn main()
                    {
                        let mut p1 = get_player("player_1");
                        let mut p2 = get_player("player_2");
                    
                        println!("******************************************** {} plays", p1.name);
                        let (found, time) = chrono_play(&p2.word);
                        p1.found = found;
                        p1.time = time;
                    
                        println!("******************************************** {} plays", p2.name);
                        let (found, time) = chrono_play(&p1.word);
                        p2.found = found;
                        p2.time = time;
                    
                        println!("******************************************** Result");
                        println!("{}: {}.{} seconds", p1.name, p1.time.as_secs(), p1.time.subsec_millis());
                        println!("{}: {}.{} seconds", p2.name, p2.time.as_secs(), p2.time.subsec_millis());
                        println!("{}", get_winner(&p1, &p2));
                    }
                    
                    fn get_player(file_path: &str) -> Player
                    {
                        unimplemented!();
                    }
                    
                    fn get_dashed_word(word: &str, letters: &str) -> String
                    {
                        unimplemented!();
                    }
                    
                    fn get_letter() -> char
                    {
                        loop
                        {
                            println!("Enter a letter:");

                            let mut user_input = String::new();
                            std::io::stdin().read_line(&mut user_input).unwrap();

                            let user_input = user_input.trim();

                            if user_input.len() != 1
                            {
                                continue;
                            }

                            if let Some(letter) = user_input.to_uppercase().pop()
                            {
                                break letter
                            }
                        }
                    }
                    
                    fn play(word: &str) -> bool
                    {
                        unimplemented!();
                    }
                    
                    fn chrono_play(word: &str) -> (bool, Duration)
                    {
                        unimplemented!();
                    }
                    
                    fn get_winner(p1: &Player, p2: &Player) -> String
                    {
                        unimplemented!();
                    }
                    
                    #[cfg(test)]
                    mod tests
                    {
                        use super::*;
                    
                        #[test]
                        fn test_get_player()
                        {
                            let player = get_player("player_test");
                    
                            assert_eq!(player,
                                Player
                                {
                                    name: String::from("David"),
                                    word: String::from("RUST"),
                                    found: false,
                                    time: Duration::new(0, 0),
                                });
                        }
                    
                        #[test]
                        fn test_get_dashed_word()
                        {
                            assert_eq!(get_dashed_word("AZERTY", ""), "------");
                            assert_eq!(get_dashed_word("TEST", "TAB"), "T--T");
                            assert_eq!(get_dashed_word("LITERALIZATION", "AEIOU"), "-I-E-A-I-A-IO-");
                            assert_eq!(get_dashed_word("LITERAL", "AEIOU"), "-I-E-A-");
                        }
                    
                        #[test]
                        fn test_get_winner()
                        {
                            let mut p1 = Player
                            {
                                    name: String::from("David"),
                                    word: String::from("RUST"),
                                    found: false,
                                    time: Duration::new(0, 0),
                            };
                    
                            let mut p2 = Player
                            {
                                    name: String::from("Rolland"),
                                    word: String::from("LANGUAGE"),
                                    found: false,
                                    time: Duration::new(0, 0),
                            };
                    
                            // Players 1 and 2 did not find the word.
                            assert_eq!(get_winner(&p1, &p2), "No winner, two losers");
                    
                            // Player 1 did not find the word.
                            // Player 2 found the word.
                            p2.found = true;
                            assert_eq!(get_winner(&p1, &p2), "The winner is Rolland");
                    
                            // Player 1 found the word.
                            // Player 2 did not find the word.
                            p1.found = true;
                            p2.found = false;
                            assert_eq!(get_winner(&p1, &p2), "The winner is David");
                    
                            // Players 1 and 2 found the word.
                    
                            // Same time.
                            p2.found = true;
                            assert_eq!(get_winner(&p1, &p2), "No winner, no loser");
                    
                            // Player 1 was faster.
                            p2.time = Duration::new(5, 0);
                            assert_eq!(get_winner(&p1, &p2), "The winner is David");
                    
                            // Player 2 was faster.
                            p1.time = Duration::new(6, 0);
                            assert_eq!(get_winner(&p1, &p2), "The winner is Rolland");
                        }
                    }
                
            

The player_test file

This file will be used to test the get_player() function.

player_test
                
                    David
                    rust
                
            

Implementation

Introduction

All information about a player will be stored in a Player structure.

                
                    struct Player
                    {
                        name: String,
                        word: String,
                        found: bool,
                        time: Duration,
                    }
                
            

This structure is made up of three fields.

name
The name of the player.
word
The word that must be found by the other player. It must contain only capital letters without accents (from 'A' to 'Z').
found
Sets to true if the player finds the other player's word. Otherwise, sets to false.
time
The time the player takes to find (or try to find) the other player's word. To store this time, we use the Duration type of the standard library.

The main function is given, so you do not have to write it. Anyway, have a look at it and try to understand how it works.

Then, create a new project:

                
                    cargo new hangman --vcs none
                    cd hangman
                
            

Replace the default main.rs by the one that is provided.

Also, in the hangman/ directory, create the player_test file (its contents are provided).

The get_player() function

This function gets the name and the word of a player from a file and store them in a new Player structure.

The function signature is as follows:

                
                    fn get_player(file_path: &str) -> Player
                
            

The format of a player's file contains only two lines.

For example:

                
                    David
                    rust
                
            

Only the first two fields of the structure are set according to the contents of the file. The last two fields are initialized to a default value.

For instance, the structure that should be returned in the previous example, is as follows:

                
                    Player
                    {
                        name: "David",
                        word: "RUST",
                        found: false,
                        time: 0ns,
                    });
                
            

As you can see, the name of the player is copied directly into the name field. On the other hand, the letters of word are converted into capital letters.

Tips:

A test is provided: test_get_player(). Take a good look at it to get a clear understanding of what get_player() is supposed to return. Then, you can test your function with the following command. Be careful! this test uses the player_test file, so you have to create it.

                
                    cargo test get_player
                
            

The get_dashed_word() function

The function signature is as follows:

                
                    fn get_dashed_word(word: &str, letters: &str) -> String
                
            

Tips:

A test is provided: test_get_dashed_word(). Take a good look at it to get a clear understanding of what get_dashed_word() is supposed to return. Then, you can test your function with the following command.

                
                    cargo test get_dashed_word
                
            

The get_letter() function

The function signature is as follows:

                
                    fn get_letter() -> char
                
            

This function is provided.

The play() function

The function signature is as follows:

                
                    fn play(word: &str) -> bool
                
            

This function asks a player to find a word. Its algorithm is as follows:

                
                    letters = empty string
                    input_count = INPUT_MAX
                    while input_count > 0:
                        dashed_word = get_dashed_word()
                        print input_count and dashed_word
                        if dashed_word == word:
                            return true
                        letter = get_letter()
                        if word does not contain letter:
                            input_count -= 1
                        if letters does not contain letter:
                            letters.push(letter)
                    return false
                
            

Note that if a player enters a letter that is in the word, the input_count variable is not decremented. But if a player enters several times a letter that is not in the word, the input_count variable is decremented each time. So a player has to remember which letter he or she has already entered.

The INPUT_MAX constant is defined at the beginning of the file. It is the maximum number of letters a player can enter.

The chrono_play() function

This function is similar to play() but it times a player. So, this function calls play() and measures how long it takes before it returns.

The function signature is as follows:

                
                    fn chrono_play(word: &str) -> (bool, Duration)
                
            

This function returns a tuple of two values.

Use the Instant::now() method to get an instant corresponding to "now". Get the instants before and after the execution of play() and return the difference. Note that the difference between two instants gives a Duration type.

The get_winner() function

This function returns a string that is printed at the end of the game.

The function signature is as follows:

                
                    fn get_winner(p1: &Player, p2: &Player) -> String
                
            

Use the rules given previously and the test_get_winner() function to determine the contents of the return value. Then, you can test your function with the following command.

                
                    cargo test get_winner
                
            

When you are done, you can create the player_1 and player_2 files with your own names and words. Then, run your code.

Do not forget to clean your directory.