The groovy-datetime module supports numerous extensions for working with the Date/Time API introduced in Java 8. This documentation refers to the data types defined by this API as "JSR 310 types."

1. Formatting and parsing

A common use case when working with date/time types is to convert them to Strings (formatting) and from Strings (parsing). Groovy provides these additional formatting methods:

Method Description Example

getDateString()

For LocalDate and LocalDateTime, formats with DateTimeFormatter.ISO_LOCAL_DATE

2018-03-10

For OffsetDateTime, formats with DateTimeFormatter.ISO_OFFSET_DATE

2018-03-10+04:00

For ZonedDateTime, formats with DateTimeFormatter.ISO_LOCAL_DATE and appends the ZoneId short name

2018-03-10EST

getDateTimeString()

For LocalDateTime, formats with DateTimeFormatter.ISO_LOCAL_DATE_TIME

2018-03-10T20:30:45

For OffsetDateTime, formats with DateTimeFormatter.ISO_OFFSET_DATE_TIME

2018-03-10T20:30:45+04:00

For ZonedDateTime, formats with DateTimeFormatter.ISO_LOCAL_DATE_TIME and appends the ZoneId short name

2018-03-10T20:30:45EST

getTimeString()

For LocalTime and LocalDateTime, formats with DateTimeFormatter.ISO_LOCAL_TIME

20:30:45

For OffsetTime and OffsetDateTime, formats with DateTimeFormatter.ISO_OFFSET_TIME formatter

20:30:45+04:00

For ZonedDateTime, formats with DateTimeFormatter.ISO_LOCAL_TIME and appends the ZoneId short name

20:30:45EST

format(FormatStyle style)

For LocalTime and OffsetTime, formats with DateTimeFormatter.ofLocalizedTime(style)

4:30 AM (with style FormatStyle.SHORT, e.g.)

For LocalDate, formats with DateTimeFormatter.ofLocalizedDate(style)

Saturday, March 10, 2018 (with style FormatStyle.FULL, e.g.)

For LocalDateTime, OffsetDateTime, and ZonedDateTime formats with DateTimeFormatter.ofLocalizedDateTime(style)

Mar 10, 2019 4:30:45 AM (with style FormatStyle.MEDIUM, e.g.)

format(String pattern)

Formats with DateTimeFormatter.ofPattern(pattern)

