Перейти к содержимому

Упражнения по Rust

Читать о Rust полезно. Но запоминается он, только когда пишешь код сам. На этой странице 18 небольших задач, они идут от простых к более сложным и сгруппированы по темам.

В каждой задаче сначала прочитай условие, попробуй решить сам и только потом открывай решение. Застрять или увидеть ошибки компилятора — совершенно нормально. Это часть обучения, а сообщения об ошибках в Rust часто оказываются лучшей подсказкой.

Как тренироваться: открой Rust Playground в соседней вкладке или создай проект командой cargo new practice (см. урок Начало работы). Напиши ответ там, а потом сравни с решением. Если твой ответ отличается, но печатает то же самое, скорее всего, он правильный. Хороших способов почти всегда больше одного.

Выведи числа от 1 до 15. Но вместо чисел, кратных 3, печатай Fizz, вместо кратных 5 — Buzz, а вместо кратных и 3, и 5 — FizzBuzz.

Подсказка: используй цикл for по 1..=15 и оператор % (остаток от деления). Проверку «кратно и тому, и другому» делай первой.

Показать решение
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}");
}
}
}

Вывод:

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
▶ Попробуй сам

Порядок важен. Если первой проверять n % 3 == 0, то для 15 напечатается Fizz, и до ветки FizzBuzz дело не дойдёт. Число, которое делится и на 3, и на 5, делится на 15.

Напиши функцию reverse(s: &str) -> String, которая возвращает текст задом наперёд. "сон" должен превратиться в "нос".

Подсказка: .chars() выдаёт символы по одному, .rev() разворачивает итератор, а .collect() умеет собрать String.

Показать решение
fn reverse(s: &str) -> String {
s.chars().rev().collect()
}
fn main() {
println!("{}", reverse("сон"));
println!("{}", reverse("hello"));
println!("{}", reverse("привет"));
}

Вывод:

нос
olleh
тевирп
▶ Попробуй сам

Почему .chars(), а не байты? String в Rust хранится в UTF-8, и каждая русская буква занимает два байта. Если развернуть байты, буквы развалятся, и получится некорректный текст. .chars() работает с целыми символами. Подробнее — в уроке Строки.

Напиши count_vowels(s: &str) -> usize, которая считает гласные (a, e, i, o, u) в строке. Считаются и строчные, и заглавные.

Подсказка: .chars().filter(...).count(). У строки вроде "aeiouAEIOU" есть метод .contains(c), которым можно проверить каждый символ.

Показать решение
fn count_vowels(s: &str) -> usize {
s.chars().filter(|c| "aeiouAEIOU".contains(*c)).count()
}
fn main() {
println!("{}", count_vowels("Hello, World"));
println!("{}", count_vowels("Rust is fun"));
println!("{}", count_vowels("AEIOU and sometimes y"));
println!("{}", count_vowels("rhythm"));
}

Вывод:

3
3
10
0
▶ Попробуй сам

Здесь мы считаем латинские гласные, поэтому и текст в примере английский. Для русского текста просто добавь в строку "аеёиоуыэюяАЕЁИОУЫЭЮЯ". filter передаёт замыканию ссылку на каждый элемент (&char), поэтому мы пишем *c, чтобы получить сам char. Этот приём объясняется в уроках Замыкания и Итераторы.

Эта программа не компилируется. Измени только параметр функции shout и два её вызова, чтобы программа дважды напечатала HELLO!.

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

Компилятор сообщает: «использование перемещённого значения word» — оно переместилось при первом вызове.

Подсказка: shout нужно только посмотреть на текст, а не владеть им. См. урок Владение.

Показать решение
fn shout(text: &str) {
println!("{}!", text.to_uppercase());
}
fn main() {
let word = String::from("hello");
shout(&word);
shout(&word);
}

Вывод:

HELLO!
HELLO!
▶ Попробуй сам

Когда функция принимает String, вызов перемещает строку внутрь, и main больше не может ею пользоваться. Если принимать &str, функция только заимствует строку. Компилятор предлагает ещё word.clone() — это тоже работает, но без всякой нужды копирует всю строку. Здесь заимствование — лучшее исправление.

Эта программа тоже не компилируется. Сделай так, чтобы она печатала Ana и при этом всё равно добавляла Ben в список.

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

Ошибка значит: «нельзя заимствовать names как изменяемую, потому что она уже заимствована как неизменяемая».

Подсказка: дело в порядке. first всё ещё используется, когда push меняет вектор. См. урок Заимствование.

