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