| /* |
| * Based on the UCB version of strftime.c with the copyright notice appearing below. |
| */ |
| |
| /* |
| ** Copyright (c) 1989 The Regents of the University of California. |
| ** All rights reserved. |
| ** |
| ** Redistribution and use in source and binary forms are permitted |
| ** provided that the above copyright notice and this paragraph are |
| ** duplicated in all such forms and that any documentation, |
| ** advertising materials, and other materials related to such |
| ** distribution and use acknowledge that the software was developed |
| ** by the University of California, Berkeley. The name of the |
| ** University may not be used to endorse or promote products derived |
| ** from this software without specific prior written permission. |
| ** THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR |
| ** IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED |
| ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. |
| */ |
| package android.text.format; |
| |
| import android.content.res.Resources; |
| |
| import libcore.icu.LocaleData; |
| import libcore.util.ZoneInfo; |
| |
| import java.nio.CharBuffer; |
| import java.time.Instant; |
| import java.time.LocalDateTime; |
| import java.time.ZoneId; |
| import java.util.Formatter; |
| import java.util.Locale; |
| import java.util.TimeZone; |
| |
| /** |
| * 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 char. |
| private static final int FORCE_LOWER_CASE = -1; |
| |
| private static final int SECSPERMIN = 60; |
| private static final int MINSPERHOUR = 60; |
| private static final int DAYSPERWEEK = 7; |
| private static final int MONSPERYEAR = 12; |
| private static final int HOURSPERDAY = 24; |
| private static final int DAYSPERLYEAR = 366; |
| private static final int DAYSPERNYEAR = 365; |
| |
| /** |
| * The Locale for which the cached LocaleData and formats have been loaded. |
| */ |
| private static Locale sLocale; |
| private static LocaleData sLocaleData; |
| private static String sTimeOnlyFormat; |
| private static String sDateOnlyFormat; |
| private static String sDateTimeFormat; |
| |
| private final LocaleData localeData; |
| private final String dateTimeFormat; |
| private final String timeOnlyFormat; |
| private final String dateOnlyFormat; |
| |
| private StringBuilder outputBuilder; |
| private Formatter numberFormatter; |
| |
| public TimeFormatter() { |
| synchronized (TimeFormatter.class) { |
| Locale locale = Locale.getDefault(); |
| |
| if (sLocale == null || !(locale.equals(sLocale))) { |
| sLocale = locale; |
| sLocaleData = LocaleData.get(locale); |
| |
| Resources r = Resources.getSystem(); |
| sTimeOnlyFormat = r.getString(com.android.internal.R.string.time_of_day); |
| sDateOnlyFormat = r.getString(com.android.internal.R.string.month_day_year); |
| sDateTimeFormat = r.getString(com.android.internal.R.string.date_and_time); |
| } |
| |
| this.dateTimeFormat = sDateTimeFormat; |
| this.timeOnlyFormat = sTimeOnlyFormat; |
| this.dateOnlyFormat = sDateOnlyFormat; |
| localeData = sLocaleData; |
| } |
| } |
| |
| /** |
| * The implementation of {@link TimeMigrationUtils#formatMillisWithFixedFormat(long)} for |
| * 2038-safe formatting with the pattern "%Y-%m-%d %H:%M:%S" and including the historic |
| * incorrect digit localization behavior. |
| */ |
| String formatMillisWithFixedFormat(long timeMillis) { |
| // This method is deliberately not a general purpose replacement for |
| // format(String, ZoneInfo.WallTime, ZoneInfo): It hard-codes the pattern used; many of the |
| // pattern characters supported by Time.format() have unusual behavior which would make |
| // using java.time.format or similar packages difficult. It would be a lot of work to share |
| // behavior and many internal Android usecases can be covered by this common pattern |
| // behavior. |
| |
| // No need to worry about overflow / underflow: long millis is representable by Instant and |
| // LocalDateTime with room to spare. |
| Instant instant = Instant.ofEpochMilli(timeMillis); |
| |
| // Date/times are calculated in the current system default time zone. |
| LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); |
| |
| // You'd think it would be as simple as: |
| // DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", locale); |
| // return formatter.format(localDateTime); |
| // but we retain Time's behavior around digits. |
| |
| StringBuilder stringBuilder = new StringBuilder(19); |
| |
| // This effectively uses the US locale because number localization is handled separately |
| // (see below). |
| stringBuilder.append(localDateTime.getYear()); |
| stringBuilder.append('-'); |
| append2DigitNumber(stringBuilder, localDateTime.getMonthValue()); |
| stringBuilder.append('-'); |
| append2DigitNumber(stringBuilder, localDateTime.getDayOfMonth()); |
| stringBuilder.append(' '); |
| append2DigitNumber(stringBuilder, localDateTime.getHour()); |
| stringBuilder.append(':'); |
| append2DigitNumber(stringBuilder, localDateTime.getMinute()); |
| stringBuilder.append(':'); |
| append2DigitNumber(stringBuilder, localDateTime.getSecond()); |
| |
| String result = stringBuilder.toString(); |
| return localizeDigits(result); |
| } |
| |
| /** Zero-pads value as needed to achieve a 2-digit number. */ |
| private static void append2DigitNumber(StringBuilder builder, int value) { |
| if (value < 10) { |
| builder.append('0'); |
| } |
| builder.append(value); |
| } |
| |
| /** |
| * Format the specified {@code wallTime} using {@code pattern}. The output is returned. |
| */ |
| public String format(String pattern, ZoneInfo.WallTime wallTime, ZoneInfo zoneInfo) { |
| try { |
| StringBuilder stringBuilder = new StringBuilder(); |
| |
| outputBuilder = stringBuilder; |
| // 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(); |
| // The localizeDigits() behavior is the source of a bug since some formats are defined |
| // as being in ASCII and not localized. |
| return localizeDigits(result); |
| } finally { |
| outputBuilder = null; |
| numberFormatter = null; |
| } |
| } |
| |
| private String localizeDigits(String s) { |
| if (localeData.zeroDigit == '0') { |
| return s; |
| } |
| |
| int length = s.length(); |
| int offsetToLocalizedDigits = localeData.zeroDigit - '0'; |
| StringBuilder result = new StringBuilder(length); |
| for (int i = 0; i < length; ++i) { |
| char ch = s.charAt(i); |
| if (ch >= '0' && ch <= '9') { |
| ch += offsetToLocalizedDigits; |
| } |
| result.append(ch); |
| } |
| return result.toString(); |
| } |
| |
| /** |
| * Format the specified {@code wallTime} using {@code pattern}. The output is written to |
| * {@link #outputBuilder}. |
| */ |
| private void formatInternal(String pattern, ZoneInfo.WallTime wallTime, ZoneInfo zoneInfo) { |
| CharBuffer formatBuffer = CharBuffer.wrap(pattern); |
| while (formatBuffer.remaining() > 0) { |
| boolean outputCurrentChar = true; |
| char currentChar = formatBuffer.get(formatBuffer.position()); |
| if (currentChar == '%') { |
| outputCurrentChar = handleToken(formatBuffer, wallTime, zoneInfo); |
| } |
| if (outputCurrentChar) { |
| outputBuilder.append(formatBuffer.get(formatBuffer.position())); |
| } |
| formatBuffer.position(formatBuffer.position() + 1); |
| } |
| } |
| |
| private boolean handleToken(CharBuffer formatBuffer, ZoneInfo.WallTime wallTime, |
| ZoneInfo zoneInfo) { |
| |
| // 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 char. |
| formatBuffer.position(formatBuffer.position() + 1); |
| char currentChar = formatBuffer.get(formatBuffer.position()); |
| switch (currentChar) { |
| case 'A': |
| modifyAndAppend((wallTime.getWeekDay() < 0 |
| || wallTime.getWeekDay() >= DAYSPERWEEK) |
| ? "?" : localeData.longWeekdayNames[wallTime.getWeekDay() + 1], |
| modifier); |
| return false; |
| case 'a': |
| modifyAndAppend((wallTime.getWeekDay() < 0 |
| || wallTime.getWeekDay() >= DAYSPERWEEK) |
| ? "?" : localeData.shortWeekdayNames[wallTime.getWeekDay() + 1], |
| modifier); |
| return false; |
| case 'B': |
| if (modifier == '-') { |
| modifyAndAppend((wallTime.getMonth() < 0 |
| || wallTime.getMonth() >= MONSPERYEAR) |
| ? "?" |
| : localeData.longStandAloneMonthNames[wallTime.getMonth()], |
| modifier); |
| } else { |
| modifyAndAppend((wallTime.getMonth() < 0 |
| || wallTime.getMonth() >= MONSPERYEAR) |
| ? "?" : localeData.longMonthNames[wallTime.getMonth()], |
| modifier); |
| } |
| return false; |
| case 'b': |
| case 'h': |
| modifyAndAppend((wallTime.getMonth() < 0 || wallTime.getMonth() >= MONSPERYEAR) |
| ? "?" : localeData.shortMonthNames[wallTime.getMonth()], |
| modifier); |
| return false; |
| case 'C': |
| outputYear(wallTime.getYear(), true, false, modifier); |
| return false; |
| case 'c': |
| formatInternal(dateTimeFormat, wallTime, zoneInfo); |
| return false; |
| case 'D': |
| formatInternal("%m/%d/%y", wallTime, zoneInfo); |
| return false; |
| case 'd': |
| numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), |
| wallTime.getMonthDay()); |
| return false; |
| case 'E': |
| case 'O': |
| // C99 locale modifiers are not supported. |
| continue; |
| case '_': |
| case '-': |
| case '0': |
| case '^': |
| case '#': |
| modifier = currentChar; |
| continue; |
| case 'e': |
| numberFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"), |
| wallTime.getMonthDay()); |
| return false; |
| case 'F': |
| formatInternal("%Y-%m-%d", wallTime, zoneInfo); |
| return false; |
| case 'H': |
| numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), |
| wallTime.getHour()); |
| return false; |
| case 'I': |
| int hour = (wallTime.getHour() % 12 != 0) ? (wallTime.getHour() % 12) : 12; |
| numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), hour); |
| return false; |
| case 'j': |
| int yearDay = wallTime.getYearDay() + 1; |
| numberFormatter.format(getFormat(modifier, "%03d", "%3d", "%d", "%03d"), |
| yearDay); |
| return false; |
| case 'k': |
| numberFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"), |
| wallTime.getHour()); |
| return false; |
| case 'l': |
| int n2 = (wallTime.getHour() % 12 != 0) ? (wallTime.getHour() % 12) : 12; |
| numberFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"), n2); |
| return false; |
| case 'M': |
| numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), |
| wallTime.getMinute()); |
| return false; |
| case 'm': |
| numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), |
| wallTime.getMonth() + 1); |
| return false; |
| case 'n': |
| outputBuilder.append('\n'); |
| return false; |
| case 'p': |
| modifyAndAppend((wallTime.getHour() >= (HOURSPERDAY / 2)) ? localeData.amPm[1] |
| : localeData.amPm[0], modifier); |
| return false; |
| case 'P': |
| modifyAndAppend((wallTime.getHour() >= (HOURSPERDAY / 2)) ? localeData.amPm[1] |
| : localeData.amPm[0], FORCE_LOWER_CASE); |
| return false; |
| case 'R': |
| formatInternal("%H:%M", wallTime, zoneInfo); |
| return false; |
| case 'r': |
| formatInternal("%I:%M:%S %p", wallTime, zoneInfo); |
| return false; |
| case 'S': |
| numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), |
| wallTime.getSecond()); |
| return false; |
| case 's': |
| int timeInSeconds = wallTime.mktime(zoneInfo); |
| outputBuilder.append(Integer.toString(timeInSeconds)); |
| return false; |
| case 'T': |
| formatInternal("%H:%M:%S", wallTime, zoneInfo); |
| return false; |
| case 't': |
| outputBuilder.append('\t'); |
| return false; |
| case 'U': |
| 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(); |
| numberFormatter.format("%d", day); |
| return false; |
| case 'V': /* ISO 8601 week number */ |
| case 'G': /* ISO 8601 year (four digits) */ |
| case 'g': /* ISO 8601 year (two digits) */ |
| { |
| int year = wallTime.getYear(); |
| int yday = wallTime.getYearDay(); |
| int wday = wallTime.getWeekDay(); |
| int w; |
| while (true) { |
| int len = isLeap(year) ? DAYSPERLYEAR : DAYSPERNYEAR; |
| // What yday (-3 ... 3) does the ISO year begin on? |
| int bot = ((yday + 11 - wday) % DAYSPERWEEK) - 3; |
| // What yday does the NEXT ISO year begin on? |
| int top = bot - (len % DAYSPERWEEK); |
| if (top < -3) { |
| top += DAYSPERWEEK; |
| } |
| top += len; |
| if (yday >= top) { |
| ++year; |
| w = 1; |
| break; |
| } |
| if (yday >= bot) { |
| w = 1 + ((yday - bot) / DAYSPERWEEK); |
| break; |
| } |
| --year; |
| yday += isLeap(year) ? DAYSPERLYEAR : DAYSPERNYEAR; |
| } |
| 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); |
| } |
| return false; |
| } |
| case 'v': |
| formatInternal("%e-%b-%Y", wallTime, zoneInfo); |
| return false; |
| case 'W': |
| int n = (wallTime.getYearDay() + DAYSPERWEEK - ( |
| wallTime.getWeekDay() != 0 ? (wallTime.getWeekDay() - 1) |
| : (DAYSPERWEEK - 1))) / DAYSPERWEEK; |
| numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), n); |
| return false; |
| case 'w': |
| numberFormatter.format("%d", wallTime.getWeekDay()); |
| return false; |
| case 'X': |
| formatInternal(timeOnlyFormat, wallTime, zoneInfo); |
| return false; |
| case 'x': |
| formatInternal(dateOnlyFormat, wallTime, zoneInfo); |
| return false; |
| case 'y': |
| outputYear(wallTime.getYear(), false, true, modifier); |
| return false; |
| case 'Y': |
| outputYear(wallTime.getYear(), true, true, modifier); |
| return false; |
| case 'Z': |
| if (wallTime.getIsDst() < 0) { |
| return false; |
| } |
| boolean isDst = wallTime.getIsDst() != 0; |
| modifyAndAppend(zoneInfo.getDisplayName(isDst, TimeZone.SHORT), modifier); |
| return false; |
| case 'z': { |
| if (wallTime.getIsDst() < 0) { |
| return false; |
| } |
| int diff = wallTime.getGmtOffset(); |
| char sign; |
| if (diff < 0) { |
| sign = '-'; |
| diff = -diff; |
| } else { |
| sign = '+'; |
| } |
| outputBuilder.append(sign); |
| diff /= SECSPERMIN; |
| diff = (diff / MINSPERHOUR) * 100 + (diff % MINSPERHOUR); |
| numberFormatter.format(getFormat(modifier, "%04d", "%4d", "%d", "%04d"), diff); |
| return false; |
| } |
| case '+': |
| formatInternal("%a %b %e %H:%M:%S %Z %Y", wallTime, zoneInfo); |
| return false; |
| case '%': |
| // If conversion char is undefined, behavior is undefined. Print out the |
| // character itself. |
| default: |
| return true; |
| } |
| } |
| return true; |
| } |
| |
| private void modifyAndAppend(CharSequence str, int modifier) { |
| switch (modifier) { |
| case FORCE_LOWER_CASE: |
| for (int i = 0; i < str.length(); i++) { |
| outputBuilder.append(brokenToLower(str.charAt(i))); |
| } |
| break; |
| case '^': |
| for (int i = 0; i < str.length(); i++) { |
| outputBuilder.append(brokenToUpper(str.charAt(i))); |
| } |
| break; |
| case '#': |
| for (int i = 0; i < str.length(); i++) { |
| char c = str.charAt(i); |
| if (brokenIsUpper(c)) { |
| c = brokenToLower(c); |
| } else if (brokenIsLower(c)) { |
| c = brokenToUpper(c); |
| } |
| outputBuilder.append(c); |
| } |
| break; |
| default: |
| outputBuilder.append(str); |
| } |
| } |
| |
| private void outputYear(int value, boolean outputTop, boolean outputBottom, int modifier) { |
| int lead; |
| int trail; |
| |
| final int DIVISOR = 100; |
| trail = value % DIVISOR; |
| lead = value / DIVISOR + trail / DIVISOR; |
| trail %= DIVISOR; |
| if (trail < 0 && lead > 0) { |
| trail += DIVISOR; |
| --lead; |
| } else if (lead < 0 && trail > 0) { |
| trail -= DIVISOR; |
| ++lead; |
| } |
| if (outputTop) { |
| if (lead == 0 && trail < 0) { |
| outputBuilder.append("-0"); |
| } else { |
| numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), lead); |
| } |
| } |
| if (outputBottom) { |
| int n = ((trail < 0) ? -trail : trail); |
| numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), n); |
| } |
| } |
| |
| private static String getFormat(int modifier, String normal, String underscore, String dash, |
| String zero) { |
| switch (modifier) { |
| case '_': |
| return underscore; |
| case '-': |
| return dash; |
| case '0': |
| return zero; |
| } |
| return normal; |
| } |
| |
| private static boolean isLeap(int year) { |
| return (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0)); |
| } |
| |
| /** |
| * 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 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 codes in |
| * order to be compatible with the old native implementation. |
| */ |
| private static char brokenToLower(char input) { |
| if (input >= 'A' && input <= 'Z') { |
| return (char) (input - 'A' + 'a'); |
| } |
| return input; |
| } |
| |
| /** |
| * 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') { |
| return (char) (input - 'a' + 'A'); |
| } |
| return input; |
| } |
| |
| } |