8042369: Remove duplicated java.time classes in build.tools.tzdb

Removed duplicated classes in builder

Reviewed-by: rriggs
This commit is contained in:
Xueming Shen 2014-06-23 12:12:30 -07:00
parent 61ad72b44a
commit 565ab8b571
14 changed files with 891 additions and 3950 deletions

@ -1,180 +0,0 @@
/*
* Copyright (c) 2012, 2013, 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.
*/
/*
* Copyright (c) 2012, Stephen Colebourne & Michael Nascimento Santos
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of JSR-310 nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package build.tools.tzdb;
/**
* A standard set of date/time fields.
*
* @since 1.8
*/
enum ChronoField {
/**
* The second-of-minute.
* <p>
* This counts the second within the minute, from 0 to 59.
* This field has the same meaning for all calendar systems.
*/
SECOND_OF_MINUTE("SecondOfMinute", 0, 59),
/**
* The second-of-day.
* <p>
* This counts the second within the day, from 0 to (24 * 60 * 60) - 1.
* This field has the same meaning for all calendar systems.
*/
SECOND_OF_DAY("SecondOfDay", 0, 86400 - 1),
/**
* The minute-of-hour.
* <p>
* This counts the minute within the hour, from 0 to 59.
* This field has the same meaning for all calendar systems.
*/
MINUTE_OF_HOUR("MinuteOfHour", 0, 59),
/**
* The hour-of-day.
* <p>
* This counts the hour within the day, from 0 to 23.
* This is the hour that would be observed on a standard 24-hour digital clock.
* This field has the same meaning for all calendar systems.
*/
HOUR_OF_DAY("HourOfDay", 0, 23),
/**
* The day-of-month.
* <p>
* This represents the concept of the day within the month.
* In the default ISO calendar system, this has values from 1 to 31 in most months.
* April, June, September, November have days from 1 to 30, while February has days
* from 1 to 28, or 29 in a leap year.
* <p>
* Non-ISO calendar systems should implement this field using the most recognized
* day-of-month values for users of the calendar system.
* Normally, this is a count of days from 1 to the length of the month.
*/
DAY_OF_MONTH("DayOfMonth", 1, 31),
/**
* The month-of-year, such as March.
* <p>
* This represents the concept of the month within the year.
* In the default ISO calendar system, this has values from January (1) to December (12).
* <p>
* Non-ISO calendar systems should implement this field using the most recognized
* month-of-year values for users of the calendar system.
* Normally, this is a count of months starting from 1.
*/
MONTH_OF_YEAR("MonthOfYear", 1, 12),
/**
* The proleptic year, such as 2012.
* <p>
* This represents the concept of the year, counting sequentially and using negative numbers.
* The proleptic year is not interpreted in terms of the era.
* See {@link #YEAR_OF_ERA} for an example showing the mapping from proleptic year to year-of-era.
* <p>
* The standard mental model for a date is based on three concepts - year, month and day.
* These map onto the {@code YEAR}, {@code MONTH_OF_YEAR} and {@code DAY_OF_MONTH} fields.
* Note that there is no reference to eras.
* The full model for a date requires four concepts - era, year, month and day. These map onto
* the {@code ERA}, {@code YEAR_OF_ERA}, {@code MONTH_OF_YEAR} and {@code DAY_OF_MONTH} fields.
* Whether this field or {@code YEAR_OF_ERA} is used depends on which mental model is being used.
* See {@link ChronoLocalDate} for more discussion on this topic.
* <p>
* Non-ISO calendar systems should implement this field as follows.
* If the calendar system has only two eras, before and after a fixed date, then the
* proleptic-year value must be the same as the year-of-era value for the later era,
* and increasingly negative for the earlier era.
* If the calendar system has more than two eras, then the proleptic-year value may be
* defined with any appropriate value, although defining it to be the same as ISO may be
* the best option.
*/
YEAR("Year", -999_999_999, 999_999_999);
private final String name;
private final int min;
private final int max;
private ChronoField(String name, int min, int max) {
this.name = name;
this.min= min;
this.max= max;
}
/**
* Checks that the specified value is valid for this field.
* <p>
*
* @param value the value to check
* @return the value that was passed in
*/
public int checkValidValue(int value) {
if (value >= min && value <= max) {
return value;
}
throw new DateTimeException("Invalid value for " + name + " value: " + value);
}
public String toString() {
return name;
}
}

@ -1,98 +0,0 @@
/*
* Copyright (c) 2012, 2013, 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.
*/
/*
* This file is available under and governed by the GNU General Public
* License version 2 only, as published by the Free Software Foundation.
* However, the following notice accompanied the original version of this
* file:
*
* Copyright (c) 2008-2012, Stephen Colebourne & Michael Nascimento Santos
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of JSR-310 nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package build.tools.tzdb;
/**
* Exception used to indicate a problem while calculating a date-time.
* <p>
* This exception is used to indicate problems with creating, querying
* and manipulating date-time objects.
*
* @since 1.8
*/
class DateTimeException extends RuntimeException {
/**
* Serialization version.
*/
private static final long serialVersionUID = -1632418723876261839L;
/**
* Constructs a new date-time exception with the specified message.
*
* @param message the message to use for this exception, may be null
*/
public DateTimeException(String message) {
super(message);
}
/**
* Constructs a new date-time exception with the specified message and cause.
*
* @param message the message to use for this exception, may be null
* @param cause the cause of the exception, may be null
*/
public DateTimeException(String message, Throwable cause) {
super(message, cause);
}
}

@ -1,363 +0,0 @@
/*
* Copyright (c) 2012, 2013, 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.
*/
/*
* This file is available under and governed by the GNU General Public
* License version 2 only, as published by the Free Software Foundation.
* However, the following notice accompanied the original version of this
* file:
*
* Copyright (c) 2007-2012, Stephen Colebourne & Michael Nascimento Santos
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of JSR-310 nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package build.tools.tzdb;
import static build.tools.tzdb.Utils.*;
import static build.tools.tzdb.LocalTime.SECONDS_PER_DAY;
import static build.tools.tzdb.ChronoField.DAY_OF_MONTH;
import static build.tools.tzdb.ChronoField.MONTH_OF_YEAR;
import static build.tools.tzdb.ChronoField.YEAR;
import java.util.Objects;
/**
* A date without a time-zone in the ISO-8601 calendar system,
* such as {@code 2007-12-03}.
*
* @since 1.8
*/
final class LocalDate {
/**
* The minimum supported {@code LocalDate}, '-999999999-01-01'.
* This could be used by an application as a "far past" date.
*/
public static final LocalDate MIN = new LocalDate(YEAR_MIN_VALUE, 1, 1);
/**
* The maximum supported {@code LocalDate}, '+999999999-12-31'.
* This could be used by an application as a "far future" date.
*/
public static final LocalDate MAX = new LocalDate(YEAR_MAX_VALUE, 12, 31);
/**
* The number of days in a 400 year cycle.
*/
private static final int DAYS_PER_CYCLE = 146097;
/**
* The number of days from year zero to year 1970.
* There are five 400 year cycles from year zero to 2000.
* There are 7 leap years from 1970 to 2000.
*/
static final long DAYS_0000_TO_1970 = (DAYS_PER_CYCLE * 5L) - (30L * 365L + 7L);
/**
* The year.
*/
private final int year;
/**
* The month-of-year.
*/
private final short month;
/**
* The day-of-month.
*/
private final short day;
/**
* Obtains an instance of {@code LocalDate} from a year, month and day.
* <p>
* The day must be valid for the year and month, otherwise an exception will be thrown.
*
* @param year the year to represent, from MIN_YEAR to MAX_YEAR
* @param month the month-of-year to represent, from 1 (January) to 12 (December)
* @param dayOfMonth the day-of-month to represent, from 1 to 31
* @return the local date, not null
* @throws DateTimeException if the value of any field is out of range
* @throws DateTimeException if the day-of-month is invalid for the month-year
*/
public static LocalDate of(int year, int month, int dayOfMonth) {
YEAR.checkValidValue(year);
MONTH_OF_YEAR.checkValidValue(month);
DAY_OF_MONTH.checkValidValue(dayOfMonth);
if (dayOfMonth > 28 && dayOfMonth > lengthOfMonth(month, isLeapYear(year))) {
if (dayOfMonth == 29) {
throw new DateTimeException("Invalid date 'February 29' as '" + year + "' is not a leap year");
} else {
throw new DateTimeException("Invalid date '" + month + " " + dayOfMonth + "'");
}
}
return new LocalDate(year, month, dayOfMonth);
}
/**
* Constructor, previously validated.
*
* @param year the year to represent, from MIN_YEAR to MAX_YEAR
* @param month the month-of-year to represent, not null
* @param dayOfMonth the day-of-month to represent, valid for year-month, from 1 to 31
*/
private LocalDate(int year, int month, int dayOfMonth) {
this.year = year;
this.month = (short) month;
this.day = (short) dayOfMonth;
}
/**
* Gets the year field.
* <p>
* This method returns the primitive {@code int} value for the year.
* <p>
* The year returned by this method is proleptic as per {@code get(YEAR)}.
* To obtain the year-of-era, use {@code get(YEAR_OF_ERA}.
*
* @return the year, from MIN_YEAR to MAX_YEAR
*/
public int getYear() {
return year;
}
/**
* Gets the month-of-year field as an int from 1 to 12.
*
* @return the month-of-year
*/
public int getMonth() {
return month;
}
/**
* Gets the day-of-month field.
* <p>
* This method returns the primitive {@code int} value for the day-of-month.
*
* @return the day-of-month, from 1 to 31
*/
public int getDayOfMonth() {
return day;
}
/**
* Gets the day-of-week field, which is an int from 1 to 7.
*
* @return the day-of-week
*/
public int getDayOfWeek() {
return (int)floorMod(toEpochDay() + 3, 7) + 1;
}
/**
* Returns a copy of this {@code LocalDate} with the specified number of days added.
* <p>
* This method adds the specified amount to the days field incrementing the
* month and year fields as necessary to ensure the result remains valid.
* The result is only invalid if the maximum/minimum year is exceeded.
* <p>
* For example, 2008-12-31 plus one day would result in 2009-01-01.
* <p>
* This instance is immutable and unaffected by this method call.
*
* @param daysToAdd the days to add, may be negative
* @return a {@code LocalDate} based on this date with the days added, not null
* @throws DateTimeException if the result exceeds the supported date range
*/
public LocalDate plusDays(long daysToAdd) {
if (daysToAdd == 0) {
return this;
}
long mjDay = addExact(toEpochDay(), daysToAdd);
return LocalDate.ofEpochDay(mjDay);
}
/**
* Returns a copy of this {@code LocalDate} with the specified number of days subtracted.
* <p>
* This method subtracts the specified amount from the days field decrementing the
* month and year fields as necessary to ensure the result remains valid.
* The result is only invalid if the maximum/minimum year is exceeded.
* <p>
* For example, 2009-01-01 minus one day would result in 2008-12-31.
* <p>
* This instance is immutable and unaffected by this method call.
*
* @param daysToSubtract the days to subtract, may be negative
* @return a {@code LocalDate} based on this date with the days subtracted, not null
* @throws DateTimeException if the result exceeds the supported date range
*/
public LocalDate minusDays(long daysToSubtract) {
return (daysToSubtract == Long.MIN_VALUE ? plusDays(Long.MAX_VALUE).plusDays(1) : plusDays(-daysToSubtract));
}
/**
* Obtains an instance of {@code LocalDate} from the epoch day count.
* <p>
* The Epoch Day count is a simple incrementing count of days
* where day 0 is 1970-01-01. Negative numbers represent earlier days.
*
* @param epochDay the Epoch Day to convert, based on the epoch 1970-01-01
* @return the local date, not null
* @throws DateTimeException if the epoch days exceeds the supported date range
*/
public static LocalDate ofEpochDay(long epochDay) {
long zeroDay = epochDay + DAYS_0000_TO_1970;
// find the march-based year
zeroDay -= 60; // adjust to 0000-03-01 so leap day is at end of four year cycle
long adjust = 0;
if (zeroDay < 0) {
// adjust negative years to positive for calculation
long adjustCycles = (zeroDay + 1) / DAYS_PER_CYCLE - 1;
adjust = adjustCycles * 400;
zeroDay += -adjustCycles * DAYS_PER_CYCLE;
}
long yearEst = (400 * zeroDay + 591) / DAYS_PER_CYCLE;
long doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400);
if (doyEst < 0) {
// fix estimate
yearEst--;
doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400);
}
yearEst += adjust; // reset any negative year
int marchDoy0 = (int) doyEst;
// convert march-based values back to january-based
int marchMonth0 = (marchDoy0 * 5 + 2) / 153;
int month = (marchMonth0 + 2) % 12 + 1;
int dom = marchDoy0 - (marchMonth0 * 306 + 5) / 10 + 1;
yearEst += marchMonth0 / 10;
// check year now we are certain it is correct
int year = YEAR.checkValidValue((int)yearEst);
return new LocalDate(year, month, dom);
}
public long toEpochDay() {
long y = year;
long m = month;
long total = 0;
total += 365 * y;
if (y >= 0) {
total += (y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400;
} else {
total -= y / -4 - y / -100 + y / -400;
}
total += ((367 * m - 362) / 12);
total += day - 1;
if (m > 2) {
total--;
if (isLeapYear(year) == false) {
total--;
}
}
return total - DAYS_0000_TO_1970;
}
/**
* Compares this date to another date.
* <p>
* The comparison is primarily based on the date, from earliest to latest.
* It is "consistent with equals", as defined by {@link Comparable}.
* <p>
* If all the dates being compared are instances of {@code LocalDate},
* then the comparison will be entirely based on the date.
* If some dates being compared are in different chronologies, then the
* chronology is also considered, see {@link java.time.temporal.ChronoLocalDate#compareTo}.
*
* @param other the other date to compare to, not null
* @return the comparator value, negative if less, positive if greater
*/
public int compareTo(LocalDate otherDate) {
int cmp = (year - otherDate.year);
if (cmp == 0) {
cmp = (month - otherDate.month);
if (cmp == 0) {
cmp = (day - otherDate.day);
}
}
return cmp;
}
/**
* Checks if this date is equal to another date.
* <p>
* Compares this {@code LocalDate} with another ensuring that the date is the same.
* <p>
* Only objects of type {@code LocalDate} are compared, other types return false.
* To compare the dates of two {@code TemporalAccessor} instances, including dates
* in two different chronologies, use {@link ChronoField#EPOCH_DAY} as a comparator.
*
* @param obj the object to check, null returns false
* @return true if this is equal to the other date
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof LocalDate) {
return compareTo((LocalDate) obj) == 0;
}
return false;
}
/**
* A hash code for this date.
*
* @return a suitable hash code
*/
@Override
public int hashCode() {
int yearValue = year;
int monthValue = month;
int dayValue = day;
return (yearValue & 0xFFFFF800) ^ ((yearValue << 11) + (monthValue << 6) + (dayValue));
}
}

