// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.dev.test.number;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.FieldPosition;
import java.text.Format;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.junit.Ignore;
import org.junit.Test;

import com.ibm.icu.dev.test.format.FormattedValueTest;
import com.ibm.icu.dev.test.serializable.SerializableTestUtility;
import com.ibm.icu.impl.number.Grouper;
import com.ibm.icu.impl.number.LocalizedNumberFormatterAsFormat;
import com.ibm.icu.impl.number.MacroProps;
import com.ibm.icu.impl.number.Padder;
import com.ibm.icu.impl.number.Padder.PadPosition;
import com.ibm.icu.impl.number.PatternStringParser;
import com.ibm.icu.number.CompactNotation;
import com.ibm.icu.number.FormattedNumber;
import com.ibm.icu.number.FractionPrecision;
import com.ibm.icu.number.IntegerWidth;
import com.ibm.icu.number.LocalizedNumberFormatter;
import com.ibm.icu.number.Notation;
import com.ibm.icu.number.NumberFormatter;
import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay;
import com.ibm.icu.number.NumberFormatter.GroupingStrategy;
import com.ibm.icu.number.NumberFormatter.SignDisplay;
import com.ibm.icu.number.NumberFormatter.UnitWidth;
import com.ibm.icu.number.Precision;
import com.ibm.icu.number.Scale;
import com.ibm.icu.number.ScientificNotation;
import com.ibm.icu.number.UnlocalizedNumberFormatter;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.NumberingSystem;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;
import com.ibm.icu.util.CurrencyAmount;
import com.ibm.icu.util.Measure;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.NoUnit;
import com.ibm.icu.util.ULocale;

public class NumberFormatterApiTest {

    private static final Currency USD = Currency.getInstance("USD");
    private static final Currency GBP = Currency.getInstance("GBP");
    private static final Currency CZK = Currency.getInstance("CZK");
    private static final Currency CAD = Currency.getInstance("CAD");
    private static final Currency ESP = Currency.getInstance("ESP");
    private static final Currency PTE = Currency.getInstance("PTE");
    private static final Currency RON = Currency.getInstance("RON");

    @Test
    public void notationSimple() {
        assertFormatDescending(
                "Basic",
                "",
                NumberFormatter.with(),
                ULocale.ENGLISH,
                "87,650",
                "8,765",
                "876.5",
                "87.65",
                "8.765",
                "0.8765",
                "0.08765",
                "0.008765",
                "0");

        assertFormatDescendingBig(
                "Big Simple",
                "notation-simple",
                NumberFormatter.with().notation(Notation.simple()),
                ULocale.ENGLISH,
                "87,650,000",
                "8,765,000",
                "876,500",
                "87,650",
                "8,765",
                "876.5",
                "87.65",
                "8.765",
                "0");

        assertFormatSingle(
                "Basic with Negative Sign",
                "",
                NumberFormatter.with(),
                ULocale.ENGLISH,
                -9876543.21,
                "-9,876,543.21");
    }

    @Test
    public void notationScientific() {
        assertFormatDescending(
                "Scientific",
                "scientific",
                NumberFormatter.with().notation(Notation.scientific()),
                ULocale.ENGLISH,
                "8.765E4",
                "8.765E3",
                "8.765E2",
                "8.765E1",
                "8.765E0",
                "8.765E-1",
                "8.765E-2",
                "8.765E-3",
                "0E0");

        assertFormatDescending(
                "Engineering",
                "engineering",
                NumberFormatter.with().notation(Notation.engineering()),
                ULocale.ENGLISH,
                "87.65E3",
                "8.765E3",
                "876.5E0",
                "87.65E0",
                "8.765E0",
                "876.5E-3",
                "87.65E-3",
                "8.765E-3",
                "0E0");

        assertFormatDescending(
                "Scientific sign always shown",
                "scientific/sign-always",
                NumberFormatter.with().notation(Notation.scientific().withExponentSignDisplay(SignDisplay.ALWAYS)),
                ULocale.ENGLISH,
                "8.765E+4",
                "8.765E+3",
                "8.765E+2",
                "8.765E+1",
                "8.765E+0",
                "8.765E-1",
                "8.765E-2",
                "8.765E-3",
                "0E+0");

        assertFormatDescending(
                "Scientific min exponent digits",
                "scientific/+ee",
                NumberFormatter.with().notation(Notation.scientific().withMinExponentDigits(2)),
                ULocale.ENGLISH,
                "8.765E04",
                "8.765E03",
                "8.765E02",
                "8.765E01",
                "8.765E00",
                "8.765E-01",
                "8.765E-02",
                "8.765E-03",
                "0E00");

        assertFormatSingle(
                "Scientific Negative",
                "scientific",
                NumberFormatter.with().notation(Notation.scientific()),
                ULocale.ENGLISH,
                -1000000,
                "-1E6");
    }

    @Test
    public void notationCompact() {
        assertFormatDescendingBig(
                "Compact Short",
                "compact-short",
                NumberFormatter.with().notation(Notation.compactShort()),
                ULocale.ENGLISH,
                "88M",
                "8.8M",
                "876K",
                "88K",
                "8.8K",
                "876",
                "88",
                "8.8",
                "0");

        assertFormatDescendingBig(
                "Compact Long",
                "compact-long",
                NumberFormatter.with().notation(Notation.compactLong()),
                ULocale.ENGLISH,
                "88 million",
                "8.8 million",
                "876 thousand",
                "88 thousand",
                "8.8 thousand",
                "876",
                "88",
                "8.8",
                "0");

        assertFormatDescending(
                "Compact Short Currency",
                "compact-short currency/USD",
                NumberFormatter.with().notation(Notation.compactShort()).unit(USD),
                ULocale.ENGLISH,
                "$88K",
                "$8.8K",
                "$876",
                "$88",
                "$8.8",
                "$0.88",
                "$0.088",
                "$0.0088",
                "$0");

        assertFormatDescending(
                "Compact Short with ISO Currency",
                "compact-short currency/USD unit-width-iso-code",
                NumberFormatter.with().notation(Notation.compactShort()).unit(USD).unitWidth(UnitWidth.ISO_CODE),
                ULocale.ENGLISH,
                "USD 88K",
                "USD 8.8K",
                "USD 876",
                "USD 88",
                "USD 8.8",
                "USD 0.88",
                "USD 0.088",
                "USD 0.0088",
                "USD 0");

        assertFormatDescending(
                "Compact Short with Long Name Currency",
                "compact-short currency/USD unit-width-full-name",
                NumberFormatter.with().notation(Notation.compactShort()).unit(USD).unitWidth(UnitWidth.FULL_NAME),
                ULocale.ENGLISH,
                "88K US dollars",
                "8.8K US dollars",
                "876 US dollars",
                "88 US dollars",
                "8.8 US dollars",
                "0.88 US dollars",
                "0.088 US dollars",
                "0.0088 US dollars",
                "0 US dollars");

        // Note: Most locales don't have compact long currency, so this currently falls back to short.
        // This test case should be fixed when proper compact long currency patterns are added.
        assertFormatDescending(
                "Compact Long Currency",
                "compact-long currency/USD",
                NumberFormatter.with().notation(Notation.compactLong()).unit(USD),
                ULocale.ENGLISH,
                "$88K", // should be something like "$88 thousand"
                "$8.8K",
                "$876",
                "$88",
                "$8.8",
                "$0.88",
                "$0.088",
                "$0.0088",
                "$0");

        // Note: Most locales don't have compact long currency, so this currently falls back to short.
        // This test case should be fixed when proper compact long currency patterns are added.
        assertFormatDescending(
                "Compact Long with ISO Currency",
                "compact-long currency/USD unit-width-iso-code",
                NumberFormatter.with().notation(Notation.compactLong()).unit(USD).unitWidth(UnitWidth.ISO_CODE),
                ULocale.ENGLISH,
                "USD 88K", // should be something like "USD 88 thousand"
                "USD 8.8K",
                "USD 876",
                "USD 88",
                "USD 8.8",
                "USD 0.88",
                "USD 0.088",
                "USD 0.0088",
                "USD 0");

        // TODO: This behavior could be improved and should be revisited.
        assertFormatDescending(
                "Compact Long with Long Name Currency",
                "compact-long currency/USD unit-width-full-name",
                NumberFormatter.with().notation(Notation.compactLong()).unit(USD).unitWidth(UnitWidth.FULL_NAME),
                ULocale.ENGLISH,
                "88 thousand US dollars",
                "8.8 thousand US dollars",
                "876 US dollars",
                "88 US dollars",
                "8.8 US dollars",
                "0.88 US dollars",
                "0.088 US dollars",
                "0.0088 US dollars",
                "0 US dollars");

        assertFormatSingle(
                "Compact Plural One",
                "compact-long",
                NumberFormatter.with().notation(Notation.compactLong()),
                ULocale.forLanguageTag("es"),
                1000000,
                "1 millón");

        assertFormatSingle(
                "Compact Plural Other",
                "compact-long",
                NumberFormatter.with().notation(Notation.compactLong()),
                ULocale.forLanguageTag("es"),
                2000000,
                "2 millones");

        assertFormatSingle(
                "Compact with Negative Sign",
                "compact-short",
                NumberFormatter.with().notation(Notation.compactShort()),
                ULocale.ENGLISH,
                -9876543.21,
                "-9.9M");

        assertFormatSingle(
                "Compact Rounding",
                "compact-short",
                NumberFormatter.with().notation(Notation.compactShort()),
                ULocale.ENGLISH,
                990000,
                "990K");

        assertFormatSingle(
                "Compact Rounding",
                "compact-short",
                NumberFormatter.with().notation(Notation.compactShort()),
                ULocale.ENGLISH,
                999000,
                "999K");

        assertFormatSingle(
                "Compact Rounding",
                "compact-short",
                NumberFormatter.with().notation(Notation.compactShort()),
                ULocale.ENGLISH,
                999900,
                "1M");

        assertFormatSingle(
                "Compact Rounding",
                "compact-short",
                NumberFormatter.with().notation(Notation.compactShort()),
                ULocale.ENGLISH,
                9900000,
                "9.9M");

        assertFormatSingle(
                "Compact Rounding",
                "compact-short",
                NumberFormatter.with().notation(Notation.compactShort()),
                ULocale.ENGLISH,
                9990000,
                "10M");

        assertFormatSingle(
                "Compact in zh-Hant-HK",
                "compact-short",
                NumberFormatter.with().notation(Notation.compactShort()),
                new ULocale("zh-Hant-HK"),
                1e7,
                "10M");

        assertFormatSingle(
                "Compact in zh-Hant",
                "compact-short",
                NumberFormatter.with().notation(Notation.compactShort()),
                new ULocale("zh-Hant"),
                1e7,
                "1000\u842C");

        Map<String, Map<String, String>> compactCustomData = new HashMap<>();
        Map<String, String> entry = new HashMap<>();
        entry.put("one", "Kun");
        entry.put("other", "0KK");
        compactCustomData.put("1000", entry);
        assertFormatSingle(
                "Compact Somali No Figure",
                null, // feature not supported in skeleton
                NumberFormatter.with().notation(CompactNotation.forCustomData(compactCustomData)),
                ULocale.ENGLISH,
                1000,
                "Kun");
    }

