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