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[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 case 'Y': // week year 214 if (tokenLen == 2) { 215 rule = TwoDigitYearField.INSTANCE; 216 } else { 217 rule = selectNumberRule(Calendar.YEAR, tokenLen < 4 ? 4 : tokenLen); 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(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 (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 == false) { 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 hashcode compatible with equals.</p> 641 * 642 * @return a hashcode 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 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 */ 778 void appendTo(Appendable buf, Calendar calendar) throws IOException; 779 } 780 781 /** 782 * <p>Inner class defining a numeric rule.</p> 783 */ 784 private interface NumberRule extends Rule { 785 /** 786 * Appends the specified value to the output buffer based on the rule implementation. 787 * 788 * @param buffer the output buffer 789 * @param value the value to be appended 790 */ 791 void appendTo(Appendable buffer, int value) throws IOException; 792 } 793 794 /** 795 * <p>Inner class to output a constant single character.</p> 796 */ 797 private static class CharacterLiteral implements Rule { 798 private final char mValue; 799 800 /** 801 * Constructs a new instance of {@code CharacterLiteral} 802 * to hold the specified value. 803 * 804 * @param value the character literal 805 */ 806 CharacterLiteral(final char value) { 807 mValue = value; 808 } 809 810 /** 811 * {@inheritDoc} 812 */ 813 @Override 814 public int estimateLength() { 815 return 1; 816 } 817 818 /** 819 * {@inheritDoc} 820 */ 821 @Override 822 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 823 buffer.append(mValue); 824 } 825 } 826 827 /** 828 * <p>Inner class to output a constant string.</p> 829 */ 830 private static class StringLiteral implements Rule { 831 private final String mValue; 832 833 /** 834 * Constructs a new instance of {@code StringLiteral} 835 * to hold the specified value. 836 * 837 * @param value the string literal 838 */ 839 StringLiteral(final String value) { 840 mValue = value; 841 } 842 843 /** 844 * {@inheritDoc} 845 */ 846 @Override 847 public int estimateLength() { 848 return mValue.length(); 849 } 850 851 /** 852 * {@inheritDoc} 853 */ 854 @Override 855 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 856 buffer.append(mValue); 857 } 858 } 859 860 /** 861 * <p>Inner class to output one of a set of values.</p> 862 */ 863 private static class TextField implements Rule { 864 private final int mField; 865 private final String[] mValues; 866 867 /** 868 * Constructs an instance of {@code TextField} 869 * with the specified field and values. 870 * 871 * @param field the field 872 * @param values the field values 873 */ 874 TextField(final int field, final String[] values) { 875 mField = field; 876 mValues = values; 877 } 878 879 /** 880 * {@inheritDoc} 881 */ 882 @Override 883 public int estimateLength() { 884 int max = 0; 885 for (int i=mValues.length; --i >= 0; ) { 886 final int len = mValues[i].length(); 887 if (len > max) { 888 max = len; 889 } 890 } 891 return max; 892 } 893 894 /** 895 * {@inheritDoc} 896 */ 897 @Override 898 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 899 buffer.append(mValues[calendar.get(mField)]); 900 } 901 } 902 903 /** 904 * <p>Inner class to output an unpadded number.</p> 905 */ 906 private static class UnpaddedNumberField implements NumberRule { 907 private final int mField; 908 909 /** 910 * Constructs an instance of {@code UnpadedNumberField} with the specified field. 911 * 912 * @param field the field 913 */ 914 UnpaddedNumberField(final int field) { 915 mField = field; 916 } 917 918 /** 919 * {@inheritDoc} 920 */ 921 @Override 922 public int estimateLength() { 923 return 4; 924 } 925 926 /** 927 * {@inheritDoc} 928 */ 929 @Override 930 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 931 appendTo(buffer, calendar.get(mField)); 932 } 933 934 /** 935 * {@inheritDoc} 936 */ 937 @Override 938 public final void appendTo(final Appendable buffer, final int value) throws IOException { 939 if (value < 10) { 940 buffer.append((char)(value + '0')); 941 } else if (value < 100) { 942 appendDigits(buffer, value); 943 } else { 944 appendFullDigits(buffer, value, 1); 945 } 946 } 947 } 948 949 /** 950 * <p>Inner class to output an unpadded month.</p> 951 */ 952 private static class UnpaddedMonthField implements NumberRule { 953 static final UnpaddedMonthField INSTANCE = new UnpaddedMonthField(); 954 955 /** 956 * Constructs an instance of {@code UnpaddedMonthField}. 957 * 958 */ 959 UnpaddedMonthField() { 960 super(); 961 } 962 963 /** 964 * {@inheritDoc} 965 */ 966 @Override 967 public int estimateLength() { 968 return 2; 969 } 970 971 /** 972 * {@inheritDoc} 973 */ 974 @Override 975 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 976 appendTo(buffer, calendar.get(Calendar.MONTH) + 1); 977 } 978 979 /** 980 * {@inheritDoc} 981 */ 982 @Override 983 public final void appendTo(final Appendable buffer, final int value) throws IOException { 984 if (value < 10) { 985 buffer.append((char)(value + '0')); 986 } else { 987 appendDigits(buffer, value); 988 } 989 } 990 } 991 992 /** 993 * <p>Inner class to output a padded number.</p> 994 */ 995 private static class PaddedNumberField implements NumberRule { 996 private final int mField; 997 private final int mSize; 998 999 /** 1000 * Constructs an instance of {@code PaddedNumberField}. 1001 * 1002 * @param field the field 1003 * @param size size of the output field 1004 */ 1005 PaddedNumberField(final int field, final int size) { 1006 if (size < 3) { 1007 // Should use UnpaddedNumberField or TwoDigitNumberField. 1008 throw new IllegalArgumentException(); 1009 } 1010 mField = field; 1011 mSize = size; 1012 } 1013 1014 /** 1015 * {@inheritDoc} 1016 */ 1017 @Override 1018 public int estimateLength() { 1019 return mSize; 1020 } 1021 1022 /** 1023 * {@inheritDoc} 1024 */ 1025 @Override 1026 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1027 appendTo(buffer, calendar.get(mField)); 1028 } 1029 1030 /** 1031 * {@inheritDoc} 1032 */ 1033 @Override 1034 public final void appendTo(final Appendable buffer, int value) throws IOException { 1035 appendFullDigits(buffer, value, mSize); 1036 } 1037 } 1038 1039 /** 1040 * <p>Inner class to output a two digit number.</p> 1041 */ 1042 private static class TwoDigitNumberField implements NumberRule { 1043 private final int mField; 1044 1045 /** 1046 * Constructs an instance of {@code TwoDigitNumberField} with the specified field. 1047 * 1048 * @param field the field 1049 */ 1050 TwoDigitNumberField(final int field) { 1051 mField = field; 1052 } 1053 1054 /** 1055 * {@inheritDoc} 1056 */ 1057 @Override 1058 public int estimateLength() { 1059 return 2; 1060 } 1061 1062 /** 1063 * {@inheritDoc} 1064 */ 1065 @Override 1066 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1067 appendTo(buffer, calendar.get(mField)); 1068 } 1069 1070 /** 1071 * {@inheritDoc} 1072 */ 1073 @Override 1074 public final void appendTo(final Appendable buffer, final int value) throws IOException { 1075 if (value < 100) { 1076 appendDigits(buffer, value); 1077 } else { 1078 appendFullDigits(buffer, value, 2); 1079 } 1080 } 1081 } 1082 1083 /** 1084 * <p>Inner class to output a two digit year.</p> 1085 */ 1086 private static class TwoDigitYearField implements NumberRule { 1087 static final TwoDigitYearField INSTANCE = new TwoDigitYearField(); 1088 1089 /** 1090 * Constructs an instance of {@code TwoDigitYearField}. 1091 */ 1092 TwoDigitYearField() { 1093 super(); 1094 } 1095 1096 /** 1097 * {@inheritDoc} 1098 */ 1099 @Override 1100 public int estimateLength() { 1101 return 2; 1102 } 1103 1104 /** 1105 * {@inheritDoc} 1106 */ 1107 @Override 1108 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1109 appendTo(buffer, calendar.get(Calendar.YEAR) % 100); 1110 } 1111 1112 /** 1113 * {@inheritDoc} 1114 */ 1115 @Override 1116 public final void appendTo(final Appendable buffer, final int value) throws IOException { 1117 appendDigits(buffer, value); 1118 } 1119 } 1120 1121 /** 1122 * <p>Inner class to output a two digit month.</p> 1123 */ 1124 private static class TwoDigitMonthField implements NumberRule { 1125 static final TwoDigitMonthField INSTANCE = new TwoDigitMonthField(); 1126 1127 /** 1128 * Constructs an instance of {@code TwoDigitMonthField}. 1129 */ 1130 TwoDigitMonthField() { 1131 super(); 1132 } 1133 1134 /** 1135 * {@inheritDoc} 1136 */ 1137 @Override 1138 public int estimateLength() { 1139 return 2; 1140 } 1141 1142 /** 1143 * {@inheritDoc} 1144 */ 1145 @Override 1146 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1147 appendTo(buffer, calendar.get(Calendar.MONTH) + 1); 1148 } 1149 1150 /** 1151 * {@inheritDoc} 1152 */ 1153 @Override 1154 public final void appendTo(final Appendable buffer, final int value) throws IOException { 1155 appendDigits(buffer, value); 1156 } 1157 } 1158 1159 /** 1160 * <p>Inner class to output the twelve hour field.</p> 1161 */ 1162 private static class TwelveHourField implements NumberRule { 1163 private final NumberRule mRule; 1164 1165 /** 1166 * Constructs an instance of {@code TwelveHourField} with the specified 1167 * {@code NumberRule}. 1168 * 1169 * @param rule the rule 1170 */ 1171 TwelveHourField(final NumberRule rule) { 1172 mRule = rule; 1173 } 1174 1175 /** 1176 * {@inheritDoc} 1177 */ 1178 @Override 1179 public int estimateLength() { 1180 return mRule.estimateLength(); 1181 } 1182 1183 /** 1184 * {@inheritDoc} 1185 */ 1186 @Override 1187 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1188 int value = calendar.get(Calendar.HOUR); 1189 if (value == 0) { 1190 value = calendar.getLeastMaximum(Calendar.HOUR) + 1; 1191 } 1192 mRule.appendTo(buffer, value); 1193 } 1194 1195 /** 1196 * {@inheritDoc} 1197 */ 1198 @Override 1199 public void appendTo(final Appendable buffer, final int value) throws IOException { 1200 mRule.appendTo(buffer, value); 1201 } 1202 } 1203 1204 /** 1205 * <p>Inner class to output the twenty four hour field.</p> 1206 */ 1207 private static class TwentyFourHourField implements NumberRule { 1208 private final NumberRule mRule; 1209 1210 /** 1211 * Constructs an instance of {@code TwentyFourHourField} with the specified 1212 * {@code NumberRule}. 1213 * 1214 * @param rule the rule 1215 */ 1216 TwentyFourHourField(final NumberRule rule) { 1217 mRule = rule; 1218 } 1219 1220 /** 1221 * {@inheritDoc} 1222 */ 1223 @Override 1224 public int estimateLength() { 1225 return mRule.estimateLength(); 1226 } 1227 1228 /** 1229 * {@inheritDoc} 1230 */ 1231 @Override 1232 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1233 int value = calendar.get(Calendar.HOUR_OF_DAY); 1234 if (value == 0) { 1235 value = calendar.getMaximum(Calendar.HOUR_OF_DAY) + 1; 1236 } 1237 mRule.appendTo(buffer, value); 1238 } 1239 1240 /** 1241 * {@inheritDoc} 1242 */ 1243 @Override 1244 public void appendTo(final Appendable buffer, final int value) throws IOException { 1245 mRule.appendTo(buffer, value); 1246 } 1247 } 1248 1249 /** 1250 * <p>Inner class to output the numeric day in week.</p> 1251 */ 1252 private static class DayInWeekField implements NumberRule { 1253 private final NumberRule mRule; 1254 1255 DayInWeekField(final NumberRule rule) { 1256 mRule = rule; 1257 } 1258 1259 @Override 1260 public int estimateLength() { 1261 return mRule.estimateLength(); 1262 } 1263 1264 @Override 1265 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1266 int value = calendar.get(Calendar.DAY_OF_WEEK); 1267 mRule.appendTo(buffer, value != Calendar.SUNDAY ? value - 1 : 7); 1268 } 1269 1270 @Override 1271 public void appendTo(final Appendable buffer, final int value) throws IOException { 1272 mRule.appendTo(buffer, value); 1273 } 1274 } 1275 1276 /** 1277 * <p>Inner class to output the numeric day in week.</p> 1278 */ 1279 private static class WeekYear implements NumberRule { 1280 private final NumberRule mRule; 1281 1282 WeekYear(final NumberRule rule) { 1283 mRule = rule; 1284 } 1285 1286 @Override 1287 public int estimateLength() { 1288 return mRule.estimateLength(); 1289 } 1290 1291 @Override 1292 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1293 mRule.appendTo(buffer, CalendarReflection.getWeekYear(calendar)); 1294 } 1295 1296 @Override 1297 public void appendTo(final Appendable buffer, final int value) throws IOException { 1298 mRule.appendTo(buffer, value); 1299 } 1300 } 1301 1302 //----------------------------------------------------------------------- 1303 1304 private static final ConcurrentMap<TimeZoneDisplayKey, String> cTimeZoneDisplayCache = 1305 new ConcurrentHashMap<TimeZoneDisplayKey, String>(7); 1306 /** 1307 * <p>Gets the time zone display name, using a cache for performance.</p> 1308 * 1309 * @param tz the zone to query 1310 * @param daylight true if daylight savings 1311 * @param style the style to use {@code TimeZone.LONG} or {@code TimeZone.SHORT} 1312 * @param locale the locale to use 1313 * @return the textual name of the time zone 1314 */ 1315 static String getTimeZoneDisplay(final TimeZone tz, final boolean daylight, final int style, final Locale locale) { 1316 final TimeZoneDisplayKey key = new TimeZoneDisplayKey(tz, daylight, style, locale); 1317 String value = cTimeZoneDisplayCache.get(key); 1318 if (value == null) { 1319 // This is a very slow call, so cache the results. 1320 value = tz.getDisplayName(daylight, style, locale); 1321 final String prior = cTimeZoneDisplayCache.putIfAbsent(key, value); 1322 if (prior != null) { 1323 value= prior; 1324 } 1325 } 1326 return value; 1327 } 1328 1329 /** 1330 * <p>Inner class to output a time zone name.</p> 1331 */ 1332 private static class TimeZoneNameRule implements Rule { 1333 private final Locale mLocale; 1334 private final int mStyle; 1335 private final String mStandard; 1336 private final String mDaylight; 1337 1338 /** 1339 * Constructs an instance of {@code TimeZoneNameRule} with the specified properties. 1340 * 1341 * @param timeZone the time zone 1342 * @param locale the locale 1343 * @param style the style 1344 */ 1345 TimeZoneNameRule(final TimeZone timeZone, final Locale locale, final int style) { 1346 mLocale = locale; 1347 mStyle = style; 1348 1349 mStandard = getTimeZoneDisplay(timeZone, false, style, locale); 1350 mDaylight = getTimeZoneDisplay(timeZone, true, style, locale); 1351 } 1352 1353 /** 1354 * {@inheritDoc} 1355 */ 1356 @Override 1357 public int estimateLength() { 1358 // We have no access to the Calendar object that will be passed to 1359 // appendTo so base estimate on the TimeZone passed to the 1360 // constructor 1361 return Math.max(mStandard.length(), mDaylight.length()); 1362 } 1363 1364 /** 1365 * {@inheritDoc} 1366 */ 1367 @Override 1368 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1369 final TimeZone zone = calendar.getTimeZone(); 1370 if (calendar.get(Calendar.DST_OFFSET) != 0) { 1371 buffer.append(getTimeZoneDisplay(zone, true, mStyle, mLocale)); 1372 } else { 1373 buffer.append(getTimeZoneDisplay(zone, false, mStyle, mLocale)); 1374 } 1375 } 1376 } 1377 1378 /** 1379 * <p>Inner class to output a time zone as a number {@code +/-HHMM} 1380 * or {@code +/-HH:MM}.</p> 1381 */ 1382 private static class TimeZoneNumberRule implements Rule { 1383 static final TimeZoneNumberRule INSTANCE_COLON = new TimeZoneNumberRule(true); 1384 static final TimeZoneNumberRule INSTANCE_NO_COLON = new TimeZoneNumberRule(false); 1385 1386 final boolean mColon; 1387 1388 /** 1389 * Constructs an instance of {@code TimeZoneNumberRule} with the specified properties. 1390 * 1391 * @param colon add colon between HH and MM in the output if {@code true} 1392 */ 1393 TimeZoneNumberRule(final boolean colon) { 1394 mColon = colon; 1395 } 1396 1397 /** 1398 * {@inheritDoc} 1399 */ 1400 @Override 1401 public int estimateLength() { 1402 return 5; 1403 } 1404 1405 /** 1406 * {@inheritDoc} 1407 */ 1408 @Override 1409 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1410 1411 int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); 1412 1413 if (offset < 0) { 1414 buffer.append('-'); 1415 offset = -offset; 1416 } else { 1417 buffer.append('+'); 1418 } 1419 1420 final int hours = offset / (60 * 60 * 1000); 1421 appendDigits(buffer, hours); 1422 1423 if (mColon) { 1424 buffer.append(':'); 1425 } 1426 1427 final int minutes = offset / (60 * 1000) - 60 * hours; 1428 appendDigits(buffer, minutes); 1429 } 1430 } 1431 1432 /** 1433 * <p>Inner class to output a time zone as a number {@code +/-HHMM} 1434 * or {@code +/-HH:MM}.</p> 1435 */ 1436 private static class Iso8601_Rule implements Rule { 1437 1438 // Sign TwoDigitHours or Z 1439 static final Iso8601_Rule ISO8601_HOURS = new Iso8601_Rule(3); 1440 // Sign TwoDigitHours Minutes or Z 1441 static final Iso8601_Rule ISO8601_HOURS_MINUTES = new Iso8601_Rule(5); 1442 // Sign TwoDigitHours : Minutes or Z 1443 static final Iso8601_Rule ISO8601_HOURS_COLON_MINUTES = new Iso8601_Rule(6); 1444 1445 /** 1446 * Factory method for Iso8601_Rules. 1447 * 1448 * @param tokenLen a token indicating the length of the TimeZone String to be formatted. 1449 * @return a Iso8601_Rule that can format TimeZone String of length {@code tokenLen}. If no such 1450 * rule exists, an IllegalArgumentException will be thrown. 1451 */ 1452 static Iso8601_Rule getRule(int tokenLen) { 1453 switch(tokenLen) { 1454 case 1: 1455 return Iso8601_Rule.ISO8601_HOURS; 1456 case 2: 1457 return Iso8601_Rule.ISO8601_HOURS_MINUTES; 1458 case 3: 1459 return Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES; 1460 default: 1461 throw new IllegalArgumentException("invalid number of X"); 1462 } 1463 } 1464 1465 final int length; 1466 1467 /** 1468 * Constructs an instance of {@code Iso8601_Rule} with the specified properties. 1469 * 1470 * @param length The number of characters in output (unless Z is output) 1471 */ 1472 Iso8601_Rule(final int length) { 1473 this.length = length; 1474 } 1475 1476 /** 1477 * {@inheritDoc} 1478 */ 1479 @Override 1480 public int estimateLength() { 1481 return length; 1482 } 1483 1484 /** 1485 * {@inheritDoc} 1486 */ 1487 @Override 1488 public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { 1489 int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); 1490 if (offset == 0) { 1491 buffer.append("Z"); 1492 return; 1493 } 1494 1495 if (offset < 0) { 1496 buffer.append('-'); 1497 offset = -offset; 1498 } else { 1499 buffer.append('+'); 1500 } 1501 1502 final int hours = offset / (60 * 60 * 1000); 1503 appendDigits(buffer, hours); 1504 1505 if (length<5) { 1506 return; 1507 } 1508 1509 if (length==6) { 1510 buffer.append(':'); 1511 } 1512 1513 final int minutes = offset / (60 * 1000) - 60 * hours; 1514 appendDigits(buffer, minutes); 1515 } 1516 } 1517 1518 // ---------------------------------------------------------------------- 1519 /** 1520 * <p>Inner class that acts as a compound key for time zone names.</p> 1521 */ 1522 private static class TimeZoneDisplayKey { 1523 private final TimeZone mTimeZone; 1524 private final int mStyle; 1525 private final Locale mLocale; 1526 1527 /** 1528 * Constructs an instance of {@code TimeZoneDisplayKey} with the specified properties. 1529 * 1530 * @param timeZone the time zone 1531 * @param daylight adjust the style for daylight saving time if {@code true} 1532 * @param style the timezone style 1533 * @param locale the timezone locale 1534 */ 1535 TimeZoneDisplayKey(final TimeZone timeZone, 1536 final boolean daylight, final int style, final Locale locale) { 1537 mTimeZone = timeZone; 1538 if (daylight) { 1539 mStyle = style | 0x80000000; 1540 } else { 1541 mStyle = style; 1542 } 1543 mLocale = locale; 1544 } 1545 1546 /** 1547 * {@inheritDoc} 1548 */ 1549 @Override 1550 public int hashCode() { 1551 return (mStyle * 31 + mLocale.hashCode() ) * 31 + mTimeZone.hashCode(); 1552 } 1553 1554 /** 1555 * {@inheritDoc} 1556 */ 1557 @Override 1558 public boolean equals(final Object obj) { 1559 if (this == obj) { 1560 return true; 1561 } 1562 if (obj instanceof TimeZoneDisplayKey) { 1563 final TimeZoneDisplayKey other = (TimeZoneDisplayKey)obj; 1564 return 1565 mTimeZone.equals(other.mTimeZone) && 1566 mStyle == other.mStyle && 1567 mLocale.equals(other.mLocale); 1568 } 1569 return false; 1570 } 1571 } 1572}