blob: 3e48c95aee4ff5320886e0ae0e6c6100e4952cce [file] [log] [blame]
/*
**********************************************************************
* Copyright (c) 2003-2009 International Business Machines
* Corporation and others. All Rights Reserved.
**********************************************************************
* Author: Alan Liu
* Created: September 4 2003
* Since: ICU 2.8
**********************************************************************
*/
package com.ibm.icu.impl;
import java.lang.ref.SoftReference;
import java.text.ParsePosition;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Set;
import com.ibm.icu.text.MessageFormat;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.util.SimpleTimeZone;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
import com.ibm.icu.util.UResourceBundleIterator;
/**
* This class, not to be instantiated, implements the meta-data
* missing from the underlying core JDK implementation of time zones.
* There are two missing features: Obtaining a list of available zones
* for a given country (as defined by the Olson database), and
* obtaining a list of equivalent zones for a given zone (as defined
* by Olson links).
*
* This class uses a data class, ZoneMetaData, which is created by the
* tool tz2icu.
*
* @author Alan Liu
* @since ICU 2.8
*/
public final class ZoneMeta {
private static final boolean ASSERT = false;
/**
* Returns a String array containing all system TimeZone IDs
* associated with the given country. These IDs may be passed to
* <code>TimeZone.getTimeZone()</code> to construct the
* corresponding TimeZone object.
* @param country a two-letter ISO 3166 country code, or <code>null</code>
* to return zones not associated with any country
* @return an array of IDs for system TimeZones in the given
* country. If there are none, return a zero-length array.
*/
public static synchronized String[] getAvailableIDs(String country) {
if(!getOlsonMeta()){
return EMPTY;
}
try{
UResourceBundle top = (ICUResourceBundle)ICUResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, "zoneinfo", ICUResourceBundle.ICU_DATA_CLASS_LOADER);
UResourceBundle regions = top.get(kREGIONS);
UResourceBundle names = top.get(kNAMES); // dereference Zones section
UResourceBundle temp = regions.get(country);
int[] vector = temp.getIntVector();
if (ASSERT) Assert.assrt("vector.length>0", vector.length>0);
String[] ret = new String[vector.length];
for (int i=0; i<vector.length; ++i) {
if (ASSERT) Assert.assrt("vector[i] >= 0 && vector[i] < OLSON_ZONE_COUNT",
vector[i] >= 0 && vector[i] < OLSON_ZONE_COUNT);
ret[i] = names.getString(vector[i]);
}
return ret;
}catch(MissingResourceException ex){
//throw away the exception
}
return EMPTY;
}
public static synchronized String[] getAvailableIDs() {
if(!getOlsonMeta()){
return EMPTY;
}
try{
UResourceBundle top = (ICUResourceBundle)ICUResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, "zoneinfo", ICUResourceBundle.ICU_DATA_CLASS_LOADER);
UResourceBundle names = top.get(kNAMES); // dereference Zones section
return names.getStringArray();
}catch(MissingResourceException ex){
//throw away the exception
}
return EMPTY;
}
public static synchronized String[] getAvailableIDs(int offset){
if(!getOlsonMeta()){
return EMPTY;
}
LinkedList vector = new LinkedList();
for (int i=0; i<OLSON_ZONE_COUNT; ++i) {
String unistr;
if ((unistr=getID(i))!=null) {
// This is VERY inefficient.
TimeZone z = TimeZone.getTimeZone(unistr);
// Make sure we get back the ID we wanted (if the ID is
// invalid we get back GMT).
if (z != null && z.getID().equals(unistr) &&
z.getRawOffset() == offset) {
vector.add(unistr);
}
}
}
if(!vector.isEmpty()){
String[] strings = new String[vector.size()];
return (String[])vector.toArray(strings);
}
return EMPTY;
}
private static String getID(int i) {
try{
UResourceBundle top = (ICUResourceBundle)ICUResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, "zoneinfo", ICUResourceBundle.ICU_DATA_CLASS_LOADER);
UResourceBundle names = top.get(kNAMES); // dereference Zones section
return names.getString(i);
}catch(MissingResourceException ex){
//throw away the exception
}
return null;
}
/**
* Returns the number of IDs in the equivalency group that
* includes the given ID. An equivalency group contains zones
* that behave identically to the given zone.
*
* <p>If there are no equivalent zones, then this method returns
* 0. This means either the given ID is not a valid zone, or it
* is and there are no other equivalent zones.
* @param id a system time zone ID
* @return the number of zones in the equivalency group containing
* 'id', or zero if there are no equivalent zones.
* @see #getEquivalentID
*/
public static synchronized int countEquivalentIDs(String id) {
UResourceBundle res = openOlsonResource(id);
int size = res.getSize();
if (size == 4 || size == 6) {
UResourceBundle r=res.get(size-1);
//result = ures_getSize(&r); // doesn't work
int[] v = r.getIntVector();
return v.length;
}
return 0;
}
/**
* Returns an ID in the equivalency group that includes the given
* ID. An equivalency group contains zones that behave
* identically to the given zone.
*
* <p>The given index must be in the range 0..n-1, where n is the
* value returned by <code>countEquivalentIDs(id)</code>. For
* some value of 'index', the returned value will be equal to the
* given id. If the given id is not a valid system time zone, or
* if 'index' is out of range, then returns an empty string.
* @param id a system time zone ID
* @param index a value from 0 to n-1, where n is the value
* returned by <code>countEquivalentIDs(id)</code>
* @return the ID of the index-th zone in the equivalency group
* containing 'id', or an empty string if 'id' is not a valid
* system ID or 'index' is out of range
* @see #countEquivalentIDs
*/
public static synchronized String getEquivalentID(String id, int index) {
String result="";
UResourceBundle res = openOlsonResource(id);
if (res != null) {
int zone = -1;
int size = res.getSize();
if (size == 4 || size == 6) {
UResourceBundle r = res.get(size-1);
int[] v = r.getIntVector();
if (index >= 0 && index < v.length) {
zone = v[index];
}
}
if (zone >= 0) {
try {
UResourceBundle top = UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, "zoneinfo",
ICUResourceBundle.ICU_DATA_CLASS_LOADER);
UResourceBundle ares = top.get(kNAMES); // dereference Zones section
result = ares.getString(zone);
} catch (MissingResourceException e) {
result = "";
}
}
}
return result;
}
private static String[] getCanonicalInfo(String id) {
if (id == null || id.length() == 0) {
return null;
}
if (canonicalMap == null) {
Map m = new HashMap();
Set s = new HashSet();
try {
UResourceBundle supplementalDataBundle = UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, "supplementalData", ICUResourceBundle.ICU_DATA_CLASS_LOADER);
UResourceBundle zoneFormatting = supplementalDataBundle.get("zoneFormatting");
UResourceBundleIterator it = zoneFormatting.getIterator();
while ( it.hasNext()) {
UResourceBundle temp = it.next();
int resourceType = temp.getType();
switch(resourceType) {
case UResourceBundle.TABLE:
String [] result = { "", "" };
UResourceBundle zoneInfo = temp;
String canonicalID = zoneInfo.getKey().replace(':','/');
String territory = zoneInfo.get("territory").getString();
result[0] = canonicalID;
if ( territory.equals("001")) {
result[1] = null;
}
else {
result[1] = territory;
}
m.put(canonicalID,result);
try {
UResourceBundle aliasBundle = zoneInfo.get("aliases");
String [] aliases = aliasBundle.getStringArray();
for (int i=0 ; i<aliases.length; i++) {
m.put(aliases[i],result);
}
} catch(MissingResourceException ex){
// Disregard if there are no aliases
}
break;
case UResourceBundle.ARRAY:
String[] territoryList = temp.getStringArray();
for (int i=0 ; i < territoryList.length; i++) {
s.add(territoryList[i]);
}
break;
}
}
} catch (MissingResourceException e) {
// throws away the exception - maps are empty for this case
}
// Some available Olson zones are not included in CLDR data (such as Asia/Riyadh87).
// Also, when we update Olson tzdata, new zones may be added.
// This code scans all available zones in zoneinfo.res, and if any of them are
// missing, add them to the map.
try{
UResourceBundle top = (ICUResourceBundle)ICUResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME,
"zoneinfo", ICUResourceBundle.ICU_DATA_CLASS_LOADER);
UResourceBundle names = top.get(kNAMES);
String[] ids = names.getStringArray();
for (int i = 0; i < ids.length; i++) {
if (m.containsKey(ids[i])) {
// Already included in CLDR data
continue;
}
// Not in CLDR data, but it could be new one whose alias is
// available in CLDR
String[] tmpinfo = null;
int nTzdataEquivalent = TimeZone.countEquivalentIDs(ids[i]);
for (int j = 0; j < nTzdataEquivalent; j++) {
String alias = TimeZone.getEquivalentID(ids[i], j);
if (alias.equals(ids[i])) {
continue;
}
tmpinfo = (String[])m.get(alias);
if (tmpinfo != null) {
break;
}
}
if (tmpinfo == null) {
// Set dereferenced zone ID as the canonical ID
UResourceBundle res = getZoneByName(top, ids[i]);
String derefID = (res.getSize() == 1) ? ids[res.getInt()] : ids[i];
m.put(ids[i], new String[] {derefID, null});
} else {
// Use the canonical ID in the existing entry
m.put(ids[i], tmpinfo);
}
}
} catch (MissingResourceException ex) {
//throw away the exception
}
synchronized (ZoneMeta.class) {
canonicalMap = m;
multiZoneTerritories = s;
}
}
return (String[])canonicalMap.get(id);
}
private static Map canonicalMap = null;
private static Set multiZoneTerritories = null;
/**
* Return the canonical id for this system tzid, which might be the id itself.
* If the given system tzid is not know, return null.
*/
public static String getCanonicalSystemID(String tzid) {
String[] info = getCanonicalInfo(tzid);
if (info != null) {
return info[0];
}
return null;
}
/**
* Return the canonical country code for this tzid. If we have none, or if the time zone
* is not associated with a country, return null.
*/
public static String getCanonicalCountry(String tzid) {
String[] info = getCanonicalInfo(tzid);
if (info != null) {
return info[1];
}
return null;
}
/**
* Return the country code if this is a 'single' time zone that can fallback to just
* the country, otherwise return null. (Note, one must also check the locale data
* to see that there is a localization for the country in order to implement
* tr#35 appendix J step 5.)
*/
public static String getSingleCountry(String tzid) {
String[] info = getCanonicalInfo(tzid);
if (info != null && info[1] != null && !multiZoneTerritories.contains(info[1])) {
return info[1];
}
return null;
}
/**
* Returns a time zone location(region) format string defined by UTR#35.
* e.g. "Italy Time", "United States (Los Angeles) Time"
*/
public static String getLocationFormat(String tzid, String city, ULocale locale) {
String[] info = getCanonicalInfo(tzid);
if (info == null) {
return null; // error
}
String country_code = info[1];
if (country_code == null) {
return null; // error!
}
String country = null;
if (country_code != null) {
try {
ICUResourceBundle rb =
(ICUResourceBundle)UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_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())) {
country = ULocale.getDisplayCountry("xx_" + country_code, locale);
}
// END WORKAROUND
} catch (MissingResourceException e) {
// fall through
}
if (country == null || country.length() == 0) {
country = country_code;
}
}
// This is not behavior specified in tr35, but behavior added by Mark.
// TR35 says to display the country _only_ if there is a localization.
if (getSingleCountry(tzid) != null) { // single country
String regPat = getTZLocalizationInfo(locale, REGION_FORMAT);
if (regPat == null) {
regPat = DEF_REGION_FORMAT;
}
MessageFormat mf = new MessageFormat(regPat);
return mf.format(new Object[] { country });
}
if (city == null) {
city = tzid.substring(tzid.lastIndexOf('/')+1).replace('_',' ');
}
String flbPat = getTZLocalizationInfo(locale, FALLBACK_FORMAT);
if (flbPat == null) {
flbPat = DEF_FALLBACK_FORMAT;
}
MessageFormat mf = new MessageFormat(flbPat);
return mf.format(new Object[] { city, country });
}
private static final String DEF_REGION_FORMAT = "{0}";
private static final String DEF_FALLBACK_FORMAT = "{1} ({0})";
public static final String
HOUR = "hourFormat",
GMT = "gmtFormat",
REGION_FORMAT = "regionFormat",
FALLBACK_FORMAT = "fallbackFormat",
ZONE_STRINGS = "zoneStrings",
FORWARD_SLASH = "/";
/**
* Get the index'd tz datum for this locale. Index must be one of the
* values PREFIX, HOUR, GMT, REGION_FORMAT, FALLBACK_FORMAT
*/
public static String getTZLocalizationInfo(ULocale locale, String format) {
String result = null;
try {
ICUResourceBundle bundle = (ICUResourceBundle) ICUResourceBundle.getBundleInstance(locale);
result = bundle.getStringWithFallback(ZONE_STRINGS+FORWARD_SLASH+format);
} catch (MissingResourceException e) {
result = null;
}
return result;
}
// private static Set getValidIDs() {
// // Construct list of time zones that are valid, according
// // to the current underlying core JDK. We have to do this
// // at runtime since we don't know what we're running on.
// Set valid = new TreeSet();
// valid.addAll(Arrays.asList(java.util.TimeZone.getAvailableIDs()));
// return valid;
// }
/**
* Empty string array.
*/
private static final String[] EMPTY = new String[0];
/**
* Given an ID, open the appropriate resource for the given time zone.
* Dereference aliases if necessary.
* @param id zone id
* @return top-level resource bundle
*/
public static UResourceBundle openOlsonResource(String id)
{
UResourceBundle res = null;
try {
ICUResourceBundle top = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, "zoneinfo", ICUResourceBundle.ICU_DATA_CLASS_LOADER);
res = getZoneByName(top, id);
// Dereference if this is an alias. Docs say result should be 1
// but it is 0 in 2.8 (?).
if (res.getSize() <= 1) {
int deref = res.getInt() + 0;
UResourceBundle ares = top.get(kZONES); // dereference Zones section
res = (ICUResourceBundle) ares.get(deref);
}
} catch (MissingResourceException e) {
res = null;
}
return res;
}
/**
* Fetch a specific zone by name. Replaces the getByKey call.
* @param top Top timezone resource
* @param id Time zone ID
* @return the zone's bundle if found, or undefined if error. Reuses oldbundle.
*/
private static UResourceBundle getZoneByName(UResourceBundle top, String id) throws MissingResourceException {
// load the Rules object
UResourceBundle tmp = top.get(kNAMES);
// search for the string
int idx = findInStringArray(tmp, id);
if((idx == -1)) {
// not found
throw new MissingResourceException(kNAMES, ((ICUResourceBundle)tmp).getResPath(), id);
//ures_close(oldbundle);
//oldbundle = NULL;
} else {
tmp = top.get(kZONES); // get Zones object from top
tmp = tmp.get(idx); // get nth Zone object
}
return tmp;
}
private static int findInStringArray(UResourceBundle array, String id){
int start = 0;
int limit = array.getSize();
int mid;
String u = null;
int lastMid = Integer.MAX_VALUE;
if((limit < 1)) {
return -1;
}
for (;;) {
mid = (int)((start + limit) / 2);
if (lastMid == mid) { /* Have we moved? */
break; /* We haven't moved, and it wasn't found. */
}
lastMid = mid;
u = array.getString(mid);
if(u==null){
break;
}
int r = id.compareTo(u);
if(r==0) {
return mid;
} else if(r<0) {
limit = mid;
} else {
start = mid;
}
}
return -1;
}
private static final String kREGIONS = "Regions";
private static final String kZONES = "Zones";
private static final String kNAMES = "Names";
private static final String kGMT_ID = "GMT";
private static final String kCUSTOM_TZ_PREFIX = "GMT";
private static ICUCache zoneCache = new SimpleCache();
/**
* The Olson data is stored the "zoneinfo" resource bundle.
* Sub-resources are organized into three ranges of data: Zones, final
* rules, and country tables. There is also a meta-data resource
* which has 3 integers: The number of zones, rules, and countries,
* respectively. The country count includes the non-country 'Default'.
*/
static int OLSON_ZONE_START = -1; // starting index of zones
static int OLSON_ZONE_COUNT = 0; // count of zones
/**
* Given a pointer to an open "zoneinfo" resource, load up the Olson
* meta-data. Return true if successful.
*/
private static boolean getOlsonMeta(ICUResourceBundle top) {
if (OLSON_ZONE_START < 0 && top != null) {
try {
UResourceBundle res = top.get(kZONES);
OLSON_ZONE_COUNT = res.getSize();
OLSON_ZONE_START = 0;
} catch (MissingResourceException e) {
// throws away the exception
}
}
return (OLSON_ZONE_START >= 0);
}
/**
* Load up the Olson meta-data. Return true if successful.
*/
private static boolean getOlsonMeta() {
if (OLSON_ZONE_START < 0) {
try {
ICUResourceBundle top = (ICUResourceBundle)ICUResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, "zoneinfo", ICUResourceBundle.ICU_DATA_CLASS_LOADER);
getOlsonMeta(top);
} catch (MissingResourceException e) {
// throws away the exception
}
}
return (OLSON_ZONE_START >= 0);
}
/**
* Lookup the given name in our system zone table. If found,
* instantiate a new zone of that name and return it. If not
* found, return 0.
*/
public static TimeZone getSystemTimeZone(String id) {
TimeZone z = (TimeZone)zoneCache.get(id);
if (z == null) {
try{
UResourceBundle top = UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, "zoneinfo", ICUResourceBundle.ICU_DATA_CLASS_LOADER);
UResourceBundle res = openOlsonResource(id);
z = new OlsonTimeZone(top, res);
z.setID(id);
zoneCache.put(id, z);
}catch(Exception ex){
return null;
}
}
return (TimeZone)z.clone();
}
public static TimeZone getGMT(){
TimeZone z = new SimpleTimeZone(0, kGMT_ID);
z.setID(kGMT_ID);
return z;
}
// Maximum value of valid custom time zone hour/min
private static final int kMAX_CUSTOM_HOUR = 23;
private static final int kMAX_CUSTOM_MIN = 59;
private static final int kMAX_CUSTOM_SEC = 59;
/**
* Parse a custom time zone identifier and return a corresponding zone.
* @param id a string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or
* GMT[+-]hh.
* @return a newly created SimpleTimeZone with the given offset and
* no Daylight Savings Time, or null if the id cannot be parsed.
*/
public static TimeZone getCustomTimeZone(String id){
int[] fields = new int[4];
if (parseCustomID(id, fields)) {
String zid = formatCustomID(fields[1], fields[2], fields[3], fields[0] < 0);
int offset = fields[0] * ((fields[1] * 60 + fields[2]) * 60 + fields[3]) * 1000;
return new SimpleTimeZone(offset, zid);
}
return null;
}
/**
* Parse a custom time zone identifier and return the normalized
* custom time zone identifier for the given custom id string.
* @param id a string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or
* GMT[+-]hh.
* @return The normalized custom id string.
*/
public static String getCustomID(String id) {
int[] fields = new int[4];
if (parseCustomID(id, fields)) {
return formatCustomID(fields[1], fields[2], fields[3], fields[0] < 0);
}
return null;
}
/*
* Parses the given custom time zone identifier
* @param id id A string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or
* GMT[+-]hh.
* @param fields An array of int (length = 4) to receive the parsed
* offset time fields. The sign is set to fields[0] (-1 or 1),
* hour is set to fields[1], minute is set to fields[2] and second is
* set to fields[3].
* @return Returns true when the given custom id is valid.
*/
static boolean parseCustomID(String id, int[] fields) {
NumberFormat numberFormat = null;
String idUppercase = id.toUpperCase();
if (id != null && id.length() > kGMT_ID.length() &&
idUppercase.startsWith(kGMT_ID)) {
ParsePosition pos = new ParsePosition(kGMT_ID.length());
int sign = 1;
int hour = 0;
int min = 0;
int sec = 0;
if (id.charAt(pos.getIndex()) == 0x002D /*'-'*/) {
sign = -1;
} else if (id.charAt(pos.getIndex()) != 0x002B /*'+'*/) {
return false;
}
pos.setIndex(pos.getIndex() + 1);
numberFormat = NumberFormat.getInstance();
numberFormat.setParseIntegerOnly(true);
// Look for either hh:mm, hhmm, or hh
int start = pos.getIndex();
Number n = numberFormat.parse(id, pos);
if (pos.getIndex() == start) {
return false;
}
hour = n.intValue();
if (pos.getIndex() < id.length()){
if (pos.getIndex() - start > 2
|| id.charAt(pos.getIndex()) != 0x003A /*':'*/) {
return false;
}
// hh:mm
pos.setIndex(pos.getIndex() + 1);
int oldPos = pos.getIndex();
n = numberFormat.parse(id, pos);
if ((pos.getIndex() - oldPos) != 2) {
// must be 2 digits
return false;
}
min = n.intValue();
if (pos.getIndex() < id.length()) {
if (id.charAt(pos.getIndex()) != 0x003A /*':'*/) {
return false;
}
// [:ss]
pos.setIndex(pos.getIndex() + 1);
oldPos = pos.getIndex();
n = numberFormat.parse(id, pos);
if (pos.getIndex() != id.length()
|| (pos.getIndex() - oldPos) != 2) {
return false;
}
sec = n.intValue();
}
} else {
// Supported formats are below -
//
// HHmmss
// Hmmss
// HHmm
// Hmm
// HH
// H
int length = pos.getIndex() - start;
if (length <= 0 || 6 < length) {
// invalid length
return false;
}
switch (length) {
case 1:
case 2:
// already set to hour
break;
case 3:
case 4:
min = hour % 100;
hour /= 100;
break;
case 5:
case 6:
sec = hour % 100;
min = (hour/100) % 100;
hour /= 10000;
break;
}
}
if (hour <= kMAX_CUSTOM_HOUR && min <= kMAX_CUSTOM_MIN && sec <= kMAX_CUSTOM_SEC) {
if (fields != null) {
if (fields.length >= 1) {
fields[0] = sign;
}
if (fields.length >= 2) {
fields[1] = hour;
}
if (fields.length >= 3) {
fields[2] = min;
}
if (fields.length >= 4) {
fields[3] = sec;
}
}
return true;
}
}
return false;
}
/**
* Creates a custom zone for the offset
* @param offset GMT offset in milliseconds
* @return A custom TimeZone for the offset with normalized time zone id
*/
public static TimeZone getCustomTimeZone(int offset) {
boolean negative = false;
int tmp = offset;
if (offset < 0) {
negative = true;
tmp = -offset;
}
int hour, min, sec, millis;
millis = tmp % 1000;
if (ASSERT) {
Assert.assrt("millis!=0", millis != 0);
}
tmp /= 1000;
sec = tmp % 60;
tmp /= 60;
min = tmp % 60;
hour = tmp / 60;
// Note: No millisecond part included in TZID for now
String zid = formatCustomID(hour, min, sec, negative);
return new SimpleTimeZone(offset, zid);
}
/*
* Returns the normalized custom TimeZone ID
*/
static String formatCustomID(int hour, int min, int sec, boolean negative) {
// Create normalized time zone ID - GMT[+|-]hh:mm[:ss]
StringBuffer zid = new StringBuffer(kCUSTOM_TZ_PREFIX);
if (hour != 0 || min != 0) {
if(negative) {
zid.append('-');
} else {
zid.append('+');
}
// Always use US-ASCII digits
if (hour < 10) {
zid.append('0');
}
zid.append(hour);
zid.append(':');
if (min < 10) {
zid.append('0');
}
zid.append(min);
if (sec != 0) {
// Optional second field
zid.append(':');
if (sec < 10) {
zid.append('0');
}
zid.append(sec);
}
}
return zid.toString();
}
private static SoftReference OLSON_TO_META_REF;
private static SoftReference META_TO_OLSON_REF;
static class OlsonToMetaMappingEntry {
String mzid;
long from;
long to;
}
private static class MetaToOlsonMappingEntry {
String id;
String territory;
}
static Map getOlsonToMetaMap() {
Map olsonToMeta = null;
synchronized(ZoneMeta.class) {
if (OLSON_TO_META_REF != null) {
olsonToMeta = (HashMap)OLSON_TO_META_REF.get();
}
if (olsonToMeta == null) {
olsonToMeta = createOlsonToMetaMap();
if (olsonToMeta == null) {
// We need to return non-null Map to avoid disaster
olsonToMeta = new HashMap();
}
OLSON_TO_META_REF = new SoftReference(olsonToMeta);
}
}
return olsonToMeta;
}
/*
* Create olson tzid to metazone mappings from metazoneInfo.res (3.8.1 or later)
*/
private static Map createOlsonToMetaMap() {
// Create olson id to metazone mapping table
HashMap olsonToMeta = null;
UResourceBundle metazoneMappingsBundle = null;
try {
UResourceBundle bundle = UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, "metazoneInfo");
metazoneMappingsBundle = bundle.get("metazoneMappings");
} catch (MissingResourceException mre) {
// do nothing
}
if (metazoneMappingsBundle != null) {
String[] tzids = getAvailableIDs();
for (int i = 0; i < tzids.length; i++) {
// Skip aliases
String canonicalID = TimeZone.getCanonicalID(tzids[i]);
if (canonicalID == null || !tzids[i].equals(canonicalID)) {
continue;
}
String tzkey = tzids[i].replace('/', ':');
try {
UResourceBundle zoneBundle = metazoneMappingsBundle.get(tzkey);
LinkedList mzMappings = new LinkedList();
for (int idx = 0; ; idx++) {
try {
UResourceBundle mz = zoneBundle.get("mz" + idx);
String[] mzstr = mz.getStringArray();
if (mzstr == null || mzstr.length != 3) {
continue;
}
OlsonToMetaMappingEntry mzmap = new OlsonToMetaMappingEntry();
mzmap.mzid = mzstr[0].intern();
mzmap.from = parseDate(mzstr[1]);
mzmap.to = parseDate(mzstr[2]);
// Add this mapping to the list
mzMappings.add(mzmap);
} catch (MissingResourceException nomz) {
// we're done
break;
} catch (IllegalArgumentException baddate) {
// skip this
}
}
if (mzMappings.size() != 0) {
// Add to the olson-to-meta map
if (olsonToMeta == null) {
olsonToMeta = new HashMap();
}
olsonToMeta.put(tzids[i], mzMappings);
}
} catch (MissingResourceException noum) {
// Does not use metazone, just skip this.
}
}
}
return olsonToMeta;
}
/**
* Returns a CLDR metazone ID for the given Olson tzid and time.
*/
public static String getMetazoneID(String olsonID, long date) {
String mzid = null;
Map olsonToMeta = getOlsonToMetaMap();
List mappings = (List)olsonToMeta.get(olsonID);
if (mappings == null) {
// The given ID might be an alias - try its canonical id
String canonicalID = getCanonicalSystemID(olsonID);
if (canonicalID != null && !canonicalID.equals(olsonID)) {
mappings = (List)olsonToMeta.get(canonicalID);
}
}
if (mappings != null) {
for (int i = 0; i < mappings.size(); i++) {
OlsonToMetaMappingEntry mzm = (OlsonToMetaMappingEntry)mappings.get(i);
if (date >= mzm.from && date < mzm.to) {
mzid = mzm.mzid;
break;
}
}
}
return mzid;
}
private static Map getMetaToOlsonMap() {
HashMap metaToOlson = null;
synchronized(ZoneMeta.class) {
if (META_TO_OLSON_REF != null) {
metaToOlson = (HashMap)META_TO_OLSON_REF.get();
}
if (metaToOlson == null) {
metaToOlson = new HashMap();
UResourceBundle metazonesBundle = null;
try {
UResourceBundle supplementalBundle = UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME,
"supplementalData");
UResourceBundle mapTimezonesBundle = supplementalBundle.get("mapTimezones");
metazonesBundle = mapTimezonesBundle.get("metazones");
} catch (MissingResourceException mre) {
// do nothing
}
if (metazonesBundle != null) {
Enumeration mzenum = metazonesBundle.getKeys();
while (mzenum.hasMoreElements()) {
String mzkey = (String)mzenum.nextElement();
if (!mzkey.startsWith("meta:")) {
continue;
}
String tzid = null;
try {
tzid = metazonesBundle.getString(mzkey);
} catch (MissingResourceException mre) {
// It should not happen..
}
if (tzid != null) {
int territoryIdx = mzkey.lastIndexOf('_');
if (territoryIdx > 0) {
String mzid = mzkey.substring(5 /* "meta:".length() */, territoryIdx);
String territory = mzkey.substring(territoryIdx + 1);
List mappings = (List)metaToOlson.get(mzid);
if (mappings == null) {
mappings = new LinkedList();
metaToOlson.put(mzid, mappings);
}
MetaToOlsonMappingEntry olsonmap = new MetaToOlsonMappingEntry();
olsonmap.id = tzid;
olsonmap.territory = territory;
mappings.add(olsonmap);
}
}
}
}
META_TO_OLSON_REF = new SoftReference(metaToOlson);
}
}
return metaToOlson;
}
/**
* Returns an Olson ID for the ginve metazone and region
*/
public static String getZoneIdByMetazone(String metazoneID, String region) {
String tzid = null;
Map metaToOlson = getMetaToOlsonMap();
List mappings = (List)metaToOlson.get(metazoneID);
if (mappings != null) {
for (int i = 0; i < mappings.size(); i++) {
MetaToOlsonMappingEntry olsonmap = (MetaToOlsonMappingEntry)mappings.get(i);
if (olsonmap.territory.equals(region)) {
tzid = olsonmap.id;
break;
} else if (olsonmap.territory.equals("001")) {
tzid = olsonmap.id;
}
}
}
return tzid;
}
// /**
// * Returns an Olson ID for the given metazone and locale
// */
// public static String getZoneIdByMetazone(String metazoneID, ULocale loc) {
// String region = loc.getCountry();
// if (region.length() == 0) {
// // Get likely region
// ULocale tmp = ULocale.addLikelySubtag(loc);
// region = tmp.getCountry();
// }
// return getZoneIdByMetazone(metazoneID, region);
// }
/*
* Convert a date string used by metazone mappings to long.
* The format used by CLDR metazone mapping is "yyyy-MM-dd HH:mm".
* We do not want to use SimpleDateFormat to parse the metazone
* mapping range strings in createOlsonToMeta, because it might be
* called from SimpleDateFormat initialization code.
*/
static long parseDate (String text) throws IllegalArgumentException {
int year = 0, month = 0, day = 0, hour = 0, min = 0;
int idx;
int n;
// "yyyy" (0 - 3)
for (idx = 0; idx <= 3; idx++) {
n = text.charAt(idx) - '0';
if (n >= 0 && n < 10) {
year = 10*year + n;
} else {
throw new IllegalArgumentException("Bad year");
}
}
// "MM" (5 - 6)
for (idx = 5; idx <= 6; idx++) {
n = text.charAt(idx) - '0';
if (n >= 0 && n < 10) {
month = 10*month + n;
} else {
throw new IllegalArgumentException("Bad month");
}
}
// "dd" (8 - 9)
for (idx = 8; idx <= 9; idx++) {
n = text.charAt(idx) - '0';
if (n >= 0 && n < 10) {
day = 10*day + n;
} else {
throw new IllegalArgumentException("Bad day");
}
}
// "HH" (11 - 12)
for (idx = 11; idx <= 12; idx++) {
n = text.charAt(idx) - '0';
if (n >= 0 && n < 10) {
hour = 10*hour + n;
} else {
throw new IllegalArgumentException("Bad hour");
}
}
// "mm" (14 - 15)
for (idx = 14; idx <= 15; idx++) {
n = text.charAt(idx) - '0';
if (n >= 0 && n < 10) {
min = 10*min + n;
} else {
throw new IllegalArgumentException("Bad minute");
}
}
long date = Grego.fieldsToDay(year, month - 1, day) * Grego.MILLIS_PER_DAY
+ hour * Grego.MILLIS_PER_HOUR + min * Grego.MILLIS_PER_MINUTE;
return date;
}
}