Java 8 Date/Time API enhancements for Groovy 2.4 and earlier
The Groovy JDK adds useful methods to java.util.Date
and java.util.Calendar
but as of yet does not include comparable methods for the newer Java 8 Date/Time API classes.
Goodtimes fills this gap by providing these java.time
extension methods, as well as new methods on java.util.Date
and java.time.Calendar
for converting to java.time
equivalents.
Note: As of Groovy 2.5, Goodtimes-provided methods are now part of Groovy JDK itself.
Goodtimes requires Java 8 or later.
Add the goodtimes jar to the classpath in your preferred way and you're set.
@Grab('com.github.bdkosher:goodtimes:1.2')
compile group: 'com.github.bdkosher', name: 'goodtimes', version: '1.2'
<dependency>
<groupId>com.github.bdkosher</groupId>
<artifactId>goodtimes</artifactId>
<version>1.2</version>
</dependency>
Clone the repo or download a source release and build with Gradle.
gradlew install
cp build/libs/goodtimes-1.2.jar $USER_HOME/.groovy/lib
Consult the goodtimes 1.2 Groovydocs for complete API information. See the Groovy metaprogramming documentation for details on how these methods manifest themselves at runtime.
Most extension methods are used to overload operators on the java.time
types.
Increment or decrement Instant
, LocalTime
, LocalDateTime
, OffsetTime
, OffsetDateTime
, ZoneDateTime
, and Duration
by one second. For LocalDate
, Period
, and DayOfWeek
, increment or decrement by one day. Increment or decrement by one hour for ZoneOffset
, one month for Month
, and one year for Year
.
def now = LocalTime.now()
def today = LocalDate.now()
def march = Month.MARCH
def utc = ZoneOffset.UTC
LocalTime oneSecondAgo = --now
LocalDate tomorrow = today++
Month april = ++march
ZoneOffset utcMinusOne = --utc
A long
or int
operand adds seconds directly to Instant
, LocalTime
, LocalDateTime
, OffsetTime
, OffsetDateTime
, ZoneDateTime
, and Duration
. Add or subtract days for LocalDate
, Period
, and DayOfWeek
. Add or subtract hours for ZoneOffset
. Add or subtract months for Month
. Add or subtract years for Year
.
def now = LocalDateTime.now()
def today = LocalDate.now()
def march = Month.MARCH
def utc = ZoneOffset.UTC
LocalDateTime oneMinuteAgo = now - 60
LocalDate oneWeekFromToday = today + 7
Month january = march - 2
ZoneOffset utcPlusFive = utc + 5
This operator delegates to the java.time
types' get()
or getLong()
methods, enabling retrieval of the specified TemporalField
(for Instant
, MonthDay
, YearMonth
, LocalTime
, LocalDateTime
, OffsetDateTime
, and ZonedDateTime
) or TemporalUnit
(for Period
and Duration
).
def sixtySeconds = Duration.parse('PT60S')
assert sixtySeconds[ChronoUnit.SECONDS] == 60
In addition to supporting TemporalField
arguments, the LocalDate
, LocalTime
, LocalDateTime
, OffsetTime
, OffsetDateTime
, and ZonedDateTime
classes can accept java.util.Calendar
constants:
def lastChristmas = LocalDate.of(2016, 12, 25)
assert lastChristmas[Calendar.YEAR] == 2016
assert lastChristmas[Calendar.MONTH] == Calendar.DECEMBER
assert lastChristmas[Calendar.DATE] == 25
Left shifting can be used to merge two different java.time
types into a larger aggregate type. For example, left-shifting a LocalTime
into a LocalDate
results in a LocalDateTime
. Left shifting is reflexive so that A << B
yields the same result as B << A
.
def thisYear = Year.of(2017)
def noon = LocalTime.of(12, 0, 0)
YearMonth december2017 = thisYear << Month.DECEMBER
LocalDate christmas = december2017 << 25
OffsetTime noonInGreenwich = noon << ZoneOffset.ofHours(0)
LocalDateTime christmasAtNoon = christmas << noon
ZonedDateTime chirstmasAtNoonInNYC = christmasAtNoon << ZoneId.of('America/New_York')
OffsetDateTime chirstmasAtNoonInGreenwich = christmasAtNoon << ZoneOffset.UTC
The right shift operator, when read as meaning "through" or "to", is used to create a Period
from two LocalDate
, YearMonth
, or Year
instances. Similarly, the >>
operator produces a Duration
from two Instant
, LocalTime
, LocalDateTime
, OffsetTime
, OffsetDateTime
, or ZonedDateTime
instances.
def today = LocalDate.now()
def tomorrow = today + 1
Period oneDay = today >> tomorrow
Period negOneDay = tomorrow >> today
A Period
and Duration
can be multiplied by a scalar. Only a Duration
can be divided.
def week = Period.ofDays(7)
def minute = Duration.ofMinutes(1)
Period fortnight = week * 2
Duration thirtySeconds = minute / 2
A Period
, Duration
, or Year
can be made positive or negated via the +
and -
operators.
def oneWeek = Period.ofDays(7)
def oneHour = Duration.ofHours(1)
assert +oneWeek == oneWeek
assert -oneHour == Duration.ofHours(-1)
A getDay
method exists on LocalDate
, LocalDateTime
, MonthDay
, OffsetDateTime
, and ZoendDateTime
as an alias for getDayOfMonth
.
def independenceDay = LocalDate.of(2017, Month.JULY, 4)
assert independenceDay.day == 4
assert independenceDay.day == independenceDay.dayOfMonth
The ZoneOffset
has getters to obtain the hours, minutes, and seconds values of the offset.
def zoneOffset = ZoneOffset.ofHoursMinutesSeconds(5, 10, 20)
assert zoneOffset.hours == 5
assert zoneOffset.minutes == 10
assert zoneOffset.seconds == 20
The legacy Calendar
class has getters to obtain the time zone information as a ZoneId
and ZoneOffset
.
def cal = Calendar.getInstance(TimeZone.getTimeZone('GMT'))
assert cal.zoneId == ZoneId.of('GMT')
assert cal.zoneOffset == ZoneOffset.ofHours(0)
Additionally, Calendar
has getYear
, getYearMonth
, getMonth
, getMonthDay
, and getDayOfMonth
methods that return the correspondingly-typed java.time
instances.
def cal = Date.parse('yyyyMMdd', '20170204').toCalendar()
assert cal.year == Year.of(2017)
assert cal.month == Month.FEBRUARY
assert cal.yearMonth == YearMonth.of(2017, Month.FEBRUARY)
assert cal.dayOfWeek == DayOfWeek.SATURDAY
assert cal.monthDay == MonthDay.of(Month.FEBRUARY, 4)
Other extension methods seek to mimic those found in the Groovy JDK for java.util.Date
and java.util.Calendar
.
The upto()
and downto()
methods of LocalTime
, LocalDateTime
, OffsetTime
, OffsetDateTime
, ZonedDateTime
, and Instant
iterate on a per second basis. The methods on LocalDate
iterate on a per day basis.
def now = LocalTime.now()
def aMinuteAgo = now - 60
now.downto(aMinuteAgo) { LocalTime t ->
// this closure will be called 61 times for each sceond between a minute ago and now
}
def today = LocalDate.now()
def tomorrow = today + 1
today.upto(tomorrow) { LocalDate d ->
// this closure will be called twice, once for today and once for tomorrow
}
A static eachMonth
method exists on Month
for iterating through every month. Similarly, a static eachDay
method exists on DayOfWeek
for iterating through the days of the week.
- The
getDateString
method- Exists for
LocalDate
,LocalDateTime
,OffsetDateTime
, andZonedDateTime
- Equivalent to calling
localDate.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
- Example:
5/5/17
- Exists for
- The
getTimeString
method- Exists for
LocalTime
,LocalDateTime
,OffsetTime
,OffsetDateTime
, andZonedDateTime
- Equivalent to calling
localTime.format(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT))
- Example:
10:59 PM
- Exists for
- The
getDateTimeString
method- Exists for
LocalDateTime
,OffsetDateTime
, andZonedDateTime
- Equivalent to calling
localDateTime.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT))
- Example:
5/5/17 10:59 PM
- Exists for
- The
format(String pattern)
method- Exists on
LocalDate
,LocalTime
,LocalDateTime
,OffsetDateTime
, andZonedDateTime
- Equivalent to
.format(DateTimeFormatter.ofPattern(pattern))
- Exists on
- The
format(String pattern, Locale locale)
method- Exists on
LocalDate
,LocalTime
,LocalDateTime
,OffsetTime
,OffsetDateTime
, andZonedDateTime
- Equivalent to
.format(DateTimeFormatter.ofPattern(pattern, locale))
- Exists on
clearTime() Similar to Date
, the LocalDateTime
, OffsetDateTime
, and ZonedDateTime
classes have a clearTime()
method that returns a new instance of the same type with the hours, minutes, seconds, and nanos set to zero.
describe() The Duration
class has a describe()
method which returns the normalized String representation as a map of ChronoUnit
keys: DAYS
, HOURS
, MINUTES
, SECONDS
, and NANOS
. The Period
class also has a describe()
method but with the ChronoUnit
keys of YEARS
, MONTHS
, and DAYS
.
Map<TemporalUnit, Long> durationDesc = Duration.parse('P2DT3H4M5.000000006S').describe()
assert durationDesc[ChronoUnit.DAYS] == 2
assert durationDesc[ChronoUnit.HOURS] == 3
assert durationDesc[ChronoUnit.MINUTES] == 4
assert durationDesc[ChronoUnit.SECONDS] == 5
assert durationDesc[ChronoUnit.NANOS] == 6
Map<TemporalUnit, Long> periodDesc = Period.parse('P6Y3M1D').describe()
assert periodDesc[ChronoUnit.DAYS] == 1
assert periodDesc[ChronoUnit.MONTHS] == 3
assert periodDesc[ChronoUnit.YEARS] == 6
Extension methods exist on Date
and Calendar
that produce a reasonably equivalent java.time
type.
def c = Calendar.instance
def d = new Date()
Instant cInstant = c.toInstant()
Instant dInstant = d.toInstant()
LocalDate cLocalDate = c.toLocalDate()
LocalDate dLocalDate = d.toLocalDate()
LocalTime cLocalTime = c.toLocalTime()
LocalTime dLocalTime = d.toLocalTime()
LocalDateTime cLocalDateTime = c.toLocalDateTime()
LocalDateTime dLocalDateTime = d.toLocalDateTime()
ZonedDateTime cZonedDateTime = c.toZonedDateTime()
ZonedDateTime dZonedDateTime = d.toZonedDateTime()
An optional ZoneOffset
, ZoneId
, or java.util.TimeZone
may be passed to the above conversion methods to alter the Time Zone of the returned Calendar
or to adjust the Time Zone of reference for the returned Date
.
def d = new Date()
def offset = ZoneOffset.UTC
def zoneId = ZoneId.of('America/Resolute')
def timeZone = TimeZone.getTimeZone('US/Eastern')
LocalDate localDateUTC = d.toLocalDate(offset)
LocalDate localDateResolute = d.toLocalDate(zoneId)
LocalDate localDateUSEastern = d.toLocalDate(timeZone)
The toOffsetDateTime
method requires a ZoneOffset
argument.
def c = Calendar.instance
def d = new Date()
def offset = ZoneOffset.UTC
OffsetDateTime cOffsetDateTime = c.toOffsetDateTime(offset)
OffsetDateTime dOffsetDateTime = d.toOffsetDateTime(offset)
The various java.time
Date/Time types have toDate()
and toCalendar()
methods as well. Nanoseconds are truncated to the nearest millisecond.
def nows = [LocalDate.now(), LocalTime.now(), LocalDateTime.now(), OffsetDateTime.now(), ZonedDateTime.now()]
List<Date> dates = nows.collect { it.toDate() }
List<Calendar> cals = nows.collect { it.toCalendar() }
Each java.time type already having a parse(CharSequence input, DateTimeFormatter formatter)
method gains two additional static methods:
parse(CharSequence input, String format)
- the format String is used to instantiate a newDateTimeFormatter
of that formatting pattern.parse(CharSequence input, String format, ZoneId zone)
- same as above plus the instantiatedDateTimeFormatter
is adjusted to the proivded zone via itswithZone
method.
def date = '2017-07-15'
def offsetDatetime = '111213 141516 +171819'
LocalDate parsedDate = LocalDate.parse(date, 'yyyy-MM-dd')
OffsetDateTime parsed = OffsetDateTime.parse(offsetDatetime, 'MMddyy HHmmss XX', ZoneId.of('UTC+0500'))
- Provide an equivalent to
groovy.time.TimeCategory
- TimeZone/ZoneId convenience methods (e.g. get all time zones at a given offset)
- Consider adding missing Date/Calendar methods from Groovy JDK (e.g.
set
andcopyWith
)