Compare dictionary

Hi

I am trying to set up unit tests for my project and I'm wondering about comparing objects...

This works...

  var inputData = {
                "data"=>[
                          {"distance"=>5"title"=>"Distance is 5"}, 
                          {"distance"=>3"title"=>"Distance is 3"},
                        ]
              };
  var expectedData = inputData;
  var actualData = inputData;
  Test.assert(actualData.equals(expectedData));
  Test.assert(actualData == expectedData);

This works...

  var inputData = {
                "data"=>[
                          {"distance"=>5"title"=>"Distance is 5"}, 
                          {"distance"=>3"title"=>"Distance is 3"},
                        ]
              };
  var expectedData = inputData;
  var actualData = inputData;
  Test.assert(actualData.equals(expectedData));
  Test.assert(actualData == expectedData);
This does not....
  var inputData = {
                "data"=>[
                          {"distance"=>5"title"=>"Distance is 5"}, 
                          {"distance"=>3"title"=>"Distance is 3"},
                        ]
              };
  var expectedData = {
                "data"=>[
                          {"distance"=>5"title"=>"Distance is 5"}, 
                          {"distance"=>3"title"=>"Distance is 3"},
                        ]
              };

  Test.assert(expectedData.equals(inputData));
  Test.assert(expectedData == inputData);
