ICU-21266 Support toSkeleton() for all functional Unit Formatters

See #1347
diff --git a/icu4c/source/i18n/measunit.cpp b/icu4c/source/i18n/measunit.cpp
index 808bf90..02624f8 100644
--- a/icu4c/source/i18n/measunit.cpp
+++ b/icu4c/source/i18n/measunit.cpp
@@ -612,21 +612,7 @@
     "teaspoon"
 };
 
-// Must be sorted by first value and then second value.
-static int32_t unitPerUnitToSingleUnit[][4] = {
-        {374, 383, 12, 1},
-        {374, 389, 12, 2},
-        {379, 383, 12, 6},
-        {379, 389, 12, 7},
-        {390, 343, 19, 0},
-        {392, 350, 19, 2},
-        {394, 343, 19, 3},
-        {394, 472, 4, 2},
-        {394, 473, 4, 3},
-        {416, 465, 3, 1},
-        {419, 12, 18, 9},
-        {476, 390, 4, 1}
-};
+// unitPerUnitToSingleUnit no longer in use! TODO: remove from code-generation code.
 
 // Shortcuts to the base unit in order to make the default constructor fast
 static const int32_t kBaseTypeIdx = 16;
@@ -2301,41 +2287,6 @@
     return false;
 }
 
-MeasureUnit MeasureUnit::resolveUnitPerUnit(
-        const MeasureUnit &unit, const MeasureUnit &perUnit, bool* isResolved) {
-    int32_t unitOffset = unit.getOffset();
-    int32_t perUnitOffset = perUnit.getOffset();
-    if (unitOffset == -1 || perUnitOffset == -1) {
-        *isResolved = false;
-        return MeasureUnit();
-    }
-
-    // binary search for (unitOffset, perUnitOffset)
-    int32_t start = 0;
-    int32_t end = UPRV_LENGTHOF(unitPerUnitToSingleUnit);
-    while (start < end) {
-        int32_t mid = (start + end) / 2;
-        int32_t *midRow = unitPerUnitToSingleUnit[mid];
-        if (unitOffset < midRow[0]) {
-            end = mid;
-        } else if (unitOffset > midRow[0]) {
-            start = mid + 1;
-        } else if (perUnitOffset < midRow[1]) {
-            end = mid;
-        } else if (perUnitOffset > midRow[1]) {
-            start = mid + 1;
-        } else {
-            // We found a resolution for our unit / per-unit combo
-            // return it.
-            *isResolved = true;
-            return MeasureUnit(midRow[2], midRow[3]);
-        }
-    }
-
-    *isResolved = false;
-    return MeasureUnit();
-}
-
 MeasureUnit *MeasureUnit::create(int typeId, int subTypeId, UErrorCode &status) {
     if (U_FAILURE(status)) {
         return NULL;
diff --git a/icu4c/source/i18n/number_longnames.cpp b/icu4c/source/i18n/number_longnames.cpp
index 8b33992..3891d53 100644
--- a/icu4c/source/i18n/number_longnames.cpp
+++ b/icu4c/source/i18n/number_longnames.cpp
@@ -223,24 +223,16 @@
                                      LongNameHandler *fillIn, UErrorCode &status) {
     // Not valid for mixed units that aren't built-in units, and there should
     // not be any built-in mixed units!
-    U_ASSERT(uprv_strlen(unitRef.getType()) > 0 || unitRef.getComplexity(status) != UMEASURE_UNIT_MIXED);
+    U_ASSERT(uprv_strcmp(unitRef.getType(), "") != 0 ||
+             unitRef.getComplexity(status) != UMEASURE_UNIT_MIXED);
     U_ASSERT(fillIn != nullptr);
-    if (uprv_strlen(unitRef.getType()) == 0 || uprv_strlen(perUnit.getType()) == 0) {
-        // TODO(ICU-20941): Unsanctioned unit. Not yet fully supported. Set an
-        // error code. Once we support not-built-in units here, unitRef may be
-        // anything, but if not built-in, perUnit has to be "none".
-        status = U_UNSUPPORTED_ERROR;
-        return;
-    }
 
     MeasureUnit unit = unitRef;
     if (uprv_strcmp(perUnit.getType(), "none") != 0) {
-        // Compound unit: first try to simplify (e.g. "meter per second" is a
-        // built-in unit).
-        bool isResolved = false;
-        MeasureUnit resolved = MeasureUnit::resolveUnitPerUnit(unit, perUnit, &isResolved);
-        if (isResolved) {
-            unit = resolved;
+        // Compound unit: first try to simplify (e.g., meters per second is its own unit).
+        MeasureUnit simplified = unit.product(perUnit.reciprocal(status), status);
+        if (uprv_strcmp(simplified.getType(), "") != 0) {
+            unit = simplified;
         } else {
             // No simplified form is available.
             forCompoundUnit(loc, unit, perUnit, width, rules, parent, fillIn, status);
@@ -248,6 +240,14 @@
         }
     }
 
+    if (uprv_strcmp(unit.getType(), "") == 0) {
+        // TODO(ICU-20941): Unsanctioned unit. Not yet fully supported. Set an
+        // error code. Once we support not-built-in units here, unitRef may be
+        // anything, but if not built-in, perUnit has to be "none".
+        status = U_UNSUPPORTED_ERROR;
+        return;
+    }
+
     UnicodeString simpleFormats[ARRAY_LENGTH];
     getMeasureData(loc, unit, width, simpleFormats, status);
     if (U_FAILURE(status)) {
@@ -263,6 +263,13 @@
                                       const MeasureUnit &perUnit, const UNumberUnitWidth &width,
                                       const PluralRules *rules, const MicroPropsGenerator *parent,
                                       LongNameHandler *fillIn, UErrorCode &status) {
+    if (uprv_strcmp(unit.getType(), "") == 0 || uprv_strcmp(perUnit.getType(), "") == 0) {
+        // TODO(ICU-20941): Unsanctioned unit. Not yet fully supported. Set an
+        // error code. Once we support not-built-in units here, unitRef may be
+        // anything, but if not built-in, perUnit has to be "none".
+        status = U_UNSUPPORTED_ERROR;
+        return;
+    }
     if (fillIn == nullptr) {
         status = U_INTERNAL_PROGRAM_ERROR;
         return;
diff --git a/icu4c/source/i18n/number_skeletons.cpp b/icu4c/source/i18n/number_skeletons.cpp
index fd7d7ca..e6d94d2 100644
--- a/icu4c/source/i18n/number_skeletons.cpp
+++ b/icu4c/source/i18n/number_skeletons.cpp
@@ -843,10 +843,6 @@
         sb.append(u' ');
     }
     if (U_FAILURE(status)) { return; }
-    if (GeneratorHelpers::perUnit(macros, sb, status)) {
-        sb.append(u' ');
-    }
-    if (U_FAILURE(status)) { return; }
     if (GeneratorHelpers::usage(macros, sb, status)) {
         sb.append(u' ');
     }
@@ -1025,14 +1021,6 @@
     status = U_NUMBER_SKELETON_SYNTAX_ERROR;
 }
 
-void blueprint_helpers::generateMeasureUnitOption(const MeasureUnit& measureUnit, UnicodeString& sb,
-                                                  UErrorCode&) {
-    // Need to do char <-> UChar conversion...
-    sb.append(UnicodeString(measureUnit.getType(), -1, US_INV));
-    sb.append(u'-');
-    sb.append(UnicodeString(measureUnit.getSubtype(), -1, US_INV));
-}
-
 void blueprint_helpers::parseMeasurePerUnitOption(const StringSegment& segment, MacroProps& macros,
                                                   UErrorCode& status) {
     // A little bit of a hack: save the current unit (numerator), call the main measure unit
@@ -1059,15 +1047,21 @@
         return;
     }
 
-    // Mixed units can only be represented by a full MeasureUnit instances, so
-    // we ignore macros.perUnit.
+    // Mixed units can only be represented by full MeasureUnit instances, so we
+    // don't split the denominator into macros.perUnit.
     if (fullUnit.complexity == UMEASURE_UNIT_MIXED) {
         macros.unit = std::move(fullUnit).build(status);
         return;
     }
 
-    // TODO(ICU-20941): Clean this up (see also
-    // https://github.com/icu-units/icu/issues/35).
+    // When we have a built-in unit (e.g. meter-per-second), we don't split it up
+    MeasureUnit testBuiltin = fullUnit.copy(status).build(status);
+    if (uprv_strcmp(testBuiltin.getType(), "") != 0) {
+        macros.unit = std::move(testBuiltin);
+        return;
+    }
+
+    // TODO(ICU-20941): Clean this up.
     for (int32_t i = 0; i < fullUnit.units.length(); i++) {
         SingleUnitImpl* subUnit = fullUnit.units[i];
         if (subUnit->dimensionality > 0) {
@@ -1523,28 +1517,17 @@
     } else if (utils::unitIsPermille(macros.unit)) {
         sb.append(u"permille", -1);
         return true;
-    } else if (uprv_strcmp(macros.unit.getType(), "") != 0) {
-        sb.append(u"measure-unit/", -1);
-        blueprint_helpers::generateMeasureUnitOption(macros.unit, sb, status);
-        return true;
     } else {
-        // TODO(icu-units#35): add support for not-built-in units.
-        status = U_UNSUPPORTED_ERROR;
-        return false;
-    }
-}
-
-bool GeneratorHelpers::perUnit(const MacroProps& macros, UnicodeString& sb, UErrorCode& status) {
-    // Per-units are currently expected to be only MeasureUnits.
-    if (utils::unitIsBaseUnit(macros.perUnit)) {
-        // Default value: ok to ignore
-        return false;
-    } else if (utils::unitIsCurrency(macros.perUnit)) {
-        status = U_UNSUPPORTED_ERROR;
-        return false;
-    } else {
-        sb.append(u"per-measure-unit/", -1);
-        blueprint_helpers::generateMeasureUnitOption(macros.perUnit, sb, status);
+        MeasureUnit unit = macros.unit;
+        if (utils::unitIsCurrency(macros.perUnit)) {
+            status = U_UNSUPPORTED_ERROR;
+            return false;
+        }
+        if (!utils::unitIsBaseUnit(macros.perUnit)) {
+            unit = unit.product(macros.perUnit.reciprocal(status), status);
+        }
+        sb.append(u"unit/", -1);
+        sb.append(unit.getIdentifier());
         return true;
     }
 }
diff --git a/icu4c/source/i18n/number_skeletons.h b/icu4c/source/i18n/number_skeletons.h
index 38dfa19..201267e 100644
--- a/icu4c/source/i18n/number_skeletons.h
+++ b/icu4c/source/i18n/number_skeletons.h
@@ -240,10 +240,10 @@
 
 void generateCurrencyOption(const CurrencyUnit& currency, UnicodeString& sb, UErrorCode& status);
 
+// "measure-unit/" is deprecated in favour of "unit/".
 void parseMeasureUnitOption(const StringSegment& segment, MacroProps& macros, UErrorCode& status);
 
-void generateMeasureUnitOption(const MeasureUnit& measureUnit, UnicodeString& sb, UErrorCode& status);
-
+// "per-measure-unit/" is deprecated in favour of "unit/".
 void parseMeasurePerUnitOption(const StringSegment& segment, MacroProps& macros, UErrorCode& status);
 
 /**
@@ -314,8 +314,6 @@
 
     static bool unit(const MacroProps& macros, UnicodeString& sb, UErrorCode& status);
 
-    static bool perUnit(const MacroProps& macros, UnicodeString& sb, UErrorCode& status);
-
     static bool usage(const MacroProps& macros, UnicodeString& sb, UErrorCode& status);
 
     static bool precision(const MacroProps& macros, UnicodeString& sb, UErrorCode& status);
diff --git a/icu4c/source/i18n/unicode/measunit.h b/icu4c/source/i18n/unicode/measunit.h
index f38c400..d86bab3 100644
--- a/icu4c/source/i18n/unicode/measunit.h
+++ b/icu4c/source/i18n/unicode/measunit.h
@@ -541,13 +541,6 @@
      * @internal
      */
     int32_t getOffset() const;
-
-    /**
-     * ICU use only.
-     * @internal
-     */
-    static MeasureUnit resolveUnitPerUnit(
-            const MeasureUnit &unit, const MeasureUnit &perUnit, bool* isResolved);
 #endif /* U_HIDE_INTERNAL_API */
 
 // All code between the "Start generated createXXX methods" comment and
diff --git a/icu4c/source/test/intltest/measfmttest.cpp b/icu4c/source/test/intltest/measfmttest.cpp
index 8690b54..deada9a 100644
--- a/icu4c/source/test/intltest/measfmttest.cpp
+++ b/icu4c/source/test/intltest/measfmttest.cpp
@@ -3494,7 +3494,7 @@
     UErrorCode status = U_ZERO_ERROR;
     Locale en("en");
     MeasureFormat fmt("en", UMEASFMT_WIDTH_SHORT, status);
-    Measure measure(50.0, MeasureUnit::createPound(status), status);
+    Measure measure(50.0, MeasureUnit::createPoundForce(status), status);
     LocalPointer<MeasureUnit> sqInch(MeasureUnit::createSquareInch(status));
     if (!assertSuccess("Create of format unit and per unit", status)) {
         return;
diff --git a/icu4c/source/test/intltest/numbertest.h b/icu4c/source/test/intltest/numbertest.h
index 802b8bb..7603263 100644
--- a/icu4c/source/test/intltest/numbertest.h
+++ b/icu4c/source/test/intltest/numbertest.h
@@ -54,6 +54,7 @@
     void notationCompact();
     void unitMeasure();
     void unitCompoundMeasure();
+    void unitSkeletons();
     void unitUsage();
     void unitUsageErrorCodes();
     void unitUsageSkeletons();
@@ -104,12 +105,15 @@
     CurrencyUnit CNY;
 
     MeasureUnit METER;
+    MeasureUnit METER_PER_SECOND;
     MeasureUnit DAY;
     MeasureUnit SQUARE_METER;
     MeasureUnit FAHRENHEIT;
     MeasureUnit SECOND;
     MeasureUnit POUND;
+    MeasureUnit POUND_FORCE;
     MeasureUnit SQUARE_MILE;
+    MeasureUnit SQUARE_INCH;
     MeasureUnit JOULE;
     MeasureUnit FURLONG;
     MeasureUnit KELVIN;
diff --git a/icu4c/source/test/intltest/numbertest_api.cpp b/icu4c/source/test/intltest/numbertest_api.cpp
index 43323fb..4580564 100644
--- a/icu4c/source/test/intltest/numbertest_api.cpp
+++ b/icu4c/source/test/intltest/numbertest_api.cpp
@@ -53,12 +53,15 @@
     }
     METER = *unit;
 
+    METER_PER_SECOND = *LocalPointer<MeasureUnit>(MeasureUnit::createMeterPerSecond(status));
     DAY = *LocalPointer<MeasureUnit>(MeasureUnit::createDay(status));
     SQUARE_METER = *LocalPointer<MeasureUnit>(MeasureUnit::createSquareMeter(status));
     FAHRENHEIT = *LocalPointer<MeasureUnit>(MeasureUnit::createFahrenheit(status));
     SECOND = *LocalPointer<MeasureUnit>(MeasureUnit::createSecond(status));
     POUND = *LocalPointer<MeasureUnit>(MeasureUnit::createPound(status));
+    POUND_FORCE = *LocalPointer<MeasureUnit>(MeasureUnit::createPoundForce(status));
     SQUARE_MILE = *LocalPointer<MeasureUnit>(MeasureUnit::createSquareMile(status));
+    SQUARE_INCH = *LocalPointer<MeasureUnit>(MeasureUnit::createSquareInch(status));
     JOULE = *LocalPointer<MeasureUnit>(MeasureUnit::createJoule(status));
     FURLONG = *LocalPointer<MeasureUnit>(MeasureUnit::createFurlong(status));
     KELVIN = *LocalPointer<MeasureUnit>(MeasureUnit::createKelvin(status));
@@ -77,6 +80,7 @@
         TESTCASE_AUTO(notationCompact);
         TESTCASE_AUTO(unitMeasure);
         TESTCASE_AUTO(unitCompoundMeasure);
+        TESTCASE_AUTO(unitSkeletons);
         TESTCASE_AUTO(unitUsage);
         TESTCASE_AUTO(unitUsageErrorCodes);
         TESTCASE_AUTO(unitUsageSkeletons);
@@ -578,6 +582,23 @@
             u"0.0088 meters",
             u"0 meters");
 
+//     // TODO(ICU-20941): Support formatting for not-built-in units
+//     assertFormatDescending(
+//             u"Hectometers",
+//             u"measure-unit/length-hectometer",
+//             u"unit/hectometer",
+//             NumberFormatter::with().unit(MeasureUnit::forIdentifier("hectometer", status)),
+//             Locale::getEnglish(),
+//             u"87,650 hm",
+//             u"8,765 hm",
+//             u"876.5 hm",
+//             u"87.65 hm",
+//             u"8.765 hm",
+//             u"0.8765 hm",
+//             u"0.08765 hm",
+//             u"0.008765 hm",
+//             u"0 hm");
+
 //    TODO: Implement Measure in C++
 //    assertFormatSingleMeasure(
 //            u"Meters with Measure Input",
@@ -694,10 +715,9 @@
             5,
             u"5 a\u00F1os");
 
-    // TODO(icu-units#35): skeleton generation.
     assertFormatSingle(
             u"Mixed unit",
-            nullptr,
+            u"unit/yard-and-foot-and-inch",
             u"unit/yard-and-foot-and-inch",
             NumberFormatter::with()
                 .unit(MeasureUnit::forIdentifier("yard-and-foot-and-inch", status)),
@@ -705,10 +725,9 @@
             3.65,
             "3 yd, 1 ft, 11.4 in");
 
-    // TODO(icu-units#35): skeleton generation.
     assertFormatSingle(
             u"Mixed unit, Scientific",
-            nullptr,
+            u"unit/yard-and-foot-and-inch E0",
             u"unit/yard-and-foot-and-inch E0",
             NumberFormatter::with()
                 .unit(MeasureUnit::forIdentifier("yard-and-foot-and-inch", status))
@@ -717,10 +736,9 @@
             3.65,
             "3 yd, 1 ft, 1.14E1 in");
 
-    // TODO(icu-units#35): skeleton generation.
     assertFormatSingle(
             u"Mixed Unit (Narrow Version)",
-            nullptr,
+            u"unit/metric-ton-and-kilogram-and-gram unit-width-narrow",
             u"unit/metric-ton-and-kilogram-and-gram unit-width-narrow",
             NumberFormatter::with()
                 .unit(MeasureUnit::forIdentifier("metric-ton-and-kilogram-and-gram", status))
@@ -729,10 +747,9 @@
             4.28571,
             u"4t 285kg 710g");
 
-    // TODO(icu-units#35): skeleton generation.
     assertFormatSingle(
             u"Mixed Unit (Short Version)",
-            nullptr,
+            u"unit/metric-ton-and-kilogram-and-gram unit-width-short",
             u"unit/metric-ton-and-kilogram-and-gram unit-width-short",
             NumberFormatter::with()
                 .unit(MeasureUnit::forIdentifier("metric-ton-and-kilogram-and-gram", status))
@@ -741,10 +758,9 @@
             4.28571,
             u"4 t, 285 kg, 710 g");
 
-    // TODO(icu-units#35): skeleton generation.
     assertFormatSingle(
             u"Mixed Unit (Full Name Version)",
-            nullptr,
+            u"unit/metric-ton-and-kilogram-and-gram unit-width-full-name",
             u"unit/metric-ton-and-kilogram-and-gram unit-width-full-name",
             NumberFormatter::with()
                 .unit(MeasureUnit::forIdentifier("metric-ton-and-kilogram-and-gram", status))
@@ -755,8 +771,8 @@
 
     assertFormatSingle(
             u"Testing  \"1 foot 12 inches\"",
-            nullptr,
-            u"unit/foot-and-inch",
+            u"unit/foot-and-inch @### unit-width-full-name",
+            u"unit/foot-and-inch @### unit-width-full-name",
             NumberFormatter::with()
                 .unit(MeasureUnit::forIdentifier("foot-and-inch", status))
                 .precision(Precision::maxSignificantDigits(4))
@@ -776,7 +792,7 @@
 
     assertFormatSingle(
             u"Negative numbers: time",
-            nullptr, // submitting after TODO(icu-units#35) is fixed: fill in skeleton!
+            u"unit/hour-and-minute-and-second",
             u"unit/hour-and-minute-and-second",
             NumberFormatter::with().unit(MeasureUnit::forIdentifier("hour-and-minute-and-second", status)),
             Locale("de-DE"),
@@ -803,16 +819,21 @@
             u"0.008765 m/s",
             u"0 m/s");
 
-    // TODO(icu-units#35): does not normalize as desired: while "unit/*" does
-    // get split into unit/perUnit, ".unit(*)" and "measure-unit/*" don't:
-    assertFormatSingle(
-        u"Built-in unit, meter-per-second",
-        u"measure-unit/speed-meter-per-second",
-        u"~unit/meter-per-second",
-        NumberFormatter::with().unit(MeasureUnit::getMeterPerSecond()),
-        Locale("en-GB"),
-        2.4,
-        u"2.4 m/s");
+    assertFormatDescending(
+            u"Meters Per Second Short, built-in m/s",
+            u"measure-unit/speed-meter-per-second",
+            u"unit/meter-per-second",
+            NumberFormatter::with().unit(METER_PER_SECOND),
+            Locale::getEnglish(),
+            u"87,650 m/s",
+            u"8,765 m/s",
+            u"876.5 m/s",
+            u"87.65 m/s",
+            u"8.765 m/s",
+            u"0.8765 m/s",
+            u"0.08765 m/s",
+            u"0.008765 m/s",
+            u"0 m/s");
 
     assertFormatDescending(
             u"Pounds Per Square Mile Short (secondary unit has per-format) and adoptPerUnit method",
@@ -863,29 +884,51 @@
     //         u"0.008765 J/fur",
     //         u"0 J/fur");
 
-    // TODO(icu-units#59): THIS UNIT TEST DEMONSTRATES UNDESIREABLE BEHAVIOUR!
-    // When specifying built-in types, one can give both a unit and a perUnit.
-    // Resolving to a built-in unit does not always work.
-    //
-    // (Unit-testing philosophy: do we leave this enabled to demonstrate current
-    // behaviour, and changing behaviour in the future? Or comment it out to
-    // avoid asserting this is "correct"?)
+    assertFormatDescending(
+            u"Pounds per Square Inch: composed",
+            u"measure-unit/force-pound-force per-measure-unit/area-square-inch",
+            u"unit/pound-force-per-square-inch",
+            NumberFormatter::with().unit(POUND_FORCE).perUnit(SQUARE_INCH),
+            Locale::getEnglish(),
+            u"87,650 psi",
+            u"8,765 psi",
+            u"876.5 psi",
+            u"87.65 psi",
+            u"8.765 psi",
+            u"0.8765 psi",
+            u"0.08765 psi",
+            u"0.008765 psi",
+            u"0 psi");
+
+    assertFormatDescending(
+            u"Pounds per Square Inch: built-in",
+            u"measure-unit/force-pound-force per-measure-unit/area-square-inch",
+            u"unit/pound-force-per-square-inch",
+            NumberFormatter::with().unit(MeasureUnit::getPoundPerSquareInch()),
+            Locale::getEnglish(),
+            u"87,650 psi",
+            u"8,765 psi",
+            u"876.5 psi",
+            u"87.65 psi",
+            u"8.765 psi",
+            u"0.8765 psi",
+            u"0.08765 psi",
+            u"0.008765 psi",
+            u"0 psi");
+
     assertFormatSingle(
-            u"DEMONSTRATING BAD BEHAVIOUR, TODO(icu-units#59)",
+            u"m/s/s simplifies to m/s^2",
             u"measure-unit/speed-meter-per-second per-measure-unit/duration-second",
-            u"measure-unit/speed-meter-per-second per-measure-unit/duration-second",
-            NumberFormatter::with()
-                .unit(MeasureUnit::getMeterPerSecond())
-                .perUnit(MeasureUnit::getSecond()),
+            u"unit/meter-per-square-second",
+            NumberFormatter::with().unit(METER_PER_SECOND).perUnit(SECOND),
             Locale("en-GB"),
             2.4,
-            "2.4 m/s/s");
+            u"2.4 m/s\u00B2");
 
     assertFormatSingle(
             u"Negative numbers: acceleration",
             u"measure-unit/acceleration-meter-per-square-second",
-            // TODO: when other PRs are merged, try: u"unit/meter-per-second-second" instead:
-            u"measure-unit/acceleration-meter-per-square-second",
+            u"unit/meter-per-second-second",
             NumberFormatter::with().unit(MeasureUnit::forIdentifier("meter-per-pow2-second", status)),
             Locale("af-ZA"),
             -9.81,
@@ -908,12 +951,10 @@
         status.assertSuccess();
     }
 
-    // .perUnit() may only be passed a built-in type, "square-second" is not a
-    // built-in type.
-    nf = NumberFormatter::with()
-             .unit(MeasureUnit::getMeter())
-             .perUnit(MeasureUnit::forIdentifier("square-second", status))
-             .locale("en-GB");
+    // .perUnit() may only be passed a built-in type, or something that combines
+    // to a built-in type together with .unit().
+    MeasureUnit SQUARE_SECOND = MeasureUnit::forIdentifier("square-second", status);
+    nf = NumberFormatter::with().unit(FURLONG).perUnit(SQUARE_SECOND).locale("en-GB");
     status.assertSuccess(); // Error is only returned once we try to format.
     num = nf.formatDouble(2.4, status);
     if (!status.expectErrorAndReset(U_UNSUPPORTED_ERROR)) {
@@ -921,6 +962,128 @@
               nf.formatDouble(2.4, status).toString(status) + "\".");
         status.assertSuccess();
     }
+    // As above, "square-second" is not a built-in type, however this time,
+    // meter-per-square-second is a built-in type.
+    assertFormatSingle(
+            u"meter per square-second works as a composed unit",
+            u"measure-unit/speed-meter-per-second per-measure-unit/duration-second",
+            u"unit/meter-per-square-second",
+            NumberFormatter::with().unit(METER).perUnit(SQUARE_SECOND),
+            Locale("en-GB"),
+            2.4,
+            u"2.4 m/s\u00B2");
+}
+
+void NumberFormatterApiTest::unitSkeletons() {
+    const struct TestCase {
+        const char *msg;
+        const char16_t *inputSkeleton;
+        const char16_t *normalizedSkeleton;
+    } cases[] = {
+        {"old-form built-in compound unit",      //
+         u"measure-unit/speed-meter-per-second", //
+         u"unit/meter-per-second"},
+
+        {"old-form compound construction, converts to built-in",        //
+         u"measure-unit/length-meter per-measure-unit/duration-second", //
+         u"unit/meter-per-second"},
+
+        {"old-form compound construction which does not simplify to a built-in", //
+         u"measure-unit/energy-joule per-measure-unit/length-meter",             //
+         u"unit/joule-per-meter"},
+
+        {"old-form compound-compound ugliness resolves neatly",                   //
+         u"measure-unit/speed-meter-per-second per-measure-unit/duration-second", //
+         u"unit/meter-per-square-second"},
+
+        {"short-form built-in units stick with the built-in", //
+         u"unit/meter-per-second",                            //
+         u"unit/meter-per-second"},
+
+        {"short-form compound units stay as is", //
+         u"unit/square-meter-per-square-meter",  //
+         u"unit/square-meter-per-square-meter"},
+
+        {"short-form compound units stay as is", //
+         u"unit/joule-per-furlong",              //
+         u"unit/joule-per-furlong"},
+
+        {"short-form that doesn't consist of built-in units", //
+         u"unit/hectometer-per-second",                       //
+         u"unit/hectometer-per-second"},
+
+        {"short-form that doesn't consist of built-in units", //
+         u"unit/meter-per-hectosecond",                       //
+         u"unit/meter-per-hectosecond"},
+
+        // // TODO: binary prefixes not supported yet!
+        // {"Round-trip example from icu-units#35", //
+        //  u"unit/kibijoule-per-furlong",          //
+        //  u"unit/kibijoule-per-furlong"},
+    };
+    for (auto &cas : cases) {
+        IcuTestErrorCode status(*this, cas.msg);
+        auto nf = NumberFormatter::forSkeleton(cas.inputSkeleton, status);
+        if (status.errIfFailureAndReset("NumberFormatter::forSkeleton failed")) {
+            continue;
+        }
+        assertEquals(                                                       //
+            UnicodeString(TRUE, cas.inputSkeleton, -1) + u" normalization", //
+            cas.normalizedSkeleton,                                         //
+            nf.toSkeleton(status));
+        status.errIfFailureAndReset("NumberFormatter::toSkeleton failed");
+    }
+
+    const struct FailCase {
+        const char *msg;
+        const char16_t *inputSkeleton;
+        UErrorCode expectedForSkelStatus;
+        UErrorCode expectedToSkelStatus;
+    } failCases[] = {
+        {"Parsing measure-unit/* results in failure if not built-in unit",
+         u"measure-unit/hectometer",     //
+         U_NUMBER_SKELETON_SYNTAX_ERROR, //
+         U_ZERO_ERROR},
+
+        {"Parsing per-measure-unit/* results in failure if not built-in unit",
+         u"measure-unit/meter per-measure-unit/hectosecond", //
+         U_NUMBER_SKELETON_SYNTAX_ERROR,                     //
+         U_ZERO_ERROR},
+    };
+    for (auto &cas : failCases) {
+        IcuTestErrorCode status(*this, cas.msg);
+        auto nf = NumberFormatter::forSkeleton(cas.inputSkeleton, status);
+        if (status.expectErrorAndReset(cas.expectedForSkelStatus, cas.msg)) {
+                continue;
+        }
+        nf.toSkeleton(status);
+        status.expectErrorAndReset(cas.expectedToSkelStatus, cas.msg);
+    }
+
+    IcuTestErrorCode status(*this, "unitSkeletons");
+    MeasureUnit METER_PER_SECOND = MeasureUnit::forIdentifier("meter-per-second", status);
+
+    assertEquals(                                //
+        ".unit(METER_PER_SECOND) normalization", //
+        u"unit/meter-per-second",                //
+        NumberFormatter::with().unit(METER_PER_SECOND).toSkeleton(status));
+    assertEquals(                                     //
+        ".unit(METER).perUnit(SECOND) normalization", //
+        u"unit/meter-per-second",
+        NumberFormatter::with().unit(METER).perUnit(SECOND).toSkeleton(status));
+    assertEquals(                                                                  //
+        ".unit(MeasureUnit::forIdentifier(\"hectometer\", status)) normalization", //
+        u"unit/hectometer",
+        NumberFormatter::with()
+            .unit(MeasureUnit::forIdentifier("hectometer", status))
+            .toSkeleton(status));
+    assertEquals(                                                                  //
+        ".unit(MeasureUnit::forIdentifier(\"hectometer\", status)) normalization", //
+        u"unit/meter-per-hectosecond",
+        NumberFormatter::with()
+            .unit(METER)
+            .perUnit(MeasureUnit::forIdentifier("hectosecond", status))
+            .toSkeleton(status));
 }
 
 void NumberFormatterApiTest::unitUsage() {
@@ -3532,11 +3695,11 @@
     }
 
     {
-        const char16_t* message = u"Measure unit field position with prefix and suffix";
+        const char16_t* message = u"Measure unit field position with prefix and suffix, composed m/s";
         FormattedNumber result = assertFormatSingle(
                 message,
                 u"measure-unit/length-meter per-measure-unit/duration-second unit-width-full-name",
-                u"unit/meter-per-second unit-width-full-name",
+                u"measure-unit/length-meter per-measure-unit/duration-second unit-width-full-name",
                 NumberFormatter::with().unit(METER).perUnit(SECOND).unitWidth(UNUM_UNIT_WIDTH_FULL_NAME),
                 "ky", // locale with the interesting data
                 68,
@@ -3554,6 +3717,28 @@
     }
 
     {
+        const char16_t* message = u"Measure unit field position with prefix and suffix, built-in m/s";
+        FormattedNumber result = assertFormatSingle(
+                message,
+                u"measure-unit/speed-meter-per-second unit-width-full-name",
+                u"unit/meter-per-second unit-width-full-name",
+                NumberFormatter::with().unit(METER_PER_SECOND).unitWidth(UNUM_UNIT_WIDTH_FULL_NAME),
+                "ky", // locale with the interesting data
+                68,
+                u"секундасына 68 метр");
+        static const UFieldPosition expectedFieldPositions[] = {
+                // field, begin index, end index
+                {UNUM_MEASURE_UNIT_FIELD, 0, 11},
+                {UNUM_INTEGER_FIELD, 12, 14},
+                {UNUM_MEASURE_UNIT_FIELD, 15, 19}};
+        assertNumberFieldPositions(
+                message,
+                result,
+                expectedFieldPositions,
+                UPRV_LENGTHOF(expectedFieldPositions));
+    }
+
+    {
         const char16_t* message = u"Measure unit field position with inner spaces";
         FormattedNumber result = assertFormatSingle(
                 message,
@@ -4141,6 +4326,15 @@
     assertEquals("Copy Assigned capacity", 4, copyAssigned.mixedMeasures.getCapacity());
 }
 
+/* For skeleton comparisons: this checks the toSkeleton output for `f` and for
+ * `conciseSkeleton` against the normalized version of `uskeleton` - this does
+ * not round-trip uskeleton itself.
+ *
+ * If `conciseSkeleton` starts with a "~", its round-trip check is skipped.
+ *
+ * If `uskeleton` is nullptr, toSkeleton is expected to return an
+ * U_UNSUPPORTED_ERROR.
+ */
 void NumberFormatterApiTest::assertFormatDescending(
         const char16_t* umessage,
         const char16_t* uskeleton,
@@ -4202,6 +4396,15 @@
     }
 }
 
+/* For skeleton comparisons: this checks the toSkeleton output for `f` and for
+ * `conciseSkeleton` against the normalized version of `uskeleton` - this does
+ * not round-trip uskeleton itself.
+ *
+ * If `conciseSkeleton` starts with a "~", its round-trip check is skipped.
+ *
+ * If `uskeleton` is nullptr, toSkeleton is expected to return an
+ * U_UNSUPPORTED_ERROR.
+ */
 void NumberFormatterApiTest::assertFormatDescendingBig(
         const char16_t* umessage,
         const char16_t* uskeleton,
@@ -4263,6 +4466,15 @@
     }
 }
 
+/* For skeleton comparisons: this checks the toSkeleton output for `f` and for
+ * `conciseSkeleton` against the normalized version of `uskeleton` - this does
+ * not round-trip uskeleton itself.
+ *
+ * If `conciseSkeleton` starts with a "~", its round-trip check is skipped.
+ *
+ * If `uskeleton` is nullptr, toSkeleton is expected to return an
+ * U_UNSUPPORTED_ERROR.
+ */
 FormattedNumber
 NumberFormatterApiTest::assertFormatSingle(
         const char16_t* umessage,
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java
index e320575..9d972e4 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LongNameHandler.java
@@ -215,16 +215,10 @@
             UnitWidth width,
             PluralRules rules,
             MicroPropsGenerator parent) {
-        if (unit.getType() == null || (perUnit != null && perUnit.getType() == null)) {
-            // TODO(ICU-20941): Unsanctioned unit. Not yet fully supported. Set an
-            // error code. Once we support not-built-in units here, unitRef may be
-            // anything, but if not built-in, perUnit has to be "none".
-            throw new UnsupportedOperationException("Unsanctioned units, not yet supported");
-        }
         if (perUnit != null) {
             // Compound unit: first try to simplify (e.g., meters per second is its own unit).
-            MeasureUnit simplified = MeasureUnit.resolveUnitPerUnit(unit, perUnit);
-            if (simplified != null) {
+            MeasureUnit simplified = unit.product(perUnit.reciprocal());
+            if (simplified.getType() != null) {
                 unit = simplified;
             } else {
                 // No simplified form is available.
@@ -232,6 +226,12 @@
             }
         }
 
+        if (unit.getType() == null) {
+            // TODO(ICU-20941): Unsanctioned unit. Not yet fully supported.
+            throw new UnsupportedOperationException("Unsanctioned unit, not yet supported: " +
+                                                    unit.getIdentifier());
+        }
+
         String[] simpleFormats = new String[ARRAY_LENGTH];
         getMeasureData(locale, unit, width, simpleFormats);
         // TODO(ICU4J): Reduce the number of object creations here?
@@ -249,6 +249,13 @@
             UnitWidth width,
             PluralRules rules,
             MicroPropsGenerator parent) {
+        if (unit.getType() == null || perUnit.getType() == null) {
+            // TODO(ICU-20941): Unsanctioned unit. Not yet fully supported. Set an
+            // error code.
+            throw new UnsupportedOperationException(
+                "Unsanctioned units, not yet supported: " + unit.getIdentifier() + "/" +
+                perUnit.getIdentifier());
+        }
         String[] primaryData = new String[ARRAY_LENGTH];
         getMeasureData(locale, unit, width, primaryData);
         String[] secondaryData = new String[ARRAY_LENGTH];
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/MeasureUnitImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/MeasureUnitImpl.java
index 6325e22..9dc64d8 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/MeasureUnitImpl.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/MeasureUnitImpl.java
@@ -70,7 +70,9 @@
         MeasureUnitImpl result = new MeasureUnitImpl();
         result.complexity = this.complexity;
         result.identifier = this.identifier;
-        result.singleUnits = new ArrayList<>(this.singleUnits);
+        for (SingleUnitImpl single : this.singleUnits) {
+            result.singleUnits.add(single.copy());
+        }
         return result;
     }
 
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java
index 07e81c4..2071e33 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java
@@ -12,6 +12,8 @@
 import com.ibm.icu.impl.StringSegment;
 import com.ibm.icu.impl.number.MacroProps;
 import com.ibm.icu.impl.number.RoundingUtils;
+import com.ibm.icu.impl.units.MeasureUnitImpl;
+import com.ibm.icu.impl.units.SingleUnitImpl;
 import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay;
 import com.ibm.icu.number.NumberFormatter.GroupingStrategy;
 import com.ibm.icu.number.NumberFormatter.SignDisplay;
@@ -904,9 +906,6 @@
         if (macros.unit != null && GeneratorHelpers.unit(macros, sb)) {
             sb.append(' ');
         }
-        if (macros.perUnit != null && GeneratorHelpers.perUnit(macros, sb)) {
-            sb.append(' ');
-        }
         if (macros.usage != null && GeneratorHelpers.usage(macros, sb)) {
             sb.append(' ');
         }
@@ -1026,6 +1025,7 @@
             sb.append(currency.getCurrencyCode());
         }
 
+        // "measure-unit/" is deprecated in favour of "unit/".
         private static void parseMeasureUnitOption(StringSegment segment, MacroProps macros) {
             // NOTE: The category (type) of the unit is guaranteed to be a valid subtag (alphanumeric)
             // http://unicode.org/reports/tr35/#Validity_Data
@@ -1048,12 +1048,7 @@
             throw new SkeletonSyntaxException("Unknown measure unit", segment);
         }
 
-        private static void generateMeasureUnitOption(MeasureUnit unit, StringBuilder sb) {
-            sb.append(unit.getType());
-            sb.append("-");
-            sb.append(unit.getSubtype());
-        }
-
+        // "per-measure-unit/" is deprecated in favour of "unit/".
         private static void parseMeasurePerUnitOption(StringSegment segment, MacroProps macros) {
             // A little bit of a hack: save the current unit (numerator), call the main measure unit
             // parsing code, put back the numerator unit, and put the new unit into per-unit.
@@ -1068,13 +1063,44 @@
          * specified via a "unit/" concise skeleton.
          */
         private static void parseIdentifierUnitOption(StringSegment segment, MacroProps macros) {
-            MeasureUnit[] units = MeasureUnit.parseCoreUnitIdentifier(segment.asString());
-            if (units == null) {
-                throw new SkeletonSyntaxException("Invalid core unit identifier", segment);
+            MeasureUnitImpl fullUnit;
+            try {
+                fullUnit = MeasureUnitImpl.forIdentifier(segment.asString());
+            } catch (IllegalArgumentException e) {
+                throw new SkeletonSyntaxException("Invalid unit stem", segment);
             }
-            macros.unit = units[0];
-            if (units.length == 2) {
-                macros.perUnit = units[1];
+
+            // Mixed units can only be represented by full MeasureUnit instances, so we
+            // don't split the denominator into macros.perUnit.
+            if (fullUnit.getComplexity() == MeasureUnit.Complexity.MIXED) {
+                macros.unit = fullUnit.build();
+                return;
+            }
+
+            // When we have a built-in unit (e.g. meter-per-second), we don't split it up
+            MeasureUnit testBuiltin = fullUnit.build();
+            if (testBuiltin.getType() != null) {
+                macros.unit = testBuiltin;
+                return;
+            }
+
+            // TODO(ICU-20941): Clean this up.
+            for (SingleUnitImpl subUnit : fullUnit.getSingleUnits()) {
+                if (subUnit.getDimensionality() > 0) {
+                    if (macros.unit == null) {
+                        macros.unit = subUnit.build();
+                    } else {
+                        macros.unit = macros.unit.product(subUnit.build());
+                    }
+                } else {
+                    // It's okay to mutate fullUnit, we're throwing it away after this:
+                    subUnit.setDimensionality(subUnit.getDimensionality() * -1);
+                    if (macros.perUnit == null) {
+                        macros.perUnit = subUnit.build();
+                    } else {
+                        macros.perUnit = macros.perUnit.product(subUnit.build());
+                    }
+                }
             }
         }
 
@@ -1468,24 +1494,17 @@
             } else if (macros.unit == MeasureUnit.PERMILLE) {
                 sb.append("permille");
                 return true;
-            } else if (macros.unit.getType() != null) {
-                sb.append("measure-unit/");
-                BlueprintHelpers.generateMeasureUnitOption(macros.unit, sb);
-                return true;
             } else {
-                // TODO(icu-units#35): add support for not-built-in units.
-                throw new UnsupportedOperationException();
-            }
-        }
-
-        private static boolean perUnit(MacroProps macros, StringBuilder sb) {
-            // Per-units are currently expected to be only MeasureUnits.
-            if (macros.perUnit instanceof Currency) {
-                throw new UnsupportedOperationException(
-                        "Cannot generate number skeleton with per-unit that is not a standard measure unit");
-            } else {
-                sb.append("per-measure-unit/");
-                BlueprintHelpers.generateMeasureUnitOption(macros.perUnit, sb);
+                MeasureUnit unit = macros.unit;
+                if (macros.perUnit != null) {
+                    if (macros.perUnit instanceof Currency) {
+                        throw new UnsupportedOperationException(
+                            "Cannot generate number skeleton with per-unit that is not a standard measure unit");
+                    }
+                    unit = unit.product(macros.perUnit.reciprocal());
+                }
+                sb.append("unit/");
+                sb.append(unit.getIdentifier());
                 return true;
             }
         }
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 15ee506..a32b0b7 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
@@ -381,7 +381,7 @@
 
 
     /**
-     * Get the type, such as "length"
+     * Get the type, such as "length". May return null.
      *
      * @stable ICU 53
      */
@@ -391,7 +391,7 @@
 
 
     /**
-     * Get the subType, such as “foot”.
+     * Get the subType, such as “foot”. May return null.
      *
      * @stable ICU 53
      */
@@ -702,47 +702,6 @@
         return null;
     }
 
-    /**
-     * For ICU use only.
-     * @internal
-     * @deprecated This API is ICU internal only.
-     */
-    @Deprecated
-    public static MeasureUnit[] parseCoreUnitIdentifier(String coreUnitIdentifier) {
-        // First search for the whole code unit identifier as a subType
-        MeasureUnit whole = findBySubType(coreUnitIdentifier);
-        if (whole != null) {
-            return new MeasureUnit[] { whole }; // found a numerator but not denominator
-        }
-
-        // If not found, try breaking apart numerator and denominator
-        int perIdx = coreUnitIdentifier.indexOf("-per-");
-        if (perIdx == -1) {
-            // String does not contain "-per-"
-            return null;
-        }
-        String numeratorStr = coreUnitIdentifier.substring(0, perIdx);
-        String denominatorStr = coreUnitIdentifier.substring(perIdx + 5);
-        MeasureUnit numerator = findBySubType(numeratorStr);
-        MeasureUnit denominator = findBySubType(denominatorStr);
-        if (numerator != null && denominator != null) {
-            return new MeasureUnit[] { numerator, denominator }; // found both a numerator and denominator
-        }
-
-        // The numerator or denominator were invalid
-        return null;
-    }
-
-    /**
-     * For ICU use only.
-     * @internal
-     * @deprecated This API is ICU internal only.
-     */
-    @Deprecated
-    public static MeasureUnit resolveUnitPerUnit(MeasureUnit unit, MeasureUnit perUnit) {
-        return unitPerUnitToSingleUnit.get(Pair.of(unit, perUnit));
-    }
-
     static final UnicodeSet ASCII = new UnicodeSet('a', 'z').freeze();
     static final UnicodeSet ASCII_HYPHEN_DIGITS = new UnicodeSet('-', '-', '0', '9', 'a', 'z').freeze();
 
@@ -2010,23 +1969,7 @@
      */
     public static final MeasureUnit TEASPOON = MeasureUnit.internalGetInstance("volume", "teaspoon");
 
-    private static HashMap<Pair<MeasureUnit, MeasureUnit>, MeasureUnit>unitPerUnitToSingleUnit =
-            new HashMap<>();
-
-    static {
-        unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.LITER, MeasureUnit.KILOMETER), MeasureUnit.LITER_PER_KILOMETER);
-        unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.POUND, MeasureUnit.SQUARE_INCH), MeasureUnit.POUND_PER_SQUARE_INCH);
-        unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.PIXEL, MeasureUnit.CENTIMETER), MeasureUnit.PIXEL_PER_CENTIMETER);
-        unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.DOT, MeasureUnit.CENTIMETER), MeasureUnit.DOT_PER_CENTIMETER);
-        unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.MILE, MeasureUnit.HOUR), MeasureUnit.MILE_PER_HOUR);
-        unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.MILLIGRAM, MeasureUnit.DECILITER), MeasureUnit.MILLIGRAM_PER_DECILITER);
-        unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.MILE, MeasureUnit.GALLON_IMPERIAL), MeasureUnit.MILE_PER_GALLON_IMPERIAL);
-        unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.KILOMETER, MeasureUnit.HOUR), MeasureUnit.KILOMETER_PER_HOUR);
-        unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.MILE, MeasureUnit.GALLON), MeasureUnit.MILE_PER_GALLON);
-        unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.PIXEL, MeasureUnit.INCH), MeasureUnit.PIXEL_PER_INCH);
-        unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.DOT, MeasureUnit.INCH), MeasureUnit.DOT_PER_INCH);
-        unitPerUnitToSingleUnit.put(Pair.<MeasureUnit, MeasureUnit>of(MeasureUnit.METER, MeasureUnit.SECOND), MeasureUnit.METER_PER_SECOND);
-    }
+    // unitPerUnitToSingleUnit no longer in use! TODO: remove from code-generation code.
 
     // End generated MeasureUnit constants
     /* Private */
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 d5a4efd..a7f706c 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
@@ -2652,7 +2652,7 @@
         // This fails unless we resolve to MeasureUnit.POUND_PER_SQUARE_INCH
         assertEquals("", "50 psi",
                 fmt.formatMeasurePerUnit(
-                        new Measure(50, MeasureUnit.POUND),
+                        new Measure(50, MeasureUnit.POUND_FORCE),
                         MeasureUnit.SQUARE_INCH,
                         new StringBuilder(),
                         new FieldPosition(0)).toString());
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 89afcf3..c25fd6d 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
@@ -536,6 +536,23 @@
                 "0.0088 meters",
                 "0 meters");
 
