Calculate average of an array with objects that has a nested Object - javascript

I am trying to loop through array of Objects and calculate the average of a nested Object containing several different keys.
This is the start array:
[{
course: "math",
id: 4,
values: {
2017: 8,
2018: 9
}
}, {
course: "math",
id: 4,
values: {
2017: 5,
2019: 7
}
}]
This is my goal:
{2017:6.5,2018:9,2019:7}
Now it returns correct for 2017 but NaN for 2018 and 2019. If anyone have better way of solving this that doesn't require so much please provide to.
This is what I have tried so far. I have been searching a lot but not really found anything I can use.
const testObject = [{
id: 4,
course: "math",
values: {
2017: 8,
2018: 9
}
},
{
id: 5,
course: "English",
values: {
2017: 8,
2018: 9
}
},
{
id: 4,
course: "math",
values: {
2017: 5,
2019: 7
}
},
{
id: 4,
course: "english",
values: {
2017: 5,
2019: 7
}
},
]
//First I filter out the id 4 and course Math
const mathid1 = testObject.filter((e) => e.id === 4 && e.course === "math");
//I than find all the different years
const ArrayOfAllYears = []
mathid1.map((element) => {
ArrayOfAllYears.push(Object.keys(element.values));
})
//I here find all the different years
const withDuplicates = ArrayOfAllYears.reduce(function(arrayOne, arrayTwo) {
return arrayOne.concat(arrayTwo);
}, []);
const withoutDuplicates = Array.from(new Set(withDuplicates));
//Here I just create the calculate average function
const Result = {}
const calculateAverage = (array) => {
const sum = array.reduce((a, b) => a + b);
return sum / array.length;
};
const newObj = {}
withoutDuplicates.map((year) => {
let reformattedArray = mathid1.map(obj => {
if (obj["values"][year]) {
return obj["values"][year]
}
})
newObj[year] = calculateAverage(reformattedArray)
})
console.log(newObj)
// I want to calculate the average of the mathid1 values and return it on a Object like {2017:..,2018..}

There are two simple steps to the problem.
First, you need to reduce the array to an object with years and values:
// this outputs
// { 2017: [8, 5], 2018: [9], 2019: [7] }
function byYear(array) {
// take each item of an array
return array.reduce((acc, data) => {
// take the values of that item
Object.entries(data.values).forEach(([year, value]) => {
// and map all the values to years
acc[year] = acc[year] || []
acc[year].push(value)
})
return acc
}, {})
}
The second step is just taking averages:
function average(object) {
const averages = {}
for (let key in object) {
averages[key] = object[key].reduce((sum, value) => sum + value) / object[key].length
}
return averages
}
And now you put them together:
average(byYear(input))
In here, the input is the filtered array. As a whole snippet:
function byYear(array) {
return array.reduce((acc, data) => {
Object.entries(data.values).forEach(([year, value]) => {
acc[year] = acc[year] || []
acc[year].push(value)
})
return acc
}, {})
}
function average(object) {
const averages = {}
for (let key in object) {
averages[key] = object[key].reduce((sum, value) => sum + value) / object[key].length
}
return averages
}
const output = average(byYear([{
course: "math",
id: 4,
values: {
2017: 8,
2018: 9
}
}, {
course: "math",
id: 4,
values: {
2017: 5,
2019: 7
}
}]))
console.log(output)

