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