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.13
 * @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));
         }
         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追記:ミリ秒のパースに問題があったので慌てて修正。英語版との名前の差異は…もうめどいからいいや。

0 件のコメント:

コメントを投稿