The opposite day, I checked out making a HashCode-inspired algorithm for ColdFusion such that you would inform if two completely different knowledge buildings had been the identical, making an allowance for ColdFusion’s unfastened sort system. Solely, as soon as I used to be completed, I noticed that it did not absolutely serve my functions. Finally, I want it for the companion app to my characteristic flags e book; and, in my companion app, the versioning of configuration knowledge must case delicate for keys. As such, I wanted to replace my FusionCode.cfc
ColdFusion element to permit for 2 configuration choices:
-
caseSensitiveKeys
– I decide if struct keys and column names are canonicalized utilizingucase()
. When enabled,key
andKEY
shall be thought-about completely different. -
typeCoercion
– I decide if strict resolution features ought to be used when inspecting a given worth. When disabled,false
and"no"
shall be thought-about completely different. As will1
and"1"
.
As a way to make sort coercion configurable, I had to usher in some strict resolution features that have a look at the underlying Java sorts representing the given ColdFusion values. This is not an apparent factor to do as a result of—no less than in Adobe ColdFusion—there’s a combination of native Java courses and customized coldfusion.runtime.*
courses. I acquired just a little assist from ChatGPT in developing with the checklist of fall-back data-types; so, I am not solely certain if it is correct.
It additionally appears that ColdFusion typically parses numeric values into java.math.BigDecimal
situations. So, I added some checks for that too:
element {
/**
* I decide if the given worth is one in every of Java's particular quantity sorts.
*/
non-public boolean perform isComplexNumber( required any worth )
/**
* I decide if the given worth is strictly a Boolean.
*/
non-public boolean perform isStrictBoolean( required any worth )
// Fall-back checks for legacy ColdFusion sorts.
isInstanceOf( worth, "coldfusion.runtime.CFBoolean" )
);
/**
* I decide if the given worth is strictly a Date.
*/
non-public boolean perform isStrictDate( required any worth )
// Fall-back checks for legacy ColdFusion sorts.
isInstanceOf( worth, "coldfusion.runtime.OleDateTime" )
);
/**
* I decide if the given worth is strictly a numeric sort.
*/
non-public boolean perform isStrictNumeric( required any worth )
isInstanceOf( worth, "coldfusion.runtime.CFShort" )
);
}
I additionally added default settings to the ColdFusion element instantiation that would subsequently be overridden on a per-call foundation. The default settings create for a stricter canonicalization (enabling key case-sensitivity and disabling sort coercion).
Within the following snippet, discover that .deepEquals()
and .getFusionCode()
each settle for an choices
argument that defaults to the constructor-provided values.
element {
/**
* I initialize the element with the given default settings.
*/
public void perform init(
boolean caseSensitiveKeys = true,
boolean typeCoercion = false
) {
variables.defaultOptions = {
// I decide if struct keys ought to be normalized throughout hashing. That means,
// ought to the important thing `"identify"` and the important thing `"NAME"` be canonicalized as the identical
// key? Or, ought to they be thought-about two completely different keys?
caseSensitiveKeys: caseSensitiveKeys,
// I decide if type-coercion ought to be allowed throughout hashing. That means,
// ought to the boolean `true` and the string `"true"` be canonicalized because the
// identical enter? Or, ought to all values be saved of their offered sorts? This
// setting DOES NOT apply to low-level Java knowledge sorts that fall beneath the
// identical ColdFusion umbrella. That means, a Java "int" and a Java "lengthy" are each
// nonetheless native "numeric" values in ColdFusion. As such, they are going to be
// canonicalized as the identical worth throughout hashing.
typeCoercion: typeCoercion
};
variables.Double = createObject( "java", "java.lang.Double" );
}
// ---
// PUBLIC METHODS.
// ---
/**
* I decide if the 2 values are equal based mostly on their generated FusionCodes.
*/
public boolean perform deepEquals(
any valueA,
any valueB,
struct choices = defaultOptions
) {
var codeA = getFusionCode( arguments?.valueA, choices );
var codeB = getFusionCode( arguments?.valueB, choices );
return ( codeA == codeB );
}
/**
* I calculate the FusionCode for the given worth.
*
* The FusionCode algorithm creates a CRC-32 checksum after which traverses the given knowledge
* construction and provides every visited worth to the checksum calculation. Since ColdFusion
* is a loosely typed / dynamically typed language, the FusionCode algorithm has to
* make some judgement calls. For instance, because the Java int `3` and the Java lengthy `3`
* are each native "numeric" sorts in ColdFusion, they are going to each be canonicalized as
* the the identical worth. Nonetheless, in terms of completely different native ColdFusion sorts,
* such because the Boolean worth `true` and the quasi-equivalent string worth `"YES"`, sort
* coercion shall be based mostly on the passed-in choices; and, on the order by which sorts
* are checked throughout the traversal.
*/
public numeric perform getFusionCode(
any worth,
struct choices = defaultOptions
) {
var checksum = createObject( "java", "java.util.zip.CRC32" ).init();
visitValue( coalesceOptions( choices ), checksum, arguments?.worth );
return checksum.getValue();
}
}
Because the choices
is outlined on a per-call foundation, not on a per-CFC-instance foundation, it signifies that each go to methodology needed to be up to date to each settle for and propagate the choices
throughout the recursion. However, it additionally signifies that the CFC is a bit simpler to check since I can simply swap-out the choices
for a similar cached CFC occasion.
<cfscript>
fusionCode = new FusionCode(
/* DEFAULT: caseSensitiveKeys = true , */
/* DEFAULT: typeCoercion = false */
);
// -- Testing key-case sensitivity. -- //
assertEquals(
{ caseSensitiveKeys = false },
{ "foo": "" },
{ "FOO": "" }
);
assertNotEquals(
{ caseSensitiveKeys = true },
{ "foo": "" },
{ "FOO": "" }
);
// -- Testing type-coercion. -- //
settings = { typeCoercion = true };
assertEquals( settings, 12.0, "12" );
assertEquals( settings, 1, "1.0" );
assertEquals( settings, 10000000, "10000000.0" );
assertNotEquals( settings, 10000000, 10000000.1 );
assertEquals( settings, true, "sure" );
assertEquals( settings, "no", false );
assertEquals( settings, "2024-02-14", createDate( 2024, 2, 14 ) );
assertEquals( settings, [ javaCast( "null", "" ) ], [ javaCast( "null", "" ) ] );
settings = { typeCoercion = false };
assertNotEquals( settings, 10000000, 10000000.1 );
assertNotEquals( settings, 1, "1" );
assertNotEquals( settings, 12.2, "12.2" );
assertNotEquals( settings, true, "sure" );
assertNotEquals( settings, true, "true" );
assertNotEquals( settings, 0, false );
assertNotEquals( settings, "false", false );
assertNotEquals( settings, "2024-02-14", createDate( 2024, 2, 14 ) );
assertNotEquals(
settings,
[ "a", javaCast( "null", "" ) ],
[ javaCast( "null", "" ), "a" ]
);
// Even when type-coercion is disabled, "advanced" numbers ought to nonetheless be coerced into
// native numbers for the sake of canonicalization. Typically, ColdFusion will use
// these values behind the scenes and we won't management that.
assertEquals(
settings,
createObject( "java", "java.math.BigInteger" ).init( "12" ),
createObject( "java", "java.math.BigDecimal" ).init( "12.0" )
);
assertEquals(
settings,
createObject( "java", "java.math.BigDecimal" ).init( 12 ),
createObject( "java", "java.math.BigDecimal" ).init( 12.0 )
);
assertNotEquals(
settings,
createObject( "java", "java.math.BigInteger" ).init( "12" ),
createObject( "java", "java.math.BigDecimal" ).init( "12.1" )
);
assertNotEquals(
settings,
createObject( "java", "java.math.BigDecimal" ).init( 12 ),
createObject( "java", "java.math.BigDecimal" ).init( 12.1 )
);
assertEquals(
settings,
createObject( "java", "java.math.BigInteger" ).init( "12" ),
12
);
assertEquals(
settings,
createObject( "java", "java.math.BigDecimal" ).init( "12.0" ),
12
);
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
/**
* I assert that the given values have the SAME FusionCode utilizing the given choices.
*/
public void perform assertEquals(
required struct choices,
required any valueA,
required any valueB
) {
if ( ! fusionCode.deepEquals( valueA, valueB, choices ) ) {
writeDump(
label = "Anticipated True, acquired False",
var = [
options: options,
valueA: valueA,
valueB: valueB
]
);
}
}
/**
* I assert that the given values have DIFFERENT FusionCodes utilizing the given choices.
*/
public void perform assertNotEquals(
required struct choices,
required any valueA,
required any valueB
) {
if ( fusionCode.deepEquals( valueA, valueB, choices ) ) {
writeDump(
label = "Anticipated False, acquired True",
var = [
options: options,
valueA: valueA,
valueB: valueB
]
);
}
}
</cfscript>
The above ColdFusion code would not output something as a result of the entire checks move.
Here is the complete code for my FusionCode.cfc
ColdFusion element:
element
output = false
trace = "I present strategies for producing a constant, repeatable token for a given ColdFusion knowledge construction (akin to Java's hashCode, however with configurable ColdFusion looseness)."
{
/**
* I initialize the element with the given default settings.
*/
public void perform init(
boolean caseSensitiveKeys = true,
boolean typeCoercion = false
) {
variables.defaultOptions = {
// I decide if struct keys ought to be normalized throughout hashing. That means,
// ought to the important thing `"identify"` and the important thing `"NAME"` be canonicalized as the identical
// key? Or, ought to they be thought-about two completely different keys?
caseSensitiveKeys: caseSensitiveKeys,
// I decide if type-coercion ought to be allowed throughout hashing. That means,
// ought to the boolean `true` and the string `"true"` be canonicalized because the
// identical enter? Or, ought to all values be saved of their offered sorts? This
// setting DOES NOT apply to low-level Java knowledge sorts that fall beneath the
// identical ColdFusion umbrella. That means, a Java "int" and a Java "lengthy" are each
// nonetheless native "numeric" values in ColdFusion. As such, they are going to be
// canonicalized as the identical worth throughout hashing.
typeCoercion: typeCoercion
};
variables.Double = createObject( "java", "java.lang.Double" );
}
// ---
// PUBLIC METHODS.
// ---
/**
* I decide if the 2 values are equal based mostly on their generated FusionCodes.
*/
public boolean perform deepEquals(
any valueA,
any valueB,
struct choices = defaultOptions
) {
var codeA = getFusionCode( arguments?.valueA, choices );
var codeB = getFusionCode( arguments?.valueB, choices );
return ( codeA == codeB );
}
/**
* I calculate the FusionCode for the given worth.
*
* The FusionCode algorithm creates a CRC-32 checksum after which traverses the given knowledge
* construction and provides every visited worth to the checksum calculation. Since ColdFusion
* is a loosely typed / dynamically typed language, the FusionCode algorithm has to
* make some judgement calls. For instance, because the Java int `3` and the Java lengthy `3`
* are each native "numeric" sorts in ColdFusion, they are going to each be canonicalized as
* the the identical worth. Nonetheless, in terms of completely different native ColdFusion sorts,
* such because the Boolean worth `true` and the quasi-equivalent string worth `"YES"`, sort
* coercion shall be based mostly on the passed-in choices; and, on the order by which sorts
* are checked throughout the traversal.
*/
public numeric perform getFusionCode(
any worth,
struct choices = defaultOptions
) {
var checksum = createObject( "java", "java.util.zip.CRC32" ).init();
visitValue( coalesceOptions( choices ), checksum, arguments?.worth );
return checksum.getValue();
}
// ---
// PRIVATE METHODS.
// ---
/**
* I merge the given choices and the default choices. The given choices takes a better
* priority, overwriting any default choices.
*/
non-public struct perform coalesceOptions( required struct choices ) {
return defaultOptions.copy().append( choices );
}
/**
* I decide if the given worth is one in every of Java's particular quantity sorts.
*/
non-public boolean perform isComplexNumber( required any worth )
/**
* I decide if the given worth is strictly a Boolean.
*/
non-public boolean perform isStrictBoolean( required any worth )
// Fall-back checks for legacy ColdFusion sorts.
isInstanceOf( worth, "coldfusion.runtime.CFBoolean" )
);
/**
* I decide if the given worth is strictly a Date.
*/
non-public boolean perform isStrictDate( required any worth )
// Fall-back checks for legacy ColdFusion sorts.
isInstanceOf( worth, "coldfusion.runtime.OleDateTime" )
);
/**
* I decide if the given worth is strictly a numeric sort.
*/
non-public boolean perform isStrictNumeric( required any worth )
isInstanceOf( worth, "coldfusion.runtime.CFShort" )
);
/**
* I obfuscate the given stringified worth in order that it would not unintentionally collide with
* a string literal. When the visited values are canonicalized, they're typically
* transformed to STRING values; and, I have to ensure that the stringified model of
* a price would not match a local string worth that may be current within the user-
* offered knowledge construction.
*/
non-public string perform obfuscate( required string worth ) {
return "[[______#value#______]]";
}
/**
* I add the given Boolean worth to the checksum.
*/
non-public void perform putBoolean(
required any checksum,
required boolean worth
) {
putString( checksum, obfuscate( worth ? "true" : "false" ) );
}
/**
* I add the given date worth to the checksum.
*/
non-public void perform putDate(
required any checksum,
required date worth
) {
putString( checksum, obfuscate( dateTimeFormat( worth, "iso" ) ) );
}
/**
* I add the given quantity worth to the checksum.
*/
non-public void perform putNumber(
required any checksum,
required numeric worth
) {
putString(
checksum,
obfuscate( Double.toString( javaCast( "double", worth ) ) )
);
}
/**
* I add the given string worth to the checksum.
*/
non-public void perform putString(
required any checksum,
required string worth
) {
checksum.replace( charsetDecode( worth, "utf-8" ) );
}
/**
* I go to the given array worth, recursively visiting every component.
*/
non-public void perform visitArray(
required struct choices,
required any checksum,
required array worth
) {
var size = arrayLen( worth );
for ( var i = 1 ; i <= size ; i++ ) {
putNumber( checksum, i );
if ( arrayIsDefined( worth, i ) ) {
visitValue( choices, checksum, worth[ i ] );
} else {
visitValue( choices, checksum /* , NULL */ );
}
}
}
/**
* I go to the given binary worth.
*/
non-public void perform visitBinary(
required struct choices,
required any checksum,
required binary worth
) {
checksum.replace( worth );
}
/**
* I go to the given advanced quantity.
*/
non-public void perform visitComplexNumber(
required struct choices,
required any checksum,
required any worth
) {
// ColdFusion appears to typically parse numeric literals into BigInteger and
// BigDecimal. Let's convert each of these to DOUBLE. I feel that there is a
// probability that some worth truncation could happen right here; however, I feel it might be edge-
// case sufficient to not fear about it.
putNumber( checksum, worth.doubleValue() );
}
/**
* I go to the given Java worth.
*/
non-public void perform visitJava(
required struct choices,
required any checksum,
required any worth
) {
putNumber( checksum, worth.hashCode() );
}
/**
* I go to the given null worth.
*/
non-public void perform visitNull(
required struct choices,
required any checksum
) {
putString( checksum, obfuscate( "null" ) );
}
/**
* I go to the given question worth, recursively visiting every row.
*/
non-public void perform visitQuery(
required struct choices,
required any checksum,
required question worth
) {
var columnNames = worth.columnList
.listToArray()
.type( "textnocase" )
.toList( "," )
;
if ( choices.caseSensitiveKeys ) {
putString( checksum, columnNames );
} else {
putString( checksum, ucase( columnNames ) );
}
for ( var i = 1 ; i <= worth.recordCount ; i++ ) {
putNumber( checksum, i );
visitStruct( choices, checksum, queryGetRow( worth, i ) );
}
}
/**
* I go to the given easy worth.
*/
non-public void perform visitSimpleValue(
required struct choices,
required any checksum,
required any worth
) {
// In the case of coercing sorts in ColdFusion, there isn't any excellent strategy. We
// would possibly come out with a special outcome relying on the order by which we examine
// the categories. For instance, the worth "1" is each a Numeric sort and a Boolean
// sort. And the worth "2023-03-03" is each a String sort and Date sort. These
// values shall be hashed in a different way relying on which sort we examine first. As
// such, I simply needed to decide and attempt to be constant. That is definitely
// not an ideal algorithm.
if ( choices.typeCoercion ) {
if ( isNumeric( worth ) ) {
putNumber( checksum, worth );
} else if ( isDate( worth ) ) {
putDate( checksum, worth );
} else if ( isBoolean( worth ) ) {
putBoolean( checksum, worth );
} else {
putString( checksum, worth );
}
// No type-coercion - strict matches solely.
} else {
if ( isStrictNumeric( worth ) ) {
putNumber( checksum, worth );
} else if ( isStrictDate( worth ) ) {
putDate( checksum, worth );
} else if ( isStrictBoolean( worth ) ) {
putBoolean( checksum, worth );
} else {
putString( checksum, worth );
}
}
}
/**
* I go to the given struct worth, recursively visiting every entry.
*/
non-public void perform visitStruct(
required struct choices,
required any checksum,
required struct worth
) {
var keys = structKeyArray( worth )
.type( "textnocase" )
;
for ( var key in keys ) {
if ( choices.caseSensitiveKeys ) {
putString( checksum, key );
} else {
putString( checksum, ucase( key ) );
}
if ( structKeyExists( worth, key ) ) {
visitValue( choices, checksum, worth[ key ] );
} else {
visitValue( choices, checksum /* , NULL */ );
}
}
}
/**
* I go to the given xml worth.
*/
non-public void perform visitXml(
required struct choices,
required any checksum,
required xml worth
) {
// Word: I am simply punting on the case-sensitivity right here since I do not use XML. Does
// anybody use XML anymore?
putString( checksum, obfuscate( toString( worth ) ) );
}
/**
* I go to the given generic worth, routing the worth to a extra particular go to methodology.
*
* Word: This methodology would not examine for values that would not in any other case be in primary knowledge
* construction. For instance, I am not checking for issues like Closures or CFC situations.
* That is meant for use with serializable knowledge.
*/
non-public void perform visitValue(
required struct choices,
required any checksum,
any worth
) {
if ( isNull( worth ) ) {
visitNull( choices, checksum );
} else if ( isArray( worth ) ) {
visitArray( choices, checksum, worth );
} else if ( isStruct( worth ) ) {
visitStruct( choices, checksum, worth );
} else if ( isQuery( worth ) ) {
visitQuery( choices, checksum, worth );
} else if ( isXmlDoc( worth ) ) {
visitXml( choices, checksum, worth );
} else if ( isBinary( worth ) ) {
visitBinary( choices, checksum, worth );
} else if ( isComplexNumber( worth ) ) {
visitComplexNumber( choices, checksum, worth );
} else if ( isSimpleValue( worth ) ) {
visitSimpleValue( choices, checksum, worth );
} else {
visitJava( choices, checksum, worth );
}
}
}
That is largely for my very own inner use and exploration. However, perhaps there’s one thing in right here that you simply discover attention-grabbing.
Wish to use code from this submit?
Take a look at the license.
https://bennadel.com/4681