/* * 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 java.nio.CharBuffer; import java.util.Formatter; import java.util.Locale; import java.util.TimeZone; import libcore.icu.LocaleData; import libcore.util.ZoneInfo; /** * Formatting logic for {@link Time}. Contains a port of Bionic's broken strftime_tz to Java. * *

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; } } /** * 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(); // This behavior is the source of a bug since some formats are defined as being // in ASCII and not localized. if (localeData.zeroDigit != '0') { result = localizeDigits(result); } return result; } finally { outputBuilder = null; numberFormatter = null; } } private String localizeDigits(String 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; } }