Because I made InternetDateFormat class.
But it hasn't tested very much.
If you use this class, you need a few test.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
/** | |
* InternetDateFormat is supported RFC3339. | |
* e.g. "1937-01-01T12:00:27.87+00:20" | |
* Because SimpleDateFormat can't parse or format to RFC3339. | |
* InternetDateFormat not invoke performance tuning, perhaps it is slowly. | |
* @version 1.14 | |
* @author Bladean Mericle | |
*/ | |
public class InternetDateFormat extends DateFormat { | |
/** Auto generated serialVersionUID. */ | |
private static final long serialVersionUID = -3948472717995863848L; | |
/** Date/time formatting with time offset. */ | |
protected boolean zoneOffsetFlagField = true; | |
/** Fractional seconds digits. */ | |
protected int fractionalSecondsLengthField = 0; | |
/** | |
* Default constructor. | |
* Output time offset, and not output fractional seconds. | |
*/ | |
public InternetDateFormat() { | |
super(); | |
setCalendar(Calendar.getInstance()); | |
setNumberFormat(NumberFormat.getInstance()); | |
} | |
/** | |
* Constructor. | |
* @param zone the given new time zone. | |
* @param aLocale the given locale. | |
*/ | |
public InternetDateFormat(TimeZone zone, Locale aLocale) { | |
super(); | |
setCalendar(Calendar.getInstance(zone, aLocale)); | |
setNumberFormat(NumberFormat.getInstance(aLocale)); | |
} | |
/** | |
* Constructor. | |
* @param anOffset date/time formatting with time offset | |
* @param aFractionalSecondsDigits fractional seconds digits (0-3) | |
*/ | |
public InternetDateFormat( | |
boolean zoneOffsetFlag, | |
int fractionalSecondsLength) { | |
this(); | |
setZoneOffset(zoneOffsetFlag); | |
setFractionalSecondsLength(fractionalSecondsLength); | |
} | |
/** | |
* Constructor. | |
* @param zone the given new time zone. | |
* @param aLocale the given locale. | |
* @param anOffset date/time formatting with time offset | |
* @param aFractionalSecondsDigits fractional seconds digits (0-3) | |
*/ | |
public InternetDateFormat( | |
TimeZone zone, | |
Locale aLocale, | |
boolean zoneOffsetFlag, | |
int fractionalSecondsLength) { | |
this(zone, aLocale); | |
setZoneOffset(zoneOffsetFlag); | |
setFractionalSecondsLength(fractionalSecondsLength); | |
} | |
/** | |
* Tell whether date/time formatting with time offset. | |
* The offset is used only format method. | |
* @return true is formatting with time offset; false otherwise. | |
*/ | |
public boolean hasZoneOffset() { | |
return zoneOffsetFlagField; | |
} | |
/** | |
* Specify whether or not date/time formatting with time offset. | |
* The offset is used only format method. | |
* @param anOffset true is formatting with time offset; false otherwise. | |
*/ | |
public void setZoneOffset(boolean zoneOffsetFlag) { | |
zoneOffsetFlagField = zoneOffsetFlag; | |
} | |
/** | |
* Gets the number of fractional seconds digits. | |
* The digits is used only format method. | |
* @return fractional seconds digits (0-3) | |
*/ | |
public int getFractionalSecondsLength() { | |
return fractionalSecondsLengthField; | |
} | |
/** | |
* Sets the number of fractional seconds digits. | |
* Digits range is 0 to 3. | |
* If digits over range, this method throws IllegalArgumentException. | |
* The digits is used only format method. | |
* @param fractionalSecondsDigits fractional seconds digits (0-3) | |
*/ | |
public void setFractionalSecondsLength(int fractionalSecondsLength) { | |
if (fractionalSecondsLength < 0 || 3 < fractionalSecondsLength) { | |
throw new IllegalArgumentException( | |
"Invalid fractional seconds length(" + fractionalSecondsLength + ")"); | |
} | |
fractionalSecondsLengthField = fractionalSecondsLength; | |
} | |
/** | |
* Formats a Date into a date/time string. | |
* @param date a Date to be formatted into a date/time string. | |
* @param toAppendTo the string buffer for the returning time string. | |
* @param fieldPosition keeps track of the position of the field within the returned string. | |
*/ | |
@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; | |
} | |
/*** | |
* Formats a Date into a fractional seconds string. | |
* @param formatCalendar be formatted date | |
* @return fractional seconds string | |
*/ | |
protected String getFractionalSeconds(Calendar calendar) { | |
if (getFractionalSecondsLength() == 0) { return ""; } | |
return "." + String.format( | |
"%03d", | |
calendar.get(Calendar.MILLISECOND)).substring( | |
0, | |
getFractionalSecondsLength()); | |
} | |
/** | |
* Parse a date/time string according to the given parse position. | |
* @param source The date/time string to be parsed | |
* @param pos the parsing position | |
* the position at which parsing terminated, or the start position if the parse failed. | |
*/ | |
@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; | |
} | |
} | |
/** | |
* Parse number, and increment parse position. | |
* @param source A String whose beginning should be parsed. | |
* @param pos the parsing position | |
* @param length parse length | |
* @return parsed number | |
*/ | |
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; | |
} | |
/** | |
* Check separator string is valid. | |
* If separetaor is invalid, this method throws IndexOutOfBoundsException. | |
* @param source A String whose beginning should be parsed. | |
* @param pos the parsing position | |
* @param separator separator string | |
*/ | |
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); | |
} | |
/** | |
* Parse fractional seconds. | |
* @param source A String whose beginning should be parsed. | |
* @param pos the parsing position | |
* @return parsed fractional seconds | |
*/ | |
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; | |
} | |
/** | |
* Parse offset. | |
* But it is not include "+" or "-". | |
* When this method is called, these operator have to be already parsed. | |
* @param source A String whose beginning should be parsed. | |
* @param pos the parsing position | |
* @return offset millisecond | |
*/ | |
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.text.ParseException; | |
import java.util.Date; | |
public class InternetDateFormatSample { | |
public static void main(String[] args) { | |
try { | |
InternetDateFormat format = new InternetDateFormat(); | |
System.out.println(format.format(new Date())); | |
System.out.println(format.parse("2013-07-04T19:52:42+09:00")); | |
format.setZoneOffset(false); | |
System.out.println(format.format(new Date())); | |
format.setFractionalSecondsLength(2); | |
System.out.println(format.format(new Date())); | |
} catch (ParseException e) { | |
e.printStackTrace(); | |
} | |
} | |
} |
FYI:Japanese Edition
2013/07/31: Update detail.
2013/12/16: Bug fix "parseFractionalSeconds".
2017/09/18: Bug fix "parse" (Thanka comment!)
Hi, thanks for sharing your code.
返信削除I found a little tricky situation where "magic" milliseconds are not correct when parsing a date formatted in String:
Code
--------
String date = "2016-05-27T12:01:02+02:00"; // no milliseconds
InternetDateFormat dateFormat = new InternetDateFormat(true, 3); // have 3 digits for milliseconds
Date d = dateFormat.parse(date);
System.out.println("before: " + date + " (" + d.getTime() + " ms)");
System.out.println("after: " + dateFormat.format(d));
Output (where 885 milliseconds appeared)
-------
before: 2016-05-27T12:01:02+02:00 (1464339662885 ms)
after: 2016-05-27T11:01:02.885+01:00
How to fix
----------
In method "parse(String source, ParsePosition pos)"
if (source.substring(pos.getIndex()).startsWith(".")) {
pos.setIndex(pos.getIndex() + 1);
parseCalendar.set(Calendar.MILLISECOND, parseFractionalSeconds(source, pos));
}
else { // <----------------------------------------------------------- ADD THIS
parseCalendar.set(Calendar.MILLISECOND, 0); // <---------------- ADD THIS
} // <---------------------------------------------------------------- ADD THIS
Loïc, loic DOT monney @@@ hefr DOC ch
Thanks!
削除