Groovy TimeCategory and Date Manipulation – Cookbook with 10+ Examples

Groovy TimeCategory and date manipulation with 13 tested examples. Write 3.days.ago, 2.hours.from.now, and natural date arithmetic using Groovy’s built-in date DSL and GDK extensions.

“There are only two hard problems in computer science: cache invalidation, naming things, and off-by-one errors in date arithmetic.”

Joshua Bloch, Effective Java

Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Beginner-Intermediate | Reading Time: 18 minutes

Our Groovy Date and Time post covers the java.time API – LocalDate, ZonedDateTime, Duration, and the modern approach to dates. This post takes a different angle: Groovy’s own date manipulation tools. The star of the show is TimeCategory, a DSL that lets you write use(TimeCategory) { println 3.days.ago } like plain English. We also cover the GDK extensions that Groovy adds to both legacy Date and modern java.time classes – operator overloading, convenience methods, and formatting shortcuts.

If you want the java.time fundamentals, read the Date and Time post. If you want groovy TimeCategory recipes, date arithmetic that reads like English, and the Groovy-specific shortcuts that make date handling actually enjoyable, keep reading. This cookbook gives you 13 tested, copy-paste recipes for the date tasks you hit in every project.

Quick Reference Table

TaskGroovy ApproachJava Equivalent
Current dateLocalDate.now()LocalDate.now()
Current date+timeLocalDateTime.now()LocalDateTime.now()
Date arithmeticuse(TimeCategory) { 3.days.from.now }LocalDate.now().plusDays(3)
Format datedate.format('yyyy-MM-dd')DateTimeFormatter.ofPattern(...)
Parse dateDate.parse('yyyy-MM-dd', str)LocalDate.parse(str, formatter)
Add days (legacy)date + 5calendar.add(Calendar.DAY, 5)
Subtract datesdate2 - date1ChronoUnit.DAYS.between(...)
Compare datesdate1 < date2date1.isBefore(date2)
DurationDuration.between(t1, t2)Duration.between(t1, t2)
Time zoneZonedDateTime.now(ZoneId.of(...))ZonedDateTime.now(ZoneId.of(...))
Epoch millisdate.time / Instant.now().toEpochMilli()date.getTime() / Instant.now().toEpochMilli()

Examples

Example 1: TimeCategory DSL – Date Arithmetic in Plain English

What we’re doing: Using Groovy’s TimeCategory to perform date arithmetic with a natural-language DSL that reads like English.

Example 1: TimeCategory DSL

import groovy.time.TimeCategory

def now = new Date()
println "Now: ${now.format('yyyy-MM-dd HH:mm')}"

use(TimeCategory) {
    // Adding time
    def tomorrow = now + 1.day
    def nextWeek = now + 1.week
    def inThreeHours = now + 3.hours
    def inNinetyMinutes = now + 90.minutes

    println "Tomorrow: ${tomorrow.format('yyyy-MM-dd')}"
    println "Next week: ${nextWeek.format('yyyy-MM-dd')}"
    println "In 3 hours: ${inThreeHours.format('HH:mm')}"
    println "In 90 min: ${inNinetyMinutes.format('HH:mm')}"

    // Subtracting time
    def yesterday = now - 1.day
    def lastMonth = now - 1.month
    def twoWeeksAgo = now - 2.weeks

    println "\nYesterday: ${yesterday.format('yyyy-MM-dd')}"
    println "Last month: ${lastMonth.format('yyyy-MM-dd')}"
    println "2 weeks ago: ${twoWeeksAgo.format('yyyy-MM-dd')}"

    // Compound durations
    def future = now + 2.years + 3.months + 5.days
    println "\n2y 3m 5d from now: ${future.format('yyyy-MM-dd')}"

    // "ago" and "from now" style
    def deadline = 10.days.from.now
    def started = 30.days.ago
    println "Deadline (10 days): ${deadline.format('yyyy-MM-dd')}"
    println "Started (30 days ago): ${started.format('yyyy-MM-dd')}"
}

Output

Now: 2026-03-12 10:30
Tomorrow: 2026-03-13
Next week: 2026-03-19
In 3 hours: 13:30
In 90 min: 12:00

Yesterday: 2026-03-11
Last month: 2026-02-12
2 weeks ago: 2026-02-26

2y 3m 5d from now: 2028-06-17
Deadline (10 days): 2026-03-22
Started (30 days ago): 2026-02-10

What happened here: TimeCategory is a Groovy category class that adds properties like .day, .hours, .weeks, .months, and .years to Integer. The use(TimeCategory) { } block activates these extensions within its scope. You can chain durations with +, and the DSL supports .from.now and .ago for readable date expressions. This is purely syntactic sugar over java.util.Date – the underlying operations use the legacy Date class. For new code, consider the java.time API (shown in later examples), but TimeCategory remains excellent for quick scripts and tests.

Example 2: java.time LocalDate – The Modern Approach

What we’re doing: Using Java’s modern java.time.LocalDate API in Groovy for date-only operations with Groovy’s operator overloading.

Example 2: LocalDate Basics

import java.time.LocalDate
import java.time.Month
import java.time.DayOfWeek
import java.time.temporal.ChronoUnit

// Creating dates
def today = LocalDate.now()
def specific = LocalDate.of(2026, 6, 15)
def parsed = LocalDate.parse('2026-12-25')
def fromMonth = LocalDate.of(2026, Month.JULY, 4)

println "Today: ${today}"
println "Specific: ${specific}"
println "Parsed: ${parsed}"
println "July 4th: ${fromMonth}"

// Date arithmetic with plus/minus methods
def nextWeek = today.plusWeeks(1)
def lastMonth = today.minusMonths(1)
def nextYear = today.plusYears(1)
println "\nNext week: ${nextWeek}"
println "Last month: ${lastMonth}"
println "Next year: ${nextYear}"

// Groovy operator overloading: + and - work with days
def threeDaysLater = today + 3    // plusDays(3)
def fiveDaysBefore = today - 5    // minusDays(5)
println "\n+3 days: ${threeDaysLater}"
println "-5 days: ${fiveDaysBefore}"

// Date properties
println "\nYear: ${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()}"

// Days between dates
def christmas = LocalDate.of(today.year, 12, 25)
def daysUntilChristmas = ChronoUnit.DAYS.between(today, christmas)
println "\nDays until Christmas: ${daysUntilChristmas}"

// Week start/end
def weekStart = today.with(DayOfWeek.MONDAY)
def weekEnd = today.with(DayOfWeek.SUNDAY)
println "Week start: ${weekStart}"
println "Week end: ${weekEnd}"

Output

Today: 2026-03-12
Specific: 2026-06-15
Parsed: 2026-12-25
July 4th: 2026-07-04

Next week: 2026-03-19
Last month: 2026-02-12
Next year: 2027-03-12

+3 days: 2026-03-15
-5 days: 2026-03-07

