Bỏ qua để đến nội dung

Ngày và giờ trong Rust với chrono

Chương trình lúc nào cũng phải làm việc với thời gian. “Việc này mất bao lâu?” “Hóa đơn đến hạn ngày nào?” “Còn bao nhiêu ngày nữa thì đi du lịch?” Rust chia những câu hỏi này thành hai loại.

Module std::time của thư viện chuẩn là một đồng hồ bấm giờ: nó đo xem bao nhiêu thời gian đã trôi qua. Nó không biết gì về lịch, tháng hay thứ trong tuần. Với những việc liên quan tới lịch, gần như mọi dự án Rust đều dùng crate chrono. Trang này bắt đầu với đồng hồ bấm giờ, rồi chuyển sang lịch.

Instant::now() ghi lại một lần đọc đồng hồ của máy tính. Sau đó, .elapsed() cho bạn biết bao nhiêu thời gian đã trôi qua kể từ lần đọc đó, dưới dạng một 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 = {total}");
println!("mất {:?}", elapsed);
println!("mất {} ms", elapsed.as_millis());
println!("dưới 5 giây? {}", elapsed < Duration::from_secs(5));
}
▶ Tự chạy thử

Ở đây cố ý không có kết quả: thời gian khác nhau trên mỗi máy và mỗi lần chạy. Bạn sẽ thấy thứ gì đó như mất 6.862793ms. Hãy thử bấm nút hai lần và so sánh.

Một Instant chỉ hữu ích để đo. Bạn không thể in nó ra như một ngày tháng, và nó chỉ đi tới. Đó chính xác là điều bạn muốn khi đo thời gian chạy code, vì nó không bao giờ bị ảnh hưởng khi ai đó chỉnh đồng hồ hệ thống.

Một Duration là một khoảng thời gian, như “90 giây”. Bạn tạo nó từ số giây hoặc mili giây, cộng các khoảng thời gian lại với nhau và so sánh chúng:

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!("tổng: {:?}", total);
println!("{} phút {} giây", long.as_secs() / 60, long.as_secs() % 60);
println!("{}", short < long);
}

Kết quả:

1.5s
90s
1500 ms
1.5 s
tổng: 91.5s
1 phút 30 giây
true
▶ Tự chạy thử

Bạn sẽ gặp Duration ở rất nhiều nơi: std::thread::sleep(Duration::from_millis(100)) tạm dừng một luồng, và thời gian chờ (timeout) trong các thư viện mạng cũng nhận một Duration.

SystemTime là thời gian trên đồng hồ của máy tính, cái bạn thấy ở góc màn hình. Việc thường làm với nó là hỏi đã bao lâu kể từ Unix epoch, ngày 1 tháng 1 năm 1970 (UTC). Con số đó được gọi là Unix timestamp:

use std::time::{SystemTime, UNIX_EPOCH};
fn main() {
let now = SystemTime::now();
let since_1970 = now
.duration_since(UNIX_EPOCH)
.expect("đồng hồ đang đặt ở thời điểm trước năm 1970");
println!("Số giây kể từ 1/1/1970: {}", since_1970.as_secs());
}
▶ Tự chạy thử

Con số phụ thuộc vào lúc bạn chạy, nên một lần nữa không có kết quả nào được hiển thị. Vì sao duration_since trả về một Result? Vì đồng hồ hệ thống có thể sai hoặc bị chỉnh lùi. Đó cũng là lý do bạn nên dùng Instant, không phải SystemTime, để đo thời gian chạy code.

std::time chỉ làm được chừng đó. Nó không hề biết “tháng Ba” hay “thứ Sáu” là gì. Để làm được điều đó, bạn cần chrono.

Thêm crate vào dự án của bạn (xem Cargo và crate nếu điều này còn mới):

Terminal window
cargo add chrono
[dependencies]
chrono = "0.4"

