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