views:

6594

answers:

5

I find it curious that the most obvious way to create Date objects in Java has been deprecated and appears to have been "substituted" with not so obvious to use lenient calendar. So...

How do you check that a date given as a combination of day, month and year is a valid date? For instance a date 2008-02-31 (as in yyyy-mm-dd) would be invalid date.

+4  A: 

The current way is to use the calendar class. It has the setLenient method that will validate the date and throw and exception if it is out of range as in your example.

Forgot to add: If you get a calendar instance and set the time using your date, this is how you get the validation.

Calendar cal = Calendar.getInstance();
try {
cal.setTime(yourDate);
catch (Exception e) {
  System.out.println("Invalid date");
}
AdamC
don't think this exactly works as-is. Calendar.setTime takes java.util.Date, so the conversion from string has already happened by the time you get a "yourDate" object.
tardate
3 problems with the example code:1. After getting a Calendar instance, you must call cal.setLenient(false). Otherwise a date like Feb 31 2007 will be considered valid.2. An exception doesn't get thrown by cal.setTime(). You must call cal.getTime() after the setTime() call, which throws an exception on invalid date.3. Typo: Missing a '}' before the catch.
Liron Yahdav
+3  A: 

You can use SimpleDateFormat

For example something like:

boolean isLegalDate(String s) {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    sdf.setLenient(false);
    return sdf.parse(s, new ParsePosition(0)) != null;
}
Maglob
+8  A: 

As shown by @Maglob, the basic approach is to test the conversion from string to date using SimpleDateFormat.parse. That will catch invalid day/month combinations like 2008-02-31.

However, in practice that is rarely enough since SimpleDateFormat.parse is exceedingly liberal. There are two behaviours you might be concerned with:

Invalid characters in the date string Surprisingly, 2008-02-2x will "pass" as a valid date with locale format = "yyyy-MM-dd" for example. Even when isLenient==false.

Years: 2, 3 or 4 digits? You may also want to enforce 4-digit years rather than allowing the default SimpleDateFormat behaviour (which will interpret "12-02-31" differently depending on whether your format was "yyyy-MM-dd" or "yy-MM-dd")

A Strict Solution with the Standard Library

So a complete string to date test could look like this: a combination of regex match, and then a forced date conversion. The trick with the regex is to make it locale-friendly.

  Date parseDate(String maybeDate, String format, boolean lenient) {
    Date date = null;

    // test date string matches format structure using regex
    // - weed out illegal characters and enforce 4-digit year
    // - create the regex based on the local format string
    String reFormat = Pattern.compile("d+|M+").matcher(Matcher.quoteReplacement(format)).replaceAll("\\\\d{1,2}");
    reFormat = Pattern.compile("y+").matcher(reFormat).replaceAll("\\\\d{4}");
    if ( Pattern.compile(reFormat).matcher(maybeDate).matches() ) {

      // date string matches format structure, 
      // - now test it can be converted to a valid date
      SimpleDateFormat sdf = (SimpleDateFormat)DateFormat.getDateInstance();
      sdf.applyPattern(format);
      sdf.setLenient(lenient);
      try { date = sdf.parse(maybeDate); } catch (ParseException e) { }
    } 
    return date;
  } 

  // used like this:
  Date date = parseDate( "21/5/2009", "d/M/yyyy", false);

Note that the regex assumes the format string contains only day, month, year, and separator characters. Aside from that, format can be in any locale format: "d/MM/yy", "yyyy-MM-dd", and so on. The format string for the current locale could be obtained like this:

Locale locale = Locale.getDefault();
SimpleDateFormat sdf = (SimpleDateFormat)DateFormat.getDateInstance(DateFormat.SHORT, locale );
String format = sdf.toPattern();

Joda Time - Better Alternative?

I've been hearing about joda time recently and thought I'd compare. Two points:

  1. Seems better at being strict about invalid characters in the date string, unlike SimpleDateFormat
  2. Can't see a way to enforce 4-digit years with it yet (but I guess you could create your own DateTimeFormatter for this purpose)

It's quite simple to use:

import org.joda.time.format.*;
import org.joda.time.DateTime;

org.joda.time.DateTime parseDate(String maybeDate, String format) {
  org.joda.time.DateTime date = null;
  try {
    DateTimeFormatter fmt = DateTimeFormat.forPattern(format);
    date =  fmt.parseDateTime(maybeDate);
  } catch (Exception e) { }
  return date;
}
tardate
+1  A: 

Two comments on the use of SimpleDateFormat.

it should be declared as a static instance if declared as static access should be synchronized as it is not thread safe

IME that is better that instantiating an instance for each parse of a date.

Tom
good point Tom. I updated the example I provided to make sure consistently using static instance.
tardate
+1  A: 

An alternative strict solution using the standard library is to perform the following:

1) Create a strict SimpleDateFormat using your pattern

2) Attempt to parse the user entered value using the format object

3) If successful, reformat the Date resulting from (2) using the same date format (from (1))

4) Compare the reformatted date against the original, user-entered value. If they're equal then the value entered strictly matches your pattern.

This way, you don't need to create complex regular expressions - in my case I needed to support all of SimpleDateFormat's pattern syntax, rather than be limited to certain types like just days, months and years.

Ben