@ -1,427 +0,0 @@
/*
* Copyright (c) 2012, 2013, 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.
*/
/*
* This file is available under and governed by the GNU General Public
* License version 2 only, as published by the Free Software Foundation.
* However, the following notice accompanied the original version of this
* file:
*
* Copyright (c) 2007-2012, Stephen Colebourne & Michael Nascimento Santos
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of JSR-310 nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package build.tools.tzdb;
import static build.tools.tzdb.Utils.*;
import static build.tools.tzdb.LocalTime.HOURS_PER_DAY;
import static build.tools.tzdb.LocalTime.MICROS_PER_DAY;
import static build.tools.tzdb.LocalTime.MILLIS_PER_DAY;
import static build.tools.tzdb.LocalTime.MINUTES_PER_DAY;
import static build.tools.tzdb.LocalTime.SECONDS_PER_DAY;
import static build.tools.tzdb.LocalTime.SECONDS_PER_MINUTE;
import static build.tools.tzdb.LocalTime.SECONDS_PER_HOUR;
import java.util.Objects;
/**
* A date-time without a time-zone in the ISO-8601 calendar system,
* such as {@code 2007-12-03T10:15:30}.
*
* @since 1.8
*/
final class LocalDateTime {
/**
* The minimum supported {@code LocalDateTime}, '-999999999-01-01T00:00:00'.
* This is the local date-time of midnight at the start of the minimum date.
* This combines {@link LocalDate#MIN} and {@link LocalTime#MIN}.
* This could be used by an application as a "far past" date-time.
*/
public static final LocalDateTime MIN = LocalDateTime.of(LocalDate.MIN, LocalTime.MIN);
/**
* The maximum supported {@code LocalDateTime}, '+999999999-12-31T23:59:59.999999999'.
* This is the local date-time just before midnight at the end of the maximum date.
* This combines {@link LocalDate#MAX} and {@link LocalTime#MAX}.
* This could be used by an application as a "far future" date-time.
*/
public static final LocalDateTime MAX = LocalDateTime.of(LocalDate.MAX, LocalTime.MAX);
/**
* The date part.
*/
private final LocalDate date;
/**
* The time part.
*/
private final LocalTime time;
/**
* Obtains an instance of {@code LocalDateTime} from year, month,
* day, hour and minute, setting the second and nanosecond to zero.
* <p>
* The day must be valid for the year and month, otherwise an exception will be thrown.
* The second and nanosecond fields will be set to zero.
*
* @param year the year to represent, from MIN_YEAR to MAX_YEAR
* @param month the month-of-year to represent, from 1 (January) to 12 (December)
* @param dayOfMonth the day-of-month to represent, from 1 to 31
* @param hour the hour-of-day to represent, from 0 to 23
* @param minute the minute-of-hour to represent, from 0 to 59
* @return the local date-time, not null
* @throws DateTimeException if the value of any field is out of range
* @throws DateTimeException if the day-of-month is invalid for the month-year
*/
public static LocalDateTime of(int year, int month, int dayOfMonth, int hour, int minute) {
LocalDate date = LocalDate.of(year, month, dayOfMonth);
LocalTime time = LocalTime.of(hour, minute);
return new LocalDateTime(date, time);
}
/**
* Obtains an instance of {@code LocalDateTime} from a date and time.
*
* @param date the local date, not null
* @param time the local time, not null
* @return the local date-time, not null
*/
public static LocalDateTime of(LocalDate date, LocalTime time) {
Objects.requireNonNull(date, "date");
Objects.requireNonNull(time, "time");
return new LocalDateTime(date, time);
}
/**
* Obtains an instance of {@code LocalDateTime} using seconds from the
* epoch of 1970-01-01T00:00:00Z.
* <p>
* This allows the {@link ChronoField#INSTANT_SECONDS epoch-second} field
* to be converted to a local date-time. This is primarily intended for
* low-level conversions rather than general application usage.
*
* @param epochSecond the number of seconds from the epoch of 1970-01-01T00:00:00Z
* @param nanoOfSecond the nanosecond within the second, from 0 to 999,999,999
* @param offset the zone offset, not null
* @return the local date-time, not null
* @throws DateTimeException if the result exceeds the supported range
*/
public static LocalDateTime ofEpochSecond(long epochSecond, int nanoOfSecond, ZoneOffset offset) {
Objects.requireNonNull(offset, "offset");
long localSecond = epochSecond + offset.getTotalSeconds(); // overflow caught later
long localEpochDay = floorDiv(localSecond, SECONDS_PER_DAY);
int secsOfDay = (int)floorMod(localSecond, SECONDS_PER_DAY);
LocalDate date = LocalDate.ofEpochDay(localEpochDay);
LocalTime time = LocalTime.ofSecondOfDay(secsOfDay); // ignore nano
return new LocalDateTime(date, time);
}
/**
* Constructor.
*
* @param date the date part of the date-time, validated not null
* @param time the time part of the date-time, validated not null
*/
private LocalDateTime(LocalDate date, LocalTime time) {
this.date = date;
this.time = time;
}
/**
* Returns a copy of this date-time with the new date and time, checking
* to see if a new object is in fact required.
*
* @param newDate the date of the new date-time, not null
* @param newTime the time of the new date-time, not null
* @return the date-time, not null
*/
private LocalDateTime with(LocalDate newDate, LocalTime newTime) {
if (date == newDate && time == newTime) {
return this;
}
return new LocalDateTime(newDate, newTime);
}
/**
* Gets the {@code LocalDate} part of this date-time.
* <p>
* This returns a {@code LocalDate} with the same year, month and day
* as this date-time.
*
* @return the date part of this date-time, not null
*/
public LocalDate getDate() {
return date;
}
/**
* Gets the year field.
* <p>
* This method returns the primitive {@code int} value for the year.
* <p>
* The year returned by this method is proleptic as per {@code get(YEAR)}.
* To obtain the year-of-era, use {@code get(YEAR_OF_ERA}.
*
* @return the year, from MIN_YEAR to MAX_YEAR
*/
public int getYear() {
return date.getYear();
}
/**
* Gets the month-of-year field as an int from 1 to 12.
*
* @return the month-of-year
*/
public int getMonth() {
return date.getMonth();
}
/**
* Gets the day-of-month field.
* <p>
* This method returns the primitive {@code int} value for the day-of-month.
*
* @return the day-of-month, from 1 to 31
*/
public int getDayOfMonth() {
return date.getDayOfMonth();
}
/**
* Gets the day-of-week field, which is an integer from 1 to 7.
*
* @return the day-of-week, from 1 to 7
*/
public int getDayOfWeek() {
return date.getDayOfWeek();
}
/**
* Gets the {@code LocalTime} part of this date-time.
* <p>
* This returns a {@code LocalTime} with the same hour, minute, second and
* nanosecond as this date-time.
*
* @return the time part of this date-time, not null
*/
public LocalTime getTime() {
return time;
}
/**
* Gets the hour-of-day field.
*
* @return the hour-of-day, from 0 to 23
*/
public int getHour() {
return time.getHour();
}
/**
* Gets the minute-of-hour field.
*
* @return the minute-of-hour, from 0 to 59
*/
public int getMinute() {
return time.getMinute();
}
/**
* Gets the second-of-minute field.
*
* @return the second-of-minute, from 0 to 59
*/
public int getSecond() {
return time.getSecond();
}
/**
* Converts this date-time to the number of seconds from the epoch
* of 1970-01-01T00:00:00Z.
* <p>
* This combines this local date-time and the specified offset to calculate the
* epoch-second value, which is the number of elapsed seconds from 1970-01-01T00:00:00Z.
* Instants on the time-line after the epoch are positive, earlier are negative.
* <p>
* This default implementation calculates from the epoch-day of the date and the
* second-of-day of the time.
*
* @param offset the offset to use for the conversion, not null
* @return the number of seconds from the epoch of 1970-01-01T00:00:00Z
*/
public long toEpochSecond(ZoneOffset offset) {
Objects.requireNonNull(offset, "offset");
long epochDay = getDate().toEpochDay();
long secs = epochDay * 86400 + getTime().toSecondOfDay();
secs -= offset.getTotalSeconds();
return secs;
}
/**
* Returns a copy of this {@code LocalDateTime} with the specified period in days added.
* <p>
* This method adds the specified amount to the days field incrementing the
* month and year fields as necessary to ensure the result remains valid.
* The result is only invalid if the maximum/minimum year is exceeded.
* <p>
* For example, 2008-12-31 plus one day would result in 2009-01-01.
* <p>
* This instance is immutable and unaffected by this method call.
*
* @param days the days to add, may be negative
* @return a {@code LocalDateTime} based on this date-time with the days added, not null
* @throws DateTimeException if the result exceeds the supported date range
*/
public LocalDateTime plusDays(long days) {
LocalDate newDate = date.plusDays(days);
return with(newDate, time);
}
/**
* Returns a copy of this {@code LocalDateTime} with the specified period in seconds added.
* <p>
* This instance is immutable and unaffected by this method call.
*
* @param seconds the seconds to add, may be negative
* @return a {@code LocalDateTime} based on this date-time with the seconds added, not null
* @throws DateTimeException if the result exceeds the supported date range
*/
public LocalDateTime plusSeconds(long seconds) {
return plusWithOverflow(date, 0, 0, seconds, 1);
}
/**
* Returns a copy of this {@code LocalDateTime} with the specified period added.
* <p>
* This instance is immutable and unaffected by this method call.
*
* @param newDate the new date to base the calculation on, not null
* @param hours the hours to add, may be negative
* @param minutes the minutes to add, may be negative
* @param seconds the seconds to add, may be negative
* @param nanos the nanos to add, may be negative
* @param sign the sign to determine add or subtract
* @return the combined result, not null
*/
private LocalDateTime plusWithOverflow(LocalDate newDate, long hours, long minutes, long seconds, int sign) {
if ((hours | minutes | seconds) == 0) {
return with(newDate, time);
}
long totDays = seconds / SECONDS_PER_DAY + // max/24*60*60
minutes / MINUTES_PER_DAY + // max/24*60
hours / HOURS_PER_DAY; // max/24
totDays *= sign; // total max*0.4237...
long totSecs = (seconds % SECONDS_PER_DAY) +
(minutes % MINUTES_PER_DAY) * SECONDS_PER_MINUTE +
(hours % HOURS_PER_DAY) * SECONDS_PER_HOUR;
long curSoD = time.toSecondOfDay();
totSecs = totSecs * sign + curSoD; // total 432000000000000
totDays += floorDiv(totSecs, SECONDS_PER_DAY);
int newSoD = (int)floorMod(totSecs, SECONDS_PER_DAY);
LocalTime newTime = (newSoD == curSoD ? time : LocalTime.ofSecondOfDay(newSoD));
return with(newDate.plusDays(totDays), newTime);
}
/**
* Compares this date-time to another date-time.
* <p>
* The comparison is primarily based on the date-time, from earliest to latest.
* It is "consistent with equals", as defined by {@link Comparable}.
* <p>
* If all the date-times being compared are instances of {@code LocalDateTime},
* then the comparison will be entirely based on the date-time.
* If some dates being compared are in different chronologies, then the
* chronology is also considered, see {@link ChronoLocalDateTime#compareTo}.
*
* @param other the other date-time to compare to, not null
* @return the comparator value, negative if less, positive if greater
*/
public int compareTo(LocalDateTime other) {
int cmp = date.compareTo(other.getDate());
if (cmp == 0) {
cmp = time.compareTo(other.getTime());
}
return cmp;
}
/**
* Checks if this date-time is equal to another date-time.
* <p>
* Compares this {@code LocalDateTime} with another ensuring that the date-time is the same.
* Only objects of type {@code LocalDateTime} are compared, other types return false.
*
* @param obj the object to check, null returns false
* @return true if this is equal to the other date-time
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof LocalDateTime) {
LocalDateTime other = (LocalDateTime) obj;
return date.equals(other.date) && time.equals(other.time);
}
return false;
}
/**
* A hash code for this date-time.
*
* @return a suitable hash code
*/
@Override
public int hashCode() {
return date.hashCode() ^ time.hashCode();
}
}

