DateTimePatternGenerator.java

// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 ********************************************************************************
 * Copyright (C) 2006-2016, Google, International Business Machines Corporation
 * and others. All Rights Reserved.
 ********************************************************************************
 */
package com.ibm.icu.text;

import com.ibm.icu.impl.ICUCache;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.PatternTokenizer;
import com.ibm.icu.impl.SimpleCache;
import com.ibm.icu.impl.SimpleFormatterImpl;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.Freezable;
import com.ibm.icu.util.ICUCloneNotSupportedException;
import com.ibm.icu.util.Region;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.ULocale.Category;
import com.ibm.icu.util.UResourceBundle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

/**
 * This class provides flexible generation of date format patterns, like "yy-MM-dd". The user can
 * build up the generator by adding successive patterns. Once that is done, a query can be made
 * using a "skeleton", which is a pattern which just includes the desired fields and lengths. The
 * generator will return the "best fit" pattern corresponding to that skeleton.
 *
 * <p>The main method people will use is getBestPattern(String skeleton), since normally this class
 * is pre-built with data from a particular locale. However, generators can be built directly from
 * other data as well.
 *
 * @stable ICU 3.6
 */
public class DateTimePatternGenerator implements Freezable<DateTimePatternGenerator>, Cloneable {
    private static final boolean DEBUG = false;

    // debugging flags
    // static boolean SHOW_DISTANCE = false;
    // TODO add hack to fix months for CJK, as per bug ticket 1099

    /**
     * Create empty generator, to be constructed with addPattern(...) etc.
     *
     * @stable ICU 3.6
     */
    public static DateTimePatternGenerator getEmptyInstance() {
        DateTimePatternGenerator instance = new DateTimePatternGenerator();
        instance.addCanonicalItems();
        instance.fillInMissing();
        return instance;
    }

    /**
     * Only for use by subclasses
     *
     * @stable ICU 3.6
     */
    protected DateTimePatternGenerator() {}

    /**
     * Construct a flexible generator according to data for the default <code>FORMAT</code> locale.
     *
     * @see Category#FORMAT
     * @stable ICU 3.6
     */
    public static DateTimePatternGenerator getInstance() {
        return getInstance(ULocale.getDefault(Category.FORMAT));
    }

    /**
     * Construct a flexible generator according to data for a given locale.
     *
     * @param uLocale The locale to pass.
     * @stable ICU 3.6
     */
    public static DateTimePatternGenerator getInstance(ULocale uLocale) {
        return getFrozenInstance(uLocale).cloneAsThawed();
    }

    /**
     * Construct a flexible generator according to data for a given locale.
     *
     * @param locale The {@link java.util.Locale} to pass.
     * @stable ICU 54
     */
    public static DateTimePatternGenerator getInstance(Locale locale) {
        return getInstance(ULocale.forLocale(locale));
    }

    /**
     * Construct a frozen instance of DateTimePatternGenerator for a given locale. This method
     * returns a cached frozen instance of DateTimePatternGenerator, so less expensive than the
     * regular factory method.
     *
     * @param uLocale The locale to pass.
     * @return A frozen DateTimePatternGenerator.
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static DateTimePatternGenerator getFrozenInstance(ULocale uLocale) {
        String localeKey = uLocale.toString();
        DateTimePatternGenerator result = DTPNG_CACHE.get(localeKey);
        if (result != null) {
            return result;
        }

        result = new DateTimePatternGenerator();
        result.initData(uLocale, false);

        // freeze and cache
        result.freeze();
        DTPNG_CACHE.put(localeKey, result);
        return result;
    }

    /**
     * Construct a non-frozen instance of DateTimePatternGenerator for a given locale that skips
     * using the standard date and time patterns. Because this is different than the normal instance
     * for the locale, it does not set or use the cache.
     *
     * @param uLocale The locale to pass.
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static DateTimePatternGenerator getInstanceNoStdPat(ULocale uLocale) {
        DateTimePatternGenerator result = new DateTimePatternGenerator();
        result.initData(uLocale, true);
        return result;
    }

    private void initData(ULocale uLocale, boolean skipStdPatterns) {
        // This instance of PatternInfo is required for calling some functions.  It is used for
        // passing additional information to the caller.  We won't use this extra information, but
        // we still need to make a temporary instance.
        PatternInfo returnInfo = new PatternInfo();

        addCanonicalItems();
        if (!skipStdPatterns) { // skip to prevent circular dependency when used by Calendar
            addICUPatterns(returnInfo, uLocale);
        }
        addCLDRData(returnInfo, uLocale);
        if (!skipStdPatterns) { // also skip to prevent circular dependency from Calendar
            setDateTimeFromCalendar(uLocale);
        } else {
            // instead, since from Calendar we do not care about dateTimePattern, use a fallback
            setDateTimeFormat("{1} {0}");
        }
        setDecimalSymbols(uLocale);
        getAllowedHourFormats(uLocale);
        fillInMissing();
    }

    private void addICUPatterns(PatternInfo returnInfo, ULocale uLocale) {
        // first load with the ICU patterns
        ICUResourceBundle rb =
                (ICUResourceBundle)
                        UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, uLocale);
        String calendarTypeToUse = getCalendarTypeToUse(uLocale);
        // TODO: See ICU-22867
        ICUResourceBundle dateTimePatterns =
                rb.getWithFallback("calendar/" + calendarTypeToUse + "/DateTimePatterns");
        if (dateTimePatterns.getType() != UResourceBundle.ARRAY || dateTimePatterns.getSize() < 8) {
            throw new MissingResourceException(
                    "Resource in wrong format",
                    "ICUResourceBundle",
                    "calendar/" + calendarTypeToUse + "/DateTimePatterns");
        }
        for (int i = 0; i < 8; i++) { // no constants available for the resource indexes
            String pattern;
            UResourceBundle patternRes = dateTimePatterns.get(i);

            switch (patternRes.getType()) {
                case UResourceBundle.STRING:
                    pattern = patternRes.getString();
                    break;
                case UResourceBundle.ARRAY:
                    pattern = patternRes.getString(0);
                    break;
                default:
                    throw new MissingResourceException(
                            "Resource in wrong format",
                            "ICUResourceBundle",
                            "calendar/" + calendarTypeToUse + "/DateTimePatterns");
            }
            addPattern(pattern, false, returnInfo);
        }
    }

    private String getCalendarTypeToUse(ULocale uLocale) {
        // Get the correct calendar type
        // TODO: C++ and Java are inconsistent (see #9952).
        String calendarTypeToUse = uLocale.getKeywordValue("calendar");
        if (calendarTypeToUse == null) {
            String[] preferredCalendarTypes =
                    Calendar.getKeywordValuesForLocale("calendar", uLocale, true);
            calendarTypeToUse = preferredCalendarTypes[0]; // the most preferred calendar
        }
        if (calendarTypeToUse == null) {
            calendarTypeToUse = "gregorian"; // fallback
        }
        return calendarTypeToUse;
    }

    private void consumeShortTimePattern(String shortTimePattern, PatternInfo returnInfo) {
        // keep this pattern to populate other time field
        // combination patterns by hackTimes later in this method.
        // ICU-20383 No longer set defaultHourFormatChar to the hour format character from
        // this pattern; instead it is set from LOCALE_TO_ALLOWED_HOUR which now
        // includes entries for both preferred and allowed formats.

        // some languages didn't add mm:ss or HH:mm, so put in a hack to compute that from the short
        // time.
        hackTimes(returnInfo, shortTimePattern);
    }

    private class AppendItemFormatsSink extends UResource.Sink {
        @Override
        public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
            int field = getAppendFormatNumber(key);
            if (field < 0) {
                return;
            }
            if (getAppendItemFormat(field) == null) {
                setAppendItemFormat(field, value.toString());
            }
        }
    }

    private class AppendItemNamesSink extends UResource.Sink {
        @Override
        public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
            int fieldAndWidth = getCLDRFieldAndWidthNumber(key);
            if (fieldAndWidth == -1) {
                return;
            }
            int field = fieldAndWidth / DisplayWidth.COUNT;
            DisplayWidth width = CLDR_FIELD_WIDTH[fieldAndWidth % DisplayWidth.COUNT];
            UResource.Table detailsTable = value.getTable();
            if (detailsTable.findValue("dn", value)) {
                if (getFieldDisplayName(field, width) == null) {
                    setFieldDisplayName(field, width, value.toString());
                }
            }
        }
    }

    private void fillInMissing() {
        for (int i = 0; i < TYPE_LIMIT; ++i) {
            if (getAppendItemFormat(i) == null) {
                setAppendItemFormat(i, "{0} \u251C{2}: {1}\u2524");
            }
            if (getFieldDisplayName(i, DisplayWidth.WIDE) == null) {
                setFieldDisplayName(i, DisplayWidth.WIDE, "F" + i);
            }
            if (getFieldDisplayName(i, DisplayWidth.ABBREVIATED) == null) {
                setFieldDisplayName(
                        i, DisplayWidth.ABBREVIATED, getFieldDisplayName(i, DisplayWidth.WIDE));
            }
            if (getFieldDisplayName(i, DisplayWidth.NARROW) == null) {
                setFieldDisplayName(
                        i, DisplayWidth.NARROW, getFieldDisplayName(i, DisplayWidth.ABBREVIATED));
            }
        }
    }

    private class AvailableFormatsSink extends UResource.Sink {
        PatternInfo returnInfo;

        public AvailableFormatsSink(PatternInfo returnInfo) {
            this.returnInfo = returnInfo;
        }

        @Override
        public void put(UResource.Key key, UResource.Value value, boolean isRoot) {
            String formatKey = key.toString();
            if (!isAvailableFormatSet(formatKey)) {
                setAvailableFormat(formatKey);
                // Add pattern with its associated skeleton. Override any duplicate derived from std
                // patterns,
                // but not a previous availableFormats entry:
                String formatValue = value.toString();
                addPatternWithSkeleton(formatValue, formatKey, true, returnInfo);
            }
        }
    }

    private void addCLDRData(PatternInfo returnInfo, ULocale uLocale) {
        ICUResourceBundle rb =
                (ICUResourceBundle)
                        UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, uLocale);
        String calendarTypeToUse = getCalendarTypeToUse(uLocale);

        //      ICU4J getWithFallback does not work well when
        //      1) A nested table is an alias to /LOCALE/...
        //      2) getWithFallback is called multiple times for going down hierarchical resource
        // path
        //      #9987 resolved the issue of alias table when full path is specified in
        // getWithFallback,
        //      but there is no easy solution when the equivalent operation is done by multiple
        // operations.
        //      This issue is addressed in #9964.

        // Load append item formats.
        AppendItemFormatsSink appendItemFormatsSink = new AppendItemFormatsSink();
        try {
            rb.getAllChildrenWithFallback(
                    "calendar/" + calendarTypeToUse + "/appendItems", appendItemFormatsSink);
        } catch (MissingResourceException e) {
        }

        // Load CLDR item names.
        AppendItemNamesSink appendItemNamesSink = new AppendItemNamesSink();
        try {
            rb.getAllChildrenWithFallback("fields", appendItemNamesSink);
        } catch (MissingResourceException e) {
        }

        // Load the available formats from CLDR.
        AvailableFormatsSink availableFormatsSink = new AvailableFormatsSink(returnInfo);
        try {
            rb.getAllChildrenWithFallback(
                    "calendar/" + calendarTypeToUse + "/availableFormats", availableFormatsSink);
        } catch (MissingResourceException e) {
        }
    }

    private void setDateTimeFromCalendar(ULocale uLocale) {
        Calendar cal = Calendar.getInstance(uLocale);
        for (int style = DateFormat.FULL; style <= DateFormat.SHORT; style++) {
            String dateTimeFormat = Calendar.getDateAtTimePattern(cal, uLocale, style);
            setDateTimeFormat(style, dateTimeFormat);
        }
    }

    private void setDecimalSymbols(ULocale uLocale) {
        // decimal point for seconds
        DecimalFormatSymbols dfs = new DecimalFormatSymbols(uLocale);
        setDecimal(String.valueOf(dfs.getDecimalSeparator()));
    }

    private static final String[] LAST_RESORT_ALLOWED_HOUR_FORMAT = {"H"};

    private String[] getAllowedHourFormatsLangCountry(String language, String country) {
        String langCountry = language + "_" + country;
        String[] list = LOCALE_TO_ALLOWED_HOUR.get(langCountry);
        if (list == null) {
            list = LOCALE_TO_ALLOWED_HOUR.get(country);
        }
        return list;
    }

    private void getAllowedHourFormats(ULocale uLocale) {
        // key can be either region or locale (lang_region)
        //        ZW{
        //            allowed{
        //                "h",
        //                "H",
        //            }
        //            preferred{"h"}
        //        }
        //        af_ZA{
        //            allowed{
        //                "h",
        //                "H",
        //                "hB",
        //                "hb",
        //            }
        //            preferred{"h"}
        //        }

        String language = uLocale.getLanguage();
        String country = ULocale.getRegionForSupplementalData(uLocale, false);

        if (language.isEmpty() || country.isEmpty()) {
            // Note: addLikelySubtags is documented not to throw in Java,
            // unlike in C++.
            ULocale max = ULocale.addLikelySubtags(uLocale);
            language = max.getLanguage();
            country = max.getCountry();
        }

        if (language.isEmpty()) {
            // Unexpected, but fail gracefully
            language = "und";
        }
        if (country.isEmpty()) {
            country = "001";
        }

        String[] list = getAllowedHourFormatsLangCountry(language, country);

        // We need to check if there is an hour cycle on locale
        Character defaultCharFromLocale = null;
        String hourCycle = uLocale.getKeywordValue("hours");
        if (hourCycle != null) {
            switch (hourCycle) {
                case "h24":
                    defaultCharFromLocale = 'k';
                    break;
                case "h23":
                    defaultCharFromLocale = 'H';
                    break;
                case "h12":
                    defaultCharFromLocale = 'h';
                    break;
                case "h11":
                    defaultCharFromLocale = 'K';
                    break;
            }
        }

        // Check if the region has an alias
        if (list == null) {
            try {
                Region region = Region.getInstance(country);
                country = region.toString();
                list = getAllowedHourFormatsLangCountry(language, country);
            } catch (IllegalArgumentException e) {
                // invalid region; fall through
            }
        }

        if (list != null) {
            defaultHourFormatChar =
                    defaultCharFromLocale != null ? defaultCharFromLocale : list[0].charAt(0);
            if (list.length > 1) {
                allowedHourFormats = Arrays.copyOfRange(list, 1, list.length);
            } else {
                allowedHourFormats = LAST_RESORT_ALLOWED_HOUR_FORMAT;
            }
        } else {
            allowedHourFormats = LAST_RESORT_ALLOWED_HOUR_FORMAT;
            defaultHourFormatChar =
                    (defaultCharFromLocale != null)
                            ? defaultCharFromLocale
                            : allowedHourFormats[0].charAt(0);
        }
    }

    private static class DayPeriodAllowedHoursSink extends UResource.Sink {
        HashMap<String, String[]> tempMap;

        private DayPeriodAllowedHoursSink(HashMap<String, String[]> tempMap) {
            this.tempMap = tempMap;
        }

        @Override
        public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
            UResource.Table timeData = value.getTable();
            for (int i = 0; timeData.getKeyAndValue(i, key, value); ++i) {
                String regionOrLocale = key.toString();
                UResource.Table formatList = value.getTable();
                String[] allowed = null;
                String preferred = null;
                for (int j = 0; formatList.getKeyAndValue(j, key, value); ++j) {
                    if (key.contentEquals("allowed")) {
                        allowed = value.getStringArrayOrStringAsArray();
                    } else if (key.contentEquals("preferred")) {
                        preferred = value.getString();
                    }
                }
                // below we construct a list[] that has an entry for the "preferred" value at [0],
                // followed by 1 or more entries for the "allowed" values.
                String[] list = null;
                if (allowed != null && allowed.length > 0) {
                    list = new String[allowed.length + 1];
                    list[0] = (preferred != null) ? preferred : allowed[0];
                    System.arraycopy(allowed, 0, list, 1, allowed.length);
                } else {
                    // fallback handling for missing data
                    list = new String[2];
                    list[0] = (preferred != null) ? preferred : LAST_RESORT_ALLOWED_HOUR_FORMAT[0];
                    list[1] = list[0];
                }
                tempMap.put(regionOrLocale, list);
            }
        }
    }

    // Get the data for dayperiod C.
    static final Map<String, String[]> LOCALE_TO_ALLOWED_HOUR;

    static {
        HashMap<String, String[]> temp = new HashMap<>();
        ICUResourceBundle suppData =
                (ICUResourceBundle)
                        ICUResourceBundle.getBundleInstance(
                                ICUData.ICU_BASE_NAME,
                                "supplementalData",
                                ICUResourceBundle.ICU_DATA_CLASS_LOADER);

        DayPeriodAllowedHoursSink allowedHoursSink = new DayPeriodAllowedHoursSink(temp);
        suppData.getAllItemsWithFallback("timeData", allowedHoursSink);

        LOCALE_TO_ALLOWED_HOUR = Collections.unmodifiableMap(temp);
    }

    /**
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public char getDefaultHourFormatChar() {
        return defaultHourFormatChar;
    }

    /**
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public void setDefaultHourFormatChar(char defaultHourFormatChar) {
        this.defaultHourFormatChar = defaultHourFormatChar;
    }

    private void hackTimes(PatternInfo returnInfo, String shortTimePattern) {
        fp.set(shortTimePattern);
        StringBuilder mmss = new StringBuilder();
        // to get mm:ss, we strip all but mm literal ss
        boolean gotMm = false;
        for (int i = 0; i < fp.items.size(); ++i) {
            Object item = fp.items.get(i);
            if (item instanceof String) {
                if (gotMm) {
                    mmss.append(fp.quoteLiteral(item.toString()));
                }
            } else {
                char ch = item.toString().charAt(0);
                if (ch == 'm') {
                    gotMm = true;
                    mmss.append(item);
                } else if (ch == 's') {
                    if (!gotMm) {
                        break; // failed
                    }
                    mmss.append(item);
                    addPattern(mmss.toString(), false, returnInfo);
                    break;
                } else if (gotMm || ch == 'z' || ch == 'Z' || ch == 'v' || ch == 'V') {
                    break; // failed
                }
            }
        }
        // to get hh:mm, we strip (literal ss) and (literal S)
        // the easiest way to do this is to mark the stuff we want to nuke, then remove it in a
        // second pass.
        BitSet variables = new BitSet();
        BitSet nuke = new BitSet();
        for (int i = 0; i < fp.items.size(); ++i) {
            Object item = fp.items.get(i);
            if (item instanceof VariableField) {
                variables.set(i);
                char ch = item.toString().charAt(0);
                if (ch == 's' || ch == 'S') {
                    nuke.set(i);
                    for (int j = i - 1; j >= 0; ++j) {
                        if (variables.get(j)) break;
                        nuke.set(i);
                    }
                }
            }
        }
        String hhmm = getFilteredPattern(fp, nuke);
        addPattern(hhmm, false, returnInfo);
    }

    private static String getFilteredPattern(FormatParser fp, BitSet nuke) {
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < fp.items.size(); ++i) {
            if (nuke.get(i)) continue;
            Object item = fp.items.get(i);
            if (item instanceof String) {
                result.append(fp.quoteLiteral(item.toString()));
            } else {
                result.append(item.toString());
            }
        }
        return result.toString();
    }

    /*private static int getAppendNameNumber(String string) {
        for (int i = 0; i < CLDR_FIELD_NAME.length; ++i) {
            if (CLDR_FIELD_NAME[i].equals(string)) return i;
        }
        return -1;
    }*/

