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.Date; 028import java.util.List; 029import java.util.Locale; 030import java.util.Map; 031import java.util.SortedMap; 032import java.util.TimeZone; 033import java.util.TreeMap; 034import java.util.concurrent.ConcurrentHashMap; 035import java.util.concurrent.ConcurrentMap; 036import java.util.regex.Matcher; 037import java.util.regex.Pattern; 038 039/** 040 * <p>FastDateParser is a fast and thread-safe version of 041 * {@link java.text.SimpleDateFormat}.</p> 042 * 043 * <p>This class can be used as a direct replacement for 044 * <code>SimpleDateFormat</code> in most parsing situations. 045 * This class is especially useful in multi-threaded server environments. 046 * <code>SimpleDateFormat</code> is not thread-safe in any JDK version, 047 * nor will it be as Sun have closed the 048 * <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4228335">bug</a>/RFE. 049 * </p> 050 * 051 * <p>Only parsing is supported, but all patterns are compatible with 052 * SimpleDateFormat.</p> 053 * 054 * <p>Timing tests indicate this class is as about as fast as SimpleDateFormat 055 * in single thread applications and about 25% faster in multi-thread applications.</p> 056 * 057 * <p>Note that the code only handles Gregorian calendars. The following non-Gregorian 058 * calendars use SimpleDateFormat internally, and so will be slower: 059 * <ul> 060 * <li>ja_JP_TH - Japanese Imperial</li> 061 * <li>th_TH (any variant) - Thai Buddhist</li> 062 * </ul> 063 * </p> 064 * 065 * @version $Id: FastDateParser.java 1555485 2014-01-05 12:08:29Z britter $ 066 * @since 3.2 067 */ 068public class FastDateParser implements DateParser, Serializable { 069 /** 070 * Required for serialization support. 071 * 072 * @see java.io.Serializable 073 */ 074 private static final long serialVersionUID = 1L; 075 076 static final Locale JAPANESE_IMPERIAL = new Locale("ja","JP","JP"); 077 078 // defining fields 079 private final String pattern; 080 private final TimeZone timeZone; 081 private final Locale locale; 082 083 // derived fields 084 private transient Pattern parsePattern; 085 private transient Strategy[] strategies; 086 private transient int thisYear; 087 088 // dynamic fields to communicate with Strategy 089 private transient String currentFormatField; 090 private transient Strategy nextStrategy; 091 092 /** 093 * <p>Constructs a new FastDateParser.</p> 094 * 095 * @param pattern non-null {@link java.text.SimpleDateFormat} compatible 096 * pattern 097 * @param timeZone non-null time zone to use 098 * @param locale non-null locale 099 */ 100 protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale) { 101 this.pattern = pattern; 102 this.timeZone = timeZone; 103 this.locale = locale; 104 init(); 105 } 106 107 /** 108 * Initialize derived fields from defining fields. 109 * This is called from constructor and from readObject (de-serialization) 110 */ 111 private void init() { 112 final Calendar definingCalendar = Calendar.getInstance(timeZone, locale); 113 thisYear= definingCalendar.get(Calendar.YEAR); 114 115 final StringBuilder regex= new StringBuilder(); 116 final List<Strategy> collector = new ArrayList<Strategy>(); 117 118 final Matcher patternMatcher= formatPattern.matcher(pattern); 119 if(!patternMatcher.lookingAt()) { 120 throw new IllegalArgumentException( 121 "Illegal pattern character '" + pattern.charAt(patternMatcher.regionStart()) + "'"); 122 } 123 124 currentFormatField= patternMatcher.group(); 125 Strategy currentStrategy= getStrategy(currentFormatField, definingCalendar); 126 for(;;) { 127 patternMatcher.region(patternMatcher.end(), patternMatcher.regionEnd()); 128 if(!patternMatcher.lookingAt()) { 129 nextStrategy = null; 130 break; 131 } 132 final String nextFormatField= patternMatcher.group(); 133 nextStrategy = getStrategy(nextFormatField, definingCalendar); 134 if(currentStrategy.addRegex(this, regex)) { 135 collector.add(currentStrategy); 136 } 137 currentFormatField= nextFormatField; 138 currentStrategy= nextStrategy; 139 } 140 if (patternMatcher.regionStart() != patternMatcher.regionEnd()) { 141 throw new IllegalArgumentException("Failed to parse \""+pattern+"\" ; gave up at index "+patternMatcher.regionStart()); 142 } 143 if(currentStrategy.addRegex(this, regex)) { 144 collector.add(currentStrategy); 145 } 146 currentFormatField= null; 147 strategies= collector.toArray(new Strategy[collector.size()]); 148 parsePattern= Pattern.compile(regex.toString()); 149 } 150 151 // Accessors 152 //----------------------------------------------------------------------- 153 /* (non-Javadoc) 154 * @see org.apache.commons.lang3.time.DateParser#getPattern() 155 */ 156 @Override 157 public String getPattern() { 158 return pattern; 159 } 160 161 /* (non-Javadoc) 162 * @see org.apache.commons.lang3.time.DateParser#getTimeZone() 163 */ 164 @Override 165 public TimeZone getTimeZone() { 166 return timeZone; 167 } 168 169 /* (non-Javadoc) 170 * @see org.apache.commons.lang3.time.DateParser#getLocale() 171 */ 172 @Override 173 public Locale getLocale() { 174 return locale; 175 } 176 177 /** 178 * Returns the generated pattern (for testing purposes). 179 * 180 * @return the generated pattern 181 */ 182 Pattern getParsePattern() { 183 return parsePattern; 184 } 185 186 // Basics 187 //----------------------------------------------------------------------- 188 /** 189 * <p>Compare another object for equality with this object.</p> 190 * 191 * @param obj the object to compare to 192 * @return <code>true</code>if equal to this instance 193 */ 194 @Override 195 public boolean equals(final Object obj) { 196 if (! (obj instanceof FastDateParser) ) { 197 return false; 198 } 199 final FastDateParser other = (FastDateParser) obj; 200 return pattern.equals(other.pattern) 201 && timeZone.equals(other.timeZone) 202 && locale.equals(other.locale); 203 } 204 205 /** 206 * <p>Return a hashcode compatible with equals.</p> 207 * 208 * @return a hashcode compatible with equals 209 */ 210 @Override 211 public int hashCode() { 212 return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode()); 213 } 214 215 /** 216 * <p>Get a string version of this formatter.</p> 217 * 218 * @return a debugging string 219 */ 220 @Override 221 public String toString() { 222 return "FastDateParser[" + pattern + "," + locale + "," + timeZone.getID() + "]"; 223 } 224 225 // Serializing 226 //----------------------------------------------------------------------- 227 /** 228 * Create the object after serialization. This implementation reinitializes the 229 * transient properties. 230 * 231 * @param in ObjectInputStream from which the object is being deserialized. 232 * @throws IOException if there is an IO issue. 233 * @throws ClassNotFoundException if a class cannot be found. 234 */ 235 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { 236 in.defaultReadObject(); 237 init(); 238 } 239 240 /* (non-Javadoc) 241 * @see org.apache.commons.lang3.time.DateParser#parseObject(java.lang.String) 242 */ 243 @Override 244 public Object parseObject(final String source) throws ParseException { 245 return parse(source); 246 } 247 248 /* (non-Javadoc) 249 * @see org.apache.commons.lang3.time.DateParser#parse(java.lang.String) 250 */ 251 @Override 252 public Date parse(final String source) throws ParseException { 253 final Date date= parse(source, new ParsePosition(0)); 254 if(date==null) { 255 // Add a note re supported date range 256 if (locale.equals(JAPANESE_IMPERIAL)) { 257 throw new ParseException( 258 "(The " +locale + " locale does not support dates before 1868 AD)\n" + 259 "Unparseable date: \""+source+"\" does not match "+parsePattern.pattern(), 0); 260 } 261 throw new ParseException("Unparseable date: \""+source+"\" does not match "+parsePattern.pattern(), 0); 262 } 263 return date; 264 } 265 266 /* (non-Javadoc) 267 * @see org.apache.commons.lang3.time.DateParser#parseObject(java.lang.String, java.text.ParsePosition) 268 */ 269 @Override 270 public Object parseObject(final String source, final ParsePosition pos) { 271 return parse(source, pos); 272 } 273 274 /* (non-Javadoc) 275 * @see org.apache.commons.lang3.time.DateParser#parse(java.lang.String, java.text.ParsePosition) 276 */ 277 @Override 278 public Date parse(final String source, final ParsePosition pos) { 279 final int offset= pos.getIndex(); 280 final Matcher matcher= parsePattern.matcher(source.substring(offset)); 281 if(!matcher.lookingAt()) { 282 return null; 283 } 284 // timing tests indicate getting new instance is 19% faster than cloning 285 final Calendar cal= Calendar.getInstance(timeZone, locale); 286 cal.clear(); 287 288 for(int i=0; i<strategies.length;) { 289 final Strategy strategy= strategies[i++]; 290 strategy.setCalendar(this, cal, matcher.group(i)); 291 } 292 pos.setIndex(offset+matcher.end()); 293 return cal.getTime(); 294 } 295 296 // Support for strategies 297 //----------------------------------------------------------------------- 298 299 /** 300 * Escape constant fields into regular expression 301 * @param regex The destination regex 302 * @param value The source field 303 * @param unquote If true, replace two success quotes ('') with single quote (') 304 * @return The <code>StringBuilder</code> 305 */ 306 private static StringBuilder escapeRegex(final StringBuilder regex, final String value, final boolean unquote) { 307 regex.append("\\Q"); 308 for(int i= 0; i<value.length(); ++i) { 309 char c= value.charAt(i); 310 switch(c) { 311 case '\'': 312 if(unquote) { 313 if(++i==value.length()) { 314 return regex; 315 } 316 c= value.charAt(i); 317 } 318 break; 319 case '\\': 320 if(++i==value.length()) { 321 break; 322 } 323 /* 324 * If we have found \E, we replace it with \E\\E\Q, i.e. we stop the quoting, 325 * quote the \ in \E, then restart the quoting. 326 * 327 * Otherwise we just output the two characters. 328 * In each case the initial \ needs to be output and the final char is done at the end 329 */ 330 regex.append(c); // we always want the original \ 331 c = value.charAt(i); // Is it followed by E ? 332 if (c == 'E') { // \E detected 333 regex.append("E\\\\E\\"); // see comment above 334 c = 'Q'; // appended below 335 } 336 break; 337 } 338 regex.append(c); 339 } 340 regex.append("\\E"); 341 return regex; 342 } 343 344 345 /** 346 * Get the short and long values displayed for a field 347 * @param field The field of interest 348 * @param definingCalendar The calendar to obtain the short and long values 349 * @param locale The locale of display names 350 * @return A Map of the field key / value pairs 351 */ 352 private static Map<String, Integer> getDisplayNames(final int field, final Calendar definingCalendar, final Locale locale) { 353 return definingCalendar.getDisplayNames(field, Calendar.ALL_STYLES, locale); 354 } 355 356 /** 357 * Adjust dates to be within 80 years before and 20 years after instantiation 358 * @param twoDigitYear The year to adjust 359 * @return A value within -80 and +20 years from instantiation of this instance 360 */ 361 int adjustYear(final int twoDigitYear) { 362 final int trial= twoDigitYear + thisYear - thisYear%100; 363 if(trial < thisYear+20) { 364 return trial; 365 } 366 return trial-100; 367 } 368 369 /** 370 * Is the next field a number? 371 * @return true, if next field will be a number 372 */ 373 boolean isNextNumber() { 374 return nextStrategy!=null && nextStrategy.isNumber(); 375 } 376 377 /** 378 * What is the width of the current field? 379 * @return The number of characters in the current format field 380 */ 381 int getFieldWidth() { 382 return currentFormatField.length(); 383 } 384 385 /** 386 * A strategy to parse a single field from the parsing pattern 387 */ 388 private static abstract class Strategy { 389 /** 390 * Is this field a number? 391 * The default implementation returns false. 392 * 393 * @return true, if field is a number 394 */ 395 boolean isNumber() { 396 return false; 397 } 398 /** 399 * Set the Calendar with the parsed field. 400 * 401 * The default implementation does nothing. 402 * 403 * @param parser The parser calling this strategy 404 * @param cal The <code>Calendar</code> to set 405 * @param value The parsed field to translate and set in cal 406 */ 407 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { 408 409 } 410 /** 411 * Generate a <code>Pattern</code> regular expression to the <code>StringBuilder</code> 412 * which will accept this field 413 * @param parser The parser calling this strategy 414 * @param regex The <code>StringBuilder</code> to append to 415 * @return true, if this field will set the calendar; 416 * false, if this field is a constant value 417 */ 418 abstract boolean addRegex(FastDateParser parser, StringBuilder regex); 419 } 420 421 /** 422 * A <code>Pattern</code> to parse the user supplied SimpleDateFormat pattern 423 */ 424 private static final Pattern formatPattern= Pattern.compile( 425 "D+|E+|F+|G+|H+|K+|M+|S+|W+|Z+|a+|d+|h+|k+|m+|s+|w+|y+|z+|''|'[^']++(''[^']*+)*+'|[^'A-Za-z]++"); 426 427 /** 428 * Obtain a Strategy given a field from a SimpleDateFormat pattern 429 * @param formatField A sub-sequence of the SimpleDateFormat pattern 430 * @param definingCalendar The calendar to obtain the short and long values 431 * @return The Strategy that will handle parsing for the field 432 */ 433 private Strategy getStrategy(String formatField, final Calendar definingCalendar) { 434 switch(formatField.charAt(0)) { 435 case '\'': 436 if(formatField.length()>2) { 437 formatField= formatField.substring(1, formatField.length()-1); 438 } 439 //$FALL-THROUGH$ 440 default: 441 return new CopyQuotedStrategy(formatField); 442 case 'D': 443 return DAY_OF_YEAR_STRATEGY; 444 case 'E': 445 return getLocaleSpecificStrategy(Calendar.DAY_OF_WEEK, definingCalendar); 446 case 'F': 447 return DAY_OF_WEEK_IN_MONTH_STRATEGY; 448 case 'G': 449 return getLocaleSpecificStrategy(Calendar.ERA, definingCalendar); 450 case 'H': 451 return MODULO_HOUR_OF_DAY_STRATEGY; 452 case 'K': 453 return HOUR_STRATEGY; 454 case 'M': 455 return formatField.length()>=3 ?getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar) :NUMBER_MONTH_STRATEGY; 456 case 'S': 457 return MILLISECOND_STRATEGY; 458 case 'W': 459 return WEEK_OF_MONTH_STRATEGY; 460 case 'a': 461 return getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar); 462 case 'd': 463 return DAY_OF_MONTH_STRATEGY; 464 case 'h': 465 return MODULO_HOUR_STRATEGY; 466 case 'k': 467 return HOUR_OF_DAY_STRATEGY; 468 case 'm': 469 return MINUTE_STRATEGY; 470 case 's': 471 return SECOND_STRATEGY; 472 case 'w': 473 return WEEK_OF_YEAR_STRATEGY; 474 case 'y': 475 return formatField.length()>2 ?LITERAL_YEAR_STRATEGY :ABBREVIATED_YEAR_STRATEGY; 476 case 'Z': 477 case 'z': 478 return getLocaleSpecificStrategy(Calendar.ZONE_OFFSET, definingCalendar); 479 } 480 } 481 482 @SuppressWarnings("unchecked") // OK because we are creating an array with no entries 483 private static ConcurrentMap<Locale, Strategy>[] caches = new ConcurrentMap[Calendar.FIELD_COUNT]; 484 485 /** 486 * Get a cache of Strategies for a particular field 487 * @param field The Calendar field 488 * @return a cache of Locale to Strategy 489 */ 490 private static ConcurrentMap<Locale, Strategy> getCache(final int field) { 491 synchronized(caches) { 492 if(caches[field]==null) { 493 caches[field]= new ConcurrentHashMap<Locale,Strategy>(3); 494 } 495 return caches[field]; 496 } 497 } 498 499 /** 500 * Construct a Strategy that parses a Text field 501 * @param field The Calendar field 502 * @param definingCalendar The calendar to obtain the short and long values 503 * @return a TextStrategy for the field and Locale 504 */ 505 private Strategy getLocaleSpecificStrategy(final int field, final Calendar definingCalendar) { 506 final ConcurrentMap<Locale,Strategy> cache = getCache(field); 507 Strategy strategy= cache.get(locale); 508 if(strategy==null) { 509 strategy= field==Calendar.ZONE_OFFSET 510 ? new TimeZoneStrategy(locale) 511 : new TextStrategy(field, definingCalendar, locale); 512 final Strategy inCache= cache.putIfAbsent(locale, strategy); 513 if(inCache!=null) { 514 return inCache; 515 } 516 } 517 return strategy; 518 } 519 520 /** 521 * A strategy that copies the static or quoted field in the parsing pattern 522 */ 523 private static class CopyQuotedStrategy extends Strategy { 524 private final String formatField; 525 526 /** 527 * Construct a Strategy that ensures the formatField has literal text 528 * @param formatField The literal text to match 529 */ 530 CopyQuotedStrategy(final String formatField) { 531 this.formatField= formatField; 532 } 533 534 /** 535 * {@inheritDoc} 536 */ 537 @Override 538 boolean isNumber() { 539 char c= formatField.charAt(0); 540 if(c=='\'') { 541 c= formatField.charAt(1); 542 } 543 return Character.isDigit(c); 544 } 545 546 /** 547 * {@inheritDoc} 548 */ 549 @Override 550 boolean addRegex(final FastDateParser parser, final StringBuilder regex) { 551 escapeRegex(regex, formatField, true); 552 return false; 553 } 554 } 555 556 /** 557 * A strategy that handles a text field in the parsing pattern 558 */ 559 private static class TextStrategy extends Strategy { 560 private final int field; 561 private final Map<String, Integer> keyValues; 562 563 /** 564 * Construct a Strategy that parses a Text field 565 * @param field The Calendar field 566 * @param definingCalendar The Calendar to use 567 * @param locale The Locale to use 568 */ 569 TextStrategy(final int field, final Calendar definingCalendar, final Locale locale) { 570 this.field= field; 571 this.keyValues= getDisplayNames(field, definingCalendar, locale); 572 } 573 574 /** 575 * {@inheritDoc} 576 */ 577 @Override 578 boolean addRegex(final FastDateParser parser, final StringBuilder regex) { 579 regex.append('('); 580 for(final String textKeyValue : keyValues.keySet()) { 581 escapeRegex(regex, textKeyValue, false).append('|'); 582 } 583 regex.setCharAt(regex.length()-1, ')'); 584 return true; 585 } 586 587 /** 588 * {@inheritDoc} 589 */ 590 @Override 591 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { 592 final Integer iVal = keyValues.get(value); 593 if(iVal == null) { 594 final StringBuilder sb= new StringBuilder(value); 595 sb.append(" not in ("); 596 for(final String textKeyValue : keyValues.keySet()) { 597 sb.append(textKeyValue).append(' '); 598 } 599 sb.setCharAt(sb.length()-1, ')'); 600 throw new IllegalArgumentException(sb.toString()); 601 } 602 cal.set(field, iVal.intValue()); 603 } 604 } 605 606 607 /** 608 * A strategy that handles a number field in the parsing pattern 609 */ 610 private static class NumberStrategy extends Strategy { 611 private final int field; 612 613 /** 614 * Construct a Strategy that parses a Number field 615 * @param field The Calendar field 616 */ 617 NumberStrategy(final int field) { 618 this.field= field; 619 } 620 621 /** 622 * {@inheritDoc} 623 */ 624 @Override 625 boolean isNumber() { 626 return true; 627 } 628 629 /** 630 * {@inheritDoc} 631 */ 632 @Override 633 boolean addRegex(final FastDateParser parser, final StringBuilder regex) { 634 if(parser.isNextNumber()) { 635 regex.append("(\\p{IsNd}{").append(parser.getFieldWidth()).append("}+)"); 636 } 637 else { 638 regex.append("(\\p{IsNd}++)"); 639 } 640 return true; 641 } 642 643 /** 644 * {@inheritDoc} 645 */ 646 @Override 647 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { 648 cal.set(field, modify(Integer.parseInt(value))); 649 } 650 651 /** 652 * Make any modifications to parsed integer 653 * @param iValue The parsed integer 654 * @return The modified value 655 */ 656 int modify(final int iValue) { 657 return iValue; 658 } 659 } 660 661 private static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR) { 662 /** 663 * {@inheritDoc} 664 */ 665 @Override 666 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { 667 int iValue= Integer.parseInt(value); 668 if(iValue<100) { 669 iValue= parser.adjustYear(iValue); 670 } 671 cal.set(Calendar.YEAR, iValue); 672 } 673 }; 674 675 /** 676 * A strategy that handles a timezone field in the parsing pattern 677 */ 678 private static class TimeZoneStrategy extends Strategy { 679 680 private final String validTimeZoneChars; 681 private final SortedMap<String, TimeZone> tzNames= new TreeMap<String, TimeZone>(String.CASE_INSENSITIVE_ORDER); 682 683 /** 684 * Index of zone id 685 */ 686 private static final int ID = 0; 687 /** 688 * Index of the long name of zone in standard time 689 */ 690 private static final int LONG_STD = 1; 691 /** 692 * Index of the short name of zone in standard time 693 */ 694 private static final int SHORT_STD = 2; 695 /** 696 * Index of the long name of zone in daylight saving time 697 */ 698 private static final int LONG_DST = 3; 699 /** 700 * Index of the short name of zone in daylight saving time 701 */ 702 private static final int SHORT_DST = 4; 703 704 /** 705 * Construct a Strategy that parses a TimeZone 706 * @param locale The Locale 707 */ 708 TimeZoneStrategy(final Locale locale) { 709 final String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings(); 710 for (String[] zone : zones) { 711 if (zone[ID].startsWith("GMT")) { 712 continue; 713 } 714 final TimeZone tz = TimeZone.getTimeZone(zone[ID]); 715 if (!tzNames.containsKey(zone[LONG_STD])){ 716 tzNames.put(zone[LONG_STD], tz); 717 } 718 if (!tzNames.containsKey(zone[SHORT_STD])){ 719 tzNames.put(zone[SHORT_STD], tz); 720 } 721 if (tz.useDaylightTime()) { 722 if (!tzNames.containsKey(zone[LONG_DST])){ 723 tzNames.put(zone[LONG_DST], tz); 724 } 725 if (!tzNames.containsKey(zone[SHORT_DST])){ 726 tzNames.put(zone[SHORT_DST], tz); 727 } 728 } 729 } 730 731 final StringBuilder sb= new StringBuilder(); 732 sb.append("(GMT[+\\-]\\d{0,1}\\d{2}|[+\\-]\\d{2}:?\\d{2}|"); 733 for(final String id : tzNames.keySet()) { 734 escapeRegex(sb, id, false).append('|'); 735 } 736 sb.setCharAt(sb.length()-1, ')'); 737 validTimeZoneChars= sb.toString(); 738 } 739 740 /** 741 * {@inheritDoc} 742 */ 743 @Override 744 boolean addRegex(final FastDateParser parser, final StringBuilder regex) { 745 regex.append(validTimeZoneChars); 746 return true; 747 } 748 749 /** 750 * {@inheritDoc} 751 */ 752 @Override 753 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { 754 TimeZone tz; 755 if(value.charAt(0)=='+' || value.charAt(0)=='-') { 756 tz= TimeZone.getTimeZone("GMT"+value); 757 } 758 else if(value.startsWith("GMT")) { 759 tz= TimeZone.getTimeZone(value); 760 } 761 else { 762 tz= tzNames.get(value); 763 if(tz==null) { 764 throw new IllegalArgumentException(value + " is not a supported timezone name"); 765 } 766 } 767 cal.setTimeZone(tz); 768 } 769 } 770 771 private static final Strategy NUMBER_MONTH_STRATEGY = new NumberStrategy(Calendar.MONTH) { 772 @Override 773 int modify(final int iValue) { 774 return iValue-1; 775 } 776 }; 777 private static final Strategy LITERAL_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR); 778 private static final Strategy WEEK_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_YEAR); 779 private static final Strategy WEEK_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_MONTH); 780 private static final Strategy DAY_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.DAY_OF_YEAR); 781 private static final Strategy DAY_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_MONTH); 782 private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH); 783 private static final Strategy HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY); 784 private static final Strategy MODULO_HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY) { 785 @Override 786 int modify(final int iValue) { 787 return iValue%24; 788 } 789 }; 790 private static final Strategy MODULO_HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR) { 791 @Override 792 int modify(final int iValue) { 793 return iValue%12; 794 } 795 }; 796 private static final Strategy HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR); 797 private static final Strategy MINUTE_STRATEGY = new NumberStrategy(Calendar.MINUTE); 798 private static final Strategy SECOND_STRATEGY = new NumberStrategy(Calendar.SECOND); 799 private static final Strategy MILLISECOND_STRATEGY = new NumberStrategy(Calendar.MILLISECOND); 800}