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