    @Test
    public void unitMeasure() {
        assertFormatDescending(
                "Meters Short",
                "measure-unit/length-meter",
                NumberFormatter.with().unit(MeasureUnit.METER),
                ULocale.ENGLISH,
                "87,650 m",
                "8,765 m",
                "876.5 m",
                "87.65 m",
                "8.765 m",
                "0.8765 m",
                "0.08765 m",
                "0.008765 m",
                "0 m");

        assertFormatDescending(
                "Meters Long",
                "measure-unit/length-meter unit-width-full-name",
                NumberFormatter.with().unit(MeasureUnit.METER).unitWidth(UnitWidth.FULL_NAME),
                ULocale.ENGLISH,
                "87,650 meters",
                "8,765 meters",
                "876.5 meters",
                "87.65 meters",
                "8.765 meters",
                "0.8765 meters",
                "0.08765 meters",
                "0.008765 meters",
                "0 meters");

        assertFormatDescending(
                "Compact Meters Long",
                "compact-long measure-unit/length-meter unit-width-full-name",
                NumberFormatter.with().notation(Notation.compactLong()).unit(MeasureUnit.METER)
                        .unitWidth(UnitWidth.FULL_NAME),
                ULocale.ENGLISH,
                "88 thousand meters",
                "8.8 thousand meters",
                "876 meters",
                "88 meters",
                "8.8 meters",
                "0.88 meters",
                "0.088 meters",
                "0.0088 meters",
                "0 meters");

        assertFormatSingleMeasure(
                "Meters with Measure Input",
                "unit-width-full-name",
                NumberFormatter.with().unitWidth(UnitWidth.FULL_NAME),
                ULocale.ENGLISH,
                new Measure(5.43, MeasureUnit.METER),
                "5.43 meters");

        assertFormatSingleMeasure(
                "Measure format method takes precedence over fluent chain",
                "measure-unit/length-meter",
                NumberFormatter.with().unit(MeasureUnit.METER),
                ULocale.ENGLISH,
                new Measure(5.43, USD),
                "$5.43");

        assertFormatSingle(
                "Meters with Negative Sign",
                "measure-unit/length-meter",
                NumberFormatter.with().unit(MeasureUnit.METER),
                ULocale.ENGLISH,
                -9876543.21,
                "-9,876,543.21 m");

        // The locale string "सान" appears only in brx.txt:
        assertFormatSingle(
                "Interesting Data Fallback 1",
                "measure-unit/duration-day unit-width-full-name",
                NumberFormatter.with().unit(MeasureUnit.DAY).unitWidth(UnitWidth.FULL_NAME),
                ULocale.forLanguageTag("brx"),
                5.43,
                "5.43 सान");

        // Requires following the alias from unitsNarrow to unitsShort:
        assertFormatSingle(
                "Interesting Data Fallback 2",
                "measure-unit/duration-day unit-width-narrow",
                NumberFormatter.with().unit(MeasureUnit.DAY).unitWidth(UnitWidth.NARROW),
                ULocale.forLanguageTag("brx"),
                5.43,
                "5.43 d");

        // en_001.txt has a unitsNarrow/area/square-meter table, but table does not contain the OTHER unit,
        // requiring fallback to the root.
        assertFormatSingle(
                "Interesting Data Fallback 3",
                "measure-unit/area-square-meter unit-width-narrow",
                NumberFormatter.with().unit(MeasureUnit.SQUARE_METER).unitWidth(UnitWidth.NARROW),
                ULocale.forLanguageTag("en-GB"),
                5.43,
                "5.43 m²");

        // Try accessing a narrow unit directly from root.
        assertFormatSingle(
                "Interesting Data Fallback 4",
                "measure-unit/area-square-meter unit-width-narrow",
                NumberFormatter.with().unit(MeasureUnit.SQUARE_METER).unitWidth(UnitWidth.NARROW),
                ULocale.forLanguageTag("root"),
                5.43,
                "5.43 m²");

        // es_US has "{0}°" for unitsNarrow/temperature/FAHRENHEIT.
        // NOTE: This example is in the documentation.
        assertFormatSingle(
                "MeasureUnit Difference between Narrow and Short (Narrow Version)",
                "measure-unit/temperature-fahrenheit unit-width-narrow",
                NumberFormatter.with().unit(MeasureUnit.FAHRENHEIT).unitWidth(UnitWidth.NARROW),
                ULocale.forLanguageTag("es-US"),
                5.43,
                "5.43°");

        assertFormatSingle(
                "MeasureUnit Difference between Narrow and Short (Short Version)",
                "measure-unit/temperature-fahrenheit unit-width-short",
                NumberFormatter.with().unit(MeasureUnit.FAHRENHEIT).unitWidth(UnitWidth.SHORT),
                ULocale.forLanguageTag("es-US"),
                5.43,
                "5.43 °F");

        assertFormatSingle(
                "MeasureUnit form without {0} in CLDR pattern",
                "measure-unit/temperature-kelvin unit-width-full-name",
                NumberFormatter.with().unit(MeasureUnit.KELVIN).unitWidth(UnitWidth.FULL_NAME),
                ULocale.forLanguageTag("es-MX"),
                1,
                "kelvin");

        assertFormatSingle(
                "MeasureUnit form without {0} in CLDR pattern and wide base form",
                "measure-unit/temperature-kelvin .00000000000000000000 unit-width-full-name",
                NumberFormatter.with()
                    .precision(Precision.fixedFraction(20))
                    .unit(MeasureUnit.KELVIN)
                    .unitWidth(UnitWidth.FULL_NAME),
                ULocale.forLanguageTag("es-MX"),
                1,
                "kelvin");
    }

    @Test
    public void unitCompoundMeasure() {
        assertFormatDescending(
                "Meters Per Second Short (unit that simplifies) and perUnit method",
                "measure-unit/length-meter per-measure-unit/duration-second",
                NumberFormatter.with().unit(MeasureUnit.METER).perUnit(MeasureUnit.SECOND),
                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)",
                "measure-unit/mass-pound per-measure-unit/area-square-mile",
                NumberFormatter.with().unit(MeasureUnit.POUND).perUnit(MeasureUnit.SQUARE_MILE),
                ULocale.ENGLISH,
                "87,650 lb/mi²",
                "8,765 lb/mi²",
                "876.5 lb/mi²",
                "87.65 lb/mi²",
                "8.765 lb/mi²",
                "0.8765 lb/mi²",
                "0.08765 lb/mi²",
                "0.008765 lb/mi²",
                "0 lb/mi²");

