/* * ----------------------------------------------------------------------- * Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/> * ----------------------------------------------------------------------- * This file (ArrayTransitionModel.java) is part of project Time4J. * * Time4J is free software: You can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation, either version 2.1 of the License, or * (at your option) any later version. * * Time4J is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Time4J. If not, see <http://www.gnu.org/licenses/>. * ----------------------------------------------------------------------- */ package net.time4j.tz.model; import net.time4j.Moment; import net.time4j.base.GregorianDate; import net.time4j.base.UnixTime; import net.time4j.base.WallTime; import net.time4j.scale.TimeScale; import net.time4j.tz.ZonalOffset; import net.time4j.tz.ZonalTransition; import java.io.IOException; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.ObjectOutput; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; /** * <p>Array-basiertes Übergangsmodell. </p> * * @author Meno Hochschild * @since 2.2 * @serial include * @doctags.concurrency {immutable} */ final class ArrayTransitionModel extends TransitionModel { //~ Statische Felder/Initialisierungen -------------------------------- private static final long serialVersionUID = -5264909488983076587L; //~ Instanzvariablen -------------------------------------------------- private transient final ZonalTransition[] transitions; // Cache private transient final List<ZonalTransition> stdTransitions; private transient int hash = 0; //~ Konstruktoren ----------------------------------------------------- ArrayTransitionModel(List<ZonalTransition> transitions) { this(transitions, true, true); } ArrayTransitionModel( List<ZonalTransition> transitions, boolean create, boolean sanityCheck ) { super(); if (transitions.isEmpty()) { throw new IllegalArgumentException("Missing timezone transitions."); } // initialize state int n = transitions.size(); ZonalTransition[] tmp = new ZonalTransition[n]; tmp = transitions.toArray(tmp); if (create) { Arrays.sort(tmp); } if (sanityCheck) { checkSanity(tmp, transitions); } this.transitions = tmp; // fill standard transition cache long end = TransitionModel.getFutureMoment(1); this.stdTransitions = getTransitions(this.transitions, 0L, end); } //~ Methoden ---------------------------------------------------------- @Override public ZonalOffset getInitialOffset() { return ZonalOffset.ofTotalSeconds( this.transitions[0].getPreviousOffset()); } @Override public ZonalTransition getStartTransition(UnixTime ut) { int index = search(ut.getPosixTime(), this.transitions); return ( (index == 0) ? null : this.transitions[index - 1]); } @Override public ZonalTransition getConflictTransition( GregorianDate localDate, WallTime localTime ) { return this.getConflictTransition(localDate, localTime, null); } @Override public Optional<ZonalTransition> findNextTransition(UnixTime ut) { int index = search(ut.getPosixTime(), this.transitions); return ( (index == this.transitions.length) ? Optional.empty() : Optional.of(this.transitions[index])); } @Override public List<ZonalOffset> getValidOffsets( GregorianDate localDate, WallTime localTime ) { return this.getValidOffsets(localDate, localTime, null); } @Override public List<ZonalTransition> getStdTransitions() { return this.stdTransitions; } @Override public List<ZonalTransition> getTransitions( UnixTime startInclusive, UnixTime endExclusive ) { return getTransitions( this.transitions, startInclusive.getPosixTime(), endExclusive.getPosixTime()); } @Override public void dump(Appendable buffer) throws IOException { this.dump(this.transitions.length, buffer); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } else if (obj instanceof ArrayTransitionModel) { ArrayTransitionModel that = (ArrayTransitionModel) obj; return Arrays.equals(this.transitions, that.transitions); } else { return false; } } @Override public int hashCode() { int h = this.hash; if (h == 0) { h = Arrays.hashCode(this.transitions); this.hash = h; } return h; } @Override public String toString() { StringBuilder sb = new StringBuilder(32); sb.append(this.getClass().getName()); sb.append("[transition-count="); sb.append(this.transitions.length); sb.append(",hash="); sb.append(this.hashCode()); sb.append(']'); return sb.toString(); } /** * <p>Wird von {@link #getConflictTransition(GregorianDate, WallTime)} * aufgerufen. </p> * * @param localDate local date in timezone * @param localTime local wall time in timezone * @param ruleModel optional last rules * @return conflict transition on the local time axis for gaps or * overlaps else {@code null} */ ZonalTransition getConflictTransition( GregorianDate localDate, WallTime localTime, RuleBasedTransitionModel ruleModel // from CompositeTransitionModel ) { long localSecs = TransitionModel.toLocalSecs(localDate, localTime); int index = searchLocal(localSecs, this.transitions); if (index == this.transitions.length) { return ( (ruleModel == null) ? null : ruleModel.getConflictTransition(localDate, localSecs)); } ZonalTransition test = this.transitions[index]; if (test.isGap()) { assert (test.getPosixTime() + test.getTotalOffset() > localSecs); if (test.getPosixTime() + test.getPreviousOffset() <= localSecs) { return test; } } else if (test.isOverlap()) { assert (test.getPosixTime() + test.getPreviousOffset() > localSecs); if (test.getPosixTime() + test.getTotalOffset() <= localSecs) { return test; } } return null; } /** * <p>Wird von {@link #getValidOffsets(GregorianDate, WallTime)} * aufgerufen. </p> * * @param localDate local date in timezone * @param localTime local wall time in timezone * @param ruleModel optional last rules * @return unmodifiable list of shifts which fits the given local time */ List<ZonalOffset> getValidOffsets( GregorianDate localDate, WallTime localTime, RuleBasedTransitionModel ruleModel // from CompositeTransitionModel ) { long localSecs = TransitionModel.toLocalSecs(localDate, localTime); int index = searchLocal(localSecs, this.transitions); if (index == this.transitions.length) { if (ruleModel == null) { ZonalTransition last = this.transitions[this.transitions.length - 1]; return TransitionModel.toList(last.getTotalOffset()); } else { return ruleModel.getValidOffsets(localDate, localSecs); } } ZonalTransition test = this.transitions[index]; if (test.isGap()) { assert (test.getPosixTime() + test.getTotalOffset() > localSecs); if (test.getPosixTime() + test.getPreviousOffset() <= localSecs) { return Collections.emptyList(); } } else if (test.isOverlap()) { assert (test.getPosixTime() + test.getPreviousOffset() > localSecs); if (test.getPosixTime() + test.getTotalOffset() <= localSecs) { return TransitionModel.toList( test.getTotalOffset(), test.getPreviousOffset()); } } return TransitionModel.toList(test.getPreviousOffset()); } // Called by CompositeTransitionModel void dump( int size, Appendable buffer ) throws IOException { for (int i = 0; i < size; i++) { ZonalTransition transition = this.transitions[i]; TransitionModel.dump(transition, buffer); } } // Called by CompositeTransitionModel ZonalTransition getLastTransition() { return this.transitions[this.transitions.length - 1]; } // Called by CompositeTransitionModel boolean equals( ArrayTransitionModel other, int s1, int s2 ) { int n1 = Math.min(s1, this.transitions.length); int n2 = Math.min(s2, other.transitions.length); if (n1 != n2) { return false; } for (int i = 0; i < n1; i++) { if (!this.transitions[i].equals(other.transitions[i])) { return false; } } return true; } // Called by CompositeTransitionModel int hashCode(int size) { int n = Math.min(size, this.transitions.length); ZonalTransition[] tmp = new ZonalTransition[n]; System.arraycopy(this.transitions, 0, tmp, 0, n); return Arrays.hashCode(tmp); } // Called by CompositeTransitionModel static void checkSanity( ZonalTransition[] transitions, List<ZonalTransition> original ) { int previous = transitions[0].getTotalOffset(); for (int i = 1; i < transitions.length; i++) { if (previous != transitions[i].getPreviousOffset()) { Moment m = Moment.of(transitions[i].getPosixTime(), TimeScale.POSIX); throw new IllegalArgumentException( "Model inconsistency detected at: " + m + " (" + transitions[i].getPosixTime() + ") " + " in transitions: " + original); } else { previous = transitions[i].getTotalOffset(); } } } /** * <p>Benutzt in der Serialisierung. </p> * * @param out serialization stream */ void writeTransitions(ObjectOutput out) throws IOException { this.writeTransitions(this.transitions.length, out); } /** * <p>Benutzt in der Serialisierung. </p> * * @param size maximum count of transitions to be serialized * @param out serialization stream */ void writeTransitions( int size, ObjectOutput out ) throws IOException { SPX.writeTransitions(this.transitions, size, out); } private static List<ZonalTransition> getTransitions( ZonalTransition[] transitions, long startInclusive, long endExclusive ) { if (startInclusive > endExclusive) { throw new IllegalArgumentException("Start after end."); } int i1 = search(startInclusive, transitions); int i2 = search(endExclusive, transitions); if (i2 == 0) { return Collections.emptyList(); } else if ((i1 > 0) && (transitions[i1 - 1].getPosixTime() == startInclusive)) { i1--; } i2--; if (transitions[i2].getPosixTime() == endExclusive) { i2--; } if (i1 > i2) { return Collections.emptyList(); } else { List<ZonalTransition> result = new ArrayList<>(i2 - i1 + 1); for (int i = i1; i <= i2; i++) { result.add(transitions[i]); } return Collections.unmodifiableList(result); } } // returns index of first transition after posixTime private static int search( long posixTime, ZonalTransition[] transitions ) { int low = 0; int high = transitions.length - 1; while (low <= high) { int middle = (low + high) / 2; if (transitions[middle].getPosixTime() <= posixTime) { low = middle + 1; } else { high = middle - 1; } } return low; } // returns index of first transition after local date and time private static int searchLocal( long localSecs, ZonalTransition[] transitions ) { int low = 0; int high = transitions.length - 1; while (low <= high) { int middle = (low + high) / 2; ZonalTransition zt = transitions[middle]; int offset = Math.max(zt.getTotalOffset(), zt.getPreviousOffset()); if (zt.getPosixTime() + offset <= localSecs) { low = middle + 1; } else { high = middle - 1; } } return low; } /** * @serialData Uses a specialized serialisation form as proxy. The format * is bit-compressed. The first byte contains the type id * {@code 126}. Then the data bytes for the internal * transitions follow. The complex algorithm exploits the * fact that allmost all transitions happen at full hours * around midnight in local standard time. Insight in details * see source code. * * @return replacement object in serialization graph */ private Object writeReplace() { return new SPX(this, SPX.ARRAY_TRANSITION_MODEL_TYPE); } /** * @serialData Blocks because a serialization proxy is required. * @param in object input stream * @throws InvalidObjectException (always) */ private void readObject(ObjectInputStream in) throws IOException { throw new InvalidObjectException("Serialization proxy required."); } }