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