The problem with your current code lies in how you build the reformattedArray variable. First, notice that your map function implicitly returns undefined whenever that year is missing from the current object:
let reformattedArray = mathid1.map(obj => {
if (obj["values"][year]) {
return obj["values"][year]
}
// There is an implicit return undefined, right here...
})
When you use the array .map method, every item of the array will be replaced by the return value of the map function. In the case that the year is not present, it will not go into the if block, and so it implicitly returns undefined upon reaching the end of the function.
So, ultimately all you have to do is remove the undefined entries from this array, and your code will work as-is.
One way to do that is to just use .filter(Boolean) on the array, which removes any falsey entries (which undefined is). Eg:
let reformattedArray = mathid1.map(obj => {
/* code here */
}).filter(Boolean); // Note the filter here...
Here is your snippet with that modification:
const testObject = [{
id: 4,
course: "math",
values: {
2017: 8,
2018: 9
}
},
{
id: 5,
course: "English",
values: {
2017: 8,
2018: 9
}
},
{
id: 4,
course: "math",
values: {
2017: 5,
2019: 7
}
},
{
id: 4,
course: "english",
values: {
2017: 5,
2019: 7
}
},
]
//First I filter out the id 4 and course Math
const mathid1 = testObject.filter((e) => e.id === 4 && e.course === "math");
//I than find all the different years
const ArrayOfAllYears = []
mathid1.map((element) => {
ArrayOfAllYears.push(Object.keys(element.values));
})
//I here find all the different years
const withDuplicates = ArrayOfAllYears.reduce(function(arrayOne, arrayTwo) {
return arrayOne.concat(arrayTwo);
}, []);
const withoutDuplicates = Array.from(new Set(withDuplicates));
//Here I just create the calculate average function
const Result = {}
const calculateAverage = (array) => {
const sum = array.reduce((a, b) => a + b);
return sum / array.length;
};
const newObj = {}
withoutDuplicates.map((year) => {
let reformattedArray = mathid1.map(obj => {
if (obj["values"][year]) {
return obj["values"][year]
}
}).filter(Boolean)
newObj[year] = calculateAverage(reformattedArray)
})
console.log(newObj)
// I want to calculate the average of the mathid1 values and return it on a Object like {2017:..,2018..}

Group items by year.
Calculate average.
const items=[{
course: "math",
id: 4,
values: {
2017: 8,
2018: 9
}
}, {
course: "math",
id: 4,
values: {
2017: 5,
2019: 7
}
}]
const groupedValues=items.reduce((groupedValues,item)=>{
Object.entries(item.values).forEach(([year,value])=>{
if(groupedValues[year]){
groupedValues[year]={value:groupedValues[year].value+value,items:groupedValues[year].items+1};
} else {
groupedValues[year]={value,items:1};
}
});
return groupedValues;
},{})
console.log(groupedValues);
const result = Object.entries(groupedValues).reduce((result,item)=>{
result[item[0]]=item[1].value/item[1].items;
return result;
},{})
console.log(result);

I would recommend extracting the years information into a map:
/** #type {Map<string, number[]} */
const years = new Map();
testObject.forEach((obj) => {
Object.keys(obj.values).forEach((key) => {
if (!years.has(key)) years.set(key, []);
years.set(key, [...years.get(key), obj.values[key]]);
});
});
Then you can simply loop over the map and create the resulting object:
const result = {};
years.forEach((values, key) => {
Object.defineProperty(result, key, {
value: values.reduce((acc, val) => acc + val) / values.length,
enumerable: true,
});
});
console.log(result);
It should output:
{ '2017': 6.5, '2018': 9, '2019': 7 }

Related

How to merge a nested array of objects in javascript with a specific shape