Year: 2026
Month: MARCH (3)
Day: 12
Day of week: THURSDAY
Day of year: 71
Leap year: false

Days until Christmas: 288
Week start: 2026-03-09
Week end: 2026-03-15

What happened here: LocalDate represents a date without time or time zone – perfect for birthdays, deadlines, and scheduling. Groovy adds operator overloading so today + 3 calls plusDays(3) and today - 5 calls minusDays(5). The with() method adjusts dates to specific criteria like the start of the week. All java.time objects are immutable – every operation returns a new instance, which makes them thread-safe by design. Use ChronoUnit.DAYS.between() for calculating the difference between two dates.

Example 3: LocalDateTime and Instant

What we’re doing: Working with date-and-time values and epoch-based timestamps using LocalDateTime and Instant.

Example 3: LocalDateTime and Instant

import java.time.LocalDateTime
import java.time.LocalDate
import java.time.LocalTime
import java.time.Instant
import java.time.ZoneOffset

// LocalDateTime - date + time, no timezone
def now = LocalDateTime.now()
println "Now: ${now}"

// Constructing specific date-times
def meeting = LocalDateTime.of(2026, 3, 15, 14, 30, 0)
println "Meeting: ${meeting}"

// Combining date and time
def date = LocalDate.of(2026, 6, 15)
def time = LocalTime.of(9, 0)
def combined = LocalDateTime.of(date, time)
println "Combined: ${combined}"

// LocalTime operations
def lunchTime = LocalTime.of(12, 30)
def endOfDay = LocalTime.of(17, 0)
println "\nLunch: ${lunchTime}"
println "End of day: ${endOfDay}"
println "Hours until EOD: ${java.time.Duration.between(lunchTime, endOfDay).toHours()}"

// Arithmetic
def later = now.plusHours(3).plusMinutes(30)
println "\n3h 30m later: ${later}"

// Extracting parts
println "\nHour: ${now.hour}"
println "Minute: ${now.minute}"
println "Second: ${now.second}"

// Instant - epoch-based timestamp
def instant = Instant.now()
println "\nInstant: ${instant}"
println "Epoch seconds: ${instant.epochSecond}"
println "Epoch millis: ${instant.toEpochMilli()}"

// Converting between types
def fromEpoch = Instant.ofEpochMilli(1741785600000L)
println "From epoch: ${fromEpoch}"

def toLocalDateTime = LocalDateTime.ofInstant(instant, ZoneOffset.UTC)
println "Instant to LocalDateTime (UTC): ${toLocalDateTime}"

// Comparing
def future = now.plusDays(1)
println "\nNow before future: ${now.isBefore(future)}"
println "Groovy style: ${now < future}"

Output

Now: 2026-03-12T10:30:15.123456
Meeting: 2026-03-15T14:30
Combined: 2026-06-15T09:00

Lunch: 12:30
End of day: 17:00
Hours until EOD: 4

3h 30m later: 2026-03-12T14:00:15.123456

Hour: 10
Minute: 30
Second: 15

Instant: 2026-03-12T10:30:15.123456Z
Epoch seconds: 1773578415
Epoch millis: 1773578415123

From epoch: 2025-03-12T12:00:00Z
Instant to LocalDateTime (UTC): 2026-03-12T10:30:15.123456

Now before future: true
Groovy style: true

What happened here: LocalDateTime holds date and time but no time zone – use it for local events like meetings or business hours. LocalTime represents time-of-day only. Instant is an epoch-based timestamp (milliseconds since 1970-01-01 UTC) – use it for logging, event timestamps, and anything that needs to be unambiguous across time zones. Groovy’s comparison operators (<, >, ==) work with all java.time types because they implement Comparable. Use isBefore() and isAfter() in Java; in Groovy, < and > are more readable.

Example 4: Date Formatting and Parsing

What we’re doing: Formatting dates to strings and parsing strings back to dates using both legacy and modern APIs.

Example 4: Formatting and Parsing

import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.text.SimpleDateFormat

// === Modern java.time formatting ===
def today = LocalDate.now()
def now = LocalDateTime.now()

// Built-in formatters
println "ISO date: ${today.format(DateTimeFormatter.ISO_LOCAL_DATE)}"
println "ISO datetime: ${now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)}"

// Custom patterns
def patterns = [
    'dd/MM/yyyy',
    'MMMM dd, yyyy',
    'EEE, MMM d yyyy',
    'yyyy-MM-dd HH:mm:ss',
    'h:mm a',
    'dd-MMM-yy'
]

println "\nCustom formats:"
patterns.each { pattern ->
    def formatter = DateTimeFormatter.ofPattern(pattern)
    try {
        println "  ${pattern.padRight(25)} -> ${now.format(formatter)}"
    } catch (e) {
        println "  ${pattern.padRight(25)} -> ${today.format(formatter)}"
    }
}

// Parsing strings to dates
println "\nParsing:"
def d1 = LocalDate.parse('2026-06-15')
def d2 = LocalDate.parse('15/06/2026', DateTimeFormatter.ofPattern('dd/MM/yyyy'))
def d3 = LocalDateTime.parse('2026-06-15 14:30:00',
    DateTimeFormatter.ofPattern('yyyy-MM-dd HH:mm:ss'))

println "Parsed d1: ${d1}"
println "Parsed d2: ${d2}"
println "Parsed d3: ${d3}"

// === Legacy Date formatting (Groovy GDK) ===
println "\nLegacy Date formatting:"
def legacyDate = new Date()
println "Groovy format: ${legacyDate.format('yyyy-MM-dd HH:mm:ss')}"
println "Short: ${legacyDate.format('dd-MMM-yyyy')}"
println "Date only: ${legacyDate.format('EEEE, MMMM dd, yyyy')}"

// Legacy parsing
def parsed = Date.parse('yyyy-MM-dd', '2026-07-04')
println "Parsed legacy: ${parsed.format('EEEE, MMMM dd, yyyy')}"

// Handling parse errors
try {
    LocalDate.parse('not-a-date')
} catch (Exception e) {
    println "\nParse error: ${e.getClass().simpleName}"
}

Output

ISO date: 2026-03-12
ISO datetime: 2026-03-12T10:30:15.123456

Custom formats:
  dd/MM/yyyy                -> 12/03/2026
  MMMM dd, yyyy             -> March 12, 2026
  EEE, MMM d yyyy           -> Thu, Mar 12 2026
  yyyy-MM-dd HH:mm:ss       -> 2026-03-12 10:30:15
  h:mm a                    -> 10:30 AM
  dd-MMM-yy                 -> 12-Mar-26

Parsing:
Parsed d1: 2026-06-15
Parsed d2: 2026-06-15
Parsed d3: 2026-06-15T14:30

Legacy Date formatting:
Groovy format: 2026-03-12 10:30:15
Short: 12-Mar-2026
Date only: Thursday, March 12, 2026
Parsed legacy: Saturday, July 04, 2026

Parse error: DateTimeParseException

