1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * Copyright (c) 1996, 2013, Oracle and/or its affiliates. All rights reserved. 4 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 5 * 6 * This code is free software; you can redistribute it and/or modify it 7 * under the terms of the GNU General Public License version 2 only, as 8 * published by the Free Software Foundation. Oracle designates this 9 * particular file as subject to the "Classpath" exception as provided 10 * by Oracle in the LICENSE file that accompanied this code. 11 * 12 * This code is distributed in the hope that it will be useful, but WITHOUT 13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 14 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 15 * version 2 for more details (a copy is included in the LICENSE file that 16 * accompanied this code). 17 * 18 * You should have received a copy of the GNU General Public License version 19 * 2 along with this work; if not, write to the Free Software Foundation, 20 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 21 * 22 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 23 * or visit www.oracle.com if you need additional information or have any 24 * questions. 25 */ 26 27 /* 28 * (C) Copyright Taligent, Inc. 1996 - All Rights Reserved 29 * (C) Copyright IBM Corp. 1996-1998 - All Rights Reserved 30 * 31 * The original version of this source code and documentation is copyrighted 32 * and owned by Taligent, Inc., a wholly-owned subsidiary of IBM. These 33 * materials are provided under terms of a License Agreement between Taligent 34 * and Sun. This technology is protected by multiple US and International 35 * patents. This notice and attribution to Taligent may not be removed. 36 * Taligent is a registered trademark of Taligent, Inc. 37 * 38 */ 39 40 package java.text; 41 42 import android.icu.text.TimeZoneFormat; 43 import android.icu.text.TimeZoneNames; 44 import android.icu.util.ULocale; 45 46 import java.io.IOException; 47 import java.io.InvalidObjectException; 48 import java.io.ObjectInputStream; 49 import java.util.Arrays; 50 import java.util.Calendar; 51 import java.util.Collection; 52 import java.util.Collections; 53 import java.util.Date; 54 import java.util.EnumSet; 55 import java.util.GregorianCalendar; 56 import java.util.HashSet; 57 import java.util.Locale; 58 import java.util.Map; 59 import java.util.Set; 60 import java.util.SimpleTimeZone; 61 import java.util.TimeZone; 62 import java.util.concurrent.ConcurrentHashMap; 63 import java.util.concurrent.ConcurrentMap; 64 import libcore.icu.LocaleData; 65 66 import sun.util.calendar.CalendarUtils; 67 68 import static java.text.DateFormatSymbols.*; 69 70 // Android-changed: Added supported API level, removed unnecessary <br> 71 // Android-changed: Clarified info about X symbol time zone parsing 72 /** 73 * <code>SimpleDateFormat</code> is a concrete class for formatting and 74 * parsing dates in a locale-sensitive manner. It allows for formatting 75 * (date → text), parsing (text → date), and normalization. 76 * 77 * <p> 78 * <code>SimpleDateFormat</code> allows you to start by choosing 79 * any user-defined patterns for date-time formatting. However, you 80 * are encouraged to create a date-time formatter with either 81 * <code>getTimeInstance</code>, <code>getDateInstance</code>, or 82 * <code>getDateTimeInstance</code> in <code>DateFormat</code>. Each 83 * of these class methods can return a date/time formatter initialized 84 * with a default format pattern. You may modify the format pattern 85 * using the <code>applyPattern</code> methods as desired. 86 * For more information on using these methods, see 87 * {@link DateFormat}. 88 * 89 * <h3>Date and Time Patterns</h3> 90 * <p> 91 * Date and time formats are specified by <em>date and time pattern</em> 92 * strings. 93 * Within date and time pattern strings, unquoted letters from 94 * <code>'A'</code> to <code>'Z'</code> and from <code>'a'</code> to 95 * <code>'z'</code> are interpreted as pattern letters representing the 96 * components of a date or time string. 97 * Text can be quoted using single quotes (<code>'</code>) to avoid 98 * interpretation. 99 * <code>"''"</code> represents a single quote. 100 * All other characters are not interpreted; they're simply copied into the 101 * output string during formatting or matched against the input string 102 * during parsing. 103 * <p> 104 * The following pattern letters are defined (all other characters from 105 * <code>'A'</code> to <code>'Z'</code> and from <code>'a'</code> to 106 * <code>'z'</code> are reserved): 107 * <blockquote> 108 * <table border=0 cellspacing=3 cellpadding=0 summary="Chart shows pattern letters, date/time component, presentation, and examples."> 109 * <tr style="background-color: rgb(204, 204, 255);"> 110 * <th align=left>Letter 111 * <th align=left>Date or Time Component 112 * <th align=left>Presentation 113 * <th align=left>Examples 114 * <th align=left>Supported (API Levels) 115 * <tr> 116 * <td><code>G</code> 117 * <td>Era designator 118 * <td><a href="#text">Text</a> 119 * <td><code>AD</code> 120 * <td>1+</td> 121 * <tr style="background-color: rgb(238, 238, 255);"> 122 * <td><code>y</code> 123 * <td>Year 124 * <td><a href="#year">Year</a> 125 * <td><code>1996</code>; <code>96</code> 126 * <td>1+</td> 127 * <tr> 128 * <td><code>Y</code> 129 * <td>Week year 130 * <td><a href="#year">Year</a> 131 * <td><code>2009</code>; <code>09</code> 132 * <td>24+</td> 133 * <tr style="background-color: rgb(238, 238, 255);"> 134 * <td><code>M</code> 135 * <td>Month in year (context sensitive) 136 * <td><a href="#month">Month</a> 137 * <td><code>July</code>; <code>Jul</code>; <code>07</code> 138 * <td>1+</td> 139 * <tr> 140 * <td><code>L</code> 141 * <td>Month in year (standalone form) 142 * <td><a href="#month">Month</a> 143 * <td><code>July</code>; <code>Jul</code>; <code>07</code> 144 * <td>TBD</td> 145 * <tr style="background-color: rgb(238, 238, 255);"> 146 * <td><code>w</code> 147 * <td>Week in year 148 * <td><a href="#number">Number</a> 149 * <td><code>27</code> 150 * <td>1+</td> 151 * <tr> 152 * <td><code>W</code> 153 * <td>Week in month 154 * <td><a href="#number">Number</a> 155 * <td><code>2</code> 156 * <td>1+</td> 157 * <tr style="background-color: rgb(238, 238, 255);"> 158 * <td><code>D</code> 159 * <td>Day in year 160 * <td><a href="#number">Number</a> 161 * <td><code>189</code> 162 * <td>1+</td> 163 * <tr> 164 * <td><code>d</code> 165 * <td>Day in month 166 * <td><a href="#number">Number</a> 167 * <td><code>10</code> 168 * <td>1+</td> 169 * <tr style="background-color: rgb(238, 238, 255);"> 170 * <td><code>F</code> 171 * <td>Day of week in month 172 * <td><a href="#number">Number</a> 173 * <td><code>2</code> 174 * <td>1+</td> 175 * <tr> 176 * <td><code>E</code> 177 * <td>Day name in week 178 * <td><a href="#text">Text</a> 179 * <td><code>Tuesday</code>; <code>Tue</code> 180 * <td>1+</td> 181 * <tr style="background-color: rgb(238, 238, 255);"> 182 * <td><code>u</code> 183 * <td>Day number of week (1 = Monday, ..., 7 = Sunday) 184 * <td><a href="#number">Number</a> 185 * <td><code>1</code> 186 * <td>24+</td> 187 * <tr> 188 * <td><code>a</code> 189 * <td>Am/pm marker 190 * <td><a href="#text">Text</a> 191 * <td><code>PM</code> 192 * <td>1+</td> 193 * <tr style="background-color: rgb(238, 238, 255);"> 194 * <td><code>H</code> 195 * <td>Hour in day (0-23) 196 * <td><a href="#number">Number</a> 197 * <td><code>0</code> 198 * <td>1+</td> 199 * <tr> 200 * <td><code>k</code> 201 * <td>Hour in day (1-24) 202 * <td><a href="#number">Number</a> 203 * <td><code>24</code> 204 * <td>1+</td> 205 * <tr style="background-color: rgb(238, 238, 255);"> 206 * <td><code>K</code> 207 * <td>Hour in am/pm (0-11) 208 * <td><a href="#number">Number</a> 209 * <td><code>0</code> 210 * <td>1+</td> 211 * <tr> 212 * <td><code>h</code> 213 * <td>Hour in am/pm (1-12) 214 * <td><a href="#number">Number</a> 215 * <td><code>12</code> 216 * <td>1+</td> 217 * <tr style="background-color: rgb(238, 238, 255);"> 218 * <td><code>m</code> 219 * <td>Minute in hour 220 * <td><a href="#number">Number</a> 221 * <td><code>30</code> 222 * <td>1+</td> 223 * <tr> 224 * <td><code>s</code> 225 * <td>Second in minute 226 * <td><a href="#number">Number</a> 227 * <td><code>55</code> 228 * <td>1+</td> 229 * <tr style="background-color: rgb(238, 238, 255);"> 230 * <td><code>S</code> 231 * <td>Millisecond 232 * <td><a href="#number">Number</a> 233 * <td><code>978</code> 234 * <td>1+</td> 235 * <tr> 236 * <td><code>z</code> 237 * <td>Time zone 238 * <td><a href="#timezone">General time zone</a> 239 * <td><code>Pacific Standard Time</code>; <code>PST</code>; <code>GMT-08:00</code> 240 * <td>1+</td> 241 * <tr style="background-color: rgb(238, 238, 255);"> 242 * <td><code>Z</code> 243 * <td>Time zone 244 * <td><a href="#rfc822timezone">RFC 822 time zone</a> 245 * <td><code>-0800</code> 246 * <td>1+</td> 247 * <tr> 248 * <td><code>X</code> 249 * <td>Time zone 250 * <td><a href="#iso8601timezone">ISO 8601 time zone</a> 251 * <td><code>-08</code>; <code>-0800</code>; <code>-08:00</code> 252 * <td>24+</td> 253 * </table> 254 * </blockquote> 255 * Pattern letters are usually repeated, as their number determines the 256 * exact presentation: 257 * <ul> 258 * <li><strong><a name="text">Text:</a></strong> 259 * For formatting, if the number of pattern letters is 4 or more, 260 * the full form is used; otherwise a short or abbreviated form 261 * is used if available. 262 * For parsing, both forms are accepted, independent of the number 263 * of pattern letters.</li> 264 * <li><strong><a name="number">Number:</a></strong> 265 * For formatting, the number of pattern letters is the minimum 266 * number of digits, and shorter numbers are zero-padded to this amount. 267 * For parsing, the number of pattern letters is ignored unless 268 * it's needed to separate two adjacent fields.</li> 269 * <li><strong><a name="year">Year:</a></strong> 270 * If the formatter's {@link #getCalendar() Calendar} is the Gregorian 271 * calendar, the following rules are applied. 272 * <ul> 273 * <li>For formatting, if the number of pattern letters is 2, the year 274 * is truncated to 2 digits; otherwise it is interpreted as a 275 * <a href="#number">number</a>. 276 * <li>For parsing, if the number of pattern letters is more than 2, 277 * the year is interpreted literally, regardless of the number of 278 * digits. So using the pattern "MM/dd/yyyy", "01/11/12" parses to 279 * Jan 11, 12 A.D. 280 * <li>For parsing with the abbreviated year pattern ("y" or "yy"), 281 * <code>SimpleDateFormat</code> must interpret the abbreviated year 282 * relative to some century. It does this by adjusting dates to be 283 * within 80 years before and 20 years after the time the <code>SimpleDateFormat</code> 284 * instance is created. For example, using a pattern of "MM/dd/yy" and a 285 * <code>SimpleDateFormat</code> instance created on Jan 1, 1997, the string 286 * "01/11/12" would be interpreted as Jan 11, 2012 while the string "05/04/64" 287 * would be interpreted as May 4, 1964. 288 * During parsing, only strings consisting of exactly two digits, as defined by 289 * {@link Character#isDigit(char)}, will be parsed into the default century. 290 * Any other numeric string, such as a one digit string, a three or more digit 291 * string, or a two digit string that isn't all digits (for example, "-1"), is 292 * interpreted literally. So "01/02/3" or "01/02/003" are parsed, using the 293 * same pattern, as Jan 2, 3 AD. Likewise, "01/02/-3" is parsed as Jan 2, 4 BC. 294 * </ul> 295 * Otherwise, calendar system specific forms are applied. 296 * For both formatting and parsing, if the number of pattern 297 * letters is 4 or more, a calendar specific {@linkplain 298 * Calendar#LONG long form} is used. Otherwise, a calendar 299 * specific {@linkplain Calendar#SHORT short or abbreviated form} 300 * is used. 301 * <br> 302 * If week year {@code 'Y'} is specified and the {@linkplain 303 * #getCalendar() calendar} doesn't support any <a 304 * href="../util/GregorianCalendar.html#week_year"> week 305 * years</a>, the calendar year ({@code 'y'}) is used instead. The 306 * support of week years can be tested with a call to {@link 307 * DateFormat#getCalendar() getCalendar()}.{@link 308 * java.util.Calendar#isWeekDateSupported() 309 * isWeekDateSupported()}.</li> 310 * <li><strong><a name="month">Month:</a></strong> 311 * If the number of pattern letters is 3 or more, the month is 312 * interpreted as <a href="#text">text</a>; otherwise, 313 * it is interpreted as a <a href="#number">number</a>. 314 * <ul> 315 * <li>Letter <em>M</em> produces context-sensitive month names, such as the 316 * embedded form of names. If a {@code DateFormatSymbols} has been set 317 * explicitly with constructor {@link #SimpleDateFormat(String, 318 * DateFormatSymbols)} or method {@link 319 * #setDateFormatSymbols(DateFormatSymbols)}, the month names given by 320 * the {@code DateFormatSymbols} are used.</li> 321 * <li>Letter <em>L</em> produces the standalone form of month names.</li> 322 * </ul> 323 * <br></li> 324 * <li><strong><a name="timezone">General time zone:</a></strong> 325 * Time zones are interpreted as <a href="#text">text</a> if they have 326 * names. For time zones representing a GMT offset value, the 327 * following syntax is used: 328 * <pre> 329 * <a name="GMTOffsetTimeZone"><i>GMTOffsetTimeZone:</i></a> 330 * <code>GMT</code> <i>Sign</i> <i>Hours</i> <code>:</code> <i>Minutes</i> 331 * <i>Sign:</i> one of 332 * <code>+ -</code> 333 * <i>Hours:</i> 334 * <i>Digit</i> 335 * <i>Digit</i> <i>Digit</i> 336 * <i>Minutes:</i> 337 * <i>Digit</i> <i>Digit</i> 338 * <i>Digit:</i> one of 339 * <code>0 1 2 3 4 5 6 7 8 9</code></pre> 340 * <i>Hours</i> must be between 0 and 23, and <i>Minutes</i> must be between 341 * 00 and 59. The format is locale independent and digits must be taken 342 * from the Basic Latin block of the Unicode standard. 343 * <p>For parsing, <a href="#rfc822timezone">RFC 822 time zones</a> are also 344 * accepted.</li> 345 * <li><strong><a name="rfc822timezone">RFC 822 time zone:</a></strong> 346 * For formatting, the RFC 822 4-digit time zone format is used: 347 * 348 * <pre> 349 * <i>RFC822TimeZone:</i> 350 * <i>Sign</i> <i>TwoDigitHours</i> <i>Minutes</i> 351 * <i>TwoDigitHours:</i> 352 * <i>Digit Digit</i></pre> 353 * <i>TwoDigitHours</i> must be between 00 and 23. Other definitions 354 * are as for <a href="#timezone">general time zones</a>. 355 * 356 * <p>For parsing, <a href="#timezone">general time zones</a> are also 357 * accepted. 358 * <li><strong><a name="iso8601timezone">ISO 8601 Time zone:</a></strong> 359 * The number of pattern letters designates the format for both formatting 360 * and parsing as follows: 361 * <pre> 362 * <i>ISO8601TimeZone:</i> 363 * <i>OneLetterISO8601TimeZone</i> 364 * <i>TwoLetterISO8601TimeZone</i> 365 * <i>ThreeLetterISO8601TimeZone</i> 366 * <i>OneLetterISO8601TimeZone:</i> 367 * <i>Sign</i> <i>TwoDigitHours</i> 368 * {@code Z} 369 * <i>TwoLetterISO8601TimeZone:</i> 370 * <i>Sign</i> <i>TwoDigitHours</i> <i>Minutes</i> 371 * {@code Z} 372 * <i>ThreeLetterISO8601TimeZone:</i> 373 * <i>Sign</i> <i>TwoDigitHours</i> {@code :} <i>Minutes</i> 374 * {@code Z}</pre> 375 * Other definitions are as for <a href="#timezone">general time zones</a> or 376 * <a href="#rfc822timezone">RFC 822 time zones</a>. 377 * 378 * <p>For formatting, if the offset value from GMT is 0, {@code "Z"} is 379 * produced. If the number of pattern letters is 1, any fraction of an hour 380 * is ignored. For example, if the pattern is {@code "X"} and the time zone is 381 * {@code "GMT+05:30"}, {@code "+05"} is produced. 382 * 383 * <p>For parsing, the letter {@code "Z"} is parsed as the UTC time zone designator (therefore 384 * {@code "09:30Z"} is parsed as {@code "09:30 UTC"}. 385 * <a href="#timezone">General time zones</a> are <em>not</em> accepted. 386 * <p>If the number of {@code "X"} pattern letters is 4 or more (e.g. {@code XXXX}), {@link 387 * IllegalArgumentException} is thrown when constructing a {@code 388 * SimpleDateFormat} or {@linkplain #applyPattern(String) applying a 389 * pattern}. 390 * </ul> 391 * <code>SimpleDateFormat</code> also supports <em>localized date and time 392 * pattern</em> strings. In these strings, the pattern letters described above 393 * may be replaced with other, locale dependent, pattern letters. 394 * <code>SimpleDateFormat</code> does not deal with the localization of text 395 * other than the pattern letters; that's up to the client of the class. 396 * 397 * <h4>Examples</h4> 398 * 399 * The following examples show how date and time patterns are interpreted in 400 * the U.S. locale. The given date and time are 2001-07-04 12:08:56 local time 401 * in the U.S. Pacific Time time zone. 402 * <blockquote> 403 * <table border=0 cellspacing=3 cellpadding=0 summary="Examples of date and time patterns interpreted in the U.S. locale"> 404 * <tr style="background-color: rgb(204, 204, 255);"> 405 * <th align=left>Date and Time Pattern 406 * <th align=left>Result 407 * <tr> 408 * <td><code>"yyyy.MM.dd G 'at' HH:mm:ss z"</code> 409 * <td><code>2001.07.04 AD at 12:08:56 PDT</code> 410 * <tr style="background-color: rgb(238, 238, 255);"> 411 * <td><code>"EEE, MMM d, ''yy"</code> 412 * <td><code>Wed, Jul 4, '01</code> 413 * <tr> 414 * <td><code>"h:mm a"</code> 415 * <td><code>12:08 PM</code> 416 * <tr style="background-color: rgb(238, 238, 255);"> 417 * <td><code>"hh 'o''clock' a, zzzz"</code> 418 * <td><code>12 o'clock PM, Pacific Daylight Time</code> 419 * <tr> 420 * <td><code>"K:mm a, z"</code> 421 * <td><code>0:08 PM, PDT</code> 422 * <tr style="background-color: rgb(238, 238, 255);"> 423 * <td><code>"yyyyy.MMMMM.dd GGG hh:mm aaa"</code> 424 * <td><code>02001.July.04 AD 12:08 PM</code> 425 * <tr> 426 * <td><code>"EEE, d MMM yyyy HH:mm:ss Z"</code> 427 * <td><code>Wed, 4 Jul 2001 12:08:56 -0700</code> 428 * <tr style="background-color: rgb(238, 238, 255);"> 429 * <td><code>"yyMMddHHmmssZ"</code> 430 * <td><code>010704120856-0700</code> 431 * <tr> 432 * <td><code>"yyyy-MM-dd'T'HH:mm:ss.SSSZ"</code> 433 * <td><code>2001-07-04T12:08:56.235-0700</code> 434 * <tr style="background-color: rgb(238, 238, 255);"> 435 * <td><code>"yyyy-MM-dd'T'HH:mm:ss.SSSXXX"</code> 436 * <td><code>2001-07-04T12:08:56.235-07:00</code> 437 * <tr> 438 * <td><code>"YYYY-'W'ww-u"</code> 439 * <td><code>2001-W27-3</code> 440 * </table> 441 * </blockquote> 442 * 443 * <h4><a name="synchronization">Synchronization</a></h4> 444 * 445 * <p> 446 * Date formats are not synchronized. 447 * It is recommended to create separate format instances for each thread. 448 * If multiple threads access a format concurrently, it must be synchronized 449 * externally. 450 * 451 * @see <a href="https://docs.oracle.com/javase/tutorial/i18n/format/simpleDateFormat.html">Java Tutorial</a> 452 * @see java.util.Calendar 453 * @see java.util.TimeZone 454 * @see DateFormat 455 * @see DateFormatSymbols 456 * @author Mark Davis, Chen-Lieh Huang, Alan Liu 457 */ 458 public class SimpleDateFormat extends DateFormat { 459 460 // the official serial version ID which says cryptically 461 // which version we're compatible with 462 static final long serialVersionUID = 4774881970558875024L; 463 464 // the internal serial version which says which version was written 465 // - 0 (default) for version up to JDK 1.1.3 466 // - 1 for version from JDK 1.1.4, which includes a new field 467 static final int currentSerialVersion = 1; 468 469 /** 470 * The version of the serialized data on the stream. Possible values: 471 * <ul> 472 * <li><b>0</b> or not present on stream: JDK 1.1.3. This version 473 * has no <code>defaultCenturyStart</code> on stream. 474 * <li><b>1</b> JDK 1.1.4 or later. This version adds 475 * <code>defaultCenturyStart</code>. 476 * </ul> 477 * When streaming out this class, the most recent format 478 * and the highest allowable <code>serialVersionOnStream</code> 479 * is written. 480 * @serial 481 * @since JDK1.1.4 482 */ 483 private int serialVersionOnStream = currentSerialVersion; 484 485 /** 486 * The pattern string of this formatter. This is always a non-localized 487 * pattern. May not be null. See class documentation for details. 488 * @serial 489 */ 490 private String pattern; 491 492 /** 493 * Saved numberFormat and pattern. 494 * @see SimpleDateFormat#checkNegativeNumberExpression 495 */ 496 transient private NumberFormat originalNumberFormat; 497 transient private String originalNumberPattern; 498 499 /** 500 * The minus sign to be used with format and parse. 501 */ 502 transient private char minusSign = '-'; 503 504 /** 505 * True when a negative sign follows a number. 506 * (True as default in Arabic.) 507 */ 508 transient private boolean hasFollowingMinusSign = false; 509 510 // BEGIN Android-removed: App compat for formatting pattern letter M. 511 // OpenJDK forces the standalone form of month when patterns contain pattern M only. 512 // This feature is not incorporated for app compatibility and because the feature is 513 // not documented in OpenJDK or Android. 514 /* 515 /** 516 * True if standalone form needs to be used. 517 * 518 transient private boolean forceStandaloneForm = false; 519 */ 520 // END Android-removed: App compat for formatting pattern letter M. 521 522 /** 523 * The compiled pattern. 524 */ 525 transient private char[] compiledPattern; 526 527 /** 528 * Tags for the compiled pattern. 529 */ 530 private final static int TAG_QUOTE_ASCII_CHAR = 100; 531 private final static int TAG_QUOTE_CHARS = 101; 532 533 /** 534 * Locale dependent digit zero. 535 * @see #zeroPaddingNumber 536 * @see java.text.DecimalFormatSymbols#getZeroDigit 537 */ 538 transient private char zeroDigit; 539 540 /** 541 * The symbols used by this formatter for week names, month names, 542 * etc. May not be null. 543 * @serial 544 * @see java.text.DateFormatSymbols 545 */ 546 private DateFormatSymbols formatData; 547 548 /** 549 * We map dates with two-digit years into the century starting at 550 * <code>defaultCenturyStart</code>, which may be any date. May 551 * not be null. 552 * @serial 553 * @since JDK1.1.4 554 */ 555 private Date defaultCenturyStart; 556 557 transient private int defaultCenturyStartYear; 558 559 private static final int MILLIS_PER_MINUTE = 60 * 1000; 560 561 // For time zones that have no names, use strings GMT+minutes and 562 // GMT-minutes. For instance, in France the time zone is GMT+60. 563 private static final String GMT = "GMT"; 564 565 /** 566 * Cache NumberFormat instances with Locale key. 567 */ 568 private static final ConcurrentMap<Locale, NumberFormat> cachedNumberFormatData 569 = new ConcurrentHashMap<>(3); 570 571 /** 572 * The Locale used to instantiate this 573 * <code>SimpleDateFormat</code>. The value may be null if this object 574 * has been created by an older <code>SimpleDateFormat</code> and 575 * deserialized. 576 * 577 * @serial 578 * @since 1.6 579 */ 580 private Locale locale; 581 582 /** 583 * Indicates whether this <code>SimpleDateFormat</code> should use 584 * the DateFormatSymbols. If true, the format and parse methods 585 * use the DateFormatSymbols values. If false, the format and 586 * parse methods call Calendar.getDisplayName or 587 * Calendar.getDisplayNames. 588 */ 589 transient boolean useDateFormatSymbols; 590 591 // Android-added: ICU TimeZoneNames field. 592 /** 593 * ICU TimeZoneNames used to format and parse time zone names. 594 */ 595 private transient TimeZoneNames timeZoneNames; 596 597 /** 598 * Constructs a <code>SimpleDateFormat</code> using the default pattern and 599 * date format symbols for the default 600 * {@link java.util.Locale.Category#FORMAT FORMAT} locale. 601 * <b>Note:</b> This constructor may not support all locales. 602 * For full coverage, use the factory methods in the {@link DateFormat} 603 * class. 604 */ SimpleDateFormat()605 public SimpleDateFormat() { 606 // BEGIN Android-changed: Android has no LocaleProviderAdapter. Use ICU locale data. 607 /* 608 this("", Locale.getDefault(Locale.Category.FORMAT)); 609 applyPatternImpl(LocaleProviderAdapter.getResourceBundleBased().getLocaleResources(locale) 610 .getDateTimePattern(SHORT, SHORT, calendar)); 611 */ 612 this(SHORT, SHORT, Locale.getDefault(Locale.Category.FORMAT)); 613 // END Android-changed: Android has no LocaleProviderAdapter. Use ICU locale data. 614 } 615 616 // BEGIN Android-added: Ctor used by DateFormat to remove use of LocaleProviderAdapter. 617 /** 618 * Constructs a <code>SimpleDateFormat</code> using the given date and time formatting styles. 619 * @param timeStyle the given date formatting style. 620 * @param dateStyle the given time formatting style. 621 * @param locale the locale whose pattern and date format symbols should be used 622 */ SimpleDateFormat(int timeStyle, int dateStyle, Locale locale)623 SimpleDateFormat(int timeStyle, int dateStyle, Locale locale) { 624 this(getDateTimeFormat(timeStyle, dateStyle, locale), locale); 625 } 626 getDateTimeFormat(int timeStyle, int dateStyle, Locale locale)627 private static String getDateTimeFormat(int timeStyle, int dateStyle, Locale locale) { 628 LocaleData localeData = LocaleData.get(locale); 629 if ((timeStyle >= 0) && (dateStyle >= 0)) { 630 Object[] dateTimeArgs = { 631 localeData.getDateFormat(dateStyle), 632 localeData.getTimeFormat(timeStyle), 633 }; 634 return MessageFormat.format("{0} {1}", dateTimeArgs); 635 } else if (timeStyle >= 0) { 636 return localeData.getTimeFormat(timeStyle); 637 } else if (dateStyle >= 0) { 638 return localeData.getDateFormat(dateStyle); 639 } else { 640 throw new IllegalArgumentException("No date or time style specified"); 641 } 642 } 643 // END Android-added: Ctor used by DateFormat to remove use of LocaleProviderAdapter. 644 645 /** 646 * Constructs a <code>SimpleDateFormat</code> using the given pattern and 647 * the default date format symbols for the default 648 * {@link java.util.Locale.Category#FORMAT FORMAT} locale. 649 * <b>Note:</b> This constructor may not support all locales. 650 * For full coverage, use the factory methods in the {@link DateFormat} 651 * class. 652 * <p>This is equivalent to calling 653 * {@link #SimpleDateFormat(String, Locale) 654 * SimpleDateFormat(pattern, Locale.getDefault(Locale.Category.FORMAT))}. 655 * 656 * @see java.util.Locale#getDefault(java.util.Locale.Category) 657 * @see java.util.Locale.Category#FORMAT 658 * @param pattern the pattern describing the date and time format 659 * @exception NullPointerException if the given pattern is null 660 * @exception IllegalArgumentException if the given pattern is invalid 661 */ SimpleDateFormat(String pattern)662 public SimpleDateFormat(String pattern) 663 { 664 this(pattern, Locale.getDefault(Locale.Category.FORMAT)); 665 } 666 667 /** 668 * Constructs a <code>SimpleDateFormat</code> using the given pattern and 669 * the default date format symbols for the given locale. 670 * <b>Note:</b> This constructor may not support all locales. 671 * For full coverage, use the factory methods in the {@link DateFormat} 672 * class. 673 * 674 * @param pattern the pattern describing the date and time format 675 * @param locale the locale whose date format symbols should be used 676 * @exception NullPointerException if the given pattern or locale is null 677 * @exception IllegalArgumentException if the given pattern is invalid 678 */ SimpleDateFormat(String pattern, Locale locale)679 public SimpleDateFormat(String pattern, Locale locale) 680 { 681 if (pattern == null || locale == null) { 682 throw new NullPointerException(); 683 } 684 685 initializeCalendar(locale); 686 this.pattern = pattern; 687 this.formatData = DateFormatSymbols.getInstanceRef(locale); 688 this.locale = locale; 689 initialize(locale); 690 } 691 692 /** 693 * Constructs a <code>SimpleDateFormat</code> using the given pattern and 694 * date format symbols. 695 * 696 * @param pattern the pattern describing the date and time format 697 * @param formatSymbols the date format symbols to be used for formatting 698 * @exception NullPointerException if the given pattern or formatSymbols is null 699 * @exception IllegalArgumentException if the given pattern is invalid 700 */ SimpleDateFormat(String pattern, DateFormatSymbols formatSymbols)701 public SimpleDateFormat(String pattern, DateFormatSymbols formatSymbols) 702 { 703 if (pattern == null || formatSymbols == null) { 704 throw new NullPointerException(); 705 } 706 707 this.pattern = pattern; 708 this.formatData = (DateFormatSymbols) formatSymbols.clone(); 709 this.locale = Locale.getDefault(Locale.Category.FORMAT); 710 initializeCalendar(this.locale); 711 initialize(this.locale); 712 useDateFormatSymbols = true; 713 } 714 715 /* Initialize compiledPattern and numberFormat fields */ initialize(Locale loc)716 private void initialize(Locale loc) { 717 // Verify and compile the given pattern. 718 compiledPattern = compile(pattern); 719 720 /* try the cache first */ 721 numberFormat = cachedNumberFormatData.get(loc); 722 if (numberFormat == null) { /* cache miss */ 723 numberFormat = NumberFormat.getIntegerInstance(loc); 724 numberFormat.setGroupingUsed(false); 725 726 /* update cache */ 727 cachedNumberFormatData.putIfAbsent(loc, numberFormat); 728 } 729 numberFormat = (NumberFormat) numberFormat.clone(); 730 731 initializeDefaultCentury(); 732 } 733 initializeCalendar(Locale loc)734 private void initializeCalendar(Locale loc) { 735 if (calendar == null) { 736 assert loc != null; 737 // The format object must be constructed using the symbols for this zone. 738 // However, the calendar should use the current default TimeZone. 739 // If this is not contained in the locale zone strings, then the zone 740 // will be formatted using generic GMT+/-H:MM nomenclature. 741 calendar = Calendar.getInstance(TimeZone.getDefault(), loc); 742 } 743 } 744 745 /** 746 * Returns the compiled form of the given pattern. The syntax of 747 * the compiled pattern is: 748 * <blockquote> 749 * CompiledPattern: 750 * EntryList 751 * EntryList: 752 * Entry 753 * EntryList Entry 754 * Entry: 755 * TagField 756 * TagField data 757 * TagField: 758 * Tag Length 759 * TaggedData 760 * Tag: 761 * pattern_char_index 762 * TAG_QUOTE_CHARS 763 * Length: 764 * short_length 765 * long_length 766 * TaggedData: 767 * TAG_QUOTE_ASCII_CHAR ascii_char 768 * 769 * </blockquote> 770 * 771 * where `short_length' is an 8-bit unsigned integer between 0 and 772 * 254. `long_length' is a sequence of an 8-bit integer 255 and a 773 * 32-bit signed integer value which is split into upper and lower 774 * 16-bit fields in two char's. `pattern_char_index' is an 8-bit 775 * integer between 0 and 18. `ascii_char' is an 7-bit ASCII 776 * character value. `data' depends on its Tag value. 777 * <p> 778 * If Length is short_length, Tag and short_length are packed in a 779 * single char, as illustrated below. 780 * <blockquote> 781 * char[0] = (Tag << 8) | short_length; 782 * </blockquote> 783 * 784 * If Length is long_length, Tag and 255 are packed in the first 785 * char and a 32-bit integer, as illustrated below. 786 * <blockquote> 787 * char[0] = (Tag << 8) | 255; 788 * char[1] = (char) (long_length >>> 16); 789 * char[2] = (char) (long_length & 0xffff); 790 * </blockquote> 791 * <p> 792 * If Tag is a pattern_char_index, its Length is the number of 793 * pattern characters. For example, if the given pattern is 794 * "yyyy", Tag is 1 and Length is 4, followed by no data. 795 * <p> 796 * If Tag is TAG_QUOTE_CHARS, its Length is the number of char's 797 * following the TagField. For example, if the given pattern is 798 * "'o''clock'", Length is 7 followed by a char sequence of 799 * <code>o&nbs;'&nbs;c&nbs;l&nbs;o&nbs;c&nbs;k</code>. 800 * <p> 801 * TAG_QUOTE_ASCII_CHAR is a special tag and has an ASCII 802 * character in place of Length. For example, if the given pattern 803 * is "'o'", the TaggedData entry is 804 * <code>((TAG_QUOTE_ASCII_CHAR&nbs;<<&nbs;8)&nbs;|&nbs;'o')</code>. 805 * 806 * @exception NullPointerException if the given pattern is null 807 * @exception IllegalArgumentException if the given pattern is invalid 808 */ compile(String pattern)809 private char[] compile(String pattern) { 810 int length = pattern.length(); 811 boolean inQuote = false; 812 StringBuilder compiledCode = new StringBuilder(length * 2); 813 StringBuilder tmpBuffer = null; 814 // BEGIN Android-removed: App compat for formatting pattern letter M. 815 // See forceStandaloneForm field 816 /* 817 int count = 0, tagcount = 0; 818 int lastTag = -1, prevTag = -1; 819 */ 820 int count = 0; 821 int lastTag = -1; 822 // END Android-removed: App compat for formatting pattern letter M. 823 824 for (int i = 0; i < length; i++) { 825 char c = pattern.charAt(i); 826 827 if (c == '\'') { 828 // '' is treated as a single quote regardless of being 829 // in a quoted section. 830 if ((i + 1) < length) { 831 c = pattern.charAt(i + 1); 832 if (c == '\'') { 833 i++; 834 if (count != 0) { 835 encode(lastTag, count, compiledCode); 836 // BEGIN Android-removed: App compat for formatting pattern letter M. 837 // See forceStandaloneForm field 838 /* 839 tagcount++; 840 prevTag = lastTag; 841 */ 842 // END Android-removed: App compat for formatting pattern letter M. 843 lastTag = -1; 844 count = 0; 845 } 846 if (inQuote) { 847 tmpBuffer.append(c); 848 } else { 849 compiledCode.append((char)(TAG_QUOTE_ASCII_CHAR << 8 | c)); 850 } 851 continue; 852 } 853 } 854 if (!inQuote) { 855 if (count != 0) { 856 encode(lastTag, count, compiledCode); 857 // BEGIN Android-removed: App compat for formatting pattern letter M. 858 // See forceStandaloneForm field 859 /* 860 tagcount++; 861 prevTag = lastTag; 862 */ 863 // END Android-removed: App compat for formatting pattern letter M. 864 lastTag = -1; 865 count = 0; 866 } 867 if (tmpBuffer == null) { 868 tmpBuffer = new StringBuilder(length); 869 } else { 870 tmpBuffer.setLength(0); 871 } 872 inQuote = true; 873 } else { 874 int len = tmpBuffer.length(); 875 if (len == 1) { 876 char ch = tmpBuffer.charAt(0); 877 if (ch < 128) { 878 compiledCode.append((char)(TAG_QUOTE_ASCII_CHAR << 8 | ch)); 879 } else { 880 compiledCode.append((char)(TAG_QUOTE_CHARS << 8 | 1)); 881 compiledCode.append(ch); 882 } 883 } else { 884 encode(TAG_QUOTE_CHARS, len, compiledCode); 885 compiledCode.append(tmpBuffer); 886 } 887 inQuote = false; 888 } 889 continue; 890 } 891 if (inQuote) { 892 tmpBuffer.append(c); 893 continue; 894 } 895 if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) { 896 if (count != 0) { 897 encode(lastTag, count, compiledCode); 898 // BEGIN Android-removed: App compat for formatting pattern letter M. 899 // See forceStandaloneForm field 900 /* 901 tagcount++; 902 prevTag = lastTag; 903 */ 904 // END Android-removed: App compat for formatting pattern letter M. 905 lastTag = -1; 906 count = 0; 907 } 908 if (c < 128) { 909 // In most cases, c would be a delimiter, such as ':'. 910 compiledCode.append((char)(TAG_QUOTE_ASCII_CHAR << 8 | c)); 911 } else { 912 // Take any contiguous non-ASCII alphabet characters and 913 // put them in a single TAG_QUOTE_CHARS. 914 int j; 915 for (j = i + 1; j < length; j++) { 916 char d = pattern.charAt(j); 917 if (d == '\'' || (d >= 'a' && d <= 'z' || d >= 'A' && d <= 'Z')) { 918 break; 919 } 920 } 921 compiledCode.append((char)(TAG_QUOTE_CHARS << 8 | (j - i))); 922 for (; i < j; i++) { 923 compiledCode.append(pattern.charAt(i)); 924 } 925 i--; 926 } 927 continue; 928 } 929 930 int tag; 931 if ((tag = DateFormatSymbols.patternChars.indexOf(c)) == -1) { 932 throw new IllegalArgumentException("Illegal pattern character " + 933 "'" + c + "'"); 934 } 935 if (lastTag == -1 || lastTag == tag) { 936 lastTag = tag; 937 count++; 938 continue; 939 } 940 encode(lastTag, count, compiledCode); 941 // BEGIN Android-removed: App compat for formatting pattern letter M. 942 // See forceStandaloneForm field 943 /* 944 tagcount++; 945 prevTag = lastTag; 946 */ 947 // END Android-removed: App compat for formatting pattern letter M. 948 lastTag = tag; 949 count = 1; 950 } 951 952 if (inQuote) { 953 throw new IllegalArgumentException("Unterminated quote"); 954 } 955 956 if (count != 0) { 957 encode(lastTag, count, compiledCode); 958 // BEGIN Android-removed: App compat for formatting pattern letter M. 959 // See forceStandaloneForm field 960 /* 961 tagcount++; 962 prevTag = lastTag; 963 */ 964 // END Android-removed: App compat for formatting pattern letter M. 965 } 966 967 // Android-removed: App compat for formatting pattern letter M. 968 // See forceStandaloneForm field 969 // forceStandaloneForm = (tagcount == 1 && prevTag == PATTERN_MONTH); 970 971 // Copy the compiled pattern to a char array 972 int len = compiledCode.length(); 973 char[] r = new char[len]; 974 compiledCode.getChars(0, len, r, 0); 975 return r; 976 } 977 978 /** 979 * Encodes the given tag and length and puts encoded char(s) into buffer. 980 */ encode(int tag, int length, StringBuilder buffer)981 private static void encode(int tag, int length, StringBuilder buffer) { 982 if (tag == PATTERN_ISO_ZONE && length >= 4) { 983 throw new IllegalArgumentException("invalid ISO 8601 format: length=" + length); 984 } 985 if (length < 255) { 986 buffer.append((char)(tag << 8 | length)); 987 } else { 988 buffer.append((char)((tag << 8) | 0xff)); 989 buffer.append((char)(length >>> 16)); 990 buffer.append((char)(length & 0xffff)); 991 } 992 } 993 994 /* Initialize the fields we use to disambiguate ambiguous years. Separate 995 * so we can call it from readObject(). 996 */ initializeDefaultCentury()997 private void initializeDefaultCentury() { 998 calendar.setTimeInMillis(System.currentTimeMillis()); 999 calendar.add( Calendar.YEAR, -80 ); 1000 parseAmbiguousDatesAsAfter(calendar.getTime()); 1001 } 1002 1003 /* Define one-century window into which to disambiguate dates using 1004 * two-digit years. 1005 */ parseAmbiguousDatesAsAfter(Date startDate)1006 private void parseAmbiguousDatesAsAfter(Date startDate) { 1007 defaultCenturyStart = startDate; 1008 calendar.setTime(startDate); 1009 defaultCenturyStartYear = calendar.get(Calendar.YEAR); 1010 } 1011 1012 /** 1013 * Sets the 100-year period 2-digit years will be interpreted as being in 1014 * to begin on the date the user specifies. 1015 * 1016 * @param startDate During parsing, two digit years will be placed in the range 1017 * <code>startDate</code> to <code>startDate + 100 years</code>. 1018 * @see #get2DigitYearStart 1019 * @since 1.2 1020 */ set2DigitYearStart(Date startDate)1021 public void set2DigitYearStart(Date startDate) { 1022 parseAmbiguousDatesAsAfter(new Date(startDate.getTime())); 1023 } 1024 1025 /** 1026 * Returns the beginning date of the 100-year period 2-digit years are interpreted 1027 * as being within. 1028 * 1029 * @return the start of the 100-year period into which two digit years are 1030 * parsed 1031 * @see #set2DigitYearStart 1032 * @since 1.2 1033 */ get2DigitYearStart()1034 public Date get2DigitYearStart() { 1035 return (Date) defaultCenturyStart.clone(); 1036 } 1037 1038 /** 1039 * Formats the given <code>Date</code> into a date/time string and appends 1040 * the result to the given <code>StringBuffer</code>. 1041 * 1042 * @param date the date-time value to be formatted into a date-time string. 1043 * @param toAppendTo where the new date-time text is to be appended. 1044 * @param pos the formatting position. On input: an alignment field, 1045 * if desired. On output: the offsets of the alignment field. 1046 * @return the formatted date-time string. 1047 * @exception NullPointerException if the given {@code date} is {@code null}. 1048 */ 1049 @Override format(Date date, StringBuffer toAppendTo, FieldPosition pos)1050 public StringBuffer format(Date date, StringBuffer toAppendTo, 1051 FieldPosition pos) 1052 { 1053 pos.beginIndex = pos.endIndex = 0; 1054 return format(date, toAppendTo, pos.getFieldDelegate()); 1055 } 1056 1057 // Called from Format after creating a FieldDelegate format(Date date, StringBuffer toAppendTo, FieldDelegate delegate)1058 private StringBuffer format(Date date, StringBuffer toAppendTo, 1059 FieldDelegate delegate) { 1060 // Convert input date to time field list 1061 calendar.setTime(date); 1062 1063 boolean useDateFormatSymbols = useDateFormatSymbols(); 1064 1065 for (int i = 0; i < compiledPattern.length; ) { 1066 int tag = compiledPattern[i] >>> 8; 1067 int count = compiledPattern[i++] & 0xff; 1068 if (count == 255) { 1069 count = compiledPattern[i++] << 16; 1070 count |= compiledPattern[i++]; 1071 } 1072 1073 switch (tag) { 1074 case TAG_QUOTE_ASCII_CHAR: 1075 toAppendTo.append((char)count); 1076 break; 1077 1078 case TAG_QUOTE_CHARS: 1079 toAppendTo.append(compiledPattern, i, count); 1080 i += count; 1081 break; 1082 1083 default: 1084 subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols); 1085 break; 1086 } 1087 } 1088 return toAppendTo; 1089 } 1090 1091 /** 1092 * Formats an Object producing an <code>AttributedCharacterIterator</code>. 1093 * You can use the returned <code>AttributedCharacterIterator</code> 1094 * to build the resulting String, as well as to determine information 1095 * about the resulting String. 1096 * <p> 1097 * Each attribute key of the AttributedCharacterIterator will be of type 1098 * <code>DateFormat.Field</code>, with the corresponding attribute value 1099 * being the same as the attribute key. 1100 * 1101 * @exception NullPointerException if obj is null. 1102 * @exception IllegalArgumentException if the Format cannot format the 1103 * given object, or if the Format's pattern string is invalid. 1104 * @param obj The object to format 1105 * @return AttributedCharacterIterator describing the formatted value. 1106 * @since 1.4 1107 */ 1108 @Override formatToCharacterIterator(Object obj)1109 public AttributedCharacterIterator formatToCharacterIterator(Object obj) { 1110 StringBuffer sb = new StringBuffer(); 1111 CharacterIteratorFieldDelegate delegate = new 1112 CharacterIteratorFieldDelegate(); 1113 1114 if (obj instanceof Date) { 1115 format((Date)obj, sb, delegate); 1116 } 1117 else if (obj instanceof Number) { 1118 format(new Date(((Number)obj).longValue()), sb, delegate); 1119 } 1120 else if (obj == null) { 1121 throw new NullPointerException( 1122 "formatToCharacterIterator must be passed non-null object"); 1123 } 1124 else { 1125 throw new IllegalArgumentException( 1126 "Cannot format given Object as a Date"); 1127 } 1128 return delegate.getIterator(sb.toString()); 1129 } 1130 1131 // Map index into pattern character string to Calendar field number 1132 private static final int[] PATTERN_INDEX_TO_CALENDAR_FIELD = { 1133 Calendar.ERA, 1134 Calendar.YEAR, 1135 Calendar.MONTH, 1136 Calendar.DATE, 1137 Calendar.HOUR_OF_DAY, 1138 Calendar.HOUR_OF_DAY, 1139 Calendar.MINUTE, 1140 Calendar.SECOND, 1141 Calendar.MILLISECOND, 1142 Calendar.DAY_OF_WEEK, 1143 Calendar.DAY_OF_YEAR, 1144 Calendar.DAY_OF_WEEK_IN_MONTH, 1145 Calendar.WEEK_OF_YEAR, 1146 Calendar.WEEK_OF_MONTH, 1147 Calendar.AM_PM, 1148 Calendar.HOUR, 1149 Calendar.HOUR, 1150 Calendar.ZONE_OFFSET, 1151 Calendar.ZONE_OFFSET, 1152 CalendarBuilder.WEEK_YEAR, // Pseudo Calendar field 1153 CalendarBuilder.ISO_DAY_OF_WEEK, // Pseudo Calendar field 1154 Calendar.ZONE_OFFSET, 1155 Calendar.MONTH, 1156 // Android-added: 'c' for standalone day of week. 1157 Calendar.DAY_OF_WEEK, 1158 // Android-added: Support for 'b'/'B' (day period). Calendar.AM_PM is just used as a 1159 // placeholder in the absence of full support for day period. 1160 Calendar.AM_PM, 1161 Calendar.AM_PM 1162 }; 1163 1164 // Map index into pattern character string to DateFormat field number 1165 private static final int[] PATTERN_INDEX_TO_DATE_FORMAT_FIELD = { 1166 DateFormat.ERA_FIELD, 1167 DateFormat.YEAR_FIELD, 1168 DateFormat.MONTH_FIELD, 1169 DateFormat.DATE_FIELD, 1170 DateFormat.HOUR_OF_DAY1_FIELD, 1171 DateFormat.HOUR_OF_DAY0_FIELD, 1172 DateFormat.MINUTE_FIELD, 1173 DateFormat.SECOND_FIELD, 1174 DateFormat.MILLISECOND_FIELD, 1175 DateFormat.DAY_OF_WEEK_FIELD, 1176 DateFormat.DAY_OF_YEAR_FIELD, 1177 DateFormat.DAY_OF_WEEK_IN_MONTH_FIELD, 1178 DateFormat.WEEK_OF_YEAR_FIELD, 1179 DateFormat.WEEK_OF_MONTH_FIELD, 1180 DateFormat.AM_PM_FIELD, 1181 DateFormat.HOUR1_FIELD, 1182 DateFormat.HOUR0_FIELD, 1183 DateFormat.TIMEZONE_FIELD, 1184 DateFormat.TIMEZONE_FIELD, 1185 DateFormat.YEAR_FIELD, 1186 DateFormat.DAY_OF_WEEK_FIELD, 1187 DateFormat.TIMEZONE_FIELD, 1188 DateFormat.MONTH_FIELD, 1189 // Android-added: 'c' for standalone day of week. 1190 DateFormat.DAY_OF_WEEK_FIELD, 1191 // Android-added: Support for 'b'/'B' (day period). DateFormat.AM_PM_FIELD is just used as a 1192 // placeholder in the absence of full support for day period. 1193 DateFormat.AM_PM_FIELD, 1194 DateFormat.AM_PM_FIELD 1195 }; 1196 1197 // Maps from DecimalFormatSymbols index to Field constant 1198 private static final Field[] PATTERN_INDEX_TO_DATE_FORMAT_FIELD_ID = { 1199 Field.ERA, 1200 Field.YEAR, 1201 Field.MONTH, 1202 Field.DAY_OF_MONTH, 1203 Field.HOUR_OF_DAY1, 1204 Field.HOUR_OF_DAY0, 1205 Field.MINUTE, 1206 Field.SECOND, 1207 Field.MILLISECOND, 1208 Field.DAY_OF_WEEK, 1209 Field.DAY_OF_YEAR, 1210 Field.DAY_OF_WEEK_IN_MONTH, 1211 Field.WEEK_OF_YEAR, 1212 Field.WEEK_OF_MONTH, 1213 Field.AM_PM, 1214 Field.HOUR1, 1215 Field.HOUR0, 1216 Field.TIME_ZONE, 1217 Field.TIME_ZONE, 1218 Field.YEAR, 1219 Field.DAY_OF_WEEK, 1220 Field.TIME_ZONE, 1221 Field.MONTH, 1222 // Android-added: 'c' for standalone day of week. 1223 Field.DAY_OF_WEEK, 1224 // Android-added: Support for 'b'/'B' (day period). Field.AM_PM is just used as a 1225 // placeholder in the absence of full support for day period. 1226 Field.AM_PM, 1227 Field.AM_PM 1228 }; 1229 1230 /** 1231 * Private member function that does the real date/time formatting. 1232 */ subFormat(int patternCharIndex, int count, FieldDelegate delegate, StringBuffer buffer, boolean useDateFormatSymbols)1233 private void subFormat(int patternCharIndex, int count, 1234 FieldDelegate delegate, StringBuffer buffer, 1235 boolean useDateFormatSymbols) 1236 { 1237 int maxIntCount = Integer.MAX_VALUE; 1238 String current = null; 1239 int beginOffset = buffer.length(); 1240 1241 int field = PATTERN_INDEX_TO_CALENDAR_FIELD[patternCharIndex]; 1242 int value; 1243 if (field == CalendarBuilder.WEEK_YEAR) { 1244 if (calendar.isWeekDateSupported()) { 1245 value = calendar.getWeekYear(); 1246 } else { 1247 // use calendar year 'y' instead 1248 patternCharIndex = PATTERN_YEAR; 1249 field = PATTERN_INDEX_TO_CALENDAR_FIELD[patternCharIndex]; 1250 value = calendar.get(field); 1251 } 1252 } else if (field == CalendarBuilder.ISO_DAY_OF_WEEK) { 1253 value = CalendarBuilder.toISODayOfWeek(calendar.get(Calendar.DAY_OF_WEEK)); 1254 } else { 1255 value = calendar.get(field); 1256 } 1257 1258 int style = (count >= 4) ? Calendar.LONG : Calendar.SHORT; 1259 if (!useDateFormatSymbols && field != CalendarBuilder.ISO_DAY_OF_WEEK) { 1260 current = calendar.getDisplayName(field, style, locale); 1261 } 1262 1263 // Note: zeroPaddingNumber() assumes that maxDigits is either 1264 // 2 or maxIntCount. If we make any changes to this, 1265 // zeroPaddingNumber() must be fixed. 1266 1267 switch (patternCharIndex) { 1268 case PATTERN_ERA: // 'G' 1269 if (useDateFormatSymbols) { 1270 String[] eras = formatData.getEras(); 1271 if (value < eras.length) { 1272 current = eras[value]; 1273 } 1274 } 1275 if (current == null) { 1276 current = ""; 1277 } 1278 break; 1279 1280 case PATTERN_WEEK_YEAR: // 'Y' 1281 case PATTERN_YEAR: // 'y' 1282 if (calendar instanceof GregorianCalendar) { 1283 if (count != 2) { 1284 zeroPaddingNumber(value, count, maxIntCount, buffer); 1285 } else { 1286 zeroPaddingNumber(value, 2, 2, buffer); 1287 } // clip 1996 to 96 1288 } else { 1289 if (current == null) { 1290 zeroPaddingNumber(value, style == Calendar.LONG ? 1 : count, 1291 maxIntCount, buffer); 1292 } 1293 } 1294 break; 1295 1296 case PATTERN_MONTH: // 'M' (context seinsive) 1297 // BEGIN Android-changed: formatMonth() method to format using ICU data. 1298 /* 1299 if (useDateFormatSymbols) { 1300 String[] months; 1301 if (count >= 4) { 1302 months = formatData.getMonths(); 1303 current = months[value]; 1304 } else if (count == 3) { 1305 months = formatData.getShortMonths(); 1306 current = months[value]; 1307 } 1308 } else { 1309 if (count < 3) { 1310 current = null; 1311 } else if (forceStandaloneForm) { 1312 current = calendar.getDisplayName(field, style | 0x8000, locale); 1313 if (current == null) { 1314 current = calendar.getDisplayName(field, style, locale); 1315 } 1316 } 1317 } 1318 if (current == null) { 1319 zeroPaddingNumber(value+1, count, maxIntCount, buffer); 1320 } 1321 */ 1322 current = formatMonth(count, value, maxIntCount, buffer, useDateFormatSymbols, 1323 false /* standalone */, field, style); 1324 // END Android-changed: formatMonth() method to format using ICU data. 1325 break; 1326 1327 case PATTERN_MONTH_STANDALONE: // 'L' 1328 // BEGIN Android-changed: formatMonth() method to format using ICU data. 1329 /* 1330 assert current == null; 1331 if (locale == null) { 1332 String[] months; 1333 if (count >= 4) { 1334 months = formatData.getMonths(); 1335 current = months[value]; 1336 } else if (count == 3) { 1337 months = formatData.getShortMonths(); 1338 current = months[value]; 1339 } 1340 } else { 1341 if (count >= 3) { 1342 current = calendar.getDisplayName(field, style | 0x8000, locale); 1343 } 1344 } 1345 if (current == null) { 1346 zeroPaddingNumber(value+1, count, maxIntCount, buffer); 1347 } 1348 */ 1349 current = formatMonth(count, value, maxIntCount, buffer, useDateFormatSymbols, 1350 true /* standalone */, field, style); 1351 // END Android-changed: formatMonth() method to format using ICU data. 1352 break; 1353 1354 case PATTERN_HOUR_OF_DAY1: // 'k' 1-based. eg, 23:59 + 1 hour =>> 24:59 1355 if (current == null) { 1356 if (value == 0) { 1357 zeroPaddingNumber(calendar.getMaximum(Calendar.HOUR_OF_DAY) + 1, 1358 count, maxIntCount, buffer); 1359 } else { 1360 zeroPaddingNumber(value, count, maxIntCount, buffer); 1361 } 1362 } 1363 break; 1364 1365 case PATTERN_DAY_OF_WEEK: // 'E' 1366 // BEGIN Android-removed: App compat for formatting pattern letter M. 1367 // See forceStandaloneForm field 1368 /* 1369 if (useDateFormatSymbols) { 1370 String[] weekdays; 1371 if (count >= 4) { 1372 weekdays = formatData.getWeekdays(); 1373 current = weekdays[value]; 1374 } else { // count < 4, use abbreviated form if exists 1375 weekdays = formatData.getShortWeekdays(); 1376 current = weekdays[value]; 1377 } 1378 } 1379 */ 1380 if (current == null) { 1381 current = formatWeekday(count, value, useDateFormatSymbols, false /* standalone */); 1382 } 1383 // END Android-removed: App compat for formatting pattern letter M. 1384 break; 1385 1386 // BEGIN Android-added: support for 'c' (standalone day of week). 1387 case PATTERN_STANDALONE_DAY_OF_WEEK: // 'c' 1388 if (current == null) { 1389 current = formatWeekday(count, value, useDateFormatSymbols, true /* standalone */); 1390 } 1391 break; 1392 // END Android-added: support for 'c' (standalone day of week). 1393 1394 case PATTERN_AM_PM: // 'a' 1395 if (useDateFormatSymbols) { 1396 String[] ampm = formatData.getAmPmStrings(); 1397 current = ampm[value]; 1398 } 1399 break; 1400 1401 // Android-added: Ignore 'b' and 'B' introduced in CLDR 32+ pattern data. http://b/68139386 1402 // Not currently supported here. 1403 case PATTERN_DAY_PERIOD: 1404 case PATTERN_FLEXIBLE_DAY_PERIOD: 1405 current = ""; 1406 break; 1407 1408 case PATTERN_HOUR1: // 'h' 1-based. eg, 11PM + 1 hour =>> 12 AM 1409 if (current == null) { 1410 if (value == 0) { 1411 zeroPaddingNumber(calendar.getLeastMaximum(Calendar.HOUR) + 1, 1412 count, maxIntCount, buffer); 1413 } else { 1414 zeroPaddingNumber(value, count, maxIntCount, buffer); 1415 } 1416 } 1417 break; 1418 1419 case PATTERN_ZONE_NAME: // 'z' 1420 if (current == null) { 1421 // BEGIN Android-changed: Format time zone name using ICU. 1422 /* 1423 if (formatData.locale == null || formatData.isZoneStringsSet) { 1424 int zoneIndex = 1425 formatData.getZoneIndex(calendar.getTimeZone().getID()); 1426 if (zoneIndex == -1) { 1427 value = calendar.get(Calendar.ZONE_OFFSET) + 1428 calendar.get(Calendar.DST_OFFSET); 1429 buffer.append(ZoneInfoFile.toCustomID(value)); 1430 } else { 1431 int index = (calendar.get(Calendar.DST_OFFSET) == 0) ? 1: 3; 1432 if (count < 4) { 1433 // Use the short name 1434 index++; 1435 } 1436 String[][] zoneStrings = formatData.getZoneStringsWrapper(); 1437 buffer.append(zoneStrings[zoneIndex][index]); 1438 } 1439 } else { 1440 TimeZone tz = calendar.getTimeZone(); 1441 boolean daylight = (calendar.get(Calendar.DST_OFFSET) != 0); 1442 int tzstyle = (count < 4 ? TimeZone.SHORT : TimeZone.LONG); 1443 buffer.append(tz.getDisplayName(daylight, tzstyle, formatData.locale)); 1444 } 1445 */ 1446 TimeZone tz = calendar.getTimeZone(); 1447 boolean daylight = (calendar.get(Calendar.DST_OFFSET) != 0); 1448 String zoneString; 1449 if (formatData.isZoneStringsSet) { 1450 // DateFormatSymbols.setZoneStrings() has be used, use those values instead of 1451 // ICU code. 1452 int tzstyle = count < 4 ? TimeZone.SHORT : TimeZone.LONG; 1453 zoneString = libcore.icu.TimeZoneNames.getDisplayName( 1454 formatData.getZoneStringsWrapper(), tz.getID(), daylight, tzstyle); 1455 } else { 1456 TimeZoneNames.NameType nameType; 1457 if (count < 4) { 1458 nameType = daylight 1459 ? TimeZoneNames.NameType.SHORT_DAYLIGHT 1460 : TimeZoneNames.NameType.SHORT_STANDARD; 1461 } else { 1462 nameType = daylight 1463 ? TimeZoneNames.NameType.LONG_DAYLIGHT 1464 : TimeZoneNames.NameType.LONG_STANDARD; 1465 } 1466 String canonicalID = android.icu.util.TimeZone.getCanonicalID(tz.getID()); 1467 zoneString = getTimeZoneNames() 1468 .getDisplayName(canonicalID, nameType, calendar.getTimeInMillis()); 1469 } 1470 if (zoneString != null) { 1471 buffer.append(zoneString); 1472 } else { 1473 int offsetMillis = calendar.get(Calendar.ZONE_OFFSET) + 1474 calendar.get(Calendar.DST_OFFSET); 1475 buffer.append(TimeZone.createGmtOffsetString(true, true, offsetMillis)); 1476 } 1477 // END Android-changed: Format time zone name using ICU. 1478 } 1479 break; 1480 1481 case PATTERN_ZONE_VALUE: // 'Z' ("-/+hhmm" form) 1482 // BEGIN Android-changed: Use shared code in TimeZone for zone offset string. 1483 /* 1484 value = (calendar.get(Calendar.ZONE_OFFSET) + 1485 calendar.get(Calendar.DST_OFFSET)) / 60000; 1486 1487 int width = 4; 1488 if (value >= 0) { 1489 buffer.append('+'); 1490 } else { 1491 width++; 1492 } 1493 1494 int num = (value / 60) * 100 + (value % 60); 1495 CalendarUtils.sprintf0d(buffer, num, width); 1496 */ 1497 value = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); 1498 final boolean includeSeparator = (count >= 4); 1499 final boolean includeGmt = (count == 4); 1500 buffer.append(TimeZone.createGmtOffsetString(includeGmt, includeSeparator, value)); 1501 1502 break; 1503 // END Android-changed: Use shared code in TimeZone for zone offset string. 1504 1505 case PATTERN_ISO_ZONE: // 'X' 1506 value = calendar.get(Calendar.ZONE_OFFSET) 1507 + calendar.get(Calendar.DST_OFFSET); 1508 1509 if (value == 0) { 1510 buffer.append('Z'); 1511 break; 1512 } 1513 1514 value /= 60000; 1515 if (value >= 0) { 1516 buffer.append('+'); 1517 } else { 1518 buffer.append('-'); 1519 value = -value; 1520 } 1521 1522 CalendarUtils.sprintf0d(buffer, value / 60, 2); 1523 if (count == 1) { 1524 break; 1525 } 1526 1527 if (count == 3) { 1528 buffer.append(':'); 1529 } 1530 CalendarUtils.sprintf0d(buffer, value % 60, 2); 1531 break; 1532 // BEGIN Android-added: Better UTS#35 conformity for fractional seconds. 1533 case PATTERN_MILLISECOND: // 'S' 1534 // Fractional seconds must be treated specially. We must always convert the parsed 1535 // value into a fractional second [0, 1) and then widen it out to the appropriate 1536 // formatted size. For example, an initial value of 789 will be converted 1537 // 0.789 and then become ".7" (S) or ".78" (SS) or "0.789" (SSS) or "0.7890" (SSSS) 1538 // in the resulting formatted output. 1539 if (current == null) { 1540 value = (int) (((double) value / 1000) * Math.pow(10, count)); 1541 zeroPaddingNumber(value, count, count, buffer); 1542 } 1543 break; 1544 // END Android-added: Better UTS#35 conformity for fractional seconds. 1545 1546 default: 1547 // case PATTERN_DAY_OF_MONTH: // 'd' 1548 // case PATTERN_HOUR_OF_DAY0: // 'H' 0-based. eg, 23:59 + 1 hour =>> 00:59 1549 // case PATTERN_MINUTE: // 'm' 1550 // case PATTERN_SECOND: // 's' 1551 // Android-removed: PATTERN_MILLISECONDS handled in an explicit case above. 1552 //// case PATTERN_MILLISECOND: // 'S' 1553 // case PATTERN_DAY_OF_YEAR: // 'D' 1554 // case PATTERN_DAY_OF_WEEK_IN_MONTH: // 'F' 1555 // case PATTERN_WEEK_OF_YEAR: // 'w' 1556 // case PATTERN_WEEK_OF_MONTH: // 'W' 1557 // case PATTERN_HOUR0: // 'K' eg, 11PM + 1 hour =>> 0 AM 1558 // case PATTERN_ISO_DAY_OF_WEEK: // 'u' pseudo field, Monday = 1, ..., Sunday = 7 1559 if (current == null) { 1560 zeroPaddingNumber(value, count, maxIntCount, buffer); 1561 } 1562 break; 1563 } // switch (patternCharIndex) 1564 1565 if (current != null) { 1566 buffer.append(current); 1567 } 1568 1569 int fieldID = PATTERN_INDEX_TO_DATE_FORMAT_FIELD[patternCharIndex]; 1570 Field f = PATTERN_INDEX_TO_DATE_FORMAT_FIELD_ID[patternCharIndex]; 1571 1572 delegate.formatted(fieldID, f, f, beginOffset, buffer.length(), buffer); 1573 } 1574 1575 // BEGIN Android-added: formatWeekday() and formatMonth() methods to format using ICU data. formatWeekday(int count, int value, boolean useDateFormatSymbols, boolean standalone)1576 private String formatWeekday(int count, int value, boolean useDateFormatSymbols, 1577 boolean standalone) { 1578 if (useDateFormatSymbols) { 1579 final String[] weekdays; 1580 if (count == 4) { 1581 weekdays = standalone ? formatData.getStandAloneWeekdays() : formatData.getWeekdays(); 1582 } else if (count == 5) { 1583 weekdays = 1584 standalone ? formatData.getTinyStandAloneWeekdays() : formatData.getTinyWeekdays(); 1585 1586 } else { // count < 4, use abbreviated form if exists 1587 weekdays = standalone ? formatData.getShortStandAloneWeekdays() : formatData.getShortWeekdays(); 1588 } 1589 1590 return weekdays[value]; 1591 } 1592 1593 return null; 1594 } 1595 formatMonth(int count, int value, int maxIntCount, StringBuffer buffer, boolean useDateFormatSymbols, boolean standalone, int field, int style)1596 private String formatMonth(int count, int value, int maxIntCount, StringBuffer buffer, 1597 boolean useDateFormatSymbols, boolean standalone, 1598 int field, int style) { 1599 String current = null; 1600 if (useDateFormatSymbols) { 1601 final String[] months; 1602 if (count == 4) { 1603 months = standalone ? formatData.getStandAloneMonths() : formatData.getMonths(); 1604 } else if (count == 5) { 1605 months = standalone ? formatData.getTinyStandAloneMonths() : formatData.getTinyMonths(); 1606 } else if (count == 3) { 1607 months = standalone ? formatData.getShortStandAloneMonths() : formatData.getShortMonths(); 1608 } else { 1609 months = null; 1610 } 1611 1612 if (months != null) { 1613 current = months[value]; 1614 } 1615 } else { 1616 if (count < 3) { 1617 current = null; 1618 } else { 1619 if (standalone) { 1620 style = Calendar.toStandaloneStyle(style); 1621 } 1622 current = calendar.getDisplayName(field, style, locale); 1623 } 1624 } 1625 1626 if (current == null) { 1627 zeroPaddingNumber(value+1, count, maxIntCount, buffer); 1628 } 1629 1630 return current; 1631 } 1632 // END Android-added: formatWeekday() and formatMonth() methods to format using ICU data. 1633 1634 /** 1635 * Formats a number with the specified minimum and maximum number of digits. 1636 */ zeroPaddingNumber(int value, int minDigits, int maxDigits, StringBuffer buffer)1637 private void zeroPaddingNumber(int value, int minDigits, int maxDigits, StringBuffer buffer) 1638 { 1639 // Optimization for 1, 2 and 4 digit numbers. This should 1640 // cover most cases of formatting date/time related items. 1641 // Note: This optimization code assumes that maxDigits is 1642 // either 2 or Integer.MAX_VALUE (maxIntCount in format()). 1643 try { 1644 if (zeroDigit == 0) { 1645 zeroDigit = ((DecimalFormat)numberFormat).getDecimalFormatSymbols().getZeroDigit(); 1646 } 1647 if (value >= 0) { 1648 if (value < 100 && minDigits >= 1 && minDigits <= 2) { 1649 if (value < 10) { 1650 if (minDigits == 2) { 1651 buffer.append(zeroDigit); 1652 } 1653 buffer.append((char)(zeroDigit + value)); 1654 } else { 1655 buffer.append((char)(zeroDigit + value / 10)); 1656 buffer.append((char)(zeroDigit + value % 10)); 1657 } 1658 return; 1659 } else if (value >= 1000 && value < 10000) { 1660 if (minDigits == 4) { 1661 buffer.append((char)(zeroDigit + value / 1000)); 1662 value %= 1000; 1663 buffer.append((char)(zeroDigit + value / 100)); 1664 value %= 100; 1665 buffer.append((char)(zeroDigit + value / 10)); 1666 buffer.append((char)(zeroDigit + value % 10)); 1667 return; 1668 } 1669 if (minDigits == 2 && maxDigits == 2) { 1670 zeroPaddingNumber(value % 100, 2, 2, buffer); 1671 return; 1672 } 1673 } 1674 } 1675 } catch (Exception e) { 1676 } 1677 1678 numberFormat.setMinimumIntegerDigits(minDigits); 1679 numberFormat.setMaximumIntegerDigits(maxDigits); 1680 numberFormat.format((long)value, buffer, DontCareFieldPosition.INSTANCE); 1681 } 1682 1683 1684 /** 1685 * Parses text from a string to produce a <code>Date</code>. 1686 * <p> 1687 * The method attempts to parse text starting at the index given by 1688 * <code>pos</code>. 1689 * If parsing succeeds, then the index of <code>pos</code> is updated 1690 * to the index after the last character used (parsing does not necessarily 1691 * use all characters up to the end of the string), and the parsed 1692 * date is returned. The updated <code>pos</code> can be used to 1693 * indicate the starting point for the next call to this method. 1694 * If an error occurs, then the index of <code>pos</code> is not 1695 * changed, the error index of <code>pos</code> is set to the index of 1696 * the character where the error occurred, and null is returned. 1697 * 1698 * <p>This parsing operation uses the {@link DateFormat#calendar 1699 * calendar} to produce a {@code Date}. All of the {@code 1700 * calendar}'s date-time fields are {@linkplain Calendar#clear() 1701 * cleared} before parsing, and the {@code calendar}'s default 1702 * values of the date-time fields are used for any missing 1703 * date-time information. For example, the year value of the 1704 * parsed {@code Date} is 1970 with {@link GregorianCalendar} if 1705 * no year value is given from the parsing operation. The {@code 1706 * TimeZone} value may be overwritten, depending on the given 1707 * pattern and the time zone value in {@code text}. Any {@code 1708 * TimeZone} value that has previously been set by a call to 1709 * {@link #setTimeZone(java.util.TimeZone) setTimeZone} may need 1710 * to be restored for further operations. 1711 * 1712 * @param text A <code>String</code>, part of which should be parsed. 1713 * @param pos A <code>ParsePosition</code> object with index and error 1714 * index information as described above. 1715 * @return A <code>Date</code> parsed from the string. In case of 1716 * error, returns null. 1717 * @exception NullPointerException if <code>text</code> or <code>pos</code> is null. 1718 */ 1719 @Override parse(String text, ParsePosition pos)1720 public Date parse(String text, ParsePosition pos) { 1721 // BEGIN Android-changed: extract parseInternal() and avoid modifying timezone during parse. 1722 // Make sure the timezone associated with this dateformat instance (set via 1723 // {@code setTimeZone} isn't change as a side-effect of parsing a date. 1724 final TimeZone tz = getTimeZone(); 1725 try { 1726 return parseInternal(text, pos); 1727 } finally { 1728 setTimeZone(tz); 1729 } 1730 } 1731 parseInternal(String text, ParsePosition pos)1732 private Date parseInternal(String text, ParsePosition pos) 1733 { 1734 // END Android-changed: extract parseInternal() and avoid modifying timezone during parse. 1735 checkNegativeNumberExpression(); 1736 1737 int start = pos.index; 1738 int oldStart = start; 1739 int textLength = text.length(); 1740 1741 boolean[] ambiguousYear = {false}; 1742 1743 CalendarBuilder calb = new CalendarBuilder(); 1744 1745 for (int i = 0; i < compiledPattern.length; ) { 1746 int tag = compiledPattern[i] >>> 8; 1747 int count = compiledPattern[i++] & 0xff; 1748 if (count == 255) { 1749 count = compiledPattern[i++] << 16; 1750 count |= compiledPattern[i++]; 1751 } 1752 1753 switch (tag) { 1754 case TAG_QUOTE_ASCII_CHAR: 1755 if (start >= textLength || text.charAt(start) != (char)count) { 1756 pos.index = oldStart; 1757 pos.errorIndex = start; 1758 return null; 1759 } 1760 start++; 1761 break; 1762 1763 case TAG_QUOTE_CHARS: 1764 while (count-- > 0) { 1765 if (start >= textLength || text.charAt(start) != compiledPattern[i++]) { 1766 pos.index = oldStart; 1767 pos.errorIndex = start; 1768 return null; 1769 } 1770 start++; 1771 } 1772 break; 1773 1774 default: 1775 // Peek the next pattern to determine if we need to 1776 // obey the number of pattern letters for 1777 // parsing. It's required when parsing contiguous 1778 // digit text (e.g., "20010704") with a pattern which 1779 // has no delimiters between fields, like "yyyyMMdd". 1780 boolean obeyCount = false; 1781 1782 // In Arabic, a minus sign for a negative number is put after 1783 // the number. Even in another locale, a minus sign can be 1784 // put after a number using DateFormat.setNumberFormat(). 1785 // If both the minus sign and the field-delimiter are '-', 1786 // subParse() needs to determine whether a '-' after a number 1787 // in the given text is a delimiter or is a minus sign for the 1788 // preceding number. We give subParse() a clue based on the 1789 // information in compiledPattern. 1790 boolean useFollowingMinusSignAsDelimiter = false; 1791 1792 if (i < compiledPattern.length) { 1793 int nextTag = compiledPattern[i] >>> 8; 1794 if (!(nextTag == TAG_QUOTE_ASCII_CHAR || 1795 nextTag == TAG_QUOTE_CHARS)) { 1796 obeyCount = true; 1797 } 1798 1799 if (hasFollowingMinusSign && 1800 (nextTag == TAG_QUOTE_ASCII_CHAR || 1801 nextTag == TAG_QUOTE_CHARS)) { 1802 int c; 1803 if (nextTag == TAG_QUOTE_ASCII_CHAR) { 1804 c = compiledPattern[i] & 0xff; 1805 } else { 1806 c = compiledPattern[i+1]; 1807 } 1808 1809 if (c == minusSign) { 1810 useFollowingMinusSignAsDelimiter = true; 1811 } 1812 } 1813 } 1814 start = subParse(text, start, tag, count, obeyCount, 1815 ambiguousYear, pos, 1816 useFollowingMinusSignAsDelimiter, calb); 1817 if (start < 0) { 1818 pos.index = oldStart; 1819 return null; 1820 } 1821 } 1822 } 1823 1824 // At this point the fields of Calendar have been set. Calendar 1825 // will fill in default values for missing fields when the time 1826 // is computed. 1827 1828 pos.index = start; 1829 1830 Date parsedDate; 1831 try { 1832 parsedDate = calb.establish(calendar).getTime(); 1833 // If the year value is ambiguous, 1834 // then the two-digit year == the default start year 1835 if (ambiguousYear[0]) { 1836 if (parsedDate.before(defaultCenturyStart)) { 1837 parsedDate = calb.addYear(100).establish(calendar).getTime(); 1838 } 1839 } 1840 } 1841 // An IllegalArgumentException will be thrown by Calendar.getTime() 1842 // if any fields are out of range, e.g., MONTH == 17. 1843 catch (IllegalArgumentException e) { 1844 pos.errorIndex = start; 1845 pos.index = oldStart; 1846 return null; 1847 } 1848 1849 return parsedDate; 1850 } 1851 1852 /** 1853 * Private code-size reduction function used by subParse. 1854 * @param text the time text being parsed. 1855 * @param start where to start parsing. 1856 * @param field the date field being parsed. 1857 * @param data the string array to parsed. 1858 * @return the new start position if matching succeeded; a negative number 1859 * indicating matching failure, otherwise. 1860 */ matchString(String text, int start, int field, String[] data, CalendarBuilder calb)1861 private int matchString(String text, int start, int field, String[] data, CalendarBuilder calb) 1862 { 1863 int i = 0; 1864 int count = data.length; 1865 1866 if (field == Calendar.DAY_OF_WEEK) { 1867 i = 1; 1868 } 1869 1870 // There may be multiple strings in the data[] array which begin with 1871 // the same prefix (e.g., Cerven and Cervenec (June and July) in Czech). 1872 // We keep track of the longest match, and return that. Note that this 1873 // unfortunately requires us to test all array elements. 1874 int bestMatchLength = 0, bestMatch = -1; 1875 for (; i<count; ++i) 1876 { 1877 int length = data[i].length(); 1878 // Always compare if we have no match yet; otherwise only compare 1879 // against potentially better matches (longer strings). 1880 if (length > bestMatchLength && 1881 text.regionMatches(true, start, data[i], 0, length)) 1882 { 1883 bestMatch = i; 1884 bestMatchLength = length; 1885 } 1886 1887 // BEGIN Android-changed: Handle abbreviated fields that end with a '.'. 1888 // When the input option ends with a period (usually an abbreviated form), attempt 1889 // to match all chars up to that period. 1890 if ((data[i].charAt(length - 1) == '.') && 1891 ((length - 1) > bestMatchLength) && 1892 text.regionMatches(true, start, data[i], 0, length - 1)) { 1893 bestMatch = i; 1894 bestMatchLength = (length - 1); 1895 } 1896 // END Android-changed: Handle abbreviated fields that end with a '.'. 1897 } 1898 if (bestMatch >= 0) 1899 { 1900 calb.set(field, bestMatch); 1901 return start + bestMatchLength; 1902 } 1903 return -start; 1904 } 1905 1906 /** 1907 * Performs the same thing as matchString(String, int, int, 1908 * String[]). This method takes a Map<String, Integer> instead of 1909 * String[]. 1910 */ matchString(String text, int start, int field, Map<String,Integer> data, CalendarBuilder calb)1911 private int matchString(String text, int start, int field, 1912 Map<String,Integer> data, CalendarBuilder calb) { 1913 if (data != null) { 1914 // BEGIN Android-removed: SortedMap instance lookup optimization in matchString(). 1915 // RI returns not the longest match as matchString(String[]) does. http://b/119913354 1916 /* 1917 // TODO: make this default when it's in the spec. 1918 if (data instanceof SortedMap) { 1919 for (String name : data.keySet()) { 1920 if (text.regionMatches(true, start, name, 0, name.length())) { 1921 calb.set(field, data.get(name)); 1922 return start + name.length(); 1923 } 1924 } 1925 return -start; 1926 } 1927 */ 1928 // END Android-removed: SortedMap instance lookup optimization in matchString(). 1929 1930 String bestMatch = null; 1931 1932 for (String name : data.keySet()) { 1933 int length = name.length(); 1934 if (bestMatch == null || length > bestMatch.length()) { 1935 if (text.regionMatches(true, start, name, 0, length)) { 1936 bestMatch = name; 1937 } 1938 } 1939 } 1940 1941 if (bestMatch != null) { 1942 calb.set(field, data.get(bestMatch)); 1943 return start + bestMatch.length(); 1944 } 1945 } 1946 return -start; 1947 } 1948 matchZoneString(String text, int start, String[] zoneNames)1949 private int matchZoneString(String text, int start, String[] zoneNames) { 1950 for (int i = 1; i <= 4; ++i) { 1951 // Checking long and short zones [1 & 2], 1952 // and long and short daylight [3 & 4]. 1953 String zoneName = zoneNames[i]; 1954 if (text.regionMatches(true, start, 1955 zoneName, 0, zoneName.length())) { 1956 return i; 1957 } 1958 } 1959 return -1; 1960 } 1961 1962 // BEGIN Android-removed: Unused private method matchDSTString. 1963 /* 1964 private boolean matchDSTString(String text, int start, int zoneIndex, int standardIndex, 1965 String[][] zoneStrings) { 1966 int index = standardIndex + 2; 1967 String zoneName = zoneStrings[zoneIndex][index]; 1968 if (text.regionMatches(true, start, 1969 zoneName, 0, zoneName.length())) { 1970 return true; 1971 } 1972 return false; 1973 } 1974 */ 1975 // END Android-removed: Unused private method matchDSTString. 1976 1977 // BEGIN Android-changed: Parse time zone strings using ICU TimeZoneNames. 1978 // Note that this change falls back to the upstream zone names parsing code if the zoneStrings 1979 // for the formatData field has been set by the user. The original code of subParseZoneString 1980 // can be found in subParseZoneStringFromSymbols(). 1981 /** 1982 * Parses the string in {@code text} (starting at {@code start}), interpreting it as a time zone 1983 * name. If a time zone is found, the internal calendar is set to that timezone and the index of 1984 * the first character after the time zone name is returned. Otherwise, returns {@code 0}. 1985 * @return the index of the next character to parse or {@code 0} on error. 1986 */ subParseZoneString(String text, int start, CalendarBuilder calb)1987 private int subParseZoneString(String text, int start, CalendarBuilder calb) { 1988 if (formatData.isZoneStringsSet) { 1989 // DateFormatSymbols.setZoneStrings() has be used, use those values instead of ICU code. 1990 return subParseZoneStringFromSymbols(text, start, calb); 1991 } else { 1992 return subParseZoneStringFromICU(text, start, calb); 1993 } 1994 } 1995 getTimeZoneNames()1996 private TimeZoneNames getTimeZoneNames() { 1997 if (timeZoneNames == null) { 1998 timeZoneNames = TimeZoneNames.getInstance(locale); 1999 } 2000 return timeZoneNames; 2001 } 2002 2003 /** 2004 * The set of name types accepted when parsing time zone names. 2005 */ 2006 private static final EnumSet<TimeZoneNames.NameType> NAME_TYPES = 2007 EnumSet.of(TimeZoneNames.NameType.LONG_GENERIC, TimeZoneNames.NameType.LONG_STANDARD, 2008 TimeZoneNames.NameType.LONG_DAYLIGHT, TimeZoneNames.NameType.SHORT_GENERIC, 2009 TimeZoneNames.NameType.SHORT_STANDARD, TimeZoneNames.NameType.SHORT_DAYLIGHT); 2010 2011 /** 2012 * Time zone name types that indicate daylight saving time. 2013 */ 2014 private static final Set<TimeZoneNames.NameType> DST_NAME_TYPES = 2015 Collections.unmodifiableSet(EnumSet.of( 2016 TimeZoneNames.NameType.LONG_DAYLIGHT, TimeZoneNames.NameType.SHORT_DAYLIGHT)); 2017 2018 /** 2019 * Parses the time zone string using the ICU4J class {@link TimeZoneNames}. 2020 */ subParseZoneStringFromICU(String text, int start, CalendarBuilder calb)2021 private int subParseZoneStringFromICU(String text, int start, CalendarBuilder calb) { 2022 String currentTimeZoneID = android.icu.util.TimeZone.getCanonicalID(getTimeZone().getID()); 2023 2024 TimeZoneNames tzNames = getTimeZoneNames(); 2025 TimeZoneNames.MatchInfo bestMatch = null; 2026 // The MetaZones associated with the current time zone are needed in two places, both of 2027 // which are avoided in some cases, so they are computed lazily. 2028 Set<String> currentTzMetaZoneIds = null; 2029 2030 Collection<TimeZoneNames.MatchInfo> matches = tzNames.find(text, start, NAME_TYPES); 2031 for (TimeZoneNames.MatchInfo match : matches) { 2032 if (bestMatch == null || bestMatch.matchLength() < match.matchLength()) { 2033 bestMatch = match; 2034 } else if (bestMatch.matchLength() == match.matchLength()) { 2035 if (currentTimeZoneID.equals(match.tzID())) { 2036 // Prefer the currently set timezone over other matches, even if they are 2037 // the same length. 2038 bestMatch = match; 2039 break; 2040 } else if (match.mzID() != null) { 2041 if (currentTzMetaZoneIds == null) { 2042 currentTzMetaZoneIds = 2043 tzNames.getAvailableMetaZoneIDs(currentTimeZoneID); 2044 } 2045 if (currentTzMetaZoneIds.contains(match.mzID())) { 2046 bestMatch = match; 2047 break; 2048 } 2049 } 2050 } 2051 } 2052 if (bestMatch == null) { 2053 // No match found, return error. 2054 return -start; 2055 } 2056 2057 String tzId = bestMatch.tzID(); 2058 if (tzId == null) { 2059 if (currentTzMetaZoneIds == null) { 2060 currentTzMetaZoneIds = tzNames.getAvailableMetaZoneIDs(currentTimeZoneID); 2061 } 2062 if (currentTzMetaZoneIds.contains(bestMatch.mzID())) { 2063 tzId = currentTimeZoneID; 2064 } else { 2065 // Match was for a meta-zone, find the matching reference zone. 2066 ULocale uLocale = ULocale.forLocale(locale); 2067 String region = uLocale.getCountry(); 2068 if (region.length() == 0) { 2069 uLocale = ULocale.addLikelySubtags(uLocale); 2070 region = uLocale.getCountry(); 2071 } 2072 tzId = tzNames.getReferenceZoneID(bestMatch.mzID(), region); 2073 } 2074 } 2075 2076 TimeZone newTimeZone = TimeZone.getTimeZone(tzId); 2077 if (!currentTimeZoneID.equals(tzId)) { 2078 setTimeZone(newTimeZone); 2079 } 2080 2081 // Same logic as in subParseZoneStringFromSymbols, see below for details. 2082 boolean isDst = DST_NAME_TYPES.contains(bestMatch.nameType()); 2083 int dstAmount = isDst ? newTimeZone.getDSTSavings() : 0; 2084 if (!isDst || dstAmount != 0) { 2085 calb.clear(Calendar.ZONE_OFFSET).set(Calendar.DST_OFFSET, dstAmount); 2086 } 2087 2088 return bestMatch.matchLength() + start; 2089 } 2090 2091 /** 2092 * Parses the time zone string using the information in {@link #formatData}. 2093 */ subParseZoneStringFromSymbols(String text, int start, CalendarBuilder calb)2094 private int subParseZoneStringFromSymbols(String text, int start, CalendarBuilder calb) { 2095 // END Android-changed: Parse time zone strings using ICU TimeZoneNames. 2096 boolean useSameName = false; // true if standard and daylight time use the same abbreviation. 2097 TimeZone currentTimeZone = getTimeZone(); 2098 2099 // At this point, check for named time zones by looking through 2100 // the locale data from the TimeZoneNames strings. 2101 // Want to be able to parse both short and long forms. 2102 int zoneIndex = formatData.getZoneIndex(currentTimeZone.getID()); 2103 TimeZone tz = null; 2104 String[][] zoneStrings = formatData.getZoneStringsWrapper(); 2105 String[] zoneNames = null; 2106 int nameIndex = 0; 2107 if (zoneIndex != -1) { 2108 zoneNames = zoneStrings[zoneIndex]; 2109 if ((nameIndex = matchZoneString(text, start, zoneNames)) > 0) { 2110 if (nameIndex <= 2) { 2111 // Check if the standard name (abbr) and the daylight name are the same. 2112 useSameName = zoneNames[nameIndex].equalsIgnoreCase(zoneNames[nameIndex + 2]); 2113 } 2114 tz = TimeZone.getTimeZone(zoneNames[0]); 2115 } 2116 } 2117 if (tz == null) { 2118 zoneIndex = formatData.getZoneIndex(TimeZone.getDefault().getID()); 2119 if (zoneIndex != -1) { 2120 zoneNames = zoneStrings[zoneIndex]; 2121 if ((nameIndex = matchZoneString(text, start, zoneNames)) > 0) { 2122 if (nameIndex <= 2) { 2123 useSameName = zoneNames[nameIndex].equalsIgnoreCase(zoneNames[nameIndex + 2]); 2124 } 2125 tz = TimeZone.getTimeZone(zoneNames[0]); 2126 } 2127 } 2128 } 2129 2130 if (tz == null) { 2131 int len = zoneStrings.length; 2132 for (int i = 0; i < len; i++) { 2133 zoneNames = zoneStrings[i]; 2134 if ((nameIndex = matchZoneString(text, start, zoneNames)) > 0) { 2135 if (nameIndex <= 2) { 2136 useSameName = zoneNames[nameIndex].equalsIgnoreCase(zoneNames[nameIndex + 2]); 2137 } 2138 tz = TimeZone.getTimeZone(zoneNames[0]); 2139 break; 2140 } 2141 } 2142 } 2143 if (tz != null) { // Matched any ? 2144 if (!tz.equals(currentTimeZone)) { 2145 setTimeZone(tz); 2146 } 2147 // If the time zone matched uses the same name 2148 // (abbreviation) for both standard and daylight time, 2149 // let the time zone in the Calendar decide which one. 2150 // 2151 // Also if tz.getDSTSaving() returns 0 for DST, use tz to 2152 // determine the local time. (6645292) 2153 int dstAmount = (nameIndex >= 3) ? tz.getDSTSavings() : 0; 2154 if (!(useSameName || (nameIndex >= 3 && dstAmount == 0))) { 2155 calb.clear(Calendar.ZONE_OFFSET).set(Calendar.DST_OFFSET, dstAmount); 2156 } 2157 return (start + zoneNames[nameIndex].length()); 2158 } 2159 return -start; 2160 } 2161 2162 /** 2163 * Parses numeric forms of time zone offset, such as "hh:mm", and 2164 * sets calb to the parsed value. 2165 * 2166 * @param text the text to be parsed 2167 * @param start the character position to start parsing 2168 * @param sign 1: positive; -1: negative 2169 * @param count 0: 'Z' or "GMT+hh:mm" parsing; 1 - 3: the number of 'X's 2170 * @param colon true - colon required between hh and mm; false - no colon required 2171 * @param calb a CalendarBuilder in which the parsed value is stored 2172 * @return updated parsed position, or its negative value to indicate a parsing error 2173 */ subParseNumericZone(String text, int start, int sign, int count, boolean colon, CalendarBuilder calb)2174 private int subParseNumericZone(String text, int start, int sign, int count, 2175 boolean colon, CalendarBuilder calb) { 2176 int index = start; 2177 2178 parse: 2179 try { 2180 char c = text.charAt(index++); 2181 // Parse hh 2182 int hours; 2183 if (!isDigit(c)) { 2184 break parse; 2185 } 2186 hours = c - '0'; 2187 c = text.charAt(index++); 2188 if (isDigit(c)) { 2189 hours = hours * 10 + (c - '0'); 2190 } else { 2191 // BEGIN Android-removed: Be more tolerant of colon. b/26426526 2192 /* 2193 // If no colon in RFC 822 or 'X' (ISO), two digits are 2194 // required. 2195 if (count > 0 || !colon) { 2196 break parse; 2197 } 2198 */ 2199 // END Android-removed: Be more tolerant of colon. b/26426526 2200 --index; 2201 } 2202 if (hours > 23) { 2203 break parse; 2204 } 2205 int minutes = 0; 2206 if (count != 1) { 2207 // Proceed with parsing mm 2208 c = text.charAt(index++); 2209 // BEGIN Android-changed: Be more tolerant of colon. b/26426526 2210 // OpenJDK will return an error code if a : is found and colonRequired is false, 2211 // this will return an error code if a : is not found and colonRequired is true. 2212 // 2213 // colon | c == ':' | OpenJDK | this 2214 // false | false | ok | ok 2215 // false | true | error | ok 2216 // true | false | ok | error 2217 // true | true | ok | ok 2218 /* 2219 if (colon) { 2220 if (c != ':') { 2221 break parse; 2222 } 2223 */ 2224 if (c == ':') { 2225 c = text.charAt(index++); 2226 } else if (colon) { 2227 break parse; 2228 } 2229 // END Android-changed: Be more tolerant of colon. b/26426526 2230 if (!isDigit(c)) { 2231 break parse; 2232 } 2233 minutes = c - '0'; 2234 c = text.charAt(index++); 2235 if (!isDigit(c)) { 2236 break parse; 2237 } 2238 minutes = minutes * 10 + (c - '0'); 2239 if (minutes > 59) { 2240 break parse; 2241 } 2242 } 2243 minutes += hours * 60; 2244 calb.set(Calendar.ZONE_OFFSET, minutes * MILLIS_PER_MINUTE * sign) 2245 .set(Calendar.DST_OFFSET, 0); 2246 return index; 2247 } catch (IndexOutOfBoundsException e) { 2248 } 2249 return 1 - index; // -(index - 1) 2250 } 2251 isDigit(char c)2252 private boolean isDigit(char c) { 2253 return c >= '0' && c <= '9'; 2254 } 2255 2256 /** 2257 * Private member function that converts the parsed date strings into 2258 * timeFields. Returns -start (for ParsePosition) if failed. 2259 * @param text the time text to be parsed. 2260 * @param start where to start parsing. 2261 * @param patternCharIndex the index of the pattern character. 2262 * @param count the count of a pattern character. 2263 * @param obeyCount if true, then the next field directly abuts this one, 2264 * and we should use the count to know when to stop parsing. 2265 * @param ambiguousYear return parameter; upon return, if ambiguousYear[0] 2266 * is true, then a two-digit year was parsed and may need to be readjusted. 2267 * @param origPos origPos.errorIndex is used to return an error index 2268 * at which a parse error occurred, if matching failure occurs. 2269 * @return the new start position if matching succeeded; -1 indicating 2270 * matching failure, otherwise. In case matching failure occurred, 2271 * an error index is set to origPos.errorIndex. 2272 */ subParse(String text, int start, int patternCharIndex, int count, boolean obeyCount, boolean[] ambiguousYear, ParsePosition origPos, boolean useFollowingMinusSignAsDelimiter, CalendarBuilder calb)2273 private int subParse(String text, int start, int patternCharIndex, int count, 2274 boolean obeyCount, boolean[] ambiguousYear, 2275 ParsePosition origPos, 2276 boolean useFollowingMinusSignAsDelimiter, CalendarBuilder calb) { 2277 Number number; 2278 int value = 0; 2279 ParsePosition pos = new ParsePosition(0); 2280 pos.index = start; 2281 if (patternCharIndex == PATTERN_WEEK_YEAR && !calendar.isWeekDateSupported()) { 2282 // use calendar year 'y' instead 2283 patternCharIndex = PATTERN_YEAR; 2284 } 2285 int field = PATTERN_INDEX_TO_CALENDAR_FIELD[patternCharIndex]; 2286 2287 // If there are any spaces here, skip over them. If we hit the end 2288 // of the string, then fail. 2289 for (;;) { 2290 if (pos.index >= text.length()) { 2291 origPos.errorIndex = start; 2292 return -1; 2293 } 2294 char c = text.charAt(pos.index); 2295 if (c != ' ' && c != '\t') { 2296 break; 2297 } 2298 ++pos.index; 2299 } 2300 // Remember the actual start index 2301 int actualStart = pos.index; 2302 2303 parsing: 2304 { 2305 // We handle a few special cases here where we need to parse 2306 // a number value. We handle further, more generic cases below. We need 2307 // to handle some of them here because some fields require extra processing on 2308 // the parsed value. 2309 if (patternCharIndex == PATTERN_HOUR_OF_DAY1 || 2310 patternCharIndex == PATTERN_HOUR1 || 2311 (patternCharIndex == PATTERN_MONTH && count <= 2) || 2312 patternCharIndex == PATTERN_YEAR || 2313 patternCharIndex == PATTERN_WEEK_YEAR) { 2314 // It would be good to unify this with the obeyCount logic below, 2315 // but that's going to be difficult. 2316 if (obeyCount) { 2317 if ((start+count) > text.length()) { 2318 break parsing; 2319 } 2320 number = numberFormat.parse(text.substring(0, start+count), pos); 2321 } else { 2322 number = numberFormat.parse(text, pos); 2323 } 2324 if (number == null) { 2325 if (patternCharIndex != PATTERN_YEAR || calendar instanceof GregorianCalendar) { 2326 break parsing; 2327 } 2328 } else { 2329 value = number.intValue(); 2330 2331 if (useFollowingMinusSignAsDelimiter && (value < 0) && 2332 (((pos.index < text.length()) && 2333 (text.charAt(pos.index) != minusSign)) || 2334 ((pos.index == text.length()) && 2335 (text.charAt(pos.index-1) == minusSign)))) { 2336 value = -value; 2337 pos.index--; 2338 } 2339 } 2340 } 2341 2342 boolean useDateFormatSymbols = useDateFormatSymbols(); 2343 2344 int index; 2345 switch (patternCharIndex) { 2346 case PATTERN_ERA: // 'G' 2347 if (useDateFormatSymbols) { 2348 if ((index = matchString(text, start, Calendar.ERA, formatData.getEras(), calb)) > 0) { 2349 return index; 2350 } 2351 } else { 2352 Map<String, Integer> map = getDisplayNamesMap(field, locale); 2353 if ((index = matchString(text, start, field, map, calb)) > 0) { 2354 return index; 2355 } 2356 } 2357 break parsing; 2358 2359 case PATTERN_WEEK_YEAR: // 'Y' 2360 case PATTERN_YEAR: // 'y' 2361 if (!(calendar instanceof GregorianCalendar)) { 2362 // calendar might have text representations for year values, 2363 // such as "\u5143" in JapaneseImperialCalendar. 2364 int style = (count >= 4) ? Calendar.LONG : Calendar.SHORT; 2365 Map<String, Integer> map = calendar.getDisplayNames(field, style, locale); 2366 if (map != null) { 2367 if ((index = matchString(text, start, field, map, calb)) > 0) { 2368 return index; 2369 } 2370 } 2371 calb.set(field, value); 2372 return pos.index; 2373 } 2374 2375 // If there are 3 or more YEAR pattern characters, this indicates 2376 // that the year value is to be treated literally, without any 2377 // two-digit year adjustments (e.g., from "01" to 2001). Otherwise 2378 // we made adjustments to place the 2-digit year in the proper 2379 // century, for parsed strings from "00" to "99". Any other string 2380 // is treated literally: "2250", "-1", "1", "002". 2381 if (count <= 2 && (pos.index - actualStart) == 2 2382 && Character.isDigit(text.charAt(actualStart)) 2383 && Character.isDigit(text.charAt(actualStart + 1))) { 2384 // Assume for example that the defaultCenturyStart is 6/18/1903. 2385 // This means that two-digit years will be forced into the range 2386 // 6/18/1903 to 6/17/2003. As a result, years 00, 01, and 02 2387 // correspond to 2000, 2001, and 2002. Years 04, 05, etc. correspond 2388 // to 1904, 1905, etc. If the year is 03, then it is 2003 if the 2389 // other fields specify a date before 6/18, or 1903 if they specify a 2390 // date afterwards. As a result, 03 is an ambiguous year. All other 2391 // two-digit years are unambiguous. 2392 int ambiguousTwoDigitYear = defaultCenturyStartYear % 100; 2393 ambiguousYear[0] = value == ambiguousTwoDigitYear; 2394 value += (defaultCenturyStartYear/100)*100 + 2395 (value < ambiguousTwoDigitYear ? 100 : 0); 2396 } 2397 calb.set(field, value); 2398 return pos.index; 2399 2400 case PATTERN_MONTH: // 'M' 2401 // BEGIN Android-changed: extract parseMonth method. 2402 /* 2403 if (count <= 2) // i.e., M or MM. 2404 { 2405 // Don't want to parse the month if it is a string 2406 // while pattern uses numeric style: M or MM. 2407 // [We computed 'value' above.] 2408 calb.set(Calendar.MONTH, value - 1); 2409 return pos.index; 2410 } 2411 2412 if (useDateFormatSymbols) { 2413 // count >= 3 // i.e., MMM or MMMM 2414 // Want to be able to parse both short and long forms. 2415 // Try count == 4 first: 2416 int newStart; 2417 if ((newStart = matchString(text, start, Calendar.MONTH, 2418 formatData.getMonths(), calb)) > 0) { 2419 return newStart; 2420 } 2421 // count == 4 failed, now try count == 3 2422 if ((index = matchString(text, start, Calendar.MONTH, 2423 formatData.getShortMonths(), calb)) > 0) { 2424 return index; 2425 } 2426 } else { 2427 Map<String, Integer> map = getDisplayNamesMap(field, locale); 2428 if ((index = matchString(text, start, field, map, calb)) > 0) { 2429 return index; 2430 } 2431 } 2432 */ 2433 { 2434 final int idx = parseMonth(text, count, value, start, field, pos, 2435 useDateFormatSymbols, false /* isStandalone */, calb); 2436 if (idx > 0) { 2437 return idx; 2438 } 2439 2440 break parsing; 2441 } 2442 2443 case PATTERN_MONTH_STANDALONE: // 'L'. 2444 { 2445 final int idx = parseMonth(text, count, value, start, field, pos, 2446 useDateFormatSymbols, true /* isStandalone */, calb); 2447 if (idx > 0) { 2448 return idx; 2449 } 2450 break parsing; 2451 } 2452 // END Android-changed: extract parseMonth method. 2453 2454 case PATTERN_HOUR_OF_DAY1: // 'k' 1-based. eg, 23:59 + 1 hour =>> 24:59 2455 if (!isLenient()) { 2456 // Validate the hour value in non-lenient 2457 if (value < 1 || value > 24) { 2458 break parsing; 2459 } 2460 } 2461 // [We computed 'value' above.] 2462 if (value == calendar.getMaximum(Calendar.HOUR_OF_DAY) + 1) { 2463 value = 0; 2464 } 2465 calb.set(Calendar.HOUR_OF_DAY, value); 2466 return pos.index; 2467 2468 case PATTERN_DAY_OF_WEEK: // 'E' 2469 // BEGIN Android-changed: extract parseWeekday method. 2470 /* 2471 { 2472 if (useDateFormatSymbols) { 2473 // Want to be able to parse both short and long forms. 2474 // Try count == 4 (DDDD) first: 2475 int newStart; 2476 if ((newStart=matchString(text, start, Calendar.DAY_OF_WEEK, 2477 formatData.getWeekdays(), calb)) > 0) { 2478 return newStart; 2479 } 2480 // DDDD failed, now try DDD 2481 if ((index = matchString(text, start, Calendar.DAY_OF_WEEK, 2482 formatData.getShortWeekdays(), calb)) > 0) { 2483 return index; 2484 } 2485 } else { 2486 int[] styles = { Calendar.LONG, Calendar.SHORT }; 2487 for (int style : styles) { 2488 Map<String,Integer> map = calendar.getDisplayNames(field, style, locale); 2489 if ((index = matchString(text, start, field, map, calb)) > 0) { 2490 return index; 2491 } 2492 } 2493 } 2494 } 2495 */ 2496 { 2497 final int idx = parseWeekday(text, start, field, useDateFormatSymbols, 2498 false /* standalone */, calb); 2499 if (idx > 0) { 2500 return idx; 2501 } 2502 break parsing; 2503 } 2504 // END Android-changed: extract parseWeekday method. 2505 2506 // BEGIN Android-added: support for 'c' (standalone day of week). 2507 case PATTERN_STANDALONE_DAY_OF_WEEK: // 'c' 2508 { 2509 final int idx = parseWeekday(text, start, field, useDateFormatSymbols, 2510 true /* standalone */, calb); 2511 if (idx > 0) { 2512 return idx; 2513 } 2514 2515 break parsing; 2516 } 2517 // END Android-added: support for 'c' (standalone day of week). 2518 2519 case PATTERN_AM_PM: // 'a' 2520 if (useDateFormatSymbols) { 2521 if ((index = matchString(text, start, Calendar.AM_PM, 2522 formatData.getAmPmStrings(), calb)) > 0) { 2523 return index; 2524 } 2525 } else { 2526 Map<String,Integer> map = getDisplayNamesMap(field, locale); 2527 if ((index = matchString(text, start, field, map, calb)) > 0) { 2528 return index; 2529 } 2530 } 2531 break parsing; 2532 2533 case PATTERN_HOUR1: // 'h' 1-based. eg, 11PM + 1 hour =>> 12 AM 2534 if (!isLenient()) { 2535 // Validate the hour value in non-lenient 2536 if (value < 1 || value > 12) { 2537 break parsing; 2538 } 2539 } 2540 // [We computed 'value' above.] 2541 if (value == calendar.getLeastMaximum(Calendar.HOUR) + 1) { 2542 value = 0; 2543 } 2544 calb.set(Calendar.HOUR, value); 2545 return pos.index; 2546 2547 case PATTERN_ZONE_NAME: // 'z' 2548 case PATTERN_ZONE_VALUE: // 'Z' 2549 { 2550 int sign = 0; 2551 try { 2552 char c = text.charAt(pos.index); 2553 if (c == '+') { 2554 sign = 1; 2555 } else if (c == '-') { 2556 sign = -1; 2557 } 2558 if (sign == 0) { 2559 // Try parsing a custom time zone "GMT+hh:mm" or "GMT". 2560 if ((c == 'G' || c == 'g') 2561 && (text.length() - start) >= GMT.length() 2562 && text.regionMatches(true, start, GMT, 0, GMT.length())) { 2563 pos.index = start + GMT.length(); 2564 2565 if ((text.length() - pos.index) > 0) { 2566 c = text.charAt(pos.index); 2567 if (c == '+') { 2568 sign = 1; 2569 } else if (c == '-') { 2570 sign = -1; 2571 } 2572 } 2573 2574 if (sign == 0) { /* "GMT" without offset */ 2575 calb.set(Calendar.ZONE_OFFSET, 0) 2576 .set(Calendar.DST_OFFSET, 0); 2577 return pos.index; 2578 } 2579 2580 // BEGIN Android-changed: Be more tolerant of colon. b/26426526 2581 /* 2582 // Parse the rest as "hh:mm" 2583 int i = subParseNumericZone(text, ++pos.index, 2584 sign, 0, true, calb); 2585 */ 2586 // Parse the rest as "hh[:]?mm" 2587 int i = subParseNumericZone(text, ++pos.index, sign, 0, 2588 false, calb); 2589 // END Android-changed: Be more tolerant of colon. b/26426526 2590 if (i > 0) { 2591 return i; 2592 } 2593 pos.index = -i; 2594 } else { 2595 // Try parsing the text as a time zone 2596 // name or abbreviation. 2597 int i = subParseZoneString(text, pos.index, calb); 2598 if (i > 0) { 2599 return i; 2600 } 2601 pos.index = -i; 2602 } 2603 } else { 2604 // BEGIN Android-changed: Be more tolerant of colon. b/26426526 2605 // Parse the rest as "hh[:]?mm" (RFC 822) 2606 /* 2607 // Parse the rest as "hhmm" (RFC 822) 2608 int i = subParseNumericZone(text, ++pos.index, 2609 sign, 0, false, calb); 2610 */ 2611 int i = subParseNumericZone(text, ++pos.index, sign, 0, 2612 false, calb); 2613 // END Android-changed: Be more tolerant of colon. b/26426526 2614 if (i > 0) { 2615 return i; 2616 } 2617 pos.index = -i; 2618 } 2619 } catch (IndexOutOfBoundsException e) { 2620 } 2621 } 2622 break parsing; 2623 2624 case PATTERN_ISO_ZONE: // 'X' 2625 { 2626 if ((text.length() - pos.index) <= 0) { 2627 break parsing; 2628 } 2629 2630 int sign; 2631 char c = text.charAt(pos.index); 2632 if (c == 'Z') { 2633 calb.set(Calendar.ZONE_OFFSET, 0).set(Calendar.DST_OFFSET, 0); 2634 return ++pos.index; 2635 } 2636 2637 // parse text as "+/-hh[[:]mm]" based on count 2638 if (c == '+') { 2639 sign = 1; 2640 } else if (c == '-') { 2641 sign = -1; 2642 } else { 2643 ++pos.index; 2644 break parsing; 2645 } 2646 int i = subParseNumericZone(text, ++pos.index, sign, count, 2647 count == 3, calb); 2648 if (i > 0) { 2649 return i; 2650 } 2651 pos.index = -i; 2652 } 2653 break parsing; 2654 2655 default: 2656 // case PATTERN_DAY_OF_MONTH: // 'd' 2657 // case PATTERN_HOUR_OF_DAY0: // 'H' 0-based. eg, 23:59 + 1 hour =>> 00:59 2658 // case PATTERN_MINUTE: // 'm' 2659 // case PATTERN_SECOND: // 's' 2660 // case PATTERN_MILLISECOND: // 'S' 2661 // case PATTERN_DAY_OF_YEAR: // 'D' 2662 // case PATTERN_DAY_OF_WEEK_IN_MONTH: // 'F' 2663 // case PATTERN_WEEK_OF_YEAR: // 'w' 2664 // case PATTERN_WEEK_OF_MONTH: // 'W' 2665 // case PATTERN_HOUR0: // 'K' 0-based. eg, 11PM + 1 hour =>> 0 AM 2666 // case PATTERN_ISO_DAY_OF_WEEK: // 'u' (pseudo field); 2667 2668 // Handle "generic" fields 2669 // BEGIN Android-changed: Better UTS#35 conformity for fractional seconds. 2670 // http://b/25863120 2671 int parseStart = pos.getIndex(); 2672 // END Android-changed: Better UTS#35 conformity for fractional seconds. 2673 if (obeyCount) { 2674 if ((start+count) > text.length()) { 2675 break parsing; 2676 } 2677 number = numberFormat.parse(text.substring(0, start+count), pos); 2678 } else { 2679 number = numberFormat.parse(text, pos); 2680 } 2681 if (number != null) { 2682 // BEGIN Android-changed: Better UTS#35 conformity for fractional seconds. 2683 /* 2684 value = number.intValue(); 2685 */ 2686 if (patternCharIndex == PATTERN_MILLISECOND) { 2687 // Fractional seconds must be treated specially. We must always 2688 // normalize them to their fractional second value [0, 1) before we attempt 2689 // to parse them. 2690 // 2691 // Case 1: 11.78 seconds is 11 seconds and 780 (not 78) milliseconds. 2692 // Case 2: 11.7890567 seconds is 11 seconds and 789 (not 7890567) milliseconds. 2693 double doubleValue = number.doubleValue(); 2694 int width = pos.getIndex() - parseStart; 2695 final double divisor = Math.pow(10, width); 2696 value = (int) ((doubleValue / divisor) * 1000); 2697 } else { 2698 value = number.intValue(); 2699 } 2700 // END Android-changed: Better UTS#35 conformity for fractional seconds. 2701 2702 if (useFollowingMinusSignAsDelimiter && (value < 0) && 2703 (((pos.index < text.length()) && 2704 (text.charAt(pos.index) != minusSign)) || 2705 ((pos.index == text.length()) && 2706 (text.charAt(pos.index-1) == minusSign)))) { 2707 value = -value; 2708 pos.index--; 2709 } 2710 2711 calb.set(field, value); 2712 return pos.index; 2713 } 2714 break parsing; 2715 } 2716 } 2717 2718 // Parsing failed. 2719 origPos.errorIndex = pos.index; 2720 return -1; 2721 } 2722 2723 // BEGIN Android-added: parseMonth and parseWeekday methods to parse using ICU data. parseMonth(String text, int count, int value, int start, int field, ParsePosition pos, boolean useDateFormatSymbols, boolean standalone, CalendarBuilder out)2724 private int parseMonth(String text, int count, int value, int start, 2725 int field, ParsePosition pos, boolean useDateFormatSymbols, 2726 boolean standalone, 2727 CalendarBuilder out) { 2728 if (count <= 2) // i.e., M or MM. 2729 { 2730 // Don't want to parse the month if it is a string 2731 // while pattern uses numeric style: M or MM. 2732 // [We computed 'value' above.] 2733 out.set(Calendar.MONTH, value - 1); 2734 return pos.index; 2735 } 2736 2737 int index = -1; 2738 if (useDateFormatSymbols) { 2739 // count >= 3 // i.e., MMM or MMMM 2740 // Want to be able to parse both short and long forms. 2741 // Try count == 4 first: 2742 if ((index = matchString( 2743 text, start, Calendar.MONTH, 2744 standalone ? formatData.getStandAloneMonths() : formatData.getMonths(), 2745 out)) > 0) { 2746 return index; 2747 } 2748 // count == 4 failed, now try count == 3 2749 if ((index = matchString( 2750 text, start, Calendar.MONTH, 2751 standalone ? formatData.getShortStandAloneMonths() : formatData.getShortMonths(), 2752 out)) > 0) { 2753 return index; 2754 } 2755 } else { 2756 Map<String, Integer> map = getDisplayNamesMap(field, locale); 2757 if ((index = matchString(text, start, field, map, out)) > 0) { 2758 return index; 2759 } 2760 } 2761 2762 return index; 2763 } 2764 parseWeekday(String text, int start, int field, boolean useDateFormatSymbols, boolean standalone, CalendarBuilder out)2765 private int parseWeekday(String text, int start, int field, boolean useDateFormatSymbols, 2766 boolean standalone, CalendarBuilder out) { 2767 int index = -1; 2768 if (useDateFormatSymbols) { 2769 // Want to be able to parse both short and long forms. 2770 // Try count == 4 (DDDD) first: 2771 if ((index=matchString( 2772 text, start, Calendar.DAY_OF_WEEK, 2773 standalone ? formatData.getStandAloneWeekdays() : formatData.getWeekdays(), 2774 out)) > 0) { 2775 return index; 2776 } 2777 2778 // DDDD failed, now try DDD 2779 if ((index = matchString( 2780 text, start, Calendar.DAY_OF_WEEK, 2781 standalone ? formatData.getShortStandAloneWeekdays() : formatData.getShortWeekdays(), 2782 out)) > 0) { 2783 return index; 2784 } 2785 } else { 2786 int[] styles = { Calendar.LONG, Calendar.SHORT }; 2787 for (int style : styles) { 2788 Map<String,Integer> map = calendar.getDisplayNames(field, style, locale); 2789 if ((index = matchString(text, start, field, map, out)) > 0) { 2790 return index; 2791 } 2792 } 2793 } 2794 2795 return index; 2796 } 2797 // END Android-added: parseMonth and parseWeekday methods to parse using ICU data. 2798 2799 // Android-changed: Always useDateFormatSymbols() for GregorianCalendar. 2800 /** 2801 * Returns true if the DateFormatSymbols has been set explicitly or locale 2802 * is null or calendar is Gregorian. 2803 */ useDateFormatSymbols()2804 private boolean useDateFormatSymbols() { 2805 // Android-changed: Always useDateFormatSymbols() for GregorianCalendar. 2806 // This is for app compat. http://b/66411240#comment14 2807 // return useDateFormatSymbols || locale == null; 2808 return useDateFormatSymbols 2809 || "java.util.GregorianCalendar".equals(calendar.getClass().getName()) 2810 || locale == null; 2811 } 2812 2813 /** 2814 * Translates a pattern, mapping each character in the from string to the 2815 * corresponding character in the to string. 2816 * 2817 * @exception IllegalArgumentException if the given pattern is invalid 2818 */ translatePattern(String pattern, String from, String to)2819 private String translatePattern(String pattern, String from, String to) { 2820 StringBuilder result = new StringBuilder(); 2821 boolean inQuote = false; 2822 for (int i = 0; i < pattern.length(); ++i) { 2823 char c = pattern.charAt(i); 2824 if (inQuote) { 2825 if (c == '\'') { 2826 inQuote = false; 2827 } 2828 } 2829 else { 2830 if (c == '\'') { 2831 inQuote = true; 2832 } else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { 2833 int ci = from.indexOf(c); 2834 if (ci >= 0) { 2835 // patternChars is longer than localPatternChars due 2836 // to serialization compatibility. The pattern letters 2837 // unsupported by localPatternChars pass through. 2838 if (ci < to.length()) { 2839 c = to.charAt(ci); 2840 } 2841 } else { 2842 throw new IllegalArgumentException("Illegal pattern " + 2843 " character '" + 2844 c + "'"); 2845 } 2846 } 2847 } 2848 result.append(c); 2849 } 2850 if (inQuote) { 2851 throw new IllegalArgumentException("Unfinished quote in pattern"); 2852 } 2853 return result.toString(); 2854 } 2855 2856 /** 2857 * Returns a pattern string describing this date format. 2858 * 2859 * @return a pattern string describing this date format. 2860 */ toPattern()2861 public String toPattern() { 2862 return pattern; 2863 } 2864 2865 /** 2866 * Returns a localized pattern string describing this date format. 2867 * 2868 * @return a localized pattern string describing this date format. 2869 */ toLocalizedPattern()2870 public String toLocalizedPattern() { 2871 return translatePattern(pattern, 2872 DateFormatSymbols.patternChars, 2873 formatData.getLocalPatternChars()); 2874 } 2875 2876 /** 2877 * Applies the given pattern string to this date format. 2878 * 2879 * @param pattern the new date and time pattern for this date format 2880 * @exception NullPointerException if the given pattern is null 2881 * @exception IllegalArgumentException if the given pattern is invalid 2882 */ applyPattern(String pattern)2883 public void applyPattern(String pattern) 2884 { 2885 applyPatternImpl(pattern); 2886 } 2887 applyPatternImpl(String pattern)2888 private void applyPatternImpl(String pattern) { 2889 compiledPattern = compile(pattern); 2890 this.pattern = pattern; 2891 } 2892 2893 /** 2894 * Applies the given localized pattern string to this date format. 2895 * 2896 * @param pattern a String to be mapped to the new date and time format 2897 * pattern for this format 2898 * @exception NullPointerException if the given pattern is null 2899 * @exception IllegalArgumentException if the given pattern is invalid 2900 */ applyLocalizedPattern(String pattern)2901 public void applyLocalizedPattern(String pattern) { 2902 String p = translatePattern(pattern, 2903 formatData.getLocalPatternChars(), 2904 DateFormatSymbols.patternChars); 2905 compiledPattern = compile(p); 2906 this.pattern = p; 2907 } 2908 2909 /** 2910 * Gets a copy of the date and time format symbols of this date format. 2911 * 2912 * @return the date and time format symbols of this date format 2913 * @see #setDateFormatSymbols 2914 */ getDateFormatSymbols()2915 public DateFormatSymbols getDateFormatSymbols() 2916 { 2917 return (DateFormatSymbols)formatData.clone(); 2918 } 2919 2920 /** 2921 * Sets the date and time format symbols of this date format. 2922 * 2923 * @param newFormatSymbols the new date and time format symbols 2924 * @exception NullPointerException if the given newFormatSymbols is null 2925 * @see #getDateFormatSymbols 2926 */ setDateFormatSymbols(DateFormatSymbols newFormatSymbols)2927 public void setDateFormatSymbols(DateFormatSymbols newFormatSymbols) 2928 { 2929 this.formatData = (DateFormatSymbols)newFormatSymbols.clone(); 2930 useDateFormatSymbols = true; 2931 } 2932 2933 /** 2934 * Creates a copy of this <code>SimpleDateFormat</code>. This also 2935 * clones the format's date format symbols. 2936 * 2937 * @return a clone of this <code>SimpleDateFormat</code> 2938 */ 2939 @Override clone()2940 public Object clone() { 2941 SimpleDateFormat other = (SimpleDateFormat) super.clone(); 2942 other.formatData = (DateFormatSymbols) formatData.clone(); 2943 return other; 2944 } 2945 2946 /** 2947 * Returns the hash code value for this <code>SimpleDateFormat</code> object. 2948 * 2949 * @return the hash code value for this <code>SimpleDateFormat</code> object. 2950 */ 2951 @Override hashCode()2952 public int hashCode() 2953 { 2954 return pattern.hashCode(); 2955 // just enough fields for a reasonable distribution 2956 } 2957 2958 /** 2959 * Compares the given object with this <code>SimpleDateFormat</code> for 2960 * equality. 2961 * 2962 * @return true if the given object is equal to this 2963 * <code>SimpleDateFormat</code> 2964 */ 2965 @Override equals(Object obj)2966 public boolean equals(Object obj) 2967 { 2968 if (!super.equals(obj)) { 2969 return false; // super does class check 2970 } 2971 SimpleDateFormat that = (SimpleDateFormat) obj; 2972 return (pattern.equals(that.pattern) 2973 && formatData.equals(that.formatData)); 2974 } 2975 2976 private static final int[] REST_OF_STYLES = { 2977 Calendar.SHORT_STANDALONE, Calendar.LONG_FORMAT, Calendar.LONG_STANDALONE, 2978 }; getDisplayNamesMap(int field, Locale locale)2979 private Map<String, Integer> getDisplayNamesMap(int field, Locale locale) { 2980 Map<String, Integer> map = calendar.getDisplayNames(field, Calendar.SHORT_FORMAT, locale); 2981 // Get all SHORT and LONG styles (avoid NARROW styles). 2982 for (int style : REST_OF_STYLES) { 2983 Map<String, Integer> m = calendar.getDisplayNames(field, style, locale); 2984 if (m != null) { 2985 map.putAll(m); 2986 } 2987 } 2988 return map; 2989 } 2990 2991 /** 2992 * After reading an object from the input stream, the format 2993 * pattern in the object is verified. 2994 * <p> 2995 * @exception InvalidObjectException if the pattern is invalid 2996 */ readObject(ObjectInputStream stream)2997 private void readObject(ObjectInputStream stream) 2998 throws IOException, ClassNotFoundException { 2999 stream.defaultReadObject(); 3000 3001 try { 3002 compiledPattern = compile(pattern); 3003 } catch (Exception e) { 3004 throw new InvalidObjectException("invalid pattern"); 3005 } 3006 3007 if (serialVersionOnStream < 1) { 3008 // didn't have defaultCenturyStart field 3009 initializeDefaultCentury(); 3010 } 3011 else { 3012 // fill in dependent transient field 3013 parseAmbiguousDatesAsAfter(defaultCenturyStart); 3014 } 3015 serialVersionOnStream = currentSerialVersion; 3016 3017 // If the deserialized object has a SimpleTimeZone, try 3018 // to replace it with a ZoneInfo equivalent in order to 3019 // be compatible with the SimpleTimeZone-based 3020 // implementation as much as possible. 3021 TimeZone tz = getTimeZone(); 3022 if (tz instanceof SimpleTimeZone) { 3023 String id = tz.getID(); 3024 TimeZone zi = TimeZone.getTimeZone(id); 3025 if (zi != null && zi.hasSameRules(tz) && zi.getID().equals(id)) { 3026 setTimeZone(zi); 3027 } 3028 } 3029 } 3030 3031 /** 3032 * Analyze the negative subpattern of DecimalFormat and set/update values 3033 * as necessary. 3034 */ checkNegativeNumberExpression()3035 private void checkNegativeNumberExpression() { 3036 if ((numberFormat instanceof DecimalFormat) && 3037 !numberFormat.equals(originalNumberFormat)) { 3038 String numberPattern = ((DecimalFormat)numberFormat).toPattern(); 3039 if (!numberPattern.equals(originalNumberPattern)) { 3040 hasFollowingMinusSign = false; 3041 3042 int separatorIndex = numberPattern.indexOf(';'); 3043 // If the negative subpattern is not absent, we have to analayze 3044 // it in order to check if it has a following minus sign. 3045 if (separatorIndex > -1) { 3046 int minusIndex = numberPattern.indexOf('-', separatorIndex); 3047 if ((minusIndex > numberPattern.lastIndexOf('0')) && 3048 (minusIndex > numberPattern.lastIndexOf('#'))) { 3049 hasFollowingMinusSign = true; 3050 minusSign = ((DecimalFormat)numberFormat).getDecimalFormatSymbols().getMinusSign(); 3051 } 3052 } 3053 originalNumberPattern = numberPattern; 3054 } 3055 originalNumberFormat = numberFormat; 3056 } 3057 } 3058 3059 } 3060