@ -1,388 +0,0 @@
/*
* Copyright (c) 2012, 2013, 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.
*/
/*
* This file is available under and governed by the GNU General Public
* License version 2 only, as published by the Free Software Foundation.
* However, the following notice accompanied the original version of this
* file:
*
* Copyright (c) 2007-2012, Stephen Colebourne & Michael Nascimento Santos
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of JSR-310 nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package build.tools.tzdb;
import static build.tools.tzdb.ChronoField.HOUR_OF_DAY;
import static build.tools.tzdb.ChronoField.MINUTE_OF_HOUR;
import static build.tools.tzdb.ChronoField.SECOND_OF_MINUTE;
import static build.tools.tzdb.ChronoField.SECOND_OF_DAY;
import java.util.Objects;
/**
* A time without time-zone in the ISO-8601 calendar system,
* such as {@code 10:15:30}.
*
*/
final class LocalTime {
/**
* The minimum supported {@code LocalTime}, '00:00'.
* This is the time of midnight at the start of the day.
*/
public static final LocalTime MIN;
/**
* The minimum supported {@code LocalTime}, '23:59:59.999999999'.
* This is the time just before midnight at the end of the day.
*/
public static final LocalTime MAX;
/**
* The time of midnight at the start of the day, '00:00'.
*/
public static final LocalTime MIDNIGHT;
/**
* The time of noon in the middle of the day, '12:00'.
*/
public static final LocalTime NOON;
/**
* Constants for the local time of each hour.
*/
private static final LocalTime[] HOURS = new LocalTime[24];
static {
for (int i = 0; i < HOURS.length; i++) {
HOURS[i] = new LocalTime(i, 0, 0);
}
MIDNIGHT = HOURS[0];
NOON = HOURS[12];
MIN = HOURS[0];
MAX = new LocalTime(23, 59, 59);
}
/**
* Hours per day.
*/
static final int HOURS_PER_DAY = 24;
/**
* Minutes per hour.
*/
static final int MINUTES_PER_HOUR = 60;
/**
* Minutes per day.
*/
static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * HOURS_PER_DAY;
/**
* Seconds per minute.
*/
static final int SECONDS_PER_MINUTE = 60;
/**
* Seconds per hour.
*/
static final int SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
/**
* Seconds per day.
*/
static final int SECONDS_PER_DAY = SECONDS_PER_HOUR * HOURS_PER_DAY;
/**
* Milliseconds per day.
*/
static final long MILLIS_PER_DAY = SECONDS_PER_DAY * 1000L;
/**
* Microseconds per day.
*/
static final long MICROS_PER_DAY = SECONDS_PER_DAY * 1000_000L;
/**
* The hour.
*/
private final byte hour;
/**
* The minute.
*/
private final byte minute;
/**
* The second.
*/
private final byte second;
/**
* Obtains an instance of {@code LocalTime} from an hour and minute.
* <p>
* The second and nanosecond fields will be set to zero by this factory method.
* <p>
* This factory may return a cached value, but applications must not rely on this.
*
* @param hour the hour-of-day to represent, from 0 to 23
* @param minute the minute-of-hour to represent, from 0 to 59
* @return the local time, not null
* @throws DateTimeException if the value of any field is out of range
*/
public static LocalTime of(int hour, int minute) {
HOUR_OF_DAY.checkValidValue(hour);
if (minute == 0) {
return HOURS[hour]; // for performance
}
MINUTE_OF_HOUR.checkValidValue(minute);
return new LocalTime(hour, minute, 0);
}
/**
* Obtains an instance of {@code LocalTime} from an hour, minute and second.
* <p>
* The nanosecond field will be set to zero by this factory method.
* <p>
* This factory may return a cached value, but applications must not rely on this.
*
* @param hour the hour-of-day to represent, from 0 to 23
* @param minute the minute-of-hour to represent, from 0 to 59
* @param second the second-of-minute to represent, from 0 to 59
* @return the local time, not null
* @throws DateTimeException if the value of any field is out of range
*/
public static LocalTime of(int hour, int minute, int second) {
HOUR_OF_DAY.checkValidValue(hour);
if ((minute | second) == 0) {
return HOURS[hour]; // for performance
}
MINUTE_OF_HOUR.checkValidValue(minute);
SECOND_OF_MINUTE.checkValidValue(second);
return new LocalTime(hour, minute, second);
}
/**
* Obtains an instance of {@code LocalTime} from a second-of-day value.
* <p>
* This factory may return a cached value, but applications must not rely on this.
*
* @param secondOfDay the second-of-day, from {@code 0} to {@code 24 * 60 * 60 - 1}
* @return the local time, not null
* @throws DateTimeException if the second-of-day value is invalid
*/
public static LocalTime ofSecondOfDay(int secondOfDay) {
SECOND_OF_DAY.checkValidValue(secondOfDay);
int hours = secondOfDay / SECONDS_PER_HOUR;
secondOfDay -= hours * SECONDS_PER_HOUR;
int minutes = secondOfDay / SECONDS_PER_MINUTE;
secondOfDay -= minutes * SECONDS_PER_MINUTE;
return create(hours, minutes, secondOfDay);
}
/**
* Creates a local time from the hour, minute, second and nanosecond fields.
* <p>
* This factory may return a cached value, but applications must not rely on this.
*
* @param hour the hour-of-day to represent, validated from 0 to 23
* @param minute the minute-of-hour to represent, validated from 0 to 59
* @param second the second-of-minute to represent, validated from 0 to 59
* @return the local time, not null
*/
private static LocalTime create(int hour, int minute, int second) {
if ((minute | second) == 0) {
return HOURS[hour];
}
return new LocalTime(hour, minute, second);
}
/**
* Constructor, previously validated.
*
* @param hour the hour-of-day to represent, validated from 0 to 23
* @param minute the minute-of-hour to represent, validated from 0 to 59
* @param second the second-of-minute to represent, validated from 0 to 59
*/
private LocalTime(int hour, int minute, int second) {
this.hour = (byte) hour;
this.minute = (byte) minute;
this.second = (byte) second;
}
/**
* Gets the hour-of-day field.
*
* @return the hour-of-day, from 0 to 23
*/
public int getHour() {
return hour;
}
/**
* Gets the minute-of-hour field.
*
* @return the minute-of-hour, from 0 to 59
*/
public int getMinute() {
return minute;
}
/**
* Gets the second-of-minute field.
*
* @return the second-of-minute, from 0 to 59
*/
public int getSecond() {
return second;
}
/**
* Returns a copy of this {@code LocalTime} with the specified period in seconds added.
* <p>
* This adds the specified number of seconds to this time, returning a new time.
* The calculation wraps around midnight.
* <p>
* This instance is immutable and unaffected by this method call.
*
* @param secondstoAdd the seconds to add, may be negative
* @return a {@code LocalTime} based on this time with the seconds added, not null
*/
public LocalTime plusSeconds(long secondstoAdd) {
if (secondstoAdd == 0) {
return this;
}
int sofd = hour * SECONDS_PER_HOUR +
minute * SECONDS_PER_MINUTE + second;
int newSofd = ((int) (secondstoAdd % SECONDS_PER_DAY) + sofd + SECONDS_PER_DAY) % SECONDS_PER_DAY;
if (sofd == newSofd) {
return this;
}
int newHour = newSofd / SECONDS_PER_HOUR;
int newMinute = (newSofd / SECONDS_PER_MINUTE) % MINUTES_PER_HOUR;
int newSecond = newSofd % SECONDS_PER_MINUTE;
return create(newHour, newMinute, newSecond);
}
/**
* Returns a copy of this {@code LocalTime} with the specified period in seconds subtracted.
* <p>
* This subtracts the specified number of seconds from this time, returning a new time.
* The calculation wraps around midnight.
* <p>
* This instance is immutable and unaffected by this method call.
*
* @param secondsToSubtract the seconds to subtract, may be negative
* @return a {@code LocalTime} based on this time with the seconds subtracted, not null
*/
public LocalTime minusSeconds(long secondsToSubtract) {
return plusSeconds(-(secondsToSubtract % SECONDS_PER_DAY));
}
/**
* Extracts the time as seconds of day,
* from {@code 0} to {@code 24 * 60 * 60 - 1}.
*
* @return the second-of-day equivalent to this time
*/
public int toSecondOfDay() {
int total = hour * SECONDS_PER_HOUR;
total += minute * SECONDS_PER_MINUTE;
total += second;
return total;
}
/**
* Compares this {@code LocalTime} to another time.
* <p>
* The comparison is based on the time-line position of the local times within a day.
* It is "consistent with equals", as defined by {@link Comparable}.
*
* @param other the other time to compare to, not null
* @return the comparator value, negative if less, positive if greater
* @throws NullPointerException if {@code other} is null
*/
public int compareTo(LocalTime other) {
int cmp = Integer.compare(hour, other.hour);
if (cmp == 0) {
cmp = Integer.compare(minute, other.minute);
if (cmp == 0) {
cmp = Integer.compare(second, other.second);
}
}
return cmp;
}
/**
* Checks if this time is equal to another time.
* <p>
* The comparison is based on the time-line position of the time within a day.
* <p>
* Only objects of type {@code LocalTime} are compared, other types return false.
* To compare the date of two {@code TemporalAccessor} instances, use
* {@link ChronoField#NANO_OF_DAY} as a comparator.
*
* @param obj the object to check, null returns false
* @return true if this is equal to the other time
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof LocalTime) {
LocalTime other = (LocalTime) obj;
return hour == other.hour && minute == other.minute &&
second == other.second;
}
return false;
}
/**
* A hash code for this time.
*
* @return a suitable hash code
*/
@Override
public int hashCode() {
long sod = toSecondOfDay();
return (int) (sod ^ (sod >>> 32));
}
}

@ -1,117 +0,0 @@
/*
* Copyright (c) 2012, 2013, 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.
*/
/*
* This file is available under and governed by the GNU General Public
* License version 2 only, as published by the Free Software Foundation.
* However, the following notice accompanied the original version of this
* file:
*
* Copyright (c) 2009-2012, Stephen Colebourne & Michael Nascimento Santos
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of JSR-310 nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package build.tools.tzdb;
import java.util.Objects;
/**
* A definition of the way a local time can be converted to the actual
* transition date-time.
* <p>
* Time zone rules are expressed in one of three ways:
* <p><ul>
* <li>Relative to UTC</li>
* <li>Relative to the standard offset in force</li>
* <li>Relative to the wall offset (what you would see on a clock on the wall)</li>
* </ul><p>
*/
public enum TimeDefinition {
/** The local date-time is expressed in terms of the UTC offset. */
UTC,
/** The local date-time is expressed in terms of the wall offset. */
WALL,
/** The local date-time is expressed in terms of the standard offset. */
STANDARD;
/**
* Converts the specified local date-time to the local date-time actually
* seen on a wall clock.
* <p>
* This method converts using the type of this enum.
* The output is defined relative to the 'before' offset of the transition.
* <p>
* The UTC type uses the UTC offset.
* The STANDARD type uses the standard offset.
* The WALL type returns the input date-time.
* The result is intended for use with the wall-offset.
*
* @param dateTime the local date-time, not null
* @param standardOffset the standard offset, not null
* @param wallOffset the wall offset, not null
* @return the date-time relative to the wall/before offset, not null
*/
public LocalDateTime createDateTime(LocalDateTime dateTime, ZoneOffset standardOffset, ZoneOffset wallOffset) {
switch (this) {
case UTC: {
int difference = wallOffset.getTotalSeconds() - ZoneOffset.UTC.getTotalSeconds();
return dateTime.plusSeconds(difference);
}
case STANDARD: {
int difference = wallOffset.getTotalSeconds() - standardOffset.getTotalSeconds();
return dateTime.plusSeconds(difference);
}
default: // WALL
return dateTime;
}
}
}

