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