+        // // TODO(ICU-20941): Support formatting for not-built-in units
+        // assertFormatDescending(
+        //         "Hectometers",
+        //         "measure-unit/length-hectometer",
+        //         "unit/hectometer",
+        //         NumberFormatter.with().unit(MeasureUnit.forIdentifier("hectometer")),
+        //         ULocale.ENGLISH,
+        //         "87,650 hm",
+        //         "8,765 ham",
+        //         "876.5 hm",
+        //         "87.65 hm",
+        //         "8.765 hm",
+        //         "0.8765 hm",
+        //         "0.08765 hm",
+        //         "0.008765 hm",
+        //         "0 hm");
+
         assertFormatSingleMeasure(
                 "Meters with Measure Input",
                 "unit-width-full-name",
@@ -656,10 +673,9 @@
                 5,
                 "5 a\u00F1os");
             
-        // TODO(icu-units#35): skeleton generation.
         assertFormatSingle(
                 "Mixed unit",
-                null,
+                "unit/yard-and-foot-and-inch",
                 "unit/yard-and-foot-and-inch",
                 NumberFormatter.with()
                         .unit(MeasureUnit.forIdentifier("yard-and-foot-and-inch")),
@@ -667,10 +683,9 @@
                 3.65,
                 "3 yd, 1 ft, 11.4 in");
 
