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.zone.ZoneOffsetTransition; 13 14 import hunt.stream.DataInput; 15 import hunt.stream.DataOutput; 16 import hunt.Exceptions; 17 18 //import hunt.io.ObjectInputStream; 19 import hunt.stream.Common; 20 import hunt.time.Duration; 21 import hunt.time.Instant; 22 import hunt.time.LocalDateTime; 23 import hunt.time.ZoneOffset; 24 import hunt.time.zone.Ser; 25 import hunt.collection.Collections; 26 import hunt.collection; 27 import hunt.Functions; 28 import hunt.Integer; 29 import hunt.util.Comparator; 30 import hunt.util.Common; 31 import hunt.util.StringBuilder; 32 /** 33 * A transition between two offsets caused by a discontinuity _in the local time-line. 34 * !(p) 35 * A transition between two offsets is normally the result of a daylight savings cutover. 36 * The discontinuity is normally a gap _in spring and an overlap _in autumn. 37 * {@code ZoneOffsetTransition} models the transition between the two offsets. 38 * !(p) 39 * Gaps occur where there are local date-times that simply do not exist. 40 * An example would be when the offset changes from {@code +03:00} to {@code +04:00}. 41 * This might be described as 'the clocks will move forward one hour tonight at 1am'. 42 * !(p) 43 * Overlaps occur where there are local date-times that exist twice. 44 * An example would be when the offset changes from {@code +04:00} to {@code +03:00}. 45 * This might be described as 'the clocks will move back one hour tonight at 2am'. 46 * 47 * @implSpec 48 * This class is immutable and thread-safe. 49 * 50 * @since 1.8 51 */ 52 final class ZoneOffsetTransition 53 : Comparable!(ZoneOffsetTransition) { //, Serializable 54 55 /** 56 * The transition epoch-second. 57 */ 58 private long epochSecond; 59 /** 60 * The local transition date-time at the transition. 61 */ 62 private LocalDateTime transition; 63 /** 64 * The offset before transition. 65 */ 66 private ZoneOffset offsetBefore; 67 /** 68 * The offset after transition. 69 */ 70 private ZoneOffset offsetAfter; 71 72 //----------------------------------------------------------------------- 73 /** 74 * Obtains an instance defining a transition between two offsets. 75 * !(p) 76 * Applications should normally obtain an instance from {@link ZoneRules}. 77 * This factory is only intended for use when creating {@link ZoneRules}. 78 * 79 * @param transition the transition date-time at the transition, which never 80 * actually occurs, expressed local to the before offset, not null 81 * @param offsetBefore the offset before the transition, not null 82 * @param offsetAfter the offset at and after the transition, not null 83 * @return the transition, not null 84 * @throws IllegalArgumentException if {@code offsetBefore} and {@code offsetAfter} 85 * are equal, or {@code transition.getNano()} returns non-zero value 86 */ 87 public static ZoneOffsetTransition of(LocalDateTime transition, ZoneOffset offsetBefore, ZoneOffset offsetAfter) { 88 assert(transition, "transition"); 89 assert(offsetBefore, "offsetBefore"); 90 assert(offsetAfter, "offsetAfter"); 91 if (offsetBefore == offsetAfter) { 92 throw new IllegalArgumentException("Offsets must not be equal"); 93 } 94 if (transition.getNano() != 0) { 95 throw new IllegalArgumentException("Nano-of-second must be zero"); 96 } 97 return new ZoneOffsetTransition(transition, offsetBefore, offsetAfter); 98 } 99 100 /** 101 * Creates an instance defining a transition between two offsets. 102 * 103 * @param transition the transition date-time with the offset before the transition, not null 104 * @param offsetBefore the offset before the transition, not null 105 * @param offsetAfter the offset at and after the transition, not null 106 */ 107 this(LocalDateTime transition, ZoneOffset offsetBefore, ZoneOffset offsetAfter) { 108 assert(transition.getNano() == 0); 109 this.epochSecond = transition.toEpochSecond(offsetBefore); 110 this.transition = transition; 111 this.offsetBefore = offsetBefore; 112 this.offsetAfter = offsetAfter; 113 } 114 115 /** 116 * Creates an instance from epoch-second and offsets. 117 * 118 * @param epochSecond the transition epoch-second 119 * @param offsetBefore the offset before the transition, not null 120 * @param offsetAfter the offset at and after the transition, not null 121 */ 122 this(long epochSecond, ZoneOffset offsetBefore, ZoneOffset offsetAfter) { 123 this.epochSecond = epochSecond; 124 this.transition = LocalDateTime.ofEpochSecond(epochSecond, 0, offsetBefore); 125 this.offsetBefore = offsetBefore; 126 this.offsetAfter = offsetAfter; 127 } 128 129 //----------------------------------------------------------------------- 130 /** 131 * Defend against malicious streams. 132 * 133 * @param s the stream to read 134 * @throws InvalidObjectException always 135 */ 136 ///@gxc 137 // private void readObject(ObjectInputStream s) /*throws InvalidObjectException*/ { 138 // throw new InvalidObjectException("Deserialization via serialization delegate"); 139 // } 140 141 /** 142 * Writes the object using a 143 * <a href="{@docRoot}/serialized-form.html#hunt.time.zone.Ser">dedicated serialized form</a>. 144 * @serialData 145 * Refer to the serialized form of 146 * <a href="{@docRoot}/serialized-form.html#hunt.time.zone.ZoneRules">ZoneRules.writeReplace</a> 147 * for the encoding of epoch seconds and offsets. 148 * <pre style="font-size:1.0em">{@code 149 * 150 * _out.writeByte(2); // identifies a ZoneOffsetTransition 151 * _out.writeEpochSec(toEpochSecond); 152 * _out.writeOffset(offsetBefore); 153 * _out.writeOffset(offsetAfter); 154 * } 155 * </pre> 156 * @return the replacing object, not null 157 */ 158 private Object writeReplace() { 159 return new Ser(Ser.ZOT, this); 160 } 161 162 /** 163 * Writes the state to the stream. 164 * 165 * @param _out the output stream, not null 166 * @throws IOException if an error occurs 167 */ 168 void writeExternal(DataOutput _out) /*throws IOException*/ { 169 Ser.writeEpochSec(epochSecond, _out); 170 Ser.writeOffset(offsetBefore, _out); 171 Ser.writeOffset(offsetAfter, _out); 172 } 173 174 /** 175 * Reads the state from the stream. 176 * 177 * @param _in the input stream, not null 178 * @return the created object, not null 179 * @throws IOException if an error occurs 180 */ 181 static ZoneOffsetTransition readExternal(DataInput _in) /*throws IOException*/ { 182 long epochSecond = Ser.readEpochSec(_in); 183 ZoneOffset before = Ser.readOffset(_in); 184 ZoneOffset after = Ser.readOffset(_in); 185 if (before == after) { 186 throw new IllegalArgumentException("Offsets must not be equal"); 187 } 188 return new ZoneOffsetTransition(epochSecond, before, after); 189 } 190 191 //----------------------------------------------------------------------- 192 /** 193 * Gets the transition instant. 194 * !(p) 195 * This is the instant of the discontinuity, which is defined as the first 196 * instant that the 'after' offset applies. 197 * !(p) 198 * The methods {@link #getInstant()}, {@link #getDateTimeBefore()} and {@link #getDateTimeAfter()} 199 * all represent the same instant. 200 * 201 * @return the transition instant, not null 202 */ 203 public Instant getInstant() { 204 return Instant.ofEpochSecond(epochSecond); 205 } 206 207 /** 208 * Gets the transition instant as an epoch second. 209 * 210 * @return the transition epoch second 211 */ 212 public long toEpochSecond() { 213 return epochSecond; 214 } 215 216 //------------------------------------------------------------------------- 217 /** 218 * Gets the local transition date-time, as would be expressed with the 'before' offset. 219 * !(p) 220 * This is the date-time where the discontinuity begins expressed with the 'before' offset. 221 * At this instant, the 'after' offset is actually used, therefore the combination of this 222 * date-time and the 'before' offset will never occur. 223 * !(p) 224 * The combination of the 'before' date-time and offset represents the same instant 225 * as the 'after' date-time and offset. 226 * 227 * @return the transition date-time expressed with the before offset, not null 228 */ 229 public LocalDateTime getDateTimeBefore() { 230 return transition; 231 } 232 233 /** 234 * Gets the local transition date-time, as would be expressed with the 'after' offset. 235 * !(p) 236 * This is the first date-time after the discontinuity, when the new offset applies. 237 * !(p) 238 * The combination of the 'before' date-time and offset represents the same instant 239 * as the 'after' date-time and offset. 240 * 241 * @return the transition date-time expressed with the after offset, not null 242 */ 243 public LocalDateTime getDateTimeAfter() { 244 return transition.plusSeconds(getDurationSeconds()); 245 } 246 247 /** 248 * Gets the offset before the transition. 249 * !(p) 250 * This is the offset _in use before the instant of the transition. 251 * 252 * @return the offset before the transition, not null 253 */ 254 public ZoneOffset getOffsetBefore() { 255 return offsetBefore; 256 } 257 258 /** 259 * Gets the offset after the transition. 260 * !(p) 261 * This is the offset _in use on and after the instant of the transition. 262 * 263 * @return the offset after the transition, not null 264 */ 265 public ZoneOffset getOffsetAfter() { 266 return offsetAfter; 267 } 268 269 /** 270 * Gets the duration of the transition. 271 * !(p) 272 * In most cases, the transition duration is one hour, however this is not always the case. 273 * The duration will be positive for a gap and negative for an overlap. 274 * Time-zones are second-based, so the nanosecond part of the duration will be zero. 275 * 276 * @return the duration of the transition, positive for gaps, negative for overlaps 277 */ 278 public Duration getDuration() { 279 return Duration.ofSeconds(getDurationSeconds()); 280 } 281 282 /** 283 * Gets the duration of the transition _in seconds. 284 * 285 * @return the duration _in seconds 286 */ 287 private int getDurationSeconds() { 288 return getOffsetAfter().getTotalSeconds() - getOffsetBefore().getTotalSeconds(); 289 } 290 291 /** 292 * Does this transition represent a gap _in the local time-line. 293 * !(p) 294 * Gaps occur where there are local date-times that simply do not exist. 295 * An example would be when the offset changes from {@code +01:00} to {@code +02:00}. 296 * This might be described as 'the clocks will move forward one hour tonight at 1am'. 297 * 298 * @return true if this transition is a gap, false if it is an overlap 299 */ 300 public bool isGap() { 301 return getOffsetAfter().getTotalSeconds() > getOffsetBefore().getTotalSeconds(); 302 } 303 304 /** 305 * Does this transition represent an overlap _in the local time-line. 306 * !(p) 307 * Overlaps occur where there are local date-times that exist twice. 308 * An example would be when the offset changes from {@code +02:00} to {@code +01:00}. 309 * This might be described as 'the clocks will move back one hour tonight at 2am'. 310 * 311 * @return true if this transition is an overlap, false if it is a gap 312 */ 313 public bool isOverlap() { 314 return getOffsetAfter().getTotalSeconds() < getOffsetBefore().getTotalSeconds(); 315 } 316 317 /** 318 * Checks if the specified offset is valid during this transition. 319 * !(p) 320 * This checks to see if the given offset will be valid at some point _in the transition. 321 * A gap will always return false. 322 * An overlap will return true if the offset is either the before or after offset. 323 * 324 * @param offset the offset to check, null returns false 325 * @return true if the offset is valid during the transition 326 */ 327 public bool isValidOffset(ZoneOffset offset) { 328 return isGap() ? false : (getOffsetBefore() == offset) || (getOffsetAfter()== offset); 329 } 330 331 /** 332 * Gets the valid offsets during this transition. 333 * !(p) 334 * A gap will return an empty list, while an overlap will return both offsets. 335 * 336 * @return the list of valid offsets 337 */ 338 List!(ZoneOffset) getValidOffsets() { 339 if (isGap()) { 340 return new ArrayList!(ZoneOffset)(); 341 } 342 auto l = new ArrayList!(ZoneOffset)(); 343 l.add(getOffsetBefore()); 344 l.add(getOffsetAfter()); 345 return l; 346 } 347 348 //----------------------------------------------------------------------- 349 /** 350 * Compares this transition to another based on the transition instant. 351 * !(p) 352 * This compares the instants of each transition. 353 * The offsets are ignored, making this order inconsistent with equals. 354 * 355 * @param transition the transition to compare to, not null 356 * @return the comparator value, negative if less, positive if greater 357 */ 358 // override 359 public int compareTo(ZoneOffsetTransition transition) { 360 return compare(epochSecond, transition.epochSecond); 361 } 362 363 override 364 public int opCmp(ZoneOffsetTransition transition) { 365 return compare(epochSecond, transition.epochSecond); 366 } 367 368 //----------------------------------------------------------------------- 369 /** 370 * Checks if this object equals another. 371 * !(p) 372 * The entire state of the object is compared. 373 * 374 * @param other the other object to compare to, null returns false 375 * @return true if equal 376 */ 377 override 378 public bool opEquals(Object other) { 379 if (other == this) { 380 return true; 381 } 382 if (cast(ZoneOffsetTransition)(other) !is null) { 383 ZoneOffsetTransition d = cast(ZoneOffsetTransition) other; 384 return epochSecond == d.epochSecond && 385 (offsetBefore == d.offsetBefore) && (offsetAfter == d.offsetAfter); 386 } 387 return false; 388 } 389 390 /** 391 * Returns a suitable hash code. 392 * 393 * @return the hash code 394 */ 395 override 396 public size_t toHash() @trusted nothrow { 397 try{ 398 return transition.toHash() ^ offsetBefore.toHash() ^ Integer.rotateLeft(cast(int)(offsetAfter.toHash()), 16); 399 }catch(Exception e){ 400 return int.init; 401 } 402 } 403 404 //----------------------------------------------------------------------- 405 /** 406 * Returns a string describing this object. 407 * 408 * @return a string for debugging, not null 409 */ 410 override 411 public string toString() { 412 StringBuilder buf = new StringBuilder(); 413 buf.append("Transition[") 414 .append(isGap() ? "Gap" : "Overlap") 415 .append(" at ") 416 .append(transition.toString) 417 .append(offsetBefore.toString) 418 .append(" to ") 419 .append(offsetAfter.toString) 420 .append(']'); 421 return buf.toString(); 422 } 423 424 }