1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package java.text; 19 20 import java.io.IOException; 21 import java.io.ObjectInputStream; 22 import java.io.ObjectOutputStream; 23 import java.io.ObjectStreamField; 24 import java.util.ArrayList; 25 import java.util.Calendar; 26 import java.util.Date; 27 import java.util.GregorianCalendar; 28 import java.util.List; 29 import java.util.Locale; 30 import java.util.SimpleTimeZone; 31 import java.util.TimeZone; 32 import libcore.icu.LocaleData; 33 import libcore.icu.TimeZoneNames; 34 35 /** 36 * Formats and parses dates in a locale-sensitive manner. Formatting turns a {@link Date} into 37 * a {@link String}, and parsing turns a {@code String} into a {@code Date}. 38 * 39 * <h4>Time Pattern Syntax</h4> 40 * <p>You can supply a Unicode <a href="http://www.unicode.org/reports/tr35/#Date_Format_Patterns">UTS #35</a> 41 * pattern describing what strings are produced/accepted, but almost all 42 * callers should use {@link DateFormat#getDateInstance}, {@link DateFormat#getDateTimeInstance}, 43 * or {@link DateFormat#getTimeInstance} to get a ready-made instance suitable for the user's 44 * locale. In cases where the system does not provide a suitable pattern, see 45 * {@link android.text.format.DateFormat#getBestDateTimePattern} which lets you specify 46 * the elements you'd like in a pattern and get back a pattern suitable for any given locale. 47 * 48 * <p>The main reason you'd create an instance this class directly is because you need to 49 * format/parse a specific machine-readable format, in which case you almost certainly want 50 * to explicitly ask for {@link Locale#US} to ensure that you get ASCII digits (rather than, 51 * say, Arabic digits). 52 * (See "<a href="../util/Locale.html#default_locale">Be wary of the default locale</a>".) 53 * The most useful non-localized pattern is {@code "yyyy-MM-dd HH:mm:ss.SSSZ"}, which corresponds 54 * to the ISO 8601 international standard date format. 55 * 56 * <p>To specify the time format, use a <i>time pattern</i> string. In this 57 * string, any character from {@code 'A'} to {@code 'Z'} or {@code 'a'} to {@code 'z'} is 58 * treated specially. All other characters are passed through verbatim. The interpretation of each 59 * of the ASCII letters is given in the table below. ASCII letters not appearing in the table are 60 * reserved for future use, and it is an error to attempt to use them. 61 * 62 * <p>The number of consecutive copies (the "count") of a pattern character further influences 63 * the format, as shown in the table. For fields of kind "number", the count is the minimum number 64 * of digits; shorter values are zero-padded to the given width and longer values overflow it. 65 * 66 * <p><table BORDER="1" WIDTH="100%" CELLPADDING="3" CELLSPACING="0" SUMMARY=""> 67 * <tr BGCOLOR="#CCCCFF" CLASS="TableHeadingColor"> 68 * <td><B>Symbol</B></td> <td><B>Meaning</B></td> <td><B>Kind</B></td> <td><B>Example</B></td> </tr> 69 * <tr> <td>{@code D}</td> <td>day in year</td> <td>(Number)</td> <td>189</td> </tr> 70 * <tr> <td>{@code E}</td> <td>day of week</td> <td>(Text)</td> <td>{@code E}/{@code EE}/{@code EEE}:Tue, {@code EEEE}:Tuesday, {@code EEEEE}:T</td> </tr> 71 * <tr> <td>{@code F}</td> <td>day of week in month</td> <td>(Number)</td> <td>2 <i>(2nd Wed in July)</i></td> </tr> 72 * <tr> <td>{@code G}</td> <td>era designator</td> <td>(Text)</td> <td>AD</td> </tr> 73 * <tr> <td>{@code H}</td> <td>hour in day (0-23)</td> <td>(Number)</td> <td>0</td> </tr> 74 * <tr> <td>{@code K}</td> <td>hour in am/pm (0-11)</td> <td>(Number)</td> <td>0</td> </tr> 75 * <tr> <td>{@code L}</td> <td>stand-alone month</td> <td>(Text)</td> <td>{@code L}:1 {@code LL}:01 {@code LLL}:Jan {@code LLLL}:January {@code LLLLL}:J</td> </tr> 76 * <tr> <td>{@code M}</td> <td>month in year</td> <td>(Text)</td> <td>{@code M}:1 {@code MM}:01 {@code MMM}:Jan {@code MMMM}:January {@code MMMMM}:J</td> </tr> 77 * <tr> <td>{@code S}</td> <td>fractional seconds</td> <td>(Number)</td> <td>978</td> </tr> 78 * <tr> <td>{@code W}</td> <td>week in month</td> <td>(Number)</td> <td>2</td> </tr> 79 * <tr> <td>{@code Z}</td> <td>time zone (RFC 822)</td> <td>(Time Zone)</td> <td>{@code Z}/{@code ZZ}/{@code ZZZ}:-0800 {@code ZZZZ}:GMT-08:00 {@code ZZZZZ}:-08:00</td> </tr> 80 * <tr> <td>{@code a}</td> <td>am/pm marker</td> <td>(Text)</td> <td>PM</td> </tr> 81 * <tr> <td>{@code c}</td> <td>stand-alone day of week</td> <td>(Text)</td> <td>{@code c}/{@code cc}/{@code ccc}:Tue, {@code cccc}:Tuesday, {@code ccccc}:T</td> </tr> 82 * <tr> <td>{@code d}</td> <td>day in month</td> <td>(Number)</td> <td>10</td> </tr> 83 * <tr> <td>{@code h}</td> <td>hour in am/pm (1-12)</td> <td>(Number)</td> <td>12</td> </tr> 84 * <tr> <td>{@code k}</td> <td>hour in day (1-24)</td> <td>(Number)</td> <td>24</td> </tr> 85 * <tr> <td>{@code m}</td> <td>minute in hour</td> <td>(Number)</td> <td>30</td> </tr> 86 * <tr> <td>{@code s}</td> <td>second in minute</td> <td>(Number)</td> <td>55</td> </tr> 87 * <tr> <td>{@code w}</td> <td>week in year</td> <td>(Number)</td> <td>27</td> </tr> 88 * <tr> <td>{@code y}</td> <td>year</td> <td>(Number)</td> <td>{@code yy}:10 {@code y}/{@code yyy}/{@code yyyy}:2010</td> </tr> 89 * <tr> <td>{@code z}</td> <td>time zone</td> <td>(Time Zone)</td> <td>{@code z}/{@code zz}/{@code zzz}:PST {@code zzzz}:Pacific Standard Time</td> </tr> 90 * <tr> <td>{@code '}</td> <td>escape for text</td> <td>(Delimiter)</td> <td>{@code 'Date='}:Date=</td> </tr> 91 * <tr> <td>{@code ''}</td> <td>single quote</td> <td>(Literal)</td> <td>{@code 'o''clock'}:o'clock</td> </tr> 92 * </table> 93 * 94 * <p>Fractional seconds are handled specially: they're zero-padded on the <i>right</i>. 95 * 96 * <p>The two pattern characters {@code L} and {@code c} are ICU-compatible extensions, not 97 * available in the RI or in Android before Android 2.3 (Gingerbread, API level 9). These 98 * extensions are necessary for correct localization in languages such as Russian 99 * that make a grammatical distinction between, say, the word "June" in the sentence "June" and 100 * in the sentence "June 10th"; the former is the stand-alone form, the latter the regular 101 * form (because the usual case is to format a complete date). The relationship between {@code E} 102 * and {@code c} is equivalent, but for weekday names. 103 * 104 * <p>Five-count patterns (such as "MMMMM") used for the shortest non-numeric 105 * representation of a field were introduced in Android 4.3 (Jelly Bean MR2, API level 18). 106 * 107 * <p>When two numeric fields are directly adjacent with no intervening delimiter 108 * characters, they constitute a run of adjacent numeric fields. Such runs are 109 * parsed specially. For example, the format "HHmmss" parses the input text 110 * "123456" to 12:34:56, parses the input text "12345" to 1:23:45, and fails to 111 * parse "1234". In other words, the leftmost field of the run is flexible, 112 * while the others keep a fixed width. If the parse fails anywhere in the run, 113 * then the leftmost field is shortened by one character, and the entire run is 114 * parsed again. This is repeated until either the parse succeeds or the 115 * leftmost field is one character in length. If the parse still fails at that 116 * point, the parse of the run fails. 117 * 118 * <p>See {@link #set2DigitYearStart} for more about handling two-digit years. 119 * 120 * <h4>Sample Code</h4> 121 * <p>If you're formatting for human use, you should use an instance returned from 122 * {@link DateFormat} as described above. This code: 123 * <pre> 124 * DateFormat[] formats = new DateFormat[] { 125 * DateFormat.getDateInstance(), 126 * DateFormat.getDateTimeInstance(), 127 * DateFormat.getTimeInstance(), 128 * }; 129 * for (DateFormat df : formats) { 130 * System.out.println(df.format(new Date(0))); 131 * } 132 * </pre> 133 * 134 * <p>Produces this output when run on an {@code en_US} device in the America/Los_Angeles time zone: 135 * <pre> 136 * Dec 31, 1969 137 * Dec 31, 1969 4:00:00 PM 138 * 4:00:00 PM 139 * </pre> 140 * And will produce similarly appropriate localized human-readable output on any user's system. 141 * 142 * <p>If you're formatting for machine use, consider this code: 143 * <pre> 144 * String[] formats = new String[] { 145 * "yyyy-MM-dd", 146 * "yyyy-MM-dd HH:mm", 147 * "yyyy-MM-dd HH:mmZ", 148 * "yyyy-MM-dd HH:mm:ss.SSSZ", 149 * "yyyy-MM-dd'T'HH:mm:ss.SSSZ", 150 * }; 151 * for (String format : formats) { 152 * SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.US); 153 * System.out.format("%30s %s\n", format, sdf.format(new Date(0))); 154 * sdf.setTimeZone(TimeZone.getTimeZone("UTC")); 155 * System.out.format("%30s %s\n", format, sdf.format(new Date(0))); 156 * } 157 * </pre> 158 * 159 * <p>Which produces this output when run in the America/Los_Angeles time zone: 160 * <pre> 161 * yyyy-MM-dd 1969-12-31 162 * yyyy-MM-dd 1970-01-01 163 * yyyy-MM-dd HH:mm 1969-12-31 16:00 164 * yyyy-MM-dd HH:mm 1970-01-01 00:00 165 * yyyy-MM-dd HH:mmZ 1969-12-31 16:00-0800 166 * yyyy-MM-dd HH:mmZ 1970-01-01 00:00+0000 167 * yyyy-MM-dd HH:mm:ss.SSSZ 1969-12-31 16:00:00.000-0800 168 * yyyy-MM-dd HH:mm:ss.SSSZ 1970-01-01 00:00:00.000+0000 169 * yyyy-MM-dd'T'HH:mm:ss.SSSZ 1969-12-31T16:00:00.000-0800 170 * yyyy-MM-dd'T'HH:mm:ss.SSSZ 1970-01-01T00:00:00.000+0000 171 * </pre> 172 * 173 * <p>As this example shows, each {@code SimpleDateFormat} instance has a {@link TimeZone}. 174 * This is because it's called upon to format instances of {@code Date}, which represents an 175 * absolute time in UTC. That is, {@code Date} does not carry time zone information. 176 * By default, {@code SimpleDateFormat} will use the system's default time zone. This is 177 * appropriate for human-readable output (for which, see the previous sample instead), but 178 * generally inappropriate for machine-readable output, where ambiguity is a problem. Note that 179 * in this example, the output that included a time but no time zone cannot be parsed back into 180 * the original {@code Date}. For this 181 * reason it is almost always necessary and desirable to include the timezone in the output. 182 * It may also be desirable to set the formatter's time zone to UTC (to ease comparison, or to 183 * make logs more readable, for example). It is often best to avoid formatting completely when 184 * writing dates/times in machine-readable form. Simply sending the "Unix time" as a {@code long} 185 * or as the string corresponding to the long is cheaper and unambiguous, and can be formatted any 186 * way the recipient deems appropriate. 187 * 188 * <h4>Synchronization</h4> 189 * {@code SimpleDateFormat} is not thread-safe. Users should create a separate instance for 190 * each thread. 191 * 192 * @see java.util.Calendar 193 * @see java.util.Date 194 * @see java.util.TimeZone 195 * @see java.text.DateFormat 196 */ 197 public class SimpleDateFormat extends DateFormat { 198 199 private static final long serialVersionUID = 4774881970558875024L; 200 201 // 'L' and 'c' are ICU-compatible extensions for stand-alone month and stand-alone weekday. 202 static final String PATTERN_CHARS = "GyMdkHmsSEDFwWahKzZLc"; 203 204 // The index of 'Z' in the PATTERN_CHARS string. This pattern character is supported by the RI, 205 // but has no corresponding public constant. 206 private static final int RFC_822_TIMEZONE_FIELD = 18; 207 208 // The index of 'L' (cf. 'M') in the PATTERN_CHARS string. This is an ICU-compatible extension 209 // necessary for correct localization in various languages (http://b/2633414). 210 private static final int STAND_ALONE_MONTH_FIELD = 19; 211 // The index of 'c' (cf. 'E') in the PATTERN_CHARS string. This is an ICU-compatible extension 212 // necessary for correct localization in various languages (http://b/2633414). 213 private static final int STAND_ALONE_DAY_OF_WEEK_FIELD = 20; 214 215 private String pattern; 216 217 private DateFormatSymbols formatData; 218 219 transient private int creationYear; 220 221 private Date defaultCenturyStart; 222 223 /** 224 * Constructs a new {@code SimpleDateFormat} for formatting and parsing 225 * dates and times in the {@code SHORT} style for the user's default locale. 226 * See "<a href="../util/Locale.html#default_locale">Be wary of the default locale</a>". 227 */ 228 public SimpleDateFormat() { 229 this(Locale.getDefault()); 230 this.pattern = defaultPattern(); 231 this.formatData = new DateFormatSymbols(Locale.getDefault()); 232 } 233 234 /** 235 * Constructs a new {@code SimpleDateFormat} using the specified 236 * non-localized pattern and the {@code DateFormatSymbols} and {@code 237 * Calendar} for the user's default locale. 238 * See "<a href="../util/Locale.html#default_locale">Be wary of the default locale</a>". 239 * 240 * @param pattern 241 * the pattern. 242 * @throws NullPointerException 243 * if the pattern is {@code null}. 244 * @throws IllegalArgumentException 245 * if {@code pattern} is not considered to be usable by this 246 * formatter. 247 */ 248 public SimpleDateFormat(String pattern) { 249 this(pattern, Locale.getDefault()); 250 } 251 252 /** 253 * Validates the pattern. 254 * 255 * @param template 256 * the pattern to validate. 257 * 258 * @throws NullPointerException 259 * if the pattern is null 260 * @throws IllegalArgumentException 261 * if the pattern is invalid 262 */ 263 private void validatePattern(String template) { 264 boolean quote = false; 265 int next, last = -1, count = 0; 266 267 final int patternLength = template.length(); 268 for (int i = 0; i < patternLength; i++) { 269 next = (template.charAt(i)); 270 if (next == '\'') { 271 if (count > 0) { 272 validatePatternCharacter((char) last); 273 count = 0; 274 } 275 if (last == next) { 276 last = -1; 277 } else { 278 last = next; 279 } 280 quote = !quote; 281 continue; 282 } 283 if (!quote 284 && (last == next || (next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z'))) { 285 if (last == next) { 286 count++; 287 } else { 288 if (count > 0) { 289 validatePatternCharacter((char) last); 290 } 291 last = next; 292 count = 1; 293 } 294 } else { 295 if (count > 0) { 296 validatePatternCharacter((char) last); 297 count = 0; 298 } 299 last = -1; 300 } 301 } 302 if (count > 0) { 303 validatePatternCharacter((char) last); 304 } 305 306 if (quote) { 307 throw new IllegalArgumentException("Unterminated quote"); 308 } 309 } 310 311 private void validatePatternCharacter(char format) { 312 int index = PATTERN_CHARS.indexOf(format); 313 if (index == -1) { 314 throw new IllegalArgumentException("Unknown pattern character '" + format + "'"); 315 } 316 } 317 318 /** 319 * Constructs a new {@code SimpleDateFormat} using the specified 320 * non-localized pattern and {@code DateFormatSymbols} and the {@code 321 * Calendar} for the user's default locale. 322 * See "<a href="../util/Locale.html#default_locale">Be wary of the default locale</a>". 323 * 324 * @param template 325 * the pattern. 326 * @param value 327 * the DateFormatSymbols. 328 * @throws NullPointerException 329 * if the pattern is {@code null}. 330 * @throws IllegalArgumentException 331 * if the pattern is invalid. 332 */ 333 public SimpleDateFormat(String template, DateFormatSymbols value) { 334 this(Locale.getDefault()); 335 validatePattern(template); 336 pattern = template; 337 formatData = (DateFormatSymbols) value.clone(); 338 } 339 340 /** 341 * Constructs a new {@code SimpleDateFormat} using the specified 342 * non-localized pattern and the {@code DateFormatSymbols} and {@code 343 * Calendar} for the specified locale. 344 * 345 * @param template 346 * the pattern. 347 * @param locale 348 * the locale. 349 * @throws NullPointerException 350 * if the pattern is {@code null}. 351 * @throws IllegalArgumentException 352 * if the pattern is invalid. 353 */ 354 public SimpleDateFormat(String template, Locale locale) { 355 this(locale); 356 validatePattern(template); 357 pattern = template; 358 formatData = new DateFormatSymbols(locale); 359 } 360 361 private SimpleDateFormat(Locale locale) { 362 numberFormat = NumberFormat.getInstance(locale); 363 numberFormat.setParseIntegerOnly(true); 364 numberFormat.setGroupingUsed(false); 365 calendar = new GregorianCalendar(locale); 366 calendar.add(Calendar.YEAR, -80); 367 creationYear = calendar.get(Calendar.YEAR); 368 defaultCenturyStart = calendar.getTime(); 369 } 370 371 /** 372 * Changes the pattern of this simple date format to the specified pattern 373 * which uses localized pattern characters. 374 * 375 * @param template 376 * the localized pattern. 377 */ 378 public void applyLocalizedPattern(String template) { 379 pattern = convertPattern(template, formatData.getLocalPatternChars(), PATTERN_CHARS, true); 380 } 381 382 /** 383 * Changes the pattern of this simple date format to the specified pattern 384 * which uses non-localized pattern characters. 385 * 386 * @param template 387 * the non-localized pattern. 388 * @throws NullPointerException 389 * if the pattern is {@code null}. 390 * @throws IllegalArgumentException 391 * if the pattern is invalid. 392 */ 393 public void applyPattern(String template) { 394 validatePattern(template); 395 pattern = template; 396 } 397 398 /** 399 * Returns a new {@code SimpleDateFormat} with the same pattern and 400 * properties as this simple date format. 401 */ 402 @Override 403 public Object clone() { 404 SimpleDateFormat clone = (SimpleDateFormat) super.clone(); 405 clone.formatData = (DateFormatSymbols) formatData.clone(); 406 clone.defaultCenturyStart = new Date(defaultCenturyStart.getTime()); 407 return clone; 408 } 409 410 private static String defaultPattern() { 411 LocaleData localeData = LocaleData.get(Locale.getDefault()); 412 return localeData.getDateFormat(SHORT) + " " + localeData.getTimeFormat(SHORT); 413 } 414 415 /** 416 * Compares the specified object with this simple date format and indicates 417 * if they are equal. In order to be equal, {@code object} must be an 418 * instance of {@code SimpleDateFormat} and have the same {@code DateFormat} 419 * properties, pattern, {@code DateFormatSymbols} and creation year. 420 * 421 * @param object 422 * the object to compare with this object. 423 * @return {@code true} if the specified object is equal to this simple date 424 * format; {@code false} otherwise. 425 * @see #hashCode 426 */ 427 @Override 428 public boolean equals(Object object) { 429 if (this == object) { 430 return true; 431 } 432 if (!(object instanceof SimpleDateFormat)) { 433 return false; 434 } 435 SimpleDateFormat simple = (SimpleDateFormat) object; 436 return super.equals(object) && pattern.equals(simple.pattern) 437 && formatData.equals(simple.formatData); 438 } 439 440 /** 441 * Formats the specified object using the rules of this simple date format 442 * and returns an {@code AttributedCharacterIterator} with the formatted 443 * date and attributes. 444 * 445 * @param object 446 * the object to format. 447 * @return an {@code AttributedCharacterIterator} with the formatted date 448 * and attributes. 449 * @throws NullPointerException 450 * if the object is {@code null}. 451 * @throws IllegalArgumentException 452 * if the object cannot be formatted by this simple date 453 * format. 454 */ 455 @Override 456 public AttributedCharacterIterator formatToCharacterIterator(Object object) { 457 if (object == null) { 458 throw new NullPointerException("object == null"); 459 } 460 if (object instanceof Date) { 461 return formatToCharacterIteratorImpl((Date) object); 462 } 463 if (object instanceof Number) { 464 return formatToCharacterIteratorImpl(new Date(((Number) object).longValue())); 465 } 466 throw new IllegalArgumentException("Bad class: " + object.getClass()); 467 } 468 469 private AttributedCharacterIterator formatToCharacterIteratorImpl(Date date) { 470 StringBuffer buffer = new StringBuffer(); 471 ArrayList<FieldPosition> fields = new ArrayList<FieldPosition>(); 472 473 // format the date, and find fields 474 formatImpl(date, buffer, null, fields); 475 476 // create and AttributedString with the formatted buffer 477 AttributedString as = new AttributedString(buffer.toString()); 478 479 // add DateFormat field attributes to the AttributedString 480 for (FieldPosition pos : fields) { 481 Format.Field attribute = pos.getFieldAttribute(); 482 as.addAttribute(attribute, attribute, pos.getBeginIndex(), pos.getEndIndex()); 483 } 484 485 // return the CharacterIterator from AttributedString 486 return as.getIterator(); 487 } 488 489 /** 490 * Formats the date. 491 * <p> 492 * If the FieldPosition {@code field} is not null, and the field 493 * specified by this FieldPosition is formatted, set the begin and end index 494 * of the formatted field in the FieldPosition. 495 * <p> 496 * If the list {@code fields} is not null, find fields of this 497 * date, set FieldPositions with these fields, and add them to the fields 498 * vector. 499 * 500 * @param date 501 * Date to Format 502 * @param buffer 503 * StringBuffer to store the resulting formatted String 504 * @param field 505 * FieldPosition to set begin and end index of the field 506 * specified, if it is part of the format for this date 507 * @param fields 508 * list used to store the FieldPositions for each field in this 509 * date 510 * @return the formatted Date 511 * @throws IllegalArgumentException 512 * if the object cannot be formatted by this Format. 513 */ 514 private StringBuffer formatImpl(Date date, StringBuffer buffer, 515 FieldPosition field, List<FieldPosition> fields) { 516 boolean quote = false; 517 int next, last = -1, count = 0; 518 calendar.setTime(date); 519 if (field != null) { 520 field.clear(); 521 } 522 523 final int patternLength = pattern.length(); 524 for (int i = 0; i < patternLength; i++) { 525 next = (pattern.charAt(i)); 526 if (next == '\'') { 527 if (count > 0) { 528 append(buffer, field, fields, (char) last, count); 529 count = 0; 530 } 531 if (last == next) { 532 buffer.append('\''); 533 last = -1; 534 } else { 535 last = next; 536 } 537 quote = !quote; 538 continue; 539 } 540 if (!quote 541 && (last == next || (next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z'))) { 542 if (last == next) { 543 count++; 544 } else { 545 if (count > 0) { 546 append(buffer, field, fields, (char) last, count); 547 } 548 last = next; 549 count = 1; 550 } 551 } else { 552 if (count > 0) { 553 append(buffer, field, fields, (char) last, count); 554 count = 0; 555 } 556 last = -1; 557 buffer.append((char) next); 558 } 559 } 560 if (count > 0) { 561 append(buffer, field, fields, (char) last, count); 562 } 563 return buffer; 564 } 565 566 private void append(StringBuffer buffer, FieldPosition position, 567 List<FieldPosition> fields, char format, int count) { 568 int field = -1; 569 int index = PATTERN_CHARS.indexOf(format); 570 if (index == -1) { 571 throw new IllegalArgumentException("Unknown pattern character '" + format + "'"); 572 } 573 574 int beginPosition = buffer.length(); 575 Field dateFormatField = null; 576 switch (index) { 577 case ERA_FIELD: 578 dateFormatField = Field.ERA; 579 buffer.append(formatData.eras[calendar.get(Calendar.ERA)]); 580 break; 581 case YEAR_FIELD: 582 dateFormatField = Field.YEAR; 583 int year = calendar.get(Calendar.YEAR); 584 /* 585 * For 'y' and 'yyy', we're consistent with Unicode and previous releases 586 * of Android. But this means we're inconsistent with the RI. 587 * http://unicode.org/reports/tr35/ 588 */ 589 if (count == 2) { 590 appendNumber(buffer, 2, year % 100); 591 } else { 592 appendNumber(buffer, count, year); 593 } 594 break; 595 case STAND_ALONE_MONTH_FIELD: // 'L' 596 dateFormatField = Field.MONTH; 597 appendMonth(buffer, count, true); 598 break; 599 case MONTH_FIELD: // 'M' 600 dateFormatField = Field.MONTH; 601 appendMonth(buffer, count, false); 602 break; 603 case DATE_FIELD: 604 dateFormatField = Field.DAY_OF_MONTH; 605 field = Calendar.DATE; 606 break; 607 case HOUR_OF_DAY1_FIELD: // 'k' 608 dateFormatField = Field.HOUR_OF_DAY1; 609 int hour = calendar.get(Calendar.HOUR_OF_DAY); 610 appendNumber(buffer, count, hour == 0 ? 24 : hour); 611 break; 612 case HOUR_OF_DAY0_FIELD: // 'H' 613 dateFormatField = Field.HOUR_OF_DAY0; 614 field = Calendar.HOUR_OF_DAY; 615 break; 616 case MINUTE_FIELD: 617 dateFormatField = Field.MINUTE; 618 field = Calendar.MINUTE; 619 break; 620 case SECOND_FIELD: 621 dateFormatField = Field.SECOND; 622 field = Calendar.SECOND; 623 break; 624 case MILLISECOND_FIELD: 625 dateFormatField = Field.MILLISECOND; 626 int value = calendar.get(Calendar.MILLISECOND); 627 appendNumber(buffer, count, value); 628 break; 629 case STAND_ALONE_DAY_OF_WEEK_FIELD: 630 dateFormatField = Field.DAY_OF_WEEK; 631 appendDayOfWeek(buffer, count, true); 632 break; 633 case DAY_OF_WEEK_FIELD: 634 dateFormatField = Field.DAY_OF_WEEK; 635 appendDayOfWeek(buffer, count, false); 636 break; 637 case DAY_OF_YEAR_FIELD: 638 dateFormatField = Field.DAY_OF_YEAR; 639 field = Calendar.DAY_OF_YEAR; 640 break; 641 case DAY_OF_WEEK_IN_MONTH_FIELD: 642 dateFormatField = Field.DAY_OF_WEEK_IN_MONTH; 643 field = Calendar.DAY_OF_WEEK_IN_MONTH; 644 break; 645 case WEEK_OF_YEAR_FIELD: 646 dateFormatField = Field.WEEK_OF_YEAR; 647 field = Calendar.WEEK_OF_YEAR; 648 break; 649 case WEEK_OF_MONTH_FIELD: 650 dateFormatField = Field.WEEK_OF_MONTH; 651 field = Calendar.WEEK_OF_MONTH; 652 break; 653 case AM_PM_FIELD: 654 dateFormatField = Field.AM_PM; 655 buffer.append(formatData.ampms[calendar.get(Calendar.AM_PM)]); 656 break; 657 case HOUR1_FIELD: // 'h' 658 dateFormatField = Field.HOUR1; 659 hour = calendar.get(Calendar.HOUR); 660 appendNumber(buffer, count, hour == 0 ? 12 : hour); 661 break; 662 case HOUR0_FIELD: // 'K' 663 dateFormatField = Field.HOUR0; 664 field = Calendar.HOUR; 665 break; 666 case TIMEZONE_FIELD: // 'z' 667 dateFormatField = Field.TIME_ZONE; 668 appendTimeZone(buffer, count, true); 669 break; 670 case RFC_822_TIMEZONE_FIELD: // 'Z' 671 dateFormatField = Field.TIME_ZONE; 672 appendNumericTimeZone(buffer, count, false); 673 break; 674 } 675 if (field != -1) { 676 appendNumber(buffer, count, calendar.get(field)); 677 } 678 679 if (fields != null) { 680 position = new FieldPosition(dateFormatField); 681 position.setBeginIndex(beginPosition); 682 position.setEndIndex(buffer.length()); 683 fields.add(position); 684 } else { 685 // Set to the first occurrence 686 if ((position.getFieldAttribute() == dateFormatField || (position 687 .getFieldAttribute() == null && position.getField() == index)) 688 && position.getEndIndex() == 0) { 689 position.setBeginIndex(beginPosition); 690 position.setEndIndex(buffer.length()); 691 } 692 } 693 } 694 695 // See http://www.unicode.org/reports/tr35/#Date_Format_Patterns for the different counts. 696 private void appendDayOfWeek(StringBuffer buffer, int count, boolean standAlone) { 697 String[] days; 698 LocaleData ld = formatData.localeData; 699 if (count == 4) { 700 days = standAlone ? ld.longStandAloneWeekdayNames : formatData.weekdays; 701 } else if (count == 5) { 702 days = standAlone ? ld.tinyStandAloneWeekdayNames : formatData.localeData.tinyWeekdayNames; 703 } else { 704 days = standAlone ? ld.shortStandAloneWeekdayNames : formatData.shortWeekdays; 705 } 706 buffer.append(days[calendar.get(Calendar.DAY_OF_WEEK)]); 707 } 708 709 // See http://www.unicode.org/reports/tr35/#Date_Format_Patterns for the different counts. 710 private void appendMonth(StringBuffer buffer, int count, boolean standAlone) { 711 int month = calendar.get(Calendar.MONTH); 712 if (count <= 2) { 713 appendNumber(buffer, count, month + 1); 714 return; 715 } 716 717 String[] months; 718 LocaleData ld = formatData.localeData; 719 if (count == 4) { 720 months = standAlone ? ld.longStandAloneMonthNames : formatData.months; 721 } else if (count == 5) { 722 months = standAlone ? ld.tinyStandAloneMonthNames : ld.tinyMonthNames; 723 } else { 724 months = standAlone ? ld.shortStandAloneMonthNames : formatData.shortMonths; 725 } 726 buffer.append(months[month]); 727 } 728 729 /** 730 * Append a representation of the time zone of 'calendar' to 'buffer'. 731 * 732 * @param count the number of z or Z characters in the format string; "zzz" would be 3, 733 * for example. 734 * @param generalTimeZone true if we should use a display name ("PDT") if available; 735 * false implies that we should use RFC 822 format ("-0800") instead. This corresponds to 'z' 736 * versus 'Z' in the format string. 737 */ 738 private void appendTimeZone(StringBuffer buffer, int count, boolean generalTimeZone) { 739 if (generalTimeZone) { 740 TimeZone tz = calendar.getTimeZone(); 741 boolean daylight = (calendar.get(Calendar.DST_OFFSET) != 0); 742 int style = count < 4 ? TimeZone.SHORT : TimeZone.LONG; 743 if (!formatData.customZoneStrings) { 744 buffer.append(tz.getDisplayName(daylight, style, formatData.locale)); 745 return; 746 } 747 // We can't call TimeZone.getDisplayName() because it would not use 748 // the custom DateFormatSymbols of this SimpleDateFormat. 749 String custom = TimeZoneNames.getDisplayName(formatData.zoneStrings, tz.getID(), daylight, style); 750 if (custom != null) { 751 buffer.append(custom); 752 return; 753 } 754 } 755 // We didn't find what we were looking for, so default to a numeric time zone. 756 appendNumericTimeZone(buffer, count, generalTimeZone); 757 } 758 759 // See http://www.unicode.org/reports/tr35/#Date_Format_Patterns for the different counts. 760 // @param generalTimeZone "GMT-08:00" rather than "-0800". 761 private void appendNumericTimeZone(StringBuffer buffer, int count, boolean generalTimeZone) { 762 int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); 763 char sign = '+'; 764 if (offset < 0) { 765 sign = '-'; 766 offset = -offset; 767 } 768 if (generalTimeZone || count == 4) { 769 buffer.append("GMT"); 770 } 771 buffer.append(sign); 772 appendNumber(buffer, 2, offset / 3600000); 773 if (generalTimeZone || count >= 4) { 774 buffer.append(':'); 775 } 776 appendNumber(buffer, 2, (offset % 3600000) / 60000); 777 } 778 779 private void appendNumber(StringBuffer buffer, int count, int value) { 780 // TODO: we could avoid using the NumberFormat in most cases for a significant speedup. 781 // The only problem is that we expose the NumberFormat to third-party code, so we'd have 782 // some work to do to work out when the optimization is valid. 783 int minimumIntegerDigits = numberFormat.getMinimumIntegerDigits(); 784 numberFormat.setMinimumIntegerDigits(count); 785 numberFormat.format(Integer.valueOf(value), buffer, new FieldPosition(0)); 786 numberFormat.setMinimumIntegerDigits(minimumIntegerDigits); 787 } 788 789 private Date error(ParsePosition position, int offset, TimeZone zone) { 790 position.setErrorIndex(offset); 791 calendar.setTimeZone(zone); 792 return null; 793 } 794 795 /** 796 * Formats the specified date as a string using the pattern of this date 797 * format and appends the string to the specified string buffer. 798 * <p> 799 * If the {@code field} member of {@code field} contains a value specifying 800 * a format field, then its {@code beginIndex} and {@code endIndex} members 801 * will be updated with the position of the first occurrence of this field 802 * in the formatted text. 803 * 804 * @param date 805 * the date to format. 806 * @param buffer 807 * the target string buffer to append the formatted date/time to. 808 * @param fieldPos 809 * on input: an optional alignment field; on output: the offsets 810 * of the alignment field in the formatted text. 811 * @return the string buffer. 812 * @throws IllegalArgumentException 813 * if there are invalid characters in the pattern. 814 */ 815 @Override 816 public StringBuffer format(Date date, StringBuffer buffer, FieldPosition fieldPos) { 817 // Harmony delegates to ICU's SimpleDateFormat, we implement it directly 818 return formatImpl(date, buffer, fieldPos, null); 819 } 820 821 /** 822 * Returns the date which is the start of the one hundred year period for two-digit year values. 823 * See {@link #set2DigitYearStart} for details. 824 */ 825 public Date get2DigitYearStart() { 826 return (Date) defaultCenturyStart.clone(); 827 } 828 829 /** 830 * Returns the {@code DateFormatSymbols} used by this simple date format. 831 * 832 * @return the {@code DateFormatSymbols} object. 833 */ 834 public DateFormatSymbols getDateFormatSymbols() { 835 return (DateFormatSymbols) formatData.clone(); 836 } 837 838 @Override 839 public int hashCode() { 840 return super.hashCode() + pattern.hashCode() + formatData.hashCode() + creationYear; 841 } 842 843 private int parse(String string, int offset, char format, int count) { 844 int index = PATTERN_CHARS.indexOf(format); 845 if (index == -1) { 846 throw new IllegalArgumentException("Unknown pattern character '" + format + "'"); 847 } 848 int field = -1; 849 // TODO: what's 'absolute' for? when is 'count' negative, and why? 850 int absolute = 0; 851 if (count < 0) { 852 count = -count; 853 absolute = count; 854 } 855 switch (index) { 856 case ERA_FIELD: 857 return parseText(string, offset, formatData.eras, Calendar.ERA); 858 case YEAR_FIELD: 859 if (count >= 3) { 860 field = Calendar.YEAR; 861 } else { 862 ParsePosition position = new ParsePosition(offset); 863 Number result = parseNumber(absolute, string, position); 864 if (result == null) { 865 return -position.getErrorIndex() - 1; 866 } 867 int year = result.intValue(); 868 // A two digit year must be exactly two digits, i.e. 01 869 if ((position.getIndex() - offset) == 2 && year >= 0) { 870 year += creationYear / 100 * 100; 871 if (year < creationYear) { 872 year += 100; 873 } 874 } 875 calendar.set(Calendar.YEAR, year); 876 return position.getIndex(); 877 } 878 break; 879 case STAND_ALONE_MONTH_FIELD: // 'L' 880 return parseMonth(string, offset, count, absolute, true); 881 case MONTH_FIELD: // 'M' 882 return parseMonth(string, offset, count, absolute, false); 883 case DATE_FIELD: 884 field = Calendar.DATE; 885 break; 886 case HOUR_OF_DAY1_FIELD: // 'k' 887 ParsePosition position = new ParsePosition(offset); 888 Number result = parseNumber(absolute, string, position); 889 if (result == null) { 890 return -position.getErrorIndex() - 1; 891 } 892 int hour = result.intValue(); 893 if (hour == 24) { 894 hour = 0; 895 } 896 calendar.set(Calendar.HOUR_OF_DAY, hour); 897 return position.getIndex(); 898 case HOUR_OF_DAY0_FIELD: // 'H' 899 field = Calendar.HOUR_OF_DAY; 900 break; 901 case MINUTE_FIELD: 902 field = Calendar.MINUTE; 903 break; 904 case SECOND_FIELD: 905 field = Calendar.SECOND; 906 break; 907 case MILLISECOND_FIELD: 908 field = Calendar.MILLISECOND; 909 break; 910 case STAND_ALONE_DAY_OF_WEEK_FIELD: 911 return parseDayOfWeek(string, offset, true); 912 case DAY_OF_WEEK_FIELD: 913 return parseDayOfWeek(string, offset, false); 914 case DAY_OF_YEAR_FIELD: 915 field = Calendar.DAY_OF_YEAR; 916 break; 917 case DAY_OF_WEEK_IN_MONTH_FIELD: 918 field = Calendar.DAY_OF_WEEK_IN_MONTH; 919 break; 920 case WEEK_OF_YEAR_FIELD: 921 field = Calendar.WEEK_OF_YEAR; 922 break; 923 case WEEK_OF_MONTH_FIELD: 924 field = Calendar.WEEK_OF_MONTH; 925 break; 926 case AM_PM_FIELD: 927 return parseText(string, offset, formatData.ampms, Calendar.AM_PM); 928 case HOUR1_FIELD: // 'h' 929 position = new ParsePosition(offset); 930 result = parseNumber(absolute, string, position); 931 if (result == null) { 932 return -position.getErrorIndex() - 1; 933 } 934 hour = result.intValue(); 935 if (hour == 12) { 936 hour = 0; 937 } 938 calendar.set(Calendar.HOUR, hour); 939 return position.getIndex(); 940 case HOUR0_FIELD: // 'K' 941 field = Calendar.HOUR; 942 break; 943 case TIMEZONE_FIELD: // 'z' 944 return parseTimeZone(string, offset); 945 case RFC_822_TIMEZONE_FIELD: // 'Z' 946 return parseTimeZone(string, offset); 947 } 948 if (field != -1) { 949 return parseNumber(absolute, string, offset, field, 0); 950 } 951 return offset; 952 } 953 954 private int parseDayOfWeek(String string, int offset, boolean standAlone) { 955 LocaleData ld = formatData.localeData; 956 int index = parseText(string, offset, 957 standAlone ? ld.longStandAloneWeekdayNames : formatData.weekdays, 958 Calendar.DAY_OF_WEEK); 959 if (index < 0) { 960 index = parseText(string, offset, 961 standAlone ? ld.shortStandAloneWeekdayNames : formatData.shortWeekdays, 962 Calendar.DAY_OF_WEEK); 963 } 964 return index; 965 } 966 967 private int parseMonth(String string, int offset, int count, int absolute, boolean standAlone) { 968 if (count <= 2) { 969 return parseNumber(absolute, string, offset, Calendar.MONTH, -1); 970 } 971 LocaleData ld = formatData.localeData; 972 int index = parseText(string, offset, 973 standAlone ? ld.longStandAloneMonthNames : formatData.months, 974 Calendar.MONTH); 975 if (index < 0) { 976 index = parseText(string, offset, 977 standAlone ? ld.shortStandAloneMonthNames : formatData.shortMonths, 978 Calendar.MONTH); 979 } 980 return index; 981 } 982 983 /** 984 * Parses a date from the specified string starting at the index specified 985 * by {@code position}. If the string is successfully parsed then the index 986 * of the {@code ParsePosition} is updated to the index following the parsed 987 * text. On error, the index is unchanged and the error index of {@code 988 * ParsePosition} is set to the index where the error occurred. 989 * 990 * @param string 991 * the string to parse using the pattern of this simple date 992 * format. 993 * @param position 994 * input/output parameter, specifies the start index in {@code 995 * string} from where to start parsing. If parsing is successful, 996 * it is updated with the index following the parsed text; on 997 * error, the index is unchanged and the error index is set to 998 * the index where the error occurred. 999 * @return the date resulting from the parse, or {@code null} if there is an 1000 * error. 1001 * @throws IllegalArgumentException 1002 * if there are invalid characters in the pattern. 1003 */ 1004 @Override 1005 public Date parse(String string, ParsePosition position) { 1006 // Harmony delegates to ICU's SimpleDateFormat, we implement it directly 1007 boolean quote = false; 1008 int next, last = -1, count = 0, offset = position.getIndex(); 1009 int length = string.length(); 1010 calendar.clear(); 1011 TimeZone zone = calendar.getTimeZone(); 1012 final int patternLength = pattern.length(); 1013 for (int i = 0; i < patternLength; i++) { 1014 next = pattern.charAt(i); 1015 if (next == '\'') { 1016 if (count > 0) { 1017 if ((offset = parse(string, offset, (char) last, count)) < 0) { 1018 return error(position, -offset - 1, zone); 1019 } 1020 count = 0; 1021 } 1022 if (last == next) { 1023 if (offset >= length || string.charAt(offset) != '\'') { 1024 return error(position, offset, zone); 1025 } 1026 offset++; 1027 last = -1; 1028 } else { 1029 last = next; 1030 } 1031 quote = !quote; 1032 continue; 1033 } 1034 if (!quote 1035 && (last == next || (next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z'))) { 1036 if (last == next) { 1037 count++; 1038 } else { 1039 if (count > 0) { 1040 if ((offset = parse(string, offset, (char) last, -count)) < 0) { 1041 return error(position, -offset - 1, zone); 1042 } 1043 } 1044 last = next; 1045 count = 1; 1046 } 1047 } else { 1048 if (count > 0) { 1049 if ((offset = parse(string, offset, (char) last, count)) < 0) { 1050 return error(position, -offset - 1, zone); 1051 } 1052 count = 0; 1053 } 1054 last = -1; 1055 if (offset >= length || string.charAt(offset) != next) { 1056 return error(position, offset, zone); 1057 } 1058 offset++; 1059 } 1060 } 1061 if (count > 0) { 1062 if ((offset = parse(string, offset, (char) last, count)) < 0) { 1063 return error(position, -offset - 1, zone); 1064 } 1065 } 1066 Date date; 1067 try { 1068 date = calendar.getTime(); 1069 } catch (IllegalArgumentException e) { 1070 return error(position, offset, zone); 1071 } 1072 position.setIndex(offset); 1073 calendar.setTimeZone(zone); 1074 return date; 1075 } 1076 1077 private Number parseNumber(int max, String string, ParsePosition position) { 1078 int length = string.length(); 1079 int index = position.getIndex(); 1080 if (max > 0 && max < length - index) { 1081 length = index + max; 1082 } 1083 while (index < length && (string.charAt(index) == ' ' || string.charAt(index) == '\t')) { 1084 ++index; 1085 } 1086 if (max == 0) { 1087 position.setIndex(index); 1088 Number n = numberFormat.parse(string, position); 1089 // In RTL locales, NumberFormat might have parsed "2012-" in an ISO date as the 1090 // negative number -2012. 1091 // Ideally, we wouldn't have this broken API that exposes a NumberFormat and expects 1092 // us to use it. The next best thing would be a way to ask the NumberFormat to parse 1093 // positive numbers only, but icu4c supports negative (BCE) years. The best we can do 1094 // is try to recognize when icu4c has done this, and undo it. 1095 if (n != null && n.longValue() < 0) { 1096 if (numberFormat instanceof DecimalFormat) { 1097 DecimalFormat df = (DecimalFormat) numberFormat; 1098 char lastChar = string.charAt(position.getIndex() - 1); 1099 char minusSign = df.getDecimalFormatSymbols().getMinusSign(); 1100 if (lastChar == minusSign) { 1101 n = Long.valueOf(-n.longValue()); // Make the value positive. 1102 position.setIndex(position.getIndex() - 1); // Spit out the negative sign. 1103 } 1104 } 1105 } 1106 return n; 1107 } 1108 1109 int result = 0; 1110 int digit; 1111 while (index < length && (digit = Character.digit(string.charAt(index), 10)) != -1) { 1112 result = result * 10 + digit; 1113 ++index; 1114 } 1115 if (index == position.getIndex()) { 1116 position.setErrorIndex(index); 1117 return null; 1118 } 1119 position.setIndex(index); 1120 return Integer.valueOf(result); 1121 } 1122 1123 private int parseNumber(int max, String string, int offset, int field, int skew) { 1124 ParsePosition position = new ParsePosition(offset); 1125 Number result = parseNumber(max, string, position); 1126 if (result == null) { 1127 return -position.getErrorIndex() - 1; 1128 } 1129 calendar.set(field, result.intValue() + skew); 1130 return position.getIndex(); 1131 } 1132 1133 private int parseText(String string, int offset, String[] options, int field) { 1134 // We search for the longest match, in case some entries are substrings of others. 1135 int bestIndex = -1; 1136 int bestLength = -1; 1137 for (int i = 0; i < options.length; ++i) { 1138 String option = options[i]; 1139 int optionLength = option.length(); 1140 if (optionLength == 0) { 1141 continue; 1142 } 1143 if (string.regionMatches(true, offset, option, 0, optionLength)) { 1144 if (bestIndex == -1 || optionLength > bestLength) { 1145 bestIndex = i; 1146 bestLength = optionLength; 1147 } 1148 } else if (option.charAt(optionLength - 1) == '.') { 1149 // If CLDR has abbreviated forms like "Aug.", we should accept "Aug" too. 1150 // https://code.google.com/p/android/issues/detail?id=59383 1151 if (string.regionMatches(true, offset, option, 0, optionLength - 1)) { 1152 if (bestIndex == -1 || optionLength - 1 > bestLength) { 1153 bestIndex = i; 1154 bestLength = optionLength - 1; 1155 } 1156 } 1157 } 1158 } 1159 if (bestIndex != -1) { 1160 calendar.set(field, bestIndex); 1161 return offset + bestLength; 1162 } 1163 return -offset - 1; 1164 } 1165 1166 private int parseTimeZone(String string, int offset) { 1167 boolean foundGMT = string.regionMatches(offset, "GMT", 0, 3); 1168 if (foundGMT) { 1169 offset += 3; 1170 } 1171 char sign; 1172 if (offset < string.length() && ((sign = string.charAt(offset)) == '+' || sign == '-')) { 1173 ParsePosition position = new ParsePosition(offset + 1); 1174 Number result = numberFormat.parse(string, position); 1175 if (result == null) { 1176 return -position.getErrorIndex() - 1; 1177 } 1178 int hour = result.intValue(); 1179 int raw = hour * 3600000; 1180 int index = position.getIndex(); 1181 if (index < string.length() && string.charAt(index) == ':') { 1182 position.setIndex(index + 1); 1183 result = numberFormat.parse(string, position); 1184 if (result == null) { 1185 return -position.getErrorIndex() - 1; 1186 } 1187 int minute = result.intValue(); 1188 raw += minute * 60000; 1189 } else if (hour >= 24) { 1190 raw = (hour / 100 * 3600000) + (hour % 100 * 60000); 1191 } 1192 if (sign == '-') { 1193 raw = -raw; 1194 } 1195 calendar.setTimeZone(new SimpleTimeZone(raw, "")); 1196 return position.getIndex(); 1197 } 1198 if (foundGMT) { 1199 calendar.setTimeZone(TimeZone.getTimeZone("GMT")); 1200 return offset; 1201 } 1202 for (String[] row : formatData.internalZoneStrings()) { 1203 for (int i = TimeZoneNames.LONG_NAME; i < TimeZoneNames.NAME_COUNT; ++i) { 1204 if (row[i] == null) { 1205 // If icu4c doesn't have a name, our array contains a null. Normally we'd 1206 // work out the correct GMT offset, but we already handled parsing GMT offsets 1207 // above, so we can just ignore these cases. http://b/8128460. 1208 continue; 1209 } 1210 if (string.regionMatches(true, offset, row[i], 0, row[i].length())) { 1211 TimeZone zone = TimeZone.getTimeZone(row[TimeZoneNames.OLSON_NAME]); 1212 if (zone == null) { 1213 return -offset - 1; 1214 } 1215 int raw = zone.getRawOffset(); 1216 if (i == TimeZoneNames.LONG_NAME_DST || i == TimeZoneNames.SHORT_NAME_DST) { 1217 // Not all time zones use a one-hour difference, so we need to query 1218 // the TimeZone. (Australia/Lord_Howe is the usual example of this.) 1219 int dstSavings = zone.getDSTSavings(); 1220 // One problem with TimeZone.getDSTSavings is that it will return 0 if the 1221 // time zone has stopped using DST, even if we're parsing a date from 1222 // the past. In that case, assume the default. 1223 if (dstSavings == 0) { 1224 // TODO: we should change this to use TimeZone.getOffset(long), 1225 // but that requires the complete date to be parsed first. 1226 dstSavings = 3600000; 1227 } 1228 raw += dstSavings; 1229 } 1230 calendar.setTimeZone(new SimpleTimeZone(raw, "")); 1231 return offset + row[i].length(); 1232 } 1233 } 1234 } 1235 return -offset - 1; 1236 } 1237 1238 /** 1239 * Sets the date which is the start of the one hundred year period for two-digit year values. 1240 * 1241 * <p>When parsing a date string using the abbreviated year pattern {@code yy}, {@code 1242 * SimpleDateFormat} must interpret the abbreviated year relative to some 1243 * century. It does this by adjusting dates to be within 80 years before and 20 1244 * years after the time the {@code SimpleDateFormat} instance was created. For 1245 * example, using a pattern of {@code MM/dd/yy}, an 1246 * instance created on Jan 1, 1997 would interpret the string {@code "01/11/12"} 1247 * as Jan 11, 2012 but interpret the string {@code "05/04/64"} as May 4, 1964. 1248 * During parsing, only strings consisting of exactly two digits, as 1249 * defined by {@link java.lang.Character#isDigit(char)}, will be parsed into the 1250 * default century. Any other numeric string, such as a one digit string, a 1251 * three or more digit string, or a two digit string that isn't all digits (for 1252 * example, {@code "-1"}), is interpreted literally. So using the same pattern, both 1253 * {@code "01/02/3"} and {@code "01/02/003"} are parsed as Jan 2, 3 AD. 1254 * Similarly, {@code "01/02/-3"} is parsed as Jan 2, 4 BC. 1255 * 1256 * <p>If the year pattern does not have exactly two 'y' characters, the year is 1257 * interpreted literally, regardless of the number of digits. So using the 1258 * pattern {@code MM/dd/yyyy}, {@code "01/11/12"} is parsed as Jan 11, 12 A.D. 1259 */ 1260 public void set2DigitYearStart(Date date) { 1261 defaultCenturyStart = (Date) date.clone(); 1262 Calendar cal = new GregorianCalendar(); 1263 cal.setTime(defaultCenturyStart); 1264 creationYear = cal.get(Calendar.YEAR); 1265 } 1266 1267 /** 1268 * Sets the {@code DateFormatSymbols} used by this simple date format. 1269 * 1270 * @param value 1271 * the new {@code DateFormatSymbols} object. 1272 */ 1273 public void setDateFormatSymbols(DateFormatSymbols value) { 1274 formatData = (DateFormatSymbols) value.clone(); 1275 } 1276 1277 /** 1278 * Returns the pattern of this simple date format using localized pattern 1279 * characters. 1280 * 1281 * @return the localized pattern. 1282 */ 1283 public String toLocalizedPattern() { 1284 return convertPattern(pattern, PATTERN_CHARS, formatData.getLocalPatternChars(), false); 1285 } 1286 1287 private static String convertPattern(String template, String fromChars, String toChars, boolean check) { 1288 if (!check && fromChars.equals(toChars)) { 1289 return template; 1290 } 1291 boolean quote = false; 1292 StringBuilder output = new StringBuilder(); 1293 int length = template.length(); 1294 for (int i = 0; i < length; i++) { 1295 int index; 1296 char next = template.charAt(i); 1297 if (next == '\'') { 1298 quote = !quote; 1299 } 1300 if (!quote && (index = fromChars.indexOf(next)) != -1) { 1301 output.append(toChars.charAt(index)); 1302 } else if (check && !quote && ((next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z'))) { 1303 throw new IllegalArgumentException("Invalid pattern character '" + next + "' in " + "'" + template + "'"); 1304 } else { 1305 output.append(next); 1306 } 1307 } 1308 if (quote) { 1309 throw new IllegalArgumentException("Unterminated quote"); 1310 } 1311 return output.toString(); 1312 } 1313 1314 /** 1315 * Returns the pattern of this simple date format using non-localized 1316 * pattern characters. 1317 * 1318 * @return the non-localized pattern. 1319 */ 1320 public String toPattern() { 1321 return pattern; 1322 } 1323 1324 private static final ObjectStreamField[] serialPersistentFields = { 1325 new ObjectStreamField("defaultCenturyStart", Date.class), 1326 new ObjectStreamField("formatData", DateFormatSymbols.class), 1327 new ObjectStreamField("pattern", String.class), 1328 new ObjectStreamField("serialVersionOnStream", int.class), 1329 }; 1330 1331 private void writeObject(ObjectOutputStream stream) throws IOException { 1332 ObjectOutputStream.PutField fields = stream.putFields(); 1333 fields.put("defaultCenturyStart", defaultCenturyStart); 1334 fields.put("formatData", formatData); 1335 fields.put("pattern", pattern); 1336 fields.put("serialVersionOnStream", 1); 1337 stream.writeFields(); 1338 } 1339 1340 private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { 1341 ObjectInputStream.GetField fields = stream.readFields(); 1342 int version = fields.get("serialVersionOnStream", 0); 1343 Date date; 1344 if (version > 0) { 1345 date = (Date) fields.get("defaultCenturyStart", new Date()); 1346 } else { 1347 date = new Date(); 1348 } 1349 set2DigitYearStart(date); 1350 formatData = (DateFormatSymbols) fields.get("formatData", null); 1351 pattern = (String) fields.get("pattern", ""); 1352 } 1353 } 1354