Rustの日付と時刻(chrono)
プログラムはいつも時間と関わっています。「この処理にどれくらいかかった?」「請求書の支払い期限は何日?」「旅行まであと何日?」。Rustでは、こうした疑問を2つの種類に分けて考えます。
標準ライブラリの std::time モジュールはストップウォッチです。どれだけ時間が経ったかを測ります。カレンダーや月、曜日のことは知りません。カレンダーを扱うときは、ほとんどのRustプロジェクトが chrono クレートを使います。このページでは、まずストップウォッチから始めて、そのあとカレンダーに進みます。
Instantで経過時間を測る
Section titled “Instantで経過時間を測る”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(時間の長さ)
Section titled “Duration(時間の長さ)”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.5s90s1500 ms1.5 s合計: 91.5s1分30秒trueDuration はいろいろな場所で登場します。std::thread::sleep(Duration::from_millis(100)) はスレッドを一時停止しますし、ネットワークライブラリのタイムアウトも Duration で指定します。
SystemTimeで現在の時刻を知る
Section titled “SystemTimeで現在の時刻を知る”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 が必要です。
chronoを追加する
Section titled “chronoを追加する”プロジェクトにクレートを追加します(初めてなら Cargoとクレートを読んでください)。
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>のように、タイムゾーンつきのある瞬間を表します。
決まった値から日付を作る
Section titled “決まった値から日付を作る”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-142025年3月14日2025-03-14 09:30:002025-03-14 17:45:10None2月30日は存在しないので、壊れた日付の代わりに None が返ります。自分で書いた決まった値なら .unwrap() で問題ありません。ユーザーから受け取った値なら、None の場合も処理しましょう。
use chrono::Datelike の行で、.year()、.month()、.day() メソッドが使えるようになります(.hour() や .minute() には Timelike を使います)。
現在の日付と時刻
Section titled “現在の日付と時刻”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回だけ取得し、その日付を関数に渡しましょう。そうすると関数をテストしやすくなります。期限の例で実際に見てみましょう。
日付を書式化する
Section titled “日付を書式化する”.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-1414/03/2025Friday, March 14, 2025Fri 14 Mar09:059:05 AMreport-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日 になります。
文字列を日付にパースする
Section titled “文字列を日付にパースする”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-027日前: 2025-01-241か月後: 2025-02-281週間後: 2025-02-072時間後: 2025-02-01 01:30:00「1か月後」に注目してください。1月31日の1か月後は2月28日です。2月31日は存在しないので、chronoは有効な最後の日を選びます。checked_ で始まるメソッドは Option を返しますが、None になるのは、結果がchronoの扱える(とても広い)年の範囲を超えたときだけです。23時30分に2時間を足すと、きちんと次の日に進んでいます。
曜日と2つの日付の差
Section titled “曜日と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)です週末? true358日、つまりまる51週間逆向き: -358日ホテルの滞在: 69時間Weekday は Mon から Sun までのバリアントを持つ列挙型(enum)なので、match で分岐できます。2つ目の日付のほうがあとなら、差はマイナスになることに注意してください。これは便利です。残り日数がマイナスなら「期限切れ」という意味になるからです。
例:期限まであと何日?
Section titled “例:期限まであと何日?”ここまでの内容を組み合わせてみましょう。この関数は、中で 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 2025Pay rent 10 Jun due todaySend report 11 Jun due tomorrowRenew passport 01 Jul 21 days leftReturn library book 09 Jun 1 day overdueDentist bill 03 Jun 7 days overduetoday を別の日付に変えて、もう一度実行してみてください。{:<20} は名前を20文字分に埋めて、列がそろうようにします(出力を参照)。この例の文字列を英語のままにしているのは、そのためです。日本語の文字は画面上で2文字分の幅を取るうえに、UTF-8では1文字3バイトなので、{:<20} では列がそろわなくなってしまいます。
選択肢はchronoだけではありません。time クレートは小さくて人気のある代替ですし、新しい jiff クレートはタイムゾーンやサマータイムを特に丁寧に扱います。ここでchronoを使っているのは、もっとも広く使われていて、Playgroundにも入っているからです。
よくある間違い
Section titled “よくある間違い”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 scopehelp: 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-142025-03-14SystemTime でコードの時間を測る。システムの時計は(たとえばインターネットと時刻を同期したときに)飛ぶことがあります。時間を測るときは 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()で日数がわかります。 - 関数の中で時計を読む代わりに、「今日」を関数に渡しましょう。
理解度チェック
Section titled “理解度チェック”理解度チェック
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
さらに詳しく
Section titled “さらに詳しく”- std::time — 標準ライブラリのドキュメント(英語):
Instant、Duration、SystemTimeの使い方が、さらに多くの例とともに載っています。 - chrono のドキュメント — docs.rs(英語):chronoのすべての型とメソッドを解説した完全なガイドです。
- chrono の書式コード — docs.rs(英語):
formatとparse_from_strで使える%コードの完全な一覧です。 - Date and Time — Rust Cookbook(英語):日付と時刻のよくある作業のための、短いレシピ集です。
- jiff のドキュメント — docs.rs(英語):タイムゾーンのサポートが充実した新しい日付・時刻クレートです。比べてみたい人向けです。