Apache CommonsのDateUtilsに機能を拡張する

 よくあるケースと思いますが、メジャーなライブラリにもうちょっと機能が欲しい時、そのライブラリを拡張して共通ライブラリとして使いまわしたりしますよね?
筆者がJavaを使う場合は、だいぶ前からApache CommonsのDateUtilsを拡張し、String・Dateの相互変換メソッドとかを追加していろんな開発案件で使いまわしてましたが、今回、文字列から日時型に変換するけど、どんな文字列パターンになるかが不明確、、という要件があったので、拡張していたクラスに機能を追加し、一般的に日時として使われる文字列からのDate変換機能を追加してみました。
※2021.04 長文からの日時文字列取得で問題があったので修正しています。

package jp.esoro.common.utils;

import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.time.DateFormatUtils;

/**
 * 日付処理クラス
 * */
public class DateUtils extends org.apache.commons.lang3.time.DateUtils {

	/**
	 * 日付表示パターン(YYYY/MM/DD hh:mm)
	 */
	public static final String FORMAT_YYYY_MM_DD_HH_MM = "yyyy/MM/dd HH:mm";

	public static final String FORMAT_YYYY_MM_DD = "yyyy/MM/dd";
	public static final String FORMAT_YYYYMMDD = "yyyyMMdd";
	public static final String FORMAT_YYMMDD = "yyMMdd";
	public static final String FORMAT_JPN_YYYY_MM_DD = "yyyy年MM月dd日";

	public static final String FORMAT_YYYY_MM_DD_HH_MM_SS = "yyyy/MM/dd HH:mm:ss";

	public static final String FORMAT_MYSQL_DATE = "yyyy-MM-dd";
	public static final String FORMAT_MYSQL_DATETIME = "yyyy-MM-dd HH:mm:ss";
	public static final String FORMAT_YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
	public static final String FORMAT_YYYYMMDDHHMMSSmi = "yyyyMMddHHmmssSSS";
	public static final String FORMAT_YYMMDDHHMMSS = "yyMMddHHmmss";
	public static final String FORMAT_YYMMDDHHMMSSmi = "yyMMddHHmmssSSS";

	/**
	 * 日付(Calendar)を文字列に編集する
	 * @param date
	 * @param pattern
	 * @return
	 */
	public static String format(Calendar date, String pattern) {
		if (date == null) {
			return null;
		}
		return DateFormatUtils.format(date, pattern);
	}

	/**
	 * 日付(Date)を文字列に編集する
	 * @param date
	 * @param pattern
	 * @return
	 */
	public static String format(Date date, String pattern) {
		if (date == null) {
			return null;
		}
		Calendar cal = Calendar.getInstance();
		cal.setTime(date);
		return format(cal, pattern);
	}

	/**
	 * システム日付を取得する
	 * @return Date
	 */
	public static Date getDateTime() {
		return Calendar.getInstance().getTime();
	}

	/**
	 * 文字列をDate型へ変換する
	 * @param value(yyyy_mm_dd)
	 * @return
	 */
	public static Date stringToDate(String value) {
	    return stringToDate(value , FORMAT_YYYY_MM_DD);
	}

	/**
	 * 文字列をDate型へ変換する
	 * @param value
	 * @param pattern 
	 * @return
	 */
	public static Date stringToDate(String value ,String pattern) {
	    SimpleDateFormat sdf = new SimpleDateFormat(pattern);
	    if (value == null) {
	    	return null;
	    }
	    try {
	        return sdf.parse(value);
	    } catch (ParseException e) {
	        return null;
	    }
	}
//ここまで以前から、以降を追加
	/**
	 * 日時文字列をDate変換する
	 * @param str Date変換したい文字列
	 * @return Date 取得出来ない場合はnull
	 * @see あらかじめ文字列パターンが判明している場合は、stringToDateを使用してください)
	 */
	public static Date extractDate(String str) {
		final String[] regexs = {"([0-9]{2,4})(/|-|¥¥.|年)([01]?[0-9])(/|-|¥¥.|月)([0123]?[0-9])(日|日¥¥s+|¥¥s|_)([0-9]{1,2})(:|-|¥¥.|時)([0-9]{1,2})(:|-|¥¥.|分)([0-9]{1,2})",
				"([0-9]{2,4})(/|-|¥¥.|年)([01]?[0-9])(/|-|¥¥.|月)([0123]?[0-9])(日|日¥¥s+|¥¥s|_)([0-9]{1,2})(:|-|¥¥.|時)([0-9]{1,2})",
				"([0-9]{2,4})(/|-|¥¥.|年)([01]?[0-9])(/|-|¥¥.|月)([0123]?[0-9])(日|日¥¥s+|¥¥s|_)([0-9]{1,2})(:|-|¥¥.|時)",
				"([0-9]{2,4})(/|-|¥¥.|年)([01]?[0-9])(/|-|¥¥.|月)([0123]?[0-9])",
				};
		Date ret = null;
		String normStr = Normalizer.normalize(str, Form.NFKC);
		try{
			String chkStr = StringUtils.removePattern(normStr, "am|AM|Am|pm|PM|Pm|午前|午後");
			
			for(String regex : regexs){
				Pattern pattern = Pattern.compile(regex, Pattern.DOTALL|Pattern.CASE_INSENSITIVE);
				Matcher matcher = pattern.matcher(chkStr);
				
				if(matcher.find()){
					if(matcher.groupCount() >= 5){
						boolean isPm = false;
						if(normStr.indexOf(matcher.group()) < 0 && matcher.groupCount() >= 7){
							if( normStr.matches(".*"+matcher.group(1)+matcher.group(2)+matcher.group(3)+matcher.group(4)+matcher.group(5)+matcher.group(6)+"(pm|PM|pm|午後).*")){
								isPm = true;
							}
						}
						
						if(matcher.group(1).length() == 2){
							ret = DateUtils.setYears(getDateTime(), Integer.valueOf("20" + matcher.group(1)));
						}
						else{
							ret = DateUtils.setYears(getDateTime(), Integer.valueOf(matcher.group(1)));
						}
						ret = DateUtils.setDays(ret, 1);
						//Month is 0 -11
						ret = DateUtils.setMonths(ret, Integer.valueOf(matcher.group(3))-1);
						ret = DateUtils.setDays(ret, Integer.valueOf(matcher.group(5)));
						
						if(matcher.groupCount() >= 7){
							int hour = Integer.valueOf(matcher.group(7));
							ret = DateUtils.setHours(ret, hour + (hour<12 && isPm?12:0));
						}
						else{
							ret = DateUtils.setHours(ret, 0);
						}
						if(matcher.groupCount() >= 9){
							ret = DateUtils.setMinutes(ret, Integer.valueOf(matcher.group(9)));
						}
						else{
							ret = DateUtils.setMinutes(ret, 0);
						}
						if(matcher.groupCount() == 11){
							ret = DateUtils.setSeconds(ret, Integer.valueOf(matcher.group(11)));
						}
						else{
							ret = DateUtils.setSeconds(ret, 0);
						}
					}
					return ret;
				}
			}
			if(ret == null){
				SimpleDateFormat sdf1 = new SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy",Locale.ENGLISH);
		        ret = sdf1.parse(chkStr);
			}
		}catch(Exception e){
	        return null;
		}
		return ret;
	}
}

で、テストです。

package test;

import jp.esoro.common.utils.DateUtils;

public class Datetest {
	static String[] dateStrs = {
			"20170131",
			"2017-9-2 15:00:00",
			"2017/9/2 15:25:00",
			"2017年9月2日8時34分51秒",
			"2017年1月2日8時32分",
			"2017年12月31日午後11時集合",
			"2017年12月2日8時",
			"2017.12.2 PM8:00",
			"2017.12.3 AM8:25",
			"2017.01.31",
			"2017.1.32"	};

	public static void main(String[] args) {
		for(String str : dateStrs){
			System.out.println(str + " is " + DateUtils.format(DateUtils.extractDate(str),DateUtils.FORMAT_YYYY_MM_DD_HH_MM_SS));
		}
	}
}

テスト結果
20170131 is null
2017-9-2 15:00:00 is 2017/09/02 15:00:00
2017/9/2 15:25:00 is 2017/09/02 15:25:00
2017年9月2日8時34分51秒 is 2017/09/02 08:34:51
2017年1月2日8時32分 is 2017/01/02 08:32:00
2017年12月31日午後11時集合 is 2017/12/31 23:00:00
2017年12月2日8時 is 2017/12/02 08:00:00
2017.12.2 PM8:00 is 2017/12/02 20:00:00
2017.12.3 AM8:25 is 2017/12/03 08:25:00
2017.01.31 is 2017/01/31 00:00:00
2017.1.32 is null

YYYYMMDDってのは、ここでは対象外です。だって、ただの数字の羅列は文字として日付とは言えないし。

2年たってバグってた事が判明。1日に戻すのを追加しました。。

カテゴリー: Java