Pular para o conteúdo

Exercícios de Rust

Ler sobre Rust é útil. Mas é escrevendo Rust que o conhecimento fixa. Esta página tem 18 problemas pequenos, em ordem do mais fácil para o mais difícil e agrupados por tema.

Em cada um, leia o enunciado, tente resolver sozinho primeiro e só depois abra a solução. É totalmente normal travar ou ver erros do compilador. Isso faz parte do aprendizado, e as mensagens de erro do Rust costumam ser a melhor dica que você vai ter.

Como praticar: abra o Rust Playground em outra aba ou crie um projeto com cargo new practice (veja Começando com Rust). Escreva sua resposta lá. Depois compare com a solução. Se a sua resposta for diferente mas imprimir a mesma coisa, provavelmente está certa. Quase sempre existe mais de um jeito bom de fazer.

Imprima os números de 1 a 15. Mas, nos múltiplos de 3, imprima Fizz; nos múltiplos de 5, imprima Buzz; e nos múltiplos dos dois, imprima FizzBuzz.

Dica: use um laço for sobre 1..=15 e o operador % (resto da divisão). Verifique “múltiplo dos dois” primeiro.

Ver solução
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}");
}
}
}

Saída:

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
▶ Experimente você mesmo

A ordem importa. Se você verificar n % 3 == 0 primeiro, o 15 imprime Fizz e nunca chega ao ramo do FizzBuzz. Um número divisível por 3 e por 5 ao mesmo tempo é divisível por 15.

Escreva uma função reverse(s: &str) -> String que devolva o texto de trás para frente. "roma" deve virar "amor".

Dica: .chars() entrega os caracteres um de cada vez, .rev() inverte um iterador e .collect() consegue montar uma String.

Ver solução
fn reverse(s: &str) -> String {
s.chars().rev().collect()
}
fn main() {
println!("{}", reverse("roma"));
println!("{}", reverse("olá"));
println!("{}", reverse("pão"));
}

Saída:

amor
álo
oãp
▶ Experimente você mesmo

Por que .chars() e não os bytes? Uma String em Rust é UTF-8, então um caractere como ã ocupa dois bytes. Inverter os bytes quebraria o caractere e geraria um texto inválido. .chars() trabalha com caracteres inteiros. Veja Strings em Rust para saber mais.

Escreva count_vowels(s: &str) -> usize, que conta as vogais (a, e, i, o, u) de uma string. Minúsculas e maiúsculas contam.

Dica: .chars().filter(...).count(). Uma string como "aeiouAEIOU" tem o método .contains(c), que você pode usar para testar cada caractere.

Ver solução
fn count_vowels(s: &str) -> usize {
s.chars().filter(|c| "aeiouAEIOU".contains(*c)).count()
}
fn main() {
println!("{}", count_vowels("Oi, Mundo"));
println!("{}", count_vowels("Rust e divertido"));
println!("{}", count_vowels("AEIOU e mais nada"));
println!("{}", count_vowels("Psst"));
}

Saída:

4
6
10
0
▶ Experimente você mesmo

O filter passa para a closure uma referência a cada item (&char), por isso escrevemos *c para pegar o próprio char. As páginas Closures em Rust e Iteradores em Rust explicam esse padrão.

Atenção: vogais com acento (á, ã, é…) são caracteres diferentes, então esta versão não conta essas vogais. Se quiser contá-las, é só acrescentá-las à string "aeiouAEIOU".

Este programa não compila. Mude só o parâmetro da função shout e as duas chamadas para que ele imprima OLÁ! duas vezes.

fn shout(text: String) {
println!("{}!", text.to_uppercase());
}
fn main() {
let word = String::from("olá");
shout(word);
shout(word);
}
error[E0382]: use of moved value: `word`
--> src/main.rs:8:11
|
6 | let word = String::from("olá");
| ---- 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

