org.novoj.utils.datePattern.DatePatternConverter.java Source code

Java tutorial

Introduction

Here is the source code for org.novoj.utils.datePattern.DatePatternConverter.java

Source

package org.novoj.utils.datePattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Pattern;

/**
 * This utility class allows you to decompose Java date / time conversion patterns and allows to convert it into the
 * jQueryUI DatePicker and TimePicker format. Also contains logic for clever Date parsing when user doesn't enter the
 * date in the precise requested format.
 *
 * @author Jan Novotn, Licensed under the LGPL
 */

public class DatePatternConverter {
    private static final Log log = LogFactory.getLog(DatePatternConverter.class);

    private static final Map<String, String> java2jQueryUIdatePatterns = new HashMap<String, String>();
    private static final Map<String, String> java2jQueryUItimePatterns = new HashMap<String, String>();
    private static final Set<Character> specialCharacters = new HashSet<Character>();
    private static final List<DateFormatHolder> dateRecognitionPatterns = new ArrayList<DateFormatHolder>();
    private static final List<DateFormatHolder> timeRecognitionPatterns = new ArrayList<DateFormatHolder>();
    private static final List<DateFormatHolder> dateTimeRecognitionPatterns = new ArrayList<DateFormatHolder>();
    private static final Pattern REGEX_APOSTROPHE = Pattern.compile("\'");