I am trying to merge an array of objects by summing up the totals of each key-value pair under the totals object. For example, the below array would yield one object with a totals object of 3 apples and 5 oranges. This should be dynamic. If pears were to be a key in another object, the resulting object would include three keys under the totals object: apples, oranges, and pears.
Sample Input:
[
{
summary: {
totals: {
apples: 2,
oranges: 3
}
}
},
{
summary: {
totals: {
apples: 1,
oranges: 2
}
}
}
]
Expected Output:
{
summary:{
totals:{
apples:3,
oranges:5
}
}
}
What I've tried:
function mergeObjects(arr) {
let shape = {
summary:{
totals:{}
}
}
return arr.reduce((prev, cur) => {
if(cur.summary.totals.apples){
shape.summary.totals.apples.push(cur.summary.totals.apples)
}
}, shape);
}
Using Array#reduce, iterate over the array while updating an object
In every iteration, using Object#entries and
, iterate over the current totals pairs and update the accumulator.
const arr = [
{ summary: { totals: { apples: 2, oranges: 3 } } },
{ summary: { totals: { apples: 1, oranges: 2 } } },
];
const res = arr.reduce((map, current) => {
const { totals: currentTotals = {} } = current.summary ?? {};
const { totals } = map.summary;
Object.entries(currentTotals).forEach(([ key, value ]) => {
totals[key] = (totals[key] ?? 0) + value;
});
return map;
}, { summary: { totals: {} } });
console.log(res);
You can try something like. Just loop through the array and sum up apples and oranges.
const arr = [
{
summary: {
totals: {
apples: 2,
oranges: 3,
},
},
},
{
summary: {
totals: {
apples: 1,
oranges: 2,
},
},
},
];
function mergeObjects(arr) {
let shape = {
summary:{
totals:{
apples:0,
oranges:0
}
}
}
arr.forEach(x => {
if(x.summary.totals.apples){
shape.summary.totals.apples += x.summary.totals.apples;
shape.summary.totals.oranges += x.summary.totals.oranges;
}
});
return shape;
}
let result = mergeObjects(arr);
console.log(result);
The second option of the reduce function initializes the value.
And the initialized value can be used in prev!
[1] store the value in prev.
[2] prev can accumulate values. You have to return to use the accumulated value. If not returned, the value will be undefined.
[3] apples is not an array type, so you cannot use the push method. it is a number type, you must use a numeric operator.
function mergeObjects(arr) {
const shape = {
summary: {
totals: {},
},
};
return arr.reduce((prev, cur) => {
const { apples, oranges } = cur.summary.totals;
// [1]
prev.summary.totals.apples
// [3]
? (prev.summary.totals.apples += apples)
: (prev.summary.totals.apples = apples);
prev.summary.totals.oranges
? (prev.summary.totals.oranges += oranges)
: (prev.summary.totals.oranges = oranges);
// [2]
return prev;
}, shape);
}
tips!
Use Destructuring Assignment
const { apples, oranges } = cur.summary.totals;
Use Ternary Operator
prev.summary.totals.apples
? (prev.summary.totals.apples += apples)
: (prev.summary.totals.apples = apples);
Make code look nice!
You can combine Object.entries() with Array#reduce() and Array.forEach()
Code:
const data = [{summary: {totals: {apples: 2,oranges: 3}}},{summary: {totals: {apples: 1,oranges: 2}}}]
const result = data.reduce((a, c) => {
Object
.entries(c.summary.totals)
.forEach(([k, v]) => a.summary.totals[k] += v)
return a
},
{ summary: { totals: { apples: 0, oranges: 0 } } })
console.log(result)

Populating an array of object with 0 if object does not exist

I am building a chart for monthly data which would have the x axis as wk1 - wk4 and y axis being the amount of goods etc. I was able to build out a solution but the problem lies when there is no data for a particular week. This is my code below.
const byAmount = obj => {
const res = [];
const keys = Object.keys(obj);
keys.forEach(key => {
res.push({
week: `wk${key}`,
amount: obj[key]
});
});
return res.sort((a, b) => a.amount - b.amount).slice(0, 5);;
};
const getWeeklyFromMonth = (arr, month) => {
const week = arr.map(a => ({ ...a, week: Math.floor((moment(a.dateScanned.$date).date() - 1) / 7) + 1 }));
let dataForMonth = [];
let total;
week.map(data => {
if (moment(data.dateScanned.$date).format("MMM") === month) {
dataForMonth.push(data);
const totalPerWeek = dataForMonth.reduce((acc, cur) => {
acc[cur.week] = acc[cur.week] + cur.amount || cur.amount;
return acc;
}, {});
total = totalPerWeek;
}
});
return byAmount(total);
}
When I run this I get the below:
[
{ week: 'wk1', amount: 14 },
{ week: 'wk2', amount: 27 },
{ week: 'wk4', amount: 43 }
]
This is fine but I want to populate the array with 0 if there is no data say for week 3. I would want it to be this
[
{ week: 'wk1', amount: 14 },
{ week: 'wk2', amount: 27 },
{ week: 'wk3', amount: 0 },
{ week: 'wk4', amount: 43 }
]
I was thinking of having an array of like [1, 2, 3, 4] and if the array includes the week number, pop it out of the array and then the remaining item should be used to populate it but I find myself scratching my head. Does anyone know a decent way to do this?
Thank you in advance.
You can try this:
const byAmount = obj => {
const res = [];
const keys = Object.keys(obj);
const [min, max] = [Math.min(...keys), Math.max(...keys)];
for(let key = min; key <= max; key++) {
res.push({
week: `wk${key}`,
amount: obj[key] || 0
});
}
return res.sort((a, b) => a.amount - b.amount).slice(0, 5);;
};

