Array.Sort() Operator Must Return INT-Sized Result In Lucee CFML 5.3.4.80
A few weeks ago, I wrote about using Subtraction to power the Array Sort operator in Lucee CFML. This has become my go-to approach for sorting arrays. However, yesterday, I ran into an edge-case in which Subtraction was throwing an error. It turns out, the number returned from the array.sort() operator has to fit into a Java int in Lucee CFML 5.3.4.80.
At InVision, when generating API responses, we usually convert Date/Time objects into UTC milliseconds so that the client-side / JavaScript code can create localized Date objects with a simple constructor call:
new Date( utcMilliseconds )
In my case, I had a list of Projects that needed to be sorted by date. And, since I had already converted the DateTime value into UTC milliseconds (for use in an API response), I went about trying to sort the projects using Subtraction:
<cfscript>
// In our API responses, we return Date/Time stamps as UTC Milliseconds so that the
// client-side code can simply do "new Date( utcMilliseconds )" and create a Date
// object in the user's local timezone.
projects = [
{
id: 1,
name: "Really old project",
createdAt: createDate( 2015, 12, 15 ).getTime() // 1450155600000
},
{
id: 500,
name: "Recent project",
createdAt: createDate( 2019, 10, 30 ).getTime() // 1572408000000
},
{
id: 1000,
name: "Current project",
createdAt: createDate( 2020, 02, 26 ).getTime() // 1582693200000
}
];
projects.sort(
( a, b ) => {
// Sort the projects by CREATED DATE DESC (with most recent dates at the
// top of the resultant array).
// --
// Ex: a: 2020, b: 2015
// Result: ( (b)2015 - (a)2020 ) => -5 => (a) is sorted first.
return( b.createdAt - a.createdAt );
}
);
dump( projects );
</cfscript>
When I run this code, however, I was getting the following ColdFusion error:
invalid call of the function ArraySort, second Argument (function) is invalid, return value of the function [lambda_6i] cannot be casted to an integer.
Can't cast Object type [Number] to a value of type [integer]
arraysort(array:object, sorttype_or_closure:object, [sort_order:string, [locale_sensitive:boolean]]):boolean
The problem, as best I can tell, is that when I subtract one UTC millisecond value from another, I end up with a value that doesn't fit into a regular Java int. So, for example, one of the operator calls in the above code will result in the falling maths:
return( 1572408000000 - 1450155600000 )
... which gives us the value (with commas), 122,252,400,000. A signed Java int can only hold +/- (with commas) 2,147,483,647. As such, our mathematical result is too large by many billions.
To fix this issue, I am simply reverting back to a more deterministic set of operator responses:
<cfscript>
// .....
projects.sort(
( a, b ) => {
if ( a.createdAt == b.createdAt ) {
return( 0 );
}
// Sort the projects by CREATED DATE DESC (with most recent dates at the
// top of the resultant array).
return( ( b.createdAt < a.createdAt ) ? -1 : 1 );
}
);
// .....
</cfscript>
This requires a bit more code; but, reduces the set of possible return values to -1, 0, and 1, which we know will always fit into the underlying Java int. And, if we re-run the ColdFusion code with this updated sort operator, we get the following output:
I still like the idea of using Subtraction to power the array.sort() operator in Lucee CFML; however, it's good to know that this approach comes with some caveats. Namely that the return value must fit into a Java int. I imagine that I'll only ever run into this issue when sorting arrays based on UTC milliseconds, since I'm not sure how else I would be dealing with such large numbers. In those cases, I can just fallback to using more traditional logic.
Want to use code from this post? Check out the license.
Reader Comments
I found a bug in Lucee after playing with your example, I would like to be able to label anonymous functions like in javascript, coz that [lambda_6i] is a bit confusing.
I tried defining a name for the component, but Lucee barfs, ACF is fine
function ( a, b ) name="comparator", throws name cannot be defined twice
https://luceeserver.atlassian.net/browse/LDEV-2792
@Zac,
Oh, that's interesting! It would never have even occurred to me that you could try to name a function using the meta-data after the function signature. But, I guess can we do that kind of thing with
localModeand other constructs, so it makes sense. Good find :)Same thing I replied with on Twitter:
Right, so technically the docs request -1, 0 and 1 from the callback, not any positive or negative number. So while it's defo buggy, you're sort of bending the rules. This version of your code works fine: https://trycf.com/gist/3a15ed54bb5feaa99976f2db0234c9b6/lucee5?theme=monokai #CFML
Seemed worth ticketing:
https://luceeserver.atlassian.net/browse/LDEV-2793
https://tracker.adobe.com/#/view/CF-4207690
@Brad,
Ah, :face-palm: -- I am so used to using
.sort()in JavaScript, it didn't occur to me that the it might be documented differently in ColdFusion / Lucee. I am sure that I read the docs, but I probably glossed right over it. And JavaScript is explicitly documented to be loosey-goosey with the values, ex from Mozilla Developer Network:etc.
Thanks for pointing out that Lucee has more explicitly documented constraints. Though, as you point out, it actually does work to some degree.
This is what the
sgnfunction is for...https://cfdocs.org/sgn
@Adam,
Oh chickens! I don't think I've ever even seen the
sgnfunction! Ha ha, no doubt this has been there since like ACF 7 or something and I just never saw it (or, if I did, wouldn't have know what it was useful for).@Adam,
Thanks for the hot tip - I wrote up an example, mostly just to drill it into my head:
www.bennadel.com/blog/4247-using-sgn-to-clamp-values-in-array-sorting-operations-in-coldfusion.htm