/*
* -----------------------------------------------------------------------
* Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/>
* -----------------------------------------------------------------------
* This file (RuleBasedTransitionModel.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.PlainTimestamp;
import net.time4j.base.GregorianDate;
import net.time4j.base.GregorianMath;
import net.time4j.base.MathUtils;
import net.time4j.base.UnixTime;
import net.time4j.base.WallTime;
import net.time4j.engine.EpochDays;
import net.time4j.format.CalendarText;
import net.time4j.tz.ZonalOffset;
import net.time4j.tz.ZonalTransition;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* <p>Regelbasiertes Übergangsmodell. </p>
*
* @author Meno Hochschild
* @since 2.2
* @serial include
* @doctags.concurrency {immutable}
*/
final class RuleBasedTransitionModel
extends TransitionModel {
//~ Statische Felder/Initialisierungen --------------------------------
private static final int LAST_CACHED_YEAR;
static {
long ly = TransitionModel.getFutureMoment(100);
long mjd = EpochDays.MODIFIED_JULIAN_DATE.transform(ly, EpochDays.UNIX);
LAST_CACHED_YEAR =
GregorianMath.readYear(GregorianMath.toPackedDate(mjd));
}
private static final long serialVersionUID = 2456700806862862287L;
//~ Instanzvariablen --------------------------------------------------
private transient final ZonalTransition initial;
private transient final List<DaylightSavingRule> rules;
private transient final ConcurrentMap<Integer, List<ZonalTransition>> tCache = new ConcurrentHashMap<>();
private transient final List<ZonalTransition> stdTransitions;
private transient final boolean gregorian;
//~ Konstruktoren -----------------------------------------------------
RuleBasedTransitionModel(
ZonalOffset stdOffset,
List<DaylightSavingRule> rules
) {
this(stdOffset, rules, true);
}
RuleBasedTransitionModel(
ZonalOffset stdOffset,
List<DaylightSavingRule> rules,
boolean create
) {
this(
new ZonalTransition(
Long.MIN_VALUE,
stdOffset.getIntegralAmount(),
stdOffset.getIntegralAmount(),
0),
rules,
create
);
}
RuleBasedTransitionModel(
ZonalTransition initial,
List<DaylightSavingRule> rules,
boolean create
) {
super();
// various data sanity checks
if (rules.isEmpty()) {
throw new IllegalArgumentException(
"Missing daylight saving rules.");
} else if (rules.size() >= 128) {
throw new IllegalArgumentException(
"Too many daylight saving rules: " + rules);
}
if (create) {
rules = new ArrayList<>(rules);
}
List<DaylightSavingRule> sortedRules = rules;
Collections.sort(sortedRules, RuleComparator.INSTANCE);
boolean hasRuleWithoutDST = false;
String calendarType = null;
if (sortedRules.size() > 1) {
for (DaylightSavingRule rule : sortedRules) {
if (rule.getSavings() == 0) {
hasRuleWithoutDST = true;
}
if (calendarType == null) {
calendarType = rule.getCalendarType();
} else if (!calendarType.equals(rule.getCalendarType())) {
throw new IllegalArgumentException(
"Rules with different calendar systems not permitted.");
}
}
if (!hasRuleWithoutDST) {
throw new IllegalArgumentException(
"No daylight saving rule with zero dst-offset found: "
+ rules);
}
}
this.gregorian = CalendarText.ISO_CALENDAR_TYPE.equals(calendarType);
ZonalTransition zt = initial;
if (initial.getPosixTime() == Long.MIN_VALUE) {
if (initial.isDaylightSaving()) {
throw new IllegalArgumentException(
"Initial transition must not have any dst-offset: "
+ initial);
}
zt = new ZonalTransition(
Moment.axis().getMinimum().getPosixTime(),
initial.getStandardOffset(),
initial.getStandardOffset(),
0
);
} else {
ZonalTransition first =
getNextTransition(initial.getPosixTime(), initial, sortedRules);
if (initial.getTotalOffset() != first.getPreviousOffset()) {
throw new IllegalArgumentException(
"Inconsistent model: " + initial + " / " + rules);
}
}
// state initialization
this.initial = zt;
this.rules = Collections.unmodifiableList(sortedRules);
// fill standard transition cache
long end = TransitionModel.getFutureMoment(1);
this.stdTransitions = getTransitions(this.initial, this.rules, 0L, end);
}
//~ Methoden ----------------------------------------------------------
@Override
public ZonalOffset getInitialOffset() {
return ZonalOffset.ofTotalSeconds(this.initial.getTotalOffset());
}
@Override
public ZonalTransition getStartTransition(UnixTime ut) {
long preModel = this.initial.getPosixTime();
if (ut.getPosixTime() <= preModel) {
return null;
}
ZonalTransition current = null;
int stdOffset = this.initial.getStandardOffset();
int n = this.rules.size();
DaylightSavingRule rule = this.rules.get(0);
DaylightSavingRule previous = this.rules.get(n - 1);
int shift = getShift(rule, stdOffset, previous.getSavings());
int year = getYear(rule, ut.getPosixTime() + shift);
List<ZonalTransition> transitions = this.getTransitions(year);
for (int i = 0; i < n; i++) {
ZonalTransition zt = transitions.get(i);
long tt = zt.getPosixTime();
if (ut.getPosixTime() < tt) {
if (current == null) {
if (i == 0) {
zt = this.getTransitions(year - 1).get(n - 1);
} else {
zt = transitions.get(i - 1);
}
if (zt.getPosixTime() > preModel) {
current = zt;
}
}
break;
} else if (tt > preModel) {
current = zt;
}
}
return current;
}
@Override
public ZonalTransition getConflictTransition(
GregorianDate localDate,
WallTime localTime
) {
long localSecs = TransitionModel.toLocalSecs(localDate, localTime);
return this.getConflictTransition(localDate, localSecs);
}
@Override
public Optional<ZonalTransition> findNextTransition(UnixTime ut) {
ZonalTransition transition = getNextTransition(ut.getPosixTime(), this.initial, this.rules);
return ((transition == null) ? Optional.empty() : Optional.of(transition));
}
@Override
public List<ZonalOffset> getValidOffsets(
GregorianDate localDate,
WallTime localTime
) {
long localSecs = TransitionModel.toLocalSecs(localDate, localTime);
return this.getValidOffsets(localDate, localSecs);
}
@Override
public List<ZonalTransition> getStdTransitions() {
return this.stdTransitions;
}
@Override
public List<ZonalTransition> getTransitions(
UnixTime startInclusive,
UnixTime endExclusive
) {
return getTransitions(
this.initial,
this.rules,
startInclusive.getPosixTime(),
endExclusive.getPosixTime());
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj instanceof RuleBasedTransitionModel) {
RuleBasedTransitionModel that = (RuleBasedTransitionModel) obj;
return (
this.initial.equals(that.initial)
&& this.rules.equals(that.rules));
} else {
return false;
}
}
@Override
public int hashCode() {
return 17 * this.initial.hashCode() + 37 * this.rules.hashCode();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(256);
sb.append(this.getClass().getName());
sb.append("[initial=");
sb.append(this.initial);
sb.append(",rules=");
sb.append(this.rules);
sb.append(']');
return sb.toString();
}
@Override
public void dump(Appendable buffer) throws IOException {
buffer.append("*** Last rules:").append(NEW_LINE);
for (DaylightSavingRule rule : this.rules) {
buffer.append(">>> ").append(rule.toString()).append(NEW_LINE);
}
}
/**
* <p>Benutzt in der Serialisierung. </p>
*
* @return ZonalTransition
*/
ZonalTransition getInitialTransition() {
return this.initial;
}
/**
* <p>Benutzt in der Serialisierung. </p>
*
* @return list of rules
*/
List<DaylightSavingRule> getRules() {
return this.rules;
}
ZonalTransition getConflictTransition(
GregorianDate localDate,
long localSecs
) {
long preModel = this.initial.getPosixTime();
int max =
Math.max(
this.initial.getPreviousOffset(),
this.initial.getTotalOffset());
if (localSecs <= preModel + max) {
return null;
}
for (ZonalTransition t : this.getTransitions(localDate)) {
long tt = t.getPosixTime();
if (t.isGap()) {
if (localSecs < tt + t.getPreviousOffset()) {
return null; // offset = t.getPreviousOffset()
} else if (localSecs < tt + t.getTotalOffset()) {
return t;
}
} else if (t.isOverlap()) {
if (localSecs < tt + t.getTotalOffset()) {
return null; // offset = t.getPreviousOffset()
} else if (localSecs < tt + t.getPreviousOffset()) {
return t;
}
}
}
return null; // offset = lastTotalOffset
}
List<ZonalOffset> getValidOffsets(
GregorianDate localDate,
long localSecs
) {
long preModel = this.initial.getPosixTime();
int last = this.initial.getTotalOffset();
int max = Math.max(this.initial.getPreviousOffset(), last);
if (localSecs <= preModel + max) {
return TransitionModel.toList(last);
}
for (ZonalTransition t : this.getTransitions(localDate)) {
long tt = t.getPosixTime();
last = t.getTotalOffset();
if (t.isGap()) {
if (localSecs < tt + t.getPreviousOffset()) {
return TransitionModel.toList(t.getPreviousOffset());
} else if (localSecs < tt + last) {
return Collections.emptyList();
}
} else if (t.isOverlap()) {
if (localSecs < tt + last) {
return TransitionModel.toList(t.getPreviousOffset());
} else if (localSecs < tt + t.getPreviousOffset()) {
return TransitionModel.toList(last, t.getPreviousOffset());
}
}
}
return TransitionModel.toList(last);
}
static List<ZonalTransition> getTransitions(
ZonalTransition initial,
List<DaylightSavingRule> rules,
long startInclusive,
long endExclusive
) {
long preModel = initial.getPosixTime();
if (startInclusive > endExclusive) {
throw new IllegalArgumentException("Start after end.");
} else if (
(endExclusive <= preModel)
|| (startInclusive == endExclusive)
) {
return Collections.emptyList();
}
List<ZonalTransition> transitions = new ArrayList<>();
int year = Integer.MIN_VALUE;
int n = rules.size();
int i = 0;
int stdOffset = initial.getStandardOffset();
while (true) {
DaylightSavingRule rule = rules.get(i % n);
DaylightSavingRule previous = rules.get((i - 1 + n) % n);
int shift = getShift(rule, stdOffset, previous.getSavings());
if (i == 0) {
year = getYear(rule, Math.max(startInclusive, preModel) + shift);
} else if ((i % n) == 0) {
year++;
}
long tt = getTransitionTime(rule, year, shift);
i++;
if (tt >= endExclusive) {
break;
} else if (
(tt >= startInclusive)
&& (tt > preModel)
) {
transitions.add(
new ZonalTransition(
tt,
stdOffset + previous.getSavings(),
stdOffset + rule.getSavings(),
rule.getSavings()));
}
}
return Collections.unmodifiableList(transitions);
}
private static ZonalTransition getNextTransition(
long ut,
ZonalTransition initial,
List<DaylightSavingRule> rules
) {
long start = Math.max(ut, initial.getPosixTime());
int year = Integer.MIN_VALUE;
int stdOffset = initial.getStandardOffset();
ZonalTransition next = null;
for (int i = 0, n = rules.size(); next == null; i++) {
DaylightSavingRule rule = rules.get(i % n);
DaylightSavingRule previous = rules.get((i - 1 + n) % n);
int shift = getShift(rule, stdOffset, previous.getSavings());
if (i == 0) {
year = getYear(rule, start + shift);
} else if ((i % n) == 0) {
year++;
}
long tt = getTransitionTime(rule, year, shift);
if (tt > start) {
next =
new ZonalTransition(
tt,
stdOffset + previous.getSavings(),
stdOffset + rule.getSavings(),
rule.getSavings());
}
}
return next;
}
private static int getShift(
DaylightSavingRule rule,
int stdOffset,
int dstOffset
) {
OffsetIndicator indicator = rule.getIndicator();
switch (indicator) {
case UTC_TIME:
return 0;
case STANDARD_TIME:
return stdOffset;
case WALL_TIME:
return stdOffset + dstOffset;
default:
throw new UnsupportedOperationException(indicator.name());
}
}
private static long getTransitionTime(
DaylightSavingRule rule,
int year,
int shift
) {
PlainTimestamp tsp = rule.getDate(year).at(rule.getTimeOfDay());
return tsp.at(ZonalOffset.ofTotalSeconds(shift)).getPosixTime();
}
private List<ZonalTransition> getTransitions(GregorianDate date) {
return this.getTransitions(this.rules.get(0).toCalendarYear(date));
}
private List<ZonalTransition> getTransitions(int year) {
Integer key = Integer.valueOf(year);
List<ZonalTransition> transitions = this.tCache.get(key);
if (transitions == null) {
List<ZonalTransition> list = new ArrayList<>();
int stdOffset = this.initial.getStandardOffset();
for (int i = 0, n = this.rules.size(); i < n; i++) {
DaylightSavingRule rule = this.rules.get(i);
DaylightSavingRule previous = this.rules.get((i - 1 + n) % n);
int shift = getShift(rule, stdOffset, previous.getSavings());
list.add(
new ZonalTransition(
getTransitionTime(rule, year, shift),
stdOffset + previous.getSavings(),
stdOffset + rule.getSavings(),
rule.getSavings()));
}
transitions = Collections.unmodifiableList(list);
if (
(year <= LAST_CACHED_YEAR)
&& this.gregorian
) {
List<ZonalTransition> old =
this.tCache.putIfAbsent(key, transitions);
if (old != null) {
transitions = old;
}
}
}
return transitions;
}
private static int getYear(
DaylightSavingRule rule,
long localSecs
) {
return rule.toCalendarYear(
EpochDays.MODIFIED_JULIAN_DATE.transform(
MathUtils.floorDivide(localSecs, 86400),
EpochDays.UNIX)
);
}
/**
* @serialData Uses a specialized serialisation form as proxy. The format
* is bit-compressed. The first byte contains the type id
* {@code 125}. Then the data bytes for the internal
* rules follow. The complex algorithm exploits the fact
* that allmost all transitions happen at full hours around
* midnight. Insight in details see source code.
*
* @return replacement object in serialization graph
*/
private Object writeReplace() {
return new SPX(this, SPX.RULE_BASED_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.");
}
}