diff options
author | Neil Fuller <nfuller@google.com> | 2014-08-29 08:27:29 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2014-08-29 08:27:30 +0000 |
commit | 8178edfc096e797917257c8cbc715f0a31fe551a (patch) | |
tree | 7624fb365c20787f67366a7d6ca5f758ea59b7b0 | |
parent | 6d3c8255ff41e5967f45d9fa0b0c677bbf3c34b5 (diff) | |
parent | 788cb18f652fca380acefdadce305415bc0602b0 (diff) | |
download | base-8178edfc096e797917257c8cbc715f0a31fe551a.tar.gz |
Merge "Fixing android.text.format.Time for non-English locales"
-rw-r--r-- | core/java/android/text/format/TimeFormatter.java | 137 |
1 files changed, 59 insertions, 78 deletions
diff --git a/core/java/android/text/format/TimeFormatter.java b/core/java/android/text/format/TimeFormatter.java index ec79b3650cb2..3a63805defe0 100644 --- a/core/java/android/text/format/TimeFormatter.java +++ b/core/java/android/text/format/TimeFormatter.java @@ -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); - } } |