Problem

One of the projects required a command line parameter for a date.

The date could either be a simple fixed date in dd.MM.yyyy format. It could also be an expression containing a formula that had the difference in days from today, for example: $TODAY – 1 for yesterday, or $TODAY + 1 for tomorrow.

The evaluated date be rendered in a particular String format, for example yyyyMMdd.

So we decided to have an expression like the following…

$DATE(date, ‘format’)

Possible values for the command line parameter would then be:

from=$DATE(12.2.2008,’yyyyMMdd’)
from=$DATE($TODAY+1,’yyyyMMdd’)
from=$DATE($TODAY-4,’yyyyMMdd’)

Further examples: double quotes are needed if spaces are included

from=”$DATE($TODAY – 1, ‘dd-M-yyyy’)”
from=”$DATE(12.2.2009, ‘dd-M-yy’)”
from=”$DATE(2.3.2009, ‘dd (M) yy’)”
from=”$DATE($Today + 3, ‘yyyyMMdd’)”
from=$DATE($today-10,’yyyyMMdd’)

Solution

It seemed like something was this was already there somewhere, but could not find anything that came close on google. This can be done with a laborious string parsing algorithm, but its better to have a more elegant, flexible solution than that. This is generally the kind of case where a regular expression would come to the rescue! Googling can come up with a lot of useful resources for learning about regular expressions, and this Eclipse plugin for Regular Expression testing was really useful for testing what one learns. So after some trial and error, the following evolved…

// Actual expression is
// \$DATE\s*\(\s*((\$TODAY\s*(?:([-|\+])\s*(\d+))?)|((0?[1-9]|[12][0-9]|3[01]).(0?[1-9]|1[012]).(19|20\d\d)))\s*,\s*\'((.+))\'\s*\
// The double \\ below is for the Java string escape character
private final String DateExpr =
    "\\$DATE\\s*\\(\\s*((\\$TODAY\\s*(?:([-|\\+])\\s*(\\d+))?)|((0?[1-9]|[12][0-9]|3[01]).(0?[1-9]|1[012]).(19|20\\d\\d)))\\s*,\\s*\\'((.+))\\'\\s*\\)";

Parts of the expression explained:

  • \$ shows that $ is not to be treated as a special character, but part of the string ($Date)
  • \s is for space
  • * is for zero or more occurances, which allows for optional spaces in between.
  • () encloses a group. Groups can be ‘captured’. A captured group is an evaluated sub-part, which we will see shortly. A group that we’re not interested in capturing – a non-capturing group – is indicated with a ?: after the opening brace – (?: )
  • [0-9] is a digit that’s within the range, in general [ ] encloses a character class.
  • ? indicates one possible occurence
  • | is OR – so [-|\+] means – or + (here again \+ indicates that + is only to be interpreted as part of the string)

The Pattern class in the JDK API details all possibilities.

Now that we have the expression, we can have a pre-compiled version of it:

	private static final Pattern pattern
	     = Pattern.compile(DateExpr, Pattern.CASE_INSENSITIVE);

We can also store the number of expected groups, just for validation, though maybe this wouldn’t really be necessary and can be skipped, its just that I’m being paranoid here.

	private static final int DateExprGroupCount = 10;

Finally, here’s the function that takes the expression, and returns the evaluated string. MyException below is just a custom exception that does the logging as well, and this can be replaced with any application defined exception.

Its a lot simpler than what it looks like, just that there’s some validation and a lot of logging that can be removed once you know that its working. Also, log.debug can be replaced with printlns if log4j isn’t being used.

