Let me be blunt: If you’re still using java.util.Calendar et al. in your Java code you just failed the code review. JodaTime is a compelling replacement for the original JDK date/time classes in pretty much every respect.
The following code fragment illustrates basic usage of a few JodaTime classes: DateTime, DateTimeFormatter, and DateTimeZone. The toUtcDate() method converts a ISO 8601 formatted DateTime to its UTC equivalent. DateTimeFormatters are used to both read and write DateTimes.
private static final DateTimeZone ZONE_UTC = DateTimeZone.UTC;
private static final DateTimeFormatter ISO_PARSE_FORMAT =
ISODateTimeFormat.dateOptionalTimeParser().withOffsetParsed();
private static final DateTimeFormatter ISO_PRINT_FORMAT =
ISODateTimeFormat.dateTimeNoMillis();
/**
* Generate the UTC version of an ISO 8601 datetime.
*
* @param iso8601
* @return ISO 8601 datetime
*/
public static String toUtcDate(final String iso8601) {
DateTime dt = ISO_PARSE_FORMAT.parseDateTime(iso8601);
DateTime utcDt = dt.withZone(ZONE_UTC);
return utcDt.toString(ISO_PRINT_FORMAT);
}
We’re going to delve a little more deeply into JodaTime by implementing a parser for (something very close to) Solr Date Format. The Solr search engine uses this language to perform simple arithmetic on timestamps. The interpretation is straightforward: add or subtract various integral time units from a base time (which can be “now”). A few examples:
2011-10-31T12:34:56-5years+3months now-90days 2011-02-14T23:23:23+9months/day
The only subtlety is the “/” operator which rounds to the nearest time unit. The last expression above represents 9 months after Valentine’s day rounded to the nearest day boundary.
Here’s the code in its entirety with commentary to follow:
package synaptic;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.joda.time.DateMidnight;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
/**
* Illustrate JodaTime usage by parsing Solr-ish date math expressions.
*/
public class Yoda {
private static final DateTimeZone ZONE_UTC = DateTimeZone.UTC;
private static final DateTimeFormatter ISO_PARSE_FORMAT =
ISODateTimeFormat.dateOptionalTimeParser().withOffsetParsed();
private static final DateTimeFormatter ISO_PRINT_FORMAT =
ISODateTimeFormat.dateTimeNoMillis();
/**
* Generate the GMT version of an ISO 8601 datetime.
*
* @param iso8601
* @return ISO 8601 datetime
*/
public static String toUtcDate(final String iso8601) {
DateTime dt = ISO_PARSE_FORMAT.parseDateTime(iso8601);
DateTime utcDt = dt.withZone(ZONE_UTC);
return utcDt.toString(ISO_PRINT_FORMAT);
}
/**
* Generate the GMT version of midnight on the given ISO 8601 datetime.
*
* @param iso8601
* @return
*/
public static String toUtcDateFloor(final String iso8601) {
DateTime dt = ISO_PARSE_FORMAT.parseDateTime(iso8601);
DateMidnight dm = dt.toDateMidnight();
DateTime utcDt = dm.toDateTime().withZone(ZONE_UTC);
return utcDt.toString(ISO_PRINT_FORMAT);
}
private static final String REGEX_ISO8601 =
"\\d{4}(-\\d{2}-\\d{2}(T(\\d{2}(:\\d{2}(:\\d{2}(\\.\\d{3})?)?)?)?(([+-]\\d{2}(:?\\d{2})?)|Z)?)?)?";
private static final String REGEX_ISO8601_PREFIX = "(" + REGEX_ISO8601 + ").*";
private static final String REGEX_OPARG =
"([+-/])(\\p{Digit}*)(years?|months?|weeks?|days?|hours?|minutes?|seconds?)";
private static final Pattern PATTERN_ISO8601_PREFIX =
Pattern.compile(REGEX_ISO8601_PREFIX);
private static final Pattern PATTERN_OPARG =
Pattern.compile(REGEX_OPARG);
/**
* Extract the ISO 8601 formatted datetime prefix from a string.
*
* @param arg
* @return ISO 8601 formatted datetime or null if none
*/
public static String datePrefix(final String arg) {
Matcher matcher = PATTERN_ISO8601_PREFIX.matcher(arg);
return matcher.matches() ? matcher.group(1) : null;
}
/**
* Compute the GMT ISO 8601 datetime corresponding to the given solr-ish datemath
* expression.
*
* @param arg
* @return ISO 8601 formatted datetime or null if bad argument
*/
public static String evalDateMath(final String arg) {
String text = arg;
DateTime dt = null;
if (text.startsWith("now") || text.startsWith("NOW")) {
text = text.substring("now".length());
dt = new DateTime();
}
else {
String prefix = datePrefix(text);
if (prefix == null) {
return null;
}
text = text.substring(prefix.length());
dt = ISO_PARSE_FORMAT.parseDateTime(prefix);
}
text = text.toLowerCase();
if (!text.isEmpty()) {
Matcher matcher = PATTERN_OPARG.matcher(text);
while (matcher.find()) {
String op = matcher.group(1);
int num = matcher.group(2).isEmpty() ? 1
: Integer.parseInt(matcher.group(2));
String unit = matcher.group(3);
if ("/".equals(op)) {
if (unit.startsWith("year")) {
dt = dt.year().roundHalfFloorCopy();
}
else if (unit.startsWith("month")) {
dt = dt.monthOfYear().roundHalfFloorCopy();
}
else if (unit.startsWith("week")) {
dt = dt.weekOfWeekyear().roundHalfFloorCopy();
}
else if (unit.startsWith("day")) {
dt = dt.dayOfMonth().roundHalfFloorCopy();
}
else if (unit.startsWith("hour")) {
dt = dt.hourOfDay().roundHalfFloorCopy();
}
else if (unit.startsWith("minute")) {
dt = dt.minuteOfHour().roundHalfFloorCopy();
}
else if (unit.startsWith("second")) {
dt = dt.secondOfMinute().roundHalfFloorCopy();
}
}
else {
if ("-".equals(op)) {
num = -num;
}
if (unit.startsWith("year")) {
dt = dt.plusYears(num);
}
else if (unit.startsWith("month")) {
dt = dt.plusMonths(num);
}
else if (unit.startsWith("week")) {
dt = dt.plusWeeks(num);
}
else if (unit.startsWith("day")) {
dt = dt.plusDays(num);
}
else if (unit.startsWith("hour")) {
dt = dt.plusHours(num);
}
else if (unit.startsWith("minute")) {
dt = dt.plusMinutes(num);
}
else if (unit.startsWith("second")) {
dt = dt.plusSeconds(num);
}
}
}
}
DateTime utcDt = dt.withZone(ZONE_UTC);
return utcDt.toString(ISO_PRINT_FORMAT);
}
/**
* @param args
*/
public static void main(String[] args) {
System.out.println(toUtcDate("2001"));
System.out.println(toUtcDate("2001-04-01T12:34:56"));
System.out.println(toUtcDate("2001-04-01T12:34:56-07"));
System.out.println();
System.out.println(datePrefix("2001-3years+5months"));
System.out.println(datePrefix("2001-04-01T12:34:56-0730-3years+5months"));
System.out.println(datePrefix("2001-04-01T12:34:56-07:30-3years-1month-2weeks"));
System.out.println();
System.out.println(evalDateMath("now"));
System.out.println(evalDateMath("now-2months"));
System.out.println(evalDateMath("2001-07-04/day"));
System.out.println(evalDateMath("2001-07-04T00:00:00+5years+21days"));
System.out.println(evalDateMath("2001-04-01T12:34:56-0730-3years+5months+90seconds"));
System.out.println(evalDateMath("2001-04-01T12:34:56-07:30-3years-1month-2weeks/day"));
System.out.println(evalDateMath("2011-02-14T23:23:23+9months/day"));
System.out.println(evalDateMath("soon"));
System.out.println();
}
}
The evalDateMath() method in line 82 begins by attempting to pull the ISO 8601 datetime prefix off the argument string. Then it proceeds to add/subtract/round sequential arithmetic clauses.
Regular expressions lend themselves nicely to this problem since Solr Date Math syntax is essentially a ISO 8601 datetime followed by any number of ( operator [integer] time_unit ) tuples. The loop beginning at line 101 grabs each such tuple in sequence and updates the time accordingly. For the “/” operator the code selects the appropriate DateTime.Property from the current time base and calls the roundHalfFloorCopy() method to produce the new rounded time. For the “+” and “-” operators the code calls the appropriate plusYears(), plusMonths(), etc. method to create the updated time.
Note that DateTime is immutable (and thread-safe) so new DateTime objects are generated at each step in the date math evaluation.
Here’s the output:
2001-01-01T05:00:00Z 2001-04-01T16:34:56Z 2001-04-01T19:34:56Z 2001 2001-04-01T12:34:56-0730 2001-04-01T12:34:56-07:30 2011-09-07T02:16:00Z 2011-07-07T02:16:00Z 2001-07-04T04:00:00Z 2006-07-25T04:00:00Z 1998-09-01T20:06:26Z 1998-02-16T07:30:00Z 2011-11-15T05:00:00Z null
Hopefully this post will inspire you to investigate JodaTime more thoroughly. It’s a more intuitive, performant, documented, and powerful alternative to the JDK date/time manipulation classes.
P.S. There is one small deficiency in our implementation above. Can you find it?


