🦀 Rust 日期时间三国杀:Jiff 安全秒 DST,Chrono 老而弥坚,Time 轻量疾跑

🦀 Rust 日期时间三国杀:Jiff 安全秒 DST,Chrono 老而弥坚,Time 轻量疾跑

Photos provided by Unsplash OR Pexels

Rust 日期时间库实战指南:Jiff vs Chrono vs Time

Jiff 是什么

Jiff 是一个 Rust 的高级日期时间库,旨在引导开发者“跳入成功的陷阱”(pit of success),通过设计使正确的使用方式成为最简单和最自然的路径。该库专注于提供难以误用的高级日期时间原语,同时确保合理的性能表现。

Jiff 的核心功能包括:

  • 时间戳(Timestamp):表示 UTC 时刻,支持纳秒级精度。
  • 时区感知日期时间(Zoned):锚定到 IANA 时区数据库(TZDB),处理本地民事时间和时区偏移/过渡规则。
  • 时间跨度(Span):用于构建持续时间,支持单位如月、小时等。
  • 单位(Unit):用于四舍五入的时间粒度,如纳秒、秒、分钟、日、周、月、年。
  • 时区(Tz):时区标识符,如 “America/New_York”。
  • 格式化和解析:支持 RFC 3339、ISO-8601、RFC 2822 和 strftime 风格的模式,实现无损往返。

Jiff 自动无缝集成 IANA TZDB,支持 DST(夏令时)感知的算术运算和四舍五入、格式化和解析时区感知日期时间、可选的 Serde 支持等。该库深受 Temporal(TC39 JavaScript 日期时间提案)的启发,旨在为 Rust 带来类似的安全性和表达力。

平台支持:

  • Unix:从 /usr/share/zoneinfo 或 TZDIR 环境变量读取 TZDB;从 /etc/localtime 获取系统时区。
  • Windows:嵌入 TZDB,使用 GetDynamicTimeZoneInformation 获取系统时区,并通过 Unicode CLDR 数据映射 Windows 时区 ID 到 IANA ID。
  • 无 std 支持:核心功能可在 no-std 环境下工作(除平台特定 TZDB 访问外)。
  • 最低 Rust 版本:1.70.0(次要版本可能提升,但补丁版本保持不变)。

Jiff 的设计哲学强调零外部运行时依赖(Unix 上除 TZDB 外),保守使用 crate,仅在必要时添加(如 Windows 的 windows-sys 或 Serde 的互操作性)。它不计划拆分成多个子 crate,以保持维护开销低。

Chrono 是什么

Chrono 是 Rust 中最受欢迎的日期时间库,已成为事实标准,提供全面的日期时间处理功能,包括 UTC、本地时间、时区偏移和 Naive(无时区)类型。

核心功能包括:

  • DateTime:时区感知日期时间,支持 Utc、Local 和 FixedOffset。
  • NaiveDateTime:无时区日期时间,用于简单计算。
  • Duration:时间跨度,支持秒级精度。
  • 格式化和解析:支持 strftime 风格的模式、RFC 3339 等。
  • 时区支持:通过 FixedOffset 手动处理,或依赖系统时区。

Chrono 设计于 2014-2015 年,灵感来源于 Python 的 datetime 和 Ruby 的 Time。平台支持广泛,包括 no-std(部分功能),最低 Rust 版本 1.31.0。

缺点:API 设计较老,容易误用 Naive 类型或 FixedOffset,导致 DST 错误;维护活跃度较低。

Time 是什么

Time(time-rs)是一个现代、轻量的 Rust 日期时间库,专注于性能和简洁性,提供 UTC 和偏移日期时间处理。

核心功能包括:

  • OffsetDateTime:带偏移的日期时间,支持 Utc 和 Local。
  • PrimitiveDateTime:无偏移的原始日期时间。
  • Duration:时间跨度,支持纳秒级精度。
  • 格式化和解析:支持多种格式,如 RFC 3339、ISO 8601。
  • 时区支持:主要通过 UtcOffset,手动处理,无原生 TZDB。

Time 设计强调 no-std 支持、零分配和极致性能。平台支持优秀,包括 WASM 和嵌入式。最低 Rust 版本 1.34.0。

缺点:时区支持较弱,不处理 DST 模糊时间;适合简单场景,不如 Jiff 安全。

如何使用 Jiff

安装

在 Cargo.toml 中添加:

[dependencies]
jiff = "0.1"  # 或最新版本

基本示例

use jiff::{Timestamp, ToSpan, Zoned};

fn main() -> Result<(), jiff::Error> {
    let ts: Timestamp = "2026-01-23T23:34:00Z".parse()?;
    let zoned = ts.in_tz("Asia/Singapore")?.checked_add(1.months())?;
    println!("{}", zoned);  // 输出带时区
    Ok(())
}

如何使用 Chrono

安装

[dependencies]
chrono = "0.4"

基本示例

use chrono::{DateTime, Duration, Utc, FixedOffset};

