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