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