    /**
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static int getAppendFormatNumber(UResource.Key key) {
        for (int i = 0; i < CLDR_FIELD_APPEND.length; ++i) {
            if (key.contentEquals(CLDR_FIELD_APPEND[i])) {
                return i;
            }
        }
        return -1;
    }

    /**
     * @internal CLDR
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static int getAppendFormatNumber(String string) {
        for (int i = 0; i < CLDR_FIELD_APPEND.length; ++i) {
            if (CLDR_FIELD_APPEND[i].equals(string)) {
                return i;
            }
        }
        return -1;
    }

    private static int getCLDRFieldAndWidthNumber(UResource.Key key) {
        for (int i = 0; i < CLDR_FIELD_NAME.length; ++i) {
            for (int j = 0; j < DisplayWidth.COUNT; ++j) {
                String fullKey = CLDR_FIELD_NAME[i].concat(CLDR_FIELD_WIDTH[j].cldrKey());
                if (key.contentEquals(fullKey)) {
                    return i * DisplayWidth.COUNT + j;
                }
            }
        }
        return -1;
    }

    /**
     * Return the best pattern matching the input skeleton. It is guaranteed to have all of the
     * fields in the skeleton.
     * <!-- From: com.ibm.icu.samples.text.datetimepatterngenerator.DateTimePatternGeneratorSample:getBestPatternExample -->
     *
     * <p>Example code:
     *
     * <pre>
     * import java.util.Date;
     *
     * import com.ibm.icu.text.DateFormat;
     * import com.ibm.icu.text.DateTimePatternGenerator;
     * import com.ibm.icu.text.SimpleDateFormat;
     * import com.ibm.icu.util.GregorianCalendar;
     * import com.ibm.icu.util.TimeZone;
     * import com.ibm.icu.util.ULocale;
     * ...
     * final String[] skeletons = {
     *         "yQQQQ", // year + full name of quarter, i.e., 4th quarter 1999
     *         "yMMMM", // year + full name of month, i.e., October 1999
     *         "MMMMd", // full name of month + day of the month, i.e., October 25
     *         "hhmm",  // 12-hour-cycle format, i.e., 1:32 PM
     *         "jjmm"   // preferred hour format for the given locale, i.e., 24-hour-cycle format for fr_FR
     * };
     * final ULocale[] locales = {
     *         new ULocale ("en_US"),
     *         new ULocale ("fr_FR"),
     *         new ULocale ("zh_CN"),
     * };
     * DateTimePatternGenerator dtfg = null;
     * Date date= new GregorianCalendar(1999,9,13,23,58,59).getTime();
     * System.out.printf("%-20s%-35s%-35s%-35s\n\n", "Skeleton", "en_US", "fr_FR","zh_CN");
     * for (String skeleton:skeletons) {
     *     System.out.printf("%-20s", skeleton);
     *     for (ULocale locale:locales) {
     *         // create a DateTimePatternGenerator instance for given locale
     *         dtfg = DateTimePatternGenerator.getInstance(locale);
     *         // use getBestPattern method to get the best pattern for the given skeleton
     *         String pattern = dtfg.getBestPattern(skeleton);
     *         // Constructs a SimpleDateFormat with the best pattern generated above and the given locale
     *         SimpleDateFormat sdf = new SimpleDateFormat(pattern, locale);
     *         // Get the format of the given date
     *         System.out.printf("%-35s",sdf.format(date));
     *     }
     *     System.out.println("\n");
     * }
     * /** output of the sample code:
     *  *************************************************************************************************************
     *    Skeleton            en_US                              fr_FR                              zh_CN
     *    yQQQQ               4th quarter 1999                   4e trimestre 1999                  1999年第四季度
     *    yMMMM               October 1999                       octobre 1999                       1999年10月
     *    MMMMd               October 13                         13 octobre                         10月13日
     *    hhmm                11:58 PM                           11:58 PM                           下午11:58
     *    jjmm                11:58 PM                           23:58                              下午11:58
     *  **************************************************************************************************************<code>/</code>
     * // Use DateTime.getPatternInstance to produce the same Date/Time format with predefined constant field value
     * final String[] dtfskeleton = {
     *         DateFormat.YEAR_QUARTER, // year + full name of quarter, i.e., 4th quarter 1999
     *         DateFormat.YEAR_MONTH,   // year + full name of month, i.e., October 1999
     *         DateFormat.MONTH_DAY     // full name of month + day of the month, i.e., October 25
     * };
     * System.out.printf("%-20s%-35s%-35s%-35s\n\n", "Skeleton", "en_US", "fr_FR","zh_CN");
     * for (String skeleton:dtfskeleton) {
     *     System.out.printf("%-20s", skeleton);
     *     for (ULocale locale:locales) {
     *         // Use DateFormat.getPatternInstance to get the date/time format for the locale,
     *         // and apply the format to the given date
     *         String df=DateFormat.getPatternInstance(skeleton,locale).format(date);
     *         System.out.printf("%-35s",df);
     *     }
     *     System.out.println("\n");
     * }
     *
     * /** output of the sample code:
     *  ************************************************************************************************************
     *  Skeleton            en_US                              fr_FR                              zh_CN
     *  yQQQQ               4th quarter 1999                   4e trimestre 1999                  1999年第四季度
     *  yMMMM               October 1999                       octobre 1999                       1999年10月
     *  MMMMd               October 13                         13 octobre                         10月13日
     *  ************************************************************************************************************<code>/</code>
     * </pre>
     *
     * @param skeleton The skeleton is a pattern containing only the variable fields. For example,
     *     "MMMdd" and "mmhh" are skeletons.
     * @return Best pattern matching the input skeleton.
     * @stable ICU 3.6
     */
    public String getBestPattern(String skeleton) {
        return getBestPattern(skeleton, null, MATCH_NO_OPTIONS);
    }

    /**
     * Return the best pattern matching the input skeleton. It is guaranteed to have all of the
     * fields in the skeleton.
     *
     * @param skeleton The skeleton is a pattern containing only the variable fields. For example,
     *     "MMMdd" and "mmhh" are skeletons.
     * @param options MATCH_xxx options for forcing the length of specified fields in the returned
     *     pattern to match those in the skeleton (when this would not happen otherwise). For
     *     default behavior, use MATCH_NO_OPTIONS.
     * @return Best pattern matching the input skeleton (and options).
     * @stable ICU 4.4
     */
    public String getBestPattern(String skeleton, int options) {
        return getBestPattern(skeleton, null, options);
    }

    /*
     * getBestPattern which takes optional skip matcher
     */
    private String getBestPattern(String skeleton, DateTimeMatcher skipMatcher, int options) {
        EnumSet<DTPGflags> flags = EnumSet.noneOf(DTPGflags.class);
        // Replace hour metacharacters 'j', 'C', and 'J', set flags as necessary
        String skeletonMapped = mapSkeletonMetacharacters(skeleton, flags);
        String datePattern, timePattern;
        synchronized (this) {
            current.set(skeletonMapped, fp, false);
            PatternWithMatcher bestWithMatcher =
                    getBestRaw(current, -1, _distanceInfo, skipMatcher);
            if (_distanceInfo.missingFieldMask == 0 && _distanceInfo.extraFieldMask == 0) {
                // we have a good item. Adjust the field types
                return adjustFieldTypes(bestWithMatcher, current, flags, options);
            }
            int neededFields = current.getFieldMask();

            // otherwise break up by date and time.
            datePattern =
                    getBestAppending(
                            current,
                            neededFields & DATE_MASK,
                            _distanceInfo,
                            skipMatcher,
                            flags,
                            options);
            timePattern =
                    getBestAppending(
                            current,
                            neededFields & TIME_MASK,
                            _distanceInfo,
                            skipMatcher,
                            flags,
                            options);
        }

        if (datePattern == null) return timePattern == null ? "" : timePattern;
        if (timePattern == null) return datePattern;
        // determine which dateTimeFormat to use
        String canonicalSkeleton =
                current.toCanonicalString(); // month fields use M, weekday fields use E
        int style = DateFormat.SHORT;
        int monthFieldLen = 0;
        int monthFieldOffset = canonicalSkeleton.indexOf('M');
        if (monthFieldOffset >= 0) {
            monthFieldLen = 1 + canonicalSkeleton.lastIndexOf('M') - monthFieldOffset;
        }
        if (monthFieldLen == 4) {
            if (canonicalSkeleton.indexOf('E') >= 0) {
                style = DateFormat.FULL;
            } else {
                style = DateFormat.LONG;
            }
        } else if (monthFieldLen == 3) {
            style = DateFormat.MEDIUM;
        }
        // and now use it to compose date and time
        return SimpleFormatterImpl.formatRawPattern(
                getDateTimeFormat(style), 2, 2, timePattern, datePattern);
    }

