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