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