Lewati ke konten

Latihan Rust

Membaca tentang Rust itu berguna. Tapi menulis Rust-lah yang membuatnya benar-benar nempel di kepala. Halaman ini berisi 18 soal kecil, diurutkan dari yang mudah sampai yang lebih sulit dan dikelompokkan per topik.

Untuk setiap soal, baca tugasnya, coba kerjakan sendiri dulu, baru setelah itu buka solusinya. Tidak apa-apa kalau kamu buntu atau melihat error dari compiler. Itu bagian dari proses belajar, dan pesan error Rust sering kali justru jadi petunjuk terbaik yang bisa kamu dapat.

Cara berlatih: buka Rust Playground di tab lain, atau buat proyek dengan cargo new practice (lihat Mulai dengan Rust). Tulis jawabanmu di sana, lalu bandingkan dengan solusinya. Kalau jawabanmu berbeda tapi mencetak hal yang sama, kemungkinan besar itu sudah benar. Hampir selalu ada lebih dari satu cara yang bagus.

Cetak angka dari 1 sampai 15. Tapi untuk kelipatan 3 cetak Fizz, untuk kelipatan 5 cetak Buzz, dan untuk kelipatan keduanya cetak FizzBuzz.

Petunjuk: pakai perulangan for pada 1..=15 dan operator % (sisa bagi). Cek “kelipatan keduanya” lebih dulu.

Lihat solusi
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}");
}
}
}

Output:

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
▶ Coba sendiri

Urutannya penting. Kalau kamu mengecek n % 3 == 0 lebih dulu, angka 15 akan mencetak Fizz dan tidak pernah sampai ke cabang FizzBuzz. Angka yang habis dibagi 3 dan 5 sekaligus pasti habis dibagi 15.

Tulis fungsi reverse(s: &str) -> String yang mengembalikan teks dalam urutan terbalik. "stressed" harus menjadi "desserts".

Petunjuk: .chars() memberimu karakter satu per satu, .rev() membalik urutan sebuah iterator, dan .collect() bisa menyusun sebuah String.

Lihat solusi
fn reverse(s: &str) -> String {
s.chars().rev().collect()
}
fn main() {
println!("{}", reverse("stressed"));
println!("{}", reverse("hello"));
println!("{}", reverse("año"));
}

Output:

desserts
olleh
oña
▶ Coba sendiri

Kenapa .chars() dan bukan byte? String di Rust memakai UTF-8, jadi karakter seperti ñ memakan dua byte. Membalik urutan byte-nya akan merusak karakter itu menjadi teks yang tidak valid. .chars() bekerja pada karakter yang utuh. Lihat String untuk penjelasan lebih lanjut.

Tulis count_vowels(s: &str) -> usize yang menghitung huruf vokal (a, e, i, o, u) di sebuah string. Huruf kecil maupun huruf besar sama-sama dihitung.

Petunjuk: .chars().filter(...).count(). String seperti "aeiouAEIOU" punya method .contains(c) yang bisa kamu pakai untuk mengecek setiap karakter.

Lihat solusi
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"));
}

Output:

3
3
10
0
▶ Coba sendiri

filter memberi closure sebuah referensi ke setiap item (&char), jadi kita menulis *c untuk mendapatkan char-nya sendiri. Halaman Closure dan Iterator menjelaskan pola ini.

Program ini tidak bisa di-compile. Ubah hanya parameter fungsi shout dan dua pemanggilannya supaya program mencetak HALO! dua kali.

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

Compiler bilang word dipakai lagi setelah nilainya di-move: pemanggilan shout yang pertama sudah memindahkan word, karena String tidak mengimplementasikan trait Copy.

Petunjuk: shout hanya perlu melihat teksnya, bukan memilikinya. Lihat Ownership.

Lihat solusi
fn shout(text: &str) {
println!("{}!", text.to_uppercase());
}
fn main() {
let word = String::from("halo");
shout(&word);
shout(&word);
}

Output:

HALO!
HALO!
▶ Coba sendiri