Rust Playground cũng có chrono, nên mọi nút bên dưới đều chạy được. chrono có hai họ kiểu:

  • Các kiểu naive (NaiveDate, NaiveTime, NaiveDateTime) là ngày và giờ không có múi giờ. Ví dụ “14 tháng 3 năm 2025” hay “09:30” ghi trên lịch. Phần lớn code hằng ngày dùng các kiểu này.
  • DateTime<Tz> là một thời điểm có múi giờ, như DateTime<Utc> hay DateTime<Local>.

NaiveDate::from_ymd_opt(year, month, day) tạo một ngày. Nó trả về một Option, vì không phải tổ hợp nào cũng là một ngày có thật:

use chrono::{Datelike, NaiveDate, NaiveTime};
fn main() {
let date = NaiveDate::from_ymd_opt(2025, 3, 14).unwrap();
println!("{date}");
println!("năm {}, tháng {}, ngày {}", 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);
}

Kết quả:

2025-03-14
năm 2025, tháng 3, ngày 14
2025-03-14 09:30:00
2025-03-14 17:45:10
None
▶ Tự chạy thử

Ngày 30 tháng 2 không tồn tại, nên bạn nhận được None thay vì một ngày hỏng. Với những giá trị cố định do chính bạn gõ, .unwrap() là ổn. Với giá trị từ người dùng, hãy xử lý trường hợp None.

Dòng use chrono::Datelike mang vào các phương thức .year(), .month() và .day(). (Timelike làm điều tương tự cho .hour() và .minute().)

Utc::now() cho thời điểm hiện tại theo UTC. Local::now() cho thời điểm đó theo múi giờ của chính máy tính:

use chrono::{Local, Utc};
fn main() {
let utc_now = Utc::now();
let local_now = Local::now();
println!("UTC: {}", utc_now);
println!("Giờ địa phương: {}", local_now);
println!("Hôm nay: {}", local_now.format("%Y-%m-%d"));
let today = local_now.date_naive();
println!("Dưới dạng NaiveDate: {today}");
}
▶ Tự chạy thử

Kết quả là ngày hôm nay, nên nó thay đổi mỗi ngày. Máy chủ của Playground dùng UTC, nên ở đó cả hai dòng đầu sẽ hiện cùng một giờ. .date_naive() bỏ đi múi giờ và giờ, chỉ để lại một NaiveDate đơn giản. Một thói quen tốt: lấy “bây giờ” một lần ở rìa chương trình, rồi truyền ngày đó vào các hàm của bạn. Cách này giúp các hàm dễ kiểm thử, như bạn sẽ thấy trong ví dụ về hạn chót.

.format() nhận một mẫu. Mỗi mã % được thay bằng một phần của ngày. %Y là năm, %m là số tháng, %d là ngày:

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}");
}

Kết quả:

2025-03-14
14/03/2025
Friday, March 14, 2025
Fri 14 Mar
09:05
9:05 AM
report-20250314.txt
▶ Tự chạy thử

Tên thứ và tên tháng (Friday, March) luôn là tiếng Anh, vì format của chrono mặc định dùng tiếng Anh. Những mã bạn sẽ dùng nhiều nhất: %Y năm, %m tháng (01–12), %d ngày (01–31), %H giờ (00–23), %M phút, %S giây, %A tên thứ, %B tên tháng. Dấu - sau % bỏ số 0 ở đầu, nên %-d in 5 thay vì 05. .format() trả về một thứ bạn có thể in ra. Hãy gọi .to_string() khi bạn cần một String.

NaiveDate::parse_from_str làm điều ngược lại: nó đọc văn bản theo cùng mẫu %. Nó trả về một Result, vì văn bản có thể không khớp:

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!("Đã đọc được: {date}"),
Err(e) => println!("Không đọc được {text:?}: {e}"),
}
}
}

Kết quả:

Ok(2025-03-14)
Ok(2025-03-14)
Ok(2025-03-14)
Ok(2025-03-14T09:30:00)
Không đọc được "2025-13-01": input is out of range
Không đọc được "March 14": input contains invalid characters
Không đọc được "2025-03": premature end of input
▶ Tự chạy thử