Why not?
Thanks
Gary
  • EDIT: To respond to your edited question:

    For your first example, both equals() and "==" work because you are comparing two references to a single dictionary instance.

    In you second example, each variable is a reference to a distinct dictionary instance (with identical contents):

    - "==" compares by reference, so it won't work to compare the contents of  two dictionary instances

    - It's not 100% clear to me what Object.equals() does for dictionaries (may or may not be equivalent to "==" in this case), but it apparently doesn't do the deep value comparison that you would want it to do.

    Neither of those works to compare dictionaries, but you can write your own function.

        function dictionaryEquals(a, b) {
            if (a == null && b == null) {
                return true;
            }
    
            if (a == null || b == null) {
                return false;
            }
    
            var a_keys = a.keys();
            if (a_keys.size() != b.keys().size()) {
                return false;
            }
    
            for (var i = 0; i < a_keys.size(); i++) {
                if (!b.hasKey(a_keys[i])) {
                    return false;
                }
    
                if (b.get(a_keys[i]) != a.get(a_keys[i])) {
                    return false;
                }
            }
            return true;
        }
    

  • Thank you, makes sense. I had hoped to avoid writing a deepEqual but seems like that is the way forward. Thanks for the example, it's a great start

  • Quick update and it's good news. After your helpful example above, I started writing a deepEqual function then came across this post from a couple of years ago that provides recursive verifyDictEquals(), verifyArrayEquals() and verifyValueEquals() functions.
    forums.garmin.com/.../storage-querk

    Below is a class that allows a unit test to be expressed as a riteway unit test:

    (:test)
    class ritewayTest
    {
      function assert(logger, theTest)
      {
        logger.debug("Given:   " + theTest["Given"]);
        logger.debug("Should:  " + theTest["Should"]);
        logger.debug("Actual:  " + theTest["Actual"]);
        logger.debug("Expected:" + theTest["Expected"]);

        return verifyValueEquals(logger, theTest["Actual"], theTest["Expected"]);
      }

      // deep compare dicts `a` and `b`, returns true if equals
      function verifyDictEquals(logger, a, b)
      {
        var a_sz = a.size();
        var b_sz = b.size();
        if (a_sz != b_sz) {
            return false;
        }
        
        var keys = a.keys();
        for (var i = 0; i < a_sz; ++i) {
            var key = keys[i];
            
            if (!verifyValueEquals(logger, a[key], b[key])) {
                return false;
            }
        }
        
        return true;
      }

      // deep compare arrays `a` and `b`, returns true if equals
      function verifyArrayEquals(logger, a, b)
      {
        var a_sz = a.size();
        var b_sz = b.size();
        if (a_sz != b_sz) {
            return false;
        }
          
        for (var i = 0; i < a_sz; ++i) {
            logger.debug(i.toString());
            if (!verifyValueEquals(logger, a[i], b[i])) {
                return false;
            }
        }
        
        return true;
      }

      // deep compare `a` with `b`, returns true if all of these hold true
      //
      //   1) value types are known
      //   2) value types are the same
      //   3) values are equal
      //
      function verifyValueEquals(logger, a, b)
      {
        logger.debug(Lang.format("Expected '$1$', got '$2$'.", [ a, b ]));

        if ((a instanceof Lang.Dictionary) && (b instanceof Lang.Dictionary))
        {
            return verifyDictEquals(logger, a, b);            
        } else if ((a instanceof Lang.Array) && (b instanceof Lang.Array))
        {
            return verifyArrayEquals(logger, a, b);            
        } else if ((a instanceof Lang.String) && (b instanceof Lang.String))
        {
            return a.equals(b);
        } else if ((a instanceof Lang.Double) && (b instanceof Lang.Double))
        {
            return Math.abs(b - a) < 0.001;
        } else if ((a instanceof Lang.Float) && (b instanceof Lang.Float))
        {
            return Math.abs(b - a) < 0.001;
        } else if ((a instanceof Lang.Long) && (b instanceof Lang.Long))
        {
            return a == b;            
        } else if ((a instanceof Lang.Number) && (b instanceof Lang.Number))
        {
            return a == b;
        } else if ((a instanceof Lang.Boolean) && (b instanceof Lang.Boolean))
        {
            return a == b;
        }
        return false;
      }

    // End of class
    }

    (:test)
    function testArrayOfDictionary(logger) {
      var expectedData = [
                        {"distance"=>5, "title"=>"Distance is 5"},
                        {"distance"=>3, "title"=>"Distance is 3"},
                      ];
      var actualData = [
                          {"distance"=>5, "title"=>"Distance is 5"},
                          {"distance"=>3, "title"=>"Distance is 3"},
                        ];
      var riteway = new ritewayTest();

      return riteway.assert(logger, {
        "Given" =>    "an array of segments from the server",
        "Should"=>    "sort the array into ascending distance order",
        "Actual" =>   actualData,
        "Expected" => expectedData
      });
    }


    Hope this is useful to someone in the future
    Gary


  • Thanks for the code!

    One nitpick for the code for comparing dictionaries:

     

     function verifyDictEquals(logger, a, b)
      {
        var a_sz = a.size();
        var b_sz = b.size();
        if (a_sz != b_sz) {
            return false;
        }
        
        var keys = a.keys();
        for (var i = 0; i < a_sz; ++i) {
            var key = keys[i];
            
            if (!verifyValueEquals(logger, a[key], b[key])) {
                return false;
            }
        }
        
        return true;
      }

    I wouldn't skip the check to verify that b.hasKey(key) (as in my example) because otherwise it won't be possible to distinguish the case where a dictionary has key X with value null from the case where dictionary lacks key X.

    e.g. you don't want to say that the following dictionaries are equal:

    {
        "data1"=>123
        "data2"=>null
    }

    {
        "data1"=>123
        "data3"=>null
    }

  • Interestingly the test fails with or without the b.hasKey(key) addition although I am not entirely sure why it fails without the addition! I agree this check should be there. Updated code below:

    (:test)
    class ritewayTest
    {
      function assert(logger, theTest)
      {
        logger.debug("Given:   " + theTest["Given"]);
        logger.debug("Should:  " + theTest["Should"]);
        logger.debug("Actual:  " + theTest["Actual"]);
        logger.debug("Expected:" + theTest["Expected"]);

        return verifyValueEquals(logger, theTest["Expected"], theTest["Actual"]);
      }

      // deep compare dicts `a` and `b`, returns true if equals
      function verifyDictEquals(logger, a, b)
      {
        var a_sz = a.size();
        var b_sz = b.size();
        if (a_sz != b_sz) {
            return false;
        }
        
        var keys = a.keys();
        for (var i = 0; i < a_sz; ++i) {
            var key = keys[i];
            if (!b.hasKey(key)) {
                return false;
            }
            if (!verifyValueEquals(logger, a[key], b[key])) {
                return false;
            }
        }
        
        return true;
      }

      // deep compare arrays `a` and `b`, returns true if equals
      function verifyArrayEquals(logger, a, b)
      {
        var a_sz = a.size();
        var b_sz = b.size();
        if (a_sz != b_sz) {
            return false;
        }
          
        for (var i = 0; i < a_sz; ++i) {
            logger.debug("arrayIndex:" + i.toString());
            if (!verifyValueEquals(logger, a[i], b[i])) {
                return false;
            }
        }
        
        return true;
      }

      // forums.garmin.com/.../storage-querk
      // deep compare `a` with `b`, returns true if all of these hold true
      //
      //   1) value types are known
      //   2) value types are the same
      //   3) values are equal
      //
      function verifyValueEquals(logger, a, b)
      {
        logger.debug(Lang.format("Expected '$1$', got '$2$'.", [ a, b ]));

        if ((a instanceof Lang.Dictionary) && (b instanceof Lang.Dictionary))
        {
            return verifyDictEquals(logger, a, b);            
        } else if ((a instanceof Lang.Array) && (b instanceof Lang.Array))
        {
            return verifyArrayEquals(logger, a, b);            
        } else if ((a instanceof Lang.String) && (b instanceof Lang.String))
        {
            return a.equals(b);
        } else if ((a instanceof Lang.Double) && (b instanceof Lang.Double))
        {
            return Math.abs(b - a) < 0.001;
        } else if ((a instanceof Lang.Float) && (b instanceof Lang.Float))
        {
            return Math.abs(b - a) < 0.001;
        } else if ((a instanceof Lang.Long) && (b instanceof Lang.Long))
        {
            return a == b;            
        } else if ((a instanceof Lang.Number) && (b instanceof Lang.Number))
        {
            return a == b;
        } else if ((a instanceof Lang.Boolean) && (b instanceof Lang.Boolean))
        {
            return a == b;
        }
        return false;
      }

    // End of class
    }

    (:test)
    function testArrayOfDictionary(logger) {
      /*
      var expectedData = [
                        {"distance"=>5, "title"=>"Distance is 5"},
                        {"distance"=>3, "data2"=>null, "title"=>"Distance is 3"},
                      ];
      var actualData = [
                          {"distance"=>5, "title"=>"Distance is 5"},
                          {"distance"=>3, "data3"=>null, "title"=>"Distance is 3"},
                        ];
      */
      var expectedData = {"data1"=>123, "data2"=>null};
      var actualData = {"data1"=>123, "data3"=>null};
      var riteway = new ritewayTest();

      return riteway.assert(logger, {
        "Given" =>    "an array of segments from the server",
        "Should"=>    "sort the array into ascending distance order",
        "Actual" =>   actualData,
        "Expected" => expectedData
      });
    }

  • PS for some reason, I cannot post as code :-(

  • Yeah the forum is janky and often frustrating.

    The reason it fails either way is because you aren't considering null to be equal to null in verifyValueEquals

    Instead of return false as the final line I would have:

    return a == b

    (And you could fold your Number and Boolean cases into that default case.).

    (Given that monkey c -- and most languages other than SQL -- consider null to equall null, I think this behavior would be reasonable.)

  • awesome, update made and the test now follows the behaviour you described above. Many thanks!

  • Based on the amount of code you have, have you considered just using something simpler like arrays?

    One thing you may run into with dictionaries is the keys may exist in the two, but might not be in the same order

  • One thing you may run into with dictionaries is the keys may exist in the two, but might not be in the same order

    I think you'll find this is accounted for in the comparison functions posted above.

    have you considered just using something simpler like arrays?

    I agree with this in general tho. Dictionaries are incredibly expensive (memory-wise) in CIQ's resource-constrained environment, and I almost never use them unless I have to. (i.e. Unless the input or output of a CIQ SDK function demands that I used them.)