Date and Time Formatting in Lucee

Introduced: 5.0

Date and Time Formatting in Lucee

Lucee provides powerful and flexible date and time formatting capabilities that support multiple locales, timezones, and custom format patterns. Whether you need basic date display, internationalized formatting, or complex timezone handling, Lucee's date formatting functions offer comprehensive solutions for any application.

Overview

Date and time formatting in Lucee allows you to convert date objects into human-readable strings according to specific patterns, locales, and timezones. The formatting system supports everything from simple date displays to complex internationalized applications with multiple timezone support.

How It Works

Lucee's date formatting system works by taking a date object and applying formatting rules based on:

  1. Format patterns - Define how the date should appear (e.g., "yyyy-MM-dd")
  2. Locales - Determine language and regional formatting conventions
  3. Timezones - Handle timezone conversions and display
  4. Date objects - Work with various date types including Date, DateTime, and Time objects

Important: Lucee uses java.util.Date internally, which never holds timezone information. Date objects represent a specific moment in time (milliseconds since Unix epoch). Timezone and locale are only applied when:

  • Creating/Parsing a date from human-readable values (to interpret what moment in time those values represent)
  • Formatting a date object into a string (to display that moment in a specific timezone)

Once created, the date object itself has no timezone - it's just a moment in time.

// Timezone used during CREATION to interpret the input values
eventDate = createDateTime(2025, 6, 20, 12, 0, 0, 0, "Europe/Berlin");
// This means: "Create a date representing the moment when it was 12:00 in Berlin"

// The resulting eventDate object has NO timezone information attached // It's just a absolute moment in time (milliseconds since epoch)
// Timezone used during FORMATTING to display that moment echo(dateTimeFormat(eventDate, "HH:mm Z", "UTC")); // 10:00 +0000 echo(dateTimeFormat(eventDate, "HH:mm Z", "America/New_York")); // 06:00 -0400 echo(dateTimeFormat(eventDate, "HH:mm Z", "Asia/Tokyo")); // 19:00 +0900
// Same date object (same moment in time), different timezone displays

The formatting functions process these inputs and return formatted strings suitable for display or storage.

Configuration

You can configure default locale and timezone settings at different levels:

Global Configuration

Set defaults in Lucee Admin or .CFConfig.json:

{
  "locale": "en_US",
  "timezone": "Europe/Zurich"
}

Application Level

Configure in Application.cfc:

component {
    this.locale = "en_US";
    this.timezone = "Europe/Zurich";
}

Runtime Configuration

Set locale and timezone dynamically in your code:

// Set and get locale
setLocale("de_DE");
currentLocale = getLocale(); // Returns: de_DE

// Set and get timezone setTimeZone("Europe/Berlin"); currentTimezone = getTimeZone(); // Returns: Europe/Berlin
// These settings affect subsequent formatting operations echo(lsDateFormat(now(), "full")); // Uses current locale echo(dateTimeFormat(now(), "yyyy-MM-dd HH:mm:ss z")); // Uses current timezone

Understanding Date Objects in Lucee

Creating Dates with Timezone Context

When creating dates, you can specify a timezone to interpret what timezone the input values represent during creation. The resulting date object is still a java.util.Date with no timezone attached:

// These create DIFFERENT date objects representing different moments in time
// The timezone parameter is used to INTERPRET the input values during creation

// "12:00 noon on June 20, 2025 interpreted as UTC time" date1 = createDateTime(2025, 6, 20, 12, 0, 0, 0, "UTC");
// "12:00 noon on June 20, 2025 interpreted as New York time" (which is 16:00 UTC during DST) date2 = createDateTime(2025, 6, 20, 12, 0, 0, 0, "America/New_York");
// "12:00 noon on June 20, 2025 interpreted as Tokyo time" (which is 03:00 UTC) date3 = createDateTime(2025, 6, 20, 12, 0, 0, 0, "Asia/Tokyo");
// All resulting objects are plain java.util.Date with no timezone info // The timezone was only used during creation to determine the moment in time
// All formatted in UTC to see the actual moments they represent: echo(dateTimeFormat(date1, "HH:mm Z", "UTC")); // 12:00 +0000 echo(dateTimeFormat(date2, "HH:mm Z", "UTC")); // 16:00 +0000 echo(dateTimeFormat(date3, "HH:mm Z", "UTC")); // 03:00 +0000
// Using named parameters for clarity - timezone only used during creation meetingTime = createDateTime( year: 2025, month: 6, day: 20, hour: 12, timezone: "Europe/Berlin" // Interpret "12:00" as "12:00 Berlin time" );
// meetingTime is now a plain Date object representing that moment in time // You still need timezone when formatting: echo(dateTimeFormat(meetingTime, "HH:mm z", "Europe/Berlin")); // 12:00 CEST echo(dateTimeFormat(meetingTime, "HH:mm z", "UTC")); // 10:00 UTC