    /*
     * Map a skeleton that may have metacharacters jJC to one without, by replacing
     * the metacharacters with locale-appropriate fields of of h/H/k/K and of a/b/B
     * (depends on defaultHourFormatChar and allowedHourFormats being set, which in
     * turn depends on initData having been run). This method also updates the flags
     * as necessary. Returns the updated skeleton.
     */
    private String mapSkeletonMetacharacters(String skeleton, EnumSet<DTPGflags> flags) {
        StringBuilder skeletonCopy = new StringBuilder();
        boolean inQuoted = false;
        for (int patPos = 0; patPos < skeleton.length(); patPos++) {
            char patChr = skeleton.charAt(patPos);
            if (patChr == '\'') {
                inQuoted = !inQuoted;
            } else if (!inQuoted) {
                // Handle special mappings for 'j' and 'C' in which fields lengths
                // 1,3,5 => hour field length 1
                // 2,4,6 => hour field length 2
                // 1,2 => abbreviated dayPeriod (field length 1..3)
                // 3,4 => long dayPeriod (field length 4)
                // 5,6 => narrow dayPeriod (field length 5)
                if (patChr == 'j' || patChr == 'C') {
                    int extraLen = 0; // 1 less than total field length
                    while (patPos + 1 < skeleton.length()
                            && skeleton.charAt(patPos + 1) == patChr) {
                        extraLen++;
                        patPos++;
                    }
                    int hourLen = 1 + (extraLen & 1);
                    int dayPeriodLen = (extraLen < 2) ? 1 : 3 + (extraLen >> 1);
                    char hourChar = 'h';
                    char dayPeriodChar = 'a';
                    if (patChr == 'j') {
                        hourChar = defaultHourFormatChar;
                    } else { // patChr == 'C'
                        String bestAllowed = allowedHourFormats[0];
                        hourChar = bestAllowed.charAt(0);
                        // in #13183 just add b/B to skeleton, no longer need to set special flags
                        char last = bestAllowed.charAt(bestAllowed.length() - 1);
                        if (last == 'b' || last == 'B') {
                            dayPeriodChar = last;
                        }
                    }
                    if (hourChar == 'H' || hourChar == 'k') {
                        dayPeriodLen = 0;
                    }
                    while (dayPeriodLen-- > 0) {
                        skeletonCopy.append(dayPeriodChar);
                    }
                    while (hourLen-- > 0) {
                        skeletonCopy.append(hourChar);
                    }
                } else if (patChr == 'J') {
                    // Get pattern for skeleton with H, then (in adjustFieldTypes)
                    // replace H or k with defaultHourFormatChar
                    skeletonCopy.append('H');
                    flags.add(DTPGflags.SKELETON_USES_CAP_J);
                } else {
                    skeletonCopy.append(patChr);
                }
            }
        }
        return skeletonCopy.toString();
    }

    /**
     * PatternInfo supplies output parameters for addPattern(...). It is used because Java doesn't
     * have real output parameters. It is treated like a struct (eg Point), so all fields are
     * public.
     *
     * @stable ICU 3.6
     */
    public static final class PatternInfo { // struct for return information
        /**
         * @stable ICU 3.6
         */
        public static final int OK = 0;

        /**
         * @stable ICU 3.6
         */
        public static final int BASE_CONFLICT = 1;

        /**
         * @stable ICU 3.6
         */
        public static final int CONFLICT = 2;

        /**
         * @stable ICU 3.6
         */
        public int status;

        /**
         * @stable ICU 3.6
         */
        public String conflictingPattern;

        /**
         * Simple constructor, since this is treated like a struct.
         *
         * @stable ICU 3.6
         */
        public PatternInfo() {}
    }

    /**
     * Adds a pattern to the generator. If the pattern has the same skeleton as an existing pattern,
     * and the override parameter is set, then the previous value is overridden. Otherwise, the
     * previous value is retained. In either case, the conflicting information is returned in
     * PatternInfo.
     *
     * <p>Note that single-field patterns (like "MMM") are automatically added, and don't need to be
     * added explicitly!
     * <!-- From: com.ibm.icu.samples.text.datetimepatterngenerator.DateTimePatternGeneratorSample:addPatternExample -->
     *
     * <p>Example code:
     *
     * <pre>
     * Date date= new GregorianCalendar(1999,9,13,23,58,59).getTime();
     * ULocale locale = ULocale.FRANCE;
     * // Create an DateTimePatternGenerator instance for the given locale
     * DateTimePatternGenerator gen = DateTimePatternGenerator.getInstance(locale);
     * SimpleDateFormat format = new SimpleDateFormat(gen.getBestPattern("MMMMddHmm"), locale);
     * DateTimePatternGenerator.PatternInfo returnInfo = new DateTimePatternGenerator.PatternInfo();
     * // Add '. von' to the existing pattern
     * gen.addPattern("dd'. von' MMMM", true, returnInfo);
     * // Apply the new pattern
     * format.applyPattern(gen.getBestPattern("MMMMddHmm"));
     * System.out.println("New Pattern for FRENCH: "+format.toPattern());
     * System.out.println("Date Time in new Pattern: "+format.format(date));
     *
     * /** output of the sample code:
     *  **************************************************************************************************
     *  New Pattern for FRENCH: dd. 'von' MMMM HH:mm
     *  Date Time in new Pattern: 13. von octobre 23:58
     *  *************************************************************************************************<code>/</code>
     * </pre>
     *
     * @param pattern Pattern to add.
     * @param override When existing values are to be overridden use true, otherwise use false.
     * @param returnInfo Returned information.
     * @stable ICU 3.6
     */
    public DateTimePatternGenerator addPattern(
            String pattern, boolean override, PatternInfo returnInfo) {
        return addPatternWithSkeleton(pattern, null, override, returnInfo);
    }

    /**
     * addPatternWithSkeleton: If skeletonToUse is specified, then an availableFormats entry is
     * being added. In this case: 1. We pass that skeleton to DateTimeMatcher().set instead of
     * having it derive a skeleton from the pattern. 2. If the new entry's skeleton or basePattern
     * does match an existing entry but that entry also had a skeleton specified (i.e. it was also
     * from availableFormats), then the new entry does not override it regardless of the value of
     * the override parameter. This prevents later availableFormats entries from a parent locale
     * overriding earlier ones from the actual specified locale. However, availableFormats entries
     * *should* override entries with matching skeleton whose skeleton was derived (i.e. entries
     * derived from the standard date/time patters for the specified locale). 3. When adding the
     * pattern (skeleton2pattern.put, basePattern_pattern.put), we set a field to indicate that the
     * added entry had a specified skeleton.
     *
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public DateTimePatternGenerator addPatternWithSkeleton(
            String pattern, String skeletonToUse, boolean override, PatternInfo returnInfo) {
        checkFrozen();
        DateTimeMatcher matcher;
        if (skeletonToUse == null) {
            matcher = new DateTimeMatcher().set(pattern, fp, false);
        } else {
            matcher = new DateTimeMatcher().set(skeletonToUse, fp, false);
        }
        String basePattern = matcher.getBasePattern();
        // We only care about base conflicts - and replacing the pattern associated with a base -
        // if:
        // 1. the conflicting previous base pattern did *not* have an explicit skeleton; in that
        // case the previous
        // base + pattern combination was derived from either (a) a canonical item, (b) a standard
        // format, or
        // (c) a pattern specified programmatically with a previous call to addPattern (which would
        // only happen
        // if we are getting here from a subsequent call to addPattern).
        // 2. a skeleton is specified for the current pattern, but override=false; in that case we
        // are checking
        // availableFormats items from root, which should not override any previous entry with the
        // same base.
        PatternWithSkeletonFlag previousPatternWithSameBase = basePattern_pattern.get(basePattern);
        if (previousPatternWithSameBase != null
                && (!previousPatternWithSameBase.skeletonWasSpecified
                        || (skeletonToUse != null && !override))) {
            returnInfo.status = PatternInfo.BASE_CONFLICT;
            returnInfo.conflictingPattern = previousPatternWithSameBase.pattern;
            if (!override) {
                return this;
            }
        }
        // The only time we get here with override=true and skeletonToUse!=null is when adding
        // availableFormats
        // items from CLDR data. In that case, we don't want an item from a parent locale to replace
        // an item with
        // same skeleton from the specified locale, so skip the current item if skeletonWasSpecified
        // is true for
        // the previously-specified conflicting item.
        PatternWithSkeletonFlag previousValue = skeleton2pattern.get(matcher);
        if (previousValue != null) {
            returnInfo.status = PatternInfo.CONFLICT;
            returnInfo.conflictingPattern = previousValue.pattern;
            if (!override || (skeletonToUse != null && previousValue.skeletonWasSpecified))
                return this;
        }
        returnInfo.status = PatternInfo.OK;
        returnInfo.conflictingPattern = "";
        PatternWithSkeletonFlag patWithSkelFlag =
                new PatternWithSkeletonFlag(pattern, skeletonToUse != null);
        if (DEBUG) {
            System.out.println(matcher + " => " + patWithSkelFlag);
        }
        skeleton2pattern.put(matcher, patWithSkelFlag);
        basePattern_pattern.put(basePattern, patWithSkelFlag);
        return this;
    }

    /**
     * Utility to return a unique skeleton from a given pattern. For example, both "MMM-dd" and
     * "dd/MMM" produce the skeleton "MMMdd".
     *
     * @param pattern Input pattern, such as "dd/MMM"
     * @return skeleton, such as "MMMdd"
     * @stable ICU 3.6
     */
    public String getSkeleton(String pattern) {
        synchronized (this) { // synchronized since a getter must be thread-safe
            current.set(pattern, fp, false);
            return current.toString();
        }
    }

    /**
     * Same as getSkeleton, but allows duplicates
     *
     * @param pattern Input pattern, such as "dd/MMM"
     * @return skeleton, such as "MMMdd"
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public String getSkeletonAllowingDuplicates(String pattern) {
        synchronized (this) { // synchronized since a getter must be thread-safe
            current.set(pattern, fp, true);
            return current.toString();
        }
    }

    /**
     * Same as getSkeleton, but allows duplicates and returns a string using canonical pattern chars
     *
     * @param pattern Input pattern, such as "ccc, d LLL"
     * @return skeleton, such as "MMMEd"
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public String getCanonicalSkeletonAllowingDuplicates(String pattern) {
        synchronized (this) { // synchronized since a getter must be thread-safe
            current.set(pattern, fp, true);
            return current.toCanonicalString();
        }
    }

    /**
     * Utility to return a unique base skeleton from a given pattern. This is the same as the
     * skeleton, except that differences in length are minimized so as to only preserve the
     * difference between string and numeric form. So for example, both "MMM-dd" and "d/MMM" produce
     * the skeleton "MMMd" (notice the single d).
     *
     * @param pattern Input pattern, such as "dd/MMM"
     * @return skeleton, such as "MMMdd"
     * @stable ICU 3.6
     */
    public String getBaseSkeleton(String pattern) {
        synchronized (this) { // synchronized since a getter must be thread-safe
            current.set(pattern, fp, false);
            return current.getBasePattern();
        }
    }

    /**
     * Return a list of all the skeletons (in canonical form) from this class, and the patterns that
     * they map to.
     *
     * @param result an output Map in which to place the mapping from skeleton to pattern. If you
     *     want to see the internal order being used, supply a LinkedHashMap. If the input value is
     *     null, then a LinkedHashMap is allocated.
     *     <p><i>Issue: an alternate API would be to just return a list of the skeletons, and then
     *     have a separate routine to get from skeleton to pattern.</i>
     * @return the input Map containing the values.
     * @stable ICU 3.6
     */
    public Map<String, String> getSkeletons(Map<String, String> result) {
        if (result == null) {
            result = new LinkedHashMap<>();
        }
        for (DateTimeMatcher item : skeleton2pattern.keySet()) {
            PatternWithSkeletonFlag patternWithSkelFlag = skeleton2pattern.get(item);
            String pattern = patternWithSkelFlag.pattern;
            if (CANONICAL_SET.contains(pattern)) {
                continue;
            }
            result.put(item.toString(), pattern);
        }
        return result;
    }

    /**
     * Return a list of all the base skeletons (in canonical form) from this class
     *
     * @stable ICU 3.6
     */
    public Set<String> getBaseSkeletons(Set<String> result) {
        if (result == null) {
            result = new HashSet<>();
        }
        result.addAll(basePattern_pattern.keySet());
        return result;
    }

    /**
     * Adjusts the field types (width and subtype) of a pattern to match what is in a skeleton. That
     * is, if you supply a pattern like "d-M H:m", and a skeleton of "MMMMddhhmm", then the input
     * pattern is adjusted to be "dd-MMMM hh:mm". This is used internally to get the best match for
     * the input skeleton, but can also be used externally.
     * <!-- From: com.ibm.icu.samples.text.datetimepatterngenerator.DateTimePatternGeneratorSample:replaceFieldTypesExample -->
     *
     * <p>Example code:
     *
     * <pre>
     * Date date= new GregorianCalendar(1999,9,13,23,58,59).getTime();
     * TimeZone zone = TimeZone.getTimeZone("Europe/Paris");
     * ULocale locale = ULocale.FRANCE;
     * DateTimePatternGenerator gen = DateTimePatternGenerator.getInstance(locale);
     * SimpleDateFormat format = new SimpleDateFormat("EEEE d MMMM y HH:mm:ss zzzz",locale);
     * format.setTimeZone(zone);
     * String pattern = format.toPattern();
     * System.out.println("Pattern before replacement:");
     * System.out.println(pattern);
     * System.out.println("Date/Time format in fr_FR:");
     * System.out.println(format.format(date));
     * // Replace zone "zzzz" in the pattern with "vvvv"
     * String newPattern = gen.replaceFieldTypes(pattern, "vvvv");
     * // Apply the new pattern
     * format.applyPattern(newPattern);
     * System.out.println("Pattern after replacement:");
     * System.out.println(newPattern);
     * System.out.println("Date/Time format in fr_FR:");
     * System.out.println(format.format(date));
     *
     * /** output of the sample code:
     * ***************************************************************************************************
     *  Pattern before replacement:
     *  EEEE d MMMM y HH:mm:ss zzzz
     *  Date/Time format in fr_FR:
     *  jeudi 14 octobre 1999 05:58:59 heure avancée d’Europe centrale
     *  Pattern after replacement:
     *  EEEE d MMMM y HH:mm:ss vvvv
     *  Date/Time format in fr_FR:
     *  jeudi 14 octobre 1999 05:58:59 heure de l’Europe centrale
     *
     * **************************************************************************************************<code>/</code>
     * </pre>
     *
     * @param pattern input pattern
     * @param skeleton For the pattern to match to.
     * @return pattern adjusted to match the skeleton fields widths and subtypes.
     * @stable ICU 3.6
     */
    public String replaceFieldTypes(String pattern, String skeleton) {
        return replaceFieldTypes(pattern, skeleton, MATCH_NO_OPTIONS);
    }

