JavaScript: Find Matches AND Differences in two Arrays of Objects

Viewed 105

Okay, so this is a tough one. I think.

I have two arrays of objects that contain some information about pods. I'm looking to provide a side by side comparison of environments to show where pods contain the exact same image; the trick is that I also need to catch the pods where the images aren't matching. The results are going to be rendered on a table which will look something like this (at least as a first iteration):

|---------------------|------------------|------------------|
|      Pod Name       | Image from Env1  |  Image from Env2 |
|---------------------|------------------|------------------|
|          foo        |     foo:1.0.0    |     foo:1.0.0    |   <---- images match
|---------------------|------------------|------------------| 
|          foo        |   foobar:1.0.0   |                  |   <---- No match for image tag; image name different from the pod name as the pods may contain multiple images
|---------------------|------------------|------------------| 
|          foo        |                  |    foobar:2.0.0  |   <---- As above
|---------------------|------------------|------------------|
|          bar        |     bar:2.0.0    |     bar:2.0.0    |   <---- images match 
|---------------------|------------------|------------------|
|          baz        |     baz:1.0.0    |                  |   <----
|---------------------|------------------|------------------|         Note the 'no match'; so own row for now
|          baz        |                  |     baz:2.0.0    |   <----
|---------------------|------------------|------------------|

I will later use CSS to highlight the matches vs. differences.

The things that I'm struggling with most are:

  • Pods may have more than 1 image. 1 of those images may match between environments, but the other may not. When there is more than one image the tags inside that pod differ (see foo pod in table above)
  • How to check the objects match between environments whilst still capturing the ones that don't match. Using nested loops cause a whole bunch of issues with duplicates and false negatives
  • I'm unsure on the best way to store the output. Likely something like: [{ "Pod Name": "foo", "Image_Env_1": foo:1.0.0", "Image_Env_2": foo:1.0.0"}... and leaving blank strings (like the blank cell in the table) for a value in the non-matching cases. But this may clash with the other image in the foo pod that has different image tags.

So Env1 array looks something like this snippet:

[
    {
        "Image": {
            "S": "foo:1.0.0"
        },
        "Pod Name": {
            "S": "foo"
        }
    },
    {
        "Image": {
            "S": "foobar:0.2.0"
        },
        "Pod Name": {
            "S": "foo"
        }
    },
    {
        "Image": {
            "S": "bar:1.0.0"
        },
        "Pod Name": {
            "S": "bar"
        }
    },
    {
        "Image": {
            "S": "baz:1.0.0"
        },
        "Pod Name": {
            "S": "baz"
        }
    },
    {
        "Image": {
            "S": "qux:1.0.0"
        },
        "Pod Name": {
            "S": "foo"
        }
    }
]

Env2 array:

[
    {
        "Image": {
            "S": "foo:2.0.0"
        },
        "Pod Name": {
            "S": "foo"
        }
    },
    {
        "Image": {
            "S": "foobar:0.2.0"
        },
        "Pod Name": {
            "S": "foo"
        }
    },
    {
        "Image": {
            "S": "bar:1.0.0"
        },
        "Pod Name": {
            "S": "bar"
        }
    },
    {
        "Image": {
            "S": "baz:3.0.0"
        },
        "Pod Name": {
            "S": "baz"
        }
    }
]

2 Answers

You could write a little helper method which aggregates the infos from your env-files, i.e. first group by pod and then aggregate the used images. Filtering entries where an image is used in multiple environments should be trivial then.

Something like (note that this would work for an arbitrary amount of environments):

function getAggregatedPodInfos(...environments) {
    const aggregatedInfos = {pods: {}, environments: []};
    environments.forEach(env => addEnvironmentInfos(env, aggregatedInfos))
    return aggregatedInfos;
}

function addImageInfoByEnvironment(pod, environment, imageName) {
    if (!pod.imageInfo[environment.envName]) {
        pod.imageInfo[environment.envName] = [];
    }
    pod.imageInfo[environment.envName].push(imageName)
}

