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.GregorianCalendar; 029import java.util.List; 030import java.util.Locale; 031import java.util.TimeZone; 032import java.util.concurrent.ConcurrentHashMap; 033import java.util.concurrent.ConcurrentMap; 034 035import org.apache.commons.lang3.Validate; 036 037/** 038 * <p>FastDatePrinter is a fast and thread-safe version of 039 * {@link java.text.SimpleDateFormat}.</p> 040 * 041 * <p>This class can be used as a direct replacement to 042 * {@code SimpleDateFormat} in most formatting situations. 043 * This class is especially useful in multi-threaded server environments. 044 * {@code SimpleDateFormat} is not thread-safe in any JDK version, 045 * nor will it be as Sun have closed the bug/RFE. 046 * </p> 047 * 048 * <p>Only formatting is supported, but all patterns are compatible with 049 * SimpleDateFormat (except time zones and some year patterns - see below).</p> 050 * 051 * <p>Java 1.4 introduced a new pattern letter, {@code 'Z'}, to represent 052 * time zones in RFC822 format (eg. {@code +0800} or {@code -1100}). 053 * This pattern letter can be used here (on all JDK versions).</p> 054 * 055 * <p>In addition, the pattern {@code 'ZZ'} has been made to represent 056 * ISO8601 full format time zones (eg. {@code +08:00} or {@code -11:00}). 057 * This introduces a minor incompatibility with Java 1.4, but at a gain of 058 * useful functionality.</p> 059 * 060 * <p>Javadoc cites for the year pattern: <i>For formatting, if the number of 061 * pattern letters is 2, the year is truncated to 2 digits; otherwise it is 062 * interpreted as a number.</i> Starting with Java 1.7 a pattern of 'Y' or 063 * 'YYY' will be formatted as '2003', while it was '03' in former Java 064 * versions. FastDatePrinter implements the behavior of Java 7.</p> 065 * 066 * @version $Id: FastDatePrinter.java 1552661 2013-12-20 13:36:30Z britter $ 067 * @since 3.2 068 */ 069public class FastDatePrinter implements DatePrinter, Serializable { 070 // A lot of the speed in this class comes from caching, but some comes 071 // from the special int to StringBuffer conversion. 072 // 073 // The following produces a padded 2 digit number: 074 // buffer.append((char)(value / 10 + '0')); 075 // buffer.append((char)(value % 10 + '0')); 076 // 077 // Note that the fastest append to StringBuffer is a single char (used here). 078 // Note that Integer.toString() is not called, the conversion is simply 079 // taking the value and adding (mathematically) the ASCII value for '0'. 080 // So, don't change this code! It works and is very fast. 081 082 /** 083 * Required for serialization support. 084 * 085 * @see java.io.Serializable 086 */ 087 private static final long serialVersionUID = 1L; 088 089 /** 090 * FULL locale dependent date or time style. 091 */ 092 public static final int FULL = DateFormat.FULL; 093 /** 094 * LONG locale dependent date or time style. 095 */ 096 public static final int LONG = DateFormat.LONG; 097 /** 098 * MEDIUM locale dependent date or time style. 099 */ 100 public static final int MEDIUM = DateFormat.MEDIUM; 101 /** 102 * SHORT locale dependent date or time style. 103 */ 104 public static final int SHORT = DateFormat.SHORT; 105 106 /** 107 * The pattern. 108 */ 109 private final String mPattern; 110 /** 111 * The time zone. 112 */ 113 private final TimeZone mTimeZone; 114 /** 115 * The locale. 116 */ 117 private final Locale mLocale; 118 /** 119 * The parsed rules. 120 */ 121 private transient Rule[] mRules; 122 /** 123 * The estimated maximum length. 124 */ 125 private transient int mMaxLengthEstimate; 126 127 // Constructor 128 //----------------------------------------------------------------------- 129 /** 130 * <p>Constructs a new FastDatePrinter.</p> 131 * 132 * @param pattern {@link java.text.SimpleDateFormat} compatible pattern 133 * @param timeZone non-null time zone to use 134 * @param locale non-null locale to use 135 * @throws NullPointerException if pattern, timeZone, or locale is null. 136 */ 137 protected FastDatePrinter(final String pattern, final TimeZone timeZone, final Locale locale) { 138 mPattern = pattern; 139 mTimeZone = timeZone; 140 mLocale = locale; 141 142 init(); 143 } 144 145 /** 146 * <p>Initializes the instance for first use.</p> 147 */ 148 private void init() { 149 final List<Rule> rulesList = parsePattern(); 150 mRules = rulesList.toArray(new Rule[rulesList.size()]); 151 152 int len = 0; 153 for (int i=mRules.length; --i >= 0; ) { 154 len += mRules[i].estimateLength(); 155 } 156 157 mMaxLengthEstimate = len; 158 } 159 160 // Parse the pattern 161 //----------------------------------------------------------------------- 162 /** 163 * <p>Returns a list of Rules given a pattern.</p> 164 * 165 * @return a {@code List} of Rule objects 166 * @throws IllegalArgumentException if pattern is invalid 167 */ 168 protected List<Rule> parsePattern() { 169 final DateFormatSymbols symbols = new DateFormatSymbols(mLocale); 170 final List<Rule> rules = new ArrayList<Rule>(); 171 172 final String[] ERAs = symbols.getEras(); 173 final String[] months = symbols.getMonths(); 174 final String[] shortMonths = symbols.getShortMonths(); 175 final String[] weekdays = symbols.getWeekdays(); 176 final String[] shortWeekdays = symbols.getShortWeekdays(); 177 final String[] AmPmStrings = symbols.getAmPmStrings(); 178 179 final int length = mPattern.length(); 180 final int[] indexRef = new int[1]; 181 182 for (int i = 0; i < length; i++) { 183 indexRef[0] = i; 184 final String token = parseToken(mPattern, indexRef); 185 i = indexRef[0]; 186 187 final int tokenLen = token.length(); 188 if (tokenLen == 0) { 189 break; 190 } 191 192 Rule rule; 193 final char c = token.charAt(0); 194 195 switch (c) { 196 case 'G': // era designator (text) 197 rule = new TextField(Calendar.ERA, ERAs); 198 break; 199 case 'y': // year (number) 200 if (tokenLen == 2) { 201 rule = TwoDigitYearField.INSTANCE; 202 } else { 203 rule = selectNumberRule(Calendar.YEAR, tokenLen < 4 ? 4 : tokenLen); 204 } 205 break; 206 case 'M': // month in year (text and number) 207 if (tokenLen >= 4) { 208 rule = new TextField(Calendar.MONTH, months); 209 } else if (tokenLen == 3) { 210 rule = new TextField(Calendar.MONTH, shortMonths); 211 } else if (tokenLen == 2) { 212 rule = TwoDigitMonthField.INSTANCE; 213 } else { 214 rule = UnpaddedMonthField.INSTANCE; 215 } 216 break; 217 case 'd': // day in month (number) 218 rule = selectNumberRule(Calendar.DAY_OF_MONTH, tokenLen); 219 break; 220 case 'h': // hour in am/pm (number, 1..12) 221 rule = new TwelveHourField(selectNumberRule(Calendar.HOUR, tokenLen)); 222 break; 223 case 'H': // hour in day (number, 0..23) 224 rule = selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen); 225 break; 226 case 'm': // minute in hour (number) 227 rule = selectNumberRule(Calendar.MINUTE, tokenLen); 228 break; 229 case 's': // second in minute (number) 230 rule = selectNumberRule(Calendar.SECOND, tokenLen); 231 break; 232 case 'S': // millisecond (number) 233 rule = selectNumberRule(Calendar.MILLISECOND, tokenLen); 234 break; 235 case 'E': // day in week (text) 236 rule = new TextField(Calendar.DAY_OF_WEEK, tokenLen < 4 ? shortWeekdays : weekdays); 237 break; 238 case 'D': // day in year (number) 239 rule = selectNumberRule(Calendar.DAY_OF_YEAR, tokenLen); 240 break; 241 case 'F': // day of week in month (number) 242 rule = selectNumberRule(Calendar.DAY_OF_WEEK_IN_MONTH, tokenLen); 243 break; 244 case 'w': // week in year (number) 245 rule = selectNumberRule(Calendar.WEEK_OF_YEAR, tokenLen); 246 break; 247 case 'W': // week in month (number) 248 rule = selectNumberRule(Calendar.WEEK_OF_MONTH, tokenLen); 249 break; 250 case 'a': // am/pm marker (text) 251 rule = new TextField(Calendar.AM_PM, AmPmStrings); 252 break; 253 case 'k': // hour in day (1..24) 254 rule = new TwentyFourHourField(selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen)); 255 break; 256 case 'K': // hour in am/pm (0..11) 257 rule = selectNumberRule(Calendar.HOUR, tokenLen); 258 break; 259 case 'z': // time zone (text) 260 if (tokenLen >= 4) { 261 rule = new TimeZoneNameRule(mTimeZone, mLocale, TimeZone.LONG); 262 } else { 263 rule = new TimeZoneNameRule(mTimeZone, mLocale, TimeZone.SHORT); 264 } 265 break; 266 case 'Z': // time zone (value) 267 if (tokenLen == 1) { 268 rule = TimeZoneNumberRule.INSTANCE_NO_COLON; 269 } else { 270 rule = TimeZoneNumberRule.INSTANCE_COLON; 271 } 272 break; 273 case '\'': // literal text 274 final String sub = token.substring(1); 275 if (sub.length() == 1) { 276 rule = new CharacterLiteral(sub.charAt(0)); 277 } else { 278 rule = new StringLiteral(sub); 279 } 280 break; 281 default: 282 throw new IllegalArgumentException("Illegal pattern component: " + token); 283 } 284 285 rules.add(rule); 286 } 287 288 return rules; 289 } 290 291 /** 292 * <p>Performs the parsing of tokens.</p> 293 * 294 * @param pattern the pattern 295 * @param indexRef index references 296 * @return parsed token 297 */ 298 protected String parseToken(final String pattern, final int[] indexRef) { 299 final StringBuilder buf = new StringBuilder(); 300 301 int i = indexRef[0]; 302 final int length = pattern.length(); 303 304 char c = pattern.charAt(i); 305 if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') { 306 // Scan a run of the same character, which indicates a time 307 // pattern. 308 buf.append(c); 309 310 while (i + 1 < length) { 311 final char peek = pattern.charAt(i + 1); 312 if (peek == c) { 313 buf.append(c); 314 i++; 315 } else { 316 break; 317 } 318 } 319 } else { 320 // This will identify token as text. 321 buf.append('\''); 322 323 boolean inLiteral = false; 324 325 for (; i < length; i++) { 326 c = pattern.charAt(i); 327 328 if (c == '\'') { 329 if (i + 1 < length && pattern.charAt(i + 1) == '\'') { 330 // '' is treated as escaped ' 331 i++; 332 buf.append(c); 333 } else { 334 inLiteral = !inLiteral; 335 } 336 } else if (!inLiteral && 337 (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) { 338 i--; 339 break; 340 } else { 341 buf.append(c); 342 } 343 } 344 } 345 346 indexRef[0] = i; 347 return buf.toString(); 348 } 349 350 /** 351 * <p>Gets an appropriate rule for the padding required.</p> 352 * 353 * @param field the field to get a rule for 354 * @param padding the padding required 355 * @return a new rule with the correct padding 356 */ 357 protected NumberRule selectNumberRule(final int field, final int padding) { 358 switch (padding) { 359 case 1: 360 return new UnpaddedNumberField(field); 361 case 2: 362 return new TwoDigitNumberField(field); 363 default: 364 return new PaddedNumberField(field, padding); 365 } 366 } 367 368 // Format methods 369 //----------------------------------------------------------------------- 370 /** 371 * <p>Formats a {@code Date}, {@code Calendar} or 372 * {@code Long} (milliseconds) object.</p> 373 * 374 * @param obj the object to format 375 * @param toAppendTo the buffer to append to 376 * @param pos the position - ignored 377 * @return the buffer passed in 378 */ 379 @Override 380 public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) { 381 if (obj instanceof Date) { 382 return format((Date) obj, toAppendTo); 383 } else if (obj instanceof Calendar) { 384 return format((Calendar) obj, toAppendTo); 385 } else if (obj instanceof Long) { 386 return format(((Long) obj).longValue(), toAppendTo); 387 } else { 388 throw new IllegalArgumentException("Unknown class: " + 389 (obj == null ? "<null>" : obj.getClass().getName())); 390 } 391 } 392 393 /* (non-Javadoc) 394 * @see org.apache.commons.lang3.time.DatePrinter#format(long) 395 */ 396 @Override 397 public String format(final long millis) { 398 final Calendar c = newCalendar(); // hard code GregorianCalendar 399 c.setTimeInMillis(millis); 400 return applyRulesToString(c); 401 } 402 403 /** 404 * Creates a String representation of the given Calendar by applying the rules of this printer to it. 405 * @param c the Calender to apply the rules to. 406 * @return a String representation of the given Calendar. 407 */ 408 private String applyRulesToString(final Calendar c) { 409 return applyRules(c, new StringBuffer(mMaxLengthEstimate)).toString(); 410 } 411 412 /** 413 * Creation method for ne calender instances. 414 * @return a new Calendar instance. 415 */ 416 private GregorianCalendar newCalendar() { 417 // hard code GregorianCalendar 418 return new GregorianCalendar(mTimeZone, mLocale); 419 } 420 421 /* (non-Javadoc) 422 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date) 423 */ 424 @Override 425 public String format(final Date date) { 426 final Calendar c = newCalendar(); // hard code GregorianCalendar 427 c.setTime(date); 428 return applyRulesToString(c); 429 } 430 431 /* (non-Javadoc) 432 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar) 433 */ 434 @Override 435 public String format(final Calendar calendar) { 436 return format(calendar, new StringBuffer(mMaxLengthEstimate)).toString(); 437 } 438 439 /* (non-Javadoc) 440 * @see org.apache.commons.lang3.time.DatePrinter#format(long, java.lang.StringBuffer) 441 */ 442 @Override 443 public StringBuffer format(final long millis, final StringBuffer buf) { 444 return format(new Date(millis), buf); 445 } 446 447 /* (non-Javadoc) 448 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date, java.lang.StringBuffer) 449 */ 450 @Override 451 public StringBuffer format(final Date date, final StringBuffer buf) { 452 final Calendar c = newCalendar(); // hard code GregorianCalendar 453 c.setTime(date); 454 return applyRules(c, buf); 455 } 456 457 /* (non-Javadoc) 458 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar, java.lang.StringBuffer) 459 */ 460 @Override 461 public StringBuffer format(final Calendar calendar, final StringBuffer buf) { 462 return applyRules(calendar, buf); 463 } 464 465 /** 466 * <p>Performs the formatting by applying the rules to the 467 * specified calendar.</p> 468 * 469 * @param calendar the calendar to format 470 * @param buf the buffer to format into 471 * @return the specified string buffer 472 */ 473 protected StringBuffer applyRules(final Calendar calendar, final StringBuffer buf) { 474 for (final Rule rule : mRules) { 475 rule.appendTo(buf, calendar); 476 } 477 return buf; 478 } 479 480 // Accessors 481 //----------------------------------------------------------------------- 482 /* (non-Javadoc) 483 * @see org.apache.commons.lang3.time.DatePrinter#getPattern() 484 */ 485 @Override 486 public String getPattern() { 487 return mPattern; 488 } 489 490 /* (non-Javadoc) 491 * @see org.apache.commons.lang3.time.DatePrinter#getTimeZone() 492 */ 493 @Override 494 public TimeZone getTimeZone() { 495 return mTimeZone; 496 } 497 498 /* (non-Javadoc) 499 * @see org.apache.commons.lang3.time.DatePrinter#getLocale() 500 */ 501 @Override 502 public Locale getLocale() { 503 return mLocale; 504 } 505 506 /** 507 * <p>Gets an estimate for the maximum string length that the 508 * formatter will produce.</p> 509 * 510 * <p>The actual formatted length will almost always be less than or 511 * equal to this amount.</p> 512 * 513 * @return the maximum formatted length 514 */ 515 public int getMaxLengthEstimate() { 516 return mMaxLengthEstimate; 517 } 518 519 // Basics 520 //----------------------------------------------------------------------- 521 /** 522 * <p>Compares two objects for equality.</p> 523 * 524 * @param obj the object to compare to 525 * @return {@code true} if equal 526 */ 527 @Override 528 public boolean equals(final Object obj) { 529 if (obj instanceof FastDatePrinter == false) { 530 return false; 531 } 532 final FastDatePrinter other = (FastDatePrinter) obj; 533 return mPattern.equals(other.mPattern) 534 && mTimeZone.equals(other.mTimeZone) 535 && mLocale.equals(other.mLocale); 536 } 537 538 /** 539 * <p>Returns a hashcode compatible with equals.</p> 540 * 541 * @return a hashcode compatible with equals 542 */ 543 @Override 544 public int hashCode() { 545 return mPattern.hashCode() + 13 * (mTimeZone.hashCode() + 13 * mLocale.hashCode()); 546 } 547 548 /** 549 * <p>Gets a debugging string version of this formatter.</p> 550 * 551 * @return a debugging string 552 */ 553 @Override 554 public String toString() { 555 return "FastDatePrinter[" + mPattern + "," + mLocale + "," + mTimeZone.getID() + "]"; 556 } 557 558 // Serializing 559 //----------------------------------------------------------------------- 560 /** 561 * Create the object after serialization. This implementation reinitializes the 562 * transient properties. 563 * 564 * @param in ObjectInputStream from which the object is being deserialized. 565 * @throws IOException if there is an IO issue. 566 * @throws ClassNotFoundException if a class cannot be found. 567 */ 568 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { 569 in.defaultReadObject(); 570 init(); 571 } 572 573 // Rules 574 //----------------------------------------------------------------------- 575 /** 576 * <p>Inner class defining a rule.</p> 577 */ 578 private interface Rule { 579 /** 580 * Returns the estimated lentgh of the result. 581 * 582 * @return the estimated length 583 */ 584 int estimateLength(); 585 586 /** 587 * Appends the value of the specified calendar to the output buffer based on the rule implementation. 588 * 589 * @param buffer the output buffer 590 * @param calendar calendar to be appended 591 */ 592 void appendTo(StringBuffer buffer, Calendar calendar); 593 } 594 595 /** 596 * <p>Inner class defining a numeric rule.</p> 597 */ 598 private interface NumberRule extends Rule { 599 /** 600 * Appends the specified value to the output buffer based on the rule implementation. 601 * 602 * @param buffer the output buffer 603 * @param value the value to be appended 604 */ 605 void appendTo(StringBuffer buffer, int value); 606 } 607 608 /** 609 * <p>Inner class to output a constant single character.</p> 610 */ 611 private static class CharacterLiteral implements Rule { 612 private final char mValue; 613 614 /** 615 * Constructs a new instance of {@code CharacterLiteral} 616 * to hold the specified value. 617 * 618 * @param value the character literal 619 */ 620 CharacterLiteral(final char value) { 621 mValue = value; 622 } 623 624 /** 625 * {@inheritDoc} 626 */ 627 @Override 628 public int estimateLength() { 629 return 1; 630 } 631 632 /** 633 * {@inheritDoc} 634 */ 635 @Override 636 public void appendTo(final StringBuffer buffer, final Calendar calendar) { 637 buffer.append(mValue); 638 } 639 } 640 641 /** 642 * <p>Inner class to output a constant string.</p> 643 */ 644 private static class StringLiteral implements Rule { 645 private final String mValue; 646 647 /** 648 * Constructs a new instance of {@code StringLiteral} 649 * to hold the specified value. 650 * 651 * @param value the string literal 652 */ 653 StringLiteral(final String value) { 654 mValue = value; 655 } 656 657 /** 658 * {@inheritDoc} 659 */ 660 @Override 661 public int estimateLength() { 662 return mValue.length(); 663 } 664 665 /** 666 * {@inheritDoc} 667 */ 668 @Override 669 public void appendTo(final StringBuffer buffer, final Calendar calendar) { 670 buffer.append(mValue); 671 } 672 } 673 674 /** 675 * <p>Inner class to output one of a set of values.</p> 676 */ 677 private static class TextField implements Rule { 678 private final int mField; 679 private final String[] mValues; 680 681 /** 682 * Constructs an instance of {@code TextField} 683 * with the specified field and values. 684 * 685 * @param field the field 686 * @param values the field values 687 */ 688 TextField(final int field, final String[] values) { 689 mField = field; 690 mValues = values; 691 } 692 693 /** 694 * {@inheritDoc} 695 */ 696 @Override 697 public int estimateLength() { 698 int max = 0; 699 for (int i=mValues.length; --i >= 0; ) { 700 final int len = mValues[i].length(); 701 if (len > max) { 702 max = len; 703 } 704 } 705 return max; 706 } 707 708 /** 709 * {@inheritDoc} 710 */ 711 @Override 712 public void appendTo(final StringBuffer buffer, final Calendar calendar) { 713 buffer.append(mValues[calendar.get(mField)]); 714 } 715 } 716 717 /** 718 * <p>Inner class to output an unpadded number.</p> 719 */ 720 private static class UnpaddedNumberField implements NumberRule { 721 private final int mField; 722 723 /** 724 * Constructs an instance of {@code UnpadedNumberField} with the specified field. 725 * 726 * @param field the field 727 */ 728 UnpaddedNumberField(final int field) { 729 mField = field; 730 } 731 732 /** 733 * {@inheritDoc} 734 */ 735 @Override 736 public int estimateLength() { 737 return 4; 738 } 739 740 /** 741 * {@inheritDoc} 742 */ 743 @Override 744 public void appendTo(final StringBuffer buffer, final Calendar calendar) { 745 appendTo(buffer, calendar.get(mField)); 746 } 747 748 /** 749 * {@inheritDoc} 750 */ 751 @Override 752 public final void appendTo(final StringBuffer buffer, final int value) { 753 if (value < 10) { 754 buffer.append((char)(value + '0')); 755 } else if (value < 100) { 756 buffer.append((char)(value / 10 + '0')); 757 buffer.append((char)(value % 10 + '0')); 758 } else { 759 buffer.append(Integer.toString(value)); 760 } 761 } 762 } 763 764 /** 765 * <p>Inner class to output an unpadded month.</p> 766 */ 767 private static class UnpaddedMonthField implements NumberRule { 768 static final UnpaddedMonthField INSTANCE = new UnpaddedMonthField(); 769 770 /** 771 * Constructs an instance of {@code UnpaddedMonthField}. 772 * 773 */ 774 UnpaddedMonthField() { 775 super(); 776 } 777 778 /** 779 * {@inheritDoc} 780 */ 781 @Override 782 public int estimateLength() { 783 return 2; 784 } 785 786 /** 787 * {@inheritDoc} 788 */ 789 @Override 790 public void appendTo(final StringBuffer buffer, final Calendar calendar) { 791 appendTo(buffer, calendar.get(Calendar.MONTH) + 1); 792 } 793 794 /** 795 * {@inheritDoc} 796 */ 797 @Override 798 public final void appendTo(final StringBuffer buffer, final int value) { 799 if (value < 10) { 800 buffer.append((char)(value + '0')); 801 } else { 802 buffer.append((char)(value / 10 + '0')); 803 buffer.append((char)(value % 10 + '0')); 804 } 805 } 806 } 807 808 /** 809 * <p>Inner class to output a padded number.</p> 810 */ 811 private static class PaddedNumberField implements NumberRule { 812 private final int mField; 813 private final int mSize; 814 815 /** 816 * Constructs an instance of {@code PaddedNumberField}. 817 * 818 * @param field the field 819 * @param size size of the output field 820 */ 821 PaddedNumberField(final int field, final int size) { 822 if (size < 3) { 823 // Should use UnpaddedNumberField or TwoDigitNumberField. 824 throw new IllegalArgumentException(); 825 } 826 mField = field; 827 mSize = size; 828 } 829 830 /** 831 * {@inheritDoc} 832 */ 833 @Override 834 public int estimateLength() { 835 return 4; 836 } 837 838 /** 839 * {@inheritDoc} 840 */ 841 @Override 842 public void appendTo(final StringBuffer buffer, final Calendar calendar) { 843 appendTo(buffer, calendar.get(mField)); 844 } 845 846 /** 847 * {@inheritDoc} 848 */ 849 @Override 850 public final void appendTo(final StringBuffer buffer, final int value) { 851 if (value < 100) { 852 for (int i = mSize; --i >= 2; ) { 853 buffer.append('0'); 854 } 855 buffer.append((char)(value / 10 + '0')); 856 buffer.append((char)(value % 10 + '0')); 857 } else { 858 int digits; 859 if (value < 1000) { 860 digits = 3; 861 } else { 862 Validate.isTrue(value > -1, "Negative values should not be possible", value); 863 digits = Integer.toString(value).length(); 864 } 865 for (int i = mSize; --i >= digits; ) { 866 buffer.append('0'); 867 } 868 buffer.append(Integer.toString(value)); 869 } 870 } 871 } 872 873 /** 874 * <p>Inner class to output a two digit number.</p> 875 */ 876 private static class TwoDigitNumberField implements NumberRule { 877 private final int mField; 878 879 /** 880 * Constructs an instance of {@code TwoDigitNumberField} with the specified field. 881 * 882 * @param field the field 883 */ 884 TwoDigitNumberField(final int field) { 885 mField = field; 886 } 887 888 /** 889 * {@inheritDoc} 890 */ 891 @Override 892 public int estimateLength() { 893 return 2; 894 } 895 896 /** 897 * {@inheritDoc} 898 */ 899 @Override 900 public void appendTo(final StringBuffer buffer, final Calendar calendar) { 901 appendTo(buffer, calendar.get(mField)); 902 } 903 904 /** 905 * {@inheritDoc} 906 */ 907 @Override 908 public final void appendTo(final StringBuffer buffer, final int value) { 909 if (value < 100) { 910 buffer.append((char)(value / 10 + '0')); 911 buffer.append((char)(value % 10 + '0')); 912 } else { 913 buffer.append(Integer.toString(value)); 914 } 915 } 916 } 917 918 /** 919 * <p>Inner class to output a two digit year.</p> 920 */ 921 private static class TwoDigitYearField implements NumberRule { 922 static final TwoDigitYearField INSTANCE = new TwoDigitYearField(); 923 924 /** 925 * Constructs an instance of {@code TwoDigitYearField}. 926 */ 927 TwoDigitYearField() { 928 super(); 929 } 930 931 /** 932 * {@inheritDoc} 933 */ 934 @Override 935 public int estimateLength() { 936 return 2; 937 } 938 939 /** 940 * {@inheritDoc} 941 */ 942 @Override 943 public void appendTo(final StringBuffer buffer, final Calendar calendar) { 944 appendTo(buffer, calendar.get(Calendar.YEAR) % 100); 945 } 946 947 /** 948 * {@inheritDoc} 949 */ 950 @Override 951 public final void appendTo(final StringBuffer buffer, final int value) { 952 buffer.append((char)(value / 10 + '0')); 953 buffer.append((char)(value % 10 + '0')); 954 } 955 } 956 957 /** 958 * <p>Inner class to output a two digit month.</p> 959 */ 960 private static class TwoDigitMonthField implements NumberRule { 961 static final TwoDigitMonthField INSTANCE = new TwoDigitMonthField(); 962 963 /** 964 * Constructs an instance of {@code TwoDigitMonthField}. 965 */ 966 TwoDigitMonthField() { 967 super(); 968 } 969 970 /** 971 * {@inheritDoc} 972 */ 973 @Override 974 public int estimateLength() { 975 return 2; 976 } 977 978 /** 979 * {@inheritDoc} 980 */ 981 @Override 982 public void appendTo(final StringBuffer buffer, final Calendar calendar) { 983 appendTo(buffer, calendar.get(Calendar.MONTH) + 1); 984 } 985 986 /** 987 * {@inheritDoc} 988 */ 989 @Override 990 public final void appendTo(final StringBuffer buffer, final int value) { 991 buffer.append((char)(value / 10 + '0')); 992 buffer.append((char)(value % 10 + '0')); 993 } 994 } 995 996 /** 997 * <p>Inner class to output the twelve hour field.</p> 998 */ 999 private static class TwelveHourField implements NumberRule { 1000 private final NumberRule mRule; 1001 1002 /** 1003 * Constructs an instance of {@code TwelveHourField} with the specified 1004 * {@code NumberRule}. 1005 * 1006 * @param rule the rule 1007 */ 1008 TwelveHourField(final NumberRule rule) { 1009 mRule = rule; 1010 } 1011 1012 /** 1013 * {@inheritDoc} 1014 */ 1015 @Override 1016 public int estimateLength() { 1017 return mRule.estimateLength(); 1018 } 1019 1020 /** 1021 * {@inheritDoc} 1022 */ 1023 @Override 1024 public void appendTo(final StringBuffer buffer, final Calendar calendar) { 1025 int value = calendar.get(Calendar.HOUR); 1026 if (value == 0) { 1027 value = calendar.getLeastMaximum(Calendar.HOUR) + 1; 1028 } 1029 mRule.appendTo(buffer, value); 1030 } 1031 1032 /** 1033 * {@inheritDoc} 1034 */ 1035 @Override 1036 public void appendTo(final StringBuffer buffer, final int value) { 1037 mRule.appendTo(buffer, value); 1038 } 1039 } 1040 1041 /** 1042 * <p>Inner class to output the twenty four hour field.</p> 1043 */ 1044 private static class TwentyFourHourField implements NumberRule { 1045 private final NumberRule mRule; 1046 1047 /** 1048 * Constructs an instance of {@code TwentyFourHourField} with the specified 1049 * {@code NumberRule}. 1050 * 1051 * @param rule the rule 1052 */ 1053 TwentyFourHourField(final NumberRule rule) { 1054 mRule = rule; 1055 } 1056 1057 /** 1058 * {@inheritDoc} 1059 */ 1060 @Override 1061 public int estimateLength() { 1062 return mRule.estimateLength(); 1063 } 1064 1065 /** 1066 * {@inheritDoc} 1067 */ 1068 @Override 1069 public void appendTo(final StringBuffer buffer, final Calendar calendar) { 1070 int value = calendar.get(Calendar.HOUR_OF_DAY); 1071 if (value == 0) { 1072 value = calendar.getMaximum(Calendar.HOUR_OF_DAY) + 1; 1073 } 1074 mRule.appendTo(buffer, value); 1075 } 1076 1077 /** 1078 * {@inheritDoc} 1079 */ 1080 @Override 1081 public void appendTo(final StringBuffer buffer, final int value) { 1082 mRule.appendTo(buffer, value); 1083 } 1084 } 1085 1086 //----------------------------------------------------------------------- 1087 1088 private static ConcurrentMap<TimeZoneDisplayKey, String> cTimeZoneDisplayCache = 1089 new ConcurrentHashMap<TimeZoneDisplayKey, String>(7); 1090 /** 1091 * <p>Gets the time zone display name, using a cache for performance.</p> 1092 * 1093 * @param tz the zone to query 1094 * @param daylight true if daylight savings 1095 * @param style the style to use {@code TimeZone.LONG} or {@code TimeZone.SHORT} 1096 * @param locale the locale to use 1097 * @return the textual name of the time zone 1098 */ 1099 static String getTimeZoneDisplay(final TimeZone tz, final boolean daylight, final int style, final Locale locale) { 1100 final TimeZoneDisplayKey key = new TimeZoneDisplayKey(tz, daylight, style, locale); 1101 String value = cTimeZoneDisplayCache.get(key); 1102 if (value == null) { 1103 // This is a very slow call, so cache the results. 1104 value = tz.getDisplayName(daylight, style, locale); 1105 final String prior = cTimeZoneDisplayCache.putIfAbsent(key, value); 1106 if (prior != null) { 1107 value= prior; 1108 } 1109 } 1110 return value; 1111 } 1112 1113 /** 1114 * <p>Inner class to output a time zone name.</p> 1115 */ 1116 private static class TimeZoneNameRule implements Rule { 1117 private final Locale mLocale; 1118 private final int mStyle; 1119 private final String mStandard; 1120 private final String mDaylight; 1121 1122 /** 1123 * Constructs an instance of {@code TimeZoneNameRule} with the specified properties. 1124 * 1125 * @param timeZone the time zone 1126 * @param locale the locale 1127 * @param style the style 1128 */ 1129 TimeZoneNameRule(final TimeZone timeZone, final Locale locale, final int style) { 1130 mLocale = locale; 1131 mStyle = style; 1132 1133 mStandard = getTimeZoneDisplay(timeZone, false, style, locale); 1134 mDaylight = getTimeZoneDisplay(timeZone, true, style, locale); 1135 } 1136 1137 /** 1138 * {@inheritDoc} 1139 */ 1140 @Override 1141 public int estimateLength() { 1142 // We have no access to the Calendar object that will be passed to 1143 // appendTo so base estimate on the TimeZone passed to the 1144 // constructor 1145 return Math.max(mStandard.length(), mDaylight.length()); 1146 } 1147 1148 /** 1149 * {@inheritDoc} 1150 */ 1151 @Override 1152 public void appendTo(final StringBuffer buffer, final Calendar calendar) { 1153 final TimeZone zone = calendar.getTimeZone(); 1154 if (zone.useDaylightTime() 1155 && calendar.get(Calendar.DST_OFFSET) != 0) { 1156 buffer.append(getTimeZoneDisplay(zone, true, mStyle, mLocale)); 1157 } else { 1158 buffer.append(getTimeZoneDisplay(zone, false, mStyle, mLocale)); 1159 } 1160 } 1161 } 1162 1163 /** 1164 * <p>Inner class to output a time zone as a number {@code +/-HHMM} 1165 * or {@code +/-HH:MM}.</p> 1166 */ 1167 private static class TimeZoneNumberRule implements Rule { 1168 static final TimeZoneNumberRule INSTANCE_COLON = new TimeZoneNumberRule(true); 1169 static final TimeZoneNumberRule INSTANCE_NO_COLON = new TimeZoneNumberRule(false); 1170 1171 final boolean mColon; 1172 1173 /** 1174 * Constructs an instance of {@code TimeZoneNumberRule} with the specified properties. 1175 * 1176 * @param colon add colon between HH and MM in the output if {@code true} 1177 */ 1178 TimeZoneNumberRule(final boolean colon) { 1179 mColon = colon; 1180 } 1181 1182 /** 1183 * {@inheritDoc} 1184 */ 1185 @Override 1186 public int estimateLength() { 1187 return 5; 1188 } 1189 1190 /** 1191 * {@inheritDoc} 1192 */ 1193 @Override 1194 public void appendTo(final StringBuffer buffer, final Calendar calendar) { 1195 int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); 1196 1197 if (offset < 0) { 1198 buffer.append('-'); 1199 offset = -offset; 1200 } else { 1201 buffer.append('+'); 1202 } 1203 1204 final int hours = offset / (60 * 60 * 1000); 1205 buffer.append((char)(hours / 10 + '0')); 1206 buffer.append((char)(hours % 10 + '0')); 1207 1208 if (mColon) { 1209 buffer.append(':'); 1210 } 1211 1212 final int minutes = offset / (60 * 1000) - 60 * hours; 1213 buffer.append((char)(minutes / 10 + '0')); 1214 buffer.append((char)(minutes % 10 + '0')); 1215 } 1216 } 1217 1218 // ---------------------------------------------------------------------- 1219 /** 1220 * <p>Inner class that acts as a compound key for time zone names.</p> 1221 */ 1222 private static class TimeZoneDisplayKey { 1223 private final TimeZone mTimeZone; 1224 private final int mStyle; 1225 private final Locale mLocale; 1226 1227 /** 1228 * Constructs an instance of {@code TimeZoneDisplayKey} with the specified properties. 1229 * 1230 * @param timeZone the time zone 1231 * @param daylight adjust the style for daylight saving time if {@code true} 1232 * @param style the timezone style 1233 * @param locale the timezone locale 1234 */ 1235 TimeZoneDisplayKey(final TimeZone timeZone, 1236 final boolean daylight, int style, final Locale locale) { 1237 mTimeZone = timeZone; 1238 if (daylight) { 1239 style |= 0x80000000; 1240 } 1241 mStyle = style; 1242 mLocale = locale; 1243 } 1244 1245 /** 1246 * {@inheritDoc} 1247 */ 1248 @Override 1249 public int hashCode() { 1250 return (mStyle * 31 + mLocale.hashCode() ) * 31 + mTimeZone.hashCode(); 1251 } 1252 1253 /** 1254 * {@inheritDoc} 1255 */ 1256 @Override 1257 public boolean equals(final Object obj) { 1258 if (this == obj) { 1259 return true; 1260 } 1261 if (obj instanceof TimeZoneDisplayKey) { 1262 final TimeZoneDisplayKey other = (TimeZoneDisplayKey)obj; 1263 return 1264 mTimeZone.equals(other.mTimeZone) && 1265 mStyle == other.mStyle && 1266 mLocale.equals(other.mLocale); 1267 } 1268 return false; 1269 } 1270 } 1271}