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
localMode
and 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
sgn
function is for...https://cfdocs.org/sgn
@Adam,
Oh chickens! I don't think I've ever even seen the
sgn
function! 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