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