Ir al contenido

Ejercicios de Rust

Leer sobre Rust está bien. Escribir Rust es lo que hace que se te quede. En esta página tienes 18 problemas pequeños, ordenados de más fácil a más difícil y agrupados por tema.

En cada uno, lee el enunciado, inténtalo tú primero y solo después abre la solución. Es totalmente normal atascarse o ver errores del compilador. Forma parte de aprender, y los mensajes de error de Rust suelen ser la mejor pista que vas a tener.

Cómo practicar: abre el Rust Playground en otra pestaña o crea un proyecto con cargo new practice (mira Empezar con Rust). Escribe ahí tu respuesta y luego compárala con la solución. Si tu respuesta es distinta pero imprime lo mismo, seguramente está bien. Casi siempre hay más de una buena forma de hacerlo.

Imprime los números del 1 al 15. Pero en los múltiplos de 3 imprime Fizz, en los múltiplos de 5 imprime Buzz, y en los múltiplos de ambos imprime FizzBuzz.

Pista: usa un bucle for sobre 1..=15 y el operador % (resto de la división). Comprueba “múltiplo de ambos” primero.

Ver solución
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}");
}
}
}

Salida:

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
▶ Pruébalo tú mismo

El orden importa. Si compruebas n % 3 == 0 primero, el 15 imprime Fizz y nunca llega a la rama de FizzBuzz. Un número divisible entre 3 y entre 5 es divisible entre 15.

Escribe una función reverse(s: &str) -> String que devuelva el texto al revés. "roma" debe convertirse en "amor".

Pista: .chars() te da los caracteres uno a uno, .rev() invierte un iterador y .collect() puede construir un String.

Ver solución
fn reverse(s: &str) -> String {
s.chars().rev().collect()
}
fn main() {
println!("{}", reverse("roma"));
println!("{}", reverse("hola"));
println!("{}", reverse("año"));
}

Salida:

amor
aloh
oña
▶ Pruébalo tú mismo

¿Por qué .chars() y no los bytes? Un String de Rust está en UTF-8, así que un carácter como ñ ocupa dos bytes. Si invirtieras los bytes, lo partirías y el texto dejaría de ser válido. .chars() trabaja con caracteres completos. Mira Strings para saber más.

Escribe count_vowels(s: &str) -> usize, que cuente las vocales (a, e, i, o, u) de una cadena de texto. Cuentan tanto las minúsculas como las mayúsculas.

Pista: .chars().filter(...).count(). Una cadena como "aeiouAEIOU" tiene un método .contains(c) que puedes usar para comprobar cada carácter.

Ver solución
fn count_vowels(s: &str) -> usize {
s.chars().filter(|c| "aeiouAEIOU".contains(*c)).count()
}
fn main() {
println!("{}", count_vowels("Hola, Mundo"));
println!("{}", count_vowels("Rust es divertido"));
println!("{}", count_vowels("AEIOU y a veces la y"));
println!("{}", count_vowels("Psst"));
}

Salida:

4
6
9
0
▶ Pruébalo tú mismo

filter le pasa a la closure una referencia a cada elemento (&char), por eso escribimos *c para obtener el char en sí. Las páginas de Closures e Iteradores explican este patrón.

Ojo: las vocales con tilde (á, é…) son caracteres distintos, así que esta versión no las cuenta. Si quieres contarlas, añádelas a la cadena "aeiouAEIOU".

Este programa no compila. Cambia solo el parámetro de la función shout y las dos llamadas para que imprima HOLA! dos veces.

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

El compilador te dice que word se movió (move) a la función en la línea 7, así que en la línea 8 ya no puedes usarla.

Pista: shout solo necesita mirar el texto, no ser su dueña. Mira Ownership.

Ver solución
fn shout(text: &str) {
println!("{}!", text.to_uppercase());
}
fn main() {
let word = String::from("hola");
shout(&word);
shout(&word);
}

Salida:

HOLA!
HOLA!
▶ Pruébalo tú mismo

Cuando una función recibe un String, al llamarla le mueves el string, y main ya no puede volver a usarlo. Si recibe &str, la función solo lo toma prestado. El compilador también sugiere word.clone(), que también funciona, pero copia todo el string sin necesidad. Aquí lo mejor es el borrowing (préstamo).

Este programa tampoco compila. Haz que imprima Ana, pero sin dejar de añadir Ben a la 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

El mensaje dice que no puedes tomar names prestado como mutable (para el push) mientras sigue prestado como inmutable (en first), porque first se usa después.