Показать решение
fn main() {
let mut names = vec![String::from("Ana")];
names.push(String::from("Ben"));
let first = &names[0];
println!("{first}");
}

Вывод:

Ana
▶ Попробуй сам

Почему Rust здесь так строг? push может понадобиться больше места, и тогда вектор переносит все элементы в новое, большее место в памяти. После этого first указывал бы на старое место, которое уже недействительно. Правило Rust (много читателей или один писатель, но никогда одновременно) останавливает эту ошибку ещё до запуска программы. Если взять ссылку после push, проблема исчезает.

Напиши first_word(s: &str) -> &str, которая возвращает первое слово (всё до первого пробела). Если пробела нет, верни всю строку. Не создавай новую String: верни срез исходной строки.

Подсказка: s.find(' ') возвращает Option<usize> с позицией первого пробела. &s[..i] — это срез от начала до i.

Показать решение
fn first_word(s: &str) -> &str {
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
}
fn main() {
println!("{:?}", first_word("привет мир"));
println!("{:?}", first_word("Rust"));
println!("{:?}", first_word(""));
}

Вывод:

"привет"
"Rust"
""
▶ Попробуй сам

Возвращённый &str указывает в ту же память, что и входная строка, поэтому ничего не копируется. Компилятор знает, что результат не может пережить входные данные. Это работает элизия времён жизни (lifetime elision): когда на входе одна ссылка и на выходе одна, писать времена жизни самому не нужно. Кстати, find возвращает позицию в байтах, но это позиция пробела, поэтому срез по ней всегда попадает на границу символа — даже в русском тексте.

Версия покороче — s.split_whitespace().next().unwrap_or(""); она ещё и пропускает лишние пробелы в начале.

Напиши largest(numbers: &[i32]) -> Option<i32>. Она возвращает самое большое число или None, если список пуст. На этот раз не пользуйся встроенным .max(): напиши цикл сам.

Подсказка: начни с первого элемента как «самого большого на данный момент». numbers.first() возвращает Option, так что пустой случай он обработает за тебя.

Показать решение
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(&[]));
}

Вывод:

Some(41)
Some(-2)
None
▶ Попробуй сам

numbers.first()? сразу возвращает None, если список пуст. Поэтому функция и возвращает Option: у вопроса «какое самое большое из ничего?» нет хорошего ответа, и Rust заставляет это явно сказать. В реальном коде ту же работу делает numbers.iter().max().

Начинать с 0 вместо первого элемента — классическая ошибка: для списка [-5, -2, -9] получился бы неверный ответ 0.

Посчитай, сколько раз каждое слово встречается в предложении. Не обращай внимания на регистр и знаки препинания, а слова выведи в алфавитном порядке.

Подсказка: используй BTreeMap<String, usize>. Она работает как HashMap, но хранит ключи отсортированными. map.entry(word).or_insert(0) даёт счётчик, к которому можно прибавить 1.

Показать решение
use std::collections::BTreeMap;
fn main() {
let text = "The cat sat on the mat. The end!";
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}");
}
}

Вывод:

cat: 1
end: 1
mat: 1
on: 1
sat: 1
the: 3
▶ Попробуй сам

Текст здесь английский, чтобы алфавитный порядок совпадал с привычным: строки в Rust сравниваются по кодам символов. Почему BTreeMap, а не HashMap? HashMap выдаёт элементы в непредсказуемом порядке, который может меняться от запуска к запуску. Если нужен отсортированный вывод, бери BTreeMap или собери HashMap в Vec и отсортируй его.

trim_matches убирает . из mat. и ! из end!, а to_lowercase делает The и the одним и тем же словом.

Возьми vec![1, 2, 3, 4, 5, 6, 7, 8] и построй новый вектор из квадратов только чётных чисел: [4, 16, 36, 64]. Потом выведи их сумму. Используй методы итераторов, а не цикл for.

Подсказка: выстрой цепочку из .iter(), .filter(...), .map(...) и .collect(). А .iter().sum() сложит числа.

Показать решение
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!("Сумма: {total}");
}

Вывод:

[4, 16, 36, 64]
Сумма: 120
▶ Попробуй сам

Читай цепочку сверху вниз, как рецепт: взять числа, оставить чётные, возвести каждое в квадрат, сложить в Vec. Итераторы ленивые, поэтому на самом деле ничего не выполняется, пока .collect() не попросит результат. См. урок Итераторы.

Напиши sum_numbers(input: &str) -> Result<i32, ParseIntError>. Она берёт текст вроде "1, 2, 3", разбирает каждое число и возвращает сумму. Если какой-то кусок не число, верни ошибку, а не падай.

