• 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 static org.threeten.bp.temporal.ChronoField.OFFSET_SECONDS;
35 
36 import java.io.DataInput;
37 import java.io.DataOutput;
38 import java.io.IOException;
39 import java.io.InvalidObjectException;
40 import java.io.ObjectStreamException;
41 import java.io.Serializable;
42 import java.util.concurrent.ConcurrentHashMap;
43 import java.util.concurrent.ConcurrentMap;
44 
45 import org.threeten.bp.jdk8.Jdk8Methods;
46 import org.threeten.bp.temporal.ChronoField;
47 import org.threeten.bp.temporal.Temporal;
48 import org.threeten.bp.temporal.TemporalAccessor;
49 import org.threeten.bp.temporal.TemporalAdjuster;
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.temporal.ValueRange;
55 import org.threeten.bp.zone.ZoneRules;
56 
57 /**
58  * A time-zone offset from Greenwich/UTC, such as {@code +02:00}.
59  * <p>
60  * A time-zone offset is the period of time that a time-zone differs from Greenwich/UTC.
61  * This is usually a fixed number of hours and minutes.
62  * <p>
63  * Different parts of the world have different time-zone offsets.
64  * The rules for how offsets vary by place and time of year are captured in the
65  * {@link ZoneId} class.
66  * <p>
67  * For example, Paris is one hour ahead of Greenwich/UTC in winter and two hours
68  * ahead in summer. The {@code ZoneId} instance for Paris will reference two
69  * {@code ZoneOffset} instances - a {@code +01:00} instance for winter,
70  * and a {@code +02:00} instance for summer.
71  * <p>
72  * In 2008, time-zone offsets around the world extended from -12:00 to +14:00.
73  * To prevent any problems with that range being extended, yet still provide
74  * validation, the range of offsets is restricted to -18:00 to 18:00 inclusive.
75  * <p>
76  * This class is designed for use with the ISO calendar system.
77  * The fields of hours, minutes and seconds make assumptions that are valid for the
78  * standard ISO definitions of those fields. This class may be used with other
79  * calendar systems providing the definition of the time fields matches those
80  * of the ISO calendar system.
81  * <p>
82  * Instances of {@code ZoneOffset} must be compared using {@link #equals}.
83  * Implementations may choose to cache certain common offsets, however
84  * applications must not rely on such caching.
85  *
86  * <h3>Specification for implementors</h3>
87  * This class is immutable and thread-safe.
88  */
89 public final class ZoneOffset
90         extends ZoneId
91         implements TemporalAccessor, TemporalAdjuster, Comparable<ZoneOffset>, Serializable {
92 
93     /**
94      * Simulate JDK 8 method reference ZoneOffset::from.
95      */
96     public static final TemporalQuery<ZoneOffset> FROM = new TemporalQuery<ZoneOffset>() {
97         @Override
98         public ZoneOffset queryFrom(TemporalAccessor temporal) {
99             return ZoneOffset.from(temporal);
100         }
101     };
102 
103     /** Cache of time-zone offset by offset in seconds. */
104     private static final ConcurrentMap<Integer, ZoneOffset> SECONDS_CACHE = new ConcurrentHashMap<Integer, ZoneOffset>(16, 0.75f, 4);
105     /** Cache of time-zone offset by ID. */
106     private static final ConcurrentMap<String, ZoneOffset> ID_CACHE = new ConcurrentHashMap<String, ZoneOffset>(16, 0.75f, 4);
107 
108     /**
109      * The number of seconds per hour.
110      */
111     private static final int SECONDS_PER_HOUR = 60 * 60;
112     /**
113      * The number of seconds per minute.
114      */
115     private static final int SECONDS_PER_MINUTE = 60;
116     /**
117      * The number of minutes per hour.
118      */
119     private static final int MINUTES_PER_HOUR = 60;
120     /**
121      * The abs maximum seconds.
122      */
123     private static final int MAX_SECONDS = 18 * SECONDS_PER_HOUR;
124     /**
125      * Serialization version.
126      */
127     private static final long serialVersionUID = 2357656521762053153L;
128 
129     /**
130      * The time-zone offset for UTC, with an ID of 'Z'.
131      */
132     public static final ZoneOffset UTC = ZoneOffset.ofTotalSeconds(0);
133     /**
134      * Constant for the maximum supported offset.
135      */
136     public static final ZoneOffset MIN = ZoneOffset.ofTotalSeconds(-MAX_SECONDS);
137     /**
138      * Constant for the maximum supported offset.
139      */
140     public static final ZoneOffset MAX = ZoneOffset.ofTotalSeconds(MAX_SECONDS);
141 
142     /**
143      * The total offset in seconds.
144      */
145     private final int totalSeconds;
146     /**
147      * The string form of the time-zone offset.
148      */
149     private final transient String id;
150 
151     //-----------------------------------------------------------------------
152     /**
153      * Obtains an instance of {@code ZoneOffset} using the ID.
154      * <p>
155      * This method parses the string ID of a {@code ZoneOffset} to
156      * return an instance. The parsing accepts all the formats generated by
157      * {@link #getId()}, plus some additional formats:
158      * <p><ul>
159      * <li>{@code Z} - for UTC
160      * <li>{@code +h}
161      * <li>{@code +hh}
162      * <li>{@code +hh:mm}
163      * <li>{@code -hh:mm}
164      * <li>{@code +hhmm}
165      * <li>{@code -hhmm}
166      * <li>{@code +hh:mm:ss}
167      * <li>{@code -hh:mm:ss}
168      * <li>{@code +hhmmss}
169      * <li>{@code -hhmmss}
170      * </ul><p>
171      * Note that &plusmn; means either the plus or minus symbol.
172      * <p>
173      * The ID of the returned offset will be normalized to one of the formats
174      * described by {@link #getId()}.
175      * <p>
176      * The maximum supported range is from +18:00 to -18:00 inclusive.
177      *
178      * @param offsetId  the offset ID, not null
179      * @return the zone-offset, not null
180      * @throws DateTimeException if the offset ID is invalid
181      */
of(String offsetId)182     public static ZoneOffset of(String offsetId) {
183         Jdk8Methods.requireNonNull(offsetId, "offsetId");
184         // "Z" is always in the cache
185         ZoneOffset offset = ID_CACHE.get(offsetId);
186         if (offset != null) {
187             return offset;
188         }
189 
190         // parse - +h, +hh, +hhmm, +hh:mm, +hhmmss, +hh:mm:ss
191         final int hours, minutes, seconds;
192         switch (offsetId.length()) {
193             case 2:
194                 offsetId = offsetId.charAt(0) + "0" + offsetId.charAt(1);  // fallthru
195             case 3:
196                 hours = parseNumber(offsetId, 1, false);
197                 minutes = 0;
198                 seconds = 0;
199                 break;
200             case 5:
201                 hours = parseNumber(offsetId, 1, false);
202                 minutes = parseNumber(offsetId, 3, false);
203                 seconds = 0;
204                 break;
205             case 6:
206                 hours = parseNumber(offsetId, 1, false);
207                 minutes = parseNumber(offsetId, 4, true);
208                 seconds = 0;
209                 break;
210             case 7:
211                 hours = parseNumber(offsetId, 1, false);
212                 minutes = parseNumber(offsetId, 3, false);
213                 seconds = parseNumber(offsetId, 5, false);
214                 break;
215             case 9:
216                 hours = parseNumber(offsetId, 1, false);
217                 minutes = parseNumber(offsetId, 4, true);
218                 seconds = parseNumber(offsetId, 7, true);
219                 break;
220             default:
221                 throw new DateTimeException("Invalid ID for ZoneOffset, invalid format: " + offsetId);
222         }
223         char first = offsetId.charAt(0);
224         if (first != '+' && first != '-') {
225             throw new DateTimeException("Invalid ID for ZoneOffset, plus/minus not found when expected: " + offsetId);
226         }
227         if (first == '-') {
228             return ofHoursMinutesSeconds(-hours, -minutes, -seconds);
229         } else {
230             return ofHoursMinutesSeconds(hours, minutes, seconds);
231         }
232     }
233 
234     /**
235      * Parse a two digit zero-prefixed number.
236      *
237      * @param offsetId  the offset ID, not null
238      * @param pos  the position to parse, valid
239      * @param precededByColon  should this number be prefixed by a precededByColon
240      * @return the parsed number, from 0 to 99
241      */
parseNumber(CharSequence offsetId, int pos, boolean precededByColon)242     private static int parseNumber(CharSequence offsetId, int pos, boolean precededByColon) {
243         if (precededByColon && offsetId.charAt(pos - 1) != ':') {
244             throw new DateTimeException("Invalid ID for ZoneOffset, colon not found when expected: " + offsetId);
245         }
246         char ch1 = offsetId.charAt(pos);
247         char ch2 = offsetId.charAt(pos + 1);
248         if (ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9') {
249             throw new DateTimeException("Invalid ID for ZoneOffset, non numeric characters found: " + offsetId);
250         }
251         return (ch1 - 48) * 10 + (ch2 - 48);
252     }
253 
254     //-----------------------------------------------------------------------
255     /**
256      * Obtains an instance of {@code ZoneOffset} using an offset in hours.
257      *
258      * @param hours  the time-zone offset in hours, from -18 to +18
259      * @return the zone-offset, not null
260      * @throws DateTimeException if the offset is not in the required range
261      */
ofHours(int hours)262     public static ZoneOffset ofHours(int hours) {
263         return ofHoursMinutesSeconds(hours, 0, 0);
264     }
265 
266     /**
267      * Obtains an instance of {@code ZoneOffset} using an offset in
268      * hours and minutes.
269      * <p>
270      * The sign of the hours and minutes components must match.
271      * Thus, if the hours is negative, the minutes must be negative or zero.
272      * If the hours is zero, the minutes may be positive, negative or zero.
273      *
274      * @param hours  the time-zone offset in hours, from -18 to +18
275      * @param minutes  the time-zone offset in minutes, from 0 to &plusmn;59, sign matches hours
276      * @return the zone-offset, not null
277      * @throws DateTimeException if the offset is not in the required range
278      */
ofHoursMinutes(int hours, int minutes)279     public static ZoneOffset ofHoursMinutes(int hours, int minutes) {
280         return ofHoursMinutesSeconds(hours, minutes, 0);
281     }
282 
283     /**
284      * Obtains an instance of {@code ZoneOffset} using an offset in
285      * hours, minutes and seconds.
286      * <p>
287      * The sign of the hours, minutes and seconds components must match.
288      * Thus, if the hours is negative, the minutes and seconds must be negative or zero.
289      *
290      * @param hours  the time-zone offset in hours, from -18 to +18
291      * @param minutes  the time-zone offset in minutes, from 0 to &plusmn;59, sign matches hours and seconds
292      * @param seconds  the time-zone offset in seconds, from 0 to &plusmn;59, sign matches hours and minutes
293      * @return the zone-offset, not null
294      * @throws DateTimeException if the offset is not in the required range
295      */
ofHoursMinutesSeconds(int hours, int minutes, int seconds)296     public static ZoneOffset ofHoursMinutesSeconds(int hours, int minutes, int seconds) {
297         validate(hours, minutes, seconds);
298         int totalSeconds = totalSeconds(hours, minutes, seconds);
299         return ofTotalSeconds(totalSeconds);
300     }
301 
302     //-----------------------------------------------------------------------
303     /**
304      * Obtains an instance of {@code ZoneOffset} from a temporal object.
305      * <p>
306      * A {@code TemporalAccessor} represents some form of date and time information.
307      * This factory converts the arbitrary temporal object to an instance of {@code ZoneOffset}.
308      * <p>
309      * The conversion uses the {@link TemporalQueries#offset()} query, which relies
310      * on extracting the {@link ChronoField#OFFSET_SECONDS OFFSET_SECONDS} field.
311      * <p>
312      * This method matches the signature of the functional interface {@link TemporalQuery}
313      * allowing it to be used in queries via method reference, {@code ZoneOffset::from}.
314      *
315      * @param temporal  the temporal object to convert, not null
316      * @return the zone-offset, not null
317      * @throws DateTimeException if unable to convert to an {@code ZoneOffset}
318      */
from(TemporalAccessor temporal)319     public static ZoneOffset from(TemporalAccessor temporal) {
320         ZoneOffset offset = temporal.query(TemporalQueries.offset());
321         if (offset == null) {
322             throw new DateTimeException("Unable to obtain ZoneOffset from TemporalAccessor: " +
323                     temporal + ", type " + temporal.getClass().getName());
324         }
325         return offset;
326     }
327 
328     //-----------------------------------------------------------------------
329     /**
330      * Validates the offset fields.
331      *
332      * @param hours  the time-zone offset in hours, from -18 to +18
333      * @param minutes  the time-zone offset in minutes, from 0 to &plusmn;59
334      * @param seconds  the time-zone offset in seconds, from 0 to &plusmn;59
335      * @throws DateTimeException if the offset is not in the required range
336      */
validate(int hours, int minutes, int seconds)337     private static void validate(int hours, int minutes, int seconds) {
338         if (hours < -18 || hours > 18) {
339             throw new DateTimeException("Zone offset hours not in valid range: value " + hours +
340                     " is not in the range -18 to 18");
341         }
342         if (hours > 0) {
343             if (minutes < 0 || seconds < 0) {
344                 throw new DateTimeException("Zone offset minutes and seconds must be positive because hours is positive");
345             }
346         } else if (hours < 0) {
347             if (minutes > 0 || seconds > 0) {
348                 throw new DateTimeException("Zone offset minutes and seconds must be negative because hours is negative");
349             }
350         } else if ((minutes > 0 && seconds < 0) || (minutes < 0 && seconds > 0)) {
351             throw new DateTimeException("Zone offset minutes and seconds must have the same sign");
352         }
353         if (Math.abs(minutes) > 59) {
354             throw new DateTimeException("Zone offset minutes not in valid range: abs(value) " +
355                     Math.abs(minutes) + " is not in the range 0 to 59");
356         }
357         if (Math.abs(seconds) > 59) {
358             throw new DateTimeException("Zone offset seconds not in valid range: abs(value) " +
359                     Math.abs(seconds) + " is not in the range 0 to 59");
360         }
361         if (Math.abs(hours) == 18 && (Math.abs(minutes) > 0 || Math.abs(seconds) > 0)) {
362             throw new DateTimeException("Zone offset not in valid range: -18:00 to +18:00");
363         }
364     }
365 
366     /**
367      * Calculates the total offset in seconds.
368      *
369      * @param hours  the time-zone offset in hours, from -18 to +18
370      * @param minutes  the time-zone offset in minutes, from 0 to &plusmn;59, sign matches hours and seconds
371      * @param seconds  the time-zone offset in seconds, from 0 to &plusmn;59, sign matches hours and minutes
372      * @return the total in seconds
373      */
totalSeconds(int hours, int minutes, int seconds)374     private static int totalSeconds(int hours, int minutes, int seconds) {
375         return hours * SECONDS_PER_HOUR + minutes * SECONDS_PER_MINUTE + seconds;
376     }
377 
378     //-----------------------------------------------------------------------
379     /**
380      * Obtains an instance of {@code ZoneOffset} specifying the total offset in seconds
381      * <p>
382      * The offset must be in the range {@code -18:00} to {@code +18:00}, which corresponds to -64800 to +64800.
383      *
384      * @param totalSeconds  the total time-zone offset in seconds, from -64800 to +64800
385      * @return the ZoneOffset, not null
386      * @throws DateTimeException if the offset is not in the required range
387      */
ofTotalSeconds(int totalSeconds)388     public static ZoneOffset ofTotalSeconds(int totalSeconds) {
389         if (Math.abs(totalSeconds) > MAX_SECONDS) {
390             throw new DateTimeException("Zone offset not in valid range: -18:00 to +18:00");
391         }
392         if (totalSeconds % (15 * SECONDS_PER_MINUTE) == 0) {
393             Integer totalSecs = totalSeconds;
394             ZoneOffset result = SECONDS_CACHE.get(totalSecs);
395             if (result == null) {
396                 result = new ZoneOffset(totalSeconds);
397                 SECONDS_CACHE.putIfAbsent(totalSecs, result);
398                 result = SECONDS_CACHE.get(totalSecs);
399                 ID_CACHE.putIfAbsent(result.getId(), result);
400             }
401             return result;
402         } else {
403             return new ZoneOffset(totalSeconds);
404         }
405     }
406 
407     //-----------------------------------------------------------------------
408     /**
409      * Constructor.
410      *
411      * @param totalSeconds  the total time-zone offset in seconds, from -64800 to +64800
412      */
ZoneOffset(int totalSeconds)413     private ZoneOffset(int totalSeconds) {
414         super();
415         this.totalSeconds = totalSeconds;
416         id = buildId(totalSeconds);
417     }
418 
buildId(int totalSeconds)419     private static String buildId(int totalSeconds) {
420         if (totalSeconds == 0) {
421             return "Z";
422         } else {
423             int absTotalSeconds = Math.abs(totalSeconds);
424             StringBuilder buf = new StringBuilder();
425             int absHours = absTotalSeconds / SECONDS_PER_HOUR;
426             int absMinutes = (absTotalSeconds / SECONDS_PER_MINUTE) % MINUTES_PER_HOUR;
427             buf.append(totalSeconds < 0 ? "-" : "+")
428                 .append(absHours < 10 ? "0" : "").append(absHours)
429                 .append(absMinutes < 10 ? ":0" : ":").append(absMinutes);
430             int absSeconds = absTotalSeconds % SECONDS_PER_MINUTE;
431             if (absSeconds != 0) {
432                 buf.append(absSeconds < 10 ? ":0" : ":").append(absSeconds);
433             }
434             return buf.toString();
435         }
436     }
437 
438     //-----------------------------------------------------------------------
439     /**
440      * Gets the total zone offset in seconds.
441      * <p>
442      * This is the primary way to access the offset amount.
443      * It returns the total of the hours, minutes and seconds fields as a
444      * single offset that can be added to a time.
445      *
446      * @return the total zone offset amount in seconds
447      */
448     public int getTotalSeconds() {
449         return totalSeconds;
450     }
451 
452     /**
453      * Gets the normalized zone offset ID.
454      * <p>
455      * The ID is minor variation to the standard ISO-8601 formatted string
456      * for the offset. There are three formats:
457      * <p><ul>
458      * <li>{@code Z} - for UTC (ISO-8601)
459      * <li>{@code +hh:mm} or {@code -hh:mm} - if the seconds are zero (ISO-8601)
460      * <li>{@code +hh:mm:ss} or {@code -hh:mm:ss} - if the seconds are non-zero (not ISO-8601)
461      * </ul><p>
462      *
463      * @return the zone offset ID, not null
464      */
465     @Override
466     public String getId() {
467         return id;
468     }
469 
470     /**
471      * Gets the associated time-zone rules.
472      * <p>
473      * The rules will always return this offset when queried.
474      * The implementation class is immutable, thread-safe and serializable.
475      *
476      * @return the rules, not null
477      */
478     @Override
479     public ZoneRules getRules() {
480         return ZoneRules.of(this);
481     }
482 
483     //-----------------------------------------------------------------------
484     /**
485      * Checks if the specified field is supported.
486      * <p>
487      * This checks if this offset can be queried for the specified field.
488      * If false, then calling the {@link #range(TemporalField) range} and
489      * {@link #get(TemporalField) get} methods will throw an exception.
490      * <p>
491      * If the field is a {@link ChronoField} then the query is implemented here.
492      * The {@code OFFSET_SECONDS} field returns true.
493      * All other {@code ChronoField} instances will return false.
494      * <p>
495      * If the field is not a {@code ChronoField}, then the result of this method
496      * is obtained by invoking {@code TemporalField.isSupportedBy(TemporalAccessor)}
497      * passing {@code this} as the argument.
498      * Whether the field is supported is determined by the field.
499      *
500      * @param field  the field to check, null returns false
501      * @return true if the field is supported on this offset, false if not
502      */
503     @Override
504     public boolean isSupported(TemporalField field) {
505         if (field instanceof ChronoField) {
506             return field == OFFSET_SECONDS;
507         }
508         return field != null && field.isSupportedBy(this);
509     }
510 
511     /**
512      * Gets the range of valid values for the specified field.
513      * <p>
514      * The range object expresses the minimum and maximum valid values for a field.
515      * This offset is used to enhance the accuracy of the returned range.
516      * If it is not possible to return the range, because the field is not supported
517      * or for some other reason, an exception is thrown.
518      * <p>
519      * If the field is a {@link ChronoField} then the query is implemented here.
520      * The {@link #isSupported(TemporalField) supported fields} will return
521      * appropriate range instances.
522      * All other {@code ChronoField} instances will throw a {@code DateTimeException}.
523      * <p>
524      * If the field is not a {@code ChronoField}, then the result of this method
525      * is obtained by invoking {@code TemporalField.rangeRefinedBy(TemporalAccessor)}
526      * passing {@code this} as the argument.
527      * Whether the range can be obtained is determined by the field.
528      *
529      * @param field  the field to query the range for, not null
530      * @return the range of valid values for the field, not null
531      * @throws DateTimeException if the range for the field cannot be obtained
532      */
533     @Override  // override for Javadoc
534     public ValueRange range(TemporalField field) {
535         if (field == OFFSET_SECONDS) {
536             return field.range();
537         } else if (field instanceof ChronoField) {
538             throw new UnsupportedTemporalTypeException("Unsupported field: " + field);
539         }
540         return field.rangeRefinedBy(this);
541     }
542 
543     /**
544      * Gets the value of the specified field from this offset as an {@code int}.
545      * <p>
546      * This queries this offset for the value for the specified field.
547      * The returned value will always be within the valid range of values for the field.
548      * If it is not possible to return the value, because the field is not supported
549      * or for some other reason, an exception is thrown.
550      * <p>
551      * If the field is a {@link ChronoField} then the query is implemented here.
552      * The {@code OFFSET_SECONDS} field returns the value of the offset.
553      * All other {@code ChronoField} instances will throw a {@code DateTimeException}.
554      * <p>
555      * If the field is not a {@code ChronoField}, then the result of this method
556      * is obtained by invoking {@code TemporalField.getFrom(TemporalAccessor)}
557      * passing {@code this} as the argument. Whether the value can be obtained,
558      * and what the value represents, is determined by the field.
559      *
560      * @param field  the field to get, not null
561      * @return the value for the field
562      * @throws DateTimeException if a value for the field cannot be obtained
563      * @throws ArithmeticException if numeric overflow occurs
564      */
565     @Override  // override for Javadoc and performance
566     public int get(TemporalField field) {
567         if (field == OFFSET_SECONDS) {
568             return totalSeconds;
569         } else if (field instanceof ChronoField) {
570             throw new UnsupportedTemporalTypeException("Unsupported field: " + field);
571         }
572         return range(field).checkValidIntValue(getLong(field), field);
573     }
574 
575     /**
576      * Gets the value of the specified field from this offset as a {@code long}.
577      * <p>
578      * This queries this offset for the value for the specified field.
579      * If it is not possible to return the value, because the field is not supported
580      * or for some other reason, an exception is thrown.
581      * <p>
582      * If the field is a {@link ChronoField} then the query is implemented here.
583      * The {@code OFFSET_SECONDS} field returns the value of the offset.
584      * All other {@code ChronoField} instances will throw a {@code DateTimeException}.
585      * <p>
586      * If the field is not a {@code ChronoField}, then the result of this method
587      * is obtained by invoking {@code TemporalField.getFrom(TemporalAccessor)}
588      * passing {@code this} as the argument. Whether the value can be obtained,
589      * and what the value represents, is determined by the field.
590      *
591      * @param field  the field to get, not null
592      * @return the value for the field
593      * @throws DateTimeException if a value for the field cannot be obtained
594      * @throws ArithmeticException if numeric overflow occurs
595      */
596     @Override
597     public long getLong(TemporalField field) {
598         if (field == OFFSET_SECONDS) {
599             return totalSeconds;
600         } else if (field instanceof ChronoField) {
601             throw new DateTimeException("Unsupported field: " + field);
602         }
603         return field.getFrom(this);
604     }
605 
606     //-----------------------------------------------------------------------
607     /**
608      * Queries this offset using the specified query.
609      * <p>
610      * This queries this offset using the specified query strategy object.
611      * The {@code TemporalQuery} object defines the logic to be used to
612      * obtain the result. Read the documentation of the query to understand
613      * what the result of this method will be.
614      * <p>
615      * The result of this method is obtained by invoking the
616      * {@link TemporalQuery#queryFrom(TemporalAccessor)} method on the
617      * specified query passing {@code this} as the argument.
618      *
619      * @param <R> the type of the result
620      * @param query  the query to invoke, not null
621      * @return the query result, null may be returned (defined by the query)
622      * @throws DateTimeException if unable to query (defined by the query)
623      * @throws ArithmeticException if numeric overflow occurs (defined by the query)
624      */
625     @SuppressWarnings("unchecked")
626     @Override
627     public <R> R query(TemporalQuery<R> query) {
628         if (query == TemporalQueries.offset() || query == TemporalQueries.zone()) {
629             return (R) this;
630         } else if (query == TemporalQueries.localDate() || query == TemporalQueries.localTime() ||
631                 query == TemporalQueries.precision() || query == TemporalQueries.chronology() || query == TemporalQueries.zoneId()) {
632             return null;
633         }
634         return query.queryFrom(this);
635     }
636 
637     /**
638      * Adjusts the specified temporal object to have the same offset as this object.
639      * <p>
640      * This returns a temporal object of the same observable type as the input
641      * with the offset changed to be the same as this.
642      * <p>
643      * The adjustment is equivalent to using {@link Temporal#with(TemporalField, long)}
644      * passing {@link ChronoField#OFFSET_SECONDS} as the field.
645      * <p>
646      * In most cases, it is clearer to reverse the calling pattern by using
647      * {@link Temporal#with(TemporalAdjuster)}:
648      * <pre>
649      *   // these two lines are equivalent, but the second approach is recommended
650      *   temporal = thisOffset.adjustInto(temporal);
651      *   temporal = temporal.with(thisOffset);
652      * </pre>
653      * <p>
654      * This instance is immutable and unaffected by this method call.
655      *
656      * @param temporal  the target object to be adjusted, not null
657      * @return the adjusted object, not null
658      * @throws DateTimeException if unable to make the adjustment
659      * @throws ArithmeticException if numeric overflow occurs
660      */
661     @Override
662     public Temporal adjustInto(Temporal temporal) {
663         return temporal.with(OFFSET_SECONDS, totalSeconds);
664     }
665 
666     //-----------------------------------------------------------------------
667     /**
668      * Compares this offset to another offset in descending order.
669      * <p>
670      * The offsets are compared in the order that they occur for the same time
671      * of day around the world. Thus, an offset of {@code +10:00} comes before an
672      * offset of {@code +09:00} and so on down to {@code -18:00}.
673      * <p>
674      * The comparison is "consistent with equals", as defined by {@link Comparable}.
675      *
676      * @param other  the other date to compare to, not null
677      * @return the comparator value, negative if less, postive if greater
678      * @throws NullPointerException if {@code other} is null
679      */
680     @Override
681     public int compareTo(ZoneOffset other) {
682         return other.totalSeconds - totalSeconds;
683     }
684 
685     //-----------------------------------------------------------------------
686     /**
687      * Checks if this offset is equal to another offset.
688      * <p>
689      * The comparison is based on the amount of the offset in seconds.
690      * This is equivalent to a comparison by ID.
691      *
692      * @param obj  the object to check, null returns false
693      * @return true if this is equal to the other offset
694      */
695     @Override
696     public boolean equals(Object obj) {
697         if (this == obj) {
698            return true;
699         }
700         if (obj instanceof ZoneOffset) {
701             return totalSeconds == ((ZoneOffset) obj).totalSeconds;
702         }
703         return false;
704     }
705 
706     /**
707      * A hash code for this offset.
708      *
709      * @return a suitable hash code
710      */
711     @Override
712     public int hashCode() {
713         return totalSeconds;
714     }
715 
716     //-----------------------------------------------------------------------
717     /**
718      * Outputs this offset as a {@code String}, using the normalized ID.
719      *
720      * @return a string representation of this offset, not null
721      */
722     @Override
723     public String toString() {
724         return id;
725     }
726 
727     // -----------------------------------------------------------------------
728     private Object writeReplace() {
729         return new Ser(Ser.ZONE_OFFSET_TYPE, this);
730     }
731 
732     /**
733      * Defend against malicious streams.
734      * @return never
735      * @throws InvalidObjectException always
736      */
737     private Object readResolve() throws ObjectStreamException {
738         throw new InvalidObjectException("Deserialization via serialization delegate");
739     }
740 
741     @Override
742     void write(DataOutput out) throws IOException {
743         out.writeByte(Ser.ZONE_OFFSET_TYPE);
744         writeExternal(out);
745     }
746 
747     void writeExternal(DataOutput out) throws IOException {
748         final int offsetSecs = totalSeconds;
749         int offsetByte = offsetSecs % 900 == 0 ? offsetSecs / 900 : 127;  // compress to -72 to +72
750         out.writeByte(offsetByte);
751         if (offsetByte == 127) {
752             out.writeInt(offsetSecs);
753         }
754     }
755 
756     static ZoneOffset readExternal(DataInput in) throws IOException {
757         int offsetByte = in.readByte();
758         return (offsetByte == 127 ? ZoneOffset.ofTotalSeconds(in.readInt()) : ZoneOffset.ofTotalSeconds(offsetByte * 900));
759     }
760 
761 }
762