I have data like this:
const data = [
{ id: 1, cat: "human", age: "10" },
{ id: 2, cat: "human", age: "20" },
{ id: 3, cat: "human", age: "10" },
{ id: 4, cat: "animal", age: "10" },
{ id: 5, cat: "animal", age: "20" },
{ id: 6, cat: "animal", age: "10" },
{ id: 7, cat: "alien", age: "10" },
{ id: 8, cat: "alien", age: "20" },
{ id: 9, cat: "alien", age: "10" },
];
I want to group this data something like that:
const gr = {
human: {
all: [
{ id: 1, cat: "human", age: "10" },
{ id: 2, cat: "human", age: "20" },
{ id: 3, cat: "human", age: "10" },
],
ages: {
"10": [
{ id: 1, cat: "human", age: "10" },
{ id: 3, cat: "human", age: "10" },
],
"20": [
{ id: 2, cat: "human", age: "20" },
],
}
},
animal: {...},
alien: {...},
}
I do first reduce like that:
const gr = data.reduce((acc, el) => {
const { cat } = el;
acc[cat] = acc[cat] || { all: [] };
acc[cat].all.push(el);
return acc;
}, {});
But I can't make a nested reduce here. I can do it separately like that:
const grAge = gr.human.all.reduce((acc,el) => {
const {age} = el;
acc[age] = acc[age] || [];
acc[age].push(el);
return acc;
},{});
gr.human["ages"] = grAge;
But obviously, this is not so efficient and needs more work. Maybe like this:
Object.keys(gr).forEach(key => {
const grAge = gr[key].all.reduce((acc,el) => {
const {age} = el;
acc[age] = acc[age] || [];
acc[age].push(el);
return acc;
},{});
gr[key]["ages"] = grAge;
});
Can I join those reduces in a single step?
If there are any other good methods I can use them, I don't need to use the reduce method.
You could take a sinle loop approach and assign the wanted structure to either allor to a nested strcture.
If you like to get a more dynamic version, you need to simplify the result structure for every nesting level (this means, the age level would contain an all property).
const
data = [{ id: 1, cat: "human", age: "10" }, { id: 2, cat: "human", age: "20" }, { id: 3, cat: "human", age: "10" }, { id: 4, cat: "animal", age: "10" }, { id: 5, cat: "animal", age: "20" }, { id: 6, cat: "animal", age: "10" }, { id: 7, cat: "alien", age: "10" }, { id: 8, cat: "alien", age: "20" }, { id: 9, cat: "alien", age: "10" }],
result = data.reduce((r, o) => {
r[o.cat] = r[o.cat] || { all: [], ages: {} };
r[o.cat].all.push(o);
r[o.cat].ages[o.age] = r[o.cat].ages[o.age] || [];
r[o.cat].ages[o.age].push(o);
return r;
}, {});
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Another way to do this would be to get every unique category and ages using Sets, and then reducing them into your final JSON :
EDIT : It seems like the Stack Overflow snippet doesn't like it, but executing it in your browser console will give out the correct result
const data = [
{ id: 1, cat: "human", age: "10" },
{ id: 2, cat: "human", age: "20" },
{ id: 3, cat: "human", age: "10" },
{ id: 4, cat: "animal", age: "10" },
{ id: 5, cat: "animal", age: "20" },
{ id: 6, cat: "animal", age: "10" },
{ id: 7, cat: "alien", age: "10" },
{ id: 8, cat: "alien", age: "20" },
{ id: 9, cat: "alien", age: "10" },
];
const output = [...new Set(data.map(thing => thing.cat))].reduce((acc, category) => {
const catData = data.filter(thing => thing.cat === category)
return {
[category]: {
all: catData,
ages : [...new Set(catData.map(catThing => catThing.age))].reduce((catAcc, age) => ({
[age]: [...catData.filter(catThing => catThing.age === age)],
...catAcc
}), {})
},
...acc
}
}, {})
console.log(output)
.as-console-wrapper { max-height: 100% !important; top: 0; }
Related
I have an array of objects with names and grades of some students. I have to sort them out in descending order which I have, using forEach method, however I can't figure out how to call it in an addEventListener so that when I press a button it displays the name and the grades of the students in my browser. In the HTML document I only have one empty paragraph with an id of "#demo" and a button with an id of "#showStudents" which is supposed to display the names and grades in the empty paragraph when clicked. Down below I have attached the code:
const studentsOrder = document.getElementById("demo");
const showStudents = document.getElementById("showStudents");
const students = [
{
name: "Michael",
grade: 9,
},
{
name: "Tom",
grade: 6,
},
{
name: "Lisa",
grade: 8,
},
{
name: "Ron",
grade: 5,
},
{
name: "Daniel",
grade: 3,
},
{
name: "John",
grade: 2,
},
{
name: "Louise",
grade: 7,
},
];
students.sort((a, b) => b.grade - a.grade);
students.forEach((e) => {
console.log(`${e.name} ${e.grade}`);
});
Based on your comments you'll have to attach the listener to the button #showStudents and show them in the #demo
const studentsOrder = document.getElementById("demo");
const showStudents = document.getElementById("showStudents");
const students = [{
name: "Michael",
grade: 9,
},
{
name: "Tom",
grade: 6,
},
{
name: "Lisa",
grade: 8,
},
{
name: "Ron",
grade: 5,
},
{
name: "Daniel",
grade: 3,
},
{
name: "John",
grade: 2,
},
{
name: "Louise",
grade: 7,
},
];
students.sort((a, b) => b.grade - a.grade);
showStudents.addEventListener('click', () => {
studentsOrder.innerHTML = ''
students.forEach(e => studentsOrder.innerHTML += `${e.name} ${e.grade} <br />`);
})
<button id="showStudents">Show Students</button>
<div id="demo"></div>
A one liner solution could be using reduce() method, something like this:
const studentsOrder = document.getElementById("demo");
const showStudents = document.getElementById("showStudents");
const students = [{
name: "Michael",
grade: 9,
},
{
name: "Tom",
grade: 6,
},
{
name: "Lisa",
grade: 8,
},
{
name: "Ron",
grade: 5,
},
{
name: "Daniel",
grade: 3,
},
{
name: "John",
grade: 2,
},
{
name: "Louise",
grade: 7,
},
];
students.sort((a, b) => b.grade - a.grade);
showStudents.addEventListener('click', () => studentsOrder.innerHTML = students.reduce((accumulator, student) => `${accumulator} <br /> ${student.name} ${student.grade}`, ''))
<button id="showStudents">Show Students</button>
<div id="demo"></div>
I have an array of objects here:
const arr = [
{ id: 1, name: "test1", quantity:1 },
{ id: 2, name: "test2", quantity:1 },
{ id: 2, name: "test3", quantity:1 },
{ id: 3, name: "test4", quantity:1 },
{ id: 4, name: "test5", quantity:1 },
{ id: 5, name: "test6", quantity:1 },
{ id: 5, name: "test7", quantity:1 },
{ id: 6, name: "test8", quantity:1 }
];
I want to add quantities of the duplicate objects together before removing them
So the result is:
const arr = [
{ id: 1, name: "test1", quantity:1 },
{ id: 2, name: "test3", quantity:2 },
{ id: 3, name: "test4", quantity:1 },
{ id: 4, name: "test5", quantity:1 },
{ id: 5, name: "test6", quantity:2 },
{ id: 6, name: "test8", quantity:1 }
];
I have seen variations of it done removing duplicates using map or reduce but I haven't seen anything that can what I want to accomplish in an eloquent way without using too many loops.
I have been thinking about how to best accomplish this all day and haven't found anything, any help would be appreciated
You can use reduce with an object to store the element with each id.
const arr = [
{ id: 1, name: "test1", quantity:1 },
{ id: 2, name: "test2", quantity:1 },
{ id: 2, name: "test3", quantity:1 },
{ id: 3, name: "test4", quantity:1 },
{ id: 4, name: "test5", quantity:1 },
{ id: 5, name: "test6", quantity:1 },
{ id: 5, name: "test7", quantity:1 },
{ id: 6, name: "test8", quantity:1 }
];
const res = Object.values(
arr.reduce((acc,curr)=>{
acc[curr.id] = acc[curr.id] || {...curr, quantity: 0};
acc[curr.id].quantity += curr.quantity;
return acc;
}, {})
);
console.log(res);
const arr = [
{ id: 1, name: "test1", quantity: 1 },
{ id: 2, name: "test2", quantity: 1 },
{ id: 2, name: "test3", quantity: 1 },
{ id: 3, name: "test4", quantity: 1 },
{ id: 4, name: "test5", quantity: 1 },
{ id: 5, name: "test6", quantity: 1 },
{ id: 5, name: "test7", quantity: 1 },
{ id: 6, name: "test8", quantity: 1 }
];
var result = arr.reduce(function (r, a) {
r[a.id] = r[a.id] || { id: a.id, quantity: 0, name: a.name };
r[a.id].quantity += a.quantity;
return r;
}, Object.create(null));
console.log(JSON.stringify(result));
Using forEach loop and build object with aggregated quantity count.
const convert = (arr) => {
const res = {};
arr.forEach(({ id, ...rest }) =>
res[id] ? (res[id].quantity += 1) : (res[id] = { id, ...rest })
);
return Object.values(res);
};
const arr = [
{ id: 1, name: "test1", quantity: 1 },
{ id: 2, name: "test2", quantity: 1 },
{ id: 2, name: "test3", quantity: 1 },
{ id: 3, name: "test4", quantity: 1 },
{ id: 4, name: "test5", quantity: 1 },
{ id: 5, name: "test6", quantity: 1 },
{ id: 5, name: "test7", quantity: 1 },
{ id: 6, name: "test8", quantity: 1 },
];
console.log(convert(arr));
I have the following array of objects:
[
{
id: 1,
someOtherStuff: 'abc,
Drink: { name: 'Coca-Cola', price: 2.5 }
},
{
id: 2,
someOtherStuff: 'def,
Drink: { name: 'Fanta Orange', price: 3 }
},
{
id: 3,
someOtherStuff: 'ghi,
Drink: { name: 'Sprite', price: 1.8 }
},
{
id: 6,
someOtherStuff: 'jkl,
Drink: { name: 'Coca-Cola', price: 2.5 }
},
{
id: 7,
someOtherStuff: 'mno,
Drink: { name: 'Coca-Cola', price: 2.5 }
}
]
i want to group them by duplicates like this:
[
{
count: 3,
drinkName: 'Coca-Cola',
price: 2.5
},
{
count: 1,
drinkName: 'Fanta Orange',
price: 3
},
{
count: 1,
drinkName: 'Sprite',
price: 1.8
}
]
I have tried in several ways to map a new array.But unfortunately I can't get it to work
I hope someone can help me with that
let result= {};
inputArray.map((item)=>{
let key = `${item.Drink.name}-${item.Drink.price}`;
if(result[key]){
result[key].count = result[key].count +1;
}else{
result[key] = {count:1,drinkName:item.Drink.name,price:item.Drink.price};
}
});
result = Object.values(result);
I am trying to transform complex JavaScript object. Below is my code. As you can see, it's a lot of code. I am looking for a better/common way to achieve the same result. Maybe ES6 map/Reduce? (I am not allow to do import/require)
function test() {
var input = {
number: 555,
obj1: {
fld1: "11",
persons: [
{
name: "smith",
phone: "222-222-2222"
}
],
},
obj2: {
obj3: {
day: "2019-02-04"
}
},
myArr: [
{
number: 444,
qty: 20,
unit: "ton",
item: {
item_id: 1,
description: "item 1"
}
},
{
number: 111,
qty: 15,
unit: "pieces",
item: {
item_id: 2,
description: "item 2"
}
}
]
}
var result = {
id: input.number,
object1: {
id: input.obj1.number,
contacts: getArr2(input)
},
object2: {
date: input.obj2.obj3.day,
},
list: getArr1(input),
}
return result; // echo back the input received
}
console.log(JSON.stringify(test()));
function getArr1(input) {
var arr = [];
input.myArr.forEach(function (prod) {
let p = {
id: prod.number,
itemId: prod.item.item_id,
description: prod.item.description,
quantity: {
value: prod.qty,
uom: prod.unit
}
}
arr.push(p);
});
return arr;
}
function getArr2(input) {
var arr = [];
input.obj1.persons.forEach(function (person) {
let p = {
name: person.name,
cell: person.phone
}
arr.push(p);
});
return arr;
}
And the result is
{
"id": 555,
"object1": {
"contacts": [{
"name": "smith",
"cell": "222-222-2222"
}]
},
"object2": {
"date": "2019-02-04"
},
"list": [{
"id": 444,
"itemId": 1,
"description": "item 1",
"quantity": {
"value": 20,
"uom": "ton"
}
}, {
"id": 111,
"itemId": 2,
"description": "item 2",
"quantity": {
"value": 15,
"uom": "pieces"
}
}]
}
You could use the power of destructuring and renaming.
function getProds(products) {
return products.map(({ number: id, qty: value, unit: uom, item: { item_id: itemId, description } }) =>
({ id, itemId, description, quantity: { value, uom } }));
}
function getPersons(persons) {
return persons.map(({ name, phone: cell }) => ({ name, cell }));
}
function convert({ number: id, obj1, obj2: { obj3: { day: date } }, myArr }) {
return {
id,
object1: {
id: obj1.number,
contacts: getPersons(obj1.persons)
},
object2: { date },
list: getProds(myArr)
};
}
var data = { number: 555, obj1: { fld1: "11", persons: [{ name: "smith", phone: "222-222-2222" }], }, obj2: { obj3: { day: "2019-02-04" } }, myArr: [{ number: 444, qty: 20, unit: "ton", item: { item_id: 1, description: "item 1" } }, { number: 111, qty: 15, unit: "pieces", item: { item_id: 2, description: "item 2" } }] };
console.log(convert(data));
.as-console-wrapper { max-height: 100% !important; top: 0; }
You're on the right track with map/reduce.
Here's an example (getArr1 would be similar):
function getArr2(input) {
// Don't need map if new object is identical
// Could also do the mapping within the reduce callback
return input.obj1.persons
.map(person => ({ name: person.name, cell: person.phone }))
.reduce((accumulator, currentValue) => {
accumulator.push(currentValue);
return accumulator;
}, []);
}
There's another example in the documentation at: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce#Remove_duplicate_items_in_array
I try to get each object of an array and compare it to the objects of another array. If they match, remove the object from the second array.
Strange thing is that if an object is found two times in an array, that object is not filtered.
I want to compare newdata to existing. If an object of newdata has the same id and cat, it will not be in the new array.
existing is
var existing = [{
name: "John",
values_: {
id: 5,
cat: true
}
},
{name: "Jake",
values_: {
id: 3,
cat: true
}
},
{
name: "Alice",
values_: {
id: 2,
cat: false
}
}
];
newdata is
var newdata = [{
name: "Mike",
properties: {
id: 1,
cat: true
}
},
{name: "Jake",
properties: {
id: 3,
cat: true
}
},
{name: "Jake",
properties: {
id: 3,
cat: true
}
},
{
name: "Alice",
properties: {
id: 2,
cat: false
}
}
];
and my filter is
existing.forEach((existingitem, existingindex, existingfeatures) => {
newdata2 = newdata.filter(item => (
existingitem.values_.id != item.properties.id &&
existingitem.values_.cat != item.properties.cat
));
});
console.log('newdata2 - ',newdata2);
The logic thing is for newdata2 to have only Mike . The problem is that I can see Jake two times. Jake should not be there, its already in existing.
If I edit newdata like so (no doubles)
var newdata = [{
name: "Mike",
properties: {
id: 1,
cat: true
}
},
{name: "Jake",
properties: {
id: 3,
cat: true
}
} ,
{
name: "Alice",
properties: {
id: 2,
cat: false
}
}
];
I still can see Jake in newdata2. But why?
Please help me fix this. Is it my filter or the way filter works? Based on the criteria, I should only get Mike at the end. Please advice.
Thank you
var existing = [{
name: "John",
values_: {
id: 5,
cat: true
}
},
{name: "Jake",
values_: {
id: 3,
cat: true
}
},
{
name: "Alice",
values_: {
id: 2,
cat: false
}
}
];
var newdata = [{
name: "Mike",
properties: {
id: 1,
cat: true
}
},
{name: "Jake",
properties: {
id: 3,
cat: true
}
},
{name: "Jake",
properties: {
id: 3,
cat: true
}
},
{
name: "Alice",
properties: {
id: 2,
cat: false
}
}
];
existing.forEach((existingitem, existingindex, existingfeatures) => {
newdata2 = newdata.filter(item => (
existingitem.values_.id != item.properties.id &&
existingitem.values_.cat != item.properties.cat
));
});
console.log('newdata2 - ',newdata2);
... think about a filter (outer) and every (inner) based approach instead of forEach and filter - maybe that makes it easier to think about a correct implementation.
var existingItemList = [{ name: "John", values_: { id: 5, cat: true }}, { name: "Jake", values_: { id: 3, cat: true }}, { name: "Alice", values_: { id: 2, cat: false }}];
var newItemList = [{ name: "Mike", properties: { id: 1, cat: true }}, { name: "Jake", properties: { id: 3, cat: true }}, { name: "Jake", properties: { id: 3, cat: true }}, { name: "Alice", properties: { id: 2, cat: false }}];
var itemList = newItemList.filter(function (newItem) { // filter `newItem` only
return existingItemList.every(function (existingItem) { // if it does not exist
return ( // in `existingItemList`.
//(newItem.name !== existingItem.name) &&
(newItem.properties.id !== existingItem.values_.id)
);
});
});
console.log('itemList : ', itemList);
.as-console-wrapper { max-height: 100%!important; top: 0; }
EDIT
follow up, referring this comment of mine ...
your comparison condition just does not fit what you are really searching/looking for.
If one still assumes that the OP wants to filter a new item only if it does not already exist in another list that it is going to be compared to ...
... one has to write a matcher function that maps and compares item fields in one and the same time.
This comparator/matcher then has to be used in a way that it will filter only the very new item that does not equal any other already existing item.
This can be achieved by a slight change to the former approach from above ...
function doesExistingItemMatchBoundNewItem(existingItem) {
var newItem = this;
return (
(newItem.properties.id === existingItem.values_.id)
&& (newItem.properties.cat === existingItem.values_.cat)
);
}
var existingItemList = [{ name: "John", values_: { id: 5, cat: true }}, { name: "Jake", values_: { id: 3, cat: true }}, { name: "Alice", values_: { id: 2, cat: false }}];
var newItemList = [{ name: "Mike", properties: { id: 1, cat: true }}, { name: "Jake", properties: { id: 3, cat: true }}, { name: "Jake", properties: { id: 3, cat: true }}, { name: "Alice", properties: { id: 2, cat: false }}];
var itemList = newItemList.filter(function (newItem) {
return !existingItemList.some(doesExistingItemMatchBoundNewItem.bind(newItem));
});
console.log('itemList : ', itemList);
.as-console-wrapper { max-height: 100%!important; top: 0; }
You could take a Set and filter the known items.
var existing = [{ name: "John", values_: { id: 5, cat: true } }, { name: "Jake", values_: { id: 3, cat: true } }, { name: "Alice", values_: { id: 2, cat: false } }],
newdata = [{ name: "Mike", properties: { id: 1, cat: true } }, { name: "Jake", properties: { id: 3, cat: true } }, { name: "Jake", properties: { id: 3, cat: true } }, { name: "Alice", properties: { id: 2, cat: false } }],
eSet = new Set(existing.map(({ values_: { id, cat } }) => [id, cat].join('|'))),
result = newdata.filter(({ properties: { id, cat } }) => !eSet.has([id, cat].join('|')));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }