Groovy Date and Time – Modern API Guide with 12 Tested Examples

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.

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 zone
  • ZonedDateTime – date and time with a specific time zone
  • Duration – 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.time object is immutable. No more accidental mutations when you pass a date to a method.
  • Thread Safety: DateTimeFormatter is thread-safe unlike SimpleDateFormat. Share formatters across threads without fear.
  • Clear Naming: LocalDate is a date. LocalTime is 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 + 5 adds 5 days), range support, and enhanced format() 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 CaseClassExample
Birthdays, deadlines, calendar datesLocalDate2026-03-09
Meeting times, alarms, schedulesLocalTime14:30:00
Timestamps without time zoneLocalDateTime2026-03-09T14:30:00
International events, flight timesZonedDateTime2026-03-09T14:30+05:30[Asia/Kolkata]
Measure elapsed timeDurationPT2H30M (2 hours 30 minutes)
Age, subscription lengthPeriodP1Y3M (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 FeatureWorks OnDescription
date + n / date - nLocalDate, LocalDateTimeAdd/subtract days using operators
date1..date2LocalDateCreate iterable date ranges
format(String)LocalDate, LocalDateTime, etc.Format with pattern string directly
date++ / date--LocalDatenext/previous day (via next()/previous())
<, >, ==, <=>All java.time typesComparison operators via Comparable
upto() / downto()LocalDate, LocalDateTimeIterate 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 LocalDate for dates without time – never use LocalDateTime with midnight as a fake date
  • Store timestamps as Instant or ZonedDateTime in databases, not LocalDateTime
  • Use DateTimeFormatter constants (like ISO_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 DateTimeFormatter instances – they’re immutable and thread-safe, so create once and reuse

DON’T:

  • Use java.util.Date in 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) – in java.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.time classes (LocalDate, LocalDateTime, ZonedDateTime) instead of java.util.Date in all new code
  • All java.time objects are immutable – always capture the return value of operations
  • Groovy adds +, -, .. (ranges), and format(String) extensions to date-time classes via the GDK
  • Use DateTimeFormatter (not SimpleDateFormat) – it’s thread-safe and more powerful
  • Use Period for calendar-based differences and Duration for time-based differences
  • Convert between old and new APIs through Instant as the bridge
  • Always use ZonedDateTime for 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.

Previous in Series: Groovy Template Engines Guide

Next in Series: Groovy GINQ – SQL-Like Collection Queries

Related Topics You Might Like:

This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *