2011年5月23日月曜日

インターネット日付フォーマット

JavaがRFC3339に対応していないらしいので、
適当に作ってみました。

よくよく考えるとSimpleDateFormatに大部分をまかせてもよかった気がしますが、
頭のエクササイズだと思ってスルーすることにしました。

今回のはま~ったく自信がないので、
流用される方はよくテストしてください。

import java.text.DateFormat;
import java.text.FieldPosition;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
/**
* RFC3339に準拠した日付フォーマットです.
* 例:"1937-01-01T12:00:27.87+00:20"
* 速度はあまり重視していません.
* @version 1.14
* @author Bladean Mericle
*/
public class InternetDateFormat extends DateFormat {
/** 自動生成されたserialVersionUID. */
private static final long serialVersionUID = -3948472717995863848L;
/** タイムゾーンオフセット. */
protected boolean zoneOffsetFlagField = true;
/** 秒数の小数点以下桁. */
protected int fractionalSecondsLengthField = 0;
/**
* デフォルトコンストラクタ.
* タイムゾーンオフセットは出力し、秒数の小数点以下は出力しません.
*/
public InternetDateFormat() {
super();
setCalendar(Calendar.getInstance());
setNumberFormat(NumberFormat.getInstance());
}
/**
* コンストラクタ.
* @param zone タイムゾーン
* @param aLocale ロケール
*/
public InternetDateFormat(TimeZone zone, Locale aLocale) {
super();
setCalendar(Calendar.getInstance(zone, aLocale));
setNumberFormat(NumberFormat.getInstance(aLocale));
}
/**
* コンストラクタ.
* @param zoneOffsetFlag タイムゾーンオフセット出力フラグ
* @param fractionalSecondsLength 秒数の小数点以下の桁(0~3)
*/
public InternetDateFormat(
boolean zoneOffsetFlag,
int fractionalSecondsLength) {
this();
setZoneOffset(zoneOffsetFlag);
setFractionalSecondsLength(fractionalSecondsLength);
}
/**
* コンストラクタ.
* @param zone タイムゾーン
* @param aLocale ロケール
* @param zoneOffsetFlag タイムゾーンオフセット出力フラグ
* @param fractionalSecondsLength 秒数の小数点以下の桁(0~3)
*/
public InternetDateFormat(
TimeZone zone,
Locale aLocale,
boolean zoneOffsetFlag,
int fractionalSecondsLength) {
this(zone, aLocale);
setZoneOffset(zoneOffsetFlag);
setFractionalSecondsLength(fractionalSecondsLength);
}
/**
* タイムゾーンオフセットを詳細に出力するかを取得します.
* この値はフォーマット時にのみ適用されます.
* 解析時には適用されません.
* @return trueの場合、詳細に出力する
*/
public boolean hasZoneOffset() {
return zoneOffsetFlagField;
}
/**
* タイムゾーンオフセットを詳細に出力するかを設定します.
* この値はフォーマット時にのみ適用されます.
* 解析時には適用されません.
* @param zoneOffsetFlag trueの場合、詳細に出力する
*/
public void setZoneOffset(boolean zoneOffsetFlag) {
zoneOffsetFlagField = zoneOffsetFlag;
}
/**
* 秒数を小数点以下第何位まで出力するかを取得します.
* この値はフォーマット時にのみ適用されます.
* 解析時には適用されません.
* @return 秒数の小数点以下の桁(0~3)
*/
public int getFractionalSecondsLength() {
return fractionalSecondsLengthField;
}
/**
* 秒数を小数点以下第何位まで出力するかを設定します.
* この値は0~3までしか設定できません.
* その範囲を超えた場合は、IllegalArgumentExceptionが発生します.
* この値はフォーマット時にのみ適用されます.
* 解析時には適用されません.
* @param fractionalSecondsLength 秒数の小数点以下の桁(0~3)
*/
public void setFractionalSecondsLength(int fractionalSecondsLength) {
if (fractionalSecondsLength < 0 || 3 < fractionalSecondsLength) {
throw new IllegalArgumentException(
"Invalid fractional seconds length(" + fractionalSecondsLength + ")");
}
fractionalSecondsLengthField = fractionalSecondsLength;
}
/**
* 日付からフォーマット文字列を取得します.
* @param date 日付
* @param toAppendTo 文字列を追記するバッファ
* @param fieldPosition 使用していません
*/
@Override
public StringBuffer format(
Date date,
StringBuffer toAppendTo,
FieldPosition fieldPosition) {
Calendar calendar = (Calendar) getCalendar().clone();
calendar.setTime(date);
if (hasZoneOffset()) {
int offset = calendar.get(Calendar.ZONE_OFFSET);
toAppendTo.append(String.format(
"%1$tFT%1$tH:%1$tM:%1$tS", calendar.getTime()));
toAppendTo.append(getFractionalSeconds(calendar));
toAppendTo.append((offset >= 0) ? "+" : "-");
toAppendTo.append(String.format(
"%1$02d:%2$02d",
offset / 3600000,
offset % 3600000 / 1000));
} else {
// タイムゾーンオフセットの補正
calendar.add(Calendar.MILLISECOND, -calendar.get(Calendar.ZONE_OFFSET));
toAppendTo.append(String.format(
"%1$tFT%1$tH:%1$tM:%1$tS", calendar.getTime()));
toAppendTo.append(getFractionalSeconds(calendar));
toAppendTo.append("Z");
}
return toAppendTo;
}
/**
* 秒数の小数点以下をドット付き文字列で取得します.
* 例えば10.527秒であれば、".527"や".5"のような結果となります.
* 桁数を0に設定していた場合は、空文字列を返します.
* @param calendar 日時
* @return 秒数の小数点以下
*/
protected String getFractionalSeconds(Calendar calendar) {
if (getFractionalSecondsLength() == 0) { return ""; }
return "." + String.format(
"%03d",
calendar.get(Calendar.MILLISECOND)).substring(
0,
getFractionalSecondsLength());
}
/**
* 文字列を解析し、日付を取得します.
* @param source 解析する文字列
* @param pos 解析位置情報
*/
@Override
public Date parse(String source, ParsePosition pos) {
try {
Calendar calendar = (Calendar) getCalendar().clone();
calendar.set(Calendar.YEAR, parseNumber(source, pos, 4));
checkSeparator(source, pos, "-");
calendar.set(Calendar.MONTH, parseNumber(source, pos, 2) - 1);
checkSeparator(source, pos, "-");
calendar.set(Calendar.DAY_OF_MONTH, parseNumber(source, pos, 2));
checkSeparator(source, pos, "T");
calendar.set(Calendar.HOUR_OF_DAY, parseNumber(source, pos, 2));
checkSeparator(source, pos, ":");
calendar.set(Calendar.MINUTE, parseNumber(source, pos, 2));
checkSeparator(source, pos, ":");
calendar.set(Calendar.SECOND, parseNumber(source, pos, 2));
if (source.substring(pos.getIndex()).startsWith(".")) {
pos.setIndex(pos.getIndex() + 1);
calendar.set(Calendar.MILLISECOND, parseFractionalSeconds(source, pos));
} else {
calendar.set(Calendar.MILLISECOND, 0);
}
String next = source.substring(pos.getIndex());
if (next.equals("Z")) {
pos.setIndex(pos.getIndex() + 1);
calendar.set(Calendar.ZONE_OFFSET, 0); // 省略時は00:00と等価
return calendar.getTime();
} else if (next.startsWith("+")) {
pos.setIndex(pos.getIndex() + 1);
calendar.set(Calendar.ZONE_OFFSET, parseZoneOffset(source, pos));
return calendar.getTime();
} else if (next.startsWith("-")) {
pos.setIndex(pos.getIndex() + 1);
calendar.set(Calendar.ZONE_OFFSET, -parseZoneOffset(source, pos));
return calendar.getTime();
}
pos.setErrorIndex(pos.getIndex());
return null;
} catch (IndexOutOfBoundsException e) {
pos.setErrorIndex(pos.getIndex());
return null;
}
}
/**
* 文字列を解析し、数値を取得します.
* @param source 解析する文字列
* @param pos 解析位置情報
* @param length 解析する長さ
* @return 解析した数値
*/
protected int parseNumber(
String source,
ParsePosition pos,
int length) {
int index = pos.getIndex();
int number = Integer.parseInt(source.substring(index, index + length));
pos.setIndex(index + length);
return number;
}
/**
* 文字列を解析し、セパレーターが正しいかチェックします.
* セパレーターが不正な場合、IndexOutOfBoundsExceptionがスローされます.
* @param source 解析する文字列
* @param pos 解析位置情報
* @param separator セパレーター
*/
protected void checkSeparator(
String source,
ParsePosition pos,
String separator) {
int index = pos.getIndex();
int length = separator.length();
if (!source.substring(index, index + length).equals(separator)) {
throw new IndexOutOfBoundsException();
}
pos.setIndex(index + length);
}
/**
* 文字列を解析し、秒数の小数点以下を取得します.
* @param source 解析する文字列
* @param pos 解析位置情報
* @return 秒数の小数点以下
*/
protected int parseFractionalSeconds(String source, ParsePosition pos) {
String milliSecond = source.substring(pos.getIndex()).split("\\D")[0];
int number = Integer.parseInt((milliSecond + "000").substring(0, 3));
pos.setIndex(pos.getIndex() + milliSecond.length());
return number;
}
/**
* 文字列を解析し、タイムゾーンオフセットを取得します.
* 書式は"HH:mm"で、正負は既に解析されたものとしています.
* @param source 解析する文字列
* @param pos 解析位置情報
* @return タイムゾーンオフセット(絶対値)
*/
protected int parseZoneOffset(
String source,
ParsePosition pos) {
int hour = parseNumber(source, pos, 2);
checkSeparator(source, pos, ":");
int minute = parseNumber(source, pos, 2);
return (hour * 60 + minute) * 60000;
}
}

2011/11/24追記:バグを見つけたので修正しました。タイムゾーン省略時はローカル時じゃなく標準時なのね…
2013/06/26追記:一箇所セペレーターだったので慌てて修正
2013/07/17追記:英語版にしてみました。
2013/12/16追記:ミリ秒のパースに問題があったので慌てて修正。英語版との名前の差異は…もうめどいからいいや。
2017/09/18追記:パースのバグを直しました。コメントくれた方ありがとう!

0 件のコメント:

コメントを投稿