@ -56,8 +56,6 @@
*/
package build.tools.tzdb;
import static build.tools.tzdb.Utils.*;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.nio.charset.StandardCharsets;
@ -179,15 +177,41 @@ public final class TzdbZoneRulesCompiler {
System.exit(1);
System.err.println("Source directory does not contain file: VERSION");
}
// load source files
printVerbose("Compiling TZDB version " + version);
// parse source files
for (Path file : srcFiles) {
printVerbose("Parsing file: " + file);
parseFile(file);
}
TzdbZoneRulesProvider provider = new TzdbZoneRulesProvider(srcFiles);
// build zone rules
printVerbose("Building rules");
buildZoneRules();
// Build the rules, zones and links into real zones.
SortedMap<String, ZoneRules> builtZones = new TreeMap<>();
// build zones
for (String zoneId : provider.getZoneIds()) {
printVerbose("Building zone " + zoneId);
builtZones.put(zoneId, provider.getZoneRules(zoneId));
}
// build aliases
Map<String, String> links = provider.getAliasMap();
for (String aliasId : links.keySet()) {
String realId = links.get(aliasId);
printVerbose("Linking alias " + aliasId + " to " + realId);
ZoneRules realRules = builtZones.get(realId);
if (realRules == null) {
realId = links.get(realId); // try again (handle alias liked to alias)
printVerbose("Relinking alias " + aliasId + " to " + realId);
realRules = builtZones.get(realId);
if (realRules == null) {
throw new IllegalArgumentException("Alias '" + aliasId + "' links to invalid zone '" + realId);
}
links.put(aliasId, realId);
}
builtZones.put(aliasId, realRules);
}
// output to file
printVerbose("Outputting tzdb file: " + dstFile);
outputFile(dstFile, version, builtZones, links);
@ -269,361 +293,13 @@ public final class TzdbZoneRulesCompiler {
}
}
private static final Pattern YEAR = Pattern.compile("(?i)(?<min>min)|(?<max>max)|(?<only>only)|(?<year>[0-9]+)");
private static final Pattern MONTH = Pattern.compile("(?i)(jan)|(feb)|(mar)|(apr)|(may)|(jun)|(jul)|(aug)|(sep)|(oct)|(nov)|(dec)");
private static final Matcher DOW = Pattern.compile("(?i)(mon)|(tue)|(wed)|(thu)|(fri)|(sat)|(sun)").matcher("");
private static final Matcher TIME = Pattern.compile("(?<neg>-)?+(?<hour>[0-9]{1,2})(:(?<minute>[0-5][0-9]))?+(:(?<second>[0-5][0-9]))?+").matcher("");
/** The TZDB rules. */
private final Map<String, List<TZDBRule>> rules = new HashMap<>();
/** The TZDB zones. */
private final Map<String, List<TZDBZone>> zones = new HashMap<>();
/** The TZDB links. */
private final Map<String, String> links = new HashMap<>();
/** The built zones. */
private final SortedMap<String, ZoneRules> builtZones = new TreeMap<>();
/** Whether to output verbose messages. */
private boolean verbose;
/**
* private contructor
*/
private TzdbZoneRulesCompiler() {
}
/**
* Parses a source file.
*
* @param file the file being read, not null
* @throws Exception if an error occurs
*/
private void parseFile(Path file) throws Exception {
int lineNumber = 1;
String line = null;
try {
List<String> lines = Files.readAllLines(file, StandardCharsets.ISO_8859_1);
List<TZDBZone> openZone = null;
for (; lineNumber < lines.size(); lineNumber++) {
line = lines.get(lineNumber);
int index = line.indexOf('#'); // remove comments (doesn't handle # in quotes)
if (index >= 0) {
line = line.substring(0, index);
}
if (line.trim().length() == 0) { // ignore blank lines
continue;
}
Scanner s = new Scanner(line);
if (openZone != null && Character.isWhitespace(line.charAt(0)) && s.hasNext()) {
if (parseZoneLine(s, openZone)) {
openZone = null;
}
} else {
if (s.hasNext()) {
String first = s.next();
if (first.equals("Zone")) {
openZone = new ArrayList<>();
try {
zones.put(s.next(), openZone);
if (parseZoneLine(s, openZone)) {
openZone = null;
}
} catch (NoSuchElementException x) {
printVerbose("Invalid Zone line in file: " + file + ", line: " + line);
throw new IllegalArgumentException("Invalid Zone line");
}
} else {
openZone = null;
if (first.equals("Rule")) {
try {
parseRuleLine(s);
} catch (NoSuchElementException x) {
printVerbose("Invalid Rule line in file: " + file + ", line: " + line);
throw new IllegalArgumentException("Invalid Rule line");
}
} else if (first.equals("Link")) {
try {
String realId = s.next();
String aliasId = s.next();
links.put(aliasId, realId);
} catch (NoSuchElementException x) {
printVerbose("Invalid Link line in file: " + file + ", line: " + line);
throw new IllegalArgumentException("Invalid Link line");
}
} else {
throw new IllegalArgumentException("Unknown line");
}
}
}
}
}
} catch (Exception ex) {
throw new Exception("Failed while parsing file '" + file + "' on line " + lineNumber + " '" + line + "'", ex);
}
}
/**
* Parses a Rule line.
*
* @param s the line scanner, not null
*/
private void parseRuleLine(Scanner s) {
TZDBRule rule = new TZDBRule();
String name = s.next();
if (rules.containsKey(name) == false) {
rules.put(name, new ArrayList<TZDBRule>());
}
rules.get(name).add(rule);
rule.startYear = parseYear(s, 0);
rule.endYear = parseYear(s, rule.startYear);
if (rule.startYear > rule.endYear) {
throw new IllegalArgumentException("Year order invalid: " + rule.startYear + " > " + rule.endYear);
}
parseOptional(s.next()); // type is unused
parseMonthDayTime(s, rule);
rule.savingsAmount = parsePeriod(s.next());
rule.text = parseOptional(s.next());
}
/**
* Parses a Zone line.
*
* @param s the line scanner, not null
* @return true if the zone is complete
*/
private boolean parseZoneLine(Scanner s, List<TZDBZone> zoneList) {
TZDBZone zone = new TZDBZone();
zoneList.add(zone);
zone.standardOffset = parseOffset(s.next());
String savingsRule = parseOptional(s.next());
if (savingsRule == null) {
zone.fixedSavingsSecs = 0;
zone.savingsRule = null;
} else {
try {
zone.fixedSavingsSecs = parsePeriod(savingsRule);
zone.savingsRule = null;
} catch (Exception ex) {
zone.fixedSavingsSecs = null;
zone.savingsRule = savingsRule;
}
}
zone.text = s.next();
if (s.hasNext()) {
zone.year = Integer.parseInt(s.next());
if (s.hasNext()) {
parseMonthDayTime(s, zone);
}
return false;
} else {
return true;
}
}
/**
* Parses a Rule line.
*
* @param s the line scanner, not null
* @param mdt the object to parse into, not null
*/
private void parseMonthDayTime(Scanner s, TZDBMonthDayTime mdt) {
mdt.month = parseMonth(s);
if (s.hasNext()) {
String dayRule = s.next();
if (dayRule.startsWith("last")) {
mdt.dayOfMonth = -1;
mdt.dayOfWeek = parseDayOfWeek(dayRule.substring(4));
mdt.adjustForwards = false;
} else {
int index = dayRule.indexOf(">=");
if (index > 0) {
mdt.dayOfWeek = parseDayOfWeek(dayRule.substring(0, index));
dayRule = dayRule.substring(index + 2);
} else {
index = dayRule.indexOf("<=");
if (index > 0) {
mdt.dayOfWeek = parseDayOfWeek(dayRule.substring(0, index));
mdt.adjustForwards = false;
dayRule = dayRule.substring(index + 2);
}
}
mdt.dayOfMonth = Integer.parseInt(dayRule);
}
if (s.hasNext()) {
String timeStr = s.next();
int secsOfDay = parseSecs(timeStr);
if (secsOfDay == 86400) {
mdt.endOfDay = true;
secsOfDay = 0;
}
LocalTime time = LocalTime.ofSecondOfDay(secsOfDay);
mdt.time = time;
mdt.timeDefinition = parseTimeDefinition(timeStr.charAt(timeStr.length() - 1));
}
}
}
private int parseYear(Scanner s, int defaultYear) {
if (s.hasNext(YEAR)) {
s.next(YEAR);
MatchResult mr = s.match();
if (mr.group(1) != null) {
return 1900; // systemv has min
} else if (mr.group(2) != null) {
return YEAR_MAX_VALUE;
} else if (mr.group(3) != null) {
return defaultYear;
}
return Integer.parseInt(mr.group(4));
/*
if (mr.group("min") != null) {
//return YEAR_MIN_VALUE;
return 1900; // systemv has min
} else if (mr.group("max") != null) {
return YEAR_MAX_VALUE;
} else if (mr.group("only") != null) {
return defaultYear;
}
return Integer.parseInt(mr.group("year"));
*/
}
throw new IllegalArgumentException("Unknown year: " + s.next());
}
private int parseMonth(Scanner s) {
if (s.hasNext(MONTH)) {
s.next(MONTH);
for (int moy = 1; moy < 13; moy++) {
if (s.match().group(moy) != null) {
return moy;
}
}
}
throw new IllegalArgumentException("Unknown month: " + s.next());
}
private int parseDayOfWeek(String str) {
if (DOW.reset(str).matches()) {
for (int dow = 1; dow < 8; dow++) {
if (DOW.group(dow) != null) {
return dow;
}
}
}
throw new IllegalArgumentException("Unknown day-of-week: " + str);
}
private String parseOptional(String str) {
return str.equals("-") ? null : str;
}
private int parseSecs(String str) {
if (str.equals("-")) {
return 0;
}
try {
if (TIME.reset(str).find()) {
int secs = Integer.parseInt(TIME.group("hour")) * 60 * 60;
if (TIME.group("minute") != null) {
secs += Integer.parseInt(TIME.group("minute")) * 60;
}
if (TIME.group("second") != null) {
secs += Integer.parseInt(TIME.group("second"));
}
if (TIME.group("neg") != null) {
secs = -secs;
}
return secs;
}
} catch (NumberFormatException x) {}
throw new IllegalArgumentException(str);
}
private ZoneOffset parseOffset(String str) {
int secs = parseSecs(str);
return ZoneOffset.ofTotalSeconds(secs);
}
private int parsePeriod(String str) {
return parseSecs(str);
}
private 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;
}
}
/**
* Build the rules, zones and links into real zones.
*
* @throws Exception if an error occurs
*/
private void buildZoneRules() throws Exception {
// build zones
for (String zoneId : zones.keySet()) {
printVerbose("Building zone " + zoneId);
List<TZDBZone> tzdbZones = zones.get(zoneId);
ZoneRulesBuilder bld = new ZoneRulesBuilder();
for (TZDBZone tzdbZone : tzdbZones) {
bld = tzdbZone.addToBuilder(bld, rules);
}
builtZones.put(zoneId, bld.toRules(zoneId));
}
// build aliases
for (String aliasId : links.keySet()) {
String realId = links.get(aliasId);
printVerbose("Linking alias " + aliasId + " to " + realId);
ZoneRules realRules = builtZones.get(realId);
if (realRules == null) {
realId = links.get(realId); // try again (handle alias liked to alias)
printVerbose("Relinking alias " + aliasId + " to " + realId);
realRules = builtZones.get(realId);
if (realRules == null) {
throw new IllegalArgumentException("Alias '" + aliasId + "' links to invalid zone '" + realId);
}
links.put(aliasId, realId);
}
builtZones.put(aliasId, realRules);
}
// remove UTC and GMT
// builtZones.remove("UTC");
// builtZones.remove("GMT");
// builtZones.remove("GMT0");
builtZones.remove("GMT+0");
builtZones.remove("GMT-0");
links.remove("GMT+0");
links.remove("GMT-0");
// remove ROC, which is not supported in j.u.tz
builtZones.remove("ROC");
links.remove("ROC");
// remove EST, HST and MST. They are supported via
// the short-id mapping
builtZones.remove("EST");
builtZones.remove("HST");
builtZones.remove("MST");
}
private TzdbZoneRulesCompiler() {}
/**
* Prints a verbose message.
@ -635,109 +311,4 @@ public final class TzdbZoneRulesCompiler {
System.out.println(message);
}
}
/**
* Class representing a month-day-time in the TZDB file.
*/
abstract class TZDBMonthDayTime {
/** The month of the cutover. */
int month = 1;
/** The day-of-month of the cutover. */
int dayOfMonth = 1;
/** Whether to adjust forwards. */
boolean adjustForwards = true;
/** The day-of-week of the cutover. */
int dayOfWeek = -1;
/** The time of the cutover. */
LocalTime time = LocalTime.MIDNIGHT;
/** Whether this is midnight end of day. */
boolean endOfDay;
/** The time of the cutover. */
TimeDefinition timeDefinition = TimeDefinition.WALL;
void adjustToFowards(int year) {
if (adjustForwards == false && dayOfMonth > 0) {
LocalDate adjustedDate = LocalDate.of(year, month, dayOfMonth).minusDays(6);
dayOfMonth = adjustedDate.getDayOfMonth();
month = adjustedDate.getMonth();
adjustForwards = true;
}
}
}
/**
* Class representing a rule line in the TZDB file.
*/
final class TZDBRule extends TZDBMonthDayTime {
/** The start year. */
int startYear;
/** The end year. */
int endYear;
/** The amount of savings. */
int savingsAmount;
/** The text name of the zone. */
String text;
void addToBuilder(ZoneRulesBuilder bld) {
adjustToFowards(2004); // irrelevant, treat as leap year
bld.addRuleToWindow(startYear, endYear, month, dayOfMonth, dayOfWeek, time, endOfDay, timeDefinition, savingsAmount);
}
}
/**
* Class representing a linked set of zone lines in the TZDB file.
*/
final class TZDBZone extends TZDBMonthDayTime {
/** The standard offset. */
ZoneOffset standardOffset;
/** The fixed savings amount. */
Integer fixedSavingsSecs;
/** The savings rule. */
String savingsRule;
/** The text name of the zone. */
String text;
/** The year of the cutover. */
int year = YEAR_MAX_VALUE;
ZoneRulesBuilder addToBuilder(ZoneRulesBuilder bld, Map<String, List<TZDBRule>> rules) {
if (year != YEAR_MAX_VALUE) {
bld.addWindow(standardOffset, toDateTime(year), timeDefinition);
} else {
bld.addWindowForever(standardOffset);
}
if (fixedSavingsSecs != null) {
bld.setFixedSavingsToWindow(fixedSavingsSecs);
} else {
List<TZDBRule> tzdbRules = rules.get(savingsRule);
if (tzdbRules == null) {
throw new IllegalArgumentException("Rule not found: " + savingsRule);
}
for (TZDBRule tzdbRule : tzdbRules) {
tzdbRule.addToBuilder(bld);
}
}
return bld;
}
private LocalDateTime toDateTime(int year) {
adjustToFowards(year);
LocalDate date;
if (dayOfMonth == -1) {
dayOfMonth = lengthOfMonth(month, isLeapYear(year));
date = LocalDate.of(year, month, dayOfMonth);
if (dayOfWeek != -1) {
date = previousOrSame(date, dayOfWeek);
}
} else {
date = LocalDate.of(year, month, dayOfMonth);
if (dayOfWeek != -1) {
date = nextOrSame(date, dayOfWeek);
}
}
LocalDateTime ldt = LocalDateTime.of(date, time);
if (endOfDay) {
ldt = ldt.plusDays(1);
}
return ldt;
}
}
}

