Wednesday 12 August 2015

Lucee: uncontrolled language design demonstrated

G'day:
My attention was drawn to this this morning via Dom congratulating Geoff at doing a good job improving some of Lucee's docs:


And Dom was right: Geoff's been doing a bunch of thankless donkey work, and Lucee's docs are better for it. However when I looked at Geoff's pull request, I noticed something interesting:

minute()


Extracts the minute value from a date/time object.

Returns: number
Usage: Minute( date [, timezone ] )

Arguments:
ArgumentType/RequiredNotes
datedatetime/requireddate object
timezonestring/optionalA datetime object is independent of a specific timezone, it is only a offset in milliseconds from 1970-1-1 00.00:00 UTC (Coordinated Universal Time). This means that the timezone only comes into play when you need specific information like hours in a day, minutes in a hour or which day it is since those calculations depend on the timezone. For these calculations, a timezone must be specified in order to translate the date object to something else.[...]

Before we start, I don't know that this is correct. A CFML datetime object extends java.util.Date amongst other things:

public final class lucee.runtime.type.dt.DateTimeImpl
 extends lucee.runtime.type.dt.DateTime
 extends java.util.Date
 extends java.lang.Object
 extends  implements lucee.runtime.type.SimpleValue, lucee.runtime.type.Objects

And a java.util.Date is time zone aware.

This can be borne out by just looking at one of them:

<cfoutput>#now().getTimezoneOffset()#</cfoutput>


And as my PC's time zone is currently set to NZST, I get this:

-720


So... no... this is not true: "A datetime object is independent of a specific time zone, it is only a offset in milliseconds from 1970-1-1 00.00:00 UTC (Coordinated Universal Time)". It's not that at all.

OK, back to the minute() function. Initially I thought "huh? minutes is minutes... and time zones don't come into what the minute component of a DateTime is. The current time, independent of time zone, has a minute component. And that is what the minute() function should fish out.

I asked on the Lucee Slack channel, and my man Simon Hooker 'splained it to me. It's for returning the minutes in a different time zone. And indeed some time zones are shifted by partial hours. Here's an example.

// minute.cfm

timeNow = now();

timeZoneInfo = getTimezoneInfo();
timeZoneOffsetSign = sgn(timeZoneInfo.utcTotalOffset) > 0 ? "+" : "";
timeZoneOffsetHours = timeZoneInfo.utcTotalOffset / 3600;
writeOutput("Local time zone offset: #timeZoneOffsetHours#<br>");
writeOutput("Time zone offset from Java: #timeNow.getTimeZoneOffset()#<br>");

writeOutput("Raw time now: #timeNow#<br>");
writeOutput("Minutes now (System time zone): #timeNow.minute()#<br>");

testTimeZone = "Pacific/Chatham";
writeOutput("Minutes now (#testTimeZone#): #timeNow.minute(testTimeZone)#<br>");

Output:

Local time zone offset: -12
Time zone offset from Java: -720
Raw time now: {ts '2015-08-11 19:49:09'}
Minutes now (System time zone): 49
Minutes now (Pacific/Chatham): 34


My computer is set to NZ's standard time zone, and the Chatham Islands are 45min ahead of the mainland, so the minutes are a different value.

However how Lucee have implemented this is poor, IMO. They have created a function called minute() which needs to be described as "gets the minute from the object in the current time zone or in a different time zone if specified. As soon as one starts adding conjunctions into the explanation of a method, one is "doing it wrong". And minute() is not a good name for a function that does that. minute() should just return the current object's minutes. That's it.

What Lucee (although this predates Lucee... it was like this in Railo too) is to implement a setTimeZone() method on their DateTime class, which does what it says: sets the time zone. So if one has a DateTime in one time zone, and wants the minute component of that time, but in a different time zone, one would do this:

timeNow.setTimeZone("Pacific/Chatham").minute()


There's a discussion to be had as to how this method behaves: does it change the timeNow object's time zone? Or does it return a new DateTime with the same values as the object it's called on, but that time zone set? I can see arguments both way, but a method called setTimeZone() would probably change the time zone of the object. This might not be desirable, so perhaps one wants to do this sort of thing instead:

createDateTime(timeNow, "Pacific/Chatham").minute(); // where createDateTime() has been updated to allow a new datetime to be created from an existing one


Either of these ways means a given method does one thing, which is what a method should do.

I can only think that a client asked for this of the Railo crew, and instead of going "how best to implement this solution?" they simply went [you need to affect a Scooby Doo voice when reading this] "OK!", and just did it.

Anyway, this is a small thing. But what it got me thinking is "I hope that bloody Lucee TAG gets underway as soon as possible". Lucee needs better language design steerage than it currently has.



I noticed one other interesting thing when testing this. Consider this code:

// illegalDlsTime.cfm

include "../../safeRun.cfm";
safeRun("CFML: One minute before clocks go forward (valid time)", function(){
    legalTime = createDateTime(2016, 3, 27, 0, 59, 00);
    writeOutput("legalTime: #legalTime#");
});
safeRun("CFML: One minute after clocks go forward (invalid time)", function(){
    illegalTime = createDateTime(2016, 3, 27, 1, 01, 00);
    writeOutput("illegalTime: #illegalTime#");
});

safeRun("Java: One minute before clocks go forward (valid time)", function(){
    legalTime = createObject("java", "java.util.Date").init(116, 2, 27, 0, 59, 00);
    writeOutput("legalTime: #legalTime#");
});

safeRun("Java: One minute createDateTime clocks go forward (invalid time)", function(){
    illegalTime = createObject("java", "java.util.Date").init(116, 2, 27, 1, 01, 00);
    writeOutput("illegalTime: #illegalTime#");
});

I've switched my time zone back to be in the UK, and 27/3/2016 is when day light saving starts next year. So the clocks go forward by one hour. This being the case, there is no such time as 01:01 on 27/3... time skips from 00:59:59 to 02:00:00.

ColdFusion addresses this correctly:

CFML: One minute before clocks go forward (valid time)
legalTime: {ts '2016-03-27 00:59:00'}Ran OK


CFML: One minute after clocks go forward (invalid time)
Type: Expression
Message: Date value passed to date function createDateTime is unspecified or invalid.
Detail: Specify a valid date in createDateTime function.


Java: One minute before clocks go forward (valid time)
legalTime: {ts '2016-03-27 00:59:00'}Ran OK


Java: One minute createDateTime clocks go forward (invalid time)
illegalTime: {ts '2016-03-27 02:01:00'}Ran OK




Lucee does not:

CFML: One minute before clocks go forward (valid time)
legalTime: {ts '2016-03-27 00:59:00'}Ran OK


CFML: One minute after clocks go forward (invalid time)
illegalTime: {ts '2016-03-27 01:01:00'}Ran OK


Java: One minute before clocks go forward (valid time)
legalTime: {ts '2016-03-27 13:59:00'}Ran OK


Java: One minute createDateTime clocks go forward (invalid time)
illegalTime: {ts '2016-03-27 14:01:00'}Ran OK




I guess Lucee is relying on java.util.Date here, which does not understand day light saving. However CFML dates do understand day light saving, so this is a bit of a shortfall in Lucee's implementation. I'll raise a bug (LDEV-483).

Finally... Lucee seems to ignore its own time zone settings (as set in Server Admin) for any of this. I could set the time zone to be anything I liked, but they only way I could get Lucee to see a different time zone is to switch my system clock. I could perhaps set -Duser.timezone on the JVM, but I did not try that: the admin setting should be sufficient. I'll raise a bug for this too (LDEV-484).

Righto, that's enough of that.

--
Adam