fn main() {
    let dt: DateTime<Utc> = "2026-01-23T23:34:00Z".parse().unwrap();
    let sg_offset = FixedOffset::east_opt(8 * 3600).unwrap();
    let sg_dt = dt.with_timezone(&sg_offset) + Duration::days(30);
    println!("{}", sg_dt);
}

如何使用 Time

安装

[dependencies]
time = "0.3"

基本示例

use time::{format_description::well_known::Rfc3339, OffsetDateTime, UtcOffset};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let dt = OffsetDateTime::parse("2026-01-23T23:34:00Z", &Rfc3339)?;
    let sg_offset = UtcOffset::from_hms(8, 0, 0)?;
    let sg_dt = dt.to_offset(sg_offset) + time::Duration::days(30);
    println!("{}", sg_dt.format(&Rfc3339)?);
    Ok(())
}

最佳实践实战

  • Jiff:始终使用 checked_* 方法处理溢出;优先 Zoned 处理 DST;启用 tzdb_embedded feature 自包含。
  • Chrono:避免 Naive 类型;手动检查 DST 边界;用于遗留生态。
  • Time:用于性能敏感场景;手动管理偏移,不依赖 TZDB。

实战项目:时区转换工具(三库对比)

Cargo.toml

[package]
name = "datetime-tool"
version = "0.1.0"
edition = "2021"

[dependencies]
jiff = { version = "0.1", features = ["serde", "tzdb_embedded"] }
chrono = "0.4"
time = "0.3"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
axum = "0.7"

src/main.rs

use axum::{routing::get, Router, Json};
use jiff::{ToSpan, Tz, Unit, Zoned};
use chrono::{DateTime, Duration, FixedOffset, Utc};
use time::{format_description::well_known::Rfc3339, OffsetDateTime, UtcOffset};
use serde::Serialize;

#[derive(Serialize)]
struct TimeResponse {
    jiff: String,
    chrono: String,
    time: String,
}

