Aller au contenu

Exercices Rust

Lire des choses sur Rust, c’est utile. Mais c’est en écrivant du Rust qu’on le retient. Cette page propose 18 petits exercices, classés du plus facile au plus difficile et regroupés par thème.

Pour chacun, lisez l’énoncé, essayez d’abord par vous-même, et n’ouvrez la solution qu’ensuite. C’est tout à fait normal de bloquer ou de voir des erreurs de compilation. Ça fait partie de l’apprentissage, et les messages d’erreur de Rust sont souvent le meilleur indice que vous aurez.

Comment vous entraîner : ouvrez le Rust Playground dans un autre onglet, ou créez un projet avec cargo new practice (voir Débuter avec Rust). Écrivez-y votre réponse, puis comparez-la avec la solution. Si votre réponse est différente mais affiche la même chose, elle est probablement correcte. Il y a presque toujours plusieurs bonnes façons de faire.

Affichez les nombres de 1 à 15. Mais pour les multiples de 3, affichez Fizz ; pour les multiples de 5, Buzz ; et pour les multiples des deux, FizzBuzz.

Indice : utilisez une boucle for sur 1..=15 et l’opérateur % (reste de la division). Testez « multiple des deux » en premier.

Voir la solution
fn main() {
for n in 1..=15 {
if n % 15 == 0 {
println!("FizzBuzz");
} else if n % 3 == 0 {
println!("Fizz");
} else if n % 5 == 0 {
println!("Buzz");
} else {
println!("{n}");
}
}
}

Sortie :

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
▶ Essayez par vous-même

L’ordre compte. Si vous testez d’abord n % 3 == 0, alors 15 affiche Fizz et n’atteint jamais la branche FizzBuzz. Un nombre divisible à la fois par 3 et par 5 est divisible par 15.

Écrivez une fonction reverse(s: &str) -> String qui renvoie le texte à l’envers. "épater" doit devenir "retapé".

Indice : .chars() vous donne les caractères un par un, .rev() inverse un itérateur, et .collect() sait construire une String.

Voir la solution
fn reverse(s: &str) -> String {
s.chars().rev().collect()
}
fn main() {
println!("{}", reverse("épater"));
println!("{}", reverse("bonjour"));
println!("{}", reverse("Noël"));
}

Sortie :

retapé
ruojnob
lëoN
▶ Essayez par vous-même

Pourquoi .chars() et pas les octets ? Une String Rust est encodée en UTF-8 : un caractère comme é ou ë occupe deux octets. Inverser les octets le couperait en deux et donnerait un texte invalide. .chars() travaille sur des caractères entiers. Pour en savoir plus, voyez Les chaînes de caractères en Rust.

Écrivez count_vowels(s: &str) -> usize, qui compte les voyelles (a, e, i, o, u) d’une chaîne. Les minuscules comme les majuscules comptent.

Indice : .chars().filter(...).count(). Une chaîne comme "aeiouAEIOU" a une méthode .contains(c) qui permet de tester chaque caractère.

Voir la solution
fn count_vowels(s: &str) -> usize {
s.chars().filter(|c| "aeiouAEIOU".contains(*c)).count()
}
fn main() {
println!("{}", count_vowels("Bonjour, le monde"));
println!("{}", count_vowels("Rust est super"));
println!("{}", count_vowels("AEIOU et parfois y"));
println!("{}", count_vowels("pfff"));
}

Sortie :

6
4
9
0
▶ Essayez par vous-même

filter passe à la closure une référence vers chaque élément (&char) : on écrit donc *c pour obtenir le char lui-même. Les pages Les closures en Rust et Les itérateurs en Rust expliquent ce schéma.

Les voyelles accentuées comme é ou à ne sont pas comptées ici. En bonus, ajoutez-les à la chaîne "aeiouAEIOU" : .contains fonctionne aussi avec elles.

Ce programme ne compile pas. Modifiez uniquement le paramètre de la fonction shout et les deux appels pour qu’il affiche BONJOUR ! deux fois.

fn shout(text: String) {
println!("{} !", text.to_uppercase());
}
fn main() {
let word = String::from("bonjour");
shout(word);
shout(word);
}
error[E0382]: use of moved value: `word`
--> src/main.rs:8:11
|
6 | let word = String::from("bonjour");
| ---- move occurs because `word` has type `String`, which does not implement the `Copy` trait
7 | shout(word);
| ---- value moved here
8 | shout(word);
| ^^^^ value used here after move

Le compilateur dit que word est utilisé après avoir été déplacé (« moved ») : le premier appel a emporté la valeur.

Indice : shout a seulement besoin de lire le texte, pas d’en être propriétaire. Voyez L’ownership en Rust.

Voir la solution
fn shout(text: &str) {
println!("{} !", text.to_uppercase());
}
fn main() {
let word = String::from("bonjour");
shout(&word);
shout(&word);
}

Sortie :

BONJOUR !
BONJOUR !
▶ Essayez par vous-même

Quand une fonction prend une String, l’appeler y déplace (move) la chaîne, et main ne peut plus l’utiliser. Prendre un &str signifie que la fonction ne fait que l’emprunter. Le compilateur suggère aussi word.clone(), qui fonctionne également, mais qui copie toute la chaîne sans raison. Ici, l’emprunt est la meilleure solution.

Ce programme ne compile pas non plus. Faites-lui afficher Ana, tout en ajoutant quand même Ben à la liste.

fn main() {
let mut names = vec![String::from("Ana")];
let first = &names[0];
names.push(String::from("Ben"));
println!("{first}");
}
error[E0502]: cannot borrow `names` as mutable because it is also borrowed as immutable
--> src/main.rs:4:5
|
3 | let first = &names[0];
| ----- immutable borrow occurs here
4 | names.push(String::from("Ben"));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 | println!("{first}");
| ----- immutable borrow later used here

Le message dit qu’on ne peut pas emprunter names de façon mutable, car il est déjà emprunté de façon immuable.

Indice : le problème, c’est l’ordre. first est encore utilisé quand push modifie le vecteur. Voyez Le borrowing en Rust.

Voir la solution
fn main() {
let mut names = vec![String::from("Ana")];
names.push(String::from("Ben"));
let first = &names[0];
println!("{first}");
}

Sortie :

Ana
▶ Essayez par vous-même

Pourquoi Rust est-il si strict ici ? push peut avoir besoin de plus de place : le vecteur peut alors déplacer tous ses éléments vers un nouvel emplacement mémoire, plus grand. first pointerait alors vers l’ancien emplacement, qui n’est plus valide. La règle de Rust (plusieurs lecteurs ou un seul écrivain, jamais les deux à la fois) arrête ce bug avant même que votre programme ne tourne. Prendre la référence après le push règle le problème.

Écrivez first_word(s: &str) -> &str, qui renvoie le premier mot (tout ce qui se trouve avant la première espace). S’il n’y a pas d’espace, renvoyez la chaîne entière. Ne créez pas de nouvelle String : renvoyez une slice de l’original.

Indice : s.find(' ') renvoie un Option<usize> avec la position de la première espace. &s[..i] est une slice qui va du début jusqu’à i.

Voir la solution
fn first_word(s: &str) -> &str {
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
}
fn main() {
println!("{:?}", first_word("bonjour le monde"));
println!("{:?}", first_word("Rust"));
println!("{:?}", first_word(""));
}

Sortie :

"bonjour"
"Rust"
""
▶ Essayez par vous-même

Le &str renvoyé pointe vers la même zone mémoire que l’entrée : rien n’est copié. Le compilateur sait que le résultat ne peut pas vivre plus longtemps que l’entrée. C’est l’élision des lifetimes en action : avec une seule référence en entrée et une en sortie, vous n’avez pas à écrire les lifetimes vous-même.

Une version plus courte est s.split_whitespace().next().unwrap_or(""), qui ignore en plus les espaces en trop au début.

Écrivez largest(numbers: &[i32]) -> Option<i32>. Elle renvoie le plus grand nombre, ou None si la liste est vide. Pour cette fois, n’utilisez pas la méthode intégrée .max() : écrivez la boucle vous-même.

Indice : commencez avec le premier élément comme « le plus grand pour l’instant ». numbers.first() renvoie un Option, donc il gère le cas de la liste vide pour vous.

Voir la solution
fn largest(numbers: &[i32]) -> Option<i32> {
let mut biggest = *numbers.first()?;
for &n in numbers {
if n > biggest {
biggest = n;
}
}
Some(biggest)
}
fn main() {
println!("{:?}", largest(&[3, 41, 7, 12]));
println!("{:?}", largest(&[-5, -2, -9]));
println!("{:?}", largest(&[]));
}

