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