|
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; |
|
} |
|
} |