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
17package android.text.format;
18
19import android.content.Context;
20import android.provider.Settings;
21import android.text.SpannableStringBuilder;
22import android.text.Spanned;
23import android.text.SpannedString;
24
25import com.android.internal.R;
26
27import java.util.Calendar;
28import java.util.Date;
29import java.util.GregorianCalendar;
30import java.util.Locale;
31import java.util.TimeZone;
32import java.text.SimpleDateFormat;
33
34/**
35    Utility class for producing strings with formatted date/time.
36
37    <p>
38    Most callers should avoid supplying their own format strings to this
39    class' {@code format} methods and rely on the correctly localized ones
40    supplied by the system. This class' factory methods return
41    appropriately-localized {@link java.text.DateFormat} instances, suitable
42    for both formatting and parsing dates. For the canonical documentation
43    of format strings, see {@link java.text.SimpleDateFormat}.
44    </p>
45    <p>
46    The format methods in this class takes as inputs a format string and a representation of a date/time.
47    The format string controls how the output is generated.
48    This class only supports a subset of the full Unicode specification.
49    Use {@link java.text.SimpleDateFormat} if you need more.
50    Formatting characters may be repeated in order to get more detailed representations
51    of that field.  For instance, the format character &apos;M&apos; is used to
52    represent the month.  Depending on how many times that character is repeated
53    you get a different representation.
54    </p>
55    <p>
56    For the month of September:<br/>
57    M -&gt; 9<br/>
58    MM -&gt; 09<br/>
59    MMM -&gt; Sep<br/>
60    MMMM -&gt; September
61    </p>
62    <p>
63    The effects of the duplication vary depending on the nature of the field.
64    See the notes on the individual field formatters for details.  For purely numeric
65    fields such as <code>HOUR</code> adding more copies of the designator will
66    zero-pad the value to that number of characters.
67    </p>
68    <p>
69    For 7 minutes past the hour:<br/>
70    m -&gt; 7<br/>
71    mm -&gt; 07<br/>
72    mmm -&gt; 007<br/>
73    mmmm -&gt; 0007
74    </p>
75    <p>
76    Examples for April 6, 1970 at 3:23am:<br/>
77    &quot;MM/dd/yy h:mmaa&quot; -&gt; &quot;04/06/70 3:23am&quot<br/>
78    &quot;MMM dd, yyyy h:mmaa&quot; -&gt; &quot;Apr 6, 1970 3:23am&quot<br/>
79    &quot;MMMM dd, yyyy h:mmaa&quot; -&gt; &quot;April 6, 1970 3:23am&quot<br/>
80    &quot;E, MMMM dd, yyyy h:mmaa&quot; -&gt; &quot;Mon, April 6, 1970 3:23am&<br/>
81    &quot;EEEE, MMMM dd, yyyy h:mmaa&quot; -&gt; &quot;Monday, April 6, 1970 3:23am&quot;<br/>
82    &quot;&apos;Noteworthy day: &apos;M/d/yy&quot; -&gt; &quot;Noteworthy day: 4/6/70&quot;
83 */
84
85public class DateFormat {
86    /**
87        Text in the format string that should be copied verbatim rather that
88        interpreted as formatting codes must be surrounded by the <code>QUOTE</code>
89        character.  If you need to embed a literal <code>QUOTE</code> character in
90        the output text then use two in a row.
91     */
92    public  static final char    QUOTE                  =    '\'';
93
94    /**
95        This designator indicates whether the <code>HOUR</code> field is before
96        or after noon.  The output is lower-case.
97
98        Examples:
99        a -> a or p
100        aa -> am or pm
101     */
102    public  static final char    AM_PM                  =    'a';
103
104    /**
105        This designator indicates whether the <code>HOUR</code> field is before
106        or after noon.  The output is capitalized.
107
108        Examples:
109        A -> A or P
110        AA -> AM or PM
111     */
112    public  static final char    CAPITAL_AM_PM          =    'A';
113
114    /**
115        This designator indicates the day of the month.
116
117        Examples for the 9th of the month:
118        d -> 9
119        dd -> 09
120     */
121    public  static final char    DATE                   =    'd';
122
123    /**
124        This designator indicates the name of the day of the week.
125
126        Examples for Sunday:
127        E -> Sun
128        EEEE -> Sunday
129     */
130    public  static final char    DAY                    =    'E';
131
132    /**
133        This designator indicates the hour of the day in 12 hour format.
134
135        Examples for 3pm:
136        h -> 3
137        hh -> 03
138     */
139    public  static final char    HOUR                   =    'h';
140
141    /**
142        This designator indicates the hour of the day in 24 hour format.
143
144        Example for 3pm:
145        k -> 15
146
147        Examples for midnight:
148        k -> 0
149        kk -> 00
150     */
151    public  static final char    HOUR_OF_DAY            =    'k';
152
153    /**
154        This designator indicates the minute of the hour.
155
156        Examples for 7 minutes past the hour:
157        m -> 7
158        mm -> 07
159     */
160    public  static final char    MINUTE                 =    'm';
161
162    /**
163        This designator indicates the month of the year. See also
164        {@link #STANDALONE_MONTH}.
165
166        Examples for September:
167        M -> 9
168        MM -> 09
169        MMM -> Sep
170        MMMM -> September
171     */
172    public  static final char    MONTH                  =    'M';
173
174    /**
175        This designator indicates the standalone month of the year,
176        necessary in some format strings in some languages. For
177        example, Russian distinguishes between the "June" in
178        "June" and that in "June 2010".
179     */
180    public  static final char    STANDALONE_MONTH       =    'L';
181
182    /**
183        This designator indicates the seconds of the minute.
184
185        Examples for 7 seconds past the minute:
186        s -> 7
187        ss -> 07
188     */
189    public  static final char    SECONDS                =    's';
190
191    /**
192        This designator indicates the offset of the timezone from GMT.
193
194        Example for US/Pacific timezone:
195        z -> -0800
196        zz -> PST
197     */
198    public  static final char    TIME_ZONE              =    'z';
199
200    /**
201        This designator indicates the year.
202
203        Examples for 2006
204        y -> 06
205        yyyy -> 2006
206     */
207    public  static final char    YEAR                   =    'y';
208
209
210    private static final Object sLocaleLock = new Object();
211    private static Locale sIs24HourLocale;
212    private static boolean sIs24Hour;
213
214
215    /**
216     * Returns true if user preference is set to 24-hour format.
217     * @param context the context to use for the content resolver
218     * @return true if 24 hour time format is selected, false otherwise.
219     */
220    public static boolean is24HourFormat(Context context) {
221        String value = Settings.System.getString(context.getContentResolver(),
222                Settings.System.TIME_12_24);
223
224        if (value == null) {
225            Locale locale = context.getResources().getConfiguration().locale;
226
227            synchronized (sLocaleLock) {
228                if (sIs24HourLocale != null && sIs24HourLocale.equals(locale)) {
229                    return sIs24Hour;
230                }
231            }
232
233            java.text.DateFormat natural =
234                java.text.DateFormat.getTimeInstance(
235                    java.text.DateFormat.LONG, locale);
236
237            if (natural instanceof SimpleDateFormat) {
238                SimpleDateFormat sdf = (SimpleDateFormat) natural;
239                String pattern = sdf.toPattern();
240
241                if (pattern.indexOf('H') >= 0) {
242                    value = "24";
243                } else {
244                    value = "12";
245                }
246            } else {
247                value = "12";
248            }
249
250            synchronized (sLocaleLock) {
251                sIs24HourLocale = locale;
252                sIs24Hour = value.equals("24");
253            }
254
255            return sIs24Hour;
256        }
257
258        return value.equals("24");
259    }
260
261    /**
262     * Returns a {@link java.text.DateFormat} object that can format the time according
263     * to the current locale and the user's 12-/24-hour clock preference.
264     * @param context the application context
265     * @return the {@link java.text.DateFormat} object that properly formats the time.
266     */
267    public static java.text.DateFormat getTimeFormat(Context context) {
268        boolean b24 = is24HourFormat(context);
269        int res;
270
271        if (b24) {
272            res = R.string.twenty_four_hour_time_format;
273        } else {
274            res = R.string.twelve_hour_time_format;
275        }
276
277        return new java.text.SimpleDateFormat(context.getString(res));
278    }
279
280    /**
281     * Returns a {@link java.text.DateFormat} object that can format the date
282     * in short form (such as 12/31/1999) according
283     * to the current locale and the user's date-order preference.
284     * @param context the application context
285     * @return the {@link java.text.DateFormat} object that properly formats the date.
286     */
287    public static java.text.DateFormat getDateFormat(Context context) {
288        String value = Settings.System.getString(context.getContentResolver(),
289                Settings.System.DATE_FORMAT);
290
291        return getDateFormatForSetting(context, value);
292    }
293
294    /**
295     * Returns a {@link java.text.DateFormat} object to format the date
296     * as if the date format setting were set to <code>value</code>,
297     * including null to use the locale's default format.
298     * @param context the application context
299     * @param value the date format setting string to interpret for
300     *              the current locale
301     * @hide
302     */
303    public static java.text.DateFormat getDateFormatForSetting(Context context,
304                                                               String value) {
305        String format = getDateFormatStringForSetting(context, value);
306
307        return new java.text.SimpleDateFormat(format);
308    }
309
310    private static String getDateFormatStringForSetting(Context context, String value) {
311        if (value != null) {
312            int month = value.indexOf('M');
313            int day = value.indexOf('d');
314            int year = value.indexOf('y');
315
316            if (month >= 0 && day >= 0 && year >= 0) {
317                String template = context.getString(R.string.numeric_date_template);
318                if (year < month && year < day) {
319                    if (month < day) {
320                        value = String.format(template, "yyyy", "MM", "dd");
321                    } else {
322                        value = String.format(template, "yyyy", "dd", "MM");
323                    }
324                } else if (month < day) {
325                    if (day < year) {
326                        value = String.format(template, "MM", "dd", "yyyy");
327                    } else { // unlikely
328                        value = String.format(template, "MM", "yyyy", "dd");
329                    }
330                } else { // day < month
331                    if (month < year) {
332                        value = String.format(template, "dd", "MM", "yyyy");
333                    } else { // unlikely
334                        value = String.format(template, "dd", "yyyy", "MM");
335                    }
336                }
337
338                return value;
339            }
340        }
341
342        /*
343         * The setting is not set; use the default.
344         * We use a resource string here instead of just DateFormat.SHORT
345         * so that we get a four-digit year instead a two-digit year.
346         */
347        value = context.getString(R.string.numeric_date_format);
348        return value;
349    }
350
351    /**
352     * Returns a {@link java.text.DateFormat} object that can format the date
353     * in long form (such as December 31, 1999) for the current locale.
354     * @param context the application context
355     * @return the {@link java.text.DateFormat} object that formats the date in long form.
356     */
357    public static java.text.DateFormat getLongDateFormat(Context context) {
358        return java.text.DateFormat.getDateInstance(java.text.DateFormat.LONG);
359    }
360
361    /**
362     * Returns a {@link java.text.DateFormat} object that can format the date
363     * in medium form (such as Dec. 31, 1999) for the current locale.
364     * @param context the application context
365     * @return the {@link java.text.DateFormat} object that formats the date in long form.
366     */
367    public static java.text.DateFormat getMediumDateFormat(Context context) {
368        return java.text.DateFormat.getDateInstance(java.text.DateFormat.MEDIUM);
369    }
370
371    /**
372     * Gets the current date format stored as a char array. The array will contain
373     * 3 elements ({@link #DATE}, {@link #MONTH}, and {@link #YEAR}) in the order
374     * specified by the user's format preference.  Note that this order is
375     * only appropriate for all-numeric dates; spelled-out (MEDIUM and LONG)
376     * dates will generally contain other punctuation, spaces, or words,
377     * not just the day, month, and year, and not necessarily in the same
378     * order returned here.
379     */
380    public static char[] getDateFormatOrder(Context context) {
381        char[] order = new char[] {DATE, MONTH, YEAR};
382        String value = getDateFormatString(context);
383        int index = 0;
384        boolean foundDate = false;
385        boolean foundMonth = false;
386        boolean foundYear = false;
387
388        for (char c : value.toCharArray()) {
389            if (!foundDate && (c == DATE)) {
390                foundDate = true;
391                order[index] = DATE;
392                index++;
393            }
394
395            if (!foundMonth && (c == MONTH || c == STANDALONE_MONTH)) {
396                foundMonth = true;
397                order[index] = MONTH;
398                index++;
399            }
400
401            if (!foundYear && (c == YEAR)) {
402                foundYear = true;
403                order[index] = YEAR;
404                index++;
405            }
406        }
407        return order;
408    }
409
410    private static String getDateFormatString(Context context) {
411        String value = Settings.System.getString(context.getContentResolver(),
412                Settings.System.DATE_FORMAT);
413
414        return getDateFormatStringForSetting(context, value);
415    }
416
417    /**
418     * Given a format string and a time in milliseconds since Jan 1, 1970 GMT, returns a
419     * CharSequence containing the requested date.
420     * @param inFormat the format string, as described in {@link android.text.format.DateFormat}
421     * @param inTimeInMillis in milliseconds since Jan 1, 1970 GMT
422     * @return a {@link CharSequence} containing the requested text
423     */
424    public static CharSequence format(CharSequence inFormat, long inTimeInMillis) {
425        return format(inFormat, new Date(inTimeInMillis));
426    }
427
428    /**
429     * Given a format string and a {@link java.util.Date} object, returns a CharSequence containing
430     * the requested date.
431     * @param inFormat the format string, as described in {@link android.text.format.DateFormat}
432     * @param inDate the date to format
433     * @return a {@link CharSequence} containing the requested text
434     */
435    public static CharSequence format(CharSequence inFormat, Date inDate) {
436        Calendar    c = new GregorianCalendar();
437
438        c.setTime(inDate);
439
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    public static boolean hasSeconds(CharSequence inFormat) {
455        if (inFormat == null) return false;
456
457        final int length = inFormat.length();
458
459        int c;
460        int count;
461
462        for (int i = 0; i < length; i += count) {
463            count = 1;
464            c = inFormat.charAt(i);
465
466            if (c == QUOTE) {
467                count = skipQuotedText(inFormat, i, length);
468            } else if (c == SECONDS) {
469                return true;
470            }
471        }
472
473        return false;
474    }
475
476    private static int skipQuotedText(CharSequence s, int i, int len) {
477        if (i + 1 < len && s.charAt(i + 1) == QUOTE) {
478            return 2;
479        }
480
481        int count = 1;
482        // skip leading quote
483        i++;
484
485        while (i < len) {
486            char c = s.charAt(i);
487
488            if (c == QUOTE) {
489                count++;
490                //  QUOTEQUOTE -> QUOTE
491                if (i + 1 < len && s.charAt(i + 1) == QUOTE) {
492                    i++;
493                } else {
494                    break;
495                }
496            } else {
497                i++;
498                count++;
499            }
500        }
501
502        return count;
503    }
504
505    /**
506     * Given a format string and a {@link java.util.Calendar} object, returns a CharSequence
507     * containing the requested date.
508     * @param inFormat the format string, as described in {@link android.text.format.DateFormat}
509     * @param inDate the date to format
510     * @return a {@link CharSequence} containing the requested text
511     */
512    public static CharSequence format(CharSequence inFormat, Calendar inDate) {
513        SpannableStringBuilder      s = new SpannableStringBuilder(inFormat);
514        int             c;
515        int             count;
516
517        int len = inFormat.length();
518
519        for (int i = 0; i < len; i += count) {
520            int temp;
521
522            count = 1;
523            c = s.charAt(i);
524
525            if (c == QUOTE) {
526                count = appendQuotedText(s, i, len);
527                len = s.length();
528                continue;
529            }
530
531            while ((i + count < len) && (s.charAt(i + count) == c)) {
532                count++;
533            }
534
535            String replacement;
536
537            switch (c) {
538                case AM_PM:
539                    replacement = DateUtils.getAMPMString(inDate.get(Calendar.AM_PM));
540                    break;
541
542                case CAPITAL_AM_PM:
543                    //FIXME: this is the same as AM_PM? no capital?
544                    replacement = DateUtils.getAMPMString(inDate.get(Calendar.AM_PM));
545                    break;
546
547                case DATE:
548                    replacement = zeroPad(inDate.get(Calendar.DATE), count);
549                    break;
550
551                case DAY:
552                    temp = inDate.get(Calendar.DAY_OF_WEEK);
553                    replacement = DateUtils.getDayOfWeekString(temp,
554                                                               count < 4 ?
555                                                               DateUtils.LENGTH_MEDIUM :
556                                                               DateUtils.LENGTH_LONG);
557                    break;
558
559                case HOUR:
560                    temp = inDate.get(Calendar.HOUR);
561
562                    if (0 == temp)
563                        temp = 12;
564
565                    replacement = zeroPad(temp, count);
566                    break;
567
568                case HOUR_OF_DAY:
569                    replacement = zeroPad(inDate.get(Calendar.HOUR_OF_DAY), count);
570                    break;
571
572                case MINUTE:
573                    replacement = zeroPad(inDate.get(Calendar.MINUTE), count);
574                    break;
575
576                case MONTH:
577                case STANDALONE_MONTH:
578                    replacement = getMonthString(inDate, count, c);
579                    break;
580
581                case SECONDS:
582                    replacement = zeroPad(inDate.get(Calendar.SECOND), count);
583                    break;
584
585                case TIME_ZONE:
586                    replacement = getTimeZoneString(inDate, count);
587                    break;
588
589                case YEAR:
590                    replacement = getYearString(inDate, count);
591                    break;
592
593                default:
594                    replacement = null;
595                    break;
596            }
597
598            if (replacement != null) {
599                s.replace(i, i + count, replacement);
600                count = replacement.length(); // CARE: count is used in the for loop above
601                len = s.length();
602            }
603        }
604
605        if (inFormat instanceof Spanned)
606            return new SpannedString(s);
607        else
608            return s.toString();
609    }
610
611    private static String getMonthString(Calendar inDate, int count, int kind) {
612        boolean standalone = (kind == STANDALONE_MONTH);
613        int month = inDate.get(Calendar.MONTH);
614
615        if (count >= 4) {
616            return standalone
617                ? DateUtils.getStandaloneMonthString(month, DateUtils.LENGTH_LONG)
618                : DateUtils.getMonthString(month, DateUtils.LENGTH_LONG);
619        } else if (count == 3) {
620            return standalone
621                ? DateUtils.getStandaloneMonthString(month, DateUtils.LENGTH_MEDIUM)
622                : DateUtils.getMonthString(month, DateUtils.LENGTH_MEDIUM);
623        } else {
624            // Calendar.JANUARY == 0, so add 1 to month.
625            return zeroPad(month+1, count);
626        }
627    }
628
629    private static String getTimeZoneString(Calendar inDate, int count) {
630        TimeZone tz = inDate.getTimeZone();
631
632        if (count < 2) { // FIXME: shouldn't this be <= 2 ?
633            return formatZoneOffset(inDate.get(Calendar.DST_OFFSET) +
634                                    inDate.get(Calendar.ZONE_OFFSET),
635                                    count);
636        } else {
637            boolean dst = inDate.get(Calendar.DST_OFFSET) != 0;
638            return tz.getDisplayName(dst, TimeZone.SHORT);
639        }
640    }
641
642    private static String formatZoneOffset(int offset, int count) {
643        offset /= 1000; // milliseconds to seconds
644        StringBuilder tb = new StringBuilder();
645
646        if (offset < 0) {
647            tb.insert(0, "-");
648            offset = -offset;
649        } else {
650            tb.insert(0, "+");
651        }
652
653        int hours = offset / 3600;
654        int minutes = (offset % 3600) / 60;
655
656        tb.append(zeroPad(hours, 2));
657        tb.append(zeroPad(minutes, 2));
658        return tb.toString();
659    }
660
661    private static String getYearString(Calendar inDate, int count) {
662        int year = inDate.get(Calendar.YEAR);
663        return (count <= 2) ? zeroPad(year % 100, 2)
664                            : String.format(Locale.getDefault(), "%d", year);
665    }
666
667    private static int appendQuotedText(SpannableStringBuilder s, int i, int len) {
668        if (i + 1 < len && s.charAt(i + 1) == QUOTE) {
669            s.delete(i, i + 1);
670            return 1;
671        }
672
673        int count = 0;
674
675        // delete leading quote
676        s.delete(i, i + 1);
677        len--;
678
679        while (i < len) {
680            char c = s.charAt(i);
681
682            if (c == QUOTE) {
683                //  QUOTEQUOTE -> QUOTE
684                if (i + 1 < len && s.charAt(i + 1) == QUOTE) {
685
686                    s.delete(i, i + 1);
687                    len--;
688                    count++;
689                    i++;
690                } else {
691                    //  Closing QUOTE ends quoted text copying
692                    s.delete(i, i + 1);
693                    break;
694                }
695            } else {
696                i++;
697                count++;
698            }
699        }
700
701        return count;
702    }
703
704    private static String zeroPad(int inValue, int inMinDigits) {
705        return String.format(Locale.getDefault(), "%0" + inMinDigits + "d", inValue);
706    }
707}
708