Sunday 8 December 2013

CFML: For the sake of completeness: createLocalisedDayOfWeekAsInteger(): a closure approach

G'day:
A coupla days ago I continued my TDD / unit testing series with an article "Unit Testing / TDD - refactoring existing code". This engendered a few comments, none of which paid any attention to the intent article, instead focused on something pretty much irrelevant to what I was discussing. This engendered this reaction from me:


Oh how I love my readership sometimes. But, actually, the comments got me thinking...

That article covered the approval process for CFLib of a function dayOfWeekAsInteger(), which is the reverse of dayOfWeekAsString(): it takes a string (in English only), and returns which day of the week that corresponds to, starting with Sunday as the first day. Why does it start on Sunday instead of Monday (as per ISO 8601)? Because it's the inverse of dayOfWeekAsString(), and that's how that does it. Is that right? Well: no. Is that relevant to either this UDF or the blog article? Also: no.


However James's suggestion for how Simon could rewrite his function to suit James's requirements (James: write your own functions if you want them different from how others have implemented them!) did get me thinking about how to do it. A thought popped into my head that one could leverage closure to create a function which was specific to a given locale. This might not be exactly what James wants, but it was what I decided to look into, because it's slightly interesting, and covers a feature of closure I've not covered anywhere on this blog yet. So it interests me.

Here's the function I knocked out this morning whilst contemplating my day (a day which is going to involve writing articles for this blog, I think):

// createLocalisedDayOfWeekAsInteger.cfm
function function createLocalisedDayOfWeekAsInteger(required string locale, boolean iso=false){
    var supportedLocales = SERVER.coldfusion.supportedLocales; // so sue me.
    if (!listFindNoCase(supportedLocales, locale)){
        throw(type="InvalidLocaleException", message="Invalid locale value specified", detail="Locale must be one of #supportedLocales#");
    }
    var baseDate = createDate(1972, 1, iso ? 31:30); // ie: in ISO mode, start on Mon. Otherwise CF mode: Sun
    var days = "";
    for (var i=0; i < 7; i++){
        days = listAppend(days, lsDateFormat(dateAdd("d", i, baseDate), "dddd", locale));
    }

    return function(required string day){
        var index = listFindNoCase(days, day);
        if (index){
            return index;
        }
        throw(type="ArgumentOutOfRangeException", message="Invalid day value", detail="day argument value (#day#) must be one of #days#");
    };
}    

This is not a TDD article, so I'll spare you the boring details of the unit tests, but I did do this work in a TDD fashion, and the tests are here. If you're following along that series, there's nothing new in that, but might be worth looking at for reinforcement anyhow.

OK, so the problem I have with a non-closure approach to this issue is that the code to work out what the days of the week actually are in any locale comes with a fair amount of overhead, and overhead one doesn't want to repeat every time one wants to call the function (it's the same every time, for a given locale, for one thing).

So what one can do with closure is to farm that logic out into the containing function, and then use closure in one's inner function to just use the results of that logic.

To make it clear, here's an example of using this function:

// basicExample.cfm
include "createLocalisedDayOfWeekAsInteger.cfm";

LSDayOfWeekAsIntegerFR = createLocalisedDayOfWeekAsInteger("fr_fr");

writeOutput("Dimanche is day of week: #LSDayOfWeekAsIntegerFR("dimanche")#");

Calling createLocalisedDayOfWeekAsInteger() returns a function which performs dayOfWeekAsInteger() for the given locale. We then use that function to perform the actual dayOfWeekAsInteger() logic.

Obviously if one is only calling the function once, then there's no overhead reduction here. But if it's the sort of thing that one then wanted to call a lot of times (unlikely in this example, granted), then the overhead is reduced to the single initial call.

The thing to focus on here (because it seems I need to tell people what they're supposed to be focusing on!) is that using closure can isolate overhead into the parent function, which then makes the returned function more lightweight.

Oh, and you'll note I also built-in support for ISO 8601 into this function too.

Happy now? (Actually, don't answer that ;-).

I will be submitting this to CFLib for the hell of it. It's mildly useful.

--
Adam