Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7e19fb0d authored by Neil Fuller's avatar Neil Fuller Committed by Android (Google) Code Review
Browse files

Merge "Fixing android.text.format.Time for non-English locales" into lmp-dev

parents d5936307 16566656
Loading
Loading
Loading
Loading
+59 −78
Original line number Diff line number Diff line
@@ -22,8 +22,7 @@ package android.text.format;

import android.content.res.Resources;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.CharBuffer;
import java.util.Formatter;
import java.util.Locale;
import java.util.TimeZone;
@@ -31,15 +30,13 @@ import libcore.icu.LocaleData;
import libcore.util.ZoneInfo;

/**
 * Formatting logic for {@link Time}. Contains a port of Bionic's broken strftime_tz to Java. The
 * main issue with this implementation is the treatment of characters as ASCII, despite returning
 * localized (UTF-16) strings from the LocaleData.
 * Formatting logic for {@link Time}. Contains a port of Bionic's broken strftime_tz to Java.
 *
 * <p>This class is not thread safe.
 */
class TimeFormatter {
    // An arbitrary value outside the range representable by a byte / ASCII character code.
    private static final int FORCE_LOWER_CASE = 0x100;
    // An arbitrary value outside the range representable by a char.
    private static final int FORCE_LOWER_CASE = -1;

