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.