Lewati ke konten

Tanggal dan Waktu di Rust dengan chrono

Program selalu berurusan dengan waktu. “Berapa lama proses ini berjalan?” “Kapan tagihan ini jatuh tempo?” “Tinggal berapa hari lagi sebelum liburan?” Rust membagi pertanyaan seperti ini menjadi dua jenis.

Modul std::time dari standard library adalah sebuah stopwatch: ia mengukur berapa banyak waktu yang berlalu. Ia tidak tahu apa-apa soal kalender, bulan, atau nama hari. Untuk urusan kalender, hampir semua proyek Rust memakai crate (library Rust) chrono. Halaman ini dimulai dari stopwatch, lalu lanjut ke kalender.

Mengukur waktu yang berlalu dengan Instant

Section titled “Mengukur waktu yang berlalu dengan Instant”

Instant::now() mencatat waktu saat ini dari jam komputer. Nanti, .elapsed() memberi tahu berapa lama waktu yang sudah berlalu sejak catatan itu, dalam bentuk 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!("butuh {:?}", elapsed);
println!("butuh {} ms", elapsed.as_millis());
println!("kurang dari 5 detik? {}", elapsed < Duration::from_secs(5));
}
▶ Coba sendiri

Output-nya sengaja tidak kami tampilkan: waktunya berbeda di setiap komputer dan setiap kali dijalankan. Kamu akan melihat sesuatu seperti butuh 6.862793ms. Coba klik tombolnya dua kali dan bandingkan hasilnya.

Instant hanya berguna untuk mengukur. Kamu tidak bisa mencetaknya sebagai tanggal, dan nilainya hanya bergerak maju. Justru itu yang kamu butuhkan untuk mengukur kecepatan kode, karena Instant tidak terpengaruh kalau seseorang mengubah jam sistem.

Duration adalah sebuah rentang waktu, misalnya “90 detik”. Kamu bisa membuatnya dari detik atau milidetik, menjumlahkan beberapa durasi, dan membandingkannya:

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: {:?}", total);
println!("{} menit {} detik", long.as_secs() / 60, long.as_secs() % 60);
println!("{}", short < long);
}

Output:

1.5s
90s
1500 ms
1.5 s
total: 91.5s
1 menit 30 detik
true
▶ Coba sendiri

Kamu akan sering bertemu Duration: std::thread::sleep(Duration::from_millis(100)) menghentikan sebuah thread sejenak, dan batas waktu (timeout) di library jaringan juga memakai Duration.

SystemTime adalah waktu di jam komputer, yang kamu lihat di pojok layar. Biasanya kita memakainya untuk bertanya sudah berapa lama sejak Unix epoch, yaitu 1 Januari 1970 (UTC). Angka itu disebut Unix timestamp:

use std::time::{SystemTime, UNIX_EPOCH};
fn main() {
let now = SystemTime::now();
let since_1970 = now
.duration_since(UNIX_EPOCH)
.expect("jam diatur ke sebelum 1970");
println!("Detik sejak 1 Jan 1970: {}", since_1970.as_secs());
}
▶ Coba sendiri

Angkanya tergantung kapan kamu menjalankannya, jadi sekali lagi tidak ada output yang ditampilkan. Kenapa duration_since mengembalikan Result? Karena jam sistem bisa saja salah atau dimundurkan. Itu juga alasan kenapa kamu sebaiknya memakai Instant, bukan SystemTime, untuk mengukur kecepatan kode.

Kurang lebih hanya itu yang bisa dilakukan std::time. Ia sama sekali tidak tahu arti “Maret” atau “Jumat”. Untuk itu, kamu butuh chrono.

Tambahkan crate-nya ke proyekmu (lihat Cargo dan crate kalau ini masih baru buatmu):

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