What happened here: Groovy supports two formatting approaches. For java.time types, use DateTimeFormatter.ofPattern() with standard pattern letters (yyyy for year, MM for month, dd for day, HH for 24-hour, hh for 12-hour, a for AM/PM). For legacy Date, Groovy adds a .format() GDK method and a static Date.parse() method, both using SimpleDateFormat patterns. Always wrap parsing in try-catch since invalid input throws exceptions. For APIs and databases, stick to ISO 8601 format (yyyy-MM-dd); use localized formats only for display.

Example 5: Date Arithmetic with java.time

What we’re doing: Performing date and time arithmetic using the modern java.time API – adding, subtracting, and calculating periods between dates.

Example 5: Date Arithmetic

import java.time.LocalDate
import java.time.Period
import java.time.temporal.ChronoUnit

def today = LocalDate.of(2026, 3, 12)

// Plus and minus
println "Plus 30 days: ${today.plusDays(30)}"
println "Plus 3 months: ${today.plusMonths(3)}"
println "Minus 1 year: ${today.minusYears(1)}"

// Using Period for compound durations
def period = Period.of(1, 6, 15)  // 1 year, 6 months, 15 days
def futureDate = today.plus(period)
println "\nPlus 1y 6m 15d: ${futureDate}"

// Period between two dates
def birthday = LocalDate.of(1990, 7, 20)
def age = Period.between(birthday, today)
println "\nAge from ${birthday}:"
println "  Years: ${age.years}"
println "  Months: ${age.months}"
println "  Days: ${age.days}"
println "  Total: ${age.years} years, ${age.months} months, ${age.days} days"

// Days between
def startDate = LocalDate.of(2026, 1, 1)
def endDate = LocalDate.of(2026, 12, 31)
println "\nDays in 2026: ${ChronoUnit.DAYS.between(startDate, endDate)}"
println "Months: ${ChronoUnit.MONTHS.between(startDate, endDate)}"
println "Weeks: ${ChronoUnit.WEEKS.between(startDate, endDate)}"

// Business days calculation (skip weekends)
def countBusinessDays(LocalDate from, LocalDate to) {
    int count = 0
    def current = from
    while (current < to) {
        if (current.dayOfWeek.value <= 5) count++  // Mon=1 to Fri=5
        current = current + 1
    }
    count
}

def projectStart = LocalDate.of(2026, 3, 1)
def projectEnd = LocalDate.of(2026, 3, 31)
println "\nBusiness days in March 2026: ${countBusinessDays(projectStart, projectEnd)}"

// Groovy operator overloading with dates
println "\nGroovy operators:"
println "today + 7: ${today + 7}"          // plusDays(7)
println "today - 3: ${today - 3}"          // minusDays(3)
println "today ++ : ${today.plusDays(1)}"   // next day
println "today -- : ${today.minusDays(1)}"  // previous day

Output

Plus 30 days: 2026-04-11
Plus 3 months: 2026-06-12
Minus 1 year: 2025-03-12

Plus 1y 6m 15d: 2027-09-27

Age from 1990-07-20:
  Years: 35
  Months: 7
  Days: 20
  Total: 35 years, 7 months, 20 days

Days in 2026: 364
Months: 11
Weeks: 52

Business days in March 2026: 22

Groovy operators:
today + 7: 2026-03-19
today - 3: 2026-03-09
today ++ : 2026-03-13
today -- : 2026-03-11

What happened here: The java.time API provides Period (years, months, days) and ChronoUnit for date arithmetic. Period.between() gives you a human-readable breakdown (35 years, 7 months, 20 days), while ChronoUnit.DAYS.between() gives you a flat count. Groovy’s operator overloading means date + 7 adds 7 days and date - 3 subtracts 3 days, making date math more concise than Java’s plusDays() and minusDays(). The business days calculator shows a practical pattern – iterate and skip weekends.

Example 6: Duration – Measuring Time Spans

What we’re doing: Using java.time.Duration to measure and manipulate time-based amounts (hours, minutes, seconds, nanoseconds).

Example 6: Duration

import java.time.Duration
import java.time.LocalTime
import java.time.LocalDateTime
import java.time.temporal.ChronoUnit

// Creating durations
def twoHours = Duration.ofHours(2)
def thirtyMinutes = Duration.ofMinutes(30)
def fiveSeconds = Duration.ofSeconds(5)
def combined = twoHours.plus(thirtyMinutes).plus(fiveSeconds)

println "2 hours: ${twoHours}"
println "30 minutes: ${thirtyMinutes}"
println "5 seconds: ${fiveSeconds}"
println "Combined: ${combined}"

// Duration between times
def start = LocalTime.of(9, 0)
def end = LocalTime.of(17, 30)
def workDay = Duration.between(start, end)
println "\nWork day: ${workDay}"
println "Hours: ${workDay.toHours()}"
println "Minutes: ${workDay.toMinutes()}"
println "Seconds: ${workDay.toSeconds()}"

// Duration between date-times
def taskStart = LocalDateTime.of(2026, 3, 10, 9, 0, 0)
def taskEnd = LocalDateTime.of(2026, 3, 12, 17, 30, 0)
def taskDuration = Duration.between(taskStart, taskEnd)
println "\nTask duration: ${taskDuration}"
println "Total hours: ${taskDuration.toHours()}"
println "Total minutes: ${taskDuration.toMinutes()}"

// Formatting duration manually (java.time.Duration lacks nice toString)
def formatDuration(Duration d) {
    def hours = d.toHours()
    def minutes = d.toMinutesPart()
    def seconds = d.toSecondsPart()
    "${hours}h ${minutes}m ${seconds}s"
}

println "\nFormatted: ${formatDuration(taskDuration)}"
println "Formatted workday: ${formatDuration(workDay)}"

// Comparing durations
def short_ = Duration.ofMinutes(30)
def long_ = Duration.ofHours(2)
println "\n30min < 2h: ${short_ < long_}"
println "30min in seconds: ${short_.toSeconds()}"

// Practical: timing code execution
def codeStart = System.nanoTime()
Thread.sleep(100)
def elapsed = Duration.ofNanos(System.nanoTime() - codeStart)
println "\nCode took: ${elapsed.toMillis()}ms"

Output

2 hours: PT2H
30 minutes: PT30M
5 seconds: PT5S
Combined: PT2H30M5S

Work day: PT8H30M
Hours: 8
Minutes: 510
Seconds: 30600

Task duration: PT56H30M
Total hours: 56
Total minutes: 3390

Formatted: 56h 30m 0s
Formatted workday: 8h 30m 0s

30min < 2h: true
30min in seconds: 1800

Code took: 102ms

What happened here: Duration represents a time-based amount in seconds and nanoseconds – use it for measuring elapsed time, defining timeouts, and specifying intervals. The PT2H30M5S format is ISO 8601 duration notation (P=period, T=time, H=hours, M=minutes, S=seconds). Java 9+ added toMinutesPart() and toSecondsPart() for extracting individual parts, which makes formatting easier. Groovy’s comparison operators work because Duration implements Comparable. For measuring code performance, use System.nanoTime() wrapped in a Duration for clean output.

