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