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