001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.lang3.time; 018 019import java.io.IOException; 020import java.io.ObjectInputStream; 021import java.io.Serializable; 022import java.text.DateFormat; 023import java.text.DateFormatSymbols; 024import java.text.FieldPosition; 025import java.util.ArrayList; 026import java.util.Calendar; 027import java.util.Date; 028import java.util.List; 029import java.util.Locale; 030import java.util.TimeZone; 031import java.util.concurrent.ConcurrentHashMap; 032import java.util.concurrent.ConcurrentMap; 033 034import org.apache.commons.lang3.exception.ExceptionUtils; 035 036/** 037 * <p>FastDatePrinter is a fast and thread-safe version of 038 * {@link java.text.SimpleDateFormat}.</p> 039 * 040 * <p>To obtain a FastDatePrinter, use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} 041 * or another variation of the factory methods of {@link FastDateFormat}.</p> 042 * 043 * <p>Since FastDatePrinter is thread safe, you can use a static member instance:</p> 044 * <code> 045 * private static final DatePrinter DATE_PRINTER = FastDateFormat.getInstance("yyyy-MM-dd"); 046 * </code> 047 * 048 * <p>This class can be used as a direct replacement to 049 * {@code SimpleDateFormat} in most formatting situations. 050 * This class is especially useful in multi-threaded server environments. 051 * {@code SimpleDateFormat} is not thread-safe in any JDK version, 052 * nor will it be as Sun have closed the bug/RFE. 053 * </p> 054 * 055 * <p>Only formatting is supported by this class, but all patterns are compatible with 056 * SimpleDateFormat (except time zones and some year patterns - see below).</p> 057 * 058 * <p>Java 1.4 introduced a new pattern letter, {@code 'Z'}, to represent 059 * time zones in RFC822 format (eg. {@code +0800} or {@code -1100}). 060 * This pattern letter can be used here (on all JDK versions).</p> 061 * 062 * <p>In addition, the pattern {@code 'ZZ'} has been made to represent 063 * ISO 8601 extended format time zones (eg. {@code +08:00} or {@code -11:00}). 064 * This introduces a minor incompatibility with Java 1.4, but at a gain of 065 * useful functionality.</p> 066 * 067 * <p>Starting with JDK7, ISO 8601 support was added using the pattern {@code 'X'}. 068 * To maintain compatibility, {@code 'ZZ'} will continue to be supported, but using 069 * one of the {@code 'X'} formats is recommended. 070 * 071 * <p>Javadoc cites for the year pattern: <i>For formatting, if the number of 072 * pattern letters is 2, the year is truncated to 2 digits; otherwise it is 073 * interpreted as a number.</i> Starting with Java 1.7 a pattern of 'Y' or 074 * 'YYY' will be formatted as '2003', while it was '03' in former Java 075 * versions. FastDatePrinter implements the behavior of Java 7.</p> 076 * 077 * @since 3.2 078 * @see FastDateParser 079 */ 080public class FastDatePrinter implements DatePrinter, Serializable { 081 // A lot of the speed in this class comes from caching, but some comes 082 // from the special int to StringBuffer conversion. 083 // 084 // The following produces a padded 2 digit number: 085 // buffer.append((char)(value / 10 + '0')); 086 // buffer.append((char)(value % 10 + '0')); 087 // 088 // Note that the fastest append to StringBuffer is a single char (used here). 089 // Note that Integer.toString() is not called, the conversion is simply 090 // taking the value and adding (mathematically) the ASCII value for '0'. 091 // So, don't change this code! It works and is very fast. 092 093 /** 094 * Required for serialization support. 095 * 096 * @see java.io.Serializable 097 */ 098 private static final long serialVersionUID = 1L; 099 100 /** 101 * FULL locale dependent date or time style. 102 */ 103 public static final int FULL = DateFormat.FULL; 104 /** 105 * LONG locale dependent date or time style. 106 */ 107 public static final int LONG = DateFormat.LONG; 108 /** 109 * MEDIUM locale dependent date or time style. 110 */ 111 public static final int MEDIUM = DateFormat.MEDIUM; 112 /** 113 * SHORT locale dependent date or time style. 114 */ 115 public static final int SHORT = DateFormat.SHORT; 116 117 /** 118 * The pattern. 119 */ 120 private final String mPattern; 121 /** 122 * The time zone. 123 */ 124 private final TimeZone mTimeZone; 125 /** 126 * The locale. 127 */ 128 private final Locale mLocale; 129 /** 130 * The parsed rules. 131 */ 132 private transient Rule[] mRules; 133 /** 134 * The estimated maximum length. 135 */ 136 private transient int mMaxLengthEstimate; 137 138 // Constructor 139 //----------------------------------------------------------------------- 140 /** 141 * <p>Constructs a new FastDatePrinter.</p> 142 * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the 143 * factory methods of {@link FastDateFormat} to get a cached FastDatePrinter instance. 144 * 145 * @param pattern {@link java.text.SimpleDateFormat} compatible pattern 146 * @param timeZone non-null time zone to use 147 * @param locale non-null locale to use 148 * @throws NullPointerException if pattern, timeZone, or locale is null. 149 */ 150 protected FastDatePrinter(final String pattern, final TimeZone timeZone, final Locale locale) { 151 mPattern = pattern; 152 mTimeZone = timeZone; 153 mLocale = locale; 154 155 init(); 156 } 157 158 /** 159 * <p>Initializes the instance for first use.</p> 160 */ 161 private void init() { 162 final List<Rule> rulesList = parsePattern(); 163 mRules = rulesList.toArray(new Rule[0]); 164 165 int len = 0; 166 for (int i=mRules.length; --i >= 0; ) { 167 len += mRules[i].estimateLength(); 168 } 169 170 mMaxLengthEstimate = len; 171 } 172 173 // Parse the pattern 174 //----------------------------------------------------------------------- 175 /** 176 * <p>Returns a list of Rules given a pattern.</p> 177 * 178 * @return a {@code List} of Rule objects 179 * @throws IllegalArgumentException if pattern is invalid 180 */ 181 protected List<Rule> parsePattern() { 182 final DateFormatSymbols symbols = new DateFormatSymbols(mLocale); 183 final List<Rule> rules = new ArrayList<>(); 184 185 final String[] ERAs = symbols.getEras(); 186 final String[] months = symbols.getMonths(); 187 final String[] shortMonths = symbols.getShortMonths(); 188 final String[] weekdays = symbols.getWeekdays(); 189 final String[] shortWeekdays = symbols.getShortWeekdays(); 190 final String[] AmPmStrings = symbols.getAmPmStrings(); 191 192 final int length = mPattern.length(); 193 final int[] indexRef = new int[1]; 194 195 for (int i = 0; i < length; i++) { 196 indexRef[0] = i; 197 final String token = parseToken(mPattern, indexRef); 198 i = indexRef[0]; 199 200 final int tokenLen = token.length(); 201 if (tokenLen == 0) { 202 break; 203 } 204 205 Rule rule; 206 final char c = token.charAt(0); 207 208 switch (c) { 209 case 'G': // era designator (text) 210 rule = new TextField(Calendar.ERA, ERAs); 211 break; 212 case 'y': // year (number) 213 case 'Y': // week year 214 if (tokenLen == 2) { 215 rule = TwoDigitYearField.INSTANCE; 216 } else { 217 rule = selectNumberRule(Calendar.YEAR, Math.max(tokenLen, 4)); 218 } 219 if (c == 'Y') { 220 rule = new WeekYear((NumberRule) rule); 221 } 222 break; 223 case 'M': // month in year (text and number) 224 if (tokenLen >= 4) { 225 rule = new TextField(Calendar.MONTH, months); 226 } else if (tokenLen == 3) { 227 rule = new TextField(Calendar.MONTH, shortMonths); 228 } else if (tokenLen == 2) { 229 rule = TwoDigitMonthField.INSTANCE; 230 } else { 231 rule = UnpaddedMonthField.INSTANCE; 232 } 233 break; 234 case 'd': // day in month (number) 235 rule = selectNumberRule(Calendar.DAY_OF_MONTH, tokenLen); 236 break; 237 case 'h': // hour in am/pm (number, 1..12) 238 rule = new TwelveHourField(selectNumberRule(Calendar.HOUR, tokenLen)); 239 break; 240 case 'H': // hour in day (number, 0..23) 241 rule = selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen); 242 break; 243 case 'm': // minute in hour (number) 244 rule = selectNumberRule(Calendar.MINUTE, tokenLen); 245 break; 246 case 's': // second in minute (number) 247 rule = selectNumberRule(Calendar.SECOND, tokenLen); 248 break; 249 case 'S': // millisecond (number) 250 rule = selectNumberRule(Calendar.MILLISECOND, tokenLen); 251 break; 252 case 'E': // day in week (text) 253 rule = new TextField(Calendar.DAY_OF_WEEK, tokenLen < 4 ? shortWeekdays : weekdays); 254 break; 255 case 'u': // day in week (number) 256 rule = new DayInWeekField(selectNumberRule(Calendar.DAY_OF_WEEK, tokenLen)); 257 break; 258 case 'D': // day in year (number) 259 rule = selectNumberRule(Calendar.DAY_OF_YEAR, tokenLen); 260 break; 261 case 'F': // day of week in month (number) 262 rule = selectNumberRule(Calendar.DAY_OF_WEEK_IN_MONTH, tokenLen); 263 break; 264 case 'w': // week in year (number) 265 rule = selectNumberRule(Calendar.WEEK_OF_YEAR, tokenLen); 266 break; 267 case 'W': // week in month (number) 268 rule = selectNumberRule(Calendar.WEEK_OF_MONTH, tokenLen); 269 break; 270 case 'a': // am/pm marker (text) 271 rule = new TextField(Calendar.AM_PM, AmPmStrings); 272 break; 273 case 'k': // hour in day (1..24) 274 rule = new TwentyFourHourField(selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen)); 275 break; 276 case 'K': // hour in am/pm (0..11) 277 rule = selectNumberRule(Calendar.HOUR, tokenLen); 278 break; 279 case 'X': // ISO 8601 280 rule = Iso8601_Rule.getRule(tokenLen); 281 break; 282 case 'z': // time zone (text) 283 if (tokenLen >= 4) { 284 rule = new TimeZoneNameRule(mTimeZone, mLocale, TimeZone.LONG); 285 } else { 286 rule = new TimeZoneNameRule(mTimeZone, mLocale, TimeZone.SHORT); 287 } 288 break; 289 case 'Z': // time zone (value) 290 if (tokenLen == 1) { 291 rule = TimeZoneNumberRule.INSTANCE_NO_COLON; 292 } else if (tokenLen == 2) { 293 rule = Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES; 294 } else { 295 rule = TimeZoneNumberRule.INSTANCE_COLON; 296 } 297 break; 298 case '\'': // literal text 299 final String sub = token.substring(1); 300 if (sub.length() == 1) { 301 rule = new CharacterLiteral(sub.charAt(0)); 302 } else { 303 rule = new StringLiteral(sub); 304 } 305 break; 306 default: 307 throw new IllegalArgumentException("Illegal pattern component: " + token); 308 } 309 310 rules.add(rule); 311 } 312 313 return rules; 314 } 315 316 /** 317 * <p>Performs the parsing of tokens.</p> 318 * 319 * @param pattern the pattern 320 * @param indexRef index references 321 * @return parsed token 322 */ 323 protected String parseToken(final String pattern, final int[] indexRef) { 324 final StringBuilder buf = new StringBuilder(); 325 326 int i = indexRef[0]; 327 final int length = pattern.length(); 328 329 char c = pattern.charAt(i); 330 if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') { 331 // Scan a run of the same character, which indicates a time 332 // pattern. 333 buf.append(c); 334 335 while (i + 1 < length) { 336 final char peek = pattern.charAt(i + 1); 337 if (peek == c) { 338 buf.append(c); 339 i++; 340 } else { 341 break; 342 } 343 } 344 } else { 345 // This will identify token as text. 346 buf.append('\''); 347 348 boolean inLiteral = false; 349 350 for (; i < length; i++) { 351 c = pattern.charAt(i); 352 353 if (c == '\'') { 354 if (i + 1 < length && pattern.charAt(i + 1) == '\'') { 355 // '' is treated as escaped ' 356 i++; 357 buf.append(c); 358 } else { 359 inLiteral = !inLiteral; 360 } 361 } else if (!inLiteral && 362 (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) { 363 i--; 364 break; 365 } else { 366 buf.append(c); 367 } 368 } 369 } 370 371 indexRef[0] = i; 372 return buf.toString(); 373 } 374 375 /** 376 * <p>Gets an appropriate rule for the padding required.</p> 377 * 378 * @param field the field to get a rule for 379 * @param padding the padding required 380 * @return a new rule with the correct padding 381 */ 382 protected NumberRule selectNumberRule(final int field, final int padding) { 383 switch (padding) { 384 case 1: 385 return new UnpaddedNumberField(field); 386 case 2: 387 return new TwoDigitNumberField(field); 388 default: 389 return new PaddedNumberField(field, padding); 390 } 391 } 392 393 // Format methods 394 //----------------------------------------------------------------------- 395 /** 396 * <p>Formats a {@code Date}, {@code Calendar} or 397 * {@code Long} (milliseconds) object.</p> 398 * @deprecated Use {{@link #format(Date)}, {{@link #format(Calendar)}, {{@link #format(long)}, or {{@link #format(Object)} 399 * @param obj the object to format 400 * @param toAppendTo the buffer to append to 401 * @param pos the position - ignored 402 * @return the buffer passed in 403 */ 404 @Deprecated 405 @Override 406 public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) { 407 if (obj instanceof Date) { 408 return format((Date) obj, toAppendTo); 409 } else if (obj instanceof Calendar) { 410 return format((Calendar) obj, toAppendTo); 411 } else if (obj instanceof Long) { 412 return format(((Long) obj).longValue(), toAppendTo); 413 } else { 414 throw new IllegalArgumentException("Unknown class: " + 415 (obj == null ? "<null>" : obj.getClass().getName())); 416 } 417 } 418 419 /** 420 * <p>Formats a {@code Date}, {@code Calendar} or 421 * {@code Long} (milliseconds) object.</p> 422 * @since 3.5 423 * @param obj the object to format 424 * @return The formatted value. 425 */ 426 String format(final Object obj) { 427 if (obj instanceof Date) { 428 return format((Date) obj); 429 } else if (obj instanceof Calendar) { 430 return format((Calendar) obj); 431 } else if (obj instanceof Long) { 432 return format(((Long) obj).longValue()); 433 } else { 434 throw new IllegalArgumentException("Unknown class: " + 435 (obj == null ? "<null>" : obj.getClass().getName())); 436 } 437 } 438 439 /* (non-Javadoc) 440 * @see org.apache.commons.lang3.time.DatePrinter#format(long) 441 */ 442 @Override 443 public String format(final long millis) { 444 final Calendar c = newCalendar(); 445 c.setTimeInMillis(millis); 446 return applyRulesToString(c); 447 } 448 449 /** 450 * Creates a String representation of the given Calendar by applying the rules of this printer to it. 451 * @param c the Calender to apply the rules to. 452 * @return a String representation of the given Calendar. 453 */ 454 private String applyRulesToString(final Calendar c) { 455 return applyRules(c, new StringBuilder(mMaxLengthEstimate)).toString(); 456 } 457 458 /** 459 * Creation method for new calender instances. 460 * @return a new Calendar instance. 461 */ 462 private Calendar newCalendar() { 463 return Calendar.getInstance(mTimeZone, mLocale); 464 } 465 466 /* (non-Javadoc) 467 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date) 468 */ 469 @Override 470 public String format(final Date date) { 471 final Calendar c = newCalendar(); 472 c.setTime(date); 473 return applyRulesToString(c); 474 } 475 476 /* (non-Javadoc) 477 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar) 478 */ 479 @Override 480 public String format(final Calendar calendar) { 481 return format(calendar, new StringBuilder(mMaxLengthEstimate)).toString(); 482 } 483 484 /* (non-Javadoc) 485 * @see org.apache.commons.lang3.time.DatePrinter#format(long, java.lang.StringBuffer) 486 */ 487 @Override 488 public StringBuffer format(final long millis, final StringBuffer buf) { 489 final Calendar c = newCalendar(); 490 c.setTimeInMillis(millis); 491 return (StringBuffer) applyRules(c, (Appendable) buf); 492 } 493 494 /* (non-Javadoc) 495 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date, java.lang.StringBuffer) 496 */ 497 @Override 498 public StringBuffer format(final Date date, final StringBuffer buf) { 499 final Calendar c = newCalendar(); 500 c.setTime(date); 501 return (StringBuffer) applyRules(c, (Appendable) buf); 502 } 503 504 /* (non-Javadoc) 505 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar, java.lang.StringBuffer) 506 */ 507 @Override 508 public StringBuffer format(final Calendar calendar, final StringBuffer buf) { 509 // do not pass in calendar directly, this will cause TimeZone of FastDatePrinter to be ignored 510 return format(calendar.getTime(), buf); 511 } 512 513 /* (non-Javadoc) 514 * @see org.apache.commons.lang3.time.DatePrinter#format(long, java.lang.Appendable) 515 */ 516 @Override 517 public <B extends Appendable> B format(final long millis, final B buf) { 518 final Calendar c = newCalendar(); 519 c.setTimeInMillis(millis); 520 return applyRules(c, buf); 521 } 522 523 /* (non-Javadoc) 524 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date, java.lang.Appendable) 525 */ 526 @Override 527 public <B extends Appendable> B format(final Date date, final B buf) { 528 final Calendar c = newCalendar(); 529 c.setTime(date); 530 return applyRules(c, buf); 531 } 532 533 /* (non-Javadoc) 534 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar, java.lang.Appendable) 535 */ 536 @Override 537 public <B extends Appendable> B format(Calendar calendar, final B buf) { 538 // do not pass in calendar directly, this will cause TimeZone of FastDatePrinter to be ignored 539 if (!calendar.getTimeZone().equals(mTimeZone)) { 540 calendar = (Calendar) calendar.clone(); 541 calendar.setTimeZone(mTimeZone); 542 } 543 return applyRules(calendar, buf); 544 } 545 546 /** 547 * Performs the formatting by applying the rules to the 548 * specified calendar. 549 * 550 * @param calendar the calendar to format 551 * @param buf the buffer to format into 552 * @return the specified string buffer 553 * 554 * @deprecated use {@link #format(Calendar)} or {@link #format(Calendar, Appendable)} 555 */ 556 @Deprecated 557 protected StringBuffer applyRules(final Calendar calendar, final StringBuffer buf) { 558 return (StringBuffer) applyRules(calendar, (Appendable) buf); 559 } 560 561 /** 562 * <p>Performs the formatting by applying the rules to the 563 * specified calendar.</p> 564 * 565 * @param calendar the calendar to format 566 * @param buf the buffer to format into 567 * @param <B> the Appendable class type, usually StringBuilder or StringBuffer. 568 * @return the specified string buffer 569 */ 570 private <B extends Appendable> B applyRules(final Calendar calendar, final B buf) { 571 try { 572 for (final Rule rule : mRules) { 573 rule.appendTo(buf, calendar); 574 } 575 } catch (final IOException ioe) { 576 ExceptionUtils.rethrow(ioe); 577 } 578 return buf; 579 } 580 581 // Accessors 582 //----------------------------------------------------------------------- 583 /* (non-Javadoc) 584 * @see org.apache.commons.lang3.time.DatePrinter#getPattern() 585 */ 586 @Override 587 public String getPattern() { 588 return mPattern; 589 } 590 591 /* (non-Javadoc) 592 * @see org.apache.commons.lang3.time.DatePrinter#getTimeZone() 593 */ 594 @Override 595 public TimeZone getTimeZone() { 596 return mTimeZone; 597 } 598 599 /* (non-Javadoc) 600 * @see org.apache.commons.lang3.time.DatePrinter#getLocale() 601 */ 602 @Override 603 public Locale getLocale() { 604 return mLocale; 605 } 606 607 /** 608 * <p>Gets an estimate for the maximum string length that the 609 * formatter will produce.</p> 610 * 611 * <p>The actual formatted length will almost always be less than or 612 * equal to this amount.</p> 613 * 614 * @return the maximum formatted length 615 */ 616 public int getMaxLengthEstimate() { 617 return mMaxLengthEstimate; 618 } 619 620 // Basics 621 //----------------------------------------------------------------------- 622 /** 623 * <p>Compares two objects for equality.</p> 624 * 625 * @param obj the object to compare to 626 * @return {@code true} if equal 627 */ 628 @Override 629 public boolean equals(final Object obj) { 630 if (!(obj instanceof FastDatePrinter)) { 631 return false; 632 } 633 final FastDatePrinter other = (FastDatePrinter) obj; 634 return mPattern.equals(other.mPattern) 635 && mTimeZone.equals(other.mTimeZone) 636 && mLocale.equals(other.mLocale); 637 } 638 639 /** 640 * <p>Returns a hash code compatible with equals.</p> 641 * 642 * @return a hash code compatible with equals 643 */ 644 @Override 645 public int hashCode() { 646 return mPattern.hashCode() + 13 * (mTimeZone.hashCode() + 13 * mLocale.hashCode()); 647 } 648 649 /** 650 * <p>Gets a debugging string version of this formatter.</p> 651 * 652 * @return a debugging string 653 */ 654 @Override 655 public String toString() { 656 return "FastDatePrinter[" + mPattern + "," + mLocale + "," + mTimeZone.getID() + "]"; 657 } 658 659 // Serializing 660 //----------------------------------------------------------------------- 661 /** 662 * Create the object after serialization. This implementation reinitializes the 663 * transient properties. 664 * 665 * @param in ObjectInputStream from which the object is being deserialized. 666 * @throws IOException if there is an IO issue. 667 * @throws ClassNotFoundException if a class cannot be found. 668 */ 669 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { 670 in.defaultReadObject(); 671 init(); 672 } 673 674 /** 675 * Appends two digits to the given buffer. 676 * 677 * @param buffer the buffer to append to. 678 * @param value the value to append digits from. 679 */ 680 private static void appendDigits(final Appendable buffer, final int value) throws IOException { 681 buffer.append((char) (value / 10 + '0')); 682 buffer.append((char) (value % 10 + '0')); 683 } 684 685 private static final int MAX_DIGITS = 10; // log10(Integer.MAX_VALUE) ~= 9.3 686 687 /** 688 * Appends all digits to the given buffer. 689 * 690 * @param buffer the buffer to append to. 691 * @param value the value to append digits from. 692 */ 693 private static void appendFullDigits(final Appendable buffer, int value, int minFieldWidth) throws IOException { 694 // specialized paths for 1 to 4 digits -> avoid the memory allocation from the temporary work array 695 // see LANG-1248 696 if (value < 10000) { 697 // less memory allocation path works for four digits or less 698 699 int nDigits = 4; 700 if (value < 1000) { 701 --nDigits; 702 if (value < 100) { 703 --nDigits; 704 if (value < 10) { 705 --nDigits; 706 } 707 } 708 } 709 // left zero pad 710 for (int i = minFieldWidth - nDigits; i > 0; --i) { 711 buffer.append('0'); 712 } 713 714 switch (nDigits) { 715 case 4: 716 buffer.append((char) (value / 1000 + '0')); 717 value %= 1000; 718 case 3: 719 if (value >= 100) { 720 buffer.append((char) (value / 100 + '0')); 721 value %= 100; 722 } else { 723 buffer.append('0'); 724 } 725 case 2: 726 if (value >= 10) { 727 buffer.append((char) (value / 10 + '0')); 728 value %= 10; 729 } else { 730 buffer.append('0'); 731 } 732 case 1: 733 buffer.append((char) (value + '0')); 734 } 735 } else { 736 // more memory allocation path works for any digits 737 738 // build up decimal representation in reverse 739 final char[] work = new char[MAX_DIGITS]; 740 int digit = 0; 741 while (value != 0) { 742 work[digit++] = (char) (value % 10 + '0'); 743 value = value / 10; 744 } 745 746 // pad with zeros 747 while (digit < minFieldWidth) { 748 buffer.append('0'); 749 --minFieldWidth; 750 } 751 752 // reverse 753 while (--digit >= 0) { 754 buffer.append(work[digit]); 755 } 756 } 757 } 758 759 // Rules 760 //----------------------------------------------------------------------- 761 /** 762 * <p>Inner class defining a rule.</p> 763 */ 764 private interface Rule { 765 /** 766 * Returns the estimated length of the result. 767 * 768 * @return the estimated length 769 */ 770 int estimateLength(); 771 772 /** 773 * Appends the value of the specified calendar to the output buffer based on the rule implementation. 774 * 775 * @param buf the output buffer 776 * @param calendar calendar to be appended 777 * @throws IOException if an I/O error occurs 778 */ 779 void appendTo(Appendable buf, Calendar calendar) throws IOException; 780 } 781 782 /** 783 * <p>Inner class defining a numeric rule.</p> 784 */ 785 private interface NumberRule extends Rule { 786 /** 787 * Appends the specified value to the output buffer based on the rule implementation. 788 * 789 * @param buffer the output buffer 790 * @param value the value to be appended 791 * @throws IOException if an I/O error occurs 792 */ 793 void appendTo(Appendable buffer, int value) throws IOException; 794 } 795 796 /** 797 * <p>Inner class to output a constant single character.</p> 798 */ 799 private static class CharacterLiteral implements Rule { 800 private final char mValue; 801 802 /** 803 * Constructs a new instance of {@code CharacterLiteral} 804 * to hold the specified value. 805 * 806 * @param value the character literal 807 */ 808 CharacterLiteral(final char value) { 809 mValue = value; 810 } 811 812 /** 813 * {@inheritDoc} 814 */ 815 @Override 816 public int estimateLength() { 817 return 1; 818 } 819 820 /** 821 * {@inheritDoc} 822 */ 823 @Override 824 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 825 buffer.append(mValue); 826 } 827 } 828 829 /** 830 * <p>Inner class to output a constant string.</p> 831 */ 832 private static class StringLiteral implements Rule { 833 private final String mValue; 834 835 /** 836 * Constructs a new instance of {@code StringLiteral} 837 * to hold the specified value. 838 * 839 * @param value the string literal 840 */ 841 StringLiteral(final String value) { 842 mValue = value; 843 } 844 845 /** 846 * {@inheritDoc} 847 */ 848 @Override 849 public int estimateLength() { 850 return mValue.length(); 851 } 852 853 /** 854 * {@inheritDoc} 855 */ 856 @Override 857 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 858 buffer.append(mValue); 859 } 860 } 861 862 /** 863 * <p>Inner class to output one of a set of values.</p> 864 */ 865 private static class TextField implements Rule { 866 private final int mField; 867 private final String[] mValues; 868 869 /** 870 * Constructs an instance of {@code TextField} 871 * with the specified field and values. 872 * 873 * @param field the field 874 * @param values the field values 875 */ 876 TextField(final int field, final String[] values) { 877 mField = field; 878 mValues = values; 879 } 880 881 /** 882 * {@inheritDoc} 883 */ 884 @Override 885 public int estimateLength() { 886 int max = 0; 887 for (int i=mValues.length; --i >= 0; ) { 888 final int len = mValues[i].length(); 889 if (len > max) { 890 max = len; 891 } 892 } 893 return max; 894 } 895 896 /** 897 * {@inheritDoc} 898 */ 899 @Override 900 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 901 buffer.append(mValues[calendar.get(mField)]); 902 } 903 } 904 905 /** 906 * <p>Inner class to output an unpadded number.</p> 907 */ 908 private static class UnpaddedNumberField implements NumberRule { 909 private final int mField; 910 911 /** 912 * Constructs an instance of {@code UnpadedNumberField} with the specified field. 913 * 914 * @param field the field 915 */ 916 UnpaddedNumberField(final int field) { 917 mField = field; 918 } 919 920 /** 921 * {@inheritDoc} 922 */ 923 @Override 924 public int estimateLength() { 925 return 4; 926 } 927 928 /** 929 * {@inheritDoc} 930 */ 931 @Override 932 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 933 appendTo(buffer, calendar.get(mField)); 934 } 935 936 /** 937 * {@inheritDoc} 938 */ 939 @Override 940 public final void appendTo(final Appendable buffer, final int value) throws IOException { 941 if (value < 10) { 942 buffer.append((char) (value + '0')); 943 } else if (value < 100) { 944 appendDigits(buffer, value); 945 } else { 946 appendFullDigits(buffer, value, 1); 947 } 948 } 949 } 950 951 /** 952 * <p>Inner class to output an unpadded month.</p> 953 */ 954 private static class UnpaddedMonthField implements NumberRule { 955 static final UnpaddedMonthField INSTANCE = new UnpaddedMonthField(); 956 957 /** 958 * Constructs an instance of {@code UnpaddedMonthField}. 959 * 960 */ 961 UnpaddedMonthField() { 962 super(); 963 } 964 965 /** 966 * {@inheritDoc} 967 */ 968 @Override 969 public int estimateLength() { 970 return 2; 971 } 972 973 /** 974 * {@inheritDoc} 975 */ 976 @Override 977 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 978 appendTo(buffer, calendar.get(Calendar.MONTH) + 1); 979 } 980 981 /** 982 * {@inheritDoc} 983 */ 984 @Override 985 public final void appendTo(final Appendable buffer, final int value) throws IOException { 986 if (value < 10) { 987 buffer.append((char) (value + '0')); 988 } else { 989 appendDigits(buffer, value); 990 } 991 } 992 } 993 994 /** 995 * <p>Inner class to output a padded number.</p> 996 */ 997 private static class PaddedNumberField implements NumberRule { 998 private final int mField; 999 private final int mSize; 1000 1001 /** 1002 * Constructs an instance of {@code PaddedNumberField}. 1003 * 1004 * @param field the field 1005 * @param size size of the output field 1006 */ 1007 PaddedNumberField(final int field, final int size) { 1008 if (size < 3) { 1009 // Should use UnpaddedNumberField or TwoDigitNumberField. 1010 throw new IllegalArgumentException(); 1011 } 1012 mField = field; 1013 mSize = size; 1014 } 1015 1016 /** 1017 * {@inheritDoc} 1018 */ 1019 @Override 1020 public int estimateLength() { 1021 return mSize; 1022 } 1023 1024 /** 1025 * {@inheritDoc} 1026 */ 1027 @Override 1028 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1029 appendTo(buffer, calendar.get(mField)); 1030 } 1031 1032 /** 1033 * {@inheritDoc} 1034 */ 1035 @Override 1036 public final void appendTo(final Appendable buffer, final int value) throws IOException { 1037 appendFullDigits(buffer, value, mSize); 1038 } 1039 } 1040 1041 /** 1042 * <p>Inner class to output a two digit number.</p> 1043 */ 1044 private static class TwoDigitNumberField implements NumberRule { 1045 private final int mField; 1046 1047 /** 1048 * Constructs an instance of {@code TwoDigitNumberField} with the specified field. 1049 * 1050 * @param field the field 1051 */ 1052 TwoDigitNumberField(final int field) { 1053 mField = field; 1054 } 1055 1056 /** 1057 * {@inheritDoc} 1058 */ 1059 @Override 1060 public int estimateLength() { 1061 return 2; 1062 } 1063 1064 /** 1065 * {@inheritDoc} 1066 */ 1067 @Override 1068 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1069 appendTo(buffer, calendar.get(mField)); 1070 } 1071 1072 /** 1073 * {@inheritDoc} 1074 */ 1075 @Override 1076 public final void appendTo(final Appendable buffer, final int value) throws IOException { 1077 if (value < 100) { 1078 appendDigits(buffer, value); 1079 } else { 1080 appendFullDigits(buffer, value, 2); 1081 } 1082 } 1083 } 1084 1085 /** 1086 * <p>Inner class to output a two digit year.</p> 1087 */ 1088 private static class TwoDigitYearField implements NumberRule { 1089 static final TwoDigitYearField INSTANCE = new TwoDigitYearField(); 1090 1091 /** 1092 * Constructs an instance of {@code TwoDigitYearField}. 1093 */ 1094 TwoDigitYearField() { 1095 super(); 1096 } 1097 1098 /** 1099 * {@inheritDoc} 1100 */ 1101 @Override 1102 public int estimateLength() { 1103 return 2; 1104 } 1105 1106 /** 1107 * {@inheritDoc} 1108 */ 1109 @Override 1110 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1111 appendTo(buffer, calendar.get(Calendar.YEAR) % 100); 1112 } 1113 1114 /** 1115 * {@inheritDoc} 1116 */ 1117 @Override 1118 public final void appendTo(final Appendable buffer, final int value) throws IOException { 1119 appendDigits(buffer, value); 1120 } 1121 } 1122 1123 /** 1124 * <p>Inner class to output a two digit month.</p> 1125 */ 1126 private static class TwoDigitMonthField implements NumberRule { 1127 static final TwoDigitMonthField INSTANCE = new TwoDigitMonthField(); 1128 1129 /** 1130 * Constructs an instance of {@code TwoDigitMonthField}. 1131 */ 1132 TwoDigitMonthField() { 1133 super(); 1134 } 1135 1136 /** 1137 * {@inheritDoc} 1138 */ 1139 @Override 1140 public int estimateLength() { 1141 return 2; 1142 } 1143 1144 /** 1145 * {@inheritDoc} 1146 */ 1147 @Override 1148 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1149 appendTo(buffer, calendar.get(Calendar.MONTH) + 1); 1150 } 1151 1152 /** 1153 * {@inheritDoc} 1154 */ 1155 @Override 1156 public final void appendTo(final Appendable buffer, final int value) throws IOException { 1157 appendDigits(buffer, value); 1158 } 1159 } 1160 1161 /** 1162 * <p>Inner class to output the twelve hour field.</p> 1163 */ 1164 private static class TwelveHourField implements NumberRule { 1165 private final NumberRule mRule; 1166 1167 /** 1168 * Constructs an instance of {@code TwelveHourField} with the specified 1169 * {@code NumberRule}. 1170 * 1171 * @param rule the rule 1172 */ 1173 TwelveHourField(final NumberRule rule) { 1174 mRule = rule; 1175 } 1176 1177 /** 1178 * {@inheritDoc} 1179 */ 1180 @Override 1181 public int estimateLength() { 1182 return mRule.estimateLength(); 1183 } 1184 1185 /** 1186 * {@inheritDoc} 1187 */ 1188 @Override 1189 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1190 int value = calendar.get(Calendar.HOUR); 1191 if (value == 0) { 1192 value = calendar.getLeastMaximum(Calendar.HOUR) + 1; 1193 } 1194 mRule.appendTo(buffer, value); 1195 } 1196 1197 /** 1198 * {@inheritDoc} 1199 */ 1200 @Override 1201 public void appendTo(final Appendable buffer, final int value) throws IOException { 1202 mRule.appendTo(buffer, value); 1203 } 1204 } 1205 1206 /** 1207 * <p>Inner class to output the twenty four hour field.</p> 1208 */ 1209 private static class TwentyFourHourField implements NumberRule { 1210 private final NumberRule mRule; 1211 1212 /** 1213 * Constructs an instance of {@code TwentyFourHourField} with the specified 1214 * {@code NumberRule}. 1215 * 1216 * @param rule the rule 1217 */ 1218 TwentyFourHourField(final NumberRule rule) { 1219 mRule = rule; 1220 } 1221 1222 /** 1223 * {@inheritDoc} 1224 */ 1225 @Override 1226 public int estimateLength() { 1227 return mRule.estimateLength(); 1228 } 1229 1230 /** 1231 * {@inheritDoc} 1232 */ 1233 @Override 1234 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1235 int value = calendar.get(Calendar.HOUR_OF_DAY); 1236 if (value == 0) { 1237 value = calendar.getMaximum(Calendar.HOUR_OF_DAY) + 1; 1238 } 1239 mRule.appendTo(buffer, value); 1240 } 1241 1242 /** 1243 * {@inheritDoc} 1244 */ 1245 @Override 1246 public void appendTo(final Appendable buffer, final int value) throws IOException { 1247 mRule.appendTo(buffer, value); 1248 } 1249 } 1250 1251 /** 1252 * <p>Inner class to output the numeric day in week.</p> 1253 */ 1254 private static class DayInWeekField implements NumberRule { 1255 private final NumberRule mRule; 1256 1257 DayInWeekField(final NumberRule rule) { 1258 mRule = rule; 1259 } 1260 1261 @Override 1262 public int estimateLength() { 1263 return mRule.estimateLength(); 1264 } 1265 1266 @Override 1267 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1268 final int value = calendar.get(Calendar.DAY_OF_WEEK); 1269 mRule.appendTo(buffer, value == Calendar.SUNDAY ? 7 : value - 1); 1270 } 1271 1272 @Override 1273 public void appendTo(final Appendable buffer, final int value) throws IOException { 1274 mRule.appendTo(buffer, value); 1275 } 1276 } 1277 1278 /** 1279 * <p>Inner class to output the numeric day in week.</p> 1280 */ 1281 private static class WeekYear implements NumberRule { 1282 private final NumberRule mRule; 1283 1284 WeekYear(final NumberRule rule) { 1285 mRule = rule; 1286 } 1287 1288 @Override 1289 public int estimateLength() { 1290 return mRule.estimateLength(); 1291 } 1292 1293 @Override 1294 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1295 mRule.appendTo(buffer, calendar.getWeekYear()); 1296 } 1297 1298 @Override 1299 public void appendTo(final Appendable buffer, final int value) throws IOException { 1300 mRule.appendTo(buffer, value); 1301 } 1302 } 1303 1304 //----------------------------------------------------------------------- 1305 1306 private static final ConcurrentMap<TimeZoneDisplayKey, String> cTimeZoneDisplayCache = 1307 new ConcurrentHashMap<>(7); 1308 /** 1309 * <p>Gets the time zone display name, using a cache for performance.</p> 1310 * 1311 * @param tz the zone to query 1312 * @param daylight true if daylight savings 1313 * @param style the style to use {@code TimeZone.LONG} or {@code TimeZone.SHORT} 1314 * @param locale the locale to use 1315 * @return the textual name of the time zone 1316 */ 1317 static String getTimeZoneDisplay(final TimeZone tz, final boolean daylight, final int style, final Locale locale) { 1318 final TimeZoneDisplayKey key = new TimeZoneDisplayKey(tz, daylight, style, locale); 1319 String value = cTimeZoneDisplayCache.get(key); 1320 if (value == null) { 1321 // This is a very slow call, so cache the results. 1322 value = tz.getDisplayName(daylight, style, locale); 1323 final String prior = cTimeZoneDisplayCache.putIfAbsent(key, value); 1324 if (prior != null) { 1325 value= prior; 1326 } 1327 } 1328 return value; 1329 } 1330 1331 /** 1332 * <p>Inner class to output a time zone name.</p> 1333 */ 1334 private static class TimeZoneNameRule implements Rule { 1335 private final Locale mLocale; 1336 private final int mStyle; 1337 private final String mStandard; 1338 private final String mDaylight; 1339 1340 /** 1341 * Constructs an instance of {@code TimeZoneNameRule} with the specified properties. 1342 * 1343 * @param timeZone the time zone 1344 * @param locale the locale 1345 * @param style the style 1346 */ 1347 TimeZoneNameRule(final TimeZone timeZone, final Locale locale, final int style) { 1348 mLocale = locale; 1349 mStyle = style; 1350 1351 mStandard = getTimeZoneDisplay(timeZone, false, style, locale); 1352 mDaylight = getTimeZoneDisplay(timeZone, true, style, locale); 1353 } 1354 1355 /** 1356 * {@inheritDoc} 1357 */ 1358 @Override 1359 public int estimateLength() { 1360 // We have no access to the Calendar object that will be passed to 1361 // appendTo so base estimate on the TimeZone passed to the 1362 // constructor 1363 return Math.max(mStandard.length(), mDaylight.length()); 1364 } 1365 1366 /** 1367 * {@inheritDoc} 1368 */ 1369 @Override 1370 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1371 final TimeZone zone = calendar.getTimeZone(); 1372 if (calendar.get(Calendar.DST_OFFSET) == 0) { 1373 buffer.append(getTimeZoneDisplay(zone, false, mStyle, mLocale)); 1374 } else { 1375 buffer.append(getTimeZoneDisplay(zone, true, mStyle, mLocale)); 1376 } 1377 } 1378 } 1379 1380 /** 1381 * <p>Inner class to output a time zone as a number {@code +/-HHMM} 1382 * or {@code +/-HH:MM}.</p> 1383 */ 1384 private static class TimeZoneNumberRule implements Rule { 1385 static final TimeZoneNumberRule INSTANCE_COLON = new TimeZoneNumberRule(true); 1386 static final TimeZoneNumberRule INSTANCE_NO_COLON = new TimeZoneNumberRule(false); 1387 1388 final boolean mColon; 1389 1390 /** 1391 * Constructs an instance of {@code TimeZoneNumberRule} with the specified properties. 1392 * 1393 * @param colon add colon between HH and MM in the output if {@code true} 1394 */ 1395 TimeZoneNumberRule(final boolean colon) { 1396 mColon = colon; 1397 } 1398 1399 /** 1400 * {@inheritDoc} 1401 */ 1402 @Override 1403 public int estimateLength() { 1404 return 5; 1405 } 1406 1407 /** 1408 * {@inheritDoc} 1409 */ 1410 @Override 1411 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1412 1413 int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); 1414 1415 if (offset < 0) { 1416 buffer.append('-'); 1417 offset = -offset; 1418 } else { 1419 buffer.append('+'); 1420 } 1421 1422 final int hours = offset / (60 * 60 * 1000); 1423 appendDigits(buffer, hours); 1424 1425 if (mColon) { 1426 buffer.append(':'); 1427 } 1428 1429 final int minutes = offset / (60 * 1000) - 60 * hours; 1430 appendDigits(buffer, minutes); 1431 } 1432 } 1433 1434 /** 1435 * <p>Inner class to output a time zone as a number {@code +/-HHMM} 1436 * or {@code +/-HH:MM}.</p> 1437 */ 1438 private static class Iso8601_Rule implements Rule { 1439 1440 // Sign TwoDigitHours or Z 1441 static final Iso8601_Rule ISO8601_HOURS = new Iso8601_Rule(3); 1442 // Sign TwoDigitHours Minutes or Z 1443 static final Iso8601_Rule ISO8601_HOURS_MINUTES = new Iso8601_Rule(5); 1444 // Sign TwoDigitHours : Minutes or Z 1445 static final Iso8601_Rule ISO8601_HOURS_COLON_MINUTES = new Iso8601_Rule(6); 1446 1447 /** 1448 * Factory method for Iso8601_Rules. 1449 * 1450 * @param tokenLen a token indicating the length of the TimeZone String to be formatted. 1451 * @return a Iso8601_Rule that can format TimeZone String of length {@code tokenLen}. If no such 1452 * rule exists, an IllegalArgumentException will be thrown. 1453 */ 1454 static Iso8601_Rule getRule(final int tokenLen) { 1455 switch(tokenLen) { 1456 case 1: 1457 return ISO8601_HOURS; 1458 case 2: 1459 return ISO8601_HOURS_MINUTES; 1460 case 3: 1461 return ISO8601_HOURS_COLON_MINUTES; 1462 default: 1463 throw new IllegalArgumentException("invalid number of X"); 1464 } 1465 } 1466 1467 final int length; 1468 1469 /** 1470 * Constructs an instance of {@code Iso8601_Rule} with the specified properties. 1471 * 1472 * @param length The number of characters in output (unless Z is output) 1473 */ 1474 Iso8601_Rule(final int length) { 1475 this.length = length; 1476 } 1477 1478 /** 1479 * {@inheritDoc} 1480 */ 1481 @Override 1482 public int estimateLength() { 1483 return length; 1484 } 1485 1486 /** 1487 * {@inheritDoc} 1488 */ 1489 @Override 1490 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1491 int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); 1492 if (offset == 0) { 1493 buffer.append("Z"); 1494 return; 1495 } 1496 1497 if (offset < 0) { 1498 buffer.append('-'); 1499 offset = -offset; 1500 } else { 1501 buffer.append('+'); 1502 } 1503 1504 final int hours = offset / (60 * 60 * 1000); 1505 appendDigits(buffer, hours); 1506 1507 if (length<5) { 1508 return; 1509 } 1510 1511 if (length==6) { 1512 buffer.append(':'); 1513 } 1514 1515 final int minutes = offset / (60 * 1000) - 60 * hours; 1516 appendDigits(buffer, minutes); 1517 } 1518 } 1519 1520 // ---------------------------------------------------------------------- 1521 /** 1522 * <p>Inner class that acts as a compound key for time zone names.</p> 1523 */ 1524 private static class TimeZoneDisplayKey { 1525 private final TimeZone mTimeZone; 1526 private final int mStyle; 1527 private final Locale mLocale; 1528 1529 /** 1530 * Constructs an instance of {@code TimeZoneDisplayKey} with the specified properties. 1531 * 1532 * @param timeZone the time zone 1533 * @param daylight adjust the style for daylight saving time if {@code true} 1534 * @param style the timezone style 1535 * @param locale the timezone locale 1536 */ 1537 TimeZoneDisplayKey(final TimeZone timeZone, 1538 final boolean daylight, final int style, final Locale locale) { 1539 mTimeZone = timeZone; 1540 if (daylight) { 1541 mStyle = style | 0x80000000; 1542 } else { 1543 mStyle = style; 1544 } 1545 mLocale = locale; 1546 } 1547 1548 /** 1549 * {@inheritDoc} 1550 */ 1551 @Override 1552 public int hashCode() { 1553 return (mStyle * 31 + mLocale.hashCode() ) * 31 + mTimeZone.hashCode(); 1554 } 1555 1556 /** 1557 * {@inheritDoc} 1558 */ 1559 @Override 1560 public boolean equals(final Object obj) { 1561 if (this == obj) { 1562 return true; 1563 } 1564 if (obj instanceof TimeZoneDisplayKey) { 1565 final TimeZoneDisplayKey other = (TimeZoneDisplayKey) obj; 1566 return 1567 mTimeZone.equals(other.mTimeZone) && 1568 mStyle == other.mStyle && 1569 mLocale.equals(other.mLocale); 1570 } 1571 return false; 1572 } 1573 } 1574}