        assertFormatDescending(
                "Joules Per Furlong Short (unit with no simplifications or special patterns)",
                "measure-unit/energy-joule per-measure-unit/length-furlong",
                NumberFormatter.with().unit(MeasureUnit.JOULE).perUnit(MeasureUnit.FURLONG),
                ULocale.ENGLISH,
                "87,650 J/fur",
                "8,765 J/fur",
                "876.5 J/fur",
                "87.65 J/fur",
                "8.765 J/fur",
                "0.8765 J/fur",
                "0.08765 J/fur",
                "0.008765 J/fur",
                "0 J/fur");
    }

    @Test
    public void unitCurrency() {
        assertFormatDescending(
                "Currency",
                "currency/GBP",
                NumberFormatter.with().unit(GBP),
                ULocale.ENGLISH,
                "£87,650.00",
                "£8,765.00",
                "£876.50",
                "£87.65",
                "£8.76",
                "£0.88",
                "£0.09",
                "£0.01",
                "£0.00");

        assertFormatDescending(
                "Currency ISO",
                "currency/GBP unit-width-iso-code",
                NumberFormatter.with().unit(GBP).unitWidth(UnitWidth.ISO_CODE),
                ULocale.ENGLISH,
                "GBP 87,650.00",
                "GBP 8,765.00",
                "GBP 876.50",
                "GBP 87.65",
                "GBP 8.76",
                "GBP 0.88",
                "GBP 0.09",
                "GBP 0.01",
                "GBP 0.00");

        assertFormatDescending(
                "Currency Long Name",
                "currency/GBP unit-width-full-name",
                NumberFormatter.with().unit(GBP).unitWidth(UnitWidth.FULL_NAME),
                ULocale.ENGLISH,
                "87,650.00 British pounds",
                "8,765.00 British pounds",
                "876.50 British pounds",
                "87.65 British pounds",
                "8.76 British pounds",
                "0.88 British pounds",
                "0.09 British pounds",
                "0.01 British pounds",
                "0.00 British pounds");

        assertFormatDescending(
                "Currency Hidden",
                "currency/GBP unit-width-hidden",
                NumberFormatter.with().unit(GBP).unitWidth(UnitWidth.HIDDEN),
                ULocale.ENGLISH,
                "87,650.00",
                "8,765.00",
                "876.50",
                "87.65",
                "8.76",
                "0.88",
                "0.09",
                "0.01",
                "0.00");

        assertFormatSingleMeasure(
                "Currency with CurrencyAmount Input",
                "",
                NumberFormatter.with(),
                ULocale.ENGLISH,
                new CurrencyAmount(5.43, GBP),
                "£5.43");

        assertFormatSingle(
                "Currency Long Name from Pattern Syntax",
                null,
                NumberFormatter.fromDecimalFormat(
                        PatternStringParser.parseToProperties("0 ¤¤¤"),
                        DecimalFormatSymbols.getInstance(ULocale.ENGLISH),
                        null).unit(GBP),
                ULocale.ENGLISH,
                1234567.89,
                "1234568 British pounds");

        assertFormatSingle(
                "Currency with Negative Sign",
                "currency/GBP",
                NumberFormatter.with().unit(GBP),
                ULocale.ENGLISH,
                -9876543.21,
                "-£9,876,543.21");

        // The full currency symbol is not shown in NARROW format.
        // NOTE: This example is in the documentation.
        assertFormatSingle(
                "Currency Difference between Narrow and Short (Narrow Version)",
                "currency/USD unit-width-narrow",
                NumberFormatter.with().unit(USD).unitWidth(UnitWidth.NARROW),
                ULocale.forLanguageTag("en-CA"),
                5.43,
                "$5.43");

        assertFormatSingle(
                "Currency Difference between Narrow and Short (Short Version)",
                "currency/USD unit-width-short",
                NumberFormatter.with().unit(USD).unitWidth(UnitWidth.SHORT),
                ULocale.forLanguageTag("en-CA"),
                5.43,
                "US$5.43");

        assertFormatSingle(
                "Currency-dependent format (Control)",
                "currency/USD unit-width-short",
                NumberFormatter.with().unit(USD).unitWidth(UnitWidth.SHORT),
                ULocale.forLanguageTag("ca"),
                444444.55,
                "444.444,55 USD");

        assertFormatSingle(
                "Currency-dependent format (Test)",
                "currency/ESP unit-width-short",
                NumberFormatter.with().unit(ESP).unitWidth(UnitWidth.SHORT),
                ULocale.forLanguageTag("ca"),
                444444.55,
                "₧ 444.445");

        assertFormatSingle(
                "Currency-dependent symbols (Control)",
                "currency/USD unit-width-short",
                NumberFormatter.with().unit(USD).unitWidth(UnitWidth.SHORT),
                ULocale.forLanguageTag("pt-PT"),
                444444.55,
                "444 444,55 US$");

        // NOTE: This is a bit of a hack on CLDR's part. They set the currency symbol to U+200B (zero-
        // width space), and they set the decimal separator to the $ symbol.
        assertFormatSingle(
                "Currency-dependent symbols (Test Short)",
                "currency/PTE unit-width-short",
                NumberFormatter.with().unit(PTE).unitWidth(UnitWidth.SHORT),
                ULocale.forLanguageTag("pt-PT"),
                444444.55,
                "444,444$55 \u200B");

        assertFormatSingle(
                "Currency-dependent symbols (Test Narrow)",
                "currency/PTE unit-width-narrow",
                NumberFormatter.with().unit(PTE).unitWidth(UnitWidth.NARROW),
                ULocale.forLanguageTag("pt-PT"),
                444444.55,
                "444,444$55 \u200B");

        assertFormatSingle(
                "Currency-dependent symbols (Test ISO Code)",
                "currency/PTE unit-width-iso-code",
                NumberFormatter.with().unit(PTE).unitWidth(UnitWidth.ISO_CODE),
                ULocale.forLanguageTag("pt-PT"),
                444444.55,
                "444,444$55 PTE");

        assertFormatSingle(
                "Plural form depending on visible digits (ICU-20499)",
                "currency/RON unit-width-full-name",
                NumberFormatter.with().unit(RON).unitWidth(UnitWidth.FULL_NAME),
                ULocale.forLanguageTag("ro-RO"),
                24,
                "24,00 lei românești");
    }

    @Test
    public void unitPercent() {
        assertFormatDescending(
                "Percent",
                "percent",
                NumberFormatter.with().unit(NoUnit.PERCENT),
                ULocale.ENGLISH,
                "87,650%",
                "8,765%",
                "876.5%",
                "87.65%",
                "8.765%",
                "0.8765%",
                "0.08765%",
                "0.008765%",
                "0%");

        assertFormatDescending(
                "Permille",
                "permille",
                NumberFormatter.with().unit(NoUnit.PERMILLE),
                ULocale.ENGLISH,
                "87,650‰",
                "8,765‰",
                "876.5‰",
                "87.65‰",
                "8.765‰",
                "0.8765‰",
                "0.08765‰",
                "0.008765‰",
                "0‰");

        assertFormatSingle(
                "NoUnit Base",
                "base-unit",
                NumberFormatter.with().unit(NoUnit.BASE),
                ULocale.ENGLISH,
                51423,
                "51,423");

        assertFormatSingle(
                "Percent with Negative Sign",
                "percent",
                NumberFormatter.with().unit(NoUnit.PERCENT),
                ULocale.ENGLISH,
                -98.7654321,
                "-98.765432%");
    }

    @Test
    public void roundingFraction() {
        assertFormatDescending(
                "Integer",
                "precision-integer",
                NumberFormatter.with().precision(Precision.integer()),
                ULocale.ENGLISH,
                "87,650",
                "8,765",
                "876",
                "88",
                "9",
                "1",
                "0",
                "0",
                "0");

        assertFormatDescending(
                "Fixed Fraction",
                ".000",
                NumberFormatter.with().precision(Precision.fixedFraction(3)),
                ULocale.ENGLISH,
                "87,650.000",
                "8,765.000",
                "876.500",
                "87.650",
                "8.765",
                "0.876",
                "0.088",
                "0.009",
                "0.000");

        assertFormatDescending(
                "Min Fraction",
                ".0+",
                NumberFormatter.with().precision(Precision.minFraction(1)),
                ULocale.ENGLISH,
                "87,650.0",
                "8,765.0",
                "876.5",
                "87.65",
                "8.765",
                "0.8765",
                "0.08765",
                "0.008765",
                "0.0");

        assertFormatDescending(
                "Max Fraction",
                ".#",
                NumberFormatter.with().precision(Precision.maxFraction(1)),
                ULocale.ENGLISH,
                "87,650",
                "8,765",
                "876.5",
                "87.6",
                "8.8",
                "0.9",
                "0.1",
                "0",
                "0");

        assertFormatDescending(
                "Min/Max Fraction",
                ".0##",
                NumberFormatter.with().precision(Precision.minMaxFraction(1, 3)),
                ULocale.ENGLISH,
                "87,650.0",
                "8,765.0",
                "876.5",
                "87.65",
                "8.765",
                "0.876",
                "0.088",
                "0.009",
                "0.0");
    }

    @Test
    public void roundingFigures() {
        assertFormatSingle(
                "Fixed Significant",
                "@@@",
                NumberFormatter.with().precision(Precision.fixedSignificantDigits(3)),
                ULocale.ENGLISH,
                -98,
                "-98.0");

        assertFormatSingle(
                "Fixed Significant Rounding",
                "@@@",
                NumberFormatter.with().precision(Precision.fixedSignificantDigits(3)),
                ULocale.ENGLISH,
                -98.7654321,
                "-98.8");

        assertFormatSingle(
                "Fixed Significant Zero",
                "@@@",
                NumberFormatter.with().precision(Precision.fixedSignificantDigits(3)),
                ULocale.ENGLISH,
                0,
                "0.00");

        assertFormatSingle(
                "Min Significant",
                "@@+",
                NumberFormatter.with().precision(Precision.minSignificantDigits(2)),
                ULocale.ENGLISH,
                -9,
                "-9.0");

        assertFormatSingle(
                "Max Significant",
                "@###",
                NumberFormatter.with().precision(Precision.maxSignificantDigits(4)),
                ULocale.ENGLISH,
                98.7654321,
                "98.77");

        assertFormatSingle(
                "Min/Max Significant",
                "@@@#",
                NumberFormatter.with().precision(Precision.minMaxSignificantDigits(3, 4)),
                ULocale.ENGLISH,
                9.99999,
                "10.0");

        assertFormatSingle(
                "Fixed Significant on zero with zero integer width",
                "@ integer-width/+",
                NumberFormatter.with().precision(Precision.fixedSignificantDigits(1)).integerWidth(IntegerWidth.zeroFillTo(0)),
                ULocale.ENGLISH,
                0,
                "0");

        assertFormatSingle(
                "Fixed Significant on zero with lots of integer width",
                "@ integer-width/+000",
                NumberFormatter.with().precision(Precision.fixedSignificantDigits(1)).integerWidth(IntegerWidth.zeroFillTo(3)),
                ULocale.ENGLISH,
                0,
                "000");
    }

    @Test
    public void roundingFractionFigures() {
        assertFormatDescending(
                "Basic Significant", // for comparison
                "@#",
                NumberFormatter.with().precision(Precision.maxSignificantDigits(2)),
                ULocale.ENGLISH,
                "88,000",
                "8,800",
                "880",
                "88",
                "8.8",
                "0.88",
                "0.088",
                "0.0088",
                "0");

        assertFormatDescending(
                "FracSig minMaxFrac minSig",
                ".0#/@@@+",
                NumberFormatter.with().precision(Precision.minMaxFraction(1, 2).withMinDigits(3)),
                ULocale.ENGLISH,
                "87,650.0",
                "8,765.0",
                "876.5",
                "87.65",
                "8.76",
                "0.876", // minSig beats maxFrac
                "0.0876", // minSig beats maxFrac
                "0.00876", // minSig beats maxFrac
                "0.0");

        assertFormatDescending(
                "FracSig minMaxFrac maxSig A",
                ".0##/@#",
                NumberFormatter.with().precision(Precision.minMaxFraction(1, 3).withMaxDigits(2)),
                ULocale.ENGLISH,
                "88,000.0", // maxSig beats maxFrac
                "8,800.0", // maxSig beats maxFrac
                "880.0", // maxSig beats maxFrac
                "88.0", // maxSig beats maxFrac
                "8.8", // maxSig beats maxFrac
                "0.88", // maxSig beats maxFrac
                "0.088",
                "0.009",
                "0.0");

        assertFormatDescending(
                "FracSig minMaxFrac maxSig B",
                ".00/@#",
                NumberFormatter.with().precision(Precision.fixedFraction(2).withMaxDigits(2)),
                ULocale.ENGLISH,
                "88,000.00", // maxSig beats maxFrac
                "8,800.00", // maxSig beats maxFrac
                "880.00", // maxSig beats maxFrac
                "88.00", // maxSig beats maxFrac
                "8.80", // maxSig beats maxFrac
                "0.88",
                "0.09",
                "0.01",
                "0.00");

        assertFormatDescending(
                "FracSig minFrac maxSig",
                ".0+/@#",
                NumberFormatter.with().precision(Precision.minFraction(1).withMaxDigits(2)),
                ULocale.ENGLISH,
                "88,000.0",
                "8,800.0",
                "880.0",
                "88.0",
                "8.8",
                "0.88",
                "0.088",
                "0.0088",
                "0.0");

        assertFormatSingle(
                "FracSig with trailing zeros A",
                ".00/@@@+",
                NumberFormatter.with().precision(Precision.fixedFraction(2).withMinDigits(3)),
                ULocale.ENGLISH,
                0.1,
                "0.10");

        assertFormatSingle(
                "FracSig with trailing zeros B",
                ".00/@@@+",
                NumberFormatter.with().precision(Precision.fixedFraction(2).withMinDigits(3)),
                ULocale.ENGLISH,
                0.0999999,
                "0.10");
    }

    @Test
    public void roundingOther() {
        assertFormatDescending(
                "Rounding None",
                "precision-unlimited",
                NumberFormatter.with().precision(Precision.unlimited()),
                ULocale.ENGLISH,
                "87,650",
                "8,765",
                "876.5",
                "87.65",
                "8.765",
                "0.8765",
                "0.08765",
                "0.008765",
                "0");

        assertFormatDescending(
                "Increment",
                "precision-increment/0.5",
                NumberFormatter.with().precision(Precision.increment(BigDecimal.valueOf(0.5))),
                ULocale.ENGLISH,
                "87,650.0",
                "8,765.0",
                "876.5",
                "87.5",
                "9.0",
                "1.0",
                "0.0",
                "0.0",
                "0.0");

        assertFormatDescending(
                "Increment with Min Fraction",
                "precision-increment/0.50",
                NumberFormatter.with().precision(Precision.increment(new BigDecimal("0.50"))),
                ULocale.ENGLISH,
                "87,650.00",
                "8,765.00",
                "876.50",
                "87.50",
                "9.00",
                "1.00",
                "0.00",
                "0.00",
                "0.00");

        assertFormatDescending(
                "Strange Increment",
                "precision-increment/3.140",
                NumberFormatter.with().precision(Precision.increment(new BigDecimal("3.140"))),
                ULocale.ENGLISH,
                "87,649.960",
                "8,763.740",
                "876.060",
                "87.920",
                "9.420",
                "0.000",
                "0.000",
                "0.000",
                "0.000");

        assertFormatDescending(
                "Increment Resolving to Power of 10",
                "precision-increment/0.010",
                NumberFormatter.with().precision(Precision.increment(new BigDecimal("0.010"))),
                ULocale.ENGLISH,
                "87,650.000",
                "8,765.000",
                "876.500",
                "87.650",
                "8.760",
                "0.880",
                "0.090",
                "0.010",
                "0.000");

        assertFormatDescending(
                "Currency Standard",
                "currency/CZK precision-currency-standard",
                NumberFormatter.with().precision(Precision.currency(CurrencyUsage.STANDARD)).unit(CZK),
                ULocale.ENGLISH,
                "CZK 87,650.00",
                "CZK 8,765.00",
                "CZK 876.50",
                "CZK 87.65",
                "CZK 8.76",
                "CZK 0.88",
                "CZK 0.09",
                "CZK 0.01",
                "CZK 0.00");

        assertFormatDescending(
                "Currency Cash",
                "currency/CZK precision-currency-cash",
                NumberFormatter.with().precision(Precision.currency(CurrencyUsage.CASH)).unit(CZK),
                ULocale.ENGLISH,
                "CZK 87,650",
                "CZK 8,765",
                "CZK 876",
                "CZK 88",
                "CZK 9",
                "CZK 1",
                "CZK 0",
                "CZK 0",
                "CZK 0");

        assertFormatDescending(
                "Currency Cash with Nickel Rounding",
                "currency/CAD precision-currency-cash",
                NumberFormatter.with().precision(Precision.currency(CurrencyUsage.CASH)).unit(CAD),
                ULocale.ENGLISH,
                "CA$87,650.00",
                "CA$8,765.00",
                "CA$876.50",
                "CA$87.65",
                "CA$8.75",
                "CA$0.90",
                "CA$0.10",
                "CA$0.00",
                "CA$0.00");

        assertFormatDescending(
                "Currency not in top-level fluent chain",
                "precision-integer", // calling .withCurrency() applies currency rounding rules immediately
                NumberFormatter.with().precision(Precision.currency(CurrencyUsage.CASH).withCurrency(CZK)),
                ULocale.ENGLISH,
                "87,650",
                "8,765",
                "876",
                "88",
                "9",
                "1",
                "0",
                "0",
                "0");

        // NOTE: Other tests cover the behavior of the other rounding modes.
        assertFormatDescending(
                "Rounding Mode CEILING",
                "precision-integer rounding-mode-ceiling",
                NumberFormatter.with().precision(Precision.integer()).roundingMode(RoundingMode.CEILING),
                ULocale.ENGLISH,
                "87,650",
                "8,765",
                "877",
                "88",
                "9",
                "1",
                "1",
                "1",
                "0");
    }

    @Test
    public void grouping() {
        assertFormatDescendingBig(
                "Western Grouping",
                "group-auto",
                NumberFormatter.with().grouping(GroupingStrategy.AUTO),
                ULocale.ENGLISH,
                "87,650,000",
                "8,765,000",
                "876,500",
                "87,650",
                "8,765",
                "876.5",
                "87.65",
                "8.765",
                "0");

        assertFormatDescendingBig(
                "Indic Grouping",
                "group-auto",
                NumberFormatter.with().grouping(GroupingStrategy.AUTO),
                new ULocale("en-IN"),
                "8,76,50,000",
                "87,65,000",
                "8,76,500",
                "87,650",
                "8,765",
                "876.5",
                "87.65",
                "8.765",
                "0");

        assertFormatDescendingBig(
                "Western Grouping, Min 2",
                "group-min2",
                NumberFormatter.with().grouping(GroupingStrategy.MIN2),
                ULocale.ENGLISH,
                "87,650,000",
                "8,765,000",
                "876,500",
                "87,650",
                "8765",
                "876.5",
                "87.65",
                "8.765",
                "0");

        assertFormatDescendingBig(
                "Indic Grouping, Min 2",
                "group-min2",
                NumberFormatter.with().grouping(GroupingStrategy.MIN2),
                new ULocale("en-IN"),
                "8,76,50,000",
                "87,65,000",
                "8,76,500",
                "87,650",
                "8765",
                "876.5",
                "87.65",
                "8.765",
                "0");

        assertFormatDescendingBig(
                "No Grouping",
                "group-off",
                NumberFormatter.with().grouping(GroupingStrategy.OFF),
                new ULocale("en-IN"),
                "87650000",
                "8765000",
                "876500",
                "87650",
                "8765",
                "876.5",
                "87.65",
                "8.765",
                "0");

        assertFormatDescendingBig(
                "Indic locale with THOUSANDS grouping",
                "group-thousands",
                NumberFormatter.with().grouping(GroupingStrategy.THOUSANDS),
                new ULocale("en-IN"),
                "87,650,000",
                "8,765,000",
                "876,500",
                "87,650",
                "8,765",
                "876.5",
                "87.65",
                "8.765",
                "0");

        // NOTE: Hungarian is interesting because it has minimumGroupingDigits=4 in locale data
        // If this test breaks due to data changes, find another locale that has minimumGroupingDigits.
        assertFormatDescendingBig(
                "Hungarian Grouping",
                "group-auto",
                NumberFormatter.with().grouping(GroupingStrategy.AUTO),
                new ULocale("hu"),
                "87 650 000",
                "8 765 000",
                "876500",
                "87650",
                "8765",
                "876,5",
                "87,65",
                "8,765",
                "0");

        assertFormatDescendingBig(
                "Hungarian Grouping, Min 2",
                "group-min2",
                NumberFormatter.with().grouping(GroupingStrategy.MIN2),
                new ULocale("hu"),
                "87 650 000",
                "8 765 000",
                "876500",
                "87650",
                "8765",
                "876,5",
                "87,65",
                "8,765",
                "0");

        assertFormatDescendingBig(
                "Hungarian Grouping, Always",
                "group-on-aligned",
                NumberFormatter.with().grouping(GroupingStrategy.ON_ALIGNED),
                new ULocale("hu"),
                "87 650 000",
                "8 765 000",
                "876 500",
                "87 650",
                "8 765",
                "876,5",
                "87,65",
                "8,765",
                "0");

        // NOTE: Bulgarian is interesting because it has no grouping in the default currency format.
        // If this test breaks due to data changes, find another locale that has no default grouping.
        assertFormatDescendingBig(
                "Bulgarian Currency Grouping",
                "currency/USD group-auto",
                NumberFormatter.with().grouping(GroupingStrategy.AUTO).unit(USD),
                new ULocale("bg"),
                "87650000,00 щ.д.",
                "8765000,00 щ.д.",
                "876500,00 щ.д.",
                "87650,00 щ.д.",
                "8765,00 щ.д.",
                "876,50 щ.д.",
                "87,65 щ.д.",
                "8,76 щ.д.",
                "0,00 щ.д.");

        assertFormatDescendingBig(
                "Bulgarian Currency Grouping, Always",
                "currency/USD group-on-aligned",
                NumberFormatter.with().grouping(GroupingStrategy.ON_ALIGNED).unit(USD),
                new ULocale("bg"),
                "87 650 000,00 щ.д.",
                "8 765 000,00 щ.д.",
                "876 500,00 щ.д.",
                "87 650,00 щ.д.",
                "8 765,00 щ.д.",
                "876,50 щ.д.",
                "87,65 щ.д.",
                "8,76 щ.д.",
                "0,00 щ.д.");

        MacroProps macros = new MacroProps();
        macros.grouping = Grouper.getInstance((short) 4, (short) 1, (short) 3);
        assertFormatDescendingBig(
                "Custom Grouping via Internal API",
                null,
                NumberFormatter.with().macros(macros),
                ULocale.ENGLISH,
                "8,7,6,5,0000",
                "8,7,6,5000",
                "876500",
                "87650",
                "8765",
                "876.5",
                "87.65",
                "8.765",
                "0");
    }

    @Test
    public void padding() {
        assertFormatDescending(
                "Padding",
                null,
                NumberFormatter.with().padding(Padder.none()),
                ULocale.ENGLISH,
                "87,650",
                "8,765",
                "876.5",
                "87.65",
                "8.765",
                "0.8765",
                "0.08765",
                "0.008765",
                "0");

        assertFormatDescending(
                "Padding",
                null,
                NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX)),
                ULocale.ENGLISH,
                "**87,650",
                "***8,765",
                "***876.5",
                "***87.65",
                "***8.765",
                "**0.8765",
                "*0.08765",
                "0.008765",
                "*******0");

        assertFormatDescending(
                "Padding with code points",
                null,
                NumberFormatter.with().padding(Padder.codePoints(0x101E4, 8, PadPosition.AFTER_PREFIX)),
                ULocale.ENGLISH,
                "𐇤𐇤87,650",
                "𐇤𐇤𐇤8,765",
                "𐇤𐇤𐇤876.5",
                "𐇤𐇤𐇤87.65",
                "𐇤𐇤𐇤8.765",
                "𐇤𐇤0.8765",
                "𐇤0.08765",
                "0.008765",
                "𐇤𐇤𐇤𐇤𐇤𐇤𐇤0");

        assertFormatDescending(
                "Padding with wide digits",
                null,
                NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX))
                        .symbols(NumberingSystem.getInstanceByName("mathsanb")),
                ULocale.ENGLISH,
                "**𝟴𝟳,𝟲𝟱𝟬",
                "***𝟴,𝟳𝟲𝟱",
                "***𝟴𝟳𝟲.𝟱",
                "***𝟴𝟳.𝟲𝟱",
                "***𝟴.𝟳𝟲𝟱",
                "**𝟬.𝟴𝟳𝟲𝟱",
                "*𝟬.𝟬𝟴𝟳𝟲𝟱",
                "𝟬.𝟬𝟬𝟴𝟳𝟲𝟱",
                "*******𝟬");

        assertFormatDescending(
                "Padding with currency spacing",
                null,
                NumberFormatter.with().padding(Padder.codePoints('*', 10, PadPosition.AFTER_PREFIX)).unit(GBP)
                        .unitWidth(UnitWidth.ISO_CODE),
                ULocale.ENGLISH,
                "GBP 87,650.00",
                "GBP 8,765.00",
                "GBP*876.50",
                "GBP**87.65",
                "GBP***8.76",
                "GBP***0.88",
                "GBP***0.09",
                "GBP***0.01",
                "GBP***0.00");

        assertFormatSingle(
                "Pad Before Prefix",
                null,
                NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.BEFORE_PREFIX)),
                ULocale.ENGLISH,
                -88.88,
                "**-88.88");

        assertFormatSingle(
                "Pad After Prefix",
                null,
                NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX)),
                ULocale.ENGLISH,
                -88.88,
                "-**88.88");

        assertFormatSingle(
                "Pad Before Suffix",
                null,
                NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.BEFORE_SUFFIX))
                        .unit(NoUnit.PERCENT),
                ULocale.ENGLISH,
                88.88,
                "88.88**%");

        assertFormatSingle(
                "Pad After Suffix",
                null,
                NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_SUFFIX))
                        .unit(NoUnit.PERCENT),
                ULocale.ENGLISH,
                88.88,
                "88.88%**");

        assertFormatSingle(
                "Currency Spacing with Zero Digit Padding Broken",
                null,
                NumberFormatter.with().padding(Padder.codePoints('0', 12, PadPosition.AFTER_PREFIX)).unit(GBP)
                        .unitWidth(UnitWidth.ISO_CODE),
                ULocale.ENGLISH,
                514.23,
                "GBP 000514.23"); // TODO: This is broken; it renders too wide (13 instead of 12).
    }

    @Test
    public void integerWidth() {
        assertFormatDescending(
                "Integer Width Default",
                "integer-width/+0",
                NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(1)),
                ULocale.ENGLISH,
                "87,650",
                "8,765",
                "876.5",
                "87.65",
                "8.765",
                "0.8765",
                "0.08765",
                "0.008765",
                "0");

        assertFormatDescending(
                "Integer Width Zero Fill 0",
                "integer-width/+",
                NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(0)),
                ULocale.ENGLISH,
                "87,650",
                "8,765",
                "876.5",
                "87.65",
                "8.765",
                ".8765",
                ".08765",
                ".008765",
                ""); // TODO: Avoid the empty string here?

        assertFormatDescending(
                "Integer Width Zero Fill 3",
                "integer-width/+000",
                NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(3)),
                ULocale.ENGLISH,
                "87,650",
                "8,765",
                "876.5",
                "087.65",
                "008.765",
                "000.8765",
                "000.08765",
                "000.008765",
                "000");

        assertFormatDescending(
                "Integer Width Max 3",
                "integer-width/##0",
                NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(1).truncateAt(3)),
                ULocale.ENGLISH,
                "650",
                "765",
                "876.5",
                "87.65",
                "8.765",
                "0.8765",
                "0.08765",
                "0.008765",
                "0");

        assertFormatDescending(
                "Integer Width Fixed 2",
                "integer-width/00",
                NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(2).truncateAt(2)),
                ULocale.ENGLISH,
                "50",
                "65",
                "76.5",
                "87.65",
                "08.765",
                "00.8765",
                "00.08765",
                "00.008765",
                "00");

        assertFormatSingle(
                "Integer Width Remove All A",
                "integer-width/00",
                NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(2).truncateAt(2)),
                ULocale.ENGLISH,
                2500,
                "00");

        assertFormatSingle(
                "Integer Width Remove All B",
                "integer-width/00",
                NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(2).truncateAt(2)),
                ULocale.ENGLISH,
                25000,
                "00");

        assertFormatSingle(
                "Integer Width Remove All B, Bytes Mode",
                "integer-width/00",
                NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(2).truncateAt(2)),
                ULocale.ENGLISH,
                // Note: this double produces all 17 significant digits
                10000000000000002000.0,
                "00");
    }

    @Test
    public void symbols() {
        assertFormatDescending(
                "French Symbols with Japanese Data 1",
                null,
                NumberFormatter.with().symbols(DecimalFormatSymbols.getInstance(ULocale.FRENCH)),
                ULocale.JAPAN,
                "87\u202F650",
                "8\u202F765",
                "876,5",
                "87,65",
                "8,765",
                "0,8765",
                "0,08765",
                "0,008765",
                "0");

        assertFormatSingle(
                "French Symbols with Japanese Data 2",
                null,
                NumberFormatter.with().notation(Notation.compactShort())
                        .symbols(DecimalFormatSymbols.getInstance(ULocale.FRENCH)),
                ULocale.JAPAN,
                12345,
                "1,2\u4E07");

        assertFormatDescending(
                "Latin Numbering System with Arabic Data",
                "currency/USD latin",
                NumberFormatter.with().symbols(NumberingSystem.LATIN).unit(USD),
                new ULocale("ar"),
                "US$ 87,650.00",
                "US$ 8,765.00",
                "US$ 876.50",
                "US$ 87.65",
                "US$ 8.76",
                "US$ 0.88",
                "US$ 0.09",
                "US$ 0.01",
                "US$ 0.00");

        assertFormatDescending(
                "Math Numbering System with French Data",
                "numbering-system/mathsanb",
                NumberFormatter.with().symbols(NumberingSystem.getInstanceByName("mathsanb")),
                ULocale.FRENCH,
                "𝟴𝟳\u202f𝟲𝟱𝟬",
                "𝟴\u202f𝟳𝟲𝟱",
                "𝟴𝟳𝟲,𝟱",
                "𝟴𝟳,𝟲𝟱",
                "𝟴,𝟳𝟲𝟱",
                "𝟬,𝟴𝟳𝟲𝟱",
                "𝟬,𝟬𝟴𝟳𝟲𝟱",
                "𝟬,𝟬𝟬𝟴𝟳𝟲𝟱",
                "𝟬");

        assertFormatSingle(
                "Swiss Symbols (used in documentation)",
                null,
                NumberFormatter.with().symbols(DecimalFormatSymbols.getInstance(new ULocale("de-CH"))),
                ULocale.ENGLISH,
                12345.67,
                "12’345.67");

        assertFormatSingle(
                "Myanmar Symbols (used in documentation)",
                null,
                NumberFormatter.with().symbols(DecimalFormatSymbols.getInstance(new ULocale("my_MY"))),
                ULocale.ENGLISH,
                12345.67,
                "\u1041\u1042,\u1043\u1044\u1045.\u1046\u1047");

        // NOTE: Locale ar puts ¤ after the number in NS arab but before the number in NS latn.

        assertFormatSingle(
                "Currency symbol should precede number in ar with NS latn",
                "currency/USD latin",
                NumberFormatter.with().symbols(NumberingSystem.LATIN).unit(USD),
                new ULocale("ar"),
                12345.67,
                "US$ 12,345.67");

        assertFormatSingle(
                "Currency symbol should precede number in ar@numbers=latn",
                "currency/USD",
                NumberFormatter.with().unit(USD),
                new ULocale("ar@numbers=latn"),
                12345.67,
                "US$ 12,345.67");

        assertFormatSingle(
                "Currency symbol should follow number in ar-EG with NS arab",
                "currency/USD",
                NumberFormatter.with().unit(USD),
                new ULocale("ar-EG"),
                12345.67,
                "١٢٬٣٤٥٫٦٧ US$");

        assertFormatSingle(
                "Currency symbol should follow number in ar@numbers=arab",
                "currency/USD",
                NumberFormatter.with().unit(USD),
                new ULocale("ar@numbers=arab"),
                12345.67,
                "١٢٬٣٤٥٫٦٧ US$");

        assertFormatSingle(
                "NumberingSystem in API should win over @numbers keyword",
                "currency/USD latin",
                NumberFormatter.with().symbols(NumberingSystem.LATIN).unit(USD),
                new ULocale("ar@numbers=arab"),
                12345.67,
                "US$ 12,345.67");

        assertEquals("NumberingSystem in API should win over @numbers keyword in reverse order",
                "US$ 12,345.67",
                NumberFormatter.withLocale(new ULocale("ar@numbers=arab"))
                    .symbols(NumberingSystem.LATIN)
                    .unit(USD)
                    .format(12345.67)
                    .toString());

        DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(new ULocale("de-CH"));
        UnlocalizedNumberFormatter f = NumberFormatter.with().symbols(symbols);
        symbols.setGroupingSeparatorString("!");
        assertFormatSingle(
                "Symbols object should be copied",
                null,
                f,
                ULocale.ENGLISH,
                12345.67,
                "12’345.67");

        assertFormatSingle(
                "The last symbols setter wins",
                "latin",
                NumberFormatter.with().symbols(symbols).symbols(NumberingSystem.LATIN),
                ULocale.ENGLISH,
                12345.67,
                "12,345.67");

        assertFormatSingle(
                "The last symbols setter wins",
                null,
                NumberFormatter.with().symbols(NumberingSystem.LATIN).symbols(symbols),
                ULocale.ENGLISH,
                12345.67,
                "12!345.67");
    }

    @Test
    @Ignore("This feature is not currently available.")
    public void symbolsOverride() {
        DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(ULocale.ENGLISH);
        dfs.setCurrencySymbol("@");
        dfs.setInternationalCurrencySymbol("foo");
        assertFormatSingle(
                "Custom Short Currency Symbol",
                "$XXX",
                NumberFormatter.with().unit(Currency.getInstance("XXX")).symbols(dfs),
                ULocale.ENGLISH,
                12.3,
                "@ 12.30");
    }

    @Test
    public void sign() {
        assertFormatSingle(
                "Sign Auto Positive",
                "sign-auto",
                NumberFormatter.with().sign(SignDisplay.AUTO),
                ULocale.ENGLISH,
                444444,
                "444,444");

        assertFormatSingle(
                "Sign Auto Negative",
                "sign-auto",
                NumberFormatter.with().sign(SignDisplay.AUTO),
                ULocale.ENGLISH,
                -444444,
                "-444,444");

        assertFormatSingle(
                "Sign Auto Zero",
                "sign-auto",
                NumberFormatter.with().sign(SignDisplay.AUTO),
                ULocale.ENGLISH,
                0,
                "0");

        assertFormatSingle(
                "Sign Always Positive",
                "sign-always",
                NumberFormatter.with().sign(SignDisplay.ALWAYS),
                ULocale.ENGLISH,
                444444,
                "+444,444");

        assertFormatSingle(
                "Sign Always Negative",
                "sign-always",
                NumberFormatter.with().sign(SignDisplay.ALWAYS),
                ULocale.ENGLISH,
                -444444,
                "-444,444");

        assertFormatSingle(
                "Sign Always Zero",
                "sign-always",
                NumberFormatter.with().sign(SignDisplay.ALWAYS),
                ULocale.ENGLISH,
                0,
                "+0");

        assertFormatSingle(
                "Sign Never Positive",
                "sign-never",
                NumberFormatter.with().sign(SignDisplay.NEVER),
                ULocale.ENGLISH,
                444444,
                "444,444");

        assertFormatSingle(
                "Sign Never Negative",
                "sign-never",
                NumberFormatter.with().sign(SignDisplay.NEVER),
                ULocale.ENGLISH,
                -444444,
                "444,444");

        assertFormatSingle(
                "Sign Never Zero",
                "sign-never",
                NumberFormatter.with().sign(SignDisplay.NEVER),
                ULocale.ENGLISH,
                0,
                "0");

        assertFormatSingle(
                "Sign Accounting Positive",
                "currency/USD sign-accounting",
                NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD),
                ULocale.ENGLISH,
                444444,
                "$444,444.00");

        assertFormatSingle(
                "Sign Accounting Negative",
                "currency/USD sign-accounting",
                NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD),
                ULocale.ENGLISH,
                -444444,
                "($444,444.00)");

        assertFormatSingle(
                "Sign Accounting Zero",
                "currency/USD sign-accounting",
                NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD),
                ULocale.ENGLISH,
                0,
                "$0.00");

        assertFormatSingle(
                "Sign Accounting-Always Positive",
                "currency/USD sign-accounting-always",
                NumberFormatter.with().sign(SignDisplay.ACCOUNTING_ALWAYS).unit(USD),
                ULocale.ENGLISH,
                444444,
                "+$444,444.00");

        assertFormatSingle(
                "Sign Accounting-Always Negative",
                "currency/USD sign-accounting-always",
                NumberFormatter.with().sign(SignDisplay.ACCOUNTING_ALWAYS).unit(USD),
                ULocale.ENGLISH,
                -444444,
                "($444,444.00)");

        assertFormatSingle(
                "Sign Accounting-Always Zero",
                "currency/USD sign-accounting-always",
                NumberFormatter.with().sign(SignDisplay.ACCOUNTING_ALWAYS).unit(USD),
                ULocale.ENGLISH,
                0,
                "+$0.00");

        assertFormatSingle(
                "Sign Except-Zero Positive",
                "sign-except-zero",
                NumberFormatter.with().sign(SignDisplay.EXCEPT_ZERO),
                ULocale.ENGLISH,
                444444,
                "+444,444");

        assertFormatSingle(
                "Sign Except-Zero Negative",
                "sign-except-zero",
                NumberFormatter.with().sign(SignDisplay.EXCEPT_ZERO),
                ULocale.ENGLISH,
                -444444,
                "-444,444");

        assertFormatSingle(
                "Sign Except-Zero Zero",
                "sign-except-zero",
                NumberFormatter.with().sign(SignDisplay.EXCEPT_ZERO),
                ULocale.ENGLISH,
                0,
                "0");

        assertFormatSingle(
                "Sign Accounting-Except-Zero Positive",
                "currency/USD sign-accounting-except-zero",
                NumberFormatter.with().sign(SignDisplay.ACCOUNTING_EXCEPT_ZERO).unit(USD),
                ULocale.ENGLISH,
                444444,
                "+$444,444.00");

        assertFormatSingle(
                "Sign Accounting-Except-Zero Negative",
                "currency/USD sign-accounting-except-zero",
                NumberFormatter.with().sign(SignDisplay.ACCOUNTING_EXCEPT_ZERO).unit(USD),
                ULocale.ENGLISH,
                -444444,
                "($444,444.00)");

        assertFormatSingle(
                "Sign Accounting-Except-Zero Zero",
                "currency/USD sign-accounting-except-zero",
                NumberFormatter.with().sign(SignDisplay.ACCOUNTING_EXCEPT_ZERO).unit(USD),
                ULocale.ENGLISH,
                0,
                "$0.00");

        assertFormatSingle(
                "Sign Accounting Negative Hidden",
                "currency/USD unit-width-hidden sign-accounting",
                NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD).unitWidth(UnitWidth.HIDDEN),
                ULocale.ENGLISH,
                -444444,
                "(444,444.00)");

        assertFormatSingle(
                "Sign Accounting Negative Narrow",
                "currency/USD unit-width-narrow sign-accounting",
                NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD).unitWidth(UnitWidth.NARROW),
                ULocale.CANADA,
                -444444,
                "($444,444.00)");

        assertFormatSingle(
                "Sign Accounting Negative Short",
                "currency/USD sign-accounting",
                NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD).unitWidth(UnitWidth.SHORT),
                ULocale.CANADA,
                -444444,
                "(US$444,444.00)");

        assertFormatSingle(
                "Sign Accounting Negative Iso Code",
                "currency/USD unit-width-iso-code sign-accounting",
                NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD).unitWidth(UnitWidth.ISO_CODE),
                ULocale.CANADA,
                -444444,
                "(USD 444,444.00)");

        // Note: CLDR does not provide an accounting pattern for long name currency.
        // We fall back to normal currency format. This may change in the future.
        assertFormatSingle(
                "Sign Accounting Negative Full Name",
                "currency/USD unit-width-full-name sign-accounting",
                NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD).unitWidth(UnitWidth.FULL_NAME),
                ULocale.CANADA,
                -444444,
                "-444,444.00 US dollars");
    }

    @Test
    public void decimal() {
        assertFormatDescending(
                "Decimal Default",
                "decimal-auto",
                NumberFormatter.with().decimal(DecimalSeparatorDisplay.AUTO),
                ULocale.ENGLISH,
                "87,650",
                "8,765",
                "876.5",
                "87.65",
                "8.765",
                "0.8765",
                "0.08765",
                "0.008765",
                "0");

        assertFormatDescending(
                "Decimal Always Shown",
                "decimal-always",
                NumberFormatter.with().decimal(DecimalSeparatorDisplay.ALWAYS),
                ULocale.ENGLISH,
                "87,650.",
                "8,765.",
                "876.5",
                "87.65",
                "8.765",
                "0.8765",
                "0.08765",
                "0.008765",
                "0.");
    }

    @Test
    public void scale() {
        assertFormatDescending(
                "Multiplier None",
                "scale/1",
                NumberFormatter.with().scale(Scale.none()),
                ULocale.ENGLISH,
                "87,650",
                "8,765",
                "876.5",
                "87.65",
                "8.765",
                "0.8765",
                "0.08765",
                "0.008765",
                "0");

        assertFormatDescending(
                "Multiplier Power of Ten",
                "scale/1000000",
                NumberFormatter.with().scale(Scale.powerOfTen(6)),
                ULocale.ENGLISH,
                "87,650,000,000",
                "8,765,000,000",
                "876,500,000",
                "87,650,000",
                "8,765,000",
                "876,500",
                "87,650",
                "8,765",
                "0");

        assertFormatDescending(
                "Multiplier Arbitrary Double",
                "scale/5.2",
                NumberFormatter.with().scale(Scale.byDouble(5.2)),
                ULocale.ENGLISH,
                "455,780",
                "45,578",
                "4,557.8",
                "455.78",
                "45.578",
                "4.5578",
                "0.45578",
                "0.045578",
                "0");

        assertFormatDescending(
                "Multiplier Arbitrary BigDecimal",
                "scale/5.2",
                NumberFormatter.with().scale(Scale.byBigDecimal(new BigDecimal("5.2"))),
                ULocale.ENGLISH,
                "455,780",
                "45,578",
                "4,557.8",
                "455.78",
                "45.578",
                "4.5578",
                "0.45578",
                "0.045578",
                "0");

        assertFormatDescending(
                "Multiplier Arbitrary Double And Power Of Ten",
                "scale/5200",
                NumberFormatter.with().scale(Scale.byDoubleAndPowerOfTen(5.2, 3)),
                ULocale.ENGLISH,
                "455,780,000",
                "45,578,000",
                "4,557,800",
                "455,780",
                "45,578",
                "4,557.8",
                "455.78",
                "45.578",
                "0");

        assertFormatDescending(
                "Multiplier Zero",
                "scale/0",
                NumberFormatter.with().scale(Scale.byDouble(0)),
                ULocale.ENGLISH,
                "0",
                "0",
                "0",
                "0",
                "0",
                "0",
                "0",
                "0",
                "0");

        assertFormatSingle(
                "Multiplier Skeleton Scientific Notation and Percent",
                "percent scale/1E2",
                NumberFormatter.with().unit(NoUnit.PERCENT).scale(Scale.powerOfTen(2)),
                ULocale.ENGLISH,
                0.5,
                "50%");

        assertFormatSingle(
                "Negative Multiplier",
                "scale/-5.2",
                NumberFormatter.with().scale(Scale.byDouble(-5.2)),
                ULocale.ENGLISH,
                2,
                "-10.4");

        assertFormatSingle(
                "Negative One Multiplier",
                "scale/-1",
                NumberFormatter.with().scale(Scale.byDouble(-1)),
                ULocale.ENGLISH,
                444444,
                "-444,444");

        assertFormatSingle(
                "Two-Type Multiplier with Overlap",
                "scale/10000",
                NumberFormatter.with().scale(Scale.byDoubleAndPowerOfTen(100, 2)),
                ULocale.ENGLISH,
                2,
                "20,000");
    }

    @Test
    public void locale() {
        // Coverage for the locale setters.
        assertEquals(NumberFormatter.with().locale(ULocale.ENGLISH), NumberFormatter.with().locale(Locale.ENGLISH));
        assertEquals(NumberFormatter.with().locale(ULocale.ENGLISH), NumberFormatter.withLocale(ULocale.ENGLISH));
        assertEquals(NumberFormatter.with().locale(ULocale.ENGLISH), NumberFormatter.withLocale(Locale.ENGLISH));
        assertNotEquals(NumberFormatter.with().locale(ULocale.ENGLISH), NumberFormatter.with().locale(Locale.FRENCH));
    }

    @Test
    public void formatTypes() {
        LocalizedNumberFormatter formatter = NumberFormatter.withLocale(ULocale.ENGLISH);

        // Double
        assertEquals("514.23", formatter.format(514.23).toString());

        // Int64
        assertEquals("51,423", formatter.format(51423L).toString());

        // BigDecimal
        assertEquals("987,654,321,234,567,890",
                formatter.format(new BigDecimal("98765432123456789E1")).toString());

        // Also test proper DecimalQuantity bytes storage when all digits are in the fraction.
        // The number needs to have exactly 40 digits, which is the size of the default buffer.
        // (issue discovered by the address sanitizer in C++)
        assertEquals("0.009876543210987654321098765432109876543211",
                formatter.precision(Precision.unlimited())
                        .format(new BigDecimal("0.009876543210987654321098765432109876543211"))
                        .toString());
    }

    @Test
    public void fieldPositionLogic() {
        String message = "Field position logic test";

        FormattedNumber fmtd = assertFormatSingle(
                message,
                "",
                NumberFormatter.with(),
                ULocale.ENGLISH,
                -9876543210.12,
                "-9,876,543,210.12");

        Object[][] expectedFieldPositions = new Object[][]{
                {NumberFormat.Field.SIGN, 0, 1},
                {NumberFormat.Field.GROUPING_SEPARATOR, 2, 3},
                {NumberFormat.Field.GROUPING_SEPARATOR, 6, 7},
                {NumberFormat.Field.GROUPING_SEPARATOR, 10, 11},
                {NumberFormat.Field.INTEGER, 1, 14},
                {NumberFormat.Field.DECIMAL_SEPARATOR, 14, 15},
                {NumberFormat.Field.FRACTION, 15, 17}};

        assertNumberFieldPositions(message, fmtd, expectedFieldPositions);

        // Test the iteration functionality of nextFieldPosition
        FieldPosition actual = new FieldPosition(NumberFormat.Field.GROUPING_SEPARATOR);
        int i = 1;
        while (fmtd.nextFieldPosition(actual)) {
            Object[] cas = expectedFieldPositions[i++];
            NumberFormat.Field expectedField = (NumberFormat.Field) cas[0];
            int expectedBeginIndex = (Integer) cas[1];
            int expectedEndIndex = (Integer) cas[2];

            assertEquals(
                    "Next for grouping, field, case #" + i,
                    expectedField,
                    actual.getFieldAttribute());
            assertEquals(
                    "Next for grouping, begin index, case #" + i,
                    expectedBeginIndex,
                    actual.getBeginIndex());
            assertEquals(
                    "Next for grouping, end index, case #" + i,
                    expectedEndIndex,
                    actual.getEndIndex());
        }
        assertEquals("Should have seen all grouping separators", 4, i);

        // Make sure strings without fraction do not contain fraction field
        actual = new FieldPosition(NumberFormat.Field.FRACTION);
        fmtd = NumberFormatter.withLocale(ULocale.ENGLISH).format(5);
        assertFalse("No fraction part in an integer", fmtd.nextFieldPosition(actual));
    }

    @Test
    public void fieldPositionCoverage() {
        {
            String message = "Measure unit field position basic";
            FormattedNumber result = assertFormatSingle(
                    message,
                    "measure-unit/temperature-fahrenheit",
                    NumberFormatter.with().unit(MeasureUnit.FAHRENHEIT),
                    ULocale.ENGLISH,
                    68,
                    "68°F");
            Object[][] expectedFieldPositions = new Object[][] {
                    // field, begin index, end index
                    {NumberFormat.Field.INTEGER, 0, 2},
                    {NumberFormat.Field.MEASURE_UNIT, 2, 4}};
            assertNumberFieldPositions(
                    message,
                    result,
                    expectedFieldPositions);
        }

        {
            String message = "Measure unit field position with compound unit";
            FormattedNumber result = assertFormatSingle(
                    message,
                    "measure-unit/temperature-fahrenheit per-measure-unit/duration-day",
                    NumberFormatter.with().unit(MeasureUnit.FAHRENHEIT).perUnit(MeasureUnit.DAY),
                    ULocale.ENGLISH,
                    68,
                    "68°F/d");
            Object[][] expectedFieldPositions = new Object[][] {
                    // field, begin index, end index
                    {NumberFormat.Field.INTEGER, 0, 2},
                    {NumberFormat.Field.MEASURE_UNIT, 2, 6}};
            assertNumberFieldPositions(
                    message,
                    result,
                    expectedFieldPositions);
        }

        {
            String message = "Measure unit field position with spaces";
            FormattedNumber result = assertFormatSingle(
                    message,
                    "measure-unit/length-meter unit-width-full-name",
                    NumberFormatter.with().unit(MeasureUnit.METER).unitWidth(UnitWidth.FULL_NAME),
                    ULocale.ENGLISH,
                    68,
                    "68 meters");
            Object[][] expectedFieldPositions = new Object[][] {
                    // field, begin index, end index
                    {NumberFormat.Field.INTEGER, 0, 2},
                    // note: field starts after the space
                    {NumberFormat.Field.MEASURE_UNIT, 3, 9}};
            assertNumberFieldPositions(
                    message,
                    result,
                    expectedFieldPositions);
        }

        {
            String message = "Measure unit field position with prefix and suffix";
            FormattedNumber result = assertFormatSingle(
                    message,
                    "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,
                    "секундасына 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,
                    "measure-unit/temperature-fahrenheit unit-width-full-name",
                    NumberFormatter.with().unit(MeasureUnit.FAHRENHEIT).unitWidth(UnitWidth.FULL_NAME),
                    new ULocale("vi"), // locale with the interesting data
                    68,
                    "68 độ F");
            Object[][] expectedFieldPositions = new Object[][] {
                    // field, begin index, end index
                    {NumberFormat.Field.INTEGER, 0, 2},
                    // Should trim leading/trailing spaces, but not inner spaces:
                    {NumberFormat.Field.MEASURE_UNIT, 3, 7}};
            assertNumberFieldPositions(
                    message,
                    result,
                    expectedFieldPositions);
        }

        {
            // Data: other{"‎{0} K"} == "\u200E{0} K"
            // If that data changes, try to find another example of a non-empty unit prefix/suffix
            // that is also all ignorables (whitespace and bidi control marks).
            String message = "Measure unit field position with fully ignorable prefix";
            FormattedNumber result = assertFormatSingle(
                    message,
                    "measure-unit/temperature-kelvin",
                    NumberFormatter.with().unit(MeasureUnit.KELVIN),
                    new ULocale("fa"), // locale with the interesting data
                    68,
                    "‎۶۸ K");
            Object[][] expectedFieldPositions = new Object[][] {
                    // field, begin index, end index
                    {NumberFormat.Field.INTEGER, 1, 3},
                    {NumberFormat.Field.MEASURE_UNIT, 4, 5}};
            assertNumberFieldPositions(
                    message,
                    result,
                    expectedFieldPositions);
        }

        {
            String message = "Compact field basic";
            FormattedNumber result = assertFormatSingle(
                    message,
                    "compact-short",
                    NumberFormatter.with().notation(Notation.compactShort()),
                    ULocale.US,
                    65000,
                    "65K");
            Object[][] expectedFieldPositions = new Object[][] {
                    // field, begin index, end index
                    {NumberFormat.Field.INTEGER, 0, 2},
                    {NumberFormat.Field.COMPACT, 2, 3}};
            assertNumberFieldPositions(
                    message,
                    result,
                    expectedFieldPositions);
        }

        {
            String message = "Compact field with spaces";
            FormattedNumber result = assertFormatSingle(
                    message,
                    "compact-long",
                    NumberFormatter.with().notation(Notation.compactLong()),
                    ULocale.US,
                    65000,
                    "65 thousand");
            Object[][] expectedFieldPositions = new Object[][] {
                    // field, begin index, end index
                    {NumberFormat.Field.INTEGER, 0, 2},
                    {NumberFormat.Field.COMPACT, 3, 11}};
            assertNumberFieldPositions(
                    message,
                    result,
                    expectedFieldPositions);
        }

        {
            String message = "Compact field with inner space";
            FormattedNumber result = assertFormatSingle(
                    message,
                    "compact-long",
                    NumberFormatter.with().notation(Notation.compactLong()),
                    new ULocale("fil"),  // locale with interesting data
                    6000,
                    "6 na libo");
            Object[][] expectedFieldPositions = new Object[][] {
                    // field, begin index, end index
                    {NumberFormat.Field.INTEGER, 0, 1},
                    {NumberFormat.Field.COMPACT, 2, 9}};
            assertNumberFieldPositions(
                    message,
                    result,
                    expectedFieldPositions);
        }

        {
            String message = "Compact field with bidi mark";
            FormattedNumber result = assertFormatSingle(
                    message,
                    "compact-long",
                    NumberFormatter.with().notation(Notation.compactLong()),
                    new ULocale("he"),  // locale with interesting data
                    6000,
                    "\u200F6 אלף");
            Object[][] expectedFieldPositions = new Object[][] {
                    // field, begin index, end index
                    {NumberFormat.Field.INTEGER, 1, 2},
                    {NumberFormat.Field.COMPACT, 3, 6}};
            assertNumberFieldPositions(
                    message,
                    result,
                    expectedFieldPositions);
        }

        {
            String message = "Compact with currency fields";
            FormattedNumber result = assertFormatSingle(
                    message,
                    "compact-short currency/USD",
                    NumberFormatter.with().notation(Notation.compactShort()).unit(USD),
                    new ULocale("sr_Latn"),  // locale with interesting data
                    65000,
                    "65 hilj. US$");
            Object[][] expectedFieldPositions = new Object[][] {
                    // field, begin index, end index
                    {NumberFormat.Field.INTEGER, 0, 2},
                    {NumberFormat.Field.COMPACT, 3, 8},
                    {NumberFormat.Field.CURRENCY, 9, 12}};
            assertNumberFieldPositions(
                    message,
                    result,
                    expectedFieldPositions);
        }

        {
            String message = "Currency long name fields";
            FormattedNumber result = assertFormatSingle(
                    message,
                    "currency/USD unit-width-full-name",
                    NumberFormatter.with().unit(USD)
                        .unitWidth(UnitWidth.FULL_NAME),
                    ULocale.ENGLISH,
                    12345,
                    "12,345.00 US dollars");
            Object[][] expectedFieldPositions = new Object[][] {
                    // field, begin index, end index
                    {NumberFormat.Field.GROUPING_SEPARATOR, 2, 3},
                    {NumberFormat.Field.INTEGER, 0, 6},
                    {NumberFormat.Field.DECIMAL_SEPARATOR, 6, 7},
                    {NumberFormat.Field.FRACTION, 7, 9},
                    {NumberFormat.Field.CURRENCY, 10, 20}};
            assertNumberFieldPositions(
                    message,
                    result,
                    expectedFieldPositions);
        }

        {
            String message = "Compact with measure unit fields";
            FormattedNumber result = assertFormatSingle(
                    message,
                    "compact-long measure-unit/length-meter unit-width-full-name",
                    NumberFormatter.with().notation(Notation.compactLong())
                        .unit(MeasureUnit.METER)
                        .unitWidth(UnitWidth.FULL_NAME),
                    ULocale.US,
                    65000,
                    "65 thousand meters");
            Object[][] expectedFieldPositions = new Object[][] {
                    // field, begin index, end index
                    {NumberFormat.Field.INTEGER, 0, 2},
                    {NumberFormat.Field.COMPACT, 3, 11},
                    {NumberFormat.Field.MEASURE_UNIT, 12, 18}};
            assertNumberFieldPositions(
                    message,
                    result,
                    expectedFieldPositions);
        }
    }

    /** Handler for serialization compatibility test suite. */
    public static class FormatHandler implements SerializableTestUtility.Handler {
        @Override
        public Object[] getTestObjects() {
            return new Object[] {
                    NumberFormatter.withLocale(ULocale.FRENCH).toFormat(),
                    NumberFormatter.forSkeleton("percent").locale(ULocale.JAPANESE).toFormat(),
                    NumberFormatter.forSkeleton("scientific .000").locale(ULocale.ENGLISH).toFormat() };
        }

        @Override
        public boolean hasSameBehavior(Object a, Object b) {
            LocalizedNumberFormatterAsFormat f1 = (LocalizedNumberFormatterAsFormat) a;
            LocalizedNumberFormatterAsFormat f2 = (LocalizedNumberFormatterAsFormat) b;
            String s1 = f1.format(514.23);
            String s2 = f1.format(514.23);
            String k1 = f1.getNumberFormatter().toSkeleton();
            String k2 = f2.getNumberFormatter().toSkeleton();
            return s1.equals(s2) && k1.equals(k2);
        }
    }

    @Test
    public void toFormat() {
        LocalizedNumberFormatter lnf = NumberFormatter.withLocale(ULocale.FRENCH)
                .precision(Precision.fixedFraction(3));
        Format format = lnf.toFormat();
        FieldPosition fpos = new FieldPosition(NumberFormat.Field.DECIMAL_SEPARATOR);
        StringBuffer sb = new StringBuffer();
        format.format(514.23, sb, fpos);
        assertEquals("Should correctly format number", "514,230", sb.toString());
        assertEquals("Should find decimal separator", 3, fpos.getBeginIndex());
        assertEquals("Should find end of decimal separator", 4, fpos.getEndIndex());
        assertEquals("LocalizedNumberFormatter should round-trip",
                lnf,
                ((LocalizedNumberFormatterAsFormat) format).getNumberFormatter());
        assertEquals("Should produce same character iterator",
                lnf.format(514.23).toCharacterIterator().getAttributes(),
                format.formatToCharacterIterator(514.23).getAttributes());
    }

    @Test
    public void plurals() {
        // TODO: Expand this test.

        assertFormatSingle(
                "Plural 1",
                "currency/USD precision-integer unit-width-full-name",
                NumberFormatter.with().unit(USD).unitWidth(UnitWidth.FULL_NAME).precision(Precision.fixedFraction(0)),
                ULocale.ENGLISH,
                1,
                "1 US dollar");

        assertFormatSingle(
                "Plural 1.00",
                "currency/USD .00 unit-width-full-name",
                NumberFormatter.with().unit(USD).unitWidth(UnitWidth.FULL_NAME).precision(Precision.fixedFraction(2)),
                ULocale.ENGLISH,
                1,
                "1.00 US dollars");
    }

    @Test
    public void validRanges() throws NoSuchMethodException, IllegalAccessException {
        Method[] methodsWithOneArgument = new Method[] { Precision.class.getDeclaredMethod("fixedFraction", Integer.TYPE),
                Precision.class.getDeclaredMethod("minFraction", Integer.TYPE),
                Precision.class.getDeclaredMethod("maxFraction", Integer.TYPE),
                Precision.class.getDeclaredMethod("fixedSignificantDigits", Integer.TYPE),
                Precision.class.getDeclaredMethod("minSignificantDigits", Integer.TYPE),
                Precision.class.getDeclaredMethod("maxSignificantDigits", Integer.TYPE),
                FractionPrecision.class.getDeclaredMethod("withMinDigits", Integer.TYPE),
                FractionPrecision.class.getDeclaredMethod("withMaxDigits", Integer.TYPE),
                ScientificNotation.class.getDeclaredMethod("withMinExponentDigits", Integer.TYPE),
                IntegerWidth.class.getDeclaredMethod("zeroFillTo", Integer.TYPE),
                IntegerWidth.class.getDeclaredMethod("truncateAt", Integer.TYPE), };
        Method[] methodsWithTwoArguments = new Method[] {
                Precision.class.getDeclaredMethod("minMaxFraction", Integer.TYPE, Integer.TYPE),
                Precision.class.getDeclaredMethod("minMaxSignificantDigits", Integer.TYPE, Integer.TYPE), };

        final int EXPECTED_MAX_INT_FRAC_SIG = 999;
        final String expectedSubstring0 = "between 0 and 999 (inclusive)";
        final String expectedSubstring1 = "between 1 and 999 (inclusive)";
        final String expectedSubstringN1 = "between -1 and 999 (inclusive)";

        // We require that the upper bounds all be 999 inclusive.
        // The lower bound may be either -1, 0, or 1.
        Set<String> methodsWithLowerBound1 = new HashSet();
        methodsWithLowerBound1.add("fixedSignificantDigits");
        methodsWithLowerBound1.add("minSignificantDigits");
        methodsWithLowerBound1.add("maxSignificantDigits");
        methodsWithLowerBound1.add("minMaxSignificantDigits");
        methodsWithLowerBound1.add("withMinDigits");
        methodsWithLowerBound1.add("withMaxDigits");
        methodsWithLowerBound1.add("withMinExponentDigits");
        // Methods with lower bound 0:
        // fixedFraction
        // minFraction
        // maxFraction
        // minMaxFraction
        // zeroFillTo
        Set<String> methodsWithLowerBoundN1 = new HashSet();
        methodsWithLowerBoundN1.add("truncateAt");

        // Some of the methods require an object to be called upon.
        Map<String, Object> targets = new HashMap<>();
        targets.put("withMinDigits", Precision.integer());
        targets.put("withMaxDigits", Precision.integer());
        targets.put("withMinExponentDigits", Notation.scientific());
        targets.put("truncateAt", IntegerWidth.zeroFillTo(0));

        for (int argument = -2; argument <= EXPECTED_MAX_INT_FRAC_SIG + 2; argument++) {
            for (Method method : methodsWithOneArgument) {
                String message = "i = " + argument + "; method = " + method.getName();
                int lowerBound = methodsWithLowerBound1.contains(method.getName()) ? 1
                        : methodsWithLowerBoundN1.contains(method.getName()) ? -1 : 0;
                String expectedSubstring = lowerBound == 0 ? expectedSubstring0
                        : lowerBound == 1 ? expectedSubstring1 : expectedSubstringN1;
                Object target = targets.get(method.getName());
                try {
                    method.invoke(target, argument);
                    assertTrue(message, argument >= lowerBound && argument <= EXPECTED_MAX_INT_FRAC_SIG);
                } catch (InvocationTargetException e) {
                    assertTrue(message, argument < lowerBound || argument > EXPECTED_MAX_INT_FRAC_SIG);
                    // Ensure the exception message contains the expected substring
                    String actualMessage = e.getCause().getMessage();
                    assertNotEquals(message + ": " + actualMessage + "; " + expectedSubstring
                            , -1, actualMessage.indexOf(expectedSubstring));
                }
            }
            for (Method method : methodsWithTwoArguments) {
                String message = "i = " + argument + "; method = " + method.getName();
                int lowerBound = methodsWithLowerBound1.contains(method.getName()) ? 1
                        : methodsWithLowerBoundN1.contains(method.getName()) ? -1 : 0;
                String expectedSubstring = lowerBound == 0 ? expectedSubstring0 : expectedSubstring1;
                Object target = targets.get(method.getName());
                // Check range on the first argument
                try {
                    // Pass EXPECTED_MAX_INT_FRAC_SIG as the second argument so arg1 <= arg2 in expected cases
                    method.invoke(target, argument, EXPECTED_MAX_INT_FRAC_SIG);
                    assertTrue(message, argument >= lowerBound && argument <= EXPECTED_MAX_INT_FRAC_SIG);
                } catch (InvocationTargetException e) {
                    assertTrue(message, argument < lowerBound || argument > EXPECTED_MAX_INT_FRAC_SIG);
                    // Ensure the exception message contains the expected substring
                    String actualMessage = e.getCause().getMessage();
                    assertNotEquals(message + ": " + actualMessage, -1, actualMessage.indexOf(expectedSubstring));
                }
                // Check range on the second argument
                try {
                    // Pass lowerBound as the first argument so arg1 <= arg2 in expected cases
                    method.invoke(target, lowerBound, argument);
                    assertTrue(message, argument >= lowerBound && argument <= EXPECTED_MAX_INT_FRAC_SIG);
                } catch (InvocationTargetException e) {
                    assertTrue(message, argument < lowerBound || argument > EXPECTED_MAX_INT_FRAC_SIG);
                    // Ensure the exception message contains the expected substring
                    String actualMessage = e.getCause().getMessage();
                    assertNotEquals(message + ": " + actualMessage, -1, actualMessage.indexOf(expectedSubstring));
                }
                // Check that first argument must be less than or equal to second argument
                try {
                    method.invoke(target, argument, argument - 1);
                    org.junit.Assert.fail();
                } catch (InvocationTargetException e) {
                    // Pass
                }
            }
        }

        // Check first argument less than or equal to second argument on IntegerWidth
        try {
            IntegerWidth.zeroFillTo(4).truncateAt(2);
            org.junit.Assert.fail();
        } catch (IllegalArgumentException e) {
            // Pass
        }
    }

    static void assertFormatDescending(
            String message,
            String skeleton,
            UnlocalizedNumberFormatter f,
            ULocale locale,
            String... expected) {
        final double[] inputs = new double[] { 87650, 8765, 876.5, 87.65, 8.765, 0.8765, 0.08765, 0.008765, 0 };
        assertFormatDescending(message, skeleton, f, locale, inputs, expected);
    }

    static void assertFormatDescendingBig(
            String message,
            String skeleton,
            UnlocalizedNumberFormatter f,
            ULocale locale,
            String... expected) {
        final double[] inputs = new double[] { 87650000, 8765000, 876500, 87650, 8765, 876.5, 87.65, 8.765, 0 };
        assertFormatDescending(message, skeleton, f, locale, inputs, expected);
    }

    static void assertFormatDescending(
            String message,
            String skeleton,
            UnlocalizedNumberFormatter f,
            ULocale locale,
            double[] inputs,
            String... expected) {
        assert expected.length == 9;
        LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation
        LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation
        for (int i = 0; i < 9; i++) {
            double d = inputs[i];
            String actual1 = l1.format(d).toString();
            assertEquals(message + ": Unsafe Path: " + d, expected[i], actual1);
            String actual2 = l2.format(d).toString();
            assertEquals(message + ": Safe Path: " + d, expected[i], actual2);
        }
        if (skeleton != null) { // if null, skeleton is declared as undefined.
            // Only compare normalized skeletons: the tests need not provide the normalized forms.
            // Use the normalized form to construct the testing formatter to guarantee no loss of info.
            String normalized = NumberFormatter.forSkeleton(skeleton).toSkeleton();
            assertEquals(message + ": Skeleton:", normalized, f.toSkeleton());
            LocalizedNumberFormatter l3 = NumberFormatter.forSkeleton(normalized).locale(locale);
            for (int i = 0; i < 9; i++) {
                double d = inputs[i];
                String actual3 = l3.format(d).toString();
                assertEquals(message + ": Skeleton Path: " + d, expected[i], actual3);
            }
        } else {
            assertUndefinedSkeleton(f);
        }
    }

    static FormattedNumber assertFormatSingle(
            String message,
            String skeleton,
            UnlocalizedNumberFormatter f,
            ULocale locale,
            Number input,
            String expected) {
        LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation
        LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation
        FormattedNumber result1 = l1.format(input);
        String actual1 = result1.toString();
        assertEquals(message + ": Unsafe Path: " + input, expected, actual1);
        String actual2 = l2.format(input).toString();
        assertEquals(message + ": Safe Path: " + input, expected, actual2);
        if (skeleton != null) { // if null, skeleton is declared as undefined.
            // Only compare normalized skeletons: the tests need not provide the normalized forms.
            // Use the normalized form to construct the testing formatter to ensure no loss of info.
            String normalized = NumberFormatter.forSkeleton(skeleton).toSkeleton();
            assertEquals(message + ": Skeleton:", normalized, f.toSkeleton());
            LocalizedNumberFormatter l3 = NumberFormatter.forSkeleton(normalized).locale(locale);
            String actual3 = l3.format(input).toString();
            assertEquals(message + ": Skeleton Path: " + input, expected, actual3);
        } else {
            assertUndefinedSkeleton(f);
        }
        return result1;
    }

    static void assertFormatSingleMeasure(
            String message,
            String skeleton,
            UnlocalizedNumberFormatter f,
            ULocale locale,
            Measure input,
            String expected) {
        LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation
        LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation
        String actual1 = l1.format(input).toString();
        assertEquals(message + ": Unsafe Path: " + input, expected, actual1);
        String actual2 = l2.format(input).toString();
        assertEquals(message + ": Safe Path: " + input, expected, actual2);
        if (skeleton != null) { // if null, skeleton is declared as undefined.
            // Only compare normalized skeletons: the tests need not provide the normalized forms.
            // Use the normalized form to construct the testing formatter to ensure no loss of info.
            String normalized = NumberFormatter.forSkeleton(skeleton).toSkeleton();
            assertEquals(message + ": Skeleton:", normalized, f.toSkeleton());
            LocalizedNumberFormatter l3 = NumberFormatter.forSkeleton(normalized).locale(locale);
            String actual3 = l3.format(input).toString();
            assertEquals(message + ": Skeleton Path: " + input, expected, actual3);
        } else {
            assertUndefinedSkeleton(f);
        }
    }

    static void assertUndefinedSkeleton(UnlocalizedNumberFormatter f) {
        try {
            String skeleton = f.toSkeleton();
            fail("Expected toSkeleton to fail, but it passed, producing: " + skeleton);
        } catch (UnsupportedOperationException expected) {}
    }

    private void assertNumberFieldPositions(String message, FormattedNumber result, Object[][] expectedFieldPositions) {
        FormattedValueTest.checkFormattedValue(message, result, result.toString(), expectedFieldPositions);
    }
}