Rust Playground juga sudah menyertakan chrono, jadi semua tombol di bawah bisa dipakai. chrono punya dua keluarga tipe:

  • Tipe naive (NaiveDate, NaiveTime, NaiveDateTime) adalah tanggal dan jam tanpa zona waktu. Seperti “14 Maret 2025” atau “09:30” yang ditulis di kalender. Sebagian besar kode sehari-hari memakai tipe ini.
  • DateTime<Tz> adalah satu titik waktu dengan zona waktu, seperti DateTime<Utc> atau DateTime<Local>.

NaiveDate::from_ymd_opt(tahun, bulan, tanggal) membuat sebuah tanggal. Hasilnya berupa Option, karena tidak semua kombinasi angka adalah tanggal yang benar-benar ada:

use chrono::{Datelike, NaiveDate, NaiveTime};
fn main() {
let date = NaiveDate::from_ymd_opt(2025, 3, 14).unwrap();
println!("{date}");
println!("tahun {}, bulan {}, tanggal {}", 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);
}

Output:

2025-03-14
tahun 2025, bulan 3, tanggal 14
2025-03-14 09:30:00
2025-03-14 17:45:10
None
▶ Coba sendiri

Tanggal 30 Februari tidak ada, jadi kamu mendapat None, bukan tanggal yang rusak. Untuk nilai tetap yang kamu ketik sendiri, .unwrap() tidak masalah. Untuk nilai dari pengguna, tangani kasus None-nya.

Baris use chrono::Datelike membawa method .year(), .month(), dan .day(). (Timelike melakukan hal yang sama untuk .hour() dan .minute().)

Utc::now() memberi waktu saat ini dalam UTC. Local::now() memberinya dalam zona waktu komputer itu sendiri:

use chrono::{Local, Utc};
fn main() {
let utc_now = Utc::now();
let local_now = Local::now();
println!("UTC: {}", utc_now);
println!("Lokal: {}", local_now);
println!("Hari ini: {}", local_now.format("%Y-%m-%d"));
let today = local_now.date_naive();
println!("Sebagai NaiveDate: {today}");
}
▶ Coba sendiri

Output-nya adalah tanggal hari ini, jadi berubah setiap hari. Server Playground memakai UTC, jadi di sana kedua baris pertama menunjukkan jam yang sama. .date_naive() membuang zona waktu dan jamnya, sehingga yang tersisa hanya NaiveDate biasa. Kebiasaan yang baik: ambil “sekarang” satu kali saja di bagian luar program, lalu kirim tanggalnya ke fungsi-fungsimu. Dengan begitu fungsimu tetap mudah dites, seperti yang akan kamu lihat di contoh tenggat waktu nanti.

.format() menerima sebuah pola. Setiap kode % diganti dengan bagian dari tanggal. %Y adalah tahun, %m nomor bulan, %d tanggal:

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("laporan-%Y%m%d.txt").to_string();
println!("{file_name}");
}

Output:

2025-03-14
14/03/2025
Friday, March 14, 2025
Fri 14 Mar
09:05
9:05 AM
laporan-20250314.txt
▶ Coba sendiri

Nama hari dan bulan (Friday, March) keluar dalam bahasa Inggris, karena itulah bawaan chrono. Kode yang paling sering kamu pakai: %Y tahun, %m bulan (01–12), %d tanggal (01–31), %H jam (00–23), %M menit, %S detik, %A nama hari, %B nama bulan. Tanda - setelah % menghapus nol di depan, jadi %-d mencetak 5, bukan 05. .format() mengembalikan sesuatu yang bisa langsung dicetak. Panggil .to_string() kalau kamu butuh String.

NaiveDate::parse_from_str melakukan kebalikannya: ia membaca teks dengan pola % yang sama. Hasilnya berupa Result, karena bisa saja teksnya tidak cocok dengan pola:

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", "14 Maret", "2025-03"] {
match NaiveDate::parse_from_str(text, "%Y-%m-%d") {
Ok(date) => println!("Berhasil: {date}"),
Err(e) => println!("Gagal mem-parse {text:?}: {e}"),
}
}
}

Output:

Ok(2025-03-14)
Ok(2025-03-14)
Ok(2025-03-14)
Ok(2025-03-14T09:30:00)
Gagal mem-parse "2025-13-01": input is out of range
Gagal mem-parse "14 Maret": input contains invalid characters
Gagal mem-parse "2025-03": premature end of input
▶ Coba sendiri