-        // TODO(icu-units#35): skeleton generation.
         assertFormatSingle(
                 "Mixed unit, Scientific",
-                null,
+                "unit/yard-and-foot-and-inch E0",
                 "unit/yard-and-foot-and-inch E0",
                 NumberFormatter.with()
                         .unit(MeasureUnit.forIdentifier("yard-and-foot-and-inch"))
@@ -679,10 +694,9 @@
                 3.65,
                 "3 yd, 1 ft, 1.14E1 in");
 
-        // TODO(icu-units#35): skeleton generation.
         assertFormatSingle(
                 "Mixed Unit (Narrow Version)",
-                null,
+                "unit/metric-ton-and-kilogram-and-gram unit-width-narrow",
                 "unit/metric-ton-and-kilogram-and-gram unit-width-narrow",
                 NumberFormatter.with()
                         .unit(MeasureUnit.forIdentifier("metric-ton-and-kilogram-and-gram"))
@@ -691,10 +705,9 @@
                 4.28571,
                 "4t 285kg 710g");
 
-        // TODO(icu-units#35): skeleton generation.
         assertFormatSingle(
                 "Mixed Unit (Short Version)",
-                null,
+                "unit/metric-ton-and-kilogram-and-gram unit-width-short",
                 "unit/metric-ton-and-kilogram-and-gram unit-width-short",
                 NumberFormatter.with()
                         .unit(MeasureUnit.forIdentifier("metric-ton-and-kilogram-and-gram"))
@@ -703,10 +716,9 @@
                 4.28571,
                 "4 t, 285 kg, 710 g");
 
