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