    /** @noinspection SimpleDateFormatWithoutLocale*/
    static {
        java2jQueryUIdatePatterns.put("yyyy", "yy");
        java2jQueryUIdatePatterns.put("yy", "y");
        java2jQueryUIdatePatterns.put("MMMMM", "MM");
        java2jQueryUIdatePatterns.put("MMM", "M");
        java2jQueryUIdatePatterns.put("MM", "mm");
        java2jQueryUIdatePatterns.put("M", "m");
        java2jQueryUIdatePatterns.put("D", "o");
        java2jQueryUIdatePatterns.put("dd", "dd");
        java2jQueryUIdatePatterns.put("d", "d");
        java2jQueryUIdatePatterns.put("EEEE", "DD");
        java2jQueryUIdatePatterns.put("EEE", "D");
        java2jQueryUIdatePatterns.put("EE", "D");
        java2jQueryUIdatePatterns.put("E", "D");

        java2jQueryUItimePatterns.put("HH", "hh");
        java2jQueryUItimePatterns.put("H", "h");
        java2jQueryUItimePatterns.put("hh", "hh");
        java2jQueryUItimePatterns.put("h", "h");
        java2jQueryUItimePatterns.put("mm", "mm");
        java2jQueryUItimePatterns.put("m", "m");
        java2jQueryUItimePatterns.put("ss", "ss");
        java2jQueryUItimePatterns.put("s", "s");
        java2jQueryUItimePatterns.put("a", "TT");

        specialCharacters.add('.');
        specialCharacters.add(':');
        specialCharacters.add('-');
        specialCharacters.add('/');
        specialCharacters.add('\\');
        specialCharacters.add(',');

        //make cartesian list of possible date combinations

        dateRecognitionPatterns.add(getDateFormat("yyyy", false));
        dateRecognitionPatterns.add(getDateFormat("d", false));
        dateRecognitionPatterns.add(getDateFormat("dd", false));
        dateRecognitionPatterns.add(getDateFormat("MMMM", true));
        dateRecognitionPatterns.add(getDateFormat("d.M", true));
        dateRecognitionPatterns.add(getDateFormat("dd.MM", true));
        dateRecognitionPatterns.add(getDateFormat("d.MM", true));
        dateRecognitionPatterns.add(getDateFormat("dd.M", true));
        dateRecognitionPatterns.add(getDateFormat("d.M.", true));
        dateRecognitionPatterns.add(getDateFormat("dd.MM.", true));
        dateRecognitionPatterns.add(getDateFormat("d.MM.", true));
        dateRecognitionPatterns.add(getDateFormat("dd.M.", true));
        dateRecognitionPatterns.add(getDateFormat("yyyy.M.d", true));
        dateRecognitionPatterns.add(getDateFormat("yyyy.M.dd", true));
        dateRecognitionPatterns.add(getDateFormat("yyyy.MM.d", true));
        dateRecognitionPatterns.add(getDateFormat("yyyy.MMM.d", true));
        dateRecognitionPatterns.add(getDateFormat("yyyy.MMMM.d", true));
        dateRecognitionPatterns.add(getDateFormat("yyyy.MM.dd", true));
        dateRecognitionPatterns.add(getDateFormat("yyyy.MMM.dd", true));
        dateRecognitionPatterns.add(getDateFormat("yyyy.MMMM.dd", true));
        dateRecognitionPatterns.add(getDateFormat("d.M.yy", true));
        dateRecognitionPatterns.add(getDateFormat("dd.M.yy", true));
        dateRecognitionPatterns.add(getDateFormat("d.MM.yy", true));
        dateRecognitionPatterns.add(getDateFormat("d.MMM.yy", true));
        dateRecognitionPatterns.add(getDateFormat("d.MMMM.yy", true));
        dateRecognitionPatterns.add(getDateFormat("dd.MM.yy", true));
        dateRecognitionPatterns.add(getDateFormat("dd.MMM.yy", true));
        dateRecognitionPatterns.add(getDateFormat("dd.MMMM.yy", true));
        dateRecognitionPatterns.add(getDateFormat("d.M.yyyy", true));
        dateRecognitionPatterns.add(getDateFormat("dd.M.yyyy", true));
        dateRecognitionPatterns.add(getDateFormat("d.MM.yyyy", true));
        dateRecognitionPatterns.add(getDateFormat("d.MMM.yyyy", true));
        dateRecognitionPatterns.add(getDateFormat("d.MMMM.yyyy", true));
        dateRecognitionPatterns.add(getDateFormat("dd.MM.yyyy", true));
        dateRecognitionPatterns.add(getDateFormat("dd.MMM.yyyy", true));
        dateRecognitionPatterns.add(getDateFormat("dd.MMMM.yyyy", true));
        Collections.reverse(dateRecognitionPatterns);

        timeRecognitionPatterns.add(getDateFormat("H", false));
        timeRecognitionPatterns.add(getDateFormat("HH", false));
        timeRecognitionPatterns.add(getDateFormat("H.m", true));
        timeRecognitionPatterns.add(getDateFormat("HH.m", true));
        timeRecognitionPatterns.add(getDateFormat("H.mm", true));
        timeRecognitionPatterns.add(getDateFormat("HH.mm", true));
        timeRecognitionPatterns.add(getDateFormat("h.a", true));
        timeRecognitionPatterns.add(getDateFormat("hh.a", true));
        timeRecognitionPatterns.add(getDateFormat("h.m.a", true));
        timeRecognitionPatterns.add(getDateFormat("hh.m.a", true));
        timeRecognitionPatterns.add(getDateFormat("hh.mm.a", true));
        timeRecognitionPatterns.add(getDateFormat("h.mm.a", true));
        timeRecognitionPatterns.add(getDateFormat("H.m.s", true));
        timeRecognitionPatterns.add(getDateFormat("HH.m.s", true));
        timeRecognitionPatterns.add(getDateFormat("H.mm.s", true));
        timeRecognitionPatterns.add(getDateFormat("HH.mm.s", true));
        timeRecognitionPatterns.add(getDateFormat("H.m.ss", true));
        timeRecognitionPatterns.add(getDateFormat("HH.m.ss", true));
        timeRecognitionPatterns.add(getDateFormat("H.mm.ss", true));
        timeRecognitionPatterns.add(getDateFormat("H.m.s.S", true));
        timeRecognitionPatterns.add(getDateFormat("HH.m.s.S", true));
        timeRecognitionPatterns.add(getDateFormat("H.mm.s.S", true));
        timeRecognitionPatterns.add(getDateFormat("HH.mm.s.S", true));
        timeRecognitionPatterns.add(getDateFormat("H.m.ss.S", true));
        timeRecognitionPatterns.add(getDateFormat("HH.m.ss.S", true));
        timeRecognitionPatterns.add(getDateFormat("H.mm.ss.S", true));
        timeRecognitionPatterns.add(getDateFormat("HH.mm.ss.S", true));
        Collections.reverse(timeRecognitionPatterns);

        for (DateFormatHolder dateFormat : dateRecognitionPatterns) {
            if (dateFormat.isCombinable()) {
                for (DateFormatHolder timeFormat : timeRecognitionPatterns) {
                    if (timeFormat.isCombinable()) {
                        dateTimeRecognitionPatterns
                                .add(getDateFormat(dateFormat.getPattern() + "." + timeFormat.getPattern(), false));
                    }
                }
            }
        }
    }

    /**
     * Construct new DateFormatHolder.
     * @param pattern
     * @param combinable
     * @return
     */
    private static DateFormatHolder getDateFormat(String pattern, boolean combinable) {
        return new DateFormatHolder(pattern, combinable);
    }