Example 7: Comparing Dates

What we’re doing: Comparing dates using Groovy operators, checking ranges, and sorting date collections.

Example 7: Date Comparisons

import java.time.LocalDate
import java.time.LocalDateTime

def today = LocalDate.of(2026, 3, 12)
def tomorrow = LocalDate.of(2026, 3, 13)
def lastWeek = LocalDate.of(2026, 3, 5)
def sameDay = LocalDate.of(2026, 3, 12)

// Comparison operators (Groovy's <=> delegates to compareTo)
println "today < tomorrow: ${today < tomorrow}"
println "today > lastWeek: ${today > lastWeek}"
println "today == sameDay: ${today == sameDay}"
println "today != tomorrow: ${today != tomorrow}"
println "today <=> tomorrow: ${today <=> tomorrow}"

// Range checks
def projectStart = LocalDate.of(2026, 3, 1)
def projectEnd = LocalDate.of(2026, 3, 31)
def checkDate = LocalDate.of(2026, 3, 15)

// Is a date within a range?
def inRange = !checkDate.isBefore(projectStart) && !checkDate.isAfter(projectEnd)
println "\n${checkDate} in project range: ${inRange}"

// Groovy range (works because LocalDate implements Comparable)
def dateRange = projectStart..projectEnd
println "Range contains ${checkDate}: ${checkDate in dateRange}"
println "Range size: ${dateRange.size()} days"

// Sorting dates
def dates = [
    LocalDate.of(2026, 12, 25),
    LocalDate.of(2026, 1, 1),
    LocalDate.of(2026, 7, 4),
    LocalDate.of(2026, 3, 17),
    LocalDate.of(2026, 10, 31)
]

println "\nUnsorted: ${dates}"
println "Sorted: ${dates.sort()}"
println "Latest: ${dates.max()}"
println "Earliest: ${dates.min()}"

// Finding dates matching criteria
def holidays = [
    [name: 'New Year', date: LocalDate.of(2026, 1, 1)],
    [name: "St Patrick's", date: LocalDate.of(2026, 3, 17)],
    [name: 'Independence Day', date: LocalDate.of(2026, 7, 4)],
    [name: 'Halloween', date: LocalDate.of(2026, 10, 31)],
    [name: 'Christmas', date: LocalDate.of(2026, 12, 25)]
]

def upcoming = holidays.findAll { it.date > today }
    .sort { it.date }
    .collect { "${it.name} (${it.date})" }
println "\nUpcoming holidays: ${upcoming.join(', ')}"

def nextHoliday = holidays.findAll { it.date >= today }.min { it.date }
println "Next holiday: ${nextHoliday.name} on ${nextHoliday.date}"

Output

today < tomorrow: true
today > lastWeek: true
today == sameDay: true
today != tomorrow: true
today <=> tomorrow: -1

2026-03-15 in project range: true
Range contains 2026-03-15: true
Range size: 31 days

Unsorted: [2026-12-25, 2026-01-01, 2026-07-04, 2026-03-17, 2026-10-31]
Sorted: [2026-01-01, 2026-03-17, 2026-07-04, 2026-10-31, 2026-12-25]
Latest: 2026-12-25
Earliest: 2026-01-01

Upcoming holidays: St Patrick's (2026-03-17), Independence Day (2026-07-04), Halloween (2026-10-31), Christmas (2026-12-25)
Next holiday: St Patrick's on 2026-03-17

What happened here: Groovy’s operator overloading makes date comparisons natural – <, >, ==, and <=> all work on any Comparable, including LocalDate. Date ranges with .. create iterable sequences you can check membership with in. The sort(), min(), and max() GDK methods work on date collections because of Comparable support. The holiday example shows a practical pattern: filter upcoming events, sort by date, and find the next one – all in a few lines of Groovy.

Example 8: Time Zones with ZonedDateTime

What we’re doing: Working with time zones using ZonedDateTime and ZoneId for global applications.

Example 8: Time Zones

import java.time.ZonedDateTime
import java.time.ZoneId
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.time.LocalDateTime

// Current time in different zones
def zones = ['America/New_York', 'Europe/London', 'Asia/Tokyo',
             'Australia/Sydney', 'Asia/Kolkata']

def utcNow = ZonedDateTime.now(ZoneId.of('UTC'))
println "UTC: ${utcNow.format(DateTimeFormatter.ofPattern('yyyy-MM-dd HH:mm z'))}"

zones.each { zone ->
    def zdt = utcNow.withZoneSameInstant(ZoneId.of(zone))
    println "${zone.padRight(22)} ${zdt.format(DateTimeFormatter.ofPattern('yyyy-MM-dd HH:mm z'))}"
}

// Creating zoned date-time
def nyMeeting = ZonedDateTime.of(2026, 3, 15, 14, 0, 0, 0,
    ZoneId.of('America/New_York'))
println "\nMeeting (NY): ${nyMeeting}"

// Convert to other zones
def londonTime = nyMeeting.withZoneSameInstant(ZoneId.of('Europe/London'))
def tokyoTime = nyMeeting.withZoneSameInstant(ZoneId.of('Asia/Tokyo'))
println "Same meeting (London): ${londonTime.format(DateTimeFormatter.ofPattern('HH:mm z'))}"
println "Same meeting (Tokyo): ${tokyoTime.format(DateTimeFormatter.ofPattern('HH:mm z'))}"

// Zone offset info
def ny = ZoneId.of('America/New_York')
def rules = ny.rules
def offset = rules.getOffset(java.time.Instant.now())
println "\nNY offset: ${offset}"
println "Is DST: ${rules.isDaylightSavings(java.time.Instant.now())}"

// Available zone IDs (sample)
def allZones = ZoneId.availableZoneIds
println "\nTotal zones available: ${allZones.size()}"
println "US zones: ${allZones.findAll { it.startsWith('US/') }.sort()}"

// Converting LocalDateTime to ZonedDateTime
def local = LocalDateTime.of(2026, 6, 15, 10, 0)
def zoned = local.atZone(ZoneId.of('Europe/Paris'))
println "\nLocal to zoned: ${zoned}"
println "As UTC: ${zoned.withZoneSameInstant(ZoneId.of('UTC'))}"

Output

UTC: 2026-03-12 10:30 UTC
America/New_York       2026-03-12 06:30 EDT
Europe/London          2026-03-12 10:30 GMT
Asia/Tokyo             2026-03-12 19:30 JST
Australia/Sydney       2026-03-12 21:30 AEDT
Asia/Kolkata           2026-03-12 16:00 IST

Meeting (NY): 2026-03-15T14:00-04:00[America/New_York]
Same meeting (London): 18:00 GMT
Same meeting (Tokyo): 03:00 JST

