• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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;
toString()59         public String toString() {
60             return this == GMT ? "gmt" : this == LOCATION ? "location" : "non-location";
61         }
62     }
63 
64     public enum Type {
65         GENERIC, SPECIFIC;
toString(boolean daylight)66         public String toString(boolean daylight) {
67             return this == GENERIC ? "generic" : daylight ? "daylight" : "standard";
68         }
69 
toString()70         public String toString() {
71             return name().toLowerCase(Locale.ENGLISH);
72         }
73     }
74 
75     public enum Length {
76         SHORT, LONG, OTHER;
toString()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 
Format(Type type, Location location, Length length)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 
TimezoneFormatter(Factory cldrFactory, String localeID, boolean includeDraft)140     public TimezoneFormatter(Factory cldrFactory, String localeID, boolean includeDraft) {
141         this(cldrFactory.make(localeID, true, includeDraft));
142     }
143 
TimezoneFormatter(Factory cldrFactory, String localeID, DraftStatus minimalDraftStatus)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      */
TimezoneFormatter(CLDRFile resolvedLocaleFile)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      */
getStringValue(String cleanPath)184     private String getStringValue(String cleanPath) {
185         checkForDraft(cleanPath);
186         return desiredLocaleFile.getWinningValue(cleanPath);
187     }
188 
getName(int territory_name, String country, boolean skipDraft2)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 
checkForDraft(String cleanPath)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      */
getFormattedZone(String zoneid, String pattern, long date)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      */
getFormattedZone(String inputZoneid, Location location, Type type, Length length, long date)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      */
getFormattedZone(String inputZoneid, Location location, Type type, Length length, boolean daylight, int gmtOffset1, String metazone, boolean noTimezoneChangeWithin184Days)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 
getFormattedZoneInternal(String inputZoneid, Location location, Type type, Length length, boolean daylight, int gmtOffset1, String metazone, boolean noTimezoneChangeWithin184Days)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 kesäaika // daylight
308             // Europe/Dublin → Am Samhraidh na hÉireann // 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 
getLocalizedExplicitTzid(String zoneid, Type type, Length length, boolean daylight)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 
getLocalizedMetazone(String metazone, Type type, Length length, boolean daylight)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 
getLocalizedCountryName(String zoneIdsCountry)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 
getLocalizedExemplarCity(String timezoneString)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      */
parse(String inputText, ParsePosition parsePosition)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      */
parse(String inputText, ParsePosition parsePosition, long[] offsetMillis)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      */
buildParsingInfo()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      */
matchesPart(String input, String prefix, String suffix)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      */
getFallbackName(String zoneid)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