    /**
     * Tries to recognize date / time pattern from arbitrary string. Uses several decomposition patterns to recognize
     * proper value. It tries to make up missing parts in the natural way (current or initial date / time).
     *
     * @param dateAsString - user entered string
     * @param dateRequested - true if you expect some date value
     * @param timeRequested - true if you expect some time value
     * @param locale - locale used to parse string (passed to the SimpleDateFormat object)
     * @return
     */
    @SuppressWarnings({ "OverlyComplexMethod" })
    public Date getDate(String dateAsString, boolean dateRequested, boolean timeRequested, Locale locale) {
        String normalizedDateValue = getNormalizedDateValue(dateAsString, locale);
        Date now = new Date();
        GregorianCalendar nowCld = new GregorianCalendar();
        nowCld.setTime(now);
        if (dateRequested && timeRequested) {
            for (DateFormatHolder dateFormat : dateTimeRecognitionPatterns) {
                Date parsedDate = tryToParse(normalizedDateValue, dateFormat, nowCld, locale, false);
                if (parsedDate != null) {
                    return parsedDate;
                }
            }
            for (DateFormatHolder dateFormat : dateRecognitionPatterns) {
                Date parsedDate = tryToParse(normalizedDateValue, dateFormat, nowCld, locale, false);
                if (parsedDate != null) {
                    return parsedDate;
                }
            }
            for (DateFormatHolder timeFormat : timeRecognitionPatterns) {
                Date parsedDate = tryToParse(normalizedDateValue, timeFormat, nowCld, locale, false);
                if (parsedDate != null) {
                    return parsedDate;
                }
            }
        } else if (dateRequested) {
            for (DateFormatHolder dateFormat : dateRecognitionPatterns) {
                Date parsedDate = tryToParse(normalizedDateValue, dateFormat, nowCld, locale, true);
                if (parsedDate != null) {
                    return parsedDate;
                }
            }
        } else if (timeRequested) {
            for (DateFormatHolder timeFormat : timeRecognitionPatterns) {
                Date parsedDate = tryToParse(normalizedDateValue, timeFormat, nowCld, locale, true);
                if (parsedDate != null) {
                    return parsedDate;
                }
            }
        }
        return null;
    }

    /**
     * This method normalizes entered date to the least valid form:
     * - white spaces are removed unless they delimit two valid values
     * - more consequential whitespaces are compressed to one character
     * - whitespace characters are converted to space character (32)
     * - special characters swallow border whitespaces
     * - more consequential special characters are compressed to one character
     * - string is trimmed
     * - special characters at the beginning of the string are ignored
     * - string is lowercased
     * - all special characters are converted to dot character
     *
     * @param value
     * @return
     */
    @SuppressWarnings({ "OverlyComplexMethod" })
    public String getNormalizedDateValue(String value, Locale locale) {
        String firstNormalization = value.trim().toLowerCase(locale);
        StringBuilder result = new StringBuilder();
        boolean whitespace = false;
        boolean specialCharacter = false;
        for (int i = 0; i < firstNormalization.length(); i++) {
            char letter = firstNormalization.charAt(i);
            if (specialCharacters.contains(letter)) {
                specialCharacter = true;
            } else if (Character.isDigit(letter) || Character.isLetter(letter)) {
                if (result.length() > 0) {
                    if (specialCharacter || whitespace) {
                        result.append('.');
                    }
                }
                whitespace = false;
                specialCharacter = false;
                result.append(letter);
            } else if (Character.isWhitespace(letter)) {
                whitespace = true;
            }
        }
        if (specialCharacter) {
            result.append(".");
        }
        return result.toString();
    }

