/* createIsoString UDF by James Moberg / SunStar Media
2023-07-16
Gist: https://gist.github.com/JamoCA/3e825f773d3bbb45f5c36ee85793e10e
Blog: https://dev.to/gamesover/createisostring-a-coldfusion-user-defined-function-udf-to-replace-datetimeformatiso-2p15
Tweet: https://twitter.com/gamesover/status/1680711586946891776
*/
public string function createIsoString(string date="", string timezone="local", string truncatedTo="MILLIS", string format="utc", boolean throwOnError=true) output=false hint="Converts date object/string into a UTC, ISO8601, RFC 339, ATOM or W3C string to a timezone with offset and optional millisecond precision" {
	if (!len(arguments.date) || arguments.date eq "now"){
		arguments.date = now();
	};
	if(!isvalid("date", arguments.date)){
		if (arguments.throwOnError) {
			throw(message="createIsoString: Can't convert value [#encodeforhtml(arguments.date)#] to a datetime value");
		}
		return "";
	}
	if (arguments.throwOnError && len(arguments.truncatedTo) && !listfindnocase("DAYS,HALF_DAYS,HOURS,MINUTES,SECONDS,MILLIS", arguments.truncatedTo)){
		throw(message="createIsoString: Can't truncate date using value [#encodeforhtml(arguments.truncatedTo)#]");
	}
	// cache java classes within current request
	if (!structkeyexists(request, "createIsoStringJava")){
		request.createIsoStringJava  = [
			"offsetDateTime": createobject("java", "java.time.OffsetDateTime")
			,"dateTimeFormatter": createobject("java", "java.time.format.DateTimeFormatter")
			,"chronoUnit": createobject("java", "java.time.temporal.ChronoUnit")
			,"zoneId": createobject("java", "java.time.ZoneId")
		];
		request.createIsoStringJava["zoneIdList"] = "," & request.createIsoStringJava.zoneId.getAvailableZoneIds().toString().replaceAll("[\[\] ]","") & ",";
		request.createIsoStringJava["units"] = [
			"DAYS": request.createIsoStringJava.ChronoUnit.DAYS
			,"HALF_DAYS": request.createIsoStringJava.ChronoUnit.HALF_DAYS
			,"HOURS": request.createIsoStringJava.ChronoUnit.HOURS
			,"MILLIS": request.createIsoStringJava.ChronoUnit.MILLIS
			,"MINUTES": request.createIsoStringJava.ChronoUnit.MINUTES
			,"SECONDS": request.createIsoStringJava.ChronoUnit.SECONDS
		];
	}
	// validate timezone
	local.tzRegex = (refind("[^a-zA-Z0-9\/\_]", arguments.timezone)) ? rereplace(arguments.timezone, "(?=[\[\]\\^$.|?*+()])", "\", "all") : arguments.timezone;
	local.zoneIdPos = refindnocase(",#local.tzRegex#,", request.createIsoStringJava.zoneIdList);
	if (arguments.throwOnError && len(arguments.timezone) && !listfindnocase("local,server", arguments.timezone) && !local.zoneIdPos){
		throw(message="createIsoString: timezone value [#encodeforhtml(arguments.truncatedTo)#] is not valid.");
	}
	local.isoDateRegex = "^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)((-(\d{2}):(\d{2})|Z)?)$";
	if (refind(local.isoDateRegex, arguments.date)){
		local.date = request.createIsoStringJava.OffsetDateTime.parse(arguments.date);
	} else {
		local.timeString = javacast("string", datetimeformat(arguments.date, "yyyy-mm-dd'T'HH:nn:ss.lllZ"));
		local.date = request.createIsoStringJava.OffsetDateTime.parse(local.timeString, request.createIsoStringJava.dateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX"));
	}
	// timezone is valid, use the correct capitalization
	if (local.zoneIdPos gt 0){
		local.date = local.date.atZoneSameInstant(request.createIsoStringJava.zoneId.of( mid(request.createIsoStringJava.zoneIdList, local.zoneIdPos+1, len(arguments.timezone)) ));
	}
	// perform optional truncation
	if (structkeyexists(request.createIsoStringJava.units, arguments.truncatedTo)){
		local.date = local.date.truncatedTo( request.createIsoStringJava.units[arguments.truncatedTo] );
	}
	// format the date to ISO 8601 w/offset
	local.isoFormat = request.createIsoStringJava.dateTimeFormatter.ISO_OFFSET_DATE_TIME;
	local.formattedDate = local.isoFormat.format(local.date).toString();
	if (find("8601", arguments.format) || findnocase("iso", arguments.format)){
		return local.formattedDate.replaceAll("Z$", "\+0000");  // ISO-8601 = +0000
	} else if (find("3339", arguments.format) || findnocase("atom", arguments.format) || findnocase("w3c", arguments.format)){
		return local.formattedDate.replaceAll("Z$", "\+00\:00");  // RFC 3339, ATOM or W3C  = +00:00
	}
	return local.formattedDate;
}