8

(This is a coldfusion question)

I've got two different Structs that may or may not contain the same data, and I want to be able to see if they do! My Structs will always contain simple values (Numbers, Strings or Booleans) because they are being created with DeserializeJSON, so hopefully this can be done easily.

I found Ben Nadel's post here, but that technique doesn't seem to working for me. Here is what I've tried so far (some cfwheels code in there):

itemA = DeSerializeJSON(model("itemsnapshot").findByKey(4).json);
itemB = DeSerializeJSON(model("itemsnapshot").findByKey(5).json);

StructDelete(itemA,"updatedAt");
StructDelete(itemB,"updatedAt");
StructDelete(itemA,"createdAt");
StructDelete(itemB,"createdAt");

writedump(itemA);
writedump(itemB);

out = itemA.Equals(itemB);
writedump(out);

And the results of that look like:

Struct
code string C112
companyid number 1
cost number 5000
deletedAt string 
description string Nightstand
id number 70634
itemtypeid string 13
projectid number 8
unittypeid string

Struct
code string C112
companyid number 1
cost number 5000
deletedAt string 
description string Nightstand
id number 70634
itemtypeid string 13
projectid number 8
unittypeid string 

boolean false

so, as you'll see above, although the data inside the Structs appear to match exactly they do not pass the Equals() test.

Has anyone else done this successfully?

Jeromy French
  • 11,812
  • 19
  • 76
  • 129
Jordan Sitkin
  • 2,303
  • 3
  • 26
  • 35

6 Answers6

9

Here's Ben's solution quickly adjusted to my needs, you can adjust it further (and hopefully make it pretier):

<cffunction name="DiffStructs" hint="Compute the differences between two structures" access="public" output="true" returntype="array" >
        <cfargument name="First" type="struct" required="true" />
        <cfargument name="Second" type="struct" required="true" />
        <cfargument name="ignoreMissing" type="boolean" required="false" default="false" />
        <cfargument name="ignoreFirstEmptyString" type="boolean" required="false" default="false" />
        <cfargument name="ignoreSecondEmptyString" type="boolean" required="false" default="false" />

        <cfset var Result = arrayNew(1) >
        <cfset var Keys = structNew() >
        <cfset var KeyName = "" >
        <cfset var obj = "" >
        <cfset var firstOk = true >
        <cfset var secondOk = true >

        <cfloop collection="#Arguments.First#" item="KeyName">
                <cfset Keys[KeyName]=1>
        </cfloop>
        <cfloop collection="#Arguments.Second#" item="KeyName">
                <cfset Keys[KeyName]=1>
        </cfloop>
        <cfloop collection="#Keys#" item="KeyName">
            <cfif NOT StructKeyExists(Arguments.First, KeyName)  >
                    <cfif NOT arguments.ignoreMissing>
                        <cfif structFind(Arguments.Second, KeyName) neq "">
                            <cfif arguments.ignoreSecondEmptyString>
                                <cfset obj = {  key = KeyName
                                                ,old = ""
                                                ,new = structFind(Arguments.Second, KeyName) } >
                                <cfset arrayAppend(Result, obj )>
                            </cfif>
                        </cfif>
                    </cfif>

            <cfelseif NOT StructKeyExists(Arguments.Second, KeyName)>
                    <cfif NOT arguments.ignoreMissing>
                        <cfif structFind(Arguments.First, KeyName) neq "">
                            <cfif arguments.ignoreFirstEmptyString >
                                <cfset obj = {  key = KeyName
                                                ,old = structFind(Arguments.First, KeyName) 
                                                ,new = "" } >
                                <cfset arrayAppend(Result, obj )>
                            </cfif>
                        </cfif>
                    </cfif>

            <cfelseif Arguments.First[KeyName] NEQ Arguments.Second[KeyName] >

                <cfset firstOk = true >
                <cfset secondOk = true >

                <cfif structFind(Arguments.Second, KeyName) eq "">
                    <cfif arguments.ignoreSecondEmptyString>
                        <cfset firstOk = false >
                    </cfif>
                </cfif>

                <cfif structFind(Arguments.First, KeyName) eq "">
                    <cfif arguments.ignoreFirstEmptyString>
                        <cfset secondOk = false >
                    </cfif>
                </cfif>

                <cfif firstOk AND secondOk >
                    <cfset obj = {  key = KeyName
                                    ,old = structFind(Arguments.First, KeyName) 
                                    ,new = structFind(Arguments.Second, KeyName) } >
                    <cfset arrayAppend(Result, obj )>
                </cfif>
            </cfif>

        </cfloop>

        <cfreturn Result>
    </cffunction>
zarko.susnjar
  • 2,053
  • 2
  • 17
  • 35
  • I think you want `NOT arguments.ignoreFirstEmptyString` on lines 25 and `NOT arguments.ignoreSecondEmptyString` on line 37. AS IS if a key exists in one structure but not in the other it will not be returned. If you use the `NOT` then it will return the key if it is in one structure and not the other. – kralco626 Mar 13 '14 at 17:10