How do I create new object after manipulation

I have a JavaScript object like the following below availability and reserved, here I need to subtract quantity value from availability.
var availability = {"bike":10,"cycle":3,"car":1};
var reserved ={"cycle":1,"bike":10}
how should I get this response as below?
response = {"bike":0,"cycle":2,"car":1};
Why not a simple for loop.
var availability = { bike: 10, cycle: 3, car: 1 };
var reserved = { cycle: 1, bike: 10 };
let response = {};
for (let key in availability) {
if (reserved[key]) {
response[key] = availability[key] - reserved[key];
} else {
response[key] = availability[key];
}
}
console.log(response);
Output:
{ bike: 0, cycle: 2, car: 1 }
There are many way to solve this, but I recommend using reduce().
var availibilty = {
"bike": 10,
"cycle": 3,
"car": 1
};
var reserved = {
"cycle": 1,
"bike": 10
}
function calc(a, b) {
const answer = Object.keys(a).reduce((acc, key) => {
return {
...acc,
[key]: a[key] - (b[key] || 0)
}
}, {});
console.log(answer);
}
calc(availibilty, reserved);
You can iterate through each key-value pair and subtract quantity in availability with the corresponding key in reserved. Then create your result object using Object.fromEntries().
const availability = { "bike" : 10, "cycle" : 3, "car" : 1 },
reserved ={ "cycle": 1,"bike": 10 },
result = Object.fromEntries(Object.entries(availability).map(([key, value]) => [key, value - (reserved[key] ?? 0)]));
console.log(result);
You can loop through the Object. keys() of one object you provided and subtract the other using reduce() method.
var availibilty = {"bike":10,"cycle":3,"car":1};
var reserved ={"cycle":1,"bike":10}
let response = Object.keys(availibilty).reduce((x, y) => {
x[k] = availibilty[y] - reserved[y];
return x;
}, {});
console.log(response);
Please find Array.reduce implementation.
Logic
Loop through keys of availability object.
Find the values of each keys from reserved object.
Store the difference as the value for same key in the accumulator array.
var availability = { "bike": 10, "cycle": 3, "car": 1 };
var reserved = { "cycle": 1, "bike": 10 };
const response = Object.keys(availability).reduce((acc, curr) => {
acc[curr] = availability[curr] - (reserved[curr] || 0);
return acc;
}, {});
console.log(response);

How can I push data to an array with fixed length in js

I have an array of object something like this.
[
{
channelName: "WhatsApp"
count: 1
date: "2021-06-05"
},{
channelName: "RCS"
count: 1
date: "2021-06-09"
}
]
There are two types of channel names 1. WhatsApp and 2nd are RCS. I want to filter out count with specific channel names and store it in a separate array. But the problem here is I want both the array length should be the same. If there is data for WhatsApp then it will add the count otherwise it will add 0 in place of it.
For that, I did something like this but this does not work .
const filterData = (data: any) => {
const category: any = [];
const whatsAppCount: any = [];
const rcsCount: any = [];
data.filter((item: any, i: number) => {
if (item.channelName === "WhatsApp") {
whatsAppCount[i] = item.count;
} else if (item.channelName === "RCS") {
rcsCount[i] = item.count;
}
category.push(item.date);
});
setGraphData({
category: category,
whatsApp: whatsAppCount,
rcs: rcsCount,
});
console.log("handleRun", { category, whatsAppCount, rcsCount });
};
Here the console log gives something like this.
whatsAppCount: [1, 2, 13, 21, empty × 2, 8, 5, empty, 18, empty, 12, 4]
rcsCount: [empty × 4, 1, 12, empty × 2, 1, empty, 8]
Here in the place of empty, I want 0. I am not sure how to do that any help would be great.
When you create the arrays, but before populating them, there are two functions that can help with initialization:
// create an array with 10 slots preallocated but empty (not `undefined`)
let arr = new Array(10);
// set all allocated slots to a value (`0` in this case)
arr = arr.fill(0);
Since you know the lengths you want ahead of time, you can use that to pre-size the arrays on construction. Then use .fill to initialize the values to 0. Once, that's done, you can continue with your counting and updating the arrays.
Reference:
Array constructor
Array.prototype.fill()
I would suggest you use the map-function, mapping the unwanted values to undefined, letting the other values "pass through" (unmodified), eg.:
const filtered = data.map((each) => {
if (wantToKeep) {
return each;
} else {
return undefined;
}
});
Note, this is not the exact solution - but a general idea.
You can use forEach and push(0) for the empty records.
const data = [
{
channelName: "WhatsApp",
count: 1,
date: "2021-06-05",
},
{
channelName: "RCS",
count: 1,
date: "2021-06-01",
},
{
channelName: "RCS",
count: 1,
date: "2021-06-06",
},
{
channelName: "WhatsApp",
count: 5,
date: "2021-06-11",
},
{
channelName: "WhatsApp",
count: 7,
date: "2021-06-23",
},
{
channelName: "RCS",
count: 1,
date: "2021-06-09",
},
];
const category = [];
const whatsAppCount = [];
const rcsCount = [];
data.forEach(x => {
if (x.channelName === "WhatsApp") {
whatsAppCount.push(x.count);
rcsCount.push(0);
} else if (x.channelName === "RCS") {
whatsAppCount.push(0);
rcsCount.push(x.count);
}
category.push(x.date);
});
console.log({ whatsAppCount });
console.log({ rcsCount });
console.log({ category });

How can I sort this array in Discord.js?

I have an array, that looks like this(size changes):
[
{ '385090261019131915': 34 },
{ '746430449240375297': 2 },
{ '810189312175374408': 1 },
{ '830832432680009789': 8 },
{ '850073735272988692': 1 }
]
The first value is the member id, the second how many messages the user has.
How can I sort the array, to get the first 10 members, sorted by their messages send?
The code:
if(command === 'leaderboard'){
const list = []
fs.readdirSync('./db/user/messages').forEach(file => {
const user = JSON.parse(fs.readFileSync(`./db/user/messages/${file}` , 'utf-8'))
userid = file.replace('.json','');
const entry = {[userid] : user.userall}
list.push(entry)
})
}
To sort an array by numbers, you can use the .sort() method with a compare function that subtracts the second value from the first one:
const arr = [34, 2, 1, 8, 1]
const sorted = arr.sort((a, b) => b - a)
console.log({ sorted })
As you're using objects, you should sort by an object key, but you're using the user ID as the key, so you don't know them. You can, however, get the value using the [Object.values()][2] method to get the value(s) and sort by them:
const arr = [
{ '385090261019131915': 34 },
{ '746430449240375297': 2 },
{ '810189312175374408': 1 },
{ '830832432680009789': 8 },
{ '850073735272988692': 1 }
]
const sorted = arr.sort((a, b) => Object.values(b)[0] - Object.values(a)[0])
console.log({ sorted })
Don't forget that Object.values() returns an array so you'll need to compare the first element.
However, instead of using the user ID as the key and the points as the value, I'd use two different keys in the object, one for the ID and one for the score:
const list = [
{ id: '385090261019131915', score: 34 },
{ id: '746430449240375297', score: 2 },
{ id: '810189312175374408', score: 1 },
{ id: '830832432680009789', score: 8 },
{ id: '850073735272988692', score: 1 }
]
const sortedList = list.sort((a, b) => b.score - a.score)
console.log({ sortedList })
And the final code:
if (command === 'leaderboard') {
const list = []
fs.readdirSync('./db/user/messages').forEach((file) => {
const user = JSON.parse(
fs.readFileSync(`./db/user/messages/${file}`, 'utf-8'),
)
const userId = file.replace('.json', '')
list.push({ id: userId, score: user.userall })
});
// sort by score
const sortedList = list.sort((a, b) => b.score - a.score)
}

Categories

Resources