NY offset: -04:00
Is DST: true

Total zones available: 603
US zones: [US/Alaska, US/Aleutian, US/Arizona, US/Central, US/East-Indiana, US/Eastern, US/Hawaii, US/Indiana-Starke, US/Michigan, US/Mountain, US/Pacific, US/Samoa]

Local to zoned: 2026-06-15T10:00+02:00[Europe/Paris]
As UTC: 2026-06-15T08:00Z[UTC]

What happened here: ZonedDateTime is the right type when time zones matter – scheduling across regions, converting meeting times, or displaying local times to users. The key method is withZoneSameInstant() – it converts to another zone while preserving the exact moment in time. Use ZoneId.of("America/New_York") with IANA zone names, never abbreviations like “EST” (they are ambiguous). DST transitions are handled automatically by the ZoneRules. Remember: store timestamps in UTC, convert to local zones only for display.

Example 9: Legacy Date GDK Methods

What we’re doing: Exploring the GDK (Groovy Development Kit) methods added to java.util.Date and Calendar for legacy code compatibility.

Example 9: Legacy GDK Methods

// Groovy adds these methods to java.util.Date:
def now = new Date()
println "Now: ${now}"

// format() - uses SimpleDateFormat
println "Formatted: ${now.format('yyyy-MM-dd HH:mm:ss')}"
println "Date only: ${now.format('EEEE, MMMM dd')}"

// Date arithmetic with + and -
def tomorrow = now + 1         // plus 1 day
def yesterday = now - 1        // minus 1 day
def nextWeek = now + 7
println "\nTomorrow: ${tomorrow.format('yyyy-MM-dd')}"
println "Yesterday: ${yesterday.format('yyyy-MM-dd')}"
println "Next week: ${nextWeek.format('yyyy-MM-dd')}"

// clearTime() - sets time to midnight
def dateOnly = new Date().clearTime()
println "\nCleared time: ${dateOnly.format('yyyy-MM-dd HH:mm:ss')}"

// Static parse()
def parsed = Date.parse('yyyy-MM-dd', '2026-07-04')
println "Parsed: ${parsed.format('EEEE, MMMM dd, yyyy')}"

// updated() - set specific fields
def modified = now.updated(
    year: 2025,
    month: Calendar.DECEMBER,
    date: 25,
    hours: 0,
    minutes: 0,
    seconds: 0
)
println "\nModified: ${modified.format('yyyy-MM-dd HH:mm:ss')}"

// copyWith() - clone with changes
def copy = now.copyWith(
    year: 2026,
    month: Calendar.JUNE,
    date: 15
)
println "Copy: ${copy.format('yyyy-MM-dd HH:mm:ss')}"

// Date comparison
def date1 = Date.parse('yyyy-MM-dd', '2026-01-01')
def date2 = Date.parse('yyyy-MM-dd', '2026-12-31')
println "\nJan < Dec: ${date1 < date2}"
println "Jan == Jan: ${date1 == Date.parse('yyyy-MM-dd', '2026-01-01')}"

// Iterating dates
def start = Date.parse('yyyy-MM-dd', '2026-03-10')
def end = Date.parse('yyyy-MM-dd', '2026-03-14')
println "\nDates in range:"
(start..end).each { d ->
    println "  ${d.format('yyyy-MM-dd (EEE)')}"
}

Output

Now: Thu Mar 12 10:30:15 EDT 2026
Formatted: 2026-03-12 10:30:15
Date only: Thursday, March 12

Tomorrow: 2026-03-13
Yesterday: 2026-03-11
Next week: 2026-03-19

Cleared time: 2026-03-12 00:00:00
Parsed: Saturday, July 04, 2026

Modified: 2025-12-25 00:00:00
Copy: 2026-06-15 10:30:15

Jan < Dec: true
Jan == Jan: true

Dates in range:
  2026-03-10 (Tue)
  2026-03-11 (Wed)
  2026-03-12 (Thu)
  2026-03-13 (Fri)
  2026-03-14 (Sat)

What happened here: Groovy adds convenience methods to java.util.Date through the GDK. The + and - operators add/subtract days. format() wraps SimpleDateFormat. clearTime() zeros out hours, minutes, and seconds – useful for date-only comparisons. updated() and copyWith() let you modify specific fields. Date ranges with .. work because Groovy implements next() and previous() on Date. While these methods are convenient for maintaining legacy code, prefer java.time for all new development – it is immutable, thread-safe, and more predictable.

Example 10: Parsing Dates from Various Formats

What we’re doing: Parsing date strings from different formats you commonly encounter in APIs, CSV files, logs, and user input.

Example 10: Parsing Various Formats

import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZonedDateTime
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeFormatterBuilder
import java.time.temporal.ChronoField

// Common formats you'll encounter
def formats = [
    '2026-03-12'                     : 'yyyy-MM-dd',
    '12/03/2026'                     : 'dd/MM/yyyy',
    '03/12/2026'                     : 'MM/dd/yyyy',
    'March 12, 2026'                 : 'MMMM dd, yyyy',
    '12-Mar-2026'                    : 'dd-MMM-yyyy',
    '2026-03-12T14:30:00'            : "yyyy-MM-dd'T'HH:mm:ss",
    '2026-03-12 14:30:00'            : 'yyyy-MM-dd HH:mm:ss',
]

println "Parsing various formats:"
formats.each { dateStr, pattern ->
    def formatter = DateTimeFormatter.ofPattern(pattern)
    try {
        def parsed = LocalDateTime.parse(dateStr, formatter)
        println "  ${dateStr.padRight(30)} -> ${parsed}"
    } catch (Exception e) {
        def parsed = LocalDate.parse(dateStr, formatter)
        println "  ${dateStr.padRight(30)} -> ${parsed}"
    }
}

// ISO 8601 with timezone
def isoString = '2026-03-12T14:30:00+05:30'
def offsetDt = OffsetDateTime.parse(isoString)
println "\nISO with offset: ${offsetDt}"
println "UTC equivalent: ${offsetDt.withOffsetSameInstant(java.time.ZoneOffset.UTC)}"

// Flexible parser that handles optional parts
def flexFormatter = new DateTimeFormatterBuilder()
    .appendPattern('yyyy-MM-dd')
    .optionalStart()
    .appendPattern(" HH:mm")
    .optionalStart()
    .appendPattern(":ss")
    .optionalEnd()
    .optionalEnd()
    .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
    .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
    .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
    .toFormatter()

def testInputs = ['2026-03-12', '2026-03-12 14:30', '2026-03-12 14:30:45']

println "\nFlexible parser:"
testInputs.each { input ->
    def parsed = LocalDateTime.parse(input, flexFormatter)
    println "  ${input.padRight(25)} -> ${parsed}"
}

// Safe parsing helper
def safeParse(String dateStr, String... patterns) {
    for (pattern in patterns) {
        try {
            return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern(pattern))
        } catch (Exception ignored) {}
    }
    return null
}

