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