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