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