Với dạng chuẩn YYYY-MM-DD, bạn cũng có thể chỉ cần gọi .parse(). Tháng 13 không tồn tại, nên ví dụ sai đầu tiên báo “out of range” (ngoài phạm vi). Các thông báo lỗi của chrono bằng tiếng Anh: “invalid characters” là ký tự không hợp lệ, “premature end of input” là đầu vào kết thúc quá sớm. Chúng ngắn nhưng đủ rõ.

Tự cộng ngày vào một ngày tháng khó hơn bạn tưởng: các tháng có độ dài khác nhau, và còn có năm nhuận. chrono làm việc đó giúp bạn. Hãy dùng checked_add_days với Days, checked_add_months với Months, hoặc cộng một 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 ngày sau: {plus_30}");
let minus_7 = start.checked_sub_days(Days::new(7)).unwrap();
println!("7 ngày trước: {minus_7}");
let next_month = start.checked_add_months(Months::new(1)).unwrap();
println!("1 tháng sau: {next_month}");
let week_later = start + TimeDelta::weeks(1);
println!("1 tuần sau: {week_later}");
let dt = start.and_hms_opt(23, 30, 0).unwrap();
let later = dt + TimeDelta::hours(2);
println!("2 giờ sau: {later}");
}

Kết quả:

30 ngày sau: 2025-03-02
7 ngày trước: 2025-01-24
1 tháng sau: 2025-02-28
1 tuần sau: 2025-02-07
2 giờ sau: 2025-02-01 01:30:00
▶ Tự chạy thử

Hãy nhìn dòng “1 tháng sau”: 31 tháng 1 cộng một tháng là 28 tháng 2, vì không có ngày 31 tháng 2. chrono chọn ngày hợp lệ cuối cùng. Các phương thức checked_ trả về một Option, chỉ là None khi kết quả nằm ngoài phạm vi năm (rất rộng) của chrono. Cộng 2 giờ vào 23:30 sẽ chuyển đúng sang ngày hôm sau.

Thứ trong tuần và khoảng cách giữa hai ngày

Phần tiêu đề “Thứ trong tuần và khoảng cách giữa hai ngày”

.weekday() (từ Datelike) cho bạn biết ngày đó là thứ mấy. Lấy một ngày trừ đi một ngày khác cho ra một TimeDelta, và .num_days() biến nó thành một con số:

use chrono::{Datelike, NaiveDate, Weekday};
fn main() {
let date = NaiveDate::from_ymd_opt(2025, 3, 15).unwrap();
let day = date.weekday();
println!("{date} là {day} ({})", date.format("%A"));
let weekend = matches!(day, Weekday::Sat | Weekday::Sun);
println!("cuối tuần? {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!("{} ngày, tức {} tuần trọn vẹn", gap.num_days(), gap.num_weeks());
println!("tính ngược lại: {} ngày", (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!("thời gian ở khách sạn: {} giờ", (check_out - check_in).num_hours());
}

Kết quả:

2025-03-15 là Sat (Saturday)
cuối tuần? true
358 ngày, tức 51 tuần trọn vẹn
tính ngược lại: -358 ngày
thời gian ở khách sạn: 69 giờ
▶ Tự chạy thử

Weekday là một enum với các biến thể từ Mon tới Sun, nên bạn có thể dùng match trên nó. Hãy để ý rằng khoảng cách có thể âm nếu ngày thứ hai muộn hơn. Điều đó rất có ích: số ngày còn lại mà âm nghĩa là “quá hạn”.

Hãy ghép mọi thứ lại. Hàm nhận “hôm nay” làm tham số thay vì gọi Local::now() bên trong. Nhờ vậy kết quả bên dưới lúc nào cũng giống nhau, và hàm dễ kiểm thử. Trong một chương trình thật, bạn sẽ truyền vào 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("hạn chót hôm nay"),
1 => String::from("hạn chót ngày mai"),
d if d > 1 => format!("còn {d} ngày"),
-1 => String::from("quá hạn 1 ngày"),
d => format!("quá hạn {} ngày", -d),
}
}
fn main() {
let today = NaiveDate::from_ymd_opt(2025, 6, 10).unwrap();
let tasks = [
("Trả tiền nhà", "2025-06-10"),
("Gửi báo cáo", "2025-06-11"),
("Gia hạn hộ chiếu", "2025-07-01"),
("Trả sách thư viện", "2025-06-09"),
("Hóa đơn nha sĩ", "2025-06-03"),
];
println!("Hôm nay là {}", 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));
}
}

