Thursday, June 27, 2013

How to Correctly Format Date/Time Strings on Android

One aspect of internationalization is to correctly format your date/time strings.  Different countries use very different formats and it's easy to incorrectly format your strings for your international users.

Your first foray into formatting date/times is probably through java.text.DateFormat (via SimpleDateFormat):

Calendar cal = new GregorianCalendar(2013, 11, 20);
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
String date = df.format(cal.getTime());
// date == "2013-12-20"

While this works great for formatting parameters (for, say, a web service), it's terrible for localization.  Your international users won't be using the same date/time format you're using and it won't pick up user preferences (e.g., date order or 12-hour vs 24-hour).

A more correct way of doing it is to use android.text.format.DateFormat (not to be confused with the previous DateFormat).  There are some methods here that return formatters defined by the system's locale, like getDateFormat() and getTimeFormat() (among others):

Calendar cal = new GregorianCalendar(2013, 11, 20);
DateFormat df = android.text.format.DateFormat.getDateFormat(this); 
String date = df.format(cal.getTime());
// date == "12/20/2013"

The problem with these formatters is that they are inflexible; what if you don't want to show a year on a date?  What if you want to include the day of the week?  There are only limited circumstances where these formatters are good enough.

The best solution is to use DateUtils.  It has two powerful methods - formatDateTime() and formatDateRange() - which take in flags to determine which fields to include.  It automatically formats to the user's locale and preferences without you having to worry about it.

DateUtils.formatDateTime() formats a single point in time.  Here's a few examples:

Calendar cal = new GregorianCalendar(2013, 11, 20);
String date = DateUtils.formatDateTime(this, cal.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE);
// date == "December 20"
date = DateUtils.formatDateTime(this, cal.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_YEAR);
// date == "12/20/2013"
date = DateUtils.formatDateTime(this, cal.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME);
// date == "00:00, 12/20/2013"

DateUtils.formatDateRange() formats a range, like "Jan 5 - Feb 12".  Why might you want to use this instead of just concatenating two calls to formatDateTime()?  Besides being easier, it can optimize output in certain circumstances by reducing redundant field usage, like months/years when they don't change throughout the range:

Calendar cal1 = new GregorianCalendar(2013, 11, 20);
Calendar cal2 = new GregorianCalendar(2013, 11, 25);
Calendar cal3 = new GregorianCalendar(2014, 0, 5);
String date = DateUtils.formatDateRange(this, cal1.getTimeInMillis(), cal2.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE);
// date == "December 20 - 24"
date = DateUtils.formatDateRange(this, cal1.getTimeInMillis(), cal3.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE);
// date == "December 20, 2013 - January 4, 2014"

One thing to watch out for with formatDateRange() is where it cuts off the day.  You may notice in the example above that the date ranges seem to be off by one day; that's because it cuts off at midnight.  If you add a millisecond it should properly format the range.

If you want your application to abide by the locale's formatting rules while still having control over what information to show, DateUtils is your place to go.  Be sure to read through all the different formatting flags so you can wield the most power with this tool.

(One final note - the example code above shows output in my locale.  In your locale it may differ - this is on purpose, of course!)