Saat sebuah fungsi menerima String, memanggilnya akan memindahkan (move) string itu ke dalam fungsi, dan main tidak bisa memakainya lagi. Dengan menerima &str, fungsi itu hanya meminjamnya. Compiler juga menyarankan word.clone(), yang juga berhasil, tapi itu menyalin seluruh string tanpa alasan. Di sini, borrowing (peminjaman) adalah solusi yang lebih baik.

Program ini juga tidak bisa di-compile. Buat supaya ia mencetak Ana, tapi tetap menambahkan Ben ke daftar.

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

Artinya: names tidak bisa dipinjam sebagai mutable karena sedang dipinjam sebagai immutable oleh first, dan first masih dipakai setelahnya.

Petunjuk: masalahnya ada di urutan. first masih dipakai saat push mengubah vektor. Lihat Borrowing.

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

Output:

Ana
▶ Coba sendiri

Kenapa Rust seketat ini? push mungkin butuh ruang lebih, sehingga vektor bisa memindahkan semua item-nya ke tempat baru yang lebih besar di memori. Akibatnya first akan menunjuk ke tempat lama, yang sudah tidak valid. Aturan Rust (banyak pembaca atau satu penulis, tidak pernah keduanya sekaligus) menghentikan bug itu sebelum programmu berjalan. Mengambil referensinya setelah push menyelesaikan masalah ini.

Tulis first_word(s: &str) -> &str yang mengembalikan kata pertama (semua yang ada sebelum spasi pertama). Kalau tidak ada spasi, kembalikan seluruh string. Jangan membuat String baru; kembalikan slice dari string aslinya.

Petunjuk: s.find(' ') mengembalikan Option<usize> berisi posisi spasi pertama. &s[..i] adalah slice dari awal sampai i.

Lihat solusi
fn first_word(s: &str) -> &str {
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
}
fn main() {
println!("{:?}", first_word("hello world"));
println!("{:?}", first_word("Rust"));
println!("{:?}", first_word(""));
}

Output:

"hello"
"Rust"
""
▶ Coba sendiri

&str yang dikembalikan menunjuk ke memori yang sama dengan input-nya, jadi tidak ada yang disalin. Compiler tahu bahwa hasilnya tidak boleh hidup lebih lama dari input-nya. Itulah lifetime elision yang sedang bekerja: dengan satu referensi masuk dan satu keluar, kamu tidak perlu menulis lifetime-nya sendiri.

Versi yang lebih pendek adalah s.split_whitespace().next().unwrap_or(""), yang juga melewati spasi berlebih di awal.

Tulis largest(numbers: &[i32]) -> Option<i32>. Fungsi ini mengembalikan angka terbesar, atau None kalau daftarnya kosong. Kali ini jangan pakai .max() bawaan; tulis sendiri perulangannya.

Petunjuk: mulai dengan item pertama sebagai “yang terbesar sejauh ini”. numbers.first() mengembalikan Option, jadi kasus daftar kosong langsung tertangani.

Lihat solusi
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(&[]));
}

Output:

Some(41)
Some(-2)
None
▶ Coba sendiri

numbers.first()? langsung mengembalikan None kalau daftarnya kosong. Itulah sebabnya fungsi ini mengembalikan Option: “yang terbesar dari tidak ada apa-apa” tidak punya jawaban yang masuk akal, dan Rust memaksamu untuk menyatakannya. Di kode sungguhan, numbers.iter().max() melakukan hal yang sama.

Memulai dengan 0 alih-alih item pertama adalah bug klasik: daftar [-5, -2, -9] akan keliru menghasilkan 0.

Hitung berapa kali setiap kata muncul di sebuah kalimat. Abaikan perbedaan huruf besar/kecil dan tanda baca, lalu cetak kata-katanya sesuai urutan abjad.

Petunjuk: pakai BTreeMap<String, usize>. Cara kerjanya mirip HashMap, tapi kuncinya selalu terurut. map.entry(word).or_insert(0) memberimu penghitung yang bisa ditambah 1.

Lihat solusi
use std::collections::BTreeMap;
fn main() {
let text = "Kucing duduk di tikar. Si kucing tidur di tikar. Selesai!";
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}");
}
}

Output:

di: 2
duduk: 1
kucing: 2
selesai: 1
si: 1
tidur: 1
tikar: 2
▶ Coba sendiri