    /**
     * Adjusts the field types (width and subtype) of a pattern to match what is in a skeleton. That
     * is, if you supply a pattern like "d-M H:m", and a skeleton of "MMMMddhhmm", then the input
     * pattern is adjusted to be "dd-MMMM hh:mm". This is used internally to get the best match for
     * the input skeleton, but can also be used externally.
     *
     * @param pattern input pattern
     * @param skeleton For the pattern to match to.
     * @param options MATCH_xxx options for forcing the length of specified fields in the returned
     *     pattern to match those in the skeleton (when this would not happen otherwise). For
     *     default behavior, use MATCH_NO_OPTIONS.
     * @return pattern adjusted to match the skeleton fields widths and subtypes.
     * @stable ICU 4.4
     */
    public String replaceFieldTypes(String pattern, String skeleton, int options) {
        synchronized (this) { // synchronized since a getter must be thread-safe
            PatternWithMatcher patternNoMatcher = new PatternWithMatcher(pattern, null);
            return adjustFieldTypes(
                    patternNoMatcher,
                    current.set(skeleton, fp, false),
                    EnumSet.noneOf(DTPGflags.class),
                    options);
        }
    }

    /**
     * The date time format is a message format pattern used to compose date and time patterns. The
     * default value is "{1} {0}", where {1} will be replaced by the date pattern and {0} will be
     * replaced by the time pattern.
     *
     * <p>This is used when the input skeleton contains both date and time fields, but there is not
     * a close match among the added patterns. For example, suppose that this object was created by
     * adding "dd-MMM" and "hh:mm", and its datetimeFormat is the default "{1} {0}". Then if the
     * input skeleton is "MMMdhmm", there is not an exact match, so the input skeleton is broken up
     * into two components "MMMd" and "hmm". There are close matches for those two skeletons, so the
     * result is put together with this pattern, resulting in "d-MMM h:mm".
     *
     * <p>There are four DateTimeFormats in a DateTimePatternGenerator object, corresponding to date
     * styles DateFormat.FULL..DateFormat.SHORT. This method sets all of them to the specified
     * pattern. To set them individually, see setDateTimeFormat(int style, ...).
     *
     * @param dateTimeFormat message format pattern, where {1} will be replaced by the date pattern
     *     and {0} will be replaced by the time pattern.
     * @stable ICU 3.6
     */
    public void setDateTimeFormat(String dateTimeFormat) {
        checkFrozen();
        for (int style = DateFormat.FULL; style <= DateFormat.SHORT; style++) {
            setDateTimeFormat(style, dateTimeFormat);
        }
    }

    /**
     * Getter corresponding to setDateTimeFormat.
     *
     * <p>There are four DateTimeFormats in a DateTimePatternGenerator object, corresponding to date
     * styles DateFormat.FULL..DateFormat.SHORT. This method gets the style for DateFormat.MEDIUM
     * (the default). To get them individually, see getDateTimeFormat(int style).
     *
     * @return pattern
     * @stable ICU 3.6
     */
    public String getDateTimeFormat() {
        return getDateTimeFormat(DateFormat.MEDIUM);
    }

    /**
     * dateTimeFormats are message patterns used to compose combinations of date and time patterns.
     * There are four length styles, corresponding to the inferred style of the date pattern: -
     * DateFormat.FULL (for date pattern with weekday and long month), else - DateFormat.LONG (for a
     * date pattern with long month), else - DateFormat.MEDIUM (for a date pattern with abbreviated
     * month), else - DateFormat.SHORT (for any other date pattern). For details on dateTimeFormats,
     * see https://www.unicode.org/reports/tr35/tr35-dates.html#dateTimeFormats. The default pattern
     * in the root locale for all styles is "{1} {0}".
     *
     * @param style one of DateFormat.FULL..DateFormat.SHORT. An exception will be thrown if out of
     *     range.
     * @param dateTimeFormat the new dateTimeFormat to set for the specified style
     * @stable ICU 71
     */
    public void setDateTimeFormat(int style, String dateTimeFormat) {
        if (style < DateFormat.FULL || style > DateFormat.SHORT) {
            throw new IllegalArgumentException("Illegal style here: " + style);
        }
        checkFrozen();
        this.dateTimeFormats[style] = dateTimeFormat;
    }

    /**
     * Getter corresponding to setDateTimeFormat.
     *
     * @param style one of DateFormat.FULL..DateFormat.SHORT. An exception will be thrown if out of
     *     range.
     * @return the current dateTimeFormat for the specified style.
     * @stable ICU 71
     */
    public String getDateTimeFormat(int style) {
        if (style < DateFormat.FULL || style > DateFormat.SHORT) {
            throw new IllegalArgumentException("Illegal style here: " + style);
        }
        return dateTimeFormats[style];
    }

    /**
     * The decimal value is used in formatting fractions of seconds. If the skeleton contains
     * fractional seconds, then this is used with the fractional seconds. For example, suppose that
     * the input pattern is "hhmmssSSSS", and the best matching pattern internally is "H:mm:ss", and
     * the decimal string is ",". Then the resulting pattern is modified to be "H:mm:ss,SSSS"
     *
     * @param decimal The decimal to set to.
     * @stable ICU 3.6
     */
    public void setDecimal(String decimal) {
        checkFrozen();
        this.decimal = decimal;
    }

    /**
     * Getter corresponding to setDecimal.
     *
     * @return string corresponding to the decimal point
     * @stable ICU 3.6
     */
    public String getDecimal() {
        return decimal;
    }