4

If you're using CF9 or Railo 3

ArrayContains([struct1], struct2);  //case-sensitive

or

ArrayFindNoCase([struct1], struct2));  //case-insensitive, 0 if not the same.
ArrayContainsNoCase([struct1], struct2); // if you use Railo
Henry
  • 32,689
  • 19
  • 120
  • 221
  • great info. although i was trying to do a simple true or false compare in my question, the diff array generated by zarko's function has inspired a change in my interface which will offer much more useful functionality. I could use one of these to do an initial check before running the full diff, though. – Jordan Sitkin Jun 30 '10 at 10:26
3

Hidden away in Coldfusion Structures is a handy little method called hashCode(). Although keep in mind that this is undocumented.

<cfif struct1.hashCode() Eq struct2.hashCode()>

</cfif>
Mister Dai
  • 796
  • 1
  • 6
  • 19
  • 1
    Nope, just tried it out on Railo and the hashCode value is always different. Although... st1.toString() Eq st2.toString() works on Railo ;) – Mister Dai Jun 30 '10 at 11:00
1

You may also perform this using the native Java method inherited by the CFC.

isThisTrue = ObjA.equals(ObjB);
Aaron Greenlee
  • 4,587
  • 4
  • 26
  • 37
  • 2
    note that this is the technique i demonstrated in the initial post. with my two structs that appear similar, it yields "false". equals() does not work in all cases. – Jordan Sitkin Feb 02 '11 at 22:59
0

Here is something I threw together quickly. It has parameter to determine whether or not to do case-sensitive comparison of values and keys. Throw these two functions (StructEquals(), ArrayEquals()) in some kind of utilities CFC.

Limitation: Does not work for structs/arrays containing queries or objects.

<cffunction name="StructEquals" access="public" returntype="boolean" output="false"
            hint="Returns whether two structures are equal, going deep.">
  <cfargument name="stc1" type="struct" required="true" hint="First struct to be compared." />
  <cfargument name="stc2" type="struct" required="true" hint="Second struct to be compared." />
  <cfargument name="blnCaseSensitive" type="boolean" required="false" default="false" hint="Whether or not values are compared case-sensitive." />
  <cfargument name="blnCaseSensitiveKeys" type="boolean" required="false" default="false" hint="Whether or not structure keys are compared case-sensitive." />
  <cfscript>
    if(StructCount(stc1) != StructCount(stc2))
      return false;

    var arrKeys1 = StructKeyArray(stc1);
    var arrKeys2 = StructKeyArray(stc2);

    ArraySort(arrKeys1, 'text');
    ArraySort(arrKeys2, 'text');

    if(!ArrayEquals(arrKeys1, arrKeys2, blnCaseSensitiveKeys, blnCaseSensitiveKeys))
      return false;

    for(var i = 1; i <= ArrayLen(arrKeys1); i++) {
      var strKey = arrKeys1[i];

      if(IsStruct(stc1[strKey])) {
        if(!IsStruct(stc2[strKey]))
          return false;
        if(!StructEquals(stc1[strKey], stc2[strKey], blnCaseSensitive, blnCaseSensitiveKeys))
          return false;
      }
      else if(IsArray(stc1[strKey])) {
        if(!IsArray(stc2[strKey]))
          return false;
        if(!ArrayEquals(stc1[strKey], stc2[strKey], blnCaseSensitive, blnCaseSensitiveKeys))
          return false;
      }
      else if(IsSimpleValue(stc1[strKey]) && IsSimpleValue(stc2[strKey])) {
        if(blnCaseSensitive) {
          if(Compare(stc1[strKey], stc2[strKey]) != 0)
            return false;
        }
        else {
          if(CompareNoCase(stc1[strKey], stc2[strKey]) != 0)
            return false;
        }
      }
      else {
        throw("Can only compare structures, arrays, and simple values. No queries or complex objects.");
      }
    }

    return true;
  </cfscript>
</cffunction>

<cffunction name="ArrayEquals" access="public" returntype="boolean" output="false"
            hint="Returns whether two arrays are equal, including deep comparison if the arrays contain structures or sub-arrays.">
  <cfargument name="arr1" type="array" required="true" hint="First struct to be compared." />
  <cfargument name="arr2" type="array" required="true" hint="Second struct to be compared." />
  <cfargument name="blnCaseSensitive" type="boolean" required="false" default="false" hint="Whether or not values are compared case-sensitive." />
  <cfargument name="blnCaseSensitiveKeys" type="boolean" required="false" default="false" hint="Whether or not structure keys are compared case-sensitive, if array contains structures." />
  <cfscript>
    if(ArrayLen(arr1) != ArrayLen(arr2))
      return false;

    for(var i = 1; i <= ArrayLen(arr1); i++) {
      if(IsStruct(arr1[i])) {
        if(!IsStruct(arr2[i]))
          return false;
        if(!StructEquals(arr1[i], arr2[i], blnCaseSensitive, blnCaseSensitiveKeys))
          return false;
      }
      else if(IsArray(arr1[i])) {
        if(!IsArray(arr2[i]))
          return false;
        if(!ArrayEquals(arr1[i], arr2[i], blnCaseSensitive, blnCaseSensitiveKeys))
          return false;
      }
      else if(IsSimpleValue(arr1[i]) && IsSimpleValue(arr2[i])) {
        if(blnCaseSensitive) {
          if(Compare(arr1[i], arr2[i]) != 0)
            return false;
        }
        else {
          if(CompareNoCase(arr1[i], arr2[i]) != 0)
            return false;
        }
      }
      else {
        throw("Can only compare structures, arrays, and simple values. No queries or complex objects.");
      }
    }

    return true;
  </cfscript>
