Rustの日付時刻処理(std::time, time, chrono)

標準ライブラリ

標準ライブラリには時刻を扱うための基礎となる型のみが定義されている。暦やタイムゾーンなどを扱うときは後述の chrono を使うのがよい。

Duration

時間はOSとは無関係なのでlibcoreに定義されている。 src/libcore/time.rs

pub struct Duration {
    secs: u64,
    nanos: u32, // Always 0 <= nanos < NANOS_PER_SEC
}

したがってこれは0秒以上264秒未満の範囲内の時間をナノ秒単位で正確に表現できる。

InstantとSystemTime

標準ライブラリには時刻を表す型が InstantSystemTime の2つある(いわゆるmonotonic clockとrealtime clock)。この2つは時刻の補正があったときの挙動が異なる。たとえば、「5秒経過 + NTPや手動設定により時刻が3秒分手前に補正された」とき、

  • Instant は約5秒ぶん進む。
  • SystemTime は2秒ぶん進む。

つまり、 SystemTimeできるだけ正しい絶対時刻を表しているが、経過時間の計算が狂うことがある(最悪、逆転進行する可能性もある)

一方、 Instant経過時間をほぼ正確に表現するが、補正しなかった分が蓄積しているのでそもそも正しい絶対時刻を表していない

Instant から意味のない値を取り出してしまわないように、 Instant から時刻表現を取り出す手段はそもそも用意されていない。2つの Instant を比較することではじめて意味が出てくる。

この InstantSystemTimeOSごとに定義される型のラッパーになっている

例: 経過時間を調べる

playground

let instant1 = Instant::now();
std::thread::sleep(Duration::from_millis(200));
let instant2 = Instant::now();
eprintln!("elapsed: {:?}", instant2 - instant1);

ベンチマーク目的ならtestcriterionなど専用のライブラリを使うのがいいだろう。

例: unix timeを調べる

playground

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クレート

timeC言語風のシンプルな日付時刻処理を実装している。今となってはchronoがあるので積極的に利用することはあまりなさそう。

  • Durationがここでも定義されているが、こちらは負の値を取ることができる。標準ライブラリの Duration との変換も可能
    • 歴史的にはこちらが先に定義され、 std::time::Duration があとから標準化されたようだ
  • TimespecTmunixでお馴染みのやつで、前者がepochからの経過時間表現で後者がカレンダー表現。この Timespecstd::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: タイムゾーン不定の日付時刻。
    • さらにこれを日付部分と時刻部分に分けた NaiveDateNaiveTime がある

「日付時刻+タイムゾーン(Asia/Tokyoなど)」を処理するにはtzdataが必要なので chrono-tz (後述) として分離されている。

また、上に挙げたものの他に Date<Utc> のような型もあるが、これは Utc.ymd(2019, 4, 21).and_hms(11, 22, 00) とやったときに出てくる中間状態的なもののようだ。日付とオフセットというのは中途半端なので、日付を外部とやりとりするときはどちらかというと NaiveDate を使うほうがよい。

例: 特定オフセットでの日付を表示

前述の通り、日付にはタイムゾーン情報のない NaiveDate を使うのがよいとされている。

playground

let datetime = Utc::now().with_timezone(&FixedOffset::east(9 * 3600));
eprintln!("{}", datetime);
// .naive_local() を取るとタイムゾーン情報を残せるが、多分あまり使わない
eprintln!("{}", datetime.naive_local().date());

例: 時刻指定+UTCへの変換+比較

playground

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日は存在せず、これより前の日付はユリウス暦とずれていくことになる。

playground

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年とおいている。これを天文暦などということもある。

playground

let date1 = NaiveDate::from_ymd(1, 1, 1);
let date2 = date1.pred();
assert_eq!(date2, NaiveDate::from_ymd(0, 12, 31));

例: 閏秒を表現する

chronoの閏秒はやや特殊で、秒を60にするのではなくミリ秒を1000にすることで表現する。前述のように完全なサポートがあるわけではないので注意。

playground

// 秒を60にするのではなく、ミリ秒を1000にすることで表現する
let datetime = FixedOffset::east(9 * 3600).ymd(2017, 1, 1).and_hms_milli(8, 59, 59, 1000);
dbg!(datetime);

例: 閏秒の計算を調べる

閏秒の大小関係は正しく判定されるが、時間計算では時刻の逆転が起きるようだ。

playground

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/TokyoEurope/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年以前の暦の扱いには注意が必要である。