Kenapa BTreeMap dan bukan HashMap? HashMap mengeluarkan item-nya dalam urutan yang tidak bisa ditebak, dan urutan itu bisa berubah dari satu kali jalan ke kali berikutnya. Kalau kamu butuh output yang terurut, pakai BTreeMap, atau kumpulkan isi HashMap ke dalam Vec lalu urutkan.

trim_matches membuang . dari tikar. dan ! dari Selesai!, sedangkan to_lowercase membuat Kucing dan kucing dianggap kata yang sama.

Dari vec![1, 2, 3, 4, 5, 6, 7, 8], buat vektor baru berisi kuadrat dari angka-angka genapnya saja: [4, 16, 36, 64]. Lalu cetak jumlahnya. Pakai method iterator, bukan perulangan for.

Petunjuk: rangkai .iter(), .filter(...), .map(...), dan .collect(). Lalu .iter().sum() menjumlahkan semuanya.

Lihat solusi
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!("Jumlah: {total}");
}

Output:

[4, 16, 36, 64]
Jumlah: 120
▶ Coba sendiri

Baca rangkaiannya dari atas ke bawah seperti resep: ambil angka-angkanya, simpan yang genap, kuadratkan masing-masing, lalu masukkan ke Vec. Iterator itu malas (lazy), jadi sebenarnya tidak ada yang dijalankan sampai .collect() meminta hasilnya. Lihat Iterator.

Tulis sum_numbers(input: &str) -> Result<i32, ParseIntError>. Fungsi ini menerima teks seperti "1, 2, 3", mem-parse setiap angka, lalu mengembalikan jumlahnya. Kalau ada bagian yang bukan angka, kembalikan error-nya alih-alih membuat program crash.

Petunjuk: pisahkan dengan ',', panggil .trim() pada setiap bagian, lalu .parse(). Pakai operator ? untuk langsung keluar saat terjadi error.

Lihat solusi
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, lima, 6", ""] {
match sum_numbers(input) {
Ok(sum) => println!("{input:?} -> jumlahnya {sum}"),
Err(e) => println!("{input:?} -> error: {e}"),
}
}
}

Output:

"1, 2, 3" -> jumlahnya 6
"10,20, 30" -> jumlahnya 60
"4, lima, 6" -> error: invalid digit found in string
"" -> error: cannot parse integer from empty string
▶ Coba sendiri

Pesan error-nya berasal dari Rust sendiri, jadi tetap dalam bahasa Inggris: invalid digit found in string berarti ada karakter yang bukan angka di dalam string, dan cannot parse integer from empty string berarti string kosong tidak bisa diubah menjadi bilangan bulat.

? melakukan dua hal hanya dengan satu karakter: kalau parse() menghasilkan Ok(n), kamu mendapat n. Kalau hasilnya Err(e), fungsi langsung mengembalikan Err(e). Itulah sebabnya fungsi ini juga harus mengembalikan Result.

Bonus: Result bisa dipakai bersama .sum(), jadi satu baris ini melakukan hal yang sama dan berhenti di error pertama:

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

Setiap pengguna mungkin punya email, mungkin juga tidak. Tulis email_domain(user: &User) -> Option<&str> yang mengembalikan bagian setelah @. Kembalikan None kalau tidak ada email, atau kalau email-nya tidak punya @. Lalu cari pengguna berdasarkan nama dan cetak domain mereka.

Petunjuk: user.email.as_deref() mengubah Option<String> menjadi Option<&str>. email.split_once('@') mengembalikan Option<(&str, &str)>. Operator ? juga bisa dipakai pada Option. Lihat Option.

Lihat solusi
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("(tanpa domain)");
println!("{name}: {domain}");
}
}

Output:

Ana: example.com
Ben: (tanpa domain)
Cleo: (tanpa domain)
Dan: (tanpa domain)
▶ Coba sendiri

Ada tiga cara berbeda untuk “gagal” di sini: Dan tidak ada, Ben tidak punya email, dan email Cleo tidak punya @. Masing-masing adalah satu langkah Option, dan ? atau .and_then() berhenti di None yang pertama. Tidak perlu cek if, tidak ada error null pointer. .unwrap_or(...) memberi nilai default di paling akhir.