Parsing Dates with Timezone Context

Similarly, when parsing date strings, you can specify the timezone to interpret what timezone the string values represent. The resulting date object is still a plain java.util.Date:

// Parse date strings with timezone context for interpretation
userInput = "2025-06-20 14:30:00";

// These create different date objects depending on how you interpret the input: date1 = parseDateTime(userInput, "yyyy-MM-dd HH:mm:ss", "UTC"); // Interprets as: "14:30 UTC time"
date2 = parseDateTime(userInput, "yyyy-MM-dd HH:mm:ss", "Europe/Berlin"); // Interprets as: "14:30 Berlin time"
// Both are plain Date objects now, timezone was only used during parsing // Same input string, different moments in time: echo(dateTimeFormat(date1, "HH:mm Z", "UTC")); // 14:30 +0000 echo(dateTimeFormat(date2, "HH:mm Z", "UTC")); // 12:30 +0000 (2 hours earlier)
// You still need to specify timezone when formatting either date: echo(dateTimeFormat(date1, "HH:mm z", "Europe/Berlin")); // 16:30 CEST echo(dateTimeFormat(date2, "HH:mm z", "Europe/Berlin")); // 14:30 CEST

Keep Dates as Objects

The principle is to keep dates as date objects as long as possible:

// Good: Work with date objects
userBirthday = createDate(1990, 5, 15, "UTC");
yearsOld = dateDiff("y", userBirthday, now());

// Good: Store date objects in database queryExecute("INSERT INTO users (birthday) VALUES (?)", [userBirthday]);
// Good: Pass date objects between functions function calculateAge(birthDate) { return dateDiff("yyyy", birthDate, now()); }
// Only format when you need to display to users function displayBirthday(birthDate, userLocale, userTimezone) { return lsDateFormat(birthDate, "long", userLocale); }

Avoid String Conversions

// Avoid: Converting to strings unnecessarily
originalDate = createDateTime(2025, 6, 20, 12, 0, 0, 0, "UTC");
dateString = dateFormat(originalDate, "yyyy-mm-dd");
// ... do some operations ...
backToDate = parseDateTime(dateString); // Potential for errors

// Better: Keep as date object originalDate = createDateTime(2025, 6, 20, 12, 0, 0, 0, "UTC"); // ... do operations with date functions ... result = dateAdd("d", 7, originalDate); // Still a date object

Implementation

Step 1: Creating and Basic Date Formatting

Start with creating dates and simple formatting:

// Create dates with different methods
today = now();
specificDate = createDateTime(2025, 6, 20, 15, 30, 45);

// Create dates with timezone context utcDate = createDateTime(2025, 6, 20, 12, 0, 0, 0, "UTC"); nyDate = createDateTime(2025, 6, 20, 12, 0, 0, 0, "America/New_York");
// These represent different moments in time! echo(dateTimeFormat(utcDate, "yyyy-MM-dd HH:mm:ss Z", "UTC")); // Output: 2025-06-20 12:00:00 +0000
echo(dateTimeFormat(nyDate, "yyyy-MM-dd HH:mm:ss Z", "UTC")); // Output: 2025-06-20 16:00:00 +0000 (4 hours later!)
// Using named parameters for clarity parisDate = createDateTime( year: 2025, month: 6, day: 20, hour: 12, timezone: "Europe/Paris" );
// Basic date formatting echo(dateFormat(today)); // 20-Jun-2025 (default format) echo(dateFormat(today, "yyyy-mm-dd")); // 2025-06-20 echo(dateFormat(today, "dd/mm/yyyy")); // 20/06/2025 echo(dateFormat(today, "mmmm d, yyyy")); // June 20, 2025
// Basic time formatting echo(timeFormat(today)); // 14:30:45 (default format) echo(timeFormat(today, "HH:mm:ss")); // 14:30:45 echo(timeFormat(today, "h:mm tt")); // 2:30 PM echo(timeFormat(today, "HH:mm")); // 14:30
// Combined date and time echo(dateTimeFormat(today, "yyyy-MM-dd HH:mm:ss")); // 2025-06-20 14:30:45

Step 2: Working with Custom Format Patterns

Use specific format patterns for precise control:

testDate = createDateTime(2025, 6, 20, 15, 30, 45, 0, "UTC");

