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