    private static final int SECSPERMIN = 60;
    private static final int MINSPERHOUR = 60;
@@ -62,10 +59,9 @@ class TimeFormatter {
    private final String dateTimeFormat;
    private final String timeOnlyFormat;
    private final String dateOnlyFormat;
    private final Locale locale;

    private StringBuilder outputBuilder;
    private Formatter outputFormatter;
    private Formatter numberFormatter;

    public TimeFormatter() {
        synchronized (TimeFormatter.class) {
@@ -84,7 +80,6 @@ class TimeFormatter {
            this.dateTimeFormat = sDateTimeFormat;
            this.timeOnlyFormat = sTimeOnlyFormat;
            this.dateOnlyFormat = sDateOnlyFormat;
            this.locale = locale;
            localeData = sLocaleData;
        }
    }
@@ -97,19 +92,21 @@ class TimeFormatter {
            StringBuilder stringBuilder = new StringBuilder();

            outputBuilder = stringBuilder;
            outputFormatter = new Formatter(stringBuilder, locale);
            // This uses the US locale because number localization is handled separately (see below)
            // and locale sensitive strings are output directly using outputBuilder.
            numberFormatter = new Formatter(stringBuilder, Locale.US);

            formatInternal(pattern, wallTime, zoneInfo);
            String result = stringBuilder.toString();
            // This behavior is the source of a bug since some formats are defined as being
            // in ASCII. Generally localization is very broken.
            // in ASCII and not localized.
            if (localeData.zeroDigit != '0') {
                result = localizeDigits(result);
            }
            return result;
        } finally {
            outputBuilder = null;
            outputFormatter = null;
            numberFormatter = null;
        }
    }

@@ -132,38 +129,30 @@ class TimeFormatter {
     * {@link #outputBuilder}.
     */
    private void formatInternal(String pattern, ZoneInfo.WallTime wallTime, ZoneInfo zoneInfo) {
        // Convert to ASCII bytes to be compatible with old implementation behavior.
        byte[] bytes = pattern.getBytes(StandardCharsets.US_ASCII);
        if (bytes.length == 0) {
            return;
        }

        ByteBuffer formatBuffer = ByteBuffer.wrap(bytes);
        CharBuffer formatBuffer = CharBuffer.wrap(pattern);
        while (formatBuffer.remaining() > 0) {
            boolean outputCurrentByte = true;
            char currentByteAsChar = convertToChar(formatBuffer.get(formatBuffer.position()));
            if (currentByteAsChar == '%') {
                outputCurrentByte = handleToken(formatBuffer, wallTime, zoneInfo);
            boolean outputCurrentChar = true;
            char currentChar = formatBuffer.get(formatBuffer.position());
            if (currentChar == '%') {
                outputCurrentChar = handleToken(formatBuffer, wallTime, zoneInfo);
            }
            if (outputCurrentByte) {
                currentByteAsChar = convertToChar(formatBuffer.get(formatBuffer.position()));
                outputBuilder.append(currentByteAsChar);
            if (outputCurrentChar) {
                outputBuilder.append(formatBuffer.get(formatBuffer.position()));
            }

            formatBuffer.position(formatBuffer.position() + 1);
        }
    }

    private boolean handleToken(ByteBuffer formatBuffer, ZoneInfo.WallTime wallTime,
    private boolean handleToken(CharBuffer formatBuffer, ZoneInfo.WallTime wallTime,
            ZoneInfo zoneInfo) {

        // The byte at formatBuffer.position() is expected to be '%' at this point.
        // The char at formatBuffer.position() is expected to be '%' at this point.
        int modifier = 0;
        while (formatBuffer.remaining() > 1) {
            // Increment the position then get the new current byte.
            // Increment the position then get the new current char.
            formatBuffer.position(formatBuffer.position() + 1);
            char currentByteAsChar = convertToChar(formatBuffer.get(formatBuffer.position()));
            switch (currentByteAsChar) {
            char currentChar = formatBuffer.get(formatBuffer.position());
            switch (currentChar) {
                case 'A':
                    modifyAndAppend((wallTime.getWeekDay() < 0
                                    || wallTime.getWeekDay() >= DAYSPERWEEK)
@@ -206,7 +195,7 @@ class TimeFormatter {
                    formatInternal("%m/%d/%y", wallTime, zoneInfo);
                    return false;
                case 'd':
                    outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
                    numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
                            wallTime.getMonthDay());
                    return false;
                case 'E':
@@ -218,46 +207,46 @@ class TimeFormatter {
                case '0':
                case '^':
                case '#':
                    modifier = currentByteAsChar;
                    modifier = currentChar;
                    continue;
                case 'e':
                    outputFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"),
                    numberFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"),
                            wallTime.getMonthDay());
                    return false;
                case 'F':
                    formatInternal("%Y-%m-%d", wallTime, zoneInfo);
                    return false;
                case 'H':
                    outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
                    numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
                            wallTime.getHour());
                    return false;
                case 'I':
                    int hour = (wallTime.getHour() % 12 != 0) ? (wallTime.getHour() % 12) : 12;
                    outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), hour);
                    numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), hour);
                    return false;
                case 'j':
                    int yearDay = wallTime.getYearDay() + 1;
                    outputFormatter.format(getFormat(modifier, "%03d", "%3d", "%d", "%03d"),
                    numberFormatter.format(getFormat(modifier, "%03d", "%3d", "%d", "%03d"),
                            yearDay);
                    return false;
                case 'k':
                    outputFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"),
                    numberFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"),
                            wallTime.getHour());
                    return false;
                case 'l':
                    int n2 = (wallTime.getHour() % 12 != 0) ? (wallTime.getHour() % 12) : 12;
                    outputFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"), n2);
                    numberFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"), n2);
                    return false;
                case 'M':
                    outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
                    numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
                            wallTime.getMinute());
                    return false;
                case 'm':
                    outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
                    numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
                            wallTime.getMonth() + 1);
                    return false;
                case 'n':
                    modifyAndAppend("\n", modifier);
                    outputBuilder.append('\n');
                    return false;
                case 'p':
                    modifyAndAppend((wallTime.getHour() >= (HOURSPERDAY / 2)) ? localeData.amPm[1]
@@ -274,27 +263,27 @@ class TimeFormatter {
                    formatInternal("%I:%M:%S %p", wallTime, zoneInfo);
                    return false;
                case 'S':
                    outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
                    numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
                            wallTime.getSecond());
                    return false;
                case 's':
                    int timeInSeconds = wallTime.mktime(zoneInfo);
                    modifyAndAppend(Integer.toString(timeInSeconds), modifier);
                    outputBuilder.append(Integer.toString(timeInSeconds));
                    return false;
                case 'T':
                    formatInternal("%H:%M:%S", wallTime, zoneInfo);
                    return false;
                case 't':
                    modifyAndAppend("\t", modifier);
                    outputBuilder.append('\t');
                    return false;
                case 'U':
                    outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
                    numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
                            (wallTime.getYearDay() + DAYSPERWEEK - wallTime.getWeekDay())
                                    / DAYSPERWEEK);
                    return false;
                case 'u':
                    int day = (wallTime.getWeekDay() == 0) ? DAYSPERWEEK : wallTime.getWeekDay();
                    outputFormatter.format("%d", day);
                    numberFormatter.format("%d", day);
                    return false;
                case 'V':   /* ISO 8601 week number */
                case 'G':   /* ISO 8601 year (four digits) */
@@ -326,9 +315,9 @@ class TimeFormatter {
                        --year;
                        yday += isLeap(year) ? DAYSPERLYEAR : DAYSPERNYEAR;
                    }
                    if (currentByteAsChar == 'V') {
                        outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), w);
                    } else if (currentByteAsChar == 'g') {
                    if (currentChar == 'V') {
                        numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), w);
                    } else if (currentChar == 'g') {
                        outputYear(year, false, true, modifier);
                    } else {
                        outputYear(year, true, true, modifier);
@@ -342,10 +331,10 @@ class TimeFormatter {
                    int n = (wallTime.getYearDay() + DAYSPERWEEK - (
                                    wallTime.getWeekDay() != 0 ? (wallTime.getWeekDay() - 1)
                                            : (DAYSPERWEEK - 1))) / DAYSPERWEEK;
                    outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), n);
                    numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), n);
                    return false;
                case 'w':
                    outputFormatter.format("%d", wallTime.getWeekDay());
                    numberFormatter.format("%d", wallTime.getWeekDay());
                    return false;
                case 'X':
                    formatInternal(timeOnlyFormat, wallTime, zoneInfo);
@@ -371,17 +360,17 @@ class TimeFormatter {
                        return false;
                    }
                    int diff = wallTime.getGmtOffset();
                    String sign;
                    char sign;
                    if (diff < 0) {
                        sign = "-";
                        sign = '-';
                        diff = -diff;
                    } else {
                        sign = "+";
                        sign = '+';
                    }
                    modifyAndAppend(sign, modifier);
                    outputBuilder.append(sign);
                    diff /= SECSPERMIN;
                    diff = (diff / MINSPERHOUR) * 100 + (diff % MINSPERHOUR);
                    outputFormatter.format(getFormat(modifier, "%04d", "%4d", "%d", "%04d"), diff);
                    numberFormatter.format(getFormat(modifier, "%04d", "%4d", "%d", "%04d"), diff);
                    return false;
                }
                case '+':
@@ -422,7 +411,6 @@ class TimeFormatter {
                break;
            default:
                outputBuilder.append(str);

        }
    }

@@ -443,14 +431,14 @@ class TimeFormatter {
        }
        if (outputTop) {
            if (lead == 0 && trail < 0) {
                modifyAndAppend("-0", modifier);
                outputBuilder.append("-0");
            } else {
                outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), lead);
                numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), lead);
            }
        }
        if (outputBottom) {
            int n = ((trail < 0) ? -trail : trail);
            outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), n);
            numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), n);
        }
    }

@@ -472,24 +460,24 @@ class TimeFormatter {
    }

    /**
     * A broken implementation of {@link Character#isUpperCase(char)} that assumes ASCII in order to
     * be compatible with the old native implementation.
     * A broken implementation of {@link Character#isUpperCase(char)} that assumes ASCII codes in
     * order to be compatible with the old native implementation.
     */
    private static boolean brokenIsUpper(char toCheck) {
        return toCheck >= 'A' && toCheck <= 'Z';
    }

    /**
     * A broken implementation of {@link Character#isLowerCase(char)} that assumes ASCII in order to
     * be compatible with the old native implementation.
     * A broken implementation of {@link Character#isLowerCase(char)} that assumes ASCII codes in
     * order to be compatible with the old native implementation.
     */
    private static boolean brokenIsLower(char toCheck) {
        return toCheck >= 'a' && toCheck <= 'z';
    }

    /**
     * A broken implementation of {@link Character#toLowerCase(char)} that assumes ASCII in order to
     * be compatible with the old native implementation.
     * A broken implementation of {@link Character#toLowerCase(char)} that assumes ASCII codes in
     * order to be compatible with the old native implementation.
     */
    private static char brokenToLower(char input) {
        if (input >= 'A' && input <= 'Z') {
@@ -499,8 +487,8 @@ class TimeFormatter {
    }

    /**
     * A broken implementation of {@link Character#toUpperCase(char)} that assumes ASCII in order to
     * be compatible with the old native implementation.
     * A broken implementation of {@link Character#toUpperCase(char)} that assumes ASCII codes in
     * order to be compatible with the old native implementation.
     */
    private static char brokenToUpper(char input) {
        if (input >= 'a' && input <= 'z') {
@@ -509,11 +497,4 @@ class TimeFormatter {
        return input;
    }

    /**
     * Safely convert a byte containing an ASCII character to a char, even for character codes
     * > 127.
     */
    private static char convertToChar(byte b) {
        return (char) (b & 0xFF);
    }
}