3

I'm struggling with merging two arrays of objects (fetched from blockchain data) into a new array based on object values.

The goal is to get the latest interaction with a user.

A simplified but close representation of the data structure this problem is faced in:

interface MsgSlice {
    messageId: string;
    messageDataSlice: {
        senderId?: string;
        receiverId: string;
        timestamp: number;
    };
};

const latestReceivedMsgs: MsgSlice[] = [
    {
        messageId: "messageId1",
        messageDataSlice: {
            senderId: "userId1",
            receiverId: "ownerId", // <- always same in that array
            timestamp: 101,
        },
    },
    {
        messageId: "messageId3",
        messageDataSlice: {
            senderId: "userId2",
            receiverId: "ownerId",
            timestamp: 103,
        },
    },
    {
        messageId: "messageId5",
        messageDataSlice: {
            senderId: "userId3",
            receiverId: "ownerId",
            timestamp: 105,
        },
    },
];

const latestSentMsgs: MsgSlice[] = [
    {
        messageId: "messageId2",
        messageDataSlice: {
            // senderId: "ownerId",
            receiverId: "userId1",
            timestamp: 102,
        },
    },
    {
        messageId: "messageId4",
        messageDataSlice: {
            receiverId: "userId3",
            timestamp: 104,
        },
    },
];

The desired result should contain the latest messageId either 'sent to' or 'received by' the corresponding user. Something like this:


const latestInteraction = [
    {
        user: "userId1",
        messageId: "messageId2",
        timestamp: 102,
    },
    {
        user: "userId2",
        messageId: "messageId3",
        timestamp: 103,
    },
    {
        user: "userId3",
        messageId: "messageId5",
        timestamp: 105,
    },
]   

As a solution I thought of looping over the arrays and per iteration also looping over the other array to compare the senderId and receiverId values. If "senderId is == one of the looped receiverIds", it could be sent into an interaction array and then time sorted and filtered. Unfortunately, I couldn't figure out how to get it working. My thinking might be limited here, and there are likely more efficient ways to do it than in my solution concept.

tenxsoydev
  • 370
  • 2
  • 10
  • Does the order of the output array matter to you? If so, how would you like them ordered? – jcalz Sep 12 '22 at 13:56
  • I've spotted a small mistake with the timestamps in the desired outcome. I edited it to better represent the goal. The output array should contain the latest `messageId` either 'sent to' or 'received by' the corresponding user. For the use-case, the order inside that array is not important as far as I can see then. But if i had to choose it would be ordering by timestamp. – tenxsoydev Sep 12 '22 at 14:02
  • Wouldn't it then just be enough to get the single object with the highest timestamp, if that's your goal? – Foxcode Sep 12 '22 at 14:08
  • 1
    Does [this approach](https://tsplay.dev/m3X92W) work for you? If so I can post it as an answer an explain; if not, what am I missing? (Please mention @jcalz if you want me to be notified of a reply) – jcalz Sep 12 '22 at 14:12
  • @Foxcode Whats given is the sentObjects (to a user) with the highest timestamp and reveivedObjects (from a user) with the highest timestamps. From that I need to compare - based on the user this interaction is happening with - if sent or received Object has the higher timestamp. – tenxsoydev Sep 12 '22 at 14:14
  • @jcalz this looks very promising and works in first implementation tests. I'll learn to know more in a full implementation. But I already think this is the solution here! – tenxsoydev Sep 12 '22 at 14:29

4 Answers4

1

The approach I'd take is to convert your received and sent messages into a single array of "interactions" that contain only the information you care about. For a received message you want to look at the senderId, whereas for a sent message you want to look at the receiverId (the idea is that you want the other user for each interaction, not the current user). That could look like this:

interface Interaction {
  user: string
  messageId: string
  timestamp: number
}

function latestInteractions(
  receivedMsgs: MsgSlice[], 
  sentMsgs: MsgSlice[]
): Interaction[] {

  const allInteractions: Interaction[] = [];
  for (const m of receivedMsgs) {
    const sender = m.messageDataSlice.senderId;
    if (sender === undefined) continue;
    allInteractions.push({
      user: sender,
      messageId: m.messageId,
      timestamp: m.messageDataSlice.timestamp
    });
  }
  for (const m of sentMsgs) {
    allInteractions.push({
      user: m.messageDataSlice.receiverId,
      messageId: m.messageId,
      timestamp: m.messageDataSlice.timestamp
    });
  }

Note that if somehow a received message doesn't have a senderId then we just skip it. Maybe we should throw an error instead? That's up to you. Now we have a single array filled with all interactions. We want to collect just one such interaction for each user in the array, and if we ever have more than one we should keep just the one with the greatest timestamp. That could look like this:

  const interactionMap: { [k: string]: Interaction } = {};
  for (const i of allInteractions) {
    if (!(i.user in interactionMap) || interactionMap[i.user].timestamp < i.timestamp) {
      interactionMap[i.user] = i;
    }
  }

The interactionMap is now a plain object whose keys are the user strings and whose values are the latest Interaction for each user. This has all the information we want, but you want an array and not an object. So we can just use the Object.values() method to get an array of the values:

  return Object.values(interactionMap);
}

That's an array in some order; if you care you can sort it according to your needs.


Let's make sure it works with your example:

const latestInteraction = latestInteractions(latestReceivedMsgs, latestSentMsgs);
console.log(latestInteraction);
/* [{
  "user": "userId1",
  "messageId": "messageId2",
  "timestamp": 102
}, {
  "user": "userId2",
  "messageId": "messageId3",
  "timestamp": 103
}, {
  "user": "userId3",
  "messageId": "messageId5",
  "timestamp": 105
}]  */

Looks good!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
1

You can use the hash grouping approach, the vanila JS solution

Live Demo:

const latestReceivedMsgs = [{messageId: "messageId1",messageDataSlice: {senderId: "userId1",receiverId: "ownerId", timestamp: 101,},},{messageId: "messageId3",messageDataSlice: {senderId: "userId2",receiverId: "ownerId",timestamp: 103,},},{messageId: "messageId5",messageDataSlice: {senderId: "userId3",receiverId: "ownerId",timestamp: 105,},},];
const latestSentMsgs = [{messageId: "messageId2",messageDataSlice: {receiverId: "userId1",timestamp: 102,},},{messageId: "messageId4",messageDataSlice: {receiverId: "userId3",timestamp: 104,},},];

const grouped = [...latestReceivedMsgs, ...latestSentMsgs]
  .reduce((acc, { messageId, messageDataSlice }) => {
    const { timestamp, senderId, receiverId } = messageDataSlice;
    const user = senderId ?? receiverId;
    const msgItem = { user, messageId, timestamp };
    if ((acc[user]?.timestamp ?? 0) < timestamp) acc[user] = msgItem;
    
    return acc;
  }, {});

const result = Object.values(grouped);

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0 }

