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