Mercurial > hg > icedtea9-forest > jdk
changeset 2853:c47eb064d6ba
4919632: RFE: SimpleDateFormat should fully support ISO8601 standard for timezone
Reviewed-by: peytoia
author | okutsu |
---|---|
date | Thu, 09 Sep 2010 15:37:57 +0900 |
parents | 93d13ea00faf |
children | 4f1eacca4e6b |
files | src/share/classes/java/text/DateFormatSymbols.java src/share/classes/java/text/SimpleDateFormat.java test/java/text/Format/DateFormat/ISO8601ZoneTest.java |
diffstat | 3 files changed, 387 insertions(+), 5 deletions(-) [+] |
line wrap: on
line diff
--- a/src/share/classes/java/text/DateFormatSymbols.java Thu Sep 02 11:13:42 2010 -0700 +++ b/src/share/classes/java/text/DateFormatSymbols.java Thu Sep 09 15:37:57 2010 +0900 @@ -226,7 +226,7 @@ * Unlocalized date-time pattern characters. For example: 'y', 'd', etc. * All locales use the same these unlocalized pattern characters. */ - static final String patternChars = "GyMdkHmsSEDFwWahKzZYu"; + static final String patternChars = "GyMdkHmsSEDFwWahKzZYuX"; static final int PATTERN_ERA = 0; // G static final int PATTERN_YEAR = 1; // y @@ -249,6 +249,7 @@ static final int PATTERN_ZONE_VALUE = 18; // Z static final int PATTERN_WEEK_YEAR = 19; // Y static final int PATTERN_ISO_DAY_OF_WEEK = 20; // u + static final int PATTERN_ISO_ZONE = 21; // X /** * Localized date-time pattern characters. For example, a locale may
--- a/src/share/classes/java/text/SimpleDateFormat.java Thu Sep 02 11:13:42 2010 -0700 +++ b/src/share/classes/java/text/SimpleDateFormat.java Thu Sep 09 15:37:57 2010 +0900 @@ -204,6 +204,11 @@ * <td>Time zone * <td><a href="#rfc822timezone">RFC 822 time zone</a> * <td><code>-0800</code> + * <tr bgcolor="#eeeeff"> + * <td><code>X</code> + * <td>Time zone + * <td><a href="#iso8601timezone">ISO 8601 time zone</a> + * <td><code>-08</code>; <code>-0800</code>; <code>-08:00</code> * </table> * </blockquote> * Pattern letters are usually repeated, as their number determines the @@ -288,6 +293,7 @@ * accepted.<br><br></li> * <li><strong><a name="rfc822timezone">RFC 822 time zone:</a></strong> * For formatting, the RFC 822 4-digit time zone format is used: + * * <pre> * <i>RFC822TimeZone:</i> * <i>Sign</i> <i>TwoDigitHours</i> <i>Minutes</i> @@ -295,8 +301,41 @@ * <i>Digit Digit</i></pre> * <i>TwoDigitHours</i> must be between 00 and 23. Other definitions * are as for <a href="#timezone">general time zones</a>. + * * <p>For parsing, <a href="#timezone">general time zones</a> are also * accepted. + * <li><strong><a name="iso8601timezone">ISO 8601 Time zone:</a></strong> + * The number of pattern letters designates the format for both formatting + * and parsing as follows: + * <pre> + * <i>ISO8601TimeZone:</i> + * <i>OneLetterISO8601TimeZone</i> + * <i>TwoLetterISO8601TimeZone</i> + * <i>ThreeLetterISO8601TimeZone</i> + * <i>OneLetterISO8601TimeZone:</i> + * <i>Sign</i> <i>TwoDigitHours</i> + * {@code Z} + * <i>TwoLetterISO8601TimeZone:</i> + * <i>Sign</i> <i>TwoDigitHours</i> <i>Minutes</i> + * {@code Z} + * <i>ThreeLetterISO8601TimeZone:</i> + * <i>Sign</i> <i>TwoDigitHours</i> {@code :} <i>Minutes</i> + * {@code Z}</pre> + * Other definitions are as for <a href="#timezone">general time zones</a> or + * <a href="#rfc822timezone">RFC 822 time zones</a>. + * + * <p>For formatting, if the offset value from GMT is 0, {@code "Z"} is + * produced. If the number of pattern letters is 1, any fraction of an hour + * is ignored. For example, if the pattern is {@code "X"} and the time zone is + * {@code "GMT+05:30"}, {@code "+05"} is produced. + * + * <p>For parsing, {@code "Z"} is parsed as the UTC time zone designator. + * <a href="#timezone">General time zones</a> are <em>not</em> accepted. + * + * <p>If the number of pattern letters is 4 or more, {@link + * IllegalArgumentException} is thrown when constructing a {@code + * SimpleDateFormat} or {@linkplain #applyPattern(String) applying a + * pattern}. * </ul> * <code>SimpleDateFormat</code> also supports <em>localized date and time * pattern</em> strings. In these strings, the pattern letters described above @@ -343,6 +382,9 @@ * <td><code>"yyyy-MM-dd'T'HH:mm:ss.SSSZ"</code> * <td><code>2001-07-04T12:08:56.235-0700</code> * <tr bgcolor="#eeeeff"> + * <td><code>"yyyy-MM-dd'T'HH:mm:ss.SSSXXX"</code> + * <td><code>2001-07-04T12:08:56.235-07:00</code> + * <tr> * <td><code>"YYYY-'W'ww-u"</code> * <td><code>2001-W27-3</code> * </table> @@ -839,6 +881,9 @@ * Encodes the given tag and length and puts encoded char(s) into buffer. */ private static final void encode(int tag, int length, StringBuilder buffer) { + if (tag == PATTERN_ISO_ZONE && length >= 4) { + throw new IllegalArgumentException("invalid ISO 8601 format: length=" + length); + } if (length < 255) { buffer.append((char)(tag << 8 | length)); } else { @@ -995,7 +1040,8 @@ Calendar.ZONE_OFFSET, // Pseudo Calendar fields CalendarBuilder.WEEK_YEAR, - CalendarBuilder.ISO_DAY_OF_WEEK + CalendarBuilder.ISO_DAY_OF_WEEK, + Calendar.ZONE_OFFSET }; // Map index into pattern character string to DateFormat field number @@ -1009,7 +1055,8 @@ DateFormat.WEEK_OF_MONTH_FIELD, DateFormat.AM_PM_FIELD, DateFormat.HOUR1_FIELD, DateFormat.HOUR0_FIELD, DateFormat.TIMEZONE_FIELD, DateFormat.TIMEZONE_FIELD, - DateFormat.YEAR_FIELD, DateFormat.DAY_OF_WEEK_FIELD + DateFormat.YEAR_FIELD, DateFormat.DAY_OF_WEEK_FIELD, + DateFormat.TIMEZONE_FIELD }; // Maps from DecimalFormatSymbols index to Field constant @@ -1021,7 +1068,8 @@ Field.WEEK_OF_YEAR, Field.WEEK_OF_MONTH, Field.AM_PM, Field.HOUR1, Field.HOUR0, Field.TIME_ZONE, Field.TIME_ZONE, - Field.YEAR, Field.DAY_OF_WEEK + Field.YEAR, Field.DAY_OF_WEEK, + Field.TIME_ZONE }; /** @@ -1189,6 +1237,34 @@ CalendarUtils.sprintf0d(buffer, num, width); break; + case PATTERN_ISO_ZONE: // 'X' + value = calendar.get(Calendar.ZONE_OFFSET) + + calendar.get(Calendar.DST_OFFSET); + + if (value == 0) { + buffer.append('Z'); + break; + } + + value /= 60000; + if (value >= 0) { + buffer.append('+'); + } else { + buffer.append('-'); + value = -value; + } + + CalendarUtils.sprintf0d(buffer, value / 60, 2); + if (count == 1) { + break; + } + + if (count == 3) { + buffer.append(':'); + } + CalendarUtils.sprintf0d(buffer, value % 60, 2); + break; + default: // case PATTERN_DAY_OF_MONTH: // 'd' // case PATTERN_HOUR_OF_DAY0: // 'H' 0-based. eg, 23:59 + 1 hour =>> 00:59 @@ -1973,6 +2049,94 @@ } break parsing; + case PATTERN_ISO_ZONE: // 'X' + { + int sign = 0; + int offset = 0; + + iso8601: { + try { + char c = text.charAt(pos.index); + if (c == 'Z') { + calb.set(Calendar.ZONE_OFFSET, 0).set(Calendar.DST_OFFSET, 0); + return ++pos.index; + } + + // parse text as "+/-hh[[:]mm]" based on count + if (c == '+') { + sign = 1; + } else if (c == '-') { + sign = -1; + } + // Look for hh. + int hours = 0; + c = text.charAt(++pos.index); + if (c < '0' || c > '9') { /* must be from '0' to '9'. */ + break parsing; + } + hours = c - '0'; + c = text.charAt(++pos.index); + if (c < '0' || c > '9') { /* must be from '0' to '9'. */ + break parsing; + } + hours *= 10; + hours += c - '0'; + if (hours > 23) { + break parsing; + } + + if (count == 1) { // "X" + offset = hours * 60; + break iso8601; + } + + c = text.charAt(++pos.index); + // Skip ':' if "XXX" + if (c == ':') { + if (count == 2) { + break parsing; + } + c = text.charAt(++pos.index); + } else { + if (count == 3) { + // missing ':' + break parsing; + } + } + + // Look for mm. + int minutes = 0; + if (c < '0' || c > '9') { /* must be from '0' to '9'. */ + break parsing; + } + minutes = c - '0'; + c = text.charAt(++pos.index); + if (c < '0' || c > '9') { /* must be from '0' to '9'. */ + break parsing; + } + minutes *= 10; + minutes += c - '0'; + + if (minutes > 59) { + break parsing; + } + + offset = hours * 60 + minutes; + } catch (StringIndexOutOfBoundsException e) { + break parsing; + } + } + + // Do the final processing for both of the above cases. We only + // arrive here if the form GMT+/-... or an RFC 822 form was seen. + if (sign != 0) { + offset *= MILLIS_PER_MINUTE * sign; + calb.set(Calendar.ZONE_OFFSET, offset).set(Calendar.DST_OFFSET, 0); + return ++pos.index; + } + } + break parsing; + default: // case PATTERN_DAY_OF_MONTH: // 'd' // case PATTERN_HOUR_OF_DAY0: // 'H' 0-based. eg, 23:59 + 1 hour =>> 00:59 @@ -2102,7 +2266,7 @@ * @exception NullPointerException if the given pattern is null * @exception IllegalArgumentException if the given pattern is invalid */ - public void applyPattern (String pattern) + public void applyPattern(String pattern) { compiledPattern = compile(pattern); this.pattern = pattern;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/java/text/Format/DateFormat/ISO8601ZoneTest.java Thu Sep 09 15:37:57 2010 +0900 @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 4919632 + * @summary Unit test for ISO8601 time zone format support + */ + +import java.text.*; +import java.util.*; + +public class ISO8601ZoneTest { + static final Date TIMESTAMP = new Date(1283758039020L); + + static final String[][] formatData = { + // time zone name, expected output at TIMESTAMP + { "America/Los_Angeles", "2010-09-06T00:27:19.020-07", }, + { "America/Los_Angeles", "2010-09-06T00:27:19.020-0700", }, + { "America/Los_Angeles", "2010-09-06T00:27:19.020-07:00", }, + { "Australia/Sydney", "2010-09-06T17:27:19.020+10", }, + { "Australia/Sydney", "2010-09-06T17:27:19.020+1000", }, + { "Australia/Sydney", "2010-09-06T17:27:19.020+10:00", }, + { "GMT-07:00", "2010-09-06T00:27:19.020-07", }, + { "GMT-07:00", "2010-09-06T00:27:19.020-0700", }, + { "GMT-07:00", "2010-09-06T00:27:19.020-07:00", }, + { "UTC", "2010-09-06T07:27:19.020Z", }, + { "UTC", "2010-09-06T07:27:19.020Z", }, + { "UTC", "2010-09-06T07:27:19.020Z", }, + }; + + static final String[] zones = { + "America/Los_Angeles", "Australia/Sydney", "GMT-07:00", + "UTC", "GMT+05:30", "GMT-01:23", + }; + + static final String[] isoZoneFormats = { + "yyyy-MM-dd'T'HH:mm:ss.SSSX", + "yyyy-MM-dd'T'HH:mm:ss.SSSXX", + "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", + }; + + static final String[][] badData = { + { "X", "1" }, + { "X", "+1" }, + { "X", "-2" }, + { "X", "-24" }, + { "X", "+24" }, + + { "XX", "9" }, + { "XX", "23" }, + { "XX", "234" }, + { "XX", "3456" }, + { "XX", "23456" }, + { "XX", "+1" }, + { "XX", "-12" }, + { "XX", "+123" }, + { "XX", "-12:34" }, + { "XX", "+12:34" }, + { "XX", "-2423" }, + { "XX", "+2423" }, + { "XX", "-1260" }, + { "XX", "+1260" }, + + { "XXX", "9" }, + { "XXX", "23" }, + { "XXX", "234" }, + { "XXX", "3456" }, + { "XXX", "23456" }, + { "XXX", "2:34" }, + { "XXX", "12:4" }, + { "XXX", "12:34" }, + { "XXX", "-1" }, + { "XXX", "+1" }, + { "XXX", "-12" }, + { "XXX", "+12" }, + { "XXX", "-123" }, + { "XXX", "+123" }, + { "XXX", "-1234" }, + { "XXX", "+1234" }, + { "XXX", "+24:23" }, + { "XXX", "+12:60" }, + { "XXX", "+1:23" }, + { "XXX", "+12:3" }, + }; + + static String[] badFormats = { + "XXXX", "XXXXX", "XXXXXX", + }; + + public static void main(String[] args) throws Exception { + TimeZone tz = TimeZone.getDefault(); + + try { + for (int i = 0; i < formatData.length; i++) { + TimeZone.setDefault(TimeZone.getTimeZone(formatData[i][0])); + formatTest(isoZoneFormats[i % isoZoneFormats.length], + formatData[i][1]); + } + + for (String zone : zones) { + TimeZone.setDefault(TimeZone.getTimeZone(zone)); + for (String fmt : isoZoneFormats) { + roundtripTest(fmt); + SimpleDateFormat f = new SimpleDateFormat(fmt); + } + + } + + for (String[] d : badData) { + badDataParsing(d[0], d[1]); + } + + for (String fmt : badFormats) { + badFormat(fmt); + } + } finally { + TimeZone.setDefault(tz); + } + + } + + static void formatTest(String fmt, String expected) throws Exception { + SimpleDateFormat sdf = new SimpleDateFormat(fmt); + String s = sdf.format(TIMESTAMP); + if (!expected.equals(s)) { + throw new RuntimeException("formatTest: got " + s + + ", expected " + expected); + } + + Date d = sdf.parse(s); + if (d.getTime() != TIMESTAMP.getTime()) { + throw new RuntimeException("formatTest: parse(" + s + + "), got " + d.getTime() + + ", expected " + TIMESTAMP.getTime()); + } + + ParsePosition pos = new ParsePosition(0); + d = sdf.parse(s + "123", pos); + if (d.getTime() != TIMESTAMP.getTime()) { + throw new RuntimeException("formatTest: parse(" + s + + "), got " + d.getTime() + + ", expected " + TIMESTAMP.getTime()); + } + if (pos.getIndex() != s.length()) { + throw new RuntimeException("formatTest: wrong resulting parse position: " + + pos.getIndex() + ", expected " + s.length()); + } + } + + static void roundtripTest(String fmt) throws Exception { + SimpleDateFormat sdf = new SimpleDateFormat(fmt); + Date date = new Date(); + + int fractionalHour = sdf.getTimeZone().getOffset(date.getTime()); + fractionalHour %= 3600000; // fraction of hour + + String s = sdf.format(date); + Date pd = sdf.parse(s); + long diffsInMillis = pd.getTime() - date.getTime(); + if (diffsInMillis != 0) { + if (diffsInMillis != fractionalHour) { + throw new RuntimeException("fmt= " + fmt + + ", diff="+diffsInMillis + + ", fraction=" + fractionalHour); + } + } + } + + + static void badDataParsing(String fmt, String text) { + try { + SimpleDateFormat sdf = new SimpleDateFormat(fmt); + sdf.parse(text); + throw new RuntimeException("didn't throw an exception: fmt=" + fmt + + ", text=" + text); + } catch (ParseException e) { + // OK + } + } + + static void badFormat(String fmt) { + try { + SimpleDateFormat sdf = new SimpleDateFormat(fmt); + throw new RuntimeException("Constructor didn't throw an exception: fmt=" + fmt); + } catch (IllegalArgumentException e) { + // OK + } + try { + SimpleDateFormat sdf = new SimpleDateFormat(); + sdf.applyPattern(fmt); + throw new RuntimeException("applyPattern didn't throw an exception: fmt=" + fmt); + } catch (IllegalArgumentException e) { + // OK + } + } +}