UPDATE

Or typescript variant:

interface MsgSlice {
  messageId: string;
  messageDataSlice: {
    senderId?: string;
    receiverId?: string;
    timestamp: number;
  };
};

interface Interaction {
  user: string
  messageId: string
  timestamp: number
};

const latestReceivedMsgs: MsgSlice[] = [{messageId: "messageId1",messageDataSlice: {senderId: "userId1",receiverId: "ownerId", // <- always same in that array},},{messageId: "messageId3",messageDataSlice: {senderId: "userId2",receiverId: "ownerId",timestamp: 103,},},{messageId: "messageId5",messageDataSlice: {senderId: "userId3",receiverId: "ownerId",timestamp: 105,},},];
const latestSentMsgs: MsgSlice[] = [{messageId: "messageId2",messageDataSlice: {receiverId: "userId1",timestamp: 102,},},{messageId: "messageId4",messageDataSlice: {receiverId: "userId3",timestamp: 104,},},];

const grouped = ([...latestReceivedMsgs, ...latestSentMsgs] as MsgSlice[])
  .reduce((acc, { messageId, messageDataSlice }) => {
    const { timestamp, senderId, receiverId } = messageDataSlice;
    const user = senderId ?? receiverId ?? "unindefined";
    const msgItem = { user, messageId, timestamp };
    if ((acc[user]?.timestamp ?? 0) < timestamp) acc[user] = msgItem
    
    return acc;
  }, {} as { [key: Interaction['user']]: Interaction });

const result: Interaction[] = Object.values(grouped);

console.log(result);
A1exandr Belan
  • 4,442
  • 3
  • 26
  • 48
  • Need to ask a stupid question here: Testing this approach and trying to satisfy typescript. Returning an array instead of an object would give me an easy time to achieve this. And it seems to work without problems. It would have the user as index, which looks kinda unfamiliar. Does anything speak against it to use it as array? – tenxsoydev Sep 12 '22 at 16:05
  • 1
    @tenxsoydev So I make typescript solution [here](https://tsplay.dev/WKRL8m) – A1exandr Belan Sep 12 '22 at 17:28
0

You could simply flatten both arrays into one, then sort them per timestamp. Such as:

let msgs: MsgSlice[] = [];
msgs.push(...latestReceivedMsgs);
msgs.push(...latestSentMsgs);

msgs.sort((a, b) => {
    return a.timestamp - b.timestamp ;
});
matreurai
  • 147
  • 12
-1

Perhaps you can join them into a single array and then sort them by their timestamp?

const sortedMsgs = [...latestReceivedMsgs, ...latestSentMsgs]
sortedMsgs.sort((a,b)=>a.messageDataSlice.timestamp-b.messageDataSlice.timestamp)
Abdulla Nilam
  • 36,589
  • 17
  • 64
  • 85
Drew
  • 21
  • 4
  • I just assumed this was for a chat room or something and you wanted it sorted by the timestamp. – Drew Sep 12 '22 at 14:03
  • The OP didn't provide enough details for this answer to be valid. Please use the comments to find out more first. – emerson.marini Sep 12 '22 at 14:03