blob: e20aadf4d885592ccdf1c3731804d7fac06117ad [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.contacts.util;
import android.content.Context;
import android.text.format.DateFormat;
import com.android.contacts.common.util.CommonDateUtils;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
/**
* Utility methods for processing dates.
*/
public class DateUtils {
public static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC");
/**
* When parsing a date without a year, the system assumes 1970, which wasn't a leap-year.
* Let's add a one-off hack for that day of the year
*/
public static final String NO_YEAR_DATE_FEB29TH = "--02-29";
// Variations of ISO 8601 date format. Do not change the order - it does affect the
// result in ambiguous cases.
private static final SimpleDateFormat[] DATE_FORMATS = {
CommonDateUtils.FULL_DATE_FORMAT,
CommonDateUtils.DATE_AND_TIME_FORMAT,
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US),
new SimpleDateFormat("yyyyMMdd", Locale.US),
new SimpleDateFormat("yyyyMMdd'T'HHmmssSSS'Z'", Locale.US),
new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US),
new SimpleDateFormat("yyyyMMdd'T'HHmm'Z'", Locale.US),
};
static {
for (SimpleDateFormat format : DATE_FORMATS) {
format.setLenient(true);
format.setTimeZone(UTC_TIMEZONE);
}
CommonDateUtils.NO_YEAR_DATE_FORMAT.setTimeZone(UTC_TIMEZONE);
}
/**
* Parses the supplied string to see if it looks like a date.
*
* @param string The string representation of the provided date
* @param mustContainYear If true, the string is parsed as a date containing a year. If false,
* the string is parsed into a valid date even if the year field is missing.
* @return A Calendar object corresponding to the date if the string is successfully parsed.
* If not, null is returned.
*/
public static Calendar parseDate(String string, boolean mustContainYear) {
ParsePosition parsePosition = new ParsePosition(0);
Date date;
if (!mustContainYear) {
final boolean noYearParsed;
// Unfortunately, we can't parse Feb 29th correctly, so let's handle this day seperately
if (NO_YEAR_DATE_FEB29TH.equals(string)) {
return getUtcDate(0, Calendar.FEBRUARY, 29);
} else {
synchronized (CommonDateUtils.NO_YEAR_DATE_FORMAT) {
date = CommonDateUtils.NO_YEAR_DATE_FORMAT.parse(string, parsePosition);
}
noYearParsed = parsePosition.getIndex() == string.length();
}
if (noYearParsed) {
return getUtcDate(date, true);
}
}
for (int i = 0; i < DATE_FORMATS.length; i++) {
SimpleDateFormat f = DATE_FORMATS[i];
synchronized (f) {
parsePosition.setIndex(0);
date = f.parse(string, parsePosition);
if (parsePosition.getIndex() == string.length()) {
return getUtcDate(date, false);
}
}
}
return null;
}
private static final Calendar getUtcDate(Date date, boolean noYear) {
final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US);
calendar.setTime(date);
if (noYear) {
calendar.set(Calendar.YEAR, 0);
}
return calendar;
}
private static final Calendar getUtcDate(int year, int month, int dayOfMonth) {
final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US);
calendar.clear();
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, month);
calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
return calendar;
}
public static boolean isYearSet(Calendar cal) {
// use the Calendar.YEAR field to track whether or not the year is set instead of
// Calendar.isSet() because doing Calendar.get() causes Calendar.isSet() to become
// true irregardless of what the previous value was
return cal.get(Calendar.YEAR) > 1;
}
/**
* Same as {@link #formatDate(Context context, String string, boolean longForm)}, with
* longForm set to {@code true} by default.
*
* @param context Valid context
* @param string String representation of a date to parse
* @return Returns the same date in a cleaned up format. If the supplied string does not look
* like a date, return it unchanged.
*/
public static String formatDate(Context context, String string) {
return formatDate(context, string, true);
}
/**
* Parses the supplied string to see if it looks like a date.
*
* @param context Valid context
* @param string String representation of a date to parse
* @param longForm If true, return the date formatted into its long string representation.
* If false, return the date formatted using its short form representation (i.e. 12/11/2012)
* @return Returns the same date in a cleaned up format. If the supplied string does not look
* like a date, return it unchanged.
*/
public static String formatDate(Context context, String string, boolean longForm) {
if (string == null) {
return null;
}
string = string.trim();
if (string.length() == 0) {
return string;
}
final Calendar cal = parseDate(string, false);
// we weren't able to parse the string successfully so just return it unchanged
if (cal == null) {
return string;
}
final boolean isYearSet = isYearSet(cal);
final java.text.DateFormat outFormat;
if (!isYearSet) {
outFormat = getLocalizedDateFormatWithoutYear(context);
} else {
outFormat =
longForm ? DateFormat.getLongDateFormat(context) :
DateFormat.getDateFormat(context);
}
synchronized (outFormat) {
outFormat.setTimeZone(UTC_TIMEZONE);
return outFormat.format(cal.getTime());
}
}
public static boolean isMonthBeforeDay(Context context) {
char[] dateFormatOrder = DateFormat.getDateFormatOrder(context);
for (int i = 0; i < dateFormatOrder.length; i++) {
if (dateFormatOrder[i] == DateFormat.DATE) {
return false;
}
if (dateFormatOrder[i] == DateFormat.MONTH) {
return true;
}
}
return false;
}
/**
* Returns a SimpleDateFormat object without the year fields by using a regular expression
* to eliminate the year in the string pattern. In the rare occurence that the resulting
* pattern cannot be reconverted into a SimpleDateFormat, it uses the provided context to
* determine whether the month field should be displayed before the day field, and returns
* either "MMMM dd" or "dd MMMM" converted into a SimpleDateFormat.
*/
public static java.text.DateFormat getLocalizedDateFormatWithoutYear(Context context) {
final String pattern = ((SimpleDateFormat) SimpleDateFormat.getDateInstance(
java.text.DateFormat.LONG)).toPattern();
// Determine the correct regex pattern for year.
// Special case handling for Spanish locale by checking for "de"
final String yearPattern = pattern.contains(
"de") ? "[^Mm]*[Yy]+[^Mm]*" : "[^DdMm]*[Yy]+[^DdMm]*";
try {
// Eliminate the substring in pattern that matches the format for that of year
return new SimpleDateFormat(pattern.replaceAll(yearPattern, ""));
} catch (IllegalArgumentException e) {
return new SimpleDateFormat(
DateUtils.isMonthBeforeDay(context) ? "MMMM dd" : "dd MMMM");
}
}
/**
* Given a calendar (possibly containing only a day of the year), returns the earliest possible
* anniversary of the date that is equal to or after the current point in time if the date
* does not contain a year, or the date converted to the local time zone (if the date contains
* a year.
*
* @param target The date we wish to convert(in the UTC time zone).
* @return If date does not contain a year (year < 1900), returns the next earliest anniversary
* that is after the current point in time (in the local time zone). Otherwise, returns the
* adjusted Date in the local time zone.
*/
public static Date getNextAnnualDate(Calendar target) {
final Calendar today = Calendar.getInstance();
today.setTime(new Date());
// Round the current time to the exact start of today so that when we compare
// today against the target date, both dates are set to exactly 0000H.
today.set(Calendar.HOUR_OF_DAY, 0);
today.set(Calendar.MINUTE, 0);
today.set(Calendar.SECOND, 0);
today.set(Calendar.MILLISECOND, 0);
final boolean isYearSet = isYearSet(target);
final int targetYear = target.get(Calendar.YEAR);
final int targetMonth = target.get(Calendar.MONTH);
final int targetDay = target.get(Calendar.DAY_OF_MONTH);
final boolean isFeb29 = (targetMonth == Calendar.FEBRUARY && targetDay == 29);
final GregorianCalendar anniversary = new GregorianCalendar();
// Convert from the UTC date to the local date. Set the year to today's year if the
// there is no provided year (targetYear < 1900)
anniversary.set(!isYearSet ? today.get(Calendar.YEAR) : targetYear,
targetMonth, targetDay);
// If the anniversary's date is before the start of today and there is no year set,
// increment the year by 1 so that the returned date is always equal to or greater than
// today. If the day is a leap year, keep going until we get the next leap year anniversary
// Otherwise if there is already a year set, simply return the exact date.
if (!isYearSet) {
int anniversaryYear = today.get(Calendar.YEAR);
if (anniversary.before(today) ||
(isFeb29 && !anniversary.isLeapYear(anniversaryYear))) {
// If the target date is not Feb 29, then set the anniversary to the next year.
// Otherwise, keep going until we find the next leap year (this is not guaranteed
// to be in 4 years time).
do {
anniversaryYear +=1;
} while (isFeb29 && !anniversary.isLeapYear(anniversaryYear));
anniversary.set(anniversaryYear, targetMonth, targetDay);
}
}
return anniversary.getTime();
}
}