Groovy date and time with the java.time API. 12+ examples covering LocalDate, LocalDateTime, ZonedDateTime, Duration, formatting. Groovy 5.x.
“The only reason for time is so that everything doesn’t happen at once. In Groovy, at least, you get to control when it does.”
Adapted from Albert Einstein
Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Beginner to Intermediate | Reading Time: 25 minutes
If you’ve ever wrestled with java.util.Date and SimpleDateFormat in Groovy, you know the pain. Mutable date objects, thread-unsafe formatters, confusing month indexing that starts at zero – it was a mess. The good news? The modern java.time API (introduced in Java 8) fixes all of that, and Groovy date handling becomes a breeze when you combine it with Groovy’s syntactic sugar.
In this guide, you’ll learn everything about Groovy date time using the modern API – from creating Groovy LocalDate and LocalDateTime instances, to formatting, parsing, doing date arithmetic, comparing dates, working with time zones, and converting between the old and new APIs. All 12 examples are tested and ready to drop straight into your projects.
If you’re comfortable with basic Groovy syntax, you’re good to go. If not, check out our Groovy String Tutorial first for a solid foundation on Groovy fundamentals.
Table of Contents
What is the Modern Date-Time API?
The modern date-time API lives in the java.time package. Think of it as the replacement for java.util.Date and java.util.Calendar – designed from the ground up to be immutable, thread-safe, and intuitive. Every Groovy date operation you’d ever need is covered by a handful of clean, well-named classes.
According to the official Groovy GDK documentation, Groovy adds convenience extensions on top of the standard java.time classes – things like operator overloading for date arithmetic and enhanced formatting methods.
Key Classes You’ll Use:
LocalDate– a date without time (e.g., 2026-03-09)LocalTime– a time without date (e.g., 14:30:00)LocalDateTime– date and time combined, no time zoneZonedDateTime– date and time with a specific time zoneDuration– time-based amount (hours, minutes, seconds)Period– date-based amount (years, months, days)DateTimeFormatter– thread-safe formatting and parsing
Why Use java.time in Groovy?
You might be wondering: “I’ve been using new Date() for years, why change?” Here’s why the modern Groovy date time API is worth the switch:
- Immutability: Every
java.timeobject is immutable. No more accidental mutations when you pass a date to a method. - Thread Safety:
DateTimeFormatteris thread-safe unlikeSimpleDateFormat. Share formatters across threads without fear. - Clear Naming:
LocalDateis a date.LocalTimeis a time. No more wondering “does this Date object have time info or not?” - Sane Months: January is 1, not 0. Finally.
- Groovy Bonus: Groovy adds operator overloading (
date + 5adds 5 days), range support, and enhancedformat()methods on top.
When to Use Which Date-Time Class?
Here’s the quick decision guide. Pick the simplest class that fits your use case:
| Use Case | Class | Example |
|---|---|---|
| Birthdays, deadlines, calendar dates | LocalDate | 2026-03-09 |
| Meeting times, alarms, schedules | LocalTime | 14:30:00 |
| Timestamps without time zone | LocalDateTime | 2026-03-09T14:30:00 |
| International events, flight times | ZonedDateTime | 2026-03-09T14:30+05:30[Asia/Kolkata] |
| Measure elapsed time | Duration | PT2H30M (2 hours 30 minutes) |
| Age, subscription length | Period | P1Y3M (1 year 3 months) |
When you don’t need time zones, use LocalDate or LocalDateTime. When you do need time zones (and you often do for anything user-facing), reach for ZonedDateTime.
Syntax and Basic Usage
Creating Date-Time Objects
Here’s the basic syntax for creating Groovy date objects with the modern API:
Basic Syntax
import java.time.*
// Current date/time
LocalDate.now()
LocalTime.now()
LocalDateTime.now()
ZonedDateTime.now()
// Specific values
LocalDate.of(2026, 3, 9)
LocalTime.of(14, 30, 0)
LocalDateTime.of(2026, 3, 9, 14, 30)
ZonedDateTime.of(2026, 3, 9, 14, 30, 0, 0, ZoneId.of("America/New_York"))
// From strings
LocalDate.parse("2026-03-09")
LocalDateTime.parse("2026-03-09T14:30:00")
All java.time classes are available in Groovy without any special imports beyond java.time.*. No third-party libraries, no Grape dependencies – it’s all built into the JDK that Groovy runs on.
Practical Examples
Example 1: Creating LocalDate Instances
What we’re doing: Creating Groovy LocalDate objects using different factory methods and extracting date components.
Example 1: LocalDate Basics
import java.time.LocalDate
import java.time.Month
// Today's date
def today = LocalDate.now()
println "Today: ${today}"
// Specific date using of()
def birthday = LocalDate.of(1990, 6, 15)
println "Birthday: ${birthday}"
// Using Month enum for clarity
def christmas = LocalDate.of(2026, Month.DECEMBER, 25)
println "Christmas: ${christmas}"
// Parse from ISO string
def parsed = LocalDate.parse("2026-03-09")
println "Parsed: ${parsed}"
// Extract components
println "Year: ${today.year}"
println "Month: ${today.month} (${today.monthValue})"
println "Day: ${today.dayOfMonth}"
println "Day of Week:${today.dayOfWeek}"
println "Day of Year:${today.dayOfYear}"
println "Leap Year: ${today.isLeapYear()}"
Output
Today: 2026-03-09 Birthday: 1990-06-15 Christmas: 2026-12-25 Parsed: 2026-03-09 Year: 2026 Month: MARCH (3) Day: 9 Day of Week:MONDAY Day of Year:68 Leap Year: false
What happened here: LocalDate.now() grabs the current system date. LocalDate.of() creates a specific date – note that months are 1-based (January = 1), not 0-based like the old Calendar class. The Month enum makes code even clearer. Every component is accessible via clean getter methods.
Example 2: Working with LocalTime
What we’re doing: Creating time-only objects and extracting individual time components.
Example 2: LocalTime
import java.time.LocalTime
// Current time
def now = LocalTime.now()
println "Now: ${now}"
// Specific times
def morning = LocalTime.of(8, 30)
println "Morning: ${morning}"
def precise = LocalTime.of(14, 30, 45, 123456789)
println "Precise: ${precise}"
// Parse from string
def parsed = LocalTime.parse("09:15:30")
println "Parsed: ${parsed}"
// Midnight and noon constants
println "Midnight: ${LocalTime.MIDNIGHT}"
println "Noon: ${LocalTime.NOON}"
println "Min: ${LocalTime.MIN}"
println "Max: ${LocalTime.MAX}"
// Extract components
println "Hour: ${now.hour}"
println "Minute: ${now.minute}"
println "Second: ${now.second}"
println "Nano: ${now.nano}"
Output
Now: 10:45:22.384729 Morning: 08:30 Precise: 14:30:45.123456789 Parsed: 09:15:30 Midnight: 00:00 Noon: 12:00 Min: 00:00 Max: 23:59:59.999999999 Hour: 10 Minute: 45 Second: 22 Nano: 384729000
What happened here: LocalTime represents time without a date or time zone. You can specify hours and minutes, or go all the way down to nanosecond precision. The built-in constants MIDNIGHT, NOON, MIN, and MAX are handy for boundary checks.
Example 3: LocalDateTime – Date and Time Combined
What we’re doing: Combining date and time into a single object and converting between the local types.
Example 3: LocalDateTime
import java.time.LocalDate
import java.time.LocalTime
import java.time.LocalDateTime
// Current date-time
def now = LocalDateTime.now()
println "Now: ${now}"
// Specific date-time
def meeting = LocalDateTime.of(2026, 3, 15, 14, 30, 0)
println "Meeting: ${meeting}"
// Combine LocalDate and LocalTime
def date = LocalDate.of(2026, 6, 15)
def time = LocalTime.of(9, 0)
def combined = LocalDateTime.of(date, time)
println "Combined: ${combined}"
// Parse from ISO string
def parsed = LocalDateTime.parse("2026-12-25T08:00:00")
println "Parsed: ${parsed}"
// Extract date and time parts
println "Date part: ${now.toLocalDate()}"
println "Time part: ${now.toLocalTime()}"
println "Year: ${now.year}"
println "Hour: ${now.hour}"
// Create from a date at a specific time
def startOfDay = date.atStartOfDay()
println "Start of day:${startOfDay}"
def atTime = date.atTime(17, 30)
println "At 5:30 PM: ${atTime}"
Output
Now: 2026-03-09T10:45:22.385192 Meeting: 2026-03-15T14:30 Combined: 2026-06-15T09:00 Parsed: 2026-12-25T08:00 Date part: 2026-03-09 Time part: 10:45:22.385192 Year: 2026 Hour: 10 Start of day:2026-06-15T00:00 At 5:30 PM: 2026-06-15T17:30
What happened here: LocalDateTime is the marriage of LocalDate and LocalTime. You can create one from scratch, combine two separate objects, or use convenience methods like atStartOfDay() and atTime(). You can also extract the date or time portion back out with toLocalDate() and toLocalTime().
Example 4: ZonedDateTime – Handling Time Zones
What we’re doing: Working with time zones using ZonedDateTime and converting between zones.
Example 4: ZonedDateTime
import java.time.*
// Current time in system default zone
def now = ZonedDateTime.now()
println "Local: ${now}"
println "Zone: ${now.zone}"
// Specific zone
def nyTime = ZonedDateTime.now(ZoneId.of("America/New_York"))
def tokyoTime = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"))
def londonTime = ZonedDateTime.now(ZoneId.of("Europe/London"))
println "New York: ${nyTime}"
println "Tokyo: ${tokyoTime}"
println "London: ${londonTime}"
// Create with specific date, time, and zone
def flight = ZonedDateTime.of(2026, 6, 15, 8, 0, 0, 0, ZoneId.of("America/Los_Angeles"))
println "Departure: ${flight}"
// Convert to another time zone (same instant, different representation)
def arrival = flight.withZoneSameInstant(ZoneId.of("Asia/Kolkata"))
println "Arrival (IST):${arrival}"
// Get UTC offset
println "LA offset: ${flight.offset}"
println "IST offset: ${arrival.offset}"
// Convert LocalDateTime to ZonedDateTime
def local = LocalDateTime.of(2026, 3, 9, 14, 30)
def zoned = local.atZone(ZoneId.of("Europe/Paris"))
println "Paris: ${zoned}"
Output
Local: 2026-03-09T10:45:22.386+05:30[Asia/Kolkata] Zone: Asia/Kolkata New York: 2026-03-09T00:15:22.386-05:00[America/New_York] Tokyo: 2026-03-09T14:15:22.386+09:00[Asia/Tokyo] London: 2026-03-09T05:15:22.386+00:00[Europe/London] Departure: 2026-06-15T08:00-07:00[America/Los_Angeles] Arrival (IST):2026-06-15T20:30+05:30[Asia/Kolkata] LA offset: -07:00 IST offset: +05:30 Paris: 2026-03-09T14:30+01:00[Europe/Paris]
What happened here: ZonedDateTime is the most complete date-time class – it knows the exact instant in time plus the time zone. The key method is withZoneSameInstant() which converts to another zone while keeping the same real-world moment. This is essential for flight times, international meetings, and anything user-facing across geographies.
Example 5: Date Arithmetic – Plus and Minus
What we’re doing: Adding and subtracting days, weeks, months, and years from dates – the most common Groovy date operation.
Example 5: Date Arithmetic
import java.time.LocalDate
import java.time.LocalDateTime
def today = LocalDate.of(2026, 3, 9)
println "Today: ${today}"
// Add days, weeks, months, years
println "Plus 10 days: ${today.plusDays(10)}"
println "Plus 2 weeks: ${today.plusWeeks(2)}"
println "Plus 3 months: ${today.plusMonths(3)}"
println "Plus 1 year: ${today.plusYears(1)}"
// Subtract
println "Minus 30 days: ${today.minusDays(30)}"
println "Minus 6 months: ${today.minusMonths(6)}"
// Chain operations
def futureDate = today.plusYears(1).plusMonths(2).plusDays(15)
println "Chained: ${futureDate}"
// Works with LocalDateTime too
def now = LocalDateTime.of(2026, 3, 9, 10, 30, 0)
println "Now: ${now}"
println "Plus 3 hours: ${now.plusHours(3)}"
println "Plus 45 minutes: ${now.plusMinutes(45)}"
println "Minus 90 seconds:${now.minusSeconds(90)}"
// Groovy GDK: use + and - operators!
def tomorrow = today + 1
def yesterday = today - 1
println "Tomorrow: ${tomorrow}"
println "Yesterday: ${yesterday}"
Output
Today: 2026-03-09 Plus 10 days: 2026-03-19 Plus 2 weeks: 2026-03-23 Plus 3 months: 2026-06-09 Plus 1 year: 2027-03-09 Minus 30 days: 2026-02-07 Minus 6 months: 2025-09-09 Chained: 2027-05-24 Now: 2026-03-09T10:30 Plus 3 hours: 2026-03-09T13:30 Plus 45 minutes: 2026-03-09T11:15 Minus 90 seconds:2026-03-09T10:28:30 Tomorrow: 2026-03-10 Yesterday: 2026-03-08
What happened here: All plus*() and minus*() methods return new instances – the original is never modified (immutability!). You can chain operations freely. Here’s the cool part: Groovy’s GDK adds + and - operators to LocalDate, so today + 1 means “add one day.” Clean and readable.
Example 6: Comparing Dates
What we’re doing: Comparing dates using built-in methods and Groovy’s comparison operators.
Example 6: Comparing Dates
import java.time.LocalDate
def today = LocalDate.of(2026, 3, 9)
def tomorrow = LocalDate.of(2026, 3, 10)
def lastWeek = LocalDate.of(2026, 3, 2)
def sameDay = LocalDate.of(2026, 3, 9)
// isBefore, isAfter, isEqual
println "Today before tomorrow? ${today.isBefore(tomorrow)}"
println "Today after last week? ${today.isAfter(lastWeek)}"
println "Today equals sameDay? ${today.isEqual(sameDay)}"
// Groovy comparison operators work too!
println "today < tomorrow: ${today < tomorrow}"
println "today > lastWeek: ${today > lastWeek}"
println "today == sameDay: ${today == sameDay}"
println "today <=> tomorrow:${today <=> tomorrow}"
// Sorting dates with Groovy
def dates = [
LocalDate.of(2026, 12, 25),
LocalDate.of(2026, 1, 1),
LocalDate.of(2026, 7, 4),
LocalDate.of(2026, 3, 9)
]
println "Sorted: ${dates.sort()}"
println "Earliest:${dates.min()}"
println "Latest: ${dates.max()}"
Output
Today before tomorrow? true Today after last week? true Today equals sameDay? true today < tomorrow: true today > lastWeek: true today == sameDay: true today <=> tomorrow:-1 Sorted: [2026-01-01, 2026-03-09, 2026-07-04, 2026-12-25] Earliest:2026-01-01 Latest: 2026-12-25
What happened here: The java.time classes implement Comparable, so Groovy’s <, >, ==, and spaceship operator (<=>) all work naturally. You can use sort(), min(), and max() on collections of dates directly.
Example 7: Formatting Dates with DateTimeFormatter
What we’re doing: Converting Groovy date objects to custom string representations using DateTimeFormatter.
Example 7: Formatting Dates
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
def date = LocalDate.of(2026, 3, 9)
def dateTime = LocalDateTime.of(2026, 3, 9, 14, 30, 45)
// Built-in formatters
println "ISO_DATE: ${date.format(DateTimeFormatter.ISO_DATE)}"
println "ISO_DATE_TIME: ${dateTime.format(DateTimeFormatter.ISO_DATE_TIME)}"
// Custom patterns
def fmt1 = DateTimeFormatter.ofPattern("dd/MM/yyyy")
println "dd/MM/yyyy: ${date.format(fmt1)}"
def fmt2 = DateTimeFormatter.ofPattern("MMMM dd, yyyy")
println "Long format: ${date.format(fmt2)}"
def fmt3 = DateTimeFormatter.ofPattern("EEE, MMM d, yyyy")
println "With day name: ${date.format(fmt3)}"
def fmt4 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
println "Full timestamp: ${dateTime.format(fmt4)}"
def fmt5 = DateTimeFormatter.ofPattern("hh:mm a")
println "12-hour time: ${dateTime.format(fmt5)}"
// Groovy shorthand - format() with pattern string directly
println "Groovy format: ${date.format('dd-MMM-yyyy')}"
println "Groovy dt format: ${dateTime.format('yyyy/MM/dd HH:mm')}"
Output
ISO_DATE: 2026-03-09 ISO_DATE_TIME: 2026-03-09T14:30:45 dd/MM/yyyy: 09/03/2026 Long format: March 09, 2026 With day name: Mon, Mar 9, 2026 Full timestamp: 2026-03-09 14:30:45 12-hour time: 02:30 PM Groovy format: 09-Mar-2026 Groovy dt format: 2026/03/09 14:30
What happened here: DateTimeFormatter.ofPattern() lets you define any date format. Common tokens: yyyy (year), MM (month number), MMM (short month name), MMMM (full month name), dd (day), EEE (day name), HH (24-hour), hh (12-hour), mm (minutes), a (AM/PM). Groovy’s GDK also adds a convenient format(String) method so you can skip creating a formatter object.
Example 8: Parsing Date Strings
What we’re doing: Converting strings in various formats into Groovy date objects using custom formatters.
Example 8: Parsing Dates
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeFormatterBuilder
import java.time.format.DateTimeParseException
// ISO format parses automatically
def iso = LocalDate.parse("2026-03-09")
println "ISO: ${iso}"
// Custom format requires a formatter
def fmt1 = DateTimeFormatter.ofPattern("dd/MM/yyyy")
def european = LocalDate.parse("09/03/2026", fmt1)
println "European: ${european}"
def fmt2 = DateTimeFormatter.ofPattern("MMMM dd, yyyy")
def verbose = LocalDate.parse("March 09, 2026", fmt2)
println "Verbose: ${verbose}"
def fmt3 = DateTimeFormatter.ofPattern("MM-dd-yyyy HH:mm:ss")
def timestamp = LocalDateTime.parse("03-09-2026 14:30:45", fmt3)
println "Timestamp: ${timestamp}"
// Case-insensitive parsing (use DateTimeFormatterBuilder)
def fmt4 = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.appendPattern("dd-MMM-yyyy")
.toFormatter()
def caseInsensitive = LocalDate.parse("09-mar-2026", fmt4)
println "Lower case: ${caseInsensitive}"
// Error handling for bad input
try {
LocalDate.parse("not-a-date")
} catch (DateTimeParseException e) {
println "Parse error: ${e.message}"
}
try {
LocalDate.parse("31/02/2026", fmt1)
} catch (DateTimeParseException e) {
println "Invalid date: ${e.message}"
}
Output
ISO: 2026-03-09 European: 2026-03-09 Verbose: 2026-03-09 Timestamp: 2026-03-09T14:30:45 Lower case: 2026-03-09 Parse error: Text 'not-a-date' could not be parsed at index 0 Invalid date: Text '31/02/2026' could not be parsed: Invalid date 'FEBRUARY 31'
What happened here: Parsing is the reverse of formatting – you give a string and a pattern, and the API builds a date object. The API validates strictly by default: February 31 throws a DateTimeParseException rather than silently rolling over to March 3. Always wrap parsing in a try-catch when dealing with user input or external data.
Example 9: Duration and Period
What we’re doing: Measuring time differences with Duration (for time-based amounts) and Period (for date-based amounts).
Example 9: Duration and Period
import java.time.*
// --- Period (date-based: years, months, days) ---
def startDate = LocalDate.of(2024, 1, 15)
def endDate = LocalDate.of(2026, 3, 9)
def period = Period.between(startDate, endDate)
println "Period: ${period}"
println "Years: ${period.years}"
println "Months: ${period.months}"
println "Days: ${period.days}"
println "Total months: ${period.toTotalMonths()}"
// Create periods directly
def sixMonths = Period.ofMonths(6)
def oneYear = Period.ofYears(1)
def custom = Period.of(1, 2, 15) // 1 year, 2 months, 15 days
println "6 months: ${sixMonths}"
println "1 year: ${oneYear}"
println "Custom: ${custom}"
// Apply period to a date
def futureDate = startDate.plus(custom)
println "Start + custom: ${futureDate}"
// --- Duration (time-based: hours, minutes, seconds) ---
def startTime = LocalDateTime.of(2026, 3, 9, 8, 0, 0)
def endTime = LocalDateTime.of(2026, 3, 9, 17, 30, 45)
def duration = Duration.between(startTime, endTime)
println "Duration: ${duration}"
println "Hours: ${duration.toHours()}"
println "Minutes: ${duration.toMinutes()}"
println "Seconds: ${duration.toSeconds()}"
// Create durations directly
def twoHours = Duration.ofHours(2)
def ninetyMins = Duration.ofMinutes(90)
println "2 hours: ${twoHours}"
println "90 minutes: ${ninetyMins}"
Output
Period: P2Y1M22D Years: 2 Months: 1 Days: 22 Total months: 25 6 months: P6M 1 year: P1Y Custom: P1Y2M15D Start + custom: 2025-03-30 Duration: PT9H30M45S Hours: 9 Minutes: 570 Seconds: 34245 2 hours: PT2H 90 minutes: PT1H30M
What happened here: Period measures calendar-based differences (years, months, days) while Duration measures exact time-based differences (hours, minutes, seconds, nanoseconds). The string representation follows ISO-8601: P2Y1M22D means 2 years, 1 month, 22 days; PT9H30M45S means 9 hours, 30 minutes, 45 seconds. Use Period for human-friendly intervals and Duration for precise timing.
Example 10: Date Ranges with Groovy
What we’re doing: Iterating over date ranges using Groovy’s range operator and GDK extensions.
Example 10: Date Ranges
import java.time.LocalDate
import java.time.DayOfWeek
def start = LocalDate.of(2026, 3, 9)
def end = LocalDate.of(2026, 3, 15)
// Groovy range operator works with LocalDate!
def dateRange = start..end
println "Range: ${dateRange}"
println "Size: ${dateRange.size()}"
println "Contains 3/12? ${dateRange.contains(LocalDate.of(2026, 3, 12))}"
// Iterate over each day
println "\nDays in range:"
dateRange.each { date ->
println " ${date} (${date.dayOfWeek})"
}
// Filter weekdays only
def weekdays = dateRange.findAll { date ->
date.dayOfWeek != DayOfWeek.SATURDAY &&
date.dayOfWeek != DayOfWeek.SUNDAY
}
println "\nWeekdays: ${weekdays}"
// Collect formatted strings
def formatted = dateRange.collect { it.format('EEE MMM d') }
println "Formatted: ${formatted}"
// Generate first day of each month in a year
def monthStarts = (1..12).collect { month ->
LocalDate.of(2026, month, 1)
}
println "\nMonth starts: ${monthStarts}"
Output
Range: [2026-03-09, 2026-03-10, 2026-03-11, 2026-03-12, 2026-03-13, 2026-03-14, 2026-03-15] Size: 7 Contains 3/12? true Days in range: 2026-03-09 (MONDAY) 2026-03-10 (TUESDAY) 2026-03-11 (WEDNESDAY) 2026-03-12 (THURSDAY) 2026-03-13 (FRIDAY) 2026-03-14 (SATURDAY) 2026-03-15 (SUNDAY) Weekdays: [2026-03-09, 2026-03-10, 2026-03-11, 2026-03-12, 2026-03-13] Formatted: [Mon Mar 9, Tue Mar 10, Wed Mar 11, Thu Mar 12, Fri Mar 13, Sat Mar 14, Sun Mar 15] Month starts: [2026-01-01, 2026-02-01, 2026-03-01, 2026-04-01, 2026-05-01, 2026-06-01, 2026-07-01, 2026-08-01, 2026-09-01, 2026-10-01, 2026-11-01, 2026-12-01]
What happened here: Groovy’s GDK adds the next() and previous() methods to LocalDate, which enables the range operator (..) to iterate day by day. You can then use all of Groovy’s collection methods – each(), findAll(), collect() – on date ranges. This is incredibly powerful for generating calendars, filtering business days, or building schedules.
Example 11: Real-World Use Case – Age Calculator
What we’re doing: Building a practical age calculator that handles birthdays, next birthday countdown, and age in different units.
Example 11: Age Calculator
import java.time.*
import java.time.temporal.ChronoUnit
def calculateAge(LocalDate birthDate) {
def today = LocalDate.of(2026, 3, 9) // fixed for reproducible output
def age = Period.between(birthDate, today)
println "Birth date: ${birthDate}"
println "Current date: ${today}"
println "Age: ${age.years} years, ${age.months} months, ${age.days} days"
// Total days alive
def totalDays = ChronoUnit.DAYS.between(birthDate, today)
println "Total days: ${totalDays}"
println "Total weeks: ${totalDays / 7 as long}"
// Next birthday
def nextBirthday = birthDate.withYear(today.year)
if (nextBirthday.isBefore(today) || nextBirthday.isEqual(today)) {
nextBirthday = nextBirthday.plusYears(1)
}
def daysUntilBirthday = ChronoUnit.DAYS.between(today, nextBirthday)
println "Next birthday: ${nextBirthday} (${daysUntilBirthday} days away)"
// Is it their birthday today?
def isBirthday = today.month == birthDate.month && today.dayOfMonth == birthDate.dayOfMonth
println "Birthday today? ${isBirthday}"
println()
}
calculateAge(LocalDate.of(1990, 6, 15))
calculateAge(LocalDate.of(2000, 3, 9)) // Birthday today!
calculateAge(LocalDate.of(1985, 12, 25))
Output
Birth date: 1990-06-15 Current date: 2026-03-09 Age: 35 years, 8 months, 22 days Total days: 13051 Total weeks: 1864 Next birthday: 2026-06-15 (98 days away) Birthday today? false Birth date: 2000-03-09 Current date: 2026-03-09 Age: 26 years, 0 months, 0 days Total days: 9496 Total weeks: 1356 Next birthday: 2027-03-09 (365 days away) Birthday today? true Birth date: 1985-12-25 Current date: 2026-03-09 Age: 40 years, 2 months, 12 days Total days: 14684 Total weeks: 2097 Next birthday: 2026-12-25 (291 days away) Birthday today? false
What happened here: Period.between() gives us the human-readable age breakdown. ChronoUnit.DAYS.between() gives us the exact day count. The next-birthday logic uses withYear() to project the birthday into the current year, then checks if it’s already passed. This is a pattern you’ll use constantly in business applications.
Example 12: Real-World Use Case – Business Day Calculator
What we’re doing: Calculating business days between two dates, skipping weekends and holidays – a common requirement in enterprise applications.
Example 12: Business Day Calculator
import java.time.*
import java.time.format.DateTimeFormatter
// Define holidays
def holidays2026 = [
LocalDate.of(2026, 1, 1), // New Year
LocalDate.of(2026, 1, 26), // Republic Day (India)
LocalDate.of(2026, 8, 15), // Independence Day
LocalDate.of(2026, 12, 25) // Christmas
] as Set
def isBusinessDay(LocalDate date, Set holidays) {
date.dayOfWeek != DayOfWeek.SATURDAY &&
date.dayOfWeek != DayOfWeek.SUNDAY &&
!holidays.contains(date)
}
def countBusinessDays(LocalDate start, LocalDate end, Set holidays) {
(start..end).count { isBusinessDay(it, holidays) }
}
def addBusinessDays(LocalDate start, int days, Set holidays) {
def current = start
def added = 0
while (added < days) {
current = current + 1
if (isBusinessDay(current, holidays)) {
added++
}
}
current
}
// Calculate business days in March 2026
def marchStart = LocalDate.of(2026, 3, 1)
def marchEnd = LocalDate.of(2026, 3, 31)
def bizDays = countBusinessDays(marchStart, marchEnd, holidays2026)
println "Business days in March 2026: ${bizDays}"
// Add 10 business days from today
def today = LocalDate.of(2026, 3, 9)
def deadline = addBusinessDays(today, 10, holidays2026)
println "10 business days from ${today}: ${deadline}"
println "That's a ${deadline.dayOfWeek}"
// Find the next business day after a weekend
def saturday = LocalDate.of(2026, 3, 14)
def nextBiz = saturday
while (!isBusinessDay(nextBiz, holidays2026)) {
nextBiz = nextBiz + 1
}
println "Next business day after ${saturday}: ${nextBiz} (${nextBiz.dayOfWeek})"
// Generate all business days in a week
def weekStart = LocalDate.of(2026, 3, 9)
def weekEnd = LocalDate.of(2026, 3, 15)
def businessDaysThisWeek = (weekStart..weekEnd).findAll { isBusinessDay(it, holidays2026) }
println "Business days this week:"
businessDaysThisWeek.each { println " ${it.format('EEE dd-MMM')}" }
Output
Business days in March 2026: 22 10 business days from 2026-03-09: 2026-03-23 That's a MONDAY Next business day after 2026-03-14: 2026-03-16 (MONDAY) Business days this week: Mon 09-Mar Tue 10-Mar Wed 11-Mar Thu 12-Mar Fri 13-Mar
What happened here: We combined Groovy date ranges, closures, and the java.time API to build a practical business day calculator. The count() method counts matching elements in a range, findAll() filters them, and the + operator advances dates. This kind of utility is common in payroll, SLA calculations, and project management tools.
Groovy GDK Date Extensions
Groovy doesn’t just let you use java.time – it enhances it. The Groovy Development Kit (GDK) adds several convenience methods to the standard date-time classes. Here’s a summary of what Groovy brings to the table:
| GDK Feature | Works On | Description |
|---|---|---|
date + n / date - n | LocalDate, LocalDateTime | Add/subtract days using operators |
date1..date2 | LocalDate | Create iterable date ranges |
format(String) | LocalDate, LocalDateTime, etc. | Format with pattern string directly |
date++ / date-- | LocalDate | next/previous day (via next()/previous()) |
<, >, ==, <=> | All java.time types | Comparison operators via Comparable |
upto() / downto() | LocalDate, LocalDateTime | Iterate with custom steps |
GDK Extensions in Action
import java.time.*
import java.time.temporal.ChronoUnit
def today = LocalDate.of(2026, 3, 9)
// Operator overloading
def nextWeek = today + 7
def lastWeek = today - 7
println "Next week: ${nextWeek}"
println "Last week: ${lastWeek}"
// Groovy format shorthand
println "Formatted: ${today.format('EEEE, MMMM dd, yyyy')}"
// upto() with custom step
println "\nEvery Monday in March:"
def marchStart = LocalDate.of(2026, 3, 2) // First Monday
def marchEnd = LocalDate.of(2026, 3, 31)
marchStart.upto(marchEnd) { date ->
if (date.dayOfWeek == DayOfWeek.MONDAY) {
println " ${date.format('MMM dd')}"
}
}
// Difference using Groovy syntax
def christmas = LocalDate.of(2026, 12, 25)
def daysUntil = ChronoUnit.DAYS.between(today, christmas)
println "\nDays until Christmas: ${daysUntil}"
Output
Next week: 2026-03-16 Last week: 2026-03-02 Formatted: Monday, March 09, 2026 Every Monday in March: Mar 02 Mar 09 Mar 16 Mar 23 Mar 30 Days until Christmas: 291
These GDK extensions are what make Groovy date handling feel so natural compared to plain Java. You write less code, it reads more like English, and you get the full power of java.time under the hood.
Converting Between Old and New API
You’ll inevitably encounter legacy code that uses java.util.Date or java.util.Calendar. Here’s how to convert back and forth. If you’re working with XML parsing in Groovy (which sometimes involves date fields), our XmlParser vs XmlSlurper guide covers handling date strings in XML documents.
Old API to New API Conversion
import java.time.*
import java.text.SimpleDateFormat
// === Old Date to New API ===
def oldDate = new Date()
println "Old Date: ${oldDate}"
// Date -> Instant -> LocalDateTime
def instant = oldDate.toInstant()
def localDT = LocalDateTime.ofInstant(instant, ZoneId.systemDefault())
println "LocalDateTime:${localDT}"
// Date -> Instant -> ZonedDateTime
def zonedDT = ZonedDateTime.ofInstant(instant, ZoneId.systemDefault())
println "ZonedDateTime:${zonedDT}"
// Date -> Instant -> LocalDate
def localDate = instant.atZone(ZoneId.systemDefault()).toLocalDate()
println "LocalDate: ${localDate}"
// === New API to Old Date ===
def newDate = LocalDate.of(2026, 3, 9)
def newDateTime = LocalDateTime.of(2026, 3, 9, 14, 30)
// LocalDate -> Date (need to go through ZonedDateTime for Instant)
def backToDate = Date.from(
newDate.atStartOfDay(ZoneId.systemDefault()).toInstant()
)
println "\nLocalDate -> Date: ${backToDate}"
// LocalDateTime -> Date
def dtBackToDate = Date.from(
newDateTime.atZone(ZoneId.systemDefault()).toInstant()
)
println "LocalDateTime -> Date:${dtBackToDate}"
// === Calendar conversions ===
def calendar = Calendar.instance
def fromCal = LocalDateTime.ofInstant(
calendar.toInstant(), ZoneId.systemDefault()
)
println "\nCalendar -> LocalDateTime: ${fromCal}"
// === SQL Date conversions ===
def sqlDate = java.sql.Date.valueOf(newDate)
println "LocalDate -> SQL Date: ${sqlDate}"
def backToLocal = sqlDate.toLocalDate()
println "SQL Date -> LocalDate: ${backToLocal}"
Output
Old Date: Mon Mar 09 10:45:22 IST 2026 LocalDateTime:2026-03-09T10:45:22.387 ZonedDateTime:2026-03-09T10:45:22.387+05:30[Asia/Kolkata] LocalDate: 2026-03-09 LocalDate -> Date: Mon Mar 09 00:00:00 IST 2026 LocalDateTime -> Date:Mon Mar 09 14:30:00 IST 2026 Calendar -> LocalDateTime: 2026-03-09T10:45:22.387 LocalDate -> SQL Date: 2026-03-09 SQL Date -> LocalDate: 2026-03-09
The bridge between old and new is the Instant class. Going from old to new: date.toInstant() then convert. Going from new to old: get an Instant via atZone().toInstant() then Date.from(). For java.sql.Date, it’s even simpler with valueOf() and toLocalDate().
Pro Tip: When working with Groovy strings and dates together, you can interpolate formatted dates directly inside GStrings:
"Report generated on ${LocalDate.now().format('dd-MMM-yyyy')}". This is great for building log messages, filenames, and reports.
Edge Cases and Best Practices
Edge Case: End-of-Month Arithmetic
End-of-Month Edge Case
import java.time.LocalDate
// Adding months near end of month
def jan31 = LocalDate.of(2026, 1, 31)
println "Jan 31 + 1 month: ${jan31.plusMonths(1)}" // Feb 28 (not 31!)
println "Jan 31 + 2 months:${jan31.plusMonths(2)}" // Mar 31
def mar31 = LocalDate.of(2026, 3, 31)
println "Mar 31 - 1 month: ${mar31.minusMonths(1)}" // Feb 28
// Leap year handling
def feb29 = LocalDate.of(2024, 2, 29) // 2024 is a leap year
println "Feb 29 2024 + 1 year: ${feb29.plusYears(1)}" // Feb 28 2025
println "Feb 29 2024 + 4 years:${feb29.plusYears(4)}" // Feb 29 2028
Output
Jan 31 + 1 month: 2026-02-28 Jan 31 + 2 months:2026-03-31 Mar 31 - 1 month: 2026-02-28 Feb 29 2024 + 1 year: 2025-02-28 Feb 29 2024 + 4 years:2028-02-29
The API handles end-of-month intelligently – it adjusts to the last valid day rather than throwing an error. This is the correct behavior, but it means adding 1 month and then subtracting 1 month doesn’t always give you back the original date. Be aware of this in financial calculations.
Best Practices
DO:
- Use
LocalDatefor dates without time – never useLocalDateTimewith midnight as a fake date - Store timestamps as
InstantorZonedDateTimein databases, notLocalDateTime - Use
DateTimeFormatterconstants (likeISO_DATE) when possible – they’re pre-built and optimized - Wrap date parsing in try-catch blocks when the input comes from users or external systems
- Cache
DateTimeFormatterinstances – they’re immutable and thread-safe, so create once and reuse
DON’T:
- Use
java.util.Datein new code – it’s effectively deprecated for application use - Use
SimpleDateFormat– it’s not thread-safe and the source of countless bugs - Assume months are 0-based (that was only
Calendar) – injava.time, January = 1 - Ignore time zones for user-facing dates – always be explicit about which zone you mean
Common Pitfalls
Pitfall 1: LocalDateTime Is Not a Timestamp
Problem: Using LocalDateTime to represent a specific moment in time.
Pitfall 1: LocalDateTime Isn’t a Timestamp
import java.time.*
// BAD: This doesn't represent a specific moment!
def meeting = LocalDateTime.of(2026, 3, 15, 14, 0)
// 2 PM where? New York? London? Tokyo? We don't know!
// GOOD: Use ZonedDateTime for real-world events
def meetingNY = ZonedDateTime.of(2026, 3, 15, 14, 0, 0, 0,
ZoneId.of("America/New_York"))
def meetingTokyo = meetingNY.withZoneSameInstant(ZoneId.of("Asia/Tokyo"))
println "Meeting NY: ${meetingNY}"
println "Meeting Tokyo: ${meetingTokyo}"
println "Same instant? ${meetingNY.toInstant() == meetingTokyo.toInstant()}"
Output
Meeting NY: 2026-03-15T14:00-04:00[America/New_York] Meeting Tokyo: 2026-03-16T03:00+09:00[Asia/Tokyo] Same instant? true
Solution: LocalDateTime is for “a date-time without a time zone” – use it for things like alarm times or birthdays. For events that happen at a specific instant (meetings, deadlines, logs), always use ZonedDateTime or Instant.
Pitfall 2: Daylight Saving Time Gaps
Problem: Creating a time that doesn’t exist due to DST transitions.
Pitfall 2: DST Gaps
import java.time.*
// In the US, clocks spring forward on second Sunday of March
// 2:00 AM -> 3:00 AM (2:30 AM doesn't exist!)
def dstDate = LocalDateTime.of(2026, 3, 8, 2, 30)
def zoned = dstDate.atZone(ZoneId.of("America/New_York"))
println "Requested: 2:30 AM on March 8"
println "Actual: ${zoned}"
println "Offset: ${zoned.offset}"
// The API adjusts automatically - it moves the time forward
// to the next valid time after the gap
Output
Requested: 2:30 AM on March 8 Actual: 2026-03-08T03:30-04:00[America/New_York] Offset: -04:00
Solution: The java.time API handles DST gaps gracefully by adjusting to the next valid time. Be aware of this behavior when scheduling tasks around DST boundaries. If you need exact control, use ZonedDateTime.ofStrict() which throws an exception instead of silently adjusting.
Pitfall 3: Forgetting Immutability
Problem: Expecting date methods to modify the original object.
Pitfall 3: Immutability
import java.time.LocalDate
def date = LocalDate.of(2026, 3, 9)
// BAD: This does nothing to 'date'!
date.plusDays(10) // Return value is discarded
println "Still: ${date}"
// GOOD: Assign the result
def newDate = date.plusDays(10)
println "New: ${newDate}"
println "Old: ${date}" // Original is unchanged
Output
Still: 2026-03-09 New: 2026-03-19 Old: 2026-03-09
Solution: Every method on java.time objects returns a new instance. Always capture the return value. This is actually a feature – it means dates are safe to share between methods, threads, and collections without defensive copying.
Conclusion
The modern Groovy date time API is a massive upgrade over the legacy java.util.Date approach. With immutable, thread-safe classes like Groovy LocalDate, LocalDateTime, and ZonedDateTime, you get clarity, safety, and Groovy’s syntactic sugar on top. Date arithmetic with + and -, date ranges with .., and format shortcuts make working with dates in Groovy genuinely enjoyable.
Here is what matters: use LocalDate for calendar dates, ZonedDateTime for real-world moments, DateTimeFormatter for parsing and formatting, and Period/Duration for measuring differences. And when you encounter legacy code, the conversion bridge through Instant makes migration simple.
If you’re looking for more ways to manage external libraries in your Groovy scripts (like adding third-party date utility libraries), check out our upcoming guide on Groovy Grape Dependency Management. And if you want to see how Groovy date formatting works alongside Groovy strings, that guide covers GString interpolation patterns that pair perfectly with date formatting.
Summary
- Use
java.timeclasses (LocalDate,LocalDateTime,ZonedDateTime) instead ofjava.util.Datein all new code - All
java.timeobjects are immutable – always capture the return value of operations - Groovy adds
+,-,..(ranges), andformat(String)extensions to date-time classes via the GDK - Use
DateTimeFormatter(notSimpleDateFormat) – it’s thread-safe and more powerful - Use
Periodfor calendar-based differences andDurationfor time-based differences - Convert between old and new APIs through
Instantas the bridge - Always use
ZonedDateTimefor timestamps that represent real-world moments
If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.
Up next: Groovy GINQ – SQL-Like Collection Queries
Frequently Asked Questions
How do I get the current date in Groovy?
Use LocalDate.now() to get today’s date without time, LocalDateTime.now() to get the current date and time, or ZonedDateTime.now() to include the time zone. All require import java.time.*. For example: def today = LocalDate.now() gives you the current date in ISO format (e.g., 2026-03-09).
How do I format a date in Groovy?
Use DateTimeFormatter with a pattern string: date.format(DateTimeFormatter.ofPattern('dd/MM/yyyy')). Groovy also adds a shorthand format() method via the GDK, so you can simply write date.format('dd-MMM-yyyy') without creating a formatter object. Common tokens include yyyy (year), MM (month), dd (day), HH (hour), mm (minute).
How do I add days to a date in Groovy?
Use date.plusDays(10) to add 10 days, or take advantage of Groovy’s GDK operator overloading with date + 10. Both return a new date instance since java.time objects are immutable. You can also use plusWeeks(), plusMonths(), and plusYears() for larger increments, and chain them: date.plusMonths(3).plusDays(15).
How do I convert java.util.Date to LocalDate in Groovy?
Convert through Instant as the bridge: def localDate = oldDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(). For the reverse (LocalDate to java.util.Date), use: Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant()). For java.sql.Date, it’s simpler: java.sql.Date.valueOf(localDate) and sqlDate.toLocalDate().
What is the difference between LocalDateTime and ZonedDateTime in Groovy?
LocalDateTime stores a date and time but has no time zone information – it represents a concept like ‘March 9 at 2:30 PM’ without specifying where. ZonedDateTime includes the time zone, so it represents an exact moment in time. Use LocalDateTime for things like alarm times or recurring schedules. Use ZonedDateTime for events, timestamps, logs, and anything that needs to represent a precise instant across different locations.
Related Posts
Previous in Series: Groovy Template Engines Guide
Next in Series: Groovy GINQ – SQL-Like Collection Queries
Related Topics You Might Like:
- Groovy String Tutorial – The Complete Guide
- Groovy Grape Dependency Management
- Groovy XmlParser vs XmlSlurper
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment