4919632: RFE: SimpleDateFormat should fully support ISO8601 standard for timezone
authorokutsu
Thu, 09 Sep 2010 15:37:57 +0900
changeset 6497 22752a2b3413
parent 6496 bfb2f426e9ef
child 6498 cc2bc2a2194a
4919632: RFE: SimpleDateFormat should fully support ISO8601 standard for timezone Reviewed-by: peytoia
jdk/src/share/classes/java/text/DateFormatSymbols.java
jdk/src/share/classes/java/text/SimpleDateFormat.java
jdk/test/java/text/Format/DateFormat/ISO8601ZoneTest.java
--- a/jdk/src/share/classes/java/text/DateFormatSymbols.java	Thu Sep 02 11:13:42 2010 -0700
+++ b/jdk/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/jdk/src/share/classes/java/text/SimpleDateFormat.java	Thu Sep 02 11:13:42 2010 -0700
+++ b/jdk/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/jdk/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
+        }
+    }
+}