Sortie :

Some(41)
Some(-2)
None
▶ Essayez par vous-même

numbers.first()? renvoie None tout de suite si la liste est vide. C’est pour cela que la fonction renvoie un Option : « le plus grand élément de rien » n’a pas de bonne réponse, et Rust vous oblige à le dire. Dans du vrai code, numbers.iter().max() fait le même travail.

Partir de 0 au lieu du premier élément est un bug classique : la liste [-5, -2, -9] donnerait à tort 0.

Comptez combien de fois chaque mot apparaît dans une phrase. Ignorez les majuscules, les minuscules et la ponctuation, et affichez les mots dans l’ordre alphabétique.

Indice : utilisez une BTreeMap<String, usize>. Elle fonctionne comme une HashMap, mais garde ses clés triées. map.entry(word).or_insert(0) vous donne un compteur auquel ajouter 1.

Voir la solution
use std::collections::BTreeMap;
fn main() {
let text = "Le chat est sur le tapis. Le chat dort !";
let mut counts: BTreeMap<String, usize> = BTreeMap::new();
for word in text.split_whitespace() {
let clean = word
.trim_matches(|c: char| !c.is_alphanumeric())
.to_lowercase();
if clean.is_empty() {
continue;
}
*counts.entry(clean).or_insert(0) += 1;
}
for (word, count) in &counts {
println!("{word} : {count}");
}
}

Sortie :

chat : 2
dort : 1
est : 1
le : 3
sur : 1
tapis : 1
▶ Essayez par vous-même

Pourquoi BTreeMap et pas HashMap ? Une HashMap vous rend ses éléments dans un ordre imprévisible, qui peut changer d’une exécution à l’autre. Si vous avez besoin d’une sortie triée, utilisez une BTreeMap, ou rassemblez la HashMap dans un Vec et triez-le.

trim_matches retire le . de tapis., et to_lowercase fait de Le et le le même mot. Le ! final, séparé par une espace comme le veut la typographie française, forme un « mot » à lui seul : une fois nettoyé, il est vide, et le continue le saute.

À partir de vec![1, 2, 3, 4, 5, 6, 7, 8], construisez un nouveau vecteur avec les carrés des nombres pairs uniquement : [4, 16, 36, 64]. Puis affichez leur somme. Utilisez les méthodes des itérateurs, pas une boucle for.

Indice : enchaînez .iter(), .filter(...), .map(...) et .collect(). Ensuite, .iter().sum() les additionne.

Voir la solution
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8];
let squares: Vec<i32> = numbers
.iter()
.filter(|n| *n % 2 == 0)
.map(|n| n * n)
.collect();
let total: i32 = squares.iter().sum();
println!("{:?}", squares);
println!("Somme : {total}");
}

Sortie :

[4, 16, 36, 64]
Somme : 120
▶ Essayez par vous-même

Lisez la chaîne de haut en bas, comme une recette : prendre les nombres, garder les pairs, mettre chacun au carré, les ranger dans un Vec. Les itérateurs sont paresseux (lazy) : rien ne s’exécute vraiment tant que .collect() ne réclame pas les résultats. Voyez Les itérateurs en Rust.

Écrivez sum_numbers(input: &str) -> Result<i32, ParseIntError>. Elle prend un texte comme "1, 2, 3", convertit chaque nombre et renvoie la somme. Si un morceau n’est pas un nombre, renvoyez l’erreur au lieu de planter.

Indice : découpez sur ',', appliquez .trim() à chaque morceau, puis .parse(). Utilisez l’opérateur ? pour sortir tout de suite en cas d’erreur.

Voir la solution
use std::num::ParseIntError;
fn sum_numbers(input: &str) -> Result<i32, ParseIntError> {
let mut total = 0;
for piece in input.split(',') {
let n: i32 = piece.trim().parse()?;
total += n;
}
Ok(total)
}
fn main() {
for input in ["1, 2, 3", "10,20, 30", "4, cinq, 6", ""] {
match sum_numbers(input) {
Ok(sum) => println!("{input:?} -> la somme vaut {sum}"),
Err(e) => println!("{input:?} -> erreur : {e}"),
}
}
}

Sortie :

