• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
3  *
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are met:
8  *
9  *  * Redistributions of source code must retain the above copyright notice,
10  *    this list of conditions and the following disclaimer.
11  *
12  *  * Redistributions in binary form must reproduce the above copyright notice,
13  *    this list of conditions and the following disclaimer in the documentation
14  *    and/or other materials provided with the distribution.
15  *
16  *  * Neither the name of JSR-310 nor the names of its contributors
17  *    may be used to endorse or promote products derived from this software
18  *    without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  */
32 package org.threeten.bp;
33 
34 import java.io.DataOutput;
35 import java.io.IOException;
36 import java.io.Serializable;
37 import java.util.Collections;
38 import java.util.HashMap;
39 import java.util.HashSet;
40 import java.util.Locale;
41 import java.util.Map;
42 import java.util.Set;
43 import java.util.TimeZone;
44 
45 import org.threeten.bp.format.DateTimeFormatterBuilder;
46 import org.threeten.bp.format.TextStyle;
47 import org.threeten.bp.jdk8.DefaultInterfaceTemporalAccessor;
48 import org.threeten.bp.jdk8.Jdk8Methods;
49 import org.threeten.bp.temporal.TemporalAccessor;
50 import org.threeten.bp.temporal.TemporalField;
51 import org.threeten.bp.temporal.TemporalQueries;
52 import org.threeten.bp.temporal.TemporalQuery;
53 import org.threeten.bp.temporal.UnsupportedTemporalTypeException;
54 import org.threeten.bp.zone.ZoneRules;
55 import org.threeten.bp.zone.ZoneRulesException;
56 import org.threeten.bp.zone.ZoneRulesProvider;
57 
58 /**
59  * A time-zone ID, such as {@code Europe/Paris}.
60  * <p>
61  * A {@code ZoneId} is used to identify the rules used to convert between
62  * an {@link Instant} and a {@link LocalDateTime}.
63  * There are two distinct types of ID:
64  * <ul>
65  * <li>Fixed offsets - a fully resolved offset from UTC/Greenwich, that uses
66  *  the same offset for all local date-times
67  * <li>Geographical regions - an area where a specific set of rules for finding
68  *  the offset from UTC/Greenwich apply
69  * </ul>
70  * Most fixed offsets are represented by {@link ZoneOffset}.
71  * Calling {@link #normalized()} on any {@code ZoneId} will ensure that a
72  * fixed offset ID will be represented as a {@code ZoneOffset}.
73  * <p>
74  * The actual rules, describing when and how the offset changes, are defined by {@link ZoneRules}.
75  * This class is simply an ID used to obtain the underlying rules.
76  * This approach is taken because rules are defined by governments and change
77  * frequently, whereas the ID is stable.
78  * <p>
79  * The distinction has other effects. Serializing the {@code ZoneId} will only send
80  * the ID, whereas serializing the rules sends the entire data set.
81  * Similarly, a comparison of two IDs only examines the ID, whereas
82  * a comparison of two rules examines the entire data set.
83  *
84  * <h3>Time-zone IDs</h3>
85  * The ID is unique within the system.
86  * There are three types of ID.
87  * <p>
88  * The simplest type of ID is that from {@code ZoneOffset}.
89  * This consists of 'Z' and IDs starting with '+' or '-'.
90  * <p>
91  * The next type of ID are offset-style IDs with some form of prefix,
92  * such as 'GMT+2' or 'UTC+01:00'.
93  * The recognised prefixes are 'UTC', 'GMT' and 'UT'.
94  * The offset is the suffix and will be normalized during creation.
95  * These IDs can be normalized to a {@code ZoneOffset} using {@code normalized()}.
96  * <p>
97  * The third type of ID are region-based IDs. A region-based ID must be of
98  * two or more characters, and not start with 'UTC', 'GMT', 'UT' '+' or '-'.
99  * Region-based IDs are defined by configuration, see {@link ZoneRulesProvider}.
100  * The configuration focuses on providing the lookup from the ID to the
101  * underlying {@code ZoneRules}.
102  * <p>
103  * Time-zone rules are defined by governments and change frequently.
104  * There are a number of organizations, known here as groups, that monitor
105  * time-zone changes and collate them.
106  * The default group is the IANA Time Zone Database (TZDB).
107  * Other organizations include IATA (the airline industry body) and Microsoft.
108  * <p>
109  * Each group defines its own format for the region ID it provides.
110  * The TZDB group defines IDs such as 'Europe/London' or 'America/New_York'.
111  * TZDB IDs take precedence over other groups.
112  * <p>
113  * It is strongly recommended that the group name is included in all IDs supplied by
114  * groups other than TZDB to avoid conflicts. For example, IATA airline time-zone
115  * region IDs are typically the same as the three letter airport code.
116  * However, the airport of Utrecht has the code 'UTC', which is obviously a conflict.
117  * The recommended format for region IDs from groups other than TZDB is 'group~region'.
118  * Thus if IATA data were defined, Utrecht airport would be 'IATA~UTC'.
119  *
120  * <h3>Serialization</h3>
121  * This class can be serialized and stores the string zone ID in the external form.
122  * The {@code ZoneOffset} subclass uses a dedicated format that only stores the
123  * offset from UTC/Greenwich.
124  * <p>
125  * A {@code ZoneId} can be deserialized in a Java Runtime where the ID is unknown.
126  * For example, if a server-side Java Runtime has been updated with a new zone ID, but
127  * the client-side Java Runtime has not been updated. In this case, the {@code ZoneId}
128  * object will exist, and can be queried using {@code getId}, {@code equals},
129  * {@code hashCode}, {@code toString}, {@code getDisplayName} and {@code normalized}.
130  * However, any call to {@code getRules} will fail with {@code ZoneRulesException}.
131  * This approach is designed to allow a {@link ZonedDateTime} to be loaded and
132  * queried, but not modified, on a Java Runtime with incomplete time-zone information.
133  *
134  * <h3>Specification for implementors</h3>
135  * This abstract class has two implementations, both of which are immutable and thread-safe.
136  * One implementation models region-based IDs, the other is {@code ZoneOffset} modelling
137  * offset-based IDs. This difference is visible in serialization.
138  */
139 public abstract class ZoneId implements Serializable {
140 
141     /**
142      * Simulate JDK 8 method reference ZoneId::from.
143      */
144     public static final TemporalQuery<ZoneId> FROM = new TemporalQuery<ZoneId>() {
145         @Override
146         public ZoneId queryFrom(TemporalAccessor temporal) {
147             return ZoneId.from(temporal);
148         }
149     };
150     /**
151      * A map of zone overrides to enable the short time-zone names to be used.
152      * <p>
153      * Use of short zone IDs has been deprecated in {@code java.util.TimeZone}.
154      * This map allows the IDs to continue to be used via the
155      * {@link #of(String, Map)} factory method.
156      * <p>
157      * This map contains a mapping of the IDs that is in line with TZDB 2005r and
158      * later, where 'EST', 'MST' and 'HST' map to IDs which do not include daylight
159      * savings.
160      * <p>
161      * This maps as follows:
162      * <p><ul>
163      * <li>EST - -05:00</li>
164      * <li>HST - -10:00</li>
165      * <li>MST - -07:00</li>
166      * <li>ACT - Australia/Darwin</li>
167      * <li>AET - Australia/Sydney</li>
168      * <li>AGT - America/Argentina/Buenos_Aires</li>
169      * <li>ART - Africa/Cairo</li>
170      * <li>AST - America/Anchorage</li>
171      * <li>BET - America/Sao_Paulo</li>
172      * <li>BST - Asia/Dhaka</li>
173      * <li>CAT - Africa/Harare</li>
174      * <li>CNT - America/St_Johns</li>
175      * <li>CST - America/Chicago</li>
176      * <li>CTT - Asia/Shanghai</li>
177      * <li>EAT - Africa/Addis_Ababa</li>
178      * <li>ECT - Europe/Paris</li>
179      * <li>IET - America/Indiana/Indianapolis</li>
180      * <li>IST - Asia/Kolkata</li>
181      * <li>JST - Asia/Tokyo</li>
182      * <li>MIT - Pacific/Apia</li>
183      * <li>NET - Asia/Yerevan</li>
184      * <li>NST - Pacific/Auckland</li>
185      * <li>PLT - Asia/Karachi</li>
186      * <li>PNT - America/Phoenix</li>
187      * <li>PRT - America/Puerto_Rico</li>
188      * <li>PST - America/Los_Angeles</li>
189      * <li>SST - Pacific/Guadalcanal</li>
190      * <li>VST - Asia/Ho_Chi_Minh</li>
191      * </ul><p>
192      * The map is unmodifiable.
193      */
194     public static final Map<String, String> SHORT_IDS;
195     static {
196         Map<String, String> base = new HashMap<String, String>();
197         base.put("ACT", "Australia/Darwin");
198         base.put("AET", "Australia/Sydney");
199         base.put("AGT", "America/Argentina/Buenos_Aires");
200         base.put("ART", "Africa/Cairo");
201         base.put("AST", "America/Anchorage");
202         base.put("BET", "America/Sao_Paulo");
203         base.put("BST", "Asia/Dhaka");
204         base.put("CAT", "Africa/Harare");
205         base.put("CNT", "America/St_Johns");
206         base.put("CST", "America/Chicago");
207         base.put("CTT", "Asia/Shanghai");
208         base.put("EAT", "Africa/Addis_Ababa");
209         base.put("ECT", "Europe/Paris");
210         base.put("IET", "America/Indiana/Indianapolis");
211         base.put("IST", "Asia/Kolkata");
212         base.put("JST", "Asia/Tokyo");
213         base.put("MIT", "Pacific/Apia");
214         base.put("NET", "Asia/Yerevan");
215         base.put("NST", "Pacific/Auckland");
216         base.put("PLT", "Asia/Karachi");
217         base.put("PNT", "America/Phoenix");
218         base.put("PRT", "America/Puerto_Rico");
219         base.put("PST", "America/Los_Angeles");
220         base.put("SST", "Pacific/Guadalcanal");
221         base.put("VST", "Asia/Ho_Chi_Minh");
222         base.put("EST", "-05:00");
223         base.put("MST", "-07:00");
224         base.put("HST", "-10:00");
225         SHORT_IDS = Collections.unmodifiableMap(base);
226     }
227     /**
228      * Serialization version.
229      */
230     private static final long serialVersionUID = 8352817235686L;
231 
232     //-----------------------------------------------------------------------
233     /**
234      * Gets the system default time-zone.
235      * <p>
236      * This queries {@link TimeZone#getDefault()} to find the default time-zone
237      * and converts it to a {@code ZoneId}. If the system default time-zone is changed,
238      * then the result of this method will also change.
239      *
240      * @return the zone ID, not null
241      * @throws DateTimeException if the converted zone ID has an invalid format
242      * @throws ZoneRulesException if the converted zone region ID cannot be found
243      */
systemDefault()244     public static ZoneId systemDefault() {
245         return ZoneId.of(TimeZone.getDefault().getID(), SHORT_IDS);
246     }
247 
248     /**
249      * Gets the set of available zone IDs.
250      * <p>
251      * This set includes the string form of all available region-based IDs.
252      * Offset-based zone IDs are not included in the returned set.
253      * The ID can be passed to {@link #of(String)} to create a {@code ZoneId}.
254      * <p>
255      * The set of zone IDs can increase over time, although in a typical application
256      * the set of IDs is fixed. Each call to this method is thread-safe.
257      *
258      * @return a modifiable copy of the set of zone IDs, not null
259      */
getAvailableZoneIds()260     public static Set<String> getAvailableZoneIds() {
261         return new HashSet<String>(ZoneRulesProvider.getAvailableZoneIds());
262     }
263 
264     //-----------------------------------------------------------------------
265     /**
266      * Obtains an instance of {@code ZoneId} using its ID using a map
267      * of aliases to supplement the standard zone IDs.
268      * <p>
269      * Many users of time-zones use short abbreviations, such as PST for
270      * 'Pacific Standard Time' and PDT for 'Pacific Daylight Time'.
271      * These abbreviations are not unique, and so cannot be used as IDs.
272      * This method allows a map of string to time-zone to be setup and reused
273      * within an application.
274      *
275      * @param zoneId  the time-zone ID, not null
276      * @param aliasMap  a map of alias zone IDs (typically abbreviations) to real zone IDs, not null
277      * @return the zone ID, not null
278      * @throws DateTimeException if the zone ID has an invalid format
279      * @throws ZoneRulesException if the zone ID is a region ID that cannot be found
280      */
of(String zoneId, Map<String, String> aliasMap)281     public static ZoneId of(String zoneId, Map<String, String> aliasMap) {
282         Jdk8Methods.requireNonNull(zoneId, "zoneId");
283         Jdk8Methods.requireNonNull(aliasMap, "aliasMap");
284         String id = aliasMap.get(zoneId);
285         id = (id != null ? id : zoneId);
286         return of(id);
287     }
288 
289     /**
290      * Obtains an instance of {@code ZoneId} from an ID ensuring that the
291      * ID is valid and available for use.
292      * <p>
293      * This method parses the ID producing a {@code ZoneId} or {@code ZoneOffset}.
294      * A {@code ZoneOffset} is returned if the ID is 'Z', or starts with '+' or '-'.
295      * The result will always be a valid ID for which {@link ZoneRules} can be obtained.
296      * <p>
297      * Parsing matches the zone ID step by step as follows.
298      * <ul>
299      * <li>If the zone ID equals 'Z', the result is {@code ZoneOffset.UTC}.
300      * <li>If the zone ID consists of a single letter, the zone ID is invalid
301      *  and {@code DateTimeException} is thrown.
302      * <li>If the zone ID starts with '+' or '-', the ID is parsed as a
303      *  {@code ZoneOffset} using {@link ZoneOffset#of(String)}.
304      * <li>If the zone ID equals 'GMT', 'UTC' or 'UT' then the result is a {@code ZoneId}
305      *  with the same ID and rules equivalent to {@code ZoneOffset.UTC}.
306      * <li>If the zone ID starts with 'UTC+', 'UTC-', 'GMT+', 'GMT-', 'UT+' or 'UT-'
307      *  then the ID is a prefixed offset-based ID. The ID is split in two, with
308      *  a two or three letter prefix and a suffix starting with the sign.
309      *  The suffix is parsed as a {@link ZoneOffset#of(String) ZoneOffset}.
310      *  The result will be a {@code ZoneId} with the specified UTC/GMT/UT prefix
311      *  and the normalized offset ID as per {@link ZoneOffset#getId()}.
312      *  The rules of the returned {@code ZoneId} will be equivalent to the
313      *  parsed {@code ZoneOffset}.
314      * <li>All other IDs are parsed as region-based zone IDs. Region IDs must
315      *  match the regular expression <code>[A-Za-z][A-Za-z0-9~/._+-]+</code>
316      *  otherwise a {@code DateTimeException} is thrown. If the zone ID is not
317      *  in the configured set of IDs, {@code ZoneRulesException} is thrown.
318      *  The detailed format of the region ID depends on the group supplying the data.
319      *  The default set of data is supplied by the IANA Time Zone Database (TZDB).
320      *  This has region IDs of the form '{area}/{city}', such as 'Europe/Paris' or 'America/New_York'.
321      *  This is compatible with most IDs from {@link java.util.TimeZone}.
322      * </ul>
323      *
324      * @param zoneId  the time-zone ID, not null
325      * @return the zone ID, not null
326      * @throws DateTimeException if the zone ID has an invalid format
327      * @throws ZoneRulesException if the zone ID is a region ID that cannot be found
328      */
of(String zoneId)329     public static ZoneId of(String zoneId) {
330         Jdk8Methods.requireNonNull(zoneId, "zoneId");
331         if (zoneId.equals("Z")) {
332             return ZoneOffset.UTC;
333         }
334         if (zoneId.length() == 1) {
335             throw new DateTimeException("Invalid zone: " + zoneId);
336         }
337         if (zoneId.startsWith("+") || zoneId.startsWith("-")) {
338             return ZoneOffset.of(zoneId);
339         }
340         if (zoneId.equals("UTC") || zoneId.equals("GMT") || zoneId.equals("UT")) {
341             return new ZoneRegion(zoneId, ZoneOffset.UTC.getRules());
342         }
343         if (zoneId.startsWith("UTC+") || zoneId.startsWith("GMT+") ||
344                 zoneId.startsWith("UTC-") || zoneId.startsWith("GMT-")) {
345             ZoneOffset offset = ZoneOffset.of(zoneId.substring(3));
346             if (offset.getTotalSeconds() == 0) {
347                 return new ZoneRegion(zoneId.substring(0, 3), offset.getRules());
348             }
349             return new ZoneRegion(zoneId.substring(0, 3) + offset.getId(), offset.getRules());
350         }
351         if (zoneId.startsWith("UT+") || zoneId.startsWith("UT-")) {
352             ZoneOffset offset = ZoneOffset.of(zoneId.substring(2));
353             if (offset.getTotalSeconds() == 0) {
354                 return new ZoneRegion("UT", offset.getRules());
355             }
356             return new ZoneRegion("UT" + offset.getId(), offset.getRules());
357         }
358         return ZoneRegion.ofId(zoneId, true);
359     }
360 
361     /**
362      * Obtains an instance of {@code ZoneId} wrapping an offset.
363      * <p>
364      * If the prefix is "GMT", "UTC", or "UT" a {@code ZoneId}
365      * with the prefix and the non-zero offset is returned.
366      * If the prefix is empty {@code ""} the {@code ZoneOffset} is returned.
367      *
368      * @param prefix  the time-zone ID, not null
369      * @param offset  the offset, not null
370      * @return the zone ID, not null
371      * @throws IllegalArgumentException if the prefix is not one of
372      *     "GMT", "UTC", or "UT", or ""
373      */
ofOffset(String prefix, ZoneOffset offset)374     public static ZoneId ofOffset(String prefix, ZoneOffset offset) {
375         Jdk8Methods.requireNonNull(prefix, "prefix");
376         Jdk8Methods.requireNonNull(offset, "offset");
377         if (prefix.length() == 0) {
378             return offset;
379         }
380         if (prefix.equals("GMT") || prefix.equals("UTC") || prefix.equals("UT")) {
381             if (offset.getTotalSeconds() == 0) {
382                 return new ZoneRegion(prefix, offset.getRules());
383             }
384             return new ZoneRegion(prefix + offset.getId(), offset.getRules());
385         }
386         throw new IllegalArgumentException("Invalid prefix, must be GMT, UTC or UT: " + prefix);
387     }
388 
389     //-----------------------------------------------------------------------
390     /**
391      * Obtains an instance of {@code ZoneId} from a temporal object.
392      * <p>
393      * A {@code TemporalAccessor} represents some form of date and time information.
394      * This factory converts the arbitrary temporal object to an instance of {@code ZoneId}.
395      * <p>
396      * The conversion will try to obtain the zone in a way that favours region-based
397      * zones over offset-based zones using {@link TemporalQueries#zone()}.
398      * <p>
399      * This method matches the signature of the functional interface {@link TemporalQuery}
400      * allowing it to be used in queries via method reference, {@code ZoneId::from}.
401      *
402      * @param temporal  the temporal object to convert, not null
403      * @return the zone ID, not null
404      * @throws DateTimeException if unable to convert to a {@code ZoneId}
405      */
from(TemporalAccessor temporal)406     public static ZoneId from(TemporalAccessor temporal) {
407         ZoneId obj = temporal.query(TemporalQueries.zone());
408         if (obj == null) {
409             throw new DateTimeException("Unable to obtain ZoneId from TemporalAccessor: " +
410                     temporal + ", type " + temporal.getClass().getName());
411         }
412         return obj;
413     }
414 
415     //-----------------------------------------------------------------------
416     /**
417      * Constructor only accessible within the package.
418      */
ZoneId()419     ZoneId() {
420         if (getClass() != ZoneOffset.class && getClass() != ZoneRegion.class) {
421             throw new AssertionError("Invalid subclass");
422         }
423     }
424 
425     //-----------------------------------------------------------------------
426     /**
427      * Gets the unique time-zone ID.
428      * <p>
429      * This ID uniquely defines this object.
430      * The format of an offset based ID is defined by {@link ZoneOffset#getId()}.
431      *
432      * @return the time-zone unique ID, not null
433      */
getId()434     public abstract String getId();
435 
436     //-----------------------------------------------------------------------
437     /**
438      * Gets the time-zone rules for this ID allowing calculations to be performed.
439      * <p>
440      * The rules provide the functionality associated with a time-zone,
441      * such as finding the offset for a given instant or local date-time.
442      * <p>
443      * A time-zone can be invalid if it is deserialized in a Java Runtime which
444      * does not have the same rules loaded as the Java Runtime that stored it.
445      * In this case, calling this method will throw a {@code ZoneRulesException}.
446      * <p>
447      * The rules are supplied by {@link ZoneRulesProvider}. An advanced provider may
448      * support dynamic updates to the rules without restarting the Java Runtime.
449      * If so, then the result of this method may change over time.
450      * Each individual call will be still remain thread-safe.
451      * <p>
452      * {@link ZoneOffset} will always return a set of rules where the offset never changes.
453      *
454      * @return the rules, not null
455      * @throws ZoneRulesException if no rules are available for this ID
456      */
getRules()457     public abstract ZoneRules getRules();
458 
459     //-----------------------------------------------------------------------
460     /**
461      * Gets the textual representation of the zone, such as 'British Time' or
462      * '+02:00'.
463      * <p>
464      * This returns the textual name used to identify the time-zone ID,
465      * suitable for presentation to the user.
466      * The parameters control the style of the returned text and the locale.
467      * <p>
468      * If no textual mapping is found then the {@link #getId() full ID} is returned.
469      *
470      * @param style  the length of the text required, not null
471      * @param locale  the locale to use, not null
472      * @return the text value of the zone, not null
473      */
getDisplayName(TextStyle style, Locale locale)474     public String getDisplayName(TextStyle style, Locale locale) {
475         return new DateTimeFormatterBuilder().appendZoneText(style).toFormatter(locale).format(new DefaultInterfaceTemporalAccessor() {
476             @Override
477             public boolean isSupported(TemporalField field) {
478                 return false;
479             }
480             @Override
481             public long getLong(TemporalField field) {
482                 throw new UnsupportedTemporalTypeException("Unsupported field: " + field);
483             }
484             @SuppressWarnings("unchecked")
485             @Override
486             public <R> R query(TemporalQuery<R> query) {
487                 if (query == TemporalQueries.zoneId()) {
488                     return (R) ZoneId.this;
489                 }
490                 return super.query(query);
491             }
492         });
493     }
494 
495     /**
496      * Normalizes the time-zone ID, returning a {@code ZoneOffset} where possible.
497      * <p>
498      * The returns a normalized {@code ZoneId} that can be used in place of this ID.
499      * The result will have {@code ZoneRules} equivalent to those returned by this object,
500      * however the ID returned by {@code getId()} may be different.
501      * <p>
502      * The normalization checks if the rules of this {@code ZoneId} have a fixed offset.
503      * If they do, then the {@code ZoneOffset} equal to that offset is returned.
504      * Otherwise {@code this} is returned.
505      *
506      * @return the time-zone unique ID, not null
507      */
508     public ZoneId normalized() {
509         try {
510             ZoneRules rules = getRules();
511             if (rules.isFixedOffset()) {
512                 return rules.getOffset(Instant.EPOCH);
513             }
514         } catch (ZoneRulesException ex) {
515             // ignore invalid objects
516         }
517         return this;
518     }
519 
520     //-----------------------------------------------------------------------
521     /**
522      * Checks if this time-zone ID is equal to another time-zone ID.
523      * <p>
524      * The comparison is based on the ID.
525      *
526      * @param obj  the object to check, null returns false
527      * @return true if this is equal to the other time-zone ID
528      */
529     @Override
530     public boolean equals(Object obj) {
531         if (this == obj) {
532            return true;
533         }
534         if (obj instanceof ZoneId) {
535             ZoneId other = (ZoneId) obj;
536             return getId().equals(other.getId());
537         }
538         return false;
539     }
540 
541     /**
542      * A hash code for this time-zone ID.
543      *
544      * @return a suitable hash code
545      */
546     @Override
547     public int hashCode() {
548         return getId().hashCode();
549     }
550 
551     //-----------------------------------------------------------------------
552     /**
553      * Outputs this zone as a {@code String}, using the ID.
554      *
555      * @return a string representation of this time-zone ID, not null
556      */
557     @Override
558     public String toString() {
559         return getId();
560     }
561 
562     //-----------------------------------------------------------------------
563     abstract void write(DataOutput out) throws IOException;
564 
565 }
566