function addEnvironmentInfos(environment, envInfo) {
    envInfo.environments.push(environment.envName);
    for (const envEntry of environment.data) {
        const podName = envEntry["Pod Name"]["S"];
        const imageName = envEntry["Image"]["S"];
        if (!envInfo.pods[podName]) {
            envInfo.pods[podName] = {imageInfo: {}};
        }
        addImageInfoByEnvironment(envInfo.pods[podName], environment, imageName);
    }
}

const res = getAggregatedPodInfos({envName: 'env1', data: env1}, {envName: 'env2', data: env2});
console.log(JSON.stringify(res))

This would print:

{
    "pods": {
        "foo": {
            "imageInfo": {
                "env1": ["foo:1.0.0", "foobar:0.2.0", "qux:1.0.0"],
                "env2": ["foo:2.0.0", "foobar:0.2.0"]
            }
        },
        "bar": {
            "imageInfo": {
                "env1": ["bar:1.0.0"],
                "env2": ["bar:1.0.0"]
            }
        },
        "baz": {
            "imageInfo": {
                "env1": ["baz:1.0.0"],
                "env2": ["baz:3.0.0"]
            }
        }
    },
    "environments": ["env1", "env2"]
}

This is pretty tough! But I think the process can be broken down into two main stages:

  1. Finding the matches and differences.
  2. Converting that information into something useful.

The first step can be further broken down into manipulating the original data and then processing it.

What I like to do when dealing with categories, like Pod Name, is to create Map objects that contain Set objects. In this case, it would probably look something like this:

// Let's assume Env1 is loaded already.
const podNameMap1 = new Map();
Env1.forEach( (value, index) => {
    // Here 'value' is element in Env1

    const podName = value['Pod Name'].S;
    const podValue = podNameMap.get(podName);
    // returns undefined if it does not exist

    if (!podValue) {
        podNameMap.set(podName, new Set(value['Image'].S) );
        // If there is no podValue, set podName to a new Set with the images in it.
    } else {
        // Here there is a podValue, and we can add a new member to the Set.
        podValue.add(value['Images'].S);
        // podValue is a Set object which means the map value needs to be set one time.
    }
});

At the end of that podNameMap1 has all the pods and all of their images. The next step is to do the same thing to Env2, at which point there are two maps of various pod names and images.

After that, the comparison can be made, which is more complicated than creating the maps:

// Let's assume podNameMap1 and podNameMap2 are both loaded.
// I don't know if it's possible for pods to be mismatched,
// I'm going to assume they are cannot be.

// Images in both
const inBothEnvsMap = new Map();
// Images in env1 but not env2
const inEnv1Map = new Map();
// Images in env2 but not env1
const inEnv2Map = new Map();

podNameMap1.forEach( (imageSet1, podName) => {
    // Here, imageSet1 and podName are self explanitory
    const imageSet2 = podNameMap1.get(podName); // assuming this is always the case

    // Computing the set intersections n stuff
    const inEnv1 = new Set();
    const inEnv2 = new Set();
    const inBothEnvs = new Set();

    // Note: I wish JS had built in set operations because there's probably
    // a better way to do this but alas...
    imageSet1.forEach( image => {
        if (imageSet2.has(image))
            inBothEnvs.add(image);
        else // in imageSet1 only
            inEnv1.add(image);
    });
    // now inEnv1 AND inBothEnvs are complete

    imageSet2.forEach( image => {
        if (!imageSet1.has(image))
            inEnv2.add(image);
    });

    // now this pod is done
    inBothEnvsMap.set(podName, inBothEnvs);
    inEnv1Map.set(podName, inEnv1);
    inEnv2Map.set(podname, inEnv2);
});

At this point, this is the "finding the matches and differences" step.

Moving on to the next step "converting that information into something useful" is the part that's less specific to simple set operations so I can't really give sound advice on that.

Related