JavaでISO 8601で協定世界時(UTC)な日付文字列をTimestamp型に変換してみる

f:id:ts0818:20190507215445j:plain

ここは日本だぞ!

「ファッキンジャップくらい分かるよ、バカ野郎。『BROTHER(監督:北野武)』」ということで、どうもボクです。

はい、のっけから、取り乱しましてすみませんでした。

全然関係ないですが、映画「ウォッチメン」のパッケージの画像って、終末時計を表してたんだそうですね...気づかなかった~、確かに、言われてみると、ニコニコバッチに付いた血が、終末時計の時刻を示す秒針っぽく見えますね。

そんでは、終末時計の時間ではないですが、今回は、Javaで日時に関してのお話です。レッツトライ~。

 

そも、ISO 8601 って何ぞ?

 Wikipediaさ~ん!

ISO 8601は、日付時刻の表記に関するISOの国際規格。

規格の最新版は2019年2月に発行された ISO 8601-1:2019 (Date and time -- Representations for information interchange -- Part 1: Basic rules) および ISO 8601-2:2019 (Date and time -- Representations for information interchange -- Part 2: Extensions) の2分冊。何度も改訂されていて、過去の規格は、ISO 8601:2004、ISO 8601:2000、ISO 8601:1988 があり、いずれも正式な題は Data elements and interchange formats – Information interchange – Representation of dates and times であった。更にその前身は ISO 2014:1976 などがあった。

ISO 8601 - Wikipedia

え~っと、何回も改定されてるって...

基本形式(基本表記・標準表記)と拡張形式(拡張表記)の2種類の表記方法があり、いずれも日付と時刻をT記号で区切る。

ISO 8601形式の時刻表記
基本形式 20190310T202921+0900
拡張形式 2019-03-10T20:29:21+09:00

ISO 8601 - Wikipedia

基本形式、拡張形式の2択であると。

日付と時刻の組合せにおいて基本形式と拡張形式との混在は許されず、どちらかに統一されていなければならない。

ISO 8601 - Wikipedia

ISO 8601に則ったシステムにおいては、どちらか一方が利用されるって考えておけば良いですかね。

 

そんでは、さっそく実装してみる

先人の皆様の知恵をお借りし、今、必殺のサン・アタック!

というわけで、

www.mkyong.com

stackoverflow.com

stackoverflow.com

⇧  上記サイト様を参考にさせていただきました。

結論から言いますと、残念がら、Java標準のparse処理は、ISO 8601 を完全にカバーできていないようです。

なので、Wikipediaの例のような日付文字列が入力値として入ってきた場合、ことごとく落ちます...残念過ぎる、というか、Javaもう少し頑張ってくれ、ラムダ式が例外処理との相性最悪なのといい、不完全な部分が多すぎる...。

もしかしたら、何かうまい方法があるのかもしれませんが、見つけられず...どなたか良さ気な方法を知ってましたら教えてちょんまげ。

さて、そんでは、絶望を感じたところで、Eclipseを起動し、適当なJavaプロジェクトを作成し、クラスも作成。

f:id:ts0818:20190403222012p:plain

ソースはこんな感じで。 

import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;

public class TestISOtoTimestamp {

	public static void main(String[] args) {
		// TODO 自動生成されたメソッド・スタブ
		//String dateString = "20190310T202921+0900"; // parseで落ちる
		//String dateString = "20190310T202921Z0900"; // parseで落ちる
		//String dateString = "20190310T202921Z";     // parseで落ちる
		//String dateString = "20190310T202921";      // parseで落ちる

		//String dateString = "2019-03-10T20:29:21+0900";  // parseで落ちる
		//String dateString = "2019-03-10T20:29:21Z09:00"; // parseで落ちる
		String dateString = "2019-03-10T20:29:21Z";

		System.out.println(convertTime(dateString));

	}

    /**
    * ISO 8601 日付文字列をTimestamp型に変換
    */
	private static Timestamp convertTime(String dateString) {
	    //DateTimeFormatter isoFormatter = DateTimeFormatter.ISO_INSTANT.withZone(ZoneOffset.UTC);
	    Instant dateInstant = Instant.parse(dateString);
	    //Instant dateInstant = Instant.from(isoFormatter.parse(dateString));
	    //LocalDateTime date = LocalDateTime.parse(dateString);
	    LocalDateTime date = LocalDateTime.ofInstant(dateInstant, ZoneId.of(ZoneOffset.UTC.getId()));

	    return Timestamp.valueOf(date);
	}
}

で、「Java アプリケーション」で実施。

f:id:ts0818:20190403221845p:plain

f:id:ts0818:20190403223037p:plain

 

はい、というわけで、なんとも残念過ぎる結末を迎えてしまったと...というか、基本形式のフォーマット1つもパースできとらんやん...

いまのところ、今回の上手くパースができる例みたいな値が来るシステムにしか遭遇してないけど、これ、パースできないような値が来た場合、地獄ですな...

そんな今の気持ちを、映画『ブラックブック(監督:ポール・バーホーベン)』の主人公のセリフで表してみましょう。

「悲しみに終わりはないの! 」


ブラックブック [DVD]

 

今回はこのへんで。

 

2019年4月4日(木)追記:↓  ここから

悲報...「2019-03-10T20:29:21+0900」みたいなISO 8601の日付文字列(拡張形式フォーマットバージョン)で来ることが発覚。

というわけで、暫定対応。

import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;

public class TestISOtoTimestamp {

	public static void main(String[] args) {
		// TODO 自動生成されたメソッド・スタブ
		//String dateString = "20190310T202921+0900"; // parseで落ちる
		//String dateString = "20190310T202921Z0900"; // parseで落ちる
		//String dateString = "20190310T202921Z";     // parseで落ちる
		//String dateString = "20190310T202921";      // parseで落ちる

		String dateString = "2019-03-10T20:29:21+0900";  // parseで落ちる
		//String dateString = "2019-03-10T20:29:21Z09:00"; // parseで落ちる
		//String dateString = "2019-03-10T20:29:21Z";

		System.out.println(convertTime(dateString));

	}

	private static Timestamp convertTime(String dateString) {
		// parseで落ちないよう、日付文字列を編集
		int index = 0;
		if(( index = dateString.indexOf("+")) > 0) {
			dateString = dateString.substring(0, index) + "Z";
		}
	    //DateTimeFormatter isoFormatter = DateTimeFormatter.ISO_INSTANT.withZone(ZoneOffset.UTC);
	    Instant dateInstant = Instant.parse(dateString);
	    //Instant dateInstant = Instant.from(isoFormatter.parse(dateString));
	    //LocalDateTime date = LocalDateTime.parse(dateString);
	    LocalDateTime date = LocalDateTime.ofInstant(dateInstant, ZoneId.of(ZoneOffset.UTC.getId()));

	    return Timestamp.valueOf(date);

	}
}

そして、実行。

f:id:ts0818:20190404220351p:plain

+0900 の部分が反映できてないから、そのへんは時間のある時に、と思ったけど、

2004-04-01T12:00:00+09:00/2007-08-31T15:00:00+09:00

日本時間 (JST) で、2004年4月1日12時0分0秒から2007年8月31日15時0分0秒まで

ISO 8601 - Wikipedia

⇧  ってことは、+0900 の部分は無視しちゃって良いってことなんすかね、だって、ここ、日本だしね!

....... ..... ...

モヤモヤ感が半端ない...。

 

k11i.biz

⇧  上記サイト様によりますと、独自のフォーマッターを作ってあげれば良いようです。

というわけで、

「悲しみに終わりはないの! 」
2019年4月4日(木)追記:↑  ここまで

 

2019年4月11日(木)追記:↓  ここから

例外の起こりえないシステムなんてねぇ!はい、ということで、パースしようのない文字列が来てしまった場合のことを考えてませんでした...。

対応バージョンを実装してみました。

import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeParseException;

public class TestISOtoTimestamp {

	public static void main(String[] args) {
		// TODO 自動生成されたメソッド・スタブ
		String dateString = "20190310T202921+0900"; // parseで落ちる
		//String dateString = "20190310T202921Z0900"; // parseで落ちる
		//String dateString = "20190310T202921Z";     // parseで落ちる
		//String dateString = "20190310T202921";      // parseで落ちる

		//String dateString = "2019-03-10T20:29:21+0900";  // parseで落ちる
		//String dateString = "2019-03-10T20:29:21Z09:00"; // parseで落ちる
		//String dateString = "2019-03-10T20:29:21Z";

		System.out.println(convertTime(dateString));

	}

	private static Timestamp convertTime(String dateString) {
		int index = 0;
		long days = 1;
		Timestamp timestamp = null;
		if(( index = dateString.indexOf("+")) > 0) {
			dateString = dateString.substring(0, index) + "Z";
		}
	    //DateTimeFormatter isoFormatter = DateTimeFormatter.ISO_INSTANT.withZone(ZoneOffset.UTC);
	    //Instant dateInstant = Instant.from(isoFormatter.parse(dateString));
	    //LocalDateTime date = LocalDateTime.parse(dateString);
	    LocalDateTime date;
		try {
			Instant dateInstant = Instant.parse(dateString);
			date = LocalDateTime.ofInstant(dateInstant, ZoneId.of(ZoneOffset.UTC.getId()));
			timestamp =Timestamp.valueOf(date);
		} catch (DateTimeParseException e) {
			// パースできない日付文字列が連携された場合
			timestamp = Timestamp.valueOf(LocalDateTime.now().plusDays(days));

		}
	    return timestamp;
	}
}

でパースで落ちてた日付文字列で実施。

f:id:ts0818:20190411222812p:plain

f:id:ts0818:20190411222840p:plain
⇧  現在の 日時に+1日された日時が表示されたので、見事にパースされない文字列が来たと...。

 

というわけで、今回も、

「悲しみに終わりはないの! 」 

2019年4月11日(木)追記:↑  ここまで