"1, 2, 3" -> la somme vaut 6
"10,20, 30" -> la somme vaut 60
"4, cinq, 6" -> erreur : invalid digit found in string
"" -> erreur : cannot parse integer from empty string
▶ Essayez par vous-même

Les messages d’erreur eux-mêmes restent en anglais, car ils viennent de la bibliothèque standard de Rust : « invalid digit found in string » signifie « chiffre invalide dans la chaîne », et « cannot parse integer from empty string » signifie « impossible de lire un entier dans une chaîne vide ».

? fait deux choses en un seul caractère : si parse() donne Ok(n), vous obtenez n. S’il donne Err(e), la fonction renvoie Err(e) immédiatement. C’est pour cela que la fonction doit elle aussi renvoyer un Result.

Bonus : Result fonctionne avec .sum(), donc cette seule ligne fait la même chose et s’arrête à la première erreur :

input.split(',').map(|p| p.trim().parse::<i32>()).sum()

Chaque utilisateur a peut-être une adresse e-mail, peut-être pas. Écrivez email_domain(user: &User) -> Option<&str>, qui renvoie la partie située après le @. Renvoyez None s’il n’y a pas d’e-mail, ou si l’e-mail ne contient pas de @. Ensuite, cherchez des utilisateurs par leur nom et affichez leur domaine.

Indice : user.email.as_deref() transforme un Option<String> en Option<&str>. email.split_once('@') renvoie un Option<(&str, &str)>. L’opérateur ? fonctionne aussi avec Option. Voyez Option en Rust.

Voir la solution
struct User {
name: String,
email: Option<String>,
}
fn email_domain(user: &User) -> Option<&str> {
let email = user.email.as_deref()?;
let (_, domain) = email.split_once('@')?;
Some(domain)
}
fn main() {
let users = vec![
User { name: String::from("Ana"), email: Some(String::from("[email protected]")) },
User { name: String::from("Ben"), email: None },
User { name: String::from("Cleo"), email: Some(String::from("cleo-chez-elle")) },
];
for name in ["Ana", "Ben", "Cleo", "Dan"] {
let domain = users
.iter()
.find(|u| u.name == name)
.and_then(email_domain)
.unwrap_or("(aucun domaine)");
println!("{name} : {domain}");
}
}

Sortie :

Ana : example.com
Ben : (aucun domaine)
Cleo : (aucun domaine)
Dan : (aucun domaine)
▶ Essayez par vous-même

Il y a ici trois façons différentes d’« échouer » : Dan n’existe pas, Ben n’a pas d’e-mail, et l’e-mail de Cleo n’a pas de @. Chacune est une étape Option, et ? ou .and_then() s’arrête au premier None. Pas de tests if, pas d’erreurs de pointeur nul. .unwrap_or(...) fournit une valeur par défaut tout à la fin.

Créez une struct Account avec un titulaire et un solde. Donnez-lui deposit(amount) et withdraw(amount). withdraw doit renvoyer un Result<u64, String> : le nouveau solde si l’opération a réussi, ou un message d’erreur s’il n’y a pas assez d’argent. Le solde ne doit jamais passer sous zéro.

Indice : les méthodes qui modifient le compte prennent &mut self. Vérifiez le solde avant de soustraire. Voyez Les structs en Rust et Result en Rust.

Voir la solution
struct Account {
owner: String,
balance: u64,
}
impl Account {
fn new(owner: &str) -> Account {
Account { owner: owner.to_string(), balance: 0 }
}
fn deposit(&mut self, amount: u64) {
self.balance += amount;
}
fn withdraw(&mut self, amount: u64) -> Result<u64, String> {
if amount > self.balance {
return Err(format!(
"impossible de retirer {amount} : le solde n'est que de {}",
self.balance
));
}
self.balance -= amount;
Ok(self.balance)
}
}
fn main() {
let mut account = Account::new("Ana");
account.deposit(100);
for amount in [30, 50, 40] {
match account.withdraw(amount) {
Ok(left) => println!("Retrait de {amount}, il reste {left}"),
Err(e) => println!("Erreur : {e}"),
}
}
println!("À la fin, {} a {}", account.owner, account.balance);
}

Sortie :

Retrait de 30, il reste 70
Retrait de 50, il reste 20
Erreur : impossible de retirer 40 : le solde n'est que de 20
À la fin, Ana a 20
▶ Essayez par vous-même

Utiliser u64 (un nombre non signé) ajoute un joli filet de sécurité : ce type ne peut tout simplement pas contenir un solde négatif. Si vous oubliiez la vérification, self.balance -= amount déclencherait une panic avec le message « attempt to subtract with overflow » (tentative de soustraction avec dépassement) en mode debug, au lieu de passer en négatif sans rien dire.

Définissez un trait Shape avec deux méthodes : area(&self) -> f64 et name(&self) -> String. Implémentez-le pour un Circle (avec un rayon) et un Rectangle (avec une largeur et une hauteur). Mettez les deux dans un même Vec et affichez le nom et l’aire de chacun avec 2 décimales.

Indice : pour ranger des types différents dans un seul Vec, utilisez Vec<Box<dyn Shape>>. Voyez Les traits en Rust et Les trait objects (dyn) en Rust. {:.2} affiche 2 décimales.

Voir la solution
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> String;
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
fn name(&self) -> String {
format!("Cercle (r = {})", self.radius)
}
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn name(&self) -> String {
format!("Rectangle ({} x {})", self.width, self.height)
}
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Rectangle { width: 3.0, height: 4.0 }),
Box::new(Circle { radius: 2.5 }),
];
let mut total = 0.0;
for shape in &shapes {
println!("{} : {:.2}", shape.name(), shape.area());
total += shape.area();
}
println!("Aire totale : {total:.2}");
}

Sortie :

Cercle (r = 1) : 3.14
Rectangle (3 x 4) : 12.00
Cercle (r = 2.5) : 19.63
Aire totale : 34.78
▶ Essayez par vous-même

La boucle ne sait pas quelle forme elle manipule, et ça lui est égal. Elle sait seulement que chaque élément « est un Shape », donc elle peut appeler area() et name(). Ajouter un Triangle plus tard, c’est écrire un nouveau impl, et la boucle ne change pas du tout.

Remarquez que Rust affiche les nombres décimaux avec un point (3.14), pas avec une virgule : c’est la notation de tous les langages de programmation.

Reprenez l’exercice 7, mais faites-le fonctionner pour n’importe quel type comparable : entiers, nombres décimaux, caractères et chaînes. La signature est largest<T: PartialOrd>(items: &[T]) -> Option<&T>.

Indice : T: PartialOrd veut dire « T accepte < et > ». Renvoyez une référence (&T) pour ne pas avoir à copier ou cloner les éléments. Voyez Les génériques en Rust.

Voir la solution
fn largest<T: PartialOrd>(items: &[T]) -> Option<&T> {
let mut biggest = items.first()?;
for item in items {
if item > biggest {
biggest = item;
}
}
Some(biggest)
}
fn main() {
println!("{:?}", largest(&[3, 41, 7, 12]));
println!("{:?}", largest(&[2.5, -1.0, 9.75]));
println!("{:?}", largest(&['r', 'u', 's', 't']));
println!("{:?}", largest(&["poire", "abricot", "tomate"]));
let empty: Vec<i32> = Vec::new();
println!("{:?}", largest(&empty));
}

Sortie :

Some(41)
Some(9.75)
Some('u')
Some("tomate")
None
▶ Essayez par vous-même

Le corps est presque identique à celui de l’exercice 7. Le seul vrai changement, c’est la partie <T: PartialOrd>. Sans cette contrainte, le compilateur refuse item > biggest, car tous les types ne sont pas comparables. Les chaînes se comparent dans l’ordre alphabétique, c’est pourquoi "tomate" l’emporte.

Remarquez le Vec<i32> sur le vecteur vide : sans aucun élément, Rust ne peut pas deviner ce qu’est T, donc vous devez le lui dire.

Une liste d’invités contient des noms écrits plusieurs fois. Écrivez find_repeats(words: &[&str]) -> Vec<String>, qui renvoie chaque nom présent plus d’une fois, une seule fois chacun, dans l’ordre alphabétique. Affichez aussi le nombre d’invités différents.

Indice : un HashSet se souvient de ce que vous avez déjà vu. Sa méthode insert renvoie false quand la valeur était déjà présente. Un HashSet n’a pas d’ordre fixe : triez donc le résultat à la fin.

Voir la solution
use std::collections::HashSet;
fn find_repeats(words: &[&str]) -> Vec<String> {
let mut seen = HashSet::new();
let mut repeats = HashSet::new();
for &word in words {
// insert renvoie false si le mot était déjà dans l'ensemble
if !seen.insert(word) {
repeats.insert(word);
}
}
let mut result: Vec<String> = repeats.into_iter().map(String::from).collect();
result.sort();
result
}
fn main() {
let guests = ["ana", "ben", "cleo", "ben", "dan", "ana", "ben"];
println!("Doublons : {:?}", find_repeats(&guests));
let unique: HashSet<&str> = guests.iter().copied().collect();
println!("Invités différents : {}", unique.len());
println!("Doublons : {:?}", find_repeats(&["x", "y", "z"]));
}

Sortie :

Doublons : ["ana", "ben"]
Invités différents : 4
Doublons : []
▶ Essayez par vous-même

Pourquoi deux ensembles ? seen contient tous les noms vus jusqu’ici. repeats contient les noms rencontrés une deuxième fois. "ben" apparaît trois fois, mais comme repeats est un ensemble, il n’y est stocké qu’une fois.

Rassembler les valeurs dans un HashSet est aussi le moyen le plus rapide de compter les valeurs uniques : les doublons disparaissent tout simplement. Sans la ligne result.sort(), l’ordre des doublons pourrait changer d’une exécution à l’autre.

Triez une liste de mots du plus court au plus long. Quand deux mots ont la même longueur, rangez-les de A à Z, sans tenir compte des majuscules et des minuscules. Puis comparez votre résultat avec un simple .sort().

Indice : sort_by prend une closure qui compare deux éléments et renvoie un Ordering. .then_with(...) ajoute une seconde comparaison qui ne compte qu’en cas d’égalité. Voyez Trier et rechercher en Rust.

Voir la solution
fn main() {
let mut words = vec!["poire", "Figue", "banane", "pomme", "kiwi", "Cerise", "datte"];
let mut plain = words.clone();
plain.sort();
println!("Tri simple : {plain:?}");
words.sort_by(|a, b| {
a.len()
.cmp(&b.len())
.then_with(|| a.to_lowercase().cmp(&b.to_lowercase()))
});
println!("Par longueur : {words:?}");
}

Sortie :

Tri simple : ["Cerise", "Figue", "banane", "datte", "kiwi", "poire", "pomme"]
Par longueur : ["kiwi", "datte", "Figue", "poire", "pomme", "banane", "Cerise"]
▶ Essayez par vous-même

Regardez le tri simple : "Cerise" et "Figue" arrivent en premier. Les chaînes se comparent selon les codes de leurs caractères, et toutes les majuscules passent avant toutes les minuscules. C’est pourquoi on compare les versions to_lowercase() pour départager les égalités.

Nous avons choisi des mots sans accents exprès : .len() compte des octets, pas des lettres, et une lettre accentuée comme é occupe deux octets. Avec "pêche", .len() vaudrait 6 et non 5. Pour compter les lettres, utilisez .chars().count().

Une façon plus courte d’écrire la même règle est words.sort_by_key(|w| (w.len(), w.to_lowercase())). Les tuples se comparent champ par champ : la longueur décide d’abord, et le mot en minuscules départage les égalités.

Écrivez shipping_cost(p: Package) -> String pour cet enum, avec un seul match :

enum Package {
Letter { grams: u32 },
Parcel { kg: u32, express: bool },
Pickup,
}

Les règles : le retrait sur place est gratuit. Les lettres jusqu’à 20 g coûtent 1 €, les lettres plus lourdes 3 €. Les colis de plus de 30 kg ne peuvent pas du tout être expédiés. Les colis express coûtent 10 € plus 2 € par kg. Les autres colis coûtent 5 € plus 1 € par kg.

Indice : une branche de match peut avoir un if après le pattern, qu’on appelle une garde (guard) : Package::Letter { grams } if grams <= 20 => .... Les branches sont testées de haut en bas, donc placez la règle « trop lourd » avant la règle express. Voyez Le pattern matching en Rust.

