Related
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 }
Here is the scenario I am looking at:
I want to reduce these objects
const data = [
{
id: 1,
totalAmount: 1500,
date: '2021-01-01',
vendor_id: 2,
totalTransaction: 12,
isRefund: false,
},
{
id: 2,
totalAmount: 200,
date: '2021-01-01',
vendor_id: 2,
totalTransaction: 2,
isRefund: true,
},
{
id: 3,
totalAmount: 200,
date: '2021-01-01',
vendor_id: 2,
totalTransaction: 1,
isRefund: true,
},
];
and I found a solution that adds their values:
const deepMergeSum = (obj1, obj2) => {
return Object.keys(obj1).reduce((acc, key) => {
if (typeof obj2[key] === 'object') {
acc[key] = deepMergeSum(obj1[key], obj2[key]);
} else if (obj2.hasOwnProperty(key) && !isNaN(parseFloat(obj2[key]))) {
acc[key] = obj1[key] + obj2[key]
}
return acc;
}, {});
};
const result = data.reduce((acc, obj) => (acc = deepMergeSum(acc, obj)));
const array = []
const newArray = [...array, result]
which results to:
const newArray = [
{
id: 6,
totalAmount: 1900,
date: '2021-01-012021-01-012021-01-01',
vendor_id: 6,
totalTransaction: 15
}
]
And now my problem is I don't know yet how to work this around to have my expected output which if isRefund is true, it must be subtracted instead of being added, retain the vendor_id and also the concatenated date instead of only one entry date:
const newArray = [
{
id: 1(generate new id if possible),
totalAmount: 1100,
date: '2021-01-01',
vendor_id: 2,
totalTransaction: 15,
isRefund: null(this can be removed if not applicable),
},
];
I will accept and try to understand any better way or workaround for this. Thank you very much.
As you want custom behaviour for several fields, and don't need the recursive aspect of the merge, I would suggest you create a custom merge function, specific to your business logic:
const data = [{id: 1,totalAmount: 1500,date: '2021-01-01',vendor_id: 2,totalTransaction: 12,isRefund: false,},{id: 2,totalAmount: 200,date: '2021-01-01',vendor_id: 2,totalTransaction: 2,isRefund: true,},{id: 3,totalAmount: 200,date: '2021-01-01',vendor_id: 2,totalTransaction: 1,isRefund: true,},];
function customMerge(a, b) {
if (a.vendor_id !== b.vendor_id || a.date !== b.date) {
throw "Both date and vendor_id must be the same";
}
return {
id: Math.max(a.id, b.id),
totalAmount: (a.isRefund ? -a.totalAmount : a.totalAmount)
+ (b.isRefund ? -b.totalAmount : b.totalAmount),
date: a.date,
vendor_id: a.vendor_id,
totalTransaction: a.totalTransaction + b.totalTransaction
};
}
const result = data.reduce(customMerge);
if (data.length > 1) result.id++; // Make id unique
console.log(result);
You could also reintroduce the isRefund property in the result for when the total amount turns out to be negative (only do this when data.length > 1 as otherwise result is just the original single object in data):
result.isRefund = result.totalAmount < 0;
result.totalAmount = Math.abs(result.totalAmount);
Distinct results for different dates and/or vendors
Then use a "dictionary" (plain object or Map) keyed by date/vendor combinations, each having an aggregating object as value.
To demonstrate, I added one more object in the data that has a different date and amount of 300:
const data = [{id: 1,totalAmount: 1500,date: '2021-01-01',vendor_id: 2,totalTransaction: 12,isRefund: false,},{id: 2,totalAmount: 200,date: '2021-01-01',vendor_id: 2,totalTransaction: 2,isRefund: true,},{id: 3,totalAmount: 200,date: '2021-01-01',vendor_id: 2,totalTransaction: 1,isRefund: true,},{id: 4,totalAmount: 300,date: '2021-01-02',vendor_id: 2,totalTransaction: 1,isRefund: false,}];
function customMerge(acc, {id, date, vendor_id, totalAmount, totalTransaction, isRefund}) {
let key = date + "_" + vendor_id;
if (!(id <= acc.id)) acc.id = id;
acc[key] ??= {
date,
vendor_id,
totalAmount: 0,
totalTransaction: 0
};
acc[key].totalAmount += isRefund ? -totalAmount : totalAmount;
acc[key].totalTransaction += totalTransaction;
return acc;
}
let {id, ...grouped} = data.reduce(customMerge, {});
let result = Object.values(grouped).map(item => Object.assign(item, { id: ++id }));
console.log(result);
it could be help, if you are looking for the same output, can add other checklist based on your requirement, for filtered date, logic would be little different but the output will be same.
const getTotalTranscation = () =>
transctionLists.reduce((prev, current) => {
const totalAmount = current.isRefund
? prev.totalAmount - current.totalAmount
: prev.totalAmount + current.totalAmount;
const totalTransaction = current.isRefund
? prev.totalTransaction - current.totalTransaction
: prev.totalTransaction + current.totalTransaction;
return {
...current,
id: current.id + 1,
totalAmount,
totalTransaction,
isRefund: totalAmount < 0,
};
});
so I have this array.
["2020-01-31 18:31:01", "2020-02-03 14:04:30", "2020-02-04 05:58:48", "2020-02-04 14:11:16"]
and I like to convert it to object.
0: {date: "2020-01-31", time: ["8:31:01"]}
1: {date: "2020-02-03", time: ["4:04:30"]}
2: {date: "2020-02-04", time: ["5:58:48", "14:11:16"]}
But I 'm getting this result
0: {date: "2020-01-31", time: "8:31:01"}
1: {date: "2020-02-03", time: "4:04:30"}
2: {date: "2020-02-04", time: "5:58:48"}
This is my code
var times = response.data.time;
var dates = [];
var t = [];
var d = '';
var newData = [];
times.forEach(time => {
var da = time.substring(0, 10);
var ti = time.substring(12, 19);
if(d == da) {
t.push(ti);
} else {
d = da
var dt = {date: da, time: ti};
newData.push(dt);
}
});
I'm having a hard time to figure this out hope you can help me.
Thanks.
Try to use Array.prototype.reduce function:
const array = ["2020-01-31 18:31:01", "2020-02-03 14:04:30", "2020-02-04 05:58:48", "2020-02-04 14:11:16"];
const newData = array.reduce((acc, cur) => {
const [date, time] = cur.split(' ');
const dateObj = acc.find(e => e.date === date);
if (dateObj) {
dateObj.time.push(time);
} else {
acc.push({ date, time: [time] });
}
return acc;
}, []);
console.log(newData);
Almost a copy of Hao Wu's solution, but faster, as it does not rely on linear find. The values are accumulated into an object, allowing for fast lookups, and then an array is extracted using Object.values:
const array = ["2020-01-31 18:31:01", "2020-02-03 14:04:30", "2020-02-04 05:58:48", "2020-02-04 14:11:16"];
const newData = Object.values(array.reduce((acc, cur) => {
const [date, time] = cur.split(' ');
if (!acc[date]) {
acc[date] = { date, time: [] };
}
acc[date].time.push(time);
return acc;
}, {}));
console.log(newData);
So my final result I'm looking for is:
result = [{x: '12/12', y: 90 }, {x: '12/11', y: 0}, {x: '12/10', y: 92}, {x: '12/9', y: 0}, ... ]
Now I have 2 arrays. First array is just an array of the last 30 days. I created it using momentjs like this:
const lastThirtyDays = [...new Array(31)].map((i, idx) =>
moment()
.startOf('day')
.subtract(idx, 'days')
.format('MM/D'),
);
which produces:
["12/12", "12/11", "12/10", "12/9", "12/8", "12/7", "12/6", "12/5", "12/4", "12/3", "12/2", "12/1", "11/30", "11/29", "11/28", "11/27", "11/26", "11/25", "11/24", "11/23", "11/22", "11/21", "11/20", "11/19", "11/18", "11/17", "11/16", "11/15", "11/14", "11/13", "11/12"]
The next array has a collection of numbers and dates. It looks more like this:
const sampleSet = [
{ date: '2019-12-11', number: 100 },
{ date: '2019-12-10', number: 99 },
{ date: '2019-12-08', number: 101 },
{ date: '2019-12-07', number: 90 },
{ date: '2019-12-05', number: 98 },
{ date: '2019-12-01', number: 96 },
{ date: '2019-11-28', number: 99 },
];
So my solution was to use a forEach and create an x y dataset. First I tried something like this:
const createDateSet = () => {
let set = [];
lastThirtyDays.forEach((date, i) => {
if (
sampleSet[i] &&
sampleSet[i].date &&
moment(sampleSet[i].date).format('MM/D') === date
) {
set.push({ x: date, y: sampleSet[i].number });
} else {
set.push({ x: date, y: 0 });
}
});
};
That didn't work. Only one of them matched. So I tried running the forEach on both arrays like this:
const createDataSetBothArrays = () => {
let set = [];
lastThirtyDays.forEach((date, i) => {
let dateItem = sampleSet[i];
if (dateItem) {
sampleSet.forEach((datum, i) => {
if (moment(datum.date).format('MM/D') === date) {
set.push({ x: date, y: datum.number });
}
});
} else {
set.push({ x: date, y: 0 });
}
});
};
But the numbers mismatched.
What would be the proper way to go about this???
Thanks!
Simply use a map and a find to get the corresponding dateSet.number :
lastThirtyDays.map(monthAndDay => {
const date = moment('12/11/2019').format('MM-DD-YYYY');
const set = sampleSet.find(set => set.date === date);
return { x: monthAndDay, y: set ? set.number : 0 };
});
You could change your lastThirtyDays array so that the date format is identical to your sampleSet. Not only does this allow you to cross-reference using an exact match, but it's safer in the event that the last thirty days span across multiple years. You can transform it back into your desired display style when constructing the result.
If a matching date isn't found, we default number to 0 using || {number: 0}.
const sampleSet = [ { date: '2019-12-11', number: 100 }, { date: '2019-12-10', number: 99 }, { date: '2019-12-08', number: 101 }, { date: '2019-12-07', number: 90 }, { date: '2019-12-05', number: 98 }, { date: '2019-12-01', number: 96 }, { date: '2019-11-28', number: 99 }, ];
const lastThirtyDays = [...new Array(31)].map((i, idx) =>
moment()
.startOf('day')
.subtract(idx, 'days')
.format('YYYY-MM-DD') // <--- Match format to sampleSet format
);
const result = lastThirtyDays.map(day => ({
x: moment(day).format('MM/D'),
y: (sampleSet.find(({date}) => date === day) || {number: 0}).number
}));
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
how to split the array list by days
const days = [
{'name':'Mon','value':1},
{'name':'Tue','value':5},
{'name':'Wed','value':10},
{'name':'Wed','value':30},
{'name':'Fri','value':18},
{'name':'Sat','value':80},
{'name':'Sat','value':90},
{'name':'Sun','value':20},
]
I having the above array list by days i wed and Sat i am having two values for thu I am not having values. i need to split the repeated key values into separate array if day are not in the list i need to add zero value for that for example my out put will be
const result = [
[1,5,10,0,18,80,20],
[0,0,30,0,0,90,0]
]
I need like this result is it possible to do in javascript.
You can do this using native javascript.
The algorithm is very simple:
For each day in daysArray you should search it in your given array and just remove first occurence from days.
Do step 1 until days is empty. With the other words, execute step 1 until days.length == 0 condition is satisfied.
let days = [ {'name':'Mon','value':1}, {'name':'Tue','value':5}, {'name':'Wed','value':10}, {'name':'Wed','value':30}, {'name':'Fri','value':18}, {'name':'Sat','value':80}, {'name':'Sat','value':90}, {'name':'Sun','value':20} ], daysArray = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
let result = [];
while(days.length){
sub_array = [];
daysArray.forEach(function(item){
let index = days.findIndex(a => a.name == item);
if(index == -1)
sub_array.push(0);
else{
sub_array.push(days[index].value);
days.splice(index, 1);
}
});
result.push(sub_array);
}
console.log(result);
Add an array of days in the order that you want - daysList.
Group the day objects into a Map using the name as key - daysMap.
Reduce the daysList, and get the days objects from the map by the current day. Iterate the array of days with Array.forEach(), and for each add a week array filled with 0s if missing, and assign the day's value to the day index di.
const daysList = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
const days = [{"name":"Mon","value":1},{"name":"Tue","value":5},{"name":"Wed","value":10},{"name":"Wed","value":30},{"name":"Fri","value":18},{"name":"Sat","value":80},{"name":"Sat","value":90},{"name":"Sun","value":20}]
// group the days by name into a map
const daysMap = days.reduce((m, o) => {
m.has(o.name) || m.set(o.name, [])
m.get(o.name).push(o)
return m
}, new Map)
// iterate the daysList
const result = daysList.reduce((r, d, di) => {
//get the array of the days with the same name from the group
const daysObjs = daysMap.get(d) || []
//iterate the daysObjs array
daysObjs.forEach(({ name, value }, wi) => {
// add a new week array filled with 0 if the row is missing
r[wi] || r.push(new Array(daysList.length).fill(0))
// assign the day value to the week array
r[wi][di] = value
})
return r
}, [])
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
You could take an object for the day indices and for the ouiter indixec which is incremented by every inset of data.
var days = [{ name: 'Mon', value: 1 }, { name: 'Tue', value: 5 }, { name: 'Wed', value: 10 }, { name: 'Wed', value: 30 }, { name: 'Fri', value: 18 }, { name: 'Sat', value: 80 }, { name: 'Sat', value: 90 }, { name: 'Sun', value: 20 }],
indices = { Mon: { day: 0, index: 0 }, Tue: { day: 1, index: 0 }, Wed: { day: 2, index: 0 }, Thu: { day: 3, index: 0 }, Fri: { day: 4, index: 0 }, Sat: { day: 5, index: 0 }, Sun: { day: 6, index: 0 } },
result = days.reduce((r, { name, value }) => {
r[indices[name].index] = r[indices[name].index] || Array.from({ length: 7 }).fill(0);
r[indices[name].index++][indices[name].day] = value;
return r;
}, []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
You can use array#reduce and a lookup object of day and create an array of value based on a given day.
const days = [{'name':'Mon','value':1}, {'name':'Tue','value':5}, {'name':'Wed','value':10}, {'name':'Wed','value':30}, {'name':'Fri','value':18}, {'name':'Sat','value':80}, {'name':'Sat','value':90}, {'name':'Sun','value':20}, ],
day = {'Mon':0, 'Tue':1, 'Wed':2, 'Thu': 3, 'Fri': 4, 'Sat': 5, 'Sun': 6},
result = days.reduce((r,o) => {
var index = 0;
if(r[index][day[o.name]]) {
while(r[index] && r[index][day[o.name]]) {
index++;
}
if(!r[index]) {
r[index] = Array.from({length: 7}, _=> 0);
}
}
r[index][day[o.name]] = o.value;
return r;
},[Array.from({length: 7}, _=> 0)]);
console.log(result);
another solution:
const res = _.chain(days)
.map('name') // get days name
.uniq() // uniq days name
.map(day => _.filter(days, { name: day })) // iterate days name and get all day items
.map(sameDays => _.map(sameDays, 'value')) // get value for each day items
.map(vs => [vs[0], vs[1] || 0]) // set 0 value if only one item for day
.thru(_.spread(_.zip)) // transpose the array
.value()