O compilador avisa, em inglês, que word foi usada depois de ser movida (moved): a primeira chamada levou o valor embora, e a segunda tenta usá-lo de novo.

Dica: shout só precisa olhar o texto, não ser dona dele. Veja Ownership (posse).

Ver solução
fn shout(text: &str) {
println!("{}!", text.to_uppercase());
}
fn main() {
let word = String::from("olá");
shout(&word);
shout(&word);
}

Saída:

OLÁ!
OLÁ!
▶ Experimente você mesmo

Quando uma função recebe String, chamá-la move a string para dentro dela, e o main não consegue mais usá-la. Receber &str significa que a função só pega a string emprestada. O compilador também sugere word.clone(), que funciona, mas copia a string inteira sem necessidade. Pegar emprestado é a correção melhor aqui.

Este programa também não compila. Faça com que ele imprima Ana, mas continue adicionando Ben à lista.

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

Em português: não dá para pegar names emprestado como mutável porque ele já está emprestado como imutável.

Dica: o problema é a ordem. first ainda está em uso quando o push altera o vetor. Veja Borrowing (empréstimo).

Ver solução
fn main() {
let mut names = vec![String::from("Ana")];
names.push(String::from("Ben"));
let first = &names[0];
println!("{first}");
}

Saída:

Ana
▶ Experimente você mesmo

Por que o Rust é tão rígido aqui? O push pode precisar de mais espaço, e aí o vetor move todos os itens para um lugar novo e maior na memória. Nesse caso, first apontaria para o lugar antigo, que não é mais válido. A regra do Rust (vários leitores ou um escritor, nunca os dois ao mesmo tempo) impede esse bug antes mesmo de o programa rodar. Pegar a referência depois do push resolve.

Escreva first_word(s: &str) -> &str, que devolve a primeira palavra (tudo antes do primeiro espaço). Se não houver espaço, devolva a string inteira. Não crie uma String nova: devolva um slice (fatia) da original.

Dica: s.find(' ') devolve um Option<usize> com a posição do primeiro espaço. &s[..i] é um slice do começo até i.

Ver solução
fn first_word(s: &str) -> &str {
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
}
fn main() {
println!("{:?}", first_word("oi mundo"));
println!("{:?}", first_word("Rust"));
println!("{:?}", first_word(""));
}

Saída:

"oi"
"Rust"
""
▶ Experimente você mesmo

O &str devolvido aponta para a mesma memória da entrada, então nada é copiado. O compilador sabe que o resultado não pode viver mais do que a entrada. É a elisão de lifetimes em ação: com uma referência entrando e uma saindo, você não precisa escrever os lifetimes.

Uma versão mais curta é s.split_whitespace().next().unwrap_or(""), que também pula espaços extras no começo.

Escreva largest(numbers: &[i32]) -> Option<i32>. Ela devolve o maior número, ou None se a lista estiver vazia. Desta vez, não use o .max() que já vem pronto: escreva o laço você mesmo.

Dica: comece com o primeiro item como “o maior até agora”. numbers.first() devolve um Option, então ele já cuida do caso da lista vazia para você.

Ver solução
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(&[]));
}

Saída:

Some(41)
Some(-2)
None
▶ Experimente você mesmo

numbers.first()? devolve None logo de cara se a lista estiver vazia. É por isso que a função devolve Option: “o maior de nada” não tem uma boa resposta, e o Rust obriga você a deixar isso claro. Em código de verdade, numbers.iter().max() faz o mesmo trabalho.

Começar com 0 em vez do primeiro item é um bug clássico: a lista [-5, -2, -9] daria 0, o que está errado.

Conte quantas vezes cada palavra aparece em uma frase. Ignore maiúsculas/minúsculas e pontuação, e imprima as palavras em ordem alfabética.

Dica: use um BTreeMap<String, usize>. Ele funciona como um HashMap, mas mantém as chaves ordenadas. map.entry(word).or_insert(0) entrega um contador para você somar 1.

