changeset 7042:8abd40f9e8ef

Remove early normalization of UTC/UT/GMT prefixes Retain requested prefix and add normalized() See #262
author scolebourne
date Tue, 19 Feb 2013 11:20:24 +0000
parents 5c37c7fd8be2
children d89b0d612e3c
files src/share/classes/java/time/ZoneId.java src/share/classes/java/time/ZoneRegion.java test/java/time/tck/java/time/TCKZoneId.java test/java/time/test/java/time/TestZoneId.java test/java/time/test/java/time/format/TestZoneTextPrinterParser.java
diffstat 5 files changed, 348 insertions(+), 258 deletions(-) [+]
line wrap: on
line diff
--- a/src/share/classes/java/time/ZoneId.java	Tue Feb 19 11:00:57 2013 +0000
+++ b/src/share/classes/java/time/ZoneId.java	Tue Feb 19 11:20:24 2013 +0000
@@ -93,6 +93,8 @@
  *  the offset from UTC/Greenwich apply
  * </ul><p>
  * Most fixed offsets are represented by {@link ZoneOffset}.
+ * Calling {@link #normalized()} on any {@code ZoneId} will ensure that a
+ * fixed offset ID will be represented as a {@code ZoneOffset}.
  * <p>
  * The actual rules, describing when and how the offset changes, are defined by {@link ZoneRules}.
  * This class is simply an ID used to obtain the underlying rules.
@@ -103,28 +105,29 @@
  * the ID, whereas serializing the rules sends the entire data set.
  * Similarly, a comparison of two IDs only examines the ID, whereas
  * a comparison of two rules examines the entire data set.
- * <p>
- * The code supports loading a {@code ZoneId} on a JVM which does not have available rules
- * for that ID. This allows the date-time object, such as {@link ZonedDateTime},
- * to still be queried.
  *
  * <h3>Time-zone IDs</h3>
  * The ID is unique within the system.
- * The formats for offset and region IDs differ.
+ * There are three types of ID.
  * <p>
- * An ID is parsed as an offset ID if it starts with 'UTC', 'GMT', 'UT' '+' or '-', or
- * is a single letter. For example, 'Z', '+02:00', '-05:00', 'UTC+05', 'GMT-6' and
- * 'UT+01:00' are all valid offset IDs.
- * Note that some IDs, such as 'D' or '+ABC' meet the criteria to be parsed as offset IDs,
- * but have an invalid offset.
+ * The simplest type of ID is that from {@code ZoneOffset}.
+ * This consists of 'Z' and IDs starting with '+' or '-'.
  * <p>
- * All other IDs are considered to be region IDs.
+ * The next type of ID are offset-style IDs with some form of prefix,
+ * such as 'GMT+2' or 'UTC+01:00'.
+ * The recognised prefixes are 'UTC', 'GMT' and 'UT'.
+ * The offset is the suffix and will be normalized during creation.
+ * These IDs can be normalized to a {@code ZoneOffset} using {@code normalized()}.
  * <p>
- * Region IDs are defined by configuration, which can be thought of as a {@code Map}
- * from region ID to {@code ZoneRules}, see {@link ZoneRulesProvider}.
+ * The third type of ID are region-based IDs. A region-based ID must be of
+ * two or more characters, and not start with 'UTC', 'GMT', 'UT' '+' or '-'.
+ * Region-based IDs are defined by configuration, see {@link ZoneRulesProvider}.
+ * The configuration focuses on providing the lookup from the ID to the
+ * underlying {@code ZoneRules}.
  * <p>
- * Time-zones are defined by governments and change frequently. There are a number of
- * organizations, known here as groups, that monitor time-zone changes and collate them.
+ * Time-zone rules are defined by governments and change frequently.
+ * There are a number of organizations, known here as groups, that monitor
+ * time-zone changes and collate them.
  * The default group is the IANA Time Zone Database (TZDB).
  * Other organizations include IATA (the airline industry body) and Microsoft.
  * <p>
@@ -139,6 +142,20 @@
  * The recommended format for region IDs from groups other than TZDB is 'group~region'.
  * Thus if IATA data were defined, Utrecht airport would be 'IATA~UTC'.
  *
+ * <h3>Serialization</h3>
+ * This class can be serialized and stores the string zone ID in the external form.
+ * The {@code ZoneOffset} subclass uses a dedicated format that only stores the
+ * offset from UTC/Greenwich.
+ * <p>
+ * A {@code ZoneId} can be deserialized in a Java Runtime where the ID is unknown.
+ * For example, if a server-side Java Runtime has been updated with a new zone ID, but
+ * the client-side Java Runtime has not been updated. In this case, the {@code ZoneId}
+ * object will exist, and can be queried using {@code getId}, {@code equals},
+ * {@code hashCode}, {@code toString}, {@code getDisplayName} and {@code normalized}.
+ * However, any call to {@code getRules} will fail with {@code ZoneRulesException}.
+ * This approach is designed to allow a {@link ZonedDateTime} to be loaded and
+ * queried, but not modified, on a Java Runtime with incomplete time-zone information.
+ *
  * <h3>Specification for implementors</h3>
  * This abstract class has two implementations, both of which are immutable and thread-safe.
  * One implementation models region-based IDs, the other is {@code ZoneOffset} modelling
@@ -310,31 +327,36 @@
      * Obtains an instance of {@code ZoneId} from an ID ensuring that the
      * ID is valid and available for use.
      * <p>
-     * This method parses the ID, applies any appropriate normalization, and validates it
-     * against the known set of IDs for which rules are available.
+     * This method parses the ID producing a {@code ZoneId} or {@code ZoneOffset}.
+     * A {@code ZoneOffset} is returned if the ID is 'Z', or starts with '+' or '-'.
+     * The result will always be a valid ID for which {@link ZoneRules} can be obtained.
      * <p>
-     * An ID is parsed as though it is an offset ID if it starts with 'UTC', 'GMT', 'UT', '+'
-     * or '-', or if it has less then two letters.
-     * The offset of {@linkplain ZoneOffset#UTC zero} may be represented in multiple ways,
-     * including 'Z', 'UTC', 'GMT', 'UT', 'UTC0', 'GMT0', 'UT0', '+00:00', '-00:00' and 'UTC+00:00'.
-     * <p>
-     * Six forms of ID are recognized:
-     * <p><ul>
-     * <li><code>Z</code> - an offset of zero, which is {@code ZoneOffset.UTC}
-     * <li><code>{offset}</code> - a {@code ZoneOffset} ID, such as '+02:00'
-     * <li><code>{utcPrefix}</code> - a {@code ZoneOffset} ID equal to 'Z'
-     * <li><code>{utcPrefix}0</code> - a {@code ZoneOffset} ID equal to 'Z'
-     * <li><code>{utcPrefix}{offset}</code> - a {@code ZoneOffset} ID equal to '{offset}'
-     * <li><code>{regionID}</code> - full region ID, loaded from configuration
-     * </ul><p>
-     * The {offset} is a valid format for {@link ZoneOffset#of(String)}, excluding 'Z'.
-     * The {utcPrefix} is 'UTC', 'GMT' or 'UT'.
-     * Region IDs must match the regular expression <code>[A-Za-z][A-Za-z0-9~/._+-]+</code>.
-     * <p>
-     * The detailed format of the region ID depends on the group supplying the data.
-     * The default set of data is supplied by the IANA Time Zone Database (TZDB)
-     * This has region IDs of the form '{area}/{city}', such as 'Europe/Paris' or 'America/New_York'.
-     * This is compatible with most IDs from {@link java.util.TimeZone}.
+     * Parsing matches the zone ID step by step as follows.
+     * <ul>
+     * <li>If the zone ID equals 'Z', the result is {@code ZoneOffset.UTC}.
+     * <li>If the zone ID consists of a single letter, the zone ID is invalid
+     *  and {@code DateTimeException} is thrown.
+     * <li>If the zone ID starts with '+' or '-', the ID is parsed as a
+     *  {@code ZoneOffset} using {@link ZoneOffset#of(String)}.
+     * <li>If the zone ID equals 'GMT', 'UTC' or 'UT' then the result is a {@code ZoneId}
+     *  with the same ID and rules equivalent to {@code ZoneOffset.UTC}.
+     * <li>If the zone ID starts with 'UTC+', 'UTC-', 'GMT+', 'GMT-', 'UT+' or 'UT-'
+     *  then the ID is a prefixed offset-based ID. The ID is split in two, with
+     *  a two or three letter prefix and a suffix starting with the sign.
+     *  The suffix is parsed as a {@link ZoneOffset#of(String) ZoneOffset}.
+     *  The result will be a {@code ZoneId} with the specified UTC/GMT/UT prefix
+     *  and the normalized offset ID as per {@link ZoneOffset#getId()}.
+     *  The rules of the returned {@code ZoneId} will be equivalent to the
+     *  parsed {@code ZoneOffset}.
+     * <li>All other IDs are parsed as region-based zone IDs. Region IDs must
+     *  match the regular expression <code>[A-Za-z][A-Za-z0-9~/._+-]+</code>
+     *  otherwise a {@code DateTimeException} is thrown. If the zone ID is not
+     *  in the configured set of IDs, {@code ZoneRulesException} is thrown.
+     *  The detailed format of the region ID depends on the group supplying the data.
+     *  The default set of data is supplied by the IANA Time Zone Database (TZDB).
+     *  This has region IDs of the form '{area}/{city}', such as 'Europe/Paris' or 'America/New_York'.
+     *  This is compatible with most IDs from {@link java.util.TimeZone}.
+     * </ul>
      *
      * @param zoneId  the time-zone ID, not null
      * @return the zone ID, not null
@@ -342,15 +364,29 @@
      * @throws ZoneRulesException if the zone ID is a region ID that cannot be found
      */
     public static ZoneId of(String zoneId) {
+        return of(zoneId, true);
+    }
+
+    /**
+     * Parses the ID, taking a flag to indicate whether {@code ZoneRulesException}
+     * should be thrown or not, used in deserialization.
+     *
+     * @param zoneId  the time-zone ID, not null
+     * @param checkAvailable  whether to check if the zone ID is available
+     * @return the zone ID, not null
+     * @throws DateTimeException if the ID format is invalid
+     * @throws ZoneRulesException if checking availability and the ID cannot be found
+     */
+    static ZoneId of(String zoneId, boolean checkAvailable) {
         Objects.requireNonNull(zoneId, "zoneId");
         if (zoneId.length() <= 1 || zoneId.startsWith("+") || zoneId.startsWith("-")) {
             return ZoneOffset.of(zoneId);
         } else if (zoneId.startsWith("UTC") || zoneId.startsWith("GMT")) {
-            return ofWithPrefix(zoneId, 3);
+            return ofWithPrefix(zoneId, 3, checkAvailable);
         } else if (zoneId.startsWith("UT")) {
-            return ofWithPrefix(zoneId, 2);
+            return ofWithPrefix(zoneId, 2, checkAvailable);
         }
-        return ZoneRegion.ofId(zoneId, true);
+        return ZoneRegion.ofId(zoneId, checkAvailable);
     }
 
     /**
@@ -359,22 +395,25 @@
      * @param zoneId  the time-zone ID, not null
      * @param prefixLength  the length of the prefix, 2 or 3
      * @return the zone ID, not null
-     * @return the zone ID, not null
      * @throws DateTimeException if the zone ID has an invalid format
      */
-    private static ZoneId ofWithPrefix(String zoneId, int prefixLength) {
-        if (zoneId.length() == prefixLength ||
-                (zoneId.length() == prefixLength + 1 && zoneId.charAt(prefixLength) == '0')) {
-            return ZoneOffset.UTC;
+    private static ZoneId ofWithPrefix(String zoneId, int prefixLength, boolean checkAvailable) {
+        String prefix = zoneId.substring(0, prefixLength);
+        if (zoneId.length() == prefixLength) {
+            return ZoneRegion.ofPrefixedOffset(prefix, ZoneOffset.UTC);
         }
-        if (zoneId.charAt(prefixLength) == '+' || zoneId.charAt(prefixLength) == '-') {
-            try {
-                return ZoneOffset.of(zoneId.substring(prefixLength));
-            } catch (DateTimeException ex) {
-                throw new DateTimeException("Invalid ID for offset-based ZoneId: " + zoneId, ex);
+        if (zoneId.charAt(prefixLength) != '+' && zoneId.charAt(prefixLength) != '-') {
+            return ZoneRegion.ofId(zoneId, checkAvailable);  // drop through to ZoneRulesProvider
+        }
+        try {
+            ZoneOffset offset = ZoneOffset.of(zoneId.substring(prefixLength));
+            if (offset == ZoneOffset.UTC) {
+                return ZoneRegion.ofPrefixedOffset(prefix, offset);
             }
+            return ZoneRegion.ofPrefixedOffset(prefix + offset.toString(), offset);
+        } catch (DateTimeException ex) {
+            throw new DateTimeException("Invalid ID for offset-based ZoneId: " + zoneId, ex);
         }
-        throw new DateTimeException("Invalid ID for offset-based ZoneId: " + zoneId);
     }
 
     //-----------------------------------------------------------------------
@@ -429,29 +468,6 @@
 
     //-----------------------------------------------------------------------
     /**
-     * Gets the time-zone rules for this ID allowing calculations to be performed.
-     * <p>
-     * The rules provide the functionality associated with a time-zone,
-     * such as finding the offset for a given instant or local date-time.
-     * <p>
-     * A time-zone can be invalid if it is deserialized in a JVM which does not
-     * have the same rules loaded as the JVM that stored it. In this case, calling
-     * this method will throw an exception.
-     * <p>
-     * The rules are supplied by {@link ZoneRulesProvider}. An advanced provider may
-     * support dynamic updates to the rules without restarting the JVM.
-     * If so, then the result of this method may change over time.
-     * Each individual call will be still remain thread-safe.
-     * <p>
-     * {@link ZoneOffset} will always return a set of rules where the offset never changes.
-     *
-     * @return the rules, not null
-     * @throws ZoneRulesException if no rules are available for this ID
-     */
-    public abstract ZoneRules getRules();
-
-    //-----------------------------------------------------------------------
-    /**
      * Gets the textual representation of the zone, such as 'British Time' or
      * '+02:00'.
      * <p>
@@ -488,6 +504,54 @@
 
     //-----------------------------------------------------------------------
     /**
+     * Gets the time-zone rules for this ID allowing calculations to be performed.
+     * <p>
+     * The rules provide the functionality associated with a time-zone,
+     * such as finding the offset for a given instant or local date-time.
+     * <p>
+     * A time-zone can be invalid if it is deserialized in a Java Runtime which
+     * does not have the same rules loaded as the Java Runtime that stored it.
+     * In this case, calling this method will throw a {@code ZoneRulesException}.
+     * <p>
+     * The rules are supplied by {@link ZoneRulesProvider}. An advanced provider may
+     * support dynamic updates to the rules without restarting the Java Runtime.
+     * If so, then the result of this method may change over time.
+     * Each individual call will be still remain thread-safe.
+     * <p>
+     * {@link ZoneOffset} will always return a set of rules where the offset never changes.
+     *
+     * @return the rules, not null
+     * @throws ZoneRulesException if no rules are available for this ID
+     */
+    public abstract ZoneRules getRules();
+
+    /**
+     * Normalizes the time-zone ID, returning a {@code ZoneOffset} where possible.
+     * <p>
+     * The returns a normalized {@code ZoneId} that can be used in place of this ID.
+     * The result will have {@code ZoneRules} equivalent to those returned by this object,
+     * however the ID returned by {@code getId()} may be different.
+     * <p>
+     * The normalization checks if the rules of this {@code ZoneId} have a fixed offset.
+     * If they do, then the {@code ZoneOffset} equal to that offset is returned.
+     * Otherwise {@code this} is returned.
+     *
+     * @return the time-zone unique ID, not null
+     */
+    public ZoneId normalized() {
+        try {
+            ZoneRules rules = getRules();
+            if (rules.isFixedOffset()) {
+                return rules.getOffset(Instant.EPOCH);
+            }
+        } catch (ZoneRulesException ex) {
+            // invalid ZoneRegion is not important to this method
+        }
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
      * Checks if this time-zone ID is equal to another time-zone ID.
      * <p>
      * The comparison is based on the ID.
@@ -536,6 +600,10 @@
      *  out.writeByte(7);  // identifies this as a ZoneId (not ZoneOffset)
      *  out.writeUTF(zoneId);
      * </pre>
+     * <p>
+     * When read back in, the {@code ZoneId} will be created as though using
+     * {@link #of(String)}, but without any exception in the case where the
+     * ID has a valid format, but is not in the known set of region-based IDs.
      *
      * @return the instance of {@code Ser}, not null
      */
--- a/src/share/classes/java/time/ZoneRegion.java	Tue Feb 19 11:00:57 2013 +0000
+++ b/src/share/classes/java/time/ZoneRegion.java	Tue Feb 19 11:20:24 2013 +0000
@@ -109,26 +109,6 @@
     private final transient ZoneRules rules;
 
     /**
-     * Obtains an instance of {@code ZoneRegion} from an identifier without checking
-     * if the time-zone has available rules.
-     * <p>
-     * This method parses the ID and applies any appropriate normalization.
-     * It does not validate the ID against the known set of IDs for which rules are available.
-     * <p>
-     * This method is intended for advanced use cases.
-     * For example, consider a system that always retrieves time-zone rules from a remote server.
-     * Using this factory would allow a {@code ZoneRegion}, and thus a {@code ZonedDateTime},
-     * to be created without loading the rules from the remote server.
-     *
-     * @param zoneId  the time-zone ID, not null
-     * @return the zone ID, not null
-     * @throws DateTimeException if the ID format is invalid
-     */
-    private static ZoneRegion ofLenient(String zoneId) {
-        return ofId(zoneId, false);
-    }
-
-    /**
      * Obtains an instance of {@code ZoneId} from an identifier.
      *
      * @param zoneId  the time-zone ID, not null
@@ -139,10 +119,7 @@
      */
     static ZoneRegion ofId(String zoneId, boolean checkAvailable) {
         Objects.requireNonNull(zoneId, "zoneId");
-        if (zoneId.length() < 2 ||
-                zoneId.startsWith("UT") ||  // includes UTC
-                zoneId.startsWith("GMT") ||
-                (PATTERN.matcher(zoneId).matches() == false)) {
+        if (zoneId.length() < 2 || PATTERN.matcher(zoneId).matches() == false) {
             throw new DateTimeException("Invalid ID for region-based ZoneId, invalid format: " + zoneId);
         }
         ZoneRules rules = null;
@@ -157,6 +134,19 @@
         return new ZoneRegion(zoneId, rules);
     }
 
+    /**
+     * Obtains an instance of {@code ZoneId} wrapping an offset.
+     * <p>
+     * For example, zone IDs like 'UTC', 'GMT', 'UT' and 'UTC+01:30' will be setup here.
+     *
+     * @param zoneId  the time-zone ID, not null
+     * @param offset  the offset, not null
+     * @return the zone ID, not null
+     */
+    static ZoneRegion ofPrefixedOffset(String zoneId, ZoneOffset offset) {
+        return new ZoneRegion(zoneId, offset.getRules());
+    }
+
     //-------------------------------------------------------------------------
     /**
      * Constructor.
@@ -218,7 +208,7 @@
 
     static ZoneId readExternal(DataInput in) throws IOException {
         String id = in.readUTF();
-        return ofLenient(id);
+        return ZoneId.of(id, false);
     }
 
 }
--- a/test/java/time/tck/java/time/TCKZoneId.java	Tue Feb 19 11:00:57 2013 +0000
+++ b/test/java/time/tck/java/time/TCKZoneId.java	Tue Feb 19 11:20:24 2013 +0000
@@ -64,22 +64,23 @@
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
-import java.io.DataInputStream;
 import java.io.DataOutputStream;
-import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectStreamConstants;
 import java.lang.reflect.Field;
 import java.time.DateTimeException;
+import java.time.Instant;
 import java.time.LocalTime;
 import java.time.ZoneId;
 import java.time.ZoneOffset;
+import java.time.format.TextStyle;
 import java.time.temporal.Queries;
 import java.time.temporal.TemporalAccessor;
 import java.time.temporal.TemporalField;
 import java.time.temporal.TemporalQuery;
 import java.time.zone.ZoneRulesException;
 import java.util.HashMap;
+import java.util.Locale;
 import java.util.Map;
 
 import org.testng.annotations.DataProvider;
@@ -114,9 +115,12 @@
         // an ID can be loaded without validation during deserialization
         String id = "QWERTYUIOPASDFGHJKLZXCVBNM~/._+-";
         ZoneId deser = deserialize(id);
-        // getting the ID and string are OK
+        // getId, equals, hashCode, toString and normalized are OK
         assertEquals(deser.getId(), id);
         assertEquals(deser.toString(), id);
+        assertEquals(deser, deser);
+        assertEquals(deser.hashCode(), deser.hashCode());
+        assertEquals(deser.normalized(), deser);
         // getting the rules is not
         try {
             deser.getRules();
@@ -133,32 +137,32 @@
         deserialize("|!?");
     }
 
-    @Test(dataProvider="offsetBasedValid", expectedExceptions=DateTimeException.class)
+    @Test(dataProvider="offsetBasedValid")
     public void test_deserialization_lenient_offsetNotAllowed_noPrefix(String input, String resolvedId) throws Exception {
-        // an ID can be loaded without validation during deserialization
-        // but there is a check to ensure the ID format is valid
-        deserialize(input);
+        ZoneId deserialized = deserialize(input);
+        assertEquals(deserialized, ZoneId.of(input));
+        assertEquals(deserialized, ZoneId.of(resolvedId));
     }
 
-    @Test(dataProvider="offsetBasedValid", expectedExceptions=DateTimeException.class)
-    public void test_deserialization_lenient_offsetNotAllowed_prefixUTC(String input, String resolvedId) throws Exception {
-        // an ID can be loaded without validation during deserialization
-        // but there is a check to ensure the ID format is valid
-        deserialize("UTC" + input);
+    @Test(dataProvider="offsetBasedValidPrefix")
+    public void test_deserialization_lenient_offsetNotAllowed_prefixUTC(String input, String resolvedId, String offsetId) throws Exception {
+        ZoneId deserialized = deserialize("UTC" + input);
+        assertEquals(deserialized, ZoneId.of("UTC" + input));
+        assertEquals(deserialized, ZoneId.of("UTC" + resolvedId));
     }
 
-    @Test(dataProvider="offsetBasedValid", expectedExceptions=DateTimeException.class)
-    public void test_deserialization_lenient_offsetNotAllowed_prefixGMT(String input, String resolvedId) throws Exception {
-        // an ID can be loaded without validation during deserialization
-        // but there is a check to ensure the ID format is valid
-        deserialize("GMT" + input);
+    @Test(dataProvider="offsetBasedValidPrefix")
+    public void test_deserialization_lenient_offsetNotAllowed_prefixGMT(String input, String resolvedId, String offsetId) throws Exception {
+        ZoneId deserialized = deserialize("GMT" + input);
+        assertEquals(deserialized, ZoneId.of("GMT" + input));
+        assertEquals(deserialized, ZoneId.of("GMT" + resolvedId));
     }
 
-    @Test(dataProvider="offsetBasedValid", expectedExceptions=DateTimeException.class)
-    public void test_deserialization_lenient_offsetNotAllowed_prefixUT(String input, String resolvedId) throws Exception {
-        // an ID can be loaded without validation during deserialization
-        // but there is a check to ensure the ID format is valid
-        deserialize("UT" + input);
+    @Test(dataProvider="offsetBasedValidPrefix")
+    public void test_deserialization_lenient_offsetNotAllowed_prefixUT(String input, String resolvedId, String offsetId) throws Exception {
+        ZoneId deserialized = deserialize("UT" + input);
+        assertEquals(deserialized, ZoneId.of("UT" + input));
+        assertEquals(deserialized, ZoneId.of("UT" + resolvedId));
     }
 
     private ZoneId deserialize(String id) throws Exception {
@@ -315,65 +319,41 @@
     }
 
     //-----------------------------------------------------------------------
-    // regular factory
-    //-----------------------------------------------------------------------
-    @DataProvider(name="offsetBasedZero")
-    Object[][] data_offsetBasedZero() {
-        return new Object[][] {
-                {""}, {"0"},
-                {"+00"},{"+0000"},{"+00:00"},{"+000000"},{"+00:00:00"},
-                {"-00"},{"-0000"},{"-00:00"},{"-000000"},{"-00:00:00"},
-        };
-    }
-
-    @Test(dataProvider="offsetBasedZero")
-    public void factory_of_String_offsetBasedZero_noPrefix(String id) {
-        if (id.length() > 0 && id.equals("0") == false) {
-            ZoneId test = ZoneId.of(id);
-            assertEquals(test, ZoneOffset.UTC);
-        }
-    }
-
-    @Test(dataProvider="offsetBasedZero")
-    public void factory_of_String_offsetBasedZero_prefixUTC(String id) {
-        ZoneId test = ZoneId.of("UTC" + id);
-        assertEquals(test, ZoneOffset.UTC);
-    }
-
-    @Test(dataProvider="offsetBasedZero")
-    public void factory_of_String_offsetBasedZero_prefixGMT(String id) {
-        ZoneId test = ZoneId.of("GMT" + id);
-        assertEquals(test, ZoneOffset.UTC);
-    }
-
-    @Test(dataProvider="offsetBasedZero")
-    public void factory_of_String_offsetBasedZero_prefixUT(String id) {
-        ZoneId test = ZoneId.of("UT" + id);
-        assertEquals(test, ZoneOffset.UTC);
-    }
-
-    @Test
-    public void factory_of_String_offsetBasedZero_z() {
-        ZoneId test = ZoneId.of("Z");
-        assertEquals(test, ZoneOffset.UTC);
-    }
-
+    // regular factory and .normalized()
     //-----------------------------------------------------------------------
     @DataProvider(name="offsetBasedValid")
     Object[][] data_offsetBasedValid() {
         return new Object[][] {
+                {"Z", "Z"},
                 {"+0", "Z"},
+                {"-0", "Z"},
+                {"+00", "Z"},
+                {"+0000", "Z"},
+                {"+00:00", "Z"},
+                {"+000000", "Z"},
+                {"+00:00:00", "Z"},
+                {"-00", "Z"},
+                {"-0000", "Z"},
+                {"-00:00", "Z"},
+                {"-000000", "Z"},
+                {"-00:00:00", "Z"},
                 {"+5", "+05:00"},
                 {"+01", "+01:00"},
-                {"+0100", "+01:00"},{"+01:00", "+01:00"},
-                {"+010000", "+01:00"},{"+01:00:00", "+01:00"},
+                {"+0100", "+01:00"},
+                {"+01:00", "+01:00"},
+                {"+010000", "+01:00"},
+                {"+01:00:00", "+01:00"},
                 {"+12", "+12:00"},
-                {"+1234", "+12:34"},{"+12:34", "+12:34"},
-                {"+123456", "+12:34:56"},{"+12:34:56", "+12:34:56"},
+                {"+1234", "+12:34"},
+                {"+12:34", "+12:34"},
+                {"+123456", "+12:34:56"},
+                {"+12:34:56", "+12:34:56"},
                 {"-02", "-02:00"},
                 {"-5", "-05:00"},
-                {"-0200", "-02:00"},{"-02:00", "-02:00"},
-                {"-020000", "-02:00"},{"-02:00:00", "-02:00"},
+                {"-0200", "-02:00"},
+                {"-02:00", "-02:00"},
+                {"-020000", "-02:00"},
+                {"-02:00:00", "-02:00"},
         };
     }
 
@@ -382,27 +362,126 @@
         ZoneId test = ZoneId.of(input);
         assertEquals(test.getId(), id);
         assertEquals(test, ZoneOffset.of(id));
+        assertEquals(test.normalized(), ZoneOffset.of(id));
+        assertEquals(test.getDisplayName(TextStyle.FULL, Locale.UK), id);
+        assertEquals(test.getRules().isFixedOffset(), true);
+        assertEquals(test.getRules().getOffset(Instant.EPOCH), ZoneOffset.of(id));
     }
 
-    @Test(dataProvider="offsetBasedValid")
-    public void factory_of_String_offsetBasedValid_prefixUTC(String input, String id) {
-        ZoneId test = ZoneId.of("UTC" + input);
-        assertEquals(test.getId(), id);
-        assertEquals(test, ZoneOffset.of(id));
+    //-----------------------------------------------------------------------
+    @DataProvider(name="offsetBasedValidPrefix")
+    Object[][] data_offsetBasedValidPrefix() {
+        return new Object[][] {
+                {"", "", "Z"},
+                {"+0", "", "Z"},
+                {"-0", "", "Z"},
+                {"+00", "", "Z"},
+                {"+0000", "", "Z"},
+                {"+00:00", "", "Z"},
+                {"+000000", "", "Z"},
+                {"+00:00:00", "", "Z"},
+                {"-00", "", "Z"},
+                {"-0000", "", "Z"},
+                {"-00:00", "", "Z"},
+                {"-000000", "", "Z"},
+                {"-00:00:00", "", "Z"},
+                {"+5", "+05:00", "+05:00"},
+                {"+01", "+01:00", "+01:00"},
+                {"+0100", "+01:00", "+01:00"},
+                {"+01:00", "+01:00", "+01:00"},
+                {"+010000", "+01:00", "+01:00"},
+                {"+01:00:00", "+01:00", "+01:00"},
+                {"+12", "+12:00", "+12:00"},
+                {"+1234", "+12:34", "+12:34"},
+                {"+12:34", "+12:34", "+12:34"},
+                {"+123456", "+12:34:56", "+12:34:56"},
+                {"+12:34:56", "+12:34:56", "+12:34:56"},
+                {"-02", "-02:00", "-02:00"},
+                {"-5", "-05:00", "-05:00"},
+                {"-0200", "-02:00", "-02:00"},
+                {"-02:00", "-02:00", "-02:00"},
+                {"-020000", "-02:00", "-02:00"},
+                {"-02:00:00", "-02:00", "-02:00"},
+        };
     }
 
-    @Test(dataProvider="offsetBasedValid")
-    public void factory_of_String_offsetBasedValid_prefixGMT(String input, String id) {
-        ZoneId test = ZoneId.of("GMT" + input);
-        assertEquals(test.getId(), id);
-        assertEquals(test, ZoneOffset.of(id));
+    @Test(dataProvider="offsetBasedValidPrefix")
+    public void factory_of_String_offsetBasedValid_prefixUTC(String input, String id, String offsetId) {
+        ZoneId test = ZoneId.of("UTC" + input);
+        assertEquals(test.getId(), "UTC" + id);
+        assertEquals(test.getRules(), ZoneOffset.of(offsetId).getRules());
+        assertEquals(test.normalized(), ZoneOffset.of(offsetId));
+        assertEquals(test.getDisplayName(TextStyle.FULL, Locale.UK), displayName("UTC" + id));
+        assertEquals(test.getRules().isFixedOffset(), true);
+        assertEquals(test.getRules().getOffset(Instant.EPOCH), ZoneOffset.of(offsetId));
     }
 
-    @Test(dataProvider="offsetBasedValid")
-    public void factory_of_String_offsetBasedValid_prefixUT(String input, String id) {
+    @Test(dataProvider="offsetBasedValidPrefix")
+    public void factory_of_String_offsetBasedValid_prefixGMT(String input, String id, String offsetId) {
+        ZoneId test = ZoneId.of("GMT" + input);
+        assertEquals(test.getId(), "GMT" + id);
+        assertEquals(test.getRules(), ZoneOffset.of(offsetId).getRules());
+        assertEquals(test.normalized(), ZoneOffset.of(offsetId));
+        assertEquals(test.getDisplayName(TextStyle.FULL, Locale.UK), displayName("GMT" + id));
+        assertEquals(test.getRules().isFixedOffset(), true);
+        assertEquals(test.getRules().getOffset(Instant.EPOCH), ZoneOffset.of(offsetId));
+    }
+
+    @Test(dataProvider="offsetBasedValidPrefix")
+    public void factory_of_String_offsetBasedValid_prefixUT(String input, String id, String offsetId) {
         ZoneId test = ZoneId.of("UT" + input);
-        assertEquals(test.getId(), id);
-        assertEquals(test, ZoneOffset.of(id));
+        assertEquals(test.getId(), "UT" + id);
+        assertEquals(test.getRules(), ZoneOffset.of(offsetId).getRules());
+        assertEquals(test.normalized(), ZoneOffset.of(offsetId));
+        assertEquals(test.getDisplayName(TextStyle.FULL, Locale.UK), displayName("UT" + id));
+        assertEquals(test.getRules().isFixedOffset(), true);
+        assertEquals(test.getRules().getOffset(Instant.EPOCH), ZoneOffset.of(offsetId));
+    }
+
+    private String displayName(String id) {
+        if (id.equals("GMT")) {
+            return "Greenwich Mean Time";
+        }
+        if (id.equals("GMT0")) {
+            return "Greenwich Mean Time";
+        }
+        if (id.equals("UTC")) {
+            return "Coordinated Universal Time";
+        }
+        return id;
+    }
+
+    //-----------------------------------------------------------------------
+    @DataProvider(name="offsetBasedValidOther")
+    Object[][] data_offsetBasedValidOther() {
+        return new Object[][] {
+                {"GMT", "Z"},
+                {"GMT0", "Z"},
+                {"UCT", "Z"},
+                {"Greenwich", "Z"},
+                {"Universal", "Z"},
+                {"Zulu", "Z"},
+                {"Etc/GMT", "Z"},
+                {"Etc/GMT+0", "Z"},
+                {"Etc/GMT+1", "-01:00"},
+                {"Etc/GMT-1", "+01:00"},
+                {"Etc/GMT+9", "-09:00"},
+                {"Etc/GMT-9", "+09:00"},
+                {"Etc/GMT0", "Z"},
+                {"Etc/UCT", "Z"},
+                {"Etc/UTC", "Z"},
+                {"Etc/Greenwich", "Z"},
+                {"Etc/Universal", "Z"},
+                {"Etc/Zulu", "Z"},
+        };
+    }
+
+    @Test(dataProvider="offsetBasedValidOther")
+    public void factory_of_String_offsetBasedValidOther(String input, String offsetId) {
+        ZoneId test = ZoneId.of(input);
+        assertEquals(test.getId(), input);
+        assertEquals(test.getRules(), ZoneOffset.of(offsetId).getRules());
+        assertEquals(test.normalized(), ZoneOffset.of(offsetId));
     }
 
     //-----------------------------------------------------------------------
@@ -422,6 +501,12 @@
                 {"-19"}, {"-19:00"}, {"-18:01"}, {"-18:00:01"}, {"-1801"}, {"-180001"},
                 {"-01_00"}, {"-01;00"}, {"-01@00"}, {"-01:AA"},
                 {"@01:00"},
+                {"0"},
+                {"UT0"},
+                {"UTZ"},
+                {"UTC0"},
+                {"UTCZ"},
+                {"GMTZ"},  // GMT0 is valid in ZoneRulesProvider
         };
     }
 
@@ -440,6 +525,9 @@
 
     @Test(dataProvider="offsetBasedInvalid", expectedExceptions=DateTimeException.class)
     public void factory_of_String_offsetBasedInvalid_prefixGMT(String id) {
+        if (id.equals("0")) {
+            throw new DateTimeException("Fake exception: GMT0 is valid, not invalid");
+        }
         ZoneId.of("GMT" + id);
     }
 
@@ -479,6 +567,7 @@
         ZoneId test = ZoneId.of("Europe/London");
         assertEquals(test.getId(), "Europe/London");
         assertEquals(test.getRules().isFixedOffset(), false);
+        assertEquals(test.normalized(), test);
     }
 
     //-----------------------------------------------------------------------
@@ -578,8 +667,10 @@
                 {"Europe/London", "Europe/London"},
                 {"Europe/Paris", "Europe/Paris"},
                 {"Europe/Berlin", "Europe/Berlin"},
-                {"UTC", "Z"},
-                {"UTC+01:00", "+01:00"},
+                {"Z", "Z"},
+                {"+01:00", "+01:00"},
+                {"UTC", "UTC"},
+                {"UTC+01:00", "UTC+01:00"},
         };
     }
 
--- a/test/java/time/test/java/time/TestZoneId.java	Tue Feb 19 11:00:57 2013 +0000
+++ b/test/java/time/test/java/time/TestZoneId.java	Tue Feb 19 11:20:24 2013 +0000
@@ -62,7 +62,6 @@
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertNotNull;
-import static org.testng.Assert.assertSame;
 import static org.testng.Assert.assertTrue;
 
 import java.lang.reflect.Field;
@@ -82,7 +81,6 @@
 import java.util.SimpleTimeZone;
 import java.util.TimeZone;
 
-import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
 /**
@@ -121,7 +119,6 @@
         assertEquals(test.getRules().isFixedOffset(), true);
         assertEquals(test.getRules().getOffset(Instant.ofEpochSecond(0L)), ZoneOffset.UTC);
         checkOffset(test.getRules(), createLDT(2008, 6, 30), ZoneOffset.UTC, 1);
-        assertSame(test, ZoneId.of("UTC+00"));
     }
 
     //-----------------------------------------------------------------------
@@ -129,9 +126,7 @@
     //-----------------------------------------------------------------------
     public void test_systemDefault() {
         ZoneId test = ZoneId.systemDefault();
-        assertEquals(test.getId(), TimeZone.getDefault()
-                                           .getID()
-                                           .replaceAll("GMT|UTC|UT", "Z"));
+        assertEquals(test.getId(), TimeZone.getDefault().getID());
     }
 
     @Test(expectedExceptions = DateTimeException.class)
@@ -157,58 +152,6 @@
     }
 
     //-----------------------------------------------------------------------
-    @DataProvider(name="String_Fixed")
-    Object[][] data_of_string_Fixed() {
-        return new Object[][] {
-            {"+0", "Z"},
-            {"+5", "+05:00"},
-            {"+01", "+01:00"},
-            {"+0100", "+01:00"},{"+01:00", "+01:00"},
-            {"+010000", "+01:00"},{"+01:00:00", "+01:00"},
-            {"+12", "+12:00"},
-            {"+1234", "+12:34"},{"+12:34", "+12:34"},
-            {"+123456", "+12:34:56"},{"+12:34:56", "+12:34:56"},
-            {"-02", "-02:00"},
-            {"-5", "-05:00"},
-            {"-0200", "-02:00"},{"-02:00", "-02:00"},
-            {"-020000", "-02:00"},{"-02:00:00", "-02:00"},
-        };
-    }
-
-    @Test(dataProvider="String_Fixed")
-    public void test_of_string_offset(String input, String id) {
-        ZoneId test = ZoneId.of(input);
-        assertEquals(test.getId(), id);
-        assertEquals(test.getDisplayName(TextStyle.FULL, Locale.UK), id);
-        assertEquals(test.getRules().isFixedOffset(), true);
-        ZoneOffset offset = ZoneOffset.of(id);
-        assertEquals(test.getRules().getOffset(Instant.ofEpochSecond(0L)), offset);
-        checkOffset(test.getRules(), createLDT(2008, 6, 30), offset, 1);
-    }
-
-    @Test(dataProvider="String_Fixed")
-    public void test_of_string_FixedUTC(String input, String id) {
-        ZoneId test = ZoneId.of("UTC" + input);
-        assertEquals(test.getId(), id);
-        assertEquals(test.getDisplayName(TextStyle.FULL, Locale.UK), id);
-        assertEquals(test.getRules().isFixedOffset(), true);
-        ZoneOffset offset = ZoneOffset.of(id);
-        assertEquals(test.getRules().getOffset(Instant.ofEpochSecond(0L)), offset);
-        checkOffset(test.getRules(), createLDT(2008, 6, 30), offset, 1);
-    }
-
-    @Test(dataProvider="String_Fixed")
-    public void test_of_string_FixedGMT(String input, String id) {
-        ZoneId test = ZoneId.of("GMT" + input);
-        assertEquals(test.getId(), id);
-        assertEquals(test.getDisplayName(TextStyle.FULL, Locale.UK), id);
-        assertEquals(test.getRules().isFixedOffset(), true);
-        ZoneOffset offset = ZoneOffset.of(id);
-        assertEquals(test.getRules().getOffset(Instant.ofEpochSecond(0L)), offset);
-        checkOffset(test.getRules(), createLDT(2008, 6, 30), offset, 1);
-    }
-
-    //-----------------------------------------------------------------------
     // Europe/London
     //-----------------------------------------------------------------------
     public void test_London() {
--- a/test/java/time/test/java/time/format/TestZoneTextPrinterParser.java	Tue Feb 19 11:00:57 2013 +0000
+++ b/test/java/time/test/java/time/format/TestZoneTextPrinterParser.java	Tue Feb 19 11:20:24 2013 +0000
@@ -41,6 +41,7 @@
 import java.time.format.DateTimeFormatterBuilder;
 import java.time.format.TextStyle;
 import java.time.zone.ZoneRulesProvider;
+import java.util.TreeSet;
 
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
@@ -49,7 +50,7 @@
 /**
  * Test ZoneTextPrinterParser
  */
-@Test(groups={"implementation"})
+@Test
 public class TestZoneTextPrinterParser extends AbstractTestPrinterParser {
 
     protected static DateTimeFormatter getFormatter(Locale locale, TextStyle style) {
@@ -70,26 +71,23 @@
             zdt = zdt.withDayOfYear(r.nextInt(365) + 1)
                      .with(ChronoField.SECOND_OF_DAY, r.nextInt(86400));
             for (String zid : zids) {
-                if (zid.equals("ROC") ||
-                    zid.startsWith("UTC") ||
-                    zid.startsWith("GMT") || zid.startsWith("Etc/GMT")) {
-                    // UTC, GMT are treated as zone offset
+                if (zid.equals("ROC") || zid.startsWith("Etc/GMT")) {
                     continue;      // TBD: match jdk behavior?
                 }
                 zdt = zdt.withZoneSameLocal(ZoneId.of(zid));
                 TimeZone tz = TimeZone.getTimeZone(zid);
                 boolean isDST = tz.inDaylightTime(new Date(zdt.toInstant().toEpochMilli()));
                 for (Locale locale : locales) {
-                    printText(locale, zdt, TextStyle.FULL,
-                              tz.getDisplayName(isDST, TimeZone.LONG, locale));
-                    printText(locale, zdt, TextStyle.SHORT,
-                              tz.getDisplayName(isDST, TimeZone.SHORT, locale));
+                    printText(locale, zdt, TextStyle.FULL, tz,
+                            tz.getDisplayName(isDST, TimeZone.LONG, locale));
+                    printText(locale, zdt, TextStyle.SHORT, tz,
+                            tz.getDisplayName(isDST, TimeZone.SHORT, locale));
                 }
             }
         }
     }
 
-    private void printText(Locale locale, ZonedDateTime zdt, TextStyle style, String expected) {
+    private void printText(Locale locale, ZonedDateTime zdt, TextStyle style, TimeZone zone, String expected) {
         String result = getFormatter(locale, style).format(zdt);
         if (!result.equals(expected)) {
             if (result.equals("FooLocation")) { // from rules provider test if same vm
@@ -97,8 +95,8 @@
             }
             System.out.println("----------------");
             System.out.printf("tdz[%s]%n", zdt.toString());
-            System.out.printf("[%-4s, %5s] :[%s]%n", locale.toString(), style.toString(),result);
-            System.out.printf("%4s, %5s  :[%s]%n", "", "", expected);
+            System.out.printf("[%-5s, %5s] :[%s]%n", locale.toString(), style.toString(),result);
+            System.out.printf(" %5s, %5s  :[%s] %s%n", "", "", expected, zone);
         }
         assertEquals(result, expected);
     }