Buat struct Account berisi pemilik dan saldo. Beri ia method deposit(amount) dan withdraw(amount). withdraw harus mengembalikan Result<u64, String>: saldo baru kalau berhasil, atau pesan error kalau uangnya tidak cukup. Saldo tidak boleh pernah kurang dari nol.

Petunjuk: method yang mengubah rekening menerima &mut self. Cek saldonya sebelum kamu menguranginya. Lihat Struct dan Result.

Lihat solusi
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!(
"tidak bisa menarik {amount}: saldo hanya {}",
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!("Menarik {amount}, sisa {left}"),
Err(e) => println!("Error: {e}"),
}
}
println!("Saldo akhir {}: {}", account.owner, account.balance);
}

Output:

Menarik 30, sisa 70
Menarik 50, sisa 20
Error: tidak bisa menarik 40: saldo hanya 20
Saldo akhir Ana: 20
▶ Coba sendiri

Memakai u64 (angka tanpa tanda/unsigned) adalah jaring pengaman tambahan yang bagus: tipe ini sama sekali tidak bisa menyimpan saldo negatif. Kalau kamu lupa melakukan pengecekan, self.balance -= amount akan panic dengan pesan “attempt to subtract with overflow” (pengurangan menyebabkan overflow) dalam mode debug, alih-alih diam-diam menjadi negatif.

Definisikan trait Shape dengan dua method: area(&self) -> f64 dan name(&self) -> String. Implementasikan trait itu untuk Circle (dengan jari-jari) dan Rectangle (dengan lebar dan tinggi). Masukkan keduanya ke satu Vec lalu cetak nama dan luas masing-masing dengan 2 angka di belakang koma.

Petunjuk: untuk menyimpan tipe-tipe yang berbeda di satu Vec, pakai Vec<Box<dyn Shape>>. Lihat Trait dan Trait Object. {:.2} mencetak 2 angka desimal.

Lihat solusi
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!("Lingkaran (r = {})", self.radius)
}
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn name(&self) -> String {
format!("Persegi panjang ({} 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!("Luas total: {total:.2}");
}

Output:

Lingkaran (r = 1): 3.14
Persegi panjang (3 x 4): 12.00
Lingkaran (r = 2.5): 19.63
Luas total: 34.78
▶ Coba sendiri

Perulangan itu tidak tahu dan tidak peduli bentuk apa yang sedang ia pegang. Yang ia tahu hanyalah setiap item “adalah sebuah Shape”, jadi ia bisa memanggil area() dan name(). Menambahkan Triangle nanti cukup dengan menulis satu impl baru, dan perulangannya sama sekali tidak berubah.

Kembali ke latihan 7, tapi kali ini buat supaya bisa dipakai untuk tipe apa pun yang bisa dibandingkan: bilangan bulat, desimal, karakter, dan string. Signature-nya adalah largest<T: PartialOrd>(items: &[T]) -> Option<&T>.

Petunjuk: T: PartialOrd berarti “T mendukung < dan >”. Kembalikan sebuah referensi (&T) supaya kamu tidak perlu menyalin atau meng-clone item-nya. Lihat Generics.

Lihat solusi
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));
}

Output:

Some(41)
Some(9.75)
Some('u')
Some("zucchini")
None
▶ Coba sendiri

Isi fungsinya hampir sama dengan latihan 7. Satu-satunya perubahan penting adalah bagian <T: PartialOrd>. Tanpa batasan (bound) itu, compiler menolak item > biggest, karena tidak semua tipe bisa dibandingkan. String dibandingkan menurut abjad, itulah sebabnya "zucchini" yang menang.

Perhatikan Vec<i32> pada vektor kosong: karena tidak ada item, Rust tidak bisa menebak T itu apa, jadi kamu harus memberitahunya.

Sebuah daftar tamu berisi beberapa nama yang ditulis lebih dari sekali. Tulis find_repeats(words: &[&str]) -> Vec<String> yang mengembalikan setiap nama yang muncul lebih dari sekali, masing-masing cukup disebut sekali, dan diurutkan sesuai abjad. Cetak juga ada berapa tamu yang berbeda.