Pista: el problema es el orden. first todavía está en uso cuando push modifica el vector. Mira Borrowing.

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

Salida:

Ana
▶ Pruébalo tú mismo

¿Por qué Rust es tan estricto aquí? Puede que push necesite más espacio, y entonces el vector mueve todos sus elementos a otro sitio de la memoria, más grande. En ese caso, first apuntaría al sitio antiguo, que ya no es válido. La regla de Rust (muchos lectores o un solo escritor, nunca las dos cosas a la vez) frena ese error antes de que tu programa llegue a ejecutarse. Tomar la referencia después del push lo soluciona.

Escribe first_word(s: &str) -> &str, que devuelva la primera palabra (todo lo que hay antes del primer espacio). Si no hay ningún espacio, devuelve la cadena entera. No crees un String nuevo: devuelve un slice (porción) de la original.

Pista: s.find(' ') devuelve un Option<usize> con la posición del primer espacio. &s[..i] es un slice desde el principio hasta i.

Ver solución
fn first_word(s: &str) -> &str {
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
}
fn main() {
println!("{:?}", first_word("hola mundo"));
println!("{:?}", first_word("Rust"));
println!("{:?}", first_word(""));
}

Salida:

"hola"
"Rust"
""
▶ Pruébalo tú mismo

El &str que devuelve apunta a la misma memoria que la entrada, así que no se copia nada. El compilador sabe que el resultado no puede vivir más que la entrada. Eso es la elisión de lifetimes en acción: con una referencia de entrada y una de salida, no tienes que escribir los lifetimes tú.

Una versión más corta es s.split_whitespace().next().unwrap_or(""), que además se salta los espacios extra del principio.

Escribe largest(numbers: &[i32]) -> Option<i32>. Devuelve el número más grande, o None si la lista está vacía. Esta vez no uses el método .max() que ya viene incluido: escribe tú el bucle.

Pista: empieza con el primer elemento como “el más grande hasta ahora”. numbers.first() devuelve un Option, así que se encarga del caso de la lista vacía por ti.

Ver solución
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(&[]));
}

Salida:

Some(41)
Some(-2)
None
▶ Pruébalo tú mismo

numbers.first()? devuelve None antes de tiempo si la lista está vacía. Por eso la función devuelve un Option: “el más grande de nada” no tiene una buena respuesta, y Rust te obliga a decirlo. En código real, numbers.iter().max() hace lo mismo.

Empezar con 0 en lugar del primer elemento es un error clásico: la lista [-5, -2, -9] daría 0, que es incorrecto.

Cuenta cuántas veces aparece cada palabra en una frase. No distingas entre mayúsculas y minúsculas, ignora los signos de puntuación e imprime las palabras en orden alfabético.

Pista: usa un BTreeMap<String, usize>. Funciona como un HashMap, pero mantiene sus claves ordenadas. map.entry(word).or_insert(0) te da un contador al que sumarle 1.

Ver solución
use std::collections::BTreeMap;
fn main() {
let text = "El gato y el perro comen. ¡El fin!";
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}");
}
}

Salida:

comen: 1
el: 3
fin: 1
gato: 1
perro: 1
y: 1
▶ Pruébalo tú mismo

¿Por qué BTreeMap y no HashMap? Un HashMap te devuelve sus elementos en un orden impredecible, que puede cambiar de una ejecución a otra. Si necesitas la salida ordenada, usa un BTreeMap, o pasa el HashMap a un Vec y ordénalo.

trim_matches quita el . de comen., y el ¡ y el ! de ¡El y fin!. to_lowercase hace que El y el cuenten como la misma palabra.

A partir de vec![1, 2, 3, 4, 5, 6, 7, 8], crea un vector nuevo solo con los cuadrados de los números pares: [4, 16, 36, 64]. Después imprime su suma. Usa métodos de iteradores, no un bucle for.

Pista: encadena .iter(), .filter(...), .map(...) y .collect(). Luego .iter().sum() los suma.

Ver solución
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!("Suma: {total}");
}

Salida:

[4, 16, 36, 64]
Suma: 120
▶ Pruébalo tú mismo

Lee la cadena de arriba abajo como una receta: toma los números, quédate con los pares, eleva cada uno al cuadrado y mételos en un Vec. Los iteradores son perezosos (lazy): no se ejecuta nada hasta que .collect() pide los resultados. Mira Iteradores.

Escribe sum_numbers(input: &str) -> Result<i32, ParseIntError>. Recibe un texto como "1, 2, 3", convierte cada trozo en un número y devuelve la suma. Si algún trozo no es un número, devuelve el error en lugar de hacer que el programa falle.

Pista: divide el texto por ',', aplica .trim() a cada trozo y luego .parse(). Usa el operador ? para salir antes cuando haya un error.

Ver solución
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:?} -> la suma es {sum}"),
Err(e) => println!("{input:?} -> error: {e}"),
}
}
}

Salida:

"1, 2, 3" -> la suma es 6
"10,20, 30" -> la suma es 60
"4, cinco, 6" -> error: invalid digit found in string
"" -> error: cannot parse integer from empty string
▶ Pruébalo tú mismo

Los mensajes de error salen en inglés porque los escribe la propia biblioteca estándar de Rust: “invalid digit found in string” significa que encontró un carácter que no es un dígito, y “cannot parse integer from empty string” significa que no se puede convertir una cadena vacía en un número.

? hace dos cosas con un solo carácter: si parse() da Ok(n), obtienes n. Si da Err(e), la función devuelve Err(e) en ese mismo momento. Por eso la función también tiene que devolver un Result.

Extra: Result funciona con .sum(), así que esta sola línea hace lo mismo y se detiene en el primer error:

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

Cada usuario puede tener un email o no. Escribe email_domain(user: &User) -> Option<&str>, que devuelva la parte que va después de la @. Devuelve None si no hay email o si el email no tiene @. Después busca usuarios por su nombre e imprime su dominio.

Pista: user.email.as_deref() convierte un Option<String> en un Option<&str>. email.split_once('@') devuelve un Option<(&str, &str)>. El operador ? también funciona con Option. Mira Option.

Ver solución
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-en-casa")) },
];
for name in ["Ana", "Ben", "Cleo", "Dan"] {
let domain = users
.iter()
.find(|u| u.name == name)
.and_then(email_domain)
.unwrap_or("(sin dominio)");
println!("{name}: {domain}");
}
}

Salida:

Ana: example.com
Ben: (sin dominio)
Cleo: (sin dominio)
Dan: (sin dominio)
▶ Pruébalo tú mismo

Aquí hay tres formas distintas de “fallar”: Dan no existe, Ben no tiene email y el email de Cleo no tiene @. Cada una es un paso con Option, y ? o .and_then() se detienen en el primer None. Sin comprobaciones con if y sin errores de puntero nulo. Al final del todo, .unwrap_or(...) da un valor por defecto.

Crea un struct Account con un titular (owner) y un saldo (balance). Dale los métodos deposit(amount) y withdraw(amount). withdraw debe devolver Result<u64, String>: el nuevo saldo si salió bien, o un mensaje de error si no hay suficiente dinero. El saldo nunca puede quedar por debajo de cero.

Pista: los métodos que modifican la cuenta reciben &mut self. Comprueba el saldo antes de restar. Mira Structs y Result.

Ver solución
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!(
"no se puede retirar {amount}: el saldo es solo {}",
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!("Retiraste {amount}, quedan {left}"),
Err(e) => println!("Error: {e}"),
}
}
println!("{} tiene {} al final", account.owner, account.balance);
}

Salida:

Retiraste 30, quedan 70
Retiraste 50, quedan 20
Error: no se puede retirar 40: el saldo es solo 20
Ana tiene 20 al final
▶ Pruébalo tú mismo

Usar u64 (un número sin signo) es una red de seguridad extra: directamente no puede guardar un saldo negativo. Si se te olvidara la comprobación, self.balance -= amount haría panic en modo debug con el mensaje “attempt to subtract with overflow” (intento de restar con desbordamiento), en lugar de quedar en negativo sin avisar.

Define un trait Shape (forma) con dos métodos: area(&self) -> f64 y name(&self) -> String. Impleméntalo para un Circle (con un radio) y para un Rectangle (con un ancho y un alto). Mete los dos en un mismo Vec e imprime el nombre y el área de cada uno con 2 decimales.

Pista: para guardar tipos distintos en un mismo Vec, usa Vec<Box<dyn Shape>>. Mira Traits y Trait objects. {:.2} imprime 2 decimales.

Ver solución
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!("Rectá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}");
}

Salida:

Círculo (r = 1): 3.14
Rectángulo (3 x 4): 12.00
Círculo (r = 2.5): 19.63
Área total: 34.78
▶ Pruébalo tú mismo

El bucle no sabe ni le importa qué forma tiene delante. Solo sabe que cada elemento “es un Shape”, así que puede llamar a area() y a name(). Si más adelante añades un Triangle, solo tienes que escribir un impl nuevo, y el bucle no cambia en absoluto.

Vuelve al ejercicio 7, pero haz que funcione con cualquier tipo que se pueda comparar: enteros, decimales, caracteres y cadenas de texto. La firma es largest<T: PartialOrd>(items: &[T]) -> Option<&T>.

Pista: T: PartialOrd significa “T admite < y >”. Devuelve una referencia (&T) para no tener que copiar ni clonar los elementos. Mira Genéricos.

Ver solución
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", "manzana", "zanahoria"]));
let empty: Vec<i32> = Vec::new();
println!("{:?}", largest(&empty));
}

Salida:

Some(41)
Some(9.75)
Some('u')
Some("zanahoria")
None
▶ Pruébalo tú mismo

El cuerpo es casi igual que en el ejercicio 7. El único cambio de verdad es la parte <T: PartialOrd>. Sin esa restricción (bound), el compilador rechaza item > biggest, porque no todos los tipos se pueden comparar. Las cadenas de texto se comparan por orden alfabético, y por eso gana "zanahoria".

Fíjate en el Vec<i32> del vector vacío: como no tiene elementos, Rust no puede adivinar qué es T, así que tienes que decírselo tú.

Conjuntos, ordenación, patrones e iteradores

Sección titulada «Conjuntos, ordenación, patrones e iteradores»

En una lista de invitados hay nombres escritos más de una vez. Escribe find_repeats(words: &[&str]) -> Vec<String>, que devuelva cada nombre que aparece más de una vez, una sola vez cada uno y en orden alfabético. Imprime también cuántos invitados distintos hay.

Pista: un HashSet recuerda lo que ya has visto. Su método insert devuelve false cuando el valor ya estaba dentro. Un HashSet no tiene un orden fijo, así que ordena el resultado al final.

Ver solución
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 devuelve false si la palabra ya estaba en el 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", "ben", "cleo", "ben", "dan", "ana", "ben"];
println!("Repetidos: {:?}", find_repeats(&guests));
let unique: HashSet<&str> = guests.iter().copied().collect();
println!("Invitados distintos: {}", unique.len());
println!("Repetidos: {:?}", find_repeats(&["x", "y", "z"]));
}

Salida:

Repetidos: ["ana", "ben"]
Invitados distintos: 4
Repetidos: []
▶ Pruébalo tú mismo

¿Por qué dos conjuntos? seen guarda todos los nombres vistos hasta ahora. repeats guarda los nombres que encontramos por segunda vez. "ben" aparece tres veces, pero como repeats es un conjunto, solo se guarda una vez.

Recoger los valores en un HashSet es también la forma más rápida de contar valores únicos: los duplicados simplemente desaparecen. Sin la línea result.sort(), el orden de los repetidos podría cambiar de una ejecución a otra.

Ordena una lista de palabras de la más corta a la más larga. Cuando dos palabras tengan la misma longitud, ponlas en orden alfabético sin distinguir entre mayúsculas y minúsculas. Después compara tu resultado con un simple .sort().

Pista: sort_by recibe un closure que compara dos elementos y devuelve un Ordering. .then_with(...) añade una segunda comparación que solo cuenta en caso de empate. Mira Ordenar y buscar.

Las palabras están en inglés a propósito: .len() cuenta bytes, y una letra con tilde como la á ocupa 2 bytes en UTF-8, lo que cambiaría el resultado.

Ver solución
fn main() {
let mut words = vec!["pear", "Fig", "banana", "apple", "kiwi", "Cherry", "date"];
let mut plain = words.clone();
plain.sort();
println!("Orden simple: {plain:?}");
words.sort_by(|a, b| {
a.len()
.cmp(&b.len())
.then_with(|| a.to_lowercase().cmp(&b.to_lowercase()))
});
println!("Por longitud: {words:?}");
}

Salida:

Orden simple: ["Cherry", "Fig", "apple", "banana", "date", "kiwi", "pear"]
Por longitud: ["Fig", "date", "kiwi", "pear", "apple", "banana", "Cherry"]
▶ Pruébalo tú mismo

Mira el orden simple: "Cherry" y "Fig" van primero. Las cadenas se comparan por los códigos de sus caracteres, y todas las letras mayúsculas van antes que todas las minúsculas. Por eso, en el desempate, comparamos las versiones con to_lowercase().

Una forma más corta de escribir la misma regla es words.sort_by_key(|w| (w.len(), w.to_lowercase())). Las tuplas se comparan campo a campo, así que primero decide la longitud y la palabra en minúsculas deshace los empates.

Escribe shipping_cost(p: Package) -> String para este enum, usando un solo match:

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

Las reglas: la recogida en tienda (Pickup) es gratis. Las cartas (Letter) de hasta 20 g cuestan $1, y las más pesadas cuestan $3. Los paquetes (Parcel) de más de 30 kg no se pueden enviar. Los paquetes exprés cuestan $10 más $2 por kg. Los demás paquetes cuestan $5 más $1 por kg.

Pista: una rama de match puede llevar un if después del patrón, llamado guarda (guard): Package::Letter { grams } if grams <= 20 => .... Las ramas se comprueban de arriba abajo, así que pon la regla de “demasiado pesado” antes que la de exprés. Mira Pattern matching.

Ver solución
enum Package {
Letter { grams: u32 },
Parcel { kg: u32, express: bool },
Pickup,
}
fn shipping_cost(p: Package) -> String {
match p {
Package::Pickup => String::from("gratis (recogida en tienda)"),
Package::Letter { grams } if grams <= 20 => String::from("$1 (carta normal)"),
Package::Letter { grams } => format!("$3 (carta grande, {grams} g)"),
Package::Parcel { kg, .. } if kg > 30 => format!("demasiado pesado para enviar ({kg} kg)"),
Package::Parcel { kg, express: true } => format!("${} (exprés)", 10 + kg * 2),
Package::Parcel { kg, express: false } => format!("${} (paquete 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));
}
}

Salida:

$1 (carta normal)
$3 (carta grande, 80 g)
$9 (paquete normal)
$18 (exprés)
demasiado pesado para enviar (35 kg)
gratis (recogida en tienda)
▶ Pruébalo tú mismo

El paquete exprés de 35 kg es el interesante. Encaja con dos ramas, pero solo gana la primera. Si pusieras la rama de exprés encima de la de “demasiado pesado”, cobraría tan tranquila $80 por un paquete que no se puede enviar.

Fíjate en que la última rama dice express: false en lugar de ... No es obligatorio, pero así la rama dice exactamente qué casos maneja. El compilador sigue comprobando que todos los casos están cubiertos. Las guardas no cuentan para esa comprobación, y por eso tiene que estar la rama Package::Letter { grams } sin guarda.

Un informe de almacén tiene un artículo por línea, como "manzanas 12". Algunas líneas están rotas: vacías, o con una cantidad que no es un número. Usando métodos de iterador (sin bucle for), quédate solo con las líneas que se pueden convertir y tienen una cantidad mayor que cero. Imprímelas como nombre: cantidad separadas por comas, y luego imprime el total.

Pista: .lines() divide el texto en líneas. filter_map se queda solo con los valores Some que devuelve su closure, así que puede filtrar y convertir en un solo paso. .ok() convierte un Result en un Option. Mira Iteradores.

Ver solución
fn main() {
let report = "manzanas 12\nkiwis x\nperas 0\n\ncerezas 40\nciruelas 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 artículos: {total}");
}

Salida:

manzanas: 12, cerezas: 40, ciruelas: 7
Total de artículos: 59
▶ Pruébalo tú mismo

Sigue una línea cada vez. La línea vacía no tiene espacio, así que split_once da None y el primer filter_map la descarta. "kiwis x" se divide bien, pero "x" no se puede convertir en número, así que la descarta el segundo filter_map. "peras 0" sobrevive a los dos, y luego filter la quita porque la cantidad es cero.

Hacemos collect en un Vec una sola vez y luego lo recorremos dos veces: una para construir el texto y otra para el total. Un iterador solo se puede usar una vez, así que guardar los datos ya limpios es más sencillo que convertirlo todo dos veces.

Si terminaste los 18, ¡felicidades! Es un avance de verdad. 🦀

  • Rustlings (en inglés): la colección oficial de pequeños programas rotos que vas arreglando uno a uno en tu terminal. Un gran siguiente paso después de esta página.
  • La ruta de Rust en Exercism (en inglés): unos 100 problemas con tests, y comentarios gratuitos sobre tus soluciones por parte de mentores humanos.
  • Rust by Example (en inglés): ejemplos cortos que puedes ejecutar, para cada tema. Muy útil cuando te atascas con la sintaxis de un ejercicio.
  • El trait Iterator — documentación de std (en inglés): todos los métodos de los iteradores (filter_map, fold, zip y muchos más), cada uno con un ejemplo pequeño.
  • HashSet — documentación de std (en inglés): todos los métodos de los conjuntos, incluidos union, intersection y difference.