1// © 2016 and later: Unicode, Inc. and others.
2// License & terms of use: http://www.unicode.org/copyright.html#License
3/*
4 *******************************************************************************
5 * Copyright (C) 2008-2016, International Business Machines Corporation and
6 * others. All Rights Reserved.
7 *******************************************************************************
8 */
9
10package com.ibm.icu.text;
11
12import java.io.Serializable;
13import java.util.HashMap;
14import java.util.HashSet;
15import java.util.LinkedHashMap;
16import java.util.LinkedHashSet;
17import java.util.Locale;
18import java.util.Map;
19import java.util.Map.Entry;
20import java.util.MissingResourceException;
21import java.util.Set;
22
23import com.ibm.icu.impl.ICUCache;
24import com.ibm.icu.impl.ICUData;
25import com.ibm.icu.impl.ICUResourceBundle;
26import com.ibm.icu.impl.SimpleCache;
27import com.ibm.icu.impl.UResource;
28import com.ibm.icu.impl.UResource.Key;
29import com.ibm.icu.impl.UResource.Value;
30import com.ibm.icu.impl.Utility;
31import com.ibm.icu.util.Calendar;
32import com.ibm.icu.util.Freezable;
33import com.ibm.icu.util.ICUCloneNotSupportedException;
34import com.ibm.icu.util.ICUException;
35import com.ibm.icu.util.ULocale;
36import com.ibm.icu.util.UResourceBundle;
37
38/**
39 * DateIntervalInfo is a public class for encapsulating localizable
40 * date time interval patterns. It is used by DateIntervalFormat.
41 *
42 * <P>
43 * For most users, ordinary use of DateIntervalFormat does not need to create
44 * DateIntervalInfo object directly.
45 * DateIntervalFormat will take care of it when creating a date interval
46 * formatter when user pass in skeleton and locale.
47 *
48 * <P>
49 * For power users, who want to create their own date interval patterns,
50 * or want to re-set date interval patterns, they could do so by
51 * directly creating DateIntervalInfo and manipulating it.
52 *
53 * <P>
54 * Logically, the interval patterns are mappings
55 * from (skeleton, the_largest_different_calendar_field)
56 * to (date_interval_pattern).
57 *
58 * <P>
59 * A skeleton
60 * <ol>
61 * <li>
62 * only keeps the field pattern letter and ignores all other parts
63 * in a pattern, such as space, punctuations, and string literals.
64 * <li>
65 * hides the order of fields.
66 * <li>
67 * might hide a field's pattern letter length.
68 *
69 * For those non-digit calendar fields, the pattern letter length is
70 * important, such as MMM, MMMM, and MMMMM; EEE and EEEE,
71 * and the field's pattern letter length is honored.
72 *
73 * For the digit calendar fields,  such as M or MM, d or dd, yy or yyyy,
74 * the field pattern length is ignored and the best match, which is defined
75 * in date time patterns, will be returned without honor the field pattern
76 * letter length in skeleton.
77 * </ol>
78 *
79 * <P>
80 * The calendar fields we support for interval formatting are:
81 * year, month, date, day-of-week, am-pm, hour, hour-of-day, minute, and
82 * second (though we do not currently have specific intervalFormat data for
83 * skeletons with seconds).
84 * Those calendar fields can be defined in the following order:
85 * year &gt; month &gt; date &gt; am-pm &gt; hour &gt;  minute &gt; second
86 *
87 * The largest different calendar fields between 2 calendars is the
88 * first different calendar field in above order.
89 *
90 * For example: the largest different calendar fields between "Jan 10, 2007"
91 * and "Feb 20, 2008" is year.
92 *
93 * <P>
94 * There is a set of pre-defined static skeleton strings.
95 * There are pre-defined interval patterns for those pre-defined skeletons
96 * in locales' resource files.
97 * For example, for a skeleton YEAR_ABBR_MONTH_DAY, which is  "yMMMd",
98 * in  en_US, if the largest different calendar field between date1 and date2
99 * is "year", the date interval pattern  is "MMM d, yyyy - MMM d, yyyy",
100 * such as "Jan 10, 2007 - Jan 10, 2008".
101 * If the largest different calendar field between date1 and date2 is "month",
102 * the date interval pattern is "MMM d - MMM d, yyyy",
103 * such as "Jan 10 - Feb 10, 2007".
104 * If the largest different calendar field between date1 and date2 is "day",
105 * the date interval pattern is ""MMM d-d, yyyy", such as "Jan 10-20, 2007".
106 *
107 * For date skeleton, the interval patterns when year, or month, or date is
108 * different are defined in resource files.
109 * For time skeleton, the interval patterns when am/pm, or hour, or minute is
110 * different are defined in resource files.
111 *
112 *
113 * <P>
114 * There are 2 dates in interval pattern. For most locales, the first date
115 * in an interval pattern is the earlier date. There might be a locale in which
116 * the first date in an interval pattern is the later date.
117 * We use fallback format for the default order for the locale.
118 * For example, if the fallback format is "{0} - {1}", it means
119 * the first date in the interval pattern for this locale is earlier date.
120 * If the fallback format is "{1} - {0}", it means the first date is the
121 * later date.
122 * For a particular interval pattern, the default order can be overriden
123 * by prefixing "latestFirst:" or "earliestFirst:" to the interval pattern.
124 * For example, if the fallback format is "{0}-{1}",
125 * but for skeleton "yMMMd", the interval pattern when day is different is
126 * "latestFirst:d-d MMM yy", it means by default, the first date in interval
127 * pattern is the earlier date. But for skeleton "yMMMd", when day is different,
128 * the first date in "d-d MMM yy" is the later date.
129 *
130 * <P>
131 * The recommended way to create a DateIntervalFormat object is to pass in
132 * the locale.
133 * By using a Locale parameter, the DateIntervalFormat object is
134 * initialized with the pre-defined interval patterns for a given or
135 * default locale.
136 * <P>
137 * Users can also create DateIntervalFormat object
138 * by supplying their own interval patterns.
139 * It provides flexibility for power usage.
140 *
141 * <P>
142 * After a DateIntervalInfo object is created, clients may modify
143 * the interval patterns using setIntervalPattern function as so desired.
144 * Currently, users can only set interval patterns when the following
145 * calendar fields are different: ERA, YEAR, MONTH, DATE,  DAY_OF_MONTH,
146 * DAY_OF_WEEK, AM_PM,  HOUR, HOUR_OF_DAY, MINUTE and SECOND.
147 * Interval patterns when other calendar fields are different is not supported.
148 * <P>
149 * DateIntervalInfo objects are cloneable.
150 * When clients obtain a DateIntervalInfo object,
151 * they can feel free to modify it as necessary.
152 * <P>
153 * DateIntervalInfo are not expected to be subclassed.
154 * Data for a calendar is loaded out of resource bundles.
155 * Through ICU 4.4, date interval patterns are only supported in the Gregoria
156 * calendar; non-Gregorian calendars are supported from ICU 4.4.1.
157 *
158 * @stable ICU 4.0
159 */
160
161public class DateIntervalInfo implements Cloneable, Freezable<DateIntervalInfo>, Serializable {
162
163    /* Save the interval pattern information.
164     * Interval pattern consists of 2 single date patterns and the separator.
165     * For example, interval pattern "MMM d - MMM d, yyyy" consists
166     * a single date pattern "MMM d", another single date pattern "MMM d, yyyy",
167     * and a separator "-".
168     * Also, the first date appears in an interval pattern could be
169     * the earlier date or the later date.
170     * And such information is saved in the interval pattern as well.
171     */
172    static final int currentSerialVersion = 1;
173
174    /**
175     * PatternInfo class saves the first and second part of interval pattern,
176     * and whether the interval pattern is earlier date first.
177     * @stable ICU 4.0
178     */
179    public static final class PatternInfo implements Cloneable, Serializable {
180        static final int currentSerialVersion = 1;
181        private static final long serialVersionUID = 1;
182        private final String fIntervalPatternFirstPart;
183        private final String fIntervalPatternSecondPart;
184        /*
185         * Whether the first date in interval pattern is later date or not.
186         * Fallback format set the default ordering.
187         * And for a particular interval pattern, the order can be
188         * overriden by prefixing the interval pattern with "latestFirst:" or
189         * "earliestFirst:"
190         * For example, given 2 date, Jan 10, 2007 to Feb 10, 2007.
191         * if the fallback format is "{0} - {1}",
192         * and the pattern is "d MMM - d MMM yyyy", the interval format is
193         * "10 Jan - 10 Feb, 2007".
194         * If the pattern is "latestFirst:d MMM - d MMM yyyy",
195         * the interval format is "10 Feb - 10 Jan, 2007"
196         */
197        private final boolean fFirstDateInPtnIsLaterDate;
198
199        /**
200         * Constructs a <code>PatternInfo</code> object.
201         * @param firstPart     The first part of interval pattern.
202         * @param secondPart    The second part of interval pattern.
203         * @param firstDateInPtnIsLaterDate Whether the first date in interval patter is later date or not.
204         * @stable ICU 4.0
205         */
206        public PatternInfo(String firstPart, String secondPart,
207                           boolean firstDateInPtnIsLaterDate) {
208            fIntervalPatternFirstPart = firstPart;
209            fIntervalPatternSecondPart = secondPart;
210            fFirstDateInPtnIsLaterDate = firstDateInPtnIsLaterDate;
211        }
212
213        /**
214         * Returns the first part of interval pattern.
215         * @return The first part of interval pattern.
216         * @stable ICU 4.0
217         */
218        public String getFirstPart() {
219            return fIntervalPatternFirstPart;
220        }
221
222        /**
223         * Returns the second part of interval pattern.
224         * @return The second part of interval pattern.
225         * @stable ICU 4.0
226         */
227        public String getSecondPart() {
228            return fIntervalPatternSecondPart;
229        }
230
231        /**
232         * Returns whether the first date in interval patter is later date or not.
233         * @return Whether the first date in interval patter is later date or not.
234         * @stable ICU 4.0
235         */
236        public boolean firstDateInPtnIsLaterDate() {
237            return fFirstDateInPtnIsLaterDate;
238        }
239
240        /**
241         * Compares the specified object with this <code>PatternInfo</code> for equality.
242         * @param a The object to be compared.
243         * @return <code>true</code> if the specified object is equal to this <code>PatternInfo</code>.
244         * @stable ICU 4.0
245         */
246        @Override
247        public boolean equals(Object a) {
248            if (a instanceof PatternInfo) {
249                PatternInfo patternInfo = (PatternInfo)a;
250                return Utility.objectEquals(fIntervalPatternFirstPart, patternInfo.fIntervalPatternFirstPart) &&
251                       Utility.objectEquals(fIntervalPatternSecondPart, patternInfo.fIntervalPatternSecondPart) &&
252                       fFirstDateInPtnIsLaterDate == patternInfo.fFirstDateInPtnIsLaterDate;
253            }
254            return false;
255        }
256
257        /**
258         * Returns the hash code of this <code>PatternInfo</code>.
259         * @return A hash code value for this object.
260         * @stable ICU 4.0
261         */
262        @Override
263        public int hashCode() {
264            int hash = fIntervalPatternFirstPart != null ? fIntervalPatternFirstPart.hashCode() : 0;
265            if (fIntervalPatternSecondPart != null) {
266                hash ^= fIntervalPatternSecondPart.hashCode();
267            }
268            if (fFirstDateInPtnIsLaterDate) {
269                hash ^= -1;
270            }
271            return hash;
272        }
273
274        /**
275         * {@inheritDoc}
276         * @internal
277         * @deprecated This API is ICU internal only.
278         */
279        @Deprecated
280        @Override
281        public String toString() {
282            return "{first=«" + fIntervalPatternFirstPart + "», second=«" + fIntervalPatternSecondPart + "», reversed:" + fFirstDateInPtnIsLaterDate + "}";
283        }
284    }
285
286    // Following is package protected since
287    // it is shared with DateIntervalFormat.
288    static final String[] CALENDAR_FIELD_TO_PATTERN_LETTER =
289    {
290        "G", "y", "M",
291        "w", "W", "d",
292        "D", "E", "F",
293        "a", "h", "H",
294        "m", "s", "S",  // MINUTE, SECOND, MILLISECOND
295        "z", " ", "Y",  // ZONE_OFFSET, DST_OFFSET, YEAR_WOY
296        "e", "u", "g",  // DOW_LOCAL, EXTENDED_YEAR, JULIAN_DAY
297        "A", " ", " ",  // MILLISECONDS_IN_DAY, IS_LEAP_MONTH.
298    };
299
300
301    private static final long serialVersionUID = 1;
302    private static final int MINIMUM_SUPPORTED_CALENDAR_FIELD =
303                                                          Calendar.SECOND;
304    //private static boolean DEBUG = true;
305
306    private static String CALENDAR_KEY = "calendar";
307    private static String INTERVAL_FORMATS_KEY = "intervalFormats";
308    private static String FALLBACK_STRING = "fallback";
309    private static String LATEST_FIRST_PREFIX = "latestFirst:";
310    private static String EARLIEST_FIRST_PREFIX = "earliestFirst:";
311
312    // DateIntervalInfo cache
313    private final static ICUCache<String, DateIntervalInfo> DIICACHE = new SimpleCache<String, DateIntervalInfo>();
314
315
316    // default interval pattern on the skeleton, {0} - {1}
317    private String fFallbackIntervalPattern;
318    // default order
319    private boolean fFirstDateInPtnIsLaterDate = false;
320
321    // HashMap( skeleton, HashMap(largest_different_field, pattern) )
322    private Map<String, Map<String, PatternInfo>> fIntervalPatterns = null;
323
324    private transient volatile boolean frozen = false;
325
326    // If true, fIntervalPatterns should not be modified in-place because it
327    // is shared with other objects. Unlike frozen which is always true once
328    // set to true, this field can go from true to false as long as frozen is
329    // false.
330    private transient boolean fIntervalPatternsReadOnly = false;
331
332
333    /**
334     * Create empty instance.
335     * It does not initialize any interval patterns except
336     * that it initialize default fall-back pattern as "{0} - {1}",
337     * which can be reset by setFallbackIntervalPattern().
338     *
339     * It should be followed by setFallbackIntervalPattern() and
340     * setIntervalPattern(),
341     * and is recommended to be used only for power users who
342     * wants to create their own interval patterns and use them to create
343     * date interval formatter.
344     * @internal
345     * @deprecated This API is ICU internal only.
346     */
347    @Deprecated
348    public DateIntervalInfo()
349    {
350        fIntervalPatterns = new HashMap<String, Map<String, PatternInfo>>();
351        fFallbackIntervalPattern = "{0} \u2013 {1}";
352    }
353
354
355    /**
356     * Construct DateIntervalInfo for the given locale,
357     * @param locale  the interval patterns are loaded from the appropriate
358     *                calendar data (specified calendar or default calendar)
359     *                in this locale.
360     * @stable ICU 4.0
361     */
362    public DateIntervalInfo(ULocale locale)
363    {
364        initializeData(locale);
365    }
366
367
368    /**
369     * Construct DateIntervalInfo for the given {@link java.util.Locale}.
370     * @param locale  the interval patterns are loaded from the appropriate
371     *                calendar data (specified calendar or default calendar)
372     *                in this locale.
373     * @stable ICU 54
374     */
375    public DateIntervalInfo(Locale locale)
376    {
377        this(ULocale.forLocale(locale));
378    }
379
380    /*
381     * Initialize the DateIntervalInfo from locale
382     * @param locale   the given locale.
383     */
384    private void initializeData(ULocale locale)
385    {
386        String key = locale.toString();
387        DateIntervalInfo dii = DIICACHE.get(key);
388        if ( dii == null ) {
389            // initialize data from scratch
390            setup(locale);
391            // Marking fIntervalPatterns read-only makes cloning cheaper.
392            fIntervalPatternsReadOnly = true;
393            // We freeze what goes in the cache without freezing this object.
394            DIICACHE.put(key, ((DateIntervalInfo) clone()).freeze());
395        } else {
396            initializeFromReadOnlyPatterns(dii);
397        }
398    }
399
400
401
402    /**
403     * Initialize this object
404     * @param dii must have read-only fIntervalPatterns.
405     */
406    private void initializeFromReadOnlyPatterns(DateIntervalInfo dii) {
407        fFallbackIntervalPattern = dii.fFallbackIntervalPattern;
408        fFirstDateInPtnIsLaterDate = dii.fFirstDateInPtnIsLaterDate;
409        fIntervalPatterns = dii.fIntervalPatterns;
410        fIntervalPatternsReadOnly = true;
411    }
412
413
414
415    /**
416     * Sink for enumerating all of the date interval skeletons.
417     */
418    private static final class DateIntervalSink extends UResource.Sink {
419
420        /**
421         * Accepted pattern letters:
422         * Calendar.YEAR
423         * Calendar.MONTH
424         * Calendar.DATE
425         * Calendar.AM_PM
426         * Calendar.HOUR
427         * Calendar.HOUR_OF_DAY
428         * Calendar.MINUTE
429         * Calendar.SECOND
430         */
431        private static final String ACCEPTED_PATTERN_LETTERS = "yMdahHms";
432
433        // Output data
434        DateIntervalInfo dateIntervalInfo;
435
436        // Alias handling
437        String nextCalendarType;
438
439        // Constructor
440        public DateIntervalSink(DateIntervalInfo dateIntervalInfo) {
441            this.dateIntervalInfo = dateIntervalInfo;
442        }
443
444        @Override
445        public void put(Key key, Value value, boolean noFallback) {
446            // Iterate over all the calendar entries and only pick the 'intervalFormats' table.
447            UResource.Table dateIntervalData = value.getTable();
448            for (int i = 0; dateIntervalData.getKeyAndValue(i, key, value); i++) {
449                if (!key.contentEquals(INTERVAL_FORMATS_KEY)) {
450                    continue;
451                }
452
453                // Handle aliases and tables. Ignore the rest.
454                if (value.getType() == ICUResourceBundle.ALIAS) {
455                    // Get the calendar type from the alias path.
456                    nextCalendarType = getCalendarTypeFromPath(value.getAliasString());
457                    break;
458
459                } else if (value.getType() == ICUResourceBundle.TABLE) {
460                    // Iterate over all the skeletons in the 'intervalFormat' table.
461                    UResource.Table skeletonData = value.getTable();
462                    for (int j = 0; skeletonData.getKeyAndValue(j, key, value); j++) {
463                        if (value.getType() == ICUResourceBundle.TABLE) {
464                            // Process the skeleton
465                            processSkeletonTable(key, value);
466                        }
467                    }
468                    break;
469                }
470            }
471        }
472
473        /** Processes the patterns for a skeleton table. */
474        public void processSkeletonTable(Key key, Value value) {
475            // Iterate over all the patterns in the current skeleton table
476            String currentSkeleton = key.toString();
477            UResource.Table patternData = value.getTable();
478            for (int k = 0; patternData.getKeyAndValue(k, key, value); k++) {
479                if (value.getType() == ICUResourceBundle.STRING) {
480                    // Process the key
481                    CharSequence patternLetter = validateAndProcessPatternLetter(key);
482
483                    // If the calendar field has a valid value
484                    if (patternLetter != null) {
485                        // Get the largest different calendar unit
486                        String lrgDiffCalUnit = patternLetter.toString();
487
488                        // Set the interval pattern
489                        setIntervalPatternIfAbsent(currentSkeleton, lrgDiffCalUnit, value);
490                    }
491                }
492            }
493        }
494
495        /**
496         * Returns and resets the next calendar type.
497         * @return Next calendar type
498         */
499        public String getAndResetNextCalendarType() {
500            String tmpCalendarType = nextCalendarType;
501            nextCalendarType = null;
502            return tmpCalendarType;
503        }
504
505        // Alias' path prefix and suffix.
506        private static final String DATE_INTERVAL_PATH_PREFIX =
507            "/LOCALE/" + CALENDAR_KEY + "/";
508        private static final String DATE_INTERVAL_PATH_SUFFIX =
509            "/" + INTERVAL_FORMATS_KEY;
510
511        /**
512         * Extracts the calendar type from the path
513         * @param path
514         * @return Calendar Type
515         */
516        private String getCalendarTypeFromPath(String path) {
517            if (path.startsWith(DATE_INTERVAL_PATH_PREFIX) &&
518                    path.endsWith(DATE_INTERVAL_PATH_SUFFIX)) {
519                return path.substring(DATE_INTERVAL_PATH_PREFIX.length(),
520                    path.length() - DATE_INTERVAL_PATH_SUFFIX.length());
521            }
522            throw new ICUException("Malformed 'intervalFormat' alias path: " + path);
523        }
524
525        /**
526         * Processes the pattern letter
527         * @param patternLetter
528         * @return Pattern letter
529         */
530        private CharSequence validateAndProcessPatternLetter(CharSequence patternLetter) {
531            // Check that patternLetter is just one letter
532            if (patternLetter.length() != 1) { return null; }
533
534            // Check that the pattern letter is accepted
535            char letter = patternLetter.charAt(0);
536            if (ACCEPTED_PATTERN_LETTERS.indexOf(letter) < 0) {
537                return null;
538            }
539
540            // Replace 'h' for 'H'
541            if (letter == CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.HOUR_OF_DAY].charAt(0)) {
542                patternLetter = CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.HOUR];
543            }
544
545            return patternLetter;
546        }
547
548        /**
549         * Stores the interval pattern for the current skeleton in the internal data structure
550         * if it's not present.
551         * @param lrgDiffCalUnit
552         * @param intervalPattern
553         */
554        private void setIntervalPatternIfAbsent(String currentSkeleton, String lrgDiffCalUnit, Value intervalPattern) {
555            // Check if the pattern has already been stored on the data structure.
556            Map<String, PatternInfo> patternsOfOneSkeleton =
557                    dateIntervalInfo.fIntervalPatterns.get(currentSkeleton);
558            if (patternsOfOneSkeleton == null || !patternsOfOneSkeleton.containsKey(lrgDiffCalUnit)) {
559                // Store the pattern
560                dateIntervalInfo.setIntervalPatternInternally(currentSkeleton, lrgDiffCalUnit,
561                        intervalPattern.toString());
562            }
563        }
564    }
565
566
567    /*
568     * Initialize DateIntervalInfo from calendar data
569     * @param calData  calendar data
570     */
571    private void setup(ULocale locale) {
572        int DEFAULT_HASH_SIZE = 19;
573        fIntervalPatterns = new HashMap<String, Map<String, PatternInfo>>(DEFAULT_HASH_SIZE);
574        // initialize to guard if there is no interval date format defined in
575        // resource files
576        fFallbackIntervalPattern = "{0} \u2013 {1}";
577
578        try {
579            // Get the correct calendar type
580            String calendarTypeToUse = locale.getKeywordValue("calendar");
581            if ( calendarTypeToUse == null ) {
582                String[] preferredCalendarTypes =
583                        Calendar.getKeywordValuesForLocale("calendar", locale, true);
584                calendarTypeToUse = preferredCalendarTypes[0]; // the most preferred calendar
585            }
586            if ( calendarTypeToUse == null ) {
587                calendarTypeToUse = "gregorian"; // fallback
588            }
589
590            // Instantiate the sink to process the data and the resource bundle
591            DateIntervalSink sink = new DateIntervalSink(this);
592            ICUResourceBundle resource =
593                    (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale);
594
595            // Get the fallback pattern
596            String fallbackPattern = resource.getStringWithFallback(CALENDAR_KEY + "/" + calendarTypeToUse
597                    + "/" + INTERVAL_FORMATS_KEY + "/" + FALLBACK_STRING);
598            setFallbackIntervalPattern(fallbackPattern);
599
600            // Already loaded calendar types
601            Set<String> loadedCalendarTypes = new HashSet<String>();
602
603            while (calendarTypeToUse != null) {
604                // Throw an exception when a loop is detected
605                if (loadedCalendarTypes.contains(calendarTypeToUse)) {
606                    throw new ICUException("Loop in calendar type fallback: " + calendarTypeToUse);
607                }
608
609                // Register the calendar type to avoid loops
610                loadedCalendarTypes.add(calendarTypeToUse);
611
612                // Get all resources for this calendar type
613                String pathToIntervalFormats = CALENDAR_KEY + "/" + calendarTypeToUse;
614                resource.getAllItemsWithFallback(pathToIntervalFormats, sink);
615
616                // Get next calendar type to load if there was an alias pointing at it
617                calendarTypeToUse = sink.getAndResetNextCalendarType();
618            }
619        } catch ( MissingResourceException e) {
620            // Will fallback to {data0} - {date1}
621        }
622    }
623
624
625    /*
626     * Split interval patterns into 2 part.
627     * @param intervalPattern  interval pattern
628     * @return the index in interval pattern which split the pattern into 2 part
629     */
630    private static int splitPatternInto2Part(String intervalPattern) {
631        boolean inQuote = false;
632        char prevCh = 0;
633        int count = 0;
634
635        /* repeatedPattern used to record whether a pattern has already seen.
636           It is a pattern applies to first calendar if it is first time seen,
637           otherwise, it is a pattern applies to the second calendar
638         */
639        int[] patternRepeated = new int[58];
640
641        int PATTERN_CHAR_BASE = 0x41;
642
643        /* loop through the pattern string character by character looking for
644         * the first repeated pattern letter, which breaks the interval pattern
645         * into 2 parts.
646         */
647        int i;
648        boolean foundRepetition = false;
649        for (i = 0; i < intervalPattern.length(); ++i) {
650            char ch = intervalPattern.charAt(i);
651
652            if (ch != prevCh && count > 0) {
653                // check the repeativeness of pattern letter
654                int repeated = patternRepeated[prevCh - PATTERN_CHAR_BASE];
655                if ( repeated == 0 ) {
656                    patternRepeated[prevCh - PATTERN_CHAR_BASE] = 1;
657                } else {
658                    foundRepetition = true;
659                    break;
660                }
661                count = 0;
662            }
663            if (ch == '\'') {
664                // Consecutive single quotes are a single quote literal,
665                // either outside of quotes or between quotes
666                if ((i+1) < intervalPattern.length() &&
667                    intervalPattern.charAt(i+1) == '\'') {
668                    ++i;
669                } else {
670                    inQuote = ! inQuote;
671                }
672            }
673            else if (!inQuote && ((ch >= 0x0061 /*'a'*/ && ch <= 0x007A /*'z'*/)
674                        || (ch >= 0x0041 /*'A'*/ && ch <= 0x005A /*'Z'*/))) {
675                // ch is a date-time pattern character
676                prevCh = ch;
677                ++count;
678            }
679        }
680        // check last pattern char, distinguish
681        // "dd MM" ( no repetition ),
682        // "d-d"(last char repeated ), and
683        // "d-d MM" ( repetition found )
684        if ( count > 0 && foundRepetition == false ) {
685            if ( patternRepeated[prevCh - PATTERN_CHAR_BASE] == 0 ) {
686                count = 0;
687            }
688        }
689        return (i - count);
690    }
691
692
693    /**
694     * Provides a way for client to build interval patterns.
695     * User could construct DateIntervalInfo by providing
696     * a list of skeletons and their patterns.
697     * <P>
698     * For example:
699     * <pre>
700     * DateIntervalInfo dIntervalInfo = new DateIntervalInfo();
701     * dIntervalInfo.setIntervalPattern("yMd", Calendar.YEAR, "'from' yyyy-M-d 'to' yyyy-M-d");
702     * dIntervalInfo.setIntervalPattern("yMMMd", Calendar.MONTH, "'from' yyyy MMM d 'to' MMM d");
703     * dIntervalInfo.setIntervalPattern("yMMMd", Calendar.DAY, "yyyy MMM d-d");
704     * dIntervalInfo.setFallbackIntervalPattern("{0} ~ {1}");
705     * </pre>
706     *
707     * Restriction:
708     * Currently, users can only set interval patterns when the following
709     * calendar fields are different: ERA, YEAR, MONTH, DATE,  DAY_OF_MONTH,
710     * DAY_OF_WEEK, AM_PM,  HOUR, HOUR_OF_DAY, MINUTE, and SECOND.
711     * Interval patterns when other calendar fields are different are
712     * not supported.
713     *
714     * @param skeleton         the skeleton on which interval pattern based
715     * @param lrgDiffCalUnit   the largest different calendar unit.
716     * @param intervalPattern  the interval pattern on the largest different
717     *                         calendar unit.
718     *                         For example, if lrgDiffCalUnit is
719     *                         "year", the interval pattern for en_US when year
720     *                         is different could be "'from' yyyy 'to' yyyy".
721     * @throws IllegalArgumentException  if setting interval pattern on
722     *                            a calendar field that is smaller
723     *                            than the MINIMUM_SUPPORTED_CALENDAR_FIELD
724     * @throws UnsupportedOperationException  if the object is frozen
725     * @stable ICU 4.0
726     */
727    public void setIntervalPattern(String skeleton,
728                                   int lrgDiffCalUnit,
729                                   String intervalPattern)
730    {
731        if ( frozen ) {
732            throw new UnsupportedOperationException("no modification is allowed after DII is frozen");
733        }
734        if ( lrgDiffCalUnit > MINIMUM_SUPPORTED_CALENDAR_FIELD ) {
735            throw new IllegalArgumentException("calendar field is larger than MINIMUM_SUPPORTED_CALENDAR_FIELD");
736        }
737        if (fIntervalPatternsReadOnly) {
738            fIntervalPatterns = cloneIntervalPatterns(fIntervalPatterns);
739            fIntervalPatternsReadOnly = false;
740        }
741        PatternInfo ptnInfo = setIntervalPatternInternally(skeleton,
742                          CALENDAR_FIELD_TO_PATTERN_LETTER[lrgDiffCalUnit],
743                          intervalPattern);
744        if ( lrgDiffCalUnit == Calendar.HOUR_OF_DAY ) {
745            setIntervalPattern(skeleton,
746                               CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.AM_PM],
747                               ptnInfo);
748            setIntervalPattern(skeleton,
749                               CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.HOUR],
750                               ptnInfo);
751        } else if ( lrgDiffCalUnit == Calendar.DAY_OF_MONTH ||
752                    lrgDiffCalUnit == Calendar.DAY_OF_WEEK ) {
753            setIntervalPattern(skeleton,
754                               CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.DATE],
755                               ptnInfo);
756        }
757    }
758
759
760    /* Set Interval pattern.
761     *
762     * It generates the interval pattern info,
763     * afer which, not only sets the interval pattern info into the hash map,
764     * but also returns the interval pattern info to the caller
765     * so that caller can re-use it.
766     *
767     * @param skeleton         skeleton on which the interval pattern based
768     * @param lrgDiffCalUnit   the largest different calendar unit.
769     * @param intervalPattern  the interval pattern on the largest different
770     *                         calendar unit.
771     * @return the interval pattern pattern information
772     */
773    private PatternInfo setIntervalPatternInternally(String skeleton,
774                                                String lrgDiffCalUnit,
775                                                String intervalPattern) {
776        Map<String, PatternInfo> patternsOfOneSkeleton = fIntervalPatterns.get(skeleton);
777        boolean emptyHash = false;
778        if (patternsOfOneSkeleton == null) {
779            patternsOfOneSkeleton = new HashMap<String, PatternInfo>();
780            emptyHash = true;
781        }
782        boolean order = fFirstDateInPtnIsLaterDate;
783        // check for "latestFirst:" or "earliestFirst:" prefix
784        if ( intervalPattern.startsWith(LATEST_FIRST_PREFIX) ) {
785            order = true;
786            int prefixLength = LATEST_FIRST_PREFIX.length();
787            intervalPattern = intervalPattern.substring(prefixLength, intervalPattern.length());
788        } else if ( intervalPattern.startsWith(EARLIEST_FIRST_PREFIX) ) {
789            order = false;
790            int earliestFirstLength = EARLIEST_FIRST_PREFIX.length();
791            intervalPattern = intervalPattern.substring(earliestFirstLength, intervalPattern.length());
792        }
793        PatternInfo itvPtnInfo = genPatternInfo(intervalPattern, order);
794
795        patternsOfOneSkeleton.put(lrgDiffCalUnit, itvPtnInfo);
796        if ( emptyHash == true ) {
797            fIntervalPatterns.put(skeleton, patternsOfOneSkeleton);
798        }
799
800        return itvPtnInfo;
801    }
802
803
804    /* Set Interval pattern.
805     *
806     * @param skeleton         skeleton on which the interval pattern based
807     * @param lrgDiffCalUnit   the largest different calendar unit.
808     * @param ptnInfo          interval pattern infomration
809     */
810    private void setIntervalPattern(String skeleton,
811                                    String lrgDiffCalUnit,
812                                    PatternInfo ptnInfo) {
813        Map<String, PatternInfo> patternsOfOneSkeleton = fIntervalPatterns.get(skeleton);
814        patternsOfOneSkeleton.put(lrgDiffCalUnit, ptnInfo);
815    }
816
817
818    /**
819     * Break interval patterns as 2 part and save them into pattern info.
820     * @param intervalPattern  interval pattern
821     * @param laterDateFirst   whether the first date in intervalPattern
822     *                         is earlier date or later date
823     * @return                 pattern info object
824     * @internal
825     * @deprecated This API is ICU internal only.
826     */
827    @Deprecated
828    public static PatternInfo genPatternInfo(String intervalPattern,
829                                      boolean laterDateFirst) {
830        int splitPoint = splitPatternInto2Part(intervalPattern);
831
832        String firstPart = intervalPattern.substring(0, splitPoint);
833        String secondPart = null;
834        if ( splitPoint < intervalPattern.length() ) {
835            secondPart = intervalPattern.substring(splitPoint, intervalPattern.length());
836        }
837
838        return new PatternInfo(firstPart, secondPart, laterDateFirst);
839    }
840
841
842    /**
843     * Get the interval pattern given the largest different calendar field.
844     * @param skeleton   the skeleton
845     * @param field      the largest different calendar field
846     * @return interval pattern  return null if interval pattern is not found.
847     * @throws IllegalArgumentException  if getting interval pattern on
848     *                            a calendar field that is smaller
849     *                            than the MINIMUM_SUPPORTED_CALENDAR_FIELD
850     * @stable ICU 4.0
851     */
852    public PatternInfo getIntervalPattern(String skeleton, int field)
853    {
854        if ( field > MINIMUM_SUPPORTED_CALENDAR_FIELD ) {
855            throw new IllegalArgumentException("no support for field less than SECOND");
856        }
857        Map<String, PatternInfo> patternsOfOneSkeleton = fIntervalPatterns.get(skeleton);
858        if ( patternsOfOneSkeleton != null ) {
859            PatternInfo intervalPattern = patternsOfOneSkeleton.
860                get(CALENDAR_FIELD_TO_PATTERN_LETTER[field]);
861            if ( intervalPattern != null ) {
862                return intervalPattern;
863            }
864        }
865        return null;
866    }
867
868
869
870    /**
871     * Get the fallback interval pattern.
872     * @return fallback interval pattern
873     * @stable ICU 4.0
874     */
875    public String getFallbackIntervalPattern()
876    {
877        return fFallbackIntervalPattern;
878    }
879
880
881    /**
882     * Re-set the fallback interval pattern.
883     *
884     * In construction, default fallback pattern is set as "{0} - {1}".
885     * And constructor taking locale as parameter will set the
886     * fallback pattern as what defined in the locale resource file.
887     *
888     * This method provides a way for user to replace the fallback pattern.
889     *
890     * @param fallbackPattern                 fall-back interval pattern.
891     * @throws UnsupportedOperationException  if the object is frozen
892     * @throws IllegalArgumentException       if there is no pattern {0} or
893     *                                        pattern {1} in fallbakckPattern
894     *
895     * @stable ICU 4.0
896     */
897    public void setFallbackIntervalPattern(String fallbackPattern)
898    {
899        if ( frozen ) {
900            throw new UnsupportedOperationException("no modification is allowed after DII is frozen");
901        }
902        int firstPatternIndex = fallbackPattern.indexOf("{0}");
903        int secondPatternIndex = fallbackPattern.indexOf("{1}");
904        if ( firstPatternIndex == -1 || secondPatternIndex == -1 ) {
905            throw new IllegalArgumentException("no pattern {0} or pattern {1} in fallbackPattern");
906        }
907        if ( firstPatternIndex > secondPatternIndex ) {
908            fFirstDateInPtnIsLaterDate = true;
909        }
910        fFallbackIntervalPattern = fallbackPattern;
911    }
912
913
914    /**
915     * Get default order -- whether the first date in pattern is later date
916     *                      or not.
917     *
918     * return default date ordering in interval pattern. TRUE if the first date
919     *        in pattern is later date, FALSE otherwise.
920     * @stable ICU 4.0
921     */
922    public boolean getDefaultOrder()
923    {
924        return fFirstDateInPtnIsLaterDate;
925    }
926
927
928    /**
929     * Clone this object.
930     * @return     a copy of the object
931     * @stable ICU4.0
932     */
933    @Override
934    public Object clone()
935    {
936        if ( frozen ) {
937            return this;
938        }
939        return cloneUnfrozenDII();
940    }
941
942
943    /*
944     * Clone an unfrozen DateIntervalInfo object.
945     * @return     a copy of the object
946     */
947    private Object cloneUnfrozenDII() //throws IllegalStateException
948    {
949        try {
950            DateIntervalInfo other = (DateIntervalInfo) super.clone();
951            other.fFallbackIntervalPattern=fFallbackIntervalPattern;
952            other.fFirstDateInPtnIsLaterDate = fFirstDateInPtnIsLaterDate;
953            if (fIntervalPatternsReadOnly) {
954                other.fIntervalPatterns = fIntervalPatterns;
955                other.fIntervalPatternsReadOnly = true;
956            } else {
957                other.fIntervalPatterns = cloneIntervalPatterns(fIntervalPatterns);
958                other.fIntervalPatternsReadOnly = false;
959            }
960            other.frozen = false;
961            return other;
962        } catch ( CloneNotSupportedException e ) {
963            ///CLOVER:OFF
964            throw new  ICUCloneNotSupportedException("clone is not supported", e);
965            ///CLOVER:ON
966        }
967    }
968
969    private static Map<String, Map<String, PatternInfo>> cloneIntervalPatterns(
970            Map<String, Map<String, PatternInfo>> patterns) {
971        Map<String, Map<String, PatternInfo>> result = new HashMap<String, Map<String, PatternInfo>>();
972        for (Entry<String, Map<String, PatternInfo>> skeletonEntry : patterns.entrySet()) {
973            String skeleton = skeletonEntry.getKey();
974            Map<String, PatternInfo> patternsOfOneSkeleton = skeletonEntry.getValue();
975            Map<String, PatternInfo> oneSetPtn = new HashMap<String, PatternInfo>();
976            for (Entry<String, PatternInfo> calEntry : patternsOfOneSkeleton.entrySet()) {
977                String calField = calEntry.getKey();
978                PatternInfo value = calEntry.getValue();
979                oneSetPtn.put(calField, value);
980            }
981            result.put(skeleton, oneSetPtn);
982        }
983        return result;
984    }
985
986
987
988    /**
989     * {@inheritDoc}
990     *
991     * @stable ICU 4.0
992     */
993    @Override
994    public boolean isFrozen() {
995        return frozen;
996    }
997
998    /**
999     * {@inheritDoc}
1000     *
1001     * @stable ICU 4.4
1002     */
1003    @Override
1004    public DateIntervalInfo freeze() {
1005        fIntervalPatternsReadOnly = true;
1006        frozen = true;
1007        return this;
1008    }
1009
1010    /**
1011     * {@inheritDoc}
1012     *
1013     * @stable ICU 4.4
1014     */
1015    @Override
1016    public DateIntervalInfo cloneAsThawed() {
1017        DateIntervalInfo result = (DateIntervalInfo) (this.cloneUnfrozenDII());
1018        return result;
1019    }
1020
1021
1022    /**
1023     * Parse skeleton, save each field's width.
1024     * It is used for looking for best match skeleton,
1025     * and adjust pattern field width.
1026     * @param skeleton            skeleton to be parsed
1027     * @param skeletonFieldWidth  parsed skeleton field width
1028     */
1029    static void parseSkeleton(String skeleton, int[] skeletonFieldWidth) {
1030        int PATTERN_CHAR_BASE = 0x41;
1031        for ( int i = 0; i < skeleton.length(); ++i ) {
1032            ++skeletonFieldWidth[skeleton.charAt(i) - PATTERN_CHAR_BASE];
1033        }
1034    }
1035
1036
1037
1038    /*
1039     * Check whether one field width is numeric while the other is string.
1040     *
1041     * TODO (xji): make it general
1042     *
1043     * @param fieldWidth          one field width
1044     * @param anotherFieldWidth   another field width
1045     * @param patternLetter       pattern letter char
1046     * @return true if one field width is numeric and the other is string,
1047     *         false otherwise.
1048     */
1049    private static boolean stringNumeric(int fieldWidth,
1050                                         int anotherFieldWidth,
1051                                         char patternLetter) {
1052        if ( patternLetter == 'M' ) {
1053            if ( fieldWidth <= 2 && anotherFieldWidth > 2 ||
1054                 fieldWidth > 2 && anotherFieldWidth <= 2 ) {
1055                return true;
1056            }
1057        }
1058        return false;
1059    }
1060
1061
1062    /*
1063     * given an input skeleton, get the best match skeleton
1064     * which has pre-defined interval pattern in resource file.
1065     *
1066     * TODO (xji): set field weight or
1067     *             isolate the funtionality in DateTimePatternGenerator
1068     * @param  inputSkeleton        input skeleton
1069     * @return 0, if there is exact match for input skeleton
1070     *         1, if there is only field width difference between
1071     *            the best match and the input skeleton
1072     *         2, the only field difference is 'v' and 'z'
1073     *        -1, if there is calendar field difference between
1074     *            the best match and the input skeleton
1075     */
1076    DateIntervalFormat.BestMatchInfo getBestSkeleton(String inputSkeleton) {
1077        String bestSkeleton = inputSkeleton;
1078        int[] inputSkeletonFieldWidth = new int[58];
1079        int[] skeletonFieldWidth = new int[58];
1080
1081        final int DIFFERENT_FIELD = 0x1000;
1082        final int STRING_NUMERIC_DIFFERENCE = 0x100;
1083        final int BASE = 0x41;
1084
1085        // TODO: this is a hack for 'v' and 'z'
1086        // resource bundle only have time skeletons ending with 'v',
1087        // but not for time skeletons ending with 'z'.
1088        boolean replaceZWithV = false;
1089        if ( inputSkeleton.indexOf('z') != -1 ) {
1090            inputSkeleton = inputSkeleton.replace('z', 'v');
1091            replaceZWithV = true;
1092        }
1093
1094        parseSkeleton(inputSkeleton, inputSkeletonFieldWidth);
1095        int bestDistance = Integer.MAX_VALUE;
1096        // 0 means exact the same skeletons;
1097        // 1 means having the same field, but with different length,
1098        // 2 means only z/v differs
1099        // -1 means having different field.
1100        int bestFieldDifference = 0;
1101        for (String skeleton : fIntervalPatterns.keySet()) {
1102            // clear skeleton field width
1103            for ( int i = 0; i < skeletonFieldWidth.length; ++i ) {
1104                skeletonFieldWidth[i] = 0;
1105            }
1106            parseSkeleton(skeleton, skeletonFieldWidth);
1107            // calculate distance
1108            int distance = 0;
1109            int fieldDifference = 1;
1110            for ( int i = 0; i < inputSkeletonFieldWidth.length; ++i ) {
1111                int inputFieldWidth = inputSkeletonFieldWidth[i];
1112                int fieldWidth = skeletonFieldWidth[i];
1113                if ( inputFieldWidth == fieldWidth ) {
1114                    continue;
1115                }
1116                if ( inputFieldWidth == 0 ) {
1117                    fieldDifference = -1;
1118                    distance += DIFFERENT_FIELD;
1119                } else if ( fieldWidth == 0 ) {
1120                    fieldDifference = -1;
1121                    distance += DIFFERENT_FIELD;
1122                } else if (stringNumeric(inputFieldWidth, fieldWidth,
1123                                         (char)(i+BASE) ) ) {
1124                    distance += STRING_NUMERIC_DIFFERENCE;
1125                } else {
1126                    distance += Math.abs(inputFieldWidth - fieldWidth);
1127                }
1128            }
1129            if ( distance < bestDistance ) {
1130                bestSkeleton = skeleton;
1131                bestDistance = distance;
1132                bestFieldDifference = fieldDifference;
1133            }
1134            if ( distance == 0 ) {
1135                bestFieldDifference = 0;
1136                break;
1137            }
1138        }
1139        if ( replaceZWithV && bestFieldDifference != -1 ) {
1140            bestFieldDifference = 2;
1141        }
1142        return new DateIntervalFormat.BestMatchInfo(bestSkeleton, bestFieldDifference);
1143    }
1144
1145    /**
1146     * Override equals
1147     * @stable ICU 4.0
1148     */
1149    @Override
1150    public boolean equals(Object a) {
1151        if ( a instanceof DateIntervalInfo ) {
1152            DateIntervalInfo dtInfo = (DateIntervalInfo)a;
1153            return fIntervalPatterns.equals(dtInfo.fIntervalPatterns);
1154        }
1155        return false;
1156    }
1157
1158    /**
1159     * Override hashcode
1160     * @stable ICU 4.0
1161     */
1162    @Override
1163    public int hashCode() {
1164        return fIntervalPatterns.hashCode();
1165    }
1166
1167    /**
1168     * @internal CLDR
1169     * @deprecated This API is ICU internal only.
1170     */
1171    @Deprecated
1172    public Map<String,Set<String>> getPatterns() {
1173        LinkedHashMap<String,Set<String>> result = new LinkedHashMap<String,Set<String>>();
1174        for (Entry<String, Map<String, PatternInfo>> entry : fIntervalPatterns.entrySet()) {
1175            result.put(entry.getKey(), new LinkedHashSet<String>(entry.getValue().keySet()));
1176        }
1177        return result;
1178    }
1179
1180    /**
1181     * Get the internal patterns, with a deep clone for safety.
1182     * @internal CLDR
1183     * @deprecated This API is ICU internal only.
1184     */
1185    @Deprecated
1186    public Map<String, Map<String, PatternInfo>> getRawPatterns() {
1187        LinkedHashMap<String, Map<String, PatternInfo>> result = new LinkedHashMap<String, Map<String, PatternInfo>>();
1188        for (Entry<String, Map<String, PatternInfo>> entry : fIntervalPatterns.entrySet()) {
1189            result.put(entry.getKey(), new LinkedHashMap<String, PatternInfo>(entry.getValue()));
1190        }
1191        return result;
1192    }
1193}// end class DateIntervalInfo
1194