@ -0,0 +1,843 @@
/*
* 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 1.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) {
throw new ZoneRulesException("Unknown time-zone ID: " + zoneId0);
}
}
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);
}
}

@ -1,176 +0,0 @@
/*
* 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.
*/
/*
* Copyright (c) 2009-2012, Stephen Colebourne & Michael Nascimento Santos
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of JSR-310 nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package build.tools.tzdb;
import java.util.Objects;
class Utils {
// Returns the largest (closest to positive infinity)
public static long floorDiv(long x, long y) {
long r = x / y;
// if the signs are different and modulo not zero, round down
if ((x ^ y) < 0 && (r * y != x)) {
r--;
}
return r;
}
// Returns the floor modulus of the {@code long} arguments.
public static long floorMod(long x, long y) {
return x - floorDiv(x, y) * y;
}
// Returns the sum of its arguments,
public static long addExact(long x, long y) {
long r = x + y;
// HD 2-12 Overflow iff both arguments have the opposite sign of the result
if (((x ^ r) & (y ^ r)) < 0) {
throw new ArithmeticException("long overflow");
}
return r;
}
// Year
// Returns true if the specified year is a leap year.
public static boolean isLeapYear(int year) {
return ((year & 3) == 0) && ((year % 100) != 0 || (year % 400) == 0);
}
// The minimum supported year, '-999,999,999'.
public static final int YEAR_MIN_VALUE = -999_999_999;
// The maximum supported year, '+999,999,999'.
public static final int YEAR_MAX_VALUE = 999_999_999;
// Gets the length of the specified month in days.
public static int lengthOfMonth(int month, boolean leapYear) {
switch (month) {
case 2: //FEBRUARY:
return (leapYear ? 29 : 28);
case 4: //APRIL:
case 6: //JUNE:
case 9: //SEPTEMBER:
case 11: //NOVEMBER:
return 30;
default:
return 31;
}
}
// Gets the maximum length of the specified month in days.
public static int maxLengthOfMonth(int month) {
switch (month) {
case 2: //FEBRUARY:
return 29;
case 4: //APRIL:
case 6: //JUNE:
case 9: //SEPTEMBER:
case 11: //NOVEMBER:
return 30;
default:
return 31;
}
}
// DayOfWeek
// Returns the day-of-week that is the specified number of days after
// this one, from 1 to 7 for Monday to Sunday.
public static int plusDayOfWeek(int dow, long days) {
int amount = (int) (days % 7);
return (dow - 1 + (amount + 7)) % 7 + 1;
}
// Returns the day-of-week that is the specified number of days before
// this one, from 1 to 7 for Monday to Sunday.
public static int minusDayOfWeek(int dow, long days) {
return plusDayOfWeek(dow, -(days % 7));
}
// Adjusts the date to the first occurrence of the specified day-of-week
// before the date being adjusted unless it is already on that day in
// which case the same object is returned.
public static LocalDate previousOrSame(LocalDate date, int dayOfWeek) {
return adjust(date, dayOfWeek, 1);
}
// Adjusts the date to the first occurrence of the specified day-of-week
// after the date being adjusted unless it is already on that day in
// which case the same object is returned.
public static LocalDate nextOrSame(LocalDate date, int dayOfWeek) {
return adjust(date, dayOfWeek, 0);
}
// Implementation of next, previous or current day-of-week.
// @param relative whether the current date is a valid answer
private static final LocalDate adjust(LocalDate date, int dow, int relative) {
int calDow = date.getDayOfWeek();
if (relative < 2 && calDow == dow) {
return date;
}
if ((relative & 1) == 0) {
int daysDiff = calDow - dow;
return date.plusDays(daysDiff >= 0 ? 7 - daysDiff : -daysDiff);
} else {
int daysDiff = dow - calDow;
return date.minusDays(daysDiff >= 0 ? 7 - daysDiff : -daysDiff);
}
}
}

@ -1,474 +0,0 @@
/*
* Copyright (c) 2012, 2013, 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.
*/
/*
* This file is available under and governed by the GNU General Public
* License version 2 only, as published by the Free Software Foundation.
* However, the following notice accompanied the original version of this
* file:
*
* Copyright (c) 2007-2012, Stephen Colebourne & Michael Nascimento Santos
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of JSR-310 nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package build.tools.tzdb;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* A time-zone offset from Greenwich/UTC, such as {@code +02:00}.
* <p>
* A time-zone offset is the period of time that a time-zone differs from Greenwich/UTC.
* This is usually a fixed number of hours and minutes.
*
* @since 1.8
*/
final class ZoneOffset implements Comparable<ZoneOffset> {
/** Cache of time-zone offset by offset in seconds. */
private static final ConcurrentMap<Integer, ZoneOffset> SECONDS_CACHE = new ConcurrentHashMap<>(16, 0.75f, 4);
/** Cache of time-zone offset by ID. */
private static final ConcurrentMap<String, ZoneOffset> ID_CACHE = new ConcurrentHashMap<>(16, 0.75f, 4);
/**
* The number of seconds per hour.
*/
private static final int SECONDS_PER_HOUR = 60 * 60;
/**
* The number of seconds per minute.
*/
private static final int SECONDS_PER_MINUTE = 60;
/**
* The number of minutes per hour.
*/
private static final int MINUTES_PER_HOUR = 60;
/**
* The abs maximum seconds.
*/
private static final int MAX_SECONDS = 18 * SECONDS_PER_HOUR;
/**
* Serialization version.
*/
private static final long serialVersionUID = 2357656521762053153L;
/**
* The time-zone offset for UTC, with an ID of 'Z'.
*/
public static final ZoneOffset UTC = ZoneOffset.ofTotalSeconds(0);
/**
* Constant for the maximum supported offset.
*/
public static final ZoneOffset MIN = ZoneOffset.ofTotalSeconds(-MAX_SECONDS);
/**
* Constant for the maximum supported offset.
*/
public static final ZoneOffset MAX = ZoneOffset.ofTotalSeconds(MAX_SECONDS);
/**
* The total offset in seconds.
*/
private final int totalSeconds;
/**
* The string form of the time-zone offset.
*/
private final transient String id;
//-----------------------------------------------------------------------
/**
* Obtains an instance of {@code ZoneOffset} using the ID.
* <p>
* This method parses the string ID of a {@code ZoneOffset} to
* return an instance. The parsing accepts all the formats generated by
* {@link #getId()}, plus some additional formats:
* <p><ul>
* <li>{@code Z} - for UTC
* <li>{@code +h}
* <li>{@code +hh}
* <li>{@code +hh:mm}
* <li>{@code -hh:mm}
* <li>{@code +hhmm}
* <li>{@code -hhmm}
* <li>{@code +hh:mm:ss}
* <li>{@code -hh:mm:ss}
* <li>{@code +hhmmss}
* <li>{@code -hhmmss}
* </ul><p>
* Note that &plusmn; means either the plus or minus symbol.
* <p>
* The ID of the returned offset will be normalized to one of the formats
* described by {@link #getId()}.
* <p>
* The maximum supported range is from +18:00 to -18:00 inclusive.
*
* @param offsetId the offset ID, not null
* @return the zone-offset, not null
* @throws DateTimeException if the offset ID is invalid
*/
@SuppressWarnings("fallthrough")
public static ZoneOffset of(String offsetId) {
Objects.requireNonNull(offsetId, "offsetId");
// "Z" is always in the cache
ZoneOffset offset = ID_CACHE.get(offsetId);
if (offset != null) {
return offset;
}
// parse - +h, +hh, +hhmm, +hh:mm, +hhmmss, +hh:mm:ss
final int hours, minutes, seconds;
switch (offsetId.length()) {
case 2:
offsetId = offsetId.charAt(0) + "0" + offsetId.charAt(1); // fallthru
case 3:
hours = parseNumber(offsetId, 1, false);
minutes = 0;
seconds = 0;
break;
case 5:
hours = parseNumber(offsetId, 1, false);
minutes = parseNumber(offsetId, 3, false);
seconds = 0;
break;
case 6:
hours = parseNumber(offsetId, 1, false);
minutes = parseNumber(offsetId, 4, true);
seconds = 0;
break;
case 7:
hours = parseNumber(offsetId, 1, false);
minutes = parseNumber(offsetId, 3, false);
seconds = parseNumber(offsetId, 5, false);
break;
case 9:
hours = parseNumber(offsetId, 1, false);
minutes = parseNumber(offsetId, 4, true);
seconds = parseNumber(offsetId, 7, true);
break;
default:
throw new DateTimeException("Zone offset ID '" + offsetId + "' is invalid");
}
char first = offsetId.charAt(0);
if (first != '+' && first != '-') {
throw new DateTimeException("Zone offset ID '" + offsetId + "' is invalid: Plus/minus not found when expected");
}
if (first == '-') {
return ofHoursMinutesSeconds(-hours, -minutes, -seconds);
} else {
return ofHoursMinutesSeconds(hours, minutes, seconds);
}
}
/**
* Parse a two digit zero-prefixed number.
*
* @param offsetId the offset ID, not null
* @param pos the position to parse, valid
* @param precededByColon should this number be prefixed by a precededByColon
* @return the parsed number, from 0 to 99
*/
private static int parseNumber(CharSequence offsetId, int pos, boolean precededByColon) {
if (precededByColon && offsetId.charAt(pos - 1) != ':') {
throw new DateTimeException("Zone offset ID '" + offsetId + "' is invalid: Colon not found when expected");
}
char ch1 = offsetId.charAt(pos);
char ch2 = offsetId.charAt(pos + 1);
if (ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9') {
throw new DateTimeException("Zone offset ID '" + offsetId + "' is invalid: Non numeric characters found");
}
return (ch1 - 48) * 10 + (ch2 - 48);
}
//-----------------------------------------------------------------------
/**
* Obtains an instance of {@code ZoneOffset} using an offset in hours.
*
* @param hours the time-zone offset in hours, from -18 to +18
* @return the zone-offset, not null
* @throws DateTimeException if the offset is not in the required range
*/
public static ZoneOffset ofHours(int hours) {
return ofHoursMinutesSeconds(hours, 0, 0);
}
/**
* Obtains an instance of {@code ZoneOffset} using an offset in
* hours and minutes.
* <p>
* The sign of the hours and minutes components must match.
* Thus, if the hours is negative, the minutes must be negative or zero.
* If the hours is zero, the minutes may be positive, negative or zero.
*
* @param hours the time-zone offset in hours, from -18 to +18
* @param minutes the time-zone offset in minutes, from 0 to &plusmn;59, sign matches hours
* @return the zone-offset, not null
* @throws DateTimeException if the offset is not in the required range
*/
public static ZoneOffset ofHoursMinutes(int hours, int minutes) {
return ofHoursMinutesSeconds(hours, minutes, 0);
}
/**
* Obtains an instance of {@code ZoneOffset} using an offset in
* hours, minutes and seconds.
* <p>
* The sign of the hours, minutes and seconds components must match.
* Thus, if the hours is negative, the minutes and seconds must be negative or zero.
*
* @param hours the time-zone offset in hours, from -18 to +18
* @param minutes the time-zone offset in minutes, from 0 to &plusmn;59, sign matches hours and seconds
* @param seconds the time-zone offset in seconds, from 0 to &plusmn;59, sign matches hours and minutes
* @return the zone-offset, not null
* @throws DateTimeException if the offset is not in the required range
*/
public static ZoneOffset ofHoursMinutesSeconds(int hours, int minutes, int seconds) {
validate(hours, minutes, seconds);
int totalSeconds = totalSeconds(hours, minutes, seconds);
return ofTotalSeconds(totalSeconds);
}
/**
* Validates the offset fields.
*
* @param hours the time-zone offset in hours, from -18 to +18
* @param minutes the time-zone offset in minutes, from 0 to &plusmn;59
* @param seconds the time-zone offset in seconds, from 0 to &plusmn;59
* @throws DateTimeException if the offset is not in the required range
*/
private static void validate(int hours, int minutes, int seconds) {
if (hours < -18 || hours > 18) {
throw new DateTimeException("Zone offset hours not in valid range: value " + hours +
" is not in the range -18 to 18");
}
if (hours > 0) {
if (minutes < 0 || seconds < 0) {
throw new DateTimeException("Zone offset minutes and seconds must be positive because hours is positive");
}
} else if (hours < 0) {
if (minutes > 0 || seconds > 0) {
throw new DateTimeException("Zone offset minutes and seconds must be negative because hours is negative");
}
} else if ((minutes > 0 && seconds < 0) || (minutes < 0 && seconds > 0)) {
throw new DateTimeException("Zone offset minutes and seconds must have the same sign");
}
if (Math.abs(minutes) > 59) {
throw new DateTimeException("Zone offset minutes not in valid range: abs(value) " +
Math.abs(minutes) + " is not in the range 0 to 59");
}
if (Math.abs(seconds) > 59) {
throw new DateTimeException("Zone offset seconds not in valid range: abs(value) " +
Math.abs(seconds) + " is not in the range 0 to 59");
}
if (Math.abs(hours) == 18 && (Math.abs(minutes) > 0 || Math.abs(seconds) > 0)) {
throw new DateTimeException("Zone offset not in valid range: -18:00 to +18:00");
}
}
/**
* Calculates the total offset in seconds.
*
* @param hours the time-zone offset in hours, from -18 to +18
* @param minutes the time-zone offset in minutes, from 0 to &plusmn;59, sign matches hours and seconds
* @param seconds the time-zone offset in seconds, from 0 to &plusmn;59, sign matches hours and minutes
* @return the total in seconds
*/
private static int totalSeconds(int hours, int minutes, int seconds) {
return hours * SECONDS_PER_HOUR + minutes * SECONDS_PER_MINUTE + seconds;
}
//-----------------------------------------------------------------------
/**
* Obtains an instance of {@code ZoneOffset} specifying the total offset in seconds
* <p>
* The offset must be in the range {@code -18:00} to {@code +18:00}, which corresponds to -64800 to +64800.
*
* @param totalSeconds the total time-zone offset in seconds, from -64800 to +64800
* @return the ZoneOffset, not null
* @throws DateTimeException if the offset is not in the required range
*/
public static ZoneOffset ofTotalSeconds(int totalSeconds) {
if (Math.abs(totalSeconds) > MAX_SECONDS) {
throw new DateTimeException("Zone offset not in valid range: -18:00 to +18:00");
}
if (totalSeconds % (15 * SECONDS_PER_MINUTE) == 0) {
Integer totalSecs = totalSeconds;
ZoneOffset result = SECONDS_CACHE.get(totalSecs);
if (result == null) {
result = new ZoneOffset(totalSeconds);
SECONDS_CACHE.putIfAbsent(totalSecs, result);
result = SECONDS_CACHE.get(totalSecs);
ID_CACHE.putIfAbsent(result.getId(), result);
}
return result;
} else {
return new ZoneOffset(totalSeconds);
}
}
/**
* Constructor.
*
* @param totalSeconds the total time-zone offset in seconds, from -64800 to +64800
*/
private ZoneOffset(int totalSeconds) {
super();
this.totalSeconds = totalSeconds;
id = buildId(totalSeconds);
}
private static String buildId(int totalSeconds) {
if (totalSeconds == 0) {
return "Z";
} else {
int absTotalSeconds = Math.abs(totalSeconds);
StringBuilder buf = new StringBuilder();
int absHours = absTotalSeconds / SECONDS_PER_HOUR;
int absMinutes = (absTotalSeconds / SECONDS_PER_MINUTE) % MINUTES_PER_HOUR;
buf.append(totalSeconds < 0 ? "-" : "+")
.append(absHours < 10 ? "0" : "").append(absHours)
.append(absMinutes < 10 ? ":0" : ":").append(absMinutes);
int absSeconds = absTotalSeconds % SECONDS_PER_MINUTE;
if (absSeconds != 0) {
buf.append(absSeconds < 10 ? ":0" : ":").append(absSeconds);
}
return buf.toString();
}
}
/**
* Gets the total zone offset in seconds.
* <p>
* This is the primary way to access the offset amount.
* It returns the total of the hours, minutes and seconds fields as a
* single offset that can be added to a time.
*
* @return the total zone offset amount in seconds
*/
public int getTotalSeconds() {
return totalSeconds;
}
/**
* Gets the normalized zone offset ID.
* <p>
* The ID is minor variation to the standard ISO-8601 formatted string
* for the offset. There are three formats:
* <p><ul>
* <li>{@code Z} - for UTC (ISO-8601)
* <li>{@code +hh:mm} or {@code -hh:mm} - if the seconds are zero (ISO-8601)
* <li>{@code +hh:mm:ss} or {@code -hh:mm:ss} - if the seconds are non-zero (not ISO-8601)
* </ul><p>
*
* @return the zone offset ID, not null
*/
public String getId() {
return id;
}
/**
* Compares this offset to another offset in descending order.
* <p>
* The offsets are compared in the order that they occur for the same time
* of day around the world. Thus, an offset of {@code +10:00} comes before an
* offset of {@code +09:00} and so on down to {@code -18:00}.
* <p>
* The comparison is "consistent with equals", as defined by {@link Comparable}.
*
* @param other the other date to compare to, not null
* @return the comparator value, negative if less, postive if greater
* @throws NullPointerException if {@code other} is null
*/
@Override
public int compareTo(ZoneOffset other) {
return other.totalSeconds - totalSeconds;
}
/**
* Checks if this offset is equal to another offset.
* <p>
* The comparison is based on the amount of the offset in seconds.
* This is equivalent to a comparison by ID.
*
* @param obj the object to check, null returns false
* @return true if this is equal to the other offset
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof ZoneOffset) {
return totalSeconds == ((ZoneOffset) obj).totalSeconds;
}
return false;
}
/**
* A hash code for this offset.
*
* @return a suitable hash code
*/
@Override
public int hashCode() {
return totalSeconds;
}
/**
* Outputs this offset as a {@code String}, using the normalized ID.
*
* @return a string representation of this offset, not null
*/
@Override
public String toString() {
return id;
}
}

