Home | History | Annotate | Download | only in util
      1 /*
      2  * *********************************************************************
      3  * Copyright (c) 2002-2004, International Business Machines Corporation and others. All Rights Reserved.
      4  * *********************************************************************
      5  * Author: Mark Davis
      6  * *********************************************************************
      7  */
      8 
      9 package org.unicode.cldr.util;
     10 
     11 import java.text.FieldPosition;
     12 import java.text.ParsePosition;
     13 import java.util.Arrays;
     14 import java.util.Date;
     15 import java.util.HashMap;
     16 import java.util.HashSet;
     17 import java.util.Iterator;
     18 import java.util.LinkedHashSet;
     19 import java.util.List;
     20 import java.util.Locale;
     21 import java.util.Map;
     22 import java.util.Set;
     23 import java.util.TreeSet;
     24 import java.util.regex.Matcher;
     25 
     26 import org.unicode.cldr.tool.LikelySubtags;
     27 import org.unicode.cldr.util.CLDRFile.DraftStatus;
     28 import org.unicode.cldr.util.SupplementalDataInfo.MetaZoneRange;
     29 
     30 import com.ibm.icu.text.DateFormat;
     31 import com.ibm.icu.text.MessageFormat;
     32 import com.ibm.icu.text.SimpleDateFormat;
     33 import com.ibm.icu.text.UFormat;
     34 import com.ibm.icu.util.BasicTimeZone;
     35 import com.ibm.icu.util.Calendar;
     36 import com.ibm.icu.util.CurrencyAmount;
     37 import com.ibm.icu.util.TimeZone;
     38 import com.ibm.icu.util.TimeZoneTransition;
     39 
     40 /**
     41  * TimezoneFormatter. Class that uses CLDR data directly to parse / format timezone names according to the specification
     42  * in TR#35. Note: there are some areas where the spec needs fixing.
     43  *
     44  *
     45  * @author davis
     46  */
     47 
     48 public class TimezoneFormatter extends UFormat {
     49 
     50     /**
     51      *
     52      */
     53     private static final long serialVersionUID = -506645087792499122L;
     54     private static final long TIME = new Date().getTime();
     55     public static boolean SHOW_DRAFT = false;
     56 
     57     public enum Location {
     58         GMT, LOCATION, NON_LOCATION;
     59         public String toString() {
     60             return this == GMT ? "gmt" : this == LOCATION ? "location" : "non-location";
     61         }
     62     }
     63 
     64     public enum Type {
     65         GENERIC, SPECIFIC;
     66         public String toString(boolean daylight) {
     67             return this == GENERIC ? "generic" : daylight ? "daylight" : "standard";
     68         }
     69 
     70         public String toString() {
     71             return name().toLowerCase(Locale.ENGLISH);
     72         }
     73     }
     74 
     75     public enum Length {
     76         SHORT, LONG, OTHER;
     77         public String toString() {
     78             return this == SHORT ? "short" : this == LONG ? "long" : "other";
     79         }
     80     }
     81 
     82     public enum Format {
     83         VVVV(Type.GENERIC, Location.LOCATION, Length.OTHER), vvvv(Type.GENERIC, Location.NON_LOCATION, Length.LONG), v(Type.GENERIC, Location.NON_LOCATION,
     84             Length.SHORT), zzzz(Type.SPECIFIC, Location.NON_LOCATION, Length.LONG), z(Type.SPECIFIC, Location.NON_LOCATION, Length.SHORT), ZZZZ(Type.GENERIC,
     85                 Location.GMT, Length.LONG), Z(Type.GENERIC, Location.GMT, Length.SHORT), ZZZZZ(Type.GENERIC, Location.GMT, Length.OTHER);
     86         final Type type;
     87         final Location location;
     88         final Length length;
     89 
     90         private Format(Type type, Location location, Length length) {
     91             this.type = type;
     92             this.location = location;
     93             this.length = length;
     94         }
     95     };
     96 
     97     // /**
     98     // * Type parameter for formatting
     99     // */
    100     // public static final int GMT = 0, GENERIC = 1, STANDARD = 2, DAYLIGHT = 3, TYPE_LIMIT = 4;
    101     //
    102     // /**
    103     // * Arrays of names, for testing. Should be const, but we can't do that in Java
    104     // */
    105     // public static final List LENGTH = Arrays.asList(new String[] {"short", "long"});
    106     // public static final List TYPE = Arrays.asList(new String[] {"gmt", "generic", "standard", "daylight"});
    107 
    108     // static fields built from Timezone Database for formatting and parsing
    109 
    110     // private static final Map zone_countries = StandardCodes.make().getZoneToCounty();
    111     // private static final Map countries_zoneSet = StandardCodes.make().getCountryToZoneSet();
    112     // private static final Map old_new = StandardCodes.make().getZoneLinkold_new();
    113 
    114     private static SupplementalDataInfo sdi = SupplementalDataInfo.getInstance();
    115 
    116     // instance fields built from CLDR data for formatting and parsing
    117 
    118     private transient SimpleDateFormat hourFormatPlus = new SimpleDateFormat();
    119     private transient SimpleDateFormat hourFormatMinus = new SimpleDateFormat();
    120     private transient MessageFormat gmtFormat, regionFormat,
    121         regionFormatStandard, regionFormatDaylight, fallbackFormat;
    122     //private transient String abbreviationFallback, preferenceOrdering;
    123     private transient Set<String> singleCountriesSet;
    124 
    125     // private for computation
    126     private transient Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
    127     private transient SimpleDateFormat rfc822Plus = new SimpleDateFormat("+HHmm");
    128     private transient SimpleDateFormat rfc822Minus = new SimpleDateFormat("-HHmm");
    129     {
    130         TimeZone gmt = TimeZone.getTimeZone("GMT");
    131         rfc822Plus.setTimeZone(gmt);
    132         rfc822Minus.setTimeZone(gmt);
    133     }
    134 
    135     // input parameters
    136     private CLDRFile desiredLocaleFile;
    137     private String inputLocaleID;
    138     private boolean skipDraft;
    139 
    140     public TimezoneFormatter(Factory cldrFactory, String localeID, boolean includeDraft) {
    141         this(cldrFactory.make(localeID, true, includeDraft));
    142     }
    143 
    144     public TimezoneFormatter(Factory cldrFactory, String localeID, DraftStatus minimalDraftStatus) {
    145         this(cldrFactory.make(localeID, true, minimalDraftStatus));
    146     }
    147 
    148     /**
    149      * Create from a cldrFactory and a locale id.
    150      *
    151      * @see CLDRFile
    152      */
    153     public TimezoneFormatter(CLDRFile resolvedLocaleFile) {
    154         desiredLocaleFile = resolvedLocaleFile;
    155         inputLocaleID = desiredLocaleFile.getLocaleID();
    156         String hourFormatString = getStringValue("//ldml/dates/timeZoneNames/hourFormat");
    157         String[] hourFormatStrings = CldrUtility.splitArray(hourFormatString, ';');
    158         ICUServiceBuilder icuServiceBuilder = new ICUServiceBuilder().setCldrFile(desiredLocaleFile);
    159         hourFormatPlus = icuServiceBuilder.getDateFormat("gregorian", 0, 1);
    160         hourFormatPlus.applyPattern(hourFormatStrings[0]);
    161         hourFormatMinus = icuServiceBuilder.getDateFormat("gregorian", 0, 1);
    162         hourFormatMinus.applyPattern(hourFormatStrings[1]);
    163         gmtFormat = new MessageFormat(getStringValue("//ldml/dates/timeZoneNames/gmtFormat"));
    164         regionFormat = new MessageFormat(getStringValue("//ldml/dates/timeZoneNames/regionFormat"));
    165         regionFormatStandard = new MessageFormat(getStringValue("//ldml/dates/timeZoneNames/regionFormat[@type=\"standard\"]"));
    166         regionFormatDaylight = new MessageFormat(getStringValue("//ldml/dates/timeZoneNames/regionFormat[@type=\"daylight\"]"));
    167         fallbackFormat = new MessageFormat(getStringValue("//ldml/dates/timeZoneNames/fallbackFormat"));
    168         checkForDraft("//ldml/dates/timeZoneNames/singleCountries");
    169         // default value if not in root. Only needed for CLDR 1.3
    170         String singleCountriesList = "Africa/Bamako America/Godthab America/Santiago America/Guayaquil"
    171             + " Asia/Shanghai Asia/Tashkent Asia/Kuala_Lumpur Europe/Madrid Europe/Lisbon"
    172             + " Europe/London Pacific/Auckland Pacific/Tahiti";
    173         String temp = desiredLocaleFile.getFullXPath("//ldml/dates/timeZoneNames/singleCountries");
    174         if (temp != null) {
    175             singleCountriesList = (String) new XPathParts(null, null).set(temp).findAttributeValue("singleCountries",
    176                 "list");
    177         }
    178         singleCountriesSet = new TreeSet<String>(CldrUtility.splitList(singleCountriesList, ' '));
    179     }
    180 
    181     /**
    182      *
    183      */
    184     private String getStringValue(String cleanPath) {
    185         checkForDraft(cleanPath);
    186         return desiredLocaleFile.getWinningValue(cleanPath);
    187     }
    188 
    189     private String getName(int territory_name, String country, boolean skipDraft2) {
    190         checkForDraft(CLDRFile.getKey(territory_name, country));
    191         return desiredLocaleFile.getName(territory_name, country);
    192     }
    193 
    194     private void checkForDraft(String cleanPath) {
    195         String xpath = desiredLocaleFile.getFullXPath(cleanPath);
    196 
    197         if (SHOW_DRAFT && xpath != null && xpath.indexOf("[@draft=\"true\"]") >= 0) {
    198             System.out.println("Draft in " + inputLocaleID + ":\t" + cleanPath);
    199         }
    200     }
    201 
    202     /**
    203      * Formatting based on pattern and date.
    204      */
    205     public String getFormattedZone(String zoneid, String pattern, long date) {
    206         Format format = Format.valueOf(pattern);
    207         return getFormattedZone(zoneid, format.location, format.type, format.length, date);
    208     }
    209 
    210     /**
    211      * Formatting based on broken out features and date.
    212      */
    213     public String getFormattedZone(String inputZoneid, Location location, Type type, Length length, long date) {
    214         String zoneid = TimeZone.getCanonicalID(inputZoneid);
    215         BasicTimeZone timeZone = (BasicTimeZone) TimeZone.getTimeZone(zoneid);
    216         int gmtOffset1 = timeZone.getOffset(date);
    217         MetaZoneRange metaZoneRange = sdi.getMetaZoneRange(zoneid, date);
    218         String metazone = metaZoneRange == null ? "?" : metaZoneRange.metazone;
    219         boolean noTimezoneChangeWithin184Days = noTimezoneChangeWithin184Days(timeZone, date);
    220         boolean daylight = gmtOffset1 != timeZone.getRawOffset();
    221         return getFormattedZone(inputZoneid, location, type, length, daylight, gmtOffset1, metazone,
    222             noTimezoneChangeWithin184Days);
    223     }
    224 
    225     /**
    226      * Low-level routine for formatting based on zone, broken-out features, plus special settings (which are usually
    227      * computed from the date, but are here for specific access.)
    228      *
    229      * @param inputZoneid
    230      * @param location
    231      * @param type
    232      * @param length
    233      * @param daylight
    234      * @param gmtOffset1
    235      * @param metazone
    236      * @param noTimezoneChangeWithin184Days
    237      * @return
    238      */
    239     public String getFormattedZone(String inputZoneid, Location location, Type type, Length length, boolean daylight,
    240         int gmtOffset1, String metazone, boolean noTimezoneChangeWithin184Days) {
    241         String formatted = getFormattedZoneInternal(inputZoneid, location, type, length, daylight, gmtOffset1,
    242             metazone, noTimezoneChangeWithin184Days);
    243         if (formatted != null) {
    244             return formatted;
    245         }
    246         if (type == Type.GENERIC && location == Location.NON_LOCATION) {
    247             formatted = getFormattedZone(inputZoneid, Location.LOCATION, type, length, daylight, gmtOffset1, metazone,
    248                 noTimezoneChangeWithin184Days);
    249             if (formatted != null) {
    250                 return formatted;
    251             }
    252         }
    253         return getFormattedZone(inputZoneid, Location.GMT, null, Length.LONG, daylight, gmtOffset1, metazone,
    254             noTimezoneChangeWithin184Days);
    255     }
    256 
    257     private String getFormattedZoneInternal(String inputZoneid, Location location, Type type, Length length,
    258         boolean daylight, int gmtOffset1, String metazone, boolean noTimezoneChangeWithin184Days) {
    259 
    260         String result;
    261         // 1. Canonicalize the Olson ID according to the table in supplemental data.
    262         // Use that canonical ID in each of the following steps.
    263         // * America/Atka => America/Adak
    264         // * Australia/ACT => Australia/Sydney
    265 
    266         String zoneid = TimeZone.getCanonicalID(inputZoneid);
    267         // BasicTimeZone timeZone = (BasicTimeZone) TimeZone.getTimeZone(zoneid);
    268         // if (zoneid == null) zoneid = inputZoneid;
    269 
    270         switch (location) {
    271         default:
    272             throw new IllegalArgumentException("Bad enum value for location: " + location);
    273 
    274         case GMT:
    275             // 2. For RFC 822 GMT format ("Z") return the results according to the RFC.
    276             // America/Los_Angeles  "-0800"
    277             // Note: The digits in this case are always from the western digits, 0..9.
    278             if (length == Length.SHORT) {
    279                 return gmtOffset1 < 0 ? rfc822Minus.format(new Date(-gmtOffset1)) : rfc822Plus.format(new Date(
    280                     gmtOffset1));
    281             }
    282 
    283             // 3. For the localized GMT format, use the gmtFormat (such as "GMT{0}" or "HMG{0}") with the hourFormat
    284             // (such as "+HH:mm;-HH:mm" or "+HH.mm;-HH.mm").
    285             // America/Los_Angeles  "GMT-08:00" // standard time
    286             // America/Los_Angeles  "HMG-07:00" // daylight time
    287             // Etc/GMT+3  "GMT-03.00" // note that TZ tzids have inverse polarity!
    288             // Note: The digits should be whatever are appropriate for the locale used to format the time zone, not
    289             // necessarily from the western digits, 0..9. For example, they might be from ...
    290 
    291             DateFormat format = gmtOffset1 < 0 ? hourFormatMinus : hourFormatPlus;
    292             calendar.setTimeInMillis(Math.abs(gmtOffset1));
    293             result = format.format(calendar);
    294             return gmtFormat.format(new Object[] { result });
    295         // 4. For ISO 8601 time zone format ("ZZZZZ") return the results according to the ISO 8601.
    296         // America/Los_Angeles  "-08:00"
    297         // Etc/GMT  Z // special case of UTC
    298         // Note: The digits in this case are always from the western digits, 0..9.
    299 
    300         // TODO
    301         case NON_LOCATION:
    302             // 5. For the non-location formats (generic or specific),
    303             // 5.1 if there is an explicit translation for the TZID in timeZoneNames according to type (generic,
    304             // standard, or daylight) in the resolved locale, return it.
    305             // America/Los_Angeles  "Heure du Pacifique (UA)" // generic
    306             // America/Los_Angeles   // standard
    307             // America/Los_Angeles  Yhdysvaltain Tyynenmeren kesaika // daylight
    308             // Europe/Dublin  Am Samhraidh na hireann // daylight
    309             // Note: This translation may not at all be literal: it would be what is most recognizable for people using
    310             // the target language.
    311 
    312             String formatValue = getLocalizedExplicitTzid(zoneid, type, length, daylight);
    313             if (formatValue != null) {
    314                 return formatValue;
    315             }
    316 
    317             // 5.2 Otherwise, if there is a metazone standard format,
    318             // and the offset and daylight offset do not change within 184 day +/- interval
    319             // around the exact formatted time, use the metazone standard format ("Mountain Standard Time" for Phoenix).
    320             // (184 is the smallest number that is at least 6 months AND the smallest number that is more than 1/2 year
    321             // (Gregorian)).
    322             if (metazone == null) {
    323                 metazone = sdi.getMetaZoneRange(zoneid, TIME).metazone;
    324             }
    325             String metaZoneName = getLocalizedMetazone(metazone, type, length, daylight);
    326             if (metaZoneName == null && noTimezoneChangeWithin184Days) {
    327                 metaZoneName = getLocalizedMetazone(metazone, Type.SPECIFIC, length, false);
    328             }
    329 
    330             // 5.3 Otherwise, if there is a metazone generic format, then do the following:
    331             // *** CHANGE to
    332             // 5.2 Get the appropriate metazone format (generic, standard, daylight).
    333             // if there is none, (do old 5.2).
    334             // if there is either one, then do the following
    335 
    336             if (metaZoneName != null) {
    337 
    338                 // 5.3.1 Compare offset at the requested time with the preferred zone for the current locale; if same,
    339                 // we use the metazone generic format.
    340                 // "Pacific Time" for Vancouver if the locale is en-CA, or for Los Angeles if locale is en-US. Note that
    341                 // the fallback is the golden zone.
    342                 // The metazone data actually supplies the preferred zone for a country.
    343                 String localeId = desiredLocaleFile.getLocaleID();
    344                 LanguageTagParser languageTagParser = new LanguageTagParser();
    345                 String defaultRegion = languageTagParser.set(localeId).getRegion();
    346                 // If the locale does not have a country the likelySubtags supplemental data is used to get the most
    347                 // likely country.
    348                 if (defaultRegion.isEmpty()) {
    349                     String localeMax = LikelySubtags.maximize(localeId, sdi.getLikelySubtags());
    350                     defaultRegion = languageTagParser.set(localeMax).getRegion();
    351                     if (defaultRegion.isEmpty()) {
    352                         return "001"; // CLARIFY
    353                     }
    354                 }
    355                 Map<String, String> regionToZone = sdi.getMetazoneToRegionToZone().get(metazone);
    356                 String preferredLocalesZone = regionToZone.get(defaultRegion);
    357                 if (preferredLocalesZone == null) {
    358                     preferredLocalesZone = regionToZone.get("001");
    359                 }
    360                 // TimeZone preferredTimeZone = TimeZone.getTimeZone(preferredZone);
    361                 // CLARIFY: do we mean that the offset is the same at the current time, or that the zone is the same???
    362                 // the following code does the latter.
    363                 if (zoneid.equals(preferredLocalesZone)) {
    364                     return metaZoneName;
    365                 }
    366 
    367                 // 5.3.2 If the zone is the preferred zone for its country but not for the country of the locale, use
    368                 // the metazone generic format + (country)
    369                 // [Generic partial location] "Pacific Time (Canada)" for the zone Vancouver in the locale en_MX.
    370 
    371                 String zoneIdsCountry = TimeZone.getRegion(zoneid);
    372                 String preferredZonesCountrysZone = regionToZone.get(zoneIdsCountry);
    373                 if (preferredZonesCountrysZone == null) {
    374                     preferredZonesCountrysZone = regionToZone.get("001");
    375                 }
    376                 if (zoneid.equals(preferredZonesCountrysZone)) {
    377                     String countryName = getLocalizedCountryName(zoneIdsCountry);
    378                     return fallbackFormat.format(new Object[] { countryName, metaZoneName }); // UGLY, should be able to
    379                     // just list
    380                 }
    381 
    382                 // If all else fails, use metazone generic format + (city).
    383                 // [Generic partial location]: "Mountain Time (Phoenix)", "Pacific Time (Whitehorse)"
    384                 String cityName = getLocalizedExemplarCity(zoneid);
    385                 return fallbackFormat.format(new Object[] { cityName, metaZoneName });
    386             }
    387             //
    388             // Otherwise, fall back.
    389             // Note: In composing the metazone + city or country: use the fallbackFormat
    390             //
    391             // {1} will be the metazone
    392             // {0} will be a qualifier (city or country)
    393             // Example: Pacific Time (Phoenix)
    394 
    395             if (length == Length.LONG) {
    396                 return getRegionFallback(zoneid,
    397                     type == Type.GENERIC || noTimezoneChangeWithin184Days ? regionFormat
    398                         : daylight ? regionFormatDaylight : regionFormatStandard);
    399             }
    400             return null;
    401 
    402         case LOCATION:
    403 
    404             // 6.1 For the generic location format:
    405             return getRegionFallback(zoneid, regionFormat);
    406 
    407         // FIX examples
    408         // Otherwise, get both the exemplar city and country name. Format them with the fallbackRegionFormat (for
    409         // example, "{1} Time ({0})". For example:
    410         // America/Buenos_Aires  "Argentina Time (Buenos Aires)"
    411         // // if the fallbackRegionFormat is "{1} Time ({0})".
    412         // America/Buenos_Aires  " (-)"
    413         // // if both are translated, and the fallbackRegionFormat is "{1} ({0})".
    414         // America/Buenos_Aires  "AR (-)"
    415         // // if Argentina is not translated.
    416         // America/Buenos_Aires  " (Buenos Aires)"
    417         // // if Buenos Aires is not translated.
    418         // America/Buenos_Aires  "AR (Buenos Aires)"
    419         // // if both are not translated.
    420         // Note: As with the regionFormat, exceptional cases need to be explicitly translated.
    421         }
    422     }
    423 
    424     private String getRegionFallback(String zoneid, MessageFormat regionFallbackFormat) {
    425         // Use as the country name, the explicitly localized country if available, otherwise the raw country code.
    426         // If the localized exemplar city is not available, use as the exemplar city the last field of the raw TZID,
    427         // stripping off the prefix and turning _ into space.
    428         // CU  "CU" // no localized country name for Cuba
    429 
    430         // CLARIFY that above applies to 5.3.2 also!
    431 
    432         // America/Los_Angeles  "Los Angeles" // no localized exemplar city
    433         // From <timezoneData> get the country code for the zone, and determine whether there is only one timezone
    434         // in the country.
    435         // If there is only one timezone or the zone id is in the singleCountries list,
    436         // format the country name with the regionFormat (for example, "{0} Time"), and return it.
    437         // Europe/Rome  IT  Italy Time // for English
    438         // Africa/Monrovia  LR  "Hora de Liberja"
    439         // America/Havana  CU  "Hora de CU" // if CU is not localized
    440         // Note: If a language does require grammatical changes when composing strings, then it should either use a
    441         // neutral format such as what is in root, or put all exceptional cases in explicitly translated strings.
    442         //
    443 
    444         // Note: <timezoneData> may not have data for new TZIDs.
    445         //
    446         // If the country for the zone cannot be resolved, format the exemplar city
    447         // (it is unlikely that the localized exemplar city is available in this case,
    448         // so the exemplar city might be composed by the last field of the raw TZID as described above)
    449         // with the regionFormat (for example, "{0} Time"), and return it.
    450         // ***FIX by changing to: if the country can't be resolved, or the zonesInRegion are not unique
    451 
    452         String zoneIdsCountry = TimeZone.getRegion(zoneid);
    453         if (zoneIdsCountry != null) {
    454             String[] zonesInRegion = TimeZone.getAvailableIDs(zoneIdsCountry);
    455             if (zonesInRegion != null && zonesInRegion.length == 1 || singleCountriesSet.contains(zoneid)) {
    456                 String countryName = getLocalizedCountryName(zoneIdsCountry);
    457                 return regionFallbackFormat.format(new Object[] { countryName });
    458             }
    459         }
    460         String cityName = getLocalizedExemplarCity(zoneid);
    461         return regionFallbackFormat.format(new Object[] { cityName });
    462     }
    463 
    464     public boolean noTimezoneChangeWithin184Days(BasicTimeZone timeZone, long date) {
    465         // TODO Fix this to look at the real times
    466         TimeZoneTransition startTransition = timeZone.getPreviousTransition(date, true);
    467         if (startTransition == null) {
    468             //System.out.println("No transition for " + timeZone.getID() + " on " + new Date(date));
    469             return true;
    470         }
    471         if (!atLeast184Days(startTransition.getTime(), date)) {
    472             return false;
    473         } else {
    474             TimeZoneTransition nextTransition = timeZone.getNextTransition(date, false);
    475             if (nextTransition != null && !atLeast184Days(date, nextTransition.getTime())) {
    476                 return false;
    477             }
    478         }
    479         return true;
    480     }
    481 
    482     private boolean atLeast184Days(long start, long end) {
    483         long transitionDays = (end - start) / (24 * 60 * 60 * 1000);
    484         return transitionDays >= 184;
    485     }
    486 
    487     private String getLocalizedExplicitTzid(String zoneid, Type type, Length length, boolean daylight) {
    488         String formatValue = desiredLocaleFile.getWinningValue("//ldml/dates/timeZoneNames/zone[@type=\"" + zoneid
    489             + "\"]/" + length.toString() + "/" + type.toString(daylight));
    490         return formatValue;
    491     }
    492 
    493     public String getLocalizedMetazone(String metazone, Type type, Length length, boolean daylight) {
    494         if (metazone == null) {
    495             return null;
    496         }
    497         String name = desiredLocaleFile.getWinningValue("//ldml/dates/timeZoneNames/metazone[@type=\"" + metazone
    498             + "\"]/" + length.toString() + "/" + type.toString(daylight));
    499         return name;
    500     }
    501 
    502     private String getLocalizedCountryName(String zoneIdsCountry) {
    503         String countryName = desiredLocaleFile.getName(CLDRFile.TERRITORY_NAME, zoneIdsCountry);
    504         if (countryName == null) {
    505             countryName = zoneIdsCountry;
    506         }
    507         return countryName;
    508     }
    509 
    510     public String getLocalizedExemplarCity(String timezoneString) {
    511         String exemplarCity = desiredLocaleFile.getWinningValue("//ldml/dates/timeZoneNames/zone[@type=\""
    512             + timezoneString + "\"]/exemplarCity");
    513         if (exemplarCity == null) {
    514             exemplarCity = timezoneString.substring(timezoneString.lastIndexOf('/') + 1).replace('_', ' ');
    515         }
    516         return exemplarCity;
    517     }
    518 
    519     /**
    520      * Used for computation in parsing
    521      */
    522     private static final int WALL_LIMIT = 2, STANDARD_LIMIT = 4;
    523     private static final String[] zoneTypes = { "\"]/long/generic", "\"]/short/generic", "\"]/long/standard",
    524         "\"]/short/standard", "\"]/long/daylight", "\"]/short/daylight" };
    525 
    526     private transient Matcher m = PatternCache.get("([-+])([0-9][0-9])([0-9][0-9])").matcher("");
    527 
    528     private transient boolean parseInfoBuilt;
    529     private transient final Map<String, String> localizedCountry_countryCode = new HashMap<String, String>();
    530     private transient final Map<String, String> exemplar_zone = new HashMap<String, String>();
    531     private transient final Map<Object, Object> localizedExplicit_zone = new HashMap<Object, Object>();
    532     private transient final Map<String, String> country_zone = new HashMap<String, String>();
    533 
    534     /**
    535      * Returns zoneid. In case of an offset, returns "Etc/GMT+/-HH" or "Etc/GMT+/-HHmm".
    536      * Remember that Olson IDs have reversed signs!
    537      */
    538     public String parse(String inputText, ParsePosition parsePosition) {
    539         long[] offsetMillisOutput = new long[1];
    540         String result = parse(inputText, parsePosition, offsetMillisOutput);
    541         if (result == null || result.length() != 0) return result;
    542         long offsetMillis = offsetMillisOutput[0];
    543         String sign = "Etc/GMT-";
    544         if (offsetMillis < 0) {
    545             offsetMillis = -offsetMillis;
    546             sign = "Etc/GMT+";
    547         }
    548         long minutes = (offsetMillis + 30 * 1000) / (60 * 1000);
    549         long hours = minutes / 60;
    550         minutes = minutes % 60;
    551         result = sign + String.valueOf(hours);
    552         if (minutes != 0) result += ":" + String.valueOf(100 + minutes).substring(1, 3);
    553         return result;
    554     }
    555 
    556     /**
    557      * Returns zoneid, or if a gmt offset, returns "" and a millis value in offsetMillis[0]. If we can't parse, return
    558      * null
    559      */
    560     public String parse(String inputText, ParsePosition parsePosition, long[] offsetMillis) {
    561         // if we haven't parsed before, build parsing info
    562         if (!parseInfoBuilt) buildParsingInfo();
    563         int startOffset = parsePosition.getIndex();
    564         // there are the following possible formats
    565 
    566         // Explicit strings
    567         // If the result is a Long it is millis, otherwise it is the zoneID
    568         Object result = localizedExplicit_zone.get(inputText);
    569         if (result != null) {
    570             if (result instanceof String) return (String) result;
    571             offsetMillis[0] = ((Long) result).longValue();
    572             return "";
    573         }
    574 
    575         // RFC 822
    576         if (m.reset(inputText).matches()) {
    577             int hours = Integer.parseInt(m.group(2));
    578             int minutes = Integer.parseInt(m.group(3));
    579             int millis = hours * 60 * 60 * 1000 + minutes * 60 * 1000;
    580             if (m.group(1).equals("-")) millis = -millis; // check sign!
    581             offsetMillis[0] = millis;
    582             return "";
    583         }
    584 
    585         // GMT-style (also fallback for daylight/standard)
    586 
    587         Object[] results = gmtFormat.parse(inputText, parsePosition);
    588         if (results != null) {
    589             if (results.length == 0) {
    590                 // for debugging
    591                 results = gmtFormat.parse(inputText, parsePosition);
    592             }
    593             String hours = (String) results[0];
    594             parsePosition.setIndex(0);
    595             Date date = hourFormatPlus.parse(hours, parsePosition);
    596             if (date != null) {
    597                 offsetMillis[0] = date.getTime();
    598                 return "";
    599             }
    600             parsePosition.setIndex(0);
    601             date = hourFormatMinus.parse(hours, parsePosition); // negative format
    602             if (date != null) {
    603                 offsetMillis[0] = -date.getTime();
    604                 return "";
    605             }
    606         }
    607 
    608         // Generic fallback, example: city or city (country)
    609 
    610         // first remove the region format if possible
    611 
    612         parsePosition.setIndex(startOffset);
    613         Object[] x = regionFormat.parse(inputText, parsePosition);
    614         if (x != null) {
    615             inputText = (String) x[0];
    616         }
    617 
    618         String city = null, country = null;
    619         parsePosition.setIndex(startOffset);
    620         x = fallbackFormat.parse(inputText, parsePosition);
    621         if (x != null) {
    622             city = (String) x[0];
    623             country = (String) x[1];
    624             // at this point, we don't really need the country, so ignore it
    625             // the city could be the last field of a zone, or could be an exemplar city
    626             // we have built the map so that both work
    627             return (String) exemplar_zone.get(city);
    628         }
    629 
    630         // see if the string is a localized country
    631         String countryCode = (String) localizedCountry_countryCode.get(inputText);
    632         if (countryCode == null) countryCode = country; // if not, try raw code
    633         return (String) country_zone.get(countryCode);
    634     }
    635 
    636     /**
    637      * Internal method. Builds parsing tables.
    638      */
    639     private void buildParsingInfo() {
    640         // TODO Auto-generated method stub
    641 
    642         // Exemplar cities (plus constructed ones)
    643         // and add all the last fields.
    644 
    645         // // do old ones first, we don't care if they are overriden
    646         // for (Iterator it = old_new.keySet().iterator(); it.hasNext();) {
    647         // String zoneid = (String) it.next();
    648         // exemplar_zone.put(getFallbackName(zoneid), zoneid);
    649         // }
    650 
    651         // then canonical ones
    652         for (String zoneid : TimeZone.getAvailableIDs()) {
    653             exemplar_zone.put(getFallbackName(zoneid), zoneid);
    654         }
    655 
    656         // now add exemplar cities, AND pick up explicit strings, AND localized countries
    657         String prefix = "//ldml/dates/timeZoneNames/zone[@type=\"";
    658         String countryPrefix = "//ldml/localeDisplayNames/territories/territory[@type=\"";
    659         Map<String, Comparable> localizedNonWall = new HashMap<String, Comparable>();
    660         Set<String> skipDuplicates = new HashSet<String>();
    661         for (Iterator<String> it = desiredLocaleFile.iterator(); it.hasNext();) {
    662             String path = it.next();
    663             // dumb, simple implementation
    664             if (path.startsWith(prefix)) {
    665                 String zoneId = matchesPart(path, prefix, "\"]/exemplarCity");
    666                 if (zoneId != null) {
    667                     String name = desiredLocaleFile.getWinningValue(path);
    668                     if (name != null) exemplar_zone.put(name, zoneId);
    669                 }
    670                 for (int i = 0; i < zoneTypes.length; ++i) {
    671                     zoneId = matchesPart(path, prefix, zoneTypes[i]);
    672                     if (zoneId != null) {
    673                         String name = desiredLocaleFile.getWinningValue(path);
    674                         if (name == null) continue;
    675                         if (i < WALL_LIMIT) { // wall time
    676                             localizedExplicit_zone.put(name, zoneId);
    677                         } else {
    678                             // TODO: if a daylight or standard string is ambiguous, return GMT!!
    679                             Object dup = localizedNonWall.get(name);
    680                             if (dup != null) {
    681                                 skipDuplicates.add(name);
    682                                 // TODO: use Etc/GMT... localizedNonWall.remove(name);
    683                                 TimeZone tz = TimeZone.getTimeZone(zoneId);
    684                                 int offset = tz.getRawOffset();
    685                                 if (i >= STANDARD_LIMIT) {
    686                                     offset += tz.getDSTSavings();
    687                                 }
    688                                 localizedNonWall.put(name, new Long(offset));
    689                             } else {
    690                                 localizedNonWall.put(name, zoneId);
    691                             }
    692                         }
    693                     }
    694                 }
    695             } else {
    696                 // now do localizedCountry_countryCode
    697                 String countryCode = matchesPart(path, countryPrefix, "\"]");
    698                 if (countryCode != null) {
    699                     String name = desiredLocaleFile.getStringValue(path);
    700                     if (name != null) localizedCountry_countryCode.put(name, countryCode);
    701                 }
    702             }
    703         }
    704         // add to main set
    705         for (Iterator<String> it = localizedNonWall.keySet().iterator(); it.hasNext();) {
    706             String key = it.next();
    707             Object value = localizedNonWall.get(key);
    708             localizedExplicit_zone.put(key, value);
    709         }
    710         // now build country_zone. Could check each time for the singleCountries list, but this is simpler
    711         for (String key : StandardCodes.make().getGoodAvailableCodes("territory")) {
    712             String[] tzids = TimeZone.getAvailableIDs(key);
    713             if (tzids == null || tzids.length == 0) continue;
    714             // only use if there is a single element OR there is a singleCountrySet element
    715             if (tzids.length == 1) {
    716                 country_zone.put(key, tzids[0]);
    717             } else {
    718                 Set<String> set = new LinkedHashSet<String>(Arrays.asList(tzids)); // make modifyable
    719                 set.retainAll(singleCountriesSet);
    720                 if (set.size() == 1) {
    721                     country_zone.put(key, set.iterator().next());
    722                 }
    723             }
    724         }
    725         parseInfoBuilt = true;
    726     }
    727 
    728     /**
    729      * Internal method for simple building tables
    730      */
    731     private String matchesPart(String input, String prefix, String suffix) {
    732         if (!input.startsWith(prefix)) return null;
    733         if (!input.endsWith(suffix)) return null;
    734         return input.substring(prefix.length(), input.length() - suffix.length());
    735     }
    736 
    737     /**
    738      * Returns the name for a timezone id that will be returned as a fallback.
    739      */
    740     public static String getFallbackName(String zoneid) {
    741         String result;
    742         int pos = zoneid.lastIndexOf('/');
    743         result = pos < 0 ? zoneid : zoneid.substring(pos + 1);
    744         result = result.replace('_', ' ');
    745         return result;
    746     }
    747 
    748     /**
    749      * Getter
    750      */
    751     public boolean isSkipDraft() {
    752         return skipDraft;
    753     }
    754 
    755     /**
    756      * Setter
    757      */
    758     public TimezoneFormatter setSkipDraft(boolean skipDraft) {
    759         this.skipDraft = skipDraft;
    760         return this;
    761     }
    762 
    763     public Object parseObject(String source, ParsePosition pos) {
    764         TimeZone foo;
    765         CurrencyAmount fii;
    766         com.ibm.icu.text.UnicodeSet fuu;
    767         return null;
    768     }
    769 
    770     public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
    771         // TODO Auto-generated method stub
    772         return null;
    773     }
    774 
    775     // The following are just for compatibility, until some fixes are made.
    776 
    777     public static final List<String> LENGTH = Arrays.asList(Length.SHORT.toString(), Length.LONG.toString());
    778     public static final int LENGTH_LIMIT = LENGTH.size();
    779     public static final int TYPE_LIMIT = Type.values().length;
    780 
    781     public String getFormattedZone(String zoneId, String pattern, boolean daylight, int offset, boolean b) {
    782         Format format = Format.valueOf(pattern);
    783         return getFormattedZone(zoneId, format.location, format.type, format.length, daylight, offset, null, false);
    784     }
    785 
    786     public String getFormattedZone(String zoneId, int length, int type, int offset, boolean b) {
    787         return getFormattedZone(zoneId, Location.LOCATION, Type.values()[type], Length.values()[length], false, offset,
    788             null, true);
    789     }
    790 
    791     public String getFormattedZone(String zoneId, String pattern, long time, boolean b) {
    792         return getFormattedZone(zoneId, pattern, time);
    793     }
    794 
    795     // end compat
    796 
    797 }
    798