I have an array of objects. I want to reduce this array of objects based on the property category, so that I can get a total sum of each category. I wrote this function which returns an object:
const expenses =
[{
title: 'burger',
sum: 57,
category: 'eating_out',
notes: 'was good',
},
{
title: 'water bill',
sum: 142.81,
category: 'utilities',
notes: 'this sucks',
},
{
title: 'electricity bill',
sum: 112.90,
category: 'utilities',
notes: 'could be worse',
}]
const totalExpensesByCategory = expenses.reduce(
(acc, curr) => {
if (!acc[curr.category]) {
acc[curr.category] = curr.sum;
} else {
acc[curr.category] = +acc[curr.category] + curr.sum;
}
return acc;
},
{}
);
console.log(totalExpensesByCategory)
I want it to be an iterable object so that I can easily render components based on those entries.
How can I write it differently so that maybe it will return an array of objects that I can use map() on?
Object.entries() doesn't seem like the ideal way to do it.
Since you have valuable info in the object keys (the category) and similar in the values (the sum) you'll need both the key and the value from each object entry to create an array element. One approach is Object.entries, followed by map to create the individual objects that go into the output array.
const expenses =
[{
title: 'burger',
sum: 57,
category: 'eating_out',
notes: 'was good',
},
{
title: 'water bill',
sum: 142.81,
category: 'utilities',
notes: 'this sucks',
},
{
title: 'electricity bill',
sum: 112.90,
category: 'utilities',
notes: 'could be worse',
}]
const totalExpensesByCategory = Object.entries(expenses.reduce(
(acc, curr) => {
if (!acc[curr.category]) {
acc[curr.category] = curr.sum;
} else {
acc[curr.category] = +acc[curr.category] + curr.sum;
}
return acc;
},
{}
)).map(([k, v]) => ({category: k, sum: v}));
console.log(totalExpensesByCategory);
There's no reason not to build it up as an array of objects inside the reduce in the first place:
const totalExpensesByCategory = expenses.reduce(
(acc, curr) => {
const categoryIndex = acc.findIndex((category) => category.name === curr.category)
if (categoryIndex === -1) {
acc.push({name: curr.category, sum: curr.sum})
} else {
acc[categoryIndex].sum = acc[categoryIndex].sum + curr.sum;
}
return acc;
},
[]
);
Heres the output:
Just use Maps instead of objects. That's what they are for. Unlike objects, Maps are iterable:
const totalExpensesByCategory = expenses.reduce(
(m, curr) =>
m.set(curr.category, (m.get(curr.category) ?? 0) + curr.sum),
new Map
);
for (let [category, sum] of totalExpensesByCategory)
console.log(category, sum)
As a bonus, your reducer looks much nicer without curly braces and return
Related
i'm trying to duplicate objects based on two properties that have multiple values differentiated by a comma.
For example:
I have an object
const obj = {
id: 1
date: "2021"
tst1: "111, 222"
tst2: "AAA, BBB"
}
And I would like the result to be an array of 2 objects in this case (because there are 2 values in tst1 OR tst2, these 2 properties will always have the same nr of values differentiated by a comma)
[{
id: 1,
date: "2021",
tst1: "111",
tst2: "AAA",
},
{
id: 1,
date: "2021",
tst1: "222",
tst2: "BBB",
}]
What I tried is this:
I created a temporary object
const tempObject = {
id: obj.id,
date: obj.date,
}
And then I would split and map the property that has multiple values, like this:
cont newObj = obj.tst1.split(",").map(function(value) {
let finalObj = {}
return finalObj = {
id: tempObject.id,
date: tempObject.date,
tst1: value,
})
And now, the newObj is an array of objects and each object contains a value of tst1.
The problem is I still have to do the same for the tst2...
And I was wondering if there is a simpler method to do this...
Thank you!
Here is an example that accepts an array of duplicate keys to differentiate. It first maps them to arrays of entries by splitting on ',' and then trimming the entries, then zips them by index to create sub-arrays of each specified property, finally it returns a result of the original object spread against an Object.fromEntries of the zipped properties.
const mapDuplicateProps = (obj, props) => {
const splitProps = props.map((p) =>
obj[p].split(',').map((s) => [p, s.trim()])
);
// [ [[ 'tst1', '111' ], [ 'tst1', '222' ]], [[ 'tst2', 'AAA' ], [ 'tst2', 'BBB' ]] ]
const dupeEntries = splitProps[0].map((_, i) => splitProps.map((p) => p[i]));
// [ [[ 'tst1', '111' ], [ 'tst2', 'AAA' ]], [[ 'tst1', '222' ], [ 'tst2', 'BBB' ]] ]
return dupeEntries.map((d) => ({ ...obj, ...Object.fromEntries(d) }));
};
const obj = {
id: 1,
date: '2021',
tst1: '111, 222',
tst2: 'AAA, BBB',
};
console.log(mapDuplicateProps(obj, ['tst1', 'tst2']));
Not sure if that's what you're searching for, but I tried making a more general use of what you try to do:
const duplicateProperties = obj => {
const properties = Object.entries(obj);
let acc = [{}];
properties.forEach(([key, value]) => {
if (typeof value === 'string' && value.includes(',')) {
const values = value.split(',');
values.forEach((v, i) => {
if (!acc[i]) {
acc[i] = {};
}
acc[i][key] = v.trim();
});
} else {
acc.forEach(o => o[key] = value);
}
});
return acc;
};
const obj = {
id: 1,
date: '2021',
tst1: '111, 222',
tst2: 'AAA, BBB',
};
console.log(duplicateProperties(obj));
You could start by determining the length of the result using Math.max(), String.split() etc.
Then you'd create an Array using Array.from(), returning the correct object for each value of the output index.
const obj = {
id: 1,
date: "2021",
tst1: "111, 222",
tst2: "AAA, BBB",
}
// Determine the length of our output array...
const length = Math.max(...Object.values(obj).map(s => (s + '').split(',').length))
// Map the object using the relevant index...
const result = Array.from({ length }, (_, idx) => {
return Object.fromEntries(Object.entries(obj).map(([key, value]) => {
const a = (value + '').split(/,\s*/);
return [key, a.length > 1 ? a[idx] : value ]
}))
})
console.log(result)
.as-console-wrapper { max-height: 100% !important; }
I have this code:
const uniform = {
zone: 'BOTTOM'
}
const sizes = [
{
_id: 'sizeId2',
zones: ['BOTTOM'],
value: '48'
},
{
_id: 'sizeId3',
zones: ['BOTTOM'],
value: '42'
},
]
sizes.map((size) => (size.zones.includes(uniform.zone) ? {
_id: size._id,
value: size.value,
} : null))
https://jsfiddle.net/pmiranda/945fsdw7/5/
// here I'm trying another way than map and the ternary
console.log(sizes.filter((size) => (size.zones.includes(uniform.zone) && {
_id: size._id,
value: size.value,
})))
I wonder, how could I replace that map with a filter? Because I think that mapping with a ternary with null could be done in a better way, plus, in the map way I'm getting that null in the end, I want to not add it to the array
I think that you'd filter then map:
sizes.filter(size => size.zones.includes(uniform.zone)).map(size => ({
_id: size._id,
value: size.value,
}));
You probably need to return the result, or assign it to a variable.
const uniform = { zone: 'BOTTOM' }
const sizes = [
{
_id: 'sizeId2',
zones: ['BOTTOM'],
value: '48'
},
{
_id: 'sizeId3',
zones: ['BOTTOM'],
value: '42'
},
]
// reduce
const newSizes = sizes.reduce((acc, size) =>
{
if(size.zones.includes(uniform.zone)) {
acc.push(
{
_id: size._id,
value: size.value
})
}
return acc;
}, []
)
console.log(newSizes);
Using either map or filter does not make sense in the cuurent usecase as you are not utilizing the return value.
If you need to just loop over the array and create new entries (containing or nnot containing null) you can use forEach or reduce.
From the "comments", reduce is more suitable for you.
Without map and filter, just create new array
let newSizes = [];
sizes.forEach(({zones, _id, value}, key) => zones.includes(uniform.zone) && (newSizes[key] = {_id, value}))
I have two arrays that have properties: minValue, maxValue, avgValue. I would like to write a very clean approach to identifying which objects match (the displayName) in the array and then assign the values of the new array to the old (minPrice, maxPrice, avgPrice).
I have this code so far
export interface Player {
displayName: string;
MinPrice: number;
MaxPrice: number;
AvgPrice: number;}
export const MERGE_PLAYER_STATS = (playerStat: Player[], auctionStat: Player[]): any => {
const reducer = (playerStat, auctionStat) => {
playerStat.MinPrice = auctionStat.minPrice,
playerStat.MaxPrice = auctionStat.maxPrice,
playerStat.AvgPrice = auctionStat.avgPrice,
playerStat.Team = auctionStat.team;
}
return reducer(playerStat, auctionStat =>
filter(auctionStat, playerStat => auctionStat.displayName.includes(playerStat.displayName)));
}
Input: two different set of player arrays that have common display Names.
playerStat: [] = [];
auctionStat: [] = [];
playerStat.push( {
displayName: "Josh Allen",
minPrice: "",
maxPrice: "",
avgPrice: ""
},
{
displayName: "No One",
minPrice: "",
maxPrice: "",
avgPrice: ""
});
auctionStat.push( {
displayName: "Josh Allen",
minPrice: 1,
maxPrice: 2,
avgPrice: 1
},
{
displayName: "No One 2",
minPrice: 1,
maxPrice: 1,
avgPrice: 2
});
The output should only have Josh Allen stats being updated from blank values to 1,2,1 respectively.
Please let me know what your clean approach this. FYI, this code is not returning what I want it to.
The simple approach would be to simply write a mapping function that copies in new objects based on matching keys.
Given an interface:
interface MyInterface {
key: string;
value: number;
}
And two arrays:
const existingArray: MyInterface[] = [
{ key: 'a', value: 1 },
{ key: 'b', value: 2 },
{ key: 'c', value: 3 }
];
const newArray: MyInterface[] = [
{ key: 'b', value: 20 },
{ key: 'c', value: 30 },
{ key: 'd', value: 40 }
];
Expected output
I want to update the existingArray with the values from newArray where the keys match.
If a value isn't found in newArray, the existing item is untouched.
If a new value is found in newArray, it isn't merged in to oldArray
All property values are to be overwritten
My expected output is:
const expected = [
{ key: 'a', value: 1 },
{ key: 'b', value: 20 },
{ key: 'c', value: 30 }
];
Solution
I would achieve this by using this merge function. It is just a javascript array map:
private merge<T>(existing: T[], updated: T[], getKey: (t: T) => string): T[] {
// start with the existing array items
return existing.map(item => {
// get the key using the callback
const key: string = getKey(item);
// find the matching item by key in the updated array
const matching: T = updated.find(x => getKey(x) === key);
if (matching) {
// if a matching item exists, copy the property values to the existing item
Object.assign(item, matching);
}
return item;
});
}
I have made it generic to allow you to use it with any type. All you need to do it provide the 2 arrays, and a callback to identify the key.
I would use it to created a merged array like this:
const merged = this.merge(existingArray, newArray, x => x.key);
DEMO: https://stackblitz.com/edit/angular-nn8hit
A word of warning
This wouldn't scale well with very large arrays. If you are using large arrays, I would optimise by creating indexes of type Map<string, T>, to keep the number of loops fixed. This implementation is out of scope of this question.
Edit:
If you only want to update specific values, you could pass in an update callback:
private merge<T>(existing: T[], updated: T[], getKey: (t: T) => string,
update: (item, matching) => void
): T[] {
return existing.map(item => {
const key: string = getKey(item);
const matching: T = updated.find(x => getKey(x) === key);
if (matching) {
update(item, matching);
}
return item;
});
}
And call it like this:
const merged = this.merge(existingArray, newArray, x => x.key,
(item, matching) => item.value = matching.value);
Use Object.assign()
Ex: oldArray = Object.assign(oldArray, newArray)
playerStat.forEach((initItem) => {
let result = auctionStat.filter((item) => (
initItem.displayName === item.displayName
))
// let me suppose the same displayName only has one
result.length && Object.assign(initItem, result[0])
})
I'm saving an object in state that looks like:
ingredients: {
salad: {
amount: 3,
basePrice: 1
},
cheese: {
amount: 2,
basePrice: 1.2
}
}
I want to use this in my component's state as follows
ingredients: {
salad: 3,
cheese: 2
}
I used Object.keys and map at first but it returns an Array of key value pairs instead of objects.
Although this works:
const newIngredientsObject = {};
for (const i in ingredientsObject)
newIngredientsObject[i] = ingredientsObject[i].amount;
return newIngredientsObject;
I want to find a solution without a helper method, as such:
const mapStateToProps = state => {
return {
ingredients: Object.keys(state.burger.ingredients).map(i => (
{ [i]: state.burger.ingredients[i].amount } )),
totalPrice: state.burger.totalPrice
}
};
You could map the entries of the object by using the amount property of the values and take Object.assign for getting a new object.
var ingredients = { salad: { amount: 3, basePrice: 1 }, cheese: { amount: 2, basePrice: 1.2 } },
result = Object.assign(
...Object.entries(ingredients).map(([key, value]) => ({ [key]: value.amount }))
);
console.log(result);
when you want to create one object to inside to another go this web
object 1 = {
content 1 = {
stuff = {
},
more Stuff = {
}
}
}
object 2 = {
key 1:"",
key 2:""
}
object 1.content 1 = object 2;
You asked a solution with Object.keys, here is one:
var x = {ingredients: {
salad: {
amount: 3,
basePrice: 1
},
cheese: {
amount: 2,
basePrice: 1.2
}
}}
Object.keys(x.ingredients).map((d,i)=>[d,x.ingredients[d].amount]).reduce((ac,d,i)=>(ac[d[0]]=d[1],ac),{})
//{salad: 3, cheese: 2}
The shortest I could come up with it using Object.keys and reduce
const ingredients = {
salad: {
amount: 3,
basePrice: 1
},
cheese: {
amount: 2,
basePrice: 1.2
}
};
let newObject = Object.keys(ingredients).reduce((acc, val) => {
acc[val] = ingredients[val].amount;
return acc;
}, {});
console.log(newObject);
ANSWER:
Thanks for the solutions provided by #ibowankenobi and #Nina Scholz
Reduce function:
Object.keys(ingredients)
.map( (d, i) => [d, ingredients[d].amount])
.reduce( (ac, d, i) => (ac[d[0]] = d[1], ac), {} );
Entries function:
Object.assign(...Object.entries(ingredients)
.map( ([key, value]) => ({ [key]: value.amount }) ) );
reduce: 0.0849609375ms
entries: 0.1650390625ms
forLoop: 0.07421875ms
reduce: 0.024169921875ms
entries: 0.048095703125ms
forLoop: 0.010009765625ms
reduce: 0.010009765625ms
entries: 0.016845703125ms
forLoop: 0.0078125ms
After some testing, it seems like using a for loop is the most efficient and after that the reduce function.
I am trying to understand how reduce works
var expense = [
{
item: "Bed",
cost: 1499,
date: "02-23-2018"
},
{
item: "Phone",
cost: 2499,
date: "02-23-2018"
},
{
item: "Book",
cost: 400,
date: "02-23-2018"
},
{
item: "Mattress",
cost: 700,
date: "02-23-2018"
},
{
item: "Food",
cost: 300,
date: "02-23-2018"
}
];
var totalExpense = expense.reduce(function (a, b) {
console.log(a.cost, b.cost);
return a.cost + b.cost;
});
console.log(totalExpense);
this gives totalExpense as NaN.
Output:
1499 2499
undefined 400
undefined 700
undefined 300
NaN
When I perform the same operation with a simple expense array, it works fine.
The first parameter you pass to reduce's callback is the previous value (a) - or the second argument you pass to reduce (the initial value)
[].reduce(function(a, b) { ... }, 0);
^ callback ^ initial value
a will hold the result of each previous iteration, So to get the total of all costs, simply add b.cost
var expense = [{
item: 'Bed',
cost: 1499,
date: '02-23-2018'
},
{
item: 'Phone',
cost: 2499,
date: '02-23-2018'
},
{
item: 'Book',
cost: 400,
date: '02-23-2018'
},
{
item: 'Mattress',
cost: 700,
date: '02-23-2018'
},
{
item: 'Food',
cost: 300,
date: '02-23-2018'
}
];
var totalExpense = expense.reduce(function(a, b) {
return a + b.cost;
}, 0);
console.log(totalExpense);
Using es6 syntax you could make it a one liner
var totalExpense = expense.reduce((a, {cost}) => a + cost, 0);
You need to provide an initial value which has a cost field in order to reference it in the accumulator. And you need to return that object:
expense.reduce(function(acc, curr){
console.log(acc.cost, curr.cost);
acc.cost += curr.cost;
return acc;
}, { cost: 0 });
Note the use of more meaningful variable names than a and b. This will make your code easier to understand. The Array.prototype.reduce callback should have an accumulator and current value. Name them as such to help yourself. The initial value provides an object with a cost field where you can write down your accumulated value.
Note that you could also just use a vanilla variable here if you wanted to. If you don't actually need an object:
var total = expense.reduce(function(acc, curr){
acc += curr.cost;
return acc;
}, 0);
console.log(total);
>> 5398
You need to return whatever the next a should be. a.cost + b.cost is a number. It doesn’t have a cost property, so a.cost will be undefined after the first iteration.
Just provide a starting value 0. Also consider giving your parameters more appropriate names.
var totalExpense = expense.reduce(function(sum, item){
return sum + item.cost;
}, 0);
You need an initial value. The reduce function takes the accumulated value as first parameter and the next element of the collection as second
var totalExpense = expense.reduce(function(accumulator, current) {
console.log({ accumulator: accumulator, currentCost: current.cost });
return accumulator + current.cost;
}, 0);
console.log({ totalExpense: totalExpense });
expense.reduce((sum, cur) => ({ cost: sum.cost + cur.cost })).cost;
Updated
As #Viktor pointed, I didn't consider special case before.
function getTotelExpense(expense) {
if (expense.length > 0 && expense[0].hasOwnProperty("cost"))
return expense.reduce((sum, cur) => ({ cost: sum.cost + cur.cost })).cost;
else return 0;
}
You can learn more in TypeError: Reduce of empty array with no initial value | MDN
Here's another way you can do it using the generic function foldMap – for more guidance on this beautiful function, I recommend this video.
const totalExpense =
expense.foldMap (item => item.cost, 0)
console.log (totalExpense)
// 5398
For this, you'll need to define Array.prototype.foldMap and Number.prototype.concat – but this implementation and where it is defined is up to you
Array.prototype.foldMap = function (f, acc)
{
return this.reduce ((acc, x, i) =>
acc.concat (f (x, i, this)), acc)
}
Number.prototype.concat = function (m)
{
return this + m
}
Here's a functioning code paste
Array.prototype.foldMap = function (f, acc)
{
return this.reduce ((acc, x, i) =>
acc.concat (f (x, i, this)), acc)
}
Number.prototype.concat = function (m)
{
return this + m
}
const expense =
[ { item: 'Bed'
, cost: 1499
, date: '02-23-2018'
}
, { item: 'Phone'
, cost: 2499
, date: '02-23-2018'
}
, { item: 'Book'
, cost: 400
, date: '02-23-2018'
}
, { item: 'Mattress'
, cost: 700
, date: '02-23-2018'
}
, { item: 'Food'
, cost: 300
, date: '02-23-2018'
}
]
const totalExpense =
expense.foldMap (item => item.cost, 0)
console.log(totalExpense)
// 5398