Petunjuk: HashSet mengingat apa yang sudah pernah kamu lihat. Method insert-nya mengembalikan false kalau nilainya sudah ada. HashSet tidak punya urutan yang tetap, jadi urutkan hasilnya di akhir.

Lihat solusi
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 mengembalikan false kalau kata itu sudah ada di set
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!("Nama berulang: {:?}", find_repeats(&guests));
let unique: HashSet<&str> = guests.iter().copied().collect();
println!("Tamu berbeda: {}", unique.len());
println!("Nama berulang: {:?}", find_repeats(&["x", "y", "z"]));
}

Output:

Nama berulang: ["ana", "ben"]
Tamu berbeda: 4
Nama berulang: []
▶ Coba sendiri

Kenapa dua set? seen menyimpan setiap nama yang sudah lewat. repeats menyimpan nama-nama yang kita temui untuk kedua kalinya. "ben" muncul tiga kali, tapi karena repeats adalah set, ia hanya disimpan sekali.

Mengumpulkan data ke dalam HashSet juga cara tercepat untuk menghitung nilai unik: duplikatnya langsung hilang. Tanpa baris result.sort(), urutan nama-nama berulang itu bisa berubah dari satu kali jalan ke kali berikutnya.

16. Mengurutkan kata berdasarkan panjangnya

Section titled “16. Mengurutkan kata berdasarkan panjangnya”

Urutkan daftar kata dari yang terpendek sampai yang terpanjang. Kalau dua kata sama panjangnya, urutkan dari A sampai Z dengan mengabaikan huruf besar/kecil. Lalu bandingkan hasilmu dengan .sort() biasa.

Petunjuk: sort_by menerima closure yang membandingkan dua item dan mengembalikan Ordering. .then_with(...) menambahkan perbandingan kedua yang hanya berpengaruh kalau hasilnya seri. Lihat Sorting.

Kata-kata di contoh ini sengaja dibiarkan dalam bahasa Inggris, karena hasilnya bergantung pada panjang setiap kata.

Lihat solusi
fn main() {
let mut words = vec!["pear", "Fig", "banana", "apple", "kiwi", "Cherry", "date"];
let mut plain = words.clone();
plain.sort();
println!("Urutan biasa: {plain:?}");
words.sort_by(|a, b| {
a.len()
.cmp(&b.len())
.then_with(|| a.to_lowercase().cmp(&b.to_lowercase()))
});
println!("Per panjang: {words:?}");
}

Output:

Urutan biasa: ["Cherry", "Fig", "apple", "banana", "date", "kiwi", "pear"]
Per panjang: ["Fig", "date", "kiwi", "pear", "apple", "banana", "Cherry"]
▶ Coba sendiri

Lihat hasil pengurutan biasa: "Cherry" dan "Fig" muncul paling depan. String dibandingkan berdasarkan kode karakternya, dan setiap huruf besar selalu datang sebelum huruf kecil mana pun. Itulah sebabnya kita membandingkan versi to_lowercase() saat menentukan pemenang ketika seri.

Cara yang lebih pendek untuk menulis aturan yang sama adalah words.sort_by_key(|w| (w.len(), w.to_lowercase())). Tuple dibandingkan field demi field, jadi panjangnya menentukan lebih dulu, lalu kata versi huruf kecil menentukan urutan kalau seri.

Tulis shipping_cost(p: Package) -> String untuk enum ini, memakai satu match saja:

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

Aturannya: ambil sendiri (pickup) gratis. Surat sampai 20 g ongkosnya $1, surat yang lebih berat $3. Paket di atas 30 kg sama sekali tidak bisa dikirim. Paket ekspres ongkosnya $10 ditambah $2 per kg. Paket lainnya $5 ditambah $1 per kg.

Petunjuk: sebuah cabang match bisa punya if setelah pattern-nya, yang disebut guard: Package::Letter { grams } if grams <= 20 => .... Cabang-cabang dicek dari atas ke bawah, jadi taruh aturan “terlalu berat” sebelum aturan ekspres. Lihat Pattern Matching.

