OffsetDateTime#parse() で ISO 8601 に厳密でない書式の日時をいい感じに解析する

1 minute read

OffsetDateTime#parse()"2017-01-02T12:34:56.001+0900" のような文字列を解析できないことに腹を立てたのでこのメモを書き殴った。反省するつもりはない。

はじめに

Java 8 で導入された Date & Time API における「(タイムゾーンではなく) UTC からのオフセット時間を含む日時」を扱う OffsetDateTime クラスを使っていて、このクラスの日時文字列を解析するメソッド OffsetDateTime#parse(CharSequence)"2017-01-02T12:34:56.001+0900" のような ISO 8601 的にちょっと正確でない表記 1 の文字列を解析できない問題に遭遇して「うがー!」となった人は僕以外にもそれなりにいるかと思います。

ここで入力として与えられる日時文字列の書式をアプリケーション的に ISO 8601 に準拠した形に制限できるのであればいざ知らず、状況によってはそのような制限を設けることができず、書式的に正しくないかもしれない文字列を同メソッドにて解析せざるを得ないこともあるかと思います。

そのような状況において、何とかして OffsetDateTime#parse() を使ってオフセット付き日時の文字列を解析したい、という問題を解決する方法の一つをメモしておきます。

DateTimeFormatter.ofPattern() で独自のフォーマッタを用意する

実現方法の基本は こちらの Stackoverflow の回答 にあるとおりで、つまりは DateTimeFormatter.ofPattern(String) で解析用の書式を指定した独自のフォーマッタを用意しつつ、解析の際は parse(CharSequence, DateTimeFormatter) メソッドを呼び出しましょう、ということです。

具体的に DateTimeFormatter.ofPattern(String) メソッドで指定する解析用の書式はどうすべきかというと、こちら にあるパターン文字列を適宜利用して組み立てることになります。

例えば、先の Stackoverflow の回答にある "uuuu-MM-dd'T'HH:mm:ss[xxx][xx][X]" では、オフセットの書式としてそれぞれ +HHMM+HH:MM+HHmm を指定していることに相当します。この書式の意味は こちら こちらです。なお X のパターン文字列によって、実際には +HHmm のオフセット書式に加えて UTC のタイムゾーンを表す Z も扱えるようになります。

このパターン文字列によって大体のオフセット付き日時文字列は問題なく解析できるようになるのですが、OffsetDateTime#parse(CharSequence) が実際に利用しているフォーマッタである DateTimeFormatter.ISO_OFFSET_DATE_TIME では、1 秒未満の時間であるミリ秒、マイクロ秒、ナノ秒単位も扱える一方で、先のパターン文字列だと 1 秒未満の時間が扱えなくなってしまっています。

これを解消するには、パターン文字列として

uuuu-MM-dd'T'HH:mm:ss[.SSSSSSSSS][.SSSSSS][.SSS][xxx][xx][X]

を利用すれば OK です。

実際に使ってみる

import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;

public class DateTimeFormatterForOffsetDateTimeDemo {
    public static void main(String[] args) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss[.SSSSSSSSS][.SSSSSS][.SSS][xxx][xx][X]");

        String[] patterns = {
                // UTC
                "2017-01-02T12:34:56Z",
                "2017-01-02T12:34:56.123Z",
                "2017-01-02T12:34:56.123456789Z",
                // 時のみ
                "2017-01-02T12:34:56+09",
                "2017-01-02T12:34:56.123+09",
                "2017-01-02T12:34:56.123456789+09",
                // 拡張表現
                "2017-01-02T12:34:56+09:00",
                "2017-01-02T12:34:56.123+09:00",
                "2017-01-02T12:34:56.123456789+09:00",
                // 基本表現 (本来は NG)
                "2017-01-02T12:34:56+0900",
                "2017-01-02T12:34:56.123+0900",
                "2017-01-02T12:34:56.123456789+0900",
        };

        for (int i = 0; i < patterns.length; i++) {
            System.out.printf("[%2d] in: %s%n    out: %s%n",
                    i + 1,
                    patterns[i],
                    OffsetDateTime.parse(patterns[i], formatter));
        }
    }
}

上記のプログラムの出力結果はこちら。

[ 1] in: 2017-01-02T12:34:56Z
    out: 2017-01-02T12:34:56Z
[ 2] in: 2017-01-02T12:34:56.123Z
    out: 2017-01-02T12:34:56.123Z
[ 3] in: 2017-01-02T12:34:56.123456789Z
    out: 2017-01-02T12:34:56.123456789Z
[ 4] in: 2017-01-02T12:34:56+09
    out: 2017-01-02T12:34:56+09:00
[ 5] in: 2017-01-02T12:34:56.123+09
    out: 2017-01-02T12:34:56.123+09:00
[ 6] in: 2017-01-02T12:34:56.123456789+09
    out: 2017-01-02T12:34:56.123456789+09:00
[ 7] in: 2017-01-02T12:34:56+09:00
    out: 2017-01-02T12:34:56+09:00
[ 8] in: 2017-01-02T12:34:56.123+09:00
    out: 2017-01-02T12:34:56.123+09:00
[ 9] in: 2017-01-02T12:34:56.123456789+09:00
    out: 2017-01-02T12:34:56.123456789+09:00
[10] in: 2017-01-02T12:34:56+0900
    out: 2017-01-02T12:34:56+09:00
[11] in: 2017-01-02T12:34:56.123+0900
    out: 2017-01-02T12:34:56.123+09:00
[12] in: 2017-01-02T12:34:56.123456789+0900
    out: 2017-01-02T12:34:56.123456789+09:00

意図どおりに、厳密に ISO 8601 に従っていないオフセット表記もちゃんと解析できていますね!


  1. ISO 8601 的にはオフセットの表現にて時分を区切るコロンが必要になるので、 +0900 ではなく +09:00 と表記しなければならない

Tags:

Categories:

Updated: