blob: ffcbc0f15c543f4ce0e3dd408537066410379907 [file] [log] [blame]
/*
**********************************************************************
* Copyright (c) 2003-2005, 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.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import com.ibm.icu.text.MessageFormat;
import com.ibm.icu.text.SimpleDateFormat;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
/**
* 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 {
/**
* 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 (country == null) {
country = "";
}
if (COUNTRY_MAP == null) {
Set valid = getValidIDs();
Set unused = new TreeSet(valid);
ArrayList list = new ArrayList(); // reuse this below
COUNTRY_MAP = new TreeMap();
for (int i=0; i<ZoneMetaData.COUNTRY.length; ++i) {
String[] z = ZoneMetaData.COUNTRY[i];
// Add all valid IDs to list
list.clear();
for (int j=1; j<z.length; ++j) {
if (valid.contains(z[j])) {
list.add(z[j]);
unused.remove(z[j]);
}
}
COUNTRY_MAP.put(z[0], list.toArray(EMPTY));
}
// If there are zones in the underlying JDK that are NOT
// in our metadata, then assign them to the non-country.
// (Better than nothing.)
if (unused.size() > 0) {
list.clear();
list.addAll(Arrays.asList((String[]) COUNTRY_MAP.get("")));
list.addAll(unused);
Collections.sort(list);
COUNTRY_MAP.put("", list.toArray(EMPTY));
}
}
String[] result = (String[]) COUNTRY_MAP.get(country);
if (result == null) {
result = EMPTY; // per API spec
}
return result;
}
/**
* 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) {
if (EQUIV_MAP == null) {
createEquivMap();
}
String[] result = (String[]) EQUIV_MAP.get(id);
return (result == null) ? 0 : result.length;
}
/**
* 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) {
if (EQUIV_MAP == null) {
createEquivMap();
}
String[] a = (String[]) EQUIV_MAP.get(id);
return (a != null && index >= 0 && index < a.length) ?
a[index] : "";
}
/**
* Create the equivalency map.
*/
private static void createEquivMap() {
EQUIV_MAP = new TreeMap();
// try leaving all ids as valid
// Set valid = getValidIDs();
ArrayList list = new ArrayList(); // reuse this below
for (int i=0; i<ZoneMetaData.EQUIV.length; ++i) {
String[] z = ZoneMetaData.EQUIV[i];
list.clear();
for (int j=0; j<z.length; ++j) {
// if (valid.contains(z[j])) {
list.add(z[j]);
// }
}
if (list.size() > 1) {
String[] a = (String[]) list.toArray(EMPTY);
for (int j=0; j<a.length; ++j) {
EQUIV_MAP.put(a[j], a);
}
}
}
}
private static String[] getCanonicalInfo(String id) {
if (canonicalMap == null) {
Map m = new HashMap();
for (int i = 0; i < ZoneInfoExt.CLDR_INFO.length; ++i) {
String[] clist = ZoneInfoExt.CLDR_INFO[i];
String c = clist[0];
m.put(c, clist);
for (int j = 3; j < clist.length; ++j) {
m.put(clist[j], clist);
}
}
synchronized (ZoneMeta.class) {
canonicalMap = m;
}
}
return (String[])canonicalMap.get(id);
}
private static Map canonicalMap = null;
/**
* Return the canonical id for this tzid, which might be the id itself.
* If there is no canonical id for it, return the passed-in id.
*/
public static String getCanonicalID(String tzid) {
String[] info = getCanonicalInfo(tzid);
if (info != null) {
return info[0];
}
return tzid;
}
/**
* 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[2] != null) {
return info[1];
}
return null;
}
/**
* Handle fallbacks for generic time (rules E.. G)
*/
public static String displayFallback(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) {
ICUResourceBundle rb =
(ICUResourceBundle)UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, locale);
String rblocname = rb.getULocale().getBaseName();
if (LocaleUtility.isFallbackOf(rblocname, locale.getBaseName())) {
country = ULocale.getDisplayCountry("xx_" + country_code, locale);
}
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 (info[2] != null) { // single country
return displayRegion(country, locale);
}
if (city == null) {
city = tzid.substring(tzid.lastIndexOf('/')+1).replace('_',' ');
}
String flbPat = getTZLocalizationInfo(locale, FALLBACK_FORMAT);
MessageFormat mf = new MessageFormat(flbPat);
return mf.format(new Object[] { city, country });
}
public static String displayRegion(String cityOrCountry, ULocale locale) {
String regPat = getTZLocalizationInfo(locale, REGION_FORMAT);
MessageFormat mf = new MessageFormat(regPat);
return mf.format(new Object[] { cityOrCountry });
}
public static String displayGMT(long value, ULocale locale) {
String msgpat = getTZLocalizationInfo(locale, GMT);
String dtepat = getTZLocalizationInfo(locale, HOUR);
int n = dtepat.indexOf(';');
if (n != -1) {
if (value < 0) {
value = - value;
dtepat = dtepat.substring(n+1);
} else {
dtepat = dtepat.substring(0, n);
}
}
final long mph = 3600000;
final long mpm = 60000;
SimpleDateFormat sdf = new SimpleDateFormat(dtepat, locale);
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
String res = sdf.format(new Long(value));
MessageFormat mf = new MessageFormat(msgpat);
res = mf.format(new Object[] { res });
return res;
}
public static final int
PREFIX = 0,
HOUR = 1,
GMT = 2,
REGION_FORMAT = 3,
FALLBACK_FORMAT = 4;
/**
* 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, int index) {
String baseName = locale.getBaseName();
for (int i = 0; i < TZ_LOCALIZATION_INFO.length; ++i) {
String[] info = TZ_LOCALIZATION_INFO[i];
String prefix = info[PREFIX];
if (prefix == null
|| (index < info.length
&& info[index] != null
&& baseName.indexOf(prefix) == 0)) {
return info[index];
}
}
throw new InternalError(); // should never get here
}
// temporary for icu4j 3.4
// locale, hour, gmt, region, fallback
private static final String[][] TZ_LOCALIZATION_INFO = {
{ "am", "+HHmm;-HHmm" },
{ "bg", "+HHmm;-HHmm", "\u0413\u0440\u0438\u0438\u043d\u0443\u0438\u0447{0}" },
{ "cy", "+HHmm;-HHmm" },
{ "el", "+HHmm;-HHmm" },
{ "hr", "+HHmm;-HHmm" },
{ "ja", "+HHmm;-HHmm", null, "{0}\u6642\u9593", "{0} ({1})\u6642\u9593" },
{ "nn", "+HH.mm;-HH.mm" },
{ "sk", "+HHmm;-HHmm" },
{ "sl", "+HHmm;-HHmm" },
{ "sr", "+HHmm;-HHmm" },
{ "sv", "+HH.mm;-HH.mm", "GMT" },
{ "th", "+HHmm;-HHmm" },
{ "uk", "+HHmm;-HHmm" },
{ "zh_Hant", "+HH:mm;-HH:mm" },
{ "zh", "+HHmm;-HHmm" },
{ null, "+HH:mm;-HH:mm", "GMT{0}", "{0}", "{0} ({1})" }
};
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];
/**
* Map of country codes to zone lists.
*/
private static Map COUNTRY_MAP = null;
/**
* Map of zones to equivalent zone lists.
*/
private static Map EQUIV_MAP = null;
}