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:
-
pw_06_rust_strings/
- AUTHORS
-
reverse/
- Cargo.lock
- Cargo.toml
-
src/
- main.rs
-
scrabble/
- Cargo.lock
- Cargo.toml
-
src/
- main.rs
-
bin/
- simple_cli.rs
-
color/
- Cargo.lock
- Cargo.toml
- colors.csv
-
src/
- main.rs
-
hangman/
- Cargo.lock
- Cargo.toml
- player_1
- player_2
- player_test
-
src/
- main.rs
Do not submit any executable or binary files.
You have to delete all the target
directories.
The AUTHORS
file
must contain the following information.
First Name
Family Name
Login
Email Address
The last character of your
AUTHORS
file must be a newline character.
For instance:
$ 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.
&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.
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()
).
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).
- A, E, I, O, U, L, N, R, S, T: 1 point
- D, G: 2 points
- B, C, M, P: 3 points
- F, H, V, W, Y: 4 points
- K: 5 points
- J, X: 8 points
- Q, Z: 10 points
Your program will take three arguments:
- The word to score. We assume that this word contains only small letters without accents.
-
The
-d
or--double
option, which multiplies the score of a word by two. This option is disabled by default. -
The
-t
or--triple
option, which multiplies the score of a word by three. This option is disabled by default.
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:
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:
-
double: if
true
, the double option is enabled. -
triple: if
true
, the triple option is enabled. - word: the word to score.
Replace the previous main.rs
file by the following one:
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:
- Use the skip() iterator adaptor to skip the first argument (because it is the path of the executable file).
-
For each argument:
-
If the argument is equal to
"--double"
or"-d"
, set arg.double totrue
. -
If the argument is equal to
"--triple"
or"-t"
, set arg.triple totrue
. -
If the argument is equal to
"-dt"
or"-td"
, set arg.double and arg.triple totrue
. - Otherwise, set arg.word to the value of the argument.
-
If the argument is equal to
-
After the loop, if arg.word is still empty,
exit with the error code 1 and the following message:
"The word to score is missing.".
- To test if a string is empty, use the is_empty() method.
- To print the error message on the standard error, use the eprintln!() macro.
- To exit your code, use the std::process::exit() function.
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:
- The
scrabble
crate (the main crate). - The
simple_cli
crate.
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:
- Only one type of error is checked (i.e. the missing word).
- The error message is too brief.
- No help is provided.
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).
[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:
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.
-
Line 3: The derive attribute specifies that the structure
must derive from Debug and StructOpt.
-
Debug: Used for display only.
It allows the
dbg!()
macro to print the structure (e.g. line 23). - StructOp: Enables the special features of StructOp.
-
Debug: Used for display only.
It allows the
- Line 4: The structop attribute is used here to specify the name and to provide a brief description of our program.
- Lines 8, 12, 16: These comments are used to give a brief description for each argument. Note that each line of comment starts with three slashes (and not only two): they are doc comments.
-
Lines 9, 13: These structopt attributes are used here
to enable short and long versions of the options.
We could have specified the names of these options
(e.g. short = "d", long = "double"),
but, by default,
the name of the short option is the first letter of the field
and the name of the long option is the full name of the field.
For instance, for the double field,
by default, the short option is
-d
and the long option is--double
. So, in our case, it matches our needs (the same for the triple field).
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:
- Hexadecimal form: #FF0000
- RGB form in decimal: RGB(255, 0, 0)
- RGB form in percent: RGB(100%, 0%, 0%)
For further details, see Web colors.
Your program will take three arguments:
- The name of a color.
-
The
-d
or--decimal
option, which prints the color in decimal with the RGB syntax. This option is disabled by default. -
The
-p
or--percent
option, which prints the color in percent with the RGB syntax. This option is disabled by default.
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
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:
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
-
Argument:
- name: the name of a color (e.g. "blue").
-
Return Value:
returns the hexadecimal representation of the color (e.g. "#0000FF").
If the color is undefined, returns an empty string:
String::new()
.
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:
- If no error occurs, the contents variable holds the contents of the file.
- If an error occurs (e.g. the file cannot be accessed), the program panics. That is, it stops and exits with an error message.
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.
-
Then for each line:
- Split the line into words separated by a semicolon.
- If the first word is equal to the name of the color, return the second word.
- If the name of the color is not in the file, return an empty string.
Here are some tips:
- To scan the contents line by line, you can use the lines() method, which is an iterator over the lines of a string.
- To split a line into words, you can use the split() method, which is an iterator over substrings separated by a specific character (in our case, the semicolon).
-
To get the first and the second words of a line
(i.e. a color name and its hexadecimal representation),
you can use the
next()
method, which returns the next element of the iterator.
So obviously, the first call to
next()
returns the first element and the second call, the second element. Actually, in the same way asread_to_string()
,next()
does not return an element directly, but a special type namedOption
that contains the element. This special type is useful in case no element is returned. But we assume that the file is valid. So, for sure, the name of a color is always present and followed by a semicolon and its hexadecimal representation. Therefore, we can extract the element directly from this special type. To do so, we can use the same function as previously:unwrap()
. To sum up: instead of usingnext()
, you have to usenext().unwrap()
. -
When you finally get the last element of the line,
you cannot return it directly because the type
of this element is
&str
, which is a string slice. This type is different from that of the return value, which isString
. Fortunately, you can easily create a newString
type from a&str
type by using theString::from()
function. -
To return an empty string, use
String::new()
.
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
-
Argument:
- hex: the hexadecimal representation of a color (e.g. "#0000FF"). We assume that this parameter is always valid.
- Return Value: returns the RGB representation of the color in decimal (e.g. "RGB(0, 0, 255")).
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
-
Argument:
- c: the 8-bit unsigned value of a color.
- Return Value: returns the value of the color in percent (between 0 and 100).
Here are some tips:
- The return value is the integer part of:
c * 100 / 255
- Be careful! an overflow may occur.
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
-
Argument:
- hex: the hexadecimal representation of a color (e.g. "#0000FF"). We assume that this parameter is always valid.
- Return Value: returns the RGB representation of the color in percent (e.g. "RGB(0%, 0%, 100%")).
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:
-
The parameter of
get_hex()
is of type&str
. Thearg.name
is of typeString
. So you have to pass to the function an immutable reference ofarg.name
(i.e.&arg.name
). -
If the string returned by
get_hex()
is empty, it means that the color name cannot be found in the file. So, you have to print an error message on the standard error and exit with the code 1.
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:
-
Each player puts its name and the word
to be found by the other player in a file.
There is one file per player.
The file of the player 1 is named
player_1
. The file of the player 2 is namedplayer_2
. We assume that these files exist and their contents are always valid. - The program asks the first player to find the word of the second player.
- The program asks the second player to find the word of the first player.
- If the first player finds the word and the second player does not, the first player wins.
- If the second player finds the word and the first player does not, the second player wins.
- If neither player finds the words, there is no winner (two losers only).
- If both players find the words, the faster wins. If they take the same time, there is neither a winner nor a loser.
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
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.
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 tofalse
. - 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
-
Argument:
- file_path: the path of a player's file. (e.g. "player_1").
-
Return Value:
returns a new
Player
structure for a player.
The format of a player's file contains only two lines.
- Line 1: the name of the player.
- Line 2: the word of the player. This word can contain only small or capital letters without accents (from 'a' to 'z' or from 'A' to 'Z').
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:
-
To convert the letters of a string into capital letters,
you can use the
to_uppercase()
method.
This method returns a
String
type. -
To initialize the
time
field, you can use theDuration::new(0, 0);
instruction.
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
-
Arguments:
- word: a reference to the word that must be found.
- letters: a reference to a string that contains all the letters that have been already entered by a player.
- Return Value: returns a string that hides the letters of a word. The hidden letters are those that are not present in the letters parameter.
Tips:
- Create an empty string (the return string).
-
For each character of word.
- If letters contains a character, append this character to the return string.
-
Otherwise, append the
'-'
character to the return string.
-
To determine whether a string contains a character,
you can use the
contains()
method.
Note that a pattern can also be of type
char
(i.e.contains('A')
). - To append a character to a string, you can use the push() method.
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
- Return Value: returns a character entered by a player.
This function is provided.
The play()
function
The function signature is as follows:
fn play(word: &str) -> bool
-
Argument:
- word: a reference to the word that must be found.
-
Return Value:
returns
true
if the player finds the word. Otherwise, returnsfalse
.
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.
-
The first value is the return value of
play()
. -
The second value is the duration of
play()
.
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
-
Arguments:
- p1: a reference to the structure of the first player.
- p2: a reference to the structure of the second player.
- Return Value: returns a string according to the result of the game.
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.