Lihat solusi
enum Package {
Letter { grams: u32 },
Parcel { kg: u32, express: bool },
Pickup,
}
fn shipping_cost(p: Package) -> String {
match p {
Package::Pickup => String::from("gratis (ambil sendiri)"),
Package::Letter { grams } if grams <= 20 => String::from("$1 (surat biasa)"),
Package::Letter { grams } => format!("$3 (surat besar, {grams} g)"),
Package::Parcel { kg, .. } if kg > 30 => format!("terlalu berat untuk dikirim ({kg} kg)"),
Package::Parcel { kg, express: true } => format!("${} (ekspres)", 10 + kg * 2),
Package::Parcel { kg, express: false } => format!("${} (paket biasa)", 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));
}
}

Output:

$1 (surat biasa)
$3 (surat besar, 80 g)
$9 (paket biasa)
$18 (ekspres)
terlalu berat untuk dikirim (35 kg)
gratis (ambil sendiri)
▶ Coba sendiri

Paket ekspres 35 kg itulah yang menarik. Paket itu cocok dengan dua cabang, tapi hanya cabang pertama yang menang. Kalau kamu menaruh cabang ekspres di atas cabang “terlalu berat”, program dengan santainya akan menagih $80 untuk paket yang sebenarnya tidak bisa dikirim.

Perhatikan cabang terakhir menulis express: false alih-alih ... Itu tidak wajib, tapi membuat cabang itu menyatakan dengan jelas apa yang ditanganinya. Compiler tetap memeriksa bahwa semua kemungkinan sudah tertangani. Guard tidak ikut dihitung dalam pemeriksaan itu, itulah sebabnya cabang Package::Letter { grams } biasa tanpa guard harus tetap ada.

Sebuah laporan gudang berisi satu barang per baris, seperti "apel 12". Beberapa barisnya rusak: kosong, atau jumlahnya bukan angka. Dengan method iterator (tanpa perulangan for), simpan hanya baris yang bisa di-parse dan jumlahnya di atas nol. Cetak hasilnya sebagai nama: jumlah yang dipisahkan koma, lalu cetak totalnya.

Petunjuk: .lines() memecah teks menjadi baris-baris. filter_map hanya menyimpan nilai Some yang dikembalikan closure-nya, jadi ia bisa menyaring dan mengonversi dalam satu langkah. .ok() mengubah Result menjadi Option. Lihat Iterator.

Lihat solusi
fn main() {
let report = "apel 12\npisang x\npir 0\n\nceri 40\nplum 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 barang: {total}");
}

Output:

apel: 12, ceri: 40, plum: 7
Total barang: 59
▶ Coba sendiri

Ikuti satu baris demi satu baris. Baris kosong tidak punya spasi, jadi split_once menghasilkan None dan filter_map pertama membuangnya. "pisang x" bisa dipisah dengan baik, tapi "x" tidak bisa di-parse, jadi filter_map kedua membuangnya. "pir 0" lolos dari keduanya, lalu filter membuangnya karena jumlahnya nol.

Kita melakukan collect ke dalam Vec sekali, lalu menelusurinya dua kali: sekali untuk menyusun teks, sekali lagi untuk totalnya. Sebuah iterator hanya bisa dipakai sekali, jadi menyimpan data yang sudah bersih lebih sederhana daripada mem-parse semuanya dua kali.

Kalau kamu sudah menyelesaikan ke-18 soal, selamat! Itu kemajuan yang nyata. 🦀

  • Rustlings (bahasa Inggris): kumpulan resmi program-program kecil yang rusak, untuk kamu perbaiki satu per satu di terminal. Langkah berikutnya yang bagus setelah halaman ini.
  • Exercism Rust track (bahasa Inggris): sekitar 100 soal lengkap dengan tes, plus masukan gratis untuk solusimu dari mentor manusia.
  • Rust by Example (bahasa Inggris): contoh-contoh singkat yang bisa dijalankan untuk setiap topik. Berguna saat kamu lupa sintaks untuk satu latihan.
  • Trait Iterator — dokumentasi std (bahasa Inggris): semua method iterator (filter_map, fold, zip, dan masih banyak lagi), masing-masing dengan contoh kecil.
  • HashSet — dokumentasi std (bahasa Inggris): semua method set, termasuk union, intersection, dan difference.