println "\nSafe parsing:"
['2026-03-12', '12/03/2026', 'March 12, 2026', 'invalid'].each { s ->
    def result = safeParse(s, 'yyyy-MM-dd', 'dd/MM/yyyy', 'MMMM dd, yyyy')
    println "  ${s.padRight(20)} -> ${result ?: 'FAILED'}"
}

Output

Parsing various formats:
  2026-03-12                     -> 2026-03-12
  12/03/2026                     -> 2026-03-12
  03/12/2026                     -> 2026-03-12
  March 12, 2026                 -> 2026-03-12
  12-Mar-2026                    -> 2026-03-12
  2026-03-12T14:30:00            -> 2026-03-12T14:30
  2026-03-12 14:30:00            -> 2026-03-12T14:30

ISO with offset: 2026-03-12T14:30+05:30
UTC equivalent: 2026-03-12T09:00Z

Flexible parser:
  2026-03-12              -> 2026-03-12T00:00
  2026-03-12 14:30        -> 2026-03-12T14:30
  2026-03-12 14:30:45     -> 2026-03-12T14:30:45

Safe parsing:
  2026-03-12           -> 2026-03-12
  12/03/2026           -> 2026-03-12
  March 12, 2026       -> 2026-03-12
  invalid              -> FAILED

What happened here: Real-world applications receive dates in many formats – APIs use ISO 8601, CSV files use locale-dependent formats, and users type whatever they feel like. The DateTimeFormatterBuilder with optionalStart() and parseDefaulting() creates flexible parsers that handle missing parts gracefully. The safeParse() helper tries multiple patterns and returns null on failure instead of throwing exceptions – this is a production pattern for handling unknown date formats. Always validate parsed dates, especially when dd/MM and MM/dd could both be valid interpretations.

Example 11: Practical Recipe – Date Range Generation

What we’re doing: Generating date ranges, iterating by week, and building monthly and weekly reports – common tasks in business applications.

Example 11: Date Ranges

import java.time.LocalDate
import java.time.DayOfWeek
import java.time.YearMonth
import java.time.temporal.TemporalAdjusters

def today = LocalDate.of(2026, 3, 12)

// Generate all dates in a month
def march = YearMonth.of(2026, 3)
def firstDay = march.atDay(1)
def lastDay = march.atEndOfMonth()
println "March 2026: ${firstDay} to ${lastDay} (${march.lengthOfMonth()} days)"

// All Mondays in March
def mondays = []
def current = firstDay.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY))
while (current <= lastDay) {
    mondays << current
    current = current.plusWeeks(1)
}
println "Mondays: ${mondays}"

// Generate weekly date ranges
def generateWeeks(LocalDate start, LocalDate end) {
    def weeks = []
    def weekStart = start
    while (weekStart <= end) {
        def weekEnd = [weekStart.plusDays(6), end].min()
        weeks << [start: weekStart, end: weekEnd]
        weekStart = weekStart.plusWeeks(1)
    }
    weeks
}

println "\nWeeks in March:"
generateWeeks(firstDay, lastDay).eachWithIndex { week, i ->
    println "  Week ${i + 1}: ${week.start} to ${week.end}"
}

// Generate all months in a year
def months2026 = (1..12).collect { YearMonth.of(2026, it) }
println "\nMonths in 2026:"
months2026.each { m ->
    println "  ${m}: ${m.lengthOfMonth()} days, starts on ${m.atDay(1).dayOfWeek}"
}

// First and last business day of month
def firstBusinessDay = firstDay
while (firstBusinessDay.dayOfWeek.value > 5) {
    firstBusinessDay = firstBusinessDay.plusDays(1)
}
def lastBusinessDay = lastDay
while (lastBusinessDay.dayOfWeek.value > 5) {
    lastBusinessDay = lastBusinessDay.minusDays(1)
}
println "\nFirst business day: ${firstBusinessDay} (${firstBusinessDay.dayOfWeek})"
println "Last business day: ${lastBusinessDay} (${lastBusinessDay.dayOfWeek})"

// Quarter dates
def quarter = ((today.monthValue - 1) / 3).intValue() + 1
def quarterStart = LocalDate.of(today.year, (quarter - 1) * 3 + 1, 1)
def quarterEnd = quarterStart.plusMonths(3).minusDays(1)
println "\nQ${quarter} 2026: ${quarterStart} to ${quarterEnd}"

Output

March 2026: 2026-03-01 to 2026-03-31 (31 days)
Mondays: [2026-03-02, 2026-03-09, 2026-03-16, 2026-03-23, 2026-03-30]

Weeks in March:
  Week 1: 2026-03-01 to 2026-03-07
  Week 2: 2026-03-08 to 2026-03-14
  Week 3: 2026-03-15 to 2026-03-21
  Week 4: 2026-03-22 to 2026-03-28
  Week 5: 2026-03-29 to 2026-03-31

Months in 2026:
  2026-01: 31 days, starts on THURSDAY
  2026-02: 28 days, starts on SUNDAY
  2026-03: 31 days, starts on SUNDAY
  2026-04: 30 days, starts on WEDNESDAY
  2026-05: 31 days, starts on FRIDAY
  2026-06: 30 days, starts on MONDAY
  2026-07: 31 days, starts on WEDNESDAY
  2026-08: 31 days, starts on SATURDAY
  2026-09: 30 days, starts on TUESDAY
  2026-10: 31 days, starts on THURSDAY
  2026-11: 30 days, starts on SUNDAY
  2026-12: 31 days, starts on TUESDAY

First business day: 2026-03-02 (MONDAY)
Last business day: 2026-03-31 (TUESDAY)

Q1 2026: 2026-01-01 to 2026-03-31

What happened here: YearMonth represents a month without a specific day – ideal for monthly reports and billing cycles. TemporalAdjusters provides adjusters like nextOrSame(DayOfWeek.MONDAY) to find specific days. The weekly range generator is a common pattern for sprint planning and reporting dashboards. Business day calculations (skipping weekends) appear in payroll, billing, and SLA systems. Quarter calculation uses integer math on the month value. These recipes compose well – combine them to build date-driven features in your applications.

Example 12: Converting Between Date Types

What we’re doing: Converting between legacy java.util.Date, Calendar, and modern java.time types – essential when working with APIs that use different date representations.

Example 12: Type Conversions

import java.time.*
import java.sql.Timestamp

// === Legacy Date to java.time ===
def legacyDate = new Date()

// Date -> Instant
def instant = legacyDate.toInstant()
println "Date -> Instant: ${instant}"

// Date -> LocalDateTime (need a zone)
def localDt = LocalDateTime.ofInstant(legacyDate.toInstant(), ZoneId.systemDefault())
println "Date -> LocalDateTime: ${localDt}"

// Date -> LocalDate
def localDate = legacyDate.toInstant()
    .atZone(ZoneId.systemDefault())
    .toLocalDate()
