9d07dc02e1
Reviewed-by: alanb, chegar, lancea, prr
854 lines
32 KiB
Java
854 lines
32 KiB
Java
/*
|
|
* Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation. Oracle designates this
|
|
* particular file as subject to the "Classpath" exception as provided
|
|
* by Oracle in the LICENSE file that accompanied this code.
|
|
*
|
|
* This code 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 General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
|
|
package build.tools.tzdb;
|
|
|
|
import java.io.IOException;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.Paths;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Map.Entry;
|
|
import java.util.NavigableMap;
|
|
import java.util.Objects;
|
|
import java.util.Set;
|
|
import java.util.TreeMap;
|
|
import java.util.TreeSet;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.time.*;
|
|
import java.time.Year;
|
|
import java.time.chrono.IsoChronology;
|
|
import java.time.temporal.TemporalAdjusters;
|
|
import java.time.zone.ZoneOffsetTransition;
|
|
import java.time.zone.ZoneOffsetTransitionRule;
|
|
import java.time.zone.ZoneOffsetTransitionRule.TimeDefinition;
|
|
import java.time.zone.ZoneRulesException;
|
|
|
|
/**
|
|
* Compile and build time-zone rules from IANA timezone data
|
|
*
|
|
* @author Xueming Shen
|
|
* @author Stephen Colebourne
|
|
* @author Michael Nascimento Santos
|
|
*
|
|
* @since 9
|
|
*/
|
|
|
|
class TzdbZoneRulesProvider {
|
|
|
|
/**
|
|
* Creates an instance.
|
|
*
|
|
* @throws ZoneRulesException if unable to load
|
|
*/
|
|
public TzdbZoneRulesProvider(List<Path> files) {
|
|
try {
|
|
load(files);
|
|
} catch (Exception ex) {
|
|
throw new ZoneRulesException("Unable to load TZDB time-zone rules", ex);
|
|
}
|
|
}
|
|
|
|
public Set<String> getZoneIds() {
|
|
return new TreeSet(regionIds);
|
|
}
|
|
|
|
public Map<String, String> getAliasMap() {
|
|
return links;
|
|
}
|
|
|
|
public ZoneRules getZoneRules(String zoneId) {
|
|
Object obj = zones.get(zoneId);
|
|
if (obj == null) {
|
|
String zoneId0 = zoneId;
|
|
if (links.containsKey(zoneId)) {
|
|
zoneId = links.get(zoneId);
|
|
obj = zones.get(zoneId);
|
|
}
|
|
if (obj == null) {
|
|
// Timezone link can be located in 'backward' file and it
|
|
// can refer to another link, so we need to check for
|
|
// link one more time, before throwing an exception
|
|
String zoneIdBack = zoneId;
|
|
if (links.containsKey(zoneId)) {
|
|
zoneId = links.get(zoneId);
|
|
obj = zones.get(zoneId);
|
|
}
|
|
if (obj == null) {
|
|
throw new ZoneRulesException("Unknown time-zone ID: " + zoneIdBack);
|
|
}
|
|
}
|
|
}
|
|
if (obj instanceof ZoneRules) {
|
|
return (ZoneRules)obj;
|
|
}
|
|
try {
|
|
ZoneRules zrules = buildRules(zoneId, (List<ZoneLine>)obj);
|
|
zones.put(zoneId, zrules);
|
|
return zrules;
|
|
} catch (Exception ex) {
|
|
throw new ZoneRulesException(
|
|
"Invalid binary time-zone data: TZDB:" + zoneId, ex);
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* All the regions that are available.
|
|
*/
|
|
private List<String> regionIds = new ArrayList<>(600);
|
|
|
|
/**
|
|
* Zone region to rules mapping
|
|
*/
|
|
private final Map<String, Object> zones = new ConcurrentHashMap<>();
|
|
|
|
/**
|
|
* compatibility list
|
|
*/
|
|
private static HashSet<String> excludedZones;
|
|
static {
|
|
// (1) exclude EST, HST and MST. They are supported
|
|
// via the short-id mapping
|
|
// (2) remove UTC and GMT
|
|
// (3) remove ROC, which is not supported in j.u.tz
|
|
excludedZones = new HashSet<>(10);
|
|
excludedZones.add("EST");
|
|
excludedZones.add("HST");
|
|
excludedZones.add("MST");
|
|
excludedZones.add("GMT+0");
|
|
excludedZones.add("GMT-0");
|
|
excludedZones.add("ROC");
|
|
}
|
|
|
|
private Map<String, String> links = new HashMap<>(150);
|
|
private Map<String, List<RuleLine>> rules = new HashMap<>(500);
|
|
|
|
private void load(List<Path> files) throws IOException {
|
|
|
|
for (Path file : files) {
|
|
List<ZoneLine> openZone = null;
|
|
try {
|
|
for (String line : Files.readAllLines(file, StandardCharsets.ISO_8859_1)) {
|
|
if (line.length() == 0 || line.charAt(0) == '#') {
|
|
continue;
|
|
}
|
|
//StringIterator itr = new StringIterator(line);
|
|
String[] tokens = split(line);
|
|
if (openZone != null && // continuing zone line
|
|
Character.isWhitespace(line.charAt(0)) &&
|
|
tokens.length > 0) {
|
|
ZoneLine zLine = new ZoneLine();
|
|
openZone.add(zLine);
|
|
if (zLine.parse(tokens, 0)) {
|
|
openZone = null;
|
|
}
|
|
continue;
|
|
}
|
|
if (line.startsWith("Zone")) { // parse Zone line
|
|
String name = tokens[1];
|
|
if (excludedZones.contains(name)){
|
|
continue;
|
|
}
|
|
if (zones.containsKey(name)) {
|
|
throw new IllegalArgumentException(
|
|
"Duplicated zone name in file: " + name +
|
|
", line: [" + line + "]");
|
|
}
|
|
openZone = new ArrayList<>(10);
|
|
zones.put(name, openZone);
|
|
regionIds.add(name);
|
|
ZoneLine zLine = new ZoneLine();
|
|
openZone.add(zLine);
|
|
if (zLine.parse(tokens, 2)) {
|
|
openZone = null;
|
|
}
|
|
} else if (line.startsWith("Rule")) { // parse Rule line
|
|
String name = tokens[1];
|
|
if (!rules.containsKey(name)) {
|
|
rules.put(name, new ArrayList<RuleLine>(10));
|
|
}
|
|
rules.get(name).add(new RuleLine().parse(tokens));
|
|
} else if (line.startsWith("Link")) { // parse link line
|
|
if (tokens.length >= 3) {
|
|
String realId = tokens[1];
|
|
String aliasId = tokens[2];
|
|
if (excludedZones.contains(aliasId)){
|
|
continue;
|
|
}
|
|
links.put(aliasId, realId);
|
|
regionIds.add(aliasId);
|
|
} else {
|
|
throw new IllegalArgumentException(
|
|
"Invalid Link line in file" +
|
|
file + ", line: [" + line + "]");
|
|
}
|
|
} else {
|
|
// skip unknown line
|
|
}
|
|
}
|
|
|
|
} catch (Exception ex) {
|
|
throw new RuntimeException("Failed while processing file [" + file +
|
|
"]", ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
private String[] split(String str) {
|
|
int off = 0;
|
|
int end = str.length();
|
|
ArrayList<String> list = new ArrayList<>(10);
|
|
while (off < end) {
|
|
char c = str.charAt(off);
|
|
if (c == '\t' || c == ' ') {
|
|
off++;
|
|
continue;
|
|
}
|
|
if (c == '#') { // comment
|
|
break;
|
|
}
|
|
int start = off;
|
|
while (off < end) {
|
|
c = str.charAt(off);
|
|
if (c == ' ' || c == '\t') {
|
|
break;
|
|
}
|
|
off++;
|
|
}
|
|
if (start != off) {
|
|
list.add(str.substring(start, off));
|
|
}
|
|
}
|
|
return list.toArray(new String[list.size()]);
|
|
}
|
|
|
|
/**
|
|
* Class representing a month-day-time in the TZDB file.
|
|
*/
|
|
private static abstract class MonthDayTime {
|
|
/** The month of the cutover. */
|
|
Month month = Month.JANUARY;
|
|
|
|
/** The day-of-month of the cutover. */
|
|
int dayOfMonth = 1;
|
|
|
|
/** Whether to adjust forwards. */
|
|
boolean adjustForwards = true;
|
|
|
|
/** The day-of-week of the cutover. */
|
|
DayOfWeek dayOfWeek;
|
|
|
|
/** The time of the cutover, in second of day */
|
|
int secsOfDay = 0;
|
|
|
|
/** Whether this is midnight end of day. */
|
|
boolean endOfDay;
|
|
/** The time of the cutover. */
|
|
|
|
TimeDefinition timeDefinition = TimeDefinition.WALL;
|
|
|
|
void adjustToForwards(int year) {
|
|
if (adjustForwards == false && dayOfMonth > 0) {
|
|
// weekDay<=monthDay case, don't have it in tzdb data for now
|
|
LocalDate adjustedDate = LocalDate.of(year, month, dayOfMonth).minusDays(6);
|
|
dayOfMonth = adjustedDate.getDayOfMonth();
|
|
month = adjustedDate.getMonth();
|
|
adjustForwards = true;
|
|
}
|
|
}
|
|
|
|
LocalDateTime toDateTime(int year) {
|
|
LocalDate date;
|
|
if (dayOfMonth < 0) {
|
|
int monthLen = month.length(IsoChronology.INSTANCE.isLeapYear(year));
|
|
date = LocalDate.of(year, month, monthLen + 1 + dayOfMonth);
|
|
if (dayOfWeek != null) {
|
|
date = date.with(TemporalAdjusters.previousOrSame(dayOfWeek));
|
|
}
|
|
} else {
|
|
date = LocalDate.of(year, month, dayOfMonth);
|
|
if (dayOfWeek != null) {
|
|
date = date.with(TemporalAdjusters.nextOrSame(dayOfWeek));
|
|
}
|
|
}
|
|
if (endOfDay) {
|
|
date = date.plusDays(1);
|
|
}
|
|
return LocalDateTime.of(date, LocalTime.ofSecondOfDay(secsOfDay));
|
|
}
|
|
|
|
/**
|
|
* Parses the MonthDaytime segment of a tzdb line.
|
|
*/
|
|
private void parse(String[] tokens, int off) {
|
|
month = parseMonth(tokens[off++]);
|
|
if (off < tokens.length) {
|
|
String dayRule = tokens[off++];
|
|
if (dayRule.startsWith("last")) {
|
|
dayOfMonth = -1;
|
|
dayOfWeek = parseDayOfWeek(dayRule.substring(4));
|
|
adjustForwards = false;
|
|
} else {
|
|
int index = dayRule.indexOf(">=");
|
|
if (index > 0) {
|
|
dayOfWeek = parseDayOfWeek(dayRule.substring(0, index));
|
|
dayRule = dayRule.substring(index + 2);
|
|
} else {
|
|
index = dayRule.indexOf("<=");
|
|
if (index > 0) {
|
|
dayOfWeek = parseDayOfWeek(dayRule.substring(0, index));
|
|
adjustForwards = false;
|
|
dayRule = dayRule.substring(index + 2);
|
|
}
|
|
}
|
|
dayOfMonth = Integer.parseInt(dayRule);
|
|
if (dayOfMonth < -28 || dayOfMonth > 31 || dayOfMonth == 0) {
|
|
throw new IllegalArgumentException(
|
|
"Day of month indicator must be between -28 and 31 inclusive excluding zero");
|
|
}
|
|
}
|
|
if (off < tokens.length) {
|
|
String timeStr = tokens[off++];
|
|
secsOfDay = parseSecs(timeStr);
|
|
if (secsOfDay == 86400) {
|
|
// time must be midnight when end of day flag is true
|
|
endOfDay = true;
|
|
secsOfDay = 0;
|
|
}
|
|
timeDefinition = parseTimeDefinition(timeStr.charAt(timeStr.length() - 1));
|
|
}
|
|
}
|
|
}
|
|
|
|
int parseYear(String year, int defaultYear) {
|
|
switch (year.toLowerCase()) {
|
|
case "min": return 1900;
|
|
case "max": return Year.MAX_VALUE;
|
|
case "only": return defaultYear;
|
|
}
|
|
return Integer.parseInt(year);
|
|
}
|
|
|
|
Month parseMonth(String mon) {
|
|
switch (mon) {
|
|
case "Jan": return Month.JANUARY;
|
|
case "Feb": return Month.FEBRUARY;
|
|
case "Mar": return Month.MARCH;
|
|
case "Apr": return Month.APRIL;
|
|
case "May": return Month.MAY;
|
|
case "Jun": return Month.JUNE;
|
|
case "Jul": return Month.JULY;
|
|
case "Aug": return Month.AUGUST;
|
|
case "Sep": return Month.SEPTEMBER;
|
|
case "Oct": return Month.OCTOBER;
|
|
case "Nov": return Month.NOVEMBER;
|
|
case "Dec": return Month.DECEMBER;
|
|
}
|
|
throw new IllegalArgumentException("Unknown month: " + mon);
|
|
}
|
|
|
|
DayOfWeek parseDayOfWeek(String dow) {
|
|
switch (dow) {
|
|
case "Mon": return DayOfWeek.MONDAY;
|
|
case "Tue": return DayOfWeek.TUESDAY;
|
|
case "Wed": return DayOfWeek.WEDNESDAY;
|
|
case "Thu": return DayOfWeek.THURSDAY;
|
|
case "Fri": return DayOfWeek.FRIDAY;
|
|
case "Sat": return DayOfWeek.SATURDAY;
|
|
case "Sun": return DayOfWeek.SUNDAY;
|
|
}
|
|
throw new IllegalArgumentException("Unknown day-of-week: " + dow);
|
|
}
|
|
|
|
String parseOptional(String str) {
|
|
return str.equals("-") ? null : str;
|
|
}
|
|
|
|
static final boolean isDigit(char c) {
|
|
return c >= '0' && c <= '9';
|
|
}
|
|
|
|
private int parseSecs(String time) {
|
|
if (time.equals("-")) {
|
|
return 0;
|
|
}
|
|
// faster hack
|
|
int secs = 0;
|
|
int sign = 1;
|
|
int off = 0;
|
|
int len = time.length();
|
|
if (off < len && time.charAt(off) == '-') {
|
|
sign = -1;
|
|
off++;
|
|
}
|
|
char c0, c1;
|
|
if (off < len && isDigit(c0 = time.charAt(off++))) {
|
|
int hour = c0 - '0';
|
|
if (off < len && isDigit(c1 = time.charAt(off))) {
|
|
hour = hour * 10 + c1 - '0';
|
|
off++;
|
|
}
|
|
secs = hour * 60 * 60;
|
|
if (off < len && time.charAt(off++) == ':') {
|
|
if (off + 1 < len &&
|
|
isDigit(c0 = time.charAt(off++)) &&
|
|
isDigit(c1 = time.charAt(off++))) {
|
|
// minutes
|
|
secs += ((c0 - '0') * 10 + c1 - '0') * 60;
|
|
if (off < len && time.charAt(off++) == ':') {
|
|
if (off + 1 < len &&
|
|
isDigit(c0 = time.charAt(off++)) &&
|
|
isDigit(c1 = time.charAt(off++))) {
|
|
// seconds
|
|
secs += ((c0 - '0') * 10 + c1 - '0');
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
return secs * sign;
|
|
}
|
|
throw new IllegalArgumentException("[" + time + "]");
|
|
}
|
|
|
|
int parseOffset(String str) {
|
|
int secs = parseSecs(str);
|
|
if (Math.abs(secs) > 18 * 60 * 60) {
|
|
throw new IllegalArgumentException(
|
|
"Zone offset not in valid range: -18:00 to +18:00");
|
|
}
|
|
return secs;
|
|
}
|
|
|
|
int parsePeriod(String str) {
|
|
return parseSecs(str);
|
|
}
|
|
|
|
TimeDefinition parseTimeDefinition(char c) {
|
|
switch (c) {
|
|
case 's':
|
|
case 'S':
|
|
// standard time
|
|
return TimeDefinition.STANDARD;
|
|
case 'u':
|
|
case 'U':
|
|
case 'g':
|
|
case 'G':
|
|
case 'z':
|
|
case 'Z':
|
|
// UTC
|
|
return TimeDefinition.UTC;
|
|
case 'w':
|
|
case 'W':
|
|
default:
|
|
// wall time
|
|
return TimeDefinition.WALL;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Class representing a rule line in the TZDB file.
|
|
*/
|
|
private static class RuleLine extends MonthDayTime {
|
|
/** The start year. */
|
|
int startYear;
|
|
|
|
/** The end year. */
|
|
int endYear;
|
|
|
|
/** The amount of savings, in seconds. */
|
|
int savingsAmount;
|
|
|
|
/** The text name of the zone. */
|
|
String text;
|
|
|
|
/**
|
|
* Converts this to a transition rule.
|
|
*
|
|
* @param standardOffset the active standard offset, not null
|
|
* @param savingsBeforeSecs the active savings before the transition in seconds
|
|
* @return the transition, not null
|
|
*/
|
|
ZoneOffsetTransitionRule toTransitionRule(ZoneOffset stdOffset, int savingsBefore) {
|
|
// rule shared by different zones, so don't change it
|
|
Month month = this.month;
|
|
int dayOfMonth = this.dayOfMonth;
|
|
DayOfWeek dayOfWeek = this.dayOfWeek;
|
|
boolean endOfDay = this.endOfDay;
|
|
|
|
// optimize stored format
|
|
if (dayOfMonth < 0) {
|
|
if (month != Month.FEBRUARY) { // not Month.FEBRUARY
|
|
dayOfMonth = month.maxLength() - 6;
|
|
}
|
|
}
|
|
if (endOfDay && dayOfMonth > 0 &&
|
|
(dayOfMonth == 28 && month == Month.FEBRUARY) == false) {
|
|
LocalDate date = LocalDate.of(2004, month, dayOfMonth).plusDays(1); // leap-year
|
|
month = date.getMonth();
|
|
dayOfMonth = date.getDayOfMonth();
|
|
if (dayOfWeek != null) {
|
|
dayOfWeek = dayOfWeek.plus(1);
|
|
}
|
|
endOfDay = false;
|
|
}
|
|
// build rule
|
|
return ZoneOffsetTransitionRule.of(
|
|
//month, dayOfMonth, dayOfWeek, time, endOfDay, timeDefinition,
|
|
month, dayOfMonth, dayOfWeek,
|
|
LocalTime.ofSecondOfDay(secsOfDay), endOfDay, timeDefinition,
|
|
stdOffset,
|
|
ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savingsBefore),
|
|
ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savingsAmount));
|
|
}
|
|
|
|
RuleLine parse(String[] tokens) {
|
|
startYear = parseYear(tokens[2], 0);
|
|
endYear = parseYear(tokens[3], startYear);
|
|
if (startYear > endYear) {
|
|
throw new IllegalArgumentException(
|
|
"Invalid <Rule> line/Year order invalid:" + startYear + " > " + endYear);
|
|
}
|
|
//parseOptional(s.next()); // type is unused
|
|
super.parse(tokens, 5); // monthdaytime parsing
|
|
savingsAmount = parsePeriod(tokens[8]);
|
|
//rule.text = parseOptional(s.next());
|
|
return this;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Class representing a linked set of zone lines in the TZDB file.
|
|
*/
|
|
private static class ZoneLine extends MonthDayTime {
|
|
/** The standard offset. */
|
|
int stdOffsetSecs;
|
|
|
|
/** The fixed savings amount. */
|
|
int fixedSavingsSecs = 0;
|
|
|
|
/** The savings rule. */
|
|
String savingsRule;
|
|
|
|
/** The text name of the zone. */
|
|
String text;
|
|
|
|
/** The cutover year */
|
|
int year = Year.MAX_VALUE;
|
|
|
|
/** The cutover date time */
|
|
LocalDateTime ldt;
|
|
|
|
/** The cutover date/time in epoch seconds/UTC */
|
|
long ldtSecs = Long.MIN_VALUE;
|
|
|
|
LocalDateTime toDateTime() {
|
|
if (ldt == null) {
|
|
ldt = toDateTime(year);
|
|
}
|
|
return ldt;
|
|
}
|
|
|
|
/**
|
|
* Creates the date-time epoch second in the wall offset for the local
|
|
* date-time at the end of the window.
|
|
*
|
|
* @param savingsSecs the amount of savings in use in seconds
|
|
* @return the created date-time epoch second in the wall offset, not null
|
|
*/
|
|
long toDateTimeEpochSecond(int savingsSecs) {
|
|
if (ldtSecs == Long.MIN_VALUE) {
|
|
ldtSecs = toDateTime().toEpochSecond(ZoneOffset.UTC);
|
|
}
|
|
switch(timeDefinition) {
|
|
case UTC: return ldtSecs;
|
|
case STANDARD: return ldtSecs - stdOffsetSecs;
|
|
default: return ldtSecs - (stdOffsetSecs + savingsSecs); // WALL
|
|
}
|
|
}
|
|
|
|
boolean parse(String[] tokens, int off) {
|
|
stdOffsetSecs = parseOffset(tokens[off++]);
|
|
savingsRule = parseOptional(tokens[off++]);
|
|
if (savingsRule != null && savingsRule.length() > 0 &&
|
|
(savingsRule.charAt(0) == '-' || isDigit(savingsRule.charAt(0)))) {
|
|
try {
|
|
fixedSavingsSecs = parsePeriod(savingsRule);
|
|
savingsRule = null;
|
|
} catch (Exception ex) {
|
|
fixedSavingsSecs = 0;
|
|
}
|
|
}
|
|
text = tokens[off++];
|
|
if (off < tokens.length) {
|
|
year = Integer.parseInt(tokens[off++]);
|
|
if (off < tokens.length) {
|
|
super.parse(tokens, off); // MonthDayTime
|
|
}
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Class representing a rule line in the TZDB file for a particular year.
|
|
*/
|
|
private static class TransRule implements Comparable<TransRule>
|
|
{
|
|
private int year;
|
|
private RuleLine rule;
|
|
|
|
/** The trans date/time */
|
|
private LocalDateTime ldt;
|
|
|
|
/** The trans date/time in epoch seconds (assume UTC) */
|
|
long ldtSecs;
|
|
|
|
TransRule(int year, RuleLine rule) {
|
|
this.year = year;
|
|
this.rule = rule;
|
|
this.ldt = rule.toDateTime(year);
|
|
this.ldtSecs = ldt.toEpochSecond(ZoneOffset.UTC);
|
|
}
|
|
|
|
ZoneOffsetTransition toTransition(ZoneOffset standardOffset, int savingsBeforeSecs) {
|
|
// copy of code in ZoneOffsetTransitionRule to avoid infinite loop
|
|
ZoneOffset wallOffset = ZoneOffset.ofTotalSeconds(
|
|
standardOffset.getTotalSeconds() + savingsBeforeSecs);
|
|
ZoneOffset offsetAfter = ZoneOffset.ofTotalSeconds(
|
|
standardOffset.getTotalSeconds() + rule.savingsAmount);
|
|
LocalDateTime dt = rule.timeDefinition
|
|
.createDateTime(ldt, standardOffset, wallOffset);
|
|
return ZoneOffsetTransition.of(dt, wallOffset, offsetAfter);
|
|
}
|
|
|
|
long toEpochSecond(ZoneOffset stdOffset, int savingsBeforeSecs) {
|
|
switch(rule.timeDefinition) {
|
|
case UTC: return ldtSecs;
|
|
case STANDARD: return ldtSecs - stdOffset.getTotalSeconds();
|
|
default: return ldtSecs - (stdOffset.getTotalSeconds() + savingsBeforeSecs); // WALL
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tests if this a real transition with the active savings in seconds
|
|
*
|
|
* @param savingsBefore the active savings in seconds
|
|
* @return true, if savings changes
|
|
*/
|
|
boolean isTransition(int savingsBefore) {
|
|
return rule.savingsAmount != savingsBefore;
|
|
}
|
|
|
|
public int compareTo(TransRule other) {
|
|
return (ldtSecs < other.ldtSecs)? -1 : ((ldtSecs == other.ldtSecs) ? 0 : 1);
|
|
}
|
|
}
|
|
|
|
private ZoneRules buildRules(String zoneId, List<ZoneLine> zones) {
|
|
if (zones.isEmpty()) {
|
|
throw new IllegalStateException("No available zone window");
|
|
}
|
|
final List<ZoneOffsetTransition> standardTransitionList = new ArrayList<>(4);
|
|
final List<ZoneOffsetTransition> transitionList = new ArrayList<>(256);
|
|
final List<ZoneOffsetTransitionRule> lastTransitionRuleList = new ArrayList<>(2);
|
|
|
|
final ZoneLine zone0 = zones.get(0);
|
|
// initialize the standard offset, wallOffset and savings for loop
|
|
|
|
//ZoneOffset stdOffset = zone0.standardOffset;
|
|
ZoneOffset stdOffset = ZoneOffset.ofTotalSeconds(zone0.stdOffsetSecs);
|
|
|
|
int savings = zone0.fixedSavingsSecs;
|
|
ZoneOffset wallOffset = ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savings);
|
|
|
|
// start ldt of each zone window
|
|
LocalDateTime zoneStart = LocalDateTime.MIN;
|
|
|
|
// first stanard offset
|
|
ZoneOffset firstStdOffset = stdOffset;
|
|
// first wall offset
|
|
ZoneOffset firstWallOffset = wallOffset;
|
|
|
|
for (ZoneLine zone : zones) {
|
|
// check if standard offset changed, update it if yes
|
|
ZoneOffset stdOffsetPrev = stdOffset; // for effectiveSavings check
|
|
if (zone.stdOffsetSecs != stdOffset.getTotalSeconds()) {
|
|
ZoneOffset stdOffsetNew = ZoneOffset.ofTotalSeconds(zone.stdOffsetSecs);
|
|
standardTransitionList.add(
|
|
ZoneOffsetTransition.of(
|
|
LocalDateTime.ofEpochSecond(zoneStart.toEpochSecond(wallOffset),
|
|
0,
|
|
stdOffset),
|
|
stdOffset,
|
|
stdOffsetNew));
|
|
stdOffset = stdOffsetNew;
|
|
}
|
|
|
|
LocalDateTime zoneEnd;
|
|
if (zone.year == Year.MAX_VALUE) {
|
|
zoneEnd = LocalDateTime.MAX;
|
|
} else {
|
|
zoneEnd = zone.toDateTime();
|
|
}
|
|
if (zoneEnd.compareTo(zoneStart) < 0) {
|
|
throw new IllegalStateException("Windows must be in date-time order: " +
|
|
zoneEnd + " < " + zoneStart);
|
|
}
|
|
// calculate effective savings at the start of the window
|
|
List<TransRule> trules = null;
|
|
List<TransRule> lastRules = null;
|
|
|
|
int effectiveSavings = zone.fixedSavingsSecs;
|
|
if (zone.savingsRule != null) {
|
|
List<RuleLine> tzdbRules = rules.get(zone.savingsRule);
|
|
if (tzdbRules == null) {
|
|
throw new IllegalArgumentException("<Rule> not found: " +
|
|
zone.savingsRule);
|
|
}
|
|
trules = new ArrayList<>(256);
|
|
lastRules = new ArrayList<>(2);
|
|
int lastRulesStartYear = Year.MIN_VALUE;
|
|
|
|
// merge the rules to transitions
|
|
for (RuleLine rule : tzdbRules) {
|
|
if (rule.startYear > zoneEnd.getYear()) {
|
|
// rules will not be used for this zone entry
|
|
continue;
|
|
}
|
|
rule.adjustToForwards(2004); // irrelevant, treat as leap year
|
|
|
|
int startYear = rule.startYear;
|
|
int endYear = rule.endYear;
|
|
if (zoneEnd.equals(LocalDateTime.MAX)) {
|
|
if (endYear == Year.MAX_VALUE) {
|
|
endYear = startYear;
|
|
lastRules.add(new TransRule(endYear, rule));
|
|
}
|
|
lastRulesStartYear = Math.max(startYear, lastRulesStartYear);
|
|
} else {
|
|
if (endYear == Year.MAX_VALUE) {
|
|
//endYear = zoneEnd.getYear();
|
|
endYear = zone.year;
|
|
}
|
|
}
|
|
int year = startYear;
|
|
while (year <= endYear) {
|
|
trules.add(new TransRule(year, rule));
|
|
year++;
|
|
}
|
|
}
|
|
|
|
// last rules, fill the gap years between different last rules
|
|
if (zoneEnd.equals(LocalDateTime.MAX)) {
|
|
lastRulesStartYear = Math.max(lastRulesStartYear, zoneStart.getYear()) + 1;
|
|
for (TransRule rule : lastRules) {
|
|
if (rule.year <= lastRulesStartYear) {
|
|
int year = rule.year;
|
|
while (year <= lastRulesStartYear) {
|
|
trules.add(new TransRule(year, rule.rule));
|
|
year++;
|
|
}
|
|
rule.year = lastRulesStartYear;
|
|
rule.ldt = rule.rule.toDateTime(year);
|
|
rule.ldtSecs = rule.ldt.toEpochSecond(ZoneOffset.UTC);
|
|
}
|
|
}
|
|
Collections.sort(lastRules);
|
|
}
|
|
// sort the merged rules
|
|
Collections.sort(trules);
|
|
|
|
effectiveSavings = 0;
|
|
for (TransRule rule : trules) {
|
|
if (rule.toEpochSecond(stdOffsetPrev, savings) >
|
|
zoneStart.toEpochSecond(wallOffset)) {
|
|
// previous savings amount found, which could be the
|
|
// savings amount at the instant that the window starts
|
|
// (hence isAfter)
|
|
break;
|
|
}
|
|
effectiveSavings = rule.rule.savingsAmount;
|
|
}
|
|
}
|
|
// check if the start of the window represents a transition
|
|
ZoneOffset effectiveWallOffset =
|
|
ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + effectiveSavings);
|
|
|
|
if (!wallOffset.equals(effectiveWallOffset)) {
|
|
transitionList.add(ZoneOffsetTransition.of(zoneStart,
|
|
wallOffset,
|
|
effectiveWallOffset));
|
|
}
|
|
savings = effectiveSavings;
|
|
// apply rules within the window
|
|
if (trules != null) {
|
|
long zoneStartEpochSecs = zoneStart.toEpochSecond(wallOffset);
|
|
for (TransRule trule : trules) {
|
|
if (trule.isTransition(savings)) {
|
|
long epochSecs = trule.toEpochSecond(stdOffset, savings);
|
|
if (epochSecs < zoneStartEpochSecs ||
|
|
epochSecs >= zone.toDateTimeEpochSecond(savings)) {
|
|
continue;
|
|
}
|
|
transitionList.add(trule.toTransition(stdOffset, savings));
|
|
savings = trule.rule.savingsAmount;
|
|
}
|
|
}
|
|
}
|
|
if (lastRules != null) {
|
|
for (TransRule trule : lastRules) {
|
|
lastTransitionRuleList.add(trule.rule.toTransitionRule(stdOffset, savings));
|
|
savings = trule.rule.savingsAmount;
|
|
}
|
|
}
|
|
|
|
// finally we can calculate the true end of the window, passing it to the next window
|
|
wallOffset = ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savings);
|
|
zoneStart = LocalDateTime.ofEpochSecond(zone.toDateTimeEpochSecond(savings),
|
|
0,
|
|
wallOffset);
|
|
}
|
|
return new ZoneRules(firstStdOffset,
|
|
firstWallOffset,
|
|
standardTransitionList,
|
|
transitionList,
|
|
lastTransitionRuleList);
|
|
}
|
|
|
|
}
|