Related
I have this array of objects
const items = [
{
id: '121',
itemDate: '2022-04-28',
itemName: 'testname1',
itemCategory: 'Category A',
itemPrice: { price: '100', currency: 'GBP' },
createdBy: {
username: 'user1',
name: 'Name 1',
date: '2022-04-28T22:41:59',
},
},
{
id: '122',
itemDate: '2022-04-28',
itemName: 'testname2',
itemCategory: 'Category B',
itemPrice: { price: '100', currency: 'GBP' },
createdBy: {
username: 'user2',
name: 'Name 2',
date: '2022-04-28T22:42:44',
},
},
{
id: '122',
itemDate: '2022-04-28',
itemName: 'testname3',
itemCategory: 'Category C',
itemPrice: { price: '200', currency: 'GBP' },
createdBy: {
username: 'user2',
name: 'Name 2',
date: '2022-04-28T22:43:16',
},
},
]
Code I'm using:
items.reduce(function (c, x) {
if (!c[x.createdBy.username])
c[x.createdBy.username] = {
username: x.createdBy.username,
total: 0,
}
c[x.createdBy.username].total += Number(x.itemPrice.price)
return c
}, [])
This part gives me the following output:
items :>> [
user1: { username: 'user1', total: 100},
user2: { username: 'user2', total: 300}
]
So I tried this to get rid of the object names:
let output = []
let totalSum = 0
for (const username in items) {
let temp = {
username: items[username].username,
total: items[username].total,
}
totalSum = totalSum + items[username].total
output.push(temp)
}
output.push({ username: 'allUsers', total: totalSum })
return output
And final output is as I want it now:
output :>> [
{ username: 'user1', total: 100 },
{ username: 'user2', total: 300 },
{ username: 'allUsers', total: 400}
]
My two questions...
Is there a way to update the .reduce part so that I'd get an object without the name at the beggining, without having to use the for loop?
Is there also a way to implement the part that would sum up all the totals?
Thank you
Code Sample (without comments/description)
const groupAndAdd = arr => (
Object.values(
arr.reduce(
(acc, {createdBy : {username}, itemPrice: {price}}) => {
acc.allUsers ??= { username: 'allUsers', total: 0};
acc.allUsers.total += +price;
if (username in acc) {
acc[username].total += +price;
} else {
acc[username] = {username, total: +price};
}
return acc;
},
{}
)
)
);
Presented below is a working demo to achieve the desired objective, with notes/comments to help understand.
Code Snippet
// method to group by user and sum prices
const groupAndAdd = arr => (
// extract the values from the intermediate result-object
Object.values(
arr.reduce( // generate result as object
(acc, {createdBy : {username}, itemPrice: {price}}) => {
// above line uses de-structuring to directly access username, price
// below uses logical nullish assignment to set-up "allUsers"
acc.allUsers ??= { username: 'allUsers', total: 0};
// accumulate the "price" to the all-users "total"
acc.allUsers.total += +price;
// if "acc" (accumulator) has "username", simply add price to total
if (username in acc) {
acc[username].total += +price;
} else {
// create an object for the "username" with initial total as "price"
acc[username] = {username, total: +price};
}
// always return the "acc" accumulator for ".reduce()"
return acc;
},
{} // initially set the "acc" to empty object
)
) // if required, use ".sort()" to move the all-users to last position in array
);
const items = [{
id: '121',
itemDate: '2022-04-28',
itemName: 'testname1',
itemCategory: 'Category A',
itemPrice: {
price: '100',
currency: 'GBP'
},
createdBy: {
username: 'user1',
name: 'Name 1',
date: '2022-04-28T22:41:59',
},
},
{
id: '122',
itemDate: '2022-04-28',
itemName: 'testname2',
itemCategory: 'Category B',
itemPrice: {
price: '100',
currency: 'GBP'
},
createdBy: {
username: 'user2',
name: 'Name 2',
date: '2022-04-28T22:42:44',
},
},
{
id: '122',
itemDate: '2022-04-28',
itemName: 'testname3',
itemCategory: 'Category C',
itemPrice: {
price: '200',
currency: 'GBP'
},
createdBy: {
username: 'user2',
name: 'Name 2',
date: '2022-04-28T22:43:16',
},
},
];
console.log('group and add prices per user: ', groupAndAdd(items));
.as-console-wrapper { max-height: 100% !important; top: 0 }
Explanation
Inline comments added to the snippet above.
PS: If you'd like to add value to stackoverflow community,
Please consider reading: What to do when my question is answered
Thank you !
For your first question, you're initialising correctly as an array, but you're using just object. Two ways you can do this.
First Option
let something = items.reduce(function(c, x) {
if (!c[x.createdBy.username])
c[x.createdBy.username] = {
username: x.createdBy.username,
total: 0,
}
c[x.createdBy.username].total += Number(x.itemPrice.price)
return c
}, {});
something = Object.values(something);
Second Option
I was thinking of using just push, but seems it's not possible, so the above is the only option.
Using push is possible, but it'll get too complicated by checking with find and updating the correct array element.
For your second question of summing up all the totals, you can use the simple syntax of:
const sum = arr.reduce((a, c) => a + c, 0);
This is the minimum code you need for array of numbers to be summed.
I have created a dynamic array of objects which is created through inquirer.
But I cannot figure out how to access a specific object in the array
EDIT: this is how the console has logged my array
So for example, how can I access the 2nd Engineer (Mark)?
Keep in mind the array will change depending on the user input
team = [
Manager {
name: 'Nicole',
id: '1',
email: 'nicole#gmail.com',
officeNumber: '5'
},
Engineer {
name: 'Zoe',
id: '2',
email: 'zoe#gmail.com',
github: 'zozo'
},
Engineer {
name: 'Mark',
id: '3',
email: 'mark#gmail.com',
github: 'emman'
},
Engineer {
name: 'Joe',
id: '4',
email: 'joe#gmail.com',
github: 'joey'
}
Intern {
name: 'Seb',
id: '5',
email: 'seb#gmail.com',
school: 'UWA'
}
]
Use find method. If there is no such Mark then find return null.
If you want find Engineer Mark
const result = data.find(x => {
return x instanceof Engineer && x.name === 'Mark'
})
[Update]
If you want find the second Engineer
const result = data.filter(x => {
return x instanceof Engineer
})[1]
As Sepehr jozef mentioned the strucure is not that handy. If we take his structure you can find it via the .find Method.
var team = [
{
name: 'Nicole',
id: '1',
email: 'nicole#gmail.com',
officeNumber: '5',
},
{
name: 'Zoe',
id: '2',
email: 'zoe#gmail.com',
github: 'zozo'
},
{
name: 'Mark',
id: '3',
email: 'mark#gmail.com',
github: 'emman'
},
{
name: 'Joe',
id: '4',
email: 'joe#gmail.com',
github: 'joey'
},
{
name: 'Seb',
id: '5',
email: 'seb#gmail.com',
school: 'UWA'
}
]
const mark = team.find(function(teamMember){
return teamMember.name === "Mark";
})
The variable "mark" contains now the object of the engineer "Mark".
first of all, your structure is wrong.
it should be:
var team = [
{
name: 'Nicole',
id: '1',
email: 'nicole#gmail.com',
officeNumber: '5',
},
{
name: 'Zoe',
id: '2',
email: 'zoe#gmail.com',
github: 'zozo'
},
{
name: 'Mark',
id: '3',
email: 'mark#gmail.com',
github: 'emman'
},
{
name: 'Joe',
id: '4',
email: 'joe#gmail.com',
github: 'joey'
},
{
name: 'Seb',
id: '5',
email: 'seb#gmail.com',
school: 'UWA'
}
]
and to get mark(2) you should use:
team[3].name
I have an array of objects like this:
const myArray = [
{
id: 1234,
name: 'foo',
status: 'OK'
},
{
id: 1235,
name: 'foo',
status: 'KO'
},
{
id: 1236,
name: 'bar',
status: 'KO'
},
{
id: 1237,
name: 'bar',
status: 'OK'
},
{
id: 1238,
name: 'baz',
status: 'KO'
}
]
and I need to filter it, keeping only one with the same name, and it should be the one with the highest id.
const expectedOutput = [
{
id: 1235,
name: 'foo',
status: 'KO'
},
{
id: 1237,
name: 'bar',
status: 'OK'
},
{
id: 1238,
name: 'baz',
status: 'KO'
}
]
I've been strugling but I can't find the best solution. Any idea?
Keep track of maxes in an object mapping names to objects:
const myArray = [
{
id: 1234,
name: 'foo',
status: 'OK'
},
{
id: 1235,
name: 'foo',
status: 'KO'
},
{
id: 1236,
name: 'bar',
status: 'KO'
},
{
id: 1237,
name: 'bar',
status: 'OK'
},
{
id: 1238,
name: 'baz',
status: 'KO'
}
];
const maxes = {};
for (const ele of myArray) {
if (!(ele.name in maxes) || ele.id > maxes[ele.name].id) {
maxes[ele.name] = ele;
}
}
const filtered = Object.values(maxes);
console.log(filtered);
.as-console-wrapper {min-height: 100%;}
You can use reduce like the following. This way it will work for both sorted and unsorted array.
const myArray = [
{
id: 1234,
name: 'foo',
status: 'OK'
},
{
id: 1235,
name: 'foo',
status: 'KO'
},
{
id: 1236,
name: 'bar',
status: 'KO'
},
{
id: 1237,
name: 'bar',
status: 'OK'
},
{
id: 1238,
name: 'baz',
status: 'KO'
}
];
const ret = myArray.reduce((acc, curr) => {
const index = acc.findIndex(item => item.name === curr.name);
if(index> -1 && acc[index].id < curr.id) {
acc[index] = curr;
} else {
acc.push(curr);
}
return acc;
}, []);
console.log(ret);
Although this will work pretty good as you have to loop through the array only once. But if you use for loop instead of reduce. It will be much faster as for loops are usually faster than map, filter, reduce etc. You can do the following for fastest result,
const myArray = [
{
id: 1234,
name: 'foo',
status: 'OK'
},
{
id: 1235,
name: 'foo',
status: 'KO'
},
{
id: 1236,
name: 'bar',
status: 'KO'
},
{
id: 1237,
name: 'bar',
status: 'OK'
},
{
id: 1238,
name: 'baz',
status: 'KO'
}
];
let ret = [];
for(let i =0;i<myArray.length; i++) {
const index = ret.findIndex(item => item.name === myArray[i].name);
if(index > -1 && ret[index].id < myArray[i].id) {
ret[index]=myArray[i];
} else {
ret.push(myArray[i]);
}
}
console.log(ret);
Since the array is already sorted by id you could use a Map object and just set each value using the name as key. Overriding the previous value if present. Note that this only matches the requirements as long as the last element with a certain name has also the highest value.
const myArray = [{id:1234,name:'foo',status:'OK'},{id:1235,name:'foo',status:'KO'},{id:1236,name:'bar',status:'KO'},{id:1237,name:'bar',status:'OK'},{id:1238,name:'baz',status:'KO'}];
const lookup = new Map();
myArray.forEach(item => lookup.set(item.name, item));
const result = Array.from(lookup.values());
console.log(result);
The order of the resulting elements is based on insertion order into the Map object. The first key inserted will be the first element of the resulting array. The second key inserted will be the second element, etc.
You could do it using Map Object.
First, create a new Map Object
Traverse the array using forEach() method.
Put name as a key into a variable named key
Check if key exists by using has(key) method in the Map Object named map
If key does not exist then set it into the Map Object by calling the set(key, value) method. In this solution, key is name and value is object.
If Key exists then get the object using get(key) method, get max id using Math.max() method, then update the object and set it into the Map Object.
const myArray = [
{
id: 1234,
name: 'foo',
status: 'OK',
},
{
id: 1235,
name: 'foo',
status: 'KO',
},
{
id: 1236,
name: 'bar',
status: 'KO',
},
{
id: 1237,
name: 'bar',
status: 'OK',
},
{
id: 1238,
name: 'baz',
status: 'KO',
},
];
const map = new Map();
myArray.forEach((x) => {
const key = x.name;
if (map.has(key))
map.set(key, { ...map.get(key), id: Math.max(map.get(key).id, x.id) });
else map.set(key, { ...x });
});
const ret = [...map.values()];
console.log(ret);
I have an array of objects, that looks like this:
data = [
{
title: 'John Doe',
departments: [
{ name: 'Marketing', slug: 'marketing'},
{ name: 'Sales', slug: 'sales'},
{ name: 'Administration', slug: 'administration'},
]
},
{
title: 'John Doe Junior',
departments: [
{ name: 'Operations', slug: 'operations'},
{ name: 'Sales', slug: 'sales'},
]
},
{
title: 'Rick Stone',
departments: [
{ name: 'Operations', slug: 'operations'},
{ name: 'Marketing', slug: 'marketin'},
]
},
]
How can I iterate over each object's departments array and create new arrays where I would have employees sorted by departments, so that the end result would like this:
operations = [
{
title: 'John Doe Junior',
departments: [
{ name: 'Operations', slug: 'operations'},
{ name: 'Sales', slug: 'sales'},
]
},
{
title: 'Rick Stone',
departments: [
{ name: 'Operations', slug: 'operations'},
{ name: 'Marketing', slug: 'marketin'},
]
},
]
marketing = [
{
title: 'John Doe',
departments: [
{ name: 'Marketing', slug: 'marketing'},
{ name: 'Sales', slug: 'sales'},
{ name: 'Administration', slug: 'administration'},
]
},
{
title: 'Rick Stone',
departments: [
{ name: 'Operations', slug: 'operations'},
{ name: 'Marketing', slug: 'marketin'},
]
},
]
What would be the way to create dynamically this kind of arrays?
Update
I have tried to come up with a solution using the suggestion from the answer, where I would dynamically create an array with department objects that would have an array of employees:
const isInDepartment = departmentToCheck => employer => employer.departments.find(department => department.slug == departmentToCheck);
var departments = [];
function check(departments, name) {
return departments.some(object => name === object.department);
}
employees.forEach((employee) => {
employee.departments.forEach((department) => {
let found = check(departments, department.slug);
if (!found) {
departments.push({ department: department.slug });
}
});
});
departments.forEach((department) => {
// push an array of employees to each department
//employees.filter(isInDepartment(department));
});
But, I don't know how can I push the array of employees to the object in the array that I am looping at the end?
This is the fiddle.
How about this? I use Array.protoype.filter operation, and I use a higher-order function (in this case a function that returns a function) to create the predicate (function that returns a boolean) that will check whether an employee is in a specific department. I added some (hopefully) clarifying comments in the code.
Edit: with the new code and context you provided this JSFiddle demo shows how it would work together.
const employees = [
{
title: 'John Doe',
departments: [
{ name: 'Marketing', slug: 'marketing'},
{ name: 'Sales', slug: 'sales'},
{ name: 'Administration', slug: 'administration'}
]
},
{
title: 'John Doe Junior',
departments: [
{ name: 'Operations', slug: 'operations'},
{ name: 'Sales', slug: 'sales'}
]
},
{
title: 'Rick Stone',
departments: [
{ name: 'Operations', slug: 'operations'},
{ name: 'Marketing', slug: 'marketin'}
]
}
];
// given a department, this returns a function that checks
// whether an employee is in the specified department
// NOTE: the "find" returns the found object (truthy)
// or undefined (falsy) if no match was found.
const isInDepartment =
departmentToCheck =>
employee => employee.departments.find(dep => dep.name == departmentToCheck);
const employeesInMarketing = employees.filter(isInDepartment('Marketing'));
const employeesInOperations = employees.filter(isInDepartment('Operations'));
console.log('Employees in marketing', employeesInMarketing);
console.log('Employees in operations', employeesInOperations);
I have two arrays of objects. One array contains list of items, another array contains list of categories. I want to create a new array based on categoryIds. I tried using lodash. But, couldn't get the correct solution.
I can do this using looping. But, I am looking for more clean approach.
var items = [
{
id: '001',
name: 'item1',
description: 'description of item1',
categoryId: 'cat1'
},
{
id: '002',
name: 'item2',
description: 'description of item2',
categoryId: 'cat2'
},
{
id: '003',
name: 'item3',
description: 'description of item3',
categoryId: 'cat1'
},
{
id: '004',
name: 'item4',
description: 'description of item4'
}
];
var categories = [
{
id: 'cat1',
name: 'Category1'
},
{
id: 'cat2',
name: 'Category2'
}
];
Expected output
[
{
categoryId: 'cat1',
name: 'Category1',
items: [
{
id: '001',
name: 'item1',
description: 'description of item1',
categoryId: 'cat1'
},
{
id: '003',
name: 'item3',
description: 'description of item3',
categoryId: 'cat1'
}
]
},
{
categoryId: 'cat2',
name: 'Category2',
items: [
{
id: '002',
name: 'item2',
description: 'description of item2',
categoryId: 'cat2'
}
]
},
{
categoryId: '',
name: '',
items: [
{
id: '004',
name: 'item4',
description: 'description of item4'
}
]
}
]
https://jsfiddle.net/sfpd3ppn/
Thanks for the help
The following does the trick:
var items = [{ id: '001', name: 'item1', description: 'description of item1', categoryId: 'cat1' }, { id: '002', name: 'item2', description: 'description of item2', categoryId: 'cat2' }, { id: '003', name: 'item3', description: 'description of item3', categoryId: 'cat1' }, { id: '004', name: 'item4', description: 'description of item4' } ];
var categories = [ { id: 'cat1', name: 'Category1' }, { id: 'cat2', name: 'Category2' } ];
var output = categories.concat([{id:'',name:''}]).map(function(v) {
return {
categoryId: v.id,
name: v.name,
items: items.filter(function(o) {
return o.categoryId === v.id || !o.categoryId && !v.id;
})
};
});
console.log(output);
I start by using .concat() to create a new categories array that holds the original categories items plus an "empty" category. Then I .map() that array to return category objects with your desired output structure, each of which has an items array that is produced by .filter()ing the original items array.
(Note that the items arrays within the output contain references to the same objects that were in the original items input, not copies of them. If you wanted copies you could add another .map() after the .filter().)
You can accomplish the desired result using a reduce. We are going to start with the original categories array and reduce the items array into it.
var items = [
{ id: '001', name: 'item1', description: 'description of item1', categoryId: 'cat1' },
{ id: '002', name: 'item2', description: 'description of item2', categoryId: 'cat2' },
{ id: '003', name: 'item3', description: 'description of item3', categoryId: 'cat1' },
{ id: '004', name: 'item4', description: 'description of item4' }
];
var categories = [
{ id: 'cat1', name: 'Category1' },
{ id: 'cat2', name: 'Category2' }
];
// Lets add the empty category at the beginning. This simplifies the logic.
categories.push({ id: '', name: '' });
// This is a function that will return a function to be used as a filter later on
function createFilter (category) {
return function (item) {
return item.id === category;
};
}
var mergedSet = items.reduce(function (previous, current) {
// Get the category ID of the current item, if it doesn't exist set to empty string
var categoryId = current.categoryId || '';
// Find the cateogry that matches the category ID
var category = previous.find(createFilter(categoryId));
// If the items property doesn't exists (we don't have any items), create an empty array
if (!category.items) { category.items = []; }
// Add the item the category
category.items.push(current);
// Return the current value that will be used in the next iteration.
// Note, the initial value of previous will be the intial value of categories.
return previous;
}, categories);
console.log(mergedSet);
/* Output
[
{ id: 'cat1',
name: 'Category1',
items:
[ { id: '001',
name: 'item1',
description: 'description of item1',
categoryId: 'cat1' },
{ id: '003',
name: 'item3',
description: 'description of item3',
categoryId: 'cat1' }
]
},
{ id: 'cat2',
name: 'Category2',
items:
[ { id: '002',
name: 'item2',
description: 'description of item2',
categoryId: 'cat2'
}
]
},
{ id: '',
name: '',
items:
[ { id: '004',
name: 'item4',
description: 'description of item4' } ] }
]
*/
Assuming the variables categories and items are assigned as you defined above:
const keyedCategories = _(categories)
.concat({ id: '', name: '' })
.keyBy('id')
.value();
const groupedItems = _.groupBy(items, (item) => _.get(item, 'categoryId', ''));
const result = _.reduce(groupedItems, (acc, value, key) => {
const { id: categoryId, name } = keyedCategories[key];
return _.concat(acc, { categoryId, name, items: value });
}, []);