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