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