日付・時刻操作

 
 Javaで日付、時刻関連の処理を作る場合、大抵はjava.util.Dateクラスを利用すると思います。
今回は、その日付操作に関するTipsです。

・日付・時刻への変換について

 例えば、あるログファイルを解析して集計させるような処理において、String → Dateへの変換処理が必要になってきます。
この場合、一般的なJava入門書や、解説ページでは、下記の様なコードサンプルが殆どです。


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

public class hoge{
public static void main(String str[])
{
String _strDate = "2005/05/02 18:00:00";
Date d = null;

//d = new Date(_strDate); …(A)
try{
d = DateFormat.getDateTimeInstance().parse(_strDate); //…(B)
}catch(ParseException pe){
System.err.println("入力された文字列は、日付として認識できません");
pe.printStackTrace();
}
System.out.println("String :" + _strDate );
System.out.println("Date :" + d.toString());
}
}
もしも、あなたが参考にしている書籍や、ページが(A)のコードだった場合はすみやかに処 分しましょう。
内容が古すぎます。コンストラクタDate(String s) はJDK1.1以降は推奨されませ ん

上記コードを実行してみましょう。結果は下記のようになります。
>java hoge
String :2005/05/02 18:00:00
Date :Mon May 02 18:00:00 JST 2005
正しく、Stringから、Dateへの変換が行われました。ここで、入力するStringを

  "2005/05/50 29:90:135"

とメチャクチャに変更してみましょう。どうなると思いますか?
ParseException がスローされると思いますか?

実際に実行してみると、このようになります。
>java hoge
String :2005/05/50 29:90:135
Date :Mon Jun 20 06:32:15 JST 2005
例外はスローされません。こ れは、日付の解釈を曖昧に行っているからです。
2005/05/50 は 2005/05/31 + 19 日 = 2005/06/19
29:90:135 は 24 + 5時間 = 1日後のAM5時
         60 + 30 分 = 1時間後の30分
      60 + 60 + 15秒 = 2分後の15秒

全てを計算して、2005/05/31 +19 +1 05 +1 : 30 +2 :15 = 2005/06/20 06:32:15 となるのです。
別の例として、このような負の表記も解釈してくれます
>java hoge
String :2005/-5/02 18:-20:00
Date :Fri Jul 02 17:40:00 JST 2004


DateFormatクラスは便利なクラスですが、標準ではこのように解釈が非常に曖昧です。
この事実を考慮せずに、このクラスを利用している例を数多く見かけます。
例えば、WEBシステム等で、ユーザーの誕生日を処理する箇所等で曖昧な解釈を受け付けてしまっていると
エラーとなるはずが、意図しない日付で登録されてし まったりします。
また、日付や時刻が意図しないデータになってしまうと、最悪のケースとして、セキュリティ侵害や、
データの破損改ざんにもつ ながりかねません。


(i)『クライアントサイドでjavascriptによる入力チェックを行っているから大丈夫
(ii)『値は、リストボックスから選ぶ方式だから大丈夫

とかいう人もいますが、
それは、性善説にならった設計で、あまり良くありません。
たとえ社内Onlyなシステムであれ、悪意をもったアクセスは必ずあると言う前提で設計す るべきです。
(個人情報保護法も施行されましたことですし…。)

(i)のケースはJavaScriptを切っていたり、回避させることで、迂回できます。
(ii)のケースはHTTPのことが分かっていません。
入力パラメータはGETであれPOSTであれ文字列として処理されます。
リストボックスに無い値を入力値にすることは不可能ではありません。

では、どうしましょう?
年月日や時刻がある範囲にあることを判断するために、String.substring(int,int)で分割して判断させますか?
    int  _Y =  Integer.parseInt(_strDate.substring(0,4));
int _M = Integer.parseInt(_strDate.substring(6,7));
int _D = Integer.parseInt(_strDate.substring(8,10));
int _h = Integer.parseInt(_strDate.substring(11,13));
int _m = Integer.parseInt(_strDate.substring(14,16));
int _s = Integer.parseInt(_strDate.substring(17,19));

if(_Y ==2005 && //2005年であること
(0 < _M ) && (_M < 13) //01〜12
&& (0 < _D) && (_D < 32) //01〜31
&& (-1 < _h) && (_h < 24)//01〜23
&& (-1 < _m) && (_m < 60)//00〜59
&& (-1 < _s) && (_s < 60)//00〜59
) ………
   ……………

これでは、31日まで日付の無い、4月や、6月、また、閏年等の判定がさらに必要になりませんか?
また、入力文字列の形式や長さが変更されるたびに、文字列の分割も変更しなくてはなりません。
『05』と入力するところを『5』と入力するだけで、動作しなくなってしまいます。

† 1 …余談ですが、私がかかわったとある大規模なWEBシステムで、『共通処理クラス』という名前でこのような処理が組 み込まれているのを見たことがあります。
もっとも、その処理では2100年までの閏年となる年や、30日までしかない月等をテーブルで持ち、判断させていましたが…。
また、別の案件では、文字列が日付として妥当がどうかを判断させるために、一旦データベースへinsertし、
その結果内容やSQLの戻り値で判断させていました。もちろん、パフォーマンスや再利用性は皆無であることは言うまでもありません…。


setLenient(boolean b)
 そこで、今回のTipsと致しましては、このsetLenient(boolean b)というメソッドをご紹介したいと思います。
Lenient とは、英語で、『寛大な』という意味があります。このメソッドは、日付の解釈を厳密に行うのかどうかを
設定します。現在の設定はisLenient()で調べることができます。下記のコードで試してみましょう。

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

public class hoge2{
public static void main(String str[])
{
String _strDate = "2005/05/02 18:00:00";
String _strBadDate = "2005/50/-2 970:-5:200";

Date d1 = null,d2 = null,d3 = null,d4 = null;

try{
DateFormat normFmt = DateFormat.getDateTimeInstance();
DateFormat cstmFmt = DateFormat.getDateTimeInstance();
cstmFmt.setLenient(false);

System.out.println("normFmt isLenient()" + normFmt.isLenient());
System.out.println("cstmFmt isLenient()" + cstmFmt.isLenient());

d1 = normFmt.parse(_strDate);
d2 = normFmt.parse(_strBadDate);
d3 = cstmFmt.parse(_strDate);
d4 = cstmFmt.parse(_strBadDate); //… (C)

}catch(ParseException pe){
System.err.println("入力された文字列は、日付として認識できません");
pe.printStackTrace();
}
System.out.println("String :" + _strDate );
System.out.println("String :" + _strBadDate );

System.out.println("Date1 :" + d1.toString());
System.out.println("Date2 :" + d2.toString());
System.out.println("Date3 :" + d3.toString());
System.out.println("Date4 :" + d4.toString());
}
}
(C)の箇所で例外になるはずですので、実行してみましょう。

>java hoge2
normFmt isLenient()true
cstmFmt isLenient()false
入力された文字列は、日付として認識できません
java.text.ParseException: Unparseable date: "2005/50/-2 970:-5:200"
at java.text.DateFormat.parse(DateFormat.java:334)
at hoge2.main(hoge2.java:23)
String :2005/05/02 18:00:00
String :2005/50/-2 970:-5:200
Date1 :Mon May 02 18:00:00 JST 2005
Date2 :Mon Jan 19 16:55:32 JST 2009
Date3 :Mon May 02 18:00:00 JST 2005
Exception in thread "main" java.lang.NullPointerException
at hoge2.main(hoge2.java:35)
正しく例外処理に入りました。(最終行のNullPointerExceptionは、d4が作成できなかった為です。)
ソースコードからもおわかりだと思いますが、isLenient()のデフォルト値は『true』で す。
厳密な解釈をさせるためには『false』にする必要があります
 また、サンプルソースコードでは、parse(String str)を利用しましたが、このメソッド例外処理が
必要になりますので少々不便ですから、実際に使用されるときには

   DateFormat.parse(String str,ParsePosition pos)

を利用されるほうが使い勝手がよいです。これは解析に失敗するとnullを返します。
最後に、これらのメソッド等を利用して、入力された文字列が厳密に日付として解釈できるかどうかを
判定してくれるisDate(String str)というメソッドを作成してみます。

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

public class DateUtil
{

public static boolean isDate(String str)
{
return isDate(str,new ParsePosition(0),false);
}
public static boolean isDate(String str,ParsePosition pos)
{
return isDate(str,pos,false);
}
public static boolean isDate(String str,boolean lenient)
{
return isDate(str,new ParsePosition(0),lenient);
}
public static boolean isDate(String str,ParsePosition pos,boolean lenient)
{
DateFormat fmt = DateFormat.getDateInstance();
fmt.setLenient(lenient);

Date d = fmt.parse(str,pos);

return (d!=null);
}

}
このソースはあくまで例です。実際には、ロケールや、日付表記のパターンなどを考慮しなくてはなりません
目的にあわせたDateFormatインスタンスを引数にとるコンストラクタを作成したり、
isDateをオーバーライドしても良いでしょう。目的にあわせて改良してみてください。

注意:DateFormatをWEBシステムのようなマ ルチスレッド環境で使う場合には注意が必要です。        
    DateFormat.parseはスレッドセーフではありません
    同期指定するか、スレッド毎に個別のインスタンスを持たせる必要があります!
    (DateFormatの同期に関しては既成のJava製品にも結構、同期不足バグが報告されています。)





ブラウザのBACKでお戻り下さい