Home Reference Source

packages/core/src/zone/ZoneOffsetTransition.js

/*
 * @copyright (c) 2016, Philipp Thürwächter & Pattrick Hüper
 * @copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
 * @license BSD-3-Clause (see LICENSE in the root directory of this source tree)
 */

import { requireNonNull } from '../assert';
import { IllegalArgumentException } from '../errors';

import { Duration } from '../Duration';
import { LocalDateTime } from '../LocalDateTime';

/**
 * A transition between two offsets caused by a discontinuity in the local time-line.
 *
 * A transition between two offsets is normally the result of a daylight savings cutover.
 * The discontinuity is normally a gap in spring and an overlap in autumn.
 * {@link ZoneOffsetTransition} models the transition between the two offsets.
 *
 * Gaps occur where there are local date-times that simply do not not exist.
 * An example would be when the offset changes from `+03:00` to `+04:00`.
 * This might be described as 'the clocks will move forward one hour tonight at 1am'.
 *
 * Overlaps occur where there are local date-times that exist twice.
 * An example would be when the offset changes from `+04:00` to `+03:00`.
 * This might be described as 'the clocks will move back one hour tonight at 2am'.
 *
 */
export class ZoneOffsetTransition {

    //-----------------------------------------------------------------------
    /**
     * Obtains an instance defining a transition between two offsets.
     *
     * Applications should normally obtain an instance from {@link ZoneRules}.
     * This factory is only intended for use when creating {@link ZoneRules}.
     *
     * @param {LocalDateTime} transition - the transition date-time at the transition, which never
     *  actually occurs, expressed local to the before offset, not null
     * @param {ZoneOffset} offsetBefore - the offset before the transition, not null
     * @param {ZoneOffset} offsetAfter - the offset at and after the transition, not null
     * @return {ZoneOffsetTransition} the transition, not null
     * @throws IllegalArgumentException if {@link offsetBefore} and {@link offsetAfter}
     *         are equal, or {@link transition.getNano} returns non-zero value
     */
    static of(transition, offsetBefore, offsetAfter) {
        return new ZoneOffsetTransition(transition, offsetBefore, offsetAfter);
    }

