Home | History | Annotate | Download | only in format
      1 /*
      2  * Based on the UCB version of strftime.c with the copyright notice appearing below.
      3  */
      4 
      5 /*
      6 ** Copyright (c) 1989 The Regents of the University of California.
      7 ** All rights reserved.
      8 **
      9 ** Redistribution and use in source and binary forms are permitted
     10 ** provided that the above copyright notice and this paragraph are
     11 ** duplicated in all such forms and that any documentation,
     12 ** advertising materials, and other materials related to such
     13 ** distribution and use acknowledge that the software was developed
     14 ** by the University of California, Berkeley. The name of the
     15 ** University may not be used to endorse or promote products derived
     16 ** from this software without specific prior written permission.
     17 ** THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
     18 ** IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
     19 ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
     20 */
     21 package android.text.format;
     22 
     23 import android.content.res.Resources;
     24 
     25 import libcore.icu.LocaleData;
     26 import libcore.util.ZoneInfo;
     27 
     28 import java.nio.CharBuffer;
     29 import java.util.Formatter;
     30 import java.util.Locale;
     31 import java.util.TimeZone;
     32 
     33 /**
     34  * Formatting logic for {@link Time}. Contains a port of Bionic's broken strftime_tz to Java.
     35  *
     36  * <p>This class is not thread safe.
     37  */
     38 class TimeFormatter {
     39     // An arbitrary value outside the range representable by a char.
     40     private static final int FORCE_LOWER_CASE = -1;
     41 
     42     private static final int SECSPERMIN = 60;
     43     private static final int MINSPERHOUR = 60;
     44     private static final int DAYSPERWEEK = 7;
     45     private static final int MONSPERYEAR = 12;
     46     private static final int HOURSPERDAY = 24;
     47     private static final int DAYSPERLYEAR = 366;
     48     private static final int DAYSPERNYEAR = 365;
     49 
     50     /**
     51      * The Locale for which the cached LocaleData and formats have been loaded.
     52      */
     53     private static Locale sLocale;
     54     private static LocaleData sLocaleData;
     55     private static String sTimeOnlyFormat;
     56     private static String sDateOnlyFormat;
     57     private static String sDateTimeFormat;
     58 
     59     private final LocaleData localeData;
     60     private final String dateTimeFormat;
     61     private final String timeOnlyFormat;
     62     private final String dateOnlyFormat;
     63 
     64     private StringBuilder outputBuilder;
     65     private Formatter numberFormatter;
     66 
     67     public TimeFormatter() {
     68         synchronized (TimeFormatter.class) {
     69             Locale locale = Locale.getDefault();
     70 
     71             if (sLocale == null || !(locale.equals(sLocale))) {
     72                 sLocale = locale;
     73                 sLocaleData = LocaleData.get(locale);
     74 
     75                 Resources r = Resources.getSystem();
     76                 sTimeOnlyFormat = r.getString(com.android.internal.R.string.time_of_day);
     77                 sDateOnlyFormat = r.getString(com.android.internal.R.string.month_day_year);
     78                 sDateTimeFormat = r.getString(com.android.internal.R.string.date_and_time);
     79             }
     80 
     81             this.dateTimeFormat = sDateTimeFormat;
     82             this.timeOnlyFormat = sTimeOnlyFormat;
     83             this.dateOnlyFormat = sDateOnlyFormat;
     84             localeData = sLocaleData;
     85         }
     86     }
     87 
     88     /**
     89      * Format the specified {@code wallTime} using {@code pattern}. The output is returned.
     90      */
     91     public String format(String pattern, ZoneInfo.WallTime wallTime, ZoneInfo zoneInfo) {
     92         try {
     93             StringBuilder stringBuilder = new StringBuilder();
     94 
     95             outputBuilder = stringBuilder;
     96             // This uses the US locale because number localization is handled separately (see below)
     97             // and locale sensitive strings are output directly using outputBuilder.
     98             numberFormatter = new Formatter(stringBuilder, Locale.US);
     99 
    100             formatInternal(pattern, wallTime, zoneInfo);
    101             String result = stringBuilder.toString();
    102             // This behavior is the source of a bug since some formats are defined as being
    103             // in ASCII and not localized.
    104             if (localeData.zeroDigit != '0') {
    105                 result = localizeDigits(result);
    106             }
    107             return result;
    108         } finally {
    109             outputBuilder = null;
    110             numberFormatter = null;
    111         }
    112     }
    113 
    114     private String localizeDigits(String s) {
    115         int length = s.length();
    116         int offsetToLocalizedDigits = localeData.zeroDigit - '0';
    117         StringBuilder result = new StringBuilder(length);
    118         for (int i = 0; i < length; ++i) {
    119             char ch = s.charAt(i);
    120             if (ch >= '0' && ch <= '9') {
    121                 ch += offsetToLocalizedDigits;
    122             }
    123             result.append(ch);
    124         }
    125         return result.toString();
    126     }
    127 
    128     /**
    129      * Format the specified {@code wallTime} using {@code pattern}. The output is written to
    130      * {@link #outputBuilder}.
    131      */
    132     private void formatInternal(String pattern, ZoneInfo.WallTime wallTime, ZoneInfo zoneInfo) {
    133         CharBuffer formatBuffer = CharBuffer.wrap(pattern);
    134         while (formatBuffer.remaining() > 0) {
    135             boolean outputCurrentChar = true;
    136             char currentChar = formatBuffer.get(formatBuffer.position());
    137             if (currentChar == '%') {
    138                 outputCurrentChar = handleToken(formatBuffer, wallTime, zoneInfo);
    139             }
    140             if (outputCurrentChar) {
    141                 outputBuilder.append(formatBuffer.get(formatBuffer.position()));
    142             }
    143             formatBuffer.position(formatBuffer.position() + 1);
    144         }
    145     }
    146 
    147     private boolean handleToken(CharBuffer formatBuffer, ZoneInfo.WallTime wallTime,
    148             ZoneInfo zoneInfo) {
    149 
    150         // The char at formatBuffer.position() is expected to be '%' at this point.
    151         int modifier = 0;
    152         while (formatBuffer.remaining() > 1) {
    153             // Increment the position then get the new current char.
    154             formatBuffer.position(formatBuffer.position() + 1);
    155             char currentChar = formatBuffer.get(formatBuffer.position());
    156             switch (currentChar) {
    157                 case 'A':
    158                     modifyAndAppend((wallTime.getWeekDay() < 0
    159                                     || wallTime.getWeekDay() >= DAYSPERWEEK)
    160                                     ? "?" : localeData.longWeekdayNames[wallTime.getWeekDay() + 1],
    161                             modifier);
    162                     return false;
    163                 case 'a':
    164                     modifyAndAppend((wallTime.getWeekDay() < 0
    165                                     || wallTime.getWeekDay() >= DAYSPERWEEK)
    166                                     ? "?" : localeData.shortWeekdayNames[wallTime.getWeekDay() + 1],
    167                             modifier);
    168                     return false;
    169                 case 'B':
    170                     if (modifier == '-') {
    171                         modifyAndAppend((wallTime.getMonth() < 0
    172                                         || wallTime.getMonth() >= MONSPERYEAR)
    173                                         ? "?"
    174                                         : localeData.longStandAloneMonthNames[wallTime.getMonth()],
    175                                 modifier);
    176                     } else {
    177                         modifyAndAppend((wallTime.getMonth() < 0
    178                                         || wallTime.getMonth() >= MONSPERYEAR)
    179                                         ? "?" : localeData.longMonthNames[wallTime.getMonth()],
    180                                 modifier);
    181                     }
    182                     return false;
    183                 case 'b':
    184                 case 'h':
    185                     modifyAndAppend((wallTime.getMonth() < 0 || wallTime.getMonth() >= MONSPERYEAR)
    186                                     ? "?" : localeData.shortMonthNames[wallTime.getMonth()],
    187                             modifier);
    188                     return false;
    189                 case 'C':
    190                     outputYear(wallTime.getYear(), true, false, modifier);
    191                     return false;
    192                 case 'c':
    193                     formatInternal(dateTimeFormat, wallTime, zoneInfo);
    194                     return false;
    195                 case 'D':
    196                     formatInternal("%m/%d/%y", wallTime, zoneInfo);
    197                     return false;
    198                 case 'd':
    199                     numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
    200                             wallTime.getMonthDay());
    201                     return false;
    202                 case 'E':
    203                 case 'O':
    204                     // C99 locale modifiers are not supported.
    205                     continue;
    206                 case '_':
    207                 case '-':
    208                 case '0':
    209                 case '^':
    210                 case '#':
    211                     modifier = currentChar;
    212                     continue;
    213                 case 'e':
    214                     numberFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"),
    215                             wallTime.getMonthDay());
    216                     return false;
    217                 case 'F':
    218                     formatInternal("%Y-%m-%d", wallTime, zoneInfo);
    219                     return false;
    220                 case 'H':
    221                     numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
    222                             wallTime.getHour());
    223                     return false;
    224                 case 'I':
    225                     int hour = (wallTime.getHour() % 12 != 0) ? (wallTime.getHour() % 12) : 12;
    226                     numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), hour);
    227                     return false;
    228                 case 'j':
    229                     int yearDay = wallTime.getYearDay() + 1;
    230                     numberFormatter.format(getFormat(modifier, "%03d", "%3d", "%d", "%03d"),
    231                             yearDay);
    232                     return false;
    233                 case 'k':
    234                     numberFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"),
    235                             wallTime.getHour());
    236                     return false;
    237                 case 'l':
    238                     int n2 = (wallTime.getHour() % 12 != 0) ? (wallTime.getHour() % 12) : 12;
    239                     numberFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"), n2);
    240                     return false;
    241                 case 'M':
    242                     numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
    243                             wallTime.getMinute());
    244                     return false;
    245                 case 'm':
    246                     numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
    247                             wallTime.getMonth() + 1);
    248                     return false;
    249                 case 'n':
    250                     outputBuilder.append('\n');
    251                     return false;
    252                 case 'p':
    253                     modifyAndAppend((wallTime.getHour() >= (HOURSPERDAY / 2)) ? localeData.amPm[1]
    254                             : localeData.amPm[0], modifier);
    255                     return false;
    256                 case 'P':
    257                     modifyAndAppend((wallTime.getHour() >= (HOURSPERDAY / 2)) ? localeData.amPm[1]
    258                             : localeData.amPm[0], FORCE_LOWER_CASE);
    259                     return false;
    260                 case 'R':
    261                     formatInternal("%H:%M", wallTime, zoneInfo);
    262                     return false;
    263                 case 'r':
    264                     formatInternal("%I:%M:%S %p", wallTime, zoneInfo);
    265                     return false;
    266                 case 'S':
    267                     numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
    268                             wallTime.getSecond());
    269                     return false;
    270                 case 's':
    271                     int timeInSeconds = wallTime.mktime(zoneInfo);
    272                     outputBuilder.append(Integer.toString(timeInSeconds));
    273                     return false;
    274                 case 'T':
    275                     formatInternal("%H:%M:%S", wallTime, zoneInfo);
    276                     return false;
    277                 case 't':
    278                     outputBuilder.append('\t');
    279                     return false;
    280                 case 'U':
    281                     numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
    282                             (wallTime.getYearDay() + DAYSPERWEEK - wallTime.getWeekDay())
    283                                     / DAYSPERWEEK);
    284                     return false;
    285                 case 'u':
    286                     int day = (wallTime.getWeekDay() == 0) ? DAYSPERWEEK : wallTime.getWeekDay();
    287                     numberFormatter.format("%d", day);
    288                     return false;
    289                 case 'V':   /* ISO 8601 week number */
    290                 case 'G':   /* ISO 8601 year (four digits) */
    291                 case 'g':   /* ISO 8601 year (two digits) */
    292                 {
    293                     int year = wallTime.getYear();
    294                     int yday = wallTime.getYearDay();
    295                     int wday = wallTime.getWeekDay();
    296                     int w;
    297                     while (true) {
    298                         int len = isLeap(year) ? DAYSPERLYEAR : DAYSPERNYEAR;
    299                         // What yday (-3 ... 3) does the ISO year begin on?
    300                         int bot = ((yday + 11 - wday) % DAYSPERWEEK) - 3;
    301                         // What yday does the NEXT ISO year begin on?
    302                         int top = bot - (len % DAYSPERWEEK);
    303                         if (top < -3) {
    304                             top += DAYSPERWEEK;
    305                         }
    306                         top += len;
    307                         if (yday >= top) {
    308                             ++year;
    309                             w = 1;
    310                             break;
    311                         }
    312                         if (yday >= bot) {
    313                             w = 1 + ((yday - bot) / DAYSPERWEEK);
    314                             break;
    315                         }
    316                         --year;
    317                         yday += isLeap(year) ? DAYSPERLYEAR : DAYSPERNYEAR;
    318                     }
    319                     if (currentChar == 'V') {
    320                         numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), w);
    321                     } else if (currentChar == 'g') {
    322                         outputYear(year, false, true, modifier);
    323                     } else {
    324                         outputYear(year, true, true, modifier);
    325                     }
    326                     return false;
    327                 }
    328                 case 'v':
    329                     formatInternal("%e-%b-%Y", wallTime, zoneInfo);
    330                     return false;
    331                 case 'W':
    332                     int n = (wallTime.getYearDay() + DAYSPERWEEK - (
    333                                     wallTime.getWeekDay() != 0 ? (wallTime.getWeekDay() - 1)
    334                                             : (DAYSPERWEEK - 1))) / DAYSPERWEEK;
    335                     numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), n);
    336                     return false;
    337                 case 'w':
    338                     numberFormatter.format("%d", wallTime.getWeekDay());
    339                     return false;
    340                 case 'X':
    341                     formatInternal(timeOnlyFormat, wallTime, zoneInfo);
    342                     return false;
    343                 case 'x':
    344                     formatInternal(dateOnlyFormat, wallTime, zoneInfo);
    345                     return false;
    346                 case 'y':
    347                     outputYear(wallTime.getYear(), false, true, modifier);
    348                     return false;
    349                 case 'Y':
    350                     outputYear(wallTime.getYear(), true, true, modifier);
    351                     return false;
    352                 case 'Z':
    353                     if (wallTime.getIsDst() < 0) {
    354                         return false;
    355                     }
    356                     boolean isDst = wallTime.getIsDst() != 0;
    357                     modifyAndAppend(zoneInfo.getDisplayName(isDst, TimeZone.SHORT), modifier);
    358                     return false;
    359                 case 'z': {
    360                     if (wallTime.getIsDst() < 0) {
    361                         return false;
    362                     }
    363                     int diff = wallTime.getGmtOffset();
    364                     char sign;
    365                     if (diff < 0) {
    366                         sign = '-';
    367                         diff = -diff;
    368                     } else {
    369                         sign = '+';
    370                     }
    371                     outputBuilder.append(sign);
    372                     diff /= SECSPERMIN;
    373                     diff = (diff / MINSPERHOUR) * 100 + (diff % MINSPERHOUR);
    374                     numberFormatter.format(getFormat(modifier, "%04d", "%4d", "%d", "%04d"), diff);
    375                     return false;
    376                 }
    377                 case '+':
    378                     formatInternal("%a %b %e %H:%M:%S %Z %Y", wallTime, zoneInfo);
    379                     return false;
    380                 case '%':
    381                     // If conversion char is undefined, behavior is undefined. Print out the
    382                     // character itself.
    383                 default:
    384                     return true;
    385             }
    386         }
    387         return true;
    388     }
    389 
    390     private void modifyAndAppend(CharSequence str, int modifier) {
    391         switch (modifier) {
    392             case FORCE_LOWER_CASE:
    393                 for (int i = 0; i < str.length(); i++) {
    394                     outputBuilder.append(brokenToLower(str.charAt(i)));
    395                 }
    396                 break;
    397             case '^':
    398                 for (int i = 0; i < str.length(); i++) {
    399                     outputBuilder.append(brokenToUpper(str.charAt(i)));
    400                 }
    401                 break;
    402             case '#':
    403                 for (int i = 0; i < str.length(); i++) {
    404                     char c = str.charAt(i);
    405                     if (brokenIsUpper(c)) {
    406                         c = brokenToLower(c);
    407                     } else if (brokenIsLower(c)) {
    408                         c = brokenToUpper(c);
    409                     }
    410                     outputBuilder.append(c);
    411                 }
    412                 break;
    413             default:
    414                 outputBuilder.append(str);
    415         }
    416     }
    417 
    418     private void outputYear(int value, boolean outputTop, boolean outputBottom, int modifier) {
    419         int lead;
    420         int trail;
    421 
    422         final int DIVISOR = 100;
    423         trail = value % DIVISOR;
    424         lead = value / DIVISOR + trail / DIVISOR;
    425         trail %= DIVISOR;
    426         if (trail < 0 && lead > 0) {
    427             trail += DIVISOR;
    428             --lead;
    429         } else if (lead < 0 && trail > 0) {
    430             trail -= DIVISOR;
    431             ++lead;
    432         }
    433         if (outputTop) {
    434             if (lead == 0 && trail < 0) {
    435                 outputBuilder.append("-0");
    436             } else {
    437                 numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), lead);
    438             }
    439         }
    440         if (outputBottom) {
    441             int n = ((trail < 0) ? -trail : trail);
    442             numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), n);
    443         }
    444     }
    445 
    446     private static String getFormat(int modifier, String normal, String underscore, String dash,
    447             String zero) {
    448         switch (modifier) {
    449             case '_':
    450                 return underscore;
    451             case '-':
    452                 return dash;
    453             case '0':
    454                 return zero;
    455         }
    456         return normal;
    457     }
    458 
    459     private static boolean isLeap(int year) {
    460         return (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0));
    461     }
    462 
    463     /**
    464      * A broken implementation of {@link Character#isUpperCase(char)} that assumes ASCII codes in
    465      * order to be compatible with the old native implementation.
    466      */
    467     private static boolean brokenIsUpper(char toCheck) {
    468         return toCheck >= 'A' && toCheck <= 'Z';
    469     }
    470 
    471     /**
    472      * A broken implementation of {@link Character#isLowerCase(char)} that assumes ASCII codes in
    473      * order to be compatible with the old native implementation.
    474      */
    475     private static boolean brokenIsLower(char toCheck) {
    476         return toCheck >= 'a' && toCheck <= 'z';
    477     }
    478 
    479     /**
    480      * A broken implementation of {@link Character#toLowerCase(char)} that assumes ASCII codes in
    481      * order to be compatible with the old native implementation.
    482      */
    483     private static char brokenToLower(char input) {
    484         if (input >= 'A' && input <= 'Z') {
    485             return (char) (input - 'A' + 'a');
    486         }
    487         return input;
    488     }
    489 
    490     /**
    491      * A broken implementation of {@link Character#toUpperCase(char)} that assumes ASCII codes in
    492      * order to be compatible with the old native implementation.
    493      */
    494     private static char brokenToUpper(char input) {
    495         if (input >= 'a' && input <= 'z') {
    496             return (char) (input - 'a' + 'A');
    497         }
    498         return input;
    499     }
    500 
    501 }
    502