Reputation: 85
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
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
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
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:
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:
{ action: { } }
object (from the comments on your question). If not required, processing entries[0]
with the code inside the for..of block suffices.key
determine the key that is checked for duplicates.
action
is the determining key, the first (commented) line would be enough. (This makes "Addy" appear in the results, counted twice.)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
Reputation:
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