1 // © 2016 and later: Unicode, Inc. and others. 2 // License & terms of use: http://www.unicode.org/copyright.html#License 3 /* 4 ********************************************************************** 5 * Copyright (c) 2003-2016 International Business Machines 6 * Corporation and others. All Rights Reserved. 7 ********************************************************************** 8 * Author: Alan Liu 9 * Created: September 4 2003 10 * Since: ICU 2.8 11 ********************************************************************** 12 */ 13 package com.ibm.icu.impl; 14 15 import java.lang.ref.SoftReference; 16 import java.text.ParsePosition; 17 import java.util.Collections; 18 import java.util.Locale; 19 import java.util.MissingResourceException; 20 import java.util.Set; 21 import java.util.TreeSet; 22 23 import com.ibm.icu.text.NumberFormat; 24 import com.ibm.icu.util.Output; 25 import com.ibm.icu.util.SimpleTimeZone; 26 import com.ibm.icu.util.TimeZone; 27 import com.ibm.icu.util.TimeZone.SystemTimeZoneType; 28 import com.ibm.icu.util.UResourceBundle; 29 30 /** 31 * This class, not to be instantiated, implements the meta-data 32 * missing from the underlying core JDK implementation of time zones. 33 * There are two missing features: Obtaining a list of available zones 34 * for a given country (as defined by the Olson database), and 35 * obtaining a list of equivalent zones for a given zone (as defined 36 * by Olson links). 37 * 38 * This class uses a data class, ZoneMetaData, which is created by the 39 * tool tz2icu. 40 * 41 * @author Alan Liu 42 * @since ICU 2.8 43 */ 44 public final class ZoneMeta { 45 private static final boolean ASSERT = false; 46 47 private static final String ZONEINFORESNAME = "zoneinfo64"; 48 private static final String kREGIONS = "Regions"; 49 private static final String kZONES = "Zones"; 50 private static final String kNAMES = "Names"; 51 52 private static final String kGMT_ID = "GMT"; 53 private static final String kCUSTOM_TZ_PREFIX = "GMT"; 54 55 private static final String kWorld = "001"; 56 57 private static SoftReference<Set<String>> REF_SYSTEM_ZONES; 58 private static SoftReference<Set<String>> REF_CANONICAL_SYSTEM_ZONES; 59 private static SoftReference<Set<String>> REF_CANONICAL_SYSTEM_LOCATION_ZONES; 60 61 /** 62 * Returns an immutable set of system time zone IDs. 63 * Etc/Unknown is excluded. 64 * @return An immutable set of system time zone IDs. 65 */ getSystemZIDs()66 private static synchronized Set<String> getSystemZIDs() { 67 Set<String> systemZones = null; 68 if (REF_SYSTEM_ZONES != null) { 69 systemZones = REF_SYSTEM_ZONES.get(); 70 } 71 if (systemZones == null) { 72 Set<String> systemIDs = new TreeSet<String>(); 73 String[] allIDs = getZoneIDs(); 74 for (String id : allIDs) { 75 // exclude Etc/Unknown 76 if (id.equals(TimeZone.UNKNOWN_ZONE_ID)) { 77 continue; 78 } 79 systemIDs.add(id); 80 } 81 systemZones = Collections.unmodifiableSet(systemIDs); 82 REF_SYSTEM_ZONES = new SoftReference<Set<String>>(systemZones); 83 } 84 return systemZones; 85 } 86 87 /** 88 * Returns an immutable set of canonical system time zone IDs. 89 * The result set is a subset of {@link #getSystemZIDs()}, but not 90 * including aliases, such as "US/Eastern". 91 * @return An immutable set of canonical system time zone IDs. 92 */ getCanonicalSystemZIDs()93 private static synchronized Set<String> getCanonicalSystemZIDs() { 94 Set<String> canonicalSystemZones = null; 95 if (REF_CANONICAL_SYSTEM_ZONES != null) { 96 canonicalSystemZones = REF_CANONICAL_SYSTEM_ZONES.get(); 97 } 98 if (canonicalSystemZones == null) { 99 Set<String> canonicalSystemIDs = new TreeSet<String>(); 100 String[] allIDs = getZoneIDs(); 101 for (String id : allIDs) { 102 // exclude Etc/Unknown 103 if (id.equals(TimeZone.UNKNOWN_ZONE_ID)) { 104 continue; 105 } 106 String canonicalID = getCanonicalCLDRID(id); 107 if (id.equals(canonicalID)) { 108 canonicalSystemIDs.add(id); 109 } 110 } 111 canonicalSystemZones = Collections.unmodifiableSet(canonicalSystemIDs); 112 REF_CANONICAL_SYSTEM_ZONES = new SoftReference<Set<String>>(canonicalSystemZones); 113 } 114 return canonicalSystemZones; 115 } 116 117 /** 118 * Returns an immutable set of canonical system time zone IDs that 119 * are associated with actual locations. 120 * The result set is a subset of {@link #getCanonicalSystemZIDs()}, but not 121 * including IDs, such as "Etc/GTM+5". 122 * @return An immutable set of canonical system time zone IDs that 123 * are associated with actual locations. 124 */ getCanonicalSystemLocationZIDs()125 private static synchronized Set<String> getCanonicalSystemLocationZIDs() { 126 Set<String> canonicalSystemLocationZones = null; 127 if (REF_CANONICAL_SYSTEM_LOCATION_ZONES != null) { 128 canonicalSystemLocationZones = REF_CANONICAL_SYSTEM_LOCATION_ZONES.get(); 129 } 130 if (canonicalSystemLocationZones == null) { 131 Set<String> canonicalSystemLocationIDs = new TreeSet<String>(); 132 String[] allIDs = getZoneIDs(); 133 for (String id : allIDs) { 134 // exclude Etc/Unknown 135 if (id.equals(TimeZone.UNKNOWN_ZONE_ID)) { 136 continue; 137 } 138 String canonicalID = getCanonicalCLDRID(id); 139 if (id.equals(canonicalID)) { 140 String region = getRegion(id); 141 if (region != null && !region.equals(kWorld)) { 142 canonicalSystemLocationIDs.add(id); 143 } 144 } 145 } 146 canonicalSystemLocationZones = Collections.unmodifiableSet(canonicalSystemLocationIDs); 147 REF_CANONICAL_SYSTEM_LOCATION_ZONES = new SoftReference<Set<String>>(canonicalSystemLocationZones); 148 } 149 return canonicalSystemLocationZones; 150 } 151 152 /** 153 * Returns an immutable set of system IDs for the given conditions. 154 * @param type a system time zone type. 155 * @param region a region, or null. 156 * @param rawOffset a zone raw offset or null. 157 * @return An immutable set of system IDs for the given conditions. 158 */ getAvailableIDs(SystemTimeZoneType type, String region, Integer rawOffset)159 public static Set<String> getAvailableIDs(SystemTimeZoneType type, String region, Integer rawOffset) { 160 Set<String> baseSet = null; 161 switch (type) { 162 case ANY: 163 baseSet = getSystemZIDs(); 164 break; 165 case CANONICAL: 166 baseSet = getCanonicalSystemZIDs(); 167 break; 168 case CANONICAL_LOCATION: 169 baseSet = getCanonicalSystemLocationZIDs(); 170 break; 171 default: 172 // never occur 173 throw new IllegalArgumentException("Unknown SystemTimeZoneType"); 174 } 175 176 if (region == null && rawOffset == null) { 177 return baseSet; 178 } 179 180 if (region != null) { 181 region = region.toUpperCase(Locale.ENGLISH); 182 } 183 184 // Filter by region/rawOffset 185 Set<String> result = new TreeSet<String>(); 186 for (String id : baseSet) { 187 if (region != null) { 188 String r = getRegion(id); 189 if (!region.equals(r)) { 190 continue; 191 } 192 } 193 if (rawOffset != null) { 194 // This is VERY inefficient. 195 TimeZone z = getSystemTimeZone(id); 196 if (z == null || !rawOffset.equals(z.getRawOffset())) { 197 continue; 198 } 199 } 200 result.add(id); 201 } 202 if (result.isEmpty()) { 203 return Collections.emptySet(); 204 } 205 206 return Collections.unmodifiableSet(result); 207 } 208 209 /** 210 * Returns the number of IDs in the equivalency group that 211 * includes the given ID. An equivalency group contains zones 212 * that behave identically to the given zone. 213 * 214 * <p>If there are no equivalent zones, then this method returns 215 * 0. This means either the given ID is not a valid zone, or it 216 * is and there are no other equivalent zones. 217 * @param id a system time zone ID 218 * @return the number of zones in the equivalency group containing 219 * 'id', or zero if there are no equivalent zones. 220 * @see #getEquivalentID 221 */ countEquivalentIDs(String id)222 public static synchronized int countEquivalentIDs(String id) { 223 int count = 0; 224 UResourceBundle res = openOlsonResource(null, id); 225 if (res != null) { 226 try { 227 UResourceBundle links = res.get("links"); 228 int[] v = links.getIntVector(); 229 count = v.length; 230 } catch (MissingResourceException ex) { 231 // throw away 232 } 233 } 234 return count; 235 } 236 237 /** 238 * Returns an ID in the equivalency group that includes the given 239 * ID. An equivalency group contains zones that behave 240 * identically to the given zone. 241 * 242 * <p>The given index must be in the range 0..n-1, where n is the 243 * value returned by <code>countEquivalentIDs(id)</code>. For 244 * some value of 'index', the returned value will be equal to the 245 * given id. If the given id is not a valid system time zone, or 246 * if 'index' is out of range, then returns an empty string. 247 * @param id a system time zone ID 248 * @param index a value from 0 to n-1, where n is the value 249 * returned by <code>countEquivalentIDs(id)</code> 250 * @return the ID of the index-th zone in the equivalency group 251 * containing 'id', or an empty string if 'id' is not a valid 252 * system ID or 'index' is out of range 253 * @see #countEquivalentIDs 254 */ getEquivalentID(String id, int index)255 public static synchronized String getEquivalentID(String id, int index) { 256 String result = ""; 257 if (index >= 0) { 258 UResourceBundle res = openOlsonResource(null, id); 259 if (res != null) { 260 int zoneIdx = -1; 261 try { 262 UResourceBundle links = res.get("links"); 263 int[] zones = links.getIntVector(); 264 if (index < zones.length) { 265 zoneIdx = zones[index]; 266 } 267 } catch (MissingResourceException ex) { 268 // throw away 269 } 270 if (zoneIdx >= 0) { 271 String tmp = getZoneID(zoneIdx); 272 if (tmp != null) { 273 result = tmp; 274 } 275 } 276 } 277 } 278 return result; 279 } 280 281 private static String[] ZONEIDS = null; 282 283 /* 284 * ICU frequently refers the zone ID array in zoneinfo resource 285 */ getZoneIDs()286 private static synchronized String[] getZoneIDs() { 287 if (ZONEIDS == null) { 288 try { 289 UResourceBundle top = UResourceBundle.getBundleInstance( 290 ICUData.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER); 291 ZONEIDS = top.getStringArray(kNAMES); 292 } catch (MissingResourceException ex) { 293 // throw away.. 294 } 295 } 296 if (ZONEIDS == null) { 297 ZONEIDS = new String[0]; 298 } 299 return ZONEIDS; 300 } 301 getZoneID(int idx)302 private static String getZoneID(int idx) { 303 if (idx >= 0) { 304 String[] ids = getZoneIDs(); 305 if (idx < ids.length) { 306 return ids[idx]; 307 } 308 } 309 return null; 310 } 311 getZoneIndex(String zid)312 private static int getZoneIndex(String zid) { 313 int zoneIdx = -1; 314 315 String[] all = getZoneIDs(); 316 if (all.length > 0) { 317 int start = 0; 318 int limit = all.length; 319 320 int lastMid = Integer.MAX_VALUE; 321 for (;;) { 322 int mid = (start + limit) / 2; 323 if (lastMid == mid) { /* Have we moved? */ 324 break; /* We haven't moved, and it wasn't found. */ 325 } 326 lastMid = mid; 327 int r = zid.compareTo(all[mid]); 328 if (r == 0) { 329 zoneIdx = mid; 330 break; 331 } else if(r < 0) { 332 limit = mid; 333 } else { 334 start = mid; 335 } 336 } 337 } 338 339 return zoneIdx; 340 } 341 342 private static ICUCache<String, String> CANONICAL_ID_CACHE = new SimpleCache<String, String>(); 343 private static ICUCache<String, String> REGION_CACHE = new SimpleCache<String, String>(); 344 private static ICUCache<String, Boolean> SINGLE_COUNTRY_CACHE = new SimpleCache<String, Boolean>(); 345 getCanonicalCLDRID(TimeZone tz)346 public static String getCanonicalCLDRID(TimeZone tz) { 347 if (tz instanceof OlsonTimeZone) { 348 return ((OlsonTimeZone)tz).getCanonicalID(); 349 } 350 return getCanonicalCLDRID(tz.getID()); 351 } 352 353 /** 354 * Return the canonical id for this tzid defined by CLDR, which might be 355 * the id itself. If the given tzid is not known, return null. 356 * 357 * Note: This internal API supports all known system IDs and "Etc/Unknown" (which is 358 * NOT a system ID). 359 */ getCanonicalCLDRID(String tzid)360 public static String getCanonicalCLDRID(String tzid) { 361 String canonical = CANONICAL_ID_CACHE.get(tzid); 362 if (canonical == null) { 363 canonical = findCLDRCanonicalID(tzid); 364 if (canonical == null) { 365 // Resolve Olson link and try it again if necessary 366 try { 367 int zoneIdx = getZoneIndex(tzid); 368 if (zoneIdx >= 0) { 369 UResourceBundle top = UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, 370 ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER); 371 UResourceBundle zones = top.get(kZONES); 372 UResourceBundle zone = zones.get(zoneIdx); 373 if (zone.getType() == UResourceBundle.INT) { 374 // It's a link - resolve link and lookup 375 tzid = getZoneID(zone.getInt()); 376 canonical = findCLDRCanonicalID(tzid); 377 } 378 if (canonical == null) { 379 canonical = tzid; 380 } 381 } 382 } catch (MissingResourceException e) { 383 // fall through 384 } 385 } 386 if (canonical != null) { 387 CANONICAL_ID_CACHE.put(tzid, canonical); 388 } 389 } 390 return canonical; 391 } 392 findCLDRCanonicalID(String tzid)393 private static String findCLDRCanonicalID(String tzid) { 394 String canonical = null; 395 String tzidKey = tzid.replace('/', ':'); 396 397 try { 398 // First, try check if the given ID is canonical 399 UResourceBundle keyTypeData = UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, 400 "keyTypeData", ICUResourceBundle.ICU_DATA_CLASS_LOADER); 401 UResourceBundle typeMap = keyTypeData.get("typeMap"); 402 UResourceBundle typeKeys = typeMap.get("timezone"); 403 try { 404 /* UResourceBundle canonicalEntry = */ typeKeys.get(tzidKey); 405 // The given tzid is available in the canonical list 406 canonical = tzid; 407 } catch (MissingResourceException e) { 408 // fall through 409 } 410 if (canonical == null) { 411 // Try alias map 412 UResourceBundle typeAlias = keyTypeData.get("typeAlias"); 413 UResourceBundle aliasesForKey = typeAlias.get("timezone"); 414 canonical = aliasesForKey.getString(tzidKey); 415 } 416 } catch (MissingResourceException e) { 417 // fall through 418 } 419 return canonical; 420 } 421 422 /** 423 * Return the region code for this tzid. 424 * If tzid is not a system zone ID, this method returns null. 425 */ getRegion(String tzid)426 public static String getRegion(String tzid) { 427 String region = REGION_CACHE.get(tzid); 428 if (region == null) { 429 int zoneIdx = getZoneIndex(tzid); 430 if (zoneIdx >= 0) { 431 try { 432 UResourceBundle top = UResourceBundle.getBundleInstance( 433 ICUData.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER); 434 UResourceBundle regions = top.get(kREGIONS); 435 if (zoneIdx < regions.getSize()) { 436 region = regions.getString(zoneIdx); 437 } 438 } catch (MissingResourceException e) { 439 // throw away 440 } 441 if (region != null) { 442 REGION_CACHE.put(tzid, region); 443 } 444 } 445 } 446 return region; 447 } 448 449 /** 450 * Return the canonical country code for this tzid. If we have none, or if the time zone 451 * is not associated with a country or unknown, return null. 452 */ getCanonicalCountry(String tzid)453 public static String getCanonicalCountry(String tzid) { 454 String country = getRegion(tzid); 455 if (country != null && country.equals(kWorld)) { 456 country = null; 457 } 458 return country; 459 } 460 461 /** 462 * Return the canonical country code for this tzid. If we have none, or if the time zone 463 * is not associated with a country or unknown, return null. When the given zone is the 464 * primary zone of the country, true is set to isPrimary. 465 */ getCanonicalCountry(String tzid, Output<Boolean> isPrimary)466 public static String getCanonicalCountry(String tzid, Output<Boolean> isPrimary) { 467 isPrimary.value = Boolean.FALSE; 468 469 String country = getRegion(tzid); 470 if (country != null && country.equals(kWorld)) { 471 return null; 472 } 473 474 // Check the cache 475 Boolean singleZone = SINGLE_COUNTRY_CACHE.get(tzid); 476 if (singleZone == null) { 477 Set<String> ids = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL_LOCATION, country, null); 478 assert(ids.size() >= 1); 479 singleZone = Boolean.valueOf(ids.size() <= 1); 480 SINGLE_COUNTRY_CACHE.put(tzid, singleZone); 481 } 482 483 if (singleZone) { 484 isPrimary.value = Boolean.TRUE; 485 } else { 486 // Note: We may cache the primary zone map in future. 487 488 // Even a country has multiple zones, one of them might be 489 // dominant and treated as a primary zone. 490 try { 491 UResourceBundle bundle = UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "metaZones"); 492 UResourceBundle primaryZones = bundle.get("primaryZones"); 493 String primaryZone = primaryZones.getString(country); 494 if (tzid.equals(primaryZone)) { 495 isPrimary.value = Boolean.TRUE; 496 } else { 497 // The given ID might not be a canonical ID 498 String canonicalID = getCanonicalCLDRID(tzid); 499 if (canonicalID != null && canonicalID.equals(primaryZone)) { 500 isPrimary.value = Boolean.TRUE; 501 } 502 } 503 } catch (MissingResourceException e) { 504 // ignore 505 } 506 } 507 508 return country; 509 } 510 511 /** 512 * Given an ID and the top-level resource of the zoneinfo resource, 513 * open the appropriate resource for the given time zone. 514 * Dereference links if necessary. 515 * @param top the top level resource of the zoneinfo resource or null. 516 * @param id zone id 517 * @return the corresponding zone resource or null if not found 518 */ openOlsonResource(UResourceBundle top, String id)519 public static UResourceBundle openOlsonResource(UResourceBundle top, String id) 520 { 521 UResourceBundle res = null; 522 int zoneIdx = getZoneIndex(id); 523 if (zoneIdx >= 0) { 524 try { 525 if (top == null) { 526 top = UResourceBundle.getBundleInstance( 527 ICUData.ICU_BASE_NAME, ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER); 528 } 529 UResourceBundle zones = top.get(kZONES); 530 UResourceBundle zone = zones.get(zoneIdx); 531 if (zone.getType() == UResourceBundle.INT) { 532 // resolve link 533 zone = zones.get(zone.getInt()); 534 } 535 res = zone; 536 } catch (MissingResourceException e) { 537 res = null; 538 } 539 } 540 return res; 541 } 542 543 544 /** 545 * System time zone object cache 546 */ 547 private static class SystemTimeZoneCache extends SoftCache<String, OlsonTimeZone, String> { 548 549 /* (non-Javadoc) 550 * @see com.ibm.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object) 551 */ 552 @Override createInstance(String key, String data)553 protected OlsonTimeZone createInstance(String key, String data) { 554 OlsonTimeZone tz = null; 555 try { 556 UResourceBundle top = UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, 557 ZONEINFORESNAME, ICUResourceBundle.ICU_DATA_CLASS_LOADER); 558 UResourceBundle res = openOlsonResource(top, data); 559 if (res != null) { 560 tz = new OlsonTimeZone(top, res, data); 561 tz.freeze(); 562 } 563 } catch (MissingResourceException e) { 564 // do nothing 565 } 566 return tz; 567 } 568 } 569 570 private static final SystemTimeZoneCache SYSTEM_ZONE_CACHE = new SystemTimeZoneCache(); 571 572 /** 573 * Returns a frozen OlsonTimeZone instance for the given ID. 574 * This method returns null when the given ID is unknown. 575 */ getSystemTimeZone(String id)576 public static OlsonTimeZone getSystemTimeZone(String id) { 577 return SYSTEM_ZONE_CACHE.getInstance(id, id); 578 } 579 580 // Maximum value of valid custom time zone hour/min 581 private static final int kMAX_CUSTOM_HOUR = 23; 582 private static final int kMAX_CUSTOM_MIN = 59; 583 private static final int kMAX_CUSTOM_SEC = 59; 584 585 /** 586 * Custom time zone object cache 587 */ 588 private static class CustomTimeZoneCache extends SoftCache<Integer, SimpleTimeZone, int[]> { 589 590 /* (non-Javadoc) 591 * @see com.ibm.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object) 592 */ 593 @Override createInstance(Integer key, int[] data)594 protected SimpleTimeZone createInstance(Integer key, int[] data) { 595 assert (data.length == 4); 596 assert (data[0] == 1 || data[0] == -1); 597 assert (data[1] >= 0 && data[1] <= kMAX_CUSTOM_HOUR); 598 assert (data[2] >= 0 && data[2] <= kMAX_CUSTOM_MIN); 599 assert (data[3] >= 0 && data[3] <= kMAX_CUSTOM_SEC); 600 String id = formatCustomID(data[1], data[2], data[3], data[0] < 0); 601 int offset = data[0] * ((data[1] * 60 + data[2]) * 60 + data[3]) * 1000; 602 SimpleTimeZone tz = new SimpleTimeZone(offset, id); 603 tz.freeze(); 604 return tz; 605 } 606 } 607 608 private static final CustomTimeZoneCache CUSTOM_ZONE_CACHE = new CustomTimeZoneCache(); 609 610 /** 611 * Parse a custom time zone identifier and return a corresponding zone. 612 * @param id a string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or 613 * GMT[+-]hh. 614 * @return a frozen SimpleTimeZone with the given offset and 615 * no Daylight Savings Time, or null if the id cannot be parsed. 616 */ getCustomTimeZone(String id)617 public static SimpleTimeZone getCustomTimeZone(String id){ 618 int[] fields = new int[4]; 619 if (parseCustomID(id, fields)) { 620 // fields[0] - sign 621 // fields[1] - hour / 5-bit 622 // fields[2] - min / 6-bit 623 // fields[3] - sec / 6-bit 624 Integer key = Integer.valueOf( 625 fields[0] * (fields[1] | fields[2] << 5 | fields[3] << 11)); 626 return CUSTOM_ZONE_CACHE.getInstance(key, fields); 627 } 628 return null; 629 } 630 631 /** 632 * Parse a custom time zone identifier and return the normalized 633 * custom time zone identifier for the given custom id string. 634 * @param id a string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or 635 * GMT[+-]hh. 636 * @return The normalized custom id string. 637 */ getCustomID(String id)638 public static String getCustomID(String id) { 639 int[] fields = new int[4]; 640 if (parseCustomID(id, fields)) { 641 return formatCustomID(fields[1], fields[2], fields[3], fields[0] < 0); 642 } 643 return null; 644 } 645 646 /* 647 * Parses the given custom time zone identifier 648 * @param id id A string of the form GMT[+-]hh:mm, GMT[+-]hhmm, or 649 * GMT[+-]hh. 650 * @param fields An array of int (length = 4) to receive the parsed 651 * offset time fields. The sign is set to fields[0] (-1 or 1), 652 * hour is set to fields[1], minute is set to fields[2] and second is 653 * set to fields[3]. 654 * @return Returns true when the given custom id is valid. 655 */ parseCustomID(String id, int[] fields)656 static boolean parseCustomID(String id, int[] fields) { 657 NumberFormat numberFormat = null; 658 659 if (id != null && id.length() > kGMT_ID.length() && 660 id.toUpperCase(Locale.ENGLISH).startsWith(kGMT_ID)) { 661 ParsePosition pos = new ParsePosition(kGMT_ID.length()); 662 int sign = 1; 663 int hour = 0; 664 int min = 0; 665 int sec = 0; 666 667 if (id.charAt(pos.getIndex()) == 0x002D /*'-'*/) { 668 sign = -1; 669 } else if (id.charAt(pos.getIndex()) != 0x002B /*'+'*/) { 670 return false; 671 } 672 pos.setIndex(pos.getIndex() + 1); 673 674 numberFormat = NumberFormat.getInstance(); 675 numberFormat.setParseIntegerOnly(true); 676 677 // Look for either hh:mm, hhmm, or hh 678 int start = pos.getIndex(); 679 680 Number n = numberFormat.parse(id, pos); 681 if (pos.getIndex() == start) { 682 return false; 683 } 684 hour = n.intValue(); 685 686 if (pos.getIndex() < id.length()){ 687 if (pos.getIndex() - start > 2 688 || id.charAt(pos.getIndex()) != 0x003A /*':'*/) { 689 return false; 690 } 691 // hh:mm 692 pos.setIndex(pos.getIndex() + 1); 693 int oldPos = pos.getIndex(); 694 n = numberFormat.parse(id, pos); 695 if ((pos.getIndex() - oldPos) != 2) { 696 // must be 2 digits 697 return false; 698 } 699 min = n.intValue(); 700 if (pos.getIndex() < id.length()) { 701 if (id.charAt(pos.getIndex()) != 0x003A /*':'*/) { 702 return false; 703 } 704 // [:ss] 705 pos.setIndex(pos.getIndex() + 1); 706 oldPos = pos.getIndex(); 707 n = numberFormat.parse(id, pos); 708 if (pos.getIndex() != id.length() 709 || (pos.getIndex() - oldPos) != 2) { 710 return false; 711 } 712 sec = n.intValue(); 713 } 714 } else { 715 // Supported formats are below - 716 // 717 // HHmmss 718 // Hmmss 719 // HHmm 720 // Hmm 721 // HH 722 // H 723 724 int length = pos.getIndex() - start; 725 if (length <= 0 || 6 < length) { 726 // invalid length 727 return false; 728 } 729 switch (length) { 730 case 1: 731 case 2: 732 // already set to hour 733 break; 734 case 3: 735 case 4: 736 min = hour % 100; 737 hour /= 100; 738 break; 739 case 5: 740 case 6: 741 sec = hour % 100; 742 min = (hour/100) % 100; 743 hour /= 10000; 744 break; 745 } 746 } 747 748 if (hour <= kMAX_CUSTOM_HOUR && min <= kMAX_CUSTOM_MIN && sec <= kMAX_CUSTOM_SEC) { 749 if (fields != null) { 750 if (fields.length >= 1) { 751 fields[0] = sign; 752 } 753 if (fields.length >= 2) { 754 fields[1] = hour; 755 } 756 if (fields.length >= 3) { 757 fields[2] = min; 758 } 759 if (fields.length >= 4) { 760 fields[3] = sec; 761 } 762 } 763 return true; 764 } 765 } 766 return false; 767 } 768 769 /** 770 * Creates a custom zone for the offset 771 * @param offset GMT offset in milliseconds 772 * @return A custom TimeZone for the offset with normalized time zone id 773 */ getCustomTimeZone(int offset)774 public static SimpleTimeZone getCustomTimeZone(int offset) { 775 boolean negative = false; 776 int tmp = offset; 777 if (offset < 0) { 778 negative = true; 779 tmp = -offset; 780 } 781 782 int hour, min, sec; 783 784 if (ASSERT) { 785 Assert.assrt("millis!=0", tmp % 1000 != 0); 786 } 787 tmp /= 1000; 788 sec = tmp % 60; 789 tmp /= 60; 790 min = tmp % 60; 791 hour = tmp / 60; 792 793 // Note: No millisecond part included in TZID for now 794 String zid = formatCustomID(hour, min, sec, negative); 795 796 return new SimpleTimeZone(offset, zid); 797 } 798 799 /* 800 * Returns the normalized custom TimeZone ID 801 */ formatCustomID(int hour, int min, int sec, boolean negative)802 static String formatCustomID(int hour, int min, int sec, boolean negative) { 803 // Create normalized time zone ID - GMT[+|-]hh:mm[:ss] 804 StringBuilder zid = new StringBuilder(kCUSTOM_TZ_PREFIX); 805 if (hour != 0 || min != 0) { 806 if(negative) { 807 zid.append('-'); 808 } else { 809 zid.append('+'); 810 } 811 // Always use US-ASCII digits 812 if (hour < 10) { 813 zid.append('0'); 814 } 815 zid.append(hour); 816 zid.append(':'); 817 if (min < 10) { 818 zid.append('0'); 819 } 820 zid.append(min); 821 822 if (sec != 0) { 823 // Optional second field 824 zid.append(':'); 825 if (sec < 10) { 826 zid.append('0'); 827 } 828 zid.append(sec); 829 } 830 } 831 return zid.toString(); 832 } 833 834 /** 835 * Returns the time zone's short ID for the zone. 836 * For example, "uslax" for zone "America/Los_Angeles". 837 * @param tz the time zone 838 * @return the short ID of the time zone, or null if the short ID is not available. 839 */ getShortID(TimeZone tz)840 public static String getShortID(TimeZone tz) { 841 String canonicalID = null; 842 843 if (tz instanceof OlsonTimeZone) { 844 canonicalID = ((OlsonTimeZone)tz).getCanonicalID(); 845 } 846 else { 847 canonicalID = getCanonicalCLDRID(tz.getID()); 848 } 849 if (canonicalID == null) { 850 return null; 851 } 852 return getShortIDFromCanonical(canonicalID); 853 } 854 855 /** 856 * Returns the time zone's short ID for the zone ID. 857 * For example, "uslax" for zone ID "America/Los_Angeles". 858 * @param id the time zone ID 859 * @return the short ID of the time zone ID, or null if the short ID is not available. 860 */ getShortID(String id)861 public static String getShortID(String id) { 862 String canonicalID = getCanonicalCLDRID(id); 863 if (canonicalID == null) { 864 return null; 865 } 866 return getShortIDFromCanonical(canonicalID); 867 } 868 getShortIDFromCanonical(String canonicalID)869 private static String getShortIDFromCanonical(String canonicalID) { 870 String shortID = null; 871 String tzidKey = canonicalID.replace('/', ':'); 872 873 try { 874 // First, try check if the given ID is canonical 875 UResourceBundle keyTypeData = UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, 876 "keyTypeData", ICUResourceBundle.ICU_DATA_CLASS_LOADER); 877 UResourceBundle typeMap = keyTypeData.get("typeMap"); 878 UResourceBundle typeKeys = typeMap.get("timezone"); 879 shortID = typeKeys.getString(tzidKey); 880 } catch (MissingResourceException e) { 881 // fall through 882 } 883 884 return shortID; 885 } 886 887 } 888