the digitalmouse
the digitalmouse

Reputation: 222

javascript compare two multidimensional arrays for changes then update the second array with those changes

I have a water monitoring web-page that shows which sprinklers in a list are running, and the amount of remaining time left until they stop/turn off.

I am using an array as a simple state machine to remember data received via web socket from server-side nodejs code. I've got Vue.js on the client-side to reactively watch the list array for changes and update the page.

For simplicity, the arrays looks something like this:

  "data": [
    {
      "id": "1",
      "isRunning": false,
      "runningEntries": [],
    },
    {
      "id": "2",
      "isRunning": true,
      "runningEntries": [
        {
          "timerID": "2",
          "remainTime": "1730",
        },
        {
          "timerID": "3",
          "remainTime": "550",
        }
      ]
    }
  ]

"runningEntries" may have 0-n entries. The watering hardware I get data from can have overlapping timers with a cumulative effect on the water delivered - in the example above, Sprinkler Id #2 will water it's target with twice the volume for 550 seconds then timer #3 ends but timer #2 keeps the sprinkler going for it's remaining time.

The server periodically sends a new list that may contain updated info somewhere down the line. I could get a duplicate of the last data which causes erratic client countdown timers (Appears to be a problem at the server-side, but I can't fix that at the moment (yet)).

I know how to compare simple arrays ( [1,2,3] !== [1,2,4] ) but I don't have much experience in searching through multi-dimensional arrays in Javascript. I'm thinking good old recursive for() loops or something more modern like .forEach() or some kind of .map()/.filter() combo. Just not sure where to start with multi-dimensional arrays.

socketData.data.forEach( sprinkler =>
  sprinkler.runningEntries.forEach( runningEntry => {
    if( runningEntry.remainTime does not exist or it's value is less than the matching remainTime entry in the state array) {
      overwrite the matching record 
      or pushing a new one on to the runningEntries stack for that sprinkler
    } else {
      do nothing/skip to the next one
    }
  })
)

I can visualize what to do (hence the pseudo-code above) and know how to get down to the data to check, but after that the brain goes mush. Any helpful hints, known similar existing examples to learn from, or pokes in the right direction would be greatly appreciated.

Upvotes: 0

Views: 528

Answers (1)

Mike Coakley
Mike Coakley

Reputation: 191

I'll give you a few options. First, I'll answer your original question. Second, I'll give you a suggestion that will make it more performant. Third, I'll offer another option that changes how you store your state.

Option 1

I believe the main issue is that as you iterate over the source array you need to find the matching elements in the state machine array. There are many methods to doing that but the easiest is to simply "do it". Meaning as you find an element to compare, then find the matching element in the other array. Since you have nested data, you'll do that at two levels.

This code will work (but is not performant):

const data = [
  {
    id: "1",
    isRunning: false,
    runningEntries: [],
  },
  {
    id: "2",
    isRunning: true,
    runningEntries: [
      {
        timerID: "2",
        remainTime: "1730",
      },
      {
        timerID: "3",
        remainTime: "550",
      },
    ],
  },
];

const stateData = [
  {
    id: "1",
    isRunning: false,
    runningEntries: [],
  },
  {
    id: "2",
    isRunning: false,
    runningEntries: [
      {
        timerID: "2",
        remainTime: "1730",
      },
    ],
  },
];

function findSprinkler(data, desiredId) {
  // For let's us break out early, forEach doesn't
  for (const sprinkler of data) {
    if (sprinkler.id === desiredId) {
      return sprinkler;
    }
  }
  return null;
}

function findTimer(data, desiredId) {
  for (const timer of data) {
    if (timer.timerID === desiredId) {
      return timer;
    }
  }
  return null;
}

for (const sprinkler of data) {
  const stateSprinkler = findSprinkler(stateData, sprinkler.id);
  if (stateSprinkler) {
    console.log("We have a matching sprinkler");
    // Do some comparison and/or updates to the sprinkler data
    for (const timer of sprinkler.runningEntries) {
      const stateTimer = findTimer(stateSprinkler.runningEntries, timer.timerID);
      if (stateTimer) {
        console.log("We have a matching timer");
        // Do some comparison and/or updates to the timer data
      }
    }
  }
}

Option 2

If you are open to it, change your data structure to use object keys instead of array indexes. Your data already has unique keys so then it is as simple as a direct comparison.

Your new data structure would look like:

"data": {
  "sprinkler1": {
    "id": "1",
    "isRunning": false,
    "runningEntries": {},
  },
  "sprinkler2": {
    "id": "2",
    "isRunning": true,
    "runningEntries": {
      "timer2": {
        "timerID": "2",
        "remainTime": "1730",
      },
      "timer3": {
        "timerID": "3",
        "remainTime": "550",
      }
    }
  }
}

I left the majority of the data intact because I'm unsure of how it is generated. But in this case you could just iterate over the object keys and do a direct comparison between the two data points (incoming data and your state). This would definitely speed up your processing on the client side but I'm unsure of the required time to generate the proper data from the server perspective - however, if you can offload work to the server, that is always better.

Here is the code:

const data = {
  sprinkler1: {
    id: "1",
    isRunning: false,
    runningEntries: {},
  },
  sprinkler2: {
    id: "2",
    isRunning: true,
    runningEntries: {
      timer2: {
        timerID: "2",
        remainTime: "1730",
      },
      timer3: {
        timerID: "3",
        remainTime: "550",
      },
    },
  },
};

const stateData = {
  sprinkler1: {
    id: "1",
    isRunning: false,
    runningEntries: {},
  },
  sprinkler2: {
    id: "2",
    isRunning: true,
    runningEntries: {
      timer2: {
        timerID: "2",
        remainTime: "1730",
      },
    },
  },
};

Object.keys(data).forEach((key) => {
  const sprinkler = data[key];
  const stateSprinkler = stateData[key];
  if (stateSprinkler) {
    console.log("We have a matching sprinkler");
    // Do some comparison and/or updates to the sprinkler data
    Object.keys(sprinkler.runningEntries).forEach((key) => {
      const timer = sprinkler.runningEntries[key];
      const stateTimer = stateSprinkler.runningEntries[key];
      if (stateTimer) {
        console.log("We have a matching timer");
        // Do some comparison and/or updates to the timer data
      }
    });
  }
});

Option 3

Instead of storing your state in the same structure as the data you receive, simply store your state records in a cache that is keyed by a key that you can build based upon the incoming data. This solution is a hybrid of both solutions above. You still iterate over the incoming data as an array but for each item you iterate over build the cache key and see if that item is in the cache. If so, perform your logic to update it. If not, put it in the cache. The only thing you would have to add is a garbage collection routine that goes through the cache and removes items that are no longer in your incoming data. It seems this would be natural with the data you have but on the surface I'm not sure.

Upvotes: 1

Related Questions