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