]> granicus.if.org Git - icu/commitdiff
ICU-20342 Adding FormattedDateInterval in Java.
authorShane Carr <shane@unicode.org>
Sat, 16 Feb 2019 00:47:13 +0000 (16:47 -0800)
committerShane F. Carr <shane@unicode.org>
Sat, 16 Feb 2019 02:04:07 +0000 (18:04 -0800)
- Adds first SpanField to ICU4J.

icu4j/main/classes/core/src/com/ibm/icu/impl/FormattedValueFieldPositionIteratorImpl.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/SimpleFormatterImpl.java
icu4j/main/classes/core/src/com/ibm/icu/text/DateIntervalFormat.java
icu4j/main/classes/core/src/com/ibm/icu/text/SimpleDateFormat.java
icu4j/main/classes/core/src/com/ibm/icu/text/UFormat.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DateIntervalFormatTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/FormattedValueTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/FormatHandler.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java

diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/FormattedValueFieldPositionIteratorImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/FormattedValueFieldPositionIteratorImpl.java
new file mode 100644 (file)
index 0000000..de87c5b
--- /dev/null
@@ -0,0 +1,155 @@
+// © 2019 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl;
+
+import java.text.AttributedCharacterIterator;
+import java.text.AttributedString;
+import java.text.FieldPosition;
+import java.text.Format;
+import java.util.List;
+
+import com.ibm.icu.text.ConstrainedFieldPosition;
+
+/**
+ * Implementation of FormattedValue based on FieldPositionIterator.
+ *
+ * In C++, this implements FormattedValue. In Java, it is a stateless
+ * collection of static functions to avoid having to use nested objects.
+ */
+public class FormattedValueFieldPositionIteratorImpl {
+
+    /** Do not construct instances of this class */
+    private FormattedValueFieldPositionIteratorImpl() {}
+
+    /** Helper class to keep track of fields with values in Java */
+    private static class FieldWithValue extends Format.Field {
+        private static final long serialVersionUID = -3850076447157793465L;
+
+        public final Format.Field field;
+        public final int value;
+
+        public FieldWithValue(Format.Field field, int value) {
+            super(field.toString());
+            this.field = field;
+            this.value = value;
+        }
+    }
+
+    public static boolean nextPosition(List<FieldPosition> attributes, ConstrainedFieldPosition cfpos) {
+        int numFields = attributes.size();
+        int i = (int) cfpos.getInt64IterationContext();
+        for (; i < numFields; i++) {
+            FieldPosition fpos = attributes.get(i);
+            Format.Field field = fpos.getFieldAttribute();
+            Object value = null;
+            if (field instanceof FieldWithValue) {
+                value = ((FieldWithValue) field).value;
+                field = ((FieldWithValue) field).field;
+            }
+            if (cfpos.matchesField(field, value)) {
+                int start = fpos.getBeginIndex();
+                int limit = fpos.getEndIndex();
+                cfpos.setState(field, value, start, limit);
+                break;
+            }
+        }
+        cfpos.setInt64IterationContext(i == numFields ? i : i + 1);
+        return i < numFields;
+    }
+
+    public static AttributedCharacterIterator toCharacterIterator(CharSequence cs, List<FieldPosition> attributes) {
+        AttributedString as = new AttributedString(cs.toString());
+
+        // add attributes to the AttributedString
+        for (int i = 0; i < attributes.size(); i++) {
+            FieldPosition fp = attributes.get(i);
+            Format.Field field = fp.getFieldAttribute();
+            Object value = field;
+            if (field instanceof FieldWithValue) {
+                value = ((FieldWithValue) field).value;
+                field = ((FieldWithValue) field).field;
+            }
+            as.addAttribute(field, value, fp.getBeginIndex(), fp.getEndIndex());
+        }
+
+        // return the CharacterIterator from AttributedString
+        return as.getIterator();
+    }
+
+    public static void addOverlapSpans(List<FieldPosition> attributes, Format.Field spanField, int firstIndex) {
+        // In order to avoid fancy data structures, this is an O(N^2) algorithm,
+        // which should be fine for all real-life applications of this function.
+        int s1a = Integer.MAX_VALUE;
+        int s1b = 0;
+        int s2a = Integer.MAX_VALUE;
+        int s2b = 0;
+        int numFields = attributes.size();
+        for (int i = 0; i<numFields; i++) {
+            FieldPosition fp1 = attributes.get(i);
+            for (int j = i + 1; j<numFields; j++) {
+                FieldPosition fp2 = attributes.get(j);
+                if (fp1.getFieldAttribute() != fp2.getFieldAttribute()) {
+                    continue;
+                }
+                // Found a duplicate
+                s1a = Math.min(s1a, fp1.getBeginIndex());
+                s1b = Math.max(s1b, fp1.getEndIndex());
+                s2a = Math.min(s2a, fp2.getBeginIndex());
+                s2b = Math.max(s2b, fp2.getEndIndex());
+                break;
+            }
+        }
+        if (s1a != Integer.MAX_VALUE) {
+            // Success: add the two span fields
+            FieldPosition newPos = new FieldPosition(new FieldWithValue(spanField, firstIndex));
+            newPos.setBeginIndex(s1a);
+            newPos.setEndIndex(s1b);
+            attributes.add(newPos);
+            newPos = new FieldPosition(new FieldWithValue(spanField, 1 - firstIndex));
+            newPos.setBeginIndex(s2a);
+            newPos.setEndIndex(s2b);
+            attributes.add(newPos);
+        }
+    }
+
+    public static void sort(List<FieldPosition> attributes) {
+        // Use bubble sort, O(N^2) but easy and no fancy data structures.
+        int numFields = attributes.size();
+        while (true) {
+            boolean isSorted = true;
+            for (int i=0; i<numFields-1; i++) {
+                FieldPosition fp1 = attributes.get(i);
+                FieldPosition fp2 = attributes.get(i + 1);
+                long comparison = 0;
+                if (fp1.getBeginIndex() != fp2.getBeginIndex()) {
+                    // Higher start index -> higher rank
+                    comparison = fp2.getBeginIndex() - fp1.getBeginIndex();
+                } else if (fp1.getEndIndex() != fp2.getEndIndex()) {
+                    // Higher length (end index) -> lower rank
+                    comparison = fp1.getEndIndex() - fp2.getEndIndex();
+                } else if (fp1.getFieldAttribute() != fp2.getFieldAttribute()) {
+                    // Span category -> lower rank
+                    // Pick other orders arbitrarily
+                    boolean fp1isSpan = fp1.getFieldAttribute() instanceof FieldWithValue;
+                    boolean fp2isSpan = fp2.getFieldAttribute() instanceof FieldWithValue;
+                    if (fp1isSpan && !fp2isSpan) {
+                        comparison = 1;
+                    } else if (fp2isSpan && !fp1isSpan) {
+                        comparison = -1;
+                    } else {
+                        comparison = fp1.hashCode() - fp2.hashCode();
+                    }
+                }
+                if (comparison < 0) {
+                    // Perform a swap
+                    isSorted = false;
+                    attributes.set(i, fp2);
+                    attributes.set(i + 1, fp1);
+                }
+            }
+            if (isSorted) {
+                break;
+            }
+        }
+    }
+}
index d361b3417f577f3d22d3cd78999741a59b3095a6..216b58a3fa18f83a9aaec74a0db0ce0b624e8331 100644 (file)
@@ -63,6 +63,7 @@ public final class SimpleFormatterImpl {
      * highest argument number plus one, not the number of occurrences of arguments.
      *
      * @param pattern The pattern string.
+     * @param sb A StringBuilder instance which may or may not be used.
      * @param min The pattern must have at least this many arguments.
      * @param max The pattern must have at most this many arguments.
      * @return The compiled-pattern string.
@@ -303,6 +304,30 @@ public final class SimpleFormatterImpl {
         return sb.toString();
     }
 
+    /** Poor-man's iterator interface. See ICU-20406. */
+    public static class Int64Iterator {
+        public static final long DONE = -1;
+
+        public static long step(CharSequence compiledPattern, long state, StringBuffer output) {
+            int i = (int) (state >>> 32);
+            assert i < compiledPattern.length();
+            i++;
+            while (i < compiledPattern.length() && compiledPattern.charAt(i) > ARG_NUM_LIMIT) {
+                int limit = i + compiledPattern.charAt(i) + 1 - ARG_NUM_LIMIT;
+                output.append(compiledPattern, i + 1, limit);
+                i = limit;
+            }
+            if (i == compiledPattern.length()) {
+                return DONE;
+            }
+            return (((long) i) << 32) | compiledPattern.charAt(i);
+        }
+
+        public static int getArgIndex(long state) {
+            return (int) state;
+        }
+    }
+
     private static StringBuilder format(
             String compiledPattern, CharSequence[] values,
             StringBuilder result, String resultCopy, boolean forbidResultAsValue,
index fac49a0065bdbf0b7101598cfb716f27826bc4a7..b92b63329c8f9f2f6eaf07b1f5b60a0abfe4bd2f 100644 (file)
@@ -8,19 +8,25 @@
 package com.ibm.icu.text;
 
 import java.io.IOException;
+import java.io.InvalidObjectException;
 import java.io.ObjectInputStream;
+import java.text.AttributedCharacterIterator;
 import java.text.FieldPosition;
 import java.text.ParsePosition;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 
+import com.ibm.icu.impl.FormattedValueFieldPositionIteratorImpl;
 import com.ibm.icu.impl.ICUCache;
 import com.ibm.icu.impl.ICUData;
 import com.ibm.icu.impl.ICUResourceBundle;
 import com.ibm.icu.impl.SimpleCache;
 import com.ibm.icu.impl.SimpleFormatterImpl;
+import com.ibm.icu.impl.Utility;
 import com.ibm.icu.text.DateIntervalInfo.PatternInfo;
 import com.ibm.icu.util.Calendar;
 import com.ibm.icu.util.DateInterval;
@@ -32,34 +38,34 @@ import com.ibm.icu.util.UResourceBundle;
 
 
 /**
- * DateIntervalFormat is a class for formatting and parsing date 
- * intervals in a language-independent manner. 
+ * DateIntervalFormat is a class for formatting and parsing date
+ * intervals in a language-independent manner.
  * Only formatting is supported. Parsing is not supported.
  *
  * <P>
  * Date interval means from one date to another date,
  * for example, from "Jan 11, 2008" to "Jan 18, 2008".
  * We introduced class DateInterval to represent it.
- * DateInterval is a pair of UDate, which is 
+ * DateInterval is a pair of UDate, which is
  * the standard milliseconds since 24:00 GMT, Jan 1, 1970.
  *
  * <P>
  * DateIntervalFormat formats a DateInterval into
- * text as compactly as possible. 
+ * text as compactly as possible.
  * For example, the date interval format from "Jan 11, 2008" to "Jan 18,. 2008"
  * is "Jan 11-18, 2008" for English.
- * And it parses text into DateInterval, 
- * although initially, parsing is not supported. 
+ * And it parses text into DateInterval,
+ * although initially, parsing is not supported.
  *
  * <P>
- * There is no structural information in date time patterns. 
- * For any punctuations and string literals inside a date time pattern, 
- * we do not know whether it is just a separator, or a prefix, or a suffix. 
- * Without such information, so, it is difficult to generate a sub-pattern 
+ * There is no structural information in date time patterns.
+ * For any punctuations and string literals inside a date time pattern,
+ * we do not know whether it is just a separator, or a prefix, or a suffix.
+ * Without such information, so, it is difficult to generate a sub-pattern
  * (or super-pattern) by algorithm.
  * So, formatting a DateInterval is pattern-driven. It is very
  * similar to formatting in SimpleDateFormat.
- * We introduce class DateIntervalInfo to save date interval 
+ * We introduce class DateIntervalInfo to save date interval
  * patterns, similar to date time pattern in SimpleDateFormat.
  *
  * <P>
@@ -68,22 +74,22 @@ import com.ibm.icu.util.UResourceBundle;
  * to (date_interval_pattern).
  *
  * <P>
- * A skeleton 
+ * A skeleton
  * <ol>
  * <li>
- * only keeps the field pattern letter and ignores all other parts 
+ * only keeps the field pattern letter and ignores all other parts
  * in a pattern, such as space, punctuations, and string literals.
  * <li>
- * hides the order of fields. 
+ * hides the order of fields.
  * <li>
  * might hide a field's pattern letter length.
  *
- * For those non-digit calendar fields, the pattern letter length is 
- * important, such as MMM, MMMM, and MMMMM; EEE and EEEE, 
+ * For those non-digit calendar fields, the pattern letter length is
+ * important, such as MMM, MMMM, and MMMMM; EEE and EEEE,
  * and the field's pattern letter length is honored.
- *    
- * For the digit calendar fields,  such as M or MM, d or dd, yy or yyyy, 
- * the field pattern length is ignored and the best match, which is defined 
+ *
+ * For the digit calendar fields,  such as M or MM, d or dd, yy or yyyy,
+ * the field pattern length is ignored and the best match, which is defined
  * in date time patterns, will be returned without honor the field pattern
  * letter length in skeleton.
  * </ol>
@@ -92,28 +98,28 @@ import com.ibm.icu.util.UResourceBundle;
  * The calendar fields we support for interval formatting are:
  * year, month, date, day-of-week, am-pm, hour, hour-of-day, minute, and
  * second (though we do not currently have specific intervalFormat data for
- * skeletons with seconds). 
+ * skeletons with seconds).
  * Those calendar fields can be defined in the following order:
  * year &gt; month &gt; date &gt; hour (in day) &gt; minute &gt; second
- *  
+ *
  * The largest different calendar fields between 2 calendars is the
  * first different calendar field in above order.
  *
- * For example: the largest different calendar fields between "Jan 10, 2007" 
+ * For example: the largest different calendar fields between "Jan 10, 2007"
  * and "Feb 20, 2008" is year.
  *
  * <P>
  * For other calendar fields, the compact interval formatting is not
  * supported. And the interval format will be fall back to fall-back
  * patterns, which is mostly "{date0} - {date1}".
- *   
+ *
  * <P>
  * There is a set of pre-defined static skeleton strings in DateFormat,
  * There are pre-defined interval patterns for those pre-defined skeletons
  * in locales' resource files.
  * For example, for a skeleton YEAR_ABBR_MONTH_DAY, which is  "yMMMd",
- * in  en_US, if the largest different calendar field between date1 and date2 
- * is "year", the date interval pattern  is "MMM d, yyyy - MMM d, yyyy", 
+ * in  en_US, if the largest different calendar field between date1 and date2
+ * is "year", the date interval pattern  is "MMM d, yyyy - MMM d, yyyy",
  * such as "Jan 10, 2007 - Jan 10, 2008".
  * If the largest different calendar field between date1 and date2 is "month",
  * the date interval pattern is "MMM d - MMM d, yyyy",
@@ -121,7 +127,7 @@ import com.ibm.icu.util.UResourceBundle;
  * If the largest different calendar field between date1 and date2 is "day",
  * the date interval pattern is ""MMM d-d, yyyy", such as "Jan 10-20, 2007".
  *
- * For date skeleton, the interval patterns when year, or month, or date is 
+ * For date skeleton, the interval patterns when year, or month, or date is
  * different are defined in resource files.
  * For time skeleton, the interval patterns when am/pm, or hour, or minute is
  * different are defined in resource files.
@@ -129,49 +135,49 @@ import com.ibm.icu.util.UResourceBundle;
  * <P>
  * If a skeleton is not found in a locale's DateIntervalInfo, which means
  * the interval patterns for the skeleton is not defined in resource file,
- * the interval pattern will falls back to the interval "fallback" pattern 
+ * the interval pattern will falls back to the interval "fallback" pattern
  * defined in resource file.
  * If the interval "fallback" pattern is not defined, the default fall-back
  * is "{date0} - {data1}".
  *
  * <P>
- * For the combination of date and time, 
+ * For the combination of date and time,
  * The rule to genearte interval patterns are:
  * <ol>
  * <li>
  *    when the year, month, or day differs, falls back to fall-back
- *    interval pattern, which mostly is the concatenate the two original 
- *    expressions with a separator between, 
- *    For example, interval pattern from "Jan 10, 2007 10:10 am" 
- *    to "Jan 11, 2007 10:10am" is 
- *    "Jan 10, 2007 10:10 am - Jan 11, 2007 10:10am" 
+ *    interval pattern, which mostly is the concatenate the two original
+ *    expressions with a separator between,
+ *    For example, interval pattern from "Jan 10, 2007 10:10 am"
+ *    to "Jan 11, 2007 10:10am" is
+ *    "Jan 10, 2007 10:10 am - Jan 11, 2007 10:10am"
  * <li>
- *    otherwise, present the date followed by the range expression 
+ *    otherwise, present the date followed by the range expression
  *    for the time.
- *    For example, interval pattern from "Jan 10, 2007 10:10 am" 
- *    to "Jan 10, 2007 11:10am" is "Jan 10, 2007 10:10 am - 11:10am" 
+ *    For example, interval pattern from "Jan 10, 2007 10:10 am"
+ *    to "Jan 10, 2007 11:10am" is "Jan 10, 2007 10:10 am - 11:10am"
  * </ol>
  *
  *
  * <P>
  * If two dates are the same, the interval pattern is the single date pattern.
- * For example, interval pattern from "Jan 10, 2007" to "Jan 10, 2007" is 
+ * For example, interval pattern from "Jan 10, 2007" to "Jan 10, 2007" is
  * "Jan 10, 2007".
  *
  * Or if the presenting fields between 2 dates have the exact same values,
- * the interval pattern is the  single date pattern. 
+ * the interval pattern is the  single date pattern.
  * For example, if user only requests year and month,
  * the interval pattern from "Jan 10, 2007" to "Jan 20, 2007" is "Jan 2007".
  *
  * <P>
- * DateIntervalFormat needs the following information for correct 
- * formatting: time zone, calendar type, pattern, date format symbols, 
+ * DateIntervalFormat needs the following information for correct
+ * formatting: time zone, calendar type, pattern, date format symbols,
  * and date interval patterns.
  * It can be instantiated in several ways:
  * <ol>
  * <li>
  *    create an instance using default or given locale plus given skeleton.
- *    Users are encouraged to created date interval formatter this way and 
+ *    Users are encouraged to created date interval formatter this way and
  *    to use the pre-defined skeleton macros, such as
  *    YEAR_NUM_MONTH, which consists the calendar fields and
  *    the format style.
@@ -179,8 +185,8 @@ import com.ibm.icu.util.UResourceBundle;
  * <li>
  *    create an instance using default or given locale plus given skeleton
  *    plus a given DateIntervalInfo.
- *    This factory method is for powerful users who want to provide their own 
- *    interval patterns. 
+ *    This factory method is for powerful users who want to provide their own
+ *    interval patterns.
  *    Locale provides the timezone, calendar, and format symbols information.
  *    Local plus skeleton provides full pattern information.
  *    DateIntervalInfo provides the date interval patterns.
@@ -191,7 +197,7 @@ import com.ibm.icu.util.UResourceBundle;
  * For the calendar field pattern letter, such as G, y, M, d, a, h, H, m, s etc.
  * DateIntervalFormat uses the same syntax as that of
  * DateTime format.
- * 
+ *
  * <P>
  * Code Sample: general usage
  * <pre>
@@ -215,54 +221,184 @@ import com.ibm.icu.util.UResourceBundle;
  *     import com.ibm.icu.text.DateIntervalInfo;
  *     import com.ibm.icu.text.DateIntervalFormat;
  *     ....................
- *     
+ *
  *     // Get DateIntervalFormat instance using default locale
  *     DateIntervalFormat dtitvfmt = DateIntervalFormat.getInstance(YEAR_MONTH_DAY);
- *     
+ *
  *     // Create an empty DateIntervalInfo object, which does not have any interval patterns inside.
  *     dtitvinf = new DateIntervalInfo();
- *     
+ *
  *     // a series of set interval patterns.
  *     // Only ERA, YEAR, MONTH, DATE,  DAY_OF_MONTH, DAY_OF_WEEK, AM_PM,  HOUR, HOUR_OF_DAY,
  *     MINUTE and SECOND are supported.
- *     dtitvinf.setIntervalPattern("yMMMd", Calendar.YEAR, "'y ~ y'"); 
+ *     dtitvinf.setIntervalPattern("yMMMd", Calendar.YEAR, "'y ~ y'");
  *     dtitvinf.setIntervalPattern("yMMMd", Calendar.MONTH, "yyyy 'diff' MMM d - MMM d");
  *     dtitvinf.setIntervalPattern("yMMMd", Calendar.DATE, "yyyy MMM d ~ d");
  *     dtitvinf.setIntervalPattern("yMMMd", Calendar.HOUR_OF_DAY, "yyyy MMM d HH:mm ~ HH:mm");
- *     
+ *
  *     // Set fallback interval pattern. Fallback pattern is used when interval pattern is not found.
  *     // If the fall-back pattern is not set,  falls back to {date0} - {date1} if interval pattern is not found.
  *     dtitvinf.setFallbackIntervalPattern("{0} - {1}");
- *     
+ *
  *     // Set above DateIntervalInfo object as the interval patterns of date interval formatter
  *     dtitvfmt.setDateIntervalInfo(dtitvinf);
- *     
+ *
  *     // Prepare to format
  *     pos = new FieldPosition(0);
  *     str = new StringBuffer("");
- *     
+ *
  *     // The 2 calendars should be equivalent, otherwise,  IllegalArgumentException will be thrown by format()
  *     Calendar fromCalendar = (Calendar) dtfmt.getCalendar().clone();
  *     Calendar toCalendar = (Calendar) dtfmt.getCalendar().clone();
  *     fromCalendar.setTimeInMillis(....);
  *     toCalendar.setTimeInMillis(...);
- *     
+ *
  *     //Formatting given 2 calendars
  *     dtitvfmt.format(fromCalendar, toCalendar, str, pos);
- * 
+ *
  *
  * </pre>
  * <h3>Synchronization</h3>
- * 
+ *
  * The format methods of DateIntervalFormat may be used concurrently from multiple threads.
- * Functions that alter the state of a DateIntervalFormat object (setters) 
+ * Functions that alter the state of a DateIntervalFormat object (setters)
  * may not be used concurrently with any other functions.
- * 
+ *
  * @stable ICU 4.0
  */
 
 public class DateIntervalFormat extends UFormat {
 
+    /**
+     * An immutable class containing the result of a date interval formatting operation.
+     *
+     * Instances of this class are immutable and thread-safe.
+     *
+     * Not intended for public subclassing.
+     *
+     * @draft ICU 64
+     * @provisional This API might change or be removed in a future release.
+     */
+    public static final class FormattedDateInterval implements FormattedValue {
+        private final String string;
+        private final List<FieldPosition> attributes;
+
+        FormattedDateInterval(CharSequence cs, List<FieldPosition> attributes) {
+            this.string = cs.toString();
+            this.attributes = Collections.unmodifiableList(attributes);
+        }
+
+        /**
+         * {@inheritDoc}
+         * @draft ICU 64
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public String toString() {
+            return string;
+        }
+
+        /**
+         * {@inheritDoc}
+         * @draft ICU 64
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public int length() {
+            return string.length();
+        }
+
+        /**
+         * {@inheritDoc}
+         * @draft ICU 64
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public char charAt(int index) {
+            return string.charAt(index);
+        }
+
+        /**
+         * {@inheritDoc}
+         * @draft ICU 64
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public CharSequence subSequence(int start, int end) {
+            return string.subSequence(start, end);
+        }
+
+        /**
+         * {@inheritDoc}
+         * @draft ICU 64
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public <A extends Appendable> A appendTo(A appendable) {
+            return Utility.appendTo(string, appendable);
+        }
+
+        /**
+         * {@inheritDoc}
+         * @draft ICU 64
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public boolean nextPosition(ConstrainedFieldPosition cfpos) {
+            return FormattedValueFieldPositionIteratorImpl.nextPosition(attributes, cfpos);
+        }
+
+        /**
+         * {@inheritDoc}
+         * @draft ICU 64
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public AttributedCharacterIterator toCharacterIterator() {
+            return FormattedValueFieldPositionIteratorImpl.toCharacterIterator(string, attributes);
+        }
+    }
+
+    /**
+     * Class for span fields in FormattedDateInterval.
+     *
+     * @draft ICU 64
+     * @provisional This API might change or be removed in a future release.
+     */
+    public static final class SpanField extends UFormat.SpanField {
+        private static final long serialVersionUID = -6330879259553618133L;
+
+        /**
+         * The concrete field used for spans in FormattedDateInterval.
+         *
+         * Instances of DATE_INTERVAL_SPAN should have an associated value. If
+         * 0, the date fields within the span are for the "from" date; if 1,
+         * the date fields within the span are for the "to" date.
+         *
+         * @draft ICU 64
+         * @provisional This API might change or be removed in a future release.
+         */
+        public static final SpanField DATE_INTERVAL_SPAN = new SpanField("date-interval-span");
+
+        private SpanField(String name) {
+            super(name);
+        }
+
+        /**
+         * serizalization method resolve instances to the constant
+         * DateIntervalFormat.SpanField values
+         * @draft ICU 64
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        protected Object readResolve() throws InvalidObjectException {
+            if (this.getName().equals(DATE_INTERVAL_SPAN.getName()))
+                return DATE_INTERVAL_SPAN;
+
+            throw new InvalidObjectException("An invalid object.");
+        }
+    }
+
     private static final long serialVersionUID = 1;
 
     /**
@@ -296,11 +432,22 @@ public class DateIntervalFormat extends UFormat {
         }
     }
 
+    /** Used to output information during formatting. */
+    private static final class FormatOutput {
+        int firstIndex = -1;
+
+        public void register(int i) {
+            if (firstIndex == -1) {
+                firstIndex = i;
+            }
+        }
+    }
+
 
     // Cache for the locale interval pattern
     private static ICUCache<String, Map<String, PatternInfo>> LOCAL_PATTERN_CACHE =
-        new SimpleCache<String, Map<String, PatternInfo>>();
-    
+        new SimpleCache<>();
+
     /*
      * The interval patterns for this locale.
      */
@@ -328,7 +475,7 @@ public class DateIntervalFormat extends UFormat {
      * relevant (locale) to this formatter.
      */
     private String fSkeleton = null;
-    
+
     /*
      * Needed for efficient deserialization. If set, it means we can use the
      * cache to initialize fIntervalPatterns.
@@ -341,15 +488,15 @@ public class DateIntervalFormat extends UFormat {
     private transient Map<String, PatternInfo> fIntervalPatterns = null;
 
     /*
-     * Patterns for fallback formatting. 
+     * Patterns for fallback formatting.
      */
     private String fDatePattern = null;
     private String fTimePattern = null;
     private String fDateTimeFormat = null;
-    
-   
+
+
     /*
-     * default constructor; private because we don't want anyone to use 
+     * default constructor; private because we don't want anyone to use
      */
     @SuppressWarnings("unused")
     private DateIntervalFormat() {
@@ -360,14 +507,14 @@ public class DateIntervalFormat extends UFormat {
      * a DateIntervalInfo, and skeleton.
      * DateFormat provides the timezone, calendar,
      * full pattern, and date format symbols information.
-     * It should be a SimpleDateFormat object which 
+     * It should be a SimpleDateFormat object which
      * has a pattern in it.
      * the DateIntervalInfo provides the interval patterns.
      *
      * @param skeleton  the skeleton of the date formatter
      * @param dtItvInfo  the DateIntervalInfo object to be adopted.
      * @param simpleDateFormat will be used for formatting
-     * 
+     *
      * @internal
      * @deprecated This API is ICU internal only.
      */
@@ -397,13 +544,13 @@ public class DateIntervalFormat extends UFormat {
         fToCalendar = (Calendar) fDateFormat.getCalendar().clone();
         initializePattern(LOCAL_PATTERN_CACHE);
 }
-    
+
 
     /**
      * Construct a DateIntervalFormat from skeleton and  the default <code>FORMAT</code> locale.
      *
-     * This is a convenient override of 
-     * getInstance(String skeleton, ULocale locale)  
+     * This is a convenient override of
+     * getInstance(String skeleton, ULocale locale)
      * with the value of locale as default <code>FORMAT</code> locale.
      *
      * @param skeleton  the skeleton on which interval format based.
@@ -411,9 +558,9 @@ public class DateIntervalFormat extends UFormat {
      * @see Category#FORMAT
      * @stable ICU 4.0
      */
-    public static final DateIntervalFormat 
+    public static final DateIntervalFormat
         getInstance(String skeleton)
-                                                 
+
     {
         return getInstance(skeleton, ULocale.getDefault(Category.FORMAT));
     }
@@ -422,8 +569,8 @@ public class DateIntervalFormat extends UFormat {
     /**
      * Construct a DateIntervalFormat from skeleton and a given locale.
      *
-     * This is a convenient override of 
-     * getInstance(String skeleton, ULocale locale)  
+     * This is a convenient override of
+     * getInstance(String skeleton, ULocale locale)
      *
      * <p>Example code:{@.jcite com.ibm.icu.samples.text.dateintervalformat.DateIntervalFormatSample:---dtitvfmtPreDefinedExample}
      * @param skeleton  the skeleton on which interval format based.
@@ -431,8 +578,8 @@ public class DateIntervalFormat extends UFormat {
      * @return          a date time interval formatter.
      * @stable ICU 4.0
      */
-    public static final DateIntervalFormat 
-        getInstance(String skeleton, Locale locale)  
+    public static final DateIntervalFormat
+        getInstance(String skeleton, Locale locale)
     {
         return getInstance(skeleton, ULocale.forLocale(locale));
     }
@@ -451,10 +598,10 @@ public class DateIntervalFormat extends UFormat {
      * such as MONTH_DAY, YEAR_MONTH_WEEKDAY_DAY etc.
      *
      * Those skeletons have pre-defined interval patterns in resource files.
-     * Users are encouraged to use them. 
+     * Users are encouraged to use them.
      * For example:
      * DateIntervalFormat.getInstance(DateFormat.MONTH_DAY, false, loc);
-     * 
+     *
      * The given Locale provides the interval patterns.
      * For example, for en_GB, if skeleton is YEAR_ABBR_MONTH_WEEKDAY_DAY,
      * which is "yMMMEEEd",
@@ -467,8 +614,8 @@ public class DateIntervalFormat extends UFormat {
      * @return          a date time interval formatter.
      * @stable ICU 4.0
      */
-    public static final DateIntervalFormat 
-        getInstance(String skeleton, ULocale locale)  
+    public static final DateIntervalFormat
+        getInstance(String skeleton, ULocale locale)
     {
         DateTimePatternGenerator generator = DateTimePatternGenerator.getInstance(locale);
         return new DateIntervalFormat(skeleton, locale, new SimpleDateFormat(generator.getBestPattern(skeleton), locale));
@@ -490,7 +637,7 @@ public class DateIntervalFormat extends UFormat {
      * @see Category#FORMAT
      * @stable ICU 4.0
      */
-    public static final DateIntervalFormat getInstance(String skeleton, 
+    public static final DateIntervalFormat getInstance(String skeleton,
                                                    DateIntervalInfo dtitvinf)
     {
         return getInstance(skeleton, ULocale.getDefault(Category.FORMAT), dtitvinf);
@@ -504,7 +651,7 @@ public class DateIntervalFormat extends UFormat {
      *
      * This is a convenient override of
      * getInstance(String skeleton, ULocale locale, DateIntervalInfo dtitvinf)
-     * 
+     *
      * <p>Example code:{@.jcite com.ibm.icu.samples.text.dateintervalformat.DateIntervalFormatSample:---dtitvfmtCustomizedExample}
      * @param skeleton  the skeleton on which interval format based.
      * @param locale    the given locale
@@ -513,7 +660,7 @@ public class DateIntervalFormat extends UFormat {
      * @stable ICU 4.0
      */
     public static final DateIntervalFormat getInstance(String skeleton,
-                                                 Locale locale, 
+                                                 Locale locale,
                                                  DateIntervalInfo dtitvinf)
     {
         return getInstance(skeleton, ULocale.forLocale(locale), dtitvinf);
@@ -536,7 +683,7 @@ public class DateIntervalFormat extends UFormat {
      * such as MONTH_DAY, YEAR_MONTH_WEEKDAY_DAY etc.
      *
      * Those skeletons have pre-defined interval patterns in resource files.
-     * Users are encouraged to use them. 
+     * Users are encouraged to use them.
      * For example:
      * DateIntervalFormat.getInstance(DateFormat.MONTH_DAY, false, loc,itvinf);
      *
@@ -549,7 +696,7 @@ public class DateIntervalFormat extends UFormat {
      * field is not found ( if user not set it ), interval format fallback to
      * the default interval pattern.
      * If user does not provide default interval pattern, it fallback to
-     * "{date0} - {date1}" 
+     * "{date0} - {date1}"
      *
      * @param skeleton  the skeleton on which interval format based.
      * @param locale    the given locale
@@ -558,22 +705,23 @@ public class DateIntervalFormat extends UFormat {
      * @stable ICU 4.0
      */
     public static final DateIntervalFormat getInstance(String skeleton,
-                                                 ULocale locale, 
+                                                 ULocale locale,
                                                  DateIntervalInfo dtitvinf)
     {
         // clone. If it is frozen, clone returns itself, otherwise, clone
         // returns a copy.
-        dtitvinf = (DateIntervalInfo)dtitvinf.clone(); 
+        dtitvinf = (DateIntervalInfo)dtitvinf.clone();
         DateTimePatternGenerator generator = DateTimePatternGenerator.getInstance(locale);
         return new DateIntervalFormat(skeleton, dtitvinf, new SimpleDateFormat(generator.getBestPattern(skeleton), locale));
     }
 
 
     /**
-     * Clone this Format object polymorphically. 
+     * Clone this Format object polymorphically.
      * @return    A copy of the object.
      * @stable ICU 4.0
      */
+    @Override
     public synchronized Object clone()
     {
         DateIntervalFormat other = (DateIntervalFormat) super.clone();
@@ -590,11 +738,11 @@ public class DateIntervalFormat extends UFormat {
 
     /**
      * Format an object to produce a string. This method handles Formattable
-     * objects with a DateInterval type. 
+     * objects with a DateInterval type.
      * If a the Formattable object type is not a DateInterval,
      * IllegalArgumentException is thrown.
      *
-     * @param obj               The object to format. 
+     * @param obj               The object to format.
      *                          Must be a DateInterval.
      * @param appendTo          Output parameter to receive result.
      *                          Result is appended to existing contents.
@@ -604,11 +752,12 @@ public class DateIntervalFormat extends UFormat {
      *                          in an interval format; in this case the fieldPosition
      *                          offsets refer to the first instance.
      * @return                  Reference to 'appendTo' parameter.
-     * @throws    IllegalArgumentException  if the formatted object is not 
+     * @throws    IllegalArgumentException  if the formatted object is not
      *                                      DateInterval object
      * @stable ICU 4.0
      */
-    public final StringBuffer 
+    @Override
+    public final StringBuffer
         format(Object obj, StringBuffer appendTo, FieldPosition fieldPosition)
     {
         if ( obj instanceof DateInterval ) {
@@ -620,7 +769,7 @@ public class DateIntervalFormat extends UFormat {
     }
 
     /**
-     * Format a DateInterval to produce a string. 
+     * Format a DateInterval to produce a string.
      *
      * @param dtInterval        DateInterval to be formatted.
      * @param appendTo          Output parameter to receive result.
@@ -633,13 +782,45 @@ public class DateIntervalFormat extends UFormat {
      * @return                  Reference to 'appendTo' parameter.
      * @stable ICU 4.0
      */
-    public final synchronized StringBuffer format(DateInterval dtInterval,
+    public final StringBuffer format(DateInterval dtInterval,
                                      StringBuffer appendTo,
-                                     FieldPosition fieldPosition)
-    {
+                                     FieldPosition fieldPosition) {
+        return formatIntervalImpl(dtInterval, appendTo, fieldPosition, null, null);
+    }
+
+    /**
+     * Format a DateInterval to produce a FormattedDateInterval.
+     *
+     * The FormattedDateInterval exposes field information about the formatted string.
+     *
+     * @param dtInterval        DateInterval to be formatted.
+     * @return                  A FormattedDateInterval containing the format result.
+     * @draft ICU 64
+     * @provisional This API might change or be removed in a future release.
+     */
+    public final FormattedDateInterval formatToValue(DateInterval dtInterval) {
+        StringBuffer sb = new StringBuffer();
+        FieldPosition ignore = new FieldPosition(0);
+        FormatOutput output = new FormatOutput();
+        List<FieldPosition> attributes = new ArrayList<>();
+        formatIntervalImpl(dtInterval, sb, ignore, output, attributes);
+        if (output.firstIndex != -1) {
+            FormattedValueFieldPositionIteratorImpl.addOverlapSpans(
+                    attributes, SpanField.DATE_INTERVAL_SPAN, output.firstIndex);
+            FormattedValueFieldPositionIteratorImpl.sort(attributes);
+        }
+        return new FormattedDateInterval(sb, attributes);
+    }
+
+    private synchronized StringBuffer formatIntervalImpl(
+            DateInterval dtInterval,
+            StringBuffer appendTo,
+            FieldPosition pos,
+            FormatOutput output,
+            List<FieldPosition> attributes) {
         fFromCalendar.setTimeInMillis(dtInterval.getFromDate());
         fToCalendar.setTimeInMillis(dtInterval.getToDate());
-        return format(fFromCalendar, fToCalendar, appendTo, fieldPosition);
+        return formatImpl(fFromCalendar, fToCalendar, appendTo, pos, output, attributes);
     }
 
     /**
@@ -648,13 +829,13 @@ public class DateIntervalFormat extends UFormat {
      */
     @Deprecated
     public String getPatterns(Calendar fromCalendar,
-            Calendar toCalendar, 
+            Calendar toCalendar,
             Output<String> part2) {
         // First, find the largest different calendar field.
         int field;
         if ( fromCalendar.get(Calendar.ERA) != toCalendar.get(Calendar.ERA) ) {
             field = Calendar.ERA;
-        } else if ( fromCalendar.get(Calendar.YEAR) != 
+        } else if ( fromCalendar.get(Calendar.YEAR) !=
                     toCalendar.get(Calendar.YEAR) ) {
             field = Calendar.YEAR;
         } else if ( fromCalendar.get(Calendar.MONTH) !=
@@ -683,8 +864,9 @@ public class DateIntervalFormat extends UFormat {
         part2.value = intervalPattern.getSecondPart();
         return intervalPattern.getFirstPart();
     }
+
     /**
-     * Format 2 Calendars to produce a string. 
+     * Format 2 Calendars to produce a string.
      *
      * @param fromCalendar      calendar set to the from date in date interval
      *                          to be formatted into date interval string
@@ -701,22 +883,58 @@ public class DateIntervalFormat extends UFormat {
      * @throws    IllegalArgumentException  if the two calendars are not equivalent.
      * @stable ICU 4.0
      */
-    public final synchronized StringBuffer format(Calendar fromCalendar,
+    public StringBuffer format(Calendar fromCalendar,
+            Calendar toCalendar,
+            StringBuffer appendTo,
+            FieldPosition pos) {
+        return formatImpl(fromCalendar, toCalendar, appendTo, pos, null, null);
+    }
+
+    /**
+     * Format 2 Calendars to produce a FormattedDateInterval.
+     *
+     * The FormattedDateInterval exposes field information about the formatted string.
+     *
+     * @param fromCalendar      calendar set to the from date in date interval
+     *                          to be formatted into date interval string
+     * @param toCalendar        calendar set to the to date in date interval
+     *                          to be formatted into date interval string
+     * @return                  A FormattedDateInterval containing the format result.
+     * @draft ICU 64
+     * @provisional This API might change or be removed in a future release.
+     */
+    public FormattedDateInterval formatToValue(Calendar fromCalendar, Calendar toCalendar) {
+        StringBuffer sb = new StringBuffer();
+        FieldPosition ignore = new FieldPosition(0);
+        FormatOutput output = new FormatOutput();
+        List<FieldPosition> attributes = new ArrayList<>();
+        formatImpl(fromCalendar, toCalendar, sb, ignore, output, attributes);
+        if (output.firstIndex != -1) {
+            FormattedValueFieldPositionIteratorImpl.addOverlapSpans(
+                    attributes, SpanField.DATE_INTERVAL_SPAN, output.firstIndex);
+            FormattedValueFieldPositionIteratorImpl.sort(attributes);
+        }
+        return new FormattedDateInterval(sb, attributes);
+    }
+
+    private synchronized StringBuffer formatImpl(Calendar fromCalendar,
                                      Calendar toCalendar,
                                      StringBuffer appendTo,
-                                     FieldPosition pos)
+                                     FieldPosition pos,
+                                     FormatOutput output,
+                                     List<FieldPosition> attributes)
     {
         // not support different calendar types and time zones
         if ( !fromCalendar.isEquivalentTo(toCalendar) ) {
             throw new IllegalArgumentException("can not format on two different calendars");
         }
-    
+
         // First, find the largest different calendar field.
         int field = -1; //init with an invalid value.
-    
+
         if ( fromCalendar.get(Calendar.ERA) != toCalendar.get(Calendar.ERA) ) {
             field = Calendar.ERA;
-        } else if ( fromCalendar.get(Calendar.YEAR) != 
+        } else if ( fromCalendar.get(Calendar.YEAR) !=
                     toCalendar.get(Calendar.YEAR) ) {
             field = Calendar.YEAR;
         } else if ( fromCalendar.get(Calendar.MONTH) !=
@@ -741,10 +959,10 @@ public class DateIntervalFormat extends UFormat {
             /* ignore the millisecond etc. small fields' difference.
              * use single date when all the above are the same.
              */
-            return fDateFormat.format(fromCalendar, appendTo, pos);
+            return fDateFormat.format(fromCalendar, appendTo, pos, attributes);
         }
         boolean fromToOnSameDay = (field==Calendar.AM_PM || field==Calendar.HOUR || field==Calendar.MINUTE || field==Calendar.SECOND);
-        
+
         // get interval pattern
         PatternInfo intervalPattern = fIntervalPatterns.get(
               DateIntervalInfo.CALENDAR_FIELD_TO_PATTERN_LETTER[field]);
@@ -755,76 +973,83 @@ public class DateIntervalFormat extends UFormat {
                  * the smallest calendar field in pattern,
                  * return single date format.
                  */
-                return fDateFormat.format(fromCalendar, appendTo, pos);
+                return fDateFormat.format(fromCalendar, appendTo, pos, attributes);
             }
 
-            return fallbackFormat(fromCalendar, toCalendar, fromToOnSameDay, appendTo, pos);
+            return fallbackFormat(fromCalendar, toCalendar, fromToOnSameDay, appendTo, pos,
+                    output, attributes);
         }
 
-        // If the first part in interval pattern is empty, 
+        // If the first part in interval pattern is empty,
         // the 2nd part of it saves the full-pattern used in fall-back.
         // For a 'real' interval pattern, the first part will never be empty.
         if ( intervalPattern.getFirstPart() == null ) {
             // fall back
             return fallbackFormat(fromCalendar, toCalendar, fromToOnSameDay, appendTo, pos,
-                                    intervalPattern.getSecondPart());
+                    output, attributes, intervalPattern.getSecondPart());
         }
         Calendar firstCal;
         Calendar secondCal;
         if ( intervalPattern.firstDateInPtnIsLaterDate() ) {
+            if (output != null) {
+                output.register(1);
+            }
             firstCal = toCalendar;
             secondCal = fromCalendar;
         } else {
+            if (output != null) {
+                output.register(0);
+            }
             firstCal = fromCalendar;
             secondCal = toCalendar;
         }
         // break the interval pattern into 2 parts
-        // first part should not be empty, 
+        // first part should not be empty,
         String originalPattern = fDateFormat.toPattern();
         fDateFormat.applyPattern(intervalPattern.getFirstPart());
-        fDateFormat.format(firstCal, appendTo, pos);
+        fDateFormat.format(firstCal, appendTo, pos, attributes);
+        // Only accept the first instance of the field
+        if (pos.getEndIndex() > 0) {
+            pos = new FieldPosition(0);
+        }
         if ( intervalPattern.getSecondPart() != null ) {
             fDateFormat.applyPattern(intervalPattern.getSecondPart());
-            FieldPosition otherPos = new FieldPosition(pos.getField());
-            fDateFormat.format(secondCal, appendTo, otherPos);
-            if (pos.getEndIndex() == 0 && otherPos.getEndIndex() > 0) {
-                pos.setBeginIndex(otherPos.getBeginIndex());
-                pos.setEndIndex(otherPos.getEndIndex());
-            }
+            fDateFormat.format(secondCal, appendTo, pos, attributes);
         }
         fDateFormat.applyPattern(originalPattern);
         return appendTo;
     }
 
-    private void adjustPosition(String combiningPattern, // has {0} and {1} in it
-                                String pat0, FieldPosition pos0, // pattern and pos corresponding to {0}
-                                String pat1, FieldPosition pos1, // pattern and pos corresponding to {1}
-                                FieldPosition posResult) {
-        int index0 = combiningPattern.indexOf("{0}");
-        int index1 = combiningPattern.indexOf("{1}");
-        if (index0 < 0 || index1 < 0) {
-            return;
-        }
-        int placeholderLen = 3; // length of "{0}" or "{1}"
-        if (index0 < index1) {
-            if (pos0.getEndIndex() > 0) {
-                posResult.setBeginIndex(pos0.getBeginIndex() + index0);
-                posResult.setEndIndex(pos0.getEndIndex() + index0);
-            } else if (pos1.getEndIndex() > 0) {
-                // here index1 >= 3
-                index1 += pat0.length() - placeholderLen; // adjust for pat0 replacing {0}
-                posResult.setBeginIndex(pos1.getBeginIndex() + index1);
-                posResult.setEndIndex(pos1.getEndIndex() + index1);
+    /** Like fallbackFormat, but specifically for ranges. */
+    private final void fallbackFormatRange(Calendar fromCalendar,
+            Calendar toCalendar,
+            StringBuffer appendTo,
+            StringBuilder patternSB,
+            FieldPosition pos,
+            FormatOutput output,
+            List<FieldPosition> attributes) {
+        String compiledPattern = SimpleFormatterImpl.compileToStringMinMaxArguments(
+                fInfo.getFallbackIntervalPattern(), patternSB, 2, 2);
+        long state = 0;
+        while (true) {
+            state = SimpleFormatterImpl.Int64Iterator.step(compiledPattern, state, appendTo);
+            if (state == SimpleFormatterImpl.Int64Iterator.DONE) {
+                break;
             }
-        } else {
-            if (pos1.getEndIndex() > 0) {
-                posResult.setBeginIndex(pos1.getBeginIndex() + index1);
-                posResult.setEndIndex(pos1.getEndIndex() + index1);
-            } else if (pos0.getEndIndex() > 0) {
-                // here index0 >= 3
-                index0 += pat1.length() - placeholderLen; // adjust for pat1 replacing {1}
-                posResult.setBeginIndex(pos0.getBeginIndex() + index0);
-                posResult.setEndIndex(pos0.getEndIndex() + index0);
+            if (SimpleFormatterImpl.Int64Iterator.getArgIndex(state) == 0) {
+                if (output != null) {
+                    output.register(0);
+                }
+                fDateFormat.format(fromCalendar, appendTo, pos, attributes);
+            } else {
+                if (output != null) {
+                    output.register(1);
+                }
+                fDateFormat.format(toCalendar, appendTo, pos, attributes);
+            }
+            // Only accept the first instance of the field
+            if (pos.getEndIndex() > 0) {
+                pos = new FieldPosition(0);
             }
         }
     }
@@ -849,40 +1074,45 @@ public class DateIntervalFormat extends UFormat {
                                               Calendar toCalendar,
                                               boolean fromToOnSameDay,
                                               StringBuffer appendTo,
-                                              FieldPosition pos)  {
-            String fullPattern = null; // for saving the pattern in fDateFormat
-            boolean formatDatePlusTimeRange = (fromToOnSameDay && fDatePattern != null && fTimePattern != null);
-            // the fall back
-            if (formatDatePlusTimeRange) {
-                fullPattern = fDateFormat.toPattern(); // save current pattern, restore later
-                fDateFormat.applyPattern(fTimePattern);
-            }
-            FieldPosition otherPos = new FieldPosition(pos.getField());
-            StringBuffer earlierDate = new StringBuffer(64);
-            earlierDate = fDateFormat.format(fromCalendar, earlierDate, pos);
-            StringBuffer laterDate = new StringBuffer(64);
-            laterDate = fDateFormat.format(toCalendar, laterDate, otherPos);
-            String fallbackPattern = fInfo.getFallbackIntervalPattern();
-            adjustPosition(fallbackPattern, earlierDate.toString(), pos, laterDate.toString(), otherPos, pos);
-            String fallbackRange = SimpleFormatterImpl.formatRawPattern(
-                    fallbackPattern, 2, 2, earlierDate, laterDate);
-            if (formatDatePlusTimeRange) {
-                // fallbackRange has just the time range, need to format the date part and combine that
-                fDateFormat.applyPattern(fDatePattern);
-                StringBuffer datePortion = new StringBuffer(64);
-                otherPos.setBeginIndex(0);
-                otherPos.setEndIndex(0);
-                datePortion = fDateFormat.format(fromCalendar, datePortion, otherPos);
-                adjustPosition(fDateTimeFormat, fallbackRange, pos, datePortion.toString(), otherPos, pos);
-                fallbackRange = SimpleFormatterImpl.formatRawPattern(
-                        fDateTimeFormat, 2, 2, fallbackRange, datePortion);
-            }
-            appendTo.append(fallbackRange);
-            if (formatDatePlusTimeRange) {
-                // restore full pattern
-                fDateFormat.applyPattern(fullPattern);
+                                              FieldPosition pos,
+                                              FormatOutput output,
+                                              List<FieldPosition> attributes)  {
+        StringBuilder patternSB = new StringBuilder();
+        boolean formatDatePlusTimeRange = (fromToOnSameDay && fDatePattern != null && fTimePattern != null);
+        if (formatDatePlusTimeRange) {
+            String compiledPattern = SimpleFormatterImpl.compileToStringMinMaxArguments(
+                    fDateTimeFormat, patternSB, 2, 2);
+
+            String fullPattern; // for saving the pattern in fDateFormat
+            fullPattern = fDateFormat.toPattern(); // save current pattern, restore later
+
+            // {0} is time range
+            // {1} is single date portion
+            long state = 0;
+            while (true) {
+                state = SimpleFormatterImpl.Int64Iterator.step(compiledPattern, state, appendTo);
+                if (state == SimpleFormatterImpl.Int64Iterator.DONE) {
+                    break;
+                }
+                if (SimpleFormatterImpl.Int64Iterator.getArgIndex(state) == 0) {
+                    fDateFormat.applyPattern(fTimePattern);
+                    fallbackFormatRange(fromCalendar, toCalendar, appendTo, patternSB, pos, output, attributes);
+                } else {
+                    fDateFormat.applyPattern(fDatePattern);
+                    fDateFormat.format(fromCalendar, appendTo, pos, attributes);
+                }
+                // Only accept the first instance of the field
+                if (pos.getEndIndex() > 0) {
+                    pos = new FieldPosition(0);
+                }
             }
-            return appendTo;
+
+            // restore full pattern
+            fDateFormat.applyPattern(fullPattern);
+        } else {
+            fallbackFormatRange(fromCalendar, toCalendar, appendTo, patternSB, pos, output, attributes);
+        }
+        return appendTo;
     }
 
 
@@ -907,11 +1137,13 @@ public class DateIntervalFormat extends UFormat {
                                               Calendar toCalendar,
                                               boolean fromToOnSameDay,
                                               StringBuffer appendTo,
-                                              FieldPosition pos, 
+                                              FieldPosition pos,
+                                              FormatOutput output,
+                                              List<FieldPosition> attributes,
                                               String fullPattern)  {
             String originalPattern = fDateFormat.toPattern();
             fDateFormat.applyPattern(fullPattern);
-            fallbackFormat(fromCalendar, toCalendar, fromToOnSameDay, appendTo, pos);
+            fallbackFormat(fromCalendar, toCalendar, fromToOnSameDay, appendTo, pos, output, attributes);
             fDateFormat.applyPattern(originalPattern);
             return appendTo;
     }
@@ -921,7 +1153,7 @@ public class DateIntervalFormat extends UFormat {
      * Date interval parsing is not supported.
      * <P>
      * This method should handle parsing of
-     * date time interval strings into Formattable objects with 
+     * date time interval strings into Formattable objects with
      * DateInterval type, which is a pair of UDate.
      * <P>
      * Before calling, set parse_pos.index to the offset you want to start
@@ -941,6 +1173,7 @@ public class DateIntervalFormat extends UFormat {
      * @internal
      * @deprecated This API is ICU internal only.
      */
+    @Override
     @Deprecated
     public Object parseObject(String source, ParsePosition parse_pos)
     {
@@ -961,7 +1194,7 @@ public class DateIntervalFormat extends UFormat {
 
 
     /**
-     * Set the date time interval patterns. 
+     * Set the date time interval patterns.
      * @param newItvPattern   the given interval patterns to copy.
      * @stable ICU 4.0
      */
@@ -1030,13 +1263,13 @@ public class DateIntervalFormat extends UFormat {
 
 
     /*
-     *  Below are for generating interval patterns locale to the formatter 
+     *  Below are for generating interval patterns locale to the formatter
      */
 
     /*
      * Initialize interval patterns locale to this formatter.
      */
-    private void initializePattern(ICUCache<String, Map<String, PatternInfo>> cache) { 
+    private void initializePattern(ICUCache<String, Map<String, PatternInfo>> cache) {
         String fullPattern = fDateFormat.toPattern();
         ULocale locale = fDateFormat.getLocale();
         String key = null;
@@ -1055,7 +1288,7 @@ public class DateIntervalFormat extends UFormat {
             if (cache != null) {
                 cache.put(key, patterns);
             }
-        } 
+        }
         fIntervalPatterns = patterns;
     }
 
@@ -1063,8 +1296,8 @@ public class DateIntervalFormat extends UFormat {
 
     /*
      * Initialize interval patterns locale to this formatter
-     * 
-     * This code is a bit complicated since 
+     *
+     * This code is a bit complicated since
      * 1. the interval patterns saved in resource bundle files are interval
      *    patterns based on date or time only.
      *    It does not have interval patterns based on both date and time.
@@ -1072,26 +1305,26 @@ public class DateIntervalFormat extends UFormat {
      *
      *    For example, it has interval patterns on skeleton "dMy" and "hm",
      *    but it does not have interval patterns on skeleton "dMyhm".
-     *    
+     *
      *    The rule to generate interval patterns for both date and time skeleton are
-     *    1) when the year, month, or day differs, concatenate the two original 
-     *    expressions with a separator between, 
-     *    For example, interval pattern from "Jan 10, 2007 10:10 am" 
-     *    to "Jan 11, 2007 10:10am" is 
-     *    "Jan 10, 2007 10:10 am - Jan 11, 2007 10:10am" 
+     *    1) when the year, month, or day differs, concatenate the two original
+     *    expressions with a separator between,
+     *    For example, interval pattern from "Jan 10, 2007 10:10 am"
+     *    to "Jan 11, 2007 10:10am" is
+     *    "Jan 10, 2007 10:10 am - Jan 11, 2007 10:10am"
      *
-     *    2) otherwise, present the date followed by the range expression 
+     *    2) otherwise, present the date followed by the range expression
      *    for the time.
-     *    For example, interval pattern from "Jan 10, 2007 10:10 am" 
-     *    to "Jan 10, 2007 11:10am" is 
-     *    "Jan 10, 2007 10:10 am - 11:10am" 
+     *    For example, interval pattern from "Jan 10, 2007 10:10 am"
+     *    to "Jan 10, 2007 11:10am" is
+     *    "Jan 10, 2007 10:10 am - 11:10am"
      *
      * 2. even a pattern does not request a certain calendar field,
      *    the interval pattern needs to include such field if such fields are
      *    different between 2 dates.
-     *    For example, a pattern/skeleton is "hm", but the interval pattern 
+     *    For example, a pattern/skeleton is "hm", but the interval pattern
      *    includes year, month, and date when year, month, and date differs.
-     * 
+     *
      *
      * @param fullPattern  formatter's full pattern
      * @param locale       the given locale.
@@ -1106,7 +1339,7 @@ public class DateIntervalFormat extends UFormat {
         }
         String skeleton = fSkeleton;
 
-        HashMap<String, PatternInfo> intervalPatterns = new HashMap<String, PatternInfo>();
+        HashMap<String, PatternInfo> intervalPatterns = new HashMap<>();
 
         /* Check whether the skeleton is a combination of date and time.
          * For the complication reason 1 explained above.
@@ -1119,7 +1352,7 @@ public class DateIntervalFormat extends UFormat {
         /* the difference between time skeleton and normalizedTimeSkeleton are:
          * 1. (Formerly, normalized time skeleton folded 'H' to 'h'; no longer true)
          * 2. 'a' is omitted in normalized time skeleton.
-         * 3. there is only one appearance for 'h', 'm','v', 'z' in normalized 
+         * 3. there is only one appearance for 'h', 'm','v', 'z' in normalized
          *    time skeleton
          *
          * The difference between date skeleton and normalizedDateSkeleton are:
@@ -1144,7 +1377,7 @@ public class DateIntervalFormat extends UFormat {
             fDateTimeFormat = getConcatenationPattern(locale);
         }
 
-        boolean found = genSeparateDateTimePtn(normalizedDateSkeleton, 
+        boolean found = genSeparateDateTimePtn(normalizedDateSkeleton,
                                                normalizedTimeSkeleton,
                                                intervalPatterns, dtpng);
 
@@ -1187,7 +1420,7 @@ public class DateIntervalFormat extends UFormat {
             }
             return intervalPatterns;
         } // end of skeleton not found
-        // interval patterns for skeleton are found in resource 
+        // interval patterns for skeleton are found in resource
         if ( time.length() == 0 ) {
             // done
         } else if ( date.length() == 0 ) {
@@ -1219,14 +1452,14 @@ public class DateIntervalFormat extends UFormat {
                 CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.YEAR], ptn);
         } else {
             /* if both present,
-             * 1) when the year, month, or day differs, 
-             * concatenate the two original expressions with a separator between, 
-             * 2) otherwise, present the date followed by the 
-             * range expression for the time. 
+             * 1) when the year, month, or day differs,
+             * concatenate the two original expressions with a separator between,
+             * 2) otherwise, present the date followed by the
+             * range expression for the time.
              */
             /*
-             * 1) when the year, month, or day differs, 
-             * concatenate the two original expressions with a separator between, 
+             * 1) when the year, month, or day differs,
+             * concatenate the two original expressions with a separator between,
              */
             // if field exists, use fall back
             if ( !fieldExistsInSkeleton(Calendar.DATE, dateSkeleton) ) {
@@ -1247,10 +1480,10 @@ public class DateIntervalFormat extends UFormat {
                     CALENDAR_FIELD_TO_PATTERN_LETTER[Calendar.YEAR] + skeleton;
                 genFallbackPattern(Calendar.YEAR, skeleton, intervalPatterns, dtpng);
             }
-            
+
             /*
-             * 2) otherwise, present the date followed by the 
-             * range expression for the time. 
+             * 2) otherwise, present the date followed by the
+             * range expression for the time.
              */
             if (fDateTimeFormat == null) {
                 fDateTimeFormat = "{1} {0}";
@@ -1298,7 +1531,7 @@ public class DateIntervalFormat extends UFormat {
         // should be used in fall-back.
         PatternInfo ptn = new PatternInfo(
                                     null, pattern, fInfo.getDefaultOrder());
-        intervalPatterns.put( 
+        intervalPatterns.put(
             DateIntervalInfo.CALENDAR_FIELD_TO_PATTERN_LETTER[field], ptn);
     }
 
@@ -1308,7 +1541,7 @@ public class DateIntervalFormat extends UFormat {
     private void genFallbackForNotFound(String field, StringBuffer skeleton) {
         if ( SimpleDateFormat.isFieldUnitIgnored(skeleton.toString(), field) ) {
             // single date
-            DateIntervalInfo.PatternInfo ptnInfo = 
+            DateIntervalInfo.PatternInfo ptnInfo =
                 new DateIntervalInfo.PatternInfo(null, fDateFormat.toPattern(),
                                                  fInfo.getDefaultOrder());
             fIntervalPatterns.put(field, ptnInfo);
@@ -1361,7 +1594,7 @@ public class DateIntervalFormat extends UFormat {
         int mCount = 0;
         int vCount = 0;
         int zCount = 0;
-    
+
         for (i = 0; i < skeleton.length(); ++i) {
             char ch = skeleton.charAt(i);
             switch ( ch ) {
@@ -1401,7 +1634,7 @@ public class DateIntervalFormat extends UFormat {
                 dateSkeleton.append(ch);
                 break;
               case 'a':
-                // 'a' is implicitly handled 
+                // 'a' is implicitly handled
                 timeSkeleton.append(ch);
                 break;
               case 'h':
@@ -1434,10 +1667,10 @@ public class DateIntervalFormat extends UFormat {
               case 'A':
                 timeSkeleton.append(ch);
                 normalizedTimeSkeleton.append(ch);
-                break;     
+                break;
             }
         }
-    
+
         /* generate normalized form for date*/
         if ( yCount != 0 ) {
             for (i = 0; i < yCount; i++) {
@@ -1465,7 +1698,7 @@ public class DateIntervalFormat extends UFormat {
         if ( dCount != 0 ) {
             normalizedDateSkeleton.append('d');
         }
-    
+
         /* generate normalized form for time */
         if ( HCount != 0 ) {
             normalizedTimeSkeleton.append('H');
@@ -1489,7 +1722,7 @@ public class DateIntervalFormat extends UFormat {
     /*
      * Generate date or time interval pattern from resource.
      *
-     * It needs to handle the following: 
+     * It needs to handle the following:
      * 1. need to adjust field width.
      *    For example, the interval patterns saved in DateIntervalInfo
      *    includes "dMMMy", but not "dMMMMy".
@@ -1508,7 +1741,7 @@ public class DateIntervalFormat extends UFormat {
      * @return whether there is interval patterns for the skeleton.
      *         true if there is, false otherwise
      */
-    private boolean genSeparateDateTimePtn(String dateSkeleton, 
+    private boolean genSeparateDateTimePtn(String dateSkeleton,
                                            String timeSkeleton,
                                            Map<String, PatternInfo> intervalPatterns,
                                            DateTimePatternGenerator dtpng)
@@ -1525,19 +1758,19 @@ public class DateIntervalFormat extends UFormat {
             skeleton = dateSkeleton;
         }
 
-        /* interval patterns for skeleton "dMMMy" (but not "dMMMMy") 
+        /* interval patterns for skeleton "dMMMy" (but not "dMMMMy")
          * are defined in resource,
          * interval patterns for skeleton "dMMMMy" are calculated by
          * 1. get the best match skeleton for "dMMMMy", which is "dMMMy"
          * 2. get the interval patterns for "dMMMy",
-         * 3. extend "MMM" to "MMMM" in above interval patterns for "dMMMMy" 
+         * 3. extend "MMM" to "MMMM" in above interval patterns for "dMMMMy"
          * getBestSkeleton() is step 1.
          */
         // best skeleton, and the difference information
         BestMatchInfo retValue = fInfo.getBestSkeleton(skeleton);
         String bestSkeleton = retValue.bestMatchSkeleton;
         int differenceInfo =  retValue.bestMatchDistanceInfo;
-   
+
         // Set patterns for fallback use, need to do this
         // before returning if differenceInfo == -1
         if (dateSkeleton.length() != 0  ) {
@@ -1551,10 +1784,10 @@ public class DateIntervalFormat extends UFormat {
         // 0 means the best matched skeleton is the same as input skeleton
         // 1 means the fields are the same, but field width are different
         // 2 means the only difference between fields are v/z,
-        // -1 means there are other fields difference 
+        // -1 means there are other fields difference
         // (this will happen, for instance, if the supplied skeleton has seconds,
         //  but no skeletons in the intervalFormats data do)
-        if ( differenceInfo == -1 ) { 
+        if ( differenceInfo == -1 ) {
             // skeleton has different fields, not only  v/z difference
             return false;
         }
@@ -1563,7 +1796,7 @@ public class DateIntervalFormat extends UFormat {
             // only has date skeleton
             genIntervalPattern(Calendar.DATE, skeleton, bestSkeleton, differenceInfo, intervalPatterns);
             SkeletonAndItsBestMatch skeletons = genIntervalPattern(
-                                                  Calendar.MONTH, skeleton, 
+                                                  Calendar.MONTH, skeleton,
                                                   bestSkeleton, differenceInfo,
                                                   intervalPatterns);
             if ( skeletons != null ) {
@@ -1596,7 +1829,7 @@ public class DateIntervalFormat extends UFormat {
      *         0 means the best matched skeleton is the same as input skeleton
      *         1 means the fields are the same, but field width are different
      *         2 means the only difference between fields are v/z,
-     *        -1 means there are other fields difference 
+     *        -1 means there are other fields difference
      *
      * @param intervalPatterns interval patterns
      *
@@ -1604,7 +1837,7 @@ public class DateIntervalFormat extends UFormat {
      *          null otherwise.
      */
     private SkeletonAndItsBestMatch genIntervalPattern(
-                   int field, String skeleton, String bestSkeleton, 
+                   int field, String skeleton, String bestSkeleton,
                    int differenceInfo, Map<String, PatternInfo> intervalPatterns) {
         SkeletonAndItsBestMatch retValue = null;
         PatternInfo pattern = fInfo.getIntervalPattern(
@@ -1612,9 +1845,9 @@ public class DateIntervalFormat extends UFormat {
         if ( pattern == null ) {
             // single date
             if ( SimpleDateFormat.isFieldUnitIgnored(bestSkeleton, field) ) {
-                PatternInfo ptnInfo = 
+                PatternInfo ptnInfo =
                     new PatternInfo(fDateFormat.toPattern(),
-                                                     null, 
+                                                     null,
                                                      fInfo.getDefaultOrder());
                 intervalPatterns.put(DateIntervalInfo.
                     CALENDAR_FIELD_TO_PATTERN_LETTER[field], ptnInfo);
@@ -1622,27 +1855,27 @@ public class DateIntervalFormat extends UFormat {
             }
 
             // for 24 hour system, interval patterns in resource file
-            // might not include pattern when am_pm differ, 
+            // might not include pattern when am_pm differ,
             // which should be the same as hour differ.
             // add it here for simplicity
             if ( field == Calendar.AM_PM ) {
-                 pattern = fInfo.getIntervalPattern(bestSkeleton, 
+                 pattern = fInfo.getIntervalPattern(bestSkeleton,
                                                          Calendar.HOUR);
                  if ( pattern != null ) {
                       // share
                       intervalPatterns.put(DateIntervalInfo.
-                          CALENDAR_FIELD_TO_PATTERN_LETTER[field], 
+                          CALENDAR_FIELD_TO_PATTERN_LETTER[field],
                           pattern);
                  }
                  return null;
-            } 
+            }
             // else, looking for pattern when 'y' differ for 'dMMMM' skeleton,
             // first, get best match pattern "MMMd",
             // since there is no pattern for 'y' differs for skeleton 'MMMd',
             // need to look for it from skeleton 'yMMMd',
             // if found, adjust field width in interval pattern from
             // "MMM" to "MMMM".
-            String fieldLetter = 
+            String fieldLetter =
                 DateIntervalInfo.CALENDAR_FIELD_TO_PATTERN_LETTER[field];
             bestSkeleton = fieldLetter + bestSkeleton;
             skeleton = fieldLetter + skeleton;
@@ -1663,17 +1896,17 @@ public class DateIntervalFormat extends UFormat {
             if ( pattern != null ) {
                 retValue = new SkeletonAndItsBestMatch(skeleton, bestSkeleton);
             }
-        } 
+        }
         if ( pattern != null ) {
             if ( differenceInfo != 0 ) {
-                String part1 = adjustFieldWidth(skeleton, bestSkeleton, 
+                String part1 = adjustFieldWidth(skeleton, bestSkeleton,
                                    pattern.getFirstPart(), differenceInfo);
-                String part2 = adjustFieldWidth(skeleton, bestSkeleton, 
+                String part2 = adjustFieldWidth(skeleton, bestSkeleton,
                                    pattern.getSecondPart(), differenceInfo);
-                pattern =  new PatternInfo(part1, part2, 
+                pattern =  new PatternInfo(part1, part2,
                                            pattern.firstDateInPtnIsLaterDate());
             } else {
-                // pattern is immutable, no need to clone; 
+                // pattern is immutable, no need to clone;
                 // pattern = (PatternInfo)pattern.clone();
             }
             intervalPatterns.put(
@@ -1714,7 +1947,7 @@ public class DateIntervalFormat extends UFormat {
                                     String bestMatchSkeleton,
                                     String bestMatchIntervalPattern,
                                     int differenceInfo ) {
-        
+
         if ( bestMatchIntervalPattern == null ) {
             return null; // the 2nd part could be null
         }
@@ -1746,10 +1979,10 @@ public class DateIntervalFormat extends UFormat {
         boolean inQuote = false;
         char prevCh = 0;
         int count = 0;
-    
+
         int PATTERN_CHAR_BASE = 0x41;
-        
-        // loop through the pattern string character by character 
+
+        // loop through the pattern string character by character
         int adjustedPtnLength = adjustedPtn.length();
         for (int i = 0; i < adjustedPtnLength; ++i) {
             char ch = adjustedPtn.charAt(i);
@@ -1757,7 +1990,7 @@ public class DateIntervalFormat extends UFormat {
                 // check the repeativeness of pattern letter
                 char skeletonChar = prevCh;
                 if ( skeletonChar == 'L' ) {
-                    // for skeleton "M+", the pattern is "...L..." 
+                    // for skeleton "M+", the pattern is "...L..."
                     skeletonChar = 'M';
                 }
                 int fieldCount = bestMatchSkeletonFieldWidth[skeletonChar - PATTERN_CHAR_BASE];
@@ -1765,8 +1998,8 @@ public class DateIntervalFormat extends UFormat {
                 if ( fieldCount == count && inputFieldCount > fieldCount ) {
                     count = inputFieldCount - fieldCount;
                     for ( int j = 0; j < count; ++j ) {
-                        adjustedPtn.insert(i, prevCh);    
-                    }                    
+                        adjustedPtn.insert(i, prevCh);
+                    }
                     i += count;
                     adjustedPtnLength += count;
                 }
@@ -1780,10 +2013,10 @@ public class DateIntervalFormat extends UFormat {
                 } else {
                     inQuote = ! inQuote;
                 }
-            } 
-            else if ( ! inQuote && ((ch >= 0x0061 /*'a'*/ && ch <= 0x007A /*'z'*/) 
+            }
+            else if ( ! inQuote && ((ch >= 0x0061 /*'a'*/ && ch <= 0x007A /*'z'*/)
                         || (ch >= 0x0041 /*'A'*/ && ch <= 0x005A /*'Z'*/))) {
-                // ch is a date-time pattern character 
+                // ch is a date-time pattern character
                 prevCh = ch;
                 ++count;
             }
@@ -1793,7 +2026,7 @@ public class DateIntervalFormat extends UFormat {
             // check the repeativeness of pattern letter
             char skeletonChar = prevCh;
             if ( skeletonChar == 'L' ) {
-                // for skeleton "M+", the pattern is "...L..." 
+                // for skeleton "M+", the pattern is "...L..."
                 skeletonChar = 'M';
             }
             int fieldCount = bestMatchSkeletonFieldWidth[skeletonChar - PATTERN_CHAR_BASE];
@@ -1801,8 +2034,8 @@ public class DateIntervalFormat extends UFormat {
             if ( fieldCount == count && inputFieldCount > fieldCount ) {
                 count = inputFieldCount - fieldCount;
                 for ( int j = 0; j < count; ++j ) {
-                    adjustedPtn.append(prevCh);    
-                }                    
+                    adjustedPtn.append(prevCh);
+                }
             }
         }
         return adjustedPtn.toString();
@@ -1813,7 +2046,7 @@ public class DateIntervalFormat extends UFormat {
      * Concat a single date pattern with a time interval pattern,
      * set it into the intervalPatterns, while field is time field.
      * This is used to handle time interval patterns on skeleton with
-     * both time and date. Present the date followed by 
+     * both time and date. Present the date followed by
      * the range expression for the time.
      * @param dtfmt                  date and time format
      * @param datePattern            date pattern
@@ -1826,10 +2059,10 @@ public class DateIntervalFormat extends UFormat {
                                                Map<String, PatternInfo> intervalPatterns)
     {
 
-        PatternInfo  timeItvPtnInfo = 
+        PatternInfo  timeItvPtnInfo =
             intervalPatterns.get(DateIntervalInfo.CALENDAR_FIELD_TO_PATTERN_LETTER[field]);
         if ( timeItvPtnInfo != null ) {
-            String timeIntervalPattern = timeItvPtnInfo.getFirstPart() + 
+            String timeIntervalPattern = timeItvPtnInfo.getFirstPart() +
                                          timeItvPtnInfo.getSecondPart();
             String pattern = SimpleFormatterImpl.formatRawPattern(
                     dtfmt, 2, 2, timeIntervalPattern, datePattern);
@@ -1837,7 +2070,7 @@ public class DateIntervalFormat extends UFormat {
                                 timeItvPtnInfo.firstDateInPtnIsLaterDate());
             intervalPatterns.put(
               DateIntervalInfo.CALENDAR_FIELD_TO_PATTERN_LETTER[field], timeItvPtnInfo);
-        } 
+        }
         // else: fall back
         // it should not happen if the interval format defined is valid
     }
@@ -1864,7 +2097,7 @@ public class DateIntervalFormat extends UFormat {
         stream.defaultReadObject();
         initializePattern(isDateIntervalInfoDefault ? LOCAL_PATTERN_CACHE : null);
     }
-    
+
     /**
      * Get the internal patterns for the skeleton
      * @internal CLDR
index 02e18571db1fedcc2f28baed81a0bb81ef368642..7cdedd74b96531b6114c368ebc6821c3992d8e56 100644 (file)
@@ -1333,6 +1333,11 @@ public class SimpleDateFormat extends DateFormat {
     @Override
     public StringBuffer format(Calendar cal, StringBuffer toAppendTo,
                                FieldPosition pos) {
+        return format(cal, toAppendTo, pos, null);
+    }
+
+    /** Internal formatting method that accepts an attributes list. */
+    StringBuffer format(Calendar cal, StringBuffer toAppendTo, FieldPosition pos, List<FieldPosition> attributes) {
         TimeZone backupTZ = null;
         if (cal != calendar && !cal.getType().equals(calendar.getType())) {
             // Different calendar type
@@ -1343,7 +1348,7 @@ public class SimpleDateFormat extends DateFormat {
             calendar.setTimeZone(cal.getTimeZone());
             cal = calendar;
         }
-        StringBuffer result = format(cal, getContext(DisplayContext.Type.CAPITALIZATION), toAppendTo, pos, null);
+        StringBuffer result = format(cal, getContext(DisplayContext.Type.CAPITALIZATION), toAppendTo, pos, attributes);
         if (backupTZ != null) {
             // Restore the original time zone
             calendar.setTimeZone(backupTZ);
@@ -2111,7 +2116,7 @@ public class SimpleDateFormat extends DateFormat {
     }
 
     private static ICUCache<String, Object[]> PARSED_PATTERN_CACHE =
-        new SimpleCache<String, Object[]>();
+        new SimpleCache<>();
     private transient Object[] patternItems;
 
     /*
@@ -2134,7 +2139,7 @@ public class SimpleDateFormat extends DateFormat {
         char itemType = 0;  // 0 for string literal, otherwise date/time pattern character
         int itemLength = 1;
 
-        List<Object> items = new ArrayList<Object>();
+        List<Object> items = new ArrayList<>();
 
         for (int i = 0; i < pattern.length(); i++) {
             char ch = pattern.charAt(i);
@@ -2375,9 +2380,9 @@ public class SimpleDateFormat extends DateFormat {
         // Hold the day period until everything else is parsed, because we need
         // the hour to interpret time correctly.
         // Using an one-element array for output parameter.
-        Output<DayPeriodRules.DayPeriod> dayPeriod = new Output<DayPeriodRules.DayPeriod>(null);
+        Output<DayPeriodRules.DayPeriod> dayPeriod = new Output<>(null);
 
-        Output<TimeType> tzTimeType = new Output<TimeType>(TimeType.UNKNOWN);
+        Output<TimeType> tzTimeType = new Output<>(TimeType.UNKNOWN);
         boolean[] ambiguousYear = { false };
 
         // item index for the first numeric field within a contiguous numeric run
@@ -3620,7 +3625,7 @@ public class SimpleDateFormat extends DateFormat {
                      // not get here but leave support in for future definition.
             {
                 // Try matching a time separator.
-                ArrayList<String> data = new ArrayList<String>(3);
+                ArrayList<String> data = new ArrayList<>(3);
                 data.add(formatData.getTimeSeparatorString());
 
                 // Add the default, if different from the locale.
@@ -4093,7 +4098,7 @@ public class SimpleDateFormat extends DateFormat {
         }
         StringBuffer toAppendTo = new StringBuffer();
         FieldPosition pos = new FieldPosition(0);
-        List<FieldPosition> attributes = new ArrayList<FieldPosition>();
+        List<FieldPosition> attributes = new ArrayList<>();
         format(cal, getContext(DisplayContext.Type.CAPITALIZATION), toAppendTo, pos, attributes);
 
         AttributedString as = new AttributedString(toAppendTo.toString());
@@ -4445,10 +4450,10 @@ public class SimpleDateFormat extends DateFormat {
 
         // initialize mapping if not there
         if (numberFormatters == null) {
-            numberFormatters = new HashMap<String, NumberFormat>();
+            numberFormatters = new HashMap<>();
         }
         if (overrideMap == null) {
-            overrideMap = new HashMap<Character, String>();
+            overrideMap = new HashMap<>();
         }
 
         // separate string into char and add to maps
@@ -4487,8 +4492,8 @@ public class SimpleDateFormat extends DateFormat {
 
     private void initNumberFormatters(ULocale loc) {
 
-       numberFormatters = new HashMap<String, NumberFormat>();
-       overrideMap = new HashMap<Character, String>();
+       numberFormatters = new HashMap<>();
+       overrideMap = new HashMap<>();
        processOverrideString(loc,override);
 
     }
index d372bec743f3cb6bf6d0e7eda6b37f9f5ae0fd6f..8c91755e5ee2cce4e077c7b47a90715927f8926d 100644 (file)
@@ -26,6 +26,27 @@ public abstract class UFormat extends Format {
     // jdk1.4.2 serialver
     private static final long serialVersionUID = -4964390515840164416L;
 
+    /**
+     * A field that represents a span of text that may be composed with other fields.
+     * SpanField classes usually have an associated value.
+     *
+     * @draft ICU 64
+     * @provisional This API might change or be removed in a future release.
+     */
+    public static abstract class SpanField extends Format.Field {
+        private static final long serialVersionUID = -4732719509273350606L;
+
+        /**
+         * Construct a new instance.
+         *
+         * @draft ICU 64
+         * @provisional This API might change or be removed in a future release.
+         */
+        protected SpanField(String name) {
+            super(name);
+        }
+    }
+
     /**
      * Default constructor.
      *
index b55b23ffd280f57d6c7c35d013c48ba1ae8b38b5..6a228a2c8a0b18cd58b5265db7b5ccb8525c0726 100644 (file)
@@ -32,6 +32,7 @@ import com.ibm.icu.dev.test.TestFmwk;
 import com.ibm.icu.impl.Utility;
 import com.ibm.icu.text.DateFormat;
 import com.ibm.icu.text.DateIntervalFormat;
+import com.ibm.icu.text.DateIntervalFormat.FormattedDateInterval;
 import com.ibm.icu.text.DateIntervalInfo;
 import com.ibm.icu.text.DateIntervalInfo.PatternInfo;
 import com.ibm.icu.text.SimpleDateFormat;
@@ -1423,7 +1424,7 @@ public class DateIntervalFormatTest extends TestFmwk {
         } catch (Exception e) { /* No op */ }
 
         // Check getPatterns()
-        Output<String> secondPart = new Output<String>();
+        Output<String> secondPart = new Output<>();
         Calendar fromCalendar = Calendar.getInstance(Locale.ENGLISH);
         fromCalendar.set(2016, 5, 22);
         Calendar toCalendar= Calendar.getInstance(Locale.ENGLISH);
@@ -1827,8 +1828,8 @@ public class DateIntervalFormatTest extends TestFmwk {
     public void TestTicket11669 () {
         // These final variables are accessed directly by the concurrent threads.
         final DateIntervalFormat formatter = DateIntervalFormat.getInstance(DateFormat.YEAR_MONTH_DAY, ULocale.US);
-        final ArrayList<DateInterval> testIntervals = new ArrayList<DateInterval>();
-        final ArrayList<String>expectedResults = new ArrayList<String>();
+        final ArrayList<DateInterval> testIntervals = new ArrayList<>();
+        final ArrayList<String>expectedResults = new ArrayList<>();
 
         // Create and save the input test data.
         TimeZone tz = TimeZone.getTimeZone("Americal/Los_Angeles");
@@ -1871,7 +1872,7 @@ public class DateIntervalFormatTest extends TestFmwk {
             }
         }
 
-        List<TestThread> threads = new ArrayList<TestThread>();
+        List<TestThread> threads = new ArrayList<>();
         for (int i=0; i<4; ++i) {
             threads.add(new TestThread());
         }
@@ -1889,4 +1890,87 @@ public class DateIntervalFormatTest extends TestFmwk {
             }
         }
     }
+
+    @Test
+    public void testFormattedDateInterval() {
+        DateIntervalFormat fmt = DateIntervalFormat.getInstance("dMMMMy", ULocale.US);
+
+        {
+            String message = "FormattedDateInterval test 1";
+            String expectedString = "July 20 \u2013 25, 2018";
+            Calendar input1 = Calendar.getInstance(ULocale.UK);
+            Calendar input2 = Calendar.getInstance(ULocale.UK);
+            input1.set(2018, 6, 20);
+            input2.set(2018, 6, 25);
+            FormattedDateInterval result = fmt.formatToValue(input1, input2);
+            Object[][] expectedFieldPositions = new Object[][] {
+                // field, begin index, end index
+                {DateFormat.Field.MONTH, 0, 4},
+                {DateIntervalFormat.SpanField.DATE_INTERVAL_SPAN, 5, 7, 0},
+                {DateFormat.Field.DAY_OF_MONTH, 5, 7},
+                {DateIntervalFormat.SpanField.DATE_INTERVAL_SPAN, 10, 12, 1},
+                {DateFormat.Field.DAY_OF_MONTH, 10, 12},
+                {DateFormat.Field.YEAR, 14, 18}};
+            FormattedValueTest.checkFormattedValue(
+                message,
+                result,
+                expectedString,
+                expectedFieldPositions);
+        }
+
+        fmt = DateIntervalFormat.getInstance("dMMMha", ULocale.US);
+
+        {
+            String message = "FormattedDateInterval test 2";
+            String expectedString = "Feb 15, 11 AM \u2013 3 PM";
+            Calendar input1 = Calendar.getInstance(ULocale.US);
+            Calendar input2 = Calendar.getInstance(ULocale.US);
+            input1.set(2019, 1, 15, 11, 0, 0);
+            input2.set(2019, 1, 15, 15, 0, 0);
+            FormattedDateInterval result = fmt.formatToValue(input1, input2);
+            Object[][] expectedFieldPositions = new Object[][] {
+                // field, begin index, end index
+                {DateFormat.Field.MONTH, 0, 3},
+                {DateFormat.Field.DAY_OF_MONTH, 4, 6},
+                {DateIntervalFormat.SpanField.DATE_INTERVAL_SPAN, 8, 13, 0},
+                {DateFormat.Field.HOUR1, 8, 10},
+                {DateFormat.Field.AM_PM, 11, 13},
+                {DateIntervalFormat.SpanField.DATE_INTERVAL_SPAN, 16, 20, 1},
+                {DateFormat.Field.HOUR1, 16, 17},
+                {DateFormat.Field.AM_PM, 18, 20}};
+            FormattedValueTest.checkFormattedValue(
+                message,
+                result,
+                expectedString,
+                expectedFieldPositions);
+        }
+
+        // To test the fallback pattern behavior, make a custom DateIntervalInfo.
+        DateIntervalInfo dtitvinf = new DateIntervalInfo();
+        dtitvinf.setFallbackIntervalPattern("<< {1} --- {0} >>");
+        fmt = DateIntervalFormat.getInstance("dMMMMy", ULocale.US, dtitvinf);
+
+        {
+            String message = "FormattedDateInterval with fallback format test 1";
+            String expectedString = "<< July 25, 2018 --- July 20, 2018 >>";
+            Date input1 = new Date(2018 - 1900, 6, 20);
+            Date input2 = new Date(2018 - 1900, 6, 25);
+            DateInterval dtiv = new DateInterval(input1.getTime(), input2.getTime());
+            FormattedDateInterval result = fmt.formatToValue(dtiv);
+            Object[][] expectedFieldPositions = new Object[][] {
+                {DateIntervalFormat.SpanField.DATE_INTERVAL_SPAN, 3, 16, 1},
+                {DateFormat.Field.MONTH, 3, 7},
+                {DateFormat.Field.DAY_OF_MONTH, 8, 10},
+                {DateFormat.Field.YEAR, 12, 16},
+                {DateIntervalFormat.SpanField.DATE_INTERVAL_SPAN, 21, 34, 0},
+                {DateFormat.Field.MONTH, 21, 25},
+                {DateFormat.Field.DAY_OF_MONTH, 26, 28},
+                {DateFormat.Field.YEAR, 30, 34}};
+            FormattedValueTest.checkFormattedValue(
+                message,
+                result,
+                expectedString,
+                expectedFieldPositions);
+        }
+    }
 }
index 7aea4833ba0d574dedff94251c72d5c6a603265d..57ed86dd964b081a1efcd83c0a5b836bfe8fab98 100644 (file)
@@ -175,6 +175,7 @@ public class FormattedValueTest {
                 Format.Field expectedField = (Format.Field) cas[0];
                 int expectedBeginIndex = (Integer) cas[1];
                 int expectedEndIndex = (Integer) cas[2];
+                Object expectedValue = cas.length == 4 ? cas[3] : expectedField;
                 if (expectedBeginIndex > i || expectedEndIndex <= i) {
                     // Field position does not overlap with the current character
                     continue;
@@ -186,8 +187,10 @@ public class FormattedValueTest {
                         allAttributes.contains(expectedField));
                 int actualBeginIndex = fpi.getRunStart(expectedField);
                 int actualEndIndex = fpi.getRunLimit(expectedField);
+                Object actualValue = fpi.getAttribute(expectedField);
                 assertEquals(baseMessage + expectedField + " begin @" + i, expectedBeginIndex, actualBeginIndex);
                 assertEquals(baseMessage + expectedField + " end @" + i, expectedEndIndex, actualEndIndex);
+                assertEquals(baseMessage + expectedField + " value @" + i, expectedValue, actualValue);
                 attributesRemaining--;
             }
             assertEquals(baseMessage + "Should have looked at every field: " + i + ": " + currentAttributes,
@@ -203,9 +206,11 @@ public class FormattedValueTest {
             Format.Field expectedField = (Format.Field) cas[0];
             int expectedStart = (Integer) cas[1];
             int expectedLimit = (Integer) cas[2];
+            Object expectedValue = cas.length == 4 ? cas[3] : null;
             assertEquals(baseMessage + "field " + i, expectedField, cfpos.getField());
             assertEquals(baseMessage + "start " + i, expectedStart, cfpos.getStart());
             assertEquals(baseMessage + "limit " + i, expectedLimit, cfpos.getLimit());
+            assertEquals(baseMessage + "value " + i, expectedValue, cfpos.getFieldValue());
             i++;
         }
         boolean afterLoopResult = fv.nextPosition(cfpos);
@@ -224,9 +229,11 @@ public class FormattedValueTest {
                 Format.Field expectedField = (Format.Field) cas[0];
                 int expectedStart = (Integer) cas[1];
                 int expectedLimit = (Integer) cas[2];
+                Object expectedValue = cas.length == 4 ? cas[3] : null;
                 assertEquals(baseMessage + "field " + i, expectedField, cfpos.getField());
                 assertEquals(baseMessage + "start " + i, expectedStart, cfpos.getStart());
                 assertEquals(baseMessage + "limit " + i, expectedLimit, cfpos.getLimit());
+                assertEquals(baseMessage + "value " + i, expectedValue, cfpos.getFieldValue());
                 i++;
             }
             afterLoopResult = fv.nextPosition(cfpos);
@@ -251,9 +258,11 @@ public class FormattedValueTest {
                 Format.Field expectedField = (Format.Field) cas[0];
                 int expectedStart = (Integer) cas[1];
                 int expectedLimit = (Integer) cas[2];
+                Object expectedValue = cas.length == 4 ? cas[3] : null;
                 assertEquals(baseMessage + "field " + i, expectedField, cfpos.getField());
                 assertEquals(baseMessage + "start " + i, expectedStart, cfpos.getStart());
                 assertEquals(baseMessage + "limit " + i, expectedLimit, cfpos.getLimit());
+                assertEquals(baseMessage + "value " + i, expectedValue, cfpos.getFieldValue());
                 i++;
             }
             afterLoopResult = fv.nextPosition(cfpos);
index d34b64453fe5e00dc7f63da2c877566eef0ed175..8d5eeb46d5421ab47661324256db298941b2b153 100644 (file)
@@ -1816,6 +1816,21 @@ public class FormatHandler
         }
     }
 
+    public static class DateIntervalSpanFieldHandler implements SerializableTestUtility.Handler
+    {
+        @Override
+        public Object[] getTestObjects()
+        {
+            return new Object[] {DateIntervalFormat.SpanField.DATE_INTERVAL_SPAN};
+        }
+
+        @Override
+        public boolean hasSameBehavior(Object a, Object b)
+        {
+            return (a == b);
+        }
+    }
+
     public static class DateFormatHandler implements SerializableTestUtility.Handler
     {
         static HashMap cannedPatterns = new HashMap();
index fe9a601709c0cf7ff07167504fa2c9fd97b2068c..7dba946f03865a63942092ff8f8ead4218765f2e 100644 (file)
@@ -819,6 +819,7 @@ public class SerializableTestUtility {
         map.put("com.ibm.icu.text.ChineseDateFormat$Field", new FormatHandler.ChineseDateFormatFieldHandler());
         map.put("com.ibm.icu.text.MessageFormat$Field", new FormatHandler.MessageFormatFieldHandler());
         map.put("com.ibm.icu.text.RelativeDateTimeFormatter$Field", new FormatHandler.RelativeDateTimeFormatterFieldHandler());
+        map.put("com.ibm.icu.text.DateIntervalFormat$SpanField", new FormatHandler.DateIntervalSpanFieldHandler());
 
         map.put("com.ibm.icu.impl.duration.BasicDurationFormat", new FormatHandler.BasicDurationFormatHandler());
         map.put("com.ibm.icu.impl.RelativeDateFormat", new FormatHandler.RelativeDateFormatHandler());