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