Groovy Additions to the Java 8 Date/Time API

After suffering through java.util.Date and Calendar for many years, Java 8's Date/Time API was a welcome and groovy addition to Java SE in and of itself. Groovy's upcoming 2.5 and 3.0 releases, however, make it even groovier.

To follow along with the examples, you will need to download either Groovy 2.5-rc1 or Groovy 3.0-alpha-2 (or later releases in those 2.5 or 3.0 lines). Date/Time API-related features are not available in Groovy 2.6 as that version is intended to be a Groovy 3.0 backport for Java 7, which predates the Date/Time API.

Creating Date/Time Instances

Groovy's great operability with Java means that the on and now factory methods of the various java.timer types can certainly be used to create instances, but there are now some Groovy-specific alternatives.

Conversion from Legacy Types

Creating a new java.util.Date in Groovy is highly convenient as the java.util package is always imported; a developer simply needs to write new Date(). To encourage use of the Java 8 Date/Time types while accomodating this habit, Groovy provides a number of extension methods on java.util.Date and java.util.Calendar that convert these legacy instances to their modern equivalents.

def date = new Date().toLocalDate()
def time = new Date().toLocalTime()
def datetime = new Date().toLocalDateTime()

def offsetDateTime = new Date().toOffsetDateTime()
def offsetTime = new Date().toOffsetTime()
def zonedDateTime = new Date().toZonedDateTime()

def monthDay = new Date().toMonthDay()
def yearMonth = new Date().toYearMonth()
def year = new Date().toYear()


Creating Java 8 Date/Time instances through this approach may be convenient (if sticking with def, you may not even need to explicitly import java.time.*), but it does sacrifice some precision, as legacy types do not support nanoseconds.

It's also worth noting that the Java 8 Date/Time data types have toDate() and toCalendar() methods for converting to their java.util equivalents... but you would never deliberately want to use them, right?

Left-Shift Composition

Groovy overrides the << operator to allow the creation of broader date/time type instances through composition of narrower types. For example, you can create a LocalDateTime instance from a LocalDate and a LocalTime, or create an OffsetDateTime from a LocalDateTime and a ZoneOffset.

def year = Year.of(2018)
def yearMonth = Month.MAY << year

def localDate = yearMonth << 21
def localDateTime = LocalTime.now() << localDate

def offsetDateTime1 = localDateTime << ZoneOffset.ofHours(-5)
def offsetDateTime2 = localDate << OffsetTime.now()

def zonedDateTime = localDateTime << ZoneId.systemDefault()


The order of the operands does not matter.

Parsing Strings

One of the more common ways of generating date/time instances is by parsing a String. Groovy adds convenience parsemethods to the Java 8 Date/Time types that accept two String arguments: the value to parse and the format the of the value. These new methods spare you from explicitly creating a DateTimeFormatter instance.

def someDate = LocalDate.parse('2012/11/23', 'yyyy/MM/dd')


Veteran Groovy developers take note: the ordering of the String arguments on the venerable Date.parse() method is format-to-use first, value-to-parse second. The new parse methods have the arguments reversed for consistency with the Java 8 Date/Time API's parse methods, which take a DateTimeFormatter in the second argument.

Durations and Periods

The right shift operator >> is overloaded in Groovy for creating Period or Duration instances from Temporal types. The operator can be read as "through," as in "May 1st, 2018 through May 9th, 2018."

Period p = LocalDate.of(2018, Month.MAY, 1) >> LocalDate.of(2018, Month.MAY, 9)
assert p.days == 8

Duration d = LocalTime.of(0, 0, 0) >> LocalTime.of(0, 0, 59)
assert d.seconds == 59


Manipulating Values

Now that we have created date/time type instances, the next thing we may want to do with them is manipulate their values. Unlike java.util.Date and Calendar, date/time types are immutable. So even though it may look as if the instances' values are being changed, new instances are being created behind the scenes which bear the changed values.

Arithmetic Operations

The + and - operators can be used to add/subtract TemporalAmount (i.e. Duration and Period) or integer values. For integers, the default unit depends on the data type being manipulated:

Data Type Unit
LocalDate ChronoUnit.DAYS
YearMonth ChronoUnit.MONTHS
Year ChronoUnit.YEARS
all others ChronoUnit.SECONDS


As an example:

// Legacy API way
use (groovy.time.TimeCategory) {
    def twoDaysFromToday = new Date() + 2 // days by default, even without TimeCategory
    def monthAndTwoDaysFromToday = twoDaysFromToday + 1.month
    def threeSecondsAgo = new Date() - 3.seconds
}

// Date/Time API way
def twoDaysFromToday = LocalDate.now() + 2

def monthAndTwoDaysFromToday = twoDaysFromToday + Period.ofMonths(1)

def threeSecondsAgo = LocalDateTime.now() - 3


Period supports +, -, and * for multiplication. Duration supports +, -, *, and division with /.

def period = Period.ofMonths(2) * 2 
assert period.months == 4

def duration = Duration.ofSeconds(10) / 5
assert duration.seconds == 2


Incrementing and Decrementing

Given that the + and - operators work for the date/time types, it's no surprise that ++ and -- work as well.

Furthermore, because these operators exist and because the various date/time types implement Comparable, date/time types can be used within ranges. Consider the following code that counts the number of Mondays in 2018.

def firstDay2018 = LocalDate.of(2018, Month.JANUARY, 1)
def lastDay2018 = firstDay2018 + Period.ofYears(1) - 1  // add a year, subtract one day

def numberOfMondaysIn2018 = (firstDay2018..lastDay2018).collect { date -> date.dayOfWeek }
                                                       .findAll { dayOfWeek -> dayOfWeek == DayOfWeek.MONDAY }
                                                       .size()


Formatting Dates

After creating and manipulating date/time instances, the next natural thing to do is format them as Strings. Groovy provides some additional features for this purpose.

Overridden Format Methods

As with parse, Groovy adds another overridden format(String pattern) method that spares developers from having to explicitly construct a DateTimeFormatter instance.

// Legacy API way
new Date().format('MM/dd/yyyy')

// Date/Time API way
LocalDate.now().format('MM/dd/yyyy')


Alternatively, instead of passing a pattern as a String, you can call format(FormatStyle style), which is Locale-specific.

import java.time.format.FormatStyle

assert LocalTime.of(2,30).format(FormatStyle.SHORT) == '2:30 AM'


dateString, timeString, and dateTimeString

Groovy has added getDateString(), getTimeString(), and getDateTimeString() methods to the new date/time type akin to those long available for java.util.Date but with two important distinctions:

  1. The methods are not avaiable on types where they are not applicable (e.g. there is no getTimeString() method for LocalDate).
  2. The formatting performed by these methods is based on ISO DateTimeFormatter instances and is consequently not locale-specific. For example, new Date().dateString yields 5/23/18 on my en_US runtime, but LocalDateTime.now().dateString produces 2018-05-23.

Conclusion

Groovy now has extension methods and overloaded operators for the Java 8 Date/Time API types, similar to what is available for java.util.Date. There are even more features than what I've discussed here, so download a copy of 2.5 or 3.0 and check out the groovydocs and documentation.

 

 

 

 

Top