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