// Year formats echo(dateFormat(testDate, "yy")); // 25 echo(dateFormat(testDate, "yyyy")); // 2025
// Month formats echo(dateFormat(testDate, "m")); // 6 echo(dateFormat(testDate, "mm")); // 06 echo(dateFormat(testDate, "mmm")); // Jun echo(dateFormat(testDate, "mmmm")); // June
// Day formats echo(dateFormat(testDate, "d")); // 20 echo(dateFormat(testDate, "dd")); // 20 echo(dateFormat(testDate, "ddd")); // Fri echo(dateFormat(testDate, "dddd")); // Friday
// Time patterns echo(timeFormat(testDate, "H")); // 15 (24-hour) echo(timeFormat(testDate, "HH")); // 15 (24-hour with leading zero) echo(timeFormat(testDate, "h")); // 3 (12-hour) echo(timeFormat(testDate, "hh")); // 03 (12-hour with leading zero) echo(timeFormat(testDate, "mm")); // 30 (minutes) echo(timeFormat(testDate, "ss")); // 45 (seconds) echo(timeFormat(testDate, "tt")); // PM (AM/PM indicator)

Step 3: Timezone Handling

Format dates in different timezones:

// Create a specific date/time
eventTime = createDateTime(2025, 6, 20, 12, 0, 0, 0, "UTC");

// Format in different timezones echo(dateTimeFormat(eventTime, "yyyy-MM-dd HH:mm:ss Z", "UTC")); // Output: 2025-06-20 12:00:00 +0000
echo(dateTimeFormat(eventTime, "yyyy-MM-dd HH:mm:ss Z", "America/New_York")); // Output: 2025-06-20 08:00:00 -0400
echo(dateTimeFormat(eventTime, "yyyy-MM-dd HH:mm:ss Z", "Europe/Berlin")); // Output: 2025-06-20 14:00:00 +0200
echo(dateTimeFormat(eventTime, "yyyy-MM-dd HH:mm:ss z", "Asia/Tokyo")); // Output: 2025-06-20 21:00:00 JST
// Working with DST (Daylight Saving Time) summerDate = createDateTime(2025, 7, 15, 12, 0, 0, 0, "UTC"); // Summer winterDate = createDateTime(2025, 1, 15, 12, 0, 0, 0, "UTC"); // Winter
echo(dateTimeFormat(summerDate, "Z", "Europe/Berlin")); // +0200 (CEST) echo(dateTimeFormat(winterDate, "Z", "Europe/Berlin")); // +0100 (CET)

Step 4: Locale-Aware Formatting

Use localized formatting for international applications:

testDate = createDateTime(2025, 6, 20, 14, 30, 45, 0, "UTC");

// Predefined locale formats echo(lsDateFormat(testDate, "short", "en_US")); // 6/20/25 echo(lsDateFormat(testDate, "medium", "en_US")); // Jun 20, 2025 echo(lsDateFormat(testDate, "long", "en_US")); // June 20, 2025 echo(lsDateFormat(testDate, "full", "en_US")); // Friday, June 20, 2025
// Different locales echo(lsDateFormat(testDate, "long", "de_DE")); // 20. Juni 2025 echo(lsDateFormat(testDate, "long", "fr_FR")); // 20 juin 2025 echo(lsDateFormat(testDate, "long", "es_ES")); // 20 de junio de 2025 echo(lsDateFormat(testDate, "long", "ja_JP")); // 2025年6月20日
// Localized time formatting echo(lsTimeFormat(testDate, "short", "en_US")); // 2:30 PM echo(lsTimeFormat(testDate, "medium", "en_US")); // 2:30:45 PM echo(lsTimeFormat(testDate, "short", "de_DE")); // 14:30 echo(lsTimeFormat(testDate, "medium", "fr_FR")); // 14:30:45
// Combined date and time with locale echo(lsDateTimeFormat(testDate, "short", "en_US")); // 6/20/25, 2:30 PM echo(lsDateTimeFormat(testDate, "full", "de_DE")); // Freitag, 20. Juni 2025 um 14:30:45 MESZ

Step 5: Working with Time Objects

Handle time-only objects created with createTime():

// Create time-only objects
morningTime = createTime(8, 30, 0, "UTC");
eveningTime = createTime(18, 45, 30, "UTC");

// Format time objects echo(timeFormat(morningTime, "h:mm tt")); // 8:30 AM echo(timeFormat(eveningTime, "HH:mm:ss")); // 18:45:30
// Time objects with timezone (Note: uses 1899-12-30 as base date) echo(dateTimeFormat(morningTime, "HH:mm:ss Z", "Europe/Zurich")); // Output: 08:30:00 +0100
// Localized time formatting echo(lsTimeFormat(morningTime, "short", "en_US")); // 8:30 AM echo(lsTimeFormat(morningTime, "short", "de_DE")); // 08:30

Step 6: Advanced Formatting Patterns

Create complex format patterns for specific needs:

currentDate = now();

