How to combine all objects into one based on key - javascript

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

Related

Comparing with sliced object string is not working with filter method

i have a array of object like this.
const ExpensesData=[
{
title: 'Toilet Paper',
amount: 94.12,
date: "2022-09-02",
},
{
title: 'New TV',
amount: 799.49,
date: "2021-08-02",
}
]
i am trying to filter objects base upon year.
i tried like this. but, no luck. it looks simple but don't know what is the issue.
const filteredData = ExpensesData.filter((e)=> e.date.slice(0,4) == "2021")
This statement works fine as it return first 4 character of the string.
const aa = ExpensesData[0].date.slice(0,4);
All i am doing is comparing it with.
I came up with this solution.
Maybe it suits you
const ExpensesData =[
{
title: 'Toilet Paper',
amount: 94.12,
date: "2022-08-03",
},
{
title: 'New TV',
amount: 799.49,
date: "2022-08-02",
}
]
const dates = ExpensesData.map(el => el.date);
dates.sort((a, b) => {
let year = [+a.slice(0, 4), +b.slice(0, 4)];
if (comparison(year)) return comparison(year);
let month = [+a.slice(5, 7), +b.slice(5, 7)];
if (comparison(month)) return comparison(month);
let day = [+a.slice(8, a.length), +b.slice(8, b.length)];
if (comparison(day)) return comparison(day);
});
function comparison(arr) {
if (arr[0] > arr[1]) {
return 1;
} else if (arr[0] < arr[1]) {
return -1;
}
}

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

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 }

convert an array to an object of key value pairs

