Thursday, May 10, 2007

Date Parsing and Formating in Java




Sounds simple. There is SimpleDateFormat you may say, or GenericTypeValidator. What's the problem then? Well folks, both libraries are not good enough to parse/format dates.

So we want to allowed users to input dates only in a format "yyyy-MM-dd" or "yyy-MM-dd HH:mm".

Let’s write a unit test for that:


import java.util.Date;

import junit.framework.TestCase;

public class DateTimeFormatTest extends TestCase{
public void testParseDate() throws Exception {
Date date = DateTimeFormat.parseDate("2005-01-01", DateTimeFormat.getDateFormat());
assertNotNull(date);

}
}


OK, now let's write a class:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.apache.commons.validator.GenericTypeValidator;

public class DateTimeFormat {
private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
private static final String DATE_FORMAT = "yyyy-MM-dd";

public static Date parseDate(String dateValue, String format) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format);
Date aParsedDate = null;
try {
aParsedDate = simpleDateFormat.parse(dateValue);
} catch (ParseException e) {
return null;
}
return aParsedDate;
}

public static String getDateFormat() {
return DATE_FORMAT;
}

public static String getDateTimeFormat() {
return DATE_TIME_FORMAT;
}

}


So now if we run a test it succeeds. Let's add additional tests.

date = DateTimeFormat.parseDate("2005-01-aa", DateTimeFormat.getDateFormat());
assertNull(date);

date = DateTimeFormat.parseDate("2005/01/01", DateTimeFormat.getDateFormat());
assertNull(date);



Hey, it's working again. But wait, what about this one:


date = DateTimeFormat.parseDate("2005-01-01 12:12", DateTimeFormat.getDateFormat());
assertNull(date);

Ups, tests don’t pass. Now there is a way that we can tell SimpleDateFormat to be more strict, just add this line

simpleDateFormat.setLenient(false);

unfortunately it won't work. I guess using this kind of parsing is not a good idea :>

But now let's use GenericTypeValidator. We write a test:

public void testValidatorParseDate() throws Exception {
Date date = DateTimeFormat.validatorParseDate("2005-01-01", DateTimeFormat.getDateFormat());
assertNotNull(date);

date = DateTimeFormat.validatorParseDate("2005-01-aa", DateTimeFormat.getDateFormat());
assertNull(date);

date = DateTimeFormat.validatorParseDate("2005/01/01", DateTimeFormat.getDateFormat());
assertNull(date);

date = DateTimeFormat.validatorParseDate("2005-01-01 12:12", DateTimeFormat.getDateFormat());
assertNull(date);
}



and we add a method to the class:


public static Date validatorParseDate(String dateValue, String format) {
Date aParsedDate = GenericTypeValidator.formatDate(dateValue, format, true);
return aParsedDate;
}



Hey this one works. Now add additional test case:


date = DateTimeFormat.validatorParseDate("2005-1-1-1", DateTimeFormat.getDateFormat());
assertNull(date);


Ups again failure. This same thing happens to the next test:


date = DateTimeFormat.validatorParseDate("2005-11-11 1:1:1", DateTimeFormat.getDateTimeFormat());
assertNull(date);


Again using this parser is not a good idea, but it's better then SimpleDateFormat. What to do then? I use regexps, but by using them I force myself to write additional regexp for every date pattern that I allow. In showed example I've only used two date patterns "yyyy-MM-dd HH:mm" and "yyyy-MM-dd" but I used to deal with systems with more then 8 date patterns and it gets pretty messy their, witch means that I have to use some more complicated techniques in order to create a good looking code. Now after refactoring, both classes looks as follow:

import java.util.Date;

import junit.framework.TestCase;

