All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.commons.lang3.time.FastDateParser Maven / Gradle / Ivy

Go to download

Apache Commons Lang, a package of Java utility classes for the classes that are in java.lang's hierarchy, or are considered to be so standard as to justify existence in java.lang.

There is a newer version: 3.14.0
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.lang3.time;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.SortedMap;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 

FastDateParser is a fast and thread-safe version of * {@link java.text.SimpleDateFormat}.

* *

This class can be used as a direct replacement for * SimpleDateFormat in most parsing situations. * This class is especially useful in multi-threaded server environments. * SimpleDateFormat is not thread-safe in any JDK version, * nor will it be as Sun have closed the * bug/RFE. *

* *

Only parsing is supported, but all patterns are compatible with * SimpleDateFormat.

* *

Timing tests indicate this class is as about as fast as SimpleDateFormat * in single thread applications and about 25% faster in multi-thread applications.

* *

Note that the code only handles Gregorian calendars. The following non-Gregorian * calendars use SimpleDateFormat internally, and so will be slower: *

    *
  • ja_JP_TH - Japanese Imperial
  • *
  • th_TH (any variant) - Thai Buddhist
  • *
*

* * @version $Id: FastDateParser.java 1552662 2013-12-20 13:36:55Z britter $ * @since 3.2 */ public class FastDateParser implements DateParser, Serializable { /** * Required for serialization support. * * @see java.io.Serializable */ private static final long serialVersionUID = 1L; static final Locale JAPANESE_IMPERIAL = new Locale("ja","JP","JP"); // defining fields private final String pattern; private final TimeZone timeZone; private final Locale locale; // derived fields private transient Pattern parsePattern; private transient Strategy[] strategies; private transient int thisYear; // dynamic fields to communicate with Strategy private transient String currentFormatField; private transient Strategy nextStrategy; /** *

Constructs a new FastDateParser.

* * @param pattern non-null {@link java.text.SimpleDateFormat} compatible * pattern * @param timeZone non-null time zone to use * @param locale non-null locale */ protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale) { this.pattern = pattern; this.timeZone = timeZone; this.locale = locale; init(); } /** * Initialize derived fields from defining fields. * This is called from constructor and from readObject (de-serialization) */ private void init() { final Calendar definingCalendar = Calendar.getInstance(timeZone, locale); thisYear= definingCalendar.get(Calendar.YEAR); final StringBuilder regex= new StringBuilder(); final List collector = new ArrayList(); final Matcher patternMatcher= formatPattern.matcher(pattern); if(!patternMatcher.lookingAt()) { throw new IllegalArgumentException( "Illegal pattern character '" + pattern.charAt(patternMatcher.regionStart()) + "'"); } currentFormatField= patternMatcher.group(); Strategy currentStrategy= getStrategy(currentFormatField, definingCalendar); for(;;) { patternMatcher.region(patternMatcher.end(), patternMatcher.regionEnd()); if(!patternMatcher.lookingAt()) { nextStrategy = null; break; } final String nextFormatField= patternMatcher.group(); nextStrategy = getStrategy(nextFormatField, definingCalendar); if(currentStrategy.addRegex(this, regex)) { collector.add(currentStrategy); } currentFormatField= nextFormatField; currentStrategy= nextStrategy; } if (patternMatcher.regionStart() != patternMatcher.regionEnd()) { throw new IllegalArgumentException("Failed to parse \""+pattern+"\" ; gave up at index "+patternMatcher.regionStart()); } if(currentStrategy.addRegex(this, regex)) { collector.add(currentStrategy); } currentFormatField= null; strategies= collector.toArray(new Strategy[collector.size()]); parsePattern= Pattern.compile(regex.toString()); } // Accessors //----------------------------------------------------------------------- /* (non-Javadoc) * @see org.apache.commons.lang3.time.DateParser#getPattern() */ @Override public String getPattern() { return pattern; } /* (non-Javadoc) * @see org.apache.commons.lang3.time.DateParser#getTimeZone() */ @Override public TimeZone getTimeZone() { return timeZone; } /* (non-Javadoc) * @see org.apache.commons.lang3.time.DateParser#getLocale() */ @Override public Locale getLocale() { return locale; } /** * Returns the generated pattern (for testing purposes). * * @return the generated pattern */ Pattern getParsePattern() { return parsePattern; } // Basics //----------------------------------------------------------------------- /** *

Compare another object for equality with this object.

