Ulises García
Ulises García

Reputation: 85

How can I remove and count duplicates in an array of objects? - Javascript

I have an issue trying to transform some data, all the possible solutions that I have found, seems to be "ugly" or not the best ones, so here is the problem.

I have this kind of data:

var myData = [
  {
    action:{
      remove_item: { 
        text: "delete",
        textToDisplay: "Remove"
      }
    }
  },
  {
    action:{
      remove_item: {
        text: "delete",
        textToDisplay: "Remove"
      }
    }
  },
  {
    action:{
      add_item: {
        text: "add",
        textToDisplay: "Add"
      }
    }
  }
];

And I need to get something like this at the end:

var myData = [
  {
    text: "delete",
    textToDisplay: "Remove"
    count: 2
  },
  {
    text: "add",
    textToDisplay: "Add"
    count: 1
  }
];

If you notice, I have removed the duplicates, added the action key and the count with the number of duplicates.

I hope someone has an interesting solution using ES6 or in a functional way.

Upvotes: 1

Views: 1904

Answers (4)

SHAHID
SHAHID

Reputation: 81

var myData = [
    {
        action: {
            text: "delete",
            textToDisplay: "Remove"
        },
        count: 1
    },
    {
        action: {
            text: "delete",
            textToDisplay: "Remove"
        },
        count: 1
    },
    {
        action: {
            text: "add",
            textToDisplay: "Add"
        }, 
        count: 1
    }
];

//findIndex method returns index of element passed, returns -1 if element is not in array

let uniqueData = [];
myData.forEach(data => {
let index = uniqueData.findIndex(item=> item.action.text === data.action.text);
    if (index === -1) { 
        uniqueData = [...uniqueData, data];
    }
    else {
        uniqueData[index].count++;
    }
})
console.log(uniqueData);

Upvotes: 2

Mμ.
Mμ.

Reputation: 8542

Based on the requirement of the structure of myData. One way to accomplish what you want to do using Map to keep count of the objects and make it into an array using Array.from.

var myData=[{action:{remove_item:{text:"delete",textToDisplay:"Remove"}}},{action:{remove_item:{text:"delete",textToDisplay:"Remove"}}},{action:{add_item:{text:"add",textToDisplay:"Add"}}}];

// map to keep track of element
// key : the properties of action (e.g add_item, remove_item)
// value : obj { text, textToDisplay, count }
var map = new Map();

// loop through each object in myData
myData.forEach(data => {
  
  // loop through each properties in data.action
  Object.keys(data.action).forEach(d => {
    let currKey = JSON.stringify(data.action[d]);
    let currValue = map.get(currKey);
    
    // if key exists, increment counter
    if (currValue) {
      currValue.count += 1;
      map.set(currKey, currValue);
    } else {
      // otherwise, set new key with in new object
      
      let newObj = {
        text: data.action[d].text,
        textToDisplay: data.action[d].textToDisplay,
        count: 1,
      }
      map.set(currKey, newObj);
    }
  })
});

// Make an array from map
var res = Array.from(map).map(e => e[1]);

console.log(res);

In my opinion, the key on data.action is redundant, because you are already specifying the actions in text property. So I believe a better data structure for my data is the following:

var myData = [
  {
    action:{
      text: "delete",
      textToDisplay: "Remove"
    }
  },
  {
    action:{
      text: "delete",
      textToDisplay: "Remove"
    }
  },
  {
    action:{
      text: "add",
      textToDisplay: "Add"
    }
  }
];

And if you think that is the case, you can change newObj in my previous code to make it work.

let newObj = {
  text: data.action.text,
  textToDisplay: data.action.textToDisplay,
  count: 1,
}

Upvotes: 1

try-catch-finally
try-catch-finally

Reputation: 7624

From the comments I assume this data is possible:

var myData = [
  {
    action:{
      remove_item: { 
        text: "delete",
        textToDisplay: "Remove"
      },
      edit_item: { 
        text: "edit",
        textToDisplay: "Edit"
      }
    }
  },
  {
    action:{
      remove_item: {
        text: "delete",
        textToDisplay: "Remove"
      },
      // note this _key_ is duplicated below (see note at the bottom)
      add_item: {
        text: "addy",
        textToDisplay: "Addy"
      }
    }
  },
  {
    action:{
      add_item: {
        text: "add",
        textToDisplay: "Add"
      }
    }
  }
];

A possible reducer could look like this:

myData.reduce(function(memo, obj, index) {
    let key,
        action = obj.action,
        entries = Object.entries(action),
        entry,
        data,
        keyIndex;

    // action may have multiple fields (see note)
    for (entry of entries) {
        // the object field name is the key (see note)
        // key = entry[0];

        // the text attribute is the key (see note)
        key = entry[1].text;

        // index where in result[] the object is 
        // already stored (if so)
        keyIndex = memo._keyIndex.indexOf(key);

        if (keyIndex == -1) {
            // key not indexed (and saved) yet
            data = {};
            Object.assign(data, entry[1], { count: 1 });
            memo.result.push(data);
            memo._keyIndex.push(key);
        } else {
            // key already indexed, get the data and 
            // increment counter
            data = memo.result[keyIndex];
            data.count++;
        }
    }

    if (index < myData.length - 1) {
        // for all items but last memo is an internal wrapper object
        return memo;
    } else {
        // for final item return the actual result
        return memo.result;
    }
}, { result: [] /* actual result, this is finally returned */, _keyIndex: [] /* temp. index of keys */ });

To understand this code, you should be familar with:

  • reduce() to transform an array to another data type entry by entry
  • Object.entries() to get key-value pairs of an object in form of an array
  • for..of loop to iterate array/object entries

The interesting part here is, that the "memo" object holds two references during the run. This allows tracking duplicate keys in _keyIndex. Only with the last element, the result array is returned from reduce().

Note:

  • The for..of loop is used to process multiple fields inside the { action: { } } object (from the comments on your question). If not required, processing entries[0] with the code inside the for..of block suffices.
  • The lines assigning key determine the key that is checked for duplicates.
    • If the field name in action is the determining key, the first (commented) line would be enough. (This makes "Addy" appear in the results, counted twice.)
    • If the value of the text field in an action object matters, the second line is required. (This makes "Addy" and "Add" appear in the results.)

Depending on the determining key, the resulting object may still have duplicates, but this code should direct you well enough.

Upvotes: 0

user5085788
user5085788

Reputation:

You can use reduce and map.

This is a very straightforward solution:

var myData = [ 
    { action: { text: "delete" } },
    { action: { text: "delete" } },
    { action: { text: "add" } }
];

const group = (arr) => {

    const reduced = arr.reduce((acc, curr) => {
        const text = curr.action.text;
        acc[text] = acc[text] || 0;
        acc[text] ++;
        return acc;
    }, {});

    return Object.getOwnPropertyNames(reduced).map((prop) => ({ text: prop, count: reduced[prop] }));
    
};

var grouped = group(myData);
console.log(JSON.stringify(grouped, null, 4));

Upvotes: 1

Related Questions