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