    /**
     * This method decomposes original pattern into the recognizable chunks. Chunks are composed of:
     * - same letters
     * - parts escaped with apostrophe (anything could be inside)
     * - unknown characters whose are not letters (comma, dot, double dot, space and so on)
     *
     * @param pattern
     * @return
     */
    @SuppressWarnings({ "OverlyLongMethod", "ImplicitNumericConversion" })
    public List<String> getPatternDecomposition(String pattern) {
        List<String> patternDecomposition = new ArrayList<String>();
        boolean escaped = false;
        char lastLetter = (char) 0;
        StringBuilder buffer = new StringBuilder();
        for (int i = 0; i < pattern.length(); i++) {
            char letter = pattern.charAt(i);
            if (letter == '\'') {
                if (escaped) {
                    if (pattern.charAt(i + 1) == '\'') {
                        i++;
                        buffer.append("\'\'");
                    } else {
                        escaped = false;
                        buffer.append('\'');
                        buffer = resetBuffer(patternDecomposition, buffer);
                    }
                } else {
                    buffer = resetBuffer(patternDecomposition, buffer);
                    escaped = true;
                    buffer.append('\'');
                }
            } else if (escaped) {
                buffer.append(letter);
            } else if (Character.isLetter(letter)) {
                if (letter == lastLetter || lastLetter == (char) 0) {
                    buffer.append(letter);
                } else {
                    buffer = resetBuffer(patternDecomposition, buffer);
                    buffer.append(letter);
                }
            } else if (specialCharacters.contains(new Character(letter))) {
                buffer = resetBuffer(patternDecomposition, buffer);
                patternDecomposition.add(String.valueOf(letter));
            } else {
                if (buffer.length() > 0 && Character.isLetter(buffer.charAt(0))) {
                    buffer = resetBuffer(patternDecomposition, buffer);
                }
                buffer.append(letter);
            }
            lastLetter = letter;
        }
        resetBuffer(patternDecomposition, buffer);
        return patternDecomposition;
    }

