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