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(String left, 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 } 142 else if(locale.equals(JAPANESE_IMPERIAL)) { 143 centuryStartYear= 0; 144 } 145 else { 146 // from 80 years ago to 20 years from now 147 definingCalendar.setTime(new Date()); 148 centuryStartYear= definingCalendar.get(Calendar.YEAR)-80; 149 } 150 century= centuryStartYear / 100 * 100; 151 startYear= centuryStartYear - century; 152 153 init(definingCalendar); 154 } 155 156 /** 157 * Initialize derived fields from defining fields. 158 * This is called from constructor and from readObject (de-serialization) 159 * 160 * @param definingCalendar the {@link java.util.Calendar} instance used to initialize this FastDateParser 161 */ 162 private void init(final Calendar definingCalendar) { 163 patterns = new ArrayList<StrategyAndWidth>(); 164 165 StrategyParser fm = new StrategyParser(pattern, definingCalendar); 166 for(;;) { 167 StrategyAndWidth field = fm.getNextStrategy(); 168 if(field==null) { 169 break; 170 } 171 patterns.add(field); 172 } 173 } 174 175 // helper classes to parse the format string 176 //----------------------------------------------------------------------- 177 178 /** 179 * Holds strategy and field width 180 */ 181 private static class StrategyAndWidth { 182 final Strategy strategy; 183 final int width; 184 185 StrategyAndWidth(Strategy strategy, int width) { 186 this.strategy = strategy; 187 this.width = width; 188 } 189 190 int getMaxWidth(ListIterator<StrategyAndWidth> lt) { 191 if(!strategy.isNumber() || !lt.hasNext()) { 192 return 0; 193 } 194 Strategy nextStrategy = lt.next().strategy; 195 lt.previous(); 196 return nextStrategy.isNumber() ?width :0; 197 } 198 } 199 200 /** 201 * Parse format into Strategies 202 */ 203 private class StrategyParser { 204 final private String pattern; 205 final private Calendar definingCalendar; 206 private int currentIdx; 207 208 StrategyParser(String pattern, Calendar definingCalendar) { 209 this.pattern = pattern; 210 this.definingCalendar = definingCalendar; 211 } 212 213 StrategyAndWidth getNextStrategy() { 214 if (currentIdx >= pattern.length()) { 215 return null; 216 } 217 218 char c = pattern.charAt(currentIdx); 219 if (isFormatLetter(c)) { 220 return letterPattern(c); 221 } 222 return literal(); 223 } 224 225 private StrategyAndWidth letterPattern(char c) { 226 int begin = currentIdx; 227 while (++currentIdx < pattern.length()) { 228 if (pattern.charAt(currentIdx) != c) { 229 break; 230 } 231 } 232 233 int width = currentIdx - begin; 234 return new StrategyAndWidth(getStrategy(c, width, definingCalendar), width); 235 } 236 237 private StrategyAndWidth literal() { 238 boolean activeQuote = false; 239 240 StringBuilder sb = new StringBuilder(); 241 while (currentIdx < pattern.length()) { 242 char c = pattern.charAt(currentIdx); 243 if (!activeQuote && isFormatLetter(c)) { 244 break; 245 } else if (c == '\'' && (++currentIdx == pattern.length() || pattern.charAt(currentIdx) != '\'')) { 246 activeQuote = !activeQuote; 247 continue; 248 } 249 ++currentIdx; 250 sb.append(c); 251 } 252 253 if (activeQuote) { 254 throw new IllegalArgumentException("Unterminated quote"); 255 } 256 257 String formatField = sb.toString(); 258 return new StrategyAndWidth(new CopyQuotedStrategy(formatField), formatField.length()); 259 } 260 } 261 262 private static boolean isFormatLetter(char c) { 263 return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z'; 264 } 265 266 // Accessors 267 //----------------------------------------------------------------------- 268 /* (non-Javadoc) 269 * @see org.apache.commons.lang3.time.DateParser#getPattern() 270 */ 271 @Override 272 public String getPattern() { 273 return pattern; 274 } 275 276 /* (non-Javadoc) 277 * @see org.apache.commons.lang3.time.DateParser#getTimeZone() 278 */ 279 @Override 280 public TimeZone getTimeZone() { 281 return timeZone; 282 } 283 284 /* (non-Javadoc) 285 * @see org.apache.commons.lang3.time.DateParser#getLocale() 286 */ 287 @Override 288 public Locale getLocale() { 289 return locale; 290 } 291 292 293 // Basics 294 //----------------------------------------------------------------------- 295 /** 296 * <p>Compare another object for equality with this object.</p> 297 * 298 * @param obj the object to compare to 299 * @return <code>true</code>if equal to this instance 300 */ 301 @Override 302 public boolean equals(final Object obj) { 303 if (!(obj instanceof FastDateParser)) { 304 return false; 305 } 306 final FastDateParser other = (FastDateParser) obj; 307 return pattern.equals(other.pattern) 308 && timeZone.equals(other.timeZone) 309 && locale.equals(other.locale); 310 } 311 312 /** 313 * <p>Return a hashcode compatible with equals.</p> 314 * 315 * @return a hashcode compatible with equals 316 */ 317 @Override 318 public int hashCode() { 319 return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode()); 320 } 321 322 /** 323 * <p>Get a string version of this formatter.</p> 324 * 325 * @return a debugging string 326 */ 327 @Override 328 public String toString() { 329 return "FastDateParser[" + pattern + "," + locale + "," + timeZone.getID() + "]"; 330 } 331 332 // Serializing 333 //----------------------------------------------------------------------- 334 /** 335 * Create the object after serialization. This implementation reinitializes the 336 * transient properties. 337 * 338 * @param in ObjectInputStream from which the object is being deserialized. 339 * @throws IOException if there is an IO issue. 340 * @throws ClassNotFoundException if a class cannot be found. 341 */ 342 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { 343 in.defaultReadObject(); 344 345 final Calendar definingCalendar = Calendar.getInstance(timeZone, locale); 346 init(definingCalendar); 347 } 348 349 /* (non-Javadoc) 350 * @see org.apache.commons.lang3.time.DateParser#parseObject(java.lang.String) 351 */ 352 @Override 353 public Object parseObject(final String source) throws ParseException { 354 return parse(source); 355 } 356 357 /* (non-Javadoc) 358 * @see org.apache.commons.lang3.time.DateParser#parse(java.lang.String) 359 */ 360 @Override 361 public Date parse(final String source) throws ParseException { 362 ParsePosition pp = new ParsePosition(0); 363 final Date date= parse(source, pp); 364 if (date == null) { 365 // Add a note re supported date range 366 if (locale.equals(JAPANESE_IMPERIAL)) { 367 throw new ParseException( 368 "(The " +locale + " locale does not support dates before 1868 AD)\n" + 369 "Unparseable date: \""+source, pp.getErrorIndex()); 370 } 371 throw new ParseException("Unparseable date: "+source, pp.getErrorIndex()); 372 } 373 return date; 374 } 375 376 /* (non-Javadoc) 377 * @see org.apache.commons.lang3.time.DateParser#parseObject(java.lang.String, java.text.ParsePosition) 378 */ 379 @Override 380 public Object parseObject(final String source, final ParsePosition pos) { 381 return parse(source, pos); 382 } 383 384 /** 385 * This implementation updates the ParsePosition if the parse succeeds. 386 * However, it sets the error index to the position before the failed field unlike 387 * the method {@link java.text.SimpleDateFormat#parse(String, ParsePosition)} which sets 388 * the error index to after the failed field. 389 * <p> 390 * To determine if the parse has succeeded, the caller must check if the current parse position 391 * given by {@link ParsePosition#getIndex()} has been updated. If the input buffer has been fully 392 * parsed, then the index will point to just after the end of the input buffer. 393 * 394 * @see org.apache.commons.lang3.time.DateParser#parse(java.lang.String, java.text.ParsePosition) 395 */ 396 @Override 397 public Date parse(final String source, final ParsePosition pos) { 398 // timing tests indicate getting new instance is 19% faster than cloning 399 final Calendar cal= Calendar.getInstance(timeZone, locale); 400 cal.clear(); 401 402 return parse(source, pos, cal) ? cal.getTime() : null; 403 } 404 405 /** 406 * Parse a formatted date string according to the format. Updates the Calendar with parsed fields. 407 * Upon success, the ParsePosition index is updated to indicate how much of the source text was consumed. 408 * Not all source text needs to be consumed. Upon parse failure, ParsePosition error index is updated to 409 * the offset of the source text which does not match the supplied format. 410 * 411 * @param source The text to parse. 412 * @param pos On input, the position in the source to start parsing, on output, updated position. 413 * @param calendar The calendar into which to set parsed fields. 414 * @return true, if source has been parsed (pos parsePosition is updated); otherwise false (and pos errorIndex is updated) 415 * @throws IllegalArgumentException when Calendar has been set to be not lenient, and a parsed field is 416 * out of range. 417 */ 418 @Override 419 public boolean parse(final String source, final ParsePosition pos, final Calendar calendar) { 420 ListIterator<StrategyAndWidth> lt = patterns.listIterator(); 421 while (lt.hasNext()) { 422 StrategyAndWidth pattern = lt.next(); 423 int maxWidth = pattern.getMaxWidth(lt); 424 if (!pattern.strategy.parse(this, calendar, source, pos, maxWidth)) { 425 return false; 426 } 427 } 428 return true; 429 } 430 431 // Support for strategies 432 //----------------------------------------------------------------------- 433 434 private static StringBuilder simpleQuote(final StringBuilder sb, final String value) { 435 for (int i = 0; i < value.length(); ++i) { 436 char c = value.charAt(i); 437 switch (c) { 438 case '\\': 439 case '^': 440 case '$': 441 case '.': 442 case '|': 443 case '?': 444 case '*': 445 case '+': 446 case '(': 447 case ')': 448 case '[': 449 case '{': 450 sb.append('\\'); 451 default: 452 sb.append(c); 453 } 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(Calendar cal, Locale locale, int field, StringBuilder regex) { 467 Map<String, Integer> values = new HashMap<String, Integer>(); 468 469 Map<String, Integer> displayNames = cal.getDisplayNames(field, Calendar.ALL_STYLES, locale); 470 TreeSet<String> sorted = new TreeSet<String>(LONGER_FIRST_LOWERCASE); 471 for (Map.Entry<String, Integer> displayName : displayNames.entrySet()) { 472 String key = displayName.getKey().toLowerCase(locale); 473 if (sorted.add(key)) { 474 values.put(key, displayName.getValue()); 475 } 476 } 477 for (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 static abstract 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 static abstract class PatternStrategy extends Strategy { 514 515 private Pattern pattern; 516 517 void createPattern(StringBuilder regex) { 518 createPattern(regex.toString()); 519 } 520 521 void createPattern(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(FastDateParser parser, Calendar calendar, String source, ParsePosition pos, int maxWidth) { 538 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 formatField 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(char f, 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<Locale, Strategy>(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 final private 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(FastDateParser parser, Calendar calendar, String source, ParsePosition pos, int maxWidth) { 673 for (int idx = 0; idx < formatField.length(); ++idx) { 674 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 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(FastDateParser parser, Calendar cal, String value) { 720 final Integer iVal = lKeyValues.get(value.toLowerCase(locale)); 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 * Construct 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(FastDateParser parser, Calendar calendar, String source, ParsePosition pos, 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 char c = source.charAt(idx); 757 if (!Character.isWhitespace(c)) { 758 break; 759 } 760 } 761 pos.setIndex(idx); 762 } else { 763 int end = idx + maxWidth; 764 if (last > end) { 765 last = end; 766 } 767 } 768 769 for (; idx < last; ++idx) { 770 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 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(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(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= "GMT[+-]\\d{1,2}:\\d{2}"; 816 817 private final Locale locale; 818 private final Map<String, TzInfo> tzNames= new HashMap<String, TzInfo>(); 819 820 private static class TzInfo { 821 TimeZone zone; 822 int dstOffset; 823 824 TzInfo(TimeZone tz, 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 * Construct 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<String>(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("GMT")) { 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 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 } 869 String key = zoneNames[i].toLowerCase(locale); 870 // ignore the data associated with duplicates supplied in 871 // the additional names 872 if (sorted.add(key)) { 873 tzNames.put(key, tzInfo); 874 } 875 } 876 } 877 // order the regex alternatives with longer strings first, greedy 878 // match will ensure longest string will be consumed 879 for (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 TimeZone tz = TimeZone.getTimeZone("GMT" + value); 893 cal.setTimeZone(tz); 894 } else if (value.regionMatches(true, 0, "GMT", 0, 3)) { 895 TimeZone tz = TimeZone.getTimeZone(value.toUpperCase()); 896 cal.setTimeZone(tz); 897 } else { 898 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(String pattern) { 913 createPattern(pattern); 914 } 915 916 /** 917 * {@inheritDoc} 918 */ 919 @Override 920 void setCalendar(FastDateParser parser, Calendar cal, 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(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(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(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(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(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}