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 |
---|---|---|
|
For |
|
For |
|
|
For |
|
|
|
For |
|
For |
|
|
For |
|
|
|
For |
|
For |
|
|
For |
|
|
|
For |
|
For |
|
|
For |
|
|
|
Formats with
|
|
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
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 aChronoUnit.NANOS
value of 999,999,999 nanoseconds translates to 999 milliseconds. -
When converting the "local" types (
LocalDate
,LocalTime
, andLocalDateTime
), the time zone of the returnedDate
orCalendar
will be the system default. -
When converting a time-only type (
LocalTime
orOffsetTime
), the year/month/day of theDate
orCalendar
is set to the current date. -
When converting a date-only type (
LocalDate
), the time value of theDate
orCalendar
will be cleared, i.e.00:00:00.000
. -
When converting an
OffsetDateTime
to aCalendar
, only the hours and minutes of theZoneOffset
convey into the correspondingTimeZone
. 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()