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.DateFormatSymbols; 023import java.text.ParseException; 024import java.text.ParsePosition; 025import java.text.SimpleDateFormat; 026import java.util.ArrayList; 027import java.util.Arrays; 028import java.util.Calendar; 029import java.util.Comparator; 030import java.util.Date; 031import java.util.HashMap; 032import java.util.List; 033import java.util.ListIterator; 034import java.util.Locale; 035import java.util.Map; 036import java.util.Objects; 037import java.util.Set; 038import java.util.TimeZone; 039import java.util.TreeMap; 040import java.util.TreeSet; 041import java.util.concurrent.ConcurrentHashMap; 042import java.util.concurrent.ConcurrentMap; 043import java.util.regex.Matcher; 044import java.util.regex.Pattern; 045import java.util.stream.Stream; 046 047import org.apache.commons.lang3.CharUtils; 048import org.apache.commons.lang3.LocaleUtils; 049import org.apache.commons.lang3.StringUtils; 050 051/** 052 * FastDateParser is a fast and thread-safe version of {@link java.text.SimpleDateFormat}. 053 * 054 * <p> 055 * To obtain a proxy to a FastDateParser, use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the factory methods of 056 * {@link FastDateFormat}. 057 * </p> 058 * 059 * <p> 060 * Since FastDateParser is thread safe, you can use a static member instance: 061 * </p> 062 * {@code 063 * private static final DateParser DATE_PARSER = FastDateFormat.getInstance("yyyy-MM-dd"); 064 * } 065 * 066 * <p> 067 * This class can be used as a direct replacement for {@link SimpleDateFormat} in most parsing situations. This class is especially useful in multi-threaded 068 * server environments. {@link SimpleDateFormat} is not thread-safe in any JDK version, nor will it be as Sun has closed the 069 * <a href="https://bugs.openjdk.org/browse/JDK-4228335">bug</a>/RFE. 070 * </p> 071 * 072 * <p> 073 * Only parsing is supported by this class, but all patterns are compatible with SimpleDateFormat. 074 * </p> 075 * 076 * <p> 077 * The class operates in lenient mode, so for example a time of 90 minutes is treated as 1 hour 30 minutes. 078 * </p> 079 * 080 * <p> 081 * Timing tests indicate this class is as about as fast as SimpleDateFormat in single thread applications and about 25% faster in multi-thread applications. 082 * </p> 083 * 084 * @since 3.2 085 * @see FastDatePrinter 086 */ 087public class FastDateParser implements DateParser, Serializable { 088 089 /** 090 * A strategy that handles a text field in the parsing pattern 091 */ 092 private static final class CaseInsensitiveTextStrategy extends PatternStrategy { 093 094 private final int field; 095 private final Locale locale; 096 private final Map<String, Integer> lKeyValues; 097 098 /** 099 * Constructs a Strategy that parses a Text field 100 * 101 * @param field The Calendar field 102 * @param definingCalendar The Calendar to use 103 * @param locale The Locale to use 104 */ 105 CaseInsensitiveTextStrategy(final int field, final Calendar definingCalendar, final Locale locale) { 106 this.field = field; 107 this.locale = LocaleUtils.toLocale(locale); 108 109 final StringBuilder regex = new StringBuilder(); 110 regex.append("((?iu)"); 111 lKeyValues = appendDisplayNames(definingCalendar, locale, field, regex); 112 regex.setLength(regex.length() - 1); 113 regex.append(")"); 114 createPattern(regex); 115 } 116 117 /** 118 * {@inheritDoc} 119 */ 120 @Override 121 void setCalendar(final FastDateParser parser, final Calendar calendar, final String value) { 122 final String lowerCase = value.toLowerCase(locale); 123 Integer iVal = lKeyValues.get(lowerCase); 124 if (iVal == null) { 125 // match missing the optional trailing period 126 iVal = lKeyValues.get(lowerCase + '.'); 127 } 128 // LANG-1669: Mimic fix done in OpenJDK 17 to resolve issue with parsing newly supported day periods added in OpenJDK 16 129 if (Calendar.AM_PM != this.field || iVal <= 1) { 130 calendar.set(field, iVal.intValue()); 131 } 132 } 133 134 /** 135 * Converts this instance to a handy debug string. 136 * 137 * @since 3.12.0 138 */ 139 @Override 140 public String toString() { 141 return "CaseInsensitiveTextStrategy [field=" + field + ", locale=" + locale + ", lKeyValues=" + lKeyValues + ", pattern=" + pattern + "]"; 142 } 143 } 144 145 /** 146 * A strategy that copies the static or quoted field in the parsing pattern 147 */ 148 private static final class CopyQuotedStrategy extends Strategy { 149 150 private final String formatField; 151 152 /** 153 * Constructs a Strategy that ensures the formatField has literal text 154 * 155 * @param formatField The literal text to match 156 */ 157 CopyQuotedStrategy(final String formatField) { 158 this.formatField = formatField; 159 } 160 161 /** 162 * {@inheritDoc} 163 */ 164 @Override 165 boolean isNumber() { 166 return false; 167 } 168 169 @Override 170 boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) { 171 for (int idx = 0; idx < formatField.length(); ++idx) { 172 final int sIdx = idx + pos.getIndex(); 173 if (sIdx == source.length()) { 174 pos.setErrorIndex(sIdx); 175 return false; 176 } 177 if (formatField.charAt(idx) != source.charAt(sIdx)) { 178 pos.setErrorIndex(sIdx); 179 return false; 180 } 181 } 182 pos.setIndex(formatField.length() + pos.getIndex()); 183 return true; 184 } 185 186 /** 187 * Converts this instance to a handy debug string. 188 * 189 * @since 3.12.0 190 */ 191 @Override 192 public String toString() { 193 return "CopyQuotedStrategy [formatField=" + formatField + "]"; 194 } 195 } 196 197 private static final class ISO8601TimeZoneStrategy extends PatternStrategy { 198 // Z, +hh, -hh, +hhmm, -hhmm, +hh:mm or -hh:mm 199 200 private static final Strategy ISO_8601_1_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}))"); 201 202 private static final Strategy ISO_8601_2_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}\\d{2}))"); 203 204 private static final Strategy ISO_8601_3_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}(?::)\\d{2}))"); 205 206 /** 207 * Factory method for ISO8601TimeZoneStrategies. 208 * 209 * @param tokenLen a token indicating the length of the TimeZone String to be formatted. 210 * @return a ISO8601TimeZoneStrategy that can format TimeZone String of length {@code tokenLen}. If no such strategy exists, an IllegalArgumentException 211 * will be thrown. 212 */ 213 static Strategy getStrategy(final int tokenLen) { 214 switch (tokenLen) { 215 case 1: 216 return ISO_8601_1_STRATEGY; 217 case 2: 218 return ISO_8601_2_STRATEGY; 219 case 3: 220 return ISO_8601_3_STRATEGY; 221 default: 222 throw new IllegalArgumentException("invalid number of X"); 223 } 224 } 225 226 /** 227 * Constructs a Strategy that parses a TimeZone 228 * 229 * @param pattern The Pattern 230 */ 231 ISO8601TimeZoneStrategy(final String pattern) { 232 createPattern(pattern); 233 } 234 235 /** 236 * {@inheritDoc} 237 */ 238 @Override 239 void setCalendar(final FastDateParser parser, final Calendar calendar, final String value) { 240 calendar.setTimeZone(FastTimeZone.getGmtTimeZone(value)); 241 } 242 } 243 244 /** 245 * A strategy that handles a number field in the parsing pattern 246 */ 247 private static class NumberStrategy extends Strategy { 248 249 private final int field; 250 251 /** 252 * Constructs a Strategy that parses a Number field 253 * 254 * @param field The Calendar field 255 */ 256 NumberStrategy(final int field) { 257 this.field = field; 258 } 259 260 /** 261 * {@inheritDoc} 262 */ 263 @Override 264 boolean isNumber() { 265 return true; 266 } 267 268 /** 269 * Make any modifications to parsed integer 270 * 271 * @param parser The parser 272 * @param iValue The parsed integer 273 * @return The modified value 274 */ 275 int modify(final FastDateParser parser, final int iValue) { 276 return iValue; 277 } 278 279 @Override 280 boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) { 281 int idx = pos.getIndex(); 282 int last = source.length(); 283 284 if (maxWidth == 0) { 285 // if no maxWidth, strip leading white space 286 for (; idx < last; ++idx) { 287 final char c = source.charAt(idx); 288 if (!Character.isWhitespace(c)) { 289 break; 290 } 291 } 292 pos.setIndex(idx); 293 } else { 294 final int end = idx + maxWidth; 295 if (last > end) { 296 last = end; 297 } 298 } 299 300 for (; idx < last; ++idx) { 301 final char c = source.charAt(idx); 302 if (!Character.isDigit(c)) { 303 break; 304 } 305 } 306 307 if (pos.getIndex() == idx) { 308 pos.setErrorIndex(idx); 309 return false; 310 } 311 312 final int value = Integer.parseInt(source.substring(pos.getIndex(), idx)); 313 pos.setIndex(idx); 314 315 calendar.set(field, modify(parser, value)); 316 return true; 317 } 318 319 /** 320 * Converts this instance to a handy debug string. 321 * 322 * @since 3.12.0 323 */ 324 @Override 325 public String toString() { 326 return getClass().getSimpleName() + " [field=" + field + "]"; 327 } 328 } 329 330 /** 331 * A strategy to parse a single field from the parsing pattern 332 */ 333 private abstract static class PatternStrategy extends Strategy { 334 335 Pattern pattern; 336 337 void createPattern(final String regex) { 338 this.pattern = Pattern.compile(regex); 339 } 340 341 void createPattern(final StringBuilder regex) { 342 createPattern(regex.toString()); 343 } 344 345 /** 346 * Is this field a number? The default implementation returns false. 347 * 348 * @return true, if field is a number 349 */ 350 @Override 351 boolean isNumber() { 352 return false; 353 } 354 355 @Override 356 boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) { 357 final Matcher matcher = pattern.matcher(source.substring(pos.getIndex())); 358 if (!matcher.lookingAt()) { 359 pos.setErrorIndex(pos.getIndex()); 360 return false; 361 } 362 pos.setIndex(pos.getIndex() + matcher.end(1)); 363 setCalendar(parser, calendar, matcher.group(1)); 364 return true; 365 } 366 367 abstract void setCalendar(FastDateParser parser, Calendar calendar, String value); 368 369 /** 370 * Converts this instance to a handy debug string. 371 * 372 * @since 3.12.0 373 */ 374 @Override 375 public String toString() { 376 return getClass().getSimpleName() + " [pattern=" + pattern + "]"; 377 } 378 379 } 380 381 /** 382 * A strategy to parse a single field from the parsing pattern 383 */ 384 private abstract static class Strategy { 385 386 /** 387 * Is this field a number? The default implementation returns false. 388 * 389 * @return true, if field is a number 390 */ 391 boolean isNumber() { 392 return false; 393 } 394 395 abstract boolean parse(FastDateParser parser, Calendar calendar, String source, ParsePosition pos, int maxWidth); 396 } 397 398 /** 399 * Holds strategy and field width 400 */ 401 private static final class StrategyAndWidth { 402 403 final Strategy strategy; 404 final int width; 405 406 StrategyAndWidth(final Strategy strategy, final int width) { 407 this.strategy = Objects.requireNonNull(strategy, "strategy"); 408 this.width = width; 409 } 410 411 int getMaxWidth(final ListIterator<StrategyAndWidth> lt) { 412 if (!strategy.isNumber() || !lt.hasNext()) { 413 return 0; 414 } 415 final Strategy nextStrategy = lt.next().strategy; 416 lt.previous(); 417 return nextStrategy.isNumber() ? width : 0; 418 } 419 420 @Override 421 public String toString() { 422 return "StrategyAndWidth [strategy=" + strategy + ", width=" + width + "]"; 423 } 424 } 425 426 /** 427 * Parse format into Strategies 428 */ 429 private final class StrategyParser { 430 private final Calendar definingCalendar; 431 private int currentIdx; 432 433 StrategyParser(final Calendar definingCalendar) { 434 this.definingCalendar = Objects.requireNonNull(definingCalendar, "definingCalendar"); 435 } 436 437 StrategyAndWidth getNextStrategy() { 438 if (currentIdx >= pattern.length()) { 439 return null; 440 } 441 final char c = pattern.charAt(currentIdx); 442 if (CharUtils.isAsciiAlpha(c)) { 443 return letterPattern(c); 444 } 445 return literal(); 446 } 447 448 private StrategyAndWidth letterPattern(final char c) { 449 final int begin = currentIdx; 450 while (++currentIdx < pattern.length()) { 451 if (pattern.charAt(currentIdx) != c) { 452 break; 453 } 454 } 455 final int width = currentIdx - begin; 456 return new StrategyAndWidth(getStrategy(c, width, definingCalendar), width); 457 } 458 459 private StrategyAndWidth literal() { 460 boolean activeQuote = false; 461 462 final StringBuilder sb = new StringBuilder(); 463 while (currentIdx < pattern.length()) { 464 final char c = pattern.charAt(currentIdx); 465 if (!activeQuote && CharUtils.isAsciiAlpha(c)) { 466 break; 467 } 468 if (c == '\'' && (++currentIdx == pattern.length() || pattern.charAt(currentIdx) != '\'')) { 469 activeQuote = !activeQuote; 470 continue; 471 } 472 ++currentIdx; 473 sb.append(c); 474 } 475 if (activeQuote) { 476 throw new IllegalArgumentException("Unterminated quote"); 477 } 478 final String formatField = sb.toString(); 479 return new StrategyAndWidth(new CopyQuotedStrategy(formatField), formatField.length()); 480 } 481 } 482 483 /** 484 * A strategy that handles a time zone field in the parsing pattern 485 */ 486 static class TimeZoneStrategy extends PatternStrategy { 487 488 private static final class TzInfo { 489 final TimeZone zone; 490 final int dstOffset; 491 492 TzInfo(final TimeZone tz, final boolean useDst) { 493 zone = tz; 494 dstOffset = useDst ? tz.getDSTSavings() : 0; 495 } 496 497 @Override 498 public String toString() { 499 return "TzInfo [zone=" + zone + ", dstOffset=" + dstOffset + "]"; 500 } 501 } 502 503 private static final String RFC_822_TIME_ZONE = "[+-]\\d{4}"; 504 505 private static final String GMT_OPTION = TimeZones.GMT_ID + "[+-]\\d{1,2}:\\d{2}"; 506 507 /** 508 * Index of zone id from {@link DateFormatSymbols#getZoneStrings()}. 509 */ 510 private static final int ID = 0; 511 512 /** 513 * Tests whether to skip the given time zone, true if TimeZone.getTimeZone(). 514 * <p> 515 * On Java 25 and up, skips short IDs if {@code ignoreTimeZoneShortIDs} is true. 516 * </p> 517 * <p> 518 * This method is package private only for testing. 519 * </p> 520 * 521 * @param tzId the ID to test. 522 * @return Whether to skip the given time zone ID. 523 */ 524 static boolean skipTimeZone(final String tzId) { 525 return tzId.equalsIgnoreCase(TimeZones.GMT_ID); 526 } 527 528 private final Locale locale; 529 530 /** 531 * Using lower case only or upper case only will cause problems with some Locales like Turkey, Armenia, Colognian and also depending on the Java 532 * version. For details, see https://garygregory.wordpress.com/2015/11/03/java-lowercase-conversion-turkey/ 533 */ 534 private final Map<String, TzInfo> tzNames = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 535 536 /** 537 * Constructs a Strategy that parses a TimeZone. 538 * 539 * @param locale The Locale. 540 */ 541 TimeZoneStrategy(final Locale locale) { 542 this.locale = LocaleUtils.toLocale(locale); 543 544 final StringBuilder sb = new StringBuilder(); 545 sb.append("((?iu)" + RFC_822_TIME_ZONE + "|" + GMT_OPTION); 546 547 final Set<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE); 548 549 // Order is undefined. 550 // TODO Use of getZoneStrings() is discouraged per its Javadoc. 551 final String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings(); 552 for (final String[] zoneNames : zones) { 553 // offset 0 is the time zone ID and is not localized 554 final String tzId = zoneNames[ID]; 555 if (skipTimeZone(tzId)) { 556 continue; 557 } 558 final TimeZone tz = TimeZones.getTimeZone(tzId); 559 // offset 1 is long standard name 560 // offset 2 is short standard name 561 final TzInfo standard = new TzInfo(tz, false); 562 TzInfo tzInfo = standard; 563 for (int i = 1; i < zoneNames.length; ++i) { 564 switch (i) { 565 case 3: // offset 3 is long daylight savings (or summertime) name 566 // offset 4 is the short summertime name 567 tzInfo = new TzInfo(tz, true); 568 break; 569 case 5: // offset 5 starts additional names, probably standard time 570 tzInfo = standard; 571 break; 572 default: 573 break; 574 } 575 final String zoneName = zoneNames[i]; 576 // ignore the data associated with duplicates supplied in the additional names 577 if (zoneName != null && sorted.add(zoneName)) { 578 tzNames.put(zoneName, tzInfo); 579 } 580 } 581 } 582 // Order is undefined. 583 for (final String tzId : TimeZones.SORTED_AVAILABLE_IDS) { 584 if (skipTimeZone(tzId)) { 585 continue; 586 } 587 final TimeZone tz = TimeZones.getTimeZone(tzId); 588 final String zoneName = tz.getDisplayName(locale); 589 if (sorted.add(zoneName)) { 590 tzNames.put(zoneName, new TzInfo(tz, tz.observesDaylightTime())); 591 } 592 } 593 // order the regex alternatives with longer strings first, greedy 594 // match will ensure the longest string will be consumed 595 sorted.forEach(zoneName -> simpleQuote(sb.append('|'), zoneName)); 596 sb.append(")"); 597 createPattern(sb); 598 } 599 600 /** 601 * {@inheritDoc} 602 */ 603 @Override 604 void setCalendar(final FastDateParser parser, final Calendar calendar, final String timeZone) { 605 final TimeZone tz = FastTimeZone.getGmtTimeZone(timeZone); 606 if (tz != null) { 607 calendar.setTimeZone(tz); 608 } else { 609 TzInfo tzInfo = tzNames.get(timeZone); 610 if (tzInfo == null) { 611 // match missing the optional trailing period 612 tzInfo = tzNames.get(timeZone + '.'); 613 if (tzInfo == null) { 614 // show chars in case this is multiple byte character issue 615 final char[] charArray = timeZone.toCharArray(); 616 throw new IllegalStateException(String.format("Can't find time zone '%s' (%d %s) in %s", timeZone, charArray.length, 617 Arrays.toString(charArray), new TreeSet<>(tzNames.keySet()))); 618 } 619 } 620 calendar.set(Calendar.DST_OFFSET, tzInfo.dstOffset); 621 calendar.set(Calendar.ZONE_OFFSET, tzInfo.zone.getRawOffset()); 622 } 623 } 624 625 /** 626 * Converts this instance to a handy debug string. 627 * 628 * @since 3.12.0 629 */ 630 @Override 631 public String toString() { 632 return "TimeZoneStrategy [locale=" + locale + ", tzNames=" + tzNames + ", pattern=" + pattern + "]"; 633 } 634 635 } 636 637 /** 638 * Required for serialization support. 639 * 640 * @see java.io.Serializable 641 */ 642 private static final long serialVersionUID = 3L; 643 644 static final Locale JAPANESE_IMPERIAL = new Locale("ja", "JP", "JP"); 645 646 /** 647 * comparator used to sort regex alternatives. Alternatives should be ordered longer first, and shorter last. ('february' before 'feb'). All entries must be 648 * lower-case by locale. 649 */ 650 private static final Comparator<String> LONGER_FIRST_LOWERCASE = Comparator.reverseOrder(); 651 652 // helper classes to parse the format string 653 654 @SuppressWarnings("unchecked") // OK because we are creating an array with no entries 655 private static final ConcurrentMap<Locale, Strategy>[] CACHES = new ConcurrentMap[Calendar.FIELD_COUNT]; 656 657 private static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR) { 658 659 /** 660 * {@inheritDoc} 661 */ 662 @Override 663 int modify(final FastDateParser parser, final int iValue) { 664 return iValue < 100 ? parser.adjustYear(iValue) : iValue; 665 } 666 }; 667 668 private static final Strategy NUMBER_MONTH_STRATEGY = new NumberStrategy(Calendar.MONTH) { 669 @Override 670 int modify(final FastDateParser parser, final int iValue) { 671 return iValue - 1; 672 } 673 }; 674 675 private static final Strategy LITERAL_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR); 676 677 private static final Strategy WEEK_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_YEAR); 678 679 private static final Strategy WEEK_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_MONTH); 680 681 private static final Strategy DAY_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.DAY_OF_YEAR); 682 683 private static final Strategy DAY_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_MONTH); 684 685 private static final Strategy DAY_OF_WEEK_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK) { 686 @Override 687 int modify(final FastDateParser parser, final int iValue) { 688 return iValue == 7 ? Calendar.SUNDAY : iValue + 1; 689 } 690 }; 691 692 private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH); 693 694 private static final Strategy HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY); 695 696 private static final Strategy HOUR24_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY) { 697 @Override 698 int modify(final FastDateParser parser, final int iValue) { 699 return iValue == 24 ? 0 : iValue; 700 } 701 }; 702 703 private static final Strategy HOUR12_STRATEGY = new NumberStrategy(Calendar.HOUR) { 704 @Override 705 int modify(final FastDateParser parser, final int iValue) { 706 return iValue == 12 ? 0 : iValue; 707 } 708 }; 709 710 private static final Strategy HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR); 711 712 private static final Strategy MINUTE_STRATEGY = new NumberStrategy(Calendar.MINUTE); 713 714 private static final Strategy SECOND_STRATEGY = new NumberStrategy(Calendar.SECOND); 715 716 private static final Strategy MILLISECOND_STRATEGY = new NumberStrategy(Calendar.MILLISECOND); 717 718 /** 719 * Gets the short and long values displayed for a field 720 * 721 * @param calendar The calendar to obtain the short and long values 722 * @param locale The locale of display names 723 * @param field The field of interest 724 * @param regex The regular expression to build 725 * @return The map of string display names to field values 726 */ 727 private static Map<String, Integer> appendDisplayNames(final Calendar calendar, final Locale locale, final int field, final StringBuilder regex) { 728 Objects.requireNonNull(calendar, "calendar"); 729 final Map<String, Integer> values = new HashMap<>(); 730 final Locale actualLocale = LocaleUtils.toLocale(locale); 731 final Map<String, Integer> displayNames = calendar.getDisplayNames(field, Calendar.ALL_STYLES, actualLocale); 732 final TreeSet<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE); 733 displayNames.forEach((k, v) -> { 734 final String keyLc = k.toLowerCase(actualLocale); 735 if (sorted.add(keyLc)) { 736 values.put(keyLc, v); 737 } 738 }); 739 sorted.forEach(symbol -> simpleQuote(regex, symbol).append('|')); 740 return values; 741 } 742 743 /** 744 * Clears the cache. 745 */ 746 static void clear() { 747 Stream.of(CACHES).filter(Objects::nonNull).forEach(ConcurrentMap::clear); 748 } 749 750 /** 751 * Gets a cache of Strategies for a particular field 752 * 753 * @param field The Calendar field 754 * @return a cache of Locale to Strategy 755 */ 756 private static ConcurrentMap<Locale, Strategy> getCache(final int field) { 757 synchronized (CACHES) { 758 if (CACHES[field] == null) { 759 CACHES[field] = new ConcurrentHashMap<>(3); 760 } 761 return CACHES[field]; 762 } 763 } 764 765 private static StringBuilder simpleQuote(final StringBuilder sb, final String value) { 766 for (int i = 0; i < value.length(); ++i) { 767 final char c = value.charAt(i); 768 switch (c) { 769 case '\\': 770 case '^': 771 case '$': 772 case '.': 773 case '|': 774 case '?': 775 case '*': 776 case '+': 777 case '(': 778 case ')': 779 case '[': 780 case '{': 781 sb.append('\\'); 782 // falls-through 783 default: 784 sb.append(c); 785 } 786 } 787 if (sb.charAt(sb.length() - 1) == '.') { 788 // trailing '.' is optional 789 sb.append('?'); 790 } 791 return sb; 792 } 793 794 /** Input pattern. */ 795 private final String pattern; 796 797 /** Input TimeZone. */ 798 private final TimeZone timeZone; 799 800 /** Input Locale. */ 801 private final Locale locale; 802 803 /** 804 * Century from Date. 805 */ 806 private final int century; 807 808 /** 809 * Start year from Date. 810 */ 811 private final int startYear; 812 813 /** Initialized from Calendar. */ 814 private transient List<StrategyAndWidth> patterns; 815 816 /** 817 * Constructs a new FastDateParser. 818 * 819 * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the factory methods of {@link FastDateFormat} to get a cached 820 * FastDateParser instance. 821 * 822 * @param pattern non-null {@link java.text.SimpleDateFormat} compatible pattern 823 * @param timeZone non-null time zone to use 824 * @param locale non-null locale 825 */ 826 protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale) { 827 this(pattern, timeZone, locale, null); 828 } 829 830 /** 831 * Constructs a new FastDateParser. 832 * 833 * @param pattern non-null {@link java.text.SimpleDateFormat} compatible pattern 834 * @param timeZone non-null time zone to use 835 * @param locale locale, null maps to the default Locale. 836 * @param centuryStart The start of the century for 2 digit year parsing 837 * @since 3.5 838 */ 839 protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) { 840 this.pattern = Objects.requireNonNull(pattern, "pattern"); 841 this.timeZone = Objects.requireNonNull(timeZone, "timeZone"); 842 this.locale = LocaleUtils.toLocale(locale); 843 final Calendar definingCalendar = Calendar.getInstance(timeZone, this.locale); 844 final int centuryStartYear; 845 if (centuryStart != null) { 846 definingCalendar.setTime(centuryStart); 847 centuryStartYear = definingCalendar.get(Calendar.YEAR); 848 } else if (this.locale.equals(JAPANESE_IMPERIAL)) { 849 centuryStartYear = 0; 850 } else { 851 // from 80 years ago to 20 years from now 852 definingCalendar.setTime(new Date()); 853 centuryStartYear = definingCalendar.get(Calendar.YEAR) - 80; 854 } 855 century = centuryStartYear / 100 * 100; 856 startYear = centuryStartYear - century; 857 init(definingCalendar); 858 } 859 860 /** 861 * Adjusts dates to be within appropriate century 862 * 863 * @param twoDigitYear The year to adjust 864 * @return A value between centuryStart(inclusive) to centuryStart+100(exclusive) 865 */ 866 private int adjustYear(final int twoDigitYear) { 867 final int trial = century + twoDigitYear; 868 return twoDigitYear >= startYear ? trial : trial + 100; 869 } 870 871 /** 872 * Compares another object for equality with this object. 873 * 874 * @param obj the object to compare to 875 * @return {@code true}if equal to this instance 876 */ 877 @Override 878 public boolean equals(final Object obj) { 879 if (!(obj instanceof FastDateParser)) { 880 return false; 881 } 882 final FastDateParser other = (FastDateParser) obj; 883 return pattern.equals(other.pattern) && timeZone.equals(other.timeZone) && locale.equals(other.locale); 884 } 885 886 /* 887 * (non-Javadoc) 888 * 889 * @see org.apache.commons.lang3.time.DateParser#getLocale() 890 */ 891 @Override 892 public Locale getLocale() { 893 return locale; 894 } 895 896 /** 897 * Constructs a Strategy that parses a Text field 898 * 899 * @param field The Calendar field 900 * @param definingCalendar The calendar to obtain the short and long values 901 * @return a TextStrategy for the field and Locale 902 */ 903 private Strategy getLocaleSpecificStrategy(final int field, final Calendar definingCalendar) { 904 return getCache(field).computeIfAbsent(locale, 905 k -> field == Calendar.ZONE_OFFSET ? new TimeZoneStrategy(locale) : new CaseInsensitiveTextStrategy(field, definingCalendar, locale)); 906 } 907 908 /* 909 * (non-Javadoc) 910 * 911 * @see org.apache.commons.lang3.time.DateParser#getPattern() 912 */ 913 @Override 914 public String getPattern() { 915 return pattern; 916 } 917 918 /** 919 * Gets a Strategy given a field from a SimpleDateFormat pattern 920 * 921 * @param f A sub-sequence of the SimpleDateFormat pattern 922 * @param width formatting width 923 * @param definingCalendar The calendar to obtain the short and long values 924 * @return The Strategy that will handle parsing for the field 925 */ 926 private Strategy getStrategy(final char f, final int width, final Calendar definingCalendar) { 927 switch (f) { 928 case 'D': 929 return DAY_OF_YEAR_STRATEGY; 930 case 'E': 931 return getLocaleSpecificStrategy(Calendar.DAY_OF_WEEK, definingCalendar); 932 case 'F': 933 return DAY_OF_WEEK_IN_MONTH_STRATEGY; 934 case 'G': 935 return getLocaleSpecificStrategy(Calendar.ERA, definingCalendar); 936 case 'H': // Hour in day (0-23) 937 return HOUR_OF_DAY_STRATEGY; 938 case 'K': // Hour in am/pm (0-11) 939 return HOUR_STRATEGY; 940 case 'M': 941 case 'L': 942 return width >= 3 ? getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar) : NUMBER_MONTH_STRATEGY; 943 case 'S': 944 return MILLISECOND_STRATEGY; 945 case 'W': 946 return WEEK_OF_MONTH_STRATEGY; 947 case 'a': 948 return getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar); 949 case 'd': 950 return DAY_OF_MONTH_STRATEGY; 951 case 'h': // Hour in am/pm (1-12), i.e. midday/midnight is 12, not 0 952 return HOUR12_STRATEGY; 953 case 'k': // Hour in day (1-24), i.e. midnight is 24, not 0 954 return HOUR24_OF_DAY_STRATEGY; 955 case 'm': 956 return MINUTE_STRATEGY; 957 case 's': 958 return SECOND_STRATEGY; 959 case 'u': 960 return DAY_OF_WEEK_STRATEGY; 961 case 'w': 962 return WEEK_OF_YEAR_STRATEGY; 963 case 'y': 964 case 'Y': 965 return width > 2 ? LITERAL_YEAR_STRATEGY : ABBREVIATED_YEAR_STRATEGY; 966 case 'X': 967 return ISO8601TimeZoneStrategy.getStrategy(width); 968 case 'Z': 969 if (width == 2) { 970 return ISO8601TimeZoneStrategy.ISO_8601_3_STRATEGY; 971 } 972 // falls-through 973 case 'z': 974 return getLocaleSpecificStrategy(Calendar.ZONE_OFFSET, definingCalendar); 975 default: 976 throw new IllegalArgumentException("Format '" + f + "' not supported"); 977 } 978 } 979 980 /* 981 * (non-Javadoc) 982 * 983 * @see org.apache.commons.lang3.time.DateParser#getTimeZone() 984 */ 985 @Override 986 public TimeZone getTimeZone() { 987 return timeZone; 988 } 989 990 /** 991 * Returns a hash code compatible with equals. 992 * 993 * @return a hash code compatible with equals 994 */ 995 @Override 996 public int hashCode() { 997 return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode()); 998 } 999 1000 /** 1001 * Initializes derived fields from defining fields. This is called from constructor and from readObject (de-serialization) 1002 * 1003 * @param definingCalendar the {@link java.util.Calendar} instance used to initialize this FastDateParser 1004 */ 1005 private void init(final Calendar definingCalendar) { 1006 patterns = new ArrayList<>(); 1007 1008 final StrategyParser strategyParser = new StrategyParser(definingCalendar); 1009 for (;;) { 1010 final StrategyAndWidth field = strategyParser.getNextStrategy(); 1011 if (field == null) { 1012 break; 1013 } 1014 patterns.add(field); 1015 } 1016 } 1017 1018 /* 1019 * (non-Javadoc) 1020 * 1021 * @see org.apache.commons.lang3.time.DateParser#parse(String) 1022 */ 1023 @Override 1024 public Date parse(final String source) throws ParseException { 1025 final ParsePosition pp = new ParsePosition(0); 1026 final Date date = parse(source, pp); 1027 if (date == null) { 1028 // Add a note regarding supported date range 1029 final int errorIndex = pp.getErrorIndex(); 1030 final String msg = String.format("Unparseable date: '%s', parse position = %s", source, pp); 1031 if (locale.equals(JAPANESE_IMPERIAL)) { 1032 throw new ParseException(String.format("; the %s locale does not support dates before 1868-01-01.", locale, msg), errorIndex); 1033 } 1034 throw new ParseException(msg, errorIndex); 1035 } 1036 return date; 1037 } 1038 1039 /** 1040 * This implementation updates the ParsePosition if the parse succeeds. However, it sets the error index to the position before the failed field unlike the 1041 * method {@link java.text.SimpleDateFormat#parse(String, ParsePosition)} which sets the error index to after the failed field. 1042 * <p> 1043 * To determine if the parse has succeeded, the caller must check if the current parse position given by {@link ParsePosition#getIndex()} has been updated. 1044 * If the input buffer has been fully parsed, then the index will point to just after the end of the input buffer. 1045 * </p> 1046 * 1047 * @see org.apache.commons.lang3.time.DateParser#parse(String, java.text.ParsePosition) 1048 */ 1049 @Override 1050 public Date parse(final String source, final ParsePosition pos) { 1051 // timing tests indicate getting new instance is 19% faster than cloning 1052 final Calendar cal = Calendar.getInstance(timeZone, locale); 1053 cal.clear(); 1054 return parse(source, pos, cal) ? cal.getTime() : null; 1055 } 1056 1057 /** 1058 * Parses a formatted date string according to the format. Updates the Calendar with parsed fields. Upon success, the ParsePosition index is updated to 1059 * indicate how much of the source text was consumed. Not all source text needs to be consumed. Upon parse failure, ParsePosition error index is updated to 1060 * the offset of the source text which does not match the supplied format. 1061 * 1062 * @param source The text to parse. 1063 * @param pos On input, the position in the source to start parsing, on output, updated position. 1064 * @param calendar The calendar into which to set parsed fields. 1065 * @return true, if source has been parsed (pos parsePosition is updated); otherwise false (and pos errorIndex is updated) 1066 * @throws IllegalArgumentException when Calendar has been set to be not lenient, and a parsed field is out of range. 1067 */ 1068 @Override 1069 public boolean parse(final String source, final ParsePosition pos, final Calendar calendar) { 1070 final ListIterator<StrategyAndWidth> lt = patterns.listIterator(); 1071 while (lt.hasNext()) { 1072 final StrategyAndWidth strategyAndWidth = lt.next(); 1073 final int maxWidth = strategyAndWidth.getMaxWidth(lt); 1074 if (!strategyAndWidth.strategy.parse(this, calendar, source, pos, maxWidth)) { 1075 return false; 1076 } 1077 } 1078 return true; 1079 } 1080 1081 /* 1082 * (non-Javadoc) 1083 * 1084 * @see org.apache.commons.lang3.time.DateParser#parseObject(String) 1085 */ 1086 @Override 1087 public Object parseObject(final String source) throws ParseException { 1088 return parse(source); 1089 } 1090 1091 /* 1092 * (non-Javadoc) 1093 * 1094 * @see org.apache.commons.lang3.time.DateParser#parseObject(String, java.text.ParsePosition) 1095 */ 1096 @Override 1097 public Object parseObject(final String source, final ParsePosition pos) { 1098 return parse(source, pos); 1099 } 1100 1101 /** 1102 * Creates the object after serialization. This implementation reinitializes the transient properties. 1103 * 1104 * @param in ObjectInputStream from which the object is being deserialized. 1105 * @throws IOException if there is an IO issue. 1106 * @throws ClassNotFoundException if a class cannot be found. 1107 */ 1108 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { 1109 in.defaultReadObject(); 1110 final Calendar definingCalendar = Calendar.getInstance(timeZone, locale); 1111 init(definingCalendar); 1112 } 1113 1114 /** 1115 * Gets a string version of this formatter. 1116 * 1117 * @return a debugging string 1118 */ 1119 @Override 1120 public String toString() { 1121 return "FastDateParser[" + pattern + ", " + locale + ", " + timeZone.getID() + "]"; 1122 } 1123 1124 /** 1125 * Converts all state of this instance to a String handy for debugging. 1126 * 1127 * @return a string. 1128 * @since 3.12.0 1129 */ 1130 public String toStringAll() { 1131 return "FastDateParser [pattern=" + pattern + ", timeZone=" + timeZone + ", locale=" + locale + ", century=" + century + ", startYear=" + startYear 1132 + ", patterns=" + StringUtils.join(patterns, ", " + System.lineSeparator() + "\t") + "]"; 1133 } 1134}