// ISO 8601 format echo(dateTimeFormat(currentDate, "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "UTC")); // Output: 2025-06-20T14:30:45.123Z
// RFC 2822 format echo(dateTimeFormat(currentDate, "EEE, dd MMM yyyy HH:mm:ss Z")); // Output: Fri, 20 Jun 2025 14:30:45 +0200
// Custom readable format echo(dateTimeFormat(currentDate, "EEEE, MMMM d, yyyy 'at' h:mm a")); // Output: Friday, June 20, 2025 at 2:30 PM
// Filename-safe format echo(dateTimeFormat(currentDate, "yyyy-MM-dd_HH-mm-ss")); // Output: 2025-06-20_14-30-45
// Week and day of year echo(dateFormat(currentDate, "yyyy-'W'ww-F")); // 2025-W25-5 (Week 25, Day 5) echo(dateFormat(currentDate, "yyyy-DDD")); // 2025-171 (Day 171 of year)

Lucee 6.1 vs 6.2 Implementation Changes

What Changed in Lucee 6.2

In Lucee 6.2, the underlying implementation was upgraded from Java's legacy SimpleDateFormat to the modern DateTimeFormatter API. This change brings improved performance and thread safety, but introduced some differences in historical timezone handling.

The Key Difference

// Example with historical date and Europe/Helsinki timezone

// Lucee 6.1 and earlier (SimpleDateFormat) myDate = createDateTime(1905, 12, 24, 17, 39, 49, 0, "UTC"); result = dateTimeFormat(myDate, "yyyy-MM-dd HH:mm:ss Z", "Europe/Helsinki"); // Output: 1905-12-24 19:39:49 +0200 (modern offset)
// Lucee 6.2+ (DateTimeFormatter) myDate = createDateTime(1905, 12, 24, 17, 39, 49, 0, "UTC"); result = dateTimeFormat(myDate, "yyyy-MM-dd HH:mm:ss Z", "Europe/Helsinki"); // Output: 1905-12-24 19:19:38 +0139 (historically accurate 1905 offset)

Why This Happened

  • SimpleDateFormat often ignores historical timezone changes for dates before certain cutoffs
  • DateTimeFormatter applies complete IANA timezone database including all historical transitions
  • For actual historical dates (like 1905), the new historically accurate behavior is technically correct

Current Behavior

Modern Lucee versions automatically detect and handle synthetic date issues:

  • createTime() objects with synthetic dates before 1899-12-31 use standard timezone offsets (the fix)
  • Actual historical dates (like 1905-12-24) continue to use historically accurate timezone rules (no fix needed)
  • Modern dates continue to use full timezone rules including DST
  • No breaking changes to existing APIs
  • Performance improvements from the DateTimeFormatter migration

Best Practices

1. Always Specify Timezone for User-Facing Dates

// Good: Explicit timezone
userDate = dateTimeFormat(now(), "yyyy-MM-dd HH:mm:ss z", session.userTimezone);

// Better: Include offset for clarity userDate = dateTimeFormat(now(), "yyyy-MM-dd HH:mm:ss Z (z)", session.userTimezone);

2. Work with Dates as Objects, Format with Timezone

The best practice is to keep dates as date objects and only apply timezone/locale when formatting:

// Store dates as date objects (no timezone attached)
function storeUserEvent(eventDate) {
    // Date objects don't have timezone - they represent a moment in time
    queryExecute("INSERT INTO events (event_date) VALUES (?)", [eventDate]);
}

// Display with user's preferred timezone function displayUserEvent(eventDate, userTimezone) { // Same date object, formatted for user's timezone return dateTimeFormat(eventDate, "MMM d, yyyy 'at' h:mm a z", userTimezone); }
// Example: Same event, different user timezones eventDate = createDateTime(2025, 6, 20, 18, 0, 0); // 6 PM on June 20, 2025
// User in New York sees: echo(displayUserEvent(eventDate, "America/New_York")); // Jun 20, 2025 at 2:00 PM EDT
// User in Tokyo sees: echo(displayUserEvent(eventDate, "Asia/Tokyo")); // Jun 21, 2025 at 3:00 AM JST
// User in London sees: echo(displayUserEvent(eventDate, "Europe/London")); // Jun 20, 2025 at 7:00 PM BST

Note: Avoid dateConvert("local2utc", date) - this function exists only for ACF compatibility and can be misleading since date objects don't actually hold timezone information (in ACF and Lucee).

3. Use Appropriate Format Functions

// For system/API dates - use consistent format
apiDate = dateTimeFormat(now(), "yyyy-MM-dd'T'HH:mm:ss'Z'", "UTC");

// For user display - use localized format userDate = lsDateTimeFormat(now(), "medium", session.userLocale, session.userTimezone);
// For file names - use safe characters fileName = "report_" & dateTimeFormat(now(), "yyyy-MM-dd_HH-mm-ss") & ".pdf";

Lucee's date and time formatting capabilities provide a robust foundation for handling dates in any application, from simple displays to complex international systems. Understanding the various functions, their parameters, and best practices ensures your application handles dates correctly across different locales and timezones.

See also