【java】時間の加算とオフセットの扱い


javaで時間を加算する実装例。
ストレイテナーのシミュレーションするときにテスト的にやったのでメモとして残す。

特に「時」(Hour)の部分がない文字列からDateFormatを通して時間に変換した後、別の時間と合計する場合は、
オフセットを適切な箇所に加算ないし減算してあげる必要がある。
これは、「時」の部分がない文字列のDateFormat#parseでは1970/01/01 00:00:00をもとに変換されたDateインスタンスを得るからである。
よって、基準となる1970/01/01 09:00:00からすると過去の日時であるため「負数」となり、
これを単純加算していくと負数+負数でどんどん小さくなり、結果的にわけのわからん時刻になる。

例えば47分2秒+14分58秒だが、当然だが「1時間2分0秒」という値がほしいのに対し、
オフセット加算をしないと「16時間2分」になる。
これは、
47分 2秒=1970/01/01 00:47:02=-29578000ミリ秒
14分58秒=1970/01/01 00:14:58=-31502000ミリ秒
で、合計すると-61080000ミリ秒となり、
1970/01/01 09:00:00からすると「過去の日時」を指すことになるので、
結果的に「1969/12/31 16:02:00」になる。
これをHH:mm:ssでパースして「16:02:00」、つまり「16時間2分」になってるように見えてしまうのだ。



 


 
たとえば以下のようなプログラムを作る

import java.util.*;  
import java.text.*;  

public class DateFormatTest {

public static void main(String[] args) throws Throwable {  
  
	DateFormat hhmmss = new SimpleDateFormat("HH:mm:ss");  
	DateFormat mmss = new SimpleDateFormat("mm:ss");  
	DateFormat yyyymmddhhmmss = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");  
	  
	long rawoffset = Calendar.getInstance().getTimeZone().getRawOffset();  
	  
	String[] test_time_strs = new String[] {  
		"47:02"  
		,"14:58"  
		,"19:26"  
		,"35:45"  
		,"03:02"  
	};  
	  
	long sum_time_non_rawoffset = 0;  
	long sum_time_add_rawoffset = 0;  
	for(int i=0; i < test_time_strs.length; i++) {  
		//Date date = hhmmss.parse(test_time_strs[i]);  
		Date date = mmss.parse(test_time_strs[i]);  
		long datetime = date.getTime();  
		  
		sum_time_non_rawoffset = sum_time_non_rawoffset + datetime;  
		sum_time_add_rawoffset = sum_time_add_rawoffset + datetime + rawoffset;  
		  
		Date sum_date_non_rawoffset = new Date(sum_time_non_rawoffset);  
		Date sum_date_add_rawoffset = new Date(sum_time_add_rawoffset);  
		  
		System.out.println("String = " + test_time_strs[i]);  
		System.out.println("Date#getTime = " + datetime);  
		System.out.println("HH:mm:ss format = " + hhmmss.format(date));  
		System.out.println("yyyy/MM/dd HH:mm:ss format = " + yyyymmddhhmmss.format(date));  
		System.out.println("------------------------------------------------------------------");  
		System.out.println("Sum(non offset) = " + sum_time_non_rawoffset);  
		System.out.println("Sum(add offset) = " + sum_time_add_rawoffset);  
		System.out.println("Date(sum non offset) = " + yyyymmddhhmmss.format(sum_date_non_rawoffset));  
		System.out.println("Date(sum add offset) = " + yyyymmddhhmmss.format(sum_date_add_rawoffset));  
		System.out.println("==================================================================");  
		  
	}  
	  
	  
}  

}


これの実行結果は以下のようになる↓

String = 47:02  
Date#getTime = -29578000  
HH:mm:ss format = 00:47:02  
yyyy/MM/dd HH:mm:ss format = 1970/01/01 00:47:02  
------------------------------------------------------------------  
Sum(non offset) = -29578000  
Sum(add offset) = 2822000  
Date(sum non offset) = 1970/01/01 00:47:02←1つだけ(加算前)なら、年月日部分を気にしなければ問題ない  
Date(sum add offset) = 1970/01/01 09:47:02  
==================================================================  
String = 14:58  
Date#getTime = -31502000  
HH:mm:ss format = 00:14:58  
yyyy/MM/dd HH:mm:ss format = 1970/01/01 00:14:58  
------------------------------------------------------------------  
Sum(non offset) = -61080000  
Sum(add offset) = 3720000  
Date(sum non offset) = 1969/12/31 16:02:00←オフセットがないので1970/01/01 09:00:00+(-61080000)で計算される。ここがずれてるから以降も全部ずれる  
Date(sum add offset) = 1970/01/01 10:02:00  
==================================================================  
String = 19:26  
Date#getTime = -31234000  
HH:mm:ss format = 00:19:26  
yyyy/MM/dd HH:mm:ss format = 1970/01/01 00:19:26  
------------------------------------------------------------------  
Sum(non offset) = -92314000  
Sum(add offset) = 4886000  
Date(sum non offset) = 1969/12/31 07:21:26  
Date(sum add offset) = 1970/01/01 10:21:26  
==================================================================  
String = 35:45  
Date#getTime = -30255000  
HH:mm:ss format = 00:35:45  
yyyy/MM/dd HH:mm:ss format = 1970/01/01 00:35:45  
------------------------------------------------------------------  
Sum(non offset) = -122569000  
Sum(add offset) = 7031000  
Date(sum non offset) = 1969/12/30 22:57:11  
Date(sum add offset) = 1970/01/01 10:57:11  
==================================================================  
String = 03:02  
Date#getTime = -32218000  
HH:mm:ss format = 00:03:02  
yyyy/MM/dd HH:mm:ss format = 1970/01/01 00:03:02  
------------------------------------------------------------------  
Sum(non offset) = -154787000  
Sum(add offset) = 7213000  
Date(sum non offset) = 1969/12/30 14:00:13←!!!?!?!??!?  
Date(sum add offset) = 1970/01/01 11:00:13←これがオフセット意識した正しい計算結果。しかし…?  
==================================================================  



「Date(sum non offset)」の行にオフセット加算なしの日時
「Date(sum add offset)」の行にオフセット加算ありの日時
をそれぞれ出力している。
冒頭述べたように、「時」部分がない場合は1970/01/01 00:00:00をもとに変換をかけるので、
そのようにして作成されたDateのインスタンスは1970/01/01 09:00:00からすると「過去の日時」となり、
基準からのミリ秒としては負数になってしまう。
よって、parseした後にオフセット(Calendar.getInstance().getTimeZone().getRawOffset())を加算して、
基準を「1970/01/01 09:00:00」まで上げてやる必要がある。

オフセットは、要するに「1970/01/01 00:00:00~09:00:00までのミリ秒」なのだ。
ここを基準にしてそこからのミリ秒(未来なら+、過去なら-)で日時を決めていくから、
時間の加減算を行うにもまずはスタート地点として「1970/01/01 09:00:00」にいることを意識しなければならない。

で、結果的に「1970/01/01 11:00:13」を得る。
ただ、この計算では
47:02(47分12秒)+14:58(14分58秒)+19:26(19分26秒)+35:45(35分45秒)+03:02(3分2秒)=2時間(0分)13秒がほしいのだが、
「1970/01/01 11:00:13」をHH:mm:ssでパースした場合は当然「11:00:13」が得られ、
つまりこれだけ見ると「11時間0分13秒」になるのでやっぱり「何言ってんだ?」という感じになってしまう。
これも全ては「1970/01/01 09:00:00」を基準にしているからであって、
実際は”1970/01/01 09:00:00から1970/01/01 11:00:13までのミリ秒”を示す値が内部的にいるわけなので、
ほしい結果(2時間0分13秒)を得るならここからさらにオフセットを減算して

long sum_date_result = sum_time_add_rawoffset - rawoffset; // ←「1970/01/01 11:00:13」を示すミリ秒から「1970/01/01 09:00:00」を示すミリ秒(オフセット)を減算  
System.out.println("Result = " + hhmmss.format(new Date(sum_date_result)));  
System.out.println("Result(yyyymmdd) = " + yyyymmddhhmmss.format(new Date(sum_date_result)));  

とすることで、

Result = 02:00:13  
Result(yyyymmdd) = 1970/01/01 02:00:13  

となり、特に「Result」の行で「2:00:13(2時間0分13秒)」が得られることが確認できる。

なお、「Result(yyyymmdd) 」はおまけである。
今回ほしい結果に年月日(1970年1月1日)の情報は不要であるが、
実際にはその日時を指しているのだということをわかるようにしておきたかっただけである。



基準となるポイントが「1970/01/01 09:00:00」になっているのは、
javaを実行する環境のタイムゾーンが「UTC(+9:00)」になっているためだ。
日本では基本的に全部そうだが、これは世界的な基準値から+9:00(プラス9時間)ずれていることを指している。
実際の世界標準時というのは別のタイムゾーンとして実在し、ここでは本当に「1970/01/01 00:00:00」が基準になる。
Windows(7)でいえば、コントロールパネル⇒「時計、言語、及び地域」⇒「日付と時刻」の項にある「タイムゾーンの変更」をクリックすることで
現在のタイムゾーン、及び他のタイムゾーンを選択することが出来る。
世界標準時ではこの実行結果は以下のように変わる(抜粋)↓

String = 47:02  
Date#getTime = 2822000  
HH:mm:ss format = 00:47:02  
yyyy/MM/dd HH:mm:ss format = 1970/01/01 00:47:02  
------------------------------------------------------------------  
Sum(non offset) = 2822000  
Sum(add offset) = 2822000  
Date(sum non offset) = 1970/01/01 00:47:02←オフセット加算の有無に関わらず結果が一致  
Date(sum add offset) = 1970/01/01 00:47:02←オフセット加算の有無に関わらず結果が一致  
==================================================================  
…(以下省略)  

要するに、世界標準時ではオフセットが0になる。
※ちなみにWindowsのコントロールパネル上では「(UTC)世界標準時」という、いかにもそれっぽいがあるが、
これを選んでもオフセットは0にならない。
TimeZoneを見てみると「sun.util.calendar.ZoneInfo[id="Asia/Karachi",offset=18000000,dstSavings=0,useDaylight=false,transitions=12,lastRule=null]」と表示される。
つまり、なんか違う地域が選択されてしまっており、javaのTimeZoneとコントロールパネル上ではタイムゾーンの採用基準が少し違うようである。
実際のオフセット0地点は「(UTC)カサブランカ」を選択したときになるようだ。(なんだかよくわからんが…)


とはいっても標準時で動くシステムである前提でもない限り、というか標準時だろうが、0なら0なりに計算して先に進むだけだから、
オフセット計算は実装しておく必要があるだろう。