JavaScript reduce throwing error for more than two items - javascript

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);

Related

how to assign object in object

how to assign the object in object and filter the value which pass and fail;
the input is:
[
{
name: 'John',
score: 90,
time: 'evening'
},
{
name: 'Doni',
score: 68,
time: 'morning'
},
{
name: 'Jiu',
score: 50,
time: 'evening'
},
{
name: 'Shin',
score: 92,
time: 'morning'
},
];
and i want the output like this :
{
"evening": {
"pass": [
{
"name": "John",
"score": 90
}
],
"fail": [
{
"name": "jiu",
"score": 50
}
]
},
"morning": {
"pass": [
{
"name": "Shin",
"score": 92
}
],
"fail": [
{
"name": "Doni",
"score": 68
}
]
}
}
do we need to use Object.assign for this ? and how many loop we use for this ??
i do love to know how to add another string in the object beside that ouput,
thanks
There's a lot of ways to do this. The simplest is probably to make a base object that represent your empty results. Then loop over the students and fill the arrays:
let students = [{name: 'John',score: 90,time: 'evening'},{name: 'Doni',score: 68,time: 'morning'},{name: 'Jiu',score: 50,time: 'evening'},{name: 'Shin',score: 92,time: 'morning'},];
// Empty case
let base = {
"evening": {"pass": [], "fail": []},
"morning": {"pass": [], "fail": []}
}
const PASSING = 70
students.forEach(({name, score, time}) => {
let key = score >= PASSING ? 'pass' : 'fail'
base[time][key].push({name, score})
})
console.log(base)
This makes is easy to have empty arrays, which is probably what you want if there are no students in a particular category.
EDIT based on comment:
To support arbitrary times, you can just create the times on the object as you find them. reduce() is good for this, but you could also use a regular loop. For example with an added afternoon time:
let students = [{name: 'Mark',score: 95,time: 'afternoon'}, {name: 'John',score: 90,time: 'evening'},{name: 'Doni',score: 68,time: 'morning'},{name: 'Jiu',score: 50,time: 'evening'},{name: 'Shin',score: 92,time: 'morning'},];
const PASSING = 70
let result = students.reduce((obj, {name, score, time}) => {
if (!obj[time]) obj[time] = {'pass': [], 'fail': [] }
let key = score >= PASSING ? 'pass' : 'fail'
obj[time][key].push({name, score})
return obj
}, {})
console.log(result)
You can do something like this:
const data = [{ name: 'John', score: 90, time: 'evening' }, { name: 'Doni', score: 68, time: 'morning' }, { name: 'Jiu', score: 50, time: 'evening' }, { name: 'Shin', score: 92, time: 'morning' }, ];
const grp = (d, p) => d.reduce((r,c) => (r[c[p]] = [...r[c[p]] || [], c], r), {})
const grpV = (d, rng) => d.reduce((r,{name, score}) => {
let key = score > rng ? 'pass' : 'fail'
r[key] = [...r[key] || [], {name, score}]
return r
}, {})
const r = Object.entries(grp(data, 'time')).map(([k,v]) => ({[k]: grpV(v, 75)}))
console.log(r)
The idea is the group 2 times one on the time and 2nd on the score.
grp: function to group by a property (in this case 'time') which returns an object with 2 properties: evening and morning each of which is an array containing the classes.
grpV: function to group by value (in this case 75) which returns an object with 2 properties: pass and fail each of which is an array containing the classes.
On the end once we have those tools we are saying ... give me the entries of the grouped by time object and for each of the groups ... group by score.
Here how something like this could look like if we ware using lodash:
const data = [{ name: 'John', score: 90, time: 'evening' }, { name: 'Doni', score: 68, time: 'morning' }, { name: 'Jiu', score: 50, time: 'evening' }, { name: 'Shin', score: 92, time: 'morning' }, ];
const partition = (x, p) => _(x)
.partition(y => y.score > p)
.map((x,i) => ({ [i==0 ? 'pass': 'fail']: _.omit(x[0], 'time')}))
.value()
const r = _(data)
.groupBy('time')
.mapValues(x => partition(x, 75))
.value()
console.log(r)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>
Adding it as an example since it does help with readability of what the ES6 example is doing to some extend.
I'm sure there are more elegant ways to do this. But this one is probably one of the simplest beginner-friendly ways you can go about this.
I loop through the input array, check the existence of the .time values as keys on the output object and create the pass and fail keys. Then evaluate the .score against the passingScore and push the necessary data to it.
Should be pretty easy to understand once you see and try the code below:
const data = [
{name: 'John',score: 90, time: 'evening'},
{name: 'Doni',score: 68, time: 'morning'},
{name: 'Jiu',score: 50, time: 'evening'},
{name: 'Shin',score: 92, time: 'morning'},
{name: 'Fubar',score: 75, time: 'noon'},
];
function formatData(data){
const passingScore = 75;
const output = {};
data.forEach(function(item){
if(!output[item.time]) output[item.time] = {pass: [], fail: []};
const stud = { name: item.name, score: item.score };
if(item.score >= passingScore) output[item.time]['pass'].push(stud)
else output[item.time]['fail'].push(stud)
});
return output;
}
console.log(formatData(data));

JavaScript reduce not working on an object

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

Group multiple elements in array with JavaScript

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);

Sum values of objects which are inside an array with underscore.js and reduce

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);

How to sort a JavaScript array by more nested objects in arrays and grab the top ###?

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; }

Categories