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
Related
I have this array:
const arr = [
{ someProp: [{ amount: 10 }]},
{ someProp: [{ amount: 12 }]},
];
and then this reduce fn:
const sum = arr.reduce((prev, curr) => prev.someProp[0].amount + curr.someProp[0].amount);
It works if there are two items in the array but with three it throws:
Cannot read property '0' of undefined"
Actually you need only to return the accumulator + the new value.
You want to begin with 0 i guess so you need to add 0 as second parameter
const arr =
[
{ someProp: [{ amount: 10 }]},
{ someProp: [{ amount: 12 }]},
{ someProp: [{ amount: 12 }]},
];
const sum = arr.reduce((acc, item) => item.someProp[0].amount + acc, 0);
console.log(sum);
If you want it more spicy here with destructuring
const arr =
[
{ someProp: [{ amount: 10 }]},
{ someProp: [{ amount: 12 }]},
{ someProp: [{ amount: 12 }]},
];
const sum = arr.reduce((acc, { someProp: [{ amount }] }) => amount + acc, 0);
console.log(sum);
Following #Pointy answer, this is how you can get it working with any number of elements:
const sum = arr.reduce((prev, curr) => prev + curr.someProp[0].amount, 0);
prev is always the previously returned element, so you can safely use it as a number (and since you're just summing it, you can use 0 as default, so the first time reduce is invoked the value of prev will be 0)
You're returning a number. The value of prev is always the prior return value from the callback, not the previous element. Thus on the third iteration you're trying to use that number as if it were an object reference.
Add a second parameter (0) to the .reduce() call, and change the function to treat prev as the running total (a simple number):
const sum = arr.reduce((prev, curr) => prev + curr.someProp[0].amount, 0);
It works as-is when there are two elements because without that second parameter, the first iteration will see element 0 as prev and element 1 as curr. That behavior works fine when you've got an array of numbers and you just want to perform a computation between them, but in this case you need that initial value explicitly there as the second argument.
This is an alternative solution... Using Array.filter().
const arr = [{
someProp: [{
amount: 10
}]
},
{
someProp: [{
amount: 12
}]
},
{
someProp: [{
amount: -231
}]
},
{
someProp: [{
amount: 21265
}]
},
{
someProp: [{
amount: 1239
}]
},
{
someProp: [{
amount: -6
}]
},
];
const sum = arr.filter((val, index) => {
if (index > 0) {
arr[index].someProp[0].amount = parseInt(val.someProp[0].amount) + parseInt(arr[index - 1].someProp[0].amount);
}
return index == arr.length - 1;
})[0].someProp[0].amount;
console.log(sum);
I have an array
[
{ price: 10 },
{ price: 10 },
{ price: 10 },
{ price: 10 },
{ price: 20 },
{ price: 20 },
]
and I want it transformed into
[
{ numElements: 4, price: 10 },
{ numElements: 2, price: 20 },
]
I have tried using arr.reduce((prev, curr) => ..., []) to accomplish this, but I can't figure out how to do it.
A traditional method might use a for/loop to wrangle the data, but these days JavaScript has a number of functional methods that can help. This code uses reduce and map. To get your data in the format you want is a two stage process.
First, use reduce to create a hash table using the price as a key (because you know the each price is going to be unique:
const obj = arr.reduce((p, c) => {
// If price exists as a key its value by 1
// otherwise set it to 1.
p[c.price] = ++p[c.price] || 1;
return p;
}, {});
OUTPUT
{
"10": 4,
"20": 2
}
As it stands you've got a perfectly good object that you can access by the key/price and I would probably just stop there:
obj['10'] // 4
But if you want to get that data into the format in your question, map over the object keys to return an array of new objects.
const out = Object.keys(obj).map(key => {
return { price: +key, numElements: obj[key] };
});
DEMO
var hash = {}, result = [];
arr.forEach(function(el){
if(hash[el.price]){
hash[el.price].numElements++;
}else{
result.push(hash[el.price]={price:el.price,numElements:1});
}
});
Run
May use a hash table for price lookup. Or with reduce and find:
arr.reduce((res,{price})=>
(( res.find(el=>el.price===price) || res[res.push({price,numElements:0})-1] )
.numElements++,res)
);
Run
You can use try this:
let arr = [
{ price: 10 },
{ price: 10 },
{ price: 10 },
{ price: 10 },
{ price: 20 },
{ price: 20 },
]
let result = []
let counter = {}
arr.forEach( el => {
if (!counter[el.price]) counter[el.price] = 1
else counter[el.price]++
console.log(counter[el.price])
})
for (let id in counter) {
result.push({numElements: counter[id], price: id})
}
Assuming that the data comes sorted on price property, with a single .reduce() you may do as follows;
var data = [{ price: 10 }, { price: 10 }, { price: 10 }, { price: 10 }, { price: 20 }, { price: 20 }],
result = data.reduce((r,d,i) => i ? r[r.length-1].price === d.price ? (r[r.length-1].numElemenets++, r)
: (r.push(Object.assign({}, d, {numElemenets: 1})),r)
: [Object.assign({}, d, {numElemenets: 1})], {});
console.log(result);
You could look up the price in the result array and if not found insert a new object.
var data = [{ price: 10 }, { price: 10 }, { price: 10 }, { price: 10 }, { price: 20 }, { price: 20 }],
grouped = data.reduce((r, { price }) => {
var t = r.find(p => price === p.price);
t || r.push(t = { numElements: 0, price });
t.numElements++;
return r;
}, []);
console.log(grouped);
I'm trying to sum values of objects inside and array with underscore.js and its reduce method. But it looks like I'm doing something wrong. Where's my problem?
let list = [{ title: 'one', time: 75 },
{ title: 'two', time: 200 },
{ title: 'three', time: 500 }]
let sum = _.reduce(list, (f, s) => {
console.log(f.time); // this logs 75
f.time + s.time
})
console.log(sum); // Cannot read property 'time' of undefined
Use the native reduce since list is already an array.
reduce callback should return something, and have an initial value.
Try this:
let list = [{ title: 'one', time: 75 },
{ title: 'two', time: 200 },
{ title: 'three', time: 500 }];
let sum = list.reduce((s, f) => {
return s + f.time; // return the sum of the accumulator and the current time, as the the new accumulator
}, 0); // initial value of 0
console.log(sum);
Note: That reduce call can be shortened even more if we omit the block and use the implicit return of the arrow function:
let sum = list.reduce((s, f) => s + f.time, 0);
Here is a dummy example. I have an array of objects:
var cars = [
{
name: "Hyundai",
plans: [
{
name: "Something",
add-ons: [
{
cost: 100
},
{
cost: 75
}
]
}, { ... }
]
},
{
name: "Jeep",
plans: [
{
name: "Something",
add-ons: [
{
cost: 50
},
{
cost: 75
}
]
}, { ... }
]
},
{
name: "Buick",
plans: [
{
name: "Something",
add-ons: [
{
cost: 35
},
{
cost: 50
}
]
}, {...}
]
}
]
What I'm trying to do is find the top 2 cars that have the cheapest add-on and reference them via another variable.
Like this:
var top2 = findTopTwo(cars);
findTopTwo(arr) {
return arr.sort(function(a, b) {
// My trouble spot
}).slice(0, 2);
}
With my simple example, the result for top2 would be:
Buick ( cheapest add-on was $35, the value used to compare against )
Jeep ( cheapest add-on was $50, value used to compare against )
So what I would do is feed all of them into an array and then sort it on the cost. That would be my naive approach. The more optimal solution would be to only store 2 objects at a given time instead of a list of all items.
The naive approach would be as simple as:
var items = [];
for ( var i in cars ){
var car = cars[i];
for (var i in car["plans"]){
for (var j = 0; j < car["plans"][i]["add-ons"]){
items.push({"name": car.name, "cost": car["plans"][i]["add-ons"][j]["cost"]});
}
}
}
return items.sort(function(a,b){ return a.cost < b.cost }).slice(0,2);
That will return a list of 2 objects, the object contains the name of the car and the cost. The more effecient thing would be to do something like this:
var biggest = function(arr){
if (arr.length < 2 ) return -1;
return arr[0].cost > arr[1].cost ? 0 : 1;
}
var items = [];
for ( var i in cars ){
var car = cars[i];
for (var i in car["plans"]){
for (var j = 0; j < car["plans"][i]["add-ons"]){
var obj = {"name": car.name, "cost": car["plans"][i]["add-ons"][j]["cost"]};
}
var index = biggest(items)
if (index < 0){
items.push(obj);
}else{
if (items[index].cost > obj.cost)
items[index] = obj;
}
}
}
return items;
this more interesting design will push the first 2 into the list, but then it will find the biggest of the 2 costs and then checks to see if the new one is smaller than it. If the new one is smaller than item[index] it will be replaced.
This will never have the array larger than 2 so it takes up less memory
Another approach. By this approach your original data will not be sorted or modified.
var cars=[{name:"Hyundai",plans:[{name:"Something","add-ons":[{cost:100},{cost:75}]}]},
{name:"Jeep",plans:[{name:"Something","add-ons":[{cost:50},{cost:75}]}]},
{name:"Buick",plans:[{name:"Something","add-ons":[{cost:35},{cost:50}]}]}];
function findTopTwo(cars) {
return cars.map(
car =>
car.plans.reduce(
(prevPlan, plan) =>
plan['add-ons'].reduce((prevAddOn, addOn) => {
if (prevAddOn.cost > addOn.cost) {
prevAddOn.cost = addOn.cost;
}
return prevAddOn;
}, prevPlan), {
cost: Number.MAX_VALUE,
name: car.name
})
)
.sort((a, b) => a.cost - b.cost)
.slice(0, 2)
.map(item => item.name);
}
console.log(findTopTwo(cars));
I had to play around with the object, but here is the gist of it -
var cars = [{
name: "Hyundai",
plans: {
addons: [{
cost: 100
}, {
cost: 75
}]
}
}, {
name: "Jeep",
plans: {
addons: [{
cost: 50
}, {
cost: 75
}]
}
}, {
name: "Buick",
plans: {
addons: [{
cost: 35
}, {
cost: 50
}]
}
}];
var top2 = findTopTwo(cars);
console.log(top2);
function findTopTwo(arr) {
return arr.sort(function (a, b) {
// this map outputs array of costs: [35, 40]
// and Math.min takes the lowest value of each
var a_max_cost = Math.min.apply(null, a.plans.addons.map(function(i){i.cost})),
b_max_cost = Math.min.apply(null, b.plans.addons.map(function(i){i.cost}));
return a_max_cost - b_max_cost;
})
.slice(0, 2);
}
Basically, you need to return a-b in the sort function, where a and b are the lowest addon values. So I calculated the max of both cars on comparison, and used those values to decide which goes where.
Edit: I see you've updated the JS object, the answer should be similar to min, you will only need to figure out which plan to use for a and b. You can do so similar to my use of the Math.max function
One simple way of doing it is to first sort the addons by price (if you don't mind the side effect that addons then remain sorted by price).
function findTopTwo(arr) {
arr.forEach(function (elem) {
elem.plans.addons = elem.plans.addons.sort(function (a, b) {
return a.cost > b.cost;
});
});
return arr.sort(function(a, b) {
return a.plans.addons[0].cost > b.plans.addons[0].cost;
}).slice(0, 2);
}
jsbin example
Using #casraf's data:
const sortedCars = cars.map(car => {
car.plans.addons.sort((a, b) => a.cost - b.cost);
return car;
}).sort((a, b) => {
return a.plans.addons[0].cost - b.plans.addons[0].cost;
});
Line 2 sorts each cars' addons array from low to high. Line 5 sorts the cars from low to high based on the first index of their respective addons property.
If the ES6 syntax is confusing, here's a translation to ES5
I suggest to use sorting with map, then take the top 2 entries and get the data from cars.
var cars = [{ name: "Hyundai", plans: [{ 'add-ons': [{ cost: 100 }, { cost: 75 }] }] }, { name: "Jeep", plans: [{ 'add-ons': [{ cost: 50 }, { cost: 75 }] }] }, { name: "Buick", plans: [{ 'add-ons': [{ cost: 35 }, { cost: 50 }] }] }],
cost = cars.
map(function (a, i) {
return {
index: i,
cost: a.plans.reduce(function (r, b) {
return Math.min(r, b['add-ons'].reduce(function (s, c) {
return Math.min(s, c.cost);
}, Infinity));
}, Infinity)
};
}).
sort(function (a, b) { return a.cost - b.cost; }),
top2 = cost.slice(0, 2).map(function (a) {
return cars[a.index];
});
console.log(top2);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I want to reduce this object to just an object containing product name and average price. What's the fastest way to do it?
var foo = { group1: [
{
name: "one",
price: 100
},
{
name: "two",
price: 100
}],
group2: [
{
name: "one",
price: 200
},
{
name: "two",
price: 200
}],
group3: [
{
name: "one",
price: 300
},
{
name: "two",
price: 300
}]
}
resulting in
var foo2 = [{
name: 'one',
price: 200
},{
name: 'two',
price: 200
}];
Thanks!
Not to rain on Evan's parade, but here's an alternative that is a bit shorter ;)
result = _.chain(original)
.flatten()
.groupBy(function(value) { return value.name; })
.map(function(value, key) {
var sum = _.reduce(value, function(memo, val) { return memo + val.price; }, 0);
return {name: key, price: sum / value.length};
})
.value();
See it in action: http://plnkr.co/edit/lcmZoLkrlfoV8CGN4Pun?p=preview
I really like redmallard's solution, but I wanted to golf a little.
Underscore doesn't include a sum function, but we can write pretty elegant functional expressions by adding a sum mixin. This function is known as add in the underscore-contribs repo.
Then we can write:
// Somewhere in the initialization of the program
_.mixin({
sum : function (arr) {
return _.reduce(arr, function (s, x) { return s + x;}, 0);
}
});
result = _.chain(original)
.flatten()
.groupBy('name') // shorthand notation
.map(function (value, key) {
var sum = _.chain(value).pluck('price').sum().value();
return { name: key, price: sum / value.length};
})
.value();
http://plnkr.co/edit/ul3odB7lr8qwgVIDOtM9
But then we can also create an avg mixin to expand our toolbelt:
// Somewhere in the initialization of the program
_.mixin({
sum : function (arr) {
return _.reduce(arr, function (s, x) { return s + x;}, 0);
},
avg : function (arr) {
return _.sum(arr)/arr.length;
}
});
result = _.chain(original)
.flatten()
.groupBy('name') // shorthand notation
.map(function (value, key) {
return { name: key, price: _.avg(value)};
})
.value();
Edit: Leaving this up for now, but I totally forgot about _.flatten, so redmallard's got a much better answer.
If you already know the product names and they appear in every group, you could do the whole thing quickly this way:
var productAveragePrices = function ( groups, names ) {
return _.map( names, function ( name ) {
var product = { name: name }, productPricesSum = 0;
_.each( groups, function ( group ) {
productPricesSum += ( _.findWhere( group, product ).price );
});
product.price = productPricesSum / _.size( groups );
return product;
});
};
var foo2 = productAveragePrices = function ( foo, ['one', 'two'] );
I put this together, which should work even if your groups have different products (eg "one" in first, second, and fourth group and "two" in first and third):
var productPriceReducer = function( memo, group ) {
_.each( group, function( product ) {
// Grabs the current product from the list we're compiling
var memoProduct = _.findWhere( memo, { name: product.name });
if ( !memoProduct ) {
// If the product doesn't exist, creates a holder for it and its prices
memoProduct = {
name: product.name,
prices: [ product.price ]
};
memo.push( memoProduct );
} else {
// Otherwise, it just adds the prices to the existing holder.
memoProduct.prices.push( product.price );
}
});
return memo;
};
// This gets us a list of products with all of their prices across groups
var productPrices = _.reduce( foo, productPriceReducer, [] );
// Then reducing to the average is pretty simple!
var productAveragePrices = _.map( productPrices, function ( product ) {
var sumPrices = _.reduce( product.prices, function ( memo, price ) {
return memo + price;
}, 0 );
return {
name: product.name,
price: sumPrices / product.prices.length
};
});
You could still do the above in one function with a counter and summing the prices, but this way, you also have the prices in case you want to, say, take the standard deviation or find the mode.