Voir la solution
enum Package {
Letter { grams: u32 },
Parcel { kg: u32, express: bool },
Pickup,
}
fn shipping_cost(p: Package) -> String {
match p {
Package::Pickup => String::from("gratuit (retrait sur place)"),
Package::Letter { grams } if grams <= 20 => String::from("1 € (lettre standard)"),
Package::Letter { grams } => format!("3 € (grande lettre, {grams} g)"),
Package::Parcel { kg, .. } if kg > 30 => format!("trop lourd pour être expédié ({kg} kg)"),
Package::Parcel { kg, express: true } => format!("{} € (express)", 10 + kg * 2),
Package::Parcel { kg, express: false } => format!("{} € (colis standard)", 5 + kg),
}
}
fn main() {
let packages = vec![
Package::Letter { grams: 15 },
Package::Letter { grams: 80 },
Package::Parcel { kg: 4, express: false },
Package::Parcel { kg: 4, express: true },
Package::Parcel { kg: 35, express: true },
Package::Pickup,
];
for p in packages {
println!("{}", shipping_cost(p));
}
}

Sortie :

1 € (lettre standard)
3 € (grande lettre, 80 g)
9 € (colis standard)
18 € (express)
trop lourd pour être expédié (35 kg)
gratuit (retrait sur place)
▶ Essayez par vous-même

Le colis express de 35 kg est le cas intéressant. Il correspond à deux branches, mais seule la première l’emporte. Si vous placiez la branche express au-dessus de la branche « trop lourd », elle facturerait joyeusement 80 € pour un colis impossible à expédier.

Remarquez que la dernière branche indique express: false au lieu de ... Ce n’est pas obligatoire, mais la branche dit ainsi exactement ce qu’elle traite. Le compilateur vérifie quand même que tous les cas sont couverts. Les gardes ne comptent pas dans cette vérification : c’est pourquoi la branche Package::Letter { grams } sans garde doit être présente.

Le rapport d’un entrepôt contient un article par ligne, comme "pommes 12". Certaines lignes sont abîmées : vides, ou avec une quantité qui n’est pas un nombre. Avec les méthodes des itérateurs (sans boucle for), gardez uniquement les lignes qui se lisent correctement et dont la quantité est supérieure à zéro. Affichez-les sous la forme nom : quantité, séparées par des virgules, puis affichez le total.

Indice : .lines() découpe le texte en lignes. filter_map ne garde que les valeurs Some renvoyées par sa closure : il peut donc filtrer et convertir en une seule étape. .ok() transforme un Result en Option. Voyez Les itérateurs en Rust.

Voir la solution
fn main() {
let report = "pommes 12\nbananes x\npoires 0\n\ncerises 40\nprunes 7";
let in_stock: Vec<(&str, u32)> = report
.lines()
.filter_map(|line| line.split_once(' '))
.filter_map(|(name, count)| count.parse::<u32>().ok().map(|n| (name, n)))
.filter(|&(_, n)| n > 0)
.collect();
let list: Vec<String> = in_stock
.iter()
.map(|(name, n)| format!("{name} : {n}"))
.collect();
let total: u32 = in_stock.iter().map(|(_, n)| n).sum();
println!("{}", list.join(", "));
println!("Total des articles : {total}");
}

Sortie :

pommes : 12, cerises : 40, prunes : 7
Total des articles : 59
▶ Essayez par vous-même

Suivez une ligne à la fois. La ligne vide ne contient pas d’espace : split_once donne None, et le premier filter_map l’écarte. "bananes x" se découpe bien, mais "x" n’est pas un nombre, donc le second filter_map l’écarte. "poires 0" passe les deux étapes, puis filter la retire parce que la quantité vaut zéro.

On fait un seul collect dans un Vec, puis on le parcourt deux fois : une fois pour construire le texte, une fois pour le total. Un itérateur ne peut servir qu’une seule fois, donc garder les données nettoyées est plus simple que de tout relire deux fois.

Si vous avez terminé les 18 exercices, bravo. C’est un vrai progrès. 🦀

  • Rustlings (en anglais) : la série officielle de petits programmes cassés à réparer un par un dans votre terminal. Une excellente suite à cette page.
  • Le parcours Rust d’Exercism (en anglais) : une centaine d’exercices avec des tests, et des retours gratuits de mentors humains sur vos solutions.
  • Rust by Example (en anglais) : de courts exemples exécutables pour chaque sujet. Pratique quand vous bloquez sur la syntaxe d’un exercice.
  • Le trait Iterator — documentation de la std (en anglais) : toutes les méthodes des itérateurs (filter_map, fold, zip et bien d’autres), chacune avec un petit exemple.
  • HashSet — documentation de la std (en anglais) : toutes les méthodes des ensembles, dont union, intersection et difference.