Ver solução
use std::collections::BTreeMap;
fn main() {
let text = "O gato viu o rato. O fim!";
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}");
}
}

Saída:

fim: 1
gato: 1
o: 3
rato: 1
viu: 1
▶ Experimente você mesmo

Por que BTreeMap e não HashMap? Um HashMap entrega os itens em uma ordem imprevisível, que pode mudar de uma execução para outra. Se você precisa da saída ordenada, use um BTreeMap, ou coloque o conteúdo do HashMap em um Vec e ordene.

trim_matches tira o . de rato. e o ! de fim!, e to_lowercase faz O e o virarem a mesma palavra.

Um detalhe: o BTreeMap ordena pelos bytes do texto, então palavras com acento (como água) iriam para o fim da lista, depois do z. Por isso a frase do exemplo não tem acentos.

A partir de vec![1, 2, 3, 4, 5, 6, 7, 8], crie um vetor novo só com os quadrados dos números pares: [4, 16, 36, 64]. Depois imprima a soma deles. Use métodos de iterador, não um laço for.

Dica: encadeie .iter(), .filter(...), .map(...) e .collect(). Depois, .iter().sum() soma tudo.

Ver solução
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!("Soma: {total}");
}

Saída:

[4, 16, 36, 64]
Soma: 120
▶ Experimente você mesmo

Leia a cadeia de cima para baixo como uma receita: pegue os números, fique com os pares, eleve cada um ao quadrado e coloque tudo em um Vec. Iteradores são preguiçosos (lazy): nada roda de verdade até o .collect() pedir os resultados. Veja Iteradores em Rust.

Escreva sum_numbers(input: &str) -> Result<i32, ParseIntError>. Ela recebe um texto como "1, 2, 3", converte cada pedaço em número e devolve a soma. Se algum pedaço não for um número, devolva o erro em vez de derrubar o programa.

Dica: divida o texto em ',', aplique .trim() em cada pedaço e depois .parse(). Use o operador ? para retornar mais cedo quando houver erro.

Ver solução
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, cinco, 6", ""] {
match sum_numbers(input) {
Ok(sum) => println!("{input:?} -> a soma é {sum}"),
Err(e) => println!("{input:?} -> erro: {e}"),
}
}
}

Saída:

"1, 2, 3" -> a soma é 6
"10,20, 30" -> a soma é 60
"4, cinco, 6" -> erro: invalid digit found in string
"" -> erro: cannot parse integer from empty string
▶ Experimente você mesmo

As mensagens de erro saem em inglês porque são escritas pela própria biblioteca padrão do Rust: “invalid digit found in string” quer dizer que apareceu um caractere que não é dígito, e “cannot parse integer from empty string” quer dizer que não dá para converter uma string vazia em número.

O ? faz duas coisas com um único caractere: se parse() der Ok(n), você recebe o n. Se der Err(e), a função devolve Err(e) na hora. É por isso que a função também precisa devolver um Result.

Bônus: Result funciona com .sum(), então esta única linha faz a mesma coisa e para no primeiro erro:

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

Cada usuário pode ter ou não um e-mail. Escreva email_domain(user: &User) -> Option<&str>, que devolve a parte depois do @. Devolva None se não houver e-mail ou se o e-mail não tiver @. Depois, procure usuários pelo nome e imprima o domínio de cada um.

Dica: user.email.as_deref() transforma um Option<String> em um Option<&str>. email.split_once('@') devolve Option<(&str, &str)>. O operador ? também funciona com Option. Veja Option em Rust.

Ver solução
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-em-casa")) },
];
for name in ["Ana", "Ben", "Cleo", "Dan"] {
let domain = users
.iter()
.find(|u| u.name == name)
.and_then(email_domain)
.unwrap_or("(sem domínio)");
println!("{name}: {domain}");
}
}

Saída:

Ana: example.com
Ben: (sem domínio)
Cleo: (sem domínio)
Dan: (sem domínio)
▶ Experimente você mesmo

Aqui existem três jeitos diferentes de “falhar”: Dan não existe, Ben não tem e-mail e o e-mail da Cleo não tem @. Cada um é uma etapa com Option, e o ? ou o .and_then() param no primeiro None. Nada de verificações com if, nada de erros de ponteiro nulo. No finalzinho, o .unwrap_or(...) dá um valor padrão.

Crie um struct Account com um titular (owner) e um saldo (balance). Dê a ele os métodos deposit(amount) e withdraw(amount). O withdraw deve devolver Result<u64, String>: o novo saldo se deu certo, ou uma mensagem de erro se não houver dinheiro suficiente. O saldo nunca pode ficar abaixo de zero.

Dica: métodos que alteram a conta recebem &mut self. Verifique o saldo antes de subtrair. Veja Structs em Rust e Result em Rust.

Ver solução
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!(
"não é possível sacar {amount}: o saldo é só {}",
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!("Sacou {amount}, restam {left}"),
Err(e) => println!("Erro: {e}"),
}
}
println!("{} tem {} no final", account.owner, account.balance);
}

Saída:

Sacou 30, restam 70
Sacou 50, restam 20
Erro: não é possível sacar 40: o saldo é só 20
Ana tem 20 no final
▶ Experimente você mesmo

Usar u64 (um número sem sinal) é uma rede de segurança extra: ele simplesmente não consegue guardar um saldo negativo. Se você esquecesse a verificação, self.balance -= amount entraria em panic no modo debug com a mensagem “attempt to subtract with overflow” (tentativa de subtrair com estouro), em vez de ficar negativo sem avisar.

Defina um trait Shape (forma) com dois métodos: area(&self) -> f64 e name(&self) -> String. Implemente esse trait para um Circle (com um raio) e um Rectangle (com largura e altura). Coloque os dois em um mesmo Vec e imprima o nome e a área de cada um com 2 casas decimais.

Dica: para guardar tipos diferentes em um único Vec, use Vec<Box<dyn Shape>>. Veja Traits em Rust e Trait objects (dyn). {:.2} imprime 2 casas decimais.

Ver solução
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!("Círculo (r = {})", self.radius)
}
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn name(&self) -> String {
format!("Retângulo ({} 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!("Área total: {total:.2}");
}

Saída:

Círculo (r = 1): 3.14
Retângulo (3 x 4): 12.00
Círculo (r = 2.5): 19.63
Área total: 34.78
▶ Experimente você mesmo

O laço não sabe (nem se importa) qual forma ele tem nas mãos. Ele só sabe que cada item “é um Shape”, então pode chamar area() e name(). Para acrescentar um Triangle depois, basta escrever um impl novo, e o laço não muda nada.

Volte ao exercício 7, mas faça a função funcionar com qualquer tipo que possa ser comparado: inteiros, decimais, caracteres e strings. A assinatura é largest<T: PartialOrd>(items: &[T]) -> Option<&T>.

Dica: T: PartialOrd quer dizer “T aceita < e >”. Devolva uma referência (&T) para não precisar copiar nem clonar os itens. Veja Genéricos em Rust.

Ver solução
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(&["pera", "abacate", "uva"]));
let empty: Vec<i32> = Vec::new();
println!("{:?}", largest(&empty));
}

Saída:

Some(41)
Some(9.75)
Some('u')
Some("uva")
None
▶ Experimente você mesmo

O corpo é quase igual ao do exercício 7. A única mudança de verdade é a parte <T: PartialOrd>. Sem essa restrição (bound), o compilador recusa item > biggest, porque nem todo tipo pode ser comparado. Strings são comparadas em ordem alfabética, e é por isso que "uva" ganha.

Repare no Vec<i32> do vetor vazio: sem nenhum item, o Rust não tem como adivinhar o que é T, então você precisa dizer.

Uma lista de convidados tem alguns nomes escritos mais de uma vez. Escreva find_repeats(words: &[&str]) -> Vec<String>, que devolve todo nome que aparece mais de uma vez, cada um listado uma única vez, em ordem alfabética. Mostre também quantos convidados diferentes existem.

Dica: um HashSet lembra o que você já viu. O método insert dele devolve false quando o valor já estava lá. Um HashSet não tem uma ordem fixa, então ordene o resultado no final.

Ver solução
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 devolve false se a palavra já estava no conjunto
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", "bia", "caio", "bia", "davi", "ana", "bia"];
println!("Repetidos: {:?}", find_repeats(&guests));
let unique: HashSet<&str> = guests.iter().copied().collect();
println!("Convidados diferentes: {}", unique.len());
println!("Repetidos: {:?}", find_repeats(&["x", "y", "z"]));
}

Saída:

Repetidos: ["ana", "bia"]
Convidados diferentes: 4
Repetidos: []
▶ Experimente você mesmo

Por que dois conjuntos? O seen guarda todos os nomes vistos até agora. O repeats guarda os nomes que encontramos pela segunda vez. "bia" aparece três vezes, mas, como repeats é um conjunto, ela só é guardada uma vez.

Coletar em um HashSet também é o jeito mais rápido de contar valores únicos: os duplicados simplesmente somem. Sem a linha result.sort(), a ordem dos repetidos poderia mudar de uma execução para outra.

Ordene uma lista de palavras da mais curta para a mais longa. Quando duas palavras tiverem o mesmo tamanho, coloque-as em ordem alfabética, ignorando maiúsculas e minúsculas. Depois compare o seu resultado com um .sort() simples.

Dica: sort_by recebe uma closure que compara dois itens e devolve um Ordering. O .then_with(...) acrescenta uma segunda comparação, que só conta em caso de empate. Veja Ordenação e busca.

Ver solução
fn main() {
let mut words = vec!["pear", "Fig", "banana", "apple", "kiwi", "Cherry", "date"];
let mut plain = words.clone();
plain.sort();
println!("Ordem simples: {plain:?}");
words.sort_by(|a, b| {
a.len()
.cmp(&b.len())
.then_with(|| a.to_lowercase().cmp(&b.to_lowercase()))
});
println!("Por tamanho: {words:?}");
}

Saída:

Ordem simples: ["Cherry", "Fig", "apple", "banana", "date", "kiwi", "pear"]
Por tamanho: ["Fig", "date", "kiwi", "pear", "apple", "banana", "Cherry"]
▶ Experimente você mesmo

As palavras ficaram em inglês de propósito: .len() conta bytes, e letras acentuadas como “ç” ou “ã” ocupam 2 bytes cada, o que mudaria o resultado.

Olhe a ordem simples: "Cherry" e "Fig" vêm primeiro. Strings são comparadas pelos códigos dos caracteres, e toda letra maiúscula vem antes de toda minúscula. É por isso que comparamos as versões em to_lowercase() no desempate.

Um jeito mais curto de escrever a mesma regra é words.sort_by_key(|w| (w.len(), w.to_lowercase())). Tuplas são comparadas campo por campo, então o tamanho decide primeiro e a palavra em minúsculas desempata.

Escreva shipping_cost(p: Package) -> String para este enum, usando um único match:

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

As regras: retirada (Pickup) é grátis. Cartas de até 20 g custam R$ 1, cartas mais pesadas custam R$ 3. Encomendas (Parcel) acima de 30 kg não podem ser enviadas. Encomendas expressas custam R$ 10 mais R$ 2 por kg. As outras encomendas custam R$ 5 mais R$ 1 por kg.

Dica: um braço do match pode ter um if depois do padrão, chamado de guard (guarda): Package::Letter { grams } if grams <= 20 => .... Os braços são verificados de cima para baixo, então coloque a regra de “pesado demais” antes da regra do expresso. Veja Pattern matching.

Ver solução
enum Package {
Letter { grams: u32 },
Parcel { kg: u32, express: bool },
Pickup,
}
fn shipping_cost(p: Package) -> String {
match p {
Package::Pickup => String::from("grátis (retirada)"),
Package::Letter { grams } if grams <= 20 => String::from("R$ 1 (carta simples)"),
Package::Letter { grams } => format!("R$ 3 (carta grande, {grams} g)"),
Package::Parcel { kg, .. } if kg > 30 => format!("pesado demais para enviar ({kg} kg)"),
Package::Parcel { kg, express: true } => format!("R$ {} (expresso)", 10 + kg * 2),
Package::Parcel { kg, express: false } => format!("R$ {} (encomenda normal)", 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));
}
}

Saída:

R$ 1 (carta simples)
R$ 3 (carta grande, 80 g)
R$ 9 (encomenda normal)
R$ 18 (expresso)
pesado demais para enviar (35 kg)
grátis (retirada)
▶ Experimente você mesmo

A encomenda expressa de 35 kg é a mais interessante. Ela combina com dois braços, mas só o primeiro vale. Se você colocasse o braço do expresso acima do braço de “pesado demais”, ele cobraria alegremente R$ 80 por uma encomenda que nem pode ser enviada.

Repare que o último braço diz express: false em vez de ... Isso não é obrigatório, mas deixa o braço dizer exatamente o que ele trata. O compilador continua verificando se todos os casos foram cobertos. Os guards não entram nessa verificação, e é por isso que o braço Package::Letter { grams } sem guard precisa estar lá.

O relatório de um depósito tem um item por linha, como "maçãs 12". Algumas linhas estão quebradas: vazias, ou com uma quantidade que não é número. Usando métodos de iteradores (sem laço for), fique só com as linhas que podem ser convertidas e têm quantidade acima de zero. Mostre-as como nome: quantidade, separadas por vírgulas, e depois mostre o total.

Dica: .lines() divide o texto em linhas. filter_map mantém só os valores Some que a closure devolve, então filtra e converte num passo só. .ok() transforma um Result em um Option. Veja Iteradores em Rust.

Ver solução
fn main() {
let report = "maçãs 12\nbananas x\nperas 0\n\ncerejas 40\nameixas 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 de itens: {total}");
}

Saída:

maçãs: 12, cerejas: 40, ameixas: 7
Total de itens: 59
▶ Experimente você mesmo

Acompanhe uma linha de cada vez. A linha vazia não tem espaço, então o split_once devolve None e o primeiro filter_map a descarta. "bananas x" se divide sem problema, mas "x" não vira número, então o segundo filter_map a descarta. "peras 0" passa pelos dois, e aí o filter a remove porque a quantidade é zero.

Fazemos collect em um Vec uma vez e depois percorremos ele duas vezes: uma para montar o texto e outra para o total. Um iterador só pode ser usado uma vez, então guardar os dados já limpos é mais simples do que converter tudo duas vezes.

Se você terminou os 18, parabéns. Isso é progresso de verdade. 🦀

Todos os links abaixo estão em inglês.

  • Rustlings: o conjunto oficial de pequenos programas quebrados que você corrige um por um, no terminal. Um ótimo próximo passo depois desta página.
  • Trilha de Rust do Exercism: cerca de 100 problemas com testes, e feedback gratuito de mentores humanos sobre as suas soluções.
  • Rust by Example: exemplos curtos para rodar, para cada tema. Ajuda quando você trava na sintaxe de um exercício.
  • Trait Iterator — documentação da std: todos os métodos de iteradores (filter_map, fold, zip e muitos outros), cada um com um exemplo pequeno.
  • HashSet — documentação da std: todos os métodos de conjuntos, incluindo union, intersection e difference.