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 public String toString() { 60 return this == GMT ? "gmt" : this == LOCATION ? "location" : "non-location"; 61 } 62 } 63 64 public enum Type { 65 GENERIC, SPECIFIC; 66 public String toString(boolean daylight) { 67 return this == GENERIC ? "generic" : daylight ? "daylight" : "standard"; 68 } 69 70 public String toString() { 71 return name().toLowerCase(Locale.ENGLISH); 72 } 73 } 74 75 public enum Length { 76 SHORT, LONG, OTHER; 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 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 140 public TimezoneFormatter(Factory cldrFactory, String localeID, boolean includeDraft) { 141 this(cldrFactory.make(localeID, true, includeDraft)); 142 } 143 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 */ 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 */ 184 private String getStringValue(String cleanPath) { 185 checkForDraft(cleanPath); 186 return desiredLocaleFile.getWinningValue(cleanPath); 187 } 188 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 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 */ 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 */ 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 */ 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 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 kesaika // daylight 308 // Europe/Dublin Am Samhraidh na hireann // 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 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 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 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 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 */ 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 */ 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 */ 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 */ 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 */ 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