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