Untuk bentuk standar YYYY-MM-DD, kamu juga bisa langsung memanggil .parse(). Bulan ke-13 tidak ada, jadi contoh gagal pertama menghasilkan “out of range” (di luar jangkauan). Pesan error-nya dari chrono, dalam bahasa Inggris: singkat, tapi cukup jelas. “invalid characters” berarti ada karakter yang tidak cocok dengan pola, dan “premature end of input” berarti teksnya habis sebelum polanya selesai.

Menambahkan hari ke sebuah tanggal secara manual ternyata cukup sulit: panjang setiap bulan berbeda, dan ada tahun kabisat. chrono mengurus semuanya untukmu. Pakai checked_add_days dengan Days, checked_add_months dengan Months, atau tambahkan sebuah 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 hari kemudian: {plus_30}");
let minus_7 = start.checked_sub_days(Days::new(7)).unwrap();
println!("7 hari sebelumnya: {minus_7}");
let next_month = start.checked_add_months(Months::new(1)).unwrap();
println!("1 bulan kemudian: {next_month}");
let week_later = start + TimeDelta::weeks(1);
println!("1 minggu kemudian: {week_later}");
let dt = start.and_hms_opt(23, 30, 0).unwrap();
let later = dt + TimeDelta::hours(2);
println!("2 jam kemudian: {later}");
}

Output:

30 hari kemudian: 2025-03-02
7 hari sebelumnya: 2025-01-24
1 bulan kemudian: 2025-02-28
1 minggu kemudian: 2025-02-07
2 jam kemudian: 2025-02-01 01:30:00
▶ Coba sendiri

Perhatikan baris “1 bulan kemudian”: 31 Januari ditambah satu bulan menjadi 28 Februari, karena tanggal 31 Februari tidak ada. chrono memilih hari terakhir yang valid. Method checked_ mengembalikan Option, yang bernilai None hanya kalau hasilnya keluar dari rentang tahun chrono (yang sangat besar). Menambahkan 2 jam ke pukul 23:30 juga dengan benar berpindah ke hari berikutnya.

.weekday() (dari Datelike) memberi tahu hari apa sebuah tanggal. Mengurangkan satu tanggal dari tanggal lain menghasilkan TimeDelta, dan .num_days() mengubahnya menjadi angka:

use chrono::{Datelike, NaiveDate, Weekday};
fn main() {
let date = NaiveDate::from_ymd_opt(2025, 3, 15).unwrap();
let day = date.weekday();
println!("{date} adalah hari {day} ({})", date.format("%A"));
let weekend = matches!(day, Weekday::Sat | Weekday::Sun);
println!("akhir pekan? {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!("{} hari, atau {} minggu penuh", gap.num_days(), gap.num_weeks());
println!("dibalik: {} hari", (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!("menginap di hotel: {} jam", (check_out - check_in).num_hours());
}

Output:

2025-03-15 adalah hari Sat (Saturday)
akhir pekan? true
358 hari, atau 51 minggu penuh
dibalik: -358 hari
menginap di hotel: 69 jam
▶ Coba sendiri

Weekday adalah sebuah enum dengan varian Mon sampai Sun, jadi kamu bisa memakai match padanya. Perhatikan bahwa selisihnya bisa negatif kalau tanggal kedua lebih belakang. Ini berguna: sisa hari yang negatif berarti “sudah lewat tenggat”.

Mari kita gabungkan semuanya. Fungsi di bawah menerima “hari ini” sebagai parameter, alih-alih memanggil Local::now() di dalamnya. Dengan begitu output di bawah selalu sama, dan fungsinya mudah dites. Di program sungguhan, kamu tinggal mengirim 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("jatuh tempo hari ini"),
1 => String::from("jatuh tempo besok"),
d if d > 1 => format!("{d} hari lagi"),
-1 => String::from("terlambat 1 hari"),
d => format!("terlambat {} hari", -d),
}
}
fn main() {
let today = NaiveDate::from_ymd_opt(2025, 6, 10).unwrap();
let tasks = [
("Bayar sewa", "2025-06-10"),
("Kirim laporan", "2025-06-11"),
("Perpanjang paspor", "2025-07-01"),
("Kembalikan buku", "2025-06-09"),
("Tagihan dokter gigi", "2025-06-03"),
];
println!("Hari ini {}", 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));
}
}

Output:

Hari ini Tuesday, 10 June 2025
Bayar sewa 10 Jun jatuh tempo hari ini
Kirim laporan 11 Jun jatuh tempo besok
Perpanjang paspor 01 Jul 21 hari lagi
Kembalikan buku 09 Jun terlambat 1 hari
Tagihan dokter gigi 03 Jun terlambat 7 hari
▶ Coba sendiri

Coba ganti today dengan tanggal lain lalu jalankan lagi. {:<20} menambahkan spasi pada nama sampai 20 karakter, sehingga kolomnya rapi (lihat Output).

chrono bukan satu-satunya pilihan. Crate time adalah alternatif populer yang lebih kecil, dan crate jiff yang lebih baru sangat memperhatikan zona waktu dan daylight saving time. Halaman ini memakai chrono karena paling banyak dipakai dan tersedia di Playground.

Lupa menulis use chrono::Datelike. Tanpa baris itu, .year(), .month(), dan .weekday() tidak bisa dipakai. Pesan error-nya bilang method itu “private”, yang membingungkan, tapi baris help memberi tahu solusi sebenarnya: trait Datelike belum di-import.

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;
|

Tertukar antara %m dan %M. %m adalah bulan, %M adalah menit. Kalau ada tanggal dan jam, pilihan yang salah tidak menghasilkan error. Programmu diam-diam mencetak menitnya:

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")); // salah: menit
println!("{}", meeting.format("%Y-%m-%d")); // benar: bulan
}

Output:

2025-05-14
2025-03-14
▶ Coba sendiri

Mengukur kecepatan kode dengan SystemTime. Jam sistem bisa melompat (misalnya saat menyinkronkan diri dengan internet). Pakai Instant untuk mengukur, dan pakai SystemTime atau chrono hanya kalau kamu butuh tanggal yang sebenarnya.

Memanggil Local::now() jauh di dalam logika program. Akibatnya hasil berubah setiap hari dan test jadi sulit ditulis. Kirim tanggalnya sebagai parameter, seperti yang dilakukan days_left.

  • std::time::Instant mengukur waktu yang berlalu; Duration adalah rentang waktu; SystemTime adalah jam dinding (cocok untuk Unix timestamp).
  • Untuk kalender, tambahkan chrono. Pakai NaiveDate / NaiveDateTime untuk tanggal tanpa zona waktu, dan Utc::now() / Local::now() untuk waktu saat ini.
  • from_ymd_opt mengembalikan Option, dan parse_from_str mengembalikan Result, karena tidak semua input adalah tanggal yang benar-benar ada.
  • .format("%Y-%m-%d") mengubah tanggal menjadi teks dengan kode %. Ingat: %m itu bulan, %M itu menit.
  • Tambahkan waktu dengan checked_add_days, checked_add_months, atau + TimeDelta. Kurangkan dua tanggal untuk mendapat TimeDelta, lalu panggil .num_days().
  • Kirim “hari ini” ke dalam fungsimu sebagai parameter, jangan membaca jam di dalam fungsi.

Kuis singkat

1. Tipe mana yang sebaiknya kamu pakai untuk mengukur berapa lama sebuah kode berjalan?

2. Apa hasil dari NaiveDate::from_ymd_opt(2025, 2, 30)?

3. Di string format chrono, apa arti %M?

4. Kamu mengurangkan dua NaiveDate: deadline - today. Bagaimana cara mendapatkan jumlah harinya?

5. Berapa hasil 31 Januari 2025 ditambah Months::new(1) di chrono?

Skor: 0 / 5