Подсказка: раздели строку по ',', к каждому куску примени .trim(), потом .parse(). Чтобы досрочно вернуть ошибку, используй оператор ?.

Показать решение
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, пять, 6", ""] {
match sum_numbers(input) {
Ok(sum) => println!("{input:?} -> сумма {sum}"),
Err(e) => println!("{input:?} -> ошибка: {e}"),
}
}
}

Вывод:

"1, 2, 3" -> сумма 6
"10,20, 30" -> сумма 60
"4, пять, 6" -> ошибка: invalid digit found in string
"" -> ошибка: cannot parse integer from empty string
▶ Попробуй сам

Тексты ошибок печатает сам Rust, поэтому они на английском: «в строке найдена недопустимая цифра» и «нельзя разобрать целое число из пустой строки». ? делает две вещи одним символом: если parse() вернул Ok(n), ты получаешь n. Если вернул Err(e), функция сразу же возвращает Err(e). Поэтому сама функция тоже должна возвращать Result.

Бонус: Result работает с .sum(), так что эта однострочная версия делает то же самое и останавливается на первой ошибке:

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

У каждого пользователя электронная почта может быть, а может и не быть. Напиши email_domain(user: &User) -> Option<&str>, которая возвращает часть после @. Верни None, если почты нет или в адресе нет @. Потом найди пользователей по имени и выведи их домены.

Подсказка: user.email.as_deref() превращает Option<String> в Option<&str>. email.split_once('@') возвращает Option<(&str, &str)>. Оператор ? работает и с Option. См. урок Option.

Показать решение
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-at-home")) },
];
for name in ["Ana", "Ben", "Cleo", "Dan"] {
let domain = users
.iter()
.find(|u| u.name == name)
.and_then(email_domain)
.unwrap_or("(нет домена)");
println!("{name}: {domain}");
}
}

Вывод:

Ana: example.com
Ben: (нет домена)
Cleo: (нет домена)
Dan: (нет домена)
▶ Попробуй сам

«Неудача» здесь бывает трёх видов: Dan не существует, у Ben нет почты, а в адресе Cleo нет @. Каждый из них — шаг с Option, и ? или .and_then() останавливаются на первом None. Никаких проверок через if, никаких ошибок нулевого указателя. А .unwrap_or(...) в самом конце даёт значение по умолчанию.

Сделай структуру Account с владельцем и балансом. Дай ей методы deposit(amount) и withdraw(amount). withdraw должен возвращать Result<u64, String>: новый баланс, если всё получилось, или сообщение об ошибке, если денег не хватает. Баланс никогда не должен становиться меньше нуля.

Подсказка: методы, которые меняют счёт, принимают &mut self. Проверяй баланс до вычитания. См. уроки Структуры и Result.

Показать решение
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!(
"нельзя снять {amount}: на счёте только {}",
self.balance
));
}
self.balance -= amount;
Ok(self.balance)
}
}
fn main() {
let mut account = Account::new("Аня");
account.deposit(100);
for amount in [30, 50, 40] {
match account.withdraw(amount) {
Ok(left) => println!("Снято {amount}, осталось {left}"),
Err(e) => println!("Ошибка: {e}"),
}
}
println!("{}: в конце на счёте {}", account.owner, account.balance);
}

Вывод:

Снято 30, осталось 70
Снято 50, осталось 20
Ошибка: нельзя снять 40: на счёте только 20
Аня: в конце на счёте 20
▶ Попробуй сам

Тип u64 (беззнаковое число) — приятная дополнительная страховка: отрицательный баланс в нём вообще не помещается. Если бы ты забыл проверку, self.balance -= amount в отладочной сборке запаниковал бы с сообщением «attempt to subtract with overflow» (попытка вычитания с переполнением), а не ушёл бы тихо в минус.

Объяви трейт Shape с двумя методами: area(&self) -> f64 и name(&self) -> String. Реализуй его для Circle (с радиусом) и Rectangle (с шириной и высотой). Сложи обе фигуры в один Vec и выведи название и площадь каждой с 2 знаками после запятой.

Подсказка: чтобы хранить разные типы в одном Vec, используй Vec<Box<dyn Shape>>. См. уроки Трейты и Трейт-объекты. {:.2} печатает 2 знака после запятой.

