| /* |
| ********************************************************************** |
| * 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; |
| } |