    /**
     * Creates an instance defining a transition between two offsets.
     * Creates an instance from epoch-second if transition is not a LocalDateTimeInstance
     *
     * @param {(LocalDateTime \ number)} transition - the transition date-time with the offset before the transition, not null
     * @param {ZoneOffset} offsetBefore - the offset before the transition, not null
     * @param {ZoneOffset} offsetAfter - the offset at and after the transition, not null
     * @private
     */
    constructor(transition, offsetBefore, offsetAfter) {
        requireNonNull(transition, 'transition');
        requireNonNull(offsetBefore, 'offsetBefore');
        requireNonNull(offsetAfter, 'offsetAfter');
        if (offsetBefore.equals(offsetAfter)) {
            throw new IllegalArgumentException('Offsets must not be equal');
        }
        if (transition.nano() !== 0) {
            throw new IllegalArgumentException('Nano-of-second must be zero');
        }
        if(transition instanceof LocalDateTime) {
            this._transition = transition;
        } else {
            this._transition = LocalDateTime.ofEpochSecond(transition, 0, offsetBefore);
        }
        this._offsetBefore = offsetBefore;
        this._offsetAfter = offsetAfter;
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the transition instant.
     *
     * This is the instant of the discontinuity, which is defined as the first
     * instant that the 'after' offset applies.
     *
     * The methods {@link getInstant}, {@link getDateTimeBefore} and {@link getDateTimeAfter}
     * all represent the same instant.
     *
     * @return {Instant} the transition instant, not null
     */
    instant() {
        return this._transition.toInstant(this._offsetBefore);
    }

    /**
     * Gets the transition instant as an epoch second.
     *
     * @return {number} the transition epoch second
     */
    toEpochSecond() {
        return this._transition.toEpochSecond(this._offsetBefore);
    }

    //-------------------------------------------------------------------------
    /**
     * Gets the local transition date-time, as would be expressed with the 'before' offset.
     *
     * This is the date-time where the discontinuity begins expressed with the 'before' offset.
     * At this instant, the 'after' offset is actually used, therefore the combination of this
     * date-time and the 'before' offset will never occur.
     *
     * The combination of the 'before' date-time and offset represents the same instant
     * as the 'after' date-time and offset.
     *
     * @return {LocalDateTime} the transition date-time expressed with the before offset, not null
     */
    dateTimeBefore(){
        return this._transition;
    }

    /**
     * Gets the local transition date-time, as would be expressed with the 'after' offset.
     *
     * This is the first date-time after the discontinuity, when the new offset applies.
     *
     * The combination of the 'before' date-time and offset represents the same instant
     * as the 'after' date-time and offset.
     *
     * @return {LocalDateTime} the transition date-time expressed with the after offset, not null
     */
    dateTimeAfter() {
        return this._transition.plusSeconds(this.durationSeconds());
    }

    /**
     * Gets the offset before the transition.
     *
     * This is the offset in use before the instant of the transition.
     *
     * @return {ZoneOffset} the offset before the transition, not null
     */
    offsetBefore() {
        return this._offsetBefore;
    }

    /**
     * Gets the offset after the transition.
     *
     * This is the offset in use on and after the instant of the transition.
     *
     * @return {ZoneOffset} the offset after the transition, not null
     */
    offsetAfter() {
        return this._offsetAfter;
    }

    /**
     * Gets the duration of the transition.
     *
     * In most cases, the transition duration is one hour, however this is not always the case.
     * The duration will be positive for a gap and negative for an overlap.
     * Time-zones are second-based, so the nanosecond part of the duration will be zero.
     *
     * @return {Duration} the duration of the transition, positive for gaps, negative for overlaps
     */
    duration() {
        return Duration.ofSeconds(this.durationSeconds());
    }

    /**
     * Gets the duration of the transition in seconds.
     *
     * @return {number} the duration in seconds
     */
    durationSeconds() {
        return this._offsetAfter.totalSeconds() - this._offsetBefore.totalSeconds();
    }

    /**
     * Does this transition represent a gap in the local time-line.
     *
     * Gaps occur where there are local date-times that simply do not not exist.
     * An example would be when the offset changes from `+01:00` to `+02:00`.
     * This might be described as 'the clocks will move forward one hour tonight at 1am'.
     *
     * @return {boolean} true if this transition is a gap, false if it is an overlap
     */
    isGap() {
        return this._offsetAfter.totalSeconds() > this._offsetBefore.totalSeconds();
    }

    /**
     * Does this transition represent a gap in the local time-line.
     *
     * Overlaps occur where there are local date-times that exist twice.
     * An example would be when the offset changes from `+02:00` to `+01:00`.
     * This might be described as 'the clocks will move back one hour tonight at 2am'.
     *
     * @return {boolean} true if this transition is an overlap, false if it is a gap
     */
    isOverlap() {
        return this._offsetAfter.totalSeconds() < this._offsetBefore.totalSeconds();
    }

    /**
     * Checks if the specified offset is valid during this transition.
     *
     * This checks to see if the given offset will be valid at some point in the transition.
     * A gap will always return false.
     * An overlap will return true if the offset is either the before or after offset.
     *
     * @param {ZoneOffset} offset - the offset to check, null returns false
     * @return {boolean} true if the offset is valid during the transition
     */
    isValidOffset(offset) {
        return this.isGap() ? false : (this._offsetBefore.equals(offset) || this._offsetAfter.equals(offset));
    }

    /**
     * Gets the valid offsets during this transition.
     *
     * A gap will return an empty list, while an overlap will return both offsets.
     *
     * @return {ZoneOffset[]} the list of valid offsets
     */
    validOffsets() {
        if (this.isGap()){
            return [];
        } else {
            return [this._offsetBefore, this._offsetAfter];
        }
    }

    //-----------------------------------------------------------------------
    /**
     * Compares this transition to another based on the transition instant.
     *
     * This compares the instants of each transition.
     * The offsets are ignored, making this order inconsistent with equals.
     *
     * @param {ZoneOffsetTransition} transition - the transition to compare to, not null
     * @return {number} the comparator value, negative if less, positive if greater
     */
    compareTo(transition) {
        return this.instant().compareTo(transition.instant());
    }

    //-----------------------------------------------------------------------
    /**
     * Checks if this object equals another.
     *
     * The entire state of the object is compared.
     *
     * @param {*} other - the other object to compare to, null returns false
     * @return true if equal
     */
    equals(other) {
        if (other === this) {
            return true;
        }
        if (other instanceof ZoneOffsetTransition) {
            const d = other;
            return this._transition.equals(d._transition) &&
                this._offsetBefore.equals(d.offsetBefore()) && this._offsetAfter.equals(d.offsetAfter());
        }
        return false;
    }

    /**
     * Returns a suitable hash code.
     *
     * @return {number} the hash code
     */
    hashCode() {
        return this._transition.hashCode() ^ this._offsetBefore.hashCode() ^ (this._offsetAfter.hashCode()>>>16);
    }

    //-----------------------------------------------------------------------
    /**
     * Returns a string describing this object.
     *
     * @return {string} a string for debugging, not null
     */
    toString() {
        return `Transition[${this.isGap() ? 'Gap' : 'Overlap' 
        } at ${this._transition.toString()}${this._offsetBefore.toString() 
        } to ${this._offsetAfter}]`;
    }

}