    /**
     * Returns true if date time pattern contains any sensible time part.
     * @param patternDecomposition
     * @return
     */
    public boolean hasTime(List<String> patternDecomposition) {
        for (String patternPart : patternDecomposition) {
            if (java2jQueryUItimePatterns.containsKey(patternPart)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns true if date time pattern contains any sensible date part.
     * @param patternDecomposition
     * @return
     */
    public boolean hasDate(List<String> patternDecomposition) {
        for (String patternPart : patternDecomposition) {
            if (java2jQueryUIdatePatterns.containsKey(patternPart)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Converts Java DateFormat pattern to the jQueryUI DatePicker Javascript format (or throws PatternNotConvertableException).
     * @param patternDecomposition
     * @return
     * @throws PatternNotConvertableException
     */
    public String javaToJavascriptDatePatternConversion(List<String> patternDecomposition)
            throws PatternNotConvertableException {
        return conversion(patternDecomposition, java2jQueryUIdatePatterns, java2jQueryUItimePatterns, true);
    }

    /**
     * Converts Java DateFormat pattern to the jQueryUI DatePicker TimePicker addon Javascript format (or throws
     * PatternNotConvertableException).
     * @param patternDecomposition
     * @return
     * @throws PatternNotConvertableException
     */
    public String javaToJavascriptTimePatternConversion(List<String> patternDecomposition)
            throws PatternNotConvertableException {
        return conversion(patternDecomposition, java2jQueryUItimePatterns, java2jQueryUIdatePatterns, false);
    }

    /**
     * Returns delimiter between date and time part of the DateFormat pattern. This part is needed to by handed forward
     * to the jQueryUI DatePicker to combine data with TimePicker addon.
     *
     * @param patternDecomposition
     * @return
     * @throws PatternNotConvertableException
     */
    @SuppressWarnings({ "ImplicitNumericConversion", "OverlyComplexMethod" })
    public String getDateAndTimeDelimiter(List<String> patternDecomposition) throws PatternNotConvertableException {
        boolean anyPatternRecognized = false;
        Map<String, String> patternSource = null;

        StringBuffer result = new StringBuffer();
        for (String patternPart : patternDecomposition) {
            char leadLetter = patternPart.charAt(0);
            if (leadLetter == '\'') {
                if (anyPatternRecognized) {
                    result.append(patternPart);
                }
            } else if (Character.isLetter(leadLetter)) {
                if (java2jQueryUIdatePatterns.containsKey(patternPart)
                        || java2jQueryUItimePatterns.containsKey(patternPart)) {

                    if (patternSource != null && patternSource.containsKey(patternPart)) {
                        result = new StringBuffer();
                    } else {
                        if (anyPatternRecognized) {
                            break;
                        } else {
                            anyPatternRecognized = true;
                            patternSource = java2jQueryUIdatePatterns.containsKey(patternPart)
                                    ? java2jQueryUIdatePatterns
                                    : java2jQueryUItimePatterns;
                        }
                    }

                }
            } else {
                if (anyPatternRecognized) {
                    if (result.length() > 0 || !specialCharacters.contains(new Character(patternPart.charAt(0)))) {
                        result.append(patternPart);
                    }
                }
            }
        }

        return REGEX_APOSTROPHE.matcher(result.toString()).replaceAll("");
    }

    /**
     * Tryes to parse string pattern into the date and returns non null value when succeeded. Otherwise returns null
     * (never throws exception).
     */
    private static Date tryToParse(String normalizedDateValue, DateFormatHolder dateFormat, Calendar nowCld,
            Locale locale, boolean resetTime) {
        try {
            if (log.isTraceEnabled()) {
                log.trace("Trying format: " + dateFormat.getPattern() + " ("
                        + dateFormat.getFormat(locale).format(new Date()) + ")");
            }
            Date parsedDate = dateFormat.getFormat(locale).parse(normalizedDateValue);
            if (normalizedDateValue.equals(dateFormat.getFormat(locale).format(parsedDate))) {
                parsedDate = normalizeDate(nowCld, parsedDate, locale, dateFormat.getPattern());
                parsedDate = normalizeHours(nowCld, parsedDate, locale, dateFormat.getPattern(), resetTime);
                return parsedDate;
            } else {
                //Format cannot be used - reverse formatting did not produce exactly same string!
                return null;
            }
        } catch (ParseException ignored) {
            //continue - this pattern wasn't just right
            return null;
        }
    }

    /**
     * Method returns date object with normalized date:
     * - when pattern doesn't contain year pattern -> current year is used
     * - when pattern doesn't contain month pattern
     *    date represents current year -> current month is used
     *                        else -> january is used
     * - when pattern doesn't contain day pattern
     *    date represents current year and month -> current day is used
     *                                 else -> first day of month is used
     */
    private static Date normalizeDate(Calendar nowCld, Date date, Locale locale, String pattern) {
        GregorianCalendar dateCld = new GregorianCalendar();
        dateCld.setTime(date);
        //set current date
        if (pattern.toLowerCase(locale).indexOf('y') == -1) {
            dateCld.set(Calendar.YEAR, nowCld.get(Calendar.YEAR));
        }

        if (pattern.indexOf('M') == -1) {
            if (nowCld.get(Calendar.YEAR) == dateCld.get(Calendar.YEAR)) {
                dateCld.set(Calendar.MONTH, nowCld.get(Calendar.MONTH));
            } else {
                dateCld.set(Calendar.MONTH, 0);
            }
        }
        if (pattern.toLowerCase(locale).indexOf('d') == -1) {
            if (nowCld.get(Calendar.YEAR) != dateCld.get(Calendar.YEAR)
                    || nowCld.get(Calendar.MONTH) != dateCld.get(Calendar.MONTH)) {
                dateCld.set(Calendar.DAY_OF_MONTH, 1);
            } else {
                dateCld.set(Calendar.DAY_OF_MONTH, nowCld.get(Calendar.DAY_OF_MONTH));
            }
        }
        return dateCld.getTime();
    }

    /**
     * Method returns date object with normalized time. When resetTime = true, time will be set to 0:0:0.0.
     * When date respresents current day (current date) and pattern doesn't contain appropriate pattern parts for
     * hour, minute, second, milliseconds - those values will be set to current time values, otherwise they will be
     * set to initial values (ie. 0).
     */
    private static Date normalizeHours(Calendar nowCld, Date date, Locale locale, String pattern,
            boolean resetTime) {
        GregorianCalendar dateCld = new GregorianCalendar();
        dateCld.setTime(date);
        boolean currentYear = nowCld.get(Calendar.YEAR) == dateCld.get(Calendar.YEAR);
        boolean currentMonth = nowCld.get(Calendar.MONTH) == dateCld.get(Calendar.MONTH);
        boolean currentDay = nowCld.get(Calendar.DAY_OF_MONTH) == dateCld.get(Calendar.DAY_OF_MONTH);
        //noinspection OverlyComplexBooleanExpression
        if (!resetTime && currentYear && currentMonth && currentDay) {
            //set current time
            if (pattern.toLowerCase(locale).indexOf('h') == -1) {
                dateCld.set(Calendar.HOUR, nowCld.get(Calendar.HOUR));
            }
            if (pattern.indexOf('m') == -1) {
                dateCld.set(Calendar.MINUTE, nowCld.get(Calendar.MINUTE));
            }
            if (pattern.indexOf('s') == -1) {
                dateCld.set(Calendar.SECOND, nowCld.get(Calendar.SECOND));
            }
            if (pattern.indexOf('S') == -1) {
                dateCld.set(Calendar.MILLISECOND, nowCld.get(Calendar.MILLISECOND));
            }
        } else {
            //set zero time
            if (pattern.toLowerCase(locale).indexOf('h') == -1) {
                dateCld.set(Calendar.HOUR, 0);
            }
            if (pattern.indexOf('m') == -1) {
                dateCld.set(Calendar.MINUTE, 0);
            }
            if (pattern.indexOf('s') == -1) {
                dateCld.set(Calendar.SECOND, 0);
            }
            if (pattern.indexOf('S') == -1) {
                dateCld.set(Calendar.MILLISECOND, 0);
            }
        }
        return dateCld.getTime();
    }

    /**
     * Converts Java DateFormat pattern to the jQueryUI DatePicker TimePicker addon Javascript format (or throws
     * PatternNotConvertableException).
     * @param patternDecomposition
     * @param patternsA
     * @param patternsB
     * @param escapeInsideAllowed
     * @return
     * @throws PatternNotConvertableException
     */
    @SuppressWarnings({ "ImplicitNumericConversion", "OverlyLongMethod", "OverlyComplexMethod" })
    private String conversion(List<String> patternDecomposition, Map<String, String> patternsA,
            Map<String, String> patternsB, boolean escapeInsideAllowed) throws PatternNotConvertableException {
        boolean firstUsablePatternRecognized = false;
        boolean otherPatternRecognized = false;

        StringBuffer unknownPatterns = new StringBuffer();
        StringBuilder result = new StringBuilder();
        StringBuffer conditionalResult = new StringBuffer();
        for (String patternPart : patternDecomposition) {
            char leadLetter = patternPart.charAt(0);
            if (leadLetter == '\'') {
                if (firstUsablePatternRecognized && !escapeInsideAllowed) {
                    throw new PatternNotConvertableException(
                            "Escaped parts in pattern are not allowed: " + patternPart);
                }
                //add escaped part without further analyzing it
                conditionalResult.append(patternPart);
            } else if (Character.isLetter(leadLetter)) {
                if (patternsA.containsKey(patternPart)) {
                    firstUsablePatternRecognized = true;
                    if (conditionalResult.length() > 0) {
                        if (otherPatternRecognized) {
                            conditionalResult = new StringBuffer();
                        } else {
                            result.append(conditionalResult);
                            conditionalResult = new StringBuffer();
                        }
                    }
                    result.append(patternsA.get(patternPart));
                } else if (patternsB.containsKey(patternPart)) {
                    otherPatternRecognized = true;
                    conditionalResult = new StringBuffer();
                    if (firstUsablePatternRecognized) {
                        break;
                    }
                } else {
                    if (unknownPatterns.length() > 0) {
                        unknownPatterns.append(", ");
                    }
                    unknownPatterns.append(patternPart);
                }
            } else if (specialCharacters.contains(new Character(patternPart.charAt(0)))) {
                if (firstUsablePatternRecognized) {
                    result.append(patternPart);
                }
            } else {
                conditionalResult.append(patternPart);
            }
        }

        if (unknownPatterns.length() > 0) {
            throw new PatternNotConvertableException(
                    "Java pattern contains unconvertable chunks, that are not accepted by jQuery UI DatePicker: "
                            + unknownPatterns);
        }

        return result.toString();
    }

    /**
     * Adds current value of a buffer to pattern decomposition and creates new one (only if it contains some value).
     * @param patternDecomposition
     * @param buffer
     * @return
     */
    private StringBuilder resetBuffer(List<String> patternDecomposition, StringBuilder buffer) {
        if (buffer.length() > 0) {
            patternDecomposition.add(buffer.toString());
            buffer = new StringBuilder();
        }
        return buffer;
    }

    /**
     * Contains pattern definition along with compiled version of SimpleDateFormatType to fast use.
     */
    private static class DateFormatHolder {
        private final String format;
        private final boolean combinable;
        private final Map<Locale, SimpleDateFormat> formats = new HashMap<Locale, SimpleDateFormat>();

        private DateFormatHolder(String format, boolean combinable) {
            this.format = format;
            this.combinable = combinable;
        }

        public SimpleDateFormat getFormat(Locale locale) {
            SimpleDateFormat sdf = formats.get(locale);
            if (sdf == null) {
                synchronized (formats) {
                    sdf = new SimpleDateFormat(format, locale);
                    sdf.setLenient(false);
                    formats.put(locale, sdf);
                }
            }
            return sdf;
        }

        public String getPattern() {
            return format;
        }

        public boolean isCombinable() {
            return combinable;
        }

    }
}