Play Framework 1.2 - Using relative dates in fixtures

When writing Play 1.2 applications, you often use a set of fixtures to use as inital or test data. For some applications, you want your models to have datetime fields that are in the near future, or in the near past, for example when you want to list recent news items. The problem is that you have to keep updating these fixtures on a regular basis, or all the dates will be progressively further in the past. This article shows how you can use dates relative to the current date in your fixtures.

Objective

We use the Joda-Time DateTime class to store datetimes. We want to be able to specify datetimes in yaml files in a format like day + 12 hours meaning noon today, \{\{ hour - 30 minutes }} meaning half an hour before the last whole hour or now + 2 hours 30 minutes meaning two hours and thirty minutes from now. When we do this, the fixtures are not really fixed anymore so maybe they should be renamed to vartures or notsofixtures, but we’ll stick with fixtures for now.

Approach

The Play Framework supports custom Binders, and we will use such a custom binder to parse a String that contains a textual relative time into a DateTime. A binder for a DateTime must implement TypeBinder<DateTime>:

@Global
public class DateTimeBinder implements TypeBinder<DateTime> {
    public Object bind(String name, Annotation[] annotations, String value,
            Class actualClass) throws Exception {
        // Try to parse 'value' into a DateTime here
    }
}

To parse our String format into a DateTime, we use a regular expression:

Pattern pattern = Pattern.compile("(year|month|week|day|hour|minute|second|now)\\s?(\\+|-)\\s?(.*)");
    Matcher matcher = pattern.matcher(value);
    if (!matcher.matches()) {
    return null;
    }

This matcher has three groups, the first contains the base, the second a plus or minus sign and the third the offset. We will provide a method to parse the base string into a DateTime. Joda-Time has a method to parse the offset into a Period and depending on the sign we add or subtract that Period from the base to get our final DateTime.

The method to parse the base into a DateTime is as follows:

private static DateTime getStartDateTime(String timeBase) {
    DateTime now = new DateTime();

    if ("now".equals(timeBase)) {
    return now;
    } else if ("year".equals(timeBase)) {
    return new DateTime(now.getYear(), 1, 1, 0, 0, 0, 0);
    } else if ("month".equals(timeBase)) {
    return new DateTime(now.getYear(), now.getMonthOfYear(), 1, 0, 0, 0, 0);
    } else if ("week".equals(timeBase)) {
    return new DateTime(now.getYear(), now.getMonthOfYear(), now.getDayOfMonth(), 0, 0, 0, 0).withDayOfWeek(DateTimeConstants.MONDAY);
    } else if ("day".equals(timeBase)) {
    return new DateTime(now.getYear(), now.getMonthOfYear(), now.getDayOfMonth(), 0, 0, 0, 0);
    } else if ("hour".equals(timeBase)) {
    return new DateTime(now.getYear(), now.getMonthOfYear(), now.getDayOfMonth(), now.getHourOfDay(), 0, 0, 0);
    } else if ("minute".equals(timeBase)) {
    return new DateTime(now.getYear(), now.getMonthOfYear(), now.getDayOfMonth(), now.getHourOfDay(), now.getMinuteOfDay(), 0, 0);
    } else if ("second".equals(timeBase)) {
    return new DateTime(now.getYear(), now.getMonthOfYear(), now.getDayOfMonth(), now.getHourOfDay(), now.getMinuteOfDay(), now.getSecondOfMinute(), 0);
    }

    throw new IllegalArgumentException("Invalid base string.");
}

And to construct the final DateTime we use the following code:

DateTime startDateTime = getStartDateTime(matcher.group(1));
DateTime result;
PeriodFormatter formatter = PeriodFormat.getDefault();
Period p = formatter.parsePeriod(matcher.group(3));

if (matcher.group(2).equals("+")) {
    result = startDateTime.plus(p);
} else {
    result = startDateTime.minus(p);
}

return result;

In addition to relative datetimes, we want our binder to also support absolute datetimes and timestamps. Unfortunately, Play does not support automatic chaining of binders, so we have to put it all in a single binder. Together, this gives us the final version of our Joda-Time DateTime binder that supports relative and absolute datetimes:

package utils.play;

import java.lang.annotation.Annotation;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.joda.time.DateTime;
import org.joda.time.DateTimeConstants;
import org.joda.time.Period;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.PeriodFormat;
import org.joda.time.format.PeriodFormatter;

import play.data.binding.Global;
import play.data.binding.TypeBinder;

@Global
public class DateTimeBinder implements TypeBinder<DateTime> {

    public Object bind(String name, Annotation[] annotations, String value,
                       Class actualClass) throws Exception {

        // Try if we're dealing with a timestamp
        try {
        Long timestamp = Long.parseLong(value);
        DateTime dt = new DateTime(timestamp);
        return dt;
    } catch (NumberFormatException e) {}

    // Try a regular date time pattern
    try {
        final DateTimeFormatter formatter = DateTimeFormat.forPattern("YYYY-MM-dd HH:mm");
        DateTime dt = formatter.parseDateTime(value);
        return dt;
    } catch(IllegalArgumentException e){}

    // Try a relative pattern
    Pattern pattern = Pattern.compile("(year|month|week|day|hour|minute|second|now)\\s?(\\+|-)\\s?(.*)");
    Matcher matcher = pattern.matcher(value);
    if (!matcher.matches()) {
        return null;
    }

    DateTime startDateTime = getStartDateTime(matcher.group(1));
    DateTime result;
    PeriodFormatter formatter = PeriodFormat.getDefault();
    Period p = formatter.parsePeriod(matcher.group(3));

    if (matcher.group(2).equals("+")) {
        result = startDateTime.plus(p);
    } else {
        result = startDateTime.minus(p);
    }

    return result;
    }

    private static DateTime getStartDateTime(String timeBase) {
    DateTime now = new DateTime();

    if ("now".equals(timeBase)) {
        return now;
    } else if ("year".equals(timeBase)) {
        return new DateTime(now.getYear(), 1, 1, 0, 0, 0, 0);
    } else if ("month".equals(timeBase)) {
        return new DateTime(now.getYear(), now.getMonthOfYear(), 1, 0, 0, 0, 0);
    } else if ("week".equals(timeBase)) {
        return new DateTime(now.getYear(), now.getMonthOfYear(), now.getDayOfMonth(), 0, 0, 0, 0).withDayOfWeek(DateTimeConstants.MONDAY);
    } else if ("day".equals(timeBase)) {
        return new DateTime(now.getYear(), now.getMonthOfYear(), now.getDayOfMonth(), 0, 0, 0, 0);
    } else if ("hour".equals(timeBase)) {
        return new DateTime(now.getYear(), now.getMonthOfYear(), now.getDayOfMonth(), now.getHourOfDay(), 0, 0, 0);
    } else if ("minute".equals(timeBase)) {
        return new DateTime(now.getYear(), now.getMonthOfYear(), now.getDayOfMonth(), now.getHourOfDay(), now.getMinuteOfDay(), 0, 0);
    } else if ("second".equals(timeBase)) {
        return new DateTime(now.getYear(), now.getMonthOfYear(), now.getDayOfMonth(), now.getHourOfDay(), now.getMinuteOfDay(), now.getSecondOfMinute(), 0);
    }

    throw new IllegalArgumentException("Invalid base string.");
    }
}

Conclusion

By creating a custom binder you can accept dates relative to the current date in your fixtures, that might help you during development of your Play application.