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