@ -1,290 +0,0 @@
/*
* Copyright (c) 2012, 2013, 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.
*/
/*
* This file is available under and governed by the GNU General Public
* License version 2 only, as published by the Free Software Foundation.
* However, the following notice accompanied the original version of this
* file:
*
* Copyright (c) 2009-2012, Stephen Colebourne & Michael Nascimento Santos
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of JSR-310 nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package build.tools.tzdb;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* A transition between two offsets caused by a discontinuity in the local time-line.
*
* @since 1.8
*/
final class ZoneOffsetTransition implements Comparable<ZoneOffsetTransition> {
/**
* The local transition date-time at the transition.
*/
private final LocalDateTime transition;
/**
* The offset before transition.
*/
private final ZoneOffset offsetBefore;
/**
* The offset after transition.
*/
private final ZoneOffset offsetAfter;
/**
* Creates an instance defining a transition between two offsets.
*
* @param transition the transition date-time with the offset before the transition, not null
* @param offsetBefore the offset before the transition, not null
* @param offsetAfter the offset at and after the transition, not null
*/
ZoneOffsetTransition(LocalDateTime transition, ZoneOffset offsetBefore, ZoneOffset offsetAfter) {
Objects.requireNonNull(transition, "transition");
Objects.requireNonNull(offsetBefore, "offsetBefore");
Objects.requireNonNull(offsetAfter, "offsetAfter");
if (offsetBefore.equals(offsetAfter)) {
throw new IllegalArgumentException("Offsets must not be equal");
}
this.transition = transition;
this.offsetBefore = offsetBefore;
this.offsetAfter = offsetAfter;
}
/**
* Creates an instance from epoch-second and offsets.
*
* @param epochSecond the transition epoch-second
* @param offsetBefore the offset before the transition, not null
* @param offsetAfter the offset at and after the transition, not null
*/
ZoneOffsetTransition(long epochSecond, ZoneOffset offsetBefore, ZoneOffset offsetAfter) {
this.transition = LocalDateTime.ofEpochSecond(epochSecond, 0, offsetBefore);
this.offsetBefore = offsetBefore;
this.offsetAfter = offsetAfter;
}
/**
* Gets the transition instant as an epoch second.
*
* @return the transition epoch second
*/
public long toEpochSecond() {
return transition.toEpochSecond(offsetBefore);
}
/**
* Gets the local transition date-time, as would be expressed with the 'before' offset.
* <p>
* 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.
* <p>
* The combination of the 'before' date-time and offset represents the same instant
* as the 'after' date-time and offset.
*
* @return the transition date-time expressed with the before offset, not null
*/
public LocalDateTime getDateTimeBefore() {
return transition;
}
/**
* Gets the local transition date-time, as would be expressed with the 'after' offset.
* <p>
* This is the first date-time after the discontinuity, when the new offset applies.
* <p>
* The combination of the 'before' date-time and offset represents the same instant
* as the 'after' date-time and offset.
*
* @return the transition date-time expressed with the after offset, not null
*/
public LocalDateTime getDateTimeAfter() {
return transition.plusSeconds(getDurationSeconds());
}
/**
* Gets the offset before the transition.
* <p>
* This is the offset in use before the instant of the transition.
*
* @return the offset before the transition, not null
*/
public ZoneOffset getOffsetBefore() {
return offsetBefore;
}
/**
* Gets the offset after the transition.
* <p>
* This is the offset in use on and after the instant of the transition.
*
* @return the offset after the transition, not null
*/
public ZoneOffset getOffsetAfter() {
return offsetAfter;
}
/**
* Gets the duration of the transition in seconds.
*
* @return the duration in seconds
*/
private int getDurationSeconds() {
return getOffsetAfter().getTotalSeconds() - getOffsetBefore().getTotalSeconds();
}
/**
* Does this transition represent a gap in the local time-line.
* <p>
* Gaps occur where there are local date-times that simply do not not exist.
* An example would be when the offset changes from {@code +01:00} to {@code +02:00}.
* This might be described as 'the clocks will move forward one hour tonight at 1am'.
*
* @return true if this transition is a gap, false if it is an overlap
*/
public boolean isGap() {
return getOffsetAfter().getTotalSeconds() > getOffsetBefore().getTotalSeconds();
}
/**
* Does this transition represent a gap in the local time-line.
* <p>
* Overlaps occur where there are local date-times that exist twice.
* An example would be when the offset changes from {@code +02:00} to {@code +01:00}.
* This might be described as 'the clocks will move back one hour tonight at 2am'.
*
* @return true if this transition is an overlap, false if it is a gap
*/
public boolean isOverlap() {
return getOffsetAfter().getTotalSeconds() < getOffsetBefore().getTotalSeconds();
}
/**
* Checks if the specified offset is valid during this transition.
* <p>
* 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 offset the offset to check, null returns false
* @return true if the offset is valid during the transition
*/
public boolean isValidOffset(ZoneOffset offset) {
return isGap() ? false : (getOffsetBefore().equals(offset) || getOffsetAfter().equals(offset));
}
/**
* Gets the valid offsets during this transition.
* <p>
* A gap will return an empty list, while an overlap will return both offsets.
*
* @return the list of valid offsets
*/
List<ZoneOffset> getValidOffsets() {
if (isGap()) {
return Collections.emptyList();
}
return Arrays.asList(getOffsetBefore(), getOffsetAfter());
}
/**
* Compares this transition to another based on the transition instant.
* <p>
* This compares the instants of each transition.
* The offsets are ignored, making this order inconsistent with equals.
*
* @param transition the transition to compare to, not null
* @return the comparator value, negative if less, positive if greater
*/
@Override
public int compareTo(ZoneOffsetTransition transition) {
return Long.compare(this.toEpochSecond(), transition.toEpochSecond());
}
/**
* Checks if this object equals another.
* <p>
* The entire state of the object is compared.
*
* @param other the other object to compare to, null returns false
* @return true if equal
*/
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (other instanceof ZoneOffsetTransition) {
ZoneOffsetTransition d = (ZoneOffsetTransition) other;
return transition.equals(d.transition) &&
offsetBefore.equals(d.offsetBefore) && offsetAfter.equals(d.offsetAfter);
}
return false;
}
/**
* Returns a suitable hash code.
*
* @return the hash code
*/
@Override
public int hashCode() {
return transition.hashCode() ^ offsetBefore.hashCode() ^ Integer.rotateLeft(offsetAfter.hashCode(), 16);
}
}

@ -1,223 +0,0 @@
/*
* Copyright (c) 2012, 2013, 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.
*/
/*
* This file is available under and governed by the GNU General Public
* License version 2 only, as published by the Free Software Foundation.
* However, the following notice accompanied the original version of this
* file:
*
* Copyright (c) 2009-2012, Stephen Colebourne & Michael Nascimento Santos
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of JSR-310 nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package build.tools.tzdb;
import static build.tools.tzdb.Utils.*;
import java.util.Objects;
/**
* A rule expressing how to create a transition.
* <p>
* This class allows rules for identifying future transitions to be expressed.
* A rule might be written in many forms:
* <p><ul>
* <li>the 16th March
* <li>the Sunday on or after the 16th March
* <li>the Sunday on or before the 16th March
* <li>the last Sunday in February
* </ul><p>
* These different rule types can be expressed and queried.
*
* <h3>Specification for implementors</h3>
* This class is immutable and thread-safe.
*
* @since 1.8
*/
final class ZoneOffsetTransitionRule {
/**
* The month of the month-day of the first day of the cutover week.
* The actual date will be adjusted by the dowChange field.
*/
final int month;
/**
* The day-of-month of the month-day of the cutover week.
* If positive, it is the start of the week where the cutover can occur.
* If negative, it represents the end of the week where cutover can occur.
* The value is the number of days from the end of the month, such that
* {@code -1} is the last day of the month, {@code -2} is the second
* to last day, and so on.
*/
final byte dom;
/**
* The cutover day-of-week, -1 to retain the day-of-month.
*/
final int dow;
/**
* The cutover time in the 'before' offset.
*/
final LocalTime time;
/**
* Whether the cutover time is midnight at the end of day.
*/
final boolean timeEndOfDay;
/**
* The definition of how the local time should be interpreted.
*/
final TimeDefinition timeDefinition;
/**
* The standard offset at the cutover.
*/
final ZoneOffset standardOffset;
/**
* The offset before the cutover.
*/
final ZoneOffset offsetBefore;
/**
* The offset after the cutover.
*/
final ZoneOffset offsetAfter;
/**
* Creates an instance defining the yearly rule to create transitions between two offsets.
*
* @param month the month of the month-day of the first day of the cutover week, from 1 to 12
* @param dayOfMonthIndicator the day of the month-day of the cutover week, positive if the week is that
* day or later, negative if the week is that day or earlier, counting from the last day of the month,
* from -28 to 31 excluding 0
* @param dayOfWeek the required day-of-week, -1 if the month-day should not be changed
* @param time the cutover time in the 'before' offset, not null
* @param timeEndOfDay whether the time is midnight at the end of day
* @param timeDefnition how to interpret the cutover
* @param standardOffset the standard offset in force at the cutover, not null
* @param offsetBefore the offset before the cutover, not null
* @param offsetAfter the offset after the cutover, not null
* @throws IllegalArgumentException if the day of month indicator is invalid
* @throws IllegalArgumentException if the end of day flag is true when the time is not midnight
*/
ZoneOffsetTransitionRule(
int month,
int dayOfMonthIndicator,
int dayOfWeek,
LocalTime time,
boolean timeEndOfDay,
TimeDefinition timeDefnition,
ZoneOffset standardOffset,
ZoneOffset offsetBefore,
ZoneOffset offsetAfter) {
Objects.requireNonNull(time, "time");
Objects.requireNonNull(timeDefnition, "timeDefnition");
Objects.requireNonNull(standardOffset, "standardOffset");
Objects.requireNonNull(offsetBefore, "offsetBefore");
Objects.requireNonNull(offsetAfter, "offsetAfter");
if (month < 1 || month > 12) {
throw new IllegalArgumentException("month must be between 1 and 12");
}
if (dayOfMonthIndicator < -28 || dayOfMonthIndicator > 31 || dayOfMonthIndicator == 0) {
throw new IllegalArgumentException("Day of month indicator must be between -28 and 31 inclusive excluding zero");
}
if (timeEndOfDay && time.equals(LocalTime.MIDNIGHT) == false) {
throw new IllegalArgumentException("Time must be midnight when end of day flag is true");
}
this.month = month;
this.dom = (byte) dayOfMonthIndicator;
this.dow = dayOfWeek;
this.time = time;
this.timeEndOfDay = timeEndOfDay;
this.timeDefinition = timeDefnition;
this.standardOffset = standardOffset;
this.offsetBefore = offsetBefore;
this.offsetAfter = offsetAfter;
}
//-----------------------------------------------------------------------
/**
* Checks if this object equals another.
* <p>
* The entire state of the object is compared.
*
* @param otherRule the other object to compare to, null returns false
* @return true if equal
*/
@Override
public boolean equals(Object otherRule) {
if (otherRule == this) {
return true;
}
if (otherRule instanceof ZoneOffsetTransitionRule) {
ZoneOffsetTransitionRule other = (ZoneOffsetTransitionRule) otherRule;
return month == other.month && dom == other.dom && dow == other.dow &&
timeDefinition == other.timeDefinition &&
time.equals(other.time) &&
timeEndOfDay == other.timeEndOfDay &&
standardOffset.equals(other.standardOffset) &&
offsetBefore.equals(other.offsetBefore) &&
offsetAfter.equals(other.offsetAfter);
}
return false;
}
/**
* Returns a suitable hash code.
*
* @return the hash code
*/
@Override
public int hashCode() {
int hash = ((time.toSecondOfDay() + (timeEndOfDay ? 1 : 0)) << 15) +
(month << 11) + ((dom + 32) << 5) +
((dow == -1 ? 8 : dow) << 2) + (timeDefinition.ordinal());
return hash ^ standardOffset.hashCode() ^
offsetBefore.hashCode() ^ offsetAfter.hashCode();
}
}

