Monday 15 July 2013

π and strings

G'day
OK, an upfront warning: the degree of usefulness of the information herein is perhaps the most limited of all the articles I have written. So that's saying something.

I was investigating some curious code I stumbled across today. Someone needed to pass Java a byte array, and had written a UDF to effect this, horsing around making a new byte array and javaCast()-ing a CF string to be a Java string and stuff. And I was just like "or, you could have just done this":

byteArray = theString.getBytes();

Giving them the benefit of the doubt, I figured perhaps Java was not so forgiving with other ColdFusion data types, so tried this:

i = 42;
ba = i.getBytes();
writeDump(variables);

Which also worked fine. Then I decided to try with a floating point number, and decided to use pi() as the number to test with:

pi = pi();
ba = pi.getBytes();
writeDump(variables);

Bam: error:

The getBytes method was not found.

Either there are no methods with the specified method name and argument types or the getBytes method is overloaded with argument types that ColdFusion cannot decipher reliably. ColdFusion found 0 methods that match the provided arguments. If this is a Java object and you verified that the method exists, use the javacast function to reduce ambiguity.

Fair enough. So to remediate this, I stuck in an intermediary toString(), and all was good:

pi = pi();
ba = pi.toString().getBytes();

writeDump([pi,ba]);

array
13.14159265359
2
binary
5146495249535750545351535657555751

To make sure the end result was the same as via the UDF, I created a test rig to generate a byte array from both, and compare them. Eyeballing the byte arrays seemed to have them matching, but I did an actual compare() instead, which necessitated me converting them back to strings again so compare() would work:

pi = pi();

ba1 = pi.toString().getBytes();
ba2 = createByteArray(pi);

s1 = toString(ba1);
s2 = toString(ba2);

writeOutput(compare(s1,s2));

This yielded an unexpected result of -1. IE: they're not the same.  Huh? So I outputted s1 and s2:

array
13.141592653589793
23.14159265359

Double huh? Initially I thought that the byte array transformation was padding the thing with some garbage, but - no - 9793 is indeed the next four DP of pi.

The difference distilled down to this:

<cfset pi = pi()>
<cfoutput>
<pre>
pi:             #pi#
toString(pi):   #toString(pi)#
pi.toString():  #pi.toString()#
</pre>
</cfoutput>

Yields:

pi:             3.14159265359
toString(pi):   3.14159265359
pi.toString():  3.141592653589793

OK, so clearly (?) pi() returns more than the 11 (!) decimal places that show up when it's output, and ColdFusion takes it upon itself to decide "nah, you don't want to see those other four decimal places". Thanks ColdFusion. How bloody nice it would be if you focused more on doing what you're told, and less focus on second-guessing what I might actually mean when I tell you to do something. Grrrrr.

For the sake of completeness I tested a few other options, and recorded the results:

cfPi                    = pi();
javacast                = javaCast("double" , pi());
doubleInitedWithCfPi    = createObject("java", "java.lang.Double").init(pi());
javaPi                    = createObject("java", "java.lang.Math").PI;
precisionEvaluateCfPi    = precisionEvaluate(pi());
bigDecimalPi            = createObject("java", "java.math.BigDecimal").init(pi());
cfToStringOnCfPi        = toString(cfPi);
javaToStringOnCfPi        = cfPi.toString();

writeDump([
    {"cfPi"=cfPi,                                    class=class(cfPi)},
    {"javacast"=javacast,                            class=class(javacast)},
    {"doubleInitedWithCfPi"=doubleInitedWithCfPi,    class=class(doubleInitedWithCfPi)},
    {"javaPi"=javaPi,                                class=class(javaPi)},
    {"precisionEvaluateCfPi"=precisionEvaluateCfPi,    class=class(precisionEvaluateCfPi)},
    {"bigDecimalPi"=bigDecimalPi,                    class=class(bigDecimalPi)},
    {"cfToStringOnCfPi"=cfToStringOnCfPi,            class=class(cfToStringOnCfPi)},
    {"javaToStringOnCfPi"=javaToStringOnCfPi,        class=class(javaToStringOnCfPi)}
]);

function class(o){
    return o.getClass().getName();
}

Results:

array
1
struct
CLASSjava.lang.Double
cfPi3.14159265359
2
struct
CLASSjava.lang.Double
javacast3.14159265359
3
struct
CLASSjava.lang.Double
doubleInitedWithCfPi3.14159265359
4
struct
CLASSjava.lang.Double
javaPi3.14159265359
5
struct
CLASSjava.math.BigDecimal
precisionEvaluateCfPi3.14159265359
6
struct
CLASSjava.math.BigDecimal
bigDecimalPi3.141592653589793115997963468544185161590576171875
7
struct
CLASSjava.lang.String
cfToStringOnCfPi3.14159265359
8
struct
CLASSjava.lang.String
javaToStringOnCfPi3.141592653589793

So it's just Java's toString() method which will coerce the lost decimal places out of pi(). This stands to reason. And using a BigDecimal returns really a lot more decimal places from pi(). This is quite handy to know. I guess. Not that I've ever actually used pi() for anything other than a test value.

Railo, btw, performs the same way with toString(), but it doesn't have all the extra precision when using a BigDecimal, for some reason. I suppose it's second-guessing what I want to see even less helpfully than ColdFusion does. Oh well.

That's my thing learned for the day. I prefer it when what I learn is slightly more useful, really. Oh well.

Home time.

--
Adam