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