if I have an array of strings like:
['person,item,cost,amount',
'John,shoes,200,2']
how could I convert this into an object that resembles:
{
'John':[
{
item:'shoes',
cost:'200',
amount:'2',
totalPriceForItems:'400'
}
If I understand correctly, you may try something like this:
const convert = data => {
const [columnsText, ...items] = data;
const columns = columnsText.split(',');
return items.reduce((acc, text) => {
const { person, ...entries } = Object.fromEntries(text.split(',').map((value, i) => [columns[i], value]));
entries.totalPriceForItems = String(entries.cost * entries.amount);
if(acc[person]) {
acc[person].push(entries);
} else {
acc[person] = [entries];
}
return acc;
}, {});
};
const result = convert([
'person,item,cost,amount',
'John,shoes,200,2',
'Bob,glasses,50,3',
'John,shirts,100,5',
]);
console.log(result);
According to your comment,
I have 8 lines of 'John,shoes,200,2' but with different amounts in the
same array. The 'person,item,cost,amount' is only mentioned once at
the start of the array
What I understand is that you have a csv with headers and multiple rows.
If that is the case, then your data would resemble something like this:
data = [
'person,item,cost,amount',
'John,shoes,200,2',
'Adam,pants,60,1',
'Kelly,skirt,180,2',
'John,skirt,150,3'
]
Then you could consider the following approach, that is generic enough to adapt to different headers, and multiple data rows with repeated keys (person names).
// here, you define a function to transform each row of your data,
// like parsing numeric attributes and calculating the totals
function transform(row) {
row.cost = Number.parseInt(row.cost)
row.amount = Number.parseInt(row.amount)
row.total = row.cost * row.amount
return row
}
// The following logic is generic, and can be used
// to map and aggregate any kind of csv with headers
hdrs = data.shift().split(',').slice(1)
rows = data.map(r => r.split(',')).reduce((acc, [n, ...kvs]) =>
({ ...acc, [n]: [...acc[n] || [], transform(Object.fromEntries(kvs.map((v, i) => [hdrs[i], v])))] }), {})
Output:
{
John: [
{ item: "shoes", cost: 200, amount: 2, total: 400 },
{ item: "skirt", cost: 150, amount: 3, total: 450 }],
Adam: [
{ item: "pants", cost: 60, amount: 1, total: 60 }],
Kelly: [
{ item: "skirt", cost: 180, amount: 2, total: 360 }]
}

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

Javascript can't group objects with 2 values

I have this object data:
[ RowDataPacket {
id: 59,
steamid: '76561198220437096',
product_id: 23,
status: 1,
date: 2017-12-18T17:27:19.000Z,
message: null,
name: 'CS.MONEY',
amount: 100,
website: 'csgo500' },
RowDataPacket {
id: 60,
steamid: '76561198220437096',
product_id: 24,
status: 1,
date: 2017-12-18T17:27:19.000Z,
message: null,
name: 'CS.MONEY',
amount: 250,
website: 'csgo500' },
RowDataPacket {
id: 61,
steamid: '76561198220437096',
product_id: 23,
status: 1,
date: 2017-12-18T17:27:19.000Z,
message: null,
name: 'CS.MONEY',
amount: 100,
website: 'csgo500' },
RowDataPacket {
id: 62,
steamid: '76561198345348530',
product_id: 6,
status: 1,
date: 2017-12-18T20:05:55.000Z,
message: null,
name: 'wal gruche',
amount: 100,
website: 'csgoatse' }
Im trying to sort this data with steamid and website, i managed to sort this only by one value like this:
var groupedOrders = {};
row.forEach(function(item){
var list = groupedOrders[item.steamid];
if(list){
list.push(item);
} else{
groupedOrders[item.steamid] = [item];
}
});
My idea was to make two dimensional array but for some reason i cant do it like this:
var list = groupedOrders[item.steamid][item.website];
It throws me an error "Cant read property ... of undefined"
Now my code looks like this:
var groupedOrders = {};
row.forEach(function(item){
var list = groupedOrders[item.steamid][item.website];
if(list){
list.push(item);
} else{
groupedOrders[item.steamid][item.website] = [item];
}
});
Do you have any ideas how to fix this errors?
The problem is that var list = groupedOrders[item.steamid][item.website] is actually saying:
var temp = groupedOrders[item.steamid];
var list = temp[item.website];
There is no entry at groupedOrders[item.steamid] and so line one sets temp to undefined. The second line tries to index into undefined which is an error.
You would have to split the code out and essentially do the whole one-key grouping twice:
var outerList = groupedOrders[item.steamid];
if (!outerList)
outerList = groupedOrders[item.steamid] = {};
var innerList = outerList[item.website];
if (innerList)
innerList.push(item);
else
outerList[item.website] = [item];
(I have not tested this code but it is the right shape.)
The following works by creating a recursive groupBy grouping function for each of the fields supplied as an argument.
These dynamically created groupBy functions are then invoked one by one, passing the result between, starting with the supplied data.
Each groupBy function instance creates an object and adds properties to it corresponding to the key values for the field being grouped.
By calling these groupBy functions successively, we create a progressively more nested tree of objects, with groups at each successive level marked as being groups using a symbol.
The final result is a nest (a tree!) of objects, with keys corresponding to the field used for indexing at that level.
Finally, we flatten the nest and the final order is visible.
const flatten = o => Object.values(o).reduce((acc, c) => (Array.isArray(c) ? [...acc, ...c] : typeof c === 'object' ? [...acc, ...flatten(c)] : [...acc, c]), []);
const flow = (...fns) => data => fns.reduce((acc, c) => c(acc), data);
const GROUP = Symbol('group');
const asGroup = (result = []) => ((result[GROUP] = true), result);
const isGroup = o => o[GROUP];
const groupBy = field => (data, key) =>
data.reduce((acc, c) =>
((key = c[field]), (acc[key] ?
(acc[key].push(c), acc) :
((acc[key] = asGroup([c])), acc))), {});
const recurse = (test) => (transform) => o =>
test(o)
? transform(o)
: Object.entries(o).reduce(
(acc, [k, v]) => (test(v) ?
((acc[k] = transform(v)), acc) :
((acc[k] = recurse(test)(transform)(v)), acc)), {});
const group = (...fields) => flow(...fields.map(flow(groupBy, recurse(isGroup))), flatten);
const rows = asGroup([
{
id: 0,
steamid: '2',
website: 'a'
},
{
id: 1,
steamid: '2',
website: 'b'
},
{
id: 2,
steamid: '2',
website: 'a'
},
{
id: 3,
steamid: '1',
website: 'b'
},
{
id: 4,
steamid: '0',
website: 'b'
}
]);
console.log(JSON.stringify(group('steamid', 'website')(rows), null, 2));

Categories

Resources