Cél és előfeltétel
A szövegből kivett szavak akkor válnak igazán hasznossá, ha összesíteni is tudjuk őket. Ebben a leckében gyakorisági táblát építesz, egy kulcshoz tartozó eredményt frissítesz, majd stabil sorrendű riportot készítesz. Ezekre később egy parancssori szövegstatisztikában is szükségünk lesz.
Előfeltétel az A Rust alapjai útvonal és az előző szövegkezelési gyakorlat: ismered az Option típust, a vektorokat és a String–&str különbséget. A példák Rust 2024 editionben, külső csomag nélkül futnak. Mielőtt kódolsz, írd le, hogy nálad mit jelent egy szó. Most minden whitespace által elválasztott token önálló szó; a kisbetűsítés, az írásjelek eltávolítása és a Unicode-normalizálás nem része a számlálásnak.
Kulcs alapján keresünk
A Vec akkor kényelmes, amikor egymás után tárolsz elemeket. A HashMap<K, V> egy kulcshoz egy értéket rendel, ezért például a Rust token előfordulásait közvetlenül a tokennel kérdezheted le. Egy kulcs egyszer szerepelhet a táblában. Ha ugyanazzal a kulccsal újra meghívod az insert metódust, a hozzá tartozó értéket felülírod.
A get eredménye Option<&V>: a hiányzó bejegyzés természetes állapot. Számlálóknál gyakran hasznos a copied().unwrap_or(0), mert a visszakapott referenciaszámból saját számot, hiány esetén nullát készít. Ettől a hiányzó kulcs még nem kerül bele a táblába. A keresést és a beszúrást külön műveletként értelmezzük.
use std::collections::HashMap;
fn main() {
let mut darabok: HashMap<&str, usize> = HashMap::new();
for szo in "Rust Rust gyors".split_whitespace() {
let darab = darabok.entry(szo).or_insert(0);
*darab += 1;
}
assert_eq!(darabok.get("Rust"), Some(&2));
assert_eq!(darabok.get("hiányzó").copied().unwrap_or(0), 0);
assert!(!darabok.contains_key("hiányzó"));
let mut sorok: Vec<_> = darabok.iter().map(|(szo, db)| (*szo, *db)).collect();
sorok.sort_unstable_by_key(|(szo, _)| *szo);
for (szo, db) in sorok {
println!("{szo}: {db}");
}
}
A kiírás ebben a példában mindig Rust: 2, majd gyors: 1, külön sorban. A táblán való bejárásnak önmagában nincs garantált sorrendje. A rendezés ezért a riport része, nem a HashMap tulajdonsága. A standard könyvtár dokumentációja a tárolási és bejárási műveletek pontos szerződését is leírja.
Az entry egy módosítható helyet ad
A entry(kulcs) megkeresi a kulcshoz tartozó helyet. Az or_insert(0) a meglévő értéket használja, vagy hiány esetén beszúrja a nullát; mindkét esetben módosítható referenciát ad vissza. A *darab += 1 ezért az értéket növeli, nem magát a referenciát változtatja meg. Az or_insert önmagában még nem számlál.
Az Entry API az and_modify lehetőségét is kínálja: egy meglévő bejegyzést külön szabállyal frissíthetsz, miközben az első értéket az or_insert adja. Érdemes ezt a szabályt üzleti mondatként megfogalmazni. A „legjobb pontszámot őrizzük” például egészen más művelet, mint a „minden pontszámot összeadunk”.
Kölcsönzött vagy saját kulcs?
Az első példában &str kulcsok voltak, amelyek a bemeneti szövegre hivatkoztak. Ez jól működik, amíg a szöveg él, és a táblát nem akarod attól függetlenül megtartani. Ha egy függvény saját eredményt ad vissza, a HashMap<String, usize> egyszerűbb szerződés: a kulcsok a tábla tulajdonába kerülnek. A másolás itt tudatos döntés, amely felszabadítja a hívót az eredeti szöveg őrzése alól.
A HashMap nem találja ki a szövegfeldolgozási szabályaidat. A Rust, a rust és a Rust, most három különböző kulcs. Ha később keresési normalizálást szeretnél, azt külön lépésként, külön tesztekkel vezesd be. A Unicode-kisbetűsítés sem azonos az általános graféma- vagy normalizálási műveletekkel. Először legyen pontos és következetes a jelenlegi feladat.
1. feladat: önálló gyakorisági tábla
Írj gyakorisag függvényt, amely &str bemenetből HashMap<String, usize> eredményt készít. Minden whitespace tokent számoljon, az írásjeleket és a betűméretet őrizze meg. Üres vagy csupa whitespace bemenethez üres táblát várunk. Teszteld úgy is, hogy az eredeti String a függvény meghívása után megszűnik, de a tábla tovább használható.
Megoldás
use std::collections::HashMap;
fn gyakorisag(szoveg: &str) -> HashMap<String, usize> {
let mut eredmeny = HashMap::new();
for szo in szoveg.split_whitespace() {
*eredmeny.entry(szo.to_owned()).or_insert(0) += 1;
}
eredmeny
}
fn main() {
let tabla = {
let bemenet = String::from("Rust Rust rust,\ná");
gyakorisag(&bemenet)
};
assert_eq!(tabla.len(), 3);
assert_eq!(tabla.get("Rust"), Some(&2));
assert_eq!(tabla.get("rust,"), Some(&1));
assert_eq!(tabla.get("á"), Some(&1));
assert!(gyakorisag("").is_empty());
assert!(gyakorisag(" \t\n").is_empty());
assert_eq!(gyakorisag("á\tá").get("á"), Some(&2));
}
A blokk végén a bemenet felszabadul, az eredmény saját kulcsai viszont megmaradnak. A tartalmat kulcsonként ellenőrizzük, a bejárási sorrendtől függetlenül.
2. feladat: személyenként a legjobb pont
Készíts legjobb függvényt, amely egy módosítható táblába játékosnév és u32 pontszám alapján jegyez be eredményt. Új játékosnál a kapott pont legyen az első érték, ismert játékosnál pedig a korábbi és az új érték közül a nagyobb maradjon. A nulla érvényes pontszám. A művelet ne növelje a tábla hosszát ugyanannál a névnél.
Megoldás
use std::collections::HashMap;
fn legjobb(tabla: &mut HashMap<String, u32>, nev: &str, pont: u32) {
tabla.entry(nev.to_owned())
.and_modify(|regi| *regi = (*regi).max(pont))
.or_insert(pont);
}
fn main() {
let mut tabla = HashMap::new();
legjobb(&mut tabla, "Réka", 0);
legjobb(&mut tabla, "Réka", 12);
legjobb(&mut tabla, "Réka", 4);
assert_eq!(tabla.get("Réka"), Some(&12));
assert_eq!(tabla.len(), 1);
legjobb(&mut tabla, "Áron", 0);
assert_eq!(tabla.get("Áron"), Some(&0));
legjobb(&mut tabla, "Réka", u32::MAX);
assert_eq!(tabla.get("Réka"), Some(&u32::MAX));
}
A maximumképzés nem adja össze a pontszámokat, ezért a legnagyobb lehetséges u32 értékkel is ugyanazt a szabályt alkalmazhatjuk. Ez a teszt a frissítési szándékot is dokumentálja.
3. feladat: stabil riport
Írj riport függvényt, amely szó szerinti kulcssorrendben készít kulcs: darab sorokat, köztük újsorral, a végén extra újsor nélkül. A bemeneti táblát ne fogyaszd el. Az üres eredmény üres String legyen. A Rust alapértelmezett szövegrendezését használjuk; magyar nyelvi ábécérendet nem ígérünk. Két különböző beszúrási sorrendhez is azonos szöveget várunk.
Megoldás
use std::collections::HashMap;
fn riport(tabla: &HashMap<String, usize>) -> String {
let mut sorok: Vec<_> = tabla.iter().collect();
sorok.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
let szovegek: Vec<_> = sorok.into_iter()
.map(|(szo, db)| format!("{szo}: {db}"))
.collect();
szovegek.join("\n")
}
fn main() {
let egyik = HashMap::from([("rust".to_owned(), 2), ("cargo".to_owned(), 1)]);
let masik = HashMap::from([("cargo".to_owned(), 1), ("rust".to_owned(), 2)]);
assert_eq!(riport(&egyik), "cargo: 1\nrust: 2");
assert_eq!(riport(&egyik), riport(&masik));
assert_eq!(egyik.len(), 2);
assert_eq!(riport(&HashMap::new()), "");
}
Elfogadási feltételek
- A gyakorisági tábla helyesen kezeli az ismétlődő, ékezetes és whitespace bemeneteket; az eltérő betűméretű vagy írásjeles tokeneket külön tartja.
- A legjobb pontszám új kulcsnál beszúr, meglévőnél csak nagyobb értékre változik; a nulla és a
u32::MAXtesztje is sikeres. - A riport beszúrási sorrendtől függetlenül ugyanazt a rendezett szöveget adja, és nem fogyasztja el a bemeneti táblát.
Összefoglaló és továbblépés
A kulcs, az érték és a frissítési szabály együtt határozza meg a táblád jelentését. A saját String kulcs az eredményt függetleníti a bemenettől, a rendezett riport pedig ellenőrizhető kimenetet ad. A következő leckében az itt már látott iterátorláncokat bontjuk érthető lépésekre.