-        // TODO(icu-units#35): skeleton generation.
         assertFormatSingle(
                 "Mixed Unit (Full Name Version)",
-                null,
+                "unit/metric-ton-and-kilogram-and-gram unit-width-full-name",
                 "unit/metric-ton-and-kilogram-and-gram unit-width-full-name",
                 NumberFormatter.with()
                         .unit(MeasureUnit.forIdentifier("metric-ton-and-kilogram-and-gram"))
@@ -717,8 +729,8 @@
 
         assertFormatSingle(
                 "Testing \"1 foot 12 inches\"",
-                null,
-                "unit/foot-and-inch",
+                "unit/foot-and-inch @### unit-width-full-name",
+                "unit/foot-and-inch @### unit-width-full-name",
                 NumberFormatter.with()
                         .unit(MeasureUnit.forIdentifier("foot-and-inch"))
                         .precision(Precision.maxSignificantDigits(4))
@@ -738,7 +750,7 @@
 
         assertFormatSingle(
                 "Negative numbers: time",
-                null, // submitting after TODO(icu-units#35) is fixed: fill in skeleton!
+                "unit/hour-and-minute-and-second",
                 "unit/hour-and-minute-and-second",
                 NumberFormatter.with().unit(MeasureUnit.forIdentifier("hour-and-minute-and-second")),
                 new ULocale("de-DE"),
@@ -751,7 +763,7 @@
         assertFormatDescending(
                 "Meters Per Second Short (unit that simplifies) and perUnit method",
                 "measure-unit/length-meter per-measure-unit/duration-second",
-                "~unit/meter-per-second", // does not round-trip to the full skeleton above
+                "unit/meter-per-second",
                 NumberFormatter.with().unit(MeasureUnit.METER).perUnit(MeasureUnit.SECOND),
                 ULocale.ENGLISH,
                 "87,650 m/s",
@@ -764,16 +776,21 @@
                 "0.008765 m/s",
                 "0 m/s");
 
-        // TODO(icu-units#35): does not normalize as desired: while "unit/*" does
-        // get split into unit/perUnit, ".unit(*)" and "measure-unit/*" don't:
-        assertFormatSingle(
-                "Built-in unit, meter-per-second",
+        assertFormatDescending(
+                "Meters Per Second Short, built-in m/s",
                 "measure-unit/speed-meter-per-second",
-                "~unit/meter-per-second",
+                "unit/meter-per-second",
                 NumberFormatter.with().unit(MeasureUnit.METER_PER_SECOND),
-                new ULocale("en-GB"),
-                2.4,
-                "2.4 m/s");
+                ULocale.ENGLISH,
+                "87,650 m/s",
+                "8,765 m/s",
+                "876.5 m/s",
+                "87.65 m/s",
+                "8.765 m/s",
+                "0.8765 m/s",
+                "0.08765 m/s",
+                "0.008765 m/s",
+                "0 m/s");
 
         assertFormatDescending(
                 "Pounds Per Square Mile Short (secondary unit has per-format)",
@@ -824,29 +841,53 @@
         //         "0.008765 J/fur",
         //         "0 J/fur");
 
-        // TODO(icu-units#59): THIS UNIT TEST DEMONSTRATES UNDESIRABLE BEHAVIOUR!
-        // When specifying built-in types, one can give both a unit and a perUnit.
-        // Resolving to a built-in unit does not always work.
-        //
-        // (Unit-testing philosophy: do we leave this enabled to demonstrate current
-        // behaviour, and changing behaviour in the future? Or comment it out to
-        // avoid asserting this is "correct"?)
+        assertFormatDescending(
+                "Pounds per Square Inch: composed",
+                "measure-unit/force-pound-force per-measure-unit/area-square-inch",
+                "unit/pound-force-per-square-inch",
+                NumberFormatter.with().unit(MeasureUnit.POUND_FORCE).perUnit(MeasureUnit.SQUARE_INCH),
+                ULocale.ENGLISH,
+                "87,650 psi",
+                "8,765 psi",
+                "876.5 psi",
+                "87.65 psi",
+                "8.765 psi",
+                "0.8765 psi",
+                "0.08765 psi",
+                "0.008765 psi",
+                "0 psi");
+
+        assertFormatDescending(
+                "Pounds per Square Inch: built-in",
+                "measure-unit/force-pound-force per-measure-unit/area-square-inch",
+                "unit/pound-force-per-square-inch",
+                NumberFormatter.with().unit(MeasureUnit.POUND_PER_SQUARE_INCH),
+                ULocale.ENGLISH,
+                "87,650 psi",
+                "8,765 psi",
+                "876.5 psi",
+                "87.65 psi",
+                "8.765 psi",
+                "0.8765 psi",
+                "0.08765 psi",
+                "0.008765 psi",
+                "0 psi");
+
         assertFormatSingle(
-                "DEMONSTRATING BAD BEHAVIOUR, TODO(icu-units#59)",
+                "m/s/s simplifies to m/s^2",
                 "measure-unit/speed-meter-per-second per-measure-unit/duration-second",
-                "measure-unit/speed-meter-per-second per-measure-unit/duration-second",
+                "unit/meter-per-square-second",
                 NumberFormatter.with()
                         .unit(MeasureUnit.METER_PER_SECOND)
                         .perUnit(MeasureUnit.SECOND),
                 new ULocale("en-GB"),
                 2.4,
-                "2.4 m/s/s");
+                "2.4 m/s\u00B2");
 
         assertFormatSingle(
                 "Negative numbers: acceleration",
                 "measure-unit/acceleration-meter-per-square-second",
-                // TODO: when other PRs are merged, try: u"unit/meter-per-second-second" instead:
-                "measure-unit/acceleration-meter-per-square-second",
+                "unit/meter-per-second-second",
                 NumberFormatter.with().unit(MeasureUnit.forIdentifier("meter-per-pow2-second")),
                 new ULocale("af-ZA"),
                 -9.81,
@@ -869,21 +910,125 @@
             // Pass
         }
 
-        // .perUnit() may only be passed a built-in type, "square-second" is not a
-        // built-in type.
+        // .perUnit() may only be passed a built-in type, or something that
+        // combines to a built-in type together with .unit().
         nf = NumberFormatter.with()
-                .unit(MeasureUnit.METER)
+                .unit(MeasureUnit.FURLONG)
                 .perUnit(MeasureUnit.forIdentifier("square-second"))
                 .locale(new ULocale("en-GB"));
-
         try {
             nf.format(2.4d);
             fail("Expected failure, got: " + nf.format(2.4d) + ".");
         } catch (UnsupportedOperationException e) {
             // pass
         }
+        // As above, "square-second" is not a built-in type, however this time,
+        // meter-per-square-second is a built-in type.
+        assertFormatSingle(
+                "meter per square-second works as a composed unit",
+                "measure-unit/speed-meter-per-second per-measure-unit/duration-second",
+                "unit/meter-per-square-second",
+                NumberFormatter.with()
+                        .unit(MeasureUnit.METER)
+                        .perUnit(MeasureUnit.forIdentifier("square-second")),
+                new ULocale("en-GB"),
+                2.4,
+                "2.4 m/s\u00B2");
     }
 
+    @Test
+    public void unitSkeletons() {
+        Object[][] cases = {
+            {"old-form built-in compound unit",     //
+             "measure-unit/speed-meter-per-second", //
+             "unit/meter-per-second"},
+
+            {"old-form compound construction, converts to built-in",       //
+             "measure-unit/length-meter per-measure-unit/duration-second", //
+             "unit/meter-per-second"},
+
+            {"old-form compound construction which does not simplify to a built-in", //
+             "measure-unit/energy-joule per-measure-unit/length-meter",              //
+             "unit/joule-per-meter"},
+
+            {"old-form compound-compound ugliness resolves neatly",                  //
+             "measure-unit/speed-meter-per-second per-measure-unit/duration-second", //
+             "unit/meter-per-square-second"},
+
+            {"short-form built-in units stick with the built-in", //
+             "unit/meter-per-second",                             //
+             "unit/meter-per-second"},
+
+            {"short-form compound units stay as is", //
+             "unit/square-meter-per-square-meter",  //
+             "unit/square-meter-per-square-meter"},
+
+            {"short-form compound units stay as is", //
+             "unit/joule-per-furlong",              //
+             "unit/joule-per-furlong"},
+
+            {"short-form that doesn't consist of built-in units", //
+             "unit/hectometer-per-second",                        //
+             "unit/hectometer-per-second"},
+
+            {"short-form that doesn't consist of built-in units", //
+             "unit/meter-per-hectosecond",                        //
+             "unit/meter-per-hectosecond"},
+
+            // // TODO: binary prefixes not supported yet!
+            // {"Round-trip example from icu-units#35", //
+            //  "unit/kibijoule-per-furlong",           //
+            //  "unit/kibijoule-per-furlong"},
+        };
+        for (Object[] cas : cases) {
+            String msg = (String)cas[0];
+            String inputSkeleton = (String)cas[1];
+            String normalizedSkeleton = (String)cas[2];
+            UnlocalizedNumberFormatter nf = NumberFormatter.forSkeleton(inputSkeleton);
+            assertEquals(msg, normalizedSkeleton, nf.toSkeleton());
+        }
+
+        Object NoException = new Object();
+        Object[][] failCases = {
+            {"Parsing measure-unit/* results in failure if not built-in unit",
+             "measure-unit/hectometer", //
+             true,                      //
+             false},
+
+            {"Parsing per-measure-unit/* results in failure if not built-in unit",
+             "measure-unit/meter per-measure-unit/hectosecond", //
+             true,                                              //
+             false},
+        };
+        for (Object[] cas : failCases) {
+            String msg = (String)cas[0];
+            String inputSkeleton = (String)cas[1];
+            boolean forSkeletonExpectFailure = (boolean)cas[2];
+            boolean toSkeletonExpectFailure = (boolean)cas[3];
+            UnlocalizedNumberFormatter nf = null;
+            try {
+                nf = NumberFormatter.forSkeleton(inputSkeleton);
+                if (forSkeletonExpectFailure) {
+                    fail("forSkeleton() should have failed: " + msg);
+                }
+            } catch (Exception e) {
+                if (!forSkeletonExpectFailure) {
+                    fail("forSkeleton() should not have failed: " + msg);
+                }
+                continue;
+            }
+            try {
+                nf.toSkeleton();
+                if (toSkeletonExpectFailure) {
+                    fail("toSkeleton() should have failed: " + msg);
+                }
+            } catch (Exception e) {
+                if (!toSkeletonExpectFailure) {
+                    fail("toSkeleton() should not have failed: " + msg);
+                }
+            }
+        }
+    }
 
     @Test
     public void unitUsage() {
@@ -1157,7 +1302,6 @@
         // to see divide-by-zero behaviour.
     }
 
-
     @Test
     public void unitUsageErrorCodes() {
         UnlocalizedNumberFormatter unloc_formatter;
@@ -3409,11 +3553,11 @@
         }
 
         {
-            String message = "Measure unit field position with prefix and suffix";
+            String message = "Measure unit field position with prefix and suffix, composed m/s";
             FormattedNumber result = assertFormatSingle(
                     message,
                     "measure-unit/length-meter per-measure-unit/duration-second unit-width-full-name",
-                    "~unit/meter-per-second unit-width-full-name", // does not round-trip to the full skeleton above
+                    "measure-unit/length-meter per-measure-unit/duration-second unit-width-full-name",
                     NumberFormatter.with().unit(MeasureUnit.METER).perUnit(MeasureUnit.SECOND).unitWidth(UnitWidth.FULL_NAME),
                     new ULocale("ky"), // locale with the interesting data
                     68,
@@ -3430,6 +3574,27 @@
         }
 
         {
+            String message = "Measure unit field position with prefix and suffix, built-in m/s";
+            FormattedNumber result = assertFormatSingle(
+                    message,
+                    "measure-unit/speed-meter-per-second unit-width-full-name",
+                    "unit/meter-per-second unit-width-full-name",
+                    NumberFormatter.with().unit(MeasureUnit.METER_PER_SECOND).unitWidth(UnitWidth.FULL_NAME),
+                    new ULocale("ky"), // locale with the interesting data
+                    68,
+                    "секундасына 68 метр");
+            Object[][] expectedFieldPositions = new Object[][] {
+                    // field, begin index, end index
+                    {NumberFormat.Field.MEASURE_UNIT, 0, 11},
+                    {NumberFormat.Field.INTEGER, 12, 14},
+                    {NumberFormat.Field.MEASURE_UNIT, 15, 19}};
+            assertNumberFieldPositions(
+                    message,
+                    result,
+                    expectedFieldPositions);
+        }
+
+        {
             String message = "Measure unit field position with inner spaces";
             FormattedNumber result = assertFormatSingle(
                     message,