ICU-21284 More MeasureFormatTest and NumberFormatterApiTest test cases

See #1530
diff --git a/icu4c/source/i18n/unicode/measfmt.h b/icu4c/source/i18n/unicode/measfmt.h
index 2155ad5..b3fe6f5 100644
--- a/icu4c/source/i18n/unicode/measfmt.h
+++ b/icu4c/source/i18n/unicode/measfmt.h
@@ -91,7 +91,8 @@ class DateFormat;
 /**
  * <p><strong>IMPORTANT:</strong> New users are strongly encouraged to see if
  * numberformatter.h fits their use case.  Although not deprecated, this header
- * is provided for backwards compatibility only.
+ * is provided for backwards compatibility only, and has much more limited
+ * capabilities.
  *
  * @see Format
  * @author Alan Liu
diff --git a/icu4c/source/i18n/units_complexconverter.cpp b/icu4c/source/i18n/units_complexconverter.cpp
index 74d9c67..2aa331a 100644
--- a/icu4c/source/i18n/units_complexconverter.cpp
+++ b/icu4c/source/i18n/units_complexconverter.cpp
@@ -10,6 +10,7 @@
 #include "cmemory.h"
 #include "number_decimalquantity.h"
 #include "number_roundingutils.h"
+#include "putilimp.h"
 #include "uarrsort.h"
 #include "uassert.h"
 #include "unicode/fmtable.h"
@@ -149,6 +150,12 @@ MaybeStackVector<Measure> ComplexUnitsConverter::convert(double quantity,
             // If quantity is at the limits of double's precision from an
             // integer value, we take that integer value.
             int64_t flooredQuantity = floor(quantity * (1 + DBL_EPSILON));
+            if (uprv_isNaN(quantity)) {
+                // With clang on Linux: floor does not support NaN, resulting in
+                // a giant negative number. For now, we produce "0 feet, NaN
+                // inches". TODO(icu-units#131): revisit desired output.
+                flooredQuantity = 0;
+            }
             intValues[i] = flooredQuantity;
 
             // Keep the residual of the quantity.
diff --git a/icu4c/source/test/intltest/measfmttest.cpp b/icu4c/source/test/intltest/measfmttest.cpp
index 23ac5cc..3af2e1e 100644
--- a/icu4c/source/test/intltest/measfmttest.cpp
+++ b/icu4c/source/test/intltest/measfmttest.cpp
@@ -19,6 +19,7 @@
 
 #include "charstr.h"
 #include "cstr.h"
+#include "cstring.h"
 #include "measunit_impl.h"
 #include "unicode/decimfmt.h"
 #include "unicode/measfmt.h"
@@ -85,6 +86,7 @@ class MeasureFormatTest : public IntlTest {
     void TestInvalidIdentifiers();
     void TestIdentifierDetails();
     void TestPrefixes();
+    void TestParseBuiltIns();
     void TestParseToBuiltIn();
     void TestKilogramIdentifier();
     void TestCompoundUnitOperations();
@@ -216,6 +218,7 @@ void MeasureFormatTest::runIndexedTest(
     TESTCASE_AUTO(TestInvalidIdentifiers);
     TESTCASE_AUTO(TestIdentifierDetails);
     TESTCASE_AUTO(TestPrefixes);
+    TESTCASE_AUTO(TestParseBuiltIns);
     TESTCASE_AUTO(TestParseToBuiltIn);
     TESTCASE_AUTO(TestKilogramIdentifier);
     TESTCASE_AUTO(TestCompoundUnitOperations);
@@ -3666,8 +3669,16 @@ void MeasureFormatTest::TestIdentifiers() {
         {"pow2-foot-and-pow2-mile", "square-foot-and-square-mile"},
         {"gram-square-gram-per-dekagram", "cubic-gram-per-dekagram"},
         {"kilogram-per-meter-per-second", "kilogram-per-meter-second"},
+        {"kilometer-per-second-per-megaparsec", "kilometer-per-megaparsec-second"},
 
         // TODO(ICU-21284): Add more test cases once the proper ranking is available.
+        // TODO(ICU-21284,icu-units#70): These cases are the wrong way around:
+        {"pound-force-foot", "foot-pound-force"},
+        {"foot-pound-force", "foot-pound-force"},
+        {"kilowatt-hour", "hour-kilowatt"},
+        {"hour-kilowatt", "hour-kilowatt"},
+        {"newton-meter", "meter-newton"},
+        {"meter-newton", "meter-newton"},
 
         // Testing prefixes are parsed and produced correctly (ensures no
         // collisions in the enum values)
@@ -3706,7 +3717,6 @@ void MeasureFormatTest::TestIdentifiers() {
         // TODO(icu-units#70): revisit when fixing normalization. For now we're
         // just checking some consistency between C&J.
         {"megafoot-mebifoot-kibifoot-kilofoot", "kibifoot-mebifoot-kilofoot-megafoot"},
-
     };
     for (const auto &cas : cases) {
         status.setScope(cas.id);
@@ -3836,6 +3846,42 @@ void MeasureFormatTest::TestPrefixes() {
     }
 }
 
+void MeasureFormatTest::TestParseBuiltIns() {
+    IcuTestErrorCode status(*this, "TestParseBuiltIns()");
+    int32_t totalCount = MeasureUnit::getAvailable(nullptr, 0, status);
+    status.expectErrorAndReset(U_BUFFER_OVERFLOW_ERROR);
+    std::unique_ptr<MeasureUnit[]> units(new MeasureUnit[totalCount]);
+    totalCount = MeasureUnit::getAvailable(units.get(), totalCount, status);
+    status.assertSuccess();
+    for (int32_t i = 0; i < totalCount; i++) {
+        MeasureUnit &unit = units[i];
+        if (uprv_strcmp(unit.getType(), "currency") == 0) {
+            continue;
+        }
+
+        // TODO(ICU-21284,icu-units#70): fix normalization. Until then, ignore:
+        if (uprv_strcmp(unit.getIdentifier(), "pound-force-foot") == 0) continue;
+        if (uprv_strcmp(unit.getIdentifier(), "kilowatt-hour") == 0) continue;
+        if (uprv_strcmp(unit.getIdentifier(), "newton-meter") == 0) continue;
+
+        // Prove that all built-in units are parseable, except "generic" temperature:
+        MeasureUnit parsed = MeasureUnit::forIdentifier(unit.getIdentifier(), status);
+        if (unit == MeasureUnit::getGenericTemperature()) {
+            status.expectErrorAndReset(U_ILLEGAL_ARGUMENT_ERROR);
+        } else {
+            status.assertSuccess();
+            CharString msg;
+            msg.append("parsed MeasureUnit '", status);
+            msg.append(parsed.getIdentifier(), status);
+            msg.append("' should equal built-in '", status);
+            msg.append(unit.getIdentifier(), status);
+            msg.append("'", status);
+            status.assertSuccess();
+            assertTrue(msg.data(), unit == parsed);
+        }
+    }
+}
+
 void MeasureFormatTest::TestParseToBuiltIn() {
     IcuTestErrorCode status(*this, "TestParseToBuiltIn()");
     const struct TestCase {
diff --git a/icu4c/source/test/intltest/numbertest_api.cpp b/icu4c/source/test/intltest/numbertest_api.cpp
index e06ab57..1208cfc 100644
--- a/icu4c/source/test/intltest/numbertest_api.cpp
+++ b/icu4c/source/test/intltest/numbertest_api.cpp
@@ -715,6 +715,16 @@ void NumberFormatterApiTest::unitMeasure() {
             5,
             u"5 a\u00F1os");
 
+    // TODO(ICU-20941): arbitrary unit formatting
+//     assertFormatSingle(
+//             u"Hubble Constant",
+//             u"unit/kilometer-per-megaparsec-second",
+//             u"unit/kilometer-per-megaparsec-second",
+//             NumberFormatter::with().unit(MeasureUnit::forIdentifier("kilometer-per-megaparsec-second", status)),
+//             Locale("en"),
+//             74, // Approximate 2019-03-18 measurement
+//             u"74 km/s.Mpc");
+
     assertFormatSingle(
             u"Mixed unit",
             u"unit/yard-and-foot-and-inch",
@@ -849,7 +859,7 @@ void NumberFormatterApiTest::unitMeasure() {
             NumberFormatter::with().unit(MeasureUnit::forIdentifier("celsius", status)),
             Locale("nl-NL"),
             -6.5,
-            u"-6,5\u00B0C");
+            u"-6,5°C");
 
     assertFormatSingle(
             u"Negative numbers: time",
@@ -868,6 +878,39 @@ void NumberFormatterApiTest::unitMeasure() {
             Locale("en"),
             100,
             u"100");
+
+    // TODO: desired behaviour for this "pathological" case?
+    // Since this is pointless, we don't test that its behaviour doesn't change.
+    // As of January 2021, the produced result has a missing sign: 23.5 Kelvin
+    // is "23 Kelvin and -272.65 degrees Celsius":
+//     assertFormatSingle(
+//             u"Meaningless: kelvin-and-celcius",
+//             u"unit/kelvin-and-celsius",
+//             u"unit/kelvin-and-celsius",
+//             NumberFormatter::with().unit(MeasureUnit::forIdentifier("kelvin-and-celsius", status)),
+//             Locale("en"),
+//             23.5,
+//             u"23 K, 272.65°C");
+
+    if (uprv_getNaN() != 0.0) {
+        assertFormatSingle(
+                u"Measured -Inf",
+                u"measure-unit/electric-ampere",
+                u"unit/ampere",
+                NumberFormatter::with().unit(MeasureUnit::getAmpere()),
+                Locale("en"),
+                -uprv_getInfinity(),
+                u"-∞ A");
+
+        assertFormatSingle(
+                u"Measured NaN",
+                u"measure-unit/temperature-celsius",
+                u"unit/celsius",
+                NumberFormatter::with().unit(MeasureUnit::forIdentifier("celsius", status)),
+                Locale("en"),
+                uprv_getNaN(),
+                u"NaN°C");
+    }
 }
 
 void NumberFormatterApiTest::unitCompoundMeasure() {
@@ -1434,6 +1477,26 @@ void NumberFormatterApiTest::unitUsage() {
             u"0E0 square centimetres");
 
     assertFormatSingle(
+            u"Negative Infinity with Unit Preferences",
+            u"measure-unit/area-acre usage/default",
+            u"unit/acre usage/default",
+            NumberFormatter::with().unit(MeasureUnit::getAcre()).usage("default"),
+            Locale::getEnglish(),
+            -uprv_getInfinity(),
+            u"-∞ km²");
+
+//     // TODO(icu-units#131): do we care about NaN?
+//     // TODO: on some platforms with MSVC, "-NaN sec" is returned.
+//     assertFormatSingle(
+//             u"NaN with Unit Preferences",
+//             u"measure-unit/area-acre usage/default",
+//             u"unit/acre usage/default",
+//             NumberFormatter::with().unit(MeasureUnit::getAcre()).usage("default"),
+//             Locale::getEnglish(),
+//             uprv_getNaN(),
+//             u"NaN cm²");
+
+    assertFormatSingle(
             u"Negative numbers: minute-and-second",
             u"measure-unit/duration-second usage/media",
             u"unit/second usage/media",
@@ -1443,6 +1506,34 @@ void NumberFormatterApiTest::unitUsage() {
             u"-1 min, 18 sec");
 
     assertFormatSingle(
+            u"Negative numbers: media seconds",
+            u"measure-unit/duration-second usage/media",
+            u"unit/second usage/media",
+            NumberFormatter::with().unit(SECOND).usage("media"),
+            Locale("nl-NL"),
+            -2.7,
+            u"-2,7 sec");
+
+//     // TODO: on some platforms with MSVC, "-NaN sec" is returned.
+//     assertFormatSingle(
+//             u"NaN minute-and-second",
+//             u"measure-unit/duration-second usage/media",
+//             u"unit/second usage/media",
+//             NumberFormatter::with().unit(SECOND).usage("media"),
+//             Locale("nl-NL"),
+//             uprv_getNaN(),
+//             u"NaN sec");
+
+    assertFormatSingle(
+            u"NaN meter-and-centimeter",
+            u"measure-unit/length-meter usage/person-height",
+            u"unit/meter usage/person-height",
+            NumberFormatter::with().unit(METER).usage("person-height"),
+            Locale("de-DE"),
+            uprv_getNaN(),
+            u"0 m, NaN cm");
+
+    assertFormatSingle(
             u"Rounding Mode propagates: rounding down",
             u"usage/road measure-unit/length-centimeter rounding-mode-floor",
             u"usage/road unit/centimeter rounding-mode-floor",
@@ -1490,6 +1581,13 @@ void NumberFormatterApiTest::unitUsageErrorCodes() {
     // Adding the unit as part of the fluent chain leads to success.
     unloc_formatter.unit(MeasureUnit::getMeter()).locale("en-GB").formatInt(1, status);
     status.assertSuccess();
+
+    // Setting unit to the "base dimensionless unit" is like clearing unit.
+    unloc_formatter = NumberFormatter::with().unit(MeasureUnit()).usage("default");
+    // This does not give an error, because usage-vs-unit isn't resolved yet.
+    status.errIfFailureAndReset("Expected behaviour: no immediate error for invalid unit");
+    unloc_formatter.locale("en-GB").formatInt(1, status);
+    status.expectErrorAndReset(U_ILLEGAL_ARGUMENT_ERROR);
 }
 
 // Tests for the "skeletons" field in unitPreferenceData, as well as precision
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java
index 7998cf5..e0665f0 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java
@@ -62,7 +62,8 @@
  * <p>
  * <strong>IMPORTANT:</strong> New users are strongly encouraged to see if
  * {@link NumberFormatter} fits their use case.  Although not deprecated, this
- * class, MeasureFormat, is provided for backwards compatibility only.
+ * class, MeasureFormat, is provided for backwards compatibility only, and has
+ * much more limited capabilities.
  * <hr>
  *
  * <p>
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java b/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java
index e05400d..7391ac1 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java
@@ -695,7 +695,8 @@ public boolean equals(Object rhs) {
      */
     @Override
     public String toString() {
-        return type + "-" + subType;
+        String result = measureUnitImpl == null ? type + "-" + subType : measureUnitImpl.getIdentifier();
+        return result == null ? "" : result;
     }
 
     /**
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java
index 08a2885..9a0fdd1 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java
@@ -3494,8 +3494,16 @@ class TestCase {
             new TestCase("pow2-foot-and-pow2-mile", "square-foot-and-square-mile"),
             new TestCase("gram-square-gram-per-dekagram", "cubic-gram-per-dekagram"),
             new TestCase("kilogram-per-meter-per-second", "kilogram-per-meter-second"),
+            new TestCase("kilometer-per-second-per-megaparsec", "kilometer-per-megaparsec-second"),
 
             // TODO(ICU-21284): Add more test cases once the proper ranking is available.
+            // TODO(ICU-21284,icu-units#70): These cases are the wrong way around:
+            new TestCase("pound-force-foot", "foot-pound-force"),
+            new TestCase("foot-pound-force", "foot-pound-force"),
+            new TestCase("kilowatt-hour", "hour-kilowatt"),
+            new TestCase("hour-kilowatt", "hour-kilowatt"),
+            new TestCase("newton-meter", "meter-newton"),
+            new TestCase("meter-newton", "meter-newton"),
 
             // Testing prefixes are parsed and produced correctly (ensures no
             // collisions in the enum values)
@@ -3665,6 +3673,35 @@ class TestCase {
     }
 
     @Test
+    public void TestParseBuiltIns() {
+        for (MeasureUnit unit : MeasureUnit.getAvailable()) {
+            System.out.println("unit ident: " + unit.getIdentifier() + ", type: " + unit.getType());
+            if (unit.getType() == "currency") {
+                continue;
+            }
+
+            // TODO(ICU-21284,icu-units#70): fix normalization. Until then, ignore:
+            if (unit.getIdentifier() == "pound-force-foot") continue;
+            if (unit.getIdentifier() == "kilowatt-hour") continue;
+            if (unit.getIdentifier() == "newton-meter") continue;
+
+            // Prove that all built-in units are parseable, except "generic" temperature:
+            if (unit == MeasureUnit.GENERIC_TEMPERATURE) {
+                try {
+                    MeasureUnit.forIdentifier(unit.getIdentifier());
+                    Assert.fail("GENERIC_TEMPERATURE should not be parseable");
+                } catch (IllegalArgumentException e) {
+                    continue;
+                }
+            } else {
+                MeasureUnit parsed = MeasureUnit.forIdentifier(unit.getIdentifier());
+                assertTrue("parsed MeasureUnit '" + parsed + "'' should equal built-in '" + unit + "'",
+                           unit.equals(parsed));
+            }
+        }
+    }
+
+    @Test
     public void TestParseToBuiltIn() {
         class TestCase {
             final String identifier;
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java
index 35b62e9..51a8794 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java
@@ -673,6 +673,17 @@ public void unitMeasure() {
                 5,
                 "5 a\u00F1os");
 
+        // TODO(ICU-20941): arbitrary unit formatting
+        // assertFormatSingle(
+        //         "Hubble Constant",
+        //         "unit/kilometer-per-megaparsec-second",
+        //         "unit/kilometer-per-megaparsec-second",
+        //         NumberFormatter.with()
+        //                 .unit(MeasureUnit.forIdentifier("kilometer-per-megaparsec-second")),
+        //         new ULocale("en"),
+        //         74, // Approximate 2019-03-18 measurement
+        //         "74 km/s.Mpc");
+
         assertFormatSingle(
                 "Mixed unit",
                 "unit/yard-and-foot-and-inch",
@@ -813,7 +824,7 @@ public void unitMeasure() {
                 NumberFormatter.with().unit(MeasureUnit.forIdentifier("celsius")),
                 new ULocale("nl-NL"),
                 -6.5,
-                "-6,5\u00B0C");
+                "-6,5°C");
 
         assertFormatSingle(
                 "Negative numbers: time",
@@ -832,6 +843,37 @@ public void unitMeasure() {
                 new ULocale("en"),
                 100,
                 "100");
+
+        // TODO: desired behaviour for this "pathological" case?
+        // Since this is pointless, we don't test that its behaviour doesn't change.
+        // As of January 2021, the produced result has a missing sign: 23.5 Kelvin
+        // is "23 Kelvin and -272.65 degrees Celsius":
+        // assertFormatSingle(
+        //         "Meaningless: kelvin-and-celcius",
+        //         "unit/kelvin-and-celsius",
+        //         "unit/kelvin-and-celsius",
+        //         NumberFormatter.with().unit(MeasureUnit.forIdentifier("kelvin-and-celsius")),
+        //         new ULocale("en"),
+        //         23.5,
+        //         "23 K, 272.65°C");
+
+        assertFormatSingle(
+                "Measured -Inf",
+                "measure-unit/electric-ampere",
+                "unit/ampere",
+                NumberFormatter.with().unit(MeasureUnit.AMPERE),
+                new ULocale("en"),
+                Double.NEGATIVE_INFINITY,
+                "-∞ A");
+
+        assertFormatSingle(
+                "Measured NaN",
+                "measure-unit/temperature-celsius",
+                "unit/celsius",
+                NumberFormatter.with().unit(MeasureUnit.forIdentifier("celsius")),
+                new ULocale("en"),
+                Double.NaN,
+                "NaN°C");
     }
 
     @Test
@@ -1402,6 +1444,30 @@ public void unitUsage() {
                "8,765E0 square metres",
                "0E0 square centimetres");
 
+        // TODO(icu-units#132): Java BigDecimal does not support Inf and NaN, so
+        // we get a misleading "0" out of this:
+        assertFormatSingle(
+                "Negative Infinity with Unit Preferences",
+                "measure-unit/area-acre usage/default",
+                "unit/acre usage/default",
+                NumberFormatter.with().unit(MeasureUnit.ACRE).usage("default"),
+                ULocale.ENGLISH,
+                Double.NEGATIVE_INFINITY,
+                // "-∞ km²");
+                "0 cm²");
+
+        // TODO(icu-units#132): Java BigDecimal does not support Inf and NaN, so
+        // we get a misleading "0" out of this:
+        assertFormatSingle(
+                "NaN with Unit Preferences",
+                "measure-unit/area-acre usage/default",
+                "unit/acre usage/default",
+                NumberFormatter.with().unit(MeasureUnit.ACRE).usage("default"),
+                ULocale.ENGLISH,
+                Double.NaN,
+                // "NaN cm²");
+                "0 cm²");
+
         assertFormatSingle(
                 "Negative numbers: minute-and-second",
                 "measure-unit/duration-second usage/media",
@@ -1412,6 +1478,39 @@ public void unitUsage() {
                 "-1 min, 18 sec");
 
         assertFormatSingle(
+                "Negative numbers: media seconds",
+                "measure-unit/duration-second usage/media",
+                "unit/second usage/media",
+                NumberFormatter.with().unit(MeasureUnit.SECOND).usage("media"),
+                new ULocale("nl-NL"),
+                -2.7,
+                "-2,7 sec");
+
+        // TODO(icu-units#132): Java BigDecimal does not support Inf and NaN, so
+        // we get a misleading "0" out of this:
+        assertFormatSingle(
+                "NaN minute-and-second",
+                "measure-unit/duration-second usage/media",
+                "unit/second usage/media",
+                NumberFormatter.with().unit(MeasureUnit.SECOND).usage("media"),
+                new ULocale("nl-NL"),
+                Double.NaN,
+                // "NaN sec");
+                "0 sec");
+
+        // TODO(icu-units#132): Java BigDecimal does not support Inf and NaN, so
+        // we get a misleading "0" out of this:
+        assertFormatSingle(
+                "NaN meter-and-centimeter",
+                "measure-unit/length-meter usage/person-height",
+                "unit/meter usage/person-height",
+                NumberFormatter.with().unit(MeasureUnit.METER).usage("person-height"),
+                new ULocale("en-DE"),
+                Double.NaN,
+                // "0 m, NaN cm");
+                "0 m, 0 cm");
+
+        assertFormatSingle(
                 "Rounding Mode propagates: rounding down",
                 "usage/road measure-unit/length-centimeter rounding-mode-floor",
                 "usage/road unit/centimeter rounding-mode-floor",
@@ -1468,6 +1567,14 @@ public void unitUsageErrorCodes() {
         // Adding the unit as part of the fluent chain leads to success.
         unloc_formatter.unit(MeasureUnit.METER).locale(new ULocale("en-GB")).format(1); /* No Exception should be thrown */
 
+        // Setting unit to the "base dimensionless unit" is like clearing unit.
+        unloc_formatter = NumberFormatter.with().unit(NoUnit.BASE).usage("default");
+        try {
+            unloc_formatter.locale(new ULocale("en-GB")).format(1);
+            fail("should throw IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // Pass
+        }
     }