001/*
002 * Copyright (c) 2007-2013, Stephen Colebourne & Michael Nascimento Santos
003 *
004 * All rights reserved.
005 *
006 * Redistribution and use in source and binary forms, with or without
007 * modification, are permitted provided that the following conditions are met:
008 *
009 *  * Redistributions of source code must retain the above copyright notice,
010 *    this list of conditions and the following disclaimer.
011 *
012 *  * Redistributions in binary form must reproduce the above copyright notice,
013 *    this list of conditions and the following disclaimer in the documentation
014 *    and/or other materials provided with the distribution.
015 *
016 *  * Neither the name of JSR-310 nor the names of its contributors
017 *    may be used to endorse or promote products derived from this software
018 *    without specific prior written permission.
019 *
020 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
021 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
022 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
023 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
024 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
025 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
026 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
027 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
028 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
029 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
030 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
031 */
032package org.threeten.bp;
033
034import static org.threeten.bp.temporal.ChronoField.OFFSET_SECONDS;
035
036import java.io.DataInput;
037import java.io.DataOutput;
038import java.io.IOException;
039import java.io.InvalidObjectException;
040import java.io.ObjectStreamException;
041import java.io.Serializable;
042import java.util.Objects;
043import java.util.concurrent.ConcurrentHashMap;
044import java.util.concurrent.ConcurrentMap;
045
046import org.threeten.bp.temporal.ChronoField;
047import org.threeten.bp.temporal.Temporal;
048import org.threeten.bp.temporal.TemporalAccessor;
049import org.threeten.bp.temporal.TemporalAdjuster;
050import org.threeten.bp.temporal.TemporalField;
051import org.threeten.bp.temporal.TemporalQueries;
052import org.threeten.bp.temporal.TemporalQuery;
053import org.threeten.bp.temporal.ValueRange;
054import org.threeten.bp.zone.ZoneRules;
055
056/**
057 * A time-zone offset from Greenwich/UTC, such as {@code +02:00}.
058 * <p>
059 * A time-zone offset is the period of time that a time-zone differs from Greenwich/UTC.
060 * This is usually a fixed number of hours and minutes.
061 * <p>
062 * Different parts of the world have different time-zone offsets.
063 * The rules for how offsets vary by place and time of year are captured in the
064 * {@link ZoneId} class.
065 * <p>
066 * For example, Paris is one hour ahead of Greenwich/UTC in winter and two hours
067 * ahead in summer. The {@code ZoneId} instance for Paris will reference two
068 * {@code ZoneOffset} instances - a {@code +01:00} instance for winter,
069 * and a {@code +02:00} instance for summer.
070 * <p>
071 * In 2008, time-zone offsets around the world extended from -12:00 to +14:00.
072 * To prevent any problems with that range being extended, yet still provide
073 * validation, the range of offsets is restricted to -18:00 to 18:00 inclusive.
074 * <p>
075 * This class is designed for use with the ISO calendar system.
076 * The fields of hours, minutes and seconds make assumptions that are valid for the
077 * standard ISO definitions of those fields. This class may be used with other
078 * calendar systems providing the definition of the time fields matches those
079 * of the ISO calendar system.
080 * <p>
081 * Instances of {@code ZoneOffset} must be compared using {@link #equals}.
082 * Implementations may choose to cache certain common offsets, however
083 * applications must not rely on such caching.
084 *
085 * <h3>Specification for implementors</h3>
086 * This class is immutable and thread-safe.
087 */
088public final class ZoneOffset
089        extends ZoneId
090        implements TemporalAccessor, TemporalAdjuster, Comparable<ZoneOffset>, Serializable {
091
092    /** Cache of time-zone offset by offset in seconds. */
093    private static final ConcurrentMap<Integer, ZoneOffset> SECONDS_CACHE = new ConcurrentHashMap<>(16, 0.75f, 4);
094    /** Cache of time-zone offset by ID. */
095    private static final ConcurrentMap<String, ZoneOffset> ID_CACHE = new ConcurrentHashMap<>(16, 0.75f, 4);
096
097    /**
098     * The number of seconds per hour.
099     */
100    private static final int SECONDS_PER_HOUR = 60 * 60;
101    /**
102     * The number of seconds per minute.
103     */
104    private static final int SECONDS_PER_MINUTE = 60;
105    /**
106     * The number of minutes per hour.
107     */
108    private static final int MINUTES_PER_HOUR = 60;
109    /**
110     * The abs maximum seconds.
111     */
112    private static final int MAX_SECONDS = 18 * SECONDS_PER_HOUR;
113    /**
114     * Serialization version.
115     */
116    private static final long serialVersionUID = 2357656521762053153L;
117
118    /**
119     * The time-zone offset for UTC, with an ID of 'Z'.
120     */
121    public static final ZoneOffset UTC = ZoneOffset.ofTotalSeconds(0);
122    /**
123     * Constant for the maximum supported offset.
124     */
125    public static final ZoneOffset MIN = ZoneOffset.ofTotalSeconds(-MAX_SECONDS);
126    /**
127     * Constant for the maximum supported offset.
128     */
129    public static final ZoneOffset MAX = ZoneOffset.ofTotalSeconds(MAX_SECONDS);
130
131    /**
132     * The total offset in seconds.
133     */
134    private final int totalSeconds;
135    /**
136     * The string form of the time-zone offset.
137     */
138    private final transient String id;
139
140    //-----------------------------------------------------------------------
141    /**
142     * Obtains an instance of {@code ZoneOffset} using the ID.
143     * <p>
144     * This method parses the string ID of a {@code ZoneOffset} to
145     * return an instance. The parsing accepts all the formats generated by
146     * {@link #getId()}, plus some additional formats:
147     * <p><ul>
148     * <li>{@code Z} - for UTC
149     * <li>{@code +h}
150     * <li>{@code +hh}
151     * <li>{@code +hh:mm}
152     * <li>{@code -hh:mm}
153     * <li>{@code +hhmm}
154     * <li>{@code -hhmm}
155     * <li>{@code +hh:mm:ss}
156     * <li>{@code -hh:mm:ss}
157     * <li>{@code +hhmmss}
158     * <li>{@code -hhmmss}
159     * </ul><p>
160     * Note that &plusmn; means either the plus or minus symbol.
161     * <p>
162     * The ID of the returned offset will be normalized to one of the formats
163     * described by {@link #getId()}.
164     * <p>
165     * The maximum supported range is from +18:00 to -18:00 inclusive.
166     *
167     * @param offsetId  the offset ID, not null
168     * @return the zone-offset, not null
169     * @throws DateTimeException if the offset ID is invalid
170     */
171    public static ZoneOffset of(String offsetId) {
172        Objects.requireNonNull(offsetId, "offsetId");
173        // "Z" is always in the cache
174        ZoneOffset offset = ID_CACHE.get(offsetId);
175        if (offset != null) {
176            return offset;
177        }
178
179        // parse - +h, +hh, +hhmm, +hh:mm, +hhmmss, +hh:mm:ss
180        final int hours, minutes, seconds;
181        switch (offsetId.length()) {
182            case 2:
183                offsetId = offsetId.charAt(0) + "0" + offsetId.charAt(1);  // fallthru
184            case 3:
185                hours = parseNumber(offsetId, 1, false);
186                minutes = 0;
187                seconds = 0;
188                break;
189            case 5:
190                hours = parseNumber(offsetId, 1, false);
191                minutes = parseNumber(offsetId, 3, false);
192                seconds = 0;
193                break;
194            case 6:
195                hours = parseNumber(offsetId, 1, false);
196                minutes = parseNumber(offsetId, 4, true);
197                seconds = 0;
198                break;
199            case 7:
200                hours = parseNumber(offsetId, 1, false);
201                minutes = parseNumber(offsetId, 3, false);
202                seconds = parseNumber(offsetId, 5, false);
203                break;
204            case 9:
205                hours = parseNumber(offsetId, 1, false);
206                minutes = parseNumber(offsetId, 4, true);
207                seconds = parseNumber(offsetId, 7, true);
208                break;
209            default:
210                throw new DateTimeException("Zone offset ID '" + offsetId + "' is invalid");
211        }
212        char first = offsetId.charAt(0);
213        if (first != '+' && first != '-') {
214            throw new DateTimeException("Zone offset ID '" + offsetId + "' is invalid: Plus/minus not found when expected");
215        }
216        if (first == '-') {
217            return ofHoursMinutesSeconds(-hours, -minutes, -seconds);
218        } else {
219            return ofHoursMinutesSeconds(hours, minutes, seconds);
220        }
221    }
222
223    /**
224     * Parse a two digit zero-prefixed number.
225     *
226     * @param offsetId  the offset ID, not null
227     * @param pos  the position to parse, valid
228     * @param precededByColon  should this number be prefixed by a precededByColon
229     * @return the parsed number, from 0 to 99
230     */
231    private static int parseNumber(CharSequence offsetId, int pos, boolean precededByColon) {
232        if (precededByColon && offsetId.charAt(pos - 1) != ':') {
233            throw new DateTimeException("Zone offset ID '" + offsetId + "' is invalid: Colon not found when expected");
234        }
235        char ch1 = offsetId.charAt(pos);
236        char ch2 = offsetId.charAt(pos + 1);
237        if (ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9') {
238            throw new DateTimeException("Zone offset ID '" + offsetId + "' is invalid: Non numeric characters found");
239        }
240        return (ch1 - 48) * 10 + (ch2 - 48);
241    }
242
243    //-----------------------------------------------------------------------
244    /**
245     * Obtains an instance of {@code ZoneOffset} using an offset in hours.
246     *
247     * @param hours  the time-zone offset in hours, from -18 to +18
248     * @return the zone-offset, not null
249     * @throws DateTimeException if the offset is not in the required range
250     */
251    public static ZoneOffset ofHours(int hours) {
252        return ofHoursMinutesSeconds(hours, 0, 0);
253    }
254
255    /**
256     * Obtains an instance of {@code ZoneOffset} using an offset in
257     * hours and minutes.
258     * <p>
259     * The sign of the hours and minutes components must match.
260     * Thus, if the hours is negative, the minutes must be negative or zero.
261     * If the hours is zero, the minutes may be positive, negative or zero.
262     *
263     * @param hours  the time-zone offset in hours, from -18 to +18
264     * @param minutes  the time-zone offset in minutes, from 0 to &plusmn;59, sign matches hours
265     * @return the zone-offset, not null
266     * @throws DateTimeException if the offset is not in the required range
267     */
268    public static ZoneOffset ofHoursMinutes(int hours, int minutes) {
269        return ofHoursMinutesSeconds(hours, minutes, 0);
270    }
271
272    /**
273     * Obtains an instance of {@code ZoneOffset} using an offset in
274     * hours, minutes and seconds.
275     * <p>
276     * The sign of the hours, minutes and seconds components must match.
277     * Thus, if the hours is negative, the minutes and seconds must be negative or zero.
278     *
279     * @param hours  the time-zone offset in hours, from -18 to +18
280     * @param minutes  the time-zone offset in minutes, from 0 to &plusmn;59, sign matches hours and seconds
281     * @param seconds  the time-zone offset in seconds, from 0 to &plusmn;59, sign matches hours and minutes
282     * @return the zone-offset, not null
283     * @throws DateTimeException if the offset is not in the required range
284     */
285    public static ZoneOffset ofHoursMinutesSeconds(int hours, int minutes, int seconds) {
286        validate(hours, minutes, seconds);
287        int totalSeconds = totalSeconds(hours, minutes, seconds);
288        return ofTotalSeconds(totalSeconds);
289    }
290
291    //-----------------------------------------------------------------------
292    /**
293     * Obtains an instance of {@code ZoneOffset} from a temporal object.
294     * <p>
295     * A {@code TemporalAccessor} represents some form of date and time information.
296     * This factory converts the arbitrary temporal object to an instance of {@code ZoneOffset}.
297     * <p>
298     * The conversion extracts the {@link ChronoField#OFFSET_SECONDS offset-seconds} field.
299     * <p>
300     * This method matches the signature of the functional interface {@link TemporalQuery}
301     * allowing it to be used in queries via method reference, {@code ZoneOffset::from}.
302     *
303     * @param temporal  the temporal object to convert, not null
304     * @return the zone-offset, not null
305     * @throws DateTimeException if unable to convert to an {@code ZoneOffset}
306     */
307    public static ZoneOffset from(TemporalAccessor temporal) {
308        if (temporal instanceof ZoneOffset) {
309            return (ZoneOffset) temporal;
310        }
311        try {
312            return ofTotalSeconds(temporal.get(OFFSET_SECONDS));
313        } catch (DateTimeException ex) {
314            throw new DateTimeException("Unable to obtain ZoneOffset from TemporalAccessor: " + temporal.getClass(), ex);
315        }
316    }
317
318    //-----------------------------------------------------------------------
319    /**
320     * Validates the offset fields.
321     *
322     * @param hours  the time-zone offset in hours, from -18 to +18
323     * @param minutes  the time-zone offset in minutes, from 0 to &plusmn;59
324     * @param seconds  the time-zone offset in seconds, from 0 to &plusmn;59
325     * @throws DateTimeException if the offset is not in the required range
326     */
327    private static void validate(int hours, int minutes, int seconds) {
328        if (hours < -18 || hours > 18) {
329            throw new DateTimeException("Zone offset hours not in valid range: value " + hours +
330                    " is not in the range -18 to 18");
331        }
332        if (hours > 0) {
333            if (minutes < 0 || seconds < 0) {
334                throw new DateTimeException("Zone offset minutes and seconds must be positive because hours is positive");
335            }
336        } else if (hours < 0) {
337            if (minutes > 0 || seconds > 0) {
338                throw new DateTimeException("Zone offset minutes and seconds must be negative because hours is negative");
339            }
340        } else if ((minutes > 0 && seconds < 0) || (minutes < 0 && seconds > 0)) {
341            throw new DateTimeException("Zone offset minutes and seconds must have the same sign");
342        }
343        if (Math.abs(minutes) > 59) {
344            throw new DateTimeException("Zone offset minutes not in valid range: abs(value) " +
345                    Math.abs(minutes) + " is not in the range 0 to 59");
346        }
347        if (Math.abs(seconds) > 59) {
348            throw new DateTimeException("Zone offset seconds not in valid range: abs(value) " +
349                    Math.abs(seconds) + " is not in the range 0 to 59");
350        }
351        if (Math.abs(hours) == 18 && (Math.abs(minutes) > 0 || Math.abs(seconds) > 0)) {
352            throw new DateTimeException("Zone offset not in valid range: -18:00 to +18:00");
353        }
354    }
355
356    /**
357     * Calculates the total offset in seconds.
358     *
359     * @param hours  the time-zone offset in hours, from -18 to +18
360     * @param minutes  the time-zone offset in minutes, from 0 to &plusmn;59, sign matches hours and seconds
361     * @param seconds  the time-zone offset in seconds, from 0 to &plusmn;59, sign matches hours and minutes
362     * @return the total in seconds
363     */
364    private static int totalSeconds(int hours, int minutes, int seconds) {
365        return hours * SECONDS_PER_HOUR + minutes * SECONDS_PER_MINUTE + seconds;
366    }
367
368    //-----------------------------------------------------------------------
369    /**
370     * Obtains an instance of {@code ZoneOffset} specifying the total offset in seconds
371     * <p>
372     * The offset must be in the range {@code -18:00} to {@code +18:00}, which corresponds to -64800 to +64800.
373     *
374     * @param totalSeconds  the total time-zone offset in seconds, from -64800 to +64800
375     * @return the ZoneOffset, not null
376     * @throws DateTimeException if the offset is not in the required range
377     */
378    public static ZoneOffset ofTotalSeconds(int totalSeconds) {
379        if (Math.abs(totalSeconds) > MAX_SECONDS) {
380            throw new DateTimeException("Zone offset not in valid range: -18:00 to +18:00");
381        }
382        if (totalSeconds % (15 * SECONDS_PER_MINUTE) == 0) {
383            Integer totalSecs = totalSeconds;
384            ZoneOffset result = SECONDS_CACHE.get(totalSecs);
385            if (result == null) {
386                result = new ZoneOffset(totalSeconds);
387                SECONDS_CACHE.putIfAbsent(totalSecs, result);
388                result = SECONDS_CACHE.get(totalSecs);
389                ID_CACHE.putIfAbsent(result.getId(), result);
390            }
391            return result;
392        } else {
393            return new ZoneOffset(totalSeconds);
394        }
395    }
396
397    //-----------------------------------------------------------------------
398    /**
399     * Constructor.
400     *
401     * @param totalSeconds  the total time-zone offset in seconds, from -64800 to +64800
402     */
403    private ZoneOffset(int totalSeconds) {
404        super();
405        this.totalSeconds = totalSeconds;
406        id = buildId(totalSeconds);
407    }
408
409    private static String buildId(int totalSeconds) {
410        if (totalSeconds == 0) {
411            return "Z";
412        } else {
413            int absTotalSeconds = Math.abs(totalSeconds);
414            StringBuilder buf = new StringBuilder();
415            int absHours = absTotalSeconds / SECONDS_PER_HOUR;
416            int absMinutes = (absTotalSeconds / SECONDS_PER_MINUTE) % MINUTES_PER_HOUR;
417            buf.append(totalSeconds < 0 ? "-" : "+")
418                .append(absHours < 10 ? "0" : "").append(absHours)
419                .append(absMinutes < 10 ? ":0" : ":").append(absMinutes);
420            int absSeconds = absTotalSeconds % SECONDS_PER_MINUTE;
421            if (absSeconds != 0) {
422                buf.append(absSeconds < 10 ? ":0" : ":").append(absSeconds);
423            }
424            return buf.toString();
425        }
426    }
427
428    //-----------------------------------------------------------------------
429    /**
430     * Gets the total zone offset in seconds.
431     * <p>
432     * This is the primary way to access the offset amount.
433     * It returns the total of the hours, minutes and seconds fields as a
434     * single offset that can be added to a time.
435     *
436     * @return the total zone offset amount in seconds
437     */
438    public int getTotalSeconds() {
439        return totalSeconds;
440    }
441
442    /**
443     * Gets the normalized zone offset ID.
444     * <p>
445     * The ID is minor variation to the standard ISO-8601 formatted string
446     * for the offset. There are three formats:
447     * <p><ul>
448     * <li>{@code Z} - for UTC (ISO-8601)
449     * <li>{@code +hh:mm} or {@code -hh:mm} - if the seconds are zero (ISO-8601)
450     * <li>{@code +hh:mm:ss} or {@code -hh:mm:ss} - if the seconds are non-zero (not ISO-8601)
451     * </ul><p>
452     *
453     * @return the zone offset ID, not null
454     */
455    @Override
456    public String getId() {
457        return id;
458    }
459
460    /**
461     * Gets the associated time-zone rules.
462     * <p>
463     * The rules will always return this offset when queried.
464     * The implementation class is immutable, thread-safe and serializable.
465     *
466     * @return the rules, not null
467     */
468    @Override
469    public ZoneRules getRules() {
470        return ZoneRules.of(this);
471    }
472
473    //-----------------------------------------------------------------------
474    /**
475     * Checks if the specified field is supported.
476     * <p>
477     * This checks if this offset can be queried for the specified field.
478     * If false, then calling the {@link #range(TemporalField) range} and
479     * {@link #get(TemporalField) get} methods will throw an exception.
480     * <p>
481     * If the field is a {@link ChronoField} then the query is implemented here.
482     * The {@code OFFSET_SECONDS} field returns true.
483     * All other {@code ChronoField} instances will return false.
484     * <p>
485     * If the field is not a {@code ChronoField}, then the result of this method
486     * is obtained by invoking {@code TemporalField.doIsSupported(TemporalAccessor)}
487     * passing {@code this} as the argument.
488     * Whether the field is supported is determined by the field.
489     *
490     * @param field  the field to check, null returns false
491     * @return true if the field is supported on this offset, false if not
492     */
493    @Override
494    public boolean isSupported(TemporalField field) {
495        if (field instanceof ChronoField) {
496            return field == OFFSET_SECONDS;
497        }
498        return field != null && field.doIsSupported(this);
499    }
500
501    /**
502     * Gets the range of valid values for the specified field.
503     * <p>
504     * The range object expresses the minimum and maximum valid values for a field.
505     * This offset is used to enhance the accuracy of the returned range.
506     * If it is not possible to return the range, because the field is not supported
507     * or for some other reason, an exception is thrown.
508     * <p>
509     * If the field is a {@link ChronoField} then the query is implemented here.
510     * The {@link #isSupported(TemporalField) supported fields} will return
511     * appropriate range instances.
512     * All other {@code ChronoField} instances will throw a {@code DateTimeException}.
513     * <p>
514     * If the field is not a {@code ChronoField}, then the result of this method
515     * is obtained by invoking {@code TemporalField.doRange(TemporalAccessor)}
516     * passing {@code this} as the argument.
517     * Whether the range can be obtained is determined by the field.
518     *
519     * @param field  the field to query the range for, not null
520     * @return the range of valid values for the field, not null
521     * @throws DateTimeException if the range for the field cannot be obtained
522     */
523    @Override  // override for Javadoc
524    public ValueRange range(TemporalField field) {
525        if (field == OFFSET_SECONDS) {
526            return field.range();
527        } else if (field instanceof ChronoField) {
528            throw new DateTimeException("Unsupported field: " + field.getName());
529        }
530        return field.doRange(this);
531    }
532
533    /**
534     * Gets the value of the specified field from this offset as an {@code int}.
535     * <p>
536     * This queries this offset for the value for the specified field.
537     * The returned value will always be within the valid range of values for the field.
538     * If it is not possible to return the value, because the field is not supported
539     * or for some other reason, an exception is thrown.
540     * <p>
541     * If the field is a {@link ChronoField} then the query is implemented here.
542     * The {@code OFFSET_SECONDS} field returns the value of the offset.
543     * All other {@code ChronoField} instances will throw a {@code DateTimeException}.
544     * <p>
545     * If the field is not a {@code ChronoField}, then the result of this method
546     * is obtained by invoking {@code TemporalField.doGet(TemporalAccessor)}
547     * passing {@code this} as the argument. Whether the value can be obtained,
548     * and what the value represents, is determined by the field.
549     *
550     * @param field  the field to get, not null
551     * @return the value for the field
552     * @throws DateTimeException if a value for the field cannot be obtained
553     * @throws ArithmeticException if numeric overflow occurs
554     */
555    @Override  // override for Javadoc and performance
556    public int get(TemporalField field) {
557        if (field == OFFSET_SECONDS) {
558            return totalSeconds;
559        } else if (field instanceof ChronoField) {
560            throw new DateTimeException("Unsupported field: " + field.getName());
561        }
562        return range(field).checkValidIntValue(getLong(field), field);
563    }
564
565    /**
566     * Gets the value of the specified field from this offset as a {@code long}.
567     * <p>
568     * This queries this offset for the value for the specified field.
569     * If it is not possible to return the value, because the field is not supported
570     * or for some other reason, an exception is thrown.
571     * <p>
572     * If the field is a {@link ChronoField} then the query is implemented here.
573     * The {@code OFFSET_SECONDS} field returns the value of the offset.
574     * All other {@code ChronoField} instances will throw a {@code DateTimeException}.
575     * <p>
576     * If the field is not a {@code ChronoField}, then the result of this method
577     * is obtained by invoking {@code TemporalField.doGet(TemporalAccessor)}
578     * passing {@code this} as the argument. Whether the value can be obtained,
579     * and what the value represents, is determined by the field.
580     *
581     * @param field  the field to get, not null
582     * @return the value for the field
583     * @throws DateTimeException if a value for the field cannot be obtained
584     * @throws ArithmeticException if numeric overflow occurs
585     */
586    @Override
587    public long getLong(TemporalField field) {
588        if (field == OFFSET_SECONDS) {
589            return totalSeconds;
590        } else if (field instanceof ChronoField) {
591            throw new DateTimeException("Unsupported field: " + field.getName());
592        }
593        return field.doGet(this);
594    }
595
596    //-----------------------------------------------------------------------
597    /**
598     * Queries this offset using the specified query.
599     * <p>
600     * This queries this offset using the specified query strategy object.
601     * The {@code TemporalQuery} object defines the logic to be used to
602     * obtain the result. Read the documentation of the query to understand
603     * what the result of this method will be.
604     * <p>
605     * The result of this method is obtained by invoking the
606     * {@link TemporalQuery#queryFrom(TemporalAccessor)} method on the
607     * specified query passing {@code this} as the argument.
608     *
609     * @param <R> the type of the result
610     * @param query  the query to invoke, not null
611     * @return the query result, null may be returned (defined by the query)
612     * @throws DateTimeException if unable to query (defined by the query)
613     * @throws ArithmeticException if numeric overflow occurs (defined by the query)
614     */
615    @SuppressWarnings("unchecked")
616    @Override
617    public <R> R query(TemporalQuery<R> query) {
618        if (query == TemporalQueries.offset() || query == TemporalQueries.zone()) {
619            return (R) this;
620        } else if (query == TemporalQueries.precision() || query == TemporalQueries.chrono() || query == TemporalQueries.zoneId()) {
621            return null;
622        }
623        return query.queryFrom(this);
624    }
625
626    /**
627     * Adjusts the specified temporal object to have the same offset as this object.
628     * <p>
629     * This returns a temporal object of the same observable type as the input
630     * with the offset changed to be the same as this.
631     * <p>
632     * The adjustment is equivalent to using {@link Temporal#with(TemporalField, long)}
633     * passing {@link ChronoField#OFFSET_SECONDS} as the field.
634     * <p>
635     * In most cases, it is clearer to reverse the calling pattern by using
636     * {@link Temporal#with(TemporalAdjuster)}:
637     * <pre>
638     *   // these two lines are equivalent, but the second approach is recommended
639     *   temporal = thisOffset.adjustInto(temporal);
640     *   temporal = temporal.with(thisOffset);
641     * </pre>
642     * <p>
643     * This instance is immutable and unaffected by this method call.
644     *
645     * @param temporal  the target object to be adjusted, not null
646     * @return the adjusted object, not null
647     * @throws DateTimeException if unable to make the adjustment
648     * @throws ArithmeticException if numeric overflow occurs
649     */
650    @Override
651    public Temporal adjustInto(Temporal temporal) {
652        return temporal.with(OFFSET_SECONDS, totalSeconds);
653    }
654
655    //-----------------------------------------------------------------------
656    /**
657     * Compares this offset to another offset in descending order.
658     * <p>
659     * The offsets are compared in the order that they occur for the same time
660     * of day around the world. Thus, an offset of {@code +10:00} comes before an
661     * offset of {@code +09:00} and so on down to {@code -18:00}.
662     * <p>
663     * The comparison is "consistent with equals", as defined by {@link Comparable}.
664     *
665     * @param other  the other date to compare to, not null
666     * @return the comparator value, negative if less, postive if greater
667     * @throws NullPointerException if {@code other} is null
668     */
669    @Override
670    public int compareTo(ZoneOffset other) {
671        return other.totalSeconds - totalSeconds;
672    }
673
674    //-----------------------------------------------------------------------
675    /**
676     * Checks if this offset is equal to another offset.
677     * <p>
678     * The comparison is based on the amount of the offset in seconds.
679     * This is equivalent to a comparison by ID.
680     *
681     * @param obj  the object to check, null returns false
682     * @return true if this is equal to the other offset
683     */
684    @Override
685    public boolean equals(Object obj) {
686        if (this == obj) {
687           return true;
688        }
689        if (obj instanceof ZoneOffset) {
690            return totalSeconds == ((ZoneOffset) obj).totalSeconds;
691        }
692        return false;
693    }
694
695    /**
696     * A hash code for this offset.
697     *
698     * @return a suitable hash code
699     */
700    @Override
701    public int hashCode() {
702        return totalSeconds;
703    }
704
705    //-----------------------------------------------------------------------
706    /**
707     * Outputs this offset as a {@code String}, using the normalized ID.
708     *
709     * @return a string representation of this offset, not null
710     */
711    @Override
712    public String toString() {
713        return id;
714    }
715
716    // -----------------------------------------------------------------------
717    private Object writeReplace() {
718        return new Ser(Ser.ZONE_OFFSET_TYPE, this);
719    }
720
721    /**
722     * Defend against malicious streams.
723     * @return never
724     * @throws InvalidObjectException always
725     */
726    private Object readResolve() throws ObjectStreamException {
727        throw new InvalidObjectException("Deserialization via serialization delegate");
728    }
729
730    @Override
731    void write(DataOutput out) throws IOException {
732        out.writeByte(Ser.ZONE_OFFSET_TYPE);
733        writeExternal(out);
734    }
735
736    void writeExternal(DataOutput out) throws IOException {
737        final int offsetSecs = totalSeconds;
738        int offsetByte = offsetSecs % 900 == 0 ? offsetSecs / 900 : 127;  // compress to -72 to +72
739        out.writeByte(offsetByte);
740        if (offsetByte == 127) {
741            out.writeInt(offsetSecs);
742        }
743    }
744
745    static ZoneOffset readExternal(DataInput in) throws IOException {
746        int offsetByte = in.readByte();
747        return (offsetByte == 127 ? ZoneOffset.ofTotalSeconds(in.readInt()) : ZoneOffset.ofTotalSeconds(offsetByte * 900));
748    }
749
750}