Rustの日付時刻処理(std::time, time, chrono)
標準ライブラリ
標準ライブラリには時刻を扱うための基礎となる型のみが定義されている。暦やタイムゾーンなどを扱うときは後述の chrono を使うのがよい。
std::time::Duration... 時間。std::time::Instant... 体内時計の時刻。std::time::SystemTime... 時計の時刻。
Duration
時間はOSとは無関係なのでlibcoreに定義されている。 src/libcore/time.rs
pub struct Duration { secs: u64, nanos: u32, // Always 0 <= nanos < NANOS_PER_SEC }
したがってこれは0秒以上264秒未満の範囲内の時間をナノ秒単位で正確に表現できる。
InstantとSystemTime
標準ライブラリには時刻を表す型が Instant と SystemTime の2つある(いわゆるmonotonic clockとrealtime clock)。この2つは時刻の補正があったときの挙動が異なる。たとえば、「5秒経過 + NTPや手動設定により時刻が3秒分手前に補正された」とき、
Instantは約5秒ぶん進む。SystemTimeは2秒ぶん進む。
つまり、 SystemTime はできるだけ正しい絶対時刻を表しているが、経過時間の計算が狂うことがある(最悪、逆転進行する可能性もある)。
一方、 Instant は経過時間をほぼ正確に表現するが、補正しなかった分が蓄積しているのでそもそも正しい絶対時刻を表していない。
Instant から意味のない値を取り出してしまわないように、 Instant から時刻表現を取り出す手段はそもそも用意されていない。2つの Instant を比較することではじめて意味が出てくる。
この Instant と SystemTime はOSごとに定義される型のラッパーになっている。
- macos/iOS以外のunix系OSでは、
Instant,SystemTimeともにstruct timespecのラッパーである。その取得はclock_gettime(2)のCLOCK_MONOTONICとCLOCK_REALTIMEモードを単に呼ぶだけになっている。 - macos/iOSでは
Instantがu64のラッパーであるという違いがある。また、Instantはmach_absolute_time、SystemTimeはgettimeofdayで取得している。- mach_absolute_timeに関する記述
- gettimeofdayなのでほぼ間違いなくunix timeを返すはず
- Windowsでは
InstantはQueryPerformanceCounterの返すDurationで、SystemTimeはGetSystemTimeAsFileTimeの返すFILETIMEである。- FILETIMEは時刻として使う場合は1601-01-01 00:00:00 UTCからの経過ナノ秒を100で割り、64bit整数として表したものを2つの32bit符号なし整数に分けてから符号つき整数にキャストして入れたものとして解釈されるので、Unix epochが謎のマジックナンバーになるし、色々面倒そう。
- wasmの現状の実装では
Instant,SystemTimeともにunix epochからの経過時間としてのDurationを使っており、shimの実装を見る感じでは単にDate.now()を呼んでいるだけでmonotonic clockとrealtime clockの区別はなさそう。 - Windowsやwasmのmonotonic timerは信頼できないので、逆転しないような対策が入っている。
- cloudabi, redox, sgxでの定義は割愛
例: 経過時間を調べる
let instant1 = Instant::now(); std::thread::sleep(Duration::from_millis(200)); let instant2 = Instant::now(); eprintln!("elapsed: {:?}", instant2 - instant1);
ベンチマーク目的ならtestやcriterionなど専用のライブラリを使うのがいいだろう。
例: unix timeを調べる
let now = SystemTime::now(); let unixtime = now.duration_since(UNIX_EPOCH).expect("back to the future"); eprintln!("unixtime = {}.{:09}", unixtime.as_secs(), unixtime.subsec_nanos())
timeクレート
time はC言語風のシンプルな日付時刻処理を実装している。今となってはchronoがあるので積極的に利用することはあまりなさそう。
Durationがここでも定義されているが、こちらは負の値を取ることができる。標準ライブラリのDurationとの変換も可能- 歴史的にはこちらが先に定義され、
std::time::Durationがあとから標準化されたようだ
- 歴史的にはこちらが先に定義され、
TimespecとTmはunixでお馴染みのやつで、前者がepochからの経過時間表現で後者がカレンダー表現。このTimespecはstd::time::SystemTimeで事足りそう。
chronoクレート
chrono は日付時刻処理に関する事実上の標準ライブラリである。日付は遡及グレゴリオ暦(1582年より前もグレゴリオ暦の規則に従って日付を割り振る)で約-200000年から約200000年まで扱える。時刻はナノ秒までで閏秒も表現可能だが、閏秒の扱いは完全ではない (Utc::now は閏秒を返さないし、時刻の加減算では閏秒は無視される)
chrono の日付時刻型は以下の4つである。
DateTime<Utc>: UTCでの特定の日付時刻 (2019-04-20T12:59:20Z)。ある瞬間を一意的に特定できるので最も基本的な日付時刻型といえる。DateTime<FixedOffset>: 日付時刻とオフセット (2019-04-20T21:59:20+0900)。DateTime<Local>: 現在のタイムゾーン上での日付時刻。データ的にはDateTime<FixedOffset>と同じ。NaiveDateTime: タイムゾーン不定の日付時刻。- さらにこれを日付部分と時刻部分に分けた
NaiveDateとNaiveTimeがある
- さらにこれを日付部分と時刻部分に分けた
「日付時刻+タイムゾーン(Asia/Tokyoなど)」を処理するにはtzdataが必要なので chrono-tz (後述) として分離されている。
また、上に挙げたものの他に Date<Utc> のような型もあるが、これは Utc.ymd(2019, 4, 21).and_hms(11, 22, 00) とやったときに出てくる中間状態的なもののようだ。日付とオフセットというのは中途半端なので、日付を外部とやりとりするときはどちらかというと NaiveDate を使うほうがよい。
例: 特定オフセットでの日付を表示
前述の通り、日付にはタイムゾーン情報のない NaiveDate を使うのがよいとされている。
let datetime = Utc::now().with_timezone(&FixedOffset::east(9 * 3600)); eprintln!("{}", datetime); // .naive_local() を取るとタイムゾーン情報を残せるが、多分あまり使わない eprintln!("{}", datetime.naive_local().date());
例: 時刻指定+UTCへの変換+比較
let threshold = FixedOffset::east(9 * 3600).ymd(2019, 5, 1).and_hms(0, 0, 0); if Utc::now() < threshold.with_timezone(&Utc) { eprintln!("Heisei"); } else { eprintln!("Reiwa"); }
例: 遡及グレゴリオ暦であることを確かめる
紀元前8年から1581年まではユリウス暦(4年に一回の閏年のみで、100年/400年ルールはなし)が用いられていたが、chronoではこれを無視して遡及グレゴリオ暦を用いて計算する(現在の日付を基準に、1581年以前にもグレゴリオ暦の規則を適用して日付を割り振っていく)。したがってchronoには1500年2月29日は存在せず、これより前の日付はユリウス暦とずれていくことになる。
let date1 = NaiveDate::from_ymd(1500, 3, 1); let date2 = date1.pred(); assert_eq!(date2, NaiveDate::from_ymd(1500, 2, 28));
例: 天文暦である(0年が存在する)ことを確かめる
西暦1年の直前(紀元前1年)を西暦-1年とする方法もあるが、chronoでは紀元前1年を0年とおいている。これを天文暦などということもある。
let date1 = NaiveDate::from_ymd(1, 1, 1); let date2 = date1.pred(); assert_eq!(date2, NaiveDate::from_ymd(0, 12, 31));
例: 閏秒を表現する
chronoの閏秒はやや特殊で、秒を60にするのではなくミリ秒を1000にすることで表現する。前述のように完全なサポートがあるわけではないので注意。
// 秒を60にするのではなく、ミリ秒を1000にすることで表現する let datetime = FixedOffset::east(9 * 3600).ymd(2017, 1, 1).and_hms_milli(8, 59, 59, 1000); dbg!(datetime);
例: 閏秒の計算を調べる
閏秒の大小関係は正しく判定されるが、時間計算では時刻の逆転が起きるようだ。
let datetime1 = Utc.ymd(2016, 12, 31).and_hms_milli(23, 59, 59, 1500); let datetime2 = Utc.ymd(2017, 1, 1).and_hms_milli(0, 0, 0, 100); assert!(datetime2 - datetime1 < chrono::Duration::seconds(0)); assert!(datetime2 > datetime1);
chrono-tz クレート
Asia/Tokyo や Europe/London などのタイムゾーンを扱うには chrono-tz クレートを使うことができる。UTC→ローカル時刻は一意に決まるのに対し、ローカル時刻→UTCは一意に決まらないことに注意が必要である。
例: タイムゾーンをパースする
playground ※chrono-tzがないのでplayground上では実行できない
let tz = "Asia/Tokyo".parse::<chrono_tz::Tz>().unwrap(); assert_eq!(tz, chrono_tz::Asia::Tokyo);
例: タイムゾーン指定で時刻を表示する
playground ※chrono-tzがないのでplayground上では実行できない
let tz = chrono_tz::Europe::London; let datetime = Utc.ymd(2018, 10, 28).and_hms(0, 30, 0).with_timezone(&tz); dbg!(datetime); // 2018-10-28T01:30:00BST let datetime = Utc.ymd(2018, 10, 28).and_hms(1, 30, 0).with_timezone(&tz); dbg!(datetime); // 2018-10-28T01:30:00GMT
例: タイムゾーン指定で時刻を組み立てる (一意に決まる場合)
Asia/Tokyo はかなり昔に遡らないと変動がないので、こういう風に処理してもあまり問題にならない。しかし後述のように夏時間などでオフセットに変更のあるタイムゾーンでは問題が起きる。
playground ※chrono-tzがないのでplayground上では実行できない
let tz = chrono_tz::Asia::Tokyo; let datetime = tz.ymd(2019, 5, 1).and_hms(0, 0, 0); dbg!(datetime); // 2019-05-01T00:00:00JST dbg!(datetime.with_timezone(&Utc)); // 2019-04-30T15:00:00Z
例: タイムゾーン指定で時刻を組み立てる (一意に決まらない場合)
曖昧性を残したまま時刻の候補を取得するメソッドがあるが、バグっているようだ。
playground ※chrono-tzがないのでplayground上では実行できない
use chrono::prelude::*; #[test] #[should_panic(expected = "invalid time")] fn test_double_time_fail() { let tz = chrono_tz::Europe::London; // この時刻はGMTとBSTの両方に存在する let datetime = tz.ymd(2018, 10, 28).and_hms(1, 30, 0); dbg!(datetime); } #[test] #[should_panic(expected = "invalid time")] fn test_skip_time_fail() { let tz = chrono_tz::Europe::London; // この時刻はGMTとBSTのどちらでも存在できない let datetime = tz.ymd(2019, 3, 31).and_hms(1, 30, 0); dbg!(datetime); } #[test] fn test_double_time() { let tz = chrono_tz::Europe::London; // この時刻はGMTとBSTの両方に存在する // tz.ymd(2018, 10, 28).and_hms_opt(1, 30, 0) と書きたいけどこれだとNoneが返ってしまう let datetime = { let naive = NaiveDate::from_ymd(2018, 10, 28).and_hms(1, 30, 0); tz.offset_from_local_datetime(&naive) .map(|off| DateTime::<chrono_tz::Tz>::from_utc(naive - off.fix(), off)) }; assert_eq!( datetime, chrono::LocalResult::Ambiguous( Utc.ymd(2018, 10, 28).and_hms(0, 30, 0).with_timezone(&tz), Utc.ymd(2018, 10, 28).and_hms(1, 30, 0).with_timezone(&tz), ), ); } #[test] fn test_skip_time() { let tz = chrono_tz::Europe::London; // この時刻はGMTとBSTのどちらでも存在できない // tz.ymd(2019, 3, 31).and_hms_opt(1, 30, 0) と書きたいけどこれだと重複時もNoneが返ってしまう let datetime = { let naive = NaiveDate::from_ymd(2019, 3, 31).and_hms(1, 30, 0); tz.offset_from_local_datetime(&naive) .map(|off| DateTime::<chrono_tz::Tz>::from_utc(naive - off.fix(), off)) }; assert_eq!(datetime, chrono::LocalResult::None); }
まとめ
- 抽象的な時間・時刻を扱うには
std::timeを使い、暦に基づく日付時刻処理をするにはchronoクレートを使うのがよい。 - 歴史的には
timeクレートが使われていたので、これを見かけることもある。 std::timeでは時間計測のためのInstantと絶対時刻の取得のためのSystemTimeが区別されている。chronoではタイムゾーン情報の有無やに応じて日付時刻型が何種類かに分かれている。基本的にはDateTime<Utc>を使い、タイムゾーンを付与するときはDateTime<FixedOffset>を使う。タイムゾーン不定の日付時刻にはNaiveDateTimeを使う。chronoを使う場合も、閏秒や1581年以前の暦の扱いには注意が必要である。