Rust 日期与时间(chrono)
程序总是要和时间打交道。“这段代码跑了多久?”“这张账单哪天到期?”“离出发旅行还有几天?”Rust 把这些问题分成两类。
标准库的 std::time 模块是一块秒表:它只负责测量过去了多少时间,不认识日历、月份,也不知道今天星期几。处理日历相关的事情,几乎所有 Rust 项目都会用 chrono 这个 crate。这一页先讲秒表,再讲日历。
用 Instant 测量耗时
Section titled “用 Instant 测量耗时”Instant::now() 从计算机的时钟读取一个时刻。之后调用 .elapsed(),就能知道从那一刻起过去了多少时间,结果是一个 Duration:
use std::time::{Duration, Instant};
fn main() { let start = Instant::now();
let mut total: u64 = 0; for i in 0..1_000_000 { total += i; }
let elapsed = start.elapsed(); println!("合计 = {total}"); println!("耗时:{:?}", elapsed); println!("耗时:{} ms", elapsed.as_millis()); println!("少于 5 秒?{}", elapsed < Duration::from_secs(5));}这里故意不展示输出:每台计算机、每次运行,耗时都不一样。你会看到类似 耗时:6.862793ms 的结果。试着点两次按钮,比较一下。
Instant 只能用来测量。它不能当作日期打印出来,而且只会往前走。这正是给代码计时需要的:就算有人修改了系统时钟,它也不受影响。
Duration:一段时间
Section titled “Duration:一段时间”Duration 表示一段时间的长度,比如“90 秒”。你可以用秒或毫秒创建它,把几个 Duration 加起来,也可以比较大小:
use std::time::Duration;
fn main() { let short = Duration::from_millis(1500); let long = Duration::from_secs(90);
println!("{:?}", short); println!("{:?}", long); println!("{} ms", short.as_millis()); println!("{:.1} s", short.as_secs_f64());
let total = short + long; println!("合计:{:?}", total); println!("{} 分 {} 秒", long.as_secs() / 60, long.as_secs() % 60); println!("{}", short < long);}输出:
1.5s90s1500 ms1.5 s合计:91.5s1 分 30 秒trueDuration 在很多地方都会出现:std::thread::sleep(Duration::from_millis(100)) 让线程暂停一会儿,网络库的超时设置也接收 Duration。
用 SystemTime 获取现实时间
Section titled “用 SystemTime 获取现实时间”SystemTime 是计算机时钟上的时间,也就是屏幕角落里显示的那个。最常见的用法,是问它距离 Unix 纪元(Unix epoch,1970 年 1 月 1 日 UTC)过去了多久。这个数字叫作 Unix 时间戳:
use std::time::{SystemTime, UNIX_EPOCH};
fn main() { let now = SystemTime::now(); let since_1970 = now .duration_since(UNIX_EPOCH) .expect("系统时钟早于 1970 年"); println!("距 1970 年 1 月 1 日的秒数:{}", since_1970.as_secs());}这个数字取决于运行的时刻,所以这里同样不展示输出。为什么 duration_since 返回的是 Result?因为系统时钟可能不准,也可能被往回调。这也是给代码计时要用 Instant 而不是 SystemTime 的原因。
std::time 能做的差不多就这些了。它根本不知道“三月”或“星期五”是什么意思。要处理这些,就需要 chrono。
添加 chrono
Section titled “添加 chrono”把这个 crate 加进项目(如果你还不熟悉,先看看 Cargo 与 crate):
cargo add chrono[dependencies]chrono = "0.4"Rust Playground 也内置了 chrono,所以下面每个按钮都能直接运行。chrono 的类型分成两大类:
- Naive 类型(
NaiveDate、NaiveTime、NaiveDateTime)是不带时区的日期和时间,就像日历上写的“2025 年 3 月 14 日”或“09:30”。日常代码大多用的是这一类。 DateTime<Tz>是带时区的某个时刻,比如DateTime<Utc>或DateTime<Local>。
用固定值创建日期
Section titled “用固定值创建日期”NaiveDate::from_ymd_opt(year, month, day) 用来创建日期。它返回一个 Option,因为并不是每种组合都是真实存在的日期:
use chrono::{Datelike, NaiveDate, NaiveTime};
fn main() { let date = NaiveDate::from_ymd_opt(2025, 3, 14).unwrap(); println!("{date}"); println!("{} 年 {} 月 {} 日", date.year(), date.month(), date.day());
let time = NaiveTime::from_hms_opt(9, 30, 0).unwrap(); let meeting = date.and_time(time); println!("{meeting}");
let other = date.and_hms_opt(17, 45, 10).unwrap(); println!("{other}");
let impossible = NaiveDate::from_ymd_opt(2025, 2, 30); println!("{:?}", impossible);}输出:
2025-03-142025 年 3 月 14 日2025-03-14 09:30:002025-03-14 17:45:10None2 月 30 日并不存在,所以你得到的是 None,而不是一个错误的日期。如果是你自己写死的固定值,用 .unwrap() 没问题;如果值来自用户输入,就要处理 None 的情况。
use chrono::Datelike 这一行引入了 .year()、.month() 和 .day() 这些方法。(Timelike 对 .hour() 和 .minute() 起同样的作用。)
当前日期和时间
Section titled “当前日期和时间”Utc::now() 返回当前的 UTC 时刻,Local::now() 返回计算机所在时区的当前时刻:
use chrono::{Local, Utc};
fn main() { let utc_now = Utc::now(); let local_now = Local::now();
println!("UTC:{}", utc_now); println!("本地:{}", local_now); println!("今天:{}", local_now.format("%Y-%m-%d"));
let today = local_now.date_naive(); println!("转成 NaiveDate:{today}");}输出是今天的日期,所以每天都不一样。Playground 的服务器使用 UTC,因此在那里前两行显示的时间是一样的。.date_naive() 会去掉时区和时间,只留下一个普通的 NaiveDate。一个好习惯:只在程序的入口处获取一次“现在”,然后把日期作为参数传给各个函数。这样函数就很容易测试,后面的截止日期示例会让你看到这一点。
.format() 接收一个格式模板,模板里的每个 % 代码都会被替换成日期的一部分。%Y 是年份,%m 是月份数字,%d 是日:
use chrono::NaiveDate;
fn main() { let date = NaiveDate::from_ymd_opt(2025, 3, 14).unwrap(); let dt = date.and_hms_opt(9, 5, 0).unwrap();
println!("{}", date.format("%Y-%m-%d")); println!("{}", date.format("%d/%m/%Y")); println!("{}", date.format("%A, %B %-d, %Y")); println!("{}", date.format("%a %d %b")); println!("{}", dt.format("%H:%M")); println!("{}", dt.format("%-I:%M %p"));
let file_name = date.format("report-%Y%m%d.txt").to_string(); println!("{file_name}");}输出:
2025-03-1414/03/2025Friday, March 14, 2025Fri 14 Mar09:059:05 AMreport-20250314.txt最常用的代码:%Y 年,%m 月(01–12),%d 日(01–31),%H 小时(00–23),%M 分钟,%S 秒,%A 星期名,%B 月份名。在 % 后面加一个 - 可以去掉前导零,所以 %-d 打印的是 5 而不是 05。星期名和月份名始终是英文,chrono 默认不做本地化。.format() 返回的东西可以直接打印;需要 String 的时候,调用 .to_string()。
把文本解析成日期
Section titled “把文本解析成日期”NaiveDate::parse_from_str 做的是反方向的事:用同样的 % 模板读取文本。它返回一个 Result,因为文本可能和模板对不上:
use chrono::{NaiveDate, NaiveDateTime};
fn main() { let a = NaiveDate::parse_from_str("2025-03-14", "%Y-%m-%d"); println!("{:?}", a);
let b = NaiveDate::parse_from_str("14/03/2025", "%d/%m/%Y"); println!("{:?}", b);
let c: Result<NaiveDate, _> = "2025-03-14".parse(); println!("{:?}", c);
let d = NaiveDateTime::parse_from_str("2025-03-14 09:30", "%Y-%m-%d %H:%M"); println!("{:?}", d);
for text in ["2025-13-01", "March 14", "2025-03"] { match NaiveDate::parse_from_str(text, "%Y-%m-%d") { Ok(date) => println!("解析成功:{date}"), Err(e) => println!("无法解析 {text:?}:{e}"), } }}输出:
Ok(2025-03-14)Ok(2025-03-14)Ok(2025-03-14)Ok(2025-03-14T09:30:00)无法解析 "2025-13-01":input is out of range无法解析 "March 14":input contains invalid characters无法解析 "2025-03":premature end of input对于标准的 YYYY-MM-DD 格式,也可以直接调用 .parse()。13 月不存在,所以第一个错误例子报的是“out of range”(超出范围)。第二个是“包含无效字符”,第三个是“输入提前结束”。这些错误信息是 chrono 自带的英文,虽然简短,但足够清楚。
手动给日期加天数其实挺难:每个月天数不同,还有闰年。chrono 会帮你处理好。可以用 checked_add_days 配合 Days,用 checked_add_months 配合 Months,或者直接加一个 TimeDelta:
use chrono::{Days, Months, NaiveDate, TimeDelta};
fn main() { let start = NaiveDate::from_ymd_opt(2025, 1, 31).unwrap();
let plus_30 = start.checked_add_days(Days::new(30)).unwrap(); println!("30 天后:{plus_30}");
let minus_7 = start.checked_sub_days(Days::new(7)).unwrap(); println!("7 天前:{minus_7}");
let next_month = start.checked_add_months(Months::new(1)).unwrap(); println!("1 个月后:{next_month}");
let week_later = start + TimeDelta::weeks(1); println!("1 周后:{week_later}");
let dt = start.and_hms_opt(23, 30, 0).unwrap(); let later = dt + TimeDelta::hours(2); println!("2 小时后:{later}");}输出:
30 天后:2025-03-027 天前:2025-01-241 个月后:2025-02-281 周后:2025-02-072 小时后:2025-02-01 01:30:00看看“1 个月后”这一行:1 月 31 日加一个月是 2 月 28 日,因为没有 2 月 31 日,chrono 会选择那个月最后一个有效的日子。checked_ 系列方法返回 Option,只有结果超出 chrono 支持的(非常大的)年份范围时才会是 None。给 23:30 加 2 小时,也正确地跨到了第二天。
星期几与两个日期之差
Section titled “星期几与两个日期之差”.weekday()(来自 Datelike)告诉你某天是星期几。用一个日期减去另一个日期,得到的是 TimeDelta,再用 .num_days() 把它变成数字:
use chrono::{Datelike, NaiveDate, Weekday};
fn main() { let date = NaiveDate::from_ymd_opt(2025, 3, 15).unwrap(); let day = date.weekday(); println!("{date} 是 {day}({})", date.format("%A"));
let weekend = matches!(day, Weekday::Sat | Weekday::Sun); println!("周末?{weekend}");
let start = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(); let end = NaiveDate::from_ymd_opt(2025, 12, 25).unwrap(); let gap = end - start; println!("{} 天,也就是 {} 个整周", gap.num_days(), gap.num_weeks()); println!("反过来减:{} 天", (start - end).num_days());
let check_in = start.and_hms_opt(14, 0, 0).unwrap(); let check_out = NaiveDate::from_ymd_opt(2025, 1, 4).unwrap().and_hms_opt(11, 0, 0).unwrap(); println!("酒店入住:{} 小时", (check_out - check_in).num_hours());}输出:
2025-03-15 是 Sat(Saturday)周末?true358 天,也就是 51 个整周反过来减:-358 天酒店入住:69 小时Weekday 是一个枚举,变体从 Mon 到 Sun,所以可以对它使用 match。注意,如果被减的日期更晚,差值可能是负数。这其实很有用:剩余天数为负,就表示“已逾期”。
示例:距离截止日期还有几天
Section titled “示例:距离截止日期还有几天”把前面的内容组合起来。下面的函数把“今天”作为参数传进来,而不是在函数内部调用 Local::now()。这样下面的输出永远不变,函数也很容易测试。在真实程序里,你会传入 Local::now().date_naive()。
use chrono::NaiveDate;
fn days_left(today: NaiveDate, deadline: NaiveDate) -> String { let days = (deadline - today).num_days(); match days { 0 => String::from("due today"), 1 => String::from("due tomorrow"), d if d > 1 => format!("{d} days left"), -1 => String::from("1 day overdue"), d => format!("{} days overdue", -d), }}
fn main() { let today = NaiveDate::from_ymd_opt(2025, 6, 10).unwrap();
let tasks = [ ("Pay rent", "2025-06-10"), ("Send report", "2025-06-11"), ("Renew passport", "2025-07-01"), ("Return library book", "2025-06-09"), ("Dentist bill", "2025-06-03"), ];
println!("Today is {}", today.format("%A, %-d %B %Y")); for (name, date_text) in tasks { let deadline = NaiveDate::parse_from_str(date_text, "%Y-%m-%d").unwrap(); println!("{:<20} {} {}", name, deadline.format("%d %b"), days_left(today, deadline)); }}输出:
Today is Tuesday, 10 June 2025Pay rent 10 Jun due todaySend report 11 Jun due tomorrowRenew passport 01 Jul 21 days leftReturn library book 09 Jun 1 day overdueDentist bill 03 Jun 7 days overdue试着把 today 改成别的日期,再运行一次。{:<20} 会把名字补齐到 20 个字符,让各列对齐(见输出)。这个例子里的字符串保留了英文,原因就在这里:汉字在屏幕上占两个字符宽,在 UTF-8 里每个占 3 个字节,用 {:<20} 补齐的话,各列就对不齐了。
chrono 不是唯一的选择。time crate 更小巧,也很流行;较新的 jiff crate 在时区和夏令时方面下了更多功夫。这里用 chrono,是因为它用得最广泛,而且 Playground 里就有。
忘了写 use chrono::Datelike。没有它,.year()、.month() 和 .weekday() 都用不了。错误信息说这个方法是“private”(私有的),挺让人困惑,但 help 那一行告诉了你真正的解决办法:
error[E0624]: method `year` is private --> src/main.rs:5:25 |5 | println!("{}", date.year()); | ^^^^ private method | = help: items from traits can only be used if the trait is in scopehelp: trait `Datelike` which provides `year` is implemented but not in scope; perhaps you want to import it |1 + use chrono::Datelike; |意思是:trait 里的方法,只有在这个 trait 被引入作用域之后才能用。加上 use chrono::Datelike; 就好了。
把 %m 和 %M 搞混。%m 是月份,%M 是分钟。对于带时间的日期,写错了也不会报错,它只会悄悄地打印出分钟数:
use chrono::NaiveDate;
fn main() { let meeting = NaiveDate::from_ymd_opt(2025, 3, 14).unwrap().and_hms_opt(9, 5, 0).unwrap(); println!("{}", meeting.format("%Y-%M-%d")); // 错误:这是分钟 println!("{}", meeting.format("%Y-%m-%d")); // 正确:这是月份}输出:
2025-05-142025-03-14用 SystemTime 给代码计时。系统时钟可能会跳变(比如和网络时间同步的时候)。测量耗时用 Instant,只有需要真实日期时才用 SystemTime 或 chrono。
在业务逻辑深处调用 Local::now()。这会让结果每天都变,测试也很难写。像 days_left 那样,把日期作为参数传进去。
std::time::Instant测量耗时;Duration表示一段时间;SystemTime是现实中的时钟(适合获取 Unix 时间戳)。- 处理日历要加上
chrono。不带时区的日期和时间用NaiveDate/NaiveDateTime,当前时刻用Utc::now()/Local::now()。 from_ymd_opt返回Option,parse_from_str返回Result,因为不是每个输入都是真实的日期。.format("%Y-%m-%d")用%代码把日期变成文本。记住:%m是月份,%M是分钟。- 用
checked_add_days、checked_add_months或+ TimeDelta增加时间。两个日期相减得到TimeDelta,再调用.num_days()。 - 把“今天”作为参数传给函数,而不是在函数内部读取时钟。
小测验
1. 要测量一段代码运行了多久,应该用哪个类型?
2. NaiveDate::from_ymd_opt(2025, 2, 30) 返回什么?
3. 在 chrono 的格式字符串里,%M 表示什么?
4. 你把两个 NaiveDate 相减:deadline - today。怎样得到相差的天数?
5. 在 chrono 里,2025 年 1 月 31 日加上 Months::new(1) 是哪一天?
得分: 0 / 5
- std::time — 标准库文档(英文):
Instant、Duration和SystemTime的用法和更多示例。 - chrono 文档 — docs.rs(英文):chrono 所有类型和方法的完整指南。
- chrono 格式代码 — docs.rs(英文):
format和parse_from_str可用的全部%代码。 - Date and Time — Rust Cookbook(英文):常见日期和时间任务的简短示例。
- jiff 文档 — docs.rs(英文):一个较新的日期时间 crate,时区支持很强,想比较的话可以看看。