createIsoString() - A ColdFusion User-Defined Function (UDF) to replace dateTimeFormat("iso")
Tags: coldfusion, cfml, java, iso8601
Published: 2023-07-16 22:46:20

<!-- createIsoString() - A ColdFusion User-Defined Function (UDF) to replace dateTimeFormat("iso") -->

> This ColdFusion UDF converts a date object or string into a [UTC, ISO8601, RFC 339, ATOM or W3C string](https://www.utctime.net/) with optional millisecond precision and ability to specify timezone.

**TLDR?** Go straight to [createIsoString UDF](#createisostring).

Here's some history of current date/time and timezone-related functions.

## <a id="dateconvert"></a> Built-in `dateConvert(conversionType, date)` and `getTimezoneInfo()`

Since 1999's release of ColdFusion 4, a built-in [dateConvert](https://cfdocs.org/dateconvert) function has been available to convert `local` to `UTC` time and vice-versa.  [getTimeZoneInfo](https://cfdocs.org/gettimezoneinfo) has also been available to retrieve only the `local` time zone based on system settings. This functions are beneficial, but I'm not sure why it's limited to only supporting `local` time.

Lucee's [getTimeZoneInfo](https://docs.lucee.org/reference/functions/gettimezoneinfo.html) function added support for `timezone` and `locale` back in May 2016. This function is a lot more useful that Adobe's.

My servers are located on the west coast and I have occassionally have requirements to output UTC strings for multiple timezones throughout the US using the local offset for the timezone.  The `dateConvert` and `getTimeZoneInfo` function don't offer much time/conversion assistance for anything "non-local".

I've noticed some oddities of using converted dates.  The function states that it returns "UTC or local-formatted time object" which can behave differently than values create using [createDateTime](https://cfdocs.org/createdatetime).  I was encountering what I believe to be comparison/diff bugs with ColdFusion 2016 and stopped using it as I couldn't wrap my head around what was happening.

## <a id="datetimeformat"></a> Built-in `dateTimeFormat(date [, mask [, timezone]])`

A [dateTimeFormat](https://cfdocs.org/datetimeformat) function was added in 2012's ColdFusion 10.  Support for an `iso` mask was added over 4 years later starting with ColdFusion 2016.  Lucee supported an `iso8601` mask with v4.2.0.001 in 2014 and currently accepts either `iso` or `iso8601`.

The strings generated by `dateTimeFormat` support the system `local` timezone with by default. Both Adobe & Lucee allow a timezone to  In addition, precision is dropped. ISO 8601, UTC, RFC 3339, ATOM & W3C all support fractional seconds, but you'd never know that by the lack of options available in the built-in function.


## <a id="settimezone"></a> Built-in `setTimezone(timezone)` *(CF2021+)*

As of CF2021, the timezone can be manually "reset" within a request using [setTimeZone](https://cfdocs.org/settimezone).  This function must have been copied from Lucee, but I'm not sure how long Lucee's had the feature.  Lucee provides a list of all available timezones in the Lucee administrator (Settings/Regional).

I'm wondering what side-effects this may have.  It's request-only, but would you need to reset the timezone back to the default after performing function-specific calculations that would require a different timezone?

Adobe shares this [CF2021-only code snippet](https://helpx.adobe.com/coldfusion/cfml-reference/coldfusion-functions/functions-s/settimezone.html) using java.util.TimeZone to retrieve the available 628 timezones on the function detail page to return a native array of timezones.  NOTE: [Java.util.TimeZone](https://docs.oracle.com/javase/8/docs/api/java/util/TimeZone.html) warns again using deprecated three-letter time zone IDs (such as "PST", "CTT", "AST") as these abbreviations are often used for multiple time zones.

```cfml
tz_list = CreateObject("java", "java.util.TimeZone").getAvailableIDs(); // get a list of all timezone supplied by java util.
writedump(var=tz_list, label="java.util.TimeZone");

```

You can also access `java.time.ZoneId` directly for this info.

```cfml
zoneIds = listtoarray(createobject("java", "java.time.ZoneId").getAvailableZoneIds().toString().replaceAll("[\[\] ]",""));
writedump(var=zoneIds, label="java.time.ZoneId");
```

## <a id="timezone-cfc"></a> TimeZone-CFC (Using ICU4J JAR)

Paul Hastings (from now-defunct blog at sustainableGIS․com) shared a [TineZone-CFC](https://github.com/rip747/TimeZone-CFC) component back in 2006 that uses the [ICU4J](https://icu.unicode.org/home/why-use-icu4j) library.  I monitor & manually update this library and noticed that they are always making time zones updates with each release.  It's much easier to upgrade this library rather than upgrade the entire JDK. For example, some time zones were updated by IANA on [March 28, 2023](https://www.iana.org/time-zones) and this data is available in [ICU 73](https://icu.unicode.org/download/73) released on 2023-06-15.  If you're using CF10-2021, do you know when the timezone tables you are using were last updated? (If you only care about local time, it probably doesn't matter.) *[UPDATE 2023-07-20: Java 11.0.20 was just released and contains IANA time zone data [2023c](https://mm.icann.org/pipermail/tz-announce/2023-March/000079.html) with multiple DST changes.]*

I recommended an update to [support fractional seconds](https://github.com/rip747/TimeZone-CFC/issues/8) back in 2018, but this library hasn't received any updates since 2010.

This TimeZone-CFC has the following helpful methods and currently lists 637 timezones (if using ICU 73):

- **isDST** determines if a given date & timezone are in DST. if no date or timezone is passed the method defaults to current date/time and server timezone. PUBLIC.
- **getAvailableTZ** returns an array of available timezones on this server (ie according to server's icu4j version). PUBLIC.
- **getTZByOffset** returns an array of available timezones on this server (ie according to server's icu4j version) with the same UTC offset. PUBLIC.
- **getTZByCountry** returns an array of available timezones on this server (ie according to server's icu4j version) within a given country. PUBLIC.
- **isValidTZ** determines if a given timezone is valid according to getAvailableTZ. PUBLIC.
- **usesDST** determines if a given timezone uses DST. PUBLIC.
- **getRawOffset** returns the raw (as opposed to DST) offset in hours for a given timezone. PUBLIC.
- **getTZOffset** returns offset in hours for a given date/time & timezone, uses DST if timezone uses and is currently in DST. PUBLIC.
- **getDST** returns DST savings for given timezone. PUBLIC.
- **castToUTC** return UTC from given datetime in given timezone. required argument thisDate, optional argument thisTZ valid timezone ID, defaults to server timezone. PUBLIC.
- **castfromUTC** return date in given timezone from UTC datetime. required argument thisDate, optional argument thisTZ valid timezone ID, defaults to server timezone. PUBLIC.
- **castToServer** returns server datetime from given datetime in given timezone. required argument thisDate valid datetime, optional argument thisTZ valid timezone ID, defaults to server timezone. PUBLIC.
- **castfromServer** return datetime in given timezone from server datetime. required argument thisDate valdi datetime, optional argument thisTZ valid timezone ID, defaults to server timezone. PUBLIC.
- **getServerTZ** returns server timezone. PUBLIC
- **getServerTZShort** returns "short" name for the server's timezone. PUBLIC
- **getServerId** returns ID for the server's timezone. PUBLIC

## <a id="getisotimestring"></a> getIsoTimeString(datetime[, convertToUTC]) UDF

[Ben Nadel](https://www.bennadel.com/) shared a [getIsoTimeString UDF](https://www.bennadel.com/blog/2501-converting-coldfusion-date-time-values-into-iso-8601-time-strings.htm) that he wrote for ColdFusion 9 back in 2013 that "takes your ColdFusion date/time object and returns a string that represents the date in ISO 8601 complaint format. This separates the date/time values with a 'T' and ends with the UTC-marker, 'Z'.".

As one commenter shared, the factional second data is not generated.  Even though it's optional, the Constant Contact APIv2 required it.  (I've used dateTimeFormat with a `yyyy-mm-dd'T'HH:nn:ss.lllZ` mask to overcome the truncation.)

## <a id="createisostring"></a> Introducing createIsoString() UDF

I've developed a UDF for internal use to support a couple different functions.  I wanted to:
- support local, UTC and other timezones when calculating the offset
  - built-in dateTimeFormat("iso") only generates values for local server offset. No other option is available.
- support date/time truncation to DAYS, HALF_DAYS, HOURS, MINUTES, SECONDS & MILLIS (or no rounding/precision truncation).
  - built-in dateTimeFormat("iso") rounds to the nearest second. No other option is available.
  - if I was adding support for fractional seconds, I may as well extend support to truncate the time using built-in [ChronoUnit](https://docs.oracle.com/javase/8/docs/api/java/time/temporal/ChronoUnit.html)
- explicitly output strings to support various [ISO/RFC/ATOM/W3C](https://www.utctime.net/) standards (ie, `+0000`, `+00:00` or `Z`)
  - built-in dateTimeFormat("iso") outputs "Z" for GMT. No other option (`+0000` or `+00:00`) is available.
- validate timezone string since Java is case-sensitive & requires explicit case. (I found using regex to validate timezone values was up to 25x faster than using [arrayFindNoCase](https://twitter.com/gamesover/status/1680692772091871232).)
- add `local` & `server` timezone options to fallback to whatever the system timezone is currently set to.
- throw an error or ignore when optional values are invalid. (Invalid dates will return an empty string while all other options will simply ignore the invalid option.)

It

### Here's a table comparison of outputs:

|dateTimeFormat("iso")|createIsoString()|createIsoString(timezone="utc")|createIsoString(timezone="US/Eastern")|createIsoString(format="8601 or atom")|
|-----|-----|-----|-----|-----|
|2015-04-11T19:02:34-07:00|2015-04-11T19:02:34.274-07:00|2015-04-12T02:02:34Z|2015-04-11T22:02:34.274-04:00|2015-04-11T19:02:34.274-07:00|
|2023-07-16T15:39:03-07:00|2023-07-16T15:39:03.552-07:00|2023-07-16T22:39:03Z|2023-07-16T18:39:03.552-04:00|2023-07-16T15:39:03.552-07:00|

### Here's the source code.  Enjoy!

[https://gist.github.com/JamoCA/3e825f773d3bbb45f5c36ee85793e10e](https://gist.github.com/JamoCA/3e825f773d3bbb45f5c36ee85793e10e)

{% gist https://gist.github.com/JamoCA/3e825f773d3bbb45f5c36ee85793e10e %}