Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.joda.time.tz.ZoneInfoCompiler Maven / Gradle / Ivy
/*
* Copyright 2001-2013 Stephen Colebourne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.joda.time.tz;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import org.joda.time.Chronology;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.joda.time.MutableDateTime;
import org.joda.time.chrono.ISOChronology;
import org.joda.time.chrono.LenientChronology;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
/**
* Compiles standard format time zone data files into binary files for each time zone
* in the database. {@link DateTimeZoneBuilder} is used to construct and encode
* compiled data files. {@link ZoneInfoProvider} loads the encoded files and
* converts them back into {@link DateTimeZone} objects.
*
* Although this tool is similar to zic, the binary formats are not
* compatible. The latest time zone database files may be obtained
* here .
*
* ZoneInfoCompiler is mutable and not thread-safe, although the main method
* may be safely invoked by multiple threads.
*
* @author Brian S O'Neill
* @since 1.0
*/
public class ZoneInfoCompiler {
// SPEC: https://man7.org/linux/man-pages/man8/zic.8.html#FILES
// Note that we match a subset of the spec, as actually seen in TZDB files
static DateTimeOfYear cStartOfYear;
static Chronology cLenientISO;
// SPEC: A name can be abbreviated by omitting all but an initial prefix; any
// abbreviation must be unambiguous in context.
static final Set RULE_LOOKUP = expand("rule", "r");
static final Set ZONE_LOOKUP = expand("zone", "z");
static final Set LINK_LOOKUP = expand("link", "l");
static final Set MIN_YEAR_LOOKUP = expand("minimum", "mi");
static final Set MAX_YEAR_LOOKUP = expand("maximum", "ma");
static final Set ONLY_YEAR_LOOKUP = expand("only", "o");
static final Map MONTH_LOOKUP = new HashMap();
static {
put(expand("january", "ja"), 1, MONTH_LOOKUP);
put(expand("february", "f"), 2, MONTH_LOOKUP);
put(expand("march", "mar"), 3, MONTH_LOOKUP);
put(expand("april", "ap"), 4, MONTH_LOOKUP);
put(expand("may", "may"), 5, MONTH_LOOKUP);
put(expand("june", "jun"), 6, MONTH_LOOKUP);
put(expand("july", "jul"), 7, MONTH_LOOKUP);
put(expand("august", "au"), 8, MONTH_LOOKUP);
put(expand("september", "s"), 9, MONTH_LOOKUP);
put(expand("october", "o"), 10, MONTH_LOOKUP);
put(expand("november", "n"), 11, MONTH_LOOKUP);
put(expand("december", "d"), 12, MONTH_LOOKUP);
}
static final Map DOW_LOOKUP = new HashMap();
static {
put(expand("monday", "m"), 1, DOW_LOOKUP);
put(expand("tuesday", "tu"), 2, DOW_LOOKUP);
put(expand("wednesday", "w"), 3, DOW_LOOKUP);
put(expand("thursday", "th"), 4, DOW_LOOKUP);
put(expand("friday", "f"), 5, DOW_LOOKUP);
put(expand("saturday", "sa"), 6, DOW_LOOKUP);
put(expand("sunday", "su"), 7, DOW_LOOKUP);
}
private static void put(Set strs, int value, Map map) {
for (Iterator it = strs.iterator(); it.hasNext();) {
map.put(it.next(), value);
}
}
private static Set expand(String whole, String shortest) {
Set set = new HashSet();
String code = whole;
while (!code.equals(shortest)) {
set.add(code);
code = code.substring(0, code.length() - 1);
}
set.add(code);
return set;
}
//-----------------------------------------------------------------------
/**
* Launches the ZoneInfoCompiler tool.
*
*
* Usage: java org.joda.time.tz.ZoneInfoCompiler <options> <source files>
* where possible options include:
* -src <directory> Specify where to read source files
* -dst <directory> Specify where to write generated files
* -verbose Output verbosely (default false)
*
*/
public static void main(String[] args) throws Exception {
if (args.length == 0) {
printUsage();
return;
}
File inputDir = null;
File outputDir = null;
boolean verbose = false;
int i;
for (i = 0; i < args.length; i++) {
if ("-src".equals(args[i])) {
if (++i >= args.length) {
printUsage();
return;
}
inputDir = new File(args[i]);
} else if ("-dst".equals(args[i])) {
if (++i >= args.length) {
printUsage();
return;
}
outputDir = new File(args[i]);
} else if ("-verbose".equals(args[i])) {
verbose = true;
} else if ("-?".equals(args[i])) {
printUsage();
return;
} else {
break;
}
}
if (i >= args.length) {
printUsage();
return;
}
File[] sources = new File[args.length - i];
for (int j=0; i ");
System.out.println("where possible options include:");
System.out.println(" -src Specify where to read source files");
System.out.println(" -dst Specify where to write generated files");
System.out.println(" -verbose Output verbosely (default false)");
}
static DateTimeOfYear getStartOfYear() {
if (cStartOfYear == null) {
cStartOfYear = new DateTimeOfYear();
}
return cStartOfYear;
}
static Chronology getLenientISOChronology() {
if (cLenientISO == null) {
cLenientISO = LenientChronology.getInstance(ISOChronology.getInstanceUTC());
}
return cLenientISO;
}
/**
* @param zimap maps string ids to DateTimeZone objects.
*/
static void writeZoneInfoMap(DataOutputStream dout, Map zimap) throws IOException {
if ( dout == null ){
throw new IllegalArgumentException("DataOutputStream must not be null.");
}
// Build the string pool.
Map idToIndex = new HashMap(zimap.size());
TreeMap indexToId = new TreeMap();
short count = 0;
for (Entry entry : zimap.entrySet()) {
String id = (String)entry.getKey();
if (!idToIndex.containsKey(id)) {
Short index = Short.valueOf(count);
idToIndex.put(id, index);
indexToId.put(index, id);
if (++count == Short.MAX_VALUE) {
throw new InternalError("Too many time zone ids");
}
}
id = ((DateTimeZone)entry.getValue()).getID();
if (!idToIndex.containsKey(id)) {
Short index = Short.valueOf(count);
idToIndex.put(id, index);
indexToId.put(index, id);
if (++count == Short.MAX_VALUE) {
throw new InternalError("Too many time zone ids");
}
}
}
// Write the string pool, ordered by index.
dout.writeShort(indexToId.size());
for (String id : indexToId.values()) {
dout.writeUTF(id);
}
// Write the mappings.
dout.writeShort(zimap.size());
for (Entry entry : zimap.entrySet()) {
String id = entry.getKey();
dout.writeShort(idToIndex.get(id).shortValue());
id = entry.getValue().getID();
dout.writeShort(idToIndex.get(id).shortValue());
}
}
static int parseYear(String str, int def) {
String lower = str.toLowerCase(Locale.ENGLISH);
if (MIN_YEAR_LOOKUP.contains(lower)) {
return Integer.MIN_VALUE;
} else if (MAX_YEAR_LOOKUP.contains(lower)) {
return Integer.MAX_VALUE;
} else if (ONLY_YEAR_LOOKUP.contains(lower)) {
return def;
}
return Integer.parseInt(str);
}
static int parseMonth(String str) {
Integer value = MONTH_LOOKUP.get(str.toLowerCase(Locale.ENGLISH));
if (value == null) {
throw new IllegalArgumentException("Unknown month: " + str);
}
return value;
}
static int parseDayOfWeek(String str) {
Integer value = DOW_LOOKUP.get(str.toLowerCase(Locale.ENGLISH));
if (value == null) {
throw new IllegalArgumentException("Unknown day-of-week: " + str);
}
return value;
}
static String parseOptional(String str) {
return (str.equals("-")) ? null : str;
}
static int parseTime(String str) {
// SPEC: (see 'AT' section)
// NOTE: negative offsets, and offsets beyond 24:00, are not supported
if (str.equals("-")) {
return 0;
}
DateTimeFormatter p = ISODateTimeFormat.hourMinuteSecondFraction();
MutableDateTime mdt = new MutableDateTime(0, getLenientISOChronology());
int pos = 0;
if (str.startsWith("-")) {
pos = 1;
}
int newPos = p.parseInto(mdt, str, pos);
if (newPos == ~pos) {
throw new IllegalArgumentException(str);
}
int millis = (int)mdt.getMillis();
if (pos == 1) {
millis = -millis;
}
return millis;
}
static char parseZoneChar(char c) {
// SPEC: Any of these forms may be followed by the letter w if the given time is local or “wall clock” time,
// s if the given time is standard time without any adjustment for daylight saving,
// or u (or g or z) if the given time is universal time;
// in the absence of an indicator, local (wall clock) time is assumed.
switch (c) {
case 's': case 'S':
// Standard time
return 's';
case 'u': case 'U': case 'g': case 'G': case 'z': case 'Z':
// UTC
return 'u';
case 'w': case 'W': default:
// Wall time
return 'w';
}
}
/**
* @return false if error.
*/
static boolean test(String id, DateTimeZone tz) {
if (!id.equals(tz.getID())) {
return true;
}
// Test to ensure that reported transitions are not duplicated.
long millis = ISOChronology.getInstanceUTC().year().set(0, 1850);
long end = ISOChronology.getInstanceUTC().year().set(0, 2050);
int offset = tz.getOffset(millis);
int stdOffset = tz.getStandardOffset(millis);
String key = tz.getNameKey(millis);
List transitions = new ArrayList();
while (true) {
long next = tz.nextTransition(millis);
if (next == millis || next > end) {
break;
}
millis = next;
int nextOffset = tz.getOffset(millis);
int nextStdOffset = tz.getStandardOffset(millis);
String nextKey = tz.getNameKey(millis);
if (offset == nextOffset && stdOffset == nextStdOffset && key.equals(nextKey)) {
System.out.println("*d* Error in " + tz.getID() + " "
+ new DateTime(millis,
ISOChronology.getInstanceUTC()));
return false;
}
if (nextKey == null || (nextKey.length() < 3 && !"??".equals(nextKey) && !"%z".equals(nextKey))) {
System.out.println("*s* Error in " + tz.getID() + " "
+ new DateTime(millis,
ISOChronology.getInstanceUTC())
+ ", nameKey=" + nextKey);
return false;
}
transitions.add(Long.valueOf(millis));
offset = nextOffset;
key = nextKey;
}
// Now verify that reverse transitions match up.
millis = ISOChronology.getInstanceUTC().year().set(0, 2050);
end = ISOChronology.getInstanceUTC().year().set(0, 1850);
for (int i=transitions.size(); --i>= 0; ) {
long prev = tz.previousTransition(millis);
if (prev == millis || prev < end) {
break;
}
millis = prev;
long trans = transitions.get(i).longValue();
if (trans - 1 != millis) {
System.out.println("*r* Error in " + tz.getID() + " "
+ new DateTime(millis,
ISOChronology.getInstanceUTC()) + " != "
+ new DateTime(trans - 1,
ISOChronology.getInstanceUTC()));
return false;
}
}
return true;
}
// Maps names to RuleSets.
private Map iRuleSets;
// List of Zone objects.
private List iZones;
// List String pairs to link.
private List iGoodLinks;
// List String pairs to link.
private List iBackLinks;
public ZoneInfoCompiler() {
iRuleSets = new HashMap();
iZones = new ArrayList();
iGoodLinks = new ArrayList();
iBackLinks = new ArrayList();
}
/**
* Returns a map of ids to DateTimeZones.
*
* @param outputDir optional directory to write compiled data files to
* @param sources optional list of source files to parse
*/
public Map compile(File outputDir, File[] sources) throws IOException {
if (sources != null) {
for (int i=0; i map = new TreeMap();
Map sourceMap = new TreeMap();
System.out.println("Writing zoneinfo files");
// write out the standard entries
for (int i = 0; i < iZones.size(); i++) {
Zone zone = iZones.get(i);
DateTimeZoneBuilder builder = new DateTimeZoneBuilder();
zone.addToBuilder(builder, iRuleSets);
DateTimeZone tz = builder.toDateTimeZone(zone.iName, true);
if (test(tz.getID(), tz)) {
map.put(tz.getID(), tz);
sourceMap.put(tz.getID(), zone);
if (outputDir != null) {
writeZone(outputDir, builder, tz);
}
}
}
// revive zones from "good" links
for (int i = 0; i < iGoodLinks.size(); i += 2) {
String baseId = iGoodLinks.get(i);
String alias = iGoodLinks.get(i + 1);
Zone sourceZone = sourceMap.get(baseId);
if (sourceZone == null) {
System.out.println("Cannot find source zone '" + baseId + "' to link alias '" + alias + "' to");
} else {
DateTimeZoneBuilder builder = new DateTimeZoneBuilder();
sourceZone.addToBuilder(builder, iRuleSets);
DateTimeZone revived = builder.toDateTimeZone(alias, true);
if (test(revived.getID(), revived)) {
map.put(revived.getID(), revived);
if (outputDir != null) {
writeZone(outputDir, builder, revived);
}
}
map.put(revived.getID(), revived);
if (ZoneInfoLogger.verbose()) {
System.out.println("Good link: " + alias + " -> " + baseId + " revived");
}
}
}
// store "back" links as aliases (where name is permanently mapped)
for (int pass = 0; pass < 2; pass++) {
for (int i = 0; i < iBackLinks.size(); i += 2) {
String id = iBackLinks.get(i);
String alias = iBackLinks.get(i + 1);
DateTimeZone tz = map.get(id);
if (tz == null) {
if (pass > 0) {
System.out.println("Cannot find time zone '" + id + "' to link alias '" + alias + "' to");
}
} else {
map.put(alias, tz);
if (ZoneInfoLogger.verbose()) {
System.out.println("Back link: " + alias + " -> " + tz.getID());
}
}
}
}
// write map that unites the time-zone data, pointing aliases and real zones at files
if (outputDir != null) {
System.out.println("Writing ZoneInfoMap");
File file = new File(outputDir, "ZoneInfoMap");
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
OutputStream out = new FileOutputStream(file);
DataOutputStream dout = new DataOutputStream(out);
try {
// Sort and filter out any duplicates that match case.
Map zimap = new TreeMap(String.CASE_INSENSITIVE_ORDER);
zimap.putAll(map);
writeZoneInfoMap(dout, zimap);
} finally {
dout.close();
}
}
return map;
}
private void writeZone(File outputDir, DateTimeZoneBuilder builder, DateTimeZone tz) throws IOException {
if (ZoneInfoLogger.verbose()) {
System.out.println("Writing " + tz.getID());
}
File file = new File(outputDir, tz.getID());
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
OutputStream out = new FileOutputStream(file);
try {
builder.writeTo(tz.getID(), out);
} finally {
out.close();
}
// Test if it can be read back.
InputStream in = new FileInputStream(file);
DateTimeZone tz2 = DateTimeZoneBuilder.readFrom(in, tz.getID());
in.close();
if (!tz.equals(tz2)) {
System.out.println("*e* Error in " + tz.getID() +
": Didn't read properly from file");
}
}
public void parseDataFile(BufferedReader in, boolean backward) throws IOException {
Zone zone = null;
String line;
while ((line = in.readLine()) != null) {
// SPEC: Leading and trailing white space on input lines is ignored.
String trimmed = line.trim();
// SPEC: An unquoted sharp character (#) in the input
// introduces a comment which extends to the end of the line the
// sharp character appears on.
// Any line that is blank (after comment stripping) is ignored
// (Note that we do not support quoted fields)
if (trimmed.length() == 0 || trimmed.charAt(0) == '#') {
continue;
}
int index = line.indexOf('#');
if (index >= 0) {
line = line.substring(0, index);
}
//System.out.println(line);
// SPEC: Fields are separated from one
// another by one or more white space characters. The white space
// characters are space, form feed, carriage return, newline, tab,
// and vertical tab.
StringTokenizer st = new StringTokenizer(line, " \f\r\t\u000b");
if (Character.isWhitespace(line.charAt(0)) && st.hasMoreTokens()) {
if (zone != null) {
// Zone continuation
zone.chain(st);
}
continue;
} else {
if (zone != null) {
iZones.add(zone);
}
zone = null;
}
// SPEC: Names must be in English and are case insensitive. They appear
// in several contexts, and include month and weekday names and
// keywords such as maximum, only, Rolling, and Zone. A name can be
// abbreviated by omitting all but an initial prefix; any
// abbreviation must be unambiguous in context.
if (st.hasMoreTokens()) {
String token = st.nextToken().toLowerCase(Locale.ENGLISH);
if (RULE_LOOKUP.contains(token)) {
Rule r = new Rule(st);
RuleSet rs = iRuleSets.get(r.iName);
if (rs == null) {
rs = new RuleSet(r);
iRuleSets.put(r.iName, rs);
} else {
rs.addRule(r);
}
} else if (ZONE_LOOKUP.contains(token)) {
if (st.countTokens() < 4) {
throw new IllegalArgumentException("Attempting to create a Zone from an incomplete tokenizer");
}
zone = new Zone(st);
} else if (LINK_LOOKUP.contains(token)) {
String real = st.nextToken();
String alias = st.nextToken();
// links in "backward" are deprecated names
// links in other files should be kept
// special case a few to try to repair terrible damage to tzdb
if (alias.equals("WET") || alias.equals("CET") || alias.equals("EET")) {
iGoodLinks.add(real);
iGoodLinks.add(alias);
} else if (alias.equals("MET")) {
iBackLinks.add("CET"); // map MET -> CET (not Europe/Brussels)
iBackLinks.add(alias);
} else if (backward || alias.equals("US/Pacific-New") || alias.startsWith("Etc/") || alias.equals("GMT")) {
iBackLinks.add(real);
iBackLinks.add(alias);
} else {
iGoodLinks.add(real);
iGoodLinks.add(alias);
}
} else {
System.out.println("Unknown line: " + line);
}
}
}
if (zone != null) {
iZones.add(zone);
}
}
// ScopedForTesting
static class DateTimeOfYear {
public final int iMonthOfYear;
public final int iDayOfMonth;
public final int iDayOfWeek;
public final boolean iAdvanceDayOfWeek;
public final int iMillisOfDay;
public final char iZoneChar;
DateTimeOfYear() {
iMonthOfYear = 1;
iDayOfMonth = 1;
iDayOfWeek = 0;
iAdvanceDayOfWeek = false;
iMillisOfDay = 0;
iZoneChar = 'w';
}
DateTimeOfYear(StringTokenizer st) {
int month = 1;
int day = 1;
int dayOfWeek = 0;
int millis = 0;
boolean advance = false;
char zoneChar = 'w';
if (st.hasMoreTokens()) {
month = parseMonth(st.nextToken());
if (st.hasMoreTokens()) {
String str = st.nextToken();
if (str.toLowerCase(Locale.ENGLISH).startsWith("last")) {
day = -1;
dayOfWeek = parseDayOfWeek(str.substring(4));
advance = false;
} else {
try {
day = Integer.parseInt(str);
dayOfWeek = 0;
advance = false;
} catch (NumberFormatException e) {
int index = str.indexOf(">=");
if (index > 0) {
day = Integer.parseInt(str.substring(index + 2));
dayOfWeek = parseDayOfWeek(str.substring(0, index));
advance = true;
} else {
index = str.indexOf("<=");
if (index > 0) {
day = Integer.parseInt(str.substring(index + 2));
dayOfWeek = parseDayOfWeek(str.substring(0, index));
advance = false;
} else {
throw new IllegalArgumentException(str);
}
}
}
}
// the SPEC treats time as a duration from 00:00, whereas we parse it as a time
// as such, we cannot handle negative or times beyond 24:00
if (st.hasMoreTokens()) {
str = st.nextToken();
zoneChar = parseZoneChar(str.charAt(str.length() - 1));
if (str.equals("24:00")) {
// handle end of year
if (month == 12 && day == 31) {
millis = parseTime("23:59:59.999");
} else {
LocalDate date = (day == -1 ?
new LocalDate(2001, month, 1).plusMonths(1) :
new LocalDate(2001, month, day).plusDays(1));
advance = (day != -1 && dayOfWeek != 0);
month = date.getMonthOfYear();
day = date.getDayOfMonth();
if (dayOfWeek != 0) {
dayOfWeek = ((dayOfWeek - 1 + 1) % 7) + 1;
}
}
} else {
millis = parseTime(str);
}
}
}
}
iMonthOfYear = month;
iDayOfMonth = day;
iDayOfWeek = dayOfWeek;
iAdvanceDayOfWeek = advance;
iMillisOfDay = millis;
iZoneChar = zoneChar;
}
/**
* Adds a recurring savings rule to the builder.
*
* @param builder the builder
* @param nameKey the name key
* @param saveMillis the save in millis
* @param fromYear the from year
* @param toYear the to year
*/
public void addRecurring(DateTimeZoneBuilder builder, String nameKey,
int saveMillis,
int fromYear,
int toYear) {
builder.addRecurringSavings(nameKey, saveMillis,
fromYear, toYear,
iZoneChar,
iMonthOfYear,
iDayOfMonth,
iDayOfWeek,
iAdvanceDayOfWeek,
iMillisOfDay);
}
/**
* Adds a cutover to the builder.
*
* @param builder the builder
* @param year the year
*/
public void addCutover(DateTimeZoneBuilder builder, int year) {
builder.addCutover(year,
iZoneChar,
iMonthOfYear,
iDayOfMonth,
iDayOfWeek,
iAdvanceDayOfWeek,
iMillisOfDay);
}
@Override
public String toString() {
return
"MonthOfYear: " + iMonthOfYear + "\n" +
"DayOfMonth: " + iDayOfMonth + "\n" +
"DayOfWeek: " + iDayOfWeek + "\n" +
"AdvanceDayOfWeek: " + iAdvanceDayOfWeek + "\n" +
"MillisOfDay: " + iMillisOfDay + "\n" +
"ZoneChar: " + iZoneChar + "\n";
}
}
// ScopedForTesting
static class Rule {
public final String iName;
public final int iFromYear;
public final int iToYear;
public final String iType;
public final DateTimeOfYear iDateTimeOfYear;
public final int iSaveMillis;
public final String iLetterS;
Rule(StringTokenizer st) {
if (st.countTokens() < 6) {
throw new IllegalArgumentException("Attempting to create a Rule from an incomplete tokenizer");
}
iName = st.nextToken().intern();
iFromYear = parseYear(st.nextToken(), 0);
iToYear = parseYear(st.nextToken(), iFromYear);
if (iToYear < iFromYear) {
throw new IllegalArgumentException();
}
iType = parseOptional(st.nextToken());
iDateTimeOfYear = new DateTimeOfYear(st);
iSaveMillis = parseTime(st.nextToken());
iLetterS = parseOptional(st.nextToken());
}
// creates a rule to go before the specified rule
Rule(Rule after) {
iName = after.iName;
iFromYear = 1800;
iToYear = after.iFromYear;
iType = null;
iDateTimeOfYear = after.iDateTimeOfYear; // does not matter
iSaveMillis = 0;
iLetterS = after.iLetterS;
}
/**
* Adds a recurring savings rule to the builder.
*
* @param builder the builder
* @param standardMillis the standard millis, pre-adjusted to the negativeSave value
* @param negativeSave the negative save value
* @param nameFormat the name format
*/
public void addRecurring(DateTimeZoneBuilder builder, int standardMillis, int negativeSave, String nameFormat) {
int saveMillis = iSaveMillis + -negativeSave;
String nameKey = formatName(nameFormat, standardMillis, saveMillis, iLetterS);
iDateTimeOfYear.addRecurring(builder, nameKey, saveMillis, iFromYear, iToYear);
}
// ScopedForTesting
static String formatName(String nameFormat, int standardMillis, int saveMillis, String letterS) {
// this method is called while adding rules to the builder
// the input parameters give the context as to whether the input is standard or 'summer' time
// saveMillis == 0 in 'winter' time, and != 0 in 'summer' time
// (negative save millis have been applied before this method is called)
// SPEC: Alternatively, a slash (/) separates standard and daylight abbreviations.
int index = nameFormat.indexOf('/');
if (index > 0) {
if (saveMillis == 0) {
// Extract standard name.
return nameFormat.substring(0, index).intern();
} else {
return nameFormat.substring(index + 1).intern();
}
}
// SPEC: The pair of characters %s is used to show where the “variable part” of the time zone abbreviation goes.
// LETTER column: Gives the “variable part” (for example, the “S” or “D” in “EST” or “EDT”) of time zone
// abbreviations to be used when this rule is in effect. If this field is “-”, the variable part is null.
// (the "-" was removed in parsing)
index = nameFormat.indexOf("%s");
if (index >= 0) {
String left = nameFormat.substring(0, index);
String right = nameFormat.substring(index + 2);
String name = left + (letterS == null ? "" : letterS) + right;
return name.intern();
}
// SPEC: Alternatively, a format can use the pair of characters %z to stand for the UT
// offset in the form ±hh, ±hhmm, or ±hhmmss, using the shortest form that does not lose information,
// where hh, mm, and ss are the hours, minutes, and seconds east (+) or west (-) of UT.
if (nameFormat.equals("%z")) {
if (saveMillis == 0) {
return formatOffset(standardMillis).intern();
} else {
return formatOffset(standardMillis + saveMillis).intern();
}
}
return nameFormat;
}
private static String formatOffset(int millis) {
String sign = millis < 0 ? "-" : "+";
int saveSecs = Math.abs(millis) / 1000;
int hours = saveSecs / 3600;
int mins = ((saveSecs / 60) % 60);
int secs = (saveSecs % 60);
if (secs == 0) {
if (mins == 0) {
return sign + twoDigitString(hours);
}
return sign + twoDigitString(hours) + twoDigitString(mins);
}
return sign + twoDigitString(hours) + twoDigitString(mins) + twoDigitString(secs);
}
private static String twoDigitString(int value) {
return Integer.toString(value + 100).substring(1);
}
@Override
public String toString() {
return
"[Rule]\n" +
"Name: " + iName + "\n" +
"FromYear: " + iFromYear + "\n" +
"ToYear: " + iToYear + "\n" +
"Type: " + iType + "\n" +
iDateTimeOfYear +
"SaveMillis: " + iSaveMillis + "\n" +
"LetterS: " + iLetterS + "\n";
}
}
private static class RuleSet {
private List iRules;
RuleSet(Rule rule) {
iRules = new ArrayList();
iRules.add(rule);
}
void addRule(Rule rule) {
if (!(rule.iName.equals(iRules.get(0).iName))) {
throw new IllegalArgumentException("Rule name mismatch");
}
iRules.add(rule);
}
/**
* Adds recurring savings rules to the builder.
*
* @param builder the builder
* @param standardMillis the standard millis
* @param nameFormat the name format
*/
public void addRecurring(DateTimeZoneBuilder builder, int standardMillis, String nameFormat) {
// a hack is necessary to remove negative SAVE values from the input tzdb file
// negative save values cause the standard offset to be set in the summer instead of the winter
// this causes the wrong name to be chosen from the CLDR data
// check if the ruleset has negative SAVE values
int negativeSave = 0;
for (int i = 0; i < iRules.size(); i++) {
Rule rule = iRules.get(i);
if (rule.iSaveMillis < 0) {
negativeSave = Math.min(negativeSave, rule.iSaveMillis);
}
}
// if negative SAVE values, then patch standard millis and name format
if (negativeSave < 0) {
if (ZoneInfoLogger.verbose()) {
System.out.println("Fixed negative save values for rule '" + iRules.get(0).iName + "'");
}
standardMillis += negativeSave;
int slashPos = nameFormat.indexOf("/");
if (slashPos > 0) {
nameFormat = nameFormat.substring(slashPos + 1) + "/" + nameFormat.substring(0, slashPos);
}
}
builder.setStandardOffset(standardMillis);
// add a fake rule that predates all other rules to ensure standard=summer (see Namibia)
if (negativeSave < 0) {
Rule rule = new Rule(iRules.get(0));
rule.addRecurring(builder, standardMillis, negativeSave, nameFormat);
}
// add each rule, passing through the negative save to alter the actual iSaveMillis value that is used
for (int i = 0; i < iRules.size(); i++) {
Rule rule = iRules.get(i);
rule.addRecurring(builder, standardMillis, negativeSave, nameFormat);
}
}
}
private static class Zone {
public final String iName;
public final int iOffsetMillis;
public final String iRules;
public final String iFormat;
public final int iUntilYear;
public final DateTimeOfYear iUntilDateTimeOfYear;
private Zone iNext;
Zone(StringTokenizer st) {
this(st.nextToken(), st);
}
private Zone(String name, StringTokenizer st) {
iName = name.intern();
iOffsetMillis = parseTime(st.nextToken());
iRules = parseOptional(st.nextToken());
iFormat = st.nextToken().intern();
int year = Integer.MAX_VALUE;
DateTimeOfYear dtOfYear = getStartOfYear();
if (st.hasMoreTokens()) {
year = Integer.parseInt(st.nextToken());
if (st.hasMoreTokens()) {
dtOfYear = new DateTimeOfYear(st);
}
}
iUntilYear = year;
iUntilDateTimeOfYear = dtOfYear;
}
void chain(StringTokenizer st) {
if (iNext != null) {
iNext.chain(st);
} else {
iNext = new Zone(iName, st);
}
}
/*
public DateTimeZone buildDateTimeZone(Map ruleSets) {
DateTimeZoneBuilder builder = new DateTimeZoneBuilder();
addToBuilder(builder, ruleSets);
return builder.toDateTimeZone(iName);
}
*/
/**
* Adds zone info to the builder.
*
* @param builder the builder
* @param ruleSets the rule sets
*/
public void addToBuilder(DateTimeZoneBuilder builder, Map ruleSets) {
addToBuilder(this, builder, ruleSets);
}
private static void addToBuilder(Zone zone, DateTimeZoneBuilder builder, Map ruleSets) {
for (; zone != null; zone = zone.iNext) {
if (zone.iRules == null) {
builder.setStandardOffset(zone.iOffsetMillis);
builder.setFixedSavings(zone.iFormat, 0);
} else {
try {
// Check if iRules actually just refers to a savings.
int saveMillis = parseTime(zone.iRules);
builder.setStandardOffset(zone.iOffsetMillis);
builder.setFixedSavings(zone.iFormat, saveMillis);
}
catch (Exception e) {
// Zone is using a RuleSet for this segment of the timeline
RuleSet rs = ruleSets.get(zone.iRules);
if (rs == null) {
throw new IllegalArgumentException
("Rules not found: " + zone.iRules);
}
rs.addRecurring(builder, zone.iOffsetMillis, zone.iFormat);
}
}
if (zone.iUntilYear == Integer.MAX_VALUE) {
break;
}
zone.iUntilDateTimeOfYear.addCutover(builder, zone.iUntilYear);
}
}
@Override
public String toString() {
String str =
"[Zone]\n" +
"Name: " + iName + "\n" +
"OffsetMillis: " + iOffsetMillis + "\n" +
"Rules: " + iRules + "\n" +
"Format: " + iFormat + "\n" +
"UntilYear: " + iUntilYear + "\n" +
iUntilDateTimeOfYear;
if (iNext == null) {
return str;
}
return str + "...\n" + iNext.toString();
}
}
}