• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
TimeFormatter()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      */
format(String pattern, ZoneInfo.WallTime wallTime, ZoneInfo zoneInfo)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 
localizeDigits(String s)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      */
formatInternal(String pattern, ZoneInfo.WallTime wallTime, ZoneInfo zoneInfo)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 
handleToken(CharBuffer formatBuffer, ZoneInfo.WallTime wallTime, ZoneInfo zoneInfo)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 
modifyAndAppend(CharSequence str, int modifier)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 
outputYear(int value, boolean outputTop, boolean outputBottom, int modifier)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 
getFormat(int modifier, String normal, String underscore, String dash, String zero)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 
isLeap(int year)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      */
brokenIsUpper(char toCheck)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      */
brokenIsLower(char toCheck)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      */
brokenToLower(char input)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      */
brokenToUpper(char input)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