Determine if a String is a valid date before parsing

34,359

Solution 1

See Lazy Error Handling in Java for an overview of how to eliminate try/catch blocks using an Option type.

Functional Java is your friend.

In essence, what you want to do is to wrap the date parsing in a function that doesn't throw anything, but indicates in its return type whether parsing was successful or not. For example:

import fj.F; import fj.F2;
import fj.data.Option;
import java.text.SimpleDateFormat;
import java.text.ParseException;
import static fj.Function.curry;
import static fj.Option.some;
import static fj.Option.none;
...

F<String, F<String, Option<Date>>> parseDate =
  curry(new F2<String, String, Option<Date>>() {
    public Option<Date> f(String pattern, String s) {
      try {
        return some(new SimpleDateFormat(pattern).parse(s));
      }
      catch (ParseException e) {
        return none();
      }
    }
  });

OK, now you've a reusable date parser that doesn't throw anything, but indicates failure by returning a value of type Option.None. Here's how you use it:

import fj.data.List;
import static fj.data.Stream.stream;
import static fj.data.Option.isSome_;
....
public Option<Date> parseWithPatterns(String s, Stream<String> patterns) { 
  return stream(s).apply(patterns.map(parseDate)).find(isSome_()); 
}

That will give you the date parsed with the first pattern that matches, or a value of type Option.None, which is type-safe whereas null isn't.

If you're wondering what Stream is... it's a lazy list. This ensures that you ignore patterns after the first successful one. No need to do too much work.

Call your function like this:

for (Date d: parseWithPatterns(someString, stream("dd/MM/yyyy", "dd-MM-yyyy")) {
  // Do something with the date here.
}

Or...

Option<Date> d = parseWithPatterns(someString,
                                   stream("dd/MM/yyyy", "dd-MM-yyyy"));
if (d.isNone()) {
  // Handle the case where neither pattern matches.
} 
else {
  // Do something with d.some()
}

Solution 2

Don't be too hard on yourself about using try-catch in logic: this is one of those situations where Java forces you to so there's not a lot you can do about it.

But in this case you could instead use DateFormat.parse(String, ParsePosition).

Solution 3

You can take advantage of regular expressions to determine which format the string is in, and whether it matches any valid format. Something like this (not tested):

(Oops, I wrote this in C# before checking to see what language you were using.)

Regex test = new Regex(@"^(?:(?<formatA>\d{2}-[a-zA-Z]{3}-\d{2})|(?<formatB>\d{2}/\d{2}/\d{3}))$", RegexOption.Compiled);
Match match = test.Match(yourString);
if (match.Success)
{
    if (!string.IsNullOrEmpty(match.Groups["formatA"]))
    {
        // Use format A.
    }
    else if (!string.IsNullOrEmpty(match.Groups["formatB"]))
    {
        // Use format B.
    }
    ...
}

Solution 4

Assuming the patterns you gave are the only likely choices, I would look at the String passed in to see which format to apply.

public Date parseDate(final String date) {
  if (date == null) {
    return null;
  }

  SimpleDateFormat format = (date.charAt(2) == '/') ? new SimpleDateFormat("dd/MMM/yyyy")
                                                   : new SimpleDateFormat("dd-MMM-yy");
  try {
    return format.parse(date);
  } catch (ParseException e) {
    // Log a complaint and include date in the complaint
  }
  return null;
}

As others have mentioned, if you can guarantee that you will never access the DateFormats in a multi-threaded manner, you can make class-level or static instances.

Solution 5

In this limited situation, the best (and fastest method) is certinally to parse out the day, then based on the next char either '/' or '-' try to parse out the rest. and if at any point there is unexpected data, return NULL then.

Share:
34,359
Morgul Master
Author by

Morgul Master

Updated on November 08, 2020

Comments

  • Morgul Master
    Morgul Master over 3 years

    I have this situation where I am reading about 130K records containing dates stored as String fields. Some records contain blanks (nulls), some contain strings like this: 'dd-MMM-yy' and some contain this 'dd/MM/yyyy'.

    I have written a method like this:

    public Date parsedate(String date){
    
       if(date !== null){
          try{
            1. create a SimpleDateFormat object using 'dd-MMM-yy' as the pattern
            2. parse the date
            3. return the parsed date
          }catch(ParseException e){
              try{
                  1. create a SimpleDateFormat object using 'dd/MM/yyy' as the pattern
                  2. parse the date
                  3. return parsed date
               }catch(ParseException e){
                  return null
               }
          }
       }else{
          return null
       }
    
    } 
    

    So you may have already spotted the problem. I am using the try .. catch as part of my logic. It would be better is I can determine before hand that the String actually contains a parseable date in some format then attempt to parse it.

    So, is there some API or library that can help with this? I do not mind writing several different Parse classes to handle the different formats and then creating a factory to select the correct6 one, but, how do I determine which one?

    Thanks.

  • van
    van almost 15 years
    Checking length should be faster in Java then searching for a char in.
  • Eddie
    Eddie almost 15 years
    @van: No need for premature optimization.
  • Morgul Master
    Morgul Master almost 15 years
    This is a very interesting answer. I will definitely have to look up Functional Java. Thanks.
  • Meow
    Meow over 7 years
    in java8 even though the docs (docs.oracle.com/javase/8/docs/api/java/text/…) doesn't say it throws anything, the code indicates that it does throw a few RuntimeException :(