1 /* 2 ******************************************************************************* 3 * Copyright (C) 2011-2014, International Business Machines Corporation and * 4 * others. All Rights Reserved. * 5 ******************************************************************************* 6 */ 7 package com.ibm.icu.text; 8 9 import java.io.IOException; 10 import java.io.InvalidObjectException; 11 import java.io.ObjectInputStream; 12 import java.io.ObjectOutputStream; 13 import java.io.ObjectStreamField; 14 import java.io.Serializable; 15 import java.text.AttributedCharacterIterator; 16 import java.text.AttributedString; 17 import java.text.FieldPosition; 18 import java.text.ParseException; 19 import java.text.ParsePosition; 20 import java.util.ArrayList; 21 import java.util.BitSet; 22 import java.util.Collection; 23 import java.util.Date; 24 import java.util.EnumSet; 25 import java.util.Iterator; 26 import java.util.List; 27 import java.util.Locale; 28 import java.util.MissingResourceException; 29 import java.util.Set; 30 31 import com.ibm.icu.impl.ICUResourceBundle; 32 import com.ibm.icu.impl.SoftCache; 33 import com.ibm.icu.impl.TZDBTimeZoneNames; 34 import com.ibm.icu.impl.TextTrieMap; 35 import com.ibm.icu.impl.TimeZoneGenericNames; 36 import com.ibm.icu.impl.TimeZoneGenericNames.GenericMatchInfo; 37 import com.ibm.icu.impl.TimeZoneGenericNames.GenericNameType; 38 import com.ibm.icu.impl.TimeZoneNamesImpl; 39 import com.ibm.icu.impl.ZoneMeta; 40 import com.ibm.icu.lang.UCharacter; 41 import com.ibm.icu.text.TimeZoneNames.MatchInfo; 42 import com.ibm.icu.text.TimeZoneNames.NameType; 43 import com.ibm.icu.util.Calendar; 44 import com.ibm.icu.util.Freezable; 45 import com.ibm.icu.util.Output; 46 import com.ibm.icu.util.TimeZone; 47 import com.ibm.icu.util.TimeZone.SystemTimeZoneType; 48 import com.ibm.icu.util.ULocale; 49 50 /** 51 * <code>TimeZoneFormat</code> supports time zone display name formatting and parsing. 52 * An instance of TimeZoneFormat works as a subformatter of {@link SimpleDateFormat}, 53 * but you can also directly get a new instance of <code>TimeZoneFormat</code> and 54 * formatting/parsing time zone display names. 55 * <p> 56 * ICU implements the time zone display names defined by <a href="http://www.unicode.org/reports/tr35/">UTS#35 57 * Unicode Locale Data Markup Language (LDML)</a>. {@link TimeZoneNames} represents the 58 * time zone display name data model and this class implements the algorithm for actual 59 * formatting and parsing. 60 * 61 * @see SimpleDateFormat 62 * @see TimeZoneNames 63 * @stable ICU 49 64 */ 65 public class TimeZoneFormat extends UFormat implements Freezable<TimeZoneFormat>, Serializable { 66 67 private static final long serialVersionUID = 2281246852693575022L; 68 69 private static final int ISO_Z_STYLE_FLAG = 0x0080; 70 private static final int ISO_LOCAL_STYLE_FLAG = 0x0100; 71 72 /** 73 * Time zone display format style enum used by format/parse APIs in <code>TimeZoneFormat</code>. 74 * 75 * @see TimeZoneFormat#format(Style, TimeZone, long) 76 * @see TimeZoneFormat#format(Style, TimeZone, long, Output) 77 * @see TimeZoneFormat#parse(Style, String, ParsePosition, Output) 78 * @stable ICU 49 79 */ 80 public enum Style { 81 /** 82 * Generic location format, such as "United States Time (New York)" and "Italy Time". 83 * This style is equivalent to the LDML date format pattern "VVVV". 84 * @stable ICU 49 85 */ 86 GENERIC_LOCATION (0x0001), 87 /** 88 * Generic long non-location format, such as "Eastern Time". 89 * This style is equivalent to the LDML date format pattern "vvvv". 90 * @stable ICU 49 91 */ 92 GENERIC_LONG (0x0002), 93 /** 94 * Generic short non-location format, such as "ET". 95 * This style is equivalent to the LDML date format pattern "v". 96 * @stable ICU 49 97 */ 98 GENERIC_SHORT (0x0004), 99 /** 100 * Specific long format, such as "Eastern Standard Time". 101 * This style is equivalent to the LDML date format pattern "zzzz". 102 * @stable ICU 49 103 */ 104 SPECIFIC_LONG (0x0008), 105 /** 106 * Specific short format, such as "EST", "PDT". 107 * This style is equivalent to the LDML date format pattern "z". 108 * @stable ICU 49 109 */ 110 SPECIFIC_SHORT (0x0010), 111 /** 112 * Localized GMT offset format, such as "GMT-05:00", "UTC+0100" 113 * This style is equivalent to the LDML date format pattern "OOOO" and "ZZZZ" 114 * @stable ICU 49 115 */ 116 LOCALIZED_GMT (0x0020), 117 /** 118 * Short localized GMT offset format, such as "GMT-5", "UTC+1:30" 119 * This style is equivalent to the LDML date format pattern "O". 120 * @stable ICU 51 121 */ 122 LOCALIZED_GMT_SHORT (0x0040), 123 /** 124 * Short ISO 8601 local time difference (basic format) or the UTC indicator. 125 * For example, "-05", "+0530", and "Z"(UTC). 126 * This style is equivalent to the LDML date format pattern "X". 127 * @stable ICU 51 128 */ 129 ISO_BASIC_SHORT (ISO_Z_STYLE_FLAG), 130 /** 131 * Short ISO 8601 locale time difference (basic format). 132 * For example, "-05" and "+0530". 133 * This style is equivalent to the LDML date format pattern "x". 134 * @stable ICU 51 135 */ 136 ISO_BASIC_LOCAL_SHORT (ISO_LOCAL_STYLE_FLAG), 137 /** 138 * Fixed width ISO 8601 local time difference (basic format) or the UTC indicator. 139 * For example, "-0500", "+0530", and "Z"(UTC). 140 * This style is equivalent to the LDML date format pattern "XX". 141 * @stable ICU 51 142 */ 143 ISO_BASIC_FIXED (ISO_Z_STYLE_FLAG), 144 /** 145 * Fixed width ISO 8601 local time difference (basic format). 146 * For example, "-0500" and "+0530". 147 * This style is equivalent to the LDML date format pattern "xx". 148 * @stable ICU 51 149 */ 150 ISO_BASIC_LOCAL_FIXED (ISO_LOCAL_STYLE_FLAG), 151 /** 152 * ISO 8601 local time difference (basic format) with optional seconds field, or the UTC indicator. 153 * For example, "-0500", "+052538", and "Z"(UTC). 154 * This style is equivalent to the LDML date format pattern "XXXX". 155 * @stable ICU 51 156 */ 157 ISO_BASIC_FULL (ISO_Z_STYLE_FLAG), 158 /** 159 * ISO 8601 local time difference (basic format) with optional seconds field. 160 * For example, "-0500" and "+052538". 161 * This style is equivalent to the LDML date format pattern "xxxx". 162 * @stable ICU 51 163 */ 164 ISO_BASIC_LOCAL_FULL (ISO_LOCAL_STYLE_FLAG), 165 /** 166 * Fixed width ISO 8601 local time difference (extended format) or the UTC indicator. 167 * For example, "-05:00", "+05:30", and "Z"(UTC). 168 * This style is equivalent to the LDML date format pattern "XXX". 169 * @stable ICU 51 170 */ 171 ISO_EXTENDED_FIXED (ISO_Z_STYLE_FLAG), 172 /** 173 * Fixed width ISO 8601 local time difference (extended format). 174 * For example, "-05:00" and "+05:30". 175 * This style is equivalent to the LDML date format pattern "xxx" and "ZZZZZ". 176 * @stable ICU 51 177 */ 178 ISO_EXTENDED_LOCAL_FIXED (ISO_LOCAL_STYLE_FLAG), 179 /** 180 * ISO 8601 local time difference (extended format) with optional seconds field, or the UTC indicator. 181 * For example, "-05:00", "+05:25:38", and "Z"(UTC). 182 * This style is equivalent to the LDML date format pattern "XXXXX". 183 * @stable ICU 51 184 */ 185 ISO_EXTENDED_FULL (ISO_Z_STYLE_FLAG), 186 /** 187 * ISO 8601 local time difference (extended format) with optional seconds field. 188 * For example, "-05:00" and "+05:25:38". 189 * This style is equivalent to the LDML date format pattern "xxxxx". 190 * @stable ICU 51 191 */ 192 ISO_EXTENDED_LOCAL_FULL (ISO_LOCAL_STYLE_FLAG), 193 /** 194 * Time Zone ID, such as "America/Los_Angeles". 195 * @stable ICU 51 196 */ 197 ZONE_ID (0x0200), 198 /** 199 * Short Time Zone ID (BCP 47 Unicode location extension, time zone type value), such as "uslax". 200 * @stable ICU 51 201 */ 202 ZONE_ID_SHORT (0x0400), 203 /** 204 * Exemplar location, such as "Los Angeles" and "Paris". 205 * @stable ICU 51 206 */ 207 EXEMPLAR_LOCATION (0x0800); 208 209 final int flag; 210 Style(int flag)211 private Style(int flag) { 212 this.flag = flag; 213 } 214 } 215 216 /** 217 * Offset pattern type enum. 218 * 219 * @see TimeZoneFormat#getGMTOffsetPattern(GMTOffsetPatternType) 220 * @see TimeZoneFormat#setGMTOffsetPattern(GMTOffsetPatternType, String) 221 * @stable ICU 49 222 */ 223 public enum GMTOffsetPatternType { 224 /** 225 * Positive offset with hours and minutes fields 226 * @stable ICU 49 227 */ 228 POSITIVE_HM ("+H:mm", "Hm", true), 229 /** 230 * Positive offset with hours, minutes and seconds fields 231 * @stable ICU 49 232 */ 233 POSITIVE_HMS ("+H:mm:ss", "Hms", true), 234 /** 235 * Negative offset with hours and minutes fields 236 * @stable ICU 49 237 */ 238 NEGATIVE_HM ("-H:mm", "Hm", false), 239 /** 240 * Negative offset with hours, minutes and seconds fields 241 * @stable ICU 49 242 */ 243 NEGATIVE_HMS ("-H:mm:ss", "Hms", false), 244 /** 245 * Positive offset with hours field 246 * @stable ICU 51 247 */ 248 POSITIVE_H ("+H", "H", true), 249 /** 250 * Negative offset with hours field 251 * @stable ICU 51 252 */ 253 NEGATIVE_H ("-H", "H", false); 254 255 private String _defaultPattern; 256 private String _required; 257 private boolean _isPositive; 258 GMTOffsetPatternType(String defaultPattern, String required, boolean isPositive)259 private GMTOffsetPatternType(String defaultPattern, String required, boolean isPositive) { 260 _defaultPattern = defaultPattern; 261 _required = required; 262 _isPositive = isPositive; 263 } 264 defaultPattern()265 private String defaultPattern() { 266 return _defaultPattern; 267 } 268 required()269 private String required() { 270 return _required; 271 } 272 isPositive()273 private boolean isPositive() { 274 return _isPositive; 275 } 276 } 277 278 /** 279 * Time type enum used for receiving time type (standard time, daylight time or unknown) 280 * in <code>TimeZoneFormat</code> APIs. 281 * 282 * @stable ICU 49 283 */ 284 public enum TimeType { 285 /** 286 * Unknown 287 * @stable ICU 49 288 */ 289 UNKNOWN, 290 /** 291 * Standard time 292 * @stable ICU 49 293 */ 294 STANDARD, 295 /** 296 * Daylight saving time 297 * @stable ICU 49 298 */ 299 DAYLIGHT; 300 } 301 302 /** 303 * Parse option enum, used for specifying optional parse behavior. 304 * @stable ICU 49 305 */ 306 public enum ParseOption { 307 /** 308 * When a time zone display name is not found within a set of display names 309 * used for the specified style, look for the name from display names used 310 * by other styles. 311 * @stable ICU 49 312 */ 313 ALL_STYLES, 314 /** 315 * When parsing a time zone display name in {@link Style#SPECIFIC_SHORT}, 316 * look for the IANA tz database compatible zone abbreviations in addition 317 * to the localized names coming from the {@link TimeZoneNames} currently 318 * used by the {@link TimeZoneFormat}. 319 * @draft ICU 54 320 * @provisional This API might change or be removed in a future release. 321 */ 322 TZ_DATABASE_ABBREVIATIONS; 323 } 324 325 /* 326 * fields to be serialized 327 */ 328 private ULocale _locale; 329 private TimeZoneNames _tznames; 330 private String _gmtPattern; 331 private String[] _gmtOffsetPatterns; 332 private String[] _gmtOffsetDigits; 333 private String _gmtZeroFormat; 334 private boolean _parseAllStyles; 335 private boolean _parseTZDBNames; 336 337 /* 338 * Transient fields 339 */ 340 private transient volatile TimeZoneGenericNames _gnames; 341 342 private transient String _gmtPatternPrefix; 343 private transient String _gmtPatternSuffix; 344 private transient Object[][] _gmtOffsetPatternItems; 345 // cache if offset hours and minutes are abutting 346 private transient boolean _abuttingOffsetHoursAndMinutes; 347 348 private transient String _region; 349 350 private volatile transient boolean _frozen; 351 352 private transient volatile TimeZoneNames _tzdbNames; 353 354 /* 355 * Static final fields 356 */ 357 private static final String TZID_GMT = "Etc/GMT"; // canonical tzid for GMT 358 359 private static final String[] ALT_GMT_STRINGS = {"GMT", "UTC", "UT"}; 360 361 private static final String DEFAULT_GMT_PATTERN = "GMT{0}"; 362 private static final String DEFAULT_GMT_ZERO = "GMT"; 363 private static final String[] DEFAULT_GMT_DIGITS = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}; 364 private static final char DEFAULT_GMT_OFFSET_SEP = ':'; 365 private static final String ASCII_DIGITS = "0123456789"; 366 private static final String ISO8601_UTC = "Z"; 367 368 private static final String UNKNOWN_ZONE_ID = "Etc/Unknown"; 369 private static final String UNKNOWN_SHORT_ZONE_ID = "unk"; 370 private static final String UNKNOWN_LOCATION = "Unknown"; 371 372 // Order of GMT offset pattern parsing, *_HMS must be evaluated first 373 // because *_HM is most likely a substring of *_HMS 374 private static final GMTOffsetPatternType[] PARSE_GMT_OFFSET_TYPES = { 375 GMTOffsetPatternType.POSITIVE_HMS, GMTOffsetPatternType.NEGATIVE_HMS, 376 GMTOffsetPatternType.POSITIVE_HM, GMTOffsetPatternType.NEGATIVE_HM, 377 GMTOffsetPatternType.POSITIVE_H, GMTOffsetPatternType.NEGATIVE_H, 378 }; 379 380 private static final int MILLIS_PER_HOUR = 60 * 60 * 1000; 381 private static final int MILLIS_PER_MINUTE = 60 * 1000; 382 private static final int MILLIS_PER_SECOND = 1000; 383 384 // Maximum offset (exclusive) in millisecond supported by offset formats 385 private static final int MAX_OFFSET = 24 * MILLIS_PER_HOUR; 386 387 // Maximum values for GMT offset fields 388 private static final int MAX_OFFSET_HOUR = 23; 389 private static final int MAX_OFFSET_MINUTE = 59; 390 private static final int MAX_OFFSET_SECOND = 59; 391 392 private static final int UNKNOWN_OFFSET = Integer.MAX_VALUE; 393 394 private static TimeZoneFormatCache _tzfCache = new TimeZoneFormatCache(); 395 396 // The filter used for searching all specific names and exemplar location names 397 private static final EnumSet<NameType> ALL_SIMPLE_NAME_TYPES = EnumSet.of( 398 NameType.LONG_STANDARD, NameType.LONG_DAYLIGHT, 399 NameType.SHORT_STANDARD, NameType.SHORT_DAYLIGHT, 400 NameType.EXEMPLAR_LOCATION 401 ); 402 403 // The filter used for searching all generic names 404 private static final EnumSet<GenericNameType> ALL_GENERIC_NAME_TYPES = EnumSet.of( 405 GenericNameType.LOCATION, GenericNameType.LONG, GenericNameType.SHORT 406 ); 407 408 private static volatile TextTrieMap<String> ZONE_ID_TRIE; 409 private static volatile TextTrieMap<String> SHORT_ZONE_ID_TRIE; 410 411 /** 412 * The protected constructor for subclassing. 413 * @param locale the locale 414 * @stable ICU 49 415 */ TimeZoneFormat(ULocale locale)416 protected TimeZoneFormat(ULocale locale) { 417 _locale = locale; 418 _tznames = TimeZoneNames.getInstance(locale); 419 // TimeZoneGenericNames _gnames will be instantiated lazily 420 421 String gmtPattern = null; 422 String hourFormats = null; 423 _gmtZeroFormat = DEFAULT_GMT_ZERO; 424 425 try { 426 ICUResourceBundle bundle = (ICUResourceBundle) ICUResourceBundle.getBundleInstance( 427 ICUResourceBundle.ICU_ZONE_BASE_NAME, locale); 428 try { 429 gmtPattern = bundle.getStringWithFallback("zoneStrings/gmtFormat"); 430 } catch (MissingResourceException e) { 431 // fall through 432 } 433 try { 434 hourFormats = bundle.getStringWithFallback("zoneStrings/hourFormat"); 435 } catch (MissingResourceException e) { 436 // fall through 437 } 438 try { 439 _gmtZeroFormat = bundle.getStringWithFallback("zoneStrings/gmtZeroFormat"); 440 } catch (MissingResourceException e) { 441 // fall through 442 } 443 } catch (MissingResourceException e) { 444 // fall through 445 } 446 447 if (gmtPattern == null) { 448 gmtPattern = DEFAULT_GMT_PATTERN; 449 } 450 initGMTPattern(gmtPattern); 451 452 String[] gmtOffsetPatterns = new String[GMTOffsetPatternType.values().length]; 453 if (hourFormats != null) { 454 String[] hourPatterns = hourFormats.split(";", 2); 455 gmtOffsetPatterns[GMTOffsetPatternType.POSITIVE_H.ordinal()] = truncateOffsetPattern(hourPatterns[0]); 456 gmtOffsetPatterns[GMTOffsetPatternType.POSITIVE_HM.ordinal()] = hourPatterns[0]; 457 gmtOffsetPatterns[GMTOffsetPatternType.POSITIVE_HMS.ordinal()] = expandOffsetPattern(hourPatterns[0]); 458 gmtOffsetPatterns[GMTOffsetPatternType.NEGATIVE_H.ordinal()] = truncateOffsetPattern(hourPatterns[1]); 459 gmtOffsetPatterns[GMTOffsetPatternType.NEGATIVE_HM.ordinal()] = hourPatterns[1]; 460 gmtOffsetPatterns[GMTOffsetPatternType.NEGATIVE_HMS.ordinal()] = expandOffsetPattern(hourPatterns[1]); 461 } else { 462 for (GMTOffsetPatternType patType : GMTOffsetPatternType.values()) { 463 gmtOffsetPatterns[patType.ordinal()] = patType.defaultPattern(); 464 } 465 } 466 initGMTOffsetPatterns(gmtOffsetPatterns); 467 468 _gmtOffsetDigits = DEFAULT_GMT_DIGITS; 469 NumberingSystem ns = NumberingSystem.getInstance(locale); 470 if (!ns.isAlgorithmic()) { 471 // we do not support algorithmic numbering system for GMT offset for now 472 _gmtOffsetDigits = toCodePoints(ns.getDescription()); 473 } 474 } 475 476 /** 477 * Returns a frozen instance of <code>TimeZoneFormat</code> for the given locale. 478 * <p><b>Note</b>: The instance returned by this method is frozen. If you want to 479 * customize a TimeZoneFormat, you must use {@link #cloneAsThawed()} to get a 480 * thawed copy first. 481 * 482 * @param locale the locale. 483 * @return a frozen instance of <code>TimeZoneFormat</code> for the given locale. 484 * @stable ICU 49 485 */ getInstance(ULocale locale)486 public static TimeZoneFormat getInstance(ULocale locale) { 487 if (locale == null) { 488 throw new NullPointerException("locale is null"); 489 } 490 return _tzfCache.getInstance(locale, locale); 491 } 492 493 /** 494 * Returns a frozen instance of <code>TimeZoneFormat</code> for the given JDK locale. 495 * <p><b>Note</b>: The instance returned by this method is frozen. If you want to 496 * customize a TimeZoneFormat, you must use {@link #cloneAsThawed()} to get a 497 * thawed copy first. 498 * 499 * @param locale the JDK locale. 500 * @return a frozen instance of <code>TimeZoneFormat</code> for the given locale. 501 * @draft ICU 54 502 * @provisional This API might change or be removed in a future release. 503 */ getInstance(Locale locale)504 public static TimeZoneFormat getInstance(Locale locale) { 505 return getInstance(ULocale.forLocale(locale)); 506 } 507 508 /** 509 * Returns the time zone display name data used by this instance. 510 * 511 * @return the time zone display name data. 512 * @see #setTimeZoneNames(TimeZoneNames) 513 * @stable ICU 49 514 */ getTimeZoneNames()515 public TimeZoneNames getTimeZoneNames() { 516 return _tznames; 517 } 518 519 /** 520 * Private method returning the instance of TimeZoneGenericNames 521 * used by this object. The instance of TimeZoneGenericNames might 522 * not be available until the first use (lazy instantiation) because 523 * it is only required for handling generic names (that are not used 524 * by DateFormat's default patterns) and it requires relatively heavy 525 * one time initialization. 526 * @return the instance of TimeZoneGenericNames used by this object. 527 */ getTimeZoneGenericNames()528 private TimeZoneGenericNames getTimeZoneGenericNames() { 529 if (_gnames == null) { // _gnames is volatile 530 synchronized(this) { 531 if (_gnames == null) { 532 _gnames = TimeZoneGenericNames.getInstance(_locale); 533 } 534 } 535 } 536 return _gnames; 537 } 538 539 /** 540 * Private method returning the instance of TZDBTimeZoneNames. 541 * The instance if used only for parsing when {@link ParseOption#TZ_DATABASE_ABBREVIATIONS} 542 * is enabled. 543 * @return an instance of TZDBTimeZoneNames. 544 */ getTZDBTimeZoneNames()545 private TimeZoneNames getTZDBTimeZoneNames() { 546 if (_tzdbNames == null) { 547 synchronized(this) { 548 if (_tzdbNames == null) { 549 _tzdbNames = new TZDBTimeZoneNames(_locale); 550 } 551 } 552 } 553 return _tzdbNames; 554 } 555 556 /** 557 * Sets the time zone display name data to this instance. 558 * 559 * @param tznames the time zone display name data. 560 * @return this object. 561 * @throws UnsupportedOperationException when this object is frozen. 562 * @see #getTimeZoneNames() 563 * @stable ICU 49 564 */ setTimeZoneNames(TimeZoneNames tznames)565 public TimeZoneFormat setTimeZoneNames(TimeZoneNames tznames) { 566 if (isFrozen()) { 567 throw new UnsupportedOperationException("Attempt to modify frozen object"); 568 } 569 _tznames = tznames; 570 // TimeZoneGenericNames must be changed to utilize the new TimeZoneNames instance. 571 _gnames = new TimeZoneGenericNames(_locale, _tznames); 572 return this; 573 } 574 575 /** 576 * Returns the localized GMT format pattern. 577 * 578 * @return the localized GMT format pattern. 579 * @see #setGMTPattern(String) 580 * @stable ICU 49 581 */ getGMTPattern()582 public String getGMTPattern() { 583 return _gmtPattern; 584 } 585 586 /** 587 * Sets the localized GMT format pattern. The pattern must contain 588 * a single argument {0}, for example "GMT {0}". 589 * 590 * @param pattern the localized GMT format pattern string 591 * @return this object. 592 * @throws IllegalArgumentException when the pattern string does not contain "{0}" 593 * @throws UnsupportedOperationException when this object is frozen. 594 * @see #getGMTPattern() 595 * @stable ICU 49 596 */ setGMTPattern(String pattern)597 public TimeZoneFormat setGMTPattern(String pattern) { 598 if (isFrozen()) { 599 throw new UnsupportedOperationException("Attempt to modify frozen object"); 600 } 601 initGMTPattern(pattern); 602 return this; 603 } 604 605 /** 606 * Returns the offset pattern used for localized GMT format. 607 * 608 * @param type the offset pattern enum 609 * @see #setGMTOffsetPattern(GMTOffsetPatternType, String) 610 * @stable ICU 49 611 */ getGMTOffsetPattern(GMTOffsetPatternType type)612 public String getGMTOffsetPattern(GMTOffsetPatternType type) { 613 return _gmtOffsetPatterns[type.ordinal()]; 614 } 615 616 /** 617 * Sets the offset pattern for the given offset type. 618 * 619 * @param type the offset pattern. 620 * @param pattern the pattern string. 621 * @return this object. 622 * @throws IllegalArgumentException when the pattern string does not have required time field letters. 623 * @throws UnsupportedOperationException when this object is frozen. 624 * @see #getGMTOffsetPattern(GMTOffsetPatternType) 625 * @stable ICU 49 626 */ setGMTOffsetPattern(GMTOffsetPatternType type, String pattern)627 public TimeZoneFormat setGMTOffsetPattern(GMTOffsetPatternType type, String pattern) { 628 if (isFrozen()) { 629 throw new UnsupportedOperationException("Attempt to modify frozen object"); 630 } 631 if (pattern == null) { 632 throw new NullPointerException("Null GMT offset pattern"); 633 } 634 635 Object[] parsedItems = parseOffsetPattern(pattern, type.required()); 636 637 _gmtOffsetPatterns[type.ordinal()] = pattern; 638 _gmtOffsetPatternItems[type.ordinal()] = parsedItems; 639 checkAbuttingHoursAndMinutes(); 640 641 return this; 642 } 643 644 /** 645 * Returns the decimal digit characters used for localized GMT format in a single string 646 * containing from 0 to 9 in the ascending order. 647 * 648 * @return the decimal digits for localized GMT format. 649 * @see #setGMTOffsetDigits(String) 650 * @stable ICU 49 651 */ getGMTOffsetDigits()652 public String getGMTOffsetDigits() { 653 StringBuilder buf = new StringBuilder(_gmtOffsetDigits.length); 654 for (String digit : _gmtOffsetDigits) { 655 buf.append(digit); 656 } 657 return buf.toString(); 658 } 659 660 /** 661 * Sets the decimal digit characters used for localized GMT format. 662 * 663 * @param digits a string contains the decimal digit characters from 0 to 9 n the ascending order. 664 * @return this object. 665 * @throws IllegalArgumentException when the string did not contain ten characters. 666 * @throws UnsupportedOperationException when this object is frozen. 667 * @see #getGMTOffsetDigits() 668 * @stable ICU 49 669 */ setGMTOffsetDigits(String digits)670 public TimeZoneFormat setGMTOffsetDigits(String digits) { 671 if (isFrozen()) { 672 throw new UnsupportedOperationException("Attempt to modify frozen object"); 673 } 674 if (digits == null) { 675 throw new NullPointerException("Null GMT offset digits"); 676 } 677 String[] digitArray = toCodePoints(digits); 678 if (digitArray.length != 10) { 679 throw new IllegalArgumentException("Length of digits must be 10"); 680 } 681 _gmtOffsetDigits = digitArray; 682 return this; 683 } 684 685 /** 686 * Returns the localized GMT format string for GMT(UTC) itself (GMT offset is 0). 687 * 688 * @return the localized GMT string string for GMT(UTC) itself. 689 * @see #setGMTZeroFormat(String) 690 * @stable ICU 49 691 */ getGMTZeroFormat()692 public String getGMTZeroFormat() { 693 return _gmtZeroFormat; 694 } 695 696 /** 697 * Sets the localized GMT format string for GMT(UTC) itself (GMT offset is 0). 698 * 699 * @param gmtZeroFormat the localized GMT format string for GMT(UTC). 700 * @return this object. 701 * @throws UnsupportedOperationException when this object is frozen. 702 * @see #getGMTZeroFormat() 703 * @stable ICU 49 704 */ setGMTZeroFormat(String gmtZeroFormat)705 public TimeZoneFormat setGMTZeroFormat(String gmtZeroFormat) { 706 if (isFrozen()) { 707 throw new UnsupportedOperationException("Attempt to modify frozen object"); 708 } 709 if (gmtZeroFormat == null) { 710 throw new NullPointerException("Null GMT zero format"); 711 } 712 if (gmtZeroFormat.length() == 0) { 713 throw new IllegalArgumentException("Empty GMT zero format"); 714 } 715 _gmtZeroFormat = gmtZeroFormat; 716 return this; 717 } 718 719 /** 720 * Sets the default parse options. 721 * <p> 722 * <b>Note:</b> By default, an instance of <code>TimeZoneFormat></code> 723 * created by {#link {@link #getInstance(ULocale)} has no parse options set. 724 * 725 * @param options the default parse options. 726 * @return this object. 727 * @see ParseOption 728 * @stable ICU 49 729 */ setDefaultParseOptions(EnumSet<ParseOption> options)730 public TimeZoneFormat setDefaultParseOptions(EnumSet<ParseOption> options) { 731 _parseAllStyles = options.contains(ParseOption.ALL_STYLES); 732 _parseTZDBNames = options.contains(ParseOption.TZ_DATABASE_ABBREVIATIONS); 733 return this; 734 } 735 736 /** 737 * Returns the default parse options used by this <code>TimeZoneFormat</code> instance. 738 * @return the default parse options. 739 * @see ParseOption 740 * @stable ICU 49 741 */ getDefaultParseOptions()742 public EnumSet<ParseOption> getDefaultParseOptions() { 743 if (_parseAllStyles && _parseTZDBNames) { 744 return EnumSet.of(ParseOption.ALL_STYLES, ParseOption.TZ_DATABASE_ABBREVIATIONS); 745 } else if (_parseAllStyles) { 746 return EnumSet.of(ParseOption.ALL_STYLES); 747 } else if (_parseTZDBNames) { 748 return EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS); 749 } 750 return EnumSet.noneOf(ParseOption.class); 751 } 752 753 /** 754 * Returns the ISO 8601 basic time zone string for the given offset. 755 * For example, "-08", "-0830" and "Z" 756 * 757 * @param offset the offset from GMT(UTC) in milliseconds. 758 * @param useUtcIndicator true if ISO 8601 UTC indicator "Z" is used when the offset is 0. 759 * @param isShort true if shortest form is used. 760 * @param ignoreSeconds true if non-zero offset seconds is appended. 761 * @return the ISO 8601 basic format. 762 * @throws IllegalArgumentException if the specified offset is out of supported range 763 * (-24 hours < offset < +24 hours). 764 * @see #formatOffsetISO8601Extended(int, boolean, boolean, boolean) 765 * @see #parseOffsetISO8601(String, ParsePosition) 766 * @stable ICU 51 767 */ formatOffsetISO8601Basic(int offset, boolean useUtcIndicator, boolean isShort, boolean ignoreSeconds)768 public final String formatOffsetISO8601Basic(int offset, boolean useUtcIndicator, boolean isShort, boolean ignoreSeconds) { 769 return formatOffsetISO8601(offset, true, useUtcIndicator, isShort, ignoreSeconds); 770 } 771 772 /** 773 * Returns the ISO 8601 extended time zone string for the given offset. 774 * For example, "-08:00", "-08:30" and "Z" 775 * 776 * @param offset the offset from GMT(UTC) in milliseconds. 777 * @param useUtcIndicator true if ISO 8601 UTC indicator "Z" is used when the offset is 0. 778 * @param isShort true if shortest form is used. 779 * @param ignoreSeconds true if non-zero offset seconds is appended. 780 * @return the ISO 8601 extended format. 781 * @throws IllegalArgumentException if the specified offset is out of supported range 782 * (-24 hours < offset < +24 hours). 783 * @see #formatOffsetISO8601Basic(int, boolean, boolean, boolean) 784 * @see #parseOffsetISO8601(String, ParsePosition) 785 * @stable ICU 51 786 */ formatOffsetISO8601Extended(int offset, boolean useUtcIndicator, boolean isShort, boolean ignoreSeconds)787 public final String formatOffsetISO8601Extended(int offset, boolean useUtcIndicator, boolean isShort, boolean ignoreSeconds) { 788 return formatOffsetISO8601(offset, false, useUtcIndicator, isShort, ignoreSeconds); 789 } 790 791 /** 792 * Returns the localized GMT(UTC) offset format for the given offset. 793 * The localized GMT offset is defined by; 794 * <ul> 795 * <li>GMT format pattern (e.g. "GMT {0}" - see {@link #getGMTPattern()}) 796 * <li>Offset time pattern (e.g. "+HH:mm" - see {@link #getGMTOffsetPattern(GMTOffsetPatternType)}) 797 * <li>Offset digits (e.g. "0123456789" - see {@link #getGMTOffsetDigits()}) 798 * <li>GMT zero format (e.g. "GMT" - see {@link #getGMTZeroFormat()}) 799 * </ul> 800 * This format always uses 2 digit hours and minutes. When the given offset has non-zero 801 * seconds, 2 digit seconds field will be appended. For example, 802 * GMT+05:00 and GMT+05:28:06. 803 * @param offset the offset from GMT(UTC) in milliseconds. 804 * @return the localized GMT format string 805 * @see #parseOffsetLocalizedGMT(String, ParsePosition) 806 * @throws IllegalArgumentException if the specified offset is out of supported range 807 * (-24 hours < offset < +24 hours). 808 * @stable ICU 49 809 */ formatOffsetLocalizedGMT(int offset)810 public String formatOffsetLocalizedGMT(int offset) { 811 return formatOffsetLocalizedGMT(offset, false); 812 } 813 814 /** 815 * Returns the short localized GMT(UTC) offset format for the given offset. 816 * The short localized GMT offset is defined by; 817 * <ul> 818 * <li>GMT format pattern (e.g. "GMT {0}" - see {@link #getGMTPattern()}) 819 * <li>Offset time pattern (e.g. "+HH:mm" - see {@link #getGMTOffsetPattern(GMTOffsetPatternType)}) 820 * <li>Offset digits (e.g. "0123456789" - see {@link #getGMTOffsetDigits()}) 821 * <li>GMT zero format (e.g. "GMT" - see {@link #getGMTZeroFormat()}) 822 * </ul> 823 * This format uses the shortest representation of offset. The hours field does not 824 * have leading zero and lower fields with zero will be truncated. For example, 825 * GMT+5 and GMT+530. 826 * @param offset the offset from GMT(UTC) in milliseconds. 827 * @return the short localized GMT format string 828 * @see #parseOffsetLocalizedGMT(String, ParsePosition) 829 * @throws IllegalArgumentException if the specified offset is out of supported range 830 * (-24 hours < offset < +24 hours). 831 * @stable ICU 51 832 */ formatOffsetShortLocalizedGMT(int offset)833 public String formatOffsetShortLocalizedGMT(int offset) { 834 return formatOffsetLocalizedGMT(offset, true); 835 } 836 837 /** 838 * Returns the display name of the time zone at the given date for 839 * the style. 840 * 841 * <p><b>Note</b>: A style may have fallback styles defined. For example, 842 * when <code>GENERIC_LONG</code> is requested, but there is no display name 843 * data available for <code>GENERIC_LONG</code> style, the implementation 844 * may use <code>GENERIC_LOCATION</code> or <code>LOCALIZED_GMT</code>. 845 * See UTS#35 UNICODE LOCALE DATA MARKUP LANGUAGE (LDML) 846 * <a href="http://www.unicode.org/reports/tr35/#Time_Zone_Fallback">Appendix J: Time Zone Display Name</a> 847 * for the details. 848 * 849 * @param style the style enum (e.g. <code>GENERIC_LONG</code>, <code>LOCALIZED_GMT</code>...) 850 * @param tz the time zone. 851 * @param date the date. 852 * @return the display name of the time zone. 853 * @see Style 854 * @see #format(Style, TimeZone, long, Output) 855 * @stable ICU 49 856 */ format(Style style, TimeZone tz, long date)857 public final String format(Style style, TimeZone tz, long date) { 858 return format(style, tz, date, null); 859 } 860 861 /** 862 * Returns the display name of the time zone at the given date for 863 * the style. This method takes an extra argument <code>Output<TimeType> timeType</code> 864 * in addition to the argument list of {@link #format(Style, TimeZone, long)}. 865 * The argument is used for receiving the time type (standard time 866 * or daylight saving time, or unknown) actually used for the display name. 867 * 868 * @param style the style enum (e.g. <code>GENERIC_LONG</code>, <code>LOCALIZED_GMT</code>...) 869 * @param tz the time zone. 870 * @param date the date. 871 * @param timeType the output argument for receiving the time type (standard/daylight/unknown) 872 * used for the display name, or specify null if the information is not necessary. 873 * @return the display name of the time zone. 874 * @see Style 875 * @see #format(Style, TimeZone, long) 876 * @stable ICU 49 877 */ format(Style style, TimeZone tz, long date, Output<TimeType> timeType)878 public String format(Style style, TimeZone tz, long date, Output<TimeType> timeType) { 879 String result = null; 880 881 if (timeType != null) { 882 timeType.value = TimeType.UNKNOWN; 883 } 884 885 boolean noOffsetFormatFallback = false; 886 887 switch (style) { 888 case GENERIC_LOCATION: 889 result = getTimeZoneGenericNames().getGenericLocationName(ZoneMeta.getCanonicalCLDRID(tz)); 890 break; 891 case GENERIC_LONG: 892 result = getTimeZoneGenericNames().getDisplayName(tz, GenericNameType.LONG, date); 893 break; 894 case GENERIC_SHORT: 895 result = getTimeZoneGenericNames().getDisplayName(tz, GenericNameType.SHORT, date); 896 break; 897 case SPECIFIC_LONG: 898 result = formatSpecific(tz, NameType.LONG_STANDARD, NameType.LONG_DAYLIGHT, date, timeType); 899 break; 900 case SPECIFIC_SHORT: 901 result = formatSpecific(tz, NameType.SHORT_STANDARD, NameType.SHORT_DAYLIGHT, date, timeType); 902 break; 903 904 case ZONE_ID: 905 result = tz.getID(); 906 noOffsetFormatFallback = true; 907 break; 908 case ZONE_ID_SHORT: 909 result = ZoneMeta.getShortID(tz); 910 if (result == null) { 911 result = UNKNOWN_SHORT_ZONE_ID; 912 } 913 noOffsetFormatFallback = true; 914 break; 915 case EXEMPLAR_LOCATION: 916 result = formatExemplarLocation(tz); 917 noOffsetFormatFallback = true; 918 break; 919 920 default: 921 // will be handled below 922 break; 923 } 924 925 if (result == null && !noOffsetFormatFallback) { 926 int[] offsets = {0, 0}; 927 tz.getOffset(date, false, offsets); 928 int offset = offsets[0] + offsets[1]; 929 930 switch (style) { 931 case GENERIC_LOCATION: 932 case GENERIC_LONG: 933 case SPECIFIC_LONG: 934 case LOCALIZED_GMT: 935 result = formatOffsetLocalizedGMT(offset); 936 break; 937 938 case GENERIC_SHORT: 939 case SPECIFIC_SHORT: 940 case LOCALIZED_GMT_SHORT: 941 result = formatOffsetShortLocalizedGMT(offset); 942 break; 943 944 case ISO_BASIC_SHORT: 945 result = formatOffsetISO8601Basic(offset, true, true, true); 946 break; 947 948 case ISO_BASIC_LOCAL_SHORT: 949 result = formatOffsetISO8601Basic(offset, false, true, true); 950 break; 951 952 case ISO_BASIC_FIXED: 953 result = formatOffsetISO8601Basic(offset, true, false, true); 954 break; 955 956 case ISO_BASIC_LOCAL_FIXED: 957 result = formatOffsetISO8601Basic(offset, false, false, true); 958 break; 959 960 case ISO_BASIC_FULL: 961 result = formatOffsetISO8601Basic(offset, true, false, false); 962 break; 963 964 case ISO_BASIC_LOCAL_FULL: 965 result = formatOffsetISO8601Basic(offset, false, false, false); 966 break; 967 968 case ISO_EXTENDED_FIXED: 969 result = formatOffsetISO8601Extended(offset, true, false, true); 970 break; 971 972 case ISO_EXTENDED_LOCAL_FIXED: 973 result = formatOffsetISO8601Extended(offset, false, false, true); 974 break; 975 976 case ISO_EXTENDED_FULL: 977 result = formatOffsetISO8601Extended(offset, true, false, false); 978 break; 979 980 case ISO_EXTENDED_LOCAL_FULL: 981 result = formatOffsetISO8601Extended(offset, false, false, false); 982 break; 983 984 default: 985 // Other cases are handled earlier and never comes into this 986 // switch statement. 987 assert false; 988 break; 989 } 990 // time type 991 if (timeType != null) { 992 timeType.value = (offsets[1] != 0) ? TimeType.DAYLIGHT : TimeType.STANDARD; 993 } 994 } 995 996 assert(result != null); 997 998 return result; 999 } 1000 1001 /** 1002 * Returns offset from GMT(UTC) in milliseconds for the given ISO 8601 1003 * basic or extended time zone string. When the given string is not an ISO 8601 time 1004 * zone string, this method sets the current position as the error index 1005 * to <code>ParsePosition pos</code> and returns 0. 1006 * 1007 * @param text the text contains ISO 8601 style time zone string (e.g. "-08", "-0800", "-08:00", and "Z") 1008 * at the position. 1009 * @param pos the position. 1010 * @return the offset from GMT(UTC) in milliseconds for the given ISO 8601 style 1011 * time zone string. 1012 * @see #formatOffsetISO8601Basic(int, boolean, boolean, boolean) 1013 * @see #formatOffsetISO8601Extended(int, boolean, boolean, boolean) 1014 * @stable ICU 49 1015 */ parseOffsetISO8601(String text, ParsePosition pos)1016 public final int parseOffsetISO8601(String text, ParsePosition pos) { 1017 return parseOffsetISO8601(text, pos, false, null); 1018 } 1019 1020 /** 1021 * Returns offset from GMT(UTC) in milliseconds for the given localized GMT 1022 * offset format string. When the given string cannot be parsed, this method 1023 * sets the current position as the error index to <code>ParsePosition pos</code> 1024 * and returns 0. 1025 * 1026 * @param text the text contains a localized GMT offset string at the position. 1027 * @param pos the position. 1028 * @return the offset from GMT(UTC) in milliseconds for the given localized GMT 1029 * offset format string. 1030 * @see #formatOffsetLocalizedGMT(int) 1031 * @stable ICU 49 1032 */ parseOffsetLocalizedGMT(String text, ParsePosition pos)1033 public int parseOffsetLocalizedGMT(String text, ParsePosition pos) { 1034 return parseOffsetLocalizedGMT(text, pos, false, null); 1035 } 1036 1037 /** 1038 * Returns offset from GMT(UTC) in milliseconds for the given short localized GMT 1039 * offset format string. When the given string cannot be parsed, this method 1040 * sets the current position as the error index to <code>ParsePosition pos</code> 1041 * and returns 0. 1042 * 1043 * @param text the text contains a short localized GMT offset string at the position. 1044 * @param pos the position. 1045 * @return the offset from GMT(UTC) in milliseconds for the given short localized GMT 1046 * offset format string. 1047 * @see #formatOffsetShortLocalizedGMT(int) 1048 * @stable ICU 51 1049 */ parseOffsetShortLocalizedGMT(String text, ParsePosition pos)1050 public int parseOffsetShortLocalizedGMT(String text, ParsePosition pos) { 1051 return parseOffsetLocalizedGMT(text, pos, true, null); 1052 } 1053 1054 /** 1055 * Returns a <code>TimeZone</code> by parsing the time zone string according to 1056 * the parse position, the style and the parse options. 1057 * 1058 * @param text the text contains a time zone string at the position. 1059 * @param style the format style. 1060 * @param pos the position. 1061 * @param options the parse options. 1062 * @param timeType The output argument for receiving the time type (standard/daylight/unknown), 1063 * or specify null if the information is not necessary. 1064 * @return A <code>TimeZone</code>, or null if the input could not be parsed. 1065 * @see Style 1066 * @see #format(Style, TimeZone, long, Output) 1067 * @stable ICU 49 1068 */ parse(Style style, String text, ParsePosition pos, EnumSet<ParseOption> options, Output<TimeType> timeType)1069 public TimeZone parse(Style style, String text, ParsePosition pos, EnumSet<ParseOption> options, Output<TimeType> timeType) { 1070 if (timeType == null) { 1071 timeType = new Output<TimeType>(TimeType.UNKNOWN); 1072 } else { 1073 timeType.value = TimeType.UNKNOWN; 1074 } 1075 1076 int startIdx = pos.getIndex(); 1077 int maxPos = text.length(); 1078 int offset; 1079 1080 // Styles using localized GMT format as fallback 1081 boolean fallbackLocalizedGMT = 1082 (style == Style.SPECIFIC_LONG || style == Style.GENERIC_LONG || style == Style.GENERIC_LOCATION); 1083 boolean fallbackShortLocalizedGMT = 1084 (style == Style.SPECIFIC_SHORT || style == Style.GENERIC_SHORT); 1085 1086 int evaluated = 0; // bit flags representing already evaluated styles 1087 ParsePosition tmpPos = new ParsePosition(startIdx); 1088 1089 int parsedOffset = UNKNOWN_OFFSET; // stores successfully parsed offset for later use 1090 int parsedPos = -1; // stores successfully parsed offset position for later use 1091 1092 // Try localized GMT format first if necessary 1093 if (fallbackLocalizedGMT || fallbackShortLocalizedGMT) { 1094 Output<Boolean> hasDigitOffset = new Output<Boolean>(false); 1095 offset = parseOffsetLocalizedGMT(text, tmpPos, fallbackShortLocalizedGMT, hasDigitOffset); 1096 if (tmpPos.getErrorIndex() == -1) { 1097 // Even when the input text was successfully parsed as a localized GMT format text, 1098 // we may still need to evaluate the specified style if - 1099 // 1) GMT zero format was used, and 1100 // 2) The input text was not completely processed 1101 if (tmpPos.getIndex() == maxPos || hasDigitOffset.value) { 1102 pos.setIndex(tmpPos.getIndex()); 1103 return getTimeZoneForOffset(offset); 1104 } 1105 parsedOffset = offset; 1106 parsedPos = tmpPos.getIndex(); 1107 } 1108 // Note: For now, no distinction between long/short localized GMT format in the parser. 1109 // This might be changed in future. 1110 // evaluated |= (fallbackLocalizedGMT ? Style.LOCALIZED_GMT.flag : Style.LOCALIZED_GMT_SHORT.flag); 1111 evaluated |= (Style.LOCALIZED_GMT.flag | Style.LOCALIZED_GMT_SHORT.flag); 1112 } 1113 1114 boolean parseTZDBAbbrev = (options == null) ? 1115 getDefaultParseOptions().contains(ParseOption.TZ_DATABASE_ABBREVIATIONS) 1116 : options.contains(ParseOption.TZ_DATABASE_ABBREVIATIONS); 1117 1118 // Try the specified style 1119 switch (style) { 1120 case LOCALIZED_GMT: 1121 { 1122 tmpPos.setIndex(startIdx); 1123 tmpPos.setErrorIndex(-1); 1124 1125 offset = parseOffsetLocalizedGMT(text, tmpPos); 1126 if (tmpPos.getErrorIndex() == -1) { 1127 pos.setIndex(tmpPos.getIndex()); 1128 return getTimeZoneForOffset(offset); 1129 } 1130 // Note: For now, no distinction between long/short localized GMT format in the parser. 1131 // This might be changed in future. 1132 evaluated |= Style.LOCALIZED_GMT_SHORT.flag; 1133 break; 1134 } 1135 case LOCALIZED_GMT_SHORT: 1136 { 1137 tmpPos.setIndex(startIdx); 1138 tmpPos.setErrorIndex(-1); 1139 1140 offset = parseOffsetShortLocalizedGMT(text, tmpPos); 1141 if (tmpPos.getErrorIndex() == -1) { 1142 pos.setIndex(tmpPos.getIndex()); 1143 return getTimeZoneForOffset(offset); 1144 } 1145 // Note: For now, no distinction between long/short localized GMT format in the parser. 1146 // This might be changed in future. 1147 evaluated |= Style.LOCALIZED_GMT.flag; 1148 break; 1149 } 1150 1151 case ISO_BASIC_SHORT: 1152 case ISO_BASIC_FIXED: 1153 case ISO_BASIC_FULL: 1154 case ISO_EXTENDED_FIXED: 1155 case ISO_EXTENDED_FULL: 1156 { 1157 tmpPos.setIndex(startIdx); 1158 tmpPos.setErrorIndex(-1); 1159 1160 offset = parseOffsetISO8601(text, tmpPos); 1161 if (tmpPos.getErrorIndex() == -1) { 1162 pos.setIndex(tmpPos.getIndex()); 1163 return getTimeZoneForOffset(offset); 1164 } 1165 break; 1166 } 1167 1168 case ISO_BASIC_LOCAL_SHORT: 1169 case ISO_BASIC_LOCAL_FIXED: 1170 case ISO_BASIC_LOCAL_FULL: 1171 case ISO_EXTENDED_LOCAL_FIXED: 1172 case ISO_EXTENDED_LOCAL_FULL: 1173 { 1174 tmpPos.setIndex(startIdx); 1175 tmpPos.setErrorIndex(-1); 1176 1177 // Exclude the case of UTC Indicator "Z" here 1178 Output<Boolean> hasDigitOffset = new Output<Boolean>(false); 1179 offset = parseOffsetISO8601(text, tmpPos, false, hasDigitOffset); 1180 if (tmpPos.getErrorIndex() == -1 && hasDigitOffset.value) { 1181 pos.setIndex(tmpPos.getIndex()); 1182 return getTimeZoneForOffset(offset); 1183 } 1184 break; 1185 } 1186 1187 case SPECIFIC_LONG: 1188 case SPECIFIC_SHORT: 1189 { 1190 // Specific styles 1191 EnumSet<NameType> nameTypes = null; 1192 if (style == Style.SPECIFIC_LONG) { 1193 nameTypes = EnumSet.of(NameType.LONG_STANDARD, NameType.LONG_DAYLIGHT); 1194 } else { 1195 assert style == Style.SPECIFIC_SHORT; 1196 nameTypes = EnumSet.of(NameType.SHORT_STANDARD, NameType.SHORT_DAYLIGHT); 1197 } 1198 Collection<MatchInfo> specificMatches = _tznames.find(text, startIdx, nameTypes); 1199 if (specificMatches != null) { 1200 MatchInfo specificMatch = null; 1201 for (MatchInfo match : specificMatches) { 1202 if (startIdx + match.matchLength() > parsedPos) { 1203 specificMatch = match; 1204 parsedPos = startIdx + match.matchLength(); 1205 } 1206 } 1207 if (specificMatch != null) { 1208 timeType.value = getTimeType(specificMatch.nameType()); 1209 pos.setIndex(parsedPos); 1210 return TimeZone.getTimeZone(getTimeZoneID(specificMatch.tzID(), specificMatch.mzID())); 1211 } 1212 } 1213 1214 if (parseTZDBAbbrev && style == Style.SPECIFIC_SHORT) { 1215 assert nameTypes.contains(NameType.SHORT_STANDARD); 1216 assert nameTypes.contains(NameType.SHORT_DAYLIGHT); 1217 1218 Collection<MatchInfo> tzdbNameMatches = 1219 getTZDBTimeZoneNames().find(text, startIdx, nameTypes); 1220 if (tzdbNameMatches != null) { 1221 MatchInfo tzdbNameMatch = null; 1222 for (MatchInfo match : tzdbNameMatches) { 1223 if (startIdx + match.matchLength() > parsedPos) { 1224 tzdbNameMatch = match; 1225 parsedPos = startIdx + match.matchLength(); 1226 } 1227 } 1228 if (tzdbNameMatch != null) { 1229 timeType.value = getTimeType(tzdbNameMatch.nameType()); 1230 pos.setIndex(parsedPos); 1231 return TimeZone.getTimeZone(getTimeZoneID(tzdbNameMatch.tzID(), tzdbNameMatch.mzID())); 1232 } 1233 } 1234 } 1235 break; 1236 } 1237 case GENERIC_LONG: 1238 case GENERIC_SHORT: 1239 case GENERIC_LOCATION: 1240 { 1241 EnumSet<GenericNameType> genericNameTypes = null; 1242 switch (style) { 1243 case GENERIC_LOCATION: 1244 genericNameTypes = EnumSet.of(GenericNameType.LOCATION); 1245 break; 1246 case GENERIC_LONG: 1247 genericNameTypes = EnumSet.of(GenericNameType.LONG, GenericNameType.LOCATION); 1248 break; 1249 case GENERIC_SHORT: 1250 genericNameTypes = EnumSet.of(GenericNameType.SHORT, GenericNameType.LOCATION); 1251 break; 1252 default: 1253 // style cannot be other than above cases 1254 assert false; 1255 break; 1256 } 1257 GenericMatchInfo bestGeneric = getTimeZoneGenericNames().findBestMatch(text, startIdx, genericNameTypes); 1258 if (bestGeneric != null && (startIdx + bestGeneric.matchLength() > parsedPos)) { 1259 timeType.value = bestGeneric.timeType(); 1260 pos.setIndex(startIdx + bestGeneric.matchLength()); 1261 return TimeZone.getTimeZone(bestGeneric.tzID()); 1262 } 1263 break; 1264 } 1265 case ZONE_ID: 1266 { 1267 tmpPos.setIndex(startIdx); 1268 tmpPos.setErrorIndex(-1); 1269 1270 String id = parseZoneID(text, tmpPos); 1271 if (tmpPos.getErrorIndex() == -1) { 1272 pos.setIndex(tmpPos.getIndex()); 1273 return TimeZone.getTimeZone(id); 1274 } 1275 break; 1276 } 1277 case ZONE_ID_SHORT: 1278 { 1279 tmpPos.setIndex(startIdx); 1280 tmpPos.setErrorIndex(-1); 1281 1282 String id = parseShortZoneID(text, tmpPos); 1283 if (tmpPos.getErrorIndex() == -1) { 1284 pos.setIndex(tmpPos.getIndex()); 1285 return TimeZone.getTimeZone(id); 1286 } 1287 break; 1288 } 1289 case EXEMPLAR_LOCATION: 1290 { 1291 tmpPos.setIndex(startIdx); 1292 tmpPos.setErrorIndex(-1); 1293 1294 String id = parseExemplarLocation(text, tmpPos); 1295 if (tmpPos.getErrorIndex() == -1) { 1296 pos.setIndex(tmpPos.getIndex()); 1297 return TimeZone.getTimeZone(id); 1298 } 1299 break; 1300 } 1301 } 1302 evaluated |= style.flag; 1303 1304 if (parsedPos > startIdx) { 1305 // When the specified style is one of SPECIFIC_XXX or GENERIC_XXX, we tried to parse the input 1306 // as localized GMT format earlier. If parsedOffset is positive, it means it was successfully 1307 // parsed as localized GMT format, but offset digits were not detected (more specifically, GMT 1308 // zero format). Then, it tried to find a match within the set of display names, but could not 1309 // find a match. At this point, we can safely assume the input text contains the localized 1310 // GMT format. 1311 assert parsedOffset != UNKNOWN_OFFSET; 1312 pos.setIndex(parsedPos); 1313 return getTimeZoneForOffset(parsedOffset); 1314 } 1315 1316 1317 // Failed to parse the input text as the time zone format in the specified style. 1318 // Check the longest match among other styles below. 1319 String parsedID = null; // stores successfully parsed zone ID for later use 1320 TimeType parsedTimeType = TimeType.UNKNOWN; // stores successfully parsed time type for later use 1321 assert parsedPos < 0; 1322 assert parsedOffset == UNKNOWN_OFFSET; 1323 1324 // ISO 8601 1325 if (parsedPos < maxPos && 1326 ((evaluated & ISO_Z_STYLE_FLAG) == 0 || (evaluated & ISO_LOCAL_STYLE_FLAG) == 0)) { 1327 tmpPos.setIndex(startIdx); 1328 tmpPos.setErrorIndex(-1); 1329 1330 Output<Boolean> hasDigitOffset = new Output<Boolean>(false); 1331 offset = parseOffsetISO8601(text, tmpPos, false, hasDigitOffset); 1332 if (tmpPos.getErrorIndex() == -1) { 1333 if (tmpPos.getIndex() == maxPos || hasDigitOffset.value) { 1334 pos.setIndex(tmpPos.getIndex()); 1335 return getTimeZoneForOffset(offset); 1336 } 1337 // Note: When ISO 8601 format contains offset digits, it should not 1338 // collide with other formats. However, ISO 8601 UTC format "Z" (single letter) 1339 // may collide with other names. In this case, we need to evaluate other names. 1340 if (parsedPos < tmpPos.getIndex()) { 1341 parsedOffset = offset; 1342 parsedID = null; 1343 parsedTimeType = TimeType.UNKNOWN; 1344 parsedPos = tmpPos.getIndex(); 1345 assert parsedPos == startIdx + 1; // only when "Z" is used 1346 } 1347 } 1348 } 1349 1350 1351 // Localized GMT format 1352 if (parsedPos < maxPos && 1353 (evaluated & Style.LOCALIZED_GMT.flag) == 0) { 1354 tmpPos.setIndex(startIdx); 1355 tmpPos.setErrorIndex(-1); 1356 1357 Output<Boolean> hasDigitOffset = new Output<Boolean>(false); 1358 offset = parseOffsetLocalizedGMT(text, tmpPos, false, hasDigitOffset); 1359 if (tmpPos.getErrorIndex() == -1) { 1360 if (tmpPos.getIndex() == maxPos || hasDigitOffset.value) { 1361 pos.setIndex(tmpPos.getIndex()); 1362 return getTimeZoneForOffset(offset); 1363 } 1364 // Evaluate other names - see the comment earlier in this method. 1365 if (parsedPos < tmpPos.getIndex()) { 1366 parsedOffset = offset; 1367 parsedID = null; 1368 parsedTimeType = TimeType.UNKNOWN; 1369 parsedPos = tmpPos.getIndex(); 1370 } 1371 } 1372 } 1373 1374 if (parsedPos < maxPos && 1375 (evaluated & Style.LOCALIZED_GMT_SHORT.flag) == 0) { 1376 tmpPos.setIndex(startIdx); 1377 tmpPos.setErrorIndex(-1); 1378 1379 Output<Boolean> hasDigitOffset = new Output<Boolean>(false); 1380 offset = parseOffsetLocalizedGMT(text, tmpPos, true, hasDigitOffset); 1381 if (tmpPos.getErrorIndex() == -1) { 1382 if (tmpPos.getIndex() == maxPos || hasDigitOffset.value) { 1383 pos.setIndex(tmpPos.getIndex()); 1384 return getTimeZoneForOffset(offset); 1385 } 1386 // Evaluate other names - see the comment earlier in this method. 1387 if (parsedPos < tmpPos.getIndex()) { 1388 parsedOffset = offset; 1389 parsedID = null; 1390 parsedTimeType = TimeType.UNKNOWN; 1391 parsedPos = tmpPos.getIndex(); 1392 } 1393 } 1394 } 1395 1396 // When ParseOption.ALL_STYLES is available, we also try to look all possible display names and IDs. 1397 // For example, when style is GENERIC_LONG, "EST" (SPECIFIC_SHORT) is never 1398 // used for America/New_York. With parseAllStyles true, this code parses "EST" 1399 // as America/New_York. 1400 1401 // Note: Adding all possible names into the trie used by the implementation is quite heavy operation, 1402 // which we want to avoid normally (note that we cache the trie, so this is applicable to the 1403 // first time only as long as the cache does not expire). 1404 1405 boolean parseAllStyles = (options == null) ? 1406 getDefaultParseOptions().contains(ParseOption.ALL_STYLES) 1407 : options.contains(ParseOption.ALL_STYLES); 1408 1409 if (parseAllStyles) { 1410 // Try all specific names and exemplar location names 1411 if (parsedPos < maxPos) { 1412 Collection<MatchInfo> specificMatches = _tznames.find(text, startIdx, ALL_SIMPLE_NAME_TYPES); 1413 MatchInfo specificMatch = null; 1414 int matchPos = -1; 1415 if (specificMatches != null) { 1416 for (MatchInfo match : specificMatches) { 1417 if (startIdx + match.matchLength() > matchPos) { 1418 specificMatch = match; 1419 matchPos = startIdx + match.matchLength(); 1420 } 1421 } 1422 } 1423 if (parsedPos < matchPos) { 1424 parsedPos = matchPos; 1425 parsedID = getTimeZoneID(specificMatch.tzID(), specificMatch.mzID()); 1426 parsedTimeType = getTimeType(specificMatch.nameType()); 1427 parsedOffset = UNKNOWN_OFFSET; 1428 } 1429 } 1430 if (parseTZDBAbbrev && parsedPos < maxPos && (evaluated & Style.SPECIFIC_SHORT.flag) == 0) { 1431 Collection<MatchInfo> tzdbNameMatches = 1432 getTZDBTimeZoneNames().find(text, startIdx, ALL_SIMPLE_NAME_TYPES); 1433 MatchInfo tzdbNameMatch = null; 1434 int matchPos = -1; 1435 if (tzdbNameMatches != null) { 1436 for (MatchInfo match : tzdbNameMatches) { 1437 if (startIdx + match.matchLength() > matchPos) { 1438 tzdbNameMatch = match; 1439 matchPos = startIdx + match.matchLength(); 1440 } 1441 } 1442 if (parsedPos < matchPos) { 1443 parsedPos = matchPos; 1444 parsedID = getTimeZoneID(tzdbNameMatch.tzID(), tzdbNameMatch.mzID()); 1445 parsedTimeType = getTimeType(tzdbNameMatch.nameType()); 1446 parsedOffset = UNKNOWN_OFFSET; 1447 } 1448 } 1449 1450 } 1451 // Try generic names 1452 if (parsedPos < maxPos) { 1453 GenericMatchInfo genericMatch = getTimeZoneGenericNames().findBestMatch(text, startIdx, ALL_GENERIC_NAME_TYPES); 1454 if (genericMatch != null && parsedPos < startIdx + genericMatch.matchLength()) { 1455 parsedPos = startIdx + genericMatch.matchLength(); 1456 parsedID = genericMatch.tzID(); 1457 parsedTimeType = genericMatch.timeType(); 1458 parsedOffset = UNKNOWN_OFFSET; 1459 } 1460 } 1461 1462 // Try time zone ID 1463 if (parsedPos < maxPos && (evaluated & Style.ZONE_ID.flag) == 0) { 1464 tmpPos.setIndex(startIdx); 1465 tmpPos.setErrorIndex(-1); 1466 1467 String id = parseZoneID(text, tmpPos); 1468 if (tmpPos.getErrorIndex() == -1 && parsedPos < tmpPos.getIndex()) { 1469 parsedPos = tmpPos.getIndex(); 1470 parsedID = id; 1471 parsedTimeType = TimeType.UNKNOWN; 1472 parsedOffset = UNKNOWN_OFFSET; 1473 } 1474 } 1475 // Try short time zone ID 1476 if (parsedPos < maxPos && (evaluated & Style.ZONE_ID_SHORT.flag) == 0) { 1477 tmpPos.setIndex(startIdx); 1478 tmpPos.setErrorIndex(-1); 1479 1480 String id = parseShortZoneID(text, tmpPos); 1481 if (tmpPos.getErrorIndex() == -1 && parsedPos < tmpPos.getIndex()) { 1482 parsedPos = tmpPos.getIndex(); 1483 parsedID = id; 1484 parsedTimeType = TimeType.UNKNOWN; 1485 parsedOffset = UNKNOWN_OFFSET; 1486 } 1487 } 1488 } 1489 1490 if (parsedPos > startIdx) { 1491 // Parsed successfully 1492 TimeZone parsedTZ = null; 1493 if (parsedID != null) { 1494 parsedTZ = TimeZone.getTimeZone(parsedID); 1495 } else { 1496 assert parsedOffset != UNKNOWN_OFFSET; 1497 parsedTZ = getTimeZoneForOffset(parsedOffset); 1498 } 1499 timeType.value = parsedTimeType; 1500 pos.setIndex(parsedPos); 1501 return parsedTZ; 1502 } 1503 1504 pos.setErrorIndex(startIdx); 1505 return null; 1506 } 1507 1508 /** 1509 * Returns a <code>TimeZone</code> by parsing the time zone string according to 1510 * the parse position, the style and the default parse options. 1511 * <p> 1512 * <b>Note</b>: This method is equivalent to {@link #parse(Style, String, ParsePosition, EnumSet, Output) 1513 * parse(style, text, pos, null, timeType)}. 1514 * 1515 * @param text the text contains a time zone string at the position. 1516 * @param style the format style 1517 * @param pos the position. 1518 * @param timeType The output argument for receiving the time type (standard/daylight/unknown), 1519 * or specify null if the information is not necessary. 1520 * @return A <code>TimeZone</code>, or null if the input could not be parsed. 1521 * @see Style 1522 * @see #parse(Style, String, ParsePosition, EnumSet, Output) 1523 * @see #format(Style, TimeZone, long, Output) 1524 * @see #setDefaultParseOptions(EnumSet) 1525 * @stable ICU 49 1526 */ 1527 public TimeZone parse(Style style, String text, ParsePosition pos, Output<TimeType> timeType) { 1528 return parse(style, text, pos, null, timeType); 1529 } 1530 1531 /** 1532 * Returns a <code>TimeZone</code> by parsing the time zone string according to 1533 * the given parse position. 1534 * <p> 1535 * <b>Note</b>: This method is equivalent to {@link #parse(Style, String, ParsePosition, EnumSet, Output) 1536 * parse(Style.GENERIC_LOCATION, text, pos, EnumSet.of(ParseOption.ALL_STYLES), timeType)}. 1537 * 1538 * @param text the text contains a time zone string at the position. 1539 * @param pos the position. 1540 * @return A <code>TimeZone</code>, or null if the input could not be parsed. 1541 * @see #parse(Style, String, ParsePosition, EnumSet, Output) 1542 * @stable ICU 49 1543 */ 1544 public final TimeZone parse(String text, ParsePosition pos) { 1545 return parse(Style.GENERIC_LOCATION, text, pos, EnumSet.of(ParseOption.ALL_STYLES), null); 1546 } 1547 1548 /** 1549 * Returns a <code>TimeZone</code> for the given text. 1550 * <p> 1551 * <b>Note</b>: The behavior of this method is equivalent to {@link #parse(String, ParsePosition)}. 1552 * @param text the time zone string 1553 * @return A <code>TimeZone</code>. 1554 * @throws ParseException when the input could not be parsed as a time zone string. 1555 * @see #parse(String, ParsePosition) 1556 * @stable ICU 49 1557 */ 1558 public final TimeZone parse(String text) throws ParseException { 1559 ParsePosition pos = new ParsePosition(0); 1560 TimeZone tz = parse(text, pos); 1561 if (pos.getErrorIndex() >= 0) { 1562 throw new ParseException("Unparseable time zone: \"" + text + "\"" , 0); 1563 } 1564 assert(tz != null); 1565 return tz; 1566 } 1567 1568 /** 1569 * {@inheritDoc} 1570 * 1571 * @stable ICU 49 1572 */ 1573 @Override 1574 public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { 1575 TimeZone tz = null; 1576 long date = System.currentTimeMillis(); 1577 1578 if (obj instanceof TimeZone) { 1579 tz = (TimeZone)obj; 1580 } else if (obj instanceof Calendar) { 1581 tz = ((Calendar)obj).getTimeZone(); 1582 date = ((Calendar)obj).getTimeInMillis(); 1583 } else { 1584 throw new IllegalArgumentException("Cannot format given Object (" + 1585 obj.getClass().getName() + ") as a time zone"); 1586 } 1587 assert(tz != null); 1588 String result = formatOffsetLocalizedGMT(tz.getOffset(date)); 1589 toAppendTo.append(result); 1590 1591 if (pos.getFieldAttribute() == DateFormat.Field.TIME_ZONE 1592 || pos.getField() == DateFormat.TIMEZONE_FIELD) { 1593 pos.setBeginIndex(0); 1594 pos.setEndIndex(result.length()); 1595 } 1596 return toAppendTo; 1597 } 1598 1599 /** 1600 * {@inheritDoc} 1601 * 1602 * @stable ICU 49 1603 */ 1604 @Override 1605 public AttributedCharacterIterator formatToCharacterIterator(Object obj) { 1606 StringBuffer toAppendTo = new StringBuffer(); 1607 FieldPosition pos = new FieldPosition(0); 1608 toAppendTo = format(obj, toAppendTo, pos); 1609 1610 // supporting only DateFormat.Field.TIME_ZONE 1611 AttributedString as = new AttributedString(toAppendTo.toString()); 1612 as.addAttribute(DateFormat.Field.TIME_ZONE, DateFormat.Field.TIME_ZONE); 1613 1614 return as.getIterator(); 1615 } 1616 1617 /** 1618 * {@inheritDoc} 1619 * 1620 * @stable ICU 49 1621 */ 1622 @Override 1623 public Object parseObject(String source, ParsePosition pos) { 1624 return parse(source, pos); 1625 } 1626 1627 /** 1628 * Private method used for localized GMT formatting. 1629 * @param offset the zone's UTC offset 1630 * @param isShort true if the short localized GMT format is desired 1631 * @return the localized GMT string 1632 */ 1633 private String formatOffsetLocalizedGMT(int offset, boolean isShort) { 1634 if (offset == 0) { 1635 return _gmtZeroFormat; 1636 } 1637 1638 StringBuilder buf = new StringBuilder(); 1639 boolean positive = true; 1640 if (offset < 0) { 1641 offset = -offset; 1642 positive = false; 1643 } 1644 1645 int offsetH = offset / MILLIS_PER_HOUR; 1646 offset = offset % MILLIS_PER_HOUR; 1647 int offsetM = offset / MILLIS_PER_MINUTE; 1648 offset = offset % MILLIS_PER_MINUTE; 1649 int offsetS = offset / MILLIS_PER_SECOND; 1650 1651 if (offsetH > MAX_OFFSET_HOUR || offsetM > MAX_OFFSET_MINUTE || offsetS > MAX_OFFSET_SECOND) { 1652 throw new IllegalArgumentException("Offset out of range :" + offset); 1653 } 1654 1655 Object[] offsetPatternItems; 1656 if (positive) { 1657 if (offsetS != 0) { 1658 offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.POSITIVE_HMS.ordinal()]; 1659 } else if (offsetM != 0 || !isShort) { 1660 offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.POSITIVE_HM.ordinal()]; 1661 } else { 1662 offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.POSITIVE_H.ordinal()]; 1663 } 1664 } else { 1665 if (offsetS != 0) { 1666 offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.NEGATIVE_HMS.ordinal()]; 1667 } else if (offsetM != 0 || !isShort) { 1668 offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.NEGATIVE_HM.ordinal()]; 1669 } else { 1670 offsetPatternItems = _gmtOffsetPatternItems[GMTOffsetPatternType.NEGATIVE_H.ordinal()]; 1671 } 1672 } 1673 1674 // Building the GMT format string 1675 buf.append(_gmtPatternPrefix); 1676 1677 for (Object item : offsetPatternItems) { 1678 if (item instanceof String) { 1679 // pattern literal 1680 buf.append((String)item); 1681 } else if (item instanceof GMTOffsetField) { 1682 // Hour/minute/second field 1683 GMTOffsetField field = (GMTOffsetField)item; 1684 switch (field.getType()) { 1685 case 'H': 1686 appendOffsetDigits(buf, offsetH, (isShort ? 1 : 2)); 1687 break; 1688 case 'm': 1689 appendOffsetDigits(buf, offsetM, 2); 1690 break; 1691 case 's': 1692 appendOffsetDigits(buf, offsetS, 2); 1693 break; 1694 } 1695 } 1696 } 1697 buf.append(_gmtPatternSuffix); 1698 return buf.toString(); 1699 } 1700 1701 /** 1702 * Numeric offset field combinations 1703 */ 1704 private enum OffsetFields { 1705 H, HM, HMS 1706 } 1707 1708 private String formatOffsetISO8601(int offset, boolean isBasic, boolean useUtcIndicator, boolean isShort, boolean ignoreSeconds) { 1709 int absOffset = offset < 0 ? -offset : offset; 1710 if (useUtcIndicator && (absOffset < MILLIS_PER_SECOND || (ignoreSeconds && absOffset < MILLIS_PER_MINUTE))) { 1711 return ISO8601_UTC; 1712 } 1713 OffsetFields minFields = isShort ? OffsetFields.H : OffsetFields.HM; 1714 OffsetFields maxFields = ignoreSeconds ? OffsetFields.HM : OffsetFields.HMS; 1715 Character sep = isBasic ? null : ':'; 1716 1717 // Note: OffsetFields.HMS as maxFields is an ICU extension. ISO 8601 specification does 1718 // not support seconds field. 1719 1720 if (absOffset >= MAX_OFFSET) { 1721 throw new IllegalArgumentException("Offset out of range :" + offset); 1722 } 1723 1724 int[] fields = new int[3]; 1725 fields[0] = absOffset / MILLIS_PER_HOUR; 1726 absOffset = absOffset % MILLIS_PER_HOUR; 1727 fields[1] = absOffset / MILLIS_PER_MINUTE; 1728 absOffset = absOffset % MILLIS_PER_MINUTE; 1729 fields[2] = absOffset / MILLIS_PER_SECOND; 1730 1731 assert(fields[0] >= 0 && fields[0] <= MAX_OFFSET_HOUR); 1732 assert(fields[1] >= 0 && fields[1] <= MAX_OFFSET_MINUTE); 1733 assert(fields[2] >= 0 && fields[2] <= MAX_OFFSET_SECOND); 1734 1735 int lastIdx = maxFields.ordinal(); 1736 while (lastIdx > minFields.ordinal()) { 1737 if (fields[lastIdx] != 0) { 1738 break; 1739 } 1740 lastIdx--; 1741 } 1742 1743 StringBuilder buf = new StringBuilder(); 1744 char sign = '+'; 1745 if (offset < 0) { 1746 // if all output fields are 0s, do not use negative sign 1747 for (int idx = 0; idx <= lastIdx; idx++) { 1748 if (fields[idx] != 0) { 1749 sign = '-'; 1750 break; 1751 } 1752 } 1753 } 1754 buf.append(sign); 1755 1756 for (int idx = 0; idx <= lastIdx; idx++) { 1757 if (sep != null && idx != 0) { 1758 buf.append(sep); 1759 } 1760 if (fields[idx] < 10) { 1761 buf.append('0'); 1762 } 1763 buf.append(fields[idx]); 1764 } 1765 return buf.toString(); 1766 } 1767 1768 /** 1769 * Private method returning the time zone's specific format string. 1770 * 1771 * @param tz the time zone 1772 * @param stdType the name type used for standard time 1773 * @param dstType the name type used for daylight time 1774 * @param date the date 1775 * @param timeType when null, actual time type is set 1776 * @return the time zone's specific format name string 1777 */ 1778 private String formatSpecific(TimeZone tz, NameType stdType, NameType dstType, long date, Output<TimeType> timeType) { 1779 assert(stdType == NameType.LONG_STANDARD || stdType == NameType.SHORT_STANDARD); 1780 assert(dstType == NameType.LONG_DAYLIGHT || dstType == NameType.SHORT_DAYLIGHT); 1781 1782 boolean isDaylight = tz.inDaylightTime(new Date(date)); 1783 String name = isDaylight? 1784 getTimeZoneNames().getDisplayName(ZoneMeta.getCanonicalCLDRID(tz), dstType, date) : 1785 getTimeZoneNames().getDisplayName(ZoneMeta.getCanonicalCLDRID(tz), stdType, date); 1786 1787 if (name != null && timeType != null) { 1788 timeType.value = isDaylight ? TimeType.DAYLIGHT : TimeType.STANDARD; 1789 } 1790 return name; 1791 } 1792 1793 /** 1794 * Private method returning the time zone's exemplar location string. 1795 * This method will never return null. 1796 * 1797 * @param tz the time zone 1798 * @return the time zone's exemplar location name. 1799 */ 1800 private String formatExemplarLocation(TimeZone tz) { 1801 String location = getTimeZoneNames().getExemplarLocationName(ZoneMeta.getCanonicalCLDRID(tz)); 1802 if (location == null) { 1803 // Use "unknown" location 1804 location = getTimeZoneNames().getExemplarLocationName(UNKNOWN_ZONE_ID); 1805 if (location == null) { 1806 // last resort 1807 location = UNKNOWN_LOCATION; 1808 } 1809 } 1810 return location; 1811 } 1812 1813 /** 1814 * Private method returns a time zone ID. If tzID is not null, the value of tzID is returned. 1815 * If tzID is null, then this method look up a time zone ID for the current region. This is a 1816 * small helper method used by the parse implementation method 1817 * 1818 * @param tzID 1819 * the time zone ID or null 1820 * @param mzID 1821 * the meta zone ID or null 1822 * @return A time zone ID 1823 * @throws IllegalArgumentException 1824 * when both tzID and mzID are null 1825 */ 1826 private String getTimeZoneID(String tzID, String mzID) { 1827 String id = tzID; 1828 if (id == null) { 1829 assert (mzID != null); 1830 id = _tznames.getReferenceZoneID(mzID, getTargetRegion()); 1831 if (id == null) { 1832 throw new IllegalArgumentException("Invalid mzID: " + mzID); 1833 } 1834 } 1835 return id; 1836 } 1837 1838 /** 1839 * Private method returning the target region. The target regions is determined by 1840 * the locale of this instance. When a generic name is coming from 1841 * a meta zone, this region is used for checking if the time zone 1842 * is a reference zone of the meta zone. 1843 * 1844 * @return the target region 1845 */ 1846 private synchronized String getTargetRegion() { 1847 if (_region == null) { 1848 _region = _locale.getCountry(); 1849 if (_region.length() == 0) { 1850 ULocale tmp = ULocale.addLikelySubtags(_locale); 1851 _region = tmp.getCountry(); 1852 if (_region.length() == 0) { 1853 _region = "001"; 1854 } 1855 } 1856 } 1857 return _region; 1858 } 1859 1860 /** 1861 * Returns the time type for the given name type 1862 * @param nameType the name type 1863 * @return the time type (unknown/standard/daylight) 1864 */ 1865 private TimeType getTimeType(NameType nameType) { 1866 switch (nameType) { 1867 case LONG_STANDARD: 1868 case SHORT_STANDARD: 1869 return TimeType.STANDARD; 1870 1871 case LONG_DAYLIGHT: 1872 case SHORT_DAYLIGHT: 1873 return TimeType.DAYLIGHT; 1874 1875 default: 1876 return TimeType.UNKNOWN; 1877 } 1878 } 1879 1880 /** 1881 * Parses the localized GMT pattern string and initialize 1882 * localized gmt pattern fields including {{@link #_gmtPatternTokens}. 1883 * This method must be also called at deserialization time. 1884 * 1885 * @param gmtPattern the localized GMT pattern string such as "GMT {0}" 1886 * @throws IllegalArgumentException when the pattern string does not contain "{0}" 1887 */ 1888 private void initGMTPattern(String gmtPattern) { 1889 // This implementation not perfect, but sufficient practically. 1890 int idx = gmtPattern.indexOf("{0}"); 1891 if (idx < 0) { 1892 throw new IllegalArgumentException("Bad localized GMT pattern: " + gmtPattern); 1893 } 1894 _gmtPattern = gmtPattern; 1895 _gmtPatternPrefix = unquote(gmtPattern.substring(0, idx)); 1896 _gmtPatternSuffix = unquote(gmtPattern.substring(idx + 3)); 1897 } 1898 1899 /** 1900 * Unquotes the message format style pattern. 1901 * 1902 * @param s the pattern 1903 * @return the unquoted pattern string 1904 */ 1905 private static String unquote(String s) { 1906 if (s.indexOf('\'') < 0) { 1907 return s; 1908 } 1909 boolean isPrevQuote = false; 1910 boolean inQuote = false; 1911 StringBuilder buf = new StringBuilder(); 1912 for (int i = 0; i < s.length(); i++) { 1913 char c = s.charAt(i); 1914 if (c == '\'') { 1915 if (isPrevQuote) { 1916 buf.append(c); 1917 isPrevQuote = false; 1918 } else { 1919 isPrevQuote = true; 1920 } 1921 inQuote = !inQuote; 1922 } else { 1923 isPrevQuote = false; 1924 buf.append(c); 1925 } 1926 } 1927 return buf.toString(); 1928 } 1929 1930 /** 1931 * Initialize localized GMT format offset hour/min/sec patterns. 1932 * This method parses patterns into optimized run-time format. 1933 * This method must be called at deserialization time. 1934 * 1935 * @param gmtOffsetPatterns patterns, String[4] 1936 * @throws IllegalArgumentException when patterns are not valid 1937 */ 1938 private void initGMTOffsetPatterns(String[] gmtOffsetPatterns) { 1939 int size = GMTOffsetPatternType.values().length; 1940 if (gmtOffsetPatterns.length < size) { 1941 throw new IllegalArgumentException("Insufficient number of elements in gmtOffsetPatterns"); 1942 } 1943 Object[][] gmtOffsetPatternItems = new Object[size][]; 1944 for (GMTOffsetPatternType t : GMTOffsetPatternType.values()) { 1945 int idx = t.ordinal(); 1946 // Note: parseOffsetPattern will validate the given pattern and throws 1947 // IllegalArgumentException when pattern is not valid 1948 Object[] parsedItems = parseOffsetPattern(gmtOffsetPatterns[idx], t.required()); 1949 gmtOffsetPatternItems[idx] = parsedItems; 1950 } 1951 1952 _gmtOffsetPatterns = new String[size]; 1953 System.arraycopy(gmtOffsetPatterns, 0, _gmtOffsetPatterns, 0, size); 1954 _gmtOffsetPatternItems = gmtOffsetPatternItems; 1955 checkAbuttingHoursAndMinutes(); 1956 } 1957 1958 private void checkAbuttingHoursAndMinutes() { 1959 _abuttingOffsetHoursAndMinutes = false; 1960 for (Object[] items : _gmtOffsetPatternItems) { 1961 boolean afterH = false; 1962 for (Object item : items) { 1963 if (item instanceof GMTOffsetField) { 1964 GMTOffsetField fld = (GMTOffsetField)item; 1965 if (afterH) { 1966 _abuttingOffsetHoursAndMinutes = true; 1967 } else if (fld.getType() == 'H') { 1968 afterH = true; 1969 } 1970 } else if (afterH) { 1971 break; 1972 } 1973 } 1974 } 1975 } 1976 1977 /** 1978 * Used for representing localized GMT time fields in the parsed pattern object. 1979 * @see TimeZoneFormat#parseOffsetPattern(String, String) 1980 */ 1981 private static class GMTOffsetField { 1982 final char _type; 1983 final int _width; 1984 1985 GMTOffsetField(char type, int width) { 1986 _type = type; 1987 _width = width; 1988 } 1989 1990 char getType() { 1991 return _type; 1992 } 1993 1994 @SuppressWarnings("unused") 1995 int getWidth() { 1996 return _width; 1997 } 1998 1999 static boolean isValid(char type, int width) { 2000 return (width == 1 || width == 2); 2001 } 2002 } 2003 2004 /** 2005 * Parse the GMT offset pattern into runtime optimized format 2006 * 2007 * @param pattern the offset pattern string 2008 * @param letters the required pattern letters such as "Hm" 2009 * @return An array of Object. Each array entry is either String (representing 2010 * pattern literal) or GMTOffsetField (hour/min/sec field) 2011 */ 2012 private static Object[] parseOffsetPattern(String pattern, String letters) { 2013 boolean isPrevQuote = false; 2014 boolean inQuote = false; 2015 StringBuilder text = new StringBuilder(); 2016 char itemType = 0; // 0 for string literal, otherwise time pattern character 2017 int itemLength = 1; 2018 boolean invalidPattern = false; 2019 2020 List<Object> items = new ArrayList<Object>(); 2021 BitSet checkBits = new BitSet(letters.length()); 2022 2023 for (int i = 0; i < pattern.length(); i++) { 2024 char ch = pattern.charAt(i); 2025 if (ch == '\'') { 2026 if (isPrevQuote) { 2027 text.append('\''); 2028 isPrevQuote = false; 2029 } else { 2030 isPrevQuote = true; 2031 if (itemType != 0) { 2032 if (GMTOffsetField.isValid(itemType, itemLength)) { 2033 items.add(new GMTOffsetField(itemType, itemLength)); 2034 } else { 2035 invalidPattern = true; 2036 break; 2037 } 2038 itemType = 0; 2039 } 2040 } 2041 inQuote = !inQuote; 2042 } else { 2043 isPrevQuote = false; 2044 if (inQuote) { 2045 text.append(ch); 2046 } else { 2047 int patFieldIdx = letters.indexOf(ch); 2048 if (patFieldIdx >= 0) { 2049 // an offset time pattern character 2050 if (ch == itemType) { 2051 itemLength++; 2052 } else { 2053 if (itemType == 0) { 2054 if (text.length() > 0) { 2055 items.add(text.toString()); 2056 text.setLength(0); 2057 } 2058 } else { 2059 if (GMTOffsetField.isValid(itemType, itemLength)) { 2060 items.add(new GMTOffsetField(itemType, itemLength)); 2061 } else { 2062 invalidPattern = true; 2063 break; 2064 } 2065 } 2066 itemType = ch; 2067 itemLength = 1; 2068 checkBits.set(patFieldIdx); 2069 } 2070 } else { 2071 // a string literal 2072 if (itemType != 0) { 2073 if (GMTOffsetField.isValid(itemType, itemLength)) { 2074 items.add(new GMTOffsetField(itemType, itemLength)); 2075 } else { 2076 invalidPattern = true; 2077 break; 2078 } 2079 itemType = 0; 2080 } 2081 text.append(ch); 2082 } 2083 } 2084 } 2085 } 2086 // handle last item 2087 if (!invalidPattern) { 2088 if (itemType == 0) { 2089 if (text.length() > 0) { 2090 items.add(text.toString()); 2091 text.setLength(0); 2092 } 2093 } else { 2094 if (GMTOffsetField.isValid(itemType, itemLength)) { 2095 items.add(new GMTOffsetField(itemType, itemLength)); 2096 } else { 2097 invalidPattern = true; 2098 } 2099 } 2100 } 2101 2102 if (invalidPattern || checkBits.cardinality() != letters.length()) { 2103 throw new IllegalStateException("Bad localized GMT offset pattern: " + pattern); 2104 } 2105 2106 return items.toArray(new Object[items.size()]); 2107 } 2108 2109 /** 2110 * Appends seconds field to the offset pattern with hour/minute 2111 * 2112 * @param offsetHM the offset pattern including hours and minutes fields 2113 * @return the offset pattern including hours, minutes and seconds fields 2114 */ 2115 //TODO This code will be obsoleted once we add hour-minute-second pattern data in CLDR 2116 private static String expandOffsetPattern(String offsetHM) { 2117 int idx_mm = offsetHM.indexOf("mm"); 2118 if (idx_mm < 0) { 2119 throw new RuntimeException("Bad time zone hour pattern data"); 2120 } 2121 String sep = ":"; 2122 int idx_H = offsetHM.substring(0, idx_mm).lastIndexOf("H"); 2123 if (idx_H >= 0) { 2124 sep = offsetHM.substring(idx_H + 1, idx_mm); 2125 } 2126 return offsetHM.substring(0, idx_mm + 2) + sep + "ss" + offsetHM.substring(idx_mm + 2); 2127 } 2128 2129 /** 2130 * Truncates minutes field from the offset pattern with hour/minute 2131 * 2132 * @param offsetHM the offset pattern including hours and minutes fields 2133 * @return the offset pattern including only hours field 2134 */ 2135 //TODO This code will be obsoleted once we add hour pattern data in CLDR 2136 private static String truncateOffsetPattern(String offsetHM) { 2137 int idx_mm = offsetHM.indexOf("mm"); 2138 if (idx_mm < 0) { 2139 throw new RuntimeException("Bad time zone hour pattern data"); 2140 } 2141 int idx_HH = offsetHM.substring(0, idx_mm).lastIndexOf("HH"); 2142 if (idx_HH >= 0) { 2143 return offsetHM.substring(0, idx_HH + 2); 2144 } 2145 int idx_H = offsetHM.substring(0, idx_mm).lastIndexOf("H"); 2146 if (idx_H >= 0) { 2147 return offsetHM.substring(0, idx_H + 1); 2148 } 2149 throw new RuntimeException("Bad time zone hour pattern data"); 2150 } 2151 2152 /** 2153 * Appends localized digits to the buffer. 2154 * <p> 2155 * Note: This code assumes that the input number is 0 - 59 2156 * 2157 * @param buf the target buffer 2158 * @param n the integer number 2159 * @param minDigits the minimum digits width 2160 */ 2161 private void appendOffsetDigits(StringBuilder buf, int n, int minDigits) { 2162 assert(n >= 0 && n < 60); 2163 int numDigits = n >= 10 ? 2 : 1; 2164 for (int i = 0; i < minDigits - numDigits; i++) { 2165 buf.append(_gmtOffsetDigits[0]); 2166 } 2167 if (numDigits == 2) { 2168 buf.append(_gmtOffsetDigits[n / 10]); 2169 } 2170 buf.append(_gmtOffsetDigits[n % 10]); 2171 } 2172 2173 /** 2174 * Creates an instance of TimeZone for the given offset 2175 * @param offset the offset 2176 * @return A TimeZone with the given offset 2177 */ 2178 private TimeZone getTimeZoneForOffset(int offset) { 2179 if (offset == 0) { 2180 // when offset is 0, we should use "Etc/GMT" 2181 return TimeZone.getTimeZone(TZID_GMT); 2182 } 2183 return ZoneMeta.getCustomTimeZone(offset); 2184 } 2185 2186 /** 2187 * Returns offset from GMT(UTC) in milliseconds for the given localized GMT 2188 * offset format string. When the given string cannot be parsed, this method 2189 * sets the current position as the error index to <code>ParsePosition pos</code> 2190 * and returns 0. 2191 * 2192 * @param text the text contains a localized GMT offset string at the position. 2193 * @param pos the position. 2194 * @param isShort true if this parser to try the short format first 2195 * @param hasDigitOffset receiving if the parsed zone string contains offset digits. 2196 * @return the offset from GMT(UTC) in milliseconds for the given localized GMT 2197 * offset format string. 2198 */ 2199 private int parseOffsetLocalizedGMT(String text, ParsePosition pos, boolean isShort, Output<Boolean> hasDigitOffset) { 2200 int start = pos.getIndex(); 2201 int offset = 0; 2202 int[] parsedLength = {0}; 2203 2204 if (hasDigitOffset != null) { 2205 hasDigitOffset.value = false; 2206 } 2207 2208 offset = parseOffsetLocalizedGMTPattern(text, start, isShort, parsedLength); 2209 2210 // For now, parseOffsetLocalizedGMTPattern handles both long and short 2211 // formats, no matter isShort is true or false. This might be changed in future 2212 // when strict parsing is necessary, or different set of patterns are used for 2213 // short/long formats. 2214 // if (parsedLength[0] == 0) { 2215 // offset = parseOffsetLocalizedGMTPattern(text, start, !isShort, parsedLength); 2216 // } 2217 2218 if (parsedLength[0] > 0) { 2219 if (hasDigitOffset != null) { 2220 hasDigitOffset.value = true; 2221 } 2222 pos.setIndex(start + parsedLength[0]); 2223 return offset; 2224 } 2225 2226 // Try the default patterns 2227 offset = parseOffsetDefaultLocalizedGMT(text, start, parsedLength); 2228 if (parsedLength[0] > 0) { 2229 if (hasDigitOffset != null) { 2230 hasDigitOffset.value = true; 2231 } 2232 pos.setIndex(start + parsedLength[0]); 2233 return offset; 2234 } 2235 2236 // Check if this is a GMT zero format 2237 if (text.regionMatches(true, start, _gmtZeroFormat, 0, _gmtZeroFormat.length())) { 2238 pos.setIndex(start + _gmtZeroFormat.length()); 2239 return 0; 2240 } 2241 2242 // Check if this is a default GMT zero format 2243 for (String defGMTZero : ALT_GMT_STRINGS) { 2244 if (text.regionMatches(true, start, defGMTZero, 0, defGMTZero.length())) { 2245 pos.setIndex(start + defGMTZero.length()); 2246 return 0; 2247 } 2248 } 2249 2250 // Nothing matched 2251 pos.setErrorIndex(start); 2252 return 0; 2253 } 2254 2255 /** 2256 * Parse localized GMT format generated by the pattern used by this formatter, except 2257 * GMT Zero format. 2258 * @param text the input text 2259 * @param start the start index 2260 * @param isShort true if the short localized GMT format is parsed. 2261 * @param parsedLen the parsed length, or 0 on failure. 2262 * @return the parsed offset in milliseconds. 2263 */ 2264 private int parseOffsetLocalizedGMTPattern(String text, int start, boolean isShort, int[] parsedLen) { 2265 int idx = start; 2266 int offset = 0; 2267 boolean parsed = false; 2268 2269 do { 2270 // Prefix part 2271 int len = _gmtPatternPrefix.length(); 2272 if (len > 0 && !text.regionMatches(true, idx, _gmtPatternPrefix, 0, len)) { 2273 // prefix match failed 2274 break; 2275 } 2276 idx += len; 2277 2278 // Offset part 2279 int[] offsetLen = new int[1]; 2280 offset = parseOffsetFields(text, idx, false, offsetLen); 2281 if (offsetLen[0] == 0) { 2282 // offset field match failed 2283 break; 2284 } 2285 idx += offsetLen[0]; 2286 2287 // Suffix part 2288 len = _gmtPatternSuffix.length(); 2289 if (len > 0 && !text.regionMatches(true, idx, _gmtPatternSuffix, 0, len)) { 2290 // no suffix match 2291 break; 2292 } 2293 idx += len; 2294 parsed = true; 2295 } while (false); 2296 2297 parsedLen[0] = parsed ? idx - start : 0; 2298 return offset; 2299 } 2300 2301 /** 2302 * Parses localized GMT offset fields into offset. 2303 * 2304 * @param text the input text 2305 * @param start the start index 2306 * @param isShort true if this is a short format - currently not used 2307 * @param parsedLen the parsed length, or 0 on failure. 2308 * @return the parsed offset in milliseconds. 2309 */ 2310 private int parseOffsetFields(String text, int start, boolean isShort, int[] parsedLen) { 2311 int outLen = 0; 2312 int offset = 0; 2313 int sign = 1; 2314 2315 if (parsedLen != null && parsedLen.length >= 1) { 2316 parsedLen[0] = 0; 2317 } 2318 2319 int offsetH, offsetM, offsetS; 2320 offsetH = offsetM = offsetS = 0; 2321 2322 int[] fields = {0, 0, 0}; 2323 for (GMTOffsetPatternType gmtPatType : PARSE_GMT_OFFSET_TYPES) { 2324 Object[] items = _gmtOffsetPatternItems[gmtPatType.ordinal()]; 2325 assert items != null; 2326 2327 outLen = parseOffsetFieldsWithPattern(text, start, items, false, fields); 2328 if (outLen > 0) { 2329 sign = gmtPatType.isPositive() ? 1 : -1; 2330 offsetH = fields[0]; 2331 offsetM = fields[1]; 2332 offsetS = fields[2]; 2333 break; 2334 } 2335 } 2336 if (outLen > 0 && _abuttingOffsetHoursAndMinutes) { 2337 // When hours field is abutting minutes field, 2338 // the parse result above may not be appropriate. 2339 // For example, "01020" is parsed as 01:02 above, 2340 // but it should be parsed as 00:10:20. 2341 int tmpLen = 0; 2342 int tmpSign = 1; 2343 for (GMTOffsetPatternType gmtPatType : PARSE_GMT_OFFSET_TYPES) { 2344 Object[] items = _gmtOffsetPatternItems[gmtPatType.ordinal()]; 2345 assert items != null; 2346 2347 // forcing parse to use single hour digit 2348 tmpLen = parseOffsetFieldsWithPattern(text, start, items, true, fields); 2349 if (tmpLen > 0) { 2350 tmpSign = gmtPatType.isPositive() ? 1 : -1; 2351 break; 2352 } 2353 } 2354 if (tmpLen > outLen) { 2355 // Better parse result with single hour digit 2356 outLen = tmpLen; 2357 sign = tmpSign; 2358 offsetH = fields[0]; 2359 offsetM = fields[1]; 2360 offsetS = fields[2]; 2361 } 2362 } 2363 2364 if (parsedLen != null && parsedLen.length >= 1) { 2365 parsedLen[0] = outLen; 2366 } 2367 2368 if (outLen > 0) { 2369 offset = ((((offsetH * 60) + offsetM) * 60) + offsetS) * 1000 * sign; 2370 } 2371 2372 return offset; 2373 } 2374 2375 /** 2376 * Parses localized GMT offset fields with the given pattern 2377 * 2378 * @param text the input text 2379 * @param start the start index 2380 * @param patternItems the pattern (already itemized) 2381 * @param forceSingleHourDigit true if hours field is parsed as a single digit 2382 * @param fields receives the parsed hours/minutes/seconds 2383 * @return parsed length 2384 */ 2385 private int parseOffsetFieldsWithPattern(String text, int start, Object[] patternItems, boolean forceSingleHourDigit, int fields[]) { 2386 assert (fields != null && fields.length >= 3); 2387 fields[0] = fields[1] = fields[2] = 0; 2388 2389 boolean failed = false; 2390 int offsetH, offsetM, offsetS; 2391 offsetH = offsetM = offsetS = 0; 2392 int idx = start; 2393 int[] tmpParsedLen = {0}; 2394 for (int i = 0; i < patternItems.length; i++) { 2395 if (patternItems[i] instanceof String) { 2396 String patStr = (String)patternItems[i]; 2397 int len = patStr.length(); 2398 if (!text.regionMatches(true, idx, patStr, 0, len)) { 2399 failed = true; 2400 break; 2401 } 2402 idx += len; 2403 } else { 2404 assert(patternItems[i] instanceof GMTOffsetField); 2405 GMTOffsetField field = (GMTOffsetField)patternItems[i]; 2406 char fieldType = field.getType(); 2407 if (fieldType == 'H') { 2408 int maxDigits = forceSingleHourDigit ? 1 : 2; 2409 offsetH = parseOffsetFieldWithLocalizedDigits(text, idx, 1, maxDigits, 0, MAX_OFFSET_HOUR, tmpParsedLen); 2410 } else if (fieldType == 'm') { 2411 offsetM = parseOffsetFieldWithLocalizedDigits(text, idx, 2, 2, 0, MAX_OFFSET_MINUTE, tmpParsedLen); 2412 } else if (fieldType == 's') { 2413 offsetS = parseOffsetFieldWithLocalizedDigits(text, idx, 2, 2, 0, MAX_OFFSET_SECOND, tmpParsedLen); 2414 } 2415 2416 if (tmpParsedLen[0] == 0) { 2417 failed = true; 2418 break; 2419 } 2420 idx += tmpParsedLen[0]; 2421 } 2422 } 2423 2424 if (failed) { 2425 return 0; 2426 } 2427 2428 fields[0] = offsetH; 2429 fields[1] = offsetM; 2430 fields[2] = offsetS; 2431 2432 return idx - start; 2433 } 2434 2435 /** 2436 * Parses the input text using the default format patterns (e.g. "UTC{0}"). 2437 * @param text the input text 2438 * @param start the start index 2439 * @param parsedLen the parsed length, or 0 on failure 2440 * @return the parsed offset in milliseconds. 2441 */ 2442 private int parseOffsetDefaultLocalizedGMT(String text, int start, int[] parsedLen) { 2443 int idx = start; 2444 int offset = 0; 2445 int parsed = 0; 2446 do { 2447 // check global default GMT alternatives 2448 int gmtLen = 0; 2449 for (String gmt : ALT_GMT_STRINGS) { 2450 int len = gmt.length(); 2451 if (text.regionMatches(true, idx, gmt, 0, len)) { 2452 gmtLen = len; 2453 break; 2454 } 2455 } 2456 if (gmtLen == 0) { 2457 break; 2458 } 2459 idx += gmtLen; 2460 2461 // offset needs a sign char and a digit at minimum 2462 if (idx + 1 >= text.length()) { 2463 break; 2464 } 2465 2466 // parse sign 2467 int sign = 1; 2468 char c = text.charAt(idx); 2469 if (c == '+') { 2470 sign = 1; 2471 } else if (c == '-') { 2472 sign = -1; 2473 } else { 2474 break; 2475 } 2476 idx++; 2477 2478 // offset part 2479 // try the default pattern with the separator first 2480 int[] lenWithSep = {0}; 2481 int offsetWithSep = parseDefaultOffsetFields(text, idx, DEFAULT_GMT_OFFSET_SEP, lenWithSep); 2482 if (lenWithSep[0] == text.length() - idx) { 2483 // maximum match 2484 offset = offsetWithSep * sign; 2485 idx += lenWithSep[0]; 2486 } else { 2487 // try abutting field pattern 2488 int[] lenAbut = {0}; 2489 int offsetAbut = parseAbuttingOffsetFields(text, idx, lenAbut); 2490 2491 if (lenWithSep[0] > lenAbut[0]) { 2492 offset = offsetWithSep * sign; 2493 idx += lenWithSep[0]; 2494 } else { 2495 offset = offsetAbut * sign; 2496 idx += lenAbut[0]; 2497 } 2498 } 2499 parsed = idx - start; 2500 } while (false); 2501 2502 parsedLen[0] = parsed; 2503 return offset; 2504 } 2505 2506 /** 2507 * Parses the input GMT offset fields with the default offset pattern. 2508 * @param text the input text 2509 * @param start the start index 2510 * @param separator the separator character, e.g. ':' 2511 * @param parsedLen the parsed length, or 0 on failure. 2512 * @return the parsed offset in milliseconds. 2513 */ 2514 private int parseDefaultOffsetFields(String text, int start, char separator, int[] parsedLen) { 2515 int max = text.length(); 2516 int idx = start; 2517 int[] len = {0}; 2518 int hour = 0, min = 0, sec = 0; 2519 2520 do { 2521 hour = parseOffsetFieldWithLocalizedDigits(text, idx, 1, 2, 0, MAX_OFFSET_HOUR, len); 2522 if (len[0] == 0) { 2523 break; 2524 } 2525 idx += len[0]; 2526 2527 if (idx + 1 < max && text.charAt(idx) == separator) { 2528 min = parseOffsetFieldWithLocalizedDigits(text, idx + 1, 2, 2, 0, MAX_OFFSET_MINUTE, len); 2529 if (len[0] == 0) { 2530 break; 2531 } 2532 idx += (1 + len[0]); 2533 2534 if (idx + 1 < max && text.charAt(idx) == separator) { 2535 sec = parseOffsetFieldWithLocalizedDigits(text, idx + 1, 2, 2, 0, MAX_OFFSET_SECOND, len); 2536 if (len[0] == 0) { 2537 break; 2538 } 2539 idx += (1 + len[0]); 2540 } 2541 } 2542 } while (false); 2543 2544 if (idx == start) { 2545 parsedLen[0] = 0; 2546 return 0; 2547 } 2548 2549 parsedLen[0] = idx - start; 2550 return hour * MILLIS_PER_HOUR + min * MILLIS_PER_MINUTE + sec * MILLIS_PER_SECOND; 2551 } 2552 2553 /** 2554 * Parses abutting localized GMT offset fields (such as 0800) into offset. 2555 * @param text the input text 2556 * @param start the start index 2557 * @param parsedLen the parsed length, or 0 on failure 2558 * @return the parsed offset in milliseconds. 2559 */ 2560 private int parseAbuttingOffsetFields(String text, int start, int[] parsedLen) { 2561 final int MAXDIGITS = 6; 2562 int[] digits = new int[MAXDIGITS]; 2563 int[] parsed = new int[MAXDIGITS]; // accumulative offsets 2564 2565 // Parse digits into int[] 2566 int idx = start; 2567 int[] len = {0}; 2568 int numDigits = 0; 2569 for (int i = 0; i < MAXDIGITS; i++) { 2570 digits[i] = parseSingleLocalizedDigit(text, idx, len); 2571 if (digits[i] < 0) { 2572 break; 2573 } 2574 idx += len[0]; 2575 parsed[i] = idx - start; 2576 numDigits++; 2577 } 2578 2579 if (numDigits == 0) { 2580 parsedLen[0] = 0; 2581 return 0; 2582 } 2583 2584 int offset = 0; 2585 while (numDigits > 0) { 2586 int hour = 0; 2587 int min = 0; 2588 int sec = 0; 2589 2590 assert(numDigits > 0 && numDigits <= 6); 2591 switch (numDigits) { 2592 case 1: // H 2593 hour = digits[0]; 2594 break; 2595 case 2: // HH 2596 hour = digits[0] * 10 + digits[1]; 2597 break; 2598 case 3: // Hmm 2599 hour = digits[0]; 2600 min = digits[1] * 10 + digits[2]; 2601 break; 2602 case 4: // HHmm 2603 hour = digits[0] * 10 + digits[1]; 2604 min = digits[2] * 10 + digits[3]; 2605 break; 2606 case 5: // Hmmss 2607 hour = digits[0]; 2608 min = digits[1] * 10 + digits[2]; 2609 sec = digits[3] * 10 + digits[4]; 2610 break; 2611 case 6: // HHmmss 2612 hour = digits[0] * 10 + digits[1]; 2613 min = digits[2] * 10 + digits[3]; 2614 sec = digits[4] * 10 + digits[5]; 2615 break; 2616 } 2617 if (hour <= MAX_OFFSET_HOUR && min <= MAX_OFFSET_MINUTE && sec <= MAX_OFFSET_SECOND) { 2618 // found a valid combination 2619 offset = hour * MILLIS_PER_HOUR + min * MILLIS_PER_MINUTE + sec * MILLIS_PER_SECOND; 2620 parsedLen[0] = parsed[numDigits - 1]; 2621 break; 2622 } 2623 numDigits--; 2624 } 2625 return offset; 2626 } 2627 2628 /** 2629 * Reads an offset field value. This method will stop parsing when 2630 * 1) number of digits reaches <code>maxDigits</code> 2631 * 2) just before already parsed number exceeds <code>maxVal</code> 2632 * 2633 * @param text the text 2634 * @param start the start offset 2635 * @param minDigits the minimum number of required digits 2636 * @param maxDigits the maximum number of digits 2637 * @param minVal the minimum value 2638 * @param maxVal the maximum value 2639 * @param parsedLen the actual parsed length is set to parsedLen[0], must not be null. 2640 * @return the integer value parsed 2641 */ 2642 private int parseOffsetFieldWithLocalizedDigits(String text, int start, int minDigits, int maxDigits, 2643 int minVal, int maxVal, int[] parsedLen) { 2644 2645 parsedLen[0] = 0; 2646 2647 int decVal = 0; 2648 int numDigits = 0; 2649 int idx = start; 2650 int[] digitLen = {0}; 2651 while (idx < text.length() && numDigits < maxDigits) { 2652 int digit = parseSingleLocalizedDigit(text, idx, digitLen); 2653 if (digit < 0) { 2654 break; 2655 } 2656 int tmpVal = decVal * 10 + digit; 2657 if (tmpVal > maxVal) { 2658 break; 2659 } 2660 decVal = tmpVal; 2661 numDigits++; 2662 idx += digitLen[0]; 2663 } 2664 2665 // Note: maxVal is checked in the while loop 2666 if (numDigits < minDigits || decVal < minVal) { 2667 decVal = -1; 2668 numDigits = 0; 2669 } else { 2670 parsedLen[0] = idx - start; 2671 } 2672 2673 2674 return decVal; 2675 } 2676 2677 /** 2678 * Reads a single decimal digit, either localized digits used by this object 2679 * or any Unicode numeric character. 2680 * @param text the text 2681 * @param start the start index 2682 * @param len the actual length read from the text 2683 * the start index is not a decimal number. 2684 * @return the integer value of the parsed digit, or -1 on failure. 2685 */ 2686 private int parseSingleLocalizedDigit(String text, int start, int[] len) { 2687 int digit = -1; 2688 len[0] = 0; 2689 if (start < text.length()) { 2690 int cp = Character.codePointAt(text, start); 2691 2692 // First, try digits configured for this instance 2693 for (int i = 0; i < _gmtOffsetDigits.length; i++) { 2694 if (cp == _gmtOffsetDigits[i].codePointAt(0)) { 2695 digit = i; 2696 break; 2697 } 2698 } 2699 // If failed, check if this is a Unicode digit 2700 if (digit < 0) { 2701 digit = UCharacter.digit(cp); 2702 } 2703 2704 if (digit >= 0) { 2705 len[0] = Character.charCount(cp); 2706 } 2707 } 2708 return digit; 2709 } 2710 2711 /** 2712 * Break input String into String[]. Each array element represents 2713 * a code point. This method is used for parsing localized digit 2714 * characters and support characters in Unicode supplemental planes. 2715 * 2716 * @param str the string 2717 * @return the array of code points in String[] 2718 */ 2719 private static String[] toCodePoints(String str) { 2720 int len = str.codePointCount(0, str.length()); 2721 String[] codePoints = new String[len]; 2722 2723 for (int i = 0, offset = 0; i < len; i++) { 2724 int code = str.codePointAt(offset); 2725 int codeLen = Character.charCount(code); 2726 codePoints[i] = str.substring(offset, offset + codeLen); 2727 offset += codeLen; 2728 } 2729 return codePoints; 2730 } 2731 2732 2733 /** 2734 * Returns offset from GMT(UTC) in milliseconds for the given ISO 8601 time zone string 2735 * (basic format, extended format, or UTC indicator). When the given string is not an ISO 8601 time 2736 * zone string, this method sets the current position as the error index 2737 * to <code>ParsePosition pos</code> and returns 0. 2738 * 2739 * @param text the text contains ISO 8601 style time zone string (e.g. "-08", "-08:00", "Z") 2740 * at the position. 2741 * @param pos the position. 2742 * @param extendedOnly <code>true</code> if parsing the text as ISO 8601 extended offset format (e.g. "-08:00"), 2743 * or <code>false</code> to evaluate the text as basic format. 2744 * @param hasDigitOffset receiving if the parsed zone string contains offset digits. 2745 * @return the offset from GMT(UTC) in milliseconds for the given ISO 8601 style 2746 * time zone string. 2747 */ 2748 private static int parseOffsetISO8601(String text, ParsePosition pos, boolean extendedOnly, Output<Boolean> hasDigitOffset) { 2749 if (hasDigitOffset != null) { 2750 hasDigitOffset.value = false; 2751 } 2752 int start = pos.getIndex(); 2753 if (start >= text.length()) { 2754 pos.setErrorIndex(start); 2755 return 0; 2756 } 2757 2758 char firstChar = text.charAt(start); 2759 if (Character.toUpperCase(firstChar) == ISO8601_UTC.charAt(0)) { 2760 // "Z" - indicates UTC 2761 pos.setIndex(start + 1); 2762 return 0; 2763 } 2764 2765 int sign; 2766 if (firstChar == '+') { 2767 sign = 1; 2768 } else if (firstChar == '-') { 2769 sign = -1; 2770 } else { 2771 // Not an ISO 8601 offset string 2772 pos.setErrorIndex(start); 2773 return 0; 2774 } 2775 ParsePosition posOffset = new ParsePosition(start + 1); 2776 int offset = parseAsciiOffsetFields(text, posOffset, ':', OffsetFields.H, OffsetFields.HMS); 2777 if (posOffset.getErrorIndex() == -1 && !extendedOnly && (posOffset.getIndex() - start <= 3)) { 2778 // If the text is successfully parsed as extended format with the options above, it can be also parsed 2779 // as basic format. For example, "0230" can be parsed as offset 2:00 (only first digits are valid for 2780 // extended format), but it can be parsed as offset 2:30 with basic format. We use longer result. 2781 ParsePosition posBasic = new ParsePosition(start + 1); 2782 int tmpOffset = parseAbuttingAsciiOffsetFields(text, posBasic, OffsetFields.H, OffsetFields.HMS, false); 2783 if (posBasic.getErrorIndex() == -1 && posBasic.getIndex() > posOffset.getIndex()) { 2784 offset = tmpOffset; 2785 posOffset.setIndex(posBasic.getIndex()); 2786 } 2787 } 2788 2789 if (posOffset.getErrorIndex() != -1) { 2790 pos.setErrorIndex(start); 2791 return 0; 2792 } 2793 2794 pos.setIndex(posOffset.getIndex()); 2795 if (hasDigitOffset != null) { 2796 hasDigitOffset.value = true; 2797 } 2798 return sign * offset; 2799 } 2800 2801 /** 2802 * Parses offset represented by contiguous ASCII digits 2803 * <p> 2804 * Note: This method expects the input position is already at the start of 2805 * ASCII digits and does not parse sign (+/-). 2806 * 2807 * @param text The text contains a sequence of ASCII digits 2808 * @param pos The parse position 2809 * @param minFields The minimum Fields to be parsed 2810 * @param maxFields The maximum Fields to be parsed 2811 * @param fixedHourWidth true if hours field must be width of 2 2812 * @return Parsed offset, 0 or positive number. 2813 */ 2814 private static int parseAbuttingAsciiOffsetFields(String text, ParsePosition pos, 2815 OffsetFields minFields, OffsetFields maxFields, boolean fixedHourWidth) { 2816 int start = pos.getIndex(); 2817 2818 int minDigits = 2 * (minFields.ordinal() + 1) - (fixedHourWidth ? 0 : 1); 2819 int maxDigits = 2 * (maxFields.ordinal() + 1); 2820 2821 int[] digits = new int[maxDigits]; 2822 int numDigits = 0; 2823 int idx = start; 2824 while (numDigits < digits.length && idx < text.length()) { 2825 int digit = ASCII_DIGITS.indexOf(text.charAt(idx)); 2826 if (digit < 0) { 2827 break; 2828 } 2829 digits[numDigits] = digit; 2830 numDigits++; 2831 idx++; 2832 } 2833 2834 if (fixedHourWidth && ((numDigits & 1) != 0)) { 2835 // Fixed digits, so the number of digits must be even number. Truncating. 2836 numDigits--; 2837 } 2838 2839 if (numDigits < minDigits) { 2840 pos.setErrorIndex(start); 2841 return 0; 2842 } 2843 2844 int hour = 0, min = 0, sec = 0; 2845 boolean bParsed = false; 2846 while (numDigits >= minDigits) { 2847 switch (numDigits) { 2848 case 1: //H 2849 hour = digits[0]; 2850 break; 2851 case 2: //HH 2852 hour = digits[0] * 10 + digits[1]; 2853 break; 2854 case 3: //Hmm 2855 hour = digits[0]; 2856 min = digits[1] * 10 + digits[2]; 2857 break; 2858 case 4: //HHmm 2859 hour = digits[0] * 10 + digits[1]; 2860 min = digits[2] * 10 + digits[3]; 2861 break; 2862 case 5: //Hmmss 2863 hour = digits[0]; 2864 min = digits[1] * 10 + digits[2]; 2865 sec = digits[3] * 10 + digits[4]; 2866 break; 2867 case 6: //HHmmss 2868 hour = digits[0] * 10 + digits[1]; 2869 min = digits[2] * 10 + digits[3]; 2870 sec = digits[4] * 10 + digits[5]; 2871 break; 2872 } 2873 2874 if (hour <= MAX_OFFSET_HOUR && min <= MAX_OFFSET_MINUTE && sec <= MAX_OFFSET_SECOND) { 2875 // Successfully parsed 2876 bParsed = true; 2877 break; 2878 } 2879 2880 // Truncating 2881 numDigits -= (fixedHourWidth ? 2 : 1); 2882 hour = min = sec = 0; 2883 } 2884 2885 if (!bParsed) { 2886 pos.setErrorIndex(start); 2887 return 0; 2888 } 2889 pos.setIndex(start + numDigits); 2890 return ((((hour * 60) + min) * 60) + sec) * 1000; 2891 } 2892 2893 /** 2894 * Parses offset represented by ASCII digits and separators. 2895 * <p> 2896 * Note: This method expects the input position is already at the start of 2897 * ASCII digits and does not parse sign (+/-). 2898 * 2899 * @param text The text 2900 * @param pos The parse position 2901 * @param sep The separator character 2902 * @param minFields The minimum Fields to be parsed 2903 * @param maxFields The maximum Fields to be parsed 2904 * @return Parsed offset, 0 or positive number. 2905 */ 2906 private static int parseAsciiOffsetFields(String text, ParsePosition pos, char sep, 2907 OffsetFields minFields, OffsetFields maxFields) { 2908 int start = pos.getIndex(); 2909 int[] fieldVal = {0, 0, 0}; 2910 int[] fieldLen = {0, -1, -1}; 2911 for (int idx = start, fieldIdx = 0; idx < text.length() && fieldIdx <= maxFields.ordinal(); idx++) { 2912 char c = text.charAt(idx); 2913 if (c == sep) { 2914 if (fieldIdx == 0) { 2915 if (fieldLen[0] == 0) { 2916 // no hours field 2917 break; 2918 } 2919 // 1 digit hour, move to next field 2920 fieldIdx++; 2921 } else { 2922 if (fieldLen[fieldIdx] != -1) { 2923 // premature minutes or seconds field 2924 break; 2925 } 2926 fieldLen[fieldIdx] = 0; 2927 } 2928 continue; 2929 } else if (fieldLen[fieldIdx] == -1) { 2930 // no separator after 2 digit field 2931 break; 2932 } 2933 int digit = ASCII_DIGITS.indexOf(c); 2934 if (digit < 0) { 2935 // not a digit 2936 break; 2937 } 2938 fieldVal[fieldIdx] = fieldVal[fieldIdx] * 10 + digit; 2939 fieldLen[fieldIdx]++; 2940 if (fieldLen[fieldIdx] >= 2) { 2941 // parsed 2 digits, move to next field 2942 fieldIdx++; 2943 } 2944 } 2945 2946 int offset = 0; 2947 int parsedLen = 0; 2948 OffsetFields parsedFields = null; 2949 do { 2950 // hour 2951 if (fieldLen[0] == 0) { 2952 break; 2953 } 2954 if (fieldVal[0] > MAX_OFFSET_HOUR) { 2955 offset = (fieldVal[0] / 10) * MILLIS_PER_HOUR; 2956 parsedFields = OffsetFields.H; 2957 parsedLen = 1; 2958 break; 2959 } 2960 offset = fieldVal[0] * MILLIS_PER_HOUR; 2961 parsedLen = fieldLen[0]; 2962 parsedFields = OffsetFields.H; 2963 2964 // minute 2965 if (fieldLen[1] != 2 || fieldVal[1] > MAX_OFFSET_MINUTE) { 2966 break; 2967 } 2968 offset += fieldVal[1] * MILLIS_PER_MINUTE; 2969 parsedLen += (1 + fieldLen[1]); 2970 parsedFields = OffsetFields.HM; 2971 2972 // second 2973 if (fieldLen[2] != 2 || fieldVal[2] > MAX_OFFSET_SECOND) { 2974 break; 2975 } 2976 offset += fieldVal[2] * MILLIS_PER_SECOND; 2977 parsedLen += (1 + fieldLen[2]); 2978 parsedFields = OffsetFields.HMS; 2979 } while (false); 2980 2981 if (parsedFields == null || parsedFields.ordinal() < minFields.ordinal()) { 2982 pos.setErrorIndex(start); 2983 return 0; 2984 } 2985 2986 pos.setIndex(start + parsedLen); 2987 return offset; 2988 } 2989 2990 /** 2991 * Parse a zone ID. 2992 * @param text the text contains a time zone ID string at the position. 2993 * @param pos the position. 2994 * @return The zone ID parsed. 2995 */ 2996 private static String parseZoneID(String text, ParsePosition pos) { 2997 String resolvedID = null; 2998 if (ZONE_ID_TRIE == null) { 2999 synchronized (TimeZoneFormat.class) { 3000 if (ZONE_ID_TRIE == null) { 3001 // Build zone ID trie 3002 TextTrieMap<String> trie = new TextTrieMap<String>(true); 3003 String[] ids = TimeZone.getAvailableIDs(); 3004 for (String id : ids) { 3005 trie.put(id, id); 3006 } 3007 ZONE_ID_TRIE = trie; 3008 } 3009 } 3010 } 3011 3012 int[] matchLen = new int[] {0}; 3013 Iterator<String> itr = ZONE_ID_TRIE.get(text, pos.getIndex(), matchLen); 3014 if (itr != null) { 3015 resolvedID = itr.next(); 3016 pos.setIndex(pos.getIndex() + matchLen[0]); 3017 } else { 3018 // TODO 3019 // We many need to handle rule based custom zone ID (See ZoneMeta.parseCustomID), 3020 // such as GM+05:00. However, the public parse method in this class also calls 3021 // parseOffsetLocalizedGMT and custom zone IDs are likely supported by the parser, 3022 // so we might not need to handle them here. 3023 pos.setErrorIndex(pos.getIndex()); 3024 } 3025 return resolvedID; 3026 } 3027 3028 /** 3029 * Parse a short zone ID. 3030 * @param text the text contains a time zone ID string at the position. 3031 * @param pos the position. 3032 * @return The zone ID for the parsed short zone ID. 3033 */ 3034 private static String parseShortZoneID(String text, ParsePosition pos) { 3035 String resolvedID = null; 3036 if (SHORT_ZONE_ID_TRIE == null) { 3037 synchronized (TimeZoneFormat.class) { 3038 if (SHORT_ZONE_ID_TRIE == null) { 3039 // Build short zone ID trie 3040 TextTrieMap<String> trie = new TextTrieMap<String>(true); 3041 Set<String> canonicalIDs = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL, null, null); 3042 for (String id : canonicalIDs) { 3043 String shortID = ZoneMeta.getShortID(id); 3044 if (shortID != null) { 3045 trie.put(shortID, id); 3046 } 3047 } 3048 // Canonical list does not contain Etc/Unknown 3049 trie.put(UNKNOWN_SHORT_ZONE_ID, UNKNOWN_ZONE_ID); 3050 SHORT_ZONE_ID_TRIE = trie; 3051 } 3052 } 3053 } 3054 3055 int[] matchLen = new int[] {0}; 3056 Iterator<String> itr = SHORT_ZONE_ID_TRIE.get(text, pos.getIndex(), matchLen); 3057 if (itr != null) { 3058 resolvedID = itr.next(); 3059 pos.setIndex(pos.getIndex() + matchLen[0]); 3060 } else { 3061 pos.setErrorIndex(pos.getIndex()); 3062 } 3063 3064 return resolvedID; 3065 } 3066 3067 /** 3068 * Parse an exemplar location string. 3069 * @param text the text contains an exemplar location string at the position. 3070 * @param pos the position. 3071 * @return The zone ID for the parsed exemplar location. 3072 */ 3073 private String parseExemplarLocation(String text, ParsePosition pos) { 3074 int startIdx = pos.getIndex(); 3075 int parsedPos = -1; 3076 String tzID = null; 3077 3078 EnumSet<NameType> nameTypes = EnumSet.of(NameType.EXEMPLAR_LOCATION); 3079 Collection<MatchInfo> exemplarMatches = _tznames.find(text, startIdx, nameTypes); 3080 if (exemplarMatches != null) { 3081 MatchInfo exemplarMatch = null; 3082 for (MatchInfo match : exemplarMatches) { 3083 if (startIdx + match.matchLength() > parsedPos) { 3084 exemplarMatch = match; 3085 parsedPos = startIdx + match.matchLength(); 3086 } 3087 } 3088 if (exemplarMatch != null) { 3089 tzID = getTimeZoneID(exemplarMatch.tzID(), exemplarMatch.mzID()); 3090 pos.setIndex(parsedPos); 3091 } 3092 } 3093 if (tzID == null) { 3094 pos.setErrorIndex(startIdx); 3095 } 3096 3097 return tzID; 3098 } 3099 3100 /** 3101 * Implements <code>TimeZoneFormat</code> object cache 3102 */ 3103 private static class TimeZoneFormatCache extends SoftCache<ULocale, TimeZoneFormat, ULocale> { 3104 3105 /* (non-Javadoc) 3106 * @see com.ibm.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object) 3107 */ 3108 @Override 3109 protected TimeZoneFormat createInstance(ULocale key, ULocale data) { 3110 TimeZoneFormat fmt = new TimeZoneFormat(data); 3111 fmt.freeze(); 3112 return fmt; 3113 } 3114 } 3115 3116 // ---------------------------------- 3117 // Serialization stuff 3118 //----------------------------------- 3119 3120 /** 3121 * @serialField _locale ULocale The locale of this TimeZoneFormat object. 3122 * @serialField _tznames TimeZoneNames The time zone name data. 3123 * @serialField _gmtPattern String The pattern string for localized GMT format. 3124 * @serialField _gmtOffsetPatterns Stirng[] The array of GMT offset patterns used by localized GMT format 3125 * (positive hour-min, positive hour-min-sec, negative hour-min, negative hour-min-sec). 3126 * @serialField _gmtOffsetDigits String[] The array of decimal digits used by localized GMT format 3127 * (the size of array is 10). 3128 * @serialField _gmtZeroFormat String The localized GMT string used for GMT(UTC). 3129 * @serialField _parseAllStyles boolean <code>true</code> if this TimeZoneFormat object is configure 3130 * for parsing all available names. 3131 */ 3132 private static final ObjectStreamField[] serialPersistentFields = { 3133 new ObjectStreamField("_locale", ULocale.class), 3134 new ObjectStreamField("_tznames", TimeZoneNames.class), 3135 new ObjectStreamField("_gmtPattern", String.class), 3136 new ObjectStreamField("_gmtOffsetPatterns", String[].class), 3137 new ObjectStreamField("_gmtOffsetDigits", String[].class), 3138 new ObjectStreamField("_gmtZeroFormat", String.class), 3139 new ObjectStreamField("_parseAllStyles", boolean.class), 3140 }; 3141 3142 /** 3143 * 3144 * @param oos the object output stream 3145 * @throws IOException 3146 */ 3147 private void writeObject(ObjectOutputStream oos) throws IOException { 3148 ObjectOutputStream.PutField fields = oos.putFields(); 3149 3150 fields.put("_locale", _locale); 3151 fields.put("_tznames", _tznames); 3152 fields.put("_gmtPattern", _gmtPattern); 3153 fields.put("_gmtOffsetPatterns", _gmtOffsetPatterns); 3154 fields.put("_gmtOffsetDigits", _gmtOffsetDigits); 3155 fields.put("_gmtZeroFormat", _gmtZeroFormat); 3156 fields.put("_parseAllStyles", _parseAllStyles); 3157 3158 oos.writeFields(); 3159 } 3160 3161 /** 3162 * 3163 * @param ois the object input stream 3164 * @throws ClassNotFoundException 3165 * @throws IOException 3166 */ 3167 private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { 3168 ObjectInputStream.GetField fields = ois.readFields(); 3169 3170 _locale = (ULocale)fields.get("_locale", null); 3171 if (_locale == null) { 3172 throw new InvalidObjectException("Missing field: locale"); 3173 } 3174 3175 _tznames = (TimeZoneNames)fields.get("_tznames", null); 3176 if (_tznames == null) { 3177 throw new InvalidObjectException("Missing field: tznames"); 3178 } 3179 3180 _gmtPattern = (String)fields.get("_gmtPattern", null); 3181 if (_gmtPattern == null) { 3182 throw new InvalidObjectException("Missing field: gmtPattern"); 3183 } 3184 3185 String[] tmpGmtOffsetPatterns = (String[])fields.get("_gmtOffsetPatterns", null); 3186 if (tmpGmtOffsetPatterns == null) { 3187 throw new InvalidObjectException("Missing field: gmtOffsetPatterns"); 3188 } else if (tmpGmtOffsetPatterns.length < 4) { 3189 throw new InvalidObjectException("Incompatible field: gmtOffsetPatterns"); 3190 } 3191 _gmtOffsetPatterns = new String[6]; 3192 if (tmpGmtOffsetPatterns.length == 4) { 3193 for (int i = 0; i < 4; i++) { 3194 _gmtOffsetPatterns[i] = tmpGmtOffsetPatterns[i]; 3195 } 3196 _gmtOffsetPatterns[GMTOffsetPatternType.POSITIVE_H.ordinal()] = truncateOffsetPattern(_gmtOffsetPatterns[GMTOffsetPatternType.POSITIVE_HM.ordinal()]); 3197 _gmtOffsetPatterns[GMTOffsetPatternType.NEGATIVE_H.ordinal()] = truncateOffsetPattern(_gmtOffsetPatterns[GMTOffsetPatternType.NEGATIVE_HM.ordinal()]); 3198 } else { 3199 _gmtOffsetPatterns = tmpGmtOffsetPatterns; 3200 } 3201 3202 _gmtOffsetDigits = (String[])fields.get("_gmtOffsetDigits", null); 3203 if (_gmtOffsetDigits == null) { 3204 throw new InvalidObjectException("Missing field: gmtOffsetDigits"); 3205 } else if (_gmtOffsetDigits.length != 10) { 3206 throw new InvalidObjectException("Incompatible field: gmtOffsetDigits"); 3207 } 3208 3209 _gmtZeroFormat = (String)fields.get("_gmtZeroFormat", null); 3210 if (_gmtZeroFormat == null) { 3211 throw new InvalidObjectException("Missing field: gmtZeroFormat"); 3212 } 3213 3214 _parseAllStyles = fields.get("_parseAllStyles", false); 3215 if (fields.defaulted("_parseAllStyles")) { 3216 throw new InvalidObjectException("Missing field: parseAllStyles"); 3217 } 3218 3219 // Optimization for TimeZoneNames 3220 // 3221 // Note: 3222 // 3223 // com.ibm.icu.impl.TimeZoneNamesImpl is a read-only object initialized 3224 // by locale only. But it loads time zone names from resource bundles and 3225 // builds trie for parsing. We want to keep TimeZoneNamesImpl as singleton 3226 // per locale. We cannot do this for custom TimeZoneNames provided by user. 3227 // 3228 // com.ibm.icu.impl.TimeZoneGenericNames is a runtime generated object 3229 // initialized by ULocale and TimeZoneNames. Like TimeZoneNamesImpl, it 3230 // also composes time zone names and trie for parsing. We also want to keep 3231 // TimeZoneGenericNames as siongleton per locale. If TimeZoneNames is 3232 // actually a TimeZoneNamesImpl, we can reuse cached TimeZoneGenericNames 3233 // instance. 3234 if (_tznames instanceof TimeZoneNamesImpl) { 3235 _tznames = TimeZoneNames.getInstance(_locale); 3236 _gnames = null; // will be created by _locale later when necessary 3237 } else { 3238 // Custom TimeZoneNames implementation is used. We need to create 3239 // a new instance of TimeZoneGenericNames here. 3240 _gnames = new TimeZoneGenericNames(_locale, _tznames); 3241 } 3242 3243 // Transient fields requiring initialization 3244 initGMTPattern(_gmtPattern); 3245 initGMTOffsetPatterns(_gmtOffsetPatterns); 3246 3247 } 3248 3249 // ---------------------------------- 3250 // Freezable stuff 3251 //----------------------------------- 3252 3253 /** 3254 * {@inheritDoc} 3255 * @stable ICU 49 3256 */ 3257 public boolean isFrozen() { 3258 return _frozen; 3259 } 3260 3261 /** 3262 * {@inheritDoc} 3263 * @stable ICU 49 3264 */ 3265 public TimeZoneFormat freeze() { 3266 _frozen = true; 3267 return this; 3268 } 3269 3270 /** 3271 * {@inheritDoc} 3272 * @stable ICU 49 3273 */ 3274 public TimeZoneFormat cloneAsThawed() { 3275 TimeZoneFormat copy = (TimeZoneFormat)super.clone(); 3276 copy._frozen = false; 3277 return copy; 3278 } 3279 } 3280 3281