NumberFormatter.java

/*
 * Copyright 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.wear.protolayout.expression.pipeline;

import static java.lang.Math.max;

import android.icu.number.IntegerWidth;
import android.icu.number.LocalizedNumberFormatter;
import android.icu.number.NumberFormatter.GroupingStrategy;
import android.icu.number.Precision;
import android.icu.text.DecimalFormat;
import android.icu.text.NumberFormat;
import android.icu.util.ULocale;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.util.Log;

import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.wear.protolayout.expression.proto.DynamicProto.FloatFormatOp;
import androidx.wear.protolayout.expression.proto.DynamicProto.Int32FormatOp;

/** Utility to number formatting. */
class NumberFormatter {

    Formatter mFormatter;
    private static final String TAG = "NumberFormatter";
    private static final int DEFAULT_MIN_INTEGER_DIGITS = 1;
    private static final int DEFAULT_MAX_FRACTION_DIGITS = 3;

    @VisibleForTesting static final int MAX_INTEGER_PART_LENGTH = 15;
    @VisibleForTesting static final int MAX_FRACTION_PART_LENGTH = 15;

    private interface Formatter {
        String format(int value);

        String format(float value);
    }

    NumberFormatter(FloatFormatOp floatFormatOp, ULocale currentLocale) {
        int minIntegerDigits =
                floatFormatOp.hasMinIntegerDigits()
                        ? floatFormatOp.getMinIntegerDigits()
                        : DEFAULT_MIN_INTEGER_DIGITS;

        int minFractionDigits = floatFormatOp.getMinFractionDigits();
        if (minFractionDigits > MAX_FRACTION_PART_LENGTH) {
            logLargeParam("MinFractionDigits", minFractionDigits, MAX_FRACTION_PART_LENGTH);
            minFractionDigits = MAX_FRACTION_PART_LENGTH;
        }

        // maxFractionDigits should be larger or equal to minFractionDigits
        int maxFractionDigits =
                max(
                        floatFormatOp.hasMaxFractionDigits()
                                ? floatFormatOp.getMaxFractionDigits()
                                : DEFAULT_MAX_FRACTION_DIGITS,
                        minFractionDigits);

        if (maxFractionDigits > MAX_FRACTION_PART_LENGTH) {
            logLargeParam("MaxFractionDigits", maxFractionDigits, MAX_FRACTION_PART_LENGTH);
            maxFractionDigits = MAX_FRACTION_PART_LENGTH;
        }

        if (minIntegerDigits > MAX_INTEGER_PART_LENGTH) {
            logLargeParam("MinIntegerDigits", minIntegerDigits, MAX_INTEGER_PART_LENGTH);
            minIntegerDigits = MAX_INTEGER_PART_LENGTH;
        }

        mFormatter =
                buildFormatter(
                        minIntegerDigits,
                        minFractionDigits,
                        maxFractionDigits,
                        floatFormatOp.getGroupingUsed(),
                        currentLocale);
    }

    NumberFormatter(Int32FormatOp int32FormatOp, ULocale currentLocale) {
        int minIntegerDigits =
                int32FormatOp.hasMinIntegerDigits()
                        ? int32FormatOp.getMinIntegerDigits()
                        : DEFAULT_MIN_INTEGER_DIGITS;

        if (minIntegerDigits > MAX_INTEGER_PART_LENGTH) {
            logLargeParam("MinIntegerDigits", minIntegerDigits, MAX_INTEGER_PART_LENGTH);
            minIntegerDigits = MAX_INTEGER_PART_LENGTH;
        }

        mFormatter =
                buildFormatter(
                        minIntegerDigits,
                        /* minFractionDigits= */ 0,
                        /* maxFractionDigits= */ 0,
                        int32FormatOp.getGroupingUsed(),
                        currentLocale);
    }

    String format(float value) {
        return mFormatter.format(value);
    }

    String format(int value) {
        return mFormatter.format(value);
    }

    @RequiresApi(VERSION_CODES.R)
    private static class Api30Impl {
        @NonNull
        @DoNotInline
        static String callFormatToString(LocalizedNumberFormatter mFmt, int value) {
            return mFmt.format(value).toString();
        }

        @NonNull
        @DoNotInline
        static String callFormatToString(LocalizedNumberFormatter mFmt, float value) {
            return mFmt.format(value).toString();
        }

        @NonNull
        @DoNotInline
        static LocalizedNumberFormatter buildLocalizedNumberFormatter(
                int minIntegerDigits,
                int minFractionDigits,
                int maxFractionDigits,
                boolean groupingUsed,
                ULocale currentLocale) {
            return android.icu.number.NumberFormatter.withLocale(currentLocale)
                    .grouping(groupingUsed ? GroupingStrategy.AUTO : GroupingStrategy.OFF)
                    .integerWidth(IntegerWidth.zeroFillTo(minIntegerDigits))
                    .precision(Precision.minMaxFraction(minFractionDigits, maxFractionDigits));
        }
    }

    private static void logLargeParam(String paramName, int oldValue, int newValue) {
        Log.w(
                TAG,
                String.format(
                        "%s (%d) is too large. Using the maximum allowed value instead: %d",
                        paramName, oldValue, newValue));
    }

    private static Formatter buildFormatter(
            int minIntegerDigits,
            int minFractionDigits,
            int maxFractionDigits,
            boolean groupingUsed,
            ULocale currentLocale) {
        if (VERSION.SDK_INT >= VERSION_CODES.R) {
            return new Formatter() {
                final LocalizedNumberFormatter mFmt =
                        Api30Impl.buildLocalizedNumberFormatter(
                                minIntegerDigits,
                                minFractionDigits,
                                maxFractionDigits,
                                groupingUsed,
                                currentLocale);

                @Override
                public String format(int value) {
                    return Api30Impl.callFormatToString(mFmt, value);
                }

                @Override
                public String format(float value) {
                    return Api30Impl.callFormatToString(mFmt, value);
                }
            };

        } else {
            return new Formatter() {
                final DecimalFormat mFmt =
                        buildDecimalFormat(
                                minIntegerDigits,
                                minFractionDigits,
                                maxFractionDigits,
                                groupingUsed,
                                currentLocale);

                @Override
                public String format(int value) {
                    return mFmt.format(value);
                }

                @Override
                public String format(float value) {
                    return mFmt.format(value);
                }
            };
        }
    }

    static DecimalFormat buildDecimalFormat(
            int minIntegerDigits,
            int minFractionDigits,
            int maxFractionDigits,
            boolean groupingUsed,
            ULocale currentLocale) {
        DecimalFormat decimalFormat = (DecimalFormat) NumberFormat.getInstance(currentLocale);
        decimalFormat.setMinimumIntegerDigits(minIntegerDigits);
        decimalFormat.setGroupingUsed(groupingUsed);
        decimalFormat.setMaximumFractionDigits(maxFractionDigits);
        decimalFormat.setMinimumFractionDigits(minFractionDigits);
        return decimalFormat;
    }
}