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 * https://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> timeZoneDisplayCache = new ConcurrentHashMap<>(7); 903 904 /** 905 * Appends two digits to the given buffer. 906 * 907 * @param buffer the buffer to append to. 908 * @param value the value to append digits from. 909 * @throws IOException If an I/O error occurs 910 */ 911 private static void appendDigits(final Appendable buffer, final int value) throws IOException { 912 buffer.append((char) (value / 10 + '0')); 913 buffer.append((char) (value % 10 + '0')); 914 } 915 916 /** 917 * Appends all digits to the given buffer. 918 * 919 * @param buffer the buffer to append to. 920 * @param value the value to append digits from. 921 * @param minFieldWidth Minimum field width. 922 * @throws IOException If an I/O error occurs 923 */ 924 private static void appendFullDigits(final Appendable buffer, int value, int minFieldWidth) throws IOException { 925 // specialized paths for 1 to 4 digits -> avoid the memory allocation from the temporary work array 926 // see LANG-1248 927 if (value < 10000) { 928 // less memory allocation path works for four digits or less 929 930 int nDigits = 4; 931 if (value < 1000) { 932 --nDigits; 933 if (value < 100) { 934 --nDigits; 935 if (value < 10) { 936 --nDigits; 937 } 938 } 939 } 940 // left zero pad 941 for (int i = minFieldWidth - nDigits; i > 0; --i) { 942 buffer.append('0'); 943 } 944 945 switch (nDigits) { 946 case 4: 947 buffer.append((char) (value / 1000 + '0')); 948 value %= 1000; 949 // falls-through 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 // falls-through 958 case 2: 959 if (value >= 10) { 960 buffer.append((char) (value / 10 + '0')); 961 value %= 10; 962 } else { 963 buffer.append('0'); 964 } 965 // falls-through 966 case 1: 967 buffer.append((char) (value + '0')); 968 } 969 } else { 970 // more memory allocation path works for any digits 971 972 // build up decimal representation in reverse 973 final char[] work = new char[MAX_DIGITS]; 974 int digit = 0; 975 while (value != 0) { 976 work[digit++] = (char) (value % 10 + '0'); 977 value /= 10; 978 } 979 980 // pad with zeros 981 while (digit < minFieldWidth) { 982 buffer.append('0'); 983 --minFieldWidth; 984 } 985 986 // reverse 987 while (--digit >= 0) { 988 buffer.append(work[digit]); 989 } 990 } 991 } 992 993 static void clear() { 994 timeZoneDisplayCache.clear(); 995 } 996 997 /** 998 * Gets the time zone display name, using a cache for performance. 999 * 1000 * @param tz the zone to query 1001 * @param daylight true if daylight savings 1002 * @param style the style to use {@code TimeZone.LONG} or {@code TimeZone.SHORT} 1003 * @param locale the locale to use 1004 * @return the textual name of the time zone 1005 */ 1006 static String getTimeZoneDisplay(final TimeZone tz, final boolean daylight, final int style, final Locale locale) { 1007 final TimeZoneDisplayKey key = new TimeZoneDisplayKey(tz, daylight, style, locale); 1008 // This is a very slow call, so cache the results. 1009 return timeZoneDisplayCache.computeIfAbsent(key, k -> tz.getDisplayName(daylight, style, locale)); 1010 } 1011 1012 /** 1013 * The pattern. 1014 */ 1015 private final String pattern; 1016 1017 /** 1018 * The time zone. 1019 */ 1020 private final TimeZone timeZone; 1021 1022 /** 1023 * The locale. 1024 */ 1025 private final Locale locale; 1026 1027 /** 1028 * The parsed rules. 1029 */ 1030 private transient Rule[] rules; 1031 1032 /** 1033 * The estimated maximum length. 1034 */ 1035 private transient int maxLengthEstimate; 1036 1037 // Constructor 1038 /** 1039 * Constructs a new FastDatePrinter. 1040 * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the 1041 * factory methods of {@link FastDateFormat} to get a cached FastDatePrinter instance. 1042 * 1043 * @param pattern {@link java.text.SimpleDateFormat} compatible pattern 1044 * @param timeZone non-null time zone to use 1045 * @param locale non-null locale to use 1046 * @throws NullPointerException if pattern, timeZone, or locale is null. 1047 */ 1048 protected FastDatePrinter(final String pattern, final TimeZone timeZone, final Locale locale) { 1049 this.pattern = pattern; 1050 this.timeZone = timeZone; 1051 this.locale = LocaleUtils.toLocale(locale); 1052 init(); 1053 } 1054 1055 /** 1056 * Performs the formatting by applying the rules to the 1057 * specified calendar. 1058 * 1059 * @param calendar the calendar to format 1060 * @param buf the buffer to format into 1061 * @param <B> the Appendable class type, usually StringBuilder or StringBuffer. 1062 * @return the specified string buffer 1063 */ 1064 private <B extends Appendable> B applyRules(final Calendar calendar, final B buf) { 1065 try { 1066 for (final Rule rule : rules) { 1067 rule.appendTo(buf, calendar); 1068 } 1069 } catch (final IOException ioe) { 1070 ExceptionUtils.asRuntimeException(ioe); 1071 } 1072 return buf; 1073 } 1074 1075 /** 1076 * Performs the formatting by applying the rules to the 1077 * specified calendar. 1078 * 1079 * @param calendar the calendar to format 1080 * @param buf the buffer to format into 1081 * @return the specified string buffer 1082 * @deprecated use {@link #format(Calendar)} or {@link #format(Calendar, Appendable)} 1083 */ 1084 @Deprecated 1085 protected StringBuffer applyRules(final Calendar calendar, final StringBuffer buf) { 1086 return (StringBuffer) applyRules(calendar, (Appendable) buf); 1087 } 1088 1089 /** 1090 * Creates a String representation of the given Calendar by applying the rules of this printer to it. 1091 * @param c the Calendar to apply the rules to. 1092 * @return a String representation of the given Calendar. 1093 */ 1094 private String applyRulesToString(final Calendar c) { 1095 return applyRules(c, new StringBuilder(maxLengthEstimate)).toString(); 1096 } 1097 1098 // Basics 1099 /** 1100 * Compares two objects for equality. 1101 * 1102 * @param obj the object to compare to 1103 * @return {@code true} if equal 1104 */ 1105 @Override 1106 public boolean equals(final Object obj) { 1107 if (!(obj instanceof FastDatePrinter)) { 1108 return false; 1109 } 1110 final FastDatePrinter other = (FastDatePrinter) obj; 1111 return pattern.equals(other.pattern) 1112 && timeZone.equals(other.timeZone) 1113 && locale.equals(other.locale); 1114 } 1115 1116 /* (non-Javadoc) 1117 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar) 1118 */ 1119 @Override 1120 public String format(final Calendar calendar) { 1121 return format(calendar, new StringBuilder(maxLengthEstimate)).toString(); 1122 } 1123 1124 /* (non-Javadoc) 1125 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar, Appendable) 1126 */ 1127 @Override 1128 public <B extends Appendable> B format(Calendar calendar, final B buf) { 1129 // do not pass in calendar directly, this will cause TimeZone of FastDatePrinter to be ignored 1130 if (!calendar.getTimeZone().equals(timeZone)) { 1131 calendar = (Calendar) calendar.clone(); 1132 calendar.setTimeZone(timeZone); 1133 } 1134 return applyRules(calendar, buf); 1135 } 1136 1137 /* (non-Javadoc) 1138 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar, StringBuffer) 1139 */ 1140 @Override 1141 public StringBuffer format(final Calendar calendar, final StringBuffer buf) { 1142 // do not pass in calendar directly, this will cause TimeZone of FastDatePrinter to be ignored 1143 return format(calendar.getTime(), buf); 1144 } 1145 1146 /* (non-Javadoc) 1147 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date) 1148 */ 1149 @Override 1150 public String format(final Date date) { 1151 final Calendar c = newCalendar(); 1152 c.setTime(date); 1153 return applyRulesToString(c); 1154 } 1155 1156 /* (non-Javadoc) 1157 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date, Appendable) 1158 */ 1159 @Override 1160 public <B extends Appendable> B format(final Date date, final B buf) { 1161 final Calendar c = newCalendar(); 1162 c.setTime(date); 1163 return applyRules(c, buf); 1164 } 1165 1166 /* (non-Javadoc) 1167 * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date, StringBuffer) 1168 */ 1169 @Override 1170 public StringBuffer format(final Date date, final StringBuffer buf) { 1171 final Calendar c = newCalendar(); 1172 c.setTime(date); 1173 return (StringBuffer) applyRules(c, (Appendable) buf); 1174 } 1175 1176 /* (non-Javadoc) 1177 * @see org.apache.commons.lang3.time.DatePrinter#format(long) 1178 */ 1179 @Override 1180 public String format(final long millis) { 1181 final Calendar c = newCalendar(); 1182 c.setTimeInMillis(millis); 1183 return applyRulesToString(c); 1184 } 1185 1186 /* (non-Javadoc) 1187 * @see org.apache.commons.lang3.time.DatePrinter#format(long, Appendable) 1188 */ 1189 @Override 1190 public <B extends Appendable> B format(final long millis, final B buf) { 1191 final Calendar c = newCalendar(); 1192 c.setTimeInMillis(millis); 1193 return applyRules(c, buf); 1194 } 1195 1196 /* (non-Javadoc) 1197 * @see org.apache.commons.lang3.time.DatePrinter#format(long, StringBuffer) 1198 */ 1199 @Override 1200 public StringBuffer format(final long millis, final StringBuffer buf) { 1201 final Calendar c = newCalendar(); 1202 c.setTimeInMillis(millis); 1203 return (StringBuffer) applyRules(c, (Appendable) buf); 1204 } 1205 1206 /** 1207 * Formats a {@link Date}, {@link Calendar} or 1208 * {@link Long} (milliseconds) object. 1209 * @param obj the object to format 1210 * @return The formatted value. 1211 * @since 3.5 1212 */ 1213 String format(final Object obj) { 1214 if (obj instanceof Date) { 1215 return format((Date) obj); 1216 } 1217 if (obj instanceof Calendar) { 1218 return format((Calendar) obj); 1219 } 1220 if (obj instanceof Long) { 1221 return format(((Long) obj).longValue()); 1222 } 1223 throw new IllegalArgumentException("Unknown class: " + ClassUtils.getName(obj, "<null>")); 1224 } 1225 1226 // Format methods 1227 /** 1228 * Formats a {@link Date}, {@link Calendar} or 1229 * {@link Long} (milliseconds) object. 1230 * @deprecated Use {{@link #format(Date)}, {{@link #format(Calendar)}, {{@link #format(long)}. 1231 * @param obj the object to format 1232 * @param toAppendTo the buffer to append to 1233 * @param pos the position - ignored 1234 * @return the buffer passed in 1235 */ 1236 @Deprecated 1237 @Override 1238 public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) { 1239 if (obj instanceof Date) { 1240 return format((Date) obj, toAppendTo); 1241 } 1242 if (obj instanceof Calendar) { 1243 return format((Calendar) obj, toAppendTo); 1244 } 1245 if (obj instanceof Long) { 1246 return format(((Long) obj).longValue(), toAppendTo); 1247 } 1248 throw new IllegalArgumentException("Unknown class: " + ClassUtils.getName(obj, "<null>")); 1249 } 1250 1251 /* (non-Javadoc) 1252 * @see org.apache.commons.lang3.time.DatePrinter#getLocale() 1253 */ 1254 @Override 1255 public Locale getLocale() { 1256 return locale; 1257 } 1258 1259 /** 1260 * Gets an estimate for the maximum string length that the 1261 * formatter will produce. 1262 * 1263 * <p>The actual formatted length will almost always be less than or 1264 * equal to this amount.</p> 1265 * 1266 * @return the maximum formatted length 1267 */ 1268 public int getMaxLengthEstimate() { 1269 return maxLengthEstimate; 1270 } 1271 1272 // Accessors 1273 /* (non-Javadoc) 1274 * @see org.apache.commons.lang3.time.DatePrinter#getPattern() 1275 */ 1276 @Override 1277 public String getPattern() { 1278 return pattern; 1279 } 1280 1281 /* (non-Javadoc) 1282 * @see org.apache.commons.lang3.time.DatePrinter#getTimeZone() 1283 */ 1284 @Override 1285 public TimeZone getTimeZone() { 1286 return timeZone; 1287 } 1288 1289 /** 1290 * Returns a hash code compatible with equals. 1291 * 1292 * @return a hash code compatible with equals 1293 */ 1294 @Override 1295 public int hashCode() { 1296 return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode()); 1297 } 1298 1299 /** 1300 * Initializes the instance for first use. 1301 */ 1302 private void init() { 1303 final List<Rule> rulesList = parsePattern(); 1304 rules = rulesList.toArray(EMPTY_RULE_ARRAY); 1305 1306 int len = 0; 1307 for (int i = rules.length; --i >= 0;) { 1308 len += rules[i].estimateLength(); 1309 } 1310 1311 maxLengthEstimate = len; 1312 } 1313 1314 /** 1315 * Creates a new Calendar instance. 1316 * @return a new Calendar instance. 1317 */ 1318 private Calendar newCalendar() { 1319 return Calendar.getInstance(timeZone, locale); 1320 } 1321 1322 // Parse the pattern 1323 /** 1324 * Returns a list of Rules given a pattern. 1325 * 1326 * @return a {@link List} of Rule objects 1327 * @throws IllegalArgumentException if pattern is invalid 1328 */ 1329 protected List<Rule> parsePattern() { 1330 final DateFormatSymbols symbols = new DateFormatSymbols(locale); 1331 final List<Rule> rules = new ArrayList<>(); 1332 1333 final String[] ERAs = symbols.getEras(); 1334 final String[] months = symbols.getMonths(); 1335 final String[] shortMonths = symbols.getShortMonths(); 1336 final String[] weekdays = symbols.getWeekdays(); 1337 final String[] shortWeekdays = symbols.getShortWeekdays(); 1338 final String[] AmPmStrings = symbols.getAmPmStrings(); 1339 1340 final int length = pattern.length(); 1341 final int[] indexRef = new int[1]; 1342 1343 for (int i = 0; i < length; i++) { 1344 indexRef[0] = i; 1345 final String token = parseToken(pattern, indexRef); 1346 i = indexRef[0]; 1347 1348 final int tokenLen = token.length(); 1349 if (tokenLen == 0) { 1350 break; 1351 } 1352 1353 Rule rule; 1354 final char c = token.charAt(0); 1355 1356 switch (c) { 1357 case 'G': // era designator (text) 1358 rule = new TextField(Calendar.ERA, ERAs); 1359 break; 1360 case 'y': // year (number) 1361 case 'Y': // week year 1362 if (tokenLen == 2) { 1363 rule = TwoDigitYearField.INSTANCE; 1364 } else { 1365 rule = selectNumberRule(Calendar.YEAR, Math.max(tokenLen, 4)); 1366 } 1367 if (c == 'Y') { 1368 rule = new WeekYear((NumberRule) rule); 1369 } 1370 break; 1371 case 'M': // month in year (text and number) 1372 if (tokenLen >= 4) { 1373 rule = new TextField(Calendar.MONTH, months); 1374 } else if (tokenLen == 3) { 1375 rule = new TextField(Calendar.MONTH, shortMonths); 1376 } else if (tokenLen == 2) { 1377 rule = TwoDigitMonthField.INSTANCE; 1378 } else { 1379 rule = UnpaddedMonthField.INSTANCE; 1380 } 1381 break; 1382 case 'L': // month in year (text and number) 1383 if (tokenLen >= 4) { 1384 rule = new TextField(Calendar.MONTH, CalendarUtils.getInstance(locale).getStandaloneLongMonthNames()); 1385 } else if (tokenLen == 3) { 1386 rule = new TextField(Calendar.MONTH, CalendarUtils.getInstance(locale).getStandaloneShortMonthNames()); 1387 } else if (tokenLen == 2) { 1388 rule = TwoDigitMonthField.INSTANCE; 1389 } else { 1390 rule = UnpaddedMonthField.INSTANCE; 1391 } 1392 break; 1393 case 'd': // day in month (number) 1394 rule = selectNumberRule(Calendar.DAY_OF_MONTH, tokenLen); 1395 break; 1396 case 'h': // hour in am/pm (number, 1..12) 1397 rule = new TwelveHourField(selectNumberRule(Calendar.HOUR, tokenLen)); 1398 break; 1399 case 'H': // hour in day (number, 0..23) 1400 rule = selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen); 1401 break; 1402 case 'm': // minute in hour (number) 1403 rule = selectNumberRule(Calendar.MINUTE, tokenLen); 1404 break; 1405 case 's': // second in minute (number) 1406 rule = selectNumberRule(Calendar.SECOND, tokenLen); 1407 break; 1408 case 'S': // millisecond (number) 1409 rule = selectNumberRule(Calendar.MILLISECOND, tokenLen); 1410 break; 1411 case 'E': // day in week (text) 1412 rule = new TextField(Calendar.DAY_OF_WEEK, tokenLen < 4 ? shortWeekdays : weekdays); 1413 break; 1414 case 'u': // day in week (number) 1415 rule = new DayInWeekField(selectNumberRule(Calendar.DAY_OF_WEEK, tokenLen)); 1416 break; 1417 case 'D': // day in year (number) 1418 rule = selectNumberRule(Calendar.DAY_OF_YEAR, tokenLen); 1419 break; 1420 case 'F': // day of week in month (number) 1421 rule = selectNumberRule(Calendar.DAY_OF_WEEK_IN_MONTH, tokenLen); 1422 break; 1423 case 'w': // week in year (number) 1424 rule = selectNumberRule(Calendar.WEEK_OF_YEAR, tokenLen); 1425 break; 1426 case 'W': // week in month (number) 1427 rule = selectNumberRule(Calendar.WEEK_OF_MONTH, tokenLen); 1428 break; 1429 case 'a': // am/pm marker (text) 1430 rule = new TextField(Calendar.AM_PM, AmPmStrings); 1431 break; 1432 case 'k': // hour in day (1..24) 1433 rule = new TwentyFourHourField(selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen)); 1434 break; 1435 case 'K': // hour in am/pm (0..11) 1436 rule = selectNumberRule(Calendar.HOUR, tokenLen); 1437 break; 1438 case 'X': // ISO 8601 1439 rule = Iso8601_Rule.getRule(tokenLen); 1440 break; 1441 case 'z': // time zone (text) 1442 if (tokenLen >= 4) { 1443 rule = new TimeZoneNameRule(timeZone, locale, TimeZone.LONG); 1444 } else { 1445 rule = new TimeZoneNameRule(timeZone, locale, TimeZone.SHORT); 1446 } 1447 break; 1448 case 'Z': // time zone (value) 1449 if (tokenLen == 1) { 1450 rule = TimeZoneNumberRule.INSTANCE_NO_COLON; 1451 } else if (tokenLen == 2) { 1452 rule = Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES; 1453 } else { 1454 rule = TimeZoneNumberRule.INSTANCE_COLON; 1455 } 1456 break; 1457 case '\'': // literal text 1458 final String sub = token.substring(1); 1459 if (sub.length() == 1) { 1460 rule = new CharacterLiteral(sub.charAt(0)); 1461 } else { 1462 rule = new StringLiteral(sub); 1463 } 1464 break; 1465 default: 1466 throw new IllegalArgumentException("Illegal pattern component: " + token); 1467 } 1468 1469 rules.add(rule); 1470 } 1471 1472 return rules; 1473 } 1474 1475 /** 1476 * Performs the parsing of tokens. 1477 * 1478 * @param pattern the pattern 1479 * @param indexRef index references 1480 * @return parsed token 1481 */ 1482 protected String parseToken(final String pattern, final int[] indexRef) { 1483 final StringBuilder buf = new StringBuilder(); 1484 1485 int i = indexRef[0]; 1486 final int length = pattern.length(); 1487 1488 char c = pattern.charAt(i); 1489 if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') { 1490 // Scan a run of the same character, which indicates a time 1491 // pattern. 1492 buf.append(c); 1493 1494 while (i + 1 < length) { 1495 final char peek = pattern.charAt(i + 1); 1496 if (peek != c) { 1497 break; 1498 } 1499 buf.append(c); 1500 i++; 1501 } 1502 } else { 1503 // This will identify token as text. 1504 buf.append('\''); 1505 1506 boolean inLiteral = false; 1507 1508 for (; i < length; i++) { 1509 c = pattern.charAt(i); 1510 1511 if (c == '\'') { 1512 if (i + 1 < length && pattern.charAt(i + 1) == '\'') { 1513 // '' is treated as escaped ' 1514 i++; 1515 buf.append(c); 1516 } else { 1517 inLiteral = !inLiteral; 1518 } 1519 } else if (!inLiteral && 1520 (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) { 1521 i--; 1522 break; 1523 } else { 1524 buf.append(c); 1525 } 1526 } 1527 } 1528 1529 indexRef[0] = i; 1530 return buf.toString(); 1531 } 1532 1533 // Serializing 1534 /** 1535 * Create the object after serialization. This implementation reinitializes the 1536 * transient properties. 1537 * 1538 * @param in ObjectInputStream from which the object is being deserialized. 1539 * @throws IOException if there is an IO issue. 1540 * @throws ClassNotFoundException if a class cannot be found. 1541 */ 1542 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { 1543 in.defaultReadObject(); 1544 init(); 1545 } 1546 1547 /** 1548 * Gets an appropriate rule for the padding required. 1549 * 1550 * @param field the field to get a rule for 1551 * @param padding the padding required 1552 * @return a new rule with the correct padding 1553 */ 1554 protected NumberRule selectNumberRule(final int field, final int padding) { 1555 switch (padding) { 1556 case 1: 1557 return new UnpaddedNumberField(field); 1558 case 2: 1559 return new TwoDigitNumberField(field); 1560 default: 1561 return new PaddedNumberField(field, padding); 1562 } 1563 } 1564 1565 /** 1566 * Gets a debugging string version of this formatter. 1567 * 1568 * @return a debugging string 1569 */ 1570 @Override 1571 public String toString() { 1572 return "FastDatePrinter[" + pattern + "," + locale + "," + timeZone.getID() + "]"; 1573 } 1574}