@ -64,6 +64,12 @@ package build.tools.tzdb;
import java.io.DataOutput;
import java.io.IOException;
import java.io.ObjectOutput;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.time.zone.ZoneOffsetTransition;
import java.time.zone.ZoneOffsetTransitionRule;
import java.time.zone.ZoneOffsetTransitionRule.TimeDefinition;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
@ -224,15 +230,15 @@ final class ZoneRules {
* @throws IOException if an error occurs
*/
static void writeRule(ZoneOffsetTransitionRule rule, DataOutput out) throws IOException {
int month = rule.month;
byte dom = rule.dom;
int dow = rule.dow;
LocalTime time = rule.time;
boolean timeEndOfDay = rule.timeEndOfDay;
TimeDefinition timeDefinition = rule.timeDefinition;
ZoneOffset standardOffset = rule.standardOffset;
ZoneOffset offsetBefore = rule.offsetBefore;
ZoneOffset offsetAfter = rule.offsetAfter;
int month = rule.getMonth().getValue();
byte dom = (byte)rule.getDayOfMonthIndicator();
int dow = rule.getDayOfWeek().getValue();
LocalTime time = rule.getLocalTime();
boolean timeEndOfDay = rule.isMidnightEndOfDay();
TimeDefinition timeDefinition = rule.getTimeDefinition();
ZoneOffset standardOffset = rule.getStandardOffset();
ZoneOffset offsetBefore = rule.getOffsetBefore();
ZoneOffset offsetAfter = rule.getOffsetAfter();
int timeSecs = (timeEndOfDay ? 86400 : time.toSecondOfDay());
int stdOffset = standardOffset.getTotalSeconds();

@ -1,743 +0,0 @@
/*
* Copyright (c) 2012, 2013, 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.
*/
/*
* This file is available under and governed by the GNU General Public
* License version 2 only, as published by the Free Software Foundation.
* However, the following notice accompanied the original version of this
* file:
*
* Copyright (c) 2009-2012, Stephen Colebourne & Michael Nascimento Santos
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of JSR-310 nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package build.tools.tzdb;
import static build.tools.tzdb.Utils.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* A mutable builder used to create all the rules for a historic time-zone.
* <p>
* The rules of a time-zone describe how the offset changes over time.
* The rules are created by building windows on the time-line within which
* the different rules apply. The rules may be one of two kinds:
* <p><ul>
* <li>Fixed savings - A single fixed amount of savings from the standard offset will apply.</li>
* <li>Rules - A set of one or more rules describe how daylight savings changes during the window.</li>
* </ul><p>
*
* <h4>Implementation notes</h4>
* This class is a mutable builder used to create zone instances.
* It must only be used from a single thread.
* The created instances are immutable and thread-safe.
*
* @since 1.8
*/
public class ZoneRulesBuilder {
/**
* The list of windows.
*/
private List<TZWindow> windowList = new ArrayList<>();
//-----------------------------------------------------------------------
/**
* Constructs an instance of the builder that can be used to create zone rules.
* <p>
* The builder is used by adding one or more windows representing portions
* of the time-line. The standard offset from UTC/Greenwich will be constant
* within a window, although two adjacent windows can have the same standard offset.
* <p>
* Within each window, there can either be a
* {@link #setFixedSavingsToWindow fixed savings amount} or a
* {@link #addRuleToWindow list of rules}.
*/
public ZoneRulesBuilder() {
}
//-----------------------------------------------------------------------
/**
* Adds a window to the builder that can be used to filter a set of rules.
* <p>
* This method defines and adds a window to the zone where the standard offset is specified.
* The window limits the effect of subsequent additions of transition rules
* or fixed savings. If neither rules or fixed savings are added to the window
* then the window will default to no savings.
* <p>
* Each window must be added sequentially, as the start instant of the window
* is derived from the until instant of the previous window.
*
* @param standardOffset the standard offset, not null
* @param until the date-time that the offset applies until, not null
* @param untilDefinition the time type for the until date-time, not null
* @return this, for chaining
* @throws IllegalStateException if the window order is invalid
*/
public ZoneRulesBuilder addWindow(
ZoneOffset standardOffset,
LocalDateTime until,
TimeDefinition untilDefinition) {
Objects.requireNonNull(standardOffset, "standardOffset");
Objects.requireNonNull(until, "until");
Objects.requireNonNull(untilDefinition, "untilDefinition");
TZWindow window = new TZWindow(standardOffset, until, untilDefinition);
if (windowList.size() > 0) {
TZWindow previous = windowList.get(windowList.size() - 1);
window.validateWindowOrder(previous);
}
windowList.add(window);
return this;
}
/**
* Adds a window that applies until the end of time to the builder that can be
* used to filter a set of rules.
* <p>
* This method defines and adds a window to the zone where the standard offset is specified.
* The window limits the effect of subsequent additions of transition rules
* or fixed savings. If neither rules or fixed savings are added to the window
* then the window will default to no savings.
* <p>
* This must be added after all other windows.
* No more windows can be added after this one.
*
* @param standardOffset the standard offset, not null
* @return this, for chaining
* @throws IllegalStateException if a forever window has already been added
*/
public ZoneRulesBuilder addWindowForever(ZoneOffset standardOffset) {
return addWindow(standardOffset, LocalDateTime.MAX, TimeDefinition.WALL);
}
//-----------------------------------------------------------------------
/**
* Sets the previously added window to have fixed savings.
* <p>
* Setting a window to have fixed savings simply means that a single daylight
* savings amount applies throughout the window. The window could be small,
* such as a single summer, or large, such as a multi-year daylight savings.
* <p>
* A window can either have fixed savings or rules but not both.
*
* @param fixedSavingAmountSecs the amount of saving to use for the whole window, not null
* @return this, for chaining
* @throws IllegalStateException if no window has yet been added
* @throws IllegalStateException if the window already has rules
*/
public ZoneRulesBuilder setFixedSavingsToWindow(int fixedSavingAmountSecs) {
if (windowList.isEmpty()) {
throw new IllegalStateException("Must add a window before setting the fixed savings");
}
TZWindow window = windowList.get(windowList.size() - 1);
window.setFixedSavings(fixedSavingAmountSecs);
return this;
}
//-----------------------------------------------------------------------
/**
* Adds a single transition rule to the current window.
* <p>
* This adds a rule such that the offset, expressed as a daylight savings amount,
* changes at the specified date-time.
*
* @param transitionDateTime the date-time that the transition occurs as defined by timeDefintion, not null
* @param timeDefinition the definition of how to convert local to actual time, not null
* @param savingAmountSecs the amount of saving from the standard offset after the transition in seconds
* @return this, for chaining
* @throws IllegalStateException if no window has yet been added
* @throws IllegalStateException if the window already has fixed savings
* @throws IllegalStateException if the window has reached the maximum capacity of 2000 rules
*/
public ZoneRulesBuilder addRuleToWindow(
LocalDateTime transitionDateTime,
TimeDefinition timeDefinition,
int savingAmountSecs) {
Objects.requireNonNull(transitionDateTime, "transitionDateTime");
return addRuleToWindow(
transitionDateTime.getYear(), transitionDateTime.getYear(),
transitionDateTime.getMonth(), transitionDateTime.getDayOfMonth(),
-1, transitionDateTime.getTime(), false, timeDefinition, savingAmountSecs);
}
/**
* Adds a single transition rule to the current window.
* <p>
* This adds a rule such that the offset, expressed as a daylight savings amount,
* changes at the specified date-time.
*
* @param year the year of the transition, from MIN_YEAR to MAX_YEAR
* @param month the month of the transition, not null
* @param dayOfMonthIndicator the day-of-month of the transition, adjusted by dayOfWeek,
* from 1 to 31 adjusted later, or -1 to -28 adjusted earlier from the last day of the month
* @param time the time that the transition occurs as defined by timeDefintion, not null
* @param timeEndOfDay whether midnight is at the end of day
* @param timeDefinition the definition of how to convert local to actual time, not null
* @param savingAmountSecs the amount of saving from the standard offset after the transition in seconds
* @return this, for chaining
* @throws DateTimeException if a date-time field is out of range
* @throws IllegalStateException if no window has yet been added
* @throws IllegalStateException if the window already has fixed savings
* @throws IllegalStateException if the window has reached the maximum capacity of 2000 rules
*/
public ZoneRulesBuilder addRuleToWindow(
int year,
int month,
int dayOfMonthIndicator,
LocalTime time,
boolean timeEndOfDay,
TimeDefinition timeDefinition,
int savingAmountSecs) {
return addRuleToWindow(year, year, month, dayOfMonthIndicator, -1, time, timeEndOfDay, timeDefinition, savingAmountSecs);
}
/**
* Adds a multi-year transition rule to the current window.
* <p>
* This adds a rule such that the offset, expressed as a daylight savings amount,
* changes at the specified date-time for each year in the range.
*
* @param startYear the start year of the rule, from MIN_YEAR to MAX_YEAR
* @param endYear the end year of the rule, from MIN_YEAR to MAX_YEAR
* @param month the month of the transition, from 1 to 12
* @param dayOfMonthIndicator the day-of-month of the transition, adjusted by dayOfWeek,
* from 1 to 31 adjusted later, or -1 to -28 adjusted earlier from the last day of the month
* @param dayOfWeek the day-of-week to adjust to, -1 if day-of-month should not be adjusted
* @param time the time that the transition occurs as defined by timeDefintion, not null
* @param timeEndOfDay whether midnight is at the end of day
* @param timeDefinition the definition of how to convert local to actual time, not null
* @param savingAmountSecs the amount of saving from the standard offset after the transition in seconds
* @return this, for chaining
* @throws DateTimeException if a date-time field is out of range
* @throws IllegalArgumentException if the day of month indicator is invalid
* @throws IllegalArgumentException if the end of day midnight flag does not match the time
* @throws IllegalStateException if no window has yet been added
* @throws IllegalStateException if the window already has fixed savings
* @throws IllegalStateException if the window has reached the maximum capacity of 2000 rules
*/
public ZoneRulesBuilder addRuleToWindow(
int startYear,
int endYear,
int month,
int dayOfMonthIndicator,
int dayOfWeek,
LocalTime time,
boolean timeEndOfDay,
TimeDefinition timeDefinition,
int savingAmountSecs) {
Objects.requireNonNull(time, "time");
Objects.requireNonNull(timeDefinition, "timeDefinition");
if (dayOfMonthIndicator < -28 || dayOfMonthIndicator > 31 || dayOfMonthIndicator == 0) {
throw new IllegalArgumentException("Day of month indicator must be between -28 and 31 inclusive excluding zero");
}
if (timeEndOfDay && time.equals(LocalTime.MIDNIGHT) == false) {
throw new IllegalArgumentException("Time must be midnight when end of day flag is true");
}
if (windowList.isEmpty()) {
throw new IllegalStateException("Must add a window before adding a rule");
}
TZWindow window = windowList.get(windowList.size() - 1);
window.addRule(startYear, endYear, month, dayOfMonthIndicator, dayOfWeek, time, timeEndOfDay, timeDefinition, savingAmountSecs);
return this;
}
//-----------------------------------------------------------------------
/**
* Completes the build converting the builder to a set of time-zone rules.
* <p>
* Calling this method alters the state of the builder.
* Further rules should not be added to this builder once this method is called.
*
* @param zoneId the time-zone ID, not null
* @return the zone rules, not null
* @throws IllegalStateException if no windows have been added
* @throws IllegalStateException if there is only one rule defined as being forever for any given window
*/
public ZoneRules toRules(String zoneId) {
Objects.requireNonNull(zoneId, "zoneId");
if (windowList.isEmpty()) {
throw new IllegalStateException("No windows have been added to the builder");
}
final List<ZoneOffsetTransition> standardTransitionList = new ArrayList<>(4);
final List<ZoneOffsetTransition> transitionList = new ArrayList<>(256);
final List<ZoneOffsetTransitionRule> lastTransitionRuleList = new ArrayList<>(2);
// initialize the standard offset calculation
final TZWindow firstWindow = windowList.get(0);
ZoneOffset loopStandardOffset = firstWindow.standardOffset;
int loopSavings = 0;
if (firstWindow.fixedSavingAmountSecs != null) {
loopSavings = firstWindow.fixedSavingAmountSecs;
}
final ZoneOffset firstWallOffset = ZoneOffset.ofTotalSeconds(loopStandardOffset.getTotalSeconds() + loopSavings);
LocalDateTime loopWindowStart = LocalDateTime.of(YEAR_MIN_VALUE, 1, 1, 0, 0);
ZoneOffset loopWindowOffset = firstWallOffset;
// build the windows and rules to interesting data
for (TZWindow window : windowList) {
// tidy the state
window.tidy(loopWindowStart.getYear());
// calculate effective savings at the start of the window
Integer effectiveSavings = window.fixedSavingAmountSecs;
if (effectiveSavings == null) {
// apply rules from this window together with the standard offset and
// savings from the last window to find the savings amount applicable
// at start of this window
effectiveSavings = 0;
for (TZRule rule : window.ruleList) {
if (rule.toEpochSecond(loopStandardOffset, loopSavings) > loopWindowStart.toEpochSecond(loopWindowOffset)) {
// previous savings amount found, which could be the savings amount at
// the instant that the window starts (hence isAfter)
break;
}
effectiveSavings = rule.savingAmountSecs;
}
}
// check if standard offset changed, and update it
if (loopStandardOffset.equals(window.standardOffset) == false) {
standardTransitionList.add(
new ZoneOffsetTransition(
LocalDateTime.ofEpochSecond(loopWindowStart.toEpochSecond(loopWindowOffset), 0, loopStandardOffset),
loopStandardOffset, window.standardOffset));
loopStandardOffset = window.standardOffset;
}
// check if the start of the window represents a transition
ZoneOffset effectiveWallOffset = ZoneOffset.ofTotalSeconds(loopStandardOffset.getTotalSeconds() + effectiveSavings);
if (loopWindowOffset.equals(effectiveWallOffset) == false) {
transitionList.add(new ZoneOffsetTransition(loopWindowStart, loopWindowOffset, effectiveWallOffset));
}
loopSavings = effectiveSavings;
// apply rules within the window
for (TZRule rule : window.ruleList) {
if (rule.isTransition(loopSavings)) {
ZoneOffsetTransition trans = rule.toTransition(loopStandardOffset, loopSavings);
if (trans.toEpochSecond() < loopWindowStart.toEpochSecond(loopWindowOffset) == false &&
trans.toEpochSecond() < window.createDateTimeEpochSecond(loopSavings)) {
transitionList.add(trans);
loopSavings = rule.savingAmountSecs;
}
}
}
// calculate last rules
for (TZRule lastRule : window.lastRuleList) {
lastTransitionRuleList.add(lastRule.toTransitionRule(loopStandardOffset, loopSavings));
loopSavings = lastRule.savingAmountSecs;
}
// finally we can calculate the true end of the window, passing it to the next window
loopWindowOffset = window.createWallOffset(loopSavings);
loopWindowStart = LocalDateTime.ofEpochSecond(
window.createDateTimeEpochSecond(loopSavings), 0, loopWindowOffset);
}
return new ZoneRules(
firstWindow.standardOffset, firstWallOffset, standardTransitionList,
transitionList, lastTransitionRuleList);
}
//-----------------------------------------------------------------------
/**
* A definition of a window in the time-line.
* The window will have one standard offset and will either have a
* fixed DST savings or a set of rules.
*/
class TZWindow {
/** The standard offset during the window, not null. */
private final ZoneOffset standardOffset;
/** The end local time, not null. */
private final LocalDateTime windowEnd;
/** The type of the end time, not null. */
private final TimeDefinition timeDefinition;
/** The fixed amount of the saving to be applied during this window. */
private Integer fixedSavingAmountSecs;
/** The rules for the current window. */
private List<TZRule> ruleList = new ArrayList<>();
/** The latest year that the last year starts at. */
private int maxLastRuleStartYear = YEAR_MIN_VALUE;
/** The last rules. */
private List<TZRule> lastRuleList = new ArrayList<>();
/**
* Constructor.
*
* @param standardOffset the standard offset applicable during the window, not null
* @param windowEnd the end of the window, relative to the time definition, null if forever
* @param timeDefinition the time definition for calculating the true end, not null
*/
TZWindow(
ZoneOffset standardOffset,
LocalDateTime windowEnd,
TimeDefinition timeDefinition) {
super();
this.windowEnd = windowEnd;
this.timeDefinition = timeDefinition;
this.standardOffset = standardOffset;
}
/**
* Sets the fixed savings amount for the window.
*
* @param fixedSavingAmount the amount of daylight saving to apply throughout the window, may be null
* @throws IllegalStateException if the window already has rules
*/
void setFixedSavings(int fixedSavingAmount) {
if (ruleList.size() > 0 || lastRuleList.size() > 0) {
throw new IllegalStateException("Window has DST rules, so cannot have fixed savings");
}
this.fixedSavingAmountSecs = fixedSavingAmount;
}
/**
* Adds a rule to the current window.
*
* @param startYear the start year of the rule, from MIN_YEAR to MAX_YEAR
* @param endYear the end year of the rule, from MIN_YEAR to MAX_YEAR
* @param month the month of the transition, not null
* @param dayOfMonthIndicator the day-of-month of the transition, adjusted by dayOfWeek,
* from 1 to 31 adjusted later, or -1 to -28 adjusted earlier from the last day of the month
* @param dayOfWeek the day-of-week to adjust to, null if day-of-month should not be adjusted
* @param time the time that the transition occurs as defined by timeDefintion, not null
* @param timeEndOfDay whether midnight is at the end of day
* @param timeDefinition the definition of how to convert local to actual time, not null
* @param savingAmountSecs the amount of saving from the standard offset in seconds
* @throws IllegalStateException if the window already has fixed savings
* @throws IllegalStateException if the window has reached the maximum capacity of 2000 rules
*/
void addRule(
int startYear,
int endYear,
int month,
int dayOfMonthIndicator,
int dayOfWeek,
LocalTime time,
boolean timeEndOfDay,
TimeDefinition timeDefinition,
int savingAmountSecs) {
if (fixedSavingAmountSecs != null) {
throw new IllegalStateException("Window has a fixed DST saving, so cannot have DST rules");
}
if (ruleList.size() >= 2000) {
throw new IllegalStateException("Window has reached the maximum number of allowed rules");
}
boolean lastRule = false;
if (endYear == YEAR_MAX_VALUE) {
lastRule = true;
endYear = startYear;
}
int year = startYear;
while (year <= endYear) {
TZRule rule = new TZRule(year, month, dayOfMonthIndicator, dayOfWeek, time, timeEndOfDay, timeDefinition, savingAmountSecs);
if (lastRule) {
lastRuleList.add(rule);
maxLastRuleStartYear = Math.max(startYear, maxLastRuleStartYear);
} else {
ruleList.add(rule);
}
year++;
}
}
/**
* Validates that this window is after the previous one.
*
* @param previous the previous window, not null
* @throws IllegalStateException if the window order is invalid
*/
void validateWindowOrder(TZWindow previous) {
if (windowEnd.compareTo(previous.windowEnd) < 0) {
throw new IllegalStateException("Windows must be added in date-time order: " +
windowEnd + " < " + previous.windowEnd);
}
}
/**
* Adds rules to make the last rules all start from the same year.
* Also add one more year to avoid weird case where penultimate year has odd offset.
*
* @param windowStartYear the window start year
* @throws IllegalStateException if there is only one rule defined as being forever
*/
void tidy(int windowStartYear) {
if (lastRuleList.size() == 1) {
throw new IllegalStateException("Cannot have only one rule defined as being forever");
}
// handle last rules
if (windowEnd.equals(LocalDateTime.MAX)) {
// setup at least one real rule, which closes off other windows nicely
maxLastRuleStartYear = Math.max(maxLastRuleStartYear, windowStartYear) + 1;
for (TZRule lastRule : lastRuleList) {
addRule(lastRule.year, maxLastRuleStartYear, lastRule.month, lastRule.dayOfMonthIndicator,
lastRule.dayOfWeek, lastRule.time, lastRule.timeEndOfDay, lastRule.timeDefinition, lastRule.savingAmountSecs);
lastRule.year = maxLastRuleStartYear + 1;
}
if (maxLastRuleStartYear == YEAR_MAX_VALUE) {
lastRuleList.clear();
} else {
maxLastRuleStartYear++;
}
} else {
// convert all within the endYear limit
int endYear = windowEnd.getYear();
for (TZRule lastRule : lastRuleList) {
addRule(lastRule.year, endYear + 1, lastRule.month, lastRule.dayOfMonthIndicator,
lastRule.dayOfWeek, lastRule.time, lastRule.timeEndOfDay, lastRule.timeDefinition, lastRule.savingAmountSecs);
}
lastRuleList.clear();
maxLastRuleStartYear = YEAR_MAX_VALUE;
}
// ensure lists are sorted
Collections.sort(ruleList);
Collections.sort(lastRuleList);
// default fixed savings to zero
if (ruleList.size() == 0 && fixedSavingAmountSecs == null) {
fixedSavingAmountSecs = 0;
}
}
/**
* Checks if the window is empty.
*
* @return true if the window is only a standard offset
*/
boolean isSingleWindowStandardOffset() {
return windowEnd.equals(LocalDateTime.MAX) && timeDefinition == TimeDefinition.WALL &&
fixedSavingAmountSecs == null && lastRuleList.isEmpty() && ruleList.isEmpty();
}
/**
* Creates 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
*/
ZoneOffset createWallOffset(int savingsSecs) {
return ZoneOffset.ofTotalSeconds(standardOffset.getTotalSeconds() + savingsSecs);
}
/**
* Creates the offset date-time 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 createDateTimeEpochSecond(int savingsSecs) {
ZoneOffset wallOffset = createWallOffset(savingsSecs);
LocalDateTime ldt = timeDefinition.createDateTime(windowEnd, standardOffset, wallOffset);
return ldt.toEpochSecond(wallOffset);
}
}
//-----------------------------------------------------------------------
/**
* A definition of the way a local time can be converted to an offset time.
*/
class TZRule implements Comparable<TZRule> {
private int year;
private int month;
private int dayOfMonthIndicator;
private int dayOfWeek;
private LocalTime time;
private boolean timeEndOfDay; // Whether the local time is end of day.
private TimeDefinition timeDefinition; // The type of the time.
private int savingAmountSecs; // The amount of the saving to be applied after this point.
/**
* Constructor.
*
* @param year the year
* @param month the month, value from 1 to 12
* @param dayOfMonthIndicator the day-of-month of the transition, adjusted by dayOfWeek,
* from 1 to 31 adjusted later, or -1 to -28 adjusted earlier from the last day of the month
* @param dayOfWeek the day-of-week, -1 if day-of-month is exact
* @param time the time, not null
* @param timeEndOfDay whether midnight is at the end of day
* @param timeDefinition the time definition, not null
* @param savingAfterSecs the savings amount in seconds
*/
TZRule(int year, int month, int dayOfMonthIndicator,
int dayOfWeek, LocalTime time, boolean timeEndOfDay,
TimeDefinition timeDefinition, int savingAfterSecs) {
this.year = year;
this.month = month;
this.dayOfMonthIndicator = dayOfMonthIndicator;
this.dayOfWeek = dayOfWeek;
this.time = time;
this.timeEndOfDay = timeEndOfDay;
this.timeDefinition = timeDefinition;
this.savingAmountSecs = savingAfterSecs;
}
/**
* Converts this to a transition.
*
* @param standardOffset the active standard offset, not null
* @param savingsBeforeSecs the active savings in seconds
* @return the transition, not null
*/
ZoneOffsetTransition toTransition(ZoneOffset standardOffset, int savingsBeforeSecs) {
// copy of code in ZoneOffsetTransitionRule to avoid infinite loop
LocalDate date = toLocalDate();
LocalDateTime ldt = LocalDateTime.of(date, time);
ZoneOffset wallOffset = ZoneOffset.ofTotalSeconds(standardOffset.getTotalSeconds() + savingsBeforeSecs);
LocalDateTime dt = timeDefinition.createDateTime(ldt, standardOffset, wallOffset);
ZoneOffset offsetAfter = ZoneOffset.ofTotalSeconds(standardOffset.getTotalSeconds() + savingAmountSecs);
return new ZoneOffsetTransition(dt, wallOffset, offsetAfter);
}
/**
* Returns the apoch second of this rules with the specified
* active standard offset and active savings
*
* @param standardOffset the active standard offset, not null
* @param savingsBeforeSecs the active savings in seconds
* @return the transition epoch second
*/
long toEpochSecond(ZoneOffset standardOffset, int savingsBeforeSecs) {
LocalDateTime ldt = LocalDateTime.of(toLocalDate(), time);
ZoneOffset wallOffset = ZoneOffset.ofTotalSeconds(standardOffset.getTotalSeconds() + savingsBeforeSecs);
return timeDefinition.createDateTime(ldt, standardOffset, wallOffset)
.toEpochSecond(wallOffset);
}
/**
* Tests if this a real transition with the active savings in seconds
*
* @param savingsBeforeSecs the active savings in seconds
* @return true, if savings in seconds changes
*/
boolean isTransition(int savingsBeforeSecs) {
return savingAmountSecs != savingsBeforeSecs;
}
/**
* 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 standardOffset, int savingsBeforeSecs) {
// optimize stored format
if (dayOfMonthIndicator < 0) {
if (month != 2) { // not Month.FEBRUARY
dayOfMonthIndicator = maxLengthOfMonth(month) - 6;
}
}
if (timeEndOfDay && dayOfMonthIndicator > 0 &&
(dayOfMonthIndicator == 28 && month == 2) == false) {
LocalDate date = LocalDate.of(2004, month, dayOfMonthIndicator).plusDays(1); // leap-year
month = date.getMonth();
dayOfMonthIndicator = date.getDayOfMonth();
if (dayOfWeek != -1) {
dayOfWeek = plusDayOfWeek(dayOfWeek, 1);
}
timeEndOfDay = false;
}
// build rule
return new ZoneOffsetTransitionRule(
month, dayOfMonthIndicator, dayOfWeek, time, timeEndOfDay, timeDefinition,
standardOffset,
ZoneOffset.ofTotalSeconds(standardOffset.getTotalSeconds() + savingsBeforeSecs),
ZoneOffset.ofTotalSeconds(standardOffset.getTotalSeconds() + savingAmountSecs));
}
public int compareTo(TZRule other) {
int cmp = year - other.year;
cmp = (cmp == 0 ? month - other.month : cmp);
if (cmp == 0) {
// convert to date to handle dow/domIndicator/timeEndOfDay
LocalDate thisDate = toLocalDate();
LocalDate otherDate = other.toLocalDate();
cmp = thisDate.compareTo(otherDate);
}
cmp = (cmp == 0 ? time.compareTo(other.time) : cmp);
return cmp;
}
private LocalDate toLocalDate() {
LocalDate date;
if (dayOfMonthIndicator < 0) {
int monthLen = lengthOfMonth(month, isLeapYear(year));
date = LocalDate.of(year, month, monthLen + 1 + dayOfMonthIndicator);
if (dayOfWeek != -1) {
date = previousOrSame(date, dayOfWeek);
}
} else {
date = LocalDate.of(year, month, dayOfMonthIndicator);
if (dayOfWeek != -1) {
date = nextOrSame(date, dayOfWeek);
}
}
if (timeEndOfDay) {
date = date.plusDays(1);
}
return date;
}
}
}