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 
17 package com.android.contacts.common.util;
18 
19 import android.content.Context;
20 import android.text.format.DateFormat;
21 import android.text.format.Time;
22 
23 
24 import java.text.ParsePosition;
25 import java.text.SimpleDateFormat;
26 import java.util.Calendar;
27 import java.util.Date;
28 import java.util.GregorianCalendar;
29 import java.util.Locale;
30 import java.util.TimeZone;
31 
32 /**
33  * Utility methods for processing dates.
34  */
35 public 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      */
parseDate(String string, boolean mustContainYear)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 
getUtcDate(Date date, boolean noYear)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 
getUtcDate(int year, int month, int dayOfMonth)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 
isYearSet(Calendar cal)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 
formatDate(Context context, String string)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      */
formatDate(Context context, String string, boolean longForm)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 
isMonthBeforeDay(Context context)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      */
getLocalizedDateFormatWithoutYear(Context context)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      */
getNextAnnualDate(Calendar target)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     /**
274      * Determine the difference, in days between two dates.  Uses similar logic as the
275      * {@link android.text.format.DateUtils.getRelativeTimeSpanString} method.
276      *
277      * @param time Instance of time object to use for calculations.
278      * @param date1 First date to check.
279      * @param date2 Second date to check.
280      * @return The absolute difference in days between the two dates.
281      */
getDayDifference(Time time, long date1, long date2)282     public static int getDayDifference(Time time, long date1, long date2) {
283         time.set(date1);
284         int startDay = Time.getJulianDay(date1, time.gmtoff);
285 
286         time.set(date2);
287         int currentDay = Time.getJulianDay(date2, time.gmtoff);
288 
289         return Math.abs(currentDay - startDay);
290     }
291 }
292