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 return sb; 452 } 453 454 /** 455 * Get the short and long values displayed for a field 456 * @param cal The calendar to obtain the short and long values 457 * @param locale The locale of display names 458 * @param field The field of interest 459 * @param regex The regular expression to build 460 * @return The map of string display names to field values 461 */ 462 private static Map<String, Integer> appendDisplayNames(final Calendar cal, final Locale locale, final int field, final StringBuilder regex) { 463 final Map<String, Integer> values = new HashMap<>(); 464 465 final Map<String, Integer> displayNames = cal.getDisplayNames(field, Calendar.ALL_STYLES, locale); 466 final TreeSet<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE); 467 for (final Map.Entry<String, Integer> displayName : displayNames.entrySet()) { 468 final String key = displayName.getKey().toLowerCase(locale); 469 if (sorted.add(key)) { 470 values.put(key, displayName.getValue()); 471 } 472 } 473 for (final String symbol : sorted) { 474 simpleQuote(regex, symbol).append('|'); 475 } 476 return values; 477 } 478 479 /** 480 * Adjust dates to be within appropriate century 481 * @param twoDigitYear The year to adjust 482 * @return A value between centuryStart(inclusive) to centuryStart+100(exclusive) 483 */ 484 private int adjustYear(final int twoDigitYear) { 485 final int trial = century + twoDigitYear; 486 return twoDigitYear >= startYear ? trial : trial + 100; 487 } 488 489 /** 490 * A strategy to parse a single field from the parsing pattern 491 */ 492 private abstract static class Strategy { 493 /** 494 * Is this field a number? 495 * The default implementation returns false. 496 * 497 * @return true, if field is a number 498 */ 499 boolean isNumber() { 500 return false; 501 } 502 503 abstract boolean parse(FastDateParser parser, Calendar calendar, String source, ParsePosition pos, int maxWidth); 504 } 505 506 /** 507 * A strategy to parse a single field from the parsing pattern 508 */ 509 private abstract static class PatternStrategy extends Strategy { 510 511 private Pattern pattern; 512 513 void createPattern(final StringBuilder regex) { 514 createPattern(regex.toString()); 515 } 516 517 void createPattern(final String regex) { 518 this.pattern = Pattern.compile(regex); 519 } 520 521 /** 522 * Is this field a number? 523 * The default implementation returns false. 524 * 525 * @return true, if field is a number 526 */ 527 @Override 528 boolean isNumber() { 529 return false; 530 } 531 532 @Override 533 boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) { 534 final Matcher matcher = pattern.matcher(source.substring(pos.getIndex())); 535 if (!matcher.lookingAt()) { 536 pos.setErrorIndex(pos.getIndex()); 537 return false; 538 } 539 pos.setIndex(pos.getIndex() + matcher.end(1)); 540 setCalendar(parser, calendar, matcher.group(1)); 541 return true; 542 } 543 544 abstract void setCalendar(FastDateParser parser, Calendar cal, String value); 545 } 546 547 /** 548 * Obtain a Strategy given a field from a SimpleDateFormat pattern 549 * @param f A sub-sequence of the SimpleDateFormat pattern 550 * @param definingCalendar The calendar to obtain the short and long values 551 * @return The Strategy that will handle parsing for the field 552 */ 553 private Strategy getStrategy(final char f, final int width, final Calendar definingCalendar) { 554 switch(f) { 555 default: 556 throw new IllegalArgumentException("Format '"+f+"' not supported"); 557 case 'D': 558 return DAY_OF_YEAR_STRATEGY; 559 case 'E': 560 return getLocaleSpecificStrategy(Calendar.DAY_OF_WEEK, definingCalendar); 561 case 'F': 562 return DAY_OF_WEEK_IN_MONTH_STRATEGY; 563 case 'G': 564 return getLocaleSpecificStrategy(Calendar.ERA, definingCalendar); 565 case 'H': // Hour in day (0-23) 566 return HOUR_OF_DAY_STRATEGY; 567 case 'K': // Hour in am/pm (0-11) 568 return HOUR_STRATEGY; 569 case 'M': 570 return width>=3 ?getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar) :NUMBER_MONTH_STRATEGY; 571 case 'S': 572 return MILLISECOND_STRATEGY; 573 case 'W': 574 return WEEK_OF_MONTH_STRATEGY; 575 case 'a': 576 return getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar); 577 case 'd': 578 return DAY_OF_MONTH_STRATEGY; 579 case 'h': // Hour in am/pm (1-12), i.e. midday/midnight is 12, not 0 580 return HOUR12_STRATEGY; 581 case 'k': // Hour in day (1-24), i.e. midnight is 24, not 0 582 return HOUR24_OF_DAY_STRATEGY; 583 case 'm': 584 return MINUTE_STRATEGY; 585 case 's': 586 return SECOND_STRATEGY; 587 case 'u': 588 return DAY_OF_WEEK_STRATEGY; 589 case 'w': 590 return WEEK_OF_YEAR_STRATEGY; 591 case 'y': 592 case 'Y': 593 return width>2 ?LITERAL_YEAR_STRATEGY :ABBREVIATED_YEAR_STRATEGY; 594 case 'X': 595 return ISO8601TimeZoneStrategy.getStrategy(width); 596 case 'Z': 597 if (width==2) { 598 return ISO8601TimeZoneStrategy.ISO_8601_3_STRATEGY; 599 } 600 //$FALL-THROUGH$ 601 case 'z': 602 return getLocaleSpecificStrategy(Calendar.ZONE_OFFSET, definingCalendar); 603 } 604 } 605 606 @SuppressWarnings("unchecked") // OK because we are creating an array with no entries 607 private static final ConcurrentMap<Locale, Strategy>[] caches = new ConcurrentMap[Calendar.FIELD_COUNT]; 608 609 /** 610 * Get a cache of Strategies for a particular field 611 * @param field The Calendar field 612 * @return a cache of Locale to Strategy 613 */ 614 private static ConcurrentMap<Locale, Strategy> getCache(final int field) { 615 synchronized (caches) { 616 if (caches[field] == null) { 617 caches[field] = new ConcurrentHashMap<>(3); 618 } 619 return caches[field]; 620 } 621 } 622 623 /** 624 * Construct a Strategy that parses a Text field 625 * @param field The Calendar field 626 * @param definingCalendar The calendar to obtain the short and long values 627 * @return a TextStrategy for the field and Locale 628 */ 629 private Strategy getLocaleSpecificStrategy(final int field, final Calendar definingCalendar) { 630 final ConcurrentMap<Locale, Strategy> cache = getCache(field); 631 Strategy strategy = cache.get(locale); 632 if (strategy == null) { 633 strategy = field == Calendar.ZONE_OFFSET 634 ? new TimeZoneStrategy(locale) 635 : new CaseInsensitiveTextStrategy(field, definingCalendar, locale); 636 final Strategy inCache = cache.putIfAbsent(locale, strategy); 637 if (inCache != null) { 638 return inCache; 639 } 640 } 641 return strategy; 642 } 643 644 /** 645 * A strategy that copies the static or quoted field in the parsing pattern 646 */ 647 private static class CopyQuotedStrategy extends Strategy { 648 649 private final String formatField; 650 651 /** 652 * Construct a Strategy that ensures the formatField has literal text 653 * @param formatField The literal text to match 654 */ 655 CopyQuotedStrategy(final String formatField) { 656 this.formatField = formatField; 657 } 658 659 /** 660 * {@inheritDoc} 661 */ 662 @Override 663 boolean isNumber() { 664 return false; 665 } 666 667 @Override 668 boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) { 669 for (int idx = 0; idx < formatField.length(); ++idx) { 670 final int sIdx = idx + pos.getIndex(); 671 if (sIdx == source.length()) { 672 pos.setErrorIndex(sIdx); 673 return false; 674 } 675 if (formatField.charAt(idx) != source.charAt(sIdx)) { 676 pos.setErrorIndex(sIdx); 677 return false; 678 } 679 } 680 pos.setIndex(formatField.length() + pos.getIndex()); 681 return true; 682 } 683 } 684 685 /** 686 * A strategy that handles a text field in the parsing pattern 687 */ 688 private static class CaseInsensitiveTextStrategy extends PatternStrategy { 689 private final int field; 690 final Locale locale; 691 private final Map<String, Integer> lKeyValues; 692 693 /** 694 * Construct a Strategy that parses a Text field 695 * @param field The Calendar field 696 * @param definingCalendar The Calendar to use 697 * @param locale The Locale to use 698 */ 699 CaseInsensitiveTextStrategy(final int field, final Calendar definingCalendar, final Locale locale) { 700 this.field = field; 701 this.locale = locale; 702 703 final StringBuilder regex = new StringBuilder(); 704 regex.append("((?iu)"); 705 lKeyValues = appendDisplayNames(definingCalendar, locale, field, regex); 706 regex.setLength(regex.length()-1); 707 regex.append(")"); 708 createPattern(regex); 709 } 710 711 /** 712 * {@inheritDoc} 713 */ 714 @Override 715 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { 716 final Integer iVal = lKeyValues.get(value.toLowerCase(locale)); 717 cal.set(field, iVal.intValue()); 718 } 719 } 720 721 722 /** 723 * A strategy that handles a number field in the parsing pattern 724 */ 725 private static class NumberStrategy extends Strategy { 726 private final int field; 727 728 /** 729 * Construct a Strategy that parses a Number field 730 * @param field The Calendar field 731 */ 732 NumberStrategy(final int field) { 733 this.field= field; 734 } 735 736 /** 737 * {@inheritDoc} 738 */ 739 @Override 740 boolean isNumber() { 741 return true; 742 } 743 744 @Override 745 boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) { 746 int idx = pos.getIndex(); 747 int last = source.length(); 748 749 if (maxWidth == 0) { 750 // if no maxWidth, strip leading white space 751 for (; idx < last; ++idx) { 752 final char c = source.charAt(idx); 753 if (!Character.isWhitespace(c)) { 754 break; 755 } 756 } 757 pos.setIndex(idx); 758 } else { 759 final int end = idx + maxWidth; 760 if (last > end) { 761 last = end; 762 } 763 } 764 765 for (; idx < last; ++idx) { 766 final char c = source.charAt(idx); 767 if (!Character.isDigit(c)) { 768 break; 769 } 770 } 771 772 if (pos.getIndex() == idx) { 773 pos.setErrorIndex(idx); 774 return false; 775 } 776 777 final int value = Integer.parseInt(source.substring(pos.getIndex(), idx)); 778 pos.setIndex(idx); 779 780 calendar.set(field, modify(parser, value)); 781 return true; 782 } 783 784 /** 785 * Make any modifications to parsed integer 786 * @param parser The parser 787 * @param iValue The parsed integer 788 * @return The modified value 789 */ 790 int modify(final FastDateParser parser, final int iValue) { 791 return iValue; 792 } 793 794 } 795 796 private static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR) { 797 /** 798 * {@inheritDoc} 799 */ 800 @Override 801 int modify(final FastDateParser parser, final int iValue) { 802 return iValue < 100 ? parser.adjustYear(iValue) : iValue; 803 } 804 }; 805 806 /** 807 * A strategy that handles a timezone field in the parsing pattern 808 */ 809 static class TimeZoneStrategy extends PatternStrategy { 810 private static final String RFC_822_TIME_ZONE = "[+-]\\d{4}"; 811 private static final String GMT_OPTION= "GMT[+-]\\d{1,2}:\\d{2}"; 812 813 private final Locale locale; 814 private final Map<String, TzInfo> tzNames= new HashMap<>(); 815 816 private static class TzInfo { 817 TimeZone zone; 818 int dstOffset; 819 820 TzInfo(final TimeZone tz, final boolean useDst) { 821 zone = tz; 822 dstOffset = useDst ?tz.getDSTSavings() :0; 823 } 824 } 825 826 /** 827 * Index of zone id 828 */ 829 private static final int ID = 0; 830 831 /** 832 * Construct a Strategy that parses a TimeZone 833 * @param locale The Locale 834 */ 835 TimeZoneStrategy(final Locale locale) { 836 this.locale = locale; 837 838 final StringBuilder sb = new StringBuilder(); 839 sb.append("((?iu)" + RFC_822_TIME_ZONE + "|" + GMT_OPTION ); 840 841 final Set<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE); 842 843 final String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings(); 844 for (final String[] zoneNames : zones) { 845 // offset 0 is the time zone ID and is not localized 846 final String tzId = zoneNames[ID]; 847 if (tzId.equalsIgnoreCase("GMT")) { 848 continue; 849 } 850 final TimeZone tz = TimeZone.getTimeZone(tzId); 851 // offset 1 is long standard name 852 // offset 2 is short standard name 853 final TzInfo standard = new TzInfo(tz, false); 854 TzInfo tzInfo = standard; 855 for (int i = 1; i < zoneNames.length; ++i) { 856 switch (i) { 857 case 3: // offset 3 is long daylight savings (or summertime) name 858 // offset 4 is the short summertime name 859 tzInfo = new TzInfo(tz, true); 860 break; 861 case 5: // offset 5 starts additional names, probably standard time 862 tzInfo = standard; 863 break; 864 default: 865 break; 866 } 867 if (zoneNames[i] != null) { 868 final String key = zoneNames[i].toLowerCase(locale); 869 // ignore the data associated with duplicates supplied in 870 // the additional names 871 if (sorted.add(key)) { 872 tzNames.put(key, tzInfo); 873 } 874 } 875 } 876 } 877 // order the regex alternatives with longer strings first, greedy 878 // match will ensure longest string will be consumed 879 for (final String zoneName : sorted) { 880 simpleQuote(sb.append('|'), zoneName); 881 } 882 sb.append(")"); 883 createPattern(sb); 884 } 885 886 /** 887 * {@inheritDoc} 888 */ 889 @Override 890 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { 891 if (value.charAt(0) == '+' || value.charAt(0) == '-') { 892 final TimeZone tz = TimeZone.getTimeZone("GMT" + value); 893 cal.setTimeZone(tz); 894 } else if (value.regionMatches(true, 0, "GMT", 0, 3)) { 895 final TimeZone tz = TimeZone.getTimeZone(value.toUpperCase()); 896 cal.setTimeZone(tz); 897 } else { 898 final TzInfo tzInfo = tzNames.get(value.toLowerCase(locale)); 899 cal.set(Calendar.DST_OFFSET, tzInfo.dstOffset); 900 cal.set(Calendar.ZONE_OFFSET, tzInfo.zone.getRawOffset()); 901 } 902 } 903 } 904 905 private static class ISO8601TimeZoneStrategy extends PatternStrategy { 906 // Z, +hh, -hh, +hhmm, -hhmm, +hh:mm or -hh:mm 907 908 /** 909 * Construct a Strategy that parses a TimeZone 910 * @param pattern The Pattern 911 */ 912 ISO8601TimeZoneStrategy(final String pattern) { 913 createPattern(pattern); 914 } 915 916 /** 917 * {@inheritDoc} 918 */ 919 @Override 920 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { 921 if (value.equals("Z")) { 922 cal.setTimeZone(TimeZone.getTimeZone("UTC")); 923 } else { 924 cal.setTimeZone(TimeZone.getTimeZone("GMT" + value)); 925 } 926 } 927 928 private static final Strategy ISO_8601_1_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}))"); 929 private static final Strategy ISO_8601_2_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}\\d{2}))"); 930 private static final Strategy ISO_8601_3_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}(?::)\\d{2}))"); 931 932 /** 933 * Factory method for ISO8601TimeZoneStrategies. 934 * 935 * @param tokenLen a token indicating the length of the TimeZone String to be formatted. 936 * @return a ISO8601TimeZoneStrategy that can format TimeZone String of length {@code tokenLen}. If no such 937 * strategy exists, an IllegalArgumentException will be thrown. 938 */ 939 static Strategy getStrategy(final int tokenLen) { 940 switch(tokenLen) { 941 case 1: 942 return ISO_8601_1_STRATEGY; 943 case 2: 944 return ISO_8601_2_STRATEGY; 945 case 3: 946 return ISO_8601_3_STRATEGY; 947 default: 948 throw new IllegalArgumentException("invalid number of X"); 949 } 950 } 951 } 952 953 private static final Strategy NUMBER_MONTH_STRATEGY = new NumberStrategy(Calendar.MONTH) { 954 @Override 955 int modify(final FastDateParser parser, final int iValue) { 956 return iValue-1; 957 } 958 }; 959 private static final Strategy LITERAL_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR); 960 private static final Strategy WEEK_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_YEAR); 961 private static final Strategy WEEK_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_MONTH); 962 private static final Strategy DAY_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.DAY_OF_YEAR); 963 private static final Strategy DAY_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_MONTH); 964 private static final Strategy DAY_OF_WEEK_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK) { 965 @Override 966 int modify(final FastDateParser parser, final int iValue) { 967 return iValue != 7 ? iValue + 1 : Calendar.SUNDAY; 968 } 969 }; 970 private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH); 971 private static final Strategy HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY); 972 private static final Strategy HOUR24_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY) { 973 @Override 974 int modify(final FastDateParser parser, final int iValue) { 975 return iValue == 24 ? 0 : iValue; 976 } 977 }; 978 private static final Strategy HOUR12_STRATEGY = new NumberStrategy(Calendar.HOUR) { 979 @Override 980 int modify(final FastDateParser parser, final int iValue) { 981 return iValue == 12 ? 0 : iValue; 982 } 983 }; 984 private static final Strategy HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR); 985 private static final Strategy MINUTE_STRATEGY = new NumberStrategy(Calendar.MINUTE); 986 private static final Strategy SECOND_STRATEGY = new NumberStrategy(Calendar.SECOND); 987 private static final Strategy MILLISECOND_STRATEGY = new NumberStrategy(Calendar.MILLISECOND); 988}