rust101-fieldlog

02 / 10 · 20 分钟

用 struct 记下一天

北口草甸这一天显示成 3.6 km/h。

你的项目里会动到:src/model.rs · src/stats.rs · src/lib.rs

你现在有 Cargo.toml、src/main.rs、src/lib.rs。这一章新建两个文件:先 model.rs,再 stats.rs。每个文件写完,才在 lib.rs 里声明它。

先写 src/model.rs

一天要放在一起的数据:日期、标题、距离、时间、爬升、笔记。用一个 struct。距离用米。8.4 这种小数加五天,可能变成 47.199999。47200 米显示出来就是 47.2 km。公里只在打印时出现。

新建 src/model.rs,先放入 DayLog。这一章还没有天气,也没有 serde。

src/model.rs
/// 一天的记录。距离用米保存,公里只在显示时换算。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DayLog {
    /// `YYYY-MM-DD`。
    pub date: String,
    pub title: String,
    pub distance_m: u32,
    pub minutes: u32,
    pub elevation_m: u32,
    pub note: String,
}

文件已经在磁盘上了,再打开 src/lib.rs,加上这一行。顺序不能反。没有 model.rs 就写 pub mod model;,编译器会报找不到文件。

加在 banner 函数后面。二进制要能用到这个模块,所以是 pub mod,不是 mod。

src/lib.rs
pub mod model;

再写 src/stats.rs

配速是 km/h。分钟为 0 的时候这一天没有移动,返回 None。不要返回 0,0 会被看成走得很慢。北口草甸走了 8400 米,用了 140 分钟,显示成 3.6 km/h。测试比较的是这个字符串。

动手

算出这一天的配速

分钟为 0 时返回 None。其他情况返回每小时公里数。

pace_kmh 的起点
pub fn pace_kmh(distance_m: u32, minutes: u32) -> Option<f64> {
    todo!("pace_kmh")
}

小时数是 minutes as f64 / 60.0。公里数是 distance_m as f64 / 1000.0。

stats.rs 里还要有 format_km、format_hours、format_pace,以及调用 pace_kmh 的测试。这些不是练习,直接从这一章结束时的文件抄进去。函数先写成 todo!() 也可以:cargo check 能过,cargo test 会在这里 panic。写完函数再测。

stats.rs 写完之后,再在 lib.rs 加上 pub mod stats;。

这一章结束时的 lib.rs。只有这两个模块。

src/lib.rs
pub fn banner() -> &'static str {
    "灯塔山脊 · 2026-09-18 到 2026-09-22"
}

pub mod model;
pub mod stats;

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

    #[test]
    fn banner_names_the_traverse() {
        let text = banner();
        assert!(text.contains("灯塔山脊"));
    }
}

这一章结束时的 model.rs。

src/model.rs
// region: day_log
/// 一天的记录。距离用米保存,公里只在显示时换算。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DayLog {
    /// `YYYY-MM-DD`。
    pub date: String,
    pub title: String,
    pub distance_m: u32,
    pub minutes: u32,
    pub elevation_m: u32,
    pub note: String,
}
// endregion: day_log

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

    #[test]
    fn meadow_day_keeps_meters() {
        let day = DayLog {
            date: "2026-09-18".to_string(),
            title: "北口草甸".to_string(),
            distance_m: 8400,
            minutes: 140,
            elevation_m: 320,
            note: "雾停在谷底。".to_string(),
        };
        assert_eq!(day.distance_m, 8400);
        assert_eq!(day.minutes, 140);
    }
}

pace_kmh 可以先留成 todo!()。format_km 和测试用这一份。

src/stats.rs
// region: pace_kmh
/// 配速,单位 km/h。
///
/// `minutes` 为 0 表示这一天没有移动,返回 `None`。
/// 不要返回 0,0 会被读成「走得很慢」。
pub fn pace_kmh(distance_m: u32, minutes: u32) -> Option<f64> {
    if minutes == 0 {
        return None;
    }
    let hours = minutes as f64 / 60.0;
    let kilometers = distance_m as f64 / 1000.0;
    Some(kilometers / hours)
}
// endregion: pace_kmh

/// 显示用的公里数,精确到 0.1 km。加总和比较仍然用米。
pub fn format_km(distance_m: u32) -> String {
    let tenths = (distance_m + 50) / 100;
    let whole = tenths / 10;
    let tenth = tenths % 10;
    format!("{whole}.{tenth}")
}

pub fn format_hours(minutes: u32) -> String {
    let hours = minutes / 60;
    let rest = minutes % 60;
    match (hours, rest) {
        (0, rest) => format!("{rest} 分钟"),
        (hours, 0) => format!("{hours} 小时"),
        (hours, rest) => format!("{hours} 小时 {rest} 分"),
    }
}

pub fn format_pace(distance_m: u32, minutes: u32) -> String {
    match pace_kmh(distance_m, minutes) {
        Some(pace) => format!("{pace:.1} km/h"),
        None => "没有移动".to_string(),
    }
}

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

    #[test]
    fn formats_tenths_of_a_kilometer() {
        assert_eq!(format_km(8400), "8.4");
        assert_eq!(format_km(11000), "11.0");
        assert_eq!(format_km(950), "1.0");
        assert_eq!(format_km(0), "0.0");
    }

    #[test]
    fn formats_hours_without_empty_parts() {
        assert_eq!(format_hours(930), "15 小时 30 分");
        assert_eq!(format_hours(120), "2 小时");
        assert_eq!(format_hours(40), "40 分钟");
    }

    #[test]
    fn pace_of_the_first_sample_day_is_three_point_six() {
        assert_eq!(format_pace(8400, 140), "3.6 km/h");
    }

    #[test]
    fn zero_minutes_is_not_a_pace() {
        assert_eq!(pace_kmh(1000, 0), None);
        assert_eq!(format_pace(0, 0), "没有移动");
    }
}

检查点

在你的 fieldlog 目录
cargo check
cargo test stats::tests::pace_of_the_first_sample_day_is_three_point_six
cargo test stats::tests::zero_minutes_is_not_a_pace

pace_kmh 还是 todo!() 时,cargo check 通过,测试 panic。换成解答后,两条测试都通过。