Показать решение
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!("Круг (r = {})", self.radius)
}
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn name(&self) -> String {
format!("Прямоугольник ({} 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!("Общая площадь: {total:.2}");
}

Вывод:

Круг (r = 1): 3.14
Прямоугольник (3 x 4): 12.00
Круг (r = 2.5): 19.63
Общая площадь: 34.78
▶ Попробуй сам

Циклу не важно, какая фигура ему попалась. Он знает только, что каждый элемент «является Shape», поэтому может вызвать area() и name(). Чтобы потом добавить Triangle, достаточно написать один новый impl, а цикл вообще не изменится.

Вернись к упражнению 7, но сделай так, чтобы функция работала с любым типом, который можно сравнивать: целыми и дробными числами, символами и строками. Сигнатура такая: largest<T: PartialOrd>(items: &[T]) -> Option<&T>.

Подсказка: T: PartialOrd значит «T поддерживает < и >». Возвращай ссылку (&T), чтобы не копировать и не клонировать элементы. См. урок Обобщения.

Показать решение
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(&["pear", "apple", "zucchini"]));
let empty: Vec<i32> = Vec::new();
println!("{:?}", largest(&empty));
}

Вывод:

Some(41)
Some(9.75)
Some('u')
Some("zucchini")
None
▶ Попробуй сам

Тело почти такое же, как в упражнении 7. Единственное настоящее изменение — часть <T: PartialOrd>. Без этого ограничения компилятор не пропустит item > biggest, потому что сравнивать можно не любой тип. Строки сравниваются по алфавиту, поэтому побеждает "zucchini" («цукини» — по-английски на букву z).

Обрати внимание на Vec<i32> у пустого вектора: когда элементов нет, Rust не может угадать, что такое T, так что это нужно указать.

Множества, сортировка, образцы и итераторы

Заголовок раздела «Множества, сортировка, образцы и итераторы»

В списке гостей некоторые имена записаны больше одного раза. Напиши find_repeats(words: &[&str]) -> Vec<String>, которая возвращает каждое имя, встречающееся больше одного раза, — по одному разу и в алфавитном порядке. Ещё выведи, сколько всего разных гостей.

Подсказка: HashSet запоминает то, что ты уже видел. Его метод insert возвращает false, если значение уже было в множестве. У HashSet нет фиксированного порядка, так что в конце отсортируй результат.

Показать решение
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 возвращает false, если слово уже было в множестве
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!("Повторы: {:?}", find_repeats(&guests));
let unique: HashSet<&str> = guests.iter().copied().collect();
println!("Разных гостей: {}", unique.len());
println!("Повторы: {:?}", find_repeats(&["x", "y", "z"]));
}

Вывод:

Повторы: ["ana", "ben"]
Разных гостей: 4
Повторы: []
▶ Попробуй сам

Зачем два множества? В seen лежат все имена, которые уже встречались. В repeats — те, что встретились второй раз. "ben" встречается трижды, но repeats — множество, поэтому он хранится там только один раз.

Собрать значения в HashSet — ещё и самый быстрый способ посчитать уникальные значения: дубликаты просто исчезают. Без строки result.sort() порядок повторов мог бы меняться от запуска к запуску.

Отсортируй список слов от самого короткого к самому длинному. Если у двух слов одинаковая длина, расставь их в алфавитном порядке, не обращая внимания на регистр. Потом сравни результат с обычным .sort().

Подсказка: sort_by принимает замыкание, которое сравнивает два элемента и возвращает Ordering. .then_with(...) добавляет второе сравнение, которое учитывается только при равенстве первого. См. урок Сортировка.

Показать решение
fn main() {
let mut words = vec!["pear", "Fig", "banana", "apple", "kiwi", "Cherry", "date"];
let mut plain = words.clone();
plain.sort();
println!("Обычная: {plain:?}");
words.sort_by(|a, b| {
a.len()
.cmp(&b.len())
.then_with(|| a.to_lowercase().cmp(&b.to_lowercase()))
});
println!("По длине: {words:?}");
}

Вывод:

Обычная: ["Cherry", "Fig", "apple", "banana", "date", "kiwi", "pear"]
По длине: ["Fig", "date", "kiwi", "pear", "apple", "banana", "Cherry"]
▶ Попробуй сам

Слова здесь английские, потому что .len() считает байты, а русская буква занимает два байта. Посмотри на обычную сортировку: "Cherry" и "Fig" оказались первыми. Строки сравниваются по кодам символов, а любая заглавная буква идёт раньше любой строчной. Поэтому при равной длине мы сравниваем версии to_lowercase().

То же правило можно записать короче: words.sort_by_key(|w| (w.len(), w.to_lowercase())). Кортежи сравниваются поле за полем, так что сначала решает длина, а при равенстве — слово в нижнем регистре.