    /**
     * Redundant patterns are those which if removed, make no difference in the resulting
     * getBestPattern values. This method returns a list of them, to help check the consistency of
     * the patterns used to build this generator.
     *
     * @param output stores the redundant patterns that are removed. To get these in internal order,
     *     supply a LinkedHashSet. If null, a collection is allocated.
     * @return the collection with added elements.
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public Collection<String> getRedundants(Collection<String> output) {
        synchronized (this) { // synchronized since a getter must be thread-safe
            if (output == null) {
                output = new LinkedHashSet<>();
            }
            for (DateTimeMatcher cur : skeleton2pattern.keySet()) {
                PatternWithSkeletonFlag patternWithSkelFlag = skeleton2pattern.get(cur);
                String pattern = patternWithSkelFlag.pattern;
                if (CANONICAL_SET.contains(pattern)) {
                    continue;
                }
                String trial = getBestPattern(cur.toString(), cur, MATCH_NO_OPTIONS);
                if (trial.equals(pattern)) {
                    output.add(pattern);
                }
            }
            /// CLOVER:OFF
            // The following would never be called since the parameter is false
            // Eclipse stated the following is "dead code"
            /*if (false) { // ordered
                DateTimePatternGenerator results = new DateTimePatternGenerator();
                PatternInfo pinfo = new PatternInfo();
                for (DateTimeMatcher cur : skeleton2pattern.keySet()) {
                    String pattern = skeleton2pattern.get(cur);
                    if (CANONICAL_SET.contains(pattern)) {
                        continue;
                    }
                    //skipMatcher = current;
                    String trial = results.getBestPattern(cur.toString());
                    if (trial.equals(pattern)) {
                        output.add(pattern);
                    } else {
                        results.addPattern(pattern, false, pinfo);
                    }
                }
            }*/
            /// CLOVER:ON
            return output;
        }
    }

    // Field numbers, used for AppendItem functions

    /**
     * @stable ICU 3.6
     */
    public static final int ERA = 0;

    /**
     * @stable ICU 3.6
     */
    public static final int YEAR = 1;

    /**
     * @stable ICU 3.6
     */
    public static final int QUARTER = 2;

    /**
     * @stable ICU 3.6
     */
    public static final int MONTH = 3;

    /**
     * @stable ICU 3.6
     */
    public static final int WEEK_OF_YEAR = 4;

    /**
     * @stable ICU 3.6
     */
    public static final int WEEK_OF_MONTH = 5;

    /**
     * @stable ICU 3.6
     */
    public static final int WEEKDAY = 6;

    /**
     * @stable ICU 3.6
     */
    public static final int DAY = 7;

    /**
     * @stable ICU 3.6
     */
    public static final int DAY_OF_YEAR = 8;

    /**
     * @stable ICU 3.6
     */
    public static final int DAY_OF_WEEK_IN_MONTH = 9;

    /**
     * @stable ICU 3.6
     */
    public static final int DAYPERIOD = 10;

    /**
     * @stable ICU 3.6
     */
    public static final int HOUR = 11;

    /**
     * @stable ICU 3.6
     */
    public static final int MINUTE = 12;

    /**
     * @stable ICU 3.6
     */
    public static final int SECOND = 13;

    /**
     * @stable ICU 3.6
     */
    public static final int FRACTIONAL_SECOND = 14;

    /**
     * @stable ICU 3.6
     */
    public static final int ZONE = 15;

    /**
     * One more than the highest normal field number.
     *
     * @deprecated ICU 58 The numeric value may change over time, see ICU ticket #12420.
     */
    @Deprecated public static final int TYPE_LIMIT = 16;

    /**
     * Field display name width constants for getFieldDisplayName
     *
     * @stable ICU 61
     */
    public enum DisplayWidth {
        /**
         * The full field name
         *
         * @stable ICU 61
         */
        WIDE(""),
        /**
         * An abbreviated field name (may be the same as the wide version, if short enough)
         *
         * @stable ICU 61
         */
        ABBREVIATED("-short"),
        /**
         * The shortest possible field name (may be the same as the abbreviated version)
         *
         * @stable ICU 61
         */
        NARROW("-narrow");

        /**
         * The count of available widths
         *
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated private static int COUNT = DisplayWidth.values().length;

        private final String cldrKey;

        DisplayWidth(String cldrKey) {
            this.cldrKey = cldrKey;
        }

        private String cldrKey() {
            return cldrKey;
        }
    }

    /** The field name width for use in appendItems */
    private static final DisplayWidth APPENDITEM_WIDTH = DisplayWidth.WIDE;

    private static final int APPENDITEM_WIDTH_INT = APPENDITEM_WIDTH.ordinal();
    private static final DisplayWidth[] CLDR_FIELD_WIDTH = DisplayWidth.values();

    // Option masks for getBestPattern, replaceFieldTypes (individual masks may be ORed together)

    /**
     * Default option mask used for {@link #getBestPattern(String, int)} and {@link
     * #replaceFieldTypes(String, String, int)}.
     *
     * @stable ICU 4.4
     * @see #getBestPattern(String, int)
     * @see #replaceFieldTypes(String, String, int)
     */
    public static final int MATCH_NO_OPTIONS = 0;

    /**
     * Option mask for forcing the width of hour field.
     *
     * @stable ICU 4.4
     * @see #getBestPattern(String, int)
     * @see #replaceFieldTypes(String, String, int)
     */
    public static final int MATCH_HOUR_FIELD_LENGTH = 1 << HOUR;

    /**
     * Option mask for forcing the width of minute field.
     *
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated public static final int MATCH_MINUTE_FIELD_LENGTH = 1 << MINUTE;

    /**
     * Option mask for forcing the width of second field.
     *
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated public static final int MATCH_SECOND_FIELD_LENGTH = 1 << SECOND;

    /**
     * Option mask for forcing the width of all date and time fields.
     *
     * @stable ICU 4.4
     * @see #getBestPattern(String, int)
     * @see #replaceFieldTypes(String, String, int)
     */
    public static final int MATCH_ALL_FIELDS_LENGTH = (1 << TYPE_LIMIT) - 1;

    /**
     * An AppendItem format is a pattern used to append a field if there is no good match. For
     * example, suppose that the input skeleton is "GyyyyMMMd", and there is no matching pattern
     * internally, but there is a pattern matching "yyyyMMMd", say "d-MM-yyyy". Then that pattern is
     * used, plus the G. The way these two are conjoined is by using the AppendItemFormat for G
     * (era). So if that value is, say "{0}, {1}" then the final resulting pattern is "d-MM-yyyy,
     * G".
     *
     * <p>There are actually three available variables: {0} is the pattern so far, {1} is the
     * element we are adding, and {2} is the name of the element.
     *
     * <p>This reflects the way that the CLDR data is organized.
     *
     * @param field such as ERA
     * @param value pattern, such as "{0}, {1}"
     * @stable ICU 3.6
     */
    public void setAppendItemFormat(int field, String value) {
        checkFrozen();
        appendItemFormats[field] = value;
    }

    /**
     * Getter corresponding to setAppendItemFormats. Values below 0 or at or above TYPE_LIMIT are
     * illegal arguments.
     *
     * @param field The index to retrieve the append item formats.
     * @return append pattern for field
     * @stable ICU 3.6
     */
    public String getAppendItemFormat(int field) {
        return appendItemFormats[field];
    }

    /**
     * Sets the names of fields, eg "era" in English for ERA. These are only used if the
     * corresponding AppendItemFormat is used, and if it contains a {2} variable.
     *
     * <p>This reflects the way that the CLDR data is organized.
     *
     * @param field Index of the append item names.
     * @param value The value to set the item to.
     * @stable ICU 3.6
     */
    public void setAppendItemName(int field, String value) {
        setFieldDisplayName(field, APPENDITEM_WIDTH, value);
    }

    /**
     * Getter corresponding to setAppendItemName. Values below 0 or at or above TYPE_LIMIT are
     * illegal arguments. Note: The more general method for getting date/time field display names is
     * getFieldDisplayName.
     *
     * @param field The index to get the append item name.
     * @return name for field
     * @see #getFieldDisplayName(int, DisplayWidth)
     * @stable ICU 3.6
     */
    public String getAppendItemName(int field) {
        return getFieldDisplayName(field, APPENDITEM_WIDTH);
    }

    /**
     * Return the default hour cycle.
     *
     * @stable ICU 67
     */
    public DateFormat.HourCycle getDefaultHourCycle() {
        switch (getDefaultHourFormatChar()) {
            case 'h':
                return DateFormat.HourCycle.HOUR_CYCLE_12;
            case 'H':
                return DateFormat.HourCycle.HOUR_CYCLE_23;
            case 'k':
                return DateFormat.HourCycle.HOUR_CYCLE_24;
            case 'K':
                return DateFormat.HourCycle.HOUR_CYCLE_11;
            default:
                throw new AssertionError("should be unreachable");
        }
    }

    /**
     * The private interface to set a display name for a particular date/time field, in one of
     * several possible display widths.
     *
     * @param field The field type, such as ERA.
     * @param width The desired DisplayWidth, such as DisplayWidth.ABBREVIATED.
     * @param value The display name to set
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    private void setFieldDisplayName(int field, DisplayWidth width, String value) {
        checkFrozen();
        if (field < TYPE_LIMIT && field >= 0) {
            fieldDisplayNames[field][width.ordinal()] = value;
        }
    }

    /**
     * The general interface to get a display name for a particular date/time field, in one of
     * several possible display widths.
     *
     * @param field The field type, such as ERA.
     * @param width The desired DisplayWidth, such as DisplayWidth.ABBREVIATED.
     * @return The display name for the field
     * @stable ICU 61
     */
    public String getFieldDisplayName(int field, DisplayWidth width) {
        if (field >= TYPE_LIMIT || field < 0) {
            return "";
        }
        return fieldDisplayNames[field][width.ordinal()];
    }

    /**
     * Determines whether a skeleton contains a single field
     *
     * @param skeleton The skeleton to determine if it contains a single field.
     * @return true or not
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static boolean isSingleField(String skeleton) {
        char first = skeleton.charAt(0);
        for (int i = 1; i < skeleton.length(); ++i) {
            if (skeleton.charAt(i) != first) return false;
        }
        return true;
    }

    /**
     * Add key to HashSet cldrAvailableFormatKeys.
     *
     * @param key of the availableFormats in CLDR
     * @stable ICU 3.6
     */
    private void setAvailableFormat(String key) {
        checkFrozen();
        cldrAvailableFormatKeys.add(key);
    }

    /**
     * This function checks the corresponding slot of CLDR_AVAIL_FORMAT_KEY[] has been added to
     * DateTimePatternGenerator. The function is to avoid the duplicate availableFomats added to the
     * pattern map from parent locales.
     *
     * @param key of the availableFormatMask in CLDR
     * @return true if the corresponding slot of CLDR_AVAIL_FORMAT_KEY[] has been added to
     *     DateTimePatternGenerator.
     * @stable ICU 3.6
     */
    private boolean isAvailableFormatSet(String key) {
        return cldrAvailableFormatKeys.contains(key);
    }

    /**
     * {@inheritDoc}
     *
     * @stable ICU 3.6
     */
    @Override
    public boolean isFrozen() {
        return frozen;
    }

    /**
     * {@inheritDoc}
     *
     * @stable ICU 4.4
     */
    @Override
    public DateTimePatternGenerator freeze() {
        frozen = true;
        return this;
    }

    /**
     * {@inheritDoc}
     *
     * @stable ICU 4.4
     */
    @Override
    public DateTimePatternGenerator cloneAsThawed() {
        DateTimePatternGenerator result = this.clone();
        frozen = false;
        return result;
    }

    /**
     * Returns a copy of this <code>DateTimePatternGenerator</code> object.
     *
     * @return A copy of this <code>DateTimePatternGenerator</code> object.
     * @stable ICU 3.6
     */
    @Override
    @SuppressWarnings("unchecked")
    public DateTimePatternGenerator clone() {
        try {
            DateTimePatternGenerator result = (DateTimePatternGenerator) super.clone();
            result.skeleton2pattern =
                    (TreeMap<DateTimeMatcher, PatternWithSkeletonFlag>) skeleton2pattern.clone();
            result.basePattern_pattern =
                    (TreeMap<String, PatternWithSkeletonFlag>) basePattern_pattern.clone();
            result.dateTimeFormats = dateTimeFormats.clone();
            result.appendItemFormats = appendItemFormats.clone();
            result.fieldDisplayNames = fieldDisplayNames.clone();
            result.current = new DateTimeMatcher();
            result.fp = new FormatParser();
            result._distanceInfo = new DistanceInfo();

            result.frozen = false;
            return result;
        } catch (CloneNotSupportedException e) {
            /// CLOVER:OFF
            throw new ICUCloneNotSupportedException("Internal Error", e);
            /// CLOVER:ON
        }
    }

    /**
     * Utility class for FormatParser. Immutable class that is only used to mark the difference
     * between a variable field and a literal string. Each variable field must consist of 1 to n
     * variable characters, representing date format fields. For example, "VVVV" is valid while "V4"
     * is not, nor is "44".
     *
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static class VariableField {
        private final String string;
        private final int canonicalIndex;

        /**
         * Create a variable field: equivalent to VariableField(string,false);
         *
         * @param string The string for the variable field.
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public VariableField(String string) {
            this(string, false);
        }

        /**
         * Create a variable field
         *
         * @param string The string for the variable field
         * @param strict If true, then only allows exactly those lengths specified by CLDR for
         *     variables. For example, "hh:mm aa" would throw an exception.
         * @throws IllegalArgumentException if the variable field is not valid.
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public VariableField(String string, boolean strict) {
            canonicalIndex = DateTimePatternGenerator.getCanonicalIndex(string, strict);
            if (canonicalIndex < 0) {
                throw new IllegalArgumentException("Illegal datetime field:\t" + string);
            }
            this.string = string;
        }

        /**
         * Get the main type of this variable. These types are ERA, QUARTER, MONTH, DAY,
         * WEEK_OF_YEAR, WEEK_OF_MONTH, WEEKDAY, DAY, DAYPERIOD (am/pm), HOUR, MINUTE,
         * SECOND,FRACTIONAL_SECOND, ZONE.
         *
         * @return main type.
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public int getType() {
            return types[canonicalIndex][1];
        }

        /**
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public static String getCanonicalCode(int type) {
            try {
                return CANONICAL_ITEMS[type];
            } catch (Exception e) {
                return String.valueOf(type);
            }
        }

        /**
         * Check if the type of this variable field is numeric.
         *
         * @return true if the type of this variable field is numeric.
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public boolean isNumeric() {
            return types[canonicalIndex][2] > 0;
        }

        /** Private method. */
        private int getCanonicalIndex() {
            return canonicalIndex;
        }

        /**
         * Get the string represented by this variable.
         *
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Override
        @Deprecated
        public String toString() {
            return string;
        }
    }

    /**
     * This class provides mechanisms for parsing a SimpleDateFormat pattern or generating a new
     * pattern, while handling the quoting. It represents the result of the parse as a list of
     * items, where each item is either a literal string or a variable field. When parsing It can be
     * used to find out which variable fields are in a date format, and in what order, such as for
     * presentation in a UI as separate text entry fields. It can also be used to construct new
     * SimpleDateFormats.
     *
     * <p>Example:
     *
     * <pre>
     * public boolean containsZone(String pattern) {
     * for (Iterator it = formatParser.set(pattern).getItems().iterator(); it.hasNext();) {
     * Object item = it.next();
     * if (item instanceof VariableField) {
     * VariableField variableField = (VariableField) item;
     * if (variableField.getType() == DateTimePatternGenerator.ZONE) {
     * return true;
     * }
     * }
     * }
     * return false;
     * }
     *  </pre>
     *
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static class FormatParser {
        private static final UnicodeSet SYNTAX_CHARS = new UnicodeSet("[a-zA-Z]").freeze();
        private static final UnicodeSet QUOTING_CHARS =
                new UnicodeSet("[[[:script=Latn:][:script=Cyrl:]]&[[:L:][:M:]]]").freeze();
        private transient PatternTokenizer tokenizer =
                new PatternTokenizer()
                        .setSyntaxCharacters(SYNTAX_CHARS)
                        .setExtraQuotingCharacters(QUOTING_CHARS)
                        .setUsingQuote(true);
        private List<Object> items = new ArrayList<>();

        /**
         * Construct an empty date format parser, to which strings and variables can be added with
         * set(...).
         *
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public FormatParser() {}

        /**
         * Parses the string into a list of items.
         *
         * @param string The string to parse.
         * @return this, for chaining
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public final FormatParser set(String string) {
            return set(string, false);
        }

        /**
         * Parses the string into a list of items, taking into account all of the quoting that may
         * be going on.
         *
         * @param string The string to parse.
         * @param strict If true, then only allows exactly those lengths specified by CLDR for
         *     variables. For example, "hh:mm aa" would throw an exception.
         * @return this, for chaining
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public FormatParser set(String string, boolean strict) {
            items.clear();
            if (string.length() == 0) return this;
            tokenizer.setPattern(string);
            StringBuffer buffer = new StringBuffer();
            StringBuffer variable = new StringBuffer();
            while (true) {
                buffer.setLength(0);
                int status = tokenizer.next(buffer);
                if (status == PatternTokenizer.DONE) break;
                if (status == PatternTokenizer.SYNTAX) {
                    if (variable.length() != 0 && buffer.charAt(0) != variable.charAt(0)) {
                        addVariable(variable, false);
                    }
                    variable.append(buffer);
                } else {
                    addVariable(variable, false);
                    items.add(buffer.toString());
                }
            }
            addVariable(variable, false);
            return this;
        }

        private void addVariable(StringBuffer variable, boolean strict) {
            if (variable.length() != 0) {
                items.add(new VariableField(variable.toString(), strict));
                variable.setLength(0);
            }
        }

        //        /** Private method. Return a collection of fields. These will be a mixture of
        // literal Strings and VariableFields. Any "a" variable field is removed.
        //         * @param output List to append the items to. If null, is allocated as an
        // ArrayList.
        //         * @return list
        //         */
        //        private List getVariableFields(List output) {
        //            if (output == null) output = new ArrayList();
        //            main:
        //                for (Iterator it = items.iterator(); it.hasNext();) {
        //                    Object item = it.next();
        //                    if (item instanceof VariableField) {
        //                        String s = item.toString();
        //                        switch(s.charAt(0)) {
        //                        //case 'Q': continue main; // HACK
        //                        case 'a': continue main; // remove
        //                        }
        //                        output.add(item);
        //                    }
        //                }
        //            //System.out.println(output);
        //            return output;
        //        }

        //        /**
        //         * Produce a string which concatenates all the variables. That is, it is the
        // logically the same as the input with all literals removed.
        //         * @return a string which is a concatenation of all the variable fields
        //         */
        //        public String getVariableFieldString() {
        //            List list = getVariableFields(null);
        //            StringBuffer result = new StringBuffer();
        //            for (Iterator it = list.iterator(); it.hasNext();) {
        //                String item = it.next().toString();
        //                result.append(item);
        //            }
        //            return result.toString();
        //        }

        /**
         * Returns modifiable list which is a mixture of Strings and VariableFields, in the order
         * found during parsing. The strings represent literals, and have all quoting removed. Thus
         * the string "dd 'de' MM" will parse into three items:
         *
         * <pre>
         * VariableField: dd
         * String: " de "
         * VariableField: MM
         * </pre>
         *
         * The list is modifiable, so you can add any strings or variables to it, or remove any
         * items.
         *
         * @return modifiable list of items.
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public List<Object> getItems() {
            return items;
        }

        /**
         * Provide display form of formatted input. Each literal string is quoted if necessary..
         * That is, if the input was "hh':'mm", the result would be "hh:mm", since the ":" doesn't
         * need quoting. See quoteLiteral().
         *
         * @return printable output string
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Override
        @Deprecated
        public String toString() {
            return toString(0, items.size());
        }

        /**
         * Provide display form of a segment of the parsed input. Each literal string is minimally
         * quoted. That is, if the input was "hh':'mm", the result would be "hh:mm", since the ":"
         * doesn't need quoting. See quoteLiteral().
         *
         * @param start item to start from
         * @param limit last item +1
         * @return printable output string
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public String toString(int start, int limit) {
            StringBuilder result = new StringBuilder();
            for (int i = start; i < limit; ++i) {
                Object item = items.get(i);
                if (item instanceof String) {
                    String itemString = (String) item;
                    result.append(tokenizer.quoteLiteral(itemString));
                } else {
                    result.append(items.get(i).toString());
                }
            }
            return result.toString();
        }

        /**
         * Returns true if it has a mixture of date and time variable fields: that is, at least one
         * date variable and at least one time variable.
         *
         * @return true or false
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public boolean hasDateAndTimeFields() {
            int foundMask = 0;
            for (Object item : items) {
                if (item instanceof VariableField) {
                    int type = ((VariableField) item).getType();
                    foundMask |= 1 << type;
                }
            }
            boolean isDate = (foundMask & DATE_MASK) != 0;
            boolean isTime = (foundMask & TIME_MASK) != 0;
            return isDate && isTime;
        }

        //        /**
        //         * Internal routine
        //         * @param value
        //         * @param result
        //         * @return list
        //         */
        //        public List getAutoPatterns(String value, List result) {
        //            if (result == null) result = new ArrayList();
        //            int fieldCount = 0;
        //            int minField = Integer.MAX_VALUE;
        //            int maxField = Integer.MIN_VALUE;
        //            for (Iterator it = items.iterator(); it.hasNext();) {
        //                Object item = it.next();
        //                if (item instanceof VariableField) {
        //                    try {
        //                        int type = ((VariableField)item).getType();
        //                        if (minField > type) minField = type;
        //                        if (maxField < type) maxField = type;
        //                        if (type == ZONE || type == DAYPERIOD || type == WEEKDAY) return
        // result; // skip anything with zones
        //                        fieldCount++;
        //                    } catch (Exception e) {
        //                        return result; // if there are any funny fields, return
        //                    }
        //                }
        //            }
        //            if (fieldCount < 3) return result; // skip
        //            // trim from start
        //            // trim first field IF there are no letters around it
        //            // and it is either the min or the max field
        //            // first field is either 0 or 1
        //            for (int i = 0; i < items.size(); ++i) {
        //                Object item = items.get(i);
        //                if (item instanceof VariableField) {
        //                    int type = ((VariableField)item).getType();
        //                    if (type != minField && type != maxField) break;
        //
        //                    if (i > 0) {
        //                        Object previousItem = items.get(0);
        //                        if (alpha.containsSome(previousItem.toString())) break;
        //                    }
        //                    int start = i+1;
        //                    if (start < items.size()) {
        //                        Object nextItem = items.get(start);
        //                        if (nextItem instanceof String) {
        //                            if (alpha.containsSome(nextItem.toString())) break;
        //                            start++; // otherwise skip over string
        //                        }
        //                    }
        //                    result.add(toString(start, items.size()));
        //                    break;
        //                }
        //            }
        //            // now trim from end
        //            for (int i = items.size()-1; i >= 0; --i) {
        //                Object item = items.get(i);
        //                if (item instanceof VariableField) {
        //                    int type = ((VariableField)item).getType();
        //                    if (type != minField && type != maxField) break;
        //                    if (i < items.size() - 1) {
        //                        Object previousItem = items.get(items.size() - 1);
        //                        if (alpha.containsSome(previousItem.toString())) break;
        //                    }
        //                    int end = i-1;
        //                    if (end > 0) {
        //                        Object nextItem = items.get(end);
        //                        if (nextItem instanceof String) {
        //                            if (alpha.containsSome(nextItem.toString())) break;
        //                            end--; // otherwise skip over string
        //                        }
        //                    }
        //                    result.add(toString(0, end+1));
        //                    break;
        //                }
        //            }
        //
        //            return result;
        //        }

        //        private static UnicodeSet alpha = new UnicodeSet("[:alphabetic:]");

        //        private int getType(Object item) {
        //            String s = item.toString();
        //            int canonicalIndex = getCanonicalIndex(s);
        //            if (canonicalIndex < 0) {
        //                throw new IllegalArgumentException("Illegal field:\t"
        //                        + s);
        //            }
        //            int type = types[canonicalIndex][1];
        //            return type;
        //        }

        /**
         * Each literal string is quoted as needed. That is, the ' quote marks will only be added if
         * needed. The exact pattern of quoting is not guaranteed, thus " de la " could be quoted as
         * " 'de la' " or as " 'de' 'la' ".
         *
         * @param string The string to check.
         * @return string with quoted literals
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public Object quoteLiteral(String string) {
            return tokenizer.quoteLiteral(string);
        }
    }

    /**
     * Used by CLDR tooling; not in ICU4C. Note, this will not work correctly with normal skeletons,
     * since fields that should be related in the two skeletons being compared - like EEE and ccc,
     * or y and U - will not be sorted in the same relative place as each other when iterating over
     * both TreeSets being compare, using TreeSet's "natural" code point ordering (this could be
     * addressed by initializing the TreeSet with a comparator that compares fields first by their
     * index from getCanonicalIndex()). However if comparing canonical skeletons from
     * getCanonicalSkeletonAllowingDuplicates it will be OK regardless, since in these skeletons all
     * fields are normalized to the canonical pattern char for those fields - M or L to M, E or c to
     * E, y or U to y, etc. - so corresponding fields will sort in the same way for both TreeMaps.
     *
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public boolean skeletonsAreSimilar(String id, String skeleton) {
        if (id.equals(skeleton)) {
            return true; // fast path
        }
        // must clone array, make sure items are in same order.
        TreeSet<String> parser1 = getSet(id);
        TreeSet<String> parser2 = getSet(skeleton);
        if (parser1.size() != parser2.size()) {
            return false;
        }
        Iterator<String> it2 = parser2.iterator();
        for (String item : parser1) {
            int index1 = getCanonicalIndex(item, false);
            String item2 = it2.next(); // same length so safe
            int index2 = getCanonicalIndex(item2, false);
            if (types[index1][1] != types[index2][1]) {
                return false;
            }
        }
        return true;
    }

    private TreeSet<String> getSet(String id) {
        final List<Object> items = fp.set(id).getItems();
        TreeSet<String> result = new TreeSet<>();
        for (Object obj : items) {
            final String item = obj.toString();
            if (item.startsWith("G") || item.startsWith("a")) {
                continue;
            }
            result.add(item);
        }
        return result;
    }

    // ========= PRIVATES ============

    private static class PatternWithMatcher {
        public String pattern;
        public DateTimeMatcher matcherWithSkeleton;

        // Simple constructor
        public PatternWithMatcher(String pat, DateTimeMatcher matcher) {
            pattern = pat;
            matcherWithSkeleton = matcher;
        }
    }

    private static class PatternWithSkeletonFlag {
        public String pattern;
        public boolean skeletonWasSpecified;

        // Simple constructor
        public PatternWithSkeletonFlag(String pat, boolean skelSpecified) {
            pattern = pat;
            skeletonWasSpecified = skelSpecified;
        }

        @Override
        public String toString() {
            return pattern + "," + skeletonWasSpecified;
        }
    }

    private TreeMap<DateTimeMatcher, PatternWithSkeletonFlag> skeleton2pattern =
            new TreeMap<>(); // items are in priority order
    private TreeMap<String, PatternWithSkeletonFlag> basePattern_pattern =
            new TreeMap<>(); // items are in priority order
    private String decimal = "?";
    // For the following, need fallback patterns in case an empty instance
    // of DateTimePatterngenerator is used for formatting.
    private String[] dateTimeFormats = {"{1} {0}", "{1} {0}", "{1} {0}", "{1} {0}"};
    private String[] appendItemFormats = new String[TYPE_LIMIT];
    private String[][] fieldDisplayNames = new String[TYPE_LIMIT][DisplayWidth.COUNT];
    private char defaultHourFormatChar = 'H';
    // private boolean chineseMonthHack = false;
    // private boolean isComplete = false;
    private volatile boolean frozen = false;

    private transient DateTimeMatcher current = new DateTimeMatcher();
    private transient FormatParser fp = new FormatParser();
    private transient DistanceInfo _distanceInfo = new DistanceInfo();

    private String[] allowedHourFormats;

    private static final int FRACTIONAL_MASK = 1 << FRACTIONAL_SECOND;
    private static final int SECOND_AND_FRACTIONAL_MASK = (1 << SECOND) | (1 << FRACTIONAL_SECOND);

    // Cache for DateTimePatternGenerator
    private static ICUCache<String, DateTimePatternGenerator> DTPNG_CACHE = new SimpleCache<>();

    private void checkFrozen() {
        if (isFrozen()) {
            throw new UnsupportedOperationException("Attempt to modify frozen object");
        }
    }

    /**
     * We only get called here if we failed to find an exact skeleton. We have broken it into date +
     * time, and look for the pieces. If we fail to find a complete skeleton, we compose in a loop
     * until we have all the fields.
     */
    private String getBestAppending(
            DateTimeMatcher source,
            int missingFields,
            DistanceInfo distInfo,
            DateTimeMatcher skipMatcher,
            EnumSet<DTPGflags> flags,
            int options) {
        String resultPattern = null;
        if (missingFields != 0) {
            PatternWithMatcher resultPatternWithMatcher =
                    getBestRaw(source, missingFields, distInfo, skipMatcher);
            resultPattern = adjustFieldTypes(resultPatternWithMatcher, source, flags, options);

            while (distInfo.missingFieldMask != 0) { // precondition: EVERY single field must work!

                // special hack for SSS. If we are missing SSS, and we had ss but found it, replace
                // the s field according to the
                // number separator
                if ((distInfo.missingFieldMask & SECOND_AND_FRACTIONAL_MASK) == FRACTIONAL_MASK
                        && (missingFields & SECOND_AND_FRACTIONAL_MASK)
                                == SECOND_AND_FRACTIONAL_MASK) {
                    resultPatternWithMatcher.pattern = resultPattern;
                    flags = EnumSet.copyOf(flags);
                    flags.add(DTPGflags.FIX_FRACTIONAL_SECONDS);
                    resultPattern =
                            adjustFieldTypes(resultPatternWithMatcher, source, flags, options);
                    distInfo.missingFieldMask &= ~FRACTIONAL_MASK; // remove bit
                    continue;
                }

                int startingMask = distInfo.missingFieldMask;
                PatternWithMatcher tempWithMatcher =
                        getBestRaw(source, distInfo.missingFieldMask, distInfo, skipMatcher);
                String temp = adjustFieldTypes(tempWithMatcher, source, flags, options);
                int foundMask = startingMask & ~distInfo.missingFieldMask;
                int topField = getTopBitNumber(foundMask);
                resultPattern =
                        SimpleFormatterImpl.formatRawPattern(
                                getAppendFormat(topField),
                                2,
                                3,
                                resultPattern,
                                temp,
                                getAppendName(topField));
            }
        }
        return resultPattern;
    }

    private String getAppendName(int foundMask) {
        return "'" + fieldDisplayNames[foundMask][APPENDITEM_WIDTH_INT] + "'";
    }

    private String getAppendFormat(int foundMask) {
        return appendItemFormats[foundMask];
    }

    //    /**
    //     * @param current2
    //     * @return
    //     */
    //    private String adjustSeconds(DateTimeMatcher current2) {
    //        // TODO Auto-generated method stub
    //        return null;
    //    }

    /**
     * @param foundMask
     */
    private int getTopBitNumber(int foundMask) {
        int i = 0;
        while (foundMask != 0) {
            foundMask >>>= 1;
            ++i;
        }
        return i - 1;
    }

    private void addCanonicalItems() {
        PatternInfo patternInfo = new PatternInfo();
        // make sure that every valid field occurs once, with a "default" length
        for (int i = 0; i < CANONICAL_ITEMS.length; ++i) {
            addPattern(String.valueOf(CANONICAL_ITEMS[i]), false, patternInfo);
        }
    }

    private PatternWithMatcher getBestRaw(
            DateTimeMatcher source,
            int includeMask,
            DistanceInfo missingFields,
            DateTimeMatcher skipMatcher) {
        //      if (SHOW_DISTANCE) System.out.println("Searching for: " + source.pattern
        //      + ", mask: " + showMask(includeMask));
        int bestDistance = Integer.MAX_VALUE;
        int bestMissingFieldMask = Integer.MIN_VALUE;
        PatternWithMatcher bestPatternWithMatcher = new PatternWithMatcher("", null);
        DistanceInfo tempInfo = new DistanceInfo();
        for (DateTimeMatcher trial : skeleton2pattern.keySet()) {
            if (trial.equals(skipMatcher)) {
                continue;
            }
            int distance = source.getDistance(trial, includeMask, tempInfo);
            //          if (SHOW_DISTANCE) System.out.println("\tDistance: " + trial.pattern + ":\t"
            //          + distance + ",\tmissing fields: " + tempInfo);

            // Because we iterate over a map the order is undefined. Can change between
            // implementations,
            // versions, and will very likely be different between Java and C/C++.
            // So if we have patterns with the same distance we also look at the missingFieldMask,
            // and we favour the smallest one. Because the field is a bitmask this technically means
            // we
            // favour differences in the "least significant fields". For example we prefer the one
            // with differences
            // in seconds field vs one with difference in the hours field.
            if (distance < bestDistance
                    || (distance == bestDistance
                            && bestMissingFieldMask < tempInfo.missingFieldMask)) {
                bestDistance = distance;
                bestMissingFieldMask = tempInfo.missingFieldMask;
                PatternWithSkeletonFlag patternWithSkelFlag = skeleton2pattern.get(trial);
                bestPatternWithMatcher.pattern = patternWithSkelFlag.pattern;
                // If the best raw match had a specified skeleton then return it too.
                // This can be passed through to adjustFieldTypes to help it do a better job.
                if (patternWithSkelFlag.skeletonWasSpecified) {
                    bestPatternWithMatcher.matcherWithSkeleton = trial;
                } else {
                    bestPatternWithMatcher.matcherWithSkeleton = null;
                }
                missingFields.setTo(tempInfo);
                if (distance == 0) {
                    break;
                }
            }
        }
        return bestPatternWithMatcher;
    }

    /*
     * @param fixFractionalSeconds TODO
     */
    // flags values
    private enum DTPGflags {
        FIX_FRACTIONAL_SECONDS,
        SKELETON_USES_CAP_J,
    // with #13183, no longer need flags for b, B
    ;
    };

    private String adjustFieldTypes(
            PatternWithMatcher patternWithMatcher,
            DateTimeMatcher inputRequest,
            EnumSet<DTPGflags> flags,
            int options) {
        fp.set(patternWithMatcher.pattern);
        StringBuilder newPattern = new StringBuilder();
        for (Object item : fp.getItems()) {
            if (item instanceof String) {
                newPattern.append(fp.quoteLiteral((String) item));
            } else {
                final VariableField variableField = (VariableField) item;

                StringBuilder fieldBuilder = new StringBuilder(variableField.toString());
                //                int canonicalIndex = getCanonicalIndex(field, true);
                //                if (canonicalIndex < 0) {
                //                    continue; // don't adjust
                //                }
                //                int type = types[canonicalIndex][1];
                int type = variableField.getType();

                // handle day periods - with #13183, no longer need special handling here,
                // integrated with normal types

                if (flags.contains(DTPGflags.FIX_FRACTIONAL_SECONDS) && type == SECOND) {
                    fieldBuilder.append(decimal);
                    inputRequest.original.appendFieldTo(FRACTIONAL_SECOND, fieldBuilder);
                } else if (inputRequest.type[type] != 0) {
                    // Here:
                    // - "reqField" is the field from the originally requested skeleton, with length
                    // "reqFieldLen".
                    // - "field" is the field from the found pattern.
                    //
                    // The adjusted field should consist of characters from the originally requested
                    // skeleton, except in the case of MONTH or WEEKDAY or YEAR, in which case it
                    // should consist of characters from the found pattern. There is some adjustment
                    // in some cases of HOUR to "defaultHourFormatChar". There is explanation
                    // how it is done below.
                    //
                    // The length of the adjusted field (adjFieldLen) should match that in the
                    // originally
                    // requested skeleton, except that in the following cases the length of the
                    // adjusted field
                    // should match that in the found pattern (i.e. the length of this pattern field
                    // should
                    // not be adjusted):
                    // 1. type is HOUR and the corresponding bit in options is not set (ticket
                    // #7180).
                    //    Note, we may want to implement a similar change for other numeric fields
                    // (MM, dd,
                    //    etc.) so the default behavior is to get locale preference for field
                    // length, but
                    //    options bits can be used to override this.
                    // 2. There is a specified skeleton for the found pattern and one of the
                    // following is true:
                    //    a) The length of the field in the skeleton (skelFieldLen) is equal to
                    // reqFieldLen.
                    //    b) The pattern field is numeric and the requested field is not, or vice
                    // versa.
                    //
                    // Old behavior was:
                    // normally we just replace the field. However HOUR is special; we only change
                    // the length

                    char reqFieldChar = inputRequest.original.getFieldChar(type);
                    int reqFieldLen = inputRequest.original.getFieldLength(type);
                    if (reqFieldChar == 'E' && reqFieldLen < 3) {
                        reqFieldLen = 3; // 1-3 for E are equivalent to 3 for c,e
                    }
                    int adjFieldLen = reqFieldLen;
                    DateTimeMatcher matcherWithSkeleton = patternWithMatcher.matcherWithSkeleton;
                    if ((type == HOUR && (options & MATCH_HOUR_FIELD_LENGTH) == 0)
                            || (type == MINUTE && (options & MATCH_MINUTE_FIELD_LENGTH) == 0)
                            || (type == SECOND && (options & MATCH_SECOND_FIELD_LENGTH) == 0)) {
                        adjFieldLen = fieldBuilder.length();
                    } else if (matcherWithSkeleton != null
                            && reqFieldChar != 'c'
                            && reqFieldChar != 'e') {
                        // (we skip this section for 'c' and 'e' because unlike the other characters
                        // considered in this function,
                        // they have no minimum field length-- 'E' and 'EE' are equivalent to 'EEE',
                        // but 'e' and 'ee' are not
                        // equivalent to 'eee' -- see the entries for "week day" in
                        // https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table for more info)
                        int skelFieldLen = matcherWithSkeleton.original.getFieldLength(type);
                        boolean patFieldIsNumeric = variableField.isNumeric();
                        boolean reqFieldIsNumeric = inputRequest.fieldIsNumeric(type);
                        if (skelFieldLen == reqFieldLen
                                || (patFieldIsNumeric && !reqFieldIsNumeric)
                                || (reqFieldIsNumeric && !patFieldIsNumeric)) {
                            // don't adjust the field length in the found pattern
                            adjFieldLen = fieldBuilder.length();
                        }
                    }
                    char c =
                            (type != HOUR
                                            && type != MONTH
                                            && type != WEEKDAY
                                            && (type != YEAR || reqFieldChar == 'Y'))
                                    ? reqFieldChar
                                    : fieldBuilder.charAt(0);
                    if (c == 'E' && adjFieldLen < 3) {
                        // see
                        // https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table:
                        // If we want a numeric day-of-week field, we have to use 'e'-- 'E' doesn't
                        // support
                        // numeric day-of-week abbreivations
                        c = 'e';
                    }
                    if (type == HOUR) {
                        // The adjustment here is required to match spec
                        // (https://www.unicode.org/reports/tr35/tr35-dates.html#dfst-hour).
                        // It is necessary to match the hour-cycle preferred by the Locale.
                        // Given that, we need to do the following adjustments:
                        // 1. When hour-cycle is h11 it should replace 'h' by 'K'.
                        // 2. When hour-cycle is h23 it should replace 'H' by 'k'.
                        // 3. When hour-cycle is h24 it should replace 'k' by 'H'.
                        // 4. When hour-cycle is h12 it should replace 'K' by 'h'.
                        if (flags.contains(DTPGflags.SKELETON_USES_CAP_J)
                                || reqFieldChar == defaultHourFormatChar) {
                            c = defaultHourFormatChar;
                        } else if (reqFieldChar == 'h' && defaultHourFormatChar == 'K') {
                            c = 'K';
                        } else if (reqFieldChar == 'H' && defaultHourFormatChar == 'k') {
                            c = 'k';
                        } else if (reqFieldChar == 'k' && defaultHourFormatChar == 'H') {
                            c = 'H';
                        } else if (reqFieldChar == 'K' && defaultHourFormatChar == 'h') {
                            c = 'h';
                        }
                    }
                    fieldBuilder = new StringBuilder();
                    for (int i = adjFieldLen; i > 0; --i) fieldBuilder.append(c);
                }
                newPattern.append(fieldBuilder);
            }
        }
        // if (SHOW_DISTANCE) System.out.println("\tRaw: " + pattern);
        return newPattern.toString();
    }

    //  public static String repeat(String s, int count) {
    //  StringBuffer result = new StringBuffer();
    //  for (int i = 0; i < count; ++i) {
    //  result.append(s);
    //  }
    //  return result.toString();
    //  }

    /**
     * internal routine
     *
     * @param pattern The pattern that is passed.
     * @return field value
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public String getFields(String pattern) {
        fp.set(pattern);
        StringBuilder newPattern = new StringBuilder();
        for (Object item : fp.getItems()) {
            if (item instanceof String) {
                newPattern.append(fp.quoteLiteral((String) item));
            } else {
                newPattern.append("{" + getName(item.toString()) + "}");
            }
        }
        return newPattern.toString();
    }

    private static String showMask(int mask) {
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < TYPE_LIMIT; ++i) {
            if ((mask & (1 << i)) == 0) continue;
            if (result.length() != 0) result.append(" | ");
            result.append(FIELD_NAME[i]);
            result.append(" ");
        }
        return result.toString();
    }

    private static final String[] CLDR_FIELD_APPEND = {
        "Era",
        "Year",
        "Quarter",
        "Month",
        "Week",
        "*",
        "Day-Of-Week",
        "Day",
        "*",
        "*",
        "*",
        "Hour",
        "Minute",
        "Second",
        "*",
        "Timezone"
    };

    private static final String[] CLDR_FIELD_NAME = {
        "era",
        "year",
        "quarter",
        "month",
        "week",
        "weekOfMonth",
        "weekday",
        "day",
        "dayOfYear",
        "weekdayOfMonth",
        "dayperiod",
        "hour",
        "minute",
        "second",
        "*",
        "zone"
    };

    private static final String[] FIELD_NAME = {
        "Era",
        "Year",
        "Quarter",
        "Month",
        "Week_in_Year",
        "Week_in_Month",
        "Weekday",
        "Day",
        "Day_Of_Year",
        "Day_of_Week_in_Month",
        "Dayperiod",
        "Hour",
        "Minute",
        "Second",
        "Fractional_Second",
        "Zone"
    };

    private static final String[] CANONICAL_ITEMS = {
        "G", "y", "Q", "M", "w", "W", "E", "d", "D", "F", "a", "H", "m", "s", "S", "v"
    };

    // canon    DateTimePatternGen      CLDR fields
    // char     field                   bundle key
    // ----     --------------------    ----------------
    // 'G', //  0 ERA                   "era"
    // 'y', //  1 YEAR                  "year"
    // 'Q', //  2 QUARTER               "quarter"
    // 'M', //  3 MONTH                 "month"
    // 'w', //  4 WEEK_OF_YEAR,         "week"
    // 'W', //  5 WEEK_OF_MONTH         "weekOfMonth"
    // 'E', //  6 WEEKDAY               "weekday"
    // 'd', //  7 DAY                   "day"
    // 'D', //  8 DAY_OF_YEAR           "dayOfYear"
    // 'F', //  9 DAY_OF_WEEK_IN_MONTH  "weekdayOfMonth"
    // 'a', // 10 DAYPERIOD             "dayperiod"
    // 'H', // 11 HOUR                  "hour"
    // 'm', // 12 MINUTE                "minute"
    // 's', // 13 SECOND                "second"
    // 'S', // 14 FRACTIONAL_SECOND
    // 'v', // 15 ZONE                  "zone"

    private static final Set<String> CANONICAL_SET = new HashSet<>(Arrays.asList(CANONICAL_ITEMS));
    private Set<String> cldrAvailableFormatKeys = new HashSet<>(20);

    private static final int DATE_MASK = (1 << DAYPERIOD) - 1,
            TIME_MASK = (1 << TYPE_LIMIT) - 1 - DATE_MASK;
    private static final int // numbers are chosen to express 'distance'
            DELTA = 0x10,
            NUMERIC = 0x100,
            NONE = 0,
            NARROW = -0x101,
            SHORTER = -0x102,
            SHORT = -0x103,
            LONG = -0x104,
            EXTRA_FIELD = 0x10000,
            MISSING_FIELD = 0x1000;

    private static String getName(String s) {
        int i = getCanonicalIndex(s, true);
        String name = FIELD_NAME[types[i][1]];
        if (types[i][2] < 0) {
            name += ":S"; // string
        } else {
            name += ":N";
        }
        return name;
    }

    /**
     * Get the canonical index, or return -1 if illegal.
     *
     * @param s
     * @param strict TODO
     */
    private static int getCanonicalIndex(String s, boolean strict) {
        int len = s.length();
        if (len == 0) {
            return -1;
        }
        int ch = s.charAt(0);
        //      verify that all are the same character
        for (int i = 1; i < len; ++i) {
            if (s.charAt(i) != ch) {
                return -1;
            }
        }
        int bestRow = -1;
        for (int i = 0; i < types.length; ++i) {
            int[] row = types[i];
            if (row[0] != ch) continue;
            bestRow = i;
            if (row[3] > len) continue;
            if (row[row.length - 1] < len) continue;
            return i;
        }
        return strict ? -1 : bestRow;
    }

    /** Gets the canonical character associated with the specified field (ERA, YEAR, etc). */
    private static char getCanonicalChar(int field, char reference) {
        // Special case: distinguish between 12-hour and 24-hour
        if (reference == 'h' || reference == 'K') {
            return 'h';
        }

        // Linear search over types (return the top entry for each field)
        for (int i = 0; i < types.length; ++i) {
            int[] row = types[i];
            if (row[1] == field) {
                return (char) row[0];
            }
        }
        throw new IllegalArgumentException("Could not find field " + field);
    }

    private static final int[][] types = {
        // the order here makes a difference only when searching for single field.
        // format is:
        // pattern character, main type, weight, min length, weight
        {'G', ERA, SHORT, 1, 3},
        {'G', ERA, LONG, 4},
        {'G', ERA, NARROW, 5},
        {'y', YEAR, NUMERIC, 1, 20},
        {'Y', YEAR, NUMERIC + DELTA, 1, 20},
        {'u', YEAR, NUMERIC + 2 * DELTA, 1, 20},
        {'r', YEAR, NUMERIC + 3 * DELTA, 1, 20},
        {'U', YEAR, SHORT, 1, 3},
        {'U', YEAR, LONG, 4},
        {'U', YEAR, NARROW, 5},
        {'Q', QUARTER, NUMERIC, 1, 2},
        {'Q', QUARTER, SHORT, 3},
        {'Q', QUARTER, LONG, 4},
        {'Q', QUARTER, NARROW, 5},
        {'q', QUARTER, NUMERIC + DELTA, 1, 2},
        {'q', QUARTER, SHORT - DELTA, 3},
        {'q', QUARTER, LONG - DELTA, 4},
        {'q', QUARTER, NARROW - DELTA, 5},
        {'M', MONTH, NUMERIC, 1, 2},
        {'M', MONTH, SHORT, 3},
        {'M', MONTH, LONG, 4},
        {'M', MONTH, NARROW, 5},
        {'L', MONTH, NUMERIC + DELTA, 1, 2},
        {'L', MONTH, SHORT - DELTA, 3},
        {'L', MONTH, LONG - DELTA, 4},
        {'L', MONTH, NARROW - DELTA, 5},
        {'l', MONTH, NUMERIC + DELTA, 1, 1},
        {'w', WEEK_OF_YEAR, NUMERIC, 1, 2},
        {'W', WEEK_OF_MONTH, NUMERIC, 1},
        {'E', WEEKDAY, SHORT, 1, 3},
        {'E', WEEKDAY, LONG, 4},
        {'E', WEEKDAY, NARROW, 5},
        {'E', WEEKDAY, SHORTER, 6},
        {'c', WEEKDAY, NUMERIC + 2 * DELTA, 1, 2},
        {'c', WEEKDAY, SHORT - 2 * DELTA, 3},
        {'c', WEEKDAY, LONG - 2 * DELTA, 4},
        {'c', WEEKDAY, NARROW - 2 * DELTA, 5},
        {'c', WEEKDAY, SHORTER - 2 * DELTA, 6},
        {
            'e', WEEKDAY, NUMERIC + DELTA, 1, 2
        }, // 'e' is currently not used in CLDR data, should not be canonical
        {'e', WEEKDAY, SHORT - DELTA, 3},
        {'e', WEEKDAY, LONG - DELTA, 4},
        {'e', WEEKDAY, NARROW - DELTA, 5},
        {'e', WEEKDAY, SHORTER - DELTA, 6},
        {'d', DAY, NUMERIC, 1, 2},
        {'g', DAY, NUMERIC + DELTA, 1, 20}, // really internal use, so we don't care
        {'D', DAY_OF_YEAR, NUMERIC, 1, 3},
        {'F', DAY_OF_WEEK_IN_MONTH, NUMERIC, 1},
        {'a', DAYPERIOD, SHORT, 1, 3},
        {'a', DAYPERIOD, LONG, 4},
        {'a', DAYPERIOD, NARROW, 5},
        {'b', DAYPERIOD, SHORT - DELTA, 1, 3},
        {'b', DAYPERIOD, LONG - DELTA, 4},
        {'b', DAYPERIOD, NARROW - DELTA, 5},
        // b needs to be closer to a than to B, so we make this 3*DELTA
        {'B', DAYPERIOD, SHORT - 3 * DELTA, 1, 3},
        {'B', DAYPERIOD, LONG - 3 * DELTA, 4},
        {'B', DAYPERIOD, NARROW - 3 * DELTA, 5},
        {'H', HOUR, NUMERIC + 10 * DELTA, 1, 2}, // 24 hour
        {'k', HOUR, NUMERIC + 11 * DELTA, 1, 2},
        {'h', HOUR, NUMERIC, 1, 2}, // 12 hour
        {'K', HOUR, NUMERIC + DELTA, 1, 2},
        {'m', MINUTE, NUMERIC, 1, 2},
        {'s', SECOND, NUMERIC, 1, 2},
        {'A', SECOND, NUMERIC + DELTA, 1, 1000},
        {'S', FRACTIONAL_SECOND, NUMERIC, 1, 1000},
        {'v', ZONE, SHORT - 2 * DELTA, 1},
        {'v', ZONE, LONG - 2 * DELTA, 4},
        {'z', ZONE, SHORT, 1, 3},
        {'z', ZONE, LONG, 4},
        {'Z', ZONE, NARROW - DELTA, 1, 3},
        {'Z', ZONE, LONG - DELTA, 4},
        {'Z', ZONE, SHORT - DELTA, 5},
        {'O', ZONE, SHORT - DELTA, 1},
        {'O', ZONE, LONG - DELTA, 4},
        {'V', ZONE, SHORT - DELTA, 1},
        {'V', ZONE, LONG - DELTA, 2},
        {'V', ZONE, LONG - 1 - DELTA, 3},
        {'V', ZONE, LONG - 2 - DELTA, 4},
        {'X', ZONE, NARROW - DELTA, 1},
        {'X', ZONE, SHORT - DELTA, 2},
        {'X', ZONE, LONG - DELTA, 4},
        {'x', ZONE, NARROW - DELTA, 1},
        {'x', ZONE, SHORT - DELTA, 2},
        {'x', ZONE, LONG - DELTA, 4},
    };

    /**
     * A compact storage mechanism for skeleton field strings. Several dozen of these will be
     * created for a typical DateTimePatternGenerator instance.
     *
     * @author sffc
     */
    private static class SkeletonFields {
        private byte[] chars = new byte[TYPE_LIMIT];
        private byte[] lengths = new byte[TYPE_LIMIT];
        private static final byte DEFAULT_CHAR = '\0';
        private static final byte DEFAULT_LENGTH = 0;

        public void clear() {
            Arrays.fill(chars, DEFAULT_CHAR);
            Arrays.fill(lengths, DEFAULT_LENGTH);
        }

        void copyFieldFrom(SkeletonFields other, int field) {
            chars[field] = other.chars[field];
            lengths[field] = other.lengths[field];
        }

        void clearField(int field) {
            chars[field] = DEFAULT_CHAR;
            lengths[field] = DEFAULT_LENGTH;
        }

        char getFieldChar(int field) {
            return (char) chars[field];
        }

        int getFieldLength(int field) {
            return lengths[field];
        }

        void populate(int field, String value) {
            // Ensure no loss in character data
            for (char ch : value.toCharArray()) {
                assert ch == value.charAt(0);
            }

            populate(field, value.charAt(0), value.length());
        }

        void populate(int field, char ch, int length) {
            assert ch <= Byte.MAX_VALUE;
            assert length <= Byte.MAX_VALUE;

            chars[field] = (byte) ch;
            lengths[field] = (byte) length;
        }

        public boolean isFieldEmpty(int field) {
            return lengths[field] == DEFAULT_LENGTH;
        }

        @Override
        public String toString() {
            return appendTo(new StringBuilder(), false, false).toString();
        }

        public String toString(boolean skipDayPeriod) {
            return appendTo(new StringBuilder(), false, skipDayPeriod).toString();
        }

        @SuppressWarnings("unused")
        public String toCanonicalString() {
            return appendTo(new StringBuilder(), true, false).toString();
        }

        public String toCanonicalString(boolean skipDayPeriod) {
            return appendTo(new StringBuilder(), true, skipDayPeriod).toString();
        }

        @SuppressWarnings("unused")
        public StringBuilder appendTo(StringBuilder sb) {
            return appendTo(sb, false, false);
        }

        private StringBuilder appendTo(StringBuilder sb, boolean canonical, boolean skipDayPeriod) {
            for (int i = 0; i < TYPE_LIMIT; ++i) {
                if (skipDayPeriod && i == DAYPERIOD) {
                    continue;
                }
                appendFieldTo(i, sb, canonical);
            }
            return sb;
        }

        public StringBuilder appendFieldTo(int field, StringBuilder sb) {
            return appendFieldTo(field, sb, false);
        }

        private StringBuilder appendFieldTo(int field, StringBuilder sb, boolean canonical) {
            char ch = (char) chars[field];
            int length = lengths[field];

            if (canonical) {
                ch = getCanonicalChar(field, ch);
            }

            for (int i = 0; i < length; i++) {
                sb.append(ch);
            }
            return sb;
        }

        public int compareTo(SkeletonFields other) {
            for (int i = 0; i < TYPE_LIMIT; ++i) {
                int charDiff = chars[i] - other.chars[i];
                if (charDiff != 0) {
                    return charDiff;
                }
                int lengthDiff = lengths[i] - other.lengths[i];
                if (lengthDiff != 0) {
                    return lengthDiff;
                }
            }
            return 0;
        }

        @Override
        public boolean equals(Object other) {
            return this == other
                    || (other != null
                            && other instanceof SkeletonFields
                            && compareTo((SkeletonFields) other) == 0);
        }

        @Override
        public int hashCode() {
            return Arrays.hashCode(chars) ^ Arrays.hashCode(lengths);
        }
    }

    private static class DateTimeMatcher implements Comparable<DateTimeMatcher> {
        // private String pattern = null;
        private int[] type = new int[TYPE_LIMIT];
        private SkeletonFields original = new SkeletonFields();
        private SkeletonFields baseOriginal = new SkeletonFields();
        private boolean addedDefaultDayPeriod = false;

        // just for testing; fix to make multi-threaded later
        // private static FormatParser fp = new FormatParser();

        public boolean fieldIsNumeric(int field) {
            return type[field] > 0;
        }

        @Override
        public String toString() {
            // for backward compatibility: addedDefaultDayPeriod true => DateTimeMatcher.set
            // added a single 'a' that was not in the provided skeleton, and it will be
            // removed when generating the skeleton to return.
            return original.toString(addedDefaultDayPeriod);
        }

        // returns a string like toString but using the canonical character for most types,
        // e.g. M for M or L, E for E or c, y for y or U, etc. The hour field is canonicalized
        // to 'H' (for 24-hour types) or 'h' (for 12-hour types)
        public String toCanonicalString() {
            // for backward compatibility: addedDefaultDayPeriod true => DateTimeMatcher.set
            // added a single 'a' that was not in the provided skeleton, and it will be
            // removed when generating the skeleton to return.
            return original.toCanonicalString(addedDefaultDayPeriod);
        }

        String getBasePattern() {
            // for backward compatibility: addedDefaultDayPeriod true => DateTimeMatcher.set
            // added a single 'a' that was not in the provided skeleton, and it will be
            // removed when generating the skeleton to return.
            return baseOriginal.toString(addedDefaultDayPeriod);
        }

        DateTimeMatcher set(String pattern, FormatParser fp, boolean allowDuplicateFields) {
            // Reset any data stored in this instance
            Arrays.fill(type, NONE);
            original.clear();
            baseOriginal.clear();
            addedDefaultDayPeriod = false;

            fp.set(pattern);
            for (Object obj : fp.getItems()) {
                if (!(obj instanceof VariableField)) {
                    continue;
                }
                VariableField item = (VariableField) obj;
                String value = item.toString();
                // don't skip 'a' anymore, dayPeriod handled specially below
                int canonicalIndex = item.getCanonicalIndex();
                //                if (canonicalIndex < 0) {
                //                    throw new IllegalArgumentException("Illegal field:\t"
                //                            + field + "\t in " + pattern);
                //                }
                int[] row = types[canonicalIndex];
                int field = row[1];
                if (!original.isFieldEmpty(field)) {
                    char ch1 = original.getFieldChar(field);
                    char ch2 = value.charAt(0);
                    if (allowDuplicateFields
                            || (ch1 == 'r' && (ch2 == 'U' || ch2 == 'y'))
                            || ((ch1 == 'U' || ch1 == 'y') && ch2 == 'r')) {
                        continue;
                    }
                    throw new IllegalArgumentException(
                            "Conflicting fields:\t" + ch1 + ", " + value + "\t in " + pattern);
                }
                original.populate(field, value);
                char repeatChar = (char) row[0];
                int repeatCount = row[3];
                if ("GEzvQ".indexOf(repeatChar) >= 0) repeatCount = 1;
                baseOriginal.populate(field, repeatChar, repeatCount);
                int subField = row[2];
                if (subField > 0) subField += value.length();
                type[field] = subField;
            }

            // #20739, we have a skeleton with minutes and milliseconds, but no seconds
            //
            // Theoretically we would need to check and fix all fields with "gaps":
            // for example year-day (no month), month-hour (no day), and so on, All the possible
            // field combinations.
            // Plus some smartness: year + hour => should we add month, or add day-of-year?
            // What about month + day-of-week, or month + am/pm indicator.
            // I think beyond a certain point we should not try to fix bad developer input and try
            // guessing what they mean.
            // Garbage in, garbage out.
            if (!original.isFieldEmpty(MINUTE)
                    && !original.isFieldEmpty(FRACTIONAL_SECOND)
                    && original.isFieldEmpty(SECOND)) {
                // Force the use of seconds
                for (int i = 0; i < types.length; ++i) {
                    int[] row = types[i];
                    if (row[1] == SECOND) {
                        // first entry for SECOND
                        original.populate(SECOND, (char) row[0], row[3]);
                        baseOriginal.populate(SECOND, (char) row[0], row[3]);
                        // We add value.length, same as above, when type is first initialized.
                        // The value we want to "fake" here is "s", and 1 means "s".length()
                        int subField = row[2];
                        type[SECOND] = (subField > 0) ? subField + 1 : subField;
                        break;
                    }
                }
            }

            // #13183, handle special behavior for day period characters (a, b, B)
            if (!original.isFieldEmpty(HOUR)) {
                if (original.getFieldChar(HOUR) == 'h' || original.getFieldChar(HOUR) == 'K') {
                    // We have a skeleton with 12-hour-cycle format
                    if (original.isFieldEmpty(DAYPERIOD)) {
                        // But we do not have a day period in the skeleton; add the default
                        // DAYPERIOD (currently "a")
                        for (int i = 0; i < types.length; ++i) {
                            int[] row = types[i];
                            if (row[1] == DAYPERIOD) {
                                // first entry for DAYPERIOD
                                original.populate(DAYPERIOD, (char) row[0], row[3]);
                                baseOriginal.populate(DAYPERIOD, (char) row[0], row[3]);
                                type[DAYPERIOD] = row[2];
                                addedDefaultDayPeriod = true;
                                break;
                            }
                        }
                    }
                } else if (!original.isFieldEmpty(DAYPERIOD)) {
                    // Skeleton has 24-hour-cycle hour format and has dayPeriod, delete dayPeriod
                    // (i.e. ignore it)
                    original.clearField(DAYPERIOD);
                    baseOriginal.clearField(DAYPERIOD);
                    type[DAYPERIOD] = NONE;
                }
            }
            return this;
        }

        int getFieldMask() {
            int result = 0;
            for (int i = 0; i < type.length; ++i) {
                if (type[i] != 0) result |= (1 << i);
            }
            return result;
        }

        @SuppressWarnings("unused")
        void extractFrom(DateTimeMatcher source, int fieldMask) {
            for (int i = 0; i < type.length; ++i) {
                if ((fieldMask & (1 << i)) != 0) {
                    type[i] = source.type[i];
                    original.copyFieldFrom(source.original, i);
                } else {
                    type[i] = NONE;
                    original.clearField(i);
                }
            }
        }

        int getDistance(DateTimeMatcher other, int includeMask, DistanceInfo distanceInfo) {
            int result = 0;
            distanceInfo.clear();
            for (int i = 0; i < TYPE_LIMIT; ++i) {
                int myType = (includeMask & (1 << i)) == 0 ? 0 : type[i];
                int otherType = other.type[i];
                if (myType == otherType) continue; // identical (maybe both zero) add 0
                if (myType == 0) { // and other is not
                    result += EXTRA_FIELD;
                    distanceInfo.addExtra(i);
                } else if (otherType == 0) { // and mine is not
                    result += MISSING_FIELD;
                    distanceInfo.addMissing(i);
                } else {
                    result += Math.abs(myType - otherType); // square of mismatch
                }
            }
            return result;
        }

        @Override
        public int compareTo(DateTimeMatcher that) {
            int result = original.compareTo(that.original);
            return result > 0 ? -1 : result < 0 ? 1 : 0; // Reverse the order.
        }

        @Override
        public boolean equals(Object other) {
            return this == other
                    || (other != null
                            && other instanceof DateTimeMatcher
                            && original.equals(((DateTimeMatcher) other).original));
        }

        @Override
        public int hashCode() {
            return original.hashCode();
        }
    }

    private static class DistanceInfo {
        int missingFieldMask;
        int extraFieldMask;

        void clear() {
            missingFieldMask = extraFieldMask = 0;
        }

        void setTo(DistanceInfo other) {
            missingFieldMask = other.missingFieldMask;
            extraFieldMask = other.extraFieldMask;
        }

        void addMissing(int field) {
            missingFieldMask |= (1 << field);
        }

        void addExtra(int field) {
            extraFieldMask |= (1 << field);
        }

        @Override
        public String toString() {
            return "missingFieldMask: "
                    + DateTimePatternGenerator.showMask(missingFieldMask)
                    + ", extraFieldMask: "
                    + DateTimePatternGenerator.showMask(extraFieldMask);
        }
    }
}
// eof