コンテンツにスキップ

Rustの日付と時刻(chrono)

プログラムはいつも時間と関わっています。「この処理にどれくらいかかった?」「請求書の支払い期限は何日?」「旅行まであと何日?」。Rustでは、こうした疑問を2つの種類に分けて考えます。

標準ライブラリの std::time モジュールはストップウォッチです。どれだけ時間が経ったかを測ります。カレンダーや月、曜日のことは知りません。カレンダーを扱うときは、ほとんどのRustプロジェクトが chrono クレートを使います。このページでは、まずストップウォッチから始めて、そのあとカレンダーに進みます。

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 のような表示になります。ボタンを2回押して、結果を比べてみてください。

Instant は時間を測るためだけのものです。日付として表示することはできず、値は前にしか進みません。コードの時間を測るにはそれがちょうどよいのです。誰かがシステムの時計を変更しても、影響を受けないからです。

Duration は「90秒」のような時間の長さです。秒やミリ秒から作り、足し合わせたり、比べたりできます。

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.5s
90s
1500 ms
1.5 s
合計: 91.5s
1分30秒
true
▶ 実際に試してみよう

Duration はいろいろな場所で登場します。std::thread::sleep(Duration::from_millis(100)) はスレッドを一時停止しますし、ネットワークライブラリのタイムアウトも Duration で指定します。

SystemTime はコンピュータの時計の時刻、つまり画面の隅に表示されている時刻です。よくある使い方は、Unixエポック(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 を返すのでしょうか? システムの時計は間違っていることがあり、時刻が巻き戻されることもあるからです。コードの時間を測るときに SystemTime ではなく Instant を使うべき理由も、ここにあります。

std::time でできるのは、だいたいここまでです。「3月」や「金曜日」が何なのかはまったく知りません。そのためには chrono が必要です。

プロジェクトにクレートを追加します(初めてなら Cargoとクレートを読んでください)。

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

Rust Playgroundにもchronoが入っているので、このページのボタンはすべて動きます。chronoの型は大きく2つのグループに分かれます。

  • Naive(ナイーブ)な型(NaiveDate、NaiveTime、NaiveDateTime)は、タイムゾーンのない日付と時刻です。カレンダーに書いた「2025年3月14日」や「9時30分」のようなものです。ふだんのコードではたいていこちらを使います。
  • DateTime<Tz> は、DateTime<Utc> や DateTime<Local> のように、タイムゾーンつきのある瞬間を表します。

NaiveDate::from_ymd_opt(年, 月, 日) で日付を作ります。戻り値は 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-14
2025年3月14日
2025-03-14 09:30:00
2025-03-14 17:45:10
None
▶ 実際に試してみよう

2月30日は存在しないので、壊れた日付の代わりに None が返ります。自分で書いた決まった値なら .unwrap() で問題ありません。ユーザーから受け取った値なら、None の場合も処理しましょう。

use chrono::Datelike の行で、.year()、.month()、.day() メソッドが使えるようになります(.hour() や .minute() には Timelike を使います)。

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を使っているので、そこでは2つの行に同じ時刻が表示されます。.date_naive() はタイムゾーンと時刻を取り除き、ただの NaiveDate にします。良い習慣として、「現在」はプログラムの入り口で1回だけ取得し、その日付を関数に渡しましょう。そうすると関数をテストしやすくなります。期限の例で実際に見てみましょう。

.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-14
14/03/2025
Friday, March 14, 2025
Fri 14 Mar
09:05
9:05 AM
report-20250314.txt
▶ 実際に試してみよう

よく使うコードは次のとおりです。%Y 年、%m 月(01〜12)、%d 日(01〜31)、%H 時(00〜23)、%M 分、%S 秒、%A 曜日名、%B 月名。% のあとに - を付けると先頭の0が消えるので、%-d は 05 ではなく 5 と表示します。.format() が返すものはそのまま表示できます。String が必要なときは .to_string() を呼びましょう。

曜日名や月名(%A、%B、%a、%b)は英語で表示されます。日本語の書き方にしたいときは、パターンに日本語をそのまま書けます。date.format("%Y年%-m月%-d日") なら 2025年3月14日 になります。

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ならその計算をまかせられます。Days と一緒に checked_add_days を使うか、Months と一緒に checked_add_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-02
7日前: 2025-01-24
1か月後: 2025-02-28
1週間後: 2025-02-07
2時間後: 2025-02-01 01:30:00
▶ 実際に試してみよう

「1か月後」に注目してください。1月31日の1か月後は2月28日です。2月31日は存在しないので、chronoは有効な最後の日を選びます。checked_ で始まるメソッドは Option を返しますが、None になるのは、結果がchronoの扱える(とても広い)年の範囲を超えたときだけです。23時30分に2時間を足すと、きちんと次の日に進んでいます。

Datelike の .weekday() で曜日がわかります。日付から別の日付を引くと 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)です
週末? true
358日、つまりまる51週間
逆向き: -358日
ホテルの滞在: 69時間
▶ 実際に試してみよう

Weekday は Mon から Sun までのバリアントを持つ列挙型(enum)なので、match で分岐できます。2つ目の日付のほうがあとなら、差はマイナスになることに注意してください。これは便利です。残り日数がマイナスなら「期限切れ」という意味になるからです。

ここまでの内容を組み合わせてみましょう。この関数は、中で 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 2025
Pay rent 10 Jun due today
Send report 11 Jun due tomorrow
Renew passport 01 Jul 21 days left
Return library book 09 Jun 1 day overdue
Dentist bill 03 Jun 7 days overdue
▶ 実際に試してみよう

today を別の日付に変えて、もう一度実行してみてください。{:<20} は名前を20文字分に埋めて、列がそろうようにします(出力を参照)。この例の文字列を英語のままにしているのは、そのためです。日本語の文字は画面上で2文字分の幅を取るうえに、UTF-8では1文字3バイトなので、{:<20} では列がそろわなくなってしまいます。

選択肢はchronoだけではありません。time クレートは小さくて人気のある代替ですし、新しい jiff クレートはタイムゾーンやサマータイムを特に丁寧に扱います。ここで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 scope
help: trait `Datelike` which provides `year` is implemented but not in scope; perhaps you want to import it
|
1 + use chrono::Datelike;
|

「トレイトのメソッドは、そのトレイトがスコープにあるときだけ使えます。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-14
2025-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 を使います。2つの日付を引くと TimeDelta になり、.num_days() で日数がわかります。
  • 関数の中で時計を読む代わりに、「今日」を関数に渡しましょう。

理解度チェック

1. コードの一部にかかる時間を測るには、どの型を使うべきですか?

2. NaiveDate::from_ymd_opt(2025, 2, 30) は何を返しますか?

3. chronoの書式文字列で、%M は何を意味しますか?

4. 2つの NaiveDate を deadline - today のように引きました。日数を得るにはどうしますか?

5. chronoで2025年1月31日に Months::new(1) を足すと、何になりますか?

スコア: 0 / 5