]> granicus.if.org Git - icu/commitdiff
ICU-10274 Add compound duration formatting for JAVA.
authorTravis Keep <keep94@gmail.com>
Wed, 24 Jul 2013 23:07:31 +0000 (23:07 +0000)
committerTravis Keep <keep94@gmail.com>
Wed, 24 Jul 2013 23:07:31 +0000 (23:07 +0000)
X-SVN-Rev: 33980

icu4j/main/classes/core/src/com/ibm/icu/text/ListFormatter.java
icu4j/main/classes/core/src/com/ibm/icu/text/TimeUnitFormat.java
icu4j/main/classes/core/src/com/ibm/icu/util/TimePeriod.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TimeUnitTest.java

index ed31e418e4fb732aa92644d82e472ecbc81654f6..fa780b1d3f5f50559bfa98eb07acf78fb7aa4786 100644 (file)
@@ -13,6 +13,9 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Locale;
 import java.util.Map;
+import java.util.MissingResourceException;
+
+import javax.management.StandardEmitterMBean;
 
 import com.ibm.icu.impl.ICUCache;
 import com.ibm.icu.impl.ICUResourceBundle;
@@ -34,6 +37,43 @@ final public class ListFormatter {
     private final String middle;
     private final String end;
     private final ULocale locale;
+    
+    /**
+     * Indicates the style of Listformatter
+     * @deprecated internal use only.
+     * @internal
+     */
+    public enum Style {
+        /**
+         * Standard style.
+         * @deprecated
+         * @internal
+         */
+        STANDARD("standard"),
+        /**
+         * Style for full durations
+         * @deprecated
+         * @internal
+         */
+        DURATION("duration"),
+        /**
+         * Style for durations in abbrevated form
+         * @deprecated
+         * @internal
+         */
+        DURATION_SHORT("duration-short");
+        
+        private final String name;
+        
+        Style(String name) {
+            this.name = name;
+        }
+        
+        public String getName() {
+            return name;
+        }
+        
+    }
 
     /**
      * <b>Internal:</b> Create a ListFormatter from component strings,
@@ -75,7 +115,7 @@ final public class ListFormatter {
      * @provisional This API might change or be removed in a future release.
      */
     public static ListFormatter getInstance(ULocale locale) {
-      return cache.get(locale);
+      return getInstance(locale, Style.STANDARD);
     }
 
     /**
@@ -88,7 +128,20 @@ final public class ListFormatter {
      * @provisional This API might change or be removed in a future release.
      */
     public static ListFormatter getInstance(Locale locale) {
-        return getInstance(ULocale.forLocale(locale));
+        return getInstance(ULocale.forLocale(locale), Style.STANDARD);
+    }
+    
+    /**
+     * Create a list formatter that is appropriate for a locale and style.
+     *
+     * @param locale the locale in question.
+     * @param style the style
+     * @return ListFormatter
+     * @deprecated Internal use only.
+     * @internal
+     */
+    public static ListFormatter getInstance(ULocale locale, Style style) {
+        return cache.get(locale, style.getName());
     }
 
     /**
@@ -199,28 +252,39 @@ final public class ListFormatter {
     }
 
     private static class Cache {
-        private final ICUCache<ULocale, ListFormatter> cache =
-            new SimpleCache<ULocale, ListFormatter>();
+        private final ICUCache<String, ListFormatter> cache =
+            new SimpleCache<String, ListFormatter>();
 
-        public ListFormatter get(ULocale locale) {
-            ListFormatter result = cache.get(locale);
+        public ListFormatter get(ULocale locale, String style) {
+            String key = String.format("%s:%s", locale.toString(), style);
+            ListFormatter result = cache.get(key);
             if (result == null) {
-                result = load(locale);
-                cache.put(locale, result);
+                result = load(locale, style);
+                cache.put(key, result);
             }
             return result;
         }
 
-        private static ListFormatter load(ULocale ulocale) {
+        private static ListFormatter load(ULocale ulocale, String style) {
             ICUResourceBundle r = (ICUResourceBundle)UResourceBundle.
                     getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, ulocale);
-            r = r.getWithFallback("listPattern/standard");
-            return new ListFormatter(
-                r.getWithFallback("2").getString(),
-                r.getWithFallback("start").getString(),
-                r.getWithFallback("middle").getString(),
-                r.getWithFallback("end").getString(),
-                ulocale);
+            // TODO(Travis Keep): This try-catch is a hack to cover missing aliases
+            // for listPattern/duration and listPattern/duration-narrow in root.txt.
+            try {
+                return new ListFormatter(
+                    r.getWithFallback("listPattern/" + style + "/2").getString(),
+                    r.getWithFallback("listPattern/" + style + "/start").getString(),
+                    r.getWithFallback("listPattern/" + style + "/middle").getString(),
+                    r.getWithFallback("listPattern/" + style + "/end").getString(),
+                    ulocale);
+            } catch (MissingResourceException e) {
+                return new ListFormatter(
+                        r.getWithFallback("listPattern/standard/2").getString(),
+                        r.getWithFallback("listPattern/standard/start").getString(),
+                        r.getWithFallback("listPattern/standard/middle").getString(),
+                        r.getWithFallback("listPattern/standard/end").getString(),
+                        ulocale);
+            }
         }
     }
 
index 62c7e2011d9de40b490c05adc165649107302e99..0620b568fba6280db05b322950499949293d2f7e 100644 (file)
@@ -6,8 +6,10 @@
  */
 package com.ibm.icu.text;
 
+import java.text.AttributedCharacterIterator;
 import java.text.FieldPosition;
 import java.text.ParsePosition;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
@@ -20,6 +22,7 @@ import com.ibm.icu.impl.ICUResourceBundle;
 import com.ibm.icu.util.TimePeriod;
 import com.ibm.icu.util.TimeUnit;
 import com.ibm.icu.util.TimeUnitAmount;
+import com.ibm.icu.util.TimeZone;
 import com.ibm.icu.util.ULocale;
 import com.ibm.icu.util.ULocale.Category;
 import com.ibm.icu.util.UResourceBundle;
@@ -96,9 +99,9 @@ public class TimeUnitFormat extends MeasureFormat {
     private transient Map<TimeUnit, Map<String, Object[]>> timeUnitToCountToPatterns;
     private transient PluralRules pluralRules;
     private transient ListFormatter listFormatter;
-    private transient MessageFormat hourMinute;
-    private transient MessageFormat minuteSecond;
-    private transient MessageFormat hourMinuteSecond;
+    private transient DateFormat hourMinute;
+    private transient DateFormat minuteSecond;
+    private transient DateFormat hourMinuteSecond;
     private transient boolean isReady;
     private int style;
 
@@ -132,8 +135,7 @@ public class TimeUnitFormat extends MeasureFormat {
     }
 
     /**
-     * Create TimeUnitFormat given a ULocale and a formatting style: full or
-     * abbreviated.
+     * Create TimeUnitFormat given a ULocale and a formatting style.
      * @param locale   locale of this time unit formatter.
      * @param style    format style, either FULL_NAME or ABBREVIATED_NAME style.
      * @throws IllegalArgumentException if the style is not FULL_NAME or
@@ -150,8 +152,7 @@ public class TimeUnitFormat extends MeasureFormat {
     }
 
     /**
-     * Create TimeUnitFormat given a Locale and a formatting style: full or
-     * abbreviated.
+     * Create TimeUnitFormat given a Locale and a formatting style.
      * @stable ICU 4.2
      */
     public TimeUnitFormat(Locale locale, int style) {
@@ -226,12 +227,18 @@ public class TimeUnitFormat extends MeasureFormat {
      */
     public StringBuffer format(Object obj, StringBuffer toAppendTo,
             FieldPosition pos) {
-        if ( !(obj instanceof TimeUnitAmount) ) {
-            throw new IllegalArgumentException("can not format non TimeUnitAmount object");
+        if ( !(obj instanceof TimeUnitAmount) && !(obj instanceof TimePeriod)) {
+            throw new IllegalArgumentException(
+                    "can only format TimeUnitAmount or TimePeriod objects");
         }
         if (!isReady) {
             setup();
         }
+        if (obj instanceof TimePeriod) {
+            // TODO: set FieldPosition, see ICU tickets 10156 and 10157.
+            toAppendTo.append(formatTimePeriod((TimePeriod) obj));
+            return toAppendTo;
+        }
         TimeUnitAmount amount = (TimeUnitAmount) obj;
         Map<String, Object[]> countToPattern = timeUnitToCountToPatterns.get(amount.getTimeUnit());
         double number = amount.getNumber().doubleValue();
@@ -242,13 +249,7 @@ public class TimeUnitFormat extends MeasureFormat {
         return pattern.format(new Object[]{amount.getNumber()}, toAppendTo, pos);
     }
     
-    /**
-     * Formats a TimePeriod. Currently there is no way to parse a formatted TimePeriod.
-     * @param timePeriod the TimePeriod to format.
-     * @return the formatted string.
-     * @draft ICU 52
-     */
-    public String formatTimePeriod(TimePeriod timePeriod) {
+    private String formatTimePeriod(TimePeriod timePeriod) {
         if (!isReady) {
             setup();
         }
@@ -258,7 +259,7 @@ public class TimeUnitFormat extends MeasureFormat {
                 return result;
             }
         }
-        String[] items = new String[timePeriod.size()];
+        String[] items = new String[timePeriod.length()];
         int idx = 0;
         for (TimeUnitAmount amount : timePeriod) {
             items[idx++] = format(amount);
@@ -267,7 +268,12 @@ public class TimeUnitFormat extends MeasureFormat {
     }
 
     /**
-     * Parse a TimeUnitAmount.
+     * Parse a TimeUnitAmount. Parsing TimePeriod objects is not supported.
+     * If parseObject is called on a formatted TimePeriod string, it try to parse it
+     * as a TimeUnitAmount. For example,
+     * <code>parseObject("5 hours and 34 minutes", pos)</code>
+     * returns a TimeUnitAmount representing 5 hours and updates pos to point to the
+     * space after the s in hours.
      * @see java.text.Format#parseObject(java.lang.String, java.text.ParsePosition)
      * @stable ICU 4.0
      */
@@ -373,75 +379,112 @@ public class TimeUnitFormat extends MeasureFormat {
             format = NumberFormat.getNumberInstance(locale);
         }
         pluralRules = PluralRules.forLocale(locale);
-        listFormatter = ListFormatter.getInstance(locale);
-        DateTimePatternGenerator df = DateTimePatternGenerator.getInstance(locale);
-        hourMinute = getPattern(df, "hm", locale, "{0}", "{1,number,00.###}", null);
-        minuteSecond = getPattern(df, "ms", locale, null, "{1}", "{2,number,00.###}");
-        hourMinuteSecond = getPattern(df, "hms", locale, "{0}", "{1,number,00}", "{2,number,00.###}");
+        if (style == FULL_NAME) {
+          listFormatter = ListFormatter.getInstance(locale, ListFormatter.Style.DURATION);
+        } else {
+          listFormatter = ListFormatter.getInstance(locale, ListFormatter.Style.DURATION_SHORT);
+        }
+        hourMinute = loadNumericDurationFormat(locale, "hm");
+        minuteSecond = loadNumericDurationFormat(locale, "ms");
+        hourMinuteSecond = loadNumericDurationFormat(locale, "hms");
         timeUnitToCountToPatterns = new HashMap<TimeUnit, Map<String, Object[]>>();
-
         Set<String> pluralKeywords = pluralRules.getKeywords();
         setup("units/duration", timeUnitToCountToPatterns, FULL_NAME, pluralKeywords);
         setup("unitsShort/duration", timeUnitToCountToPatterns, ABBREVIATED_NAME, pluralKeywords);
         isReady = true;
     }
     
-    private MessageFormat getPattern(DateTimePatternGenerator dtpg, String skeleton, ULocale locale, 
-            String h, String m, String s) {
-        String pat = dtpg.getBestPattern(skeleton);
-        StringBuilder buffer = new StringBuilder();
-        for (Object item : new DateTimePatternGenerator.FormatParser().set(pat).getItems()) {
-            if (item instanceof DateTimePatternGenerator.VariableField) {
-                DateTimePatternGenerator.VariableField fld = (DateTimePatternGenerator.VariableField)item;
-                switch (fld.getType()) {
-                case DateTimePatternGenerator.HOUR: buffer.append(h); break;
-                case DateTimePatternGenerator.MINUTE: buffer.append(m); break;
-                case DateTimePatternGenerator.SECOND: buffer.append(s); break;
-                }
-            } else {
-                buffer.append(item);
-            }
-        }
-        return new MessageFormat(buffer.toString(), locale);
+    // type is one of "hm", "ms" or "hms"
+    private static DateFormat loadNumericDurationFormat(ULocale ulocale, String type) {
+        ICUResourceBundle r = (ICUResourceBundle)UResourceBundle.
+                getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, ulocale);
+        r = r.getWithFallback(String.format("durationUnits/%s", type));
+        // We replace 'h' with 'H' because 'h' does not make sense in the context of durations.
+        DateFormat result = new SimpleDateFormat(r.getString().replace("h", "H"));
+        result.setTimeZone(TimeZone.GMT_ZONE);
+        return result;
     }
     
     private String formatPeriodAsNumeric(TimePeriod timePeriod) {
         TimeUnit biggestUnit = null, smallestUnit = null;
+        Number smallestUnitAmount = null;
         for (TimeUnitAmount tua : timePeriod) {
             if (biggestUnit == null) {
                 biggestUnit = tua.getTimeUnit();
             }
             smallestUnit = tua.getTimeUnit();
+            smallestUnitAmount = tua.getNumber();
         }
-        // We have to trim the result of  MessageFormat.format() not sure why.
+        long millis = (long) (((getAmountOrZero(timePeriod, TimeUnit.HOUR) * 60.0
+                + getAmountOrZero(timePeriod, TimeUnit.MINUTE)) * 60.0
+                + getAmountOrZero(timePeriod, TimeUnit.SECOND)) * 1000.0);
+        Date d = new Date(millis);
         if (biggestUnit == TimeUnit.HOUR && smallestUnit == TimeUnit.SECOND) {
-            return hourMinuteSecond.format(new Object[]{
-                    getZeroedAmount(timePeriod, TimeUnit.HOUR),
-                    getZeroedAmount(timePeriod, TimeUnit.MINUTE),
-                    getZeroedAmount(timePeriod, TimeUnit.SECOND)}).trim();
-            
+            return numericFormat(
+                    d, hourMinuteSecond, DateFormat.Field.SECOND, smallestUnitAmount);
         }
         if (biggestUnit == TimeUnit.MINUTE && smallestUnit == TimeUnit.SECOND) {
-            return minuteSecond.format(new Object[]{
-                    null,
-                    getZeroedAmount(timePeriod, TimeUnit.MINUTE),
-                    getZeroedAmount(timePeriod, TimeUnit.SECOND)}).trim();
-            
+            return numericFormat(
+                    d, minuteSecond, DateFormat.Field.SECOND, smallestUnitAmount);          
         }
         if (biggestUnit == TimeUnit.HOUR && smallestUnit == TimeUnit.MINUTE) {
-            return hourMinute.format(new Object[]{
-                    getZeroedAmount(timePeriod, TimeUnit.HOUR),
-                    getZeroedAmount(timePeriod, TimeUnit.MINUTE)}).trim();            
+            return numericFormat(d, hourMinute, DateFormat.Field.MINUTE, smallestUnitAmount);
         }
         return null;
     }
-    
-    private Number getZeroedAmount(TimePeriod timePeriod, TimeUnit timeUnit) {
+
+    /**
+     * numericFormat allows us to show fractional durations using numeric
+     * style e.g 12:34:56.7. This function is necessary because there is no way to express
+     * fractions of durations other than seconds with current DateFormat objects.
+     * 
+     * After formatting the duration using a DateFormat object in the usual way, it
+     * replaces the smallest field in the formatted string with the exact fractional
+     * amount of that smallest field formatted with this object's NumberFormat object.
+     * 
+     * @param duration The duration to format in milliseconds. The loss of precision here
+     * is ok because we also pass in the exact amount of the smallest field.
+     * @param formatter formats the date.
+     * @param smallestField the smallest defined field in duration to be formatted.
+     * @param smallestAmount the exact fractional value of the smallest amount. 
+     * @return duration formatted numeric style.
+     */
+    private String numericFormat(
+            Date duration,
+            DateFormat formatter,
+            DateFormat.Field smallestField,
+            Number smallestAmount) {
+        // Format the smallest amount ahead of time.
+        String smallestAmountFormatted = format.format(smallestAmount);
+        
+        // Format the duration using the provided DateFormat object. The smallest
+        // field in this result will be missing the fractional part.
+        AttributedCharacterIterator iterator = formatter.formatToCharacterIterator(duration);
+        
+        // The final formatted duration will be written here.
+        StringBuilder builder = new StringBuilder();
+        
+        // iterate through formatted text copying to 'builder' one character at a time.
+        // When we get to the smallest amount, skip over it and copy
+        // 'smallestAmountFormatted' to the builder instead.
+        for (iterator.first(); iterator.getIndex() < iterator.getEndIndex();) {
+            if (iterator.getAttributes().containsKey(smallestField)) {
+                builder.append(smallestAmountFormatted);
+                iterator.setIndex(iterator.getRunLimit(smallestField));
+            } else {
+                builder.append(iterator.current());
+                iterator.next();
+            }
+        }
+        return builder.toString();
+    }
+
+    private static double getAmountOrZero(TimePeriod timePeriod, TimeUnit timeUnit) {
         TimeUnitAmount tua = timePeriod.getAmount(timeUnit);
         if (tua == null) {
-            return Double.valueOf(0);
+            return 0.0;
         }
-        return tua.getNumber();
+        return tua.getNumber().doubleValue();
     }
 
     private void setup(String resourceKey, Map<TimeUnit, Map<String, Object[]>> timeUnitToCountToPatterns,
index a33df30d1eabbb2b5d2113a0a685fe69091c734a..d14b571f2acd4e39474a24d1cfa520e59dcd6624 100644 (file)
@@ -26,68 +26,40 @@ import java.util.NoSuchElementException;
 public final class TimePeriod implements Iterable<TimeUnitAmount> {
     
     private final TimeUnitAmount[] fields;
-    private final int size;
+    private final int length;
     private final int hash;
-    
-    private TimePeriod(TimeUnitAmount[] fields, int size, int hash) {
-        this.fields = fields;
-        this.size = size;
-        this.hash = hash;
-    }
        
     /**
-     * Returns a new TimePeriod that matches the given time unit amounts.
-     * @param amounts the TimeUnitAmounts. Must be non-empty. Normalization of the
-     *   amounts and inclusion/exclusion of 0 amounts is up to caller. The Number
-     *   in each TimeUnitAmount must either be a Byte, Short, Integer, Long, Float,
-     *   Double, BigInteger, or BigDecimal or it must implement Cloneable and have
-     *   a public clone method.
-     * @return the new TimePeriod object
-     * @throws IllegalArgumentException if multiple TimeUnitAmount objects match
-     * the same time unit or if any but the smallest TimeUnit has a fractional value
-     * Or if amounts is empty.
-     * @draft ICU 52
-     * @provisional This API might change or be removed in a future release.
-     */
-    public static TimePeriod forAmounts(TimeUnitAmount ...amounts) {
-        return forAmounts(Arrays.asList(amounts));      
-    }
-
-    /**
-     * Returns a new TimePeriod that matches the given time unit amounts.
+     * Constructor.
      * @param amounts the TimeUnitAmounts. Must be non-empty. Normalization of the
      *   amounts and inclusion/exclusion of 0 amounts is up to caller. The Number
      *   object in each TimeUnitAmount must not change. Otherwise the created
      *   TimePeriod object may not work as expected.
-     * @return the new TimePeriod object
      * @throws IllegalArgumentException if multiple TimeUnitAmount objects match
      * the same time unit or if any but the smallest TimeUnit has a fractional value
      * Or if amounts is empty.
      * @draft ICU 52
      * @provisional This API might change or be removed in a future release.
      */
-    public static TimePeriod forAmounts(Iterable<TimeUnitAmount> amounts) {
-        TimeUnitAmount[] fields = new TimeUnitAmount[TimeUnit.TIME_UNIT_COUNT];
-        int size = 0;
+    public TimePeriod(TimeUnitAmount ...amounts) {
+        fields = new TimeUnitAmount[TimeUnit.TIME_UNIT_COUNT];
+        int tempSize = 0;
         for (TimeUnitAmount tua : amounts) {
             int index = tua.getTimeUnit().getIndex();
             if (fields[index] != null) {
                 throw new IllegalArgumentException(
                         "Only one TimeUnitAmount per unit allowed.");
             }
-            // This line is necessary to guarantee immutability of the TimePeriod
-            // class. A Number object, which is in TimeUnitAmount, need not be immutable,
-            // but Double is immutable.
             fields[index] = tua;
-            size++;
+            tempSize++;
         }
-        if (size == 0) {
+        length = tempSize;
+        if (length == 0) {
             throw new IllegalArgumentException(
                     "There must be at least one TimeUnitAmount.");
         }
-        TimePeriod result = new TimePeriod(fields, size, computeHash(fields));
         boolean fractionalFieldEncountered = false;
-        for (TimeUnitAmount tua : result) {
+        for (TimeUnitAmount tua : this) {
             if (fractionalFieldEncountered) {
                 throw new IllegalArgumentException(
                     "Only the smallest time unit can have a fractional amount.");
@@ -97,7 +69,7 @@ public final class TimePeriod implements Iterable<TimeUnitAmount> {
                 fractionalFieldEncountered = true;
             }
         }
-        return result;
+        hash = computeHash(fields);  
     }
         
     /**
@@ -126,11 +98,10 @@ public final class TimePeriod implements Iterable<TimeUnitAmount> {
 
     /**
      * Returns the number of TimeUnitAmount objects in this object.
-     * @internal
-     * @deprecated This API is ICU internal only.
+     * @draft ICU 52
      */
-    public int size() {
-      return size;
+    public int length() {
+      return length;
     }
     
     /**
index 9f8b637f5f93a17ced6d06ed682d16a0143e89ad..41f4e6a1090201ea481d2d2a81de28ecb28c6452 100644 (file)
@@ -23,26 +23,39 @@ import com.ibm.icu.util.ULocale;
  *
  */
 public class TimeUnitTest extends TestFmwk {
-    private static final TimePeriod _19m = TimePeriod.forAmounts(
+    private static final TimePeriod _19m = new TimePeriod(
             new TimeUnitAmount(19.0, TimeUnit.MINUTE));
-    private static final TimePeriod _19m_28s = TimePeriod.forAmounts(
+    private static final TimePeriod _19m_28s = new TimePeriod(
             new TimeUnitAmount(19.0, TimeUnit.MINUTE),
             new TimeUnitAmount(28.0, TimeUnit.SECOND));
-    private static final TimePeriod _1h_23_5s = TimePeriod.forAmounts(
+    private static final TimePeriod _1h_23_5s = new TimePeriod(
             new TimeUnitAmount(1.0, TimeUnit.HOUR),
             new TimeUnitAmount(23.5, TimeUnit.SECOND));
-    private static final TimePeriod _1h_0m_23s = TimePeriod.forAmounts(
+    private static final TimePeriod _1h_23_5m = new TimePeriod(
+            new TimeUnitAmount(1.0, TimeUnit.HOUR),
+            new TimeUnitAmount(23.5, TimeUnit.MINUTE));
+    private static final TimePeriod _1h_0m_23s = new TimePeriod(
             new TimeUnitAmount(1.0, TimeUnit.HOUR),
             new TimeUnitAmount(0.0, TimeUnit.MINUTE),
             new TimeUnitAmount(23.0, TimeUnit.SECOND));
-    private static final TimePeriod _5h_17m = TimePeriod.forAmounts(
+    private static final TimePeriod _5h_17m = new TimePeriod(
             new TimeUnitAmount(5.0, TimeUnit.HOUR),
             new TimeUnitAmount(17.0, TimeUnit.MINUTE));
-    private static final TimePeriod _2y_5M_3w_4d = TimePeriod.forAmounts(
+    private static final TimePeriod _2y_5M_3w_4d = new TimePeriod(
             new TimeUnitAmount(2.0, TimeUnit.YEAR),
             new TimeUnitAmount(5.0, TimeUnit.MONTH),
             new TimeUnitAmount(3.0, TimeUnit.WEEK),
             new TimeUnitAmount(4.0, TimeUnit.DAY));
+    private static final TimePeriod _0h_0m_17s = new TimePeriod(
+            new TimeUnitAmount(0.0, TimeUnit.HOUR),
+            new TimeUnitAmount(0.0, TimeUnit.MINUTE),
+            new TimeUnitAmount(17.0, TimeUnit.SECOND));
+    private static final TimePeriod _6h_56_92m = new TimePeriod(
+            new TimeUnitAmount(6.0, TimeUnit.HOUR),
+            new TimeUnitAmount(56.92, TimeUnit.MINUTE));
+    private static final TimePeriod _1m_59_9996s = new TimePeriod(
+            new TimeUnitAmount(1.0, TimeUnit.MINUTE),
+            new TimeUnitAmount(59.9996, TimeUnit.SECOND));
             
     public static void main(String[] args) throws Exception{
         new TimeUnitTest().run(args);
@@ -358,33 +371,52 @@ public class TimeUnitTest extends TestFmwk {
     
     public void TestFormatPeriodEn() {
         Object[][] fullData = {
+                {_1m_59_9996s, "1 minute, 59.9996 seconds"},
                 {_19m, "19 minutes"},
-                {_1h_23_5s, "1 hour and 23.5 seconds"},
-                {_1h_0m_23s, "1 hour, 0 minutes, and 23 seconds"},
-                {_2y_5M_3w_4d, "2 years, 5 months, 3 weeks, and 4 days"}};
+                {_1h_23_5s, "1 hour, 23.5 seconds"},
+                {_1h_23_5m, "1 hour, 23.5 minutes"},
+                {_1h_0m_23s, "1 hour, 0 minutes, 23 seconds"},
+                {_2y_5M_3w_4d, "2 years, 5 months, 3 weeks, 4 days"}};
         Object[][] abbrevData = {
+                {_1m_59_9996s, "1 min, 59.9996 secs"},
                 {_19m, "19 mins"},
-                {_1h_23_5s, "1 hr and 23.5 secs"},
-                {_1h_0m_23s, "1 hr, 0 mins, and 23 secs"},
-                {_2y_5M_3w_4d, "2 yrs, 5 mths, 3 wks, and 4 days"}};
+                {_1h_23_5s, "1 hr, 23.5 secs"},
+                {_1h_23_5m, "1 hr, 23.5 mins"},
+                {_1h_0m_23s, "1 hr, 0 mins, 23 secs"},
+                {_2y_5M_3w_4d, "2 yrs, 5 mths, 3 wks, 4 days"}};
         Object[][] numericData = {
+                {_1m_59_9996s, "1:59.9996"},
                 {_19m, "19 mins"},
                 {_1h_23_5s, "1:00:23.5"},
                 {_1h_0m_23s, "1:00:23"},
+                {_1h_23_5m, "1:23.5"},
                 {_5h_17m, "5:17"},
                 {_19m_28s, "19:28"},
-                {_2y_5M_3w_4d, "2 yrs, 5 mths, 3 wks, and 4 days"}};
+                {_2y_5M_3w_4d, "2 yrs, 5 mths, 3 wks, 4 days"},
+                {_0h_0m_17s, "0:00:17"},
+                {_6h_56_92m, "6:56.92"}};
         TimeUnitFormat tuf = new TimeUnitFormat(ULocale.ENGLISH, TimeUnitFormat.FULL_NAME);
+        NumberFormat nf = NumberFormat.getNumberInstance(ULocale.ENGLISH);
+        nf.setMaximumFractionDigits(4);
+        tuf.setNumberFormat(nf);
         verifyFormatPeriod("en FULL", tuf, fullData);
         tuf = new TimeUnitFormat(ULocale.ENGLISH, TimeUnitFormat.ABBREVIATED_NAME);
+        tuf.setNumberFormat(nf);
         verifyFormatPeriod("en ABBREV", tuf, abbrevData);       
         tuf = new TimeUnitFormat(ULocale.ENGLISH, TimeUnitFormat.NUMERIC);
+        tuf.setNumberFormat(nf);
         verifyFormatPeriod("en NUMERIC", tuf, numericData);
     }
     
+    public void TestTimePeriodLength() {
+        assertEquals("length", 2, new TimePeriod(
+                new TimeUnitAmount(3.0, TimeUnit.HOUR),
+                new TimeUnitAmount(5.0, TimeUnit.MINUTE)).length()); 
+    }
+    
     public void TestTimePeriodForAmounts() {
         try {
-            TimePeriod.forAmounts(
+            new TimePeriod(
                     new TimeUnitAmount(3.0, TimeUnit.HOUR),
                     new TimeUnitAmount(5.0, TimeUnit.HOUR));
             errln("Expected IllegalArgumentException on duplicate TimeUnits.");
@@ -392,13 +424,13 @@ public class TimeUnitTest extends TestFmwk {
             // expected
         }
         try {
-            TimePeriod.forAmounts();
+            new TimePeriod();
             errln("Expected IllegalArgumentException on missing TimeUnitAmounts.");
         } catch (IllegalArgumentException e) {
             // expected
         }
         try {
-            TimePeriod.forAmounts(
+            new TimePeriod(
                     new TimeUnitAmount(3.5, TimeUnit.HOUR),
                     new TimeUnitAmount(5.0, TimeUnit.MINUTE));
             errln("Expected IllegalArgumentException. Only smallest time unit can have a fractional amount.");
@@ -408,12 +440,12 @@ public class TimeUnitTest extends TestFmwk {
     }
     
     public void TestTimePeriodEqualsHashCode() {
-        TimePeriod our_19m_28s = TimePeriod.forAmounts(
+        TimePeriod our_19m_28s = new TimePeriod(
                 new TimeUnitAmount(28.0, TimeUnit.SECOND),
                 new TimeUnitAmount(19.0, TimeUnit.MINUTE));
         assertEquals("TimePeriod equals", _19m_28s, our_19m_28s);
         assertEquals("Hash code", _19m_28s.hashCode(), our_19m_28s.hashCode());
-        TimePeriod our_19m_29s = TimePeriod.forAmounts(
+        TimePeriod our_19m_29s = new TimePeriod(
                 new TimeUnitAmount(29.0, TimeUnit.SECOND),
                 new TimeUnitAmount(19.0, TimeUnit.MINUTE));
         assertNotEquals("TimePeriod not equals", _19m_28s, our_19m_29s);
@@ -427,7 +459,7 @@ public class TimeUnitTest extends TestFmwk {
         StringBuilder builder = new StringBuilder();
         boolean failure = false;
         for (Object[] testCase : testData) {
-            String actual = tuf.formatTimePeriod((TimePeriod) testCase[0]);
+            String actual = tuf.format(testCase[0]);
             if (!testCase[1].equals(actual)) {
                 builder.append(String.format("%s: Expected: '%s', got: '%s'\n", desc, testCase[1], actual));
                 failure = true;