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.
Đo thời gian đã trôi qua với Instant
Phần tiêu đề “Đo thời gian đã trôi qua với Instant”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));}Ở đâ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.
Duration
Phần tiêu đề “Duration”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.5s90s1500 ms1.5 stổng: 91.5s1 phút 30 giâytrueBạ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.
Giờ thực với SystemTime
Phần tiêu đề “Giờ thực với SystemTime”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());}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 chrono
Phần tiêu đề “Thêm 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):
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>hayDateTime<Local>.
Tạo ngày từ giá trị cố định
Phần tiêu đề “Tạo ngày từ giá trị cố định”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-14năm 2025, tháng 3, ngày 142025-03-14 09:30:002025-03-14 17:45:10NoneNgà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().)
Ngày giờ hiện tại
Phần tiêu đề “Ngày giờ hiện tại”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}");}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.
Định dạng ngày tháng
Phần tiêu đề “Định dạng ngày tháng”.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-1414/03/2025Friday, March 14, 2025Fri 14 Mar09:059:05 AMreport-20250314.txtTê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.
Chuyển văn bản thành ngày tháng
Phần tiêu đề “Chuyển văn bản thành ngày tháng”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 rangeKhông đọc được "March 14": input contains invalid charactersKhông đọc được "2025-03": premature end of inputVớ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ính toán với ngày tháng
Phần tiêu đề “Tính toán với ngày tháng”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-027 ngày trước: 2025-01-241 tháng sau: 2025-02-281 tuần sau: 2025-02-072 giờ sau: 2025-02-01 01:30:00Hã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? true358 ngày, tức 51 tuần trọn vẹntính ngược lại: -358 ngàythời gian ở khách sạn: 69 giờ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”.
Ví dụ: còn bao nhiêu ngày tới hạn chót
Phần tiêu đề “Ví dụ: còn bao nhiêu ngày tới hạn chót”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 2025Trả tiền nhà 10 Jun hạn chót hôm nayGửi báo cáo 11 Jun hạn chót ngày maiGia hạn hộ chiếu 01 Jul còn 21 ngàyTrả sách thư viện 09 Jun quá hạn 1 ngàyHóa đơn nha sĩ 03 Jun quá hạn 7 ngàyHã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.
Lỗi thường gặp
Phần tiêu đề “Lỗi thường gặp”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 scopehelp: 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-142025-03-14Đ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.
Tóm tắt
Phần tiêu đề “Tóm tắt”std::time::Instantđo thời gian đã trôi qua;Durationlà một khoảng thời gian;SystemTimelà giờ thực trên đồng hồ (hợp cho Unix timestamp).- Với lịch, hãy thêm
chrono. DùngNaiveDate/NaiveDateTimecho 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_opttrả về mộtOption, vàparse_from_strtrả về mộtResult, 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:%mlà tháng,%Mlà phút.- Cộng thời gian bằng
checked_add_days,checked_add_monthshoặc+ TimeDelta. Trừ hai ngày để có mộtTimeDelta, 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
Phần tiêu đề “Kiểm tra nhanh”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
Đọc thêm
Phần tiêu đề “Đọc thêm”- std::time — thư viện chuẩn Rust (tiếng Anh):
Instant,DurationvàSystemTimevới nhiều ví dụ hơn. - Tài liệu chrono — docs.rs (tiếng Anh): hướng dẫn đầy đủ về mọi kiểu và phương thức của chrono.
- Mã định dạng của chrono — docs.rs (tiếng Anh): danh sách đầy đủ các mã
%choformatvàparse_from_str. - Date and Time — Rust Cookbook (tiếng Anh): các công thức ngắn cho những việc thường gặp với ngày và giờ.
- Tài liệu jiff — docs.rs (tiếng Anh): một crate ngày giờ mới hơn, hỗ trợ múi giờ rất tốt, nếu bạn muốn so sánh.