• 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 import android.icu.text.DateFormatSymbols;
25 import android.icu.text.DecimalFormatSymbols;
26 
27 import com.android.i18n.timezone.WallTime;
28 import com.android.i18n.timezone.ZoneInfoData;
29 
30 import java.nio.CharBuffer;
31 import java.time.Instant;
32 import java.time.LocalDateTime;
33 import java.time.ZoneId;
34 import java.util.Formatter;
35 import java.util.Locale;
36 import java.util.TimeZone;
37 
38 /**
39  * Formatting logic for {@link Time}. Contains a port of Bionic's broken strftime_tz to Java.
40  *
41  * <p>This class is not thread safe.
42  */
43 @android.ravenwood.annotation.RavenwoodKeepWholeClass
44 class TimeFormatter {
45     // An arbitrary value outside the range representable by a char.
46     private static final int FORCE_LOWER_CASE = -1;
47 
48     private static final int SECSPERMIN = 60;
49     private static final int MINSPERHOUR = 60;
50     private static final int DAYSPERWEEK = 7;
51     private static final int MONSPERYEAR = 12;
52     private static final int HOURSPERDAY = 24;
53     private static final int DAYSPERLYEAR = 366;
54     private static final int DAYSPERNYEAR = 365;
55 
56     /**
57      * The Locale for which the cached symbols and formats have been loaded.
58      */
59     private static Locale sLocale;
60     private static DateFormatSymbols sDateFormatSymbols;
61     private static DecimalFormatSymbols sDecimalFormatSymbols;
62     private static String sTimeOnlyFormat;
63     private static String sDateOnlyFormat;
64     private static String sDateTimeFormat;
65 
66     private final DateFormatSymbols dateFormatSymbols;
67     private final DecimalFormatSymbols decimalFormatSymbols;
68     private final String dateTimeFormat;
69     private final String timeOnlyFormat;
70     private final String dateOnlyFormat;
71 
72     private StringBuilder outputBuilder;
73     private Formatter numberFormatter;
74 
TimeFormatter()75     public TimeFormatter() {
76         synchronized (TimeFormatter.class) {
77             Locale locale = Locale.getDefault();
78 
79             if (sLocale == null || !(locale.equals(sLocale))) {
80                 sLocale = locale;
81                 sDateFormatSymbols = DateFormat.getIcuDateFormatSymbols(locale);
82                 sDecimalFormatSymbols = DecimalFormatSymbols.getInstance(locale);
83 
84                 Resources r = Resources.getSystem();
85                 sTimeOnlyFormat = r.getString(com.android.internal.R.string.time_of_day);
86                 sDateOnlyFormat = r.getString(com.android.internal.R.string.month_day_year);
87                 sDateTimeFormat = r.getString(com.android.internal.R.string.date_and_time);
88             }
89 
90             this.dateFormatSymbols = sDateFormatSymbols;
91             this.decimalFormatSymbols = sDecimalFormatSymbols;
92             this.dateTimeFormat = sDateTimeFormat;
93             this.timeOnlyFormat = sTimeOnlyFormat;
94             this.dateOnlyFormat = sDateOnlyFormat;
95         }
96     }
97 
98     /**
99      * The implementation of {@link TimeMigrationUtils#formatMillisWithFixedFormat(long)} for
100      * 2038-safe formatting with the pattern "%Y-%m-%d %H:%M:%S" and including the historic
101      * incorrect digit localization behavior.
102      */
formatMillisWithFixedFormat(long timeMillis)103     String formatMillisWithFixedFormat(long timeMillis) {
104         // This method is deliberately not a general purpose replacement for format(String,
105         // ZoneInfoData.WallTime, ZoneInfoData): It hard-codes the pattern used; many of the
106         // pattern characters supported by Time.format() have unusual behavior which would make
107         // using java.time.format or similar packages difficult. It would be a lot of work to share
108         // behavior and many internal Android usecases can be covered by this common pattern
109         // behavior.
110 
111         // No need to worry about overflow / underflow: long millis is representable by Instant and
112         // LocalDateTime with room to spare.
113         Instant instant = Instant.ofEpochMilli(timeMillis);
114 
115         // Date/times are calculated in the current system default time zone.
116         LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
117 
118         // You'd think it would be as simple as:
119         // DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", locale);
120         // return formatter.format(localDateTime);
121         // but we retain Time's behavior around digits.
122 
123         StringBuilder stringBuilder = new StringBuilder(19);
124 
125         // This effectively uses the US locale because number localization is handled separately
126         // (see below).
127         stringBuilder.append(localDateTime.getYear());
128         stringBuilder.append('-');
129         append2DigitNumber(stringBuilder, localDateTime.getMonthValue());
130         stringBuilder.append('-');
131         append2DigitNumber(stringBuilder, localDateTime.getDayOfMonth());
132         stringBuilder.append(' ');
133         append2DigitNumber(stringBuilder, localDateTime.getHour());
134         stringBuilder.append(':');
135         append2DigitNumber(stringBuilder, localDateTime.getMinute());
136         stringBuilder.append(':');
137         append2DigitNumber(stringBuilder, localDateTime.getSecond());
138 
139         String result = stringBuilder.toString();
140         return localizeDigits(result);
141     }
142 
143     /** Zero-pads value as needed to achieve a 2-digit number. */
append2DigitNumber(StringBuilder builder, int value)144     private static void append2DigitNumber(StringBuilder builder, int value) {
145         if (value < 10) {
146             builder.append('0');
147         }
148         builder.append(value);
149     }
150 
151     /**
152      * Format the specified {@code wallTime} using {@code pattern}. The output is returned.
153      */
format(String pattern, WallTime wallTime, ZoneInfoData zoneInfoData)154     public String format(String pattern, WallTime wallTime,
155             ZoneInfoData zoneInfoData) {
156         try {
157             StringBuilder stringBuilder = new StringBuilder();
158 
159             outputBuilder = stringBuilder;
160             // This uses the US locale because number localization is handled separately (see below)
161             // and locale sensitive strings are output directly using outputBuilder.
162             numberFormatter = new Formatter(stringBuilder, Locale.US);
163 
164             formatInternal(pattern, wallTime, zoneInfoData);
165             String result = stringBuilder.toString();
166             // The localizeDigits() behavior is the source of a bug since some formats are defined
167             // as being in ASCII and not localized.
168             return localizeDigits(result);
169         } finally {
170             outputBuilder = null;
171             numberFormatter = null;
172         }
173     }
174 
localizeDigits(String s)175     private String localizeDigits(String s) {
176         if (decimalFormatSymbols.getZeroDigit() == '0') {
177             return s;
178         }
179 
180         int length = s.length();
181         int offsetToLocalizedDigits = decimalFormatSymbols.getZeroDigit() - '0';
182         StringBuilder result = new StringBuilder(length);
183         for (int i = 0; i < length; ++i) {
184             char ch = s.charAt(i);
185             if (ch >= '0' && ch <= '9') {
186                 ch += offsetToLocalizedDigits;
187             }
188             result.append(ch);
189         }
190         return result.toString();
191     }
192 
193     /**
194      * Format the specified {@code wallTime} using {@code pattern}. The output is written to
195      * {@link #outputBuilder}.
196      */
formatInternal(String pattern, WallTime wallTime, ZoneInfoData zoneInfoData)197     private void formatInternal(String pattern, WallTime wallTime,
198             ZoneInfoData zoneInfoData) {
199         CharBuffer formatBuffer = CharBuffer.wrap(pattern);
200         while (formatBuffer.remaining() > 0) {
201             boolean outputCurrentChar = true;
202             char currentChar = formatBuffer.get(formatBuffer.position());
203             if (currentChar == '%') {
204                 outputCurrentChar = handleToken(formatBuffer, wallTime, zoneInfoData);
205             }
206             if (outputCurrentChar) {
207                 outputBuilder.append(formatBuffer.get(formatBuffer.position()));
208             }
209             formatBuffer.position(formatBuffer.position() + 1);
210         }
211     }
212 
handleToken(CharBuffer formatBuffer, WallTime wallTime, ZoneInfoData zoneInfoData)213     private boolean handleToken(CharBuffer formatBuffer, WallTime wallTime,
214             ZoneInfoData zoneInfoData) {
215 
216         // The char at formatBuffer.position() is expected to be '%' at this point.
217         int modifier = 0;
218         while (formatBuffer.remaining() > 1) {
219             // Increment the position then get the new current char.
220             formatBuffer.position(formatBuffer.position() + 1);
221             char currentChar = formatBuffer.get(formatBuffer.position());
222             switch (currentChar) {
223                 case 'A':
224                     modifyAndAppend(
225                         (wallTime.getWeekDay() < 0 || wallTime.getWeekDay() >= DAYSPERWEEK)
226                             ? "?"
227                             : dateFormatSymbols.getWeekdays(DateFormatSymbols.FORMAT,
228                                 DateFormatSymbols.WIDE)[wallTime.getWeekDay() + 1],
229                             modifier);
230                     return false;
231                 case 'a':
232                     modifyAndAppend(
233                         (wallTime.getWeekDay() < 0 || wallTime.getWeekDay() >= DAYSPERWEEK)
234                             ? "?"
235                             : dateFormatSymbols.getWeekdays(DateFormatSymbols.FORMAT,
236                                 DateFormatSymbols.ABBREVIATED)[wallTime.getWeekDay() + 1],
237                             modifier);
238                     return false;
239                 case 'B':
240                     if (modifier == '-') {
241                         modifyAndAppend(
242                             (wallTime.getMonth() < 0 || wallTime.getMonth() >= MONSPERYEAR)
243                                 ? "?"
244                                 : dateFormatSymbols.getMonths(DateFormatSymbols.STANDALONE,
245                                     DateFormatSymbols.WIDE)[wallTime.getMonth()],
246                                 modifier);
247                     } else {
248                         modifyAndAppend(
249                             (wallTime.getMonth() < 0 || wallTime.getMonth() >= MONSPERYEAR)
250                                 ? "?"
251                                 : dateFormatSymbols.getMonths(DateFormatSymbols.FORMAT,
252                                     DateFormatSymbols.WIDE)[wallTime.getMonth()],
253                                 modifier);
254                     }
255                     return false;
256                 case 'b':
257                 case 'h':
258                     modifyAndAppend((wallTime.getMonth() < 0 || wallTime.getMonth() >= MONSPERYEAR)
259                             ? "?"
260                             : dateFormatSymbols.getMonths(DateFormatSymbols.FORMAT,
261                                 DateFormatSymbols.ABBREVIATED)[wallTime.getMonth()],
262                             modifier);
263                     return false;
264                 case 'C':
265                     outputYear(wallTime.getYear(), true, false, modifier);
266                     return false;
267                 case 'c':
268                     formatInternal(dateTimeFormat, wallTime, zoneInfoData);
269                     return false;
270                 case 'D':
271                     formatInternal("%m/%d/%y", wallTime, zoneInfoData);
272                     return false;
273                 case 'd':
274                     numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
275                             wallTime.getMonthDay());
276                     return false;
277                 case 'E':
278                 case 'O':
279                     // C99 locale modifiers are not supported.
280                     continue;
281                 case '_':
282                 case '-':
283                 case '0':
284                 case '^':
285                 case '#':
286                     modifier = currentChar;
287                     continue;
288                 case 'e':
289                     numberFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"),
290                             wallTime.getMonthDay());
291                     return false;
292                 case 'F':
293                     formatInternal("%Y-%m-%d", wallTime, zoneInfoData);
294                     return false;
295                 case 'H':
296                     numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
297                             wallTime.getHour());
298                     return false;
299                 case 'I':
300                     int hour = (wallTime.getHour() % 12 != 0) ? (wallTime.getHour() % 12) : 12;
301                     numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), hour);
302                     return false;
303                 case 'j':
304                     int yearDay = wallTime.getYearDay() + 1;
305                     numberFormatter.format(getFormat(modifier, "%03d", "%3d", "%d", "%03d"),
306                             yearDay);
307                     return false;
308                 case 'k':
309                     numberFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"),
310                             wallTime.getHour());
311                     return false;
312                 case 'l':
313                     int n2 = (wallTime.getHour() % 12 != 0) ? (wallTime.getHour() % 12) : 12;
314                     numberFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"), n2);
315                     return false;
316                 case 'M':
317                     numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
318                             wallTime.getMinute());
319                     return false;
320                 case 'm':
321                     numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
322                             wallTime.getMonth() + 1);
323                     return false;
324                 case 'n':
325                     outputBuilder.append('\n');
326                     return false;
327                 case 'p':
328                     modifyAndAppend((wallTime.getHour() >= (HOURSPERDAY / 2))
329                             ? dateFormatSymbols.getAmPmStrings()[1]
330                             : dateFormatSymbols.getAmPmStrings()[0], modifier);
331                     return false;
332                 case 'P':
333                     modifyAndAppend((wallTime.getHour() >= (HOURSPERDAY / 2))
334                             ? dateFormatSymbols.getAmPmStrings()[1]
335                             : dateFormatSymbols.getAmPmStrings()[0], FORCE_LOWER_CASE);
336                     return false;
337                 case 'R':
338                     formatInternal("%H:%M", wallTime, zoneInfoData);
339                     return false;
340                 case 'r':
341                     formatInternal("%I:%M:%S %p", wallTime, zoneInfoData);
342                     return false;
343                 case 'S':
344                     numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
345                             wallTime.getSecond());
346                     return false;
347                 case 's':
348                     int timeInSeconds = wallTime.mktime(zoneInfoData);
349                     outputBuilder.append(Integer.toString(timeInSeconds));
350                     return false;
351                 case 'T':
352                     formatInternal("%H:%M:%S", wallTime, zoneInfoData);
353                     return false;
354                 case 't':
355                     outputBuilder.append('\t');
356                     return false;
357                 case 'U':
358                     numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
359                             (wallTime.getYearDay() + DAYSPERWEEK - wallTime.getWeekDay())
360                                     / DAYSPERWEEK);
361                     return false;
362                 case 'u':
363                     int day = (wallTime.getWeekDay() == 0) ? DAYSPERWEEK : wallTime.getWeekDay();
364                     numberFormatter.format("%d", day);
365                     return false;
366                 case 'V':   /* ISO 8601 week number */
367                 case 'G':   /* ISO 8601 year (four digits) */
368                 case 'g':   /* ISO 8601 year (two digits) */
369                 {
370                     int year = wallTime.getYear();
371                     int yday = wallTime.getYearDay();
372                     int wday = wallTime.getWeekDay();
373                     int w;
374                     while (true) {
375                         int len = isLeap(year) ? DAYSPERLYEAR : DAYSPERNYEAR;
376                         // What yday (-3 ... 3) does the ISO year begin on?
377                         int bot = ((yday + 11 - wday) % DAYSPERWEEK) - 3;
378                         // What yday does the NEXT ISO year begin on?
379                         int top = bot - (len % DAYSPERWEEK);
380                         if (top < -3) {
381                             top += DAYSPERWEEK;
382                         }
383                         top += len;
384                         if (yday >= top) {
385                             ++year;
386                             w = 1;
387                             break;
388                         }
389                         if (yday >= bot) {
390                             w = 1 + ((yday - bot) / DAYSPERWEEK);
391                             break;
392                         }
393                         --year;
394                         yday += isLeap(year) ? DAYSPERLYEAR : DAYSPERNYEAR;
395                     }
396                     if (currentChar == 'V') {
397                         numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), w);
398                     } else if (currentChar == 'g') {
399                         outputYear(year, false, true, modifier);
400                     } else {
401                         outputYear(year, true, true, modifier);
402                     }
403                     return false;
404                 }
405                 case 'v':
406                     formatInternal("%e-%b-%Y", wallTime, zoneInfoData);
407                     return false;
408                 case 'W':
409                     int n = (wallTime.getYearDay() + DAYSPERWEEK - (
410                                     wallTime.getWeekDay() != 0 ? (wallTime.getWeekDay() - 1)
411                                             : (DAYSPERWEEK - 1))) / DAYSPERWEEK;
412                     numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), n);
413                     return false;
414                 case 'w':
415                     numberFormatter.format("%d", wallTime.getWeekDay());
416                     return false;
417                 case 'X':
418                     formatInternal(timeOnlyFormat, wallTime, zoneInfoData);
419                     return false;
420                 case 'x':
421                     formatInternal(dateOnlyFormat, wallTime, zoneInfoData);
422                     return false;
423                 case 'y':
424                     outputYear(wallTime.getYear(), false, true, modifier);
425                     return false;
426                 case 'Y':
427                     outputYear(wallTime.getYear(), true, true, modifier);
428                     return false;
429                 case 'Z':
430                     if (wallTime.getIsDst() < 0) {
431                         return false;
432                     }
433                     boolean isDst = wallTime.getIsDst() != 0;
434                     modifyAndAppend(TimeZone.getTimeZone(zoneInfoData.getID())
435                             .getDisplayName(isDst, TimeZone.SHORT), modifier);
436                     return false;
437                 case 'z': {
438                     if (wallTime.getIsDst() < 0) {
439                         return false;
440                     }
441                     int diff = wallTime.getGmtOffset();
442                     char sign;
443                     if (diff < 0) {
444                         sign = '-';
445                         diff = -diff;
446                     } else {
447                         sign = '+';
448                     }
449                     outputBuilder.append(sign);
450                     diff /= SECSPERMIN;
451                     diff = (diff / MINSPERHOUR) * 100 + (diff % MINSPERHOUR);
452                     numberFormatter.format(getFormat(modifier, "%04d", "%4d", "%d", "%04d"), diff);
453                     return false;
454                 }
455                 case '+':
456                     formatInternal("%a %b %e %H:%M:%S %Z %Y", wallTime, zoneInfoData);
457                     return false;
458                 case '%':
459                     // If conversion char is undefined, behavior is undefined. Print out the
460                     // character itself.
461                 default:
462                     return true;
463             }
464         }
465         return true;
466     }
467 
modifyAndAppend(CharSequence str, int modifier)468     private void modifyAndAppend(CharSequence str, int modifier) {
469         switch (modifier) {
470             case FORCE_LOWER_CASE:
471                 for (int i = 0; i < str.length(); i++) {
472                     outputBuilder.append(brokenToLower(str.charAt(i)));
473                 }
474                 break;
475             case '^':
476                 for (int i = 0; i < str.length(); i++) {
477                     outputBuilder.append(brokenToUpper(str.charAt(i)));
478                 }
479                 break;
480             case '#':
481                 for (int i = 0; i < str.length(); i++) {
482                     char c = str.charAt(i);
483                     if (brokenIsUpper(c)) {
484                         c = brokenToLower(c);
485                     } else if (brokenIsLower(c)) {
486                         c = brokenToUpper(c);
487                     }
488                     outputBuilder.append(c);
489                 }
490                 break;
491             default:
492                 outputBuilder.append(str);
493         }
494     }
495 
outputYear(int value, boolean outputTop, boolean outputBottom, int modifier)496     private void outputYear(int value, boolean outputTop, boolean outputBottom, int modifier) {
497         int lead;
498         int trail;
499 
500         final int DIVISOR = 100;
501         trail = value % DIVISOR;
502         lead = value / DIVISOR + trail / DIVISOR;
503         trail %= DIVISOR;
504         if (trail < 0 && lead > 0) {
505             trail += DIVISOR;
506             --lead;
507         } else if (lead < 0 && trail > 0) {
508             trail -= DIVISOR;
509             ++lead;
510         }
511         if (outputTop) {
512             if (lead == 0 && trail < 0) {
513                 outputBuilder.append("-0");
514             } else {
515                 numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), lead);
516             }
517         }
518         if (outputBottom) {
519             int n = ((trail < 0) ? -trail : trail);
520             numberFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), n);
521         }
522     }
523 
getFormat(int modifier, String normal, String underscore, String dash, String zero)524     private static String getFormat(int modifier, String normal, String underscore, String dash,
525             String zero) {
526         switch (modifier) {
527             case '_':
528                 return underscore;
529             case '-':
530                 return dash;
531             case '0':
532                 return zero;
533         }
534         return normal;
535     }
536 
isLeap(int year)537     private static boolean isLeap(int year) {
538         return (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0));
539     }
540 
541     /**
542      * A broken implementation of {@link Character#isUpperCase(char)} that assumes ASCII codes in
543      * order to be compatible with the old native implementation.
544      */
brokenIsUpper(char toCheck)545     private static boolean brokenIsUpper(char toCheck) {
546         return toCheck >= 'A' && toCheck <= 'Z';
547     }
548 
549     /**
550      * A broken implementation of {@link Character#isLowerCase(char)} that assumes ASCII codes in
551      * order to be compatible with the old native implementation.
552      */
brokenIsLower(char toCheck)553     private static boolean brokenIsLower(char toCheck) {
554         return toCheck >= 'a' && toCheck <= 'z';
555     }
556 
557     /**
558      * A broken implementation of {@link Character#toLowerCase(char)} that assumes ASCII codes in
559      * order to be compatible with the old native implementation.
560      */
brokenToLower(char input)561     private static char brokenToLower(char input) {
562         if (input >= 'A' && input <= 'Z') {
563             return (char) (input - 'A' + 'a');
564         }
565         return input;
566     }
567 
568     /**
569      * A broken implementation of {@link Character#toUpperCase(char)} that assumes ASCII codes in
570      * order to be compatible with the old native implementation.
571      */
brokenToUpper(char input)572     private static char brokenToUpper(char input) {
573         if (input >= 'a' && input <= 'z') {
574             return (char) (input - 'a' + 'A');
575         }
576         return input;
577     }
578 
579 }
580