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。
/// 一天的记录。距离用米保存,公里只在显示时换算。
#[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。
pub mod model;再写 src/stats.rs
配速是 km/h。分钟为 0 的时候这一天没有移动,返回 None。不要返回 0,0 会被看成走得很慢。北口草甸走了 8400 米,用了 140 分钟,显示成 3.6 km/h。测试比较的是这个字符串。
动手
算出这一天的配速
分钟为 0 时返回 None。其他情况返回每小时公里数。
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。只有这两个模块。
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。
// 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 和测试用这一份。
// 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), "没有移动");
}
}检查点
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。换成解答后,两条测试都通过。