println "Date -> LocalDate: ${localDate}"

// Date -> ZonedDateTime
def zonedDt = legacyDate.toInstant().atZone(ZoneId.systemDefault())
println "Date -> ZonedDateTime: ${zonedDt}"

// === java.time to Legacy Date ===
def nowLocal = LocalDateTime.now()

// LocalDateTime -> Date
def backToDate = Date.from(nowLocal.atZone(ZoneId.systemDefault()).toInstant())
println "\nLocalDateTime -> Date: ${backToDate}"

// LocalDate -> Date (at start of day)
def dateOnly = LocalDate.of(2026, 6, 15)
def asDate = Date.from(dateOnly.atStartOfDay(ZoneId.systemDefault()).toInstant())
println "LocalDate -> Date: ${asDate}"

// Instant -> Date
def fromInstant = Date.from(Instant.now())
println "Instant -> Date: ${fromInstant}"

// === Calendar conversions ===
def calendar = Calendar.getInstance()

// Calendar -> LocalDateTime
def calToLocal = LocalDateTime.ofInstant(
    calendar.toInstant(),
    calendar.timeZone.toZoneId()
)
println "\nCalendar -> LocalDateTime: ${calToLocal}"

// LocalDateTime -> Calendar
def calBack = Calendar.getInstance()
calBack.time = Date.from(nowLocal.atZone(ZoneId.systemDefault()).toInstant())
println "LocalDateTime -> Calendar: ${calBack.time}"

// === SQL types ===
def sqlDate = java.sql.Date.valueOf(LocalDate.now())
def sqlTimestamp = Timestamp.valueOf(LocalDateTime.now())
println "\nSQL Date: ${sqlDate}"
println "SQL Timestamp: ${sqlTimestamp}"

// SQL -> java.time
println "SQL Date -> LocalDate: ${sqlDate.toLocalDate()}"
println "SQL Timestamp -> LocalDateTime: ${sqlTimestamp.toLocalDateTime()}"

// === Epoch conversions ===
def epochMillis = System.currentTimeMillis()
def fromEpoch = Instant.ofEpochMilli(epochMillis)
println "\nEpoch millis: ${epochMillis}"
println "From epoch: ${fromEpoch}"
println "To LocalDate: ${fromEpoch.atZone(ZoneId.systemDefault()).toLocalDate()}"

Output

Date -> Instant: 2026-03-12T14:30:15.123Z
Date -> LocalDateTime: 2026-03-12T10:30:15.123
Date -> LocalDate: 2026-03-12
Date -> ZonedDateTime: 2026-03-12T10:30:15.123-04:00[America/New_York]

LocalDateTime -> Date: Thu Mar 12 10:30:15 EDT 2026
LocalDate -> Date: Mon Jun 15 00:00:00 EDT 2026
Instant -> Date: Thu Mar 12 10:30:15 EDT 2026

Calendar -> LocalDateTime: 2026-03-12T10:30:15.123
LocalDateTime -> Calendar: Thu Mar 12 10:30:15 EDT 2026

SQL Date: 2026-03-12
SQL Timestamp: 2026-03-12 10:30:15.123456

SQL Date -> LocalDate: 2026-03-12
SQL Timestamp -> LocalDateTime: 2026-03-12T10:30:15.123456

Epoch millis: 1773578415123
From epoch: 2026-03-12T14:30:15.123Z
To LocalDate: 2026-03-12

What happened here: In real projects, you will encounter all date types – legacy Date from older libraries, Calendar from XML/JSON parsers, java.sql.Date and Timestamp from JDBC, and modern java.time types. The bridge between old and new is Instant – convert to Instant first, then to your target type. Going from LocalDateTime to Date requires a ZoneId because Date is inherently UTC-epoch-based while LocalDateTime has no zone. SQL types have direct conversion methods since Java 8. Keep this example as a reference – these conversions come up in every project that uses multiple date libraries.

Example 13: Practical Recipe – Scheduling and Cron-like Patterns

What we’re doing: Building a simple scheduler that finds the next occurrence of recurring events – useful for cron-like scheduling without external libraries.

Example 13: Date-Based Scheduling

import java.time.LocalDate
import java.time.LocalDateTime
import java.time.DayOfWeek
import java.time.temporal.TemporalAdjusters

def now = LocalDateTime.of(2026, 3, 12, 10, 30)
def today = now.toLocalDate()

// Find next occurrence of a specific day of week
def nextFriday = today.with(TemporalAdjusters.next(DayOfWeek.FRIDAY))
def nextMonday = today.with(TemporalAdjusters.next(DayOfWeek.MONDAY))
println "Next Friday: ${nextFriday}"
println "Next Monday: ${nextMonday}"

// First Monday of next month
def firstMondayNextMonth = today
    .plusMonths(1)
    .with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY))
println "First Monday next month: ${firstMondayNextMonth}"

// Last Friday of current month
def lastFriday = today.with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY))
println "Last Friday this month: ${lastFriday}"

// Recurring event generator
def generateRecurring(LocalDate start, int occurrences, Closure nextDate) {
    def dates = [start]
    def current = start
    (occurrences - 1).times {
        current = nextDate(current)
        dates << current
    }
    dates
}

// Every 2 weeks (bi-weekly meetings)
def biweekly = generateRecurring(today, 6) { it.plusWeeks(2) }
println "\nBi-weekly meetings:"
biweekly.each { println "  ${it} (${it.dayOfWeek})" }

// Monthly on the 15th
def monthly = generateRecurring(LocalDate.of(2026, 3, 15), 6) { it.plusMonths(1) }
println "\nMonthly on 15th:"
monthly.each { println "  ${it}" }

// First Monday of each month
def firstMondays = generateRecurring(
    LocalDate.of(2026, 1, 1).with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)),
    6
) { it.plusMonths(1).with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)) }
println "\nFirst Mondays:"
firstMondays.each { println "  ${it}" }

// Is today a special date?
def isWeekend = today.dayOfWeek in [DayOfWeek.SATURDAY, DayOfWeek.SUNDAY]
def isMonthEnd = today == today.with(TemporalAdjusters.lastDayOfMonth())
def isQuarterEnd = today.monthValue % 3 == 0 && isMonthEnd

println "\n${today} checks:"
println "  Weekend: ${isWeekend}"
println "  Month end: ${isMonthEnd}"
println "  Quarter end: ${isQuarterEnd}"
println "  Day of week: ${today.dayOfWeek}"

Output

Next Friday: 2026-03-13
Next Monday: 2026-03-16
First Monday next month: 2026-04-06
Last Friday this month: 2026-03-27

Bi-weekly meetings:
  2026-03-12 (THURSDAY)
  2026-03-26 (THURSDAY)
  2026-04-09 (THURSDAY)
  2026-04-23 (THURSDAY)
  2026-05-07 (THURSDAY)
  2026-05-21 (THURSDAY)

