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