blob: 8e857a79fc497975b1b5879ff455e76ca14819d8 [file] [log] [blame]
/*
*******************************************************************************
* Copyright (C) 2007-2010, International Business Machines Corporation and *
* others. All Rights Reserved. *
*******************************************************************************
*/
package com.ibm.icu.impl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Set;
import com.ibm.icu.impl.ZoneMeta.OlsonToMetaMappingEntry;
import com.ibm.icu.text.MessageFormat;
import com.ibm.icu.text.NumberingSystem;
import com.ibm.icu.util.BasicTimeZone;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.TimeZoneTransition;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
/**
* @author yoshito
*
*/
public class ZoneStringFormat {
private static final int millisPerHour = 60 * 60 * 1000;
private static final int millisPerMinute = 60 * 1000;
private static final int millisPerSecond = 1000;
private static final String DEFAULT_DIGIT_STRING = "0123456789";
private static final String DEFAULT_GMT_FORMAT = "GMT{0}";
private static final String DEFAULT_HOUR_FORMAT = "+HH:mm;-HH:mm";
/**
* Constructs a ZoneStringFormat by zone strings array.
* The internal structure of zoneStrings is compatible with
* the one used by getZoneStrings/setZoneStrings in DateFormatSymbols.
*
* @param zoneStrings zone strings
*/
public ZoneStringFormat(String[][] zoneStrings) {
tzidToStrings = new HashMap<String, ZoneStrings>();
zoneStringsTrie = new TextTrieMap<ZoneStringInfo>(true);
for (int i = 0; i < zoneStrings.length; i++) {
String tzid = zoneStrings[i][0];
String[] names = new String[ZSIDX_MAX];
for (int j = 1; j < zoneStrings[i].length; j++) {
if (zoneStrings[i][j] != null) {
int typeIdx = getNameTypeIndex(j);
if (typeIdx != -1) {
names[typeIdx] = zoneStrings[i][j];
// Put the name into the trie
int type = getNameType(typeIdx);
ZoneStringInfo zsinfo = new ZoneStringInfo(tzid, zoneStrings[i][j], type);
zoneStringsTrie.put(zoneStrings[i][j], zsinfo);
}
}
}
ZoneStrings zstrings = new ZoneStrings(names, true, null);
tzidToStrings.put(tzid, zstrings);
}
isFullyLoaded = true;
}
/**
* Gets an instance of ZoneStringFormat for the specified locale
* @param locale the locale
* @return An instance of ZoneStringFormat for the locale
*/
public static ZoneStringFormat getInstance(ULocale locale) {
ZoneStringFormat zzf = ZSFORMAT_CACHE.get(locale);
if (zzf == null) {
zzf = new ZoneStringFormat(locale);
ZSFORMAT_CACHE.put(locale, zzf);
}
return zzf;
}
public String[][] getZoneStrings() {
return getZoneStrings(System.currentTimeMillis());
}
private boolean inDaylightTime(TimeZone tz, long date) {
int[] offsets = {0,0};
tz.getOffset(date, false, offsets);
return (offsets[1] != 0);
}
// APIs used by SimpleDateFormat to get a zone string
public String getSpecificLongString(TimeZone tz, long date) {
return getString(tz.getID(), inDaylightTime(tz,date) ? ZSIDX_LONG_DAYLIGHT : ZSIDX_LONG_STANDARD, date, false /* not used */);
}
public String getSpecificShortString(TimeZone tz, long date, boolean commonlyUsedOnly) {
return getString(tz.getID(), inDaylightTime(tz,date) ? ZSIDX_SHORT_DAYLIGHT : ZSIDX_SHORT_STANDARD, date, commonlyUsedOnly);
}
public String getGenericLongString(TimeZone tz, long date) {
return getGenericString(tz, date, false /* long */, false /* not used */);
}
public String getGenericShortString(TimeZone tz, long date, boolean commonlyUsedOnly) {
return getGenericString(tz, date, true /* long */, commonlyUsedOnly);
}
public String getGenericLocationString(TimeZone tz, long date) {
return getString(tz.getID(), ZSIDX_LOCATION, date, false /* not used */);
}
public String getLongGMTString( TimeZone tz, long date, boolean daylight ) {
int offset;
if (daylight && tz.useDaylightTime()) {
offset = tz.getRawOffset() + tz.getDSTSavings();
} else {
offset = tz.getRawOffset();
}
return getLongGMTString(tz,date,offset);
}
public String getLongGMTString( TimeZone tz, long date ) {
return getLongGMTString(tz,date,tz.getOffset(date));
}
public String getLongGMTString( TimeZone tz, long date, int offsetIn) {
// Note: This code is optimized for performance, but as a result, it makes assumptions
// about the content and structure of the underlying CLDR data.
// Specifically, it assumes that the H or HH in the pattern occurs before the mm,
// and that there are no quoted literals in the pattern that contain H or m.
// As of CLDR 1.8.1, all of the data conforms to these rules, so we should probably be OK.
StringBuffer buf = new StringBuffer();
int offset = offsetIn;
int hfPosition = 0;
if (offset < 0) {
offset = -offset;
hfPosition = 1;
}
int offsetH = offset / millisPerHour;
offset = offset % millisPerHour;
int offsetM = offset / millisPerMinute;
offset = offset % millisPerMinute;
int offsetS = offset / millisPerSecond;
int subPosition = gmtFormat.indexOf("{0}");
for ( int i = 0 ; i < gmtFormat.length(); i++ ) {
if ( i == subPosition ) {
String hmString = hourFormats[hfPosition];
for ( int j = 0 ; j < hmString.length() ; j++) {
switch (hmString.charAt(j)) {
case 'H':
if ( j+1 < hmString.length() && hmString.charAt(j+1) == 'H' ) {
j++;
if (offsetH < 10) {
buf.append(digitString.charAt(0));
}
}
if ( offsetH >= 10 ) {
buf.append(digitString.charAt(offsetH/10));
}
buf.append(digitString.charAt(offsetH%10));
break;
case 'm':
if ( j+1 < hmString.length() && hmString.charAt(j+1) == 'm' ) {
j++;
}
buf.append(digitString.charAt(offsetM/10));
buf.append(digitString.charAt(offsetM%10));
if ( offsetS > 0 ) {
int lastH = hmString.lastIndexOf('H');
int firstm = hmString.indexOf('m');
if ( lastH + 1 < firstm ) {
buf.append(hmString.substring(lastH+1,firstm));
}
buf.append(digitString.charAt(offsetS/10));
buf.append(digitString.charAt(offsetS%10));
}
break;
default:
buf.append(hmString.charAt(j));
break;
}
}
i += 3;
} else {
buf.append(gmtFormat.charAt(i));
}
}
return buf.toString();
}
public String getShortGMTString( TimeZone tz, long date, boolean daylight ) {
int offset;
if (daylight && tz.useDaylightTime()) {
offset = tz.getRawOffset() + tz.getDSTSavings();
} else {
offset = tz.getRawOffset();
}
return getShortGMTString(tz,date,offset);
}
public String getShortGMTString( TimeZone tz, long date ) {
return getShortGMTString(tz,date,tz.getOffset(date));
}
public String getShortGMTString( TimeZone tz, long date, int offset ) {
StringBuffer buf = new StringBuffer();
// RFC822 format, must use ASCII digits
int val = offset;
char sign = '+';
if (val < 0) {
val = -val;
sign = '-';
}
buf.append(sign);
int offsetH = val / millisPerHour;
val = val % millisPerHour;
int offsetM = val / millisPerMinute;
val = val % millisPerMinute;
int offsetS = val / millisPerSecond;
int num = 0, denom = 0;
if (offsetS == 0) {
val = offsetH*100 + offsetM; // HHmm
num = val % 10000;
denom = 1000;
} else {
val = offsetH*10000 + offsetM*100 + offsetS; // HHmmss
num = val % 1000000;
denom = 100000;
}
while (denom >= 1) {
char digit = (char)((num / denom) + '0');
buf.append(digit);
num = num % denom;
denom /= 10;
}
return buf.toString();
}
// APIs used by SimpleDateFormat to lookup a zone string
public static class ZoneStringInfo {
private String id;
private String str;
private int type;
private ZoneStringInfo(String id, String str, int type) {
this.id = id;
this.str = str;
this.type = type;
}
public String getID() {
return id;
}
public String getString() {
return str;
}
public boolean isStandard() {
if ((type & STANDARD_LONG) != 0 || (type & STANDARD_SHORT) != 0) {
return true;
}
return false;
}
public boolean isDaylight() {
if ((type & DAYLIGHT_LONG) != 0 || (type & DAYLIGHT_SHORT) != 0) {
return true;
}
return false;
}
public boolean isGeneric() {
return !isStandard() && !isDaylight();
}
private int getType() {
return type;
}
}
public ZoneStringInfo findSpecificLong(String text, int start) {
return find(text, start, STANDARD_LONG | DAYLIGHT_LONG);
}
public ZoneStringInfo findSpecificShort(String text, int start) {
return find(text, start, STANDARD_SHORT | DAYLIGHT_SHORT);
}
public ZoneStringInfo findGenericLong(String text, int start) {
return find(text, start, GENERIC_LONG | STANDARD_LONG | LOCATION);
}
public ZoneStringInfo findGenericShort(String text, int start) {
return find(text, start, GENERIC_SHORT | STANDARD_SHORT | LOCATION);
}
public ZoneStringInfo findGenericLocation(String text, int start) {
return find(text, start, LOCATION);
}
// Following APIs are not used by SimpleDateFormat, but public for testing purpose
public String getLongStandard(String tzid, long date) {
return getString(tzid, ZSIDX_LONG_STANDARD, date, false /* not used */);
}
public String getLongDaylight(String tzid, long date) {
return getString(tzid, ZSIDX_LONG_DAYLIGHT, date, false /* not used */);
}
public String getLongGenericNonLocation(String tzid, long date) {
return getString(tzid, ZSIDX_LONG_GENERIC, date, false /* not used */);
}
public String getLongGenericPartialLocation(String tzid, long date) {
return getGenericPartialLocationString(tzid, false, date, false /* not used */);
}
public String getShortStandard(String tzid, long date, boolean commonlyUsedOnly) {
return getString(tzid, ZSIDX_SHORT_STANDARD, date, commonlyUsedOnly);
}
public String getShortDaylight(String tzid, long date, boolean commonlyUsedOnly) {
return getString(tzid, ZSIDX_SHORT_DAYLIGHT, date, commonlyUsedOnly);
}
public String getShortGenericNonLocation(String tzid, long date, boolean commonlyUsedOnly) {
return getString(tzid, ZSIDX_SHORT_GENERIC, date, commonlyUsedOnly);
}
public String getShortGenericPartialLocation(String tzid, long date, boolean commonlyUsedOnly) {
return getGenericPartialLocationString(tzid, true, date, commonlyUsedOnly);
}
public String getGenericLocation(String tzid) {
return getString(tzid, ZSIDX_LOCATION, 0L /* not used */, false /* not used */);
}
/**
* Constructs a ZoneStringFormat by locale. Because an instance of ZoneStringFormat
* is read-only, only one instance for a locale is sufficient. Thus, this
* constructor is protected and only called from getInstance(ULocale) to
* create one for a locale.
* @param locale The locale
*/
protected ZoneStringFormat(ULocale locale) {
this.locale = locale;
tzidToStrings = new HashMap<String, ZoneStrings>();
mzidToStrings = new HashMap<String, ZoneStrings>();
zoneStringsTrie = new TextTrieMap<ZoneStringInfo>(true);
NumberingSystem ns = NumberingSystem.getInstance(locale);
if (ns.isAlgorithmic()) {
digitString = DEFAULT_DIGIT_STRING; // Using complex ns for GMT formatting doesn't make sense
} else {
digitString = ns.getDescription();
}
gmtFormat = null;
String hourFormatString = null;
try {
ICUResourceBundle bundle = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_ZONE_BASE_NAME, locale);
gmtFormat = bundle.getStringWithFallback("zoneStrings/gmtFormat");
hourFormatString = bundle.getStringWithFallback("zoneStrings/hourFormat");
} catch (MissingResourceException e) {
}
if ( gmtFormat == null) {
gmtFormat = DEFAULT_GMT_FORMAT;
}
if ( hourFormatString == null) {
hourFormatString = DEFAULT_HOUR_FORMAT;
}
hourFormats = hourFormatString.split(";", 2);
}
// Load only a single zone
private synchronized void loadZone(String id) {
if (isFullyLoaded) {
return;
}
String tzid = ZoneMeta.getCanonicalSystemID(id);
if (tzid == null || tzidToStrings.containsKey(tzid)) {
return;
}
ICUResourceBundle zoneStringsBundle = null;
try {
ICUResourceBundle bundle = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_ZONE_BASE_NAME, locale);
zoneStringsBundle = bundle.getWithFallback("zoneStrings");
} catch (MissingResourceException e) {
// If no locale bundles are available, zoneStringsBundle will be null.
// We still want to go through the rest of zone strings initialization,
// because generic location format is generated from tzid for the case.
// The rest of code should work even zoneStrings is null.
}
String[] zstrarray = new String[ZSIDX_MAX];
String[] mzstrarray = new String[ZSIDX_MAX];
String[][] mzPartialLoc = new String[10][4]; // maximum 10 metazones per zone
addSingleZone(tzid, zoneStringsBundle,
getFallbackFormat(locale), getRegionFormat(locale),
zstrarray, mzstrarray, mzPartialLoc);
}
// Loading all zone strings
private synchronized void loadFull() {
if (isFullyLoaded) {
return;
}
ICUResourceBundle zoneStringsBundle = null;
try {
ICUResourceBundle bundle = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_ZONE_BASE_NAME, locale);
zoneStringsBundle = bundle.getWithFallback("zoneStrings");
} catch (MissingResourceException e) {
// If no locale bundles are available, zoneStringsBundle will be null.
// We still want to go through the rest of zone strings initialization,
// because generic location format is generated from tzid for the case.
// The rest of code should work even zoneStrings is null.
}
String[] zoneIDs = TimeZone.getAvailableIDs();
String[] zstrarray = new String[ZSIDX_MAX];
String[] mzstrarray = new String[ZSIDX_MAX];
String[][] mzPartialLoc = new String[10][4]; // maximum 10 metazones per zone
for (int i = 0; i < zoneIDs.length; i++) {
// Skip aliases
String tzid = ZoneMeta.getCanonicalSystemID(zoneIDs[i]);
if (tzid == null || !zoneIDs[i].equals(tzid)) {
continue;
}
if (tzidToStrings.containsKey(tzid)) {
continue;
}
addSingleZone(tzid, zoneStringsBundle,
getFallbackFormat(locale), getRegionFormat(locale),
zstrarray, mzstrarray, mzPartialLoc);
}
isFullyLoaded = true;
}
// This internal initialization code must be called in a synchronized block
private void addSingleZone(String tzid, ICUResourceBundle zoneStringsBundle,
MessageFormat fallbackFmt, MessageFormat regionFmt,
String[] zstrarray, String[] mzstrarray, String[][] mzPartialLoc) {
if (tzidToStrings.containsKey(tzid)) {
return;
}
String zoneKey = tzid.replace('/', ':');
zstrarray[ZSIDX_LONG_STANDARD] = getZoneStringFromBundle(zoneStringsBundle, zoneKey, RESKEY_LONG_STANDARD);
zstrarray[ZSIDX_SHORT_STANDARD] = getZoneStringFromBundle(zoneStringsBundle, zoneKey, RESKEY_SHORT_STANDARD);
zstrarray[ZSIDX_LONG_DAYLIGHT] = getZoneStringFromBundle(zoneStringsBundle, zoneKey, RESKEY_LONG_DAYLIGHT);
zstrarray[ZSIDX_SHORT_DAYLIGHT] = getZoneStringFromBundle(zoneStringsBundle, zoneKey, RESKEY_SHORT_DAYLIGHT);
zstrarray[ZSIDX_LONG_GENERIC] = getZoneStringFromBundle(zoneStringsBundle, zoneKey, RESKEY_LONG_GENERIC);
zstrarray[ZSIDX_SHORT_GENERIC] = getZoneStringFromBundle(zoneStringsBundle, zoneKey, RESKEY_SHORT_GENERIC);
// Compose location format string
String countryCode = ZoneMeta.getCanonicalCountry(tzid);
String country = null;
String city = null;
if (countryCode != null) {
city = getZoneStringFromBundle(zoneStringsBundle, zoneKey, RESKEY_EXEMPLAR_CITY);
if (city == null) {
city = tzid.substring(tzid.lastIndexOf('/') + 1).replace('_', ' ');
}
country = getLocalizedCountry(countryCode, locale);
if (ZoneMeta.getSingleCountry(tzid) != null) {
// If the zone is only one zone in the country, do not add city
zstrarray[ZSIDX_LOCATION] = regionFmt.format(new Object[] {country});
} else {
zstrarray[ZSIDX_LOCATION] = fallbackFmt.format(new Object[] {city, country});
}
} else {
if (tzid.startsWith("Etc/")) {
// "Etc/xxx" is not associated with a specific location, so localized
// GMT format is always used as generic location format.
zstrarray[ZSIDX_LOCATION] = null;
} else {
// When a new time zone ID, which is actually associated with a specific
// location, is added in tzdata, but the current CLDR data does not have
// the information yet, ICU creates a generic location string based on
// the ID. This implementation supports canonical time zone round trip
// with format pattern "VVVV". See #6602 for the details.
String location = tzid;
int slashIdx = location.lastIndexOf('/');
if (slashIdx == -1) {
// A time zone ID without slash in the tz database is not
// associated with a specific location. For instances,
// MET, CET, EET and WET fall into this catetory.
zstrarray[ZSIDX_LOCATION] = null;
} else {
location = tzid.substring(slashIdx + 1);
zstrarray[ZSIDX_LOCATION] = regionFmt.format(new Object[] {location});
}
}
}
boolean commonlyUsed = isCommonlyUsed(zoneStringsBundle, zoneKey);
// Resolve metazones used by this zone
int mzPartialLocIdx = 0;
List<OlsonToMetaMappingEntry> metazoneMappings = ZoneMeta.getOlsonToMatazones(tzid);
if (metazoneMappings != null) {
Iterator<OlsonToMetaMappingEntry> it = metazoneMappings.iterator();
while (it.hasNext()) {
ZoneMeta.OlsonToMetaMappingEntry mzmap = it.next();
ZoneStrings mzStrings = mzidToStrings.get(mzmap.mzid);
if (mzStrings == null) {
// If the metazone strings are not yet processed, do it now.
String mzkey = "meta:" + mzmap.mzid;
boolean mzCommonlyUsed = isCommonlyUsed(zoneStringsBundle, mzkey);
mzstrarray[ZSIDX_LONG_STANDARD] = getZoneStringFromBundle(zoneStringsBundle, mzkey, RESKEY_LONG_STANDARD);
mzstrarray[ZSIDX_SHORT_STANDARD] = getZoneStringFromBundle(zoneStringsBundle, mzkey, RESKEY_SHORT_STANDARD);
mzstrarray[ZSIDX_LONG_DAYLIGHT] = getZoneStringFromBundle(zoneStringsBundle, mzkey, RESKEY_LONG_DAYLIGHT);
mzstrarray[ZSIDX_SHORT_DAYLIGHT] = getZoneStringFromBundle(zoneStringsBundle, mzkey, RESKEY_SHORT_DAYLIGHT);
mzstrarray[ZSIDX_LONG_GENERIC] = getZoneStringFromBundle(zoneStringsBundle, mzkey, RESKEY_LONG_GENERIC);
mzstrarray[ZSIDX_SHORT_GENERIC] = getZoneStringFromBundle(zoneStringsBundle, mzkey, RESKEY_SHORT_GENERIC);
mzstrarray[ZSIDX_LOCATION] = null;
mzStrings = new ZoneStrings(mzstrarray, mzCommonlyUsed, null);
mzidToStrings.put(mzmap.mzid, mzStrings);
// Add metazone strings to the zone string trie
String preferredIdForLocale = ZoneMeta.getZoneIdByMetazone(mzmap.mzid, getRegion());
for (int j = 0; j < mzstrarray.length; j++) {
if (mzstrarray[j] != null) {
int type = getNameType(j);
ZoneStringInfo zsinfo = new ZoneStringInfo(preferredIdForLocale, mzstrarray[j], type);
zoneStringsTrie.put(mzstrarray[j], zsinfo);
}
}
}
// Compose generic partial location format
String lg = mzStrings.getString(ZSIDX_LONG_GENERIC);
String sg = mzStrings.getString(ZSIDX_SHORT_GENERIC);
if (lg != null || sg != null) {
boolean addMzPartialLocationNames = true;
for (int j = 0; j < mzPartialLocIdx; j++) {
if (mzPartialLoc[j][0].equals(mzmap.mzid)) {
// already added
addMzPartialLocationNames = false;
break;
}
}
if (addMzPartialLocationNames) {
String locationPart = null;
// Check if the zone is the preferred zone for the territory associated with the zone
String preferredID = ZoneMeta.getZoneIdByMetazone(mzmap.mzid, countryCode);
if (tzid.equals(preferredID)) {
// Use country for the location
locationPart = country;
} else {
// Use city for the location
locationPart = city;
}
mzPartialLoc[mzPartialLocIdx][0] = mzmap.mzid;
mzPartialLoc[mzPartialLocIdx][1] = null;
mzPartialLoc[mzPartialLocIdx][2] = null;
mzPartialLoc[mzPartialLocIdx][3] = null;
if (locationPart != null) {
if (lg != null) {
mzPartialLoc[mzPartialLocIdx][1] = fallbackFmt.format(new Object[] {locationPart, lg});
}
if (sg != null) {
mzPartialLoc[mzPartialLocIdx][2] = fallbackFmt.format(new Object[] {locationPart, sg});
boolean shortMzCommonlyUsed = mzStrings.isShortFormatCommonlyUsed();
if (shortMzCommonlyUsed) {
mzPartialLoc[mzPartialLocIdx][3] = "1";
}
}
}
mzPartialLocIdx++;
}
}
}
}
String[][] genericPartialLocationNames = null;
if (mzPartialLocIdx != 0) {
// metazone generic partial location names are collected
genericPartialLocationNames = new String[mzPartialLocIdx][];
for (int mzi = 0; mzi < mzPartialLocIdx; mzi++) {
genericPartialLocationNames[mzi] = mzPartialLoc[mzi].clone();
}
}
// Finally, create ZoneStrings instance and put it into the tzidToStinrgs map
ZoneStrings zstrings = new ZoneStrings(zstrarray, commonlyUsed, genericPartialLocationNames);
tzidToStrings.put(tzid, zstrings);
// Also add all available names to the zone string trie
if (zstrarray != null) {
for (int j = 0; j < zstrarray.length; j++) {
if (zstrarray[j] != null) {
int type = getNameType(j);
ZoneStringInfo zsinfo = new ZoneStringInfo(tzid, zstrarray[j], type);
zoneStringsTrie.put(zstrarray[j], zsinfo);
}
}
}
if (genericPartialLocationNames != null) {
for (int j = 0; j < genericPartialLocationNames.length; j++) {
ZoneStringInfo zsinfo;
if (genericPartialLocationNames[j][1] != null) {
zsinfo = new ZoneStringInfo(tzid, genericPartialLocationNames[j][1], GENERIC_LONG);
zoneStringsTrie.put(genericPartialLocationNames[j][1], zsinfo);
}
if (genericPartialLocationNames[j][2] != null) {
zsinfo = new ZoneStringInfo(tzid, genericPartialLocationNames[j][1], GENERIC_SHORT);
zoneStringsTrie.put(genericPartialLocationNames[j][2], zsinfo);
}
}
}
}
// Name types, these bit flag are used for zone string lookup
private static final int LOCATION = 0x0001;
private static final int GENERIC_LONG = 0x0002;
private static final int GENERIC_SHORT = 0x0004;
private static final int STANDARD_LONG = 0x0008;
private static final int STANDARD_SHORT = 0x0010;
private static final int DAYLIGHT_LONG = 0x0020;
private static final int DAYLIGHT_SHORT = 0x0040;
// Name type index, these constants are used for index in ZoneStrings.strings
private static final int ZSIDX_LOCATION = 0;
private static final int ZSIDX_LONG_STANDARD = 1;
private static final int ZSIDX_SHORT_STANDARD = 2;
private static final int ZSIDX_LONG_DAYLIGHT = 3;
private static final int ZSIDX_SHORT_DAYLIGHT = 4;
private static final int ZSIDX_LONG_GENERIC = 5;
private static final int ZSIDX_SHORT_GENERIC = 6;
private static final int ZSIDX_MAX = ZSIDX_SHORT_GENERIC + 1;
// ZoneStringFormat cache
private static ICUCache<ULocale, ZoneStringFormat> ZSFORMAT_CACHE = new SimpleCache<ULocale, ZoneStringFormat>();
/*
* The translation type of the translated zone strings
*/
private static final String
RESKEY_SHORT_GENERIC = "sg",
RESKEY_SHORT_STANDARD = "ss",
RESKEY_SHORT_DAYLIGHT = "sd",
RESKEY_LONG_GENERIC = "lg",
RESKEY_LONG_STANDARD = "ls",
RESKEY_LONG_DAYLIGHT = "ld",
RESKEY_EXEMPLAR_CITY = "ec",
RESKEY_COMMONLY_USED = "cu";
// Window size used for DST check for a zone in a metazone
private static final long DST_CHECK_RANGE = 184L*(24*60*60*1000);
// Map from zone id to ZoneStrings
private Map<String, ZoneStrings> tzidToStrings;
// Map from metazone id to ZoneStrings
private Map<String, ZoneStrings> mzidToStrings;
// Zone string dictionary, used for look up
private TextTrieMap<ZoneStringInfo> zoneStringsTrie;
// Locale used for initializing zone strings
private ULocale locale;
// Region used for resolving a zone in a metazone, initialized by locale
private transient String region;
// Loading status
private boolean isFullyLoaded = false;
// Digit string - used for fast GMT formatting
private String digitString;
private String gmtFormat;
private String[] hourFormats;
/*
* Private method to get a zone string except generic partial location types.
*/
private String getString(String tzid, int typeIdx, long date, boolean commonlyUsedOnly) {
if (!isFullyLoaded) {
// Lazy loading
loadZone(tzid);
}
String result = null;
ZoneStrings zstrings = tzidToStrings.get(tzid);
if (zstrings == null) {
// ICU's own array does not have entries for aliases
String canonicalID = ZoneMeta.getCanonicalSystemID(tzid);
if (canonicalID != null && !canonicalID.equals(tzid)) {
// Canonicalize tzid here. The rest of operations
// require tzid to be canonicalized.
tzid = canonicalID;
zstrings = tzidToStrings.get(tzid);
}
}
if (zstrings != null) {
switch (typeIdx) {
case ZSIDX_LONG_STANDARD:
case ZSIDX_LONG_DAYLIGHT:
case ZSIDX_LONG_GENERIC:
case ZSIDX_LOCATION:
result = zstrings.getString(typeIdx);
break;
case ZSIDX_SHORT_STANDARD:
case ZSIDX_SHORT_DAYLIGHT:
case ZSIDX_SHORT_GENERIC:
if (!commonlyUsedOnly || zstrings.isShortFormatCommonlyUsed()) {
result = zstrings.getString(typeIdx);
}
break;
}
}
if (result == null && mzidToStrings != null && typeIdx != ZSIDX_LOCATION) {
// Try metazone
String mzid = ZoneMeta.getMetazoneID(tzid, date);
if (mzid != null) {
ZoneStrings mzstrings = mzidToStrings.get(mzid);
if (mzstrings != null) {
switch (typeIdx) {
case ZSIDX_LONG_STANDARD:
case ZSIDX_LONG_DAYLIGHT:
case ZSIDX_LONG_GENERIC:
result = mzstrings.getString(typeIdx);
break;
case ZSIDX_SHORT_STANDARD:
case ZSIDX_SHORT_DAYLIGHT:
case ZSIDX_SHORT_GENERIC:
if (!commonlyUsedOnly || mzstrings.isShortFormatCommonlyUsed()) {
result = mzstrings.getString(typeIdx);
}
break;
}
}
}
}
return result;
}
/*
* Private method to get a generic string, with fallback logic involved,
* that is,
*
* 1. If a generic non-location string is avaiable for the zone, return it.
* 2. If a generic non-location string is associated with a metazone and
* the zone never use daylight time around the given date, use the standard
* string (if available).
*
* Note: In CLDR1.5.1, the same localization is used for generic and standard.
* In this case, we do not use the standard string and do the rest.
*
* 3. If a generic non-location string is associated with a metazone and
* the offset at the given time is different from the preferred zone for the
* current locale, then return the generic partial location string (if avaiable)
* 4. If a generic non-location string is not available, use generic location
* string.
*/
private String getGenericString(TimeZone tz, long date, boolean isShort, boolean commonlyUsedOnly) {
String result = null;
String tzid = tz.getID();
if (!isFullyLoaded) {
// Lazy loading
loadZone(tzid);
}
ZoneStrings zstrings = tzidToStrings.get(tzid);
if (zstrings == null) {
// ICU's own array does not have entries for aliases
String canonicalID = ZoneMeta.getCanonicalSystemID(tzid);
if (canonicalID != null && !canonicalID.equals(tzid)) {
// Canonicalize tzid here. The rest of operations
// require tzid to be canonicalized.
tzid = canonicalID;
zstrings = tzidToStrings.get(tzid);
}
}
if (zstrings != null) {
if (isShort) {
if (!commonlyUsedOnly || zstrings.isShortFormatCommonlyUsed()) {
result = zstrings.getString(ZSIDX_SHORT_GENERIC);
}
} else {
result = zstrings.getString(ZSIDX_LONG_GENERIC);
}
}
if (result == null && mzidToStrings != null) {
// try metazone
String mzid = ZoneMeta.getMetazoneID(tzid, date);
if (mzid != null) {
boolean useStandard = false;
if (!inDaylightTime(tz,date)) {
useStandard = true;
// Check if the zone actually uses daylight saving time around the time
if (tz instanceof BasicTimeZone) {
BasicTimeZone btz = (BasicTimeZone)tz;
TimeZoneTransition before = btz.getPreviousTransition(date, true);
if (before != null
&& (date - before.getTime() < DST_CHECK_RANGE)
&& before.getFrom().getDSTSavings() != 0) {
useStandard = false;
} else {
TimeZoneTransition after = btz.getNextTransition(date, false);
if (after != null
&& (after.getTime() - date < DST_CHECK_RANGE)
&& after.getTo().getDSTSavings() != 0) {
useStandard = false;
}
}
} else {
// If not BasicTimeZone... only if the instance is not an ICU's implementation.
// We may get a wrong answer in edge case, but it should practically work OK.
int[] offsets = new int[2];
tz.getOffset(date - DST_CHECK_RANGE, false, offsets);
if (offsets[1] != 0) {
useStandard = false;
} else {
tz.getOffset(date + DST_CHECK_RANGE, false, offsets);
if (offsets[1] != 0){
useStandard = false;
}
}
}
}
if (useStandard) {
result = getString(tzid, (isShort ? ZSIDX_SHORT_STANDARD : ZSIDX_LONG_STANDARD),
date, commonlyUsedOnly);
// Note:
// In CLDR 1.5.1, a same localization is used for both generic and standard
// for some metazones in some locales. This is actually data bugs and should
// be resolved in later versions of CLDR. For now, we check if the standard
// name is different from its generic name below.
if (result != null) {
String genericNonLocation = getString(tzid, (isShort ? ZSIDX_SHORT_GENERIC : ZSIDX_LONG_GENERIC),
date, commonlyUsedOnly);
if (genericNonLocation != null && result.equalsIgnoreCase(genericNonLocation)) {
result = null;
}
}
}
if (result == null){
ZoneStrings mzstrings = mzidToStrings.get(mzid);
if (mzstrings != null) {
if (isShort) {
if (!commonlyUsedOnly || mzstrings.isShortFormatCommonlyUsed()) {
result = mzstrings.getString(ZSIDX_SHORT_GENERIC);
}
} else {
result = mzstrings.getString(ZSIDX_LONG_GENERIC);
}
}
if (result != null) {
// Check if the offsets at the given time matches the preferred zone's offsets
String preferredId = ZoneMeta.getZoneIdByMetazone(mzid, getRegion());
if (!tzid.equals(preferredId)) {
// Check if the offsets at the given time are identical with the preferred zone
int[] offsets = {0,0};
tz.getOffset(date,false,offsets);
int raw = offsets[0];
int sav = offsets[1];
TimeZone preferredZone = TimeZone.getTimeZone(preferredId);
int[] preferredOffsets = new int[2];
// Check offset in preferred time zone with wall time.
// With getOffset(time, false, preferredOffsets),
// you may get incorrect results because of time overlap at DST->STD
// transition.
preferredZone.getOffset(date + raw + sav, true, preferredOffsets);
if (raw != preferredOffsets[0] || sav != preferredOffsets[1]) {
// Use generic partial location string as fallback
result = zstrings.getGenericPartialLocationString(mzid, isShort, commonlyUsedOnly);
}
}
}
}
}
}
if (result == null) {
// Use location format as the final fallback
result = getString(tzid, ZSIDX_LOCATION, date, false /* not used */);
}
return result;
}
/*
* Private method to get a generic partial location string
*/
private String getGenericPartialLocationString(String tzid, boolean isShort, long date, boolean commonlyUsedOnly) {
if (!isFullyLoaded) {
// Lazy loading
loadZone(tzid);
}
String result = null;
String mzid = ZoneMeta.getMetazoneID(tzid, date);
if (mzid != null) {
ZoneStrings zstrings = tzidToStrings.get(tzid);
if (zstrings != null) {
result = zstrings.getGenericPartialLocationString(mzid, isShort, commonlyUsedOnly);
}
}
return result;
}
/*
* Gets zoneStrings compatible with DateFormatSymbols for the
* specified date. In CLDR 1.5, zone names can be changed
* time to time. This method generates flat 2-dimensional
* String array including zone ids and its localized strings
* at the moment. Thus, even you construct a new ZoneStringFormat
* by the zone strings array returned by this method, you will
* loose historic name changes. Also, commonly used flag for
* short types is not reflected in the result.
*/
private String[][] getZoneStrings(long date) {
loadFull();
Set<String> tzids = tzidToStrings.keySet();
String[][] zoneStrings = new String[tzids.size()][8];
int idx = 0;
for (String tzid : tzids) {
zoneStrings[idx][0] = tzid;
zoneStrings[idx][1] = getLongStandard(tzid, date);
zoneStrings[idx][2] = getShortStandard(tzid, date, false);
zoneStrings[idx][3] = getLongDaylight(tzid, date);
zoneStrings[idx][4] = getShortDaylight(tzid, date, false);
zoneStrings[idx][5] = getGenericLocation(tzid);
zoneStrings[idx][6] = getLongGenericNonLocation(tzid, date);
zoneStrings[idx][7] = getShortGenericNonLocation(tzid, date, false);
idx++;
}
return zoneStrings;
}
/*
* ZoneStrings is an internal implementation class for
* holding localized name information for a zone/metazone
*/
private static class ZoneStrings {
private String[] strings;
private String[][] genericPartialLocationStrings;
private boolean commonlyUsed;
private ZoneStrings(String[] zstrarray, boolean commonlyUsed, String[][] genericPartialLocationStrings) {
if (zstrarray != null) {
int lastIdx = -1;
for (int i = 0; i < zstrarray.length; i++) {
if (zstrarray[i] != null) {
lastIdx = i;
}
}
if (lastIdx != -1) {
strings = new String[lastIdx + 1];
System.arraycopy(zstrarray, 0, strings, 0, lastIdx + 1);
}
}
this.commonlyUsed = commonlyUsed;
this.genericPartialLocationStrings = genericPartialLocationStrings;
}
private String getString(int typeIdx) {
if (strings != null && typeIdx >= 0 && typeIdx < strings.length) {
return strings[typeIdx];
}
return null;
}
private boolean isShortFormatCommonlyUsed() {
return commonlyUsed;
}
private String getGenericPartialLocationString(String mzid, boolean isShort, boolean commonlyUsedOnly) {
String result = null;
if (genericPartialLocationStrings != null) {
for (int i = 0; i < genericPartialLocationStrings.length; i++) {
if (genericPartialLocationStrings[i][0].equals(mzid)) {
if (isShort) {
if (!commonlyUsedOnly || genericPartialLocationStrings[i][3] != null) {
result = genericPartialLocationStrings[i][2];
}
} else {
result = genericPartialLocationStrings[i][1];
}
break;
}
}
}
return result;
}
}
/*
* Returns a localized zone string from bundle.
*/
private static String getZoneStringFromBundle(ICUResourceBundle bundle, String key, String type) {
String zstring = null;
if (bundle != null) {
try {
zstring = bundle.getStringWithFallback(key + "/" + type);
} catch (MissingResourceException ex) {
// throw away the exception
}
}
return zstring;
}
/*
* Returns if the short strings of the zone/metazone is commonly used.
*/
private static boolean isCommonlyUsed(ICUResourceBundle bundle, String key) {
boolean commonlyUsed = false;
if (bundle != null) {
try {
UResourceBundle cuRes = bundle.getWithFallback(key + "/" + RESKEY_COMMONLY_USED);
int cuValue = cuRes.getInt();
commonlyUsed = (cuValue != 0);
} catch (MissingResourceException ex) {
// throw away the exception
}
}
return commonlyUsed;
}
/*
* Returns a localized country string for the country code. If no actual
* localized string is found, countryCode itself is returned.
*/
private static String getLocalizedCountry(String countryCode, ULocale locale) {
String countryStr = null;
if (countryCode != null) {
ICUResourceBundle rb =
(ICUResourceBundle)UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_REGION_BASE_NAME, locale);
//
// TODO: There is a design bug in UResourceBundle and getLoadingStatus() does not work well.
//
// if (rb.getLoadingStatus() != ICUResourceBundle.FROM_ROOT && rb.getLoadingStatus() != ICUResourceBundle.FROM_DEFAULT) {
// country = ULocale.getDisplayCountry("xx_" + country_code, locale);
// }
// START WORKAROUND
ULocale rbloc = rb.getULocale();
if (!rbloc.equals(ULocale.ROOT) && rbloc.getLanguage().equals(locale.getLanguage())) {
countryStr = ULocale.getDisplayCountry("xx_" + countryCode, locale);
}
// END WORKAROUND
if (countryStr == null || countryStr.length() == 0) {
countryStr = countryCode;
}
}
return countryStr;
}
/*
* Gets an instance of MessageFormat used for formatting zone fallback string
*/
private static MessageFormat getFallbackFormat(ULocale locale) {
String fallbackPattern = ZoneMeta.getTZLocalizationInfo(locale, ZoneMeta.FALLBACK_FORMAT);
if (fallbackPattern == null) {
fallbackPattern = "{1} ({0})";
}
return new MessageFormat(fallbackPattern, locale);
}
/*
* Gets an instance of MessageFormat used for formatting zone region string
*/
private static MessageFormat getRegionFormat(ULocale locale) {
String regionPattern = ZoneMeta.getTZLocalizationInfo(locale, ZoneMeta.REGION_FORMAT);
if (regionPattern == null) {
regionPattern = "{0}";
}
return new MessageFormat(regionPattern, locale);
}
/*
* Index value mapping between DateFormatSymbols's zoneStrings and
* the string types defined in this class.
*/
private static final int[] INDEXMAP = {
-1, // 0 - zone id
ZSIDX_LONG_STANDARD, // 1 - long standard
ZSIDX_SHORT_STANDARD, // 2 - short standard
ZSIDX_LONG_DAYLIGHT, // 3 - long daylight
ZSIDX_SHORT_DAYLIGHT, // 4 - short daylight
ZSIDX_LOCATION, // 5 - generic location
ZSIDX_LONG_GENERIC, // 6 - long generic non-location
ZSIDX_SHORT_GENERIC // 7 - short generic non-location
};
/*
* Convert from zone string array index for zoneStrings used by DateFormatSymbols#get/setZoneStrings
* to the type constants defined by this class, such as ZSIDX_LONG_STANDARD.
*/
private static int getNameTypeIndex(int i) {
int idx = -1;
if (i >= 1 && i < INDEXMAP.length) {
idx = INDEXMAP[i];
}
return idx;
}
/*
* Mapping from name type index to name type
*/
private static final int[] NAMETYPEMAP = {
LOCATION, // ZSIDX_LOCATION
STANDARD_LONG, // ZSIDX_LONG_STANDARD
STANDARD_SHORT, // ZSIDX_SHORT_STANDARD
DAYLIGHT_LONG, // ZSIDX_LONG_DAYLIGHT
DAYLIGHT_SHORT, // ZSIDX_SHORT_DAYLIGHT
GENERIC_LONG, // ZSIDX_LONG_GENERIC
GENERIC_SHORT, // ZSIDX_SHORT_GENERIC
};
private static int getNameType(int typeIdx) {
int type = -1;
if (typeIdx >= 0 && typeIdx < NAMETYPEMAP.length) {
type = NAMETYPEMAP[typeIdx];
}
return type;
}
/*
* Returns region used for ZoneMeta#getZoneIdByMetazone.
*/
private String getRegion() {
if (region == null) {
if (locale != null) {
region = locale.getCountry();
if (region.length() == 0) {
ULocale tmp = ULocale.addLikelySubtags(locale);
region = tmp.getCountry();
}
} else {
region = "";
}
}
return region;
}
// This method does lazy zone string loading
private ZoneStringInfo find(String text, int start, int types) {
ZoneStringInfo result = subFind(text, start, types);
if (isFullyLoaded) {
return result;
}
// When zone string data is partially loaded,
// this method return the result only when
// the input text is fully consumed.
if (result != null) {
int matchLen = result.getString().length();
if (text.length() - start == matchLen) {
return result;
}
}
// Now load all zone strings
loadFull();
return subFind(text, start, types);
}
/*
* Find a prefix matching time zone for the given zone string types.
* @param text The text contains a time zone string
* @param start The start index within the text
* @param types The bit mask representing a set of requested types
* @return If any zone string matched for the requested types, returns a
* ZoneStringInfo for the longest match. If no matches are found for
* the requested types, returns a ZoneStringInfo for the longest match
* for any other types. If nothing matches at all, returns null.
*/
private ZoneStringInfo subFind(String text, int start, int types) {
ZoneStringInfo result = null;
ZoneStringSearchResultHandler handler = new ZoneStringSearchResultHandler();
zoneStringsTrie.find(text, start, handler);
List<ZoneStringInfo> list = handler.getMatchedZoneStrings();
ZoneStringInfo fallback = null;
if (list != null && list.size() > 0) {
Iterator<ZoneStringInfo> it = list.iterator();
while (it.hasNext()) {
ZoneStringInfo tmp = it.next();
if ((types & tmp.getType()) != 0) {
if (result == null || result.getString().length() < tmp.getString().length()) {
result = tmp;
} else if (result.getString().length() == tmp.getString().length()) {
// Tie breaker - there are some examples that a
// long standard name is identical with a location
// name - for example, "Uruguay Time". In this case,
// we interpret it as generic, not specific.
if (tmp.isGeneric() && !result.isGeneric()) {
result = tmp;
}
}
} else if (result == null) {
if (fallback == null || fallback.getString().length() < tmp.getString().length()) {
fallback = tmp;
} else if (fallback.getString().length() == tmp.getString().length()) {
if (tmp.isGeneric() && !fallback.isGeneric()) {
fallback = tmp;
}
}
}
}
}
if (result == null && fallback != null) {
result = fallback;
}
return result;
}
private static class ZoneStringSearchResultHandler implements TextTrieMap.ResultHandler<ZoneStringInfo> {
private ArrayList<ZoneStringInfo> resultList;
public boolean handlePrefixMatch(int matchLength, Iterator<ZoneStringInfo> values) {
if (resultList == null) {
resultList = new ArrayList<ZoneStringInfo>();
}
while (values.hasNext()) {
ZoneStringInfo zsitem = values.next();
if (zsitem == null) {
break;
}
int i = 0;
for (; i < resultList.size(); i++) {
ZoneStringInfo tmp = resultList.get(i);
if (zsitem.getType() == tmp.getType()) {
if (matchLength > tmp.getString().length()) {
resultList.set(i, zsitem);
}
break;
}
}
if (i == resultList.size()) {
// not found in the current list
resultList.add(zsitem);
}
}
return true;
}
List<ZoneStringInfo> getMatchedZoneStrings() {
if (resultList == null || resultList.size() == 0) {
return null;
}
return resultList;
}
}
}