</cffunction>

Unit Tests for anyone interested:

public void function test_StructEquals() {
  AssertTrue(utils.StructEquals({}, StructNew()));
  AssertTrue(utils.StructEquals({}, StructNew(), true, true));

  AssertFalse(utils.StructEquals({}, {"a": "b", "c": "d"}));

  AssertTrue(utils.StructEquals({"a": "b", "c": "d"}, {"C": "D", "A": "B"}));
  AssertFalse(utils.StructEquals({"a": "b", "c": "d"}, {"C": "D", "A": "B"}, true, false));
  AssertFalse(utils.StructEquals({"a": "b", "c": "d"}, {"C": "D", "A": "B"}, false, true));

  AssertTrue(utils.StructEquals({"a": "b", "c": "d"}, {"C": "d", "A": "b"}));
  AssertTrue(utils.StructEquals({"a": "b", "c": "d"}, {"C": "d", "A": "b"}, true, false));
  AssertFalse(utils.StructEquals({"a": "b", "c": "d"}, {"C": "d", "A": "b"}, false, true));

  AssertTrue(utils.StructEquals({"a": "b", "c": "d"}, {"c": "D", "a": "B"}));
  AssertFalse(utils.StructEquals({"a": "b", "c": "d"}, {"c": "D", "a": "B"}, true, false));
  AssertTrue(utils.StructEquals({"a": "b", "c": "d"}, {"c": "D", "a": "B"}, false, true));

  var stc1 = {
    "test": {
      "hello": "world",
      "goodbye": "space",
      "somearr": [
        { "a": 1, "b": 2 },
        "WORD",
        [
          { "x": 97, "y": 98, "z": 99 },
          { "i": 50, "j": 51, "k": 52 }
        ]
      ]
    }
  };
  var stc2 = {
    "test": {
      "goodbye": "space",
      "hello": "world",
      "somearr": [
        { "a": 1, "b": 2 },
        "WORD",
        [
          { "z": 99, "x": 97, "y": 98 },
          { "i": 50, "k": 52, "j": 51 }
        ]
      ]
    }
  };

  AssertTrue(utils.StructEquals(stc1, stc2, true, true));
  stc2.test.somearr[2] = "WOrD";
  AssertTrue(utils.StructEquals(stc1, stc2));
  AssertTrue(utils.StructEquals(stc1, stc2, false, true));
  AssertFalse(utils.StructEquals(stc1, stc2, true, false));
  stc2.test.somearr[3][1] = { "z": 99, "X": 97, "y": 98 };
  AssertTrue(utils.StructEquals(stc1, stc2));
  AssertFalse(utils.StructEquals(stc1, stc2, false, true));
  AssertFalse(utils.StructEquals(stc1, stc2, true, false));
  stc2.test.somearr[2] = "WORD";
  AssertTrue(utils.StructEquals(stc1, stc2));
  AssertFalse(utils.StructEquals(stc1, stc2, false, true));
  AssertTrue(utils.StructEquals(stc1, stc2, true, false));
}

public void function test_ArrayEquals() {
  AssertTrue(utils.ArrayEquals([], ArrayNew(1)));
  AssertTrue(utils.ArrayEquals([], ArrayNew(1), true, true));

  AssertFalse(utils.ArrayEquals([], [1, 2, 3]));

  AssertTrue(utils.ArrayEquals(['a', 'b', 'c'], ['A', 'B', 'C']));
  AssertFalse(utils.ArrayEquals(['a', 'b', 'c'], ['A', 'B', 'C'], true, false));
  AssertTrue(utils.ArrayEquals(['a', 'b', 'c'], ['A', 'B', 'C'], false, true));

  AssertFalse(utils.ArrayEquals(['a', 'b', 'c'], ['a', 'c', 'b']));

  AssertTrue(utils.ArrayEquals([1, 2, 3], [1, 2, 3]));
  AssertFalse(utils.ArrayEquals([1, 2, 3], [1, 3, 2]));
}
Kip
  • 107,154
  • 87
  • 232
  • 265
0
if(serializeJSON(itemA) eq serializeJSON(itemB))
  //They match!
else
  //They don't!

You are already using JSON to manipulate these, you should just keep with that. Not that anyone cares a decade later, but that method works for me.

Bryce Rakop
  • 146
  • 7