public static String evalExpr(String expr) {

	String value = expr;

	// currently only date expression is supported
	if (!expr.toUpperCase().startsWith("$DATE"))
		return expr;

	Matcher matcher = pattern.matcher(expr);

	if (matcher.find()) {
		log.debug("Could find a match for expression: " + matcher.group());
		log.debug("Group count: " + matcher.groupCount());

		if (matcher.groupCount() != DateExprGroupCount)
			throw new MyException(
			     "Date Parameter Parsing error - group count does not match expected "
			     + DateExprGroupCount, log);

		// Looping through just for debugging and logging purpose
		for (int i = 0; i < matcher.groupCount(); i++)
			log.debug("RegEx group " + i + ": [" + matcher.group(i) + "]");
	}
	else {
		throw new MyException(
		"Date Parameter Parsing error - does not match expected pattern", log);
	}

	//	all is fine so far, extract the following values:
	//	Date Value, and Date Format

	Date day = null;

	DateFormat outFormat = new SimpleDateFormat(matcher.group(9));

	if (matcher.group(1).toUpperCase().startsWith("$TODAY")) {

		Calendar date = Calendar.getInstance();

		date.set(Calendar.HOUR_OF_DAY, 0);
		date.set(Calendar.MINUTE, 0);
		date.set(Calendar.SECOND, 0);

		//	Operator is + or -
		String opr = matcher.group(3);

		if (opr != null) {
			String incStr = matcher.group(4);

			int inc;
			try {
				inc = Integer.parseInt(incStr);
			} catch (Exception e) {
				throw new MyException("Reading Filter Parameter", e, log);
			}

			if (opr.equals("-"))
				inc = -inc;

			date.add(Calendar.DAY_OF_MONTH, inc);
		}

		day = date.getTime();
	}
	else {  //	expected numerical date

		DateFormat inFormat = new SimpleDateFormat("dd.MM.yyyy");

		try {
			day = inFormat.parse(matcher.group(1).trim());
		} catch (ParseException e) {
			throw new MyException("Reading Filter Parameter", e, log);
		}
	}

	value = outFormat.format(day);

	log.debug("Resultant date string: " + value);

	return value;
}

To take it out for a spin, try out the following…

public static void main(String[] args) {
	System.out.println(evalExpr("$DATE($TODAY - 394, 'yyyyMMdd-HHmmss')"));
	System.out.println(evalExpr("$DATE(1.2.2009, 'yyyyMMdd')"));
}

If we don’t need the date formatting but just something like $TODAY + 1, it gets much simpler:

private final String TodayExpr =
    "\\$TODAY\\s*(?:([-|\\+])\\s*((\\d+)))?";
private final Pattern pattern
	= Pattern.compile(TodayExpr, Pattern.CASE_INSENSITIVE);

And the evaluation function can return a Date object:

public Date evalExpr(String expr) {

	Date dateVal = null;

	// currently only date expression is supported
	if (!expr.toUpperCase().startsWith("$TODAY")) {
		try {
			dateVal = DateFormat.parse(expr);
		} catch (ParseException e) {
			throw new RuntimeException("Reading Parameter" + e);
		}

		return dateVal;
	}

	Matcher matcher = pattern.matcher(expr);

	if (matcher.find()) {
		log.debug("Could find a match for date expression: " + matcher.group());
		log.debug("Group count: " + matcher.groupCount());

		if (matcher.groupCount() != 3)
			throw new RuntimeException(
			"Date Parameter Parsing error - group count does not match.");

		// Looping through just for debugging and logging purpose
		for (int i = 0; i < matcher.groupCount(); i++)
			log.debug("RegEx group " + i + ": [" + matcher.group(i) + "]");
	}
	else {
		throw new RuntimeException(
		"Date Parameter Parsing error - does not match expected pattern");
	}

	//	all is fine so far, extract the following values:
	//	Date Value, and Date Format

	Calendar date = Calendar.getInstance();

	date.set(Calendar.HOUR_OF_DAY, 0);
	date.set(Calendar.MINUTE, 0);
	date.set(Calendar.SECOND, 0);

	//	Operator is + or -
	String opr = matcher.group(1);

	if (opr != null) {
		String incStr = matcher.group(2);

		int inc = 0;
		try {
			inc = Integer.parseInt(incStr);
		} catch (Exception e) {
			throw new RuntimeException("Reading Increment Parameter" + e);
		}

		if (opr.equals("-"))
			inc = -inc;

		date.add(Calendar.DAY_OF_MONTH, inc);
	}

	dateVal = date.getTime();

	log.debug("Resultant date string: " + dateVal);

	return dateVal;
}

These were written seperately, though its quite possible that these two can be refactored to work together if required.