1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.text.format;
18 
19 import android.annotation.NonNull;
20 import android.app.compat.CompatChanges;
21 import android.compat.annotation.ChangeId;
22 import android.compat.annotation.EnabledSince;
23 import android.compat.annotation.UnsupportedAppUsage;
24 import android.content.Context;
25 import android.icu.text.DateFormatSymbols;
26 import android.icu.text.DateTimePatternGenerator;
27 import android.icu.util.ULocale;
28 import android.os.Build;
29 import android.provider.Settings;
30 import android.text.SpannableStringBuilder;
31 import android.text.Spanned;
32 import android.text.SpannedString;
33 
34 import java.text.SimpleDateFormat;
35 import java.util.Calendar;
36 import java.util.Date;
37 import java.util.GregorianCalendar;
38 import java.util.Locale;
39 import java.util.TimeZone;
40 
41 /**
42  * Utility class for producing strings with formatted date/time.
43  *
44  * <p>Most callers should avoid supplying their own format strings to this
45  * class' {@code format} methods and rely on the correctly localized ones
46  * supplied by the system. This class' factory methods return
47  * appropriately-localized {@link java.text.DateFormat} instances, suitable
48  * for both formatting and parsing dates. For the canonical documentation
49  * of format strings, see {@link java.text.SimpleDateFormat}.
50  *
51  * <p>In cases where the system does not provide a suitable pattern,
52  * this class offers the {@link #getBestDateTimePattern} method.
53  *
54  * <p>The {@code format} methods in this class implement a subset of Unicode
55  * <a href="http://www.unicode.org/reports/tr35/#Date_Format_Patterns">UTS #35</a> patterns.
56  * The subset currently supported by this class includes the following format characters:
57  * {@code acdEHhLKkLMmsyz}. Up to API level 17, only {@code adEhkMmszy} were supported.
58  * Note that this class incorrectly implements {@code k} as if it were {@code H} for backwards
59  * compatibility.
60  *
61  * <p>See {@link java.text.SimpleDateFormat} for more documentation
62  * about patterns, or if you need a more complete or correct implementation.
63  * Note that the non-{@code format} methods in this class are implemented by
64  * {@code SimpleDateFormat}.
65  */
66 public class DateFormat {
67     /**
68      * @deprecated Use a literal {@code '} instead.
69      * @removed
70      */
71     @Deprecated
72     public  static final char    QUOTE                  =    '\'';
73 
74     /**
75      * @deprecated Use a literal {@code 'a'} instead.
76      * @removed
77      */
78     @Deprecated
79     public  static final char    AM_PM                  =    'a';
80 
81     /**
82      * @deprecated Use a literal {@code 'a'} instead; 'A' was always equivalent to 'a'.
83      * @removed
84      */
85     @Deprecated
86     public  static final char    CAPITAL_AM_PM          =    'A';
87 
88     /**
89      * @deprecated Use a literal {@code 'd'} instead.
90      * @removed
91      */
92     @Deprecated
93     public  static final char    DATE                   =    'd';
94 
95     /**
96      * @deprecated Use a literal {@code 'E'} instead.
97      * @removed
98      */
99     @Deprecated
100     public  static final char    DAY                    =    'E';
101 
102     /**
103      * @deprecated Use a literal {@code 'h'} instead.
104      * @removed
105      */
106     @Deprecated
107     public  static final char    HOUR                   =    'h';
108 
109     /**
110      * @deprecated Use a literal {@code 'H'} (for compatibility with {@link SimpleDateFormat}
111      * and Unicode) or {@code 'k'} (for compatibility with Android releases up to and including
112      * Jelly Bean MR-1) instead. Note that the two are incompatible.
113      *
114      * @removed
115      */
116     @Deprecated
117     public  static final char    HOUR_OF_DAY            =    'k';
118 
119     /**
120      * @deprecated Use a literal {@code 'm'} instead.
121      * @removed
122      */
123     @Deprecated
124     public  static final char    MINUTE                 =    'm';
125 
126     /**
127      * @deprecated Use a literal {@code 'M'} instead.
128      * @removed
129      */
130     @Deprecated
131     public  static final char    MONTH                  =    'M';
132 
133     /**
134      * @deprecated Use a literal {@code 'L'} instead.
135      * @removed
136      */
137     @Deprecated
138     public  static final char    STANDALONE_MONTH       =    'L';
139 
140     /**
141      * @deprecated Use a literal {@code 's'} instead.
142      * @removed
143      */
144     @Deprecated
145     public  static final char    SECONDS                =    's';
146 
147     /**
148      * @deprecated Use a literal {@code 'z'} instead.
149      * @removed
150      */
151     @Deprecated
152     public  static final char    TIME_ZONE              =    'z';
153 
154     /**
155      * @deprecated Use a literal {@code 'y'} instead.
156      * @removed
157      */
158     @Deprecated
159     public  static final char    YEAR                   =    'y';
160 
161 
162     private static final Object sLocaleLock = new Object();
163     private static Locale sIs24HourLocale;
164     private static boolean sIs24Hour;
165 
166     /**
167      * {@link #getBestDateTimePattern(Locale, String)} does not allow non-consecutive repeated
168      * symbol in the skeleton. For example, please use a skeleton of {@code "jmm"} or
169      * {@code "hmma"} instead of {@code "ahmma"} or {@code "jmma"}, because the field 'j' could
170      * mean using 12-hour in some locales and, in this case, is duplicated as the 'a' field.
171      */
172     @ChangeId
173     @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
174     static final long DISALLOW_DUPLICATE_FIELD_IN_SKELETON = 170233598L;
175 
176     /**
177      * Returns true if times should be formatted as 24 hour times, false if times should be
178      * formatted as 12 hour (AM/PM) times. Based on the user's chosen locale and other preferences.
179      * @param context the context to use for the content resolver
180      * @return true if 24 hour time format is selected, false otherwise.
181      */
is24HourFormat(Context context)182     public static boolean is24HourFormat(Context context) {
183         return is24HourFormat(context, context.getUserId());
184     }
185 
186     /**
187      * Returns true if times should be formatted as 24 hour times, false if times should be
188      * formatted as 12 hour (AM/PM) times. Based on the user's chosen locale and other preferences.
189      * @param context the context to use for the content resolver
190      * @param userHandle the user handle of the user to query.
191      * @return true if 24 hour time format is selected, false otherwise.
192      *
193      * @hide
194      */
195     @UnsupportedAppUsage
is24HourFormat(Context context, int userHandle)196     public static boolean is24HourFormat(Context context, int userHandle) {
197         final String value = Settings.System.getStringForUser(context.getContentResolver(),
198                 Settings.System.TIME_12_24, userHandle);
199         if (value != null) {
200             return value.equals("24");
201         }
202 
203         return is24HourLocale(context.getResources().getConfiguration().locale);
204     }
205 
206     /**
207      * Returns true if the specified locale uses a 24-hour time format by default, ignoring user
208      * settings.
209      * @param locale the locale to check
210      * @return true if the locale uses a 24 hour time format by default, false otherwise
211      * @hide
212      */
is24HourLocale(@onNull Locale locale)213     public static boolean is24HourLocale(@NonNull Locale locale) {
214         synchronized (sLocaleLock) {
215             if (sIs24HourLocale != null && sIs24HourLocale.equals(locale)) {
216                 return sIs24Hour;
217             }
218         }
219 
220         final java.text.DateFormat natural =
221                 java.text.DateFormat.getTimeInstance(java.text.DateFormat.LONG, locale);
222 
223         final boolean is24Hour;
224         if (natural instanceof SimpleDateFormat) {
225             final SimpleDateFormat sdf = (SimpleDateFormat) natural;
226             final String pattern = sdf.toPattern();
227             is24Hour = hasDesignator(pattern, 'H');
228         } else {
229             is24Hour = false;
230         }
231 
232         synchronized (sLocaleLock) {
233             sIs24HourLocale = locale;
234             sIs24Hour = is24Hour;
235         }
236 
237         return is24Hour;
238     }
239 
240     /**
241      * Returns the best possible localized form of the given skeleton for the given
242      * locale. A skeleton is similar to, and uses the same format characters as, a Unicode
243      * <a href="http://www.unicode.org/reports/tr35/#Date_Format_Patterns">UTS #35</a>
244      * pattern.
245      *
246      * <p>One difference is that order is irrelevant. For example, "MMMMd" will return
247      * "MMMM d" in the {@code en_US} locale, but "d. MMMM" in the {@code de_CH} locale.
248      *
249      * <p>Note also in that second example that the necessary punctuation for German was
250      * added. For the same input in {@code es_ES}, we'd have even more extra text:
251      * "d 'de' MMMM".
252      *
253      * <p>This method will automatically correct for grammatical necessity. Given the
254      * same "MMMMd" input, this method will return "d LLLL" in the {@code fa_IR} locale,
255      * where stand-alone months are necessary. Lengths are preserved where meaningful,
256      * so "Md" would give a different result to "MMMd", say, except in a locale such as
257      * {@code ja_JP} where there is only one length of month.
258      *
259      * <p>This method will only return patterns that are in CLDR, and is useful whenever
260      * you know what elements you want in your format string but don't want to make your
261      * code specific to any one locale.
262      *
263      * @param locale the locale into which the skeleton should be localized
264      * @param skeleton a skeleton as described above
265      * @return a string pattern suitable for use with {@link java.text.SimpleDateFormat}.
266      */
getBestDateTimePattern(Locale locale, String skeleton)267     public static String getBestDateTimePattern(Locale locale, String skeleton) {
268         ULocale uLocale = ULocale.forLocale(locale);
269         DateTimePatternGenerator dtpg = DateTimePatternGenerator.getInstance(uLocale);
270         boolean allowDuplicateFields = !CompatChanges.isChangeEnabled(
271                 DISALLOW_DUPLICATE_FIELD_IN_SKELETON);
272         String pattern = dtpg.getBestPattern(skeleton, DateTimePatternGenerator.MATCH_NO_OPTIONS,
273                 allowDuplicateFields);
274         return getCompatibleEnglishPattern(uLocale, pattern);
275     }
276 
277     /**
278      * Returns a {@link java.text.DateFormat} object that can format the time according
279      * to the context's locale and the user's 12-/24-hour clock preference.
280      * @param context the application context
281      * @return the {@link java.text.DateFormat} object that properly formats the time.
282      */
getTimeFormat(Context context)283     public static java.text.DateFormat getTimeFormat(Context context) {
284         final Locale locale = context.getResources().getConfiguration().locale;
285         return new java.text.SimpleDateFormat(getTimeFormatString(context), locale);
286     }
287 
288     /**
289      * Returns a String pattern that can be used to format the time according
290      * to the context's locale and the user's 12-/24-hour clock preference.
291      * @param context the application context
292      * @hide
293      */
294     @UnsupportedAppUsage
getTimeFormatString(Context context)295     public static String getTimeFormatString(Context context) {
296         return getTimeFormatString(context, context.getUserId());
297     }
298 
299     /**
300      * Returns a String pattern that can be used to format the time according
301      * to the context's locale and the user's 12-/24-hour clock preference.
302      * @param context the application context
303      * @param userHandle the user handle of the user to query the format for
304      * @hide
305      */
306     @UnsupportedAppUsage
getTimeFormatString(Context context, int userHandle)307     public static String getTimeFormatString(Context context, int userHandle) {
308         ULocale uLocale = ULocale.forLocale(context.getResources().getConfiguration().locale);
309         DateTimePatternGenerator dtpg = DateTimePatternGenerator.getInstance(uLocale);
310         String pattern = is24HourFormat(context, userHandle) ? dtpg.getBestPattern("Hm")
311             : dtpg.getBestPattern("hm");
312         return getCompatibleEnglishPattern(uLocale, pattern);
313     }
314 
315     /**
316      * Returns a {@link java.text.DateFormat} object that can format the date
317      * in short form according to the context's locale.
318      *
319      * @param context the application context
320      * @return the {@link java.text.DateFormat} object that properly formats the date.
321      */
getDateFormat(Context context)322     public static java.text.DateFormat getDateFormat(Context context) {
323         final Locale locale = context.getResources().getConfiguration().locale;
324         return java.text.DateFormat.getDateInstance(java.text.DateFormat.SHORT, locale);
325     }
326 
327     /**
328      * Returns a {@link java.text.DateFormat} object that can format the date
329      * in long form (such as {@code Monday, January 3, 2000}) for the context's locale.
330      * @param context the application context
331      * @return the {@link java.text.DateFormat} object that formats the date in long form.
332      */
getLongDateFormat(Context context)333     public static java.text.DateFormat getLongDateFormat(Context context) {
334         final Locale locale = context.getResources().getConfiguration().locale;
335         return java.text.DateFormat.getDateInstance(java.text.DateFormat.LONG, locale);
336     }
337 
338     /**
339      * Returns a {@link java.text.DateFormat} object that can format the date
340      * in medium form (such as {@code Jan 3, 2000}) for the context's locale.
341      * @param context the application context
342      * @return the {@link java.text.DateFormat} object that formats the date in long form.
343      */
getMediumDateFormat(Context context)344     public static java.text.DateFormat getMediumDateFormat(Context context) {
345         final Locale locale = context.getResources().getConfiguration().locale;
346         return java.text.DateFormat.getDateInstance(java.text.DateFormat.MEDIUM, locale);
347     }
348 
349     /**
350      * Gets the current date format stored as a char array. Returns a 3 element
351      * array containing the day ({@code 'd'}), month ({@code 'M'}), and year ({@code 'y'}))
352      * in the order specified by the user's format preference.  Note that this order is
353      * <i>only</i> appropriate for all-numeric dates; spelled-out (MEDIUM and LONG)
354      * dates will generally contain other punctuation, spaces, or words,
355      * not just the day, month, and year, and not necessarily in the same
356      * order returned here.
357      */
getDateFormatOrder(Context context)358     public static char[] getDateFormatOrder(Context context) {
359         return getDateFormatOrder(getDateFormatString(context));
360     }
361 
362     /**
363      * @hide Used by internal framework class {@link android.widget.DatePickerSpinnerDelegate}.
364      */
getDateFormatOrder(String pattern)365     public static char[] getDateFormatOrder(String pattern) {
366         char[] result = new char[3];
367         int resultIndex = 0;
368         boolean sawDay = false;
369         boolean sawMonth = false;
370         boolean sawYear = false;
371 
372         for (int i = 0; i < pattern.length(); ++i) {
373             char ch = pattern.charAt(i);
374             if (ch == 'd' || ch == 'L' || ch == 'M' || ch == 'y') {
375                 if (ch == 'd' && !sawDay) {
376                     result[resultIndex++] = 'd';
377                     sawDay = true;
378                 } else if ((ch == 'L' || ch == 'M') && !sawMonth) {
379                     result[resultIndex++] = 'M';
380                     sawMonth = true;
381                 } else if ((ch == 'y') && !sawYear) {
382                     result[resultIndex++] = 'y';
383                     sawYear = true;
384                 }
385             } else if (ch == 'G') {
386                 // Ignore the era specifier, if present.
387             } else if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
388                 throw new IllegalArgumentException("Bad pattern character '" + ch + "' in "
389                     + pattern);
390             } else if (ch == '\'') {
391                 if (i < pattern.length() - 1 && pattern.charAt(i + 1) == '\'') {
392                     ++i;
393                 } else {
394                     i = pattern.indexOf('\'', i + 1);
395                     if (i == -1) {
396                         throw new IllegalArgumentException("Bad quoting in " + pattern);
397                     }
398                     ++i;
399                 }
400             } else {
401                 // Ignore spaces and punctuation.
402             }
403         }
404         return result;
405     }
406 
getDateFormatString(Context context)407     private static String getDateFormatString(Context context) {
408         final Locale locale = context.getResources().getConfiguration().locale;
409         java.text.DateFormat df = java.text.DateFormat.getDateInstance(
410                 java.text.DateFormat.SHORT, locale);
411         if (df instanceof SimpleDateFormat) {
412             return ((SimpleDateFormat) df).toPattern();
413         }
414 
415         throw new AssertionError("!(df instanceof SimpleDateFormat)");
416     }
417 
418     /**
419      * Given a format string and a time in milliseconds since Jan 1, 1970 GMT, returns a
420      * CharSequence containing the requested date.
421      * @param inFormat the format string, as described in {@link android.text.format.DateFormat}
422      * @param inTimeInMillis in milliseconds since Jan 1, 1970 GMT
423      * @return a {@link CharSequence} containing the requested text
424      */
format(CharSequence inFormat, long inTimeInMillis)425     public static CharSequence format(CharSequence inFormat, long inTimeInMillis) {
426         return format(inFormat, new Date(inTimeInMillis));
427     }
428 
429     /**
430      * Given a format string and a {@link java.util.Date} object, returns a CharSequence containing
431      * the requested date.
432      * @param inFormat the format string, as described in {@link android.text.format.DateFormat}
433      * @param inDate the date to format
434      * @return a {@link CharSequence} containing the requested text
435      */
format(CharSequence inFormat, Date inDate)436     public static CharSequence format(CharSequence inFormat, Date inDate) {
437         Calendar c = new GregorianCalendar();
438         c.setTime(inDate);
439         return format(inFormat, c);
440     }
441 
442     /**
443      * Indicates whether the specified format string contains seconds.
444      *
445      * Always returns false if the input format is null.
446      *
447      * @param inFormat the format string, as described in {@link android.text.format.DateFormat}
448      *
449      * @return true if the format string contains {@link #SECONDS}, false otherwise
450      *
451      * @hide
452      */
453     @UnsupportedAppUsage
hasSeconds(CharSequence inFormat)454     public static boolean hasSeconds(CharSequence inFormat) {
455         return hasDesignator(inFormat, SECONDS);
456     }
457 
458     /**
459      * Test if a format string contains the given designator. Always returns
460      * {@code false} if the input format is {@code null}.
461      *
462      * Note that this is intended for searching for designators, not arbitrary
463      * characters. So searching for a literal single quote would not work correctly.
464      *
465      * @hide
466      */
467     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
hasDesignator(CharSequence inFormat, char designator)468     public static boolean hasDesignator(CharSequence inFormat, char designator) {
469         if (inFormat == null) return false;
470 
471         final int length = inFormat.length();
472 
473         boolean insideQuote = false;
474         for (int i = 0; i < length; i++) {
475             final char c = inFormat.charAt(i);
476             if (c == QUOTE) {
477                 insideQuote = !insideQuote;
478             } else if (!insideQuote) {
479                 if (c == designator) {
480                     return true;
481                 }
482             }
483         }
484 
485         return false;
486     }
487 
488     /**
489      * Given a format string and a {@link java.util.Calendar} object, returns a CharSequence
490      * containing the requested date.
491      * @param inFormat the format string, as described in {@link android.text.format.DateFormat}
492      * @param inDate the date to format
493      * @return a {@link CharSequence} containing the requested text
494      */
format(CharSequence inFormat, Calendar inDate)495     public static CharSequence format(CharSequence inFormat, Calendar inDate) {
496         SpannableStringBuilder s = new SpannableStringBuilder(inFormat);
497         int count;
498 
499         DateFormatSymbols dfs = getIcuDateFormatSymbols(Locale.getDefault());
500         String[] amPm = dfs.getAmPmStrings();
501 
502         int len = inFormat.length();
503 
504         for (int i = 0; i < len; i += count) {
505             count = 1;
506             int c = s.charAt(i);
507 
508             if (c == QUOTE) {
509                 count = appendQuotedText(s, i);
510                 len = s.length();
511                 continue;
512             }
513 
514             while ((i + count < len) && (s.charAt(i + count) == c)) {
515                 count++;
516             }
517 
518             String replacement;
519             switch (c) {
520                 case 'A':
521                 case 'a':
522                     replacement = amPm[inDate.get(Calendar.AM_PM) - Calendar.AM];
523                     break;
524                 case 'd':
525                     replacement = zeroPad(inDate.get(Calendar.DATE), count);
526                     break;
527                 case 'c':
528                 case 'E':
529                     replacement = getDayOfWeekString(dfs,
530                                                      inDate.get(Calendar.DAY_OF_WEEK), count, c);
531                     break;
532                 case 'K': // hour in am/pm (0-11)
533                 case 'h': // hour in am/pm (1-12)
534                     {
535                         int hour = inDate.get(Calendar.HOUR);
536                         if (c == 'h' && hour == 0) {
537                             hour = 12;
538                         }
539                         replacement = zeroPad(hour, count);
540                     }
541                     break;
542                 case 'H': // hour in day (0-23)
543                 case 'k': // hour in day (1-24) [but see note below]
544                     {
545                         int hour = inDate.get(Calendar.HOUR_OF_DAY);
546                         // Historically on Android 'k' was interpreted as 'H', which wasn't
547                         // implemented, so pretty much all callers that want to format 24-hour
548                         // times are abusing 'k'. http://b/8359981.
549                         if (false && c == 'k' && hour == 0) {
550                             hour = 24;
551                         }
552                         replacement = zeroPad(hour, count);
553                     }
554                     break;
555                 case 'L':
556                 case 'M':
557                     replacement = getMonthString(dfs, inDate.get(Calendar.MONTH), count, c);
558                     break;
559                 case 'm':
560                     replacement = zeroPad(inDate.get(Calendar.MINUTE), count);
561                     break;
562                 case 's':
563                     replacement = zeroPad(inDate.get(Calendar.SECOND), count);
564                     break;
565                 case 'y':
566                     replacement = getYearString(inDate.get(Calendar.YEAR), count);
567                     break;
568                 case 'z':
569                     replacement = getTimeZoneString(inDate, count);
570                     break;
571                 default:
572                     replacement = null;
573                     break;
574             }
575 
576             if (replacement != null) {
577                 s.replace(i, i + count, replacement);
578                 count = replacement.length(); // CARE: count is used in the for loop above
579                 len = s.length();
580             }
581         }
582 
583         if (inFormat instanceof Spanned) {
584             return new SpannedString(s);
585         } else {
586             return s.toString();
587         }
588     }
589 
getDayOfWeekString(DateFormatSymbols dfs, int day, int count, int kind)590     private static String getDayOfWeekString(DateFormatSymbols dfs, int day, int count, int kind) {
591         boolean standalone = (kind == 'c');
592         int context = standalone ? DateFormatSymbols.STANDALONE : DateFormatSymbols.FORMAT;
593         final int width;
594         if (count == 5) {
595             width = DateFormatSymbols.NARROW;
596         } else if (count == 4) {
597             width = DateFormatSymbols.WIDE;
598         } else {
599             width = DateFormatSymbols.ABBREVIATED;
600         }
601         return dfs.getWeekdays(context, width)[day];
602     }
603 
getMonthString(DateFormatSymbols dfs, int month, int count, int kind)604     private static String getMonthString(DateFormatSymbols dfs, int month, int count, int kind) {
605         boolean standalone = (kind == 'L');
606         int monthContext = standalone ? DateFormatSymbols.STANDALONE : DateFormatSymbols.FORMAT;
607         if (count == 5) {
608             return dfs.getMonths(monthContext, DateFormatSymbols.NARROW)[month];
609         } else if (count == 4) {
610             return dfs.getMonths(monthContext, DateFormatSymbols.WIDE)[month];
611         } else if (count == 3) {
612             return dfs.getMonths(monthContext, DateFormatSymbols.ABBREVIATED)[month];
613         } else {
614             // Calendar.JANUARY == 0, so add 1 to month.
615             return zeroPad(month+1, count);
616         }
617     }
618 
getTimeZoneString(Calendar inDate, int count)619     private static String getTimeZoneString(Calendar inDate, int count) {
620         TimeZone tz = inDate.getTimeZone();
621         if (count < 2) { // FIXME: shouldn't this be <= 2 ?
622             return formatZoneOffset(inDate.get(Calendar.DST_OFFSET) +
623                                     inDate.get(Calendar.ZONE_OFFSET),
624                                     count);
625         } else {
626             boolean dst = inDate.get(Calendar.DST_OFFSET) != 0;
627             return tz.getDisplayName(dst, TimeZone.SHORT);
628         }
629     }
630 
formatZoneOffset(int offset, int count)631     private static String formatZoneOffset(int offset, int count) {
632         offset /= 1000; // milliseconds to seconds
633         StringBuilder tb = new StringBuilder();
634 
635         if (offset < 0) {
636             tb.insert(0, "-");
637             offset = -offset;
638         } else {
639             tb.insert(0, "+");
640         }
641 
642         int hours = offset / 3600;
643         int minutes = (offset % 3600) / 60;
644 
645         tb.append(zeroPad(hours, 2));
646         tb.append(zeroPad(minutes, 2));
647         return tb.toString();
648     }
649 
getYearString(int year, int count)650     private static String getYearString(int year, int count) {
651         return (count <= 2) ? zeroPad(year % 100, 2)
652                             : String.format(Locale.getDefault(), "%d", year);
653     }
654 
655 
656     /**
657      * Strips quotation marks from the {@code formatString} and appends the result back to the
658      * {@code formatString}.
659      *
660      * @param formatString the format string, as described in
661      *                     {@link android.text.format.DateFormat}, to be modified
662      * @param index        index of the first quote
663      * @return the length of the quoted text that was appended.
664      * @hide
665      */
appendQuotedText(SpannableStringBuilder formatString, int index)666     public static int appendQuotedText(SpannableStringBuilder formatString, int index) {
667         int length = formatString.length();
668         if (index + 1 < length && formatString.charAt(index + 1) == QUOTE) {
669             formatString.delete(index, index + 1);
670             return 1;
671         }
672 
673         int count = 0;
674 
675         // delete leading quote
676         formatString.delete(index, index + 1);
677         length--;
678 
679         while (index < length) {
680             char c = formatString.charAt(index);
681 
682             if (c == QUOTE) {
683                 //  QUOTEQUOTE -> QUOTE
684                 if (index + 1 < length && formatString.charAt(index + 1) == QUOTE) {
685 
686                     formatString.delete(index, index + 1);
687                     length--;
688                     count++;
689                     index++;
690                 } else {
691                     //  Closing QUOTE ends quoted text copying
692                     formatString.delete(index, index + 1);
693                     break;
694                 }
695             } else {
696                 index++;
697                 count++;
698             }
699         }
700 
701         return count;
702     }
703 
zeroPad(int inValue, int inMinDigits)704     private static String zeroPad(int inValue, int inMinDigits) {
705         return String.format(Locale.getDefault(), "%0" + inMinDigits + "d", inValue);
706     }
707 
708     /**
709      * We use Gregorian calendar for date formats in android.text.format and various UI widget
710      * historically. It's a utility method to get an {@link DateFormatSymbols} instance. Note that
711      * {@link DateFormatSymbols} has cache, and external cache is not needed unless same instance is
712      * requested repeatedly in the performance critical code.
713      *
714      * @hide
715      */
getIcuDateFormatSymbols(Locale locale)716     public static DateFormatSymbols getIcuDateFormatSymbols(Locale locale) {
717         return new DateFormatSymbols(android.icu.util.GregorianCalendar.class, locale);
718     }
719 
720     /**
721      * See http://b/266731719. It mirrors the implementation in
722      * {@link libcore.icu.SimpleDateFormatData.DateTimeFormatStringGenerator#postProcessPattern}
723      */
getCompatibleEnglishPattern(ULocale locale, String pattern)724     private static String getCompatibleEnglishPattern(ULocale locale, String pattern) {
725         if (pattern == null || locale == null || !"en".equals(locale.getLanguage())) {
726             return pattern;
727         }
728 
729         String region = locale.getCountry();
730         if (region != null && !region.isEmpty() && !"US".equals(region)) {
731             return pattern;
732         }
733 
734         return pattern.replace('\u202f', ' ');
735     }
736 }
737