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