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