async fn get_time() -> Json<TimeResponse> {
    // Jiff
    let j_now = Zoned::now();
    let j_future = j_now.checked_add(1.months()).unwrap().round(Unit::Day).unwrap();

    // Chrono
    let c_now: DateTime<Utc> = Utc::now();
    let sg_offset = FixedOffset::east_opt(8 * 3600).unwrap();
    let c_sg = c_now.with_timezone(&sg_offset);
    let c_future = c_sg + Duration::days(30);

    // Time
    let t_now = OffsetDateTime::now_utc();
    let t_sg_offset = UtcOffset::from_hms(8, 0, 0).unwrap();
    let t_sg = t_now.to_offset(t_sg_offset);
    let t_future = t_sg + time::Duration::days(30);

    Json(TimeResponse {
        jiff: j_future.to_string(),
        chrono: c_future.to_rfc3339(),
        time: t_future.format(&Rfc3339).unwrap(),
    })
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/time", get(get_time));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

config.toml(配置示例)

default_tz = "Asia/Singapore"

Rust 日期时间库对比:Jiff vs Chrono vs Time

维度JiffChronoTime
正确性极高(DST 安全)中等(易出错)中等(偏移简单)
时区支持原生 TZDBFixedOffsetUtcOffset
性能合理优秀最快
生态增长中完美良好
推荐新项目遗留性能敏感

详细的参考资料

Rust 日期时间库对比:Jiff vs Chrono vs time

作为资深 Rust 工程师,我为你整理一份清晰、实用、代码并排的对比,聚焦三个主流库:

  • chrono:老牌王者,生态最广,但设计较老,容易误用
  • time (time-rs/time):现代、轻量、性能优秀,但时区支持较弱
  • jiff:最新(BurntSushi 出品),正确性最高,DST/模糊时间最安全,推荐新项目

快速结论表

维度chrono (0.4.x)time (0.3.x)jiff (0.1.x → 接近 1.0)你的推荐( +08)
正确性(DST、模糊时间)中等(很多坑)中等(FixedOffset 为主)极高(Temporal 风格,强制安全)jiff
时区支持手动 FixedOffset,TZDB 需额外基本 FixedOffset,无原生 TZDB原生完整 IANA TZDB + 系统时区jiff
性能优秀最快(轻量设计)非常接近(热路径优化好)time 微胜 / 平手
API 安全性低(Naive / FixedOffset 陷阱多)中等最高(pit of success)jiff
生态兼容性完美(sqlx、diesel、serde…)良好良好(快速增长,serde 优秀)chrono(遗留) / jiff(新)
学习曲线中等(文档老)低(简洁)低(现代、详细对比文档)jiff
二进制大小 / no-std中等最小小(保守设计)time / jiff
维护活跃度缓慢(基本维护)活跃非常活跃(目标 1.0)jiff
新项目首选(2026)遗留 / 极致兼容性能敏感、无时区复杂需求绝大多数新项目jiff

一句话总结
你在 +8.00,新项目直接用 jiff(时区正确性最强,+08 无夏令时也最安全)。
如果追求极致性能且不怎么用真实时区 → time。
已有 chrono 生态依赖 → 短期继续 chrono,逐步迁移 jiff。

代码并排对比(相同需求,3 种写法)

1. 获取当前 +8.00 时间

// chrono
use chrono::{Local, FixedOffset};
let now = Local::now();                          // 系统时区(希望是 +08)
let sg_offset = FixedOffset::east_opt(8*3600).unwrap();
let sg_now = now.with_timezone(&sg_offset);      // 手动偏移,容易错

// time
use time::{OffsetDateTime, utc_offset::UtcOffset};
let now = OffsetDateTime::now_local().unwrap();  // 系统时区
let sg_now = now.to_offset(UtcOffset::from_hms(8, 0, 0).unwrap());

// jiff(最推荐)
use jiff::Zoned;
let sg_now = Zoned::now();                       // 自动系统时区( +08)
println!("{}", sg_now);                          // 带 [Asia/Singapore]

2. 解析带时区字符串 → 加 3 个月

let input = "2026-03-15T14:30:00+08:00";

// chrono(不安全加法)
use chrono::{DateTime, Duration, Utc};
let dt: DateTime<Utc> = input.parse().unwrap();
let future = dt + Duration::days(90);            // 静默跨越 DST/闰月

// time(更安全,但仍 FixedOffset)
use time::{format_description::well_known::Rfc3339, PrimitiveDateTime};
let parsed = OffsetDateTime::parse(input, &Rfc3339).unwrap();
let future = parsed + time::Duration::days(90);

// jiff(强制 checked + 日历算术)
use jiff::{Timestamp, ToSpan};
let ts: Timestamp = input.parse().unwrap();
let sg = ts.in_tz("Asia/Singapore").unwrap();
let future = sg.checked_add(3.months()).unwrap();  // 日历月,安全

3. 四舍五入到最近 15 分钟(会议/日志常见)

// chrono(手动实现)
use chrono::Timelike;
let mut dt = Local::now();
let m = dt.minute() / 15 * 15;
dt = dt.with_minute(m).unwrap().with_second(0).unwrap();

// time(也手动)
use time::ext::NumericalStdDuration;
let now = OffsetDateTime::now_local().unwrap();
let rounded = now - (now.minute() % 15).minutes();

// jiff(一行)
use jiff::Unit;
let rounded = Zoned::now().round(Unit::Minute * 15).unwrap();

4. 处理美国 DST 模糊时间(秋季后退 1:30 出现两次)

use jiff::tz::Disambiguation;

// chrono → 通常选一个或 None,很容易错
// time   → FixedOffset 根本不处理 DST 模糊

// jiff(唯一真正安全的)
let ambiguous = "2025-11-02T01:30:00"
    .parse::<jiff::civil::DateTime>()
    .unwrap()
    .to_zoned("America/New_York", Disambiguation::Earlier)
    .unwrap();  // 明确选第一个(-04:00)

5. Serde JSON(Web API 返回)

// chrono(需额外 with)
#[derive(serde::Serialize)]
struct Event {
    #[serde(with = "chrono::serde::ts_seconds")]
    time: chrono::DateTime<Utc>,
}

// time(内置支持好)
#[derive(serde::Serialize)]
struct Event {
    time: time::OffsetDateTime,   // 默认 RFC3339
}

// jiff(最现代)
#[derive(serde::Serialize)]
struct Event {
    time: jiff::Zoned,            // 默认带时区名称 ISO8601
}

三个库的真实优劣势总结(工程视角)

chrono
优点:生态王者,几乎所有数据库、框架、polars/arrow 都原生支持
缺点:设计 2014–2015 年风格,FixedOffset 滥用,DST 算术不安全,闰秒诡异
适合:遗留系统、追求零迁移成本

time
优点:极致轻量、性能最高、API 简洁现代、无 std 支持优秀
缺点:时区支持弱(无原生 TZDB,靠 FixedOffset),DST/模糊时间几乎不处理
适合:性能敏感、无复杂时区需求(纯 UTC 或固定偏移)、嵌入式/no-std

jiff
优点:正确性最高(Temporal 启发)、DST/模糊时间强制安全、原生 TZDB、坑最少
缺点:生态还在追赶(虽 serde 很好,但部分数据库 crate 迁移中)
适合:新项目、金融、日志、跨时区协作、任何需要“不出错”的场景

针对你( +08)的最终建议

  • 新项目 / 学习 / 个人项目直接用 jiff
    (无夏令时,但未来可能对接全球系统,jiff 的安全性和表达力最强)

  • 已有 chrono 项目 → 继续用 chrono,但新模块试用 jiff(可共存,边界转 Timestamp)

  • 极致性能 + 简单 UTC → 可以考虑 time

  • 下一步行动
    复制上面 jiff 示例到一个 main.rs,跑一遍对比感受差异
    然后在新 crate 里全面用 jiff 写一个小型 Web API 或日志处理器

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)