Kết quả:

Hôm nay là Tuesday, 10 June 2025
Trả tiền nhà 10 Jun hạn chót hôm nay
Gửi báo cáo 11 Jun hạn chót ngày mai
Gia hạn hộ chiếu 01 Jul còn 21 ngày
Trả sách thư viện 09 Jun quá hạn 1 ngày
Hóa đơn nha sĩ 03 Jun quá hạn 7 ngày
▶ Tự chạy thử

Hãy thử đổi today sang một ngày khác rồi chạy lại. {:<20} đệm tên cho đủ 20 ký tự để các cột thẳng hàng (xem In ra màn hình).

chrono không phải lựa chọn duy nhất. Crate time là một lựa chọn thay thế nhỏ hơn và phổ biến, còn crate jiff mới hơn thì đặc biệt chăm chút cho múi giờ và giờ mùa hè; trang này dùng chrono vì nó được dùng rộng rãi nhất và có sẵn trên Playground.

Quên use chrono::Datelike. Không có nó, .year(), .month() và .weekday() không dùng được. Lỗi nói rằng phương thức là “private” (riêng tư), điều này khá khó hiểu, nhưng dòng help cho bạn biết cách sửa thật sự:

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 scope
help: trait `Datelike` which provides `year` is implemented but not in scope; perhaps you want to import it
|
1 + use chrono::Datelike;
|

Dòng help nói rằng trait Datelike cung cấp year nhưng chưa nằm trong phạm vi, và gợi ý thêm use chrono::Datelike;.

Nhầm %m với %M. %m là tháng, %M là phút. Với một giá trị có cả ngày và giờ, dùng nhầm không gây lỗi gì. Nó lặng lẽ in ra số phút:

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")); // sai: phút
println!("{}", meeting.format("%Y-%m-%d")); // đúng: tháng
}

Kết quả:

2025-05-14
2025-03-14
▶ Tự chạy thử

Đo thời gian chạy code bằng SystemTime. Đồng hồ hệ thống có thể nhảy (ví dụ khi đồng bộ với internet). Hãy dùng Instant để đo, và chỉ dùng SystemTime hay chrono khi bạn cần ngày tháng thật.

Gọi Local::now() sâu bên trong logic. Làm vậy khiến kết quả thay đổi mỗi ngày và test khó viết. Hãy truyền ngày vào dưới dạng tham số, như days_left đã làm.

  • std::time::Instant đo thời gian đã trôi qua; Duration là một khoảng thời gian; SystemTime là giờ thực trên đồng hồ (hợp cho Unix timestamp).
  • Với lịch, hãy thêm chrono. Dùng NaiveDate / NaiveDateTime cho ngày giờ không có múi giờ, và Utc::now() / Local::now() cho thời điểm hiện tại.
  • from_ymd_opt trả về một Option, và parse_from_str trả về một Result, vì không phải đầu vào nào cũng là một ngày có thật.
  • .format("%Y-%m-%d") biến một ngày thành văn bản bằng các mã %. Nhớ rằng: %m là tháng, %M là phút.
  • Cộng thời gian bằng checked_add_days, checked_add_months hoặc + TimeDelta. Trừ hai ngày để có một TimeDelta, rồi gọi .num_days().
  • Truyền “hôm nay” vào các hàm thay vì đọc đồng hồ bên trong chúng.

Kiểm tra nhanh

1. Bạn nên dùng kiểu nào để đo một đoạn code chạy mất bao lâu?

2. NaiveDate::from_ymd_opt(2025, 2, 30) trả về gì?

3. Trong một chuỗi định dạng của chrono, %M nghĩa là gì?

4. Bạn trừ hai NaiveDate: deadline - today. Làm sao để lấy số ngày?

5. Ngày 31 tháng 1 năm 2025 cộng Months::new(1) trong chrono là ngày nào?

Điểm: 0 / 5