public class DateTimeFormatTest extends TestCase{

public void testValidatorParse() throws Exception {
String dateFormat = DateTimeFormat.getDateFormat();
String dateFormatRegexp = DateTimeFormat.getDateFormatRegexp();

Date date = DateTimeFormat.validatorParse("2005-01-01", dateFormat, dateFormatRegexp);
assertNotNull(date);

date = DateTimeFormat.validatorParse("2005-01-1", dateFormat, dateFormatRegexp);
assertNull(date);

date = DateTimeFormat.validatorParse("2005-01-01", DateTimeFormat.getDateTimeFormat(), dateFormatRegexp);
assertNull(date);

date = DateTimeFormat.validatorParse("2005-01-01", dateFormat, DateTimeFormat.getDateTimeFormatRegexp());
assertNull(date);

date = DateTimeFormat.validatorParse("2005-01-01 12:12", DateTimeFormat.getDateTimeFormat(), DateTimeFormat.getDateTimeFormatRegexp());
assertNotNull(date);
}

public void testParse() throws Exception {
String dateFormat = DateTimeFormat.getDateFormat();
String dateFormatRegexp = DateTimeFormat.getDateFormatRegexp();

Date date = DateTimeFormat.parse("2005-01-01", dateFormat, dateFormatRegexp);
assertNotNull(date);

date = DateTimeFormat.parse("2005-01-1", dateFormat, dateFormatRegexp);
assertNull(date);

date = DateTimeFormat.parse("2005-01-01", DateTimeFormat.getDateTimeFormat(), dateFormatRegexp);
assertNull(date);

date = DateTimeFormat.parse("2005-01-01", dateFormat, DateTimeFormat.getDateTimeFormatRegexp());
assertNull(date);

date = DateTimeFormat.parse("2005-01-01 12:12", DateTimeFormat.getDateTimeFormat(), DateTimeFormat.getDateTimeFormatRegexp());
assertNotNull(date);
}

public void testParseDate() throws Exception {
Date date = DateTimeFormat.parseDate("2005-01-01");
assertNotNull(date);

date = DateTimeFormat.parseDate("2005-01-aa");
assertNull(date);

date = DateTimeFormat.parseDate("2005/01/01");
assertNull(date);

date = DateTimeFormat.parseDate("2005-01-01 12:12");
assertNull(date);

date = DateTimeFormat.parseDate("2005-01-01 12:12");
assertNull(date);

date = DateTimeFormat.parseDate("2005-1-1-1");
assertNull(date);
}

public void testParseDateTime() throws Exception {
Date date = DateTimeFormat.parseDateTime("2005-01-01 11:11");
assertNotNull(date);

date = DateTimeFormat.parseDateTime("2005-11-11 1:1:1");
assertNull(date);

date = DateTimeFormat.parseDateTime("2005-11-11 9:11");
assertNull(date);

date = DateTimeFormat.parseDateTime("2005-11-11");
assertNull(date);
}

public void testValidatorParseDate() throws Exception {
Date date = DateTimeFormat.parseDate("2005-01-01");
assertNotNull(date);

date = DateTimeFormat.validatorParseDate("2005-01-aa");
assertNull(date);

date = DateTimeFormat.validatorParseDate("2005/01/01");
assertNull(date);

date = DateTimeFormat.validatorParseDate("2005-01-01 12:12");
assertNull(date);

date = DateTimeFormat.validatorParseDate("2005-1-1-1");
assertNull(date);

date = DateTimeFormat.validatorParseDateTime("2005-11-11 1:1:1");
assertNull(date);

date = DateTimeFormat.validatorParseDateTime("2005-11-11 11:11");
assertNotNull(date);
}

public void testValidatorParseDateTime() throws Exception {
Date date = DateTimeFormat.validatorParseDateTime("2005-01-01 11:11");
assertNotNull(date);

date = DateTimeFormat.validatorParseDateTime("2005-11-11 1:1:1");
assertNull(date);

date = DateTimeFormat.validatorParseDateTime("2005-11-11 9:11");
assertNull(date);

date = DateTimeFormat.validatorParseDateTime("2005-11-11");
assertNull(date);
}
}



And the main class

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.apache.commons.validator.GenericTypeValidator;

public class DateTimeFormat {
private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
private static final String DATE_FORMAT = "yyyy-MM-dd";
private static final String DATE_FORMAT_REGEXP = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$";
private static final String DATE_TIME_FORMAT_REGEXP = "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$";

/**
* @param dateValue
* @param format
* @param regexp
* @return parsed date if dateValue matches format and regexp, else null
*/
public static Date validatorParse(String dateValue, String format, String regexp) {
Date parsedDate = GenericTypeValidator.formatDate(dateValue, format, true);
return matchRegexp(dateValue, regexp, parsedDate);
}

/**
* @param dateValue
* @param regexp
* @param parsedDate
* @return parsedDate if dateValue matches regexp, else null
*/
private static Date matchRegexp(String dateValue, String regexp, Date parsedDate) {
if(!dateValue.matches(regexp)){
return null;
}
return parsedDate;
}

/**
* @param dateValue
* @return date parsed by SimpleDateFormat
*/
public static Date parseDate(String dateValue) {
return parse(dateValue, getDateFormat(), getDateFormatRegexp());
}

/**
* @param dateTimeValue
* @return date parsed by SimpleDateFormat
*/
public static Date parseDateTime(String dateTimeValue) {
return parse(dateTimeValue, getDateTimeFormat(), getDateTimeFormatRegexp());
}

/**
* @param dateTimeValue
* @return date parsed by GenericTypeValidator
*/
public static Date validatorParseDateTime(String dateTimeValue) {
return validatorParse(dateTimeValue, getDateTimeFormat(), getDateTimeFormatRegexp());
}

/**
* @param dateValue
* @return date parsed by GenericTypeValidator
*/
public static Date validatorParseDate(String dateValue) {
return validatorParse(dateValue, getDateFormat(), getDateFormatRegexp());
}

/**
* @param dateValue
* @param format
* @param regexp
* @return parsed date if dateValue matches format and regexp, else null
*/
public static Date parse(String dateValue, String format, String regexp) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format);
simpleDateFormat.setLenient(false);
Date parsedDate = null;
try {
parsedDate = simpleDateFormat.parse(dateValue);
} catch (ParseException e) {
return null;
}
return matchRegexp(dateValue, regexp, parsedDate);
}

public static String getDateFormat() {
return DATE_FORMAT;
}

public static String getDateTimeFormat() {
return DATE_TIME_FORMAT;
}

public static String getDateFormatRegexp() {
return DATE_FORMAT_REGEXP;
}

public static String getDateTimeFormatRegexp() {
return DATE_TIME_FORMAT_REGEXP;
}

}


Because both SimpleDateFormat and GenericTypeValidator need to be enhanced in order to work properly I'll stick with SimpleDateFormat, mainly because I don't have to use huge apache library in order to parse anything. Two things bother me. Why do I have to write my own validator, and why there is no way in java to remove this redundancy that I see in tests. I'll live it for the future investigation.

No comments: