用 chrono 处理日期与时间
你会学到什么
- 为什么处理「日历日期」要用
chrono而不是标准库的std::time。 - 用
NaiveDate::parse_from_str把字符串解析成日期。 - 用
format配合格式串把日期格式化成2026-06-15或2026年06月15日。 - 用
chrono::Duration做日期加减,以及求两个日期之间相差的天数。 - 为什么测试要用固定的
NaiveDate,而不是now()。
为什么用 chrono,而不是 std::time
标准库里有 std::time::SystemTime 和 Duration,但它们很底层:
SystemTime 只是「距某个纪元的一段时长」,不懂日历。它无法回答
「2026 年 6 月有几天」「6 月 25 日往后 10 天是几号」,也不能解析
"2026-06-15" 这样的字符串,更没有按「年月日」格式化的能力。
chrono 在其之上补齐了这些:日历日期、时区、解析与格式化。
需要给用户展示日期、读写日期字符串、做日历运算时,就用 chrono;
只是想测量「一段代码跑了多久」,那 std::time::Instant 就够了。
项目结构
这个例子拆成多个文件,方便对照阅读:
45_chrono/
├── Cargo.toml
└── src/
├── lib.rs // 模块声明 + 重新导出
├── parsing.rs // 解析与格式化
├── duration.rs // 日期加减与差值
└── main.rs // 调用库、打印演示
运行代码
cd examples
cargo run -p rt_45_chrono
cargo test -p rt_45_chrono
代码讲解
选 NaiveDate 让结果可复现
chrono 有带时区的 DateTime<Utc> / DateTime<Local>,也有
不带时区的 NaiveDate / NaiveTime / NaiveDateTime。本章统一用
NaiveDate,因为它最简单,而且不依赖系统时钟——这对写测试至关重要:
Utc::now() / Local::now() 每次返回的值都不一样,断言无从下手。
构造日期用 from_ymd_opt(返回 Option,非法日期得到 None):
let d = chrono::NaiveDate::from_ymd_opt(2026, 6, 15).unwrap();
注意:旧版的
from_ymd(不带_opt)已被弃用,会在 panic 时吞掉错误,别再用。
解析:字符串 → 日期
parsing.rs 里的 parse_date 用 parse_from_str,第二个参数是格式串:
pub fn parse_date(s: &str) -> Result<NaiveDate, chrono::ParseError> {
NaiveDate::parse_from_str(s, "%Y-%m-%d")
}
%Y 是四位年、%m 两位月、%d 两位日。解析可能失败(格式不符、
日期非法如 2026-02-30),所以返回 Result,由调用方决定怎么处理。
格式化:日期 → 字符串
格式化是解析的逆操作,同样用格式串。普通字符(包括汉字)会原样输出:
date.format("%Y-%m-%d").to_string() // "2026-06-15"
date.format("%Y年%m月%d日").to_string() // "2026年06月15日"
format 返回的是一个惰性的「可显示对象」,记得 .to_string() 落地成 String。
日期运算:Duration
duration.rs 的核心是 chrono::Duration,表示一段时间间隔。
日期加上一个 Duration 得到新日期,跨月、跨年都会自动处理:
date + Duration::days(10) // 6/25 + 10 天,自动跨到 7/5
date - Duration::days(7)
两个日期相减得到 Duration,再用 num_days() 取整数天数;
to 在 from 之后为正,反之为负:
pub fn days_between(from: NaiveDate, to: NaiveDate) -> i64 {
(to - from).num_days()
}
测试断言确定的值
因为输入是固定的 NaiveDate,测试可以断言精确结果,比如
days_between(2025-1-1, 2026-1-1) == 365、跨月加法 6/25 + 10 == 7/5。
main.rs 里才用 Local::now().date_naive() 展示「今天」,那行输出每天都会变,
所以它不进测试。
常见错误
- 用了弃用的
NaiveDate::from_ymd/and_hms:在-D warnings下会直接编译失败, 一律换成带_opt的版本并处理Option。 - 把
now()写进断言:结果非确定,测试时灵时不灵。测试只用固定日期。 - 忘了
parse_from_str返回Result:直接当成日期用会编译不过,需要先?或unwrap。
练习
- 给
parsing增加一个解析「日期 + 时间」的函数,用NaiveDateTime::parse_from_str和格式串"%Y-%m-%d %H:%M:%S"。 - 给
duration增加weeks_between,复用days_between再除以 7。 - 试着用
date.weekday()打印某天是星期几,并为它写一个固定输入的测试。
小结
chrono 把标准库缺失的「日历能力」补齐:解析、格式化、按年月日运算。
测试时优先用不带时区的 NaiveDate,让结果可复现。
下一步
文本处理里另一个高频需求是「按模式匹配与提取」。下一章学习用
regex 写正则表达式:[37c-regex]。
完整示例代码
下面是 examples/45_chrono/ 的完整源码。无需 clone 仓库,直接在页面上阅读、复制、对照运行。
examples/45_chrono/src/main.rs
//! 演示 `rt_45_chrono` 库的用法,带标注的输出。
use chrono::Local;
use rt_45_chrono::{add_days, days_between, format_cn, format_iso, parse_date, sub_days};
fn main() {
// 1. 解析字符串为日期。
let date = parse_date("2026-06-15").expect("日期格式应当合法");
println!("解析结果 : {date}");
// 2. 两种格式化。
println!("ISO 格式 : {}", format_iso(date));
println!("中文格式 : {}", format_cn(date));
// 3. 日期加减。
let later = add_days(date, 10);
let earlier = sub_days(date, 7);
println!("10 天后 : {}", format_iso(later));
println!("7 天前 : {}", format_iso(earlier));
// 4. 求差值。
println!("两日期相差天数 : {} 天", days_between(date, later));
// 5. 当前日期(注意:依赖系统时钟,每次运行都会变,所以不写进测试)。
let today = Local::now().date_naive();
println!("今天 : {}", format_cn(today));
println!("距 2026-06-15 : {} 天", days_between(date, today));
} examples/45_chrono/src/lib.rs
//! 用 `chrono` crate 处理日期与时间。
//!
//! 标准库的 `std::time` 只提供「时间点 / 时间间隔」这类底层能力,
//! 既不能解析 `"2026-06-15"` 这样的日期字符串,也不会按「年月日」做日历运算。
//! `chrono` 在其之上提供了完整的日历日期、时区与格式化支持。
//!
//! 本 crate 把功能拆成两个模块逐一演示:
//! - `parsing`:解析与格式化(把字符串变成日期,再把日期格式化成字符串)。
//! - `duration`:日期运算(加减天数、求两个日期之间相差的天数)。
//!
//! 为了让测试结果**确定**,示例统一使用不带时区的 [`chrono::NaiveDate`],
//! 而不是依赖「当前时间」`Utc::now()` / `Local::now()`。
pub mod duration;
pub mod parsing;
// 重新导出常用函数,调用方可以直接 `use rt_45_chrono::parse_date;`。
pub use duration::{add_days, days_between, sub_days};
pub use parsing::{format_cn, format_iso, parse_date}; examples/45_chrono/src/duration.rs
//! 日期运算:加减天数与求差值。
//!
//! 关键类型是 [`chrono::Duration`],它表示一段「时间间隔」。
//! - 日期 `+ Duration` 得到新日期。
//! - 两个日期相减 `d2 - d1` 得到一个 `Duration`,再用 `num_days()` 取天数。
use chrono::{Duration, NaiveDate};
/// 在 `date` 基础上往后推 `days` 天。
///
/// # 示例
///
/// ```
/// use rt_45_chrono::add_days;
/// use chrono::NaiveDate;
/// let d = NaiveDate::from_ymd_opt(2026, 6, 15).unwrap();
/// assert_eq!(add_days(d, 10), NaiveDate::from_ymd_opt(2026, 6, 25).unwrap());
/// ```
pub fn add_days(date: NaiveDate, days: i64) -> NaiveDate {
date + Duration::days(days)
}
/// 在 `date` 基础上往前推 `days` 天。
pub fn sub_days(date: NaiveDate, days: i64) -> NaiveDate {
date - Duration::days(days)
}
/// 求 `from` 到 `to` 之间相差的天数。
///
/// 若 `to` 在 `from` 之后结果为正,反之为负。
///
/// # 示例
///
/// ```
/// use rt_45_chrono::days_between;
/// use chrono::NaiveDate;
/// let a = NaiveDate::from_ymd_opt(2026, 6, 15).unwrap();
/// let b = NaiveDate::from_ymd_opt(2026, 6, 25).unwrap();
/// assert_eq!(days_between(a, b), 10);
/// ```
pub fn days_between(from: NaiveDate, to: NaiveDate) -> i64 {
(to - from).num_days()
}
#[cfg(test)]
mod tests {
use super::*;
fn d(y: i32, m: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(y, m, day).unwrap()
}
#[test]
fn add_and_sub_cross_month() {
// 跨月加法:6 月只有 30 天,6/25 + 10 天 = 7/5。
assert_eq!(add_days(d(2026, 6, 25), 10), d(2026, 7, 5));
// 减法回到原点。
assert_eq!(sub_days(d(2026, 7, 5), 10), d(2026, 6, 25));
}
#[test]
fn days_between_sign() {
assert_eq!(days_between(d(2026, 6, 15), d(2026, 6, 25)), 10);
// 反向为负。
assert_eq!(days_between(d(2026, 6, 25), d(2026, 6, 15)), -10);
// 相同日期为 0。
assert_eq!(days_between(d(2026, 6, 15), d(2026, 6, 15)), 0);
}
#[test]
fn days_between_cross_year() {
// 2024 是闰年,2024-2-29 存在;这里跨年算整年。
assert_eq!(days_between(d(2025, 1, 1), d(2026, 1, 1)), 365);
}
} examples/45_chrono/src/parsing.rs
//! 日期的解析与格式化。
//!
//! - **解析**:用 [`NaiveDate::parse_from_str`] 把字符串按指定格式变成日期。
//! - **格式化**:用 [`NaiveDate::format`] 配合格式串把日期变回字符串。
//!
//! 格式串里的占位符(部分常用):
//! - `%Y` 四位年、`%m` 两位月、`%d` 两位日。
//! - 普通字符(包括汉字「年月日」)原样输出。
use chrono::NaiveDate;
/// 把 `"2026-06-15"` 这样的字符串解析成 [`NaiveDate`]。
///
/// 解析可能失败(比如格式不符、日期非法),所以返回 `Result`。
///
/// # 示例
///
/// ```
/// use rt_45_chrono::parse_date;
/// let d = parse_date("2026-06-15").unwrap();
/// assert_eq!(d.to_string(), "2026-06-15");
/// ```
pub fn parse_date(s: &str) -> Result<NaiveDate, chrono::ParseError> {
NaiveDate::parse_from_str(s, "%Y-%m-%d")
}
/// 把日期格式化成 ISO 风格的 `YYYY-MM-DD` 字符串。
pub fn format_iso(date: NaiveDate) -> String {
date.format("%Y-%m-%d").to_string()
}
/// 把日期格式化成中文风格的 `YYYY年MM月DD日` 字符串。
pub fn format_cn(date: NaiveDate) -> String {
date.format("%Y年%m月%d日").to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_valid_date() {
let d = parse_date("2026-06-15").unwrap();
assert_eq!(d, NaiveDate::from_ymd_opt(2026, 6, 15).unwrap());
}
#[test]
fn parse_invalid_date_is_err() {
// 2 月没有 30 号,解析应当失败。
assert!(parse_date("2026-02-30").is_err());
// 格式不符也失败。
assert!(parse_date("not-a-date").is_err());
}
#[test]
fn format_round_trip() {
let d = NaiveDate::from_ymd_opt(2026, 6, 15).unwrap();
assert_eq!(format_iso(d), "2026-06-15");
assert_eq!(format_cn(d), "2026年06月15日");
}
} examples/45_chrono/Cargo.toml
[package]
name = "rt_45_chrono"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
chrono = "0.4"