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 parse
methods 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:
- The methods are not avaiable on types where they are not applicable (e.g. there is no
getTimeString()
method forLocalDate
). - The formatting performed by these methods is based on ISO
DateTimeFormatter
instances and is consequently not locale-specific. For example,new Date().dateString
yields5/23/18
on myen_US
runtime, butLocalDateTime.now().dateString
produces2018-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.