데이터 모델링 연습 문서의 문제 2 풀이 코드다.
//! p2 단어 빈도 상위 k개: HashMap entry로 세고, (개수 내림차순, 단어 오름차순)으로 정렬해
//! 실행할 때마다 같은 결과를 낸다. 변형: 키 함수가 원소를 빌린 &str을 돌려주면 컴파일되지 않고,
//! sort_by(비교 함수)와 sort_by_cached_key는 clone 없이 또는 키를 한 번만 만들어 같은 결과를 낸다.
//! 의존성: 없음 (표준 라이브러리만)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/4.연습문제/14_data-modeling-practice_p2.rs"
use std::cmp::Reverse;
use std::collections::HashMap;
const TAG: &str = "p2";
/// 코드 조각을 rustc로 컴파일해 stderr를 돌려준다.
fn compile_errors(code: &str) -> String {
let dir =
std::env::temp_dir().join(format!("vrM{}_{}_{}", TAG, std::process::id(), code.len()));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("snippet.rs");
std::fs::write(&file, code).unwrap();
let out = std::process::Command::new("rustc")
.args([
"--edition",
"2024",
"--crate-type=lib",
"--emit=metadata",
"--out-dir",
])
.arg(&dir)
.arg(&file)
.output()
.expect("rustc 실행");
let _ = std::fs::remove_dir_all(&dir);
String::from_utf8_lossy(&out.stderr).into_owned()
}
fn expect_phrases(code: &str, phrases: &[&str]) {
let stderr = compile_errors(code);
for phrase in phrases {
assert!(stderr.contains(phrase), "'{phrase}' 기대, 실제:\n{stderr}");
println!("컴파일 확인: {phrase}");
}
}
fn top_k(text: &str, k: usize) -> Vec<(String, usize)> {
let mut counts: HashMap<String, usize> = HashMap::new();
for raw in text.split_whitespace() {
let word = raw
.trim_matches(|c: char| !c.is_alphanumeric())
.to_lowercase();
if word.is_empty() {
continue;
}
*counts.entry(word).or_insert(0) += 1;
}
let mut pairs: Vec<(String, usize)> = counts.into_iter().collect();
pairs.sort_by_key(|(w, c)| (Reverse(*c), w.clone()));
pairs.truncate(k);
pairs
}
/// 변형 1: 비교 함수로 정렬하면 단어를 clone하지 않는다.
fn sort_cmp(pairs: &mut [(String, usize)]) {
pairs.sort_by(|(wa, ca), (wb, cb)| cb.cmp(ca).then_with(|| wa.cmp(wb)));
}
/// 변형 2: 키를 원소마다 한 번만 만든다.
fn sort_cached(pairs: &mut [(String, usize)]) {
pairs.sort_by_cached_key(|(w, c)| (Reverse(*c), w.clone()));
}
fn main() {
let got = top_k("The cat and the hat, and THE bat!", 2);
println!("{got:?}");
assert_eq!(got, vec![("the".to_string(), 3), ("and".to_string(), 2)]);
// 개수가 같으면 단어 순서
let got = top_k("b a c b a c d", 3);
assert_eq!(
got,
vec![
("a".to_string(), 2),
("b".to_string(), 2),
("c".to_string(), 2)
]
);
// k가 단어 수보다 크면 있는 만큼
assert_eq!(top_k("x y", 10).len(), 2);
assert!(top_k("... !!!", 3).is_empty());
// 같은 입력을 100번 돌려도 결과가 같다(HashMap 순서에 기대지 않는다)
let first = top_k("q w e r t y q w e q", 4);
for _ in 0..100 {
assert_eq!(top_k("q w e r t y q w e q", 4), first);
}
println!("{first:?}");
// 변형: 키 함수가 원소 안의 &str을 돌려주면 수명 오류
expect_phrases(
r#"use std::cmp::Reverse;
pub fn f(pairs: &mut Vec<(String, usize)>) {
pairs.sort_by_key(|(w, c)| (Reverse(*c), w.as_str()));
}"#,
&[
"error: lifetime may not live long enough",
"returning this value requires that `'1` must outlive `'2`",
],
);
let base: Vec<(String, usize)> = [("b", 2), ("a", 2), ("z", 5), ("c", 1), ("y", 5)]
.iter()
.map(|(w, c)| (w.to_string(), *c))
.collect();
let mut by_key = base.clone();
by_key.sort_by_key(|(w, c)| (Reverse(*c), w.clone()));
let mut by_cmp = base.clone();
sort_cmp(&mut by_cmp);
let mut by_cached = base.clone();
sort_cached(&mut by_cached);
assert_eq!(by_key, by_cmp);
assert_eq!(by_key, by_cached);
println!("{by_cmp:?}");
println!("ALL CHECKS PASSED");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn example_from_problem() {
assert_eq!(
top_k("The cat and the hat, and THE bat!", 2),
vec![("the".to_string(), 3), ("and".to_string(), 2)]
);
}
}