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