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.CharBuffer;
26import java.util.Formatter;
27import java.util.Locale;
28import java.util.TimeZone;
29import libcore.icu.LocaleData;
30import 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 */
37class 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