1/*
2 * Copyright (C) 2010 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 com.android.contacts.util;
18
19import android.content.Context;
20import android.text.format.DateFormat;
21
22import com.android.contacts.common.util.CommonDateUtils;
23
24import java.text.ParsePosition;
25import java.text.SimpleDateFormat;
26import java.util.Calendar;
27import java.util.Date;
28import java.util.GregorianCalendar;
29import java.util.Locale;
30import java.util.TimeZone;
31
32/**
33 * Utility methods for processing dates.
34 */
35public class DateUtils {
36    public static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC");
37
38    /**
39     * When parsing a date without a year, the system assumes 1970, which wasn't a leap-year.
40     * Let's add a one-off hack for that day of the year
41     */
42    public static final String NO_YEAR_DATE_FEB29TH = "--02-29";
43
44    // Variations of ISO 8601 date format.  Do not change the order - it does affect the
45    // result in ambiguous cases.
46    private static final SimpleDateFormat[] DATE_FORMATS = {
47        CommonDateUtils.FULL_DATE_FORMAT,
48        CommonDateUtils.DATE_AND_TIME_FORMAT,
49        new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US),
50        new SimpleDateFormat("yyyyMMdd", Locale.US),
51        new SimpleDateFormat("yyyyMMdd'T'HHmmssSSS'Z'", Locale.US),
52        new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US),
53        new SimpleDateFormat("yyyyMMdd'T'HHmm'Z'", Locale.US),
54    };
55
56    static {
57        for (SimpleDateFormat format : DATE_FORMATS) {
58            format.setLenient(true);
59            format.setTimeZone(UTC_TIMEZONE);
60        }
61        CommonDateUtils.NO_YEAR_DATE_FORMAT.setTimeZone(UTC_TIMEZONE);
62    }
63
64    /**
65     * Parses the supplied string to see if it looks like a date.
66     *
67     * @param string The string representation of the provided date
68     * @param mustContainYear If true, the string is parsed as a date containing a year. If false,
69     * the string is parsed into a valid date even if the year field is missing.
70     * @return A Calendar object corresponding to the date if the string is successfully parsed.
71     * If not, null is returned.
72     */
73    public static Calendar parseDate(String string, boolean mustContainYear) {
74        ParsePosition parsePosition = new ParsePosition(0);
75        Date date;
76        if (!mustContainYear) {
77            final boolean noYearParsed;
78            // Unfortunately, we can't parse Feb 29th correctly, so let's handle this day seperately
79            if (NO_YEAR_DATE_FEB29TH.equals(string)) {
80                return getUtcDate(0, Calendar.FEBRUARY, 29);
81            } else {
82                synchronized (CommonDateUtils.NO_YEAR_DATE_FORMAT) {
83                    date = CommonDateUtils.NO_YEAR_DATE_FORMAT.parse(string, parsePosition);
84                }
85                noYearParsed = parsePosition.getIndex() == string.length();
86            }
87
88            if (noYearParsed) {
89                return getUtcDate(date, true);
90            }
91        }
92        for (int i = 0; i < DATE_FORMATS.length; i++) {
93            SimpleDateFormat f = DATE_FORMATS[i];
94            synchronized (f) {
95                parsePosition.setIndex(0);
96                date = f.parse(string, parsePosition);
97                if (parsePosition.getIndex() == string.length()) {
98                    return getUtcDate(date, false);
99                }
100            }
101        }
102        return null;
103    }
104
105    private static final Calendar getUtcDate(Date date, boolean noYear) {
106        final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US);
107        calendar.setTime(date);
108        if (noYear) {
109            calendar.set(Calendar.YEAR, 0);
110        }
111        return calendar;
112    }
113
114    private static final Calendar getUtcDate(int year, int month, int dayOfMonth) {
115        final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US);
116        calendar.clear();
117        calendar.set(Calendar.YEAR, year);
118        calendar.set(Calendar.MONTH, month);
119        calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
120        return calendar;
121    }
122
123    public static boolean isYearSet(Calendar cal) {
124        // use the Calendar.YEAR field to track whether or not the year is set instead of
125        // Calendar.isSet() because doing Calendar.get() causes Calendar.isSet() to become
126        // true irregardless of what the previous value was
127        return cal.get(Calendar.YEAR) > 1;
128    }
129
130    /**
131     * Same as {@link #formatDate(Context context, String string, boolean longForm)}, with
132     * longForm set to {@code true} by default.
133     *
134     * @param context Valid context
135     * @param string String representation of a date to parse
136     * @return Returns the same date in a cleaned up format. If the supplied string does not look
137     * like a date, return it unchanged.
138     */
139
140    public static String formatDate(Context context, String string) {
141        return formatDate(context, string, true);
142    }
143
144    /**
145     * Parses the supplied string to see if it looks like a date.
146     *
147     * @param context Valid context
148     * @param string String representation of a date to parse
149     * @param longForm If true, return the date formatted into its long string representation.
150     * If false, return the date formatted using its short form representation (i.e. 12/11/2012)
151     * @return Returns the same date in a cleaned up format. If the supplied string does not look
152     * like a date, return it unchanged.
153     */
154    public static String formatDate(Context context, String string, boolean longForm) {
155        if (string == null) {
156            return null;
157        }
158
159        string = string.trim();
160        if (string.length() == 0) {
161            return string;
162        }
163        final Calendar cal = parseDate(string, false);
164
165        // we weren't able to parse the string successfully so just return it unchanged
166        if (cal == null) {
167            return string;
168        }
169
170        final boolean isYearSet = isYearSet(cal);
171        final java.text.DateFormat outFormat;
172        if (!isYearSet) {
173            outFormat = getLocalizedDateFormatWithoutYear(context);
174        } else {
175            outFormat =
176                    longForm ? DateFormat.getLongDateFormat(context) :
177                    DateFormat.getDateFormat(context);
178        }
179        synchronized (outFormat) {
180            outFormat.setTimeZone(UTC_TIMEZONE);
181            return outFormat.format(cal.getTime());
182        }
183    }
184
185    public static boolean isMonthBeforeDay(Context context) {
186        char[] dateFormatOrder = DateFormat.getDateFormatOrder(context);
187        for (int i = 0; i < dateFormatOrder.length; i++) {
188            if (dateFormatOrder[i] == DateFormat.DATE) {
189                return false;
190            }
191            if (dateFormatOrder[i] == DateFormat.MONTH) {
192                return true;
193            }
194        }
195        return false;
196    }
197
198    /**
199     * Returns a SimpleDateFormat object without the year fields by using a regular expression
200     * to eliminate the year in the string pattern. In the rare occurence that the resulting
201     * pattern cannot be reconverted into a SimpleDateFormat, it uses the provided context to
202     * determine whether the month field should be displayed before the day field, and returns
203     * either "MMMM dd" or "dd MMMM" converted into a SimpleDateFormat.
204     */
205    public static java.text.DateFormat getLocalizedDateFormatWithoutYear(Context context) {
206        final String pattern = ((SimpleDateFormat) SimpleDateFormat.getDateInstance(
207                java.text.DateFormat.LONG)).toPattern();
208        // Determine the correct regex pattern for year.
209        // Special case handling for Spanish locale by checking for "de"
210        final String yearPattern = pattern.contains(
211                "de") ? "[^Mm]*[Yy]+[^Mm]*" : "[^DdMm]*[Yy]+[^DdMm]*";
212        try {
213         // Eliminate the substring in pattern that matches the format for that of year
214            return new SimpleDateFormat(pattern.replaceAll(yearPattern, ""));
215        } catch (IllegalArgumentException e) {
216            return new SimpleDateFormat(
217                    DateUtils.isMonthBeforeDay(context) ? "MMMM dd" : "dd MMMM");
218        }
219    }
220
221    /**
222     * Given a calendar (possibly containing only a day of the year), returns the earliest possible
223     * anniversary of the date that is equal to or after the current point in time if the date
224     * does not contain a year, or the date converted to the local time zone (if the date contains
225     * a year.
226     *
227     * @param target The date we wish to convert(in the UTC time zone).
228     * @return If date does not contain a year (year < 1900), returns the next earliest anniversary
229     * that is after the current point in time (in the local time zone). Otherwise, returns the
230     * adjusted Date in the local time zone.
231     */
232    public static Date getNextAnnualDate(Calendar target) {
233        final Calendar today = Calendar.getInstance();
234        today.setTime(new Date());
235
236        // Round the current time to the exact start of today so that when we compare
237        // today against the target date, both dates are set to exactly 0000H.
238        today.set(Calendar.HOUR_OF_DAY, 0);
239        today.set(Calendar.MINUTE, 0);
240        today.set(Calendar.SECOND, 0);
241        today.set(Calendar.MILLISECOND, 0);
242
243        final boolean isYearSet = isYearSet(target);
244        final int targetYear = target.get(Calendar.YEAR);
245        final int targetMonth = target.get(Calendar.MONTH);
246        final int targetDay = target.get(Calendar.DAY_OF_MONTH);
247        final boolean isFeb29 = (targetMonth == Calendar.FEBRUARY && targetDay == 29);
248        final GregorianCalendar anniversary = new GregorianCalendar();
249        // Convert from the UTC date to the local date. Set the year to today's year if the
250        // there is no provided year (targetYear < 1900)
251        anniversary.set(!isYearSet ? today.get(Calendar.YEAR) : targetYear,
252                targetMonth, targetDay);
253        // If the anniversary's date is before the start of today and there is no year set,
254        // increment the year by 1 so that the returned date is always equal to or greater than
255        // today. If the day is a leap year, keep going until we get the next leap year anniversary
256        // Otherwise if there is already a year set, simply return the exact date.
257        if (!isYearSet) {
258            int anniversaryYear = today.get(Calendar.YEAR);
259            if (anniversary.before(today) ||
260                    (isFeb29 && !anniversary.isLeapYear(anniversaryYear))) {
261                // If the target date is not Feb 29, then set the anniversary to the next year.
262                // Otherwise, keep going until we find the next leap year (this is not guaranteed
263                // to be in 4 years time).
264                do {
265                    anniversaryYear +=1;
266                } while (isFeb29 && !anniversary.isLeapYear(anniversaryYear));
267                anniversary.set(anniversaryYear, targetMonth, targetDay);
268            }
269        }
270        return anniversary.getTime();
271    }
272}
273