Rust 첫걸음 연습 문서의 문제 1 풀이 코드다.

//! 04 Rust 첫걸음 연습 p1: 두 u8의 평균을 넘침 없이 구한다. 그냥 (a + b) / 2는 dev 빌드에서 panic이다.
//! 의존성: 없음 (표준 라이브러리만)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/4.연습문제/04_rust-basics-practice_p1.rs"

use std::hint::black_box;
use std::panic;

/// 고치기 전: u8끼리 더하므로 합이 255를 넘으면 넘친다.
fn average_naive(a: u8, b: u8) -> u8 {
    (a + b) / 2
}

/// 풀이 1: 더 넓은 타입(u16)으로 바꿔 더한다. 결과는 반드시 0..=255라 try_from이 실패하지 않는다.
fn average_widen(a: u8, b: u8) -> u8 {
    let sum = u16::from(a) + u16::from(b);
    u8::try_from(sum / 2).expect("두 u8의 평균은 u8 범위 안이다")
}

/// 풀이 2: 넓히지 않고, 각각 반으로 나눈 뒤 둘 다 홀수일 때만 1을 더한다.
fn average_halves(a: u8, b: u8) -> u8 {
    a / 2 + b / 2 + ((a % 2) & (b % 2))
}

fn main() {
    let cases: [(u8, u8, u8); 6] = [
        (200, 100, 150),
        (255, 255, 255),
        (1, 2, 1),
        (3, 5, 4),
        (0, 0, 0),
        (254, 255, 254),
    ];
    for (a, b, want) in cases {
        assert_eq!(average_widen(a, b), want, "widen {a} {b}");
        assert_eq!(average_halves(a, b), want, "halves {a} {b}");
    }
    // 모든 쌍에서 두 풀이가 정수 평균(내림)과 같다.
    for a in 0..=255u8 {
        for b in 0..=255u8 {
            let want = ((u32::from(a) + u32::from(b)) / 2) as u8;
            assert_eq!(average_widen(a, b), want);
            assert_eq!(average_halves(a, b), want);
            assert_eq!(a.midpoint(b), want); // 표준 라이브러리의 midpoint도 같다
        }
    }
    println!("두 풀이: 65,536쌍 모두 일치");

    // 고치기 전 함수는 합이 255를 넘으면 panic한다(dev 프로필).
    assert_eq!(average_naive(black_box(100), black_box(50)), 75);
    let old = panic::take_hook();
    panic::set_hook(Box::new(|_| {}));
    let r = panic::catch_unwind(|| average_naive(black_box(200), black_box(100)));
    panic::set_hook(old);
    let msg = r.unwrap_err().downcast_ref::<&str>().map(|s| s.to_string());
    assert_eq!(msg.as_deref(), Some("attempt to add with overflow"));
    println!("average_naive(200, 100) -> panic: attempt to add with overflow");
    // release처럼 감으면 (200 + 100) mod 256 = 44, 44 / 2 = 22라는 틀린 값이 나온다.
    assert_eq!(200u8.wrapping_add(100) / 2, 22);
    println!("감아 돌면 22 (틀린 값)");

    // 변형: as로 고친 판에 clippy pedantic이 거는 린트.
    let dir = std::env::temp_dir().join(format!("vr_{}_p1", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let file = dir.join("snippet.rs");
    std::fs::write(
        &file,
        "pub fn f(a: u8, b: u8) -> u8 {\n    ((a as u16 + b as u16) / 2) as u8\n}\n",
    )
    .unwrap();
    let out = std::process::Command::new("clippy-driver")
        .args(["--edition", "2024", "--crate-type=lib", "--emit=metadata"])
        .args(["-W", "clippy::pedantic", "--out-dir"])
        .arg(&dir)
        .arg(&file)
        .output()
        .expect("clippy-driver 실행");
    let err = String::from_utf8_lossy(&out.stderr);
    assert!(err.contains("clippy::cast_possible_truncation"), "{err}");
    assert!(err.contains("clippy::cast_lossless"), "{err}");
    assert!(err.contains("clippy::manual_midpoint"), "{err}");
    println!("as 판 + pedantic -> cast_possible_truncation, cast_lossless, manual_midpoint");

    println!("ALL CHECKS PASSED");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn example() {
        assert_eq!(average_widen(200, 100), 150);
        assert_eq!(average_halves(200, 100), 150);
    }
}