03/10/2018 (with pattern ’MM/dd/yyyy', e.g.)

For parsing, Groovy adds a static parse method to many of the JSR 310 types. The method takes two arguments: the value to be formatted and the pattern to use. The pattern is defined by the java.time.format.DateTimeFormatter API. As an example:

def date = LocalDate.parse('Jun 3, 04', 'MMM d, yy')
assert date == LocalDate.of(2004, Month.JUNE, 3)

def time = LocalTime.parse('4:45', 'H:mm')
assert time == LocalTime.of(4, 45, 0)

def offsetTime = OffsetTime.parse('09:47:51-1234', 'HH:mm:ssZ')
assert offsetTime == OffsetTime.of(9, 47, 51, 0, ZoneOffset.ofHoursMinutes(-12, -34))

def dateTime = ZonedDateTime.parse('2017/07/11 9:47PM Pacific Standard Time', 'yyyy/MM/dd h:mma zzzz')
assert dateTime == ZonedDateTime.of(
        LocalDate.of(2017, 7, 11),
        LocalTime.of(21, 47, 0),
        ZoneId.of('America/Los_Angeles')
)

Note that these parse methods have a different argument ordering than the static parse method Groovy added to java.util.Date. This was done to be consistent with the existing parse methods of the Date/Time API.

2. Manipulating date/time

2.1. Addition and subtraction

Temporal types have plus and minus methods for adding or subtracting a provided java.time.temporal.TemporalAmount argument. Because Groovy maps the + and - operators to single-argument methods of these names, a more natural expression syntax can be used to add and subtract.

def aprilFools = LocalDate.of(2018, Month.APRIL, 1)

def nextAprilFools = aprilFools + Period.ofDays(365) // add 365 days
assert nextAprilFools.year == 2019

def idesOfMarch = aprilFools - Period.ofDays(17) // subtract 17 days
assert idesOfMarch.dayOfMonth == 15
assert idesOfMarch.month == Month.MARCH

Groovy provides additional plus and minus methods that accept an integer argument, enabling the above to be rewritten more succinctly:

def nextAprilFools = aprilFools + 365 // add 365 days
def idesOfMarch = aprilFools - 17 // subtract 17 days

The unit of these integers depends on the JSR 310 type operand. As evident above, integers used with ChronoLocalDate types like LocalDate have a unit of days. Integers used with Year and YearMonth have a unit of years and months, respectively. All other types have a unit of seconds, such as LocalTime, for instance:

def mars = LocalTime.of(12, 34, 56) // 12:34:56 pm

def thirtySecondsToMars = mars - 30 // go back 30 seconds
assert thirtySecondsToMars.second == 26

2.2. Multiplication and division

The * operator can be used to multiply Period and Duration instances by an integer value; the / operator can be used to divide Duration instances by an integer value.

def period = Period.ofMonths(1) * 2 // a 1-month period times 2
assert period.months == 2

def duration = Duration.ofSeconds(10) / 5// a 10-second duration divided by 5
assert duration.seconds == 2

2.3. Incrementing and decrementing

The ++ and -- operators can be used increment and decrement date/time values by one unit. Since the JSR 310 types are immutable, the operation will create a new instance with the incremented/decremented value and reassign it to the reference.

def year = Year.of(2000)
--year // decrement by one year
assert year.value == 1999

def offsetTime = OffsetTime.of(0, 0, 0, 0, ZoneOffset.UTC) // 00:00:00.000 UTC
offsetTime++ // increment by one second
assert offsetTime.second == 1

2.4. Negation

The Duration and Period types represent a negative or positive length of time. These can be negated with the unary - operator.

def duration = Duration.ofSeconds(-15)
def negated = -duration
assert negated.seconds == 15

3. Interacting with date/time values

3.1. Property notation

The getLong(TemporalField) method of TemporalAccessor types (e.g. LocalDate, LocalTime, ZonedDateTime, etc.) and the get(TemporalUnit) method of TemporalAmount types (namely Period and Duration), can be invoked with Groovy’s property notation. For example:

def date = LocalDate.of(2018, Month.MARCH, 12)
assert date[ChronoField.YEAR] == 2018
assert date[ChronoField.MONTH_OF_YEAR] == Month.MARCH.value
assert date[ChronoField.DAY_OF_MONTH] == 12
assert date[ChronoField.DAY_OF_WEEK] == DayOfWeek.MONDAY.value

def period = Period.ofYears(2).withMonths(4).withDays(6)
assert period[ChronoUnit.YEARS] == 2
assert period[ChronoUnit.MONTHS] == 4
assert period[ChronoUnit.DAYS] == 6

3.2. Ranges, upto, and downto

The JSR 310 types can be used with the range operator. The following example iterates between today and the LocalDate six days from now, printing out the day of the week for each iteration. As both range bounds are inclusive, this prints all seven days of the week.

def start = LocalDate.now()
def end = start + 6 // 6 days later
(start..end).each { date ->
    println date.dayOfWeek
}

The upto method will accomplish the same as the range in the above example. The upto method iterates from the earlier start value (inclusive) to the later end value (also inclusive), calling the closure with the incremented next value once per iteration.

def start = LocalDate.now()
def end = start + 6 // 6 days later
start.upto(end) { next ->
    println next.dayOfWeek
}

The downto method iterates in the opposite direction, from a later start value to an earlier end value.

The unit of iteration for upto, downto, and ranges is the same as the unit for addition and subtraction: LocalDate iterates by one day at a time, YearMonth iterates by one month, Year by one year, and everything else by one second. Both methods also support an optional a TemporalUnit argument to change the unit of iteration.

Consider the following example, where March 1st, 2018 is iterated up to March 2nd, 2018 using an iteration unit of months.

def start = LocalDate.of(2018, Month.MARCH, 1)
def end = start + 1 // 1 day later

int iterationCount = 0
start.upto(end, ChronoUnit.MONTHS) { next ->
    println next
    ++iterationCount
}

assert iterationCount == 1

Since the start date is inclusive, the closure is called with a next date value of March 1st. The upto method then increments the date by one month, yielding the date, April 1st. Because this date is after the specified end date of March 2nd, the iteration stops immediately, having only called the closure once. This behavior is the same for the downto method except that the iteration will stop as soon as the value of next becomes earlier than the targeted end date.

In short, when iterating with the upto or downto methods with a custom unit of iteration, the current value of iteration will never exceed the end value.

3.3. Combining date/time values

The left-shift operator (<<) can be used to combine two JSR 310 types into an aggregate type. For example, a LocalDate can be left-shifted into a LocalTime to produce a composite LocalDateTime instance.

MonthDay monthDay = Month.JUNE << 3 // June 3rd
LocalDate date = monthDay << Year.of(2015) // 3-Jun-2015
LocalDateTime dateTime = date << LocalTime.NOON // 3-Jun-2015 @ 12pm
OffsetDateTime offsetDateTime = dateTime << ZoneOffset.ofHours(-5) // 3-Jun-2015 @ 12pm UTC-5

The left-shift operator is reflexive; the order of the operands does not matter.

def year = Year.of(2000)
def month = Month.DECEMBER

YearMonth a = year << month
YearMonth b = month << year
assert a == b

3.4. Creating periods and durations

The right-shift operator (>>) produces a value representing the period or duration between the operands. For ChronoLocalDate, YearMonth, and Year, the operator yields a Period instance:

def newYears = LocalDate.of(2018, Month.JANUARY, 1)
def aprilFools = LocalDate.of(2018, Month.APRIL, 1)

def period = newYears >> aprilFools
assert period instanceof Period
assert period.months == 3

The operator produces a Duration for the time-aware JSR types:

def duration = LocalTime.NOON >> (LocalTime.NOON + 30)
assert duration instanceof Duration
assert duration.seconds == 30

If the value on the left-hand side of the operator is earlier than the value on the right-hand side, the result is positive. If the left-hand side is later than the right-hand side, the result is negative:

def decade = Year.of(2010) >> Year.of(2000)
assert decade.years == -10

4. Converting between legacy and JSR 310 types

Despite the shortcomings of Date, Calendar, and TimeZone types in the java.util package they are farily common in Java APIs (at least in those prior to Java 8). To accommodate use of such APIs, Groovy provides methods for converting between the JSR 310 types and legacy types.

Most JSR types have been fitted with toDate() and toCalendar() methods for converting to relatively equivalent java.util.Date and java.util.Calendar values. Both ZoneId and ZoneOffset have been given a toTimeZone() method for converting to java.util.TimeZone.

// LocalDate to java.util.Date
def valentines = LocalDate.of(2018, Month.FEBRUARY, 14)
assert valentines.toDate().format('MMMM dd, yyyy') == 'February 14, 2018'

// LocalTime to java.util.Date
def noon = LocalTime.of(12, 0, 0)
assert noon.toDate().format('HH:mm:ss') == '12:00:00'

// ZoneId to java.util.TimeZone
def newYork = ZoneId.of('America/New_York')
assert newYork.toTimeZone() == TimeZone.getTimeZone('America/New_York')

// ZonedDateTime to java.util.Calendar
def valAtNoonInNY = ZonedDateTime.of(valentines, noon, newYork)
assert valAtNoonInNY.toCalendar().getTimeZone().toZoneId() == newYork

Note that when converting to a legacy type:

  • Nanosecond values are truncated to milliseconds. A LocalTime, for example, with a ChronoUnit.NANOS value of 999,999,999 nanoseconds translates to 999 milliseconds.

  • When converting the "local" types (LocalDate, LocalTime, and LocalDateTime), the time zone of the returned Date or Calendar will be the system default.

  • When converting a time-only type (LocalTime or OffsetTime), the year/month/day of the Date or Calendar is set to the current date.

  • When converting a date-only type (LocalDate), the time value of the Date or Calendar will be cleared, i.e. 00:00:00.000.

  • When converting an OffsetDateTime to a Calendar, only the hours and minutes of the ZoneOffset convey into the corresponding TimeZone. Fortunately, Zone Offsets with non-zero seconds are rare.

Groovy has added a number of methods to Date and Calendar for converting into the various JSR 310 types:

Date legacy = Date.parse('yyyy-MM-dd HH:mm:ss.SSS', '2010-04-03 10:30:58.999')

assert legacy.toLocalDate() == LocalDate.of(2010, 4, 3)
assert legacy.toLocalTime() == LocalTime.of(10, 30, 58, 999_000_000) // 999M ns = 999ms
assert legacy.toOffsetTime().hour == 10
assert legacy.toYear() == Year.of(2010)
assert legacy.toMonth() == Month.APRIL
assert legacy.toDayOfWeek() == DayOfWeek.SATURDAY
assert legacy.toMonthDay() == MonthDay.of(Month.APRIL, 3)
assert legacy.toYearMonth() == YearMonth.of(2010, Month.APRIL)
assert legacy.toLocalDateTime().year == 2010
assert legacy.toOffsetDateTime().dayOfMonth == 3
assert legacy.toZonedDateTime().zone == ZoneId.systemDefault()