1 /*
2  * Based on the UCB version of strftime.c with the copyright notice appearing below.
3  */
4 
5 /*
6 ** Copyright (c) 1989 The Regents of the University of California.
7 ** All rights reserved.
8 **
9 ** Redistribution and use in source and binary forms are permitted
10 ** provided that the above copyright notice and this paragraph are
11 ** duplicated in all such forms and that any documentation,
12 ** advertising materials, and other materials related to such
13 ** distribution and use acknowledge that the software was developed
14 ** by the University of California, Berkeley. The name of the
15 ** University may not be used to endorse or promote products derived
16 ** from this software without specific prior written permission.
17 ** THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
18 ** IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
19 ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
20 */
21 package android.text.format;
22 
23 import android.content.res.Resources;
24 
25 import libcore.icu.LocaleData;
26 import libcore.util.ZoneInfo;
27 
28 import java.nio.CharBuffer;
29 import java.util.Formatter;
30 import java.util.Locale;
31 import java.util.TimeZone;
32 
33 /**
34  * Formatting logic for {@link Time}. Contains a port of Bionic's broken strftime_tz to Java.
35  *
36  * <p>This class is not thread safe.
37  */
38 class TimeFormatter {
39     // An arbitrary value outside the range representable by a char.
40     private static final int FORCE_LOWER_CASE = -1;
41 
42     private static final int SECSPERMIN = 60;
43     private static final int MINSPERHOUR = 60;
44     private static final int DAYSPERWEEK = 7;
45     private static final int MONSPERYEAR = 12;
46     private static final int HOURSPERDAY = 24;
47     private static final int DAYSPERLYEAR = 366;
48     private static final int DAYSPERNYEAR = 365;
49 
50     /**
51      * The Locale for which the cached LocaleData and formats have been loaded.
52      */
53     private static Locale sLocale;
54     private static LocaleData sLocaleData;
55     private static String sTimeOnlyFormat;
56     private static String sDateOnlyFormat;
57     private static String sDateTimeFormat;
58 
59     private final LocaleData localeData;
60     private final String dateTimeFormat;
61     private final String timeOnlyFormat;
62     private final String dateOnlyFormat;
63 
64     private StringBuilder outputBuilder;
65     private Formatter numberFormatter;
66 
TimeFormatter()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      */
format(String pattern, ZoneInfo.WallTime wallTime, ZoneInfo zoneInfo)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 
localizeDigits(String s)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      */
formatInternal(String pattern, ZoneInfo.WallTime wallTime, ZoneInfo zoneInfo)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 
handleToken(CharBuffer formatBuffer, ZoneInfo.WallTime wallTime, ZoneInfo zoneInfo)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 
modifyAndAppend(CharSequence str, int modifier)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 
outputYear(int value, boolean outputTop, boolean outputBottom, int modifier)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 
getFormat(int modifier, String normal, String underscore, String dash, String zero)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 
isLeap(int year)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      */
brokenIsUpper(char toCheck)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      */
brokenIsLower(char toCheck)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      */
brokenToLower(char input)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      */
brokenToUpper(char input)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