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