Compare 2 Dictionaries

How can I compare two dictionaries with the same keys (or JSON objects) to find the keys whose values don't match?

dict_1
{
  "A": "1",
  "B": "2", <---
  "C": "3",
  "D": "4"
}

dict_2
{
  "A": "1",
  "B": "5", <---
  "C": "3",
  "D": "4"
}

I know how to iterate through the values in an array and how to retrieve names/values in a dictionary keys collection, but it seems like there should be a straightforward way to do this and it's escaping me.

It does always feel like that, but equality proves more elusive than we expect – "a veneer that hides ... complexities"

For a list of "shallow" differences (ignoring the fact that JSON keys can have stuctured values like Arrays and Objects), you could try something like this:

Shallow difference between two dictionaries.kmmacros (6.3 KB)


Expand disclosure triangle to view JS source
return (() => {
    "use strict";

    const main = () =>
        either(
            alert("Shallow differences between two dictionaries.")
        )(
            diffs => diffs
        )(
            bindLR(
                jsonParseLR(kmvar.local_DictA)
            )(
                dictA => fmapLR(
                    shallowDiff(dictA)
                )(
                    jsonParseLR(kmvar.local_DictB)
                )
            )
        );


    // ---------- SHALLOW DICTIONARY DIFFERENCE ----------

    // shallowDiff :: Dict -> Dict -> [(String, (a, b))]
    const shallowDiff = x =>
    // A list of any differences between two
    // dictionaries.
        y => {
            const [xkeys, ykeys] = [x, y].map(Object.keys);

            return (
                xkeys.length >= ykeys.length
                    ? xkeys
                    : ykeys
            )
            .flatMap(k => {
                const [xv, yv] = [x, y].map(
                    dict => dict[k]
                );

                return xv === yv
                    ? []
                    : [[k, [xv, yv]]];
            });
        };

    // ----------------------- JXA -----------------------

    // alert :: String => String -> IO String
    const alert = title =>
        s => {
            const sa = Object.assign(
                Application("System Events"), {
                    includeStandardAdditions: true
                });

            return (
                sa.activate(),
                sa.displayDialog(s, {
                    withTitle: title,
                    buttons: ["OK"],
                    defaultButton: "OK"
                }),
                s
            );
        };

    // --------------------- GENERIC ---------------------

    // Left :: a -> Either a b
    const Left = x => ({
        type: "Either",
        Left: x
    });


    // Right :: b -> Either a b
    const Right = x => ({
        type: "Either",
        Right: x
    });


    // bindLR (>>=) :: Either a ->
    // (a -> Either b) -> Either b
    const bindLR = lr =>
        // Bind operator for the Either option type.
        // If lr has a Left value then lr unchanged,
        // otherwise the function mf applied to the
        // Right value in lr.
        mf => "Left" in lr
            ? lr
            : mf(lr.Right);


    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        // Application of the function fl to the
        // contents of any Left value in e, or
        // the application of fr to its Right value.
        fr => e => "Left" in e
            ? fl(e.Left)
            : fr(e.Right);


    // fmapLR (<$>) :: (b -> c) -> Either a b -> Either a c
    const fmapLR = f =>
        // Either f mapped into the contents of any Right
        // value in e, or e unchanged if is a Left value.
        e => "Left" in e
            ? e
            : Right(f(e.Right));

    // jsonParseLR :: String -> Either String a
    const jsonParseLR = s => {
        try {
            return Right(JSON.parse(s));
        } catch (e) {
            return Left(
                [
                    e.message,
                    `(line:${e.line} col:${e.column})`
                ].join("\n")
            );
        }
    };


    return JSON.stringify(main(), null, 2);
})();

Seems so.

Thanks for sharing the JavaScript approach. Here's what I have been using with native actions, making a before and after list of the relevant variables/values, then comparing them line by line. It works, but I thought I was overcomplicating it. Maybe not.

1 Like

and, FWIW, a variant – For Each through JSON keys, comparing values:

With For Each Key in JSON- Shallow differences dictionaries.kmmacros (8.8 KB)

2 Likes

@peternlewis

Not sure how feasible or desirable this looks to you, but perhaps

(in analogy to the array index count (array length) returned by the %JSONValue% token's [0] index for for Array values)

it might helpful in this kind of case to be able to count the number of keys in a Dictionary object, using the same syntax ?

i.e. if the Keyboard Maestro Variable name local_Dict were bound to the JSON string:

{
  "A": "alpha",
  "B": "beta",
  "C": "gamma",
  "D": "delta"
}

then we might get the value 4 returned by the token expression:

%JSONValue%local_Dict[0]%

(Under the hood, in JS terms, Object.keys(dict).length)


The analogy seems fairly natural and direct, in the sense that in the case of an Array:

Object.keys(
    ["Alpha", "Beta", "Gamma", "Delta"]
);

evaluates to:

[
  "0",
  "1",
  "2",
  "3"
]

So something corresponding to Object.keys(someValue).length would apply to both JSON Objects and JSON Arrays.


In the OP's case (comparing dictionaries):

  1. inequality could be detected early as a difference in the number of dictionary keys.
  2. listing specific differences would be simplified by easily discovering which of the two dictionaries had more keys.
1 Like

This is exactly what I was looking for: to count the dictionary keys or pairs within a JSON object and then access them by index number.

Since I couldn't find a way to do that, I came up with the "workaround" I posted, but it didn't occur to me to use the key name from the first dictionary in the second dictionary. My brain was stuck on comparing the lines in order. Thanks for the insight :+1:

2 Likes

That can work, but I'm never very clear about the extent to which the ECMAScript standard for JS guarantees that order is necessarily preserved for Object keys (other than the numeric key indices of Arrays).

Possibly prudent not to rely too heavily on key order ?


I think the picture is that, in practice, if not in theory, most JavaScript interpreters preserve insertion order for string keys.

(of course what that means in practice depends on the history of how the object was formed, and may be a little counter-intuitive. Omega comes to the top, in the following example, with the JavaScript for Automation interpreter)

Expand disclosure triangle to view JS source
(() => {
    "use strict";

    const main = () => {
        const dict1 = [
            "Alpha", "Gamma", "Beta",
            "Zeta", "Epsilon"
        ]
        .reduce(
            (a, k) => (a[k] = k, a),
            {}
        );

        const Omega = "Omega";

        const dict2 = Object.assign(
            {Omega},
            dict1
        );

        return sj(dict2);
    };

    // --------------------- GENERIC ---------------------

    // sj :: a -> String
    const sj = (...args) =>
    // Abbreviation of showJSON for quick testing.
    // Default indent size is two, which can be
    // overriden by any integer supplied as the
    // first argument of more than one.
        JSON.stringify.apply(
            null,
            1 < args.length && !isNaN(args[0])
                ? [args[1], null, args[0]]
                : [args[0], null, 2]
        );

    return main();
})();

{
  "Omega": "Omega",
  "Alpha": "Alpha",
  "Gamma": "Gamma",
  "Beta": "Beta",
  "Zeta": "Zeta",
  "Epsilon": "Epsilon"
}
1 Like