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