* * @param obj the object to compare to * @return trueif equal to this instance */ @Override public boolean equals(final Object obj) { if (! (obj instanceof FastDateParser) ) { return false; } final FastDateParser other = (FastDateParser) obj; return pattern.equals(other.pattern) && timeZone.equals(other.timeZone) && locale.equals(other.locale); } /** *

Return a hashcode compatible with equals.

* * @return a hashcode compatible with equals */ @Override public int hashCode() { return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode()); } /** *

Get a string version of this formatter.

* * @return a debugging string */ @Override public String toString() { return "FastDateParser[" + pattern + "," + locale + "," + timeZone.getID() + "]"; } // Serializing //----------------------------------------------------------------------- /** * Create the object after serialization. This implementation reinitializes the * transient properties. * * @param in ObjectInputStream from which the object is being deserialized. * @throws IOException if there is an IO issue. * @throws ClassNotFoundException if a class cannot be found. */ private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); init(); } /* (non-Javadoc) * @see org.apache.commons.lang3.time.DateParser#parseObject(java.lang.String) */ @Override public Object parseObject(final String source) throws ParseException { return parse(source); } /* (non-Javadoc) * @see org.apache.commons.lang3.time.DateParser#parse(java.lang.String) */ @Override public Date parse(final String source) throws ParseException { final Date date= parse(source, new ParsePosition(0)); if(date==null) { // Add a note re supported date range if (locale.equals(JAPANESE_IMPERIAL)) { throw new ParseException( "(The " +locale + " locale does not support dates before 1868 AD)\n" + "Unparseable date: \""+source+"\" does not match "+parsePattern.pattern(), 0); } throw new ParseException("Unparseable date: \""+source+"\" does not match "+parsePattern.pattern(), 0); } return date; } /* (non-Javadoc) * @see org.apache.commons.lang3.time.DateParser#parseObject(java.lang.String, java.text.ParsePosition) */ @Override public Object parseObject(final String source, final ParsePosition pos) { return parse(source, pos); } /* (non-Javadoc) * @see org.apache.commons.lang3.time.DateParser#parse(java.lang.String, java.text.ParsePosition) */ @Override public Date parse(final String source, final ParsePosition pos) { final int offset= pos.getIndex(); final Matcher matcher= parsePattern.matcher(source.substring(offset)); if(!matcher.lookingAt()) { return null; } // timing tests indicate getting new instance is 19% faster than cloning final Calendar cal= Calendar.getInstance(timeZone, locale); cal.clear(); for(int i=0; iStringBuilder
*/ private static StringBuilder escapeRegex(final StringBuilder regex, final String value, final boolean unquote) { regex.append("\\Q"); for(int i= 0; i getDisplayNames(final int field, final Calendar definingCalendar, final Locale locale) { return definingCalendar.getDisplayNames(field, Calendar.ALL_STYLES, locale); } /** * Adjust dates to be within 80 years before and 20 years after instantiation * @param twoDigitYear The year to adjust * @return A value within -80 and +20 years from instantiation of this instance */ int adjustYear(final int twoDigitYear) { final int trial= twoDigitYear + thisYear - thisYear%100; if(trial < thisYear+20) { return trial; } return trial-100; } /** * Is the next field a number? * @return true, if next field will be a number */ boolean isNextNumber() { return nextStrategy!=null && nextStrategy.isNumber(); } /** * What is the width of the current field? * @return The number of characters in the current format field */ int getFieldWidth() { return currentFormatField.length(); } /** * A strategy to parse a single field from the parsing pattern */ private static abstract class Strategy { /** * Is this field a number? * The default implementation returns true. * * @return true, if field is a number */ boolean isNumber() { return false; } /** * Set the Calendar with the parsed field. * * The default implementation does nothing. * * @param parser The parser calling this strategy * @param cal The Calendar to set * @param value The parsed field to translate and set in cal */ void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { } /** * Generate a Pattern regular expression to the StringBuilder * which will accept this field * @param parser The parser calling this strategy * @param regex The StringBuilder to append to * @return true, if this field will set the calendar; * false, if this field is a constant value */ abstract boolean addRegex(FastDateParser parser, StringBuilder regex); } /** * A Pattern to parse the user supplied SimpleDateFormat pattern */ private static final Pattern formatPattern= Pattern.compile( "D+|E+|F+|G+|H+|K+|M+|S+|W+|Z+|a+|d+|h+|k+|m+|s+|w+|y+|z+|''|'[^']++(''[^']*+)*+'|[^'A-Za-z]++"); /** * Obtain a Strategy given a field from a SimpleDateFormat pattern * @param formatField A sub-sequence of the SimpleDateFormat pattern * @param definingCalendar The calendar to obtain the short and long values * @return The Strategy that will handle parsing for the field */ private Strategy getStrategy(String formatField, final Calendar definingCalendar) { switch(formatField.charAt(0)) { case '\'': if(formatField.length()>2) { formatField= formatField.substring(1, formatField.length()-1); } //$FALL-THROUGH$ default: return new CopyQuotedStrategy(formatField); case 'D': return DAY_OF_YEAR_STRATEGY; case 'E': return getLocaleSpecificStrategy(Calendar.DAY_OF_WEEK, definingCalendar); case 'F': return DAY_OF_WEEK_IN_MONTH_STRATEGY; case 'G': return getLocaleSpecificStrategy(Calendar.ERA, definingCalendar); case 'H': return MODULO_HOUR_OF_DAY_STRATEGY; case 'K': return HOUR_STRATEGY; case 'M': return formatField.length()>=3 ?getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar) :NUMBER_MONTH_STRATEGY; case 'S': return MILLISECOND_STRATEGY; case 'W': return WEEK_OF_MONTH_STRATEGY; case 'a': return getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar); case 'd': return DAY_OF_MONTH_STRATEGY; case 'h': return MODULO_HOUR_STRATEGY; case 'k': return HOUR_OF_DAY_STRATEGY; case 'm': return MINUTE_STRATEGY; case 's': return SECOND_STRATEGY; case 'w': return WEEK_OF_YEAR_STRATEGY; case 'y': return formatField.length()>2 ?LITERAL_YEAR_STRATEGY :ABBREVIATED_YEAR_STRATEGY; case 'Z': case 'z': return getLocaleSpecificStrategy(Calendar.ZONE_OFFSET, definingCalendar); } } @SuppressWarnings("unchecked") // OK because we are creating an array with no entries private static ConcurrentMap[] caches = new ConcurrentMap[Calendar.FIELD_COUNT]; /** * Get a cache of Strategies for a particular field * @param field The Calendar field * @return a cache of Locale to Strategy */ private static ConcurrentMap getCache(final int field) { synchronized(caches) { if(caches[field]==null) { caches[field]= new ConcurrentHashMap(3); } return caches[field]; } } /** * Construct a Strategy that parses a Text field * @param field The Calendar field * @param definingCalendar The calendar to obtain the short and long values * @return a TextStrategy for the field and Locale */ private Strategy getLocaleSpecificStrategy(final int field, final Calendar definingCalendar) { final ConcurrentMap cache = getCache(field); Strategy strategy= cache.get(locale); if(strategy==null) { strategy= field==Calendar.ZONE_OFFSET ? new TimeZoneStrategy(locale) : new TextStrategy(field, definingCalendar, locale); final Strategy inCache= cache.putIfAbsent(locale, strategy); if(inCache!=null) { return inCache; } } return strategy; } /** * A strategy that copies the static or quoted field in the parsing pattern */ private static class CopyQuotedStrategy extends Strategy { private final String formatField; /** * Construct a Strategy that ensures the formatField has literal text * @param formatField The literal text to match */ CopyQuotedStrategy(final String formatField) { this.formatField= formatField; } /** * {@inheritDoc} */ @Override boolean isNumber() { char c= formatField.charAt(0); if(c=='\'') { c= formatField.charAt(1); } return Character.isDigit(c); } /** * {@inheritDoc} */ @Override boolean addRegex(final FastDateParser parser, final StringBuilder regex) { escapeRegex(regex, formatField, true); return false; } } /** * A strategy that handles a text field in the parsing pattern */ private static class TextStrategy extends Strategy { private final int field; private final Map keyValues; /** * Construct a Strategy that parses a Text field * @param field The Calendar field * @param definingCalendar The Calendar to use * @param locale The Locale to use */ TextStrategy(final int field, final Calendar definingCalendar, final Locale locale) { this.field= field; this.keyValues= getDisplayNames(field, definingCalendar, locale); } /** * {@inheritDoc} */ @Override boolean addRegex(final FastDateParser parser, final StringBuilder regex) { regex.append('('); for(final String textKeyValue : keyValues.keySet()) { escapeRegex(regex, textKeyValue, false).append('|'); } regex.setCharAt(regex.length()-1, ')'); return true; } /** * {@inheritDoc} */ @Override void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { final Integer iVal = keyValues.get(value); if(iVal == null) { final StringBuilder sb= new StringBuilder(value); sb.append(" not in ("); for(final String textKeyValue : keyValues.keySet()) { sb.append(textKeyValue).append(' '); } sb.setCharAt(sb.length()-1, ')'); throw new IllegalArgumentException(sb.toString()); } cal.set(field, iVal.intValue()); } } /** * A strategy that handles a number field in the parsing pattern */ private static class NumberStrategy extends Strategy { private final int field; /** * Construct a Strategy that parses a Number field * @param field The Calendar field */ NumberStrategy(final int field) { this.field= field; } /** * {@inheritDoc} */ @Override boolean isNumber() { return true; } /** * {@inheritDoc} */ @Override boolean addRegex(final FastDateParser parser, final StringBuilder regex) { if(parser.isNextNumber()) { regex.append("(\\p{IsNd}{").append(parser.getFieldWidth()).append("}+)"); } else { regex.append("(\\p{IsNd}++)"); } return true; } /** * {@inheritDoc} */ @Override void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { cal.set(field, modify(Integer.parseInt(value))); } /** * Make any modifications to parsed integer * @param iValue The parsed integer * @return The modified value */ int modify(final int iValue) { return iValue; } } private static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR) { /** * {@inheritDoc} */ @Override void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { int iValue= Integer.parseInt(value); if(iValue<100) { iValue= parser.adjustYear(iValue); } cal.set(Calendar.YEAR, iValue); } }; /** * A strategy that handles a timezone field in the parsing pattern */ private static class TimeZoneStrategy extends Strategy { private final String validTimeZoneChars; private final SortedMap tzNames= new TreeMap(String.CASE_INSENSITIVE_ORDER); /** * Construct a Strategy that parses a TimeZone * @param locale The Locale */ TimeZoneStrategy(final Locale locale) { for(final String id : TimeZone.getAvailableIDs()) { if(id.startsWith("GMT")) { continue; } final TimeZone tz= TimeZone.getTimeZone(id); tzNames.put(tz.getDisplayName(false, TimeZone.SHORT, locale), tz); tzNames.put(tz.getDisplayName(false, TimeZone.LONG, locale), tz); if(tz.useDaylightTime()) { tzNames.put(tz.getDisplayName(true, TimeZone.SHORT, locale), tz); tzNames.put(tz.getDisplayName(true, TimeZone.LONG, locale), tz); } } final StringBuilder sb= new StringBuilder(); sb.append("(GMT[+\\-]\\d{0,1}\\d{2}|[+\\-]\\d{2}:?\\d{2}|"); for(final String id : tzNames.keySet()) { escapeRegex(sb, id, false).append('|'); } sb.setCharAt(sb.length()-1, ')'); validTimeZoneChars= sb.toString(); } /** * {@inheritDoc} */ @Override boolean addRegex(final FastDateParser parser, final StringBuilder regex) { regex.append(validTimeZoneChars); return true; } /** * {@inheritDoc} */ @Override void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { TimeZone tz; if(value.charAt(0)=='+' || value.charAt(0)=='-') { tz= TimeZone.getTimeZone("GMT"+value); } else if(value.startsWith("GMT")) { tz= TimeZone.getTimeZone(value); } else { tz= tzNames.get(value); if(tz==null) { throw new IllegalArgumentException(value + " is not a supported timezone name"); } } cal.setTimeZone(tz); } } private static final Strategy NUMBER_MONTH_STRATEGY = new NumberStrategy(Calendar.MONTH) { @Override int modify(final int iValue) { return iValue-1; } }; private static final Strategy LITERAL_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR); private static final Strategy WEEK_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_YEAR); private static final Strategy WEEK_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_MONTH); private static final Strategy DAY_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.DAY_OF_YEAR); private static final Strategy DAY_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_MONTH); private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH); private static final Strategy HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY); private static final Strategy MODULO_HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY) { @Override int modify(final int iValue) { return iValue%24; } }; private static final Strategy MODULO_HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR) { @Override int modify(final int iValue) { return iValue%12; } }; private static final Strategy HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR); private static final Strategy MINUTE_STRATEGY = new NumberStrategy(Calendar.MINUTE); private static final Strategy SECOND_STRATEGY = new NumberStrategy(Calendar.SECOND); private static final Strategy MILLISECOND_STRATEGY = new NumberStrategy(Calendar.MILLISECOND); }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy