常用生态库 intermediate 20 分钟 更新 2026-06-15

用 chrono 处理日期与时间

用 chrono 解析、格式化日期时间,并做日期加减与差值计算。

用 chrono 处理日期与时间

你会学到什么

  • 为什么处理「日历日期」要用 chrono 而不是标准库的 std::time
  • NaiveDate::parse_from_str 把字符串解析成日期。
  • format 配合格式串把日期格式化成 2026-06-152026年06月15日
  • chrono::Duration 做日期加减,以及求两个日期之间相差的天数。
  • 为什么测试要用固定的 NaiveDate,而不是 now()

为什么用 chrono,而不是 std::time

标准库里有 std::time::SystemTimeDuration,但它们很底层: 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_dateparse_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() 取整数天数; tofrom 之后为正,反之为负:

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/5main.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"