Monthly on 15th:
  2026-03-15
  2026-04-15
  2026-05-15
  2026-06-15
  2026-07-15
  2026-08-15

First Mondays:
  2026-01-05
  2026-02-02
  2026-03-02
  2026-04-06
  2026-05-04
  2026-06-01

2026-03-12 checks:
  Weekend: false
  Month end: false
  Quarter end: false
  Day of week: THURSDAY

What happened here: TemporalAdjusters is the power tool for date scheduling. Methods like next(), firstInMonth(), and lastInMonth() find specific days without manual iteration. The generateRecurring() function takes a closure that defines the recurrence rule – making it reusable for any pattern: weekly, bi-weekly, monthly, or custom. Groovy’s in operator checks if the day of week is in a list, making weekend checks readable. These patterns are the building blocks for scheduling features, calendar applications, and recurring billing systems.

Common Pitfalls

Date-time code is notoriously error-prone. Here are the traps that catch developers most often.

Pitfall 1: Mutable Legacy Date

Bad: Shared Mutable Date

// BAD - java.util.Date is mutable, sharing it causes bugs
def startDate = new Date()
def event = [name: 'Meeting', date: startDate]
startDate.time = 0  // Modifies the Date inside the event too!
println "Event date was silently changed: ${event.date}"

Good: Use Immutable java.time

import java.time.LocalDate
// GOOD - java.time types are immutable, safe to share
def startDate = LocalDate.of(2026, 3, 12)
def event = [name: 'Meeting', date: startDate]
// startDate cannot be modified - any operation returns a new instance
def newDate = startDate.plusDays(1)  // Creates new object
println "Event date unchanged: ${event.date}"

Pitfall 2: Month Numbering

Bad: Calendar Month is Zero-Based

// BAD - Calendar months are 0-based (January = 0)
def cal = Calendar.getInstance()
cal.set(2026, 3, 12)  // This is APRIL, not March!
println "Expected March, got: ${cal.time.format('MMMM')}"  // April

Good: Use Constants or java.time

import java.time.LocalDate
// GOOD - java.time months are 1-based (January = 1)
def date = LocalDate.of(2026, 3, 12)  // March, as expected
println "Correct: ${date.month}"  // MARCH

// If using Calendar, use constants
def cal = Calendar.getInstance()
cal.set(2026, Calendar.MARCH, 12)  // Clear and correct

Pitfall 3: TimeCategory Scope Leaking

Bad: Using TimeCategory Methods Outside use Block

import groovy.time.TimeCategory
// BAD - TimeCategory methods don't exist outside use() block
// def tomorrow = 1.day.from.now  // MissingPropertyException!

// GOOD - keep TimeCategory inside use() block
use(TimeCategory) {
    def tomorrow = 1.day.from.now
    println "Tomorrow: ${tomorrow.format('yyyy-MM-dd')}"
}

Conclusion

Groovy gives you the best of both worlds for date-time handling. The TimeCategory DSL makes quick date arithmetic read like English – perfect for scripts, tests, and prototyping. The modern java.time API provides immutable, thread-safe types for production code, and Groovy’s operator overloading makes it more concise than Java. Legacy Date and Calendar classes get GDK enhancements that reduce boilerplate when you cannot avoid them.

The key rules to remember: use LocalDate for dates without time, LocalDateTime for date-and-time without zones, ZonedDateTime when time zones matter, and Instant for machine timestamps. Store dates in UTC, convert to local zones only for display. Prefer java.time over legacy Date for all new code – it is safer, clearer, and handles edge cases (DST, leap years, month boundaries) correctly.

To understand how Groovy and Java interact under the hood, see our previous post on Groovy-Java Interoperability. For file-based date operations and timestamp handling in configuration files, check Groovy Property Files. For automating date-dependent tasks, see Groovy Execute Commands.

Best Practices

  • DO use java.time types (LocalDate, LocalDateTime, ZonedDateTime) for all new code.
  • DO store timestamps in UTC and convert to local time zones only for display.
  • DO use DateTimeFormatter for formatting and parsing – it is thread-safe, unlike SimpleDateFormat.
  • DO use TemporalAdjusters for finding specific days (first Monday, last Friday, etc.).
  • DO wrap date parsing in try-catch and provide helpful error messages to users.
  • DON’T use java.util.Date or Calendar in new code – they are mutable and error-prone.
  • DON’T use SimpleDateFormat in multi-threaded code – it is not thread-safe.
  • DON’T assume month numbering – Calendar is 0-based, java.time is 1-based.
  • DON’T share mutable Date instances – clone them or convert to immutable java.time types.
  • DON’T use time zone abbreviations like “EST” – use IANA names like “America/New_York” instead.

Up next: Groovy Execute Commands – Running System Commands

Frequently Asked Questions

What is Groovy TimeCategory and how do I use it?

TimeCategory is a Groovy category class that adds date arithmetic methods to integers, allowing you to write expressions like 3.days.ago, 2.hours.from.now, and 1.week + 3.days. Use it inside a use(TimeCategory) { } block. The DSL methods are only available within that block scope. TimeCategory works with java.util.Date under the hood – for production code with the modern java.time API, use plusDays(), plusHours(), etc., or Groovy’s + and - operators.

Should I use java.util.Date or java.time in Groovy?

Use java.time for all new code. The modern API (LocalDate, LocalDateTime, ZonedDateTime, Instant) is immutable, thread-safe, and has clearer semantics. The legacy java.util.Date is mutable (sharing instances causes bugs), has confusing month numbering (0-based), and SimpleDateFormat is not thread-safe. Groovy enhances both APIs, but the java.time extensions plus operator overloading make modern date code in Groovy more concise than even Java itself.

How do I convert between java.util.Date and java.time types in Groovy?

The bridge is Instant. To convert Date to LocalDateTime: LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()). To convert back: Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()). For LocalDate, use date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(). SQL types have direct methods: java.sql.Date.valueOf(localDate) and sqlDate.toLocalDate().

How does Groovy handle date comparison and sorting?

Groovy’s operator overloading makes date comparison natural. Use <, >, ==, <=, >= on any java.time type – they all implement Comparable. For example, date1 < date2 is equivalent to date1.isBefore(date2). You can sort date collections with .sort(), find extremes with .min() and .max(), and use date ranges with date1..date2. The in operator checks range membership: checkDate in (startDate..endDate).

How do I handle time zones correctly in Groovy?

Use ZonedDateTime when time zones matter. Create with ZonedDateTime.now(ZoneId.of('America/New_York')) and convert between zones with withZoneSameInstant(). Always use IANA zone names (America/New_York) instead of abbreviations (EST), which are ambiguous. Store timestamps in UTC (use Instant or ZonedDateTime with UTC zone), and convert to local zones only when displaying to users. The JVM handles DST transitions automatically when you use proper ZoneId values.

Previous in Series: Groovy-Java Interoperability – Smooth JVM Integration

Next in Series: Groovy Execute Commands – Running System Commands

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 *