17. Стоимость доставки с охранными условиями match

Заголовок раздела «17. Стоимость доставки с охранными условиями match»

Напиши shipping_cost(p: Package) -> String для этого перечисления, используя один match:

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

Правила такие: самовывоз бесплатный. Письма до 20 г стоят $1, письма тяжелее — $3. Посылки тяжелее 30 кг отправить вообще нельзя. Экспресс-посылки стоят $10 плюс $2 за каждый килограмм. Остальные посылки — $5 плюс $1 за килограмм.

Подсказка: у ветки match после образца может стоять if — это называется охранное условие (guard): Package::Letter { grams } if grams <= 20 => .... Ветки проверяются сверху вниз, поэтому правило «слишком тяжело» ставь перед правилом для экспресса. См. урок Сопоставление с образцом.

Показать решение
enum Package {
Letter { grams: u32 },
Parcel { kg: u32, express: bool },
Pickup,
}
fn shipping_cost(p: Package) -> String {
match p {
Package::Pickup => String::from("бесплатно (самовывоз)"),
Package::Letter { grams } if grams <= 20 => String::from("$1 (обычное письмо)"),
Package::Letter { grams } => format!("$3 (большое письмо, {grams} г)"),
Package::Parcel { kg, .. } if kg > 30 => format!("слишком тяжёлая, не отправить ({kg} кг)"),
Package::Parcel { kg, express: true } => format!("${} (экспресс)", 10 + kg * 2),
Package::Parcel { kg, express: false } => format!("${} (обычная посылка)", 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));
}
}

Вывод:

$1 (обычное письмо)
$3 (большое письмо, 80 г)
$9 (обычная посылка)
$18 (экспресс)
слишком тяжёлая, не отправить (35 кг)
бесплатно (самовывоз)
▶ Попробуй сам

Самая интересная — экспресс-посылка на 35 кг. Она подходит под две ветки, но срабатывает только первая. Если поставить ветку для экспресса выше ветки «слишком тяжело», программа радостно возьмёт $80 за посылку, которую нельзя отправить.

Обрати внимание: в последней ветке написано express: false, а не ... Это не обязательно, но так ветка точно говорит, что она обрабатывает. Компилятор всё равно проверяет, что все случаи покрыты. Охранные условия в этой проверке не учитываются, поэтому ветка Package::Letter { grams } без условия обязана быть на месте.

В отчёте склада по одному товару на строку, например "яблоки 12". Некоторые строки испорчены: пустые или с количеством, которое не число. С помощью методов итераторов (без цикла for) оставь только строки, которые удалось разобрать и у которых количество больше нуля. Выведи их в виде название: количество через запятую, а потом выведи итог.

Подсказка: .lines() делит текст на строки. filter_map оставляет только те значения Some, которые вернуло замыкание, поэтому фильтрует и преобразует за один шаг. .ok() превращает Result в Option. См. урок Итераторы.

Показать решение
fn main() {
let report = "яблоки 12\nбананы x\nгруши 0\n\nвишни 40\nсливы 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}");
}

Вывод:

яблоки: 12, вишни: 40, сливы: 7
Всего товаров: 59
▶ Попробуй сам

Пройди по строкам по одной. В пустой строке нет пробела, поэтому split_once даёт None, и первый filter_map её отбрасывает. "бананы x" делится нормально, но "x" не разбирается как число, поэтому её отбрасывает второй filter_map. "груши 0" проходит оба шага, а потом её убирает filter, потому что количество равно нулю.

Мы один раз собираем всё в Vec через collect, а потом проходим по нему дважды: чтобы построить текст и чтобы посчитать итог. Итератор можно использовать только один раз, поэтому проще сохранить очищенные данные, чем разбирать всё два раза.

Если ты решил все 18 — отличная работа. Это настоящий прогресс. 🦀

  • Rustlings (на английском): официальный набор маленьких сломанных программ, которые ты по одной чинишь в терминале. Отличный следующий шаг после этой страницы.
  • Rust на Exercism (на английском): около 100 задач с тестами и бесплатные отзывы живых наставников о твоих решениях.
  • Rust на примерах (Rust by Example) (перевод на русский): короткие запускаемые примеры по каждой теме. Удобно, когда застрял на синтаксисе в каком-то упражнении.
  • Трейт Iterator — документация std (на английском): все методы итераторов (filter_map, fold, zip и многие